IrisCTF 2023 WriteUps
在 RWCTF 解個幾題之後感覺沒有我會的類型之後就跑來 Solo
這場了,雖然開始的時候已經過了 1/4 的時間不過最後還是有用
nyahello
拿到第五名。
Binary Exploitation
babyseek
1 |
|
checksec:
1 | Arch: amd64-64-little |
因為是 No RELRO,所以直接蓋 exit@got
即可,而這個就只需要計算 _IO_write_ptr
和
exit@got
的 offset 就行了。
1 | from pwn import * |
ret2libm
1 |
|
checksec:
1 | Arch: amd64-64-little |
它給你了 libm 的 address,所以我一開始想手動 ROP 呼叫
execve("/bin/sh", 0, 0)
,但是我在 libm 裡面找不到
/bin/sh
,它也沒有 syscall; ret
的 gadget
可用,所以放棄了這條路。後來觀察一下會發現 libc 和 libm 間的 offset
是固定的,因此可以求出 libc 在哪,所以就變成了很標準的 ret2libc。
1 | from pwn import * |
baby?socat
run.sh:
1 |
|
chal.c:
1 |
|
這題預期解是利用 socat 的 address parser 在處理 quotes 的時候有
bug,不過我用 unintended 解了這題。方法就是 RTFM: man socat
而已,在 ADDRESS SPECIFICATIONS
的地方有寫說它支援
!!
作為 dual address specifications,前者作為 read
的來源,而後者是 write 的對象。所以你只要輸入 /!!exec:env
就能解了。
irisctf{they_even_fixed_it_for_unbalanced_double_quotes}
Michael Bank
1 | using System.Globalization; |
題目關鍵在於它轉換 currency 時會動 CurrentCulture
,而
login 時又有 ToLower
,這就讓我想到 Hacking GitHub with Unicode's dotless
'i' 這篇文章。測試一下會發現在 tr
(土耳其)語系下
I
會被轉換成 ı
,而在 en-US
則會變成 i
,所以只要在 tr
語系下 register
然後再切回 en-US
login 就能登入 michael 帳號。
剩下就只要把錢充到目標金額即可拿到 flag。
1 | from pwn import * |
Infinite Descent
這題是個 arm fireware 的題目
1 |
|
1 |
|
可見它用了 arm semihosting 功能來做輸入輸出,那個可以想成是一個 syscall (X) 做 I/O 而已,不過那和這題無關。
總之可知這題可以用 descend
函數去不斷的做
recursion,到最後理論上應該是會 stack overflow
才對。不過這題的關鍵在於它是個 firmware,所以它的 memory layout
可能和我們預期中的不太一樣。
我是先 setup debug 環境,先裝 gdb-multiarch
並在 qemu
的指令加上 -S -gdb tcp::8899
就能讓它開在 8899。
(-s
代表的是 -gdb tcp::1234
)
然後另一個視窗就用 gdb-multiarch chal.elf
打開後用
target remote:8889
就能連線了,不過我因為用的是 gef 所以用
gef-remote --qemu-binary chal.elf localhost 8889
才有比較好的整合。
如果要讓
context
指令能動的話要用 root 跑,所以要加上sudo -E
(Ref)
然後因為有 symbol 所以要找 address 都很容易,測試一下可知 text 段和
literal string 大概都很接近 0
。而 data 和 stack 分別在
20000000
和 2000c000
,且中間沒有保護!!! (用
readelf -A chal.elf
也行)
既然如此只要讓 stack overflow 一直往上,直到 input
包含
&last_message
的話就能把它的內容改掉成 flag
的位置,等它回到 main 時就能 print flag 了。
1 | from pwn import * |
實際上會需要用 gdb 做些 debug 去算一些位置,所以我還有寫個 gdb python script 去輔助:
1 | gdb.execute('gef config context.enable 0') |
另外是這題題目其實有說也能自己 build,就先到 ARM-software/CMSIS_5
的 release 下載它的檔案,然後當作 zip 解壓縮到 CMSIS_5
的資料夾。之後要安裝 arm-none-eabi-gcc
和
arm-none-eabi-newlib
兩個套件 (Arch Linux) 之後就能 make
編譯了。
Cryptography
babynotrsa
modular inverse 就搞定了
babymixup
1 | from Crypto.Cipher import AES |
用第一組回推 key,然後解密 flag。
1 | from Crypto.Cipher import AES |
Nonces and Keys
這題用 AES-128-OFB
和 key
0x13371337133713371337133713371337
加密了一個
sqlite3
的檔案,因為 header 以知所以可以回推
iv,之後解密之後還原 db 檔案,在裡面 select 一下即可。
1 | from Crypto.Cipher import AES |
SMarT 1
一個 home-rolled cipher,是一個只有 2 rounds 的 SPN (大概吧),而 sbox 是用 AES 的。
以第一題來說它指實作了 encrypt
,而題目有給你
key,所以實作 decrypt
出來即可。我這邊是直接讓 copilot
輔助寫出來的 XD。
1 | from pwn import xor |
SMarT 2
延續上題,這次沒有 key 所以要從已知的 plaintext/ciphertext pair 回推 key。
從題目名稱可看出大寫的三個字母是 SMT,所以我就直接在 z3 重寫了一次
encrypt
函數,然後也真的就這樣解了。
1 | from z3 import * |
不過這題因為只有 2 rounds,就他人所說是可以把它 reduce 成
S(pt^key1)^key2 = ct
,然後 byte by byte bruteforce
即可。
Miscellaneous
Name that song
這題給了一首歌的檔案,要找到原曲名。因為它並不是正常常見的歌,shazam 之類的毫無作用。
我是先在 vlc 發現說它有給樂器資訊,同時用 strings
在裡面找到了一些 SNR56.WAV
之類的文字。Google 它可以找到 The Mod Archive
這個網站,上面有很多和它同類型的音樂。
不過我在 Google SNR56.WAV
的第一個結果找到的不是對的,所以換了 Yandex 用同樣的關鍵字查找到 moon
gun,聽了一下就剛好是這首歌。
irisctf{moon_gun}
Host Issues
chal_serv.py:
1 | from flask import Flask, request |
chal.py:
1 | import os |
chal.sh:
1 |
|
所以這題目標很明顯,就是要透過控制環境變數讓
http://flag_domain:25566/flag
去 fetch flag。我一樣就先
man curl
在裡面找到 http_proxy
的環境變數,然後只要把它設成 http://127.0.0.1:25566/
,那麼
curl 就會發送這樣的請求到 proxy server:
1 | GET http://flag_domain:25566/flag |
而這個 flask 那邊也能接受,所以就能得到 flag 了。
irisctf{very_helpful_error_message}
不過 intended
solution 說是透過 RESOLV_HOST_CONF
可以讀檔,而這個是在
glibc 的這邊找到的。
Nameless
pyjail
1 | #!/usr/bin/python3 |
這個 pyjail 會遞迴地把 co_names
清空,然後對
co_consts
做些修改,然後回傳的東西需要是一個函數
res
,之後用 res(vars(), vars)
呼叫之。
所以我們會想讓它為 lambda x, y: ...
的形式,因為參數是放在 co_varnames
的所以沒事,function
local variables (a:=1
etc) 也是
co_varnames
。
首先 x
是個 dict,裡面有
__builtins__
,所以可以用 [*x][idx]
存取,不過
index 因為 int 都會被 replace 成 0 的關係需要自己用 not[]
去湊。拿到 builtins
module 之後就用
y(__builtins__)
把它轉換成 dict,之後再用一樣的方法拿到
breakpoint
直接 call。
1 | def gen(n): |
另外是說數字的部分還有些有趣的組法,例如
-~-~-~-~-~-~-~-~-~-~-~(not[])
,因為 python 的
~x
其實就是 -x-1
而已,因為二補數要符合
x+(~x)=-1
。
Reverse Engineering
baby_rev
丟進 IDA 然後 z3:
1 | from z3 import * |
雖然以這題來說 z3 其實是很多餘,不過就 z3 比較好偷懶 XD。
Meaning of Python 1
一個 python flag
checker,它看起來會對輸入做一些奇怪的操作,然後
zlib.compress
後之後再做其他操作,最後和一個 constant byte
string 比較而已。不過仔細看它根本就沒有動到原本的 string,因為是
immutable 的,所以就把最後的結果 zlib.decompress
就搞定了。
Meaning of Python 2
它是個 obfuscated python script,簡單 reverse 一下可知它在
exec(zlib.decompress(something))
,所以把那個壓縮的腳本解出來之後
foramt 一下又是另一個 obfuscated python
script,但是它做的事和前一題很像,所以猜測說它也是沒有動到輸入,所以就把最後的結果
zlib.decompress
就搞定了。
Scoreboard Website Easter Egg
scoreboard 頁面上有個 /static/theme_min.js
裡面包含了
obfuscated javascript,想辦法自己下 breakpoint 去 debug 之後可知它會在
localStorage 存一些狀態資訊,然後輸入方法是透過你按下的 category tab
來決定。經過 17 個輸入之後會做些 check,然後可以由此 derive 個 AES key
然後解密。
而解法也很簡單,就是把 category names 弄下來,一個一個爆破而已:
1 | M = 1 << 64 |
根據作者所說你也可以這樣手動按拿到 flag:
1 | go to homepage |
Web Exploitation
babystrechy
1 |
|
這題關鍵是 PASSWORD_DEFAULT
是 bcrypt,它只取前 72
個字元,所以直接爆就行了:
1 | from pwn import * |
babycsrf
1 | from flask import Flask, request |
直接用個頁面上面定義 setMessage
並且包含
/api
這個 script 就好了:
1 | <script> |
irisctf{jsonp_is_never_the_answer}
是說這應該不叫 CSRF,而是 XSSI (Cross-Site Script Inclusion) 吧...
Feeling Tagged
1 | from flask import Flask, request, redirect |
基本上就是要繞基於 BeautifulSoup
的一個 html
sanitizer,這種東西會出現問題的原因主要在於瀏覽器 (Chromium, Firefox) 在
parse html 時行為一般都和這種 server side 的 library
不一樣,所以很容易產生出不同的行為。
這邊 BeautifulSoup
用的底層 parser 是
html.parser
,然後我就開始隨便試一些 html
的玩法,發現說它也會考慮 CDATA,但是在 HTML5 中 CDATA 只會在一些特別的
context 下有作用,所以這就能繞過了:
1 | <![CDATA[><script>alert(1)</script>]]> |
BeautifulSoup
把它當成完整的 CDATA tag,但對 Chromium
來說 <![CDATA[>
被當成了一個 comment,所以後面的
script 就能執行。
irisctf{security_by_option}
作者的
writeup 是利用 <!-->
在 HTML5 中是一個 closed
comment 這個事實來繞的。
metacalc
1 | const { Sheet } = require('metacalc'); |
這邊使用的 metacalc
是 0.0.2
版本,然後它還有上一個 patch:
1 | --- sheet.o.js 2022-08-11 17:32:27.803553441 -0700 |
所以 node_modules/metacalc/lib/sheet.js
會變成:
1 | ; |
看起來就像是個 node.js jail,一般的關鍵都是要想辦法取得 vm
外面的物件,所以突破口肯定和 Math
有關。雖然你不能直接存用
Math.__proto__
拿到外面的
Object.prototype
,但是可以用
Object.getPrototypeOf
繞掉這個,因此完整的 payload
如下:
1 | =({}).constructor.getPrototypeOf(Math).constructor.constructor("return process")().mainModule.require("child_process").execSync("cat /flag").toString() |
irisctf{be_careful_of_implicit_calls}