Challenge / Misc

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

MediumPublished 2025-01-12Sanitized local writeup

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.

Utterly Broken Shell sanitized attack graph

Walkthrough flow

01

Remote prompt exposes a reduced Bash regex allowlist...

02

Backend still evaluates accepted input through Bash...

03

Old $0 substring trick is blocked because literal...

04

Use a normal underscore-only variable to store...

05

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.

100% coverage
Evidence verdict

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-generated 0, 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:

text
^[${}![: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:

text
/home/restricted_user/broken_shell.sh

From that path, every character needed for /bin/sh is available. The solver uses a work variable and one-character shifts:

text
____=${___}
____=${____:!_}
...
_____=${_____}${____::!_}

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:

bash
cd <local workspace>
python3 solve/solve.py

The script:

  1. Connects to the remote service.
  2. Stores 0 in an underscore-only variable using $(()).
  3. Uses ${!__} to recover $0.
  4. Calculates wrapper-path character offsets for /bin/sh.
  5. Sends allowed-character payloads to assemble /bin/sh.
  6. Executes the assembled shell and reads the flag.
  7. Stores the raw candidate in loot/flag-candidate.txt and a redacted transcript in analysis/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 eval remains and variable assignment/parameter expansion survive.
  • Special parameter indirection can recreate forbidden digits and recover $0 without 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

FileSizeSHA256TypeNotes
0remote-only or no provided filesNo local artifacts found under files/

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-09T14:29:03Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-09T14:29:28Zartifact inventoryanalysis/artifact-inventory.json0 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-09T14:29:28Zhypothesis recordedhypothesis-board.mdRemote 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.MediumConnect once, capture banner/regex/prompt, then run harmless symbol-only probes and compare against prior Broken-Shell behavior.
2026-06-09T14:29:28Zcheckpoint recordedanalysis/checkpoint-triage-20260609T142928400610Z-f2970950.mdCheckpoint for TRIAGEHighUse checkpoint to drive next decision
2026-06-09T14:29:38Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-09T14:29:49Zlocal memory searchanalysis/research/local-memory-search-20260609T142949975979Z-fb69d415.mdFound 5 safe prior-note result(s)MediumRecord useful result or skip
2026-06-09T14:30:58Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-09T14:30:58Zevaluatoranalysis/evaluator-20260609T143058362639Z-67c5a3d5.mdValidate firstHighCapture initial remote behavior into analysis/remote/initial-probe.txt, then update evaluator before any flag attempt.
2026-06-09T14:39:15Zevaluatoranalysis/evaluator-20260609T143915854365Z-09c67e15.mdProceedHighCreate 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:32Zflag captureloot/flag.txtHTB-format flag captured; raw value kept in loot onlyHighWrite solution and run completion gate
2026-06-09T14:42:43Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional 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 disclose eval at line 41.
  • The previous Broken Shell payload ${0:35:2} is blocked because digits are no longer allowed.
  • $_ at eval time expands to echo, which allows generated echo output 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 digit 0 without typing a digit.
  • ___=${!__} indirectly expands variable 0, recovering $0 as /home/restricted_user/broken_shell.sh.
  • The solver builds /bin/sh by copying $0, shifting a work variable one byte at a time with ${var:!_}, and appending ${var::!_} into an accumulator.
  • Executing the assembled /bin/sh opens 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:

bash
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.

  1. Remote prompt exposes a reduced Bash regex allowlist with only $, braces, !, whitespace, :, _, =, and parentheses.
  2. Backend still evaluates accepted input through Bash eval.
  3. Old $0 substring trick is blocked because literal digits are disallowed.
  4. Use a normal underscore-only variable to store arithmetic expansion 0 without typing digits.
  5. Use indirect expansion through that variable to recover $0.
  6. Use one-character substring shifts from the wrapper path to assemble /bin/sh in another underscore-only variable.
  7. Execute the assembled shell and read the flag from the unfiltered child shell.

Reusable Lessons

  • If eval remains, 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>md was 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.txt
  • analysis/remote/special-parameter-probe.txt
  • analysis/remote/underscore-variable-indirect-probe.txt
  • analysis/evaluator-20260609T143915854365Z-09c67e15.md
  • analysis/remote/solve-transcript-redacted.txt
  • solve/solve.py
  • loot/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.

RankPathEvidenceMissing ProofCheapest ValidationConfidenceStatus
1Remote 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.MediumActive

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit 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.

  1. Remote prompt exposes a reduced Bash regex allowlist with only $, braces, !, whitespace, :, _, =, and parentheses.
  2. Backend still evaluates accepted input through Bash eval.
  3. Old $0 substring trick is blocked because literal digits are disallowed.
  4. Use a normal underscore-only variable to store arithmetic expansion 0 without typing digits.
  5. Use indirect expansion through that variable to recover $0.
  6. Use one-character substring shifts from the wrapper path to assemble /bin/sh in another underscore-only variable.
  7. Execute the assembled shell and read the flag from the unfiltered child shell.

Reusable Lessons

  • If eval remains, 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>md was 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.txt
  • analysis/remote/special-parameter-probe.txt
  • analysis/remote/underscore-variable-indirect-probe.txt
  • analysis/evaluator-20260609T143915854365Z-09c67e15.md
  • analysis/remote/solve-transcript-redacted.txt
  • solve/solve.py
  • loot/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

FileSizeSHA256TypeNotes
0remote-only or no provided filesNo local artifacts found under files/

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-09T14:29:03Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-09T14:29:28Zartifact inventoryanalysis/artifact-inventory.json0 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-09T14:29:28Zhypothesis recordedhypothesis-board.mdRemote 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.MediumConnect once, capture banner/regex/prompt, then run harmless symbol-only probes and compare against prior Broken-Shell behavior.
2026-06-09T14:29:28Zcheckpoint recordedanalysis/checkpoint-triage-20260609T142928400610Z-f2970950.mdCheckpoint for TRIAGEHighUse checkpoint to drive next decision
2026-06-09T14:29:38Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-09T14:29:49Zlocal memory searchanalysis/research/local-memory-search-20260609T142949975979Z-fb69d415.mdFound 5 safe prior-note result(s)MediumRecord useful result or skip
2026-06-09T14:30:58Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate 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:43Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional 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 disclose eval at line 41.
  • The previous Broken Shell payload ${0:35:2} is blocked because digits are no longer allowed.
  • $_ at eval time expands to echo, which allows generated echo output 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 digit 0 without typing a digit.
  • ___=${!__} indirectly expands variable 0, recovering $0 as /home/restricted_user/broken_shell.sh.
  • The solver builds /bin/sh by copying $0, shifting a work variable one byte at a time with ${var:!_}, and appending ${var::!_} into an accumulator.
  • Executing the assembled /bin/sh opens 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:

bash
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.