Jerrytok
Jerrytok is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
Jerrytok attack path
Jerrytok 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 Web evidence, validation, and reusable operator lessons.
Walkthrough flow
Source and route audit
Trust boundary flaw
Exploit request chain
Admin or proof proof
Source coverage
High source coverage
Status: complete. This article is generated from 4 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.
- Web/Jerrytok/writeup.md
- htb-challenge/Web/Jerrytok/notes.md
- htb-challenge/Web/Jerrytok/memory-summary.md
- htb-challenge/Web/Jerrytok/hypothesis-board.md
Technical Walkthrough
Writeup
Challenge
- Name: Jerrytok
- Category: Web
- Difficulty: Medium
- Mode: hybrid
Summary
JerryTok is a Symfony/Twig application with server-side template injection in
the location query parameter. Direct command execution is blocked by
disable_functions, but the template can still call file_put_contents and
chmod through Twig's sort filter. The solve writes a .htaccess file that
enables CGI scripts in the public web root, writes an executable shell CGI that
calls the SUID /readflag helper, then requests that CGI endpoint to retrieve
the flag.
Artifact Inventory
files/a12c738f-4a20-4868-a833-da74ea8fd3ac.zip: original HTB archive.files/extracted/web_jerrytok/challenge/src/Controller/DefaultController.php:
route/controller containing the template injection.
files/extracted/web_jerrytok/entrypoint.sh: runtime hardening with
disabled command functions and open_basedir=/www.
files/extracted/web_jerrytok/config/httpd.conf: Apache configuration with
AllowOverride All for /www/public.
files/extracted/web_jerrytok/readflag.c: SUID helper that reads/root/flag.- Remote instance:
http://<TARGET>:32250.
Analysis
The vulnerable controller builds a Twig template directly from user-controlled
input:
$message = $this->container->get('twig')->createTemplate(
"Located at: {$location} from your ship's computer"
)
->render();A harmless local request with location={{7*7}} rendered 49, recorded in
analysis/local-ssti-7x7.txt.
The obvious Twig callback approach, such as calling system, fails because
entrypoint.sh disables command execution functions. Direct file reads outside
/www also fail under the configured runtime controls. The working primitive is
to use Twig's sort filter with enabled PHP callables:
file_put_contentswrites controlled files under/www/public.chmodmakes the written script executable..htaccessenables CGI execution for.shfiles in/www/public.
Local Docker validation confirmed the exact chain without storing raw flag data
in analysis:
analysis/local-ssti-htaccess-cgi-validation.txtanalysis/local-solver-transcript.txtanalysis/source-audit.md
Public research was treated as advisory only and is recorded in
analysis/research-jerrytok-community-sources.md. The RAG result was generic,
not challenge-specific, and was recorded as checklist-only in
analysis/rag-records.md.
Solve
The solver is solve/solve.py. It performs four steps:
- Send an SSTI payload that writes
/www/public/.htaccess:
Options +ExecCGI and AddHandler cgi-script .sh.
- Send an SSTI payload that writes
/www/public/jerrytok.shwith a CGI header
and /readflag.
- Send an SSTI payload that calls
chmodon the shell script. - Request
/jerrytok.sh, extract the HTB-format flag value, and write it to
the requested output file.
Remote execution was run through the harness wrapper:
python3 scripts/challenge_exec.py Web/Jerrytok -- python3 Web/Jerrytok/solve/solve.py \
--base-url http://<TARGET>:32250 \
--output Web/Jerrytok/analysis/flag-candidate.txt \
--transcript Web/Jerrytok/analysis/remote-solve-transcript.txtThe candidate was captured with challenge_harness.py capture-flag, then the
temporary candidate file was removed.
Flag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
- Blocking only obvious command execution functions is not enough when a
template injection can still call file-writing and permission-changing
functions.
- Apache
.htaccessbehavior can turn a file-write primitive into execution if
overrides and CGI handlers are available.
- For Medium Web challenges, local reproduction prevents wasted remote probing:
the final remote run was a single validated write/chmod/read sequence.
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: Jerrytok
- Category: Web
- Difficulty: Medium
- Mode: hybrid
- Remote instance: none
- Start time: 2026-06-12T15:28:58Z
- 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 |
|---|---|---|---|---|
files/a12c738f-4a20-4868-a833-da74ea8fd3ac.zip | 718681 | <hash redacted> | Zip archive data, at least v1.0 to extract, compression method=store | zip entries: 86 shown in artifact inventory JSON |
files/extracted/web_jerrytok/Dockerfile | 1441 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/build-docker.sh | 133 | <hash redacted> | Bourne-Again shell script text executable, ASCII text | |
files/extracted/web_jerrytok/challenge/.env | 1971 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/.env.test | 215 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/.gitignore | 468 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/assets/app.js | 321 | <hash redacted> | Java source, Unicode text, UTF-8 text | |
files/extracted/web_jerrytok/challenge/assets/bootstrap.js | 210 | <hash redacted> | Java source, ASCII text | |
files/extracted/web_jerrytok/challenge/assets/controllers/hello_controller.js | 500 | <hash redacted> | Java source, ASCII text | |
files/extracted/web_jerrytok/challenge/assets/controllers.json | 323 | <hash redacted> | JSON data | |
files/extracted/web_jerrytok/challenge/assets/styles/app.css | 40 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/bin/console | 492 | <hash redacted> | a /usr/bin/env php script text executable, ASCII text | |
files/extracted/web_jerrytok/challenge/bin/phpunit | 776 | <hash redacted> | a /usr/bin/env php script text executable, ASCII text | |
files/extracted/web_jerrytok/challenge/composer.json | 3222 | <hash redacted> | JSON data | |
files/extracted/web_jerrytok/challenge/config/bundles.php | 925 | <hash redacted> | PHP script text, ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/asset_mapper.yaml | 125 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/cache.yaml | 687 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/debug.yaml | 261 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/doctrine.yaml | 1516 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/doctrine_migrations.yaml | 268 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/framework.yaml | 411 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/mailer.yaml | 56 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/messenger.yaml | 861 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/monolog.yaml | 2009 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/notifier.yaml | 357 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/routing.yaml | 315 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/security.yaml | 1732 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/translation.yaml | 163 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/twig.yaml | 91 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/validator.yaml | 316 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/packages/web_profiler.yaml | 337 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/preload.php | 184 | <hash redacted> | PHP script text, ASCII text | |
files/extracted/web_jerrytok/challenge/config/routes/framework.yaml | 120 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/routes/security.yaml | 79 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/routes/web_profiler.yaml | 258 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/routes.yaml | 72 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/config/services.yaml | 1328 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/importmap.php | 849 | <hash redacted> | PHP script text, ASCII text | |
files/extracted/web_jerrytok/challenge/migrations/.gitignore | 0 | <hash redacted> | empty | |
files/extracted/web_jerrytok/challenge/phpunit.xml.dist | 1195 | <hash redacted> | XML 1.0 document text, ASCII text | |
files/extracted/web_jerrytok/challenge/public/.htacess | 698 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/public/index.php | 199 | <hash redacted> | PHP script text, ASCII text | |
files/extracted/web_jerrytok/challenge/public/static/css/main.css | 6332 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/challenge/public/static/images/ehV1Ut.jpg | 82291 | <hash redacted> | JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, progressive, precision 8, 1396x1113, components 3 | |
files/extracted/web_jerrytok/challenge/public/static/images/favicon.png | 16467 | <hash redacted> | PNG image data, 862 x 760, 8-bit colormap, non-interlaced | |
files/extracted/web_jerrytok/challenge/public/static/images/sticker_220x200-bg_ffffff-pad_220x200_ffffff.u1_gs3scf.jpg | 9535 | <hash redacted> | JPEG image data, JFIF standard 1.01, resolution (DPI), density 72x72, segment length 16, baseline, precision 8, 220x200, components 3 | |
files/extracted/web_jerrytok/challenge/public/static/images/sticker_220x200-bg_ffffff-pad_220x200_ffffff.u3_xijmuh.jpg | 8164 | <hash redacted> | JPEG image data, JFIF standard 1.01, resolution (DPI), density 72x72, segment length 16, baseline, precision 8, 220x200, components 3 | |
files/extracted/web_jerrytok/challenge/public/static/images/sticker_220x200-pad_220x200_ffffff.u1_emzmha.jpg | 8378 | <hash redacted> | JPEG image data, JFIF standard 1.01, resolution (DPI), density 72x72, segment length 16, baseline, precision 8, 220x200, components 3 | |
files/extracted/web_jerrytok/challenge/public/static/images/sticker_375x360.u1_a1pvp9.png | 42267 | <hash redacted> | PNG image data, 375 x 360, 8-bit/color RGBA, non-interlaced | |
files/extracted/web_jerrytok/challenge/public/static/images/sticker_375x360.u1_hpcnno.png | 57149 | <hash redacted> | PNG image data, 375 x 360, 8-bit/color RGBA, non-interlaced | |
files/extracted/web_jerrytok/challenge/public/static/images/sticker_375x360.u2_gpiuxw.png | 80113 | <hash redacted> | PNG image data, 375 x 360, 8-bit/color RGBA, non-interlaced | |
files/extracted/web_jerrytok/challenge/public/static/images/transdimensional_council_of_ricks_pin_by_stewartisme-d7dtrr7_jxkjm9.png | 98312 | <hash redacted> | PNG image data, 830 x 962, 8-bit/color RGBA, non-interlaced | |
files/extracted/web_jerrytok/challenge/public/static/images/tumblr_nwv20kmNTl1ujcz1oo1_1280_cvjpyj.jpg | 286820 | <hash redacted> | JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, Exif Standard: [TIFF image data, little-endian, direntries=10, manufacturer=Apple, model=iPhone 6, orientation=upper-left, xresolution=150, yresolution=158, resolutionunit=2, software=8.4.1, datetime=2015:10:26 23:03:21, GPS-Data], baseline, precision 8, 1280x960, components 3 | |
files/extracted/web_jerrytok/challenge/src/Controller/.gitignore | 0 | <hash redacted> | empty | |
files/extracted/web_jerrytok/challenge/src/Controller/DefaultController.php | 894 | <hash redacted> | PHP script text, ASCII text | |
files/extracted/web_jerrytok/challenge/src/Entity/.gitignore | 0 | <hash redacted> | empty | |
files/extracted/web_jerrytok/challenge/src/Kernel.php | 201 | <hash redacted> | PHP script text, ASCII text | |
files/extracted/web_jerrytok/challenge/symfony.lock | 8024 | <hash redacted> | JSON data | |
files/extracted/web_jerrytok/challenge/templates/base.html.twig | 5024 | <hash redacted> | HTML document text, ASCII text | |
files/extracted/web_jerrytok/challenge/tests/bootstrap.php | 320 | <hash redacted> | PHP script text, ASCII text | |
files/extracted/web_jerrytok/challenge/translations/.gitignore | 0 | <hash redacted> | empty | |
files/extracted/web_jerrytok/config/httpd.conf | 2833 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/config/supervisord.conf | 270 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/entrypoint.sh | 436 | <hash redacted> | Neil Brown's ash script text executable, ASCII text | |
files/extracted/web_jerrytok/flag | 27 | <hash redacted> | ASCII text | |
files/extracted/web_jerrytok/readflag.c | 106 | <hash redacted> | c program text, ASCII text |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-12T15:28:58Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-12T15:29:08Z | artifact inventory | analysis/artifact-inventory.json | 66 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-12T15:37:04Z | hypothesis recorded | hypothesis-board.md | Exploit Twig SSTI in location to write .htaccess and a CGI shell under /www/public, then execute /readflag through the CGI shell. | High | Run solve.py through challenge_exec against the remote once. |
| 2026-06-12T15:37:04Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-12T15:37:04Z | checkpoint recorded | analysis/checkpoint-analysis-20260612T153704538887Z-f0bcefb2.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-12T15:37:04Z | research record | analysis/research/research-records.md | Research tagged MATCHED | Medium | Validate against current evidence |
| 2026-06-12T15:37:31Z | RAG query | analysis/rag/rag-query-20260612T153713598450Z-657fe640.txt | RAG helper exited 0; output saved | Medium | Record retrieval tag and validation |
| 2026-06-12T15:37:58Z | RAG record | analysis/rag-records.md | Retrieved memory tagged GENERIC | Medium | Validate or reject with live evidence |
| 2026-06-12T15:37:58Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-12T15:37:58Z | instrumentation plan | analysis/instrumentation-plan.md | Prove and execute the source-backed Twig SSTI to CGI-readflag chain with bounded remote mutation. | High | Stop after one failed remote flag response or any 500 during the write/chmod sequence; do not fuzz or branch blindly. |
| 2026-06-12T15:38:07Z | evaluator | analysis/evaluator-20260612T153807844284Z-b8dbe751.md | Proceed | High | Run solve.py against <TARGET>:32250 through challenge_exec. |
| 2026-06-12T15:38:29Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-12T15:39:40Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
-
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: Web
- Challenge: Jerrytok
- 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.
Reusable Lessons
-
Dead Ends
-
Tool Quirks
-
Evidence Paths
-
Ingestion Decision
- Proposed for LightRAG: yes/no
- 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 | Exploit Twig SSTI in location to write .htaccess and a CGI shell under /www/public, then execute /readflag through the CGI shell. | DefaultController creates a Twig template from user-controlled location; entrypoint disables direct command functions but leaves file_put_contents and chmod enabled; Apache allows .htaccess overrides in /www/public; local validation executed /cmd.sh with redacted flag-like output. | Remote target must match the local source and accept the same write/chmod sequence. | Run solve.py through challenge_exec against the remote once. | High | Active |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit Condition |
|---|
Technical analogy
How to remember this solve
Think of the web app like a building with signs on every door. The solve usually comes from reading the map carefully, finding the door the app forgot to hide, then sending the exact request that proves you understand the route.
For Jerrytok, 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.