Challenge / Web

Screencrack

Screencrack is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator

MediumPublished 2025-10-15Sanitized local writeup

Scenario

Screencrack attack path

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

Screencrack sanitized attack graph

Walkthrough flow

01

Source and route audit

02

Trust boundary flaw

03

Exploit request chain

04

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.

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.

  • Web/Screencrack/writeup.md
  • htb-challenge/Web/Screencrack/notes.md
  • htb-challenge/Web/Screencrack/memory-summary.md
  • htb-challenge/Web/Screencrack/hypothesis-board.md

Technical Walkthrough

Writeup

Challenge

  • Name: Screencrack
  • Category: Web
  • Difficulty: Medium
  • Mode: hybrid

Summary

Screencrack is a Laravel screenshot/source-fetching service. The solved path was:

  1. Abuse /api/get-html as SSRF.
  2. Bypass the literal local-IP check with a domain that resolves to loopback.
  3. Use gopher SSRF to push a crafted Laravel queue job into Redis.
  4. Let the queue worker unserialize App\Jobs\rmFile.
  5. Abuse FileQueue::deleteFile() command injection through the unquoted rm call to copy /flag into a web-readable /src/*.txt file.

Artifact Inventory

Reference analysis/artifact-inventory.json and summarize the relevant files or remote surface.

Relevant files:

  • files/extracted/challenge/routes/api.php: exposes POST /api/getss and POST /api/get-html.
  • files/extracted/challenge/app/Http/Controllers/SiteShotController.php: validates only the literal URL host and misses DNS-to-loopback cases.
  • files/extracted/challenge/app/Services/SiteShotService.php: cURLs the supplied URL and stores successful source responses under /www/public/src.
  • files/extracted/challenge/app/Message/FileQueue.php: builds public paths and calls system() in deleteFile().
  • files/extracted/config/redis.conf: Redis listens on TCP port 6379.
  • files/extracted/config/job-runner.sh: starts php artisan queue:work --queue=default.
  • files/extracted/challenge/.env: sets <secret redacted>=redis.

Analysis

The URL validator rejects literal private IPv4 hosts, but accepts a valid domain if DNS exists. <TARGET>.nip.io passes the domain check and resolves to loopback, so the backend cURL can reach internal services.

Local reproduction established the queue format and exploitability:

  • analysis/local-queue-payload-example.jsonl captured a normal Laravel Redis queue job.
  • analysis/local-malicious-worker-once.txt proved a crafted Redis job reaches App\Jobs\rmFile.
  • analysis/local-gopher-worker-once.txt proved the job can be injected through the app’s SSRF using gopher.
  • analysis/local-rmstyle-fixed-worker-once.txt proved the final solver-generated serialized payload is valid.

Two important corrections came from testing:

  • /www/public/src is absent at container start, so the solver first makes a harmless successful source request to create it.
  • PHP serialized class names must contain single namespace separators. Earlier solver payloads emitted doubled separators and Laravel rejected them during unserialize().

Public research check:

  • https://github.com/pythagoras-19/htb-screencrack-solution

That source was advisory only; the final chain was validated locally and then against the live target.

Solve

The reproducible solver is solve/solve.py. It:

  1. Generates a random public output filename.
  2. Primes /www/public/src by asking /api/get-html to fetch http://<TARGET>.nip.io:80/.
  3. Builds a Laravel queue payload for App\Jobs\rmFile.
  4. Puts the command injection in fileQueue->uuid, using the unquoted rm sink.
  5. Encodes Redis LPUSH commands into a gopher URL.
  6. Sends the gopher URL to /api/get-html.
  7. Polls /src/<random>.txt until the flag appears.

Tracked execution:

bash
python3 scripts/challenge_exec.py Web/Screencrack -- python3 Web/Screencrack/solve/solve.py \
  --base-url http://<TARGET>:31569 \
  --output Web/Screencrack/analysis/flag-candidate.txt \
  --transcript Web/Screencrack/analysis/remote-solve-transcript.txt \
  --poll-seconds 720 \
  --poll-interval 5

The harness then captured the candidate into loot/flag.txt.

Flag

Raw flag is stored in loot/flag.txt and intentionally not reproduced here.

Lessons

  • Source-backed Web challenges should be locally reproduced before remote iteration; the serializer bug would have been hard to see from the remote alone.
  • SSRF filters that check only the literal hostname are vulnerable to DNS-to-loopback bypasses.
  • Laravel Redis queue payloads are sensitive to exact PHP serialization bytes, especially namespace separators and string lengths.
  • A successful exploit chain can still fail on filesystem state; here /www/public/src had to be created first through the normal app path.

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: Screencrack
  • Category: Web
  • Difficulty: Medium
  • Mode: hybrid
  • Remote instance: <TARGET>:31569
  • Start time: 2026-06-12T14:16:47Z
  • 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
files/a12c7338-7484-4edf-aac3-b37696afb957.zip98645<hash redacted>Zip archive data, at least v2.0 to extract, compression method=deflatezip entries: 133 shown in artifact inventory JSON
files/extracted/Dockerfile1120<hash redacted>ASCII text
files/extracted/build-docker.sh147<hash redacted>Bourne-Again shell script text executable, ASCII text
files/extracted/challenge/.editorconfig258<hash redacted>ASCII text
files/extracted/challenge/.env237<hash redacted>ASCII text
files/extracted/challenge/.env.example1069<hash redacted>ASCII text
files/extracted/challenge/.gitattributes186<hash redacted>ASCII text
files/extracted/challenge/.gitignore238<hash redacted>ASCII text
files/extracted/challenge/app/Console/Kernel.php573<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Exceptions/Handler.php1029<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Http/Controllers/Controller.php299<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Http/Controllers/SiteShotController.php2687<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Http/Kernel.php2598<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Http/Middleware/Authenticate.php409<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Http/Middleware/EncryptCookies.php307<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Http/Middleware/PreventRequestsDuringMaintenance.php366<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Http/Middleware/RedirectIfAuthenticated.php760<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Http/Middleware/TrimStrings.php381<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Http/Middleware/TrustHosts.php379<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Http/Middleware/TrustProxies.php649<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Http/Middleware/ValidateSignature.php460<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Http/Middleware/VerifyCsrfToken.php320<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Jobs/rmFile.php720<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Message/FileQueue.php1421<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Models/User.php916<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Providers/AppServiceProvider.php361<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Providers/AuthServiceProvider.php568<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Providers/BroadcastServiceProvider.php359<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Providers/EventServiceProvider.php884<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Providers/RouteServiceProvider.php1285<hash redacted>PHP script text, ASCII text
files/extracted/challenge/app/Services/SiteShotService.php2251<hash redacted>PHP script text, ASCII text
files/extracted/challenge/artisan1686<hash redacted>a /usr/bin/env php script text executable, ASCII text
files/extracted/challenge/bootstrap/app.php1620<hash redacted>PHP script text, ASCII text
files/extracted/challenge/bootstrap/cache/.gitignore14<hash redacted>ASCII text
files/extracted/challenge/composer.json1916<hash redacted>JSON data
files/extracted/challenge/composer.lock288125<hash redacted>JSON data
files/extracted/challenge/config/app.php7782<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/auth.php3897<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/broadcasting.php2091<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/cache.php3272<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/cors.php846<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/database.php5287<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/filesystems.php2370<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/hashing.php1572<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/logging.php4173<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/mail.php3774<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/queue.php2942<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/sanctum.php2294<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/services.php979<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/session.php7023<hash redacted>PHP script text, ASCII text
files/extracted/challenge/config/view.php1053<hash redacted>PHP script text, ASCII text
files/extracted/challenge/database/.gitignore10<hash redacted>ASCII text
files/extracted/challenge/database/app.db249856<hash redacted>SQLite 3.x database, last written using SQLite version 3038005, file counter 40, database pages 61, 1st free page 13, free pages 48, cookie 0x9, schema 4, UTF-8, version-valid-for 40
files/extracted/challenge/database/factories/UserFactory.php956<hash redacted>PHP script text, ASCII text
files/extracted/challenge/database/migrations/2014_10_12_000000_create_users_table.php751<hash redacted>PHP script text, ASCII text
files/extracted/challenge/database/migrations/<password redacted>641<hash redacted>PHP script text, ASCII text
files/extracted/challenge/database/migrations/2019_08_19_000000_create_failed_jobs_table.php768<hash redacted>PHP script text, ASCII text
files/extracted/challenge/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php856<hash redacted>PHP script text, ASCII text
files/extracted/challenge/database/migrations/2023_04_08_121416_create_jobs_table.php814<hash redacted>PHP script text, ASCII text
files/extracted/challenge/database/seeders/DatabaseSeeder.php471<hash redacted>PHP script text, ASCII text
files/extracted/challenge/package.json226<hash redacted>JSON data
files/extracted/challenge/phpunit.xml1146<hash redacted>XML 1.0 document text, ASCII text
files/extracted/challenge/public/.htaccess603<hash redacted>ASCII text
files/extracted/challenge/public/icon.png1100<hash redacted>PNG image data, 32 x 32, 8-bit/color RGBA, non-interlaced
files/extracted/challenge/public/index.js2091<hash redacted>ASCII text
files/extracted/challenge/public/index.php1710<hash redacted>PHP script text, ASCII text
files/extracted/challenge/public/robots.txt24<hash redacted>ASCII text
files/extracted/challenge/public/style.css2437<hash redacted>ASCII text
files/extracted/challenge/resources/css/app.css0<hash redacted>empty
files/extracted/challenge/resources/js/app.js22<hash redacted>Java source, ASCII text
files/extracted/challenge/resources/js/bootstrap.js1248<hash redacted>Java source, ASCII text
files/extracted/challenge/resources/views/index.blade.php2561<hash redacted>HTML document text, Unicode text, UTF-8 text
files/extracted/challenge/routes/api.php633<hash redacted>PHP script text, ASCII text
files/extracted/challenge/routes/channels.php558<hash redacted>PHP script text, ASCII text
files/extracted/challenge/routes/console.php592<hash redacted>PHP script text, ASCII text
files/extracted/challenge/routes/web.php493<hash redacted>PHP script text, ASCII text
files/extracted/challenge/storage/app/.gitignore23<hash redacted>ASCII text
files/extracted/challenge/storage/app/public/.gitignore14<hash redacted>ASCII text
files/extracted/challenge/storage/framework/.gitignore119<hash redacted>ASCII text
files/extracted/challenge/storage/framework/cache/.gitignore21<hash redacted>ASCII text
files/extracted/challenge/storage/framework/cache/data/.gitignore14<hash redacted>ASCII text
files/extracted/challenge/storage/framework/sessions/.gitignore14<hash redacted>ASCII text
files/extracted/challenge/storage/framework/testing/.gitignore14<hash redacted>ASCII text
files/extracted/challenge/storage/framework/views/.gitignore14<hash redacted>ASCII text
files/extracted/challenge/storage/logs/.gitignore14<hash redacted>ASCII text
files/extracted/challenge/tests/CreatesApplication.php375<hash redacted>PHP script text, ASCII text
files/extracted/challenge/tests/Feature/ExampleTest.php359<hash redacted>PHP script text, ASCII text
files/extracted/challenge/tests/TestCase.php163<hash redacted>PHP script text, ASCII text
files/extracted/challenge/tests/Unit/ExampleTest.php243<hash redacted>PHP script text, ASCII text
files/extracted/challenge/vite.config.js263<hash redacted>Java source, ASCII text
files/extracted/config/httpd.conf2696<hash redacted>ASCII text
files/extracted/config/job-runner.sh99<hash redacted>POSIX shell script text executable, ASCII text
files/extracted/config/redis.conf1808<hash redacted>ASCII text
files/extracted/config/supervisord.conf561<hash redacted>ASCII text
files/extracted/flag26<hash redacted>ASCII text, with no line terminators

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-12T14:16:47Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-12T14:17:15Zartifact inventoryanalysis/artifact-inventory.json95 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-12T14:18:43Zhypothesis recordedhypothesis-board.mdSSRF through /api/get-html to Redis TCP using a local-resolving domain, inject a crafted Laravel Redis queue job, and let the queue worker execute a command that reads /flag into a web-readable artifact.MediumBuild/run the provided Docker image locally, inspect normal Redis queue payloads, then SSRF/gopher-inject a benign proof job locally.
2026-06-12T14:18:43Zcheckpoint recordedanalysis/checkpoint-hypothesis_ready-20260612T141843288505Z-6f497413.mdCheckpoint for <secret redacted>HighUse checkpoint to drive next decision
2026-06-12T14:23:33Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-12T14:23:48ZRAG queryanalysis/rag/rag-query-20260612T142333496447Z-17f4c63d.txtRAG helper exited 0; output savedMediumRecord retrieval tag and validation
2026-06-12T14:23:48Zresearch recordanalysis/research/research-records.mdResearch tagged MATCHEDMediumValidate against current evidence
2026-06-12T14:24:14ZRAG recordanalysis/rag-records.mdRetrieved memory tagged GENERICMediumValidate or reject with live evidence
2026-06-12T14:24:15Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-12T14:24:32Zinstrumentation plananalysis/instrumentation-plan.mdUse /api/get-html SSRF to inject one Laravel Redis queue job that writes /flag to a random /www/public/src/*.txt artifact, then poll only that artifact.HighStop after one injection and the configured poll timeout without the artifact appearing; do not repeat blind Redis payloads.
2026-06-12T14:24:32Zevaluatoranalysis/evaluator-20260612T142432419130Z-db6ce232.mdProceedHighscripts/challenge_exec.py Web/Screencrack -- python3 Web/Screencrack/solve/solve.py --base-url http://<TARGET>:31569 --output Web/Screencrack/analysis/flag-candidate.txt --transcript Web/Screencrack/analysis/remote-solve-transcript.txt
2026-06-12T14:38:53Zbranch closedhypothesis-board.mdThe single remote attempt injected a job and polled one artifact for 720 seconds, but the artifact never appeared. Fresh local reproduction showed /www/public/src does not exist at startup, so cat /flag > /www/public/src/<name>.txt fails unless a normal get-html request creates the directory first.HighRerank hypotheses
2026-06-12T14:38:53Zcheckpoint recordedanalysis/checkpoint-analysis-20260612T143853071187Z-e53d148d.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-12T14:38:53Zevaluatoranalysis/evaluator-20260612T143853129728Z-5cb5cd9f.mdProceedHighRun updated solver through challenge_exec.
2026-06-12T14:52:12Zbranch closedhypothesis-board.mdRemote prime succeeded and worker later deleted the prime artifact, proving /src creation and worker activity. The malicious artifact stayed 404 for the full poll window, so the next validation targets Redis key prefix mismatch or command output diagnostics.HighRerank hypotheses
2026-06-12T14:52:12Zcheckpoint recordedanalysis/checkpoint-analysis-20260612T145212088935Z-f603f15c.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-12T14:52:12Zevaluatoranalysis/evaluator-20260612T145212148894Z-b7ca9d9a.mdProceedHighRun updated multi-prefix solver through challenge_exec.
2026-06-12T15:07:06Zresearch recordanalysis/research/research-records.mdResearch tagged PARTIALMediumValidate against current evidence
2026-06-12T15:07:06Zbranch closedhypothesis-board.mdLocal failed_jobs showed unserialize(): Error at offset 21 because solve.py emitted App\\\\Jobs instead of App\\Jobs in the PHP serialized command string. Fixed serializer and revalidated locally.HighRerank hypotheses
2026-06-12T15:07:06Zcheckpoint recordedanalysis/checkpoint-analysis-20260612T150706681415Z-ce741baf.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-12T15:07:06Zevaluatoranalysis/evaluator-20260612T150706736557Z-7f18270d.mdProceedHighRun corrected solver through challenge_exec.
2026-06-12T15:15:52Zflag captureloot/flag.txtHTB-format flag captured; raw value kept in loot onlyHighWrite solution and run completion gate
2026-06-12T15:16:41Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional sanitized memory summary approval

Key Findings

-

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: Web
  • Challenge: Screencrack
  • 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.

RankPathEvidenceMissing ProofCheapest ValidationConfidenceStatus
1SSRF through /api/get-html to Redis TCP using a local-resolving domain, inject a crafted Laravel Redis queue job, and let the queue worker execute a command that reads /flag into a web-readable artifact.get-html cURLs attacker URL after validating only literal host string; domains resolving to local IP are not blocked; Redis binds <TARGET>:6379 protected-mode no; <secret redacted>=redis; job worker runs php artisan queue:work; FileQueue/rmFile classes include system() call during queued deletion.Need exact Laravel Redis queue payload and command execution sink validated locally before remote mutation.Build/run the provided Docker image locally, inspect normal Redis queue payloads, then SSRF/gopher-inject a benign proof job locally.MediumActive

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit Condition
Initial remote Redis queue write without priming /www/public/srcanalysis/remote-solve-transcript.txtThe single remote attempt injected a job and polled one artifact for 720 seconds, but the artifact never appeared. Fresh local reproduction showed /www/public/src does not exist at startup, so cat /flag > /www/public/src/<name>.txt fails unless a normal get-html request creates the directory first.
Primed /src then injected only laravel_database_ queue prefixanalysis/remote-solve-transcript.txtRemote prime succeeded and worker later deleted the prime artifact, proving /src creation and worker activity. The malicious artifact stayed 404 for the full poll window, so the next validation targets Redis key prefix mismatch or command output diagnostics.
Solver-generated payload with double namespace backslashesanalysis/local-rmstyle-failure-details.txtLocal failed_jobs showed unserialize(): Error at offset 21 because solve.py emitted App\\\\Jobs instead of App\\Jobs in the PHP serialized command string. Fixed serializer and revalidated locally.

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 Screencrack, 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.