Utterly Broken Shell
Utterly Broken Shell is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
Utterly Broken Shell attack path
Utterly Broken Shell is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Objective
Challenge walkthrough focused on Misc evidence, validation, and reusable operator lessons.
Walkthrough flow
Remote prompt exposes a reduced Bash regex allowlist...
Backend still evaluates accepted input through Bash...
Old $0 substring trick is blocked because literal...
Use a normal underscore-only variable to store...
Use indirect expansion through that variable to...
Source coverage
High source coverage
Status: complete. This article is generated from 6 sanitized Markdown sources and keeps raw flags, credentials, keys, cookies, and reusable secrets out of the rendered blog.
High confidence: the page is reconstructed from a primary walkthrough plus multiple supporting notes or evidence sources. Treat the chain as source-backed, while still checking the listed source files for sensitive values.
- Misc/Utterly-Broken-Shell/writeup.md
- htb-challenge/Misc/Utterly-Broken-Shell/notes.md
- htb-challenge/Misc/Utterly-Broken-Shell/memory-summary.md
- htb-challenge/Misc/Utterly-Broken-Shell/hypothesis-board.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Misc__Utterly-Broken-Shell__memory-summary.md.19710872a8.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Misc__Utterly-Broken-Shell__notes.md.075ff2664b.md
Technical Walkthrough
Writeup
Challenge
- Name: Utterly-Broken-Shell
- Category: Misc
- Difficulty: Medium
- Mode: remote
Summary
The challenge is a sequel to Broken Shell with a much tighter regex. Digits, slashes, and letters are blocked, so the old $0 substring bypass no longer works directly. The service still passes accepted input to Bash eval, and underscore-only variables remain usable. The solve creates forbidden characters indirectly, recovers $0, assembles /bin/sh from characters in the wrapper path, and then uses the spawned unfiltered shell to read the flag.
Artifact Inventory
- Remote-only challenge:
<TARGET>:30368. analysis/remote/initial-probe.txt: captured banner, prompt, reduced regex, and basic allowed/disallowed behavior.analysis/remote/special-parameter-probe.txt: validated$_and arithmetic expansion behavior.analysis/remote/underscore-variable-indirect-probe.txt: validated underscore-only variables, arithmetic-generated0, indirect expansion to$0, and wrapper-path substring extraction.analysis/remote/solve-transcript-redacted.txt: redacted solve transcript.solve/solve.py: reproducible solver.
Analysis
The banner reports the allowed character regex:
^[${}![:space:]:_=()]+$That blocks the old Broken Shell payload because literal digits and slashes are no longer allowed. Harmless probes confirmed the backend still reaches Bash eval in /home/restricted_user/broken_shell.sh and that $_ expands to echo at evaluation time. This gives a small string source but not enough on its own.
The decisive primitive is that underscore-only variable names are valid Bash variables and pass the regex. The command __=$(()) stores the arithmetic expansion 0 without typing a literal digit. Then ___=${!__} performs indirect expansion through variable name 0, recovering $0, which is the wrapper path:
/home/restricted_user/broken_shell.shFrom that path, every character needed for /bin/sh is available. The solver uses a work variable and one-character shifts:
____=${___}
____=${____:!_}
...
_____=${_____}${____::!_}Here !_ evaluates to arithmetic 1, so each shift removes one character and ${____::!_} extracts one character. Repeating this builds /bin/sh in _____. Executing ${_____} starts a child shell outside the wrapper's regex loop.
Solve
Run:
cd <local workspace>
python3 solve/solve.pyThe script:
- Connects to the remote service.
- Stores
0in an underscore-only variable using$(()). - Uses
${!__}to recover$0. - Calculates wrapper-path character offsets for
/bin/sh. - Sends allowed-character payloads to assemble
/bin/sh. - Executes the assembled shell and reads the flag.
- Stores the raw candidate in
loot/flag-candidate.txtand a redacted transcript inanalysis/remote/solve-transcript-redacted.txt.
The harness captured the final flag into loot/flag.txt.
Flag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
- Blocking obvious shell metacharacters is not enough when
evalremains and variable assignment/parameter expansion survive. - Special parameter indirection can recreate forbidden digits and recover
$0without typing them. - Use normal variables, not
$_, as persistent build buffers;$_is overwritten by wrapper/prompt commands. - Keep remote transcripts redacted and raw flags under
loot/only.
Source-Backed Dossier
The sections below are merged from companion Markdown notes for the same case. They are rendered after sanitization so the article stays precise without publishing raw flags, credentials, or target-specific secrets.
Notes
Scope
- Challenge: Utterly-Broken-Shell
- Category: Misc
- Difficulty: Medium
- Mode: remote
- Remote instance: <TARGET>:30368
- Start time: 2026-06-09T14:29:03Z
- Operator: harness
- State file:
challenge-state.json
Harness Status
- Current phase: see
challenge-state.json - Next allowed actions: see
next-action.json - Raw flags and sensitive material stay in
loot/only. Do not paste them here.
Artifact Inventory
| File | Size | SHA256 | Type | Notes |
|---|---|---|---|---|
| — | 0 | — | remote-only or no provided files | No local artifacts found under files/ |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-09T14:29:03Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-09T14:29:28Z | artifact inventory | analysis/artifact-inventory.json | 0 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-09T14:29:28Z | hypothesis recorded | hypothesis-board.md | Remote filtered shell still evaluates accepted input; previous Broken Shell shell-expansion tricks are patched, so enumerate the new regex and backend behavior, recover wrapper constraints if possible, then build a command using only accepted characters. | Medium | Connect once, capture banner/regex/prompt, then run harmless symbol-only probes and compare against prior Broken-Shell behavior. |
| 2026-06-09T14:29:28Z | checkpoint recorded | analysis/checkpoint-triage-20260609T142928400610Z-f2970950.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-09T14:29:38Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-09T14:29:49Z | local memory search | analysis/research/local-memory-search-20260609T142949975979Z-fb69d415.md | Found 5 safe prior-note result(s) | Medium | Record useful result or skip |
| 2026-06-09T14:30:58Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-09T14:30:58Z | evaluator | analysis/evaluator-20260609T143058362639Z-67c5a3d5.md | Validate first | High | Capture initial remote behavior into analysis/remote/initial-probe.txt, then update evaluator before any flag attempt. |
| 2026-06-09T14:39:15Z | evaluator | analysis/evaluator-20260609T143915854365Z-09c67e15.md | Proceed | High | Create solve/solve.py that assembles /bin/sh using only allowed characters, enters child shell, captures flag to loot/flag-candidate.txt, and writes redacted transcript. |
| 2026-06-09T14:41:32Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-09T14:42:43Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- Remote service:
<TARGET>:30368. - New banner exposes the reduced allowlist:
^[${}![:space:]:_=()]+$. - The backend still evaluates accepted input through
/home/restricted_user/broken_shell.sh; invalid syntax/errors discloseevalat line 41. - The previous Broken Shell payload
${0:35:2}is blocked because digits are no longer allowed. $_at eval time expands toecho, which allows generatedechooutput and simple substring leaks, but$_is not stable enough to use as a persistent build buffer.- Normal underscore-only variable names such as
__,___,____, and_____are allowed and persist across prompt turns. __=$(())creates the forbidden digit0without typing a digit.___=${!__}indirectly expands variable0, recovering$0as/home/restricted_user/broken_shell.sh.- The solver builds
/bin/shby copying$0, shifting a work variable one byte at a time with${var:!_}, and appending${var::!_}into an accumulator. - Executing the assembled
/bin/shopens an unfiltered child shell, allowing normal commands to read the flag. - Reproducible solver:
solve/solve.py. - Raw flag captured by the harness and stored only in
loot/flag.txt.
RAG / Advisory Memory
RAG output is advisory only. Record evaluated retrievals with:
scripts/challenge_harness.py rag-record <workspace> --query "..." --tag MATCHED|PARTIAL|MISSING|<secret redacted>|GENERIC --validation "..."Secrets/Flags
Raw flags and sensitive material stay in loot/ only. Use scripts/challenge_harness.py capture-flag to validate and record flag capture without printing the value.
Memory Summary
Metadata
- Platform: HackTheBox Challenges
- Category: Misc
- Challenge: Utterly-Broken-Shell
- Difficulty: Medium
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
- Remote prompt exposes a reduced Bash regex allowlist with only
$, braces,!, whitespace,:,_,=, and parentheses. - Backend still evaluates accepted input through Bash
eval. - Old
$0substring trick is blocked because literal digits are disallowed. - Use a normal underscore-only variable to store arithmetic expansion
0without typing digits. - Use indirect expansion through that variable to recover
$0. - Use one-character substring shifts from the wrapper path to assemble
/bin/shin another underscore-only variable. - Execute the assembled shell and read the flag from the unfiltered child shell.
Reusable Lessons
- If
evalremains, a reduced allowlist must also remove variable assignment, indirect expansion, arithmetic expansion, and substring expansion or the shell can still synthesize forbidden characters. $_is useful as a transient source string but should not be trusted as persistent state in wrapper-loop challenges.- Underscore-only variables can act as persistent registers when alphabetic variable names are blocked.
Dead Ends
- Old Broken Shell
${0:35:2}route: blocked by the new regex because digits are disallowed. - Direct slash/path/glob/cp/redirection methods from the previous challenge: blocked by the new regex.
Tool Quirks
- The harness local-memory-record command only accepts files inside the active workspace, so a sanitized
<secret redacted>mdwas created from prior local memory and recorded as advisory context. - The first solver parser failed on a too-strict wrapper-path regex; the live output was clear and the parser was patched before exploitation completed.
Evidence Paths
analysis/remote/initial-probe.txtanalysis/remote/special-parameter-probe.txtanalysis/remote/underscore-variable-indirect-probe.txtanalysis/evaluator-20260609T143915854365Z-09c67e15.mdanalysis/remote/solve-transcript-redacted.txtsolve/solve.pyloot/flag.txt
Ingestion Decision
- Proposed for LightRAG: yes
- Requires user approval before ingestion: yes
Hypothesis Board
Keep no more than 3 active hypotheses on Easy/Medium and 5 on Hard unless the user explicitly asks for breadth.
| Rank | Path | Evidence | Missing Proof | Cheapest Validation | Confidence | Status |
|---|---|---|---|---|---|---|
| 1 | Remote filtered shell still evaluates accepted input; previous Broken Shell shell-expansion tricks are patched, so enumerate the new regex and backend behavior, recover wrapper constraints if possible, then build a command using only accepted characters. | Challenge title/scenario says the regex was reduced to patch previous bypasses; prior local Broken-Shell workspace shows eval-based sandbox and substring-$0 bypass. | Connect once, capture banner/regex/prompt, then run harmless symbol-only probes and compare against prior Broken-Shell behavior. | Medium | Active |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit Condition |
|---|
Memory Summary
approval_required: true
Sanitized Memory Summary
Metadata
- Platform: HackTheBox Challenges
- Category: Misc
- Challenge: Utterly-Broken-Shell
- Difficulty: Medium
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
- Remote prompt exposes a reduced Bash regex allowlist with only
$, braces,!, whitespace,:,_,=, and parentheses. - Backend still evaluates accepted input through Bash
eval. - Old
$0substring trick is blocked because literal digits are disallowed. - Use a normal underscore-only variable to store arithmetic expansion
0without typing digits. - Use indirect expansion through that variable to recover
$0. - Use one-character substring shifts from the wrapper path to assemble
/bin/shin another underscore-only variable. - Execute the assembled shell and read the flag from the unfiltered child shell.
Reusable Lessons
- If
evalremains, a reduced allowlist must also remove variable assignment, indirect expansion, arithmetic expansion, and substring expansion or the shell can still synthesize forbidden characters. $_is useful as a transient source string but should not be trusted as persistent state in wrapper-loop challenges.- Underscore-only variables can act as persistent registers when alphabetic variable names are blocked.
Dead Ends
- Old Broken Shell
${0:35:2}route: blocked by the new regex because digits are disallowed. - Direct slash/path/glob/cp/redirection methods from the previous challenge: blocked by the new regex.
Tool Quirks
- The harness local-memory-record command only accepts files inside the active workspace, so a sanitized
<secret redacted>mdwas created from prior local memory and recorded as advisory context. - The first solver parser failed on a too-strict wrapper-path regex; the live output was clear and the parser was patched before exploitation completed.
Evidence Paths
analysis/remote/initial-probe.txtanalysis/remote/special-parameter-probe.txtanalysis/remote/underscore-variable-indirect-probe.txtanalysis/evaluator-20260609T143915854365Z-09c67e15.mdanalysis/remote/solve-transcript-redacted.txtsolve/solve.pyloot/flag.txt
Ingestion Decision
- Proposed for LightRAG: yes
- Requires user approval before ingestion: yes
Notes
Notes
Scope
- Challenge: Utterly-Broken-Shell
- Category: Misc
- Difficulty: Medium
- Mode: remote
- Remote instance: <TARGET>:30368
- Start time: 2026-06-09T14:29:03Z
- Operator: harness
- State file:
challenge-state.json
Harness Status
- Current phase: see
challenge-state.json - Next allowed actions: see
next-action.json - Raw flags and sensitive material stay in
loot/only. Do not paste them here.
Artifact Inventory
| File | Size | SHA256 | Type | Notes |
|---|---|---|---|---|
| — | 0 | — | remote-only or no provided files | No local artifacts found under files/ |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-09T14:29:03Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-09T14:29:28Z | artifact inventory | analysis/artifact-inventory.json | 0 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-09T14:29:28Z | hypothesis recorded | hypothesis-board.md | Remote filtered shell still evaluates accepted input; previous Broken Shell shell-expansion tricks are patched, so enumerate the new regex and backend behavior, recover wrapper constraints if possible, then build a command using only accepted characters. | Medium | Connect once, capture banner/regex/prompt, then run harmless symbol-only probes and compare against prior Broken-Shell behavior. |
| 2026-06-09T14:29:28Z | checkpoint recorded | analysis/checkpoint-triage-20260609T142928400610Z-f2970950.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-09T14:29:38Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-09T14:29:49Z | local memory search | analysis/research/local-memory-search-20260609T142949975979Z-fb69d415.md | Found 5 safe prior-note result(s) | Medium | Record useful result or skip |
| 2026-06-09T14:30:58Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-09T14: <REDACTED>, then update evaluator before any flag attempt. | |||||
| 2026-06-09T14: <REDACTED>, enters child shell, captures flag to loot/flag-candidate.txt, and writes redacted transcript. | |||||
| 2026-06-09T14: <REDACTED> | |||||
| 2026-06-09T14:42:43Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- Remote service:
<TARGET>:30368. - New banner exposes the reduced allowlist:
^[${}![:space:]:_=()]+$. - The backend still evaluates accepted input through
/home/restricted_user/broken_shell.sh; invalid syntax/errors discloseevalat line 41. - The previous Broken Shell payload
${0:35:2}is blocked because digits are no longer allowed. $_at eval time expands toecho, which allows generatedechooutput and simple substring leaks, but$_is not stable enough to use as a persistent build buffer.- Normal underscore-only variable names such as
__,___,____, and_____are allowed and persist across prompt turns. __=$(())creates the forbidden digit0without typing a digit.___=${!__}indirectly expands variable0, recovering$0as/home/restricted_user/broken_shell.sh.- The solver builds
/bin/shby copying$0, shifting a work variable one byte at a time with${var:!_}, and appending${var::!_}into an accumulator. - Executing the assembled
/bin/shopens an unfiltered child shell, allowing normal commands to read the flag. - Reproducible solver:
solve/solve.py. - Raw flag captured by the harness and stored only in
loot/flag.txt.
RAG / Advisory Memory
RAG output is advisory only. Record evaluated retrievals with:
scripts/challenge_harness.py rag-record <workspace> --query "..." --tag MATCHED|PARTIAL|MISSING|<secret redacted>|GENERIC --validation "..."Secrets/Flags
Raw flags and sensitive material stay in loot/ only. Use scripts/challenge_harness.py capture-flag to validate and record flag capture without printing the value.
Technical analogy
How to remember this solve
Think of the challenge like a timed puzzle booth. If the task is too fast or repetitive for a person, the intended move is usually to write a small helper that performs the simple action perfectly.
For Utterly Broken Shell, keep the mental model simple: identify the trusted assumption, prove it with the smallest safe test, then automate or repeat only the part that directly leads to the flag.