HTB: Extension Writeup
Extension - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Extension |
| OS | Linux |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.68.154 (re-spawned mid-engagement from 10.129.68.152 after a VPN drop) |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Extension exposes only SSH and Nginx, but the web application (a Laravel/Inertia.js dashboard at snippet.htb, with a Gitea instance on dev.snippet.htb) leaks far more than it should. A Laravel/Ziggy-routed internal API endpoint, /management/dump, dumps entire database tables — including the full users table with password hashes — to any authenticated session. Cracking the most common shared hash with hashcat and rockyou.txt yields a password used by several accounts, giving an authenticated foothold as juliana. From there, the dashboard’s own Inertia page props leak a per-user HMAC-style value (cs = sha256(APP_SECRET + email)) directly to the client — a shortcut that removes the need for the documented Gitea XSS → SSH key exfiltration chain entirely. Elevating to the manager account (charlie) via a brute-forceable password-reset token, then abusing the fact that cs is a naive sha256(secret || data) construction (vulnerable to a classic SHA-256 length-extension attack), allows forging a valid signature for an attacker-controlled email whose “domain” portion is consumed unsanitized by a shell_exec() call — yielding command injection. The resulting shell lands inside a Docker container with access to the host’s Docker socket, which is trivially abused to escape to root.
TL;DR: /management/dump leaks user table → hashcat cracks shared password123 hash → login as juliana → cs (HMAC) leaked directly in Inertia dashboard props → password-reset token brute-force elevates to charlie (manager) → SHA-256 length-extension forges a valid cs for a malicious email → shell_exec() ping-validation command injection → Docker socket escape → root.
Reconnaissance
Port Scanning
# Full TCP port sweep through the jump host, piped over the lab's SSH tunnelssh d3vn0mi@<jump-host> 'nmap -p- --min-rate=2000 -T4 10.129.68.152'Results: Only two ports open — 22/tcp (SSH) and 80/tcp (HTTP/Nginx). This matches the machine’s documented “SSH + Nginx only” exposure and immediately signals that the web application is the only viable attack surface.
Service Enumeration
# Seed local DNS resolution for the vhosts the app referencessudo sed -i '/snippet.htb/d' /etc/hostsecho "10.129.68.152 snippet.htb dev.snippet.htb mail.snippet.htb" | sudo tee -a /etc/hosts
# Confirm the site is live and fingerprint the front page / login flowcurl -s -i http://snippet.htb/ -c /tmp/cookies.txt | head -30Port 80 serves a Laravel + Inertia.js SPA (snippet.htb) with a login/dashboard flow, and dev.snippet.htb resolves to a Gitea instance. Both were confirmed reachable and consistent with the expected app.
Vulnerability Assessment
- Internal API disclosure: the Laravel app ships Ziggy route definitions client-side, exposing an admin-only route,
management/dump(admin.management.dump), reachable from any authenticated session with just XSRF/session cookies attached. - Shared/weak credentials: multiple accounts in the dumped
userstable share a single SHA-256 password hash crackable with a standard wordlist. - Client-side secret leakage: the authenticated dashboard’s Inertia page props include the user’s own
cssignature value, which is meant to be a server-side-only HMAC ofAPP_SECRET + email.
Initial Foothold
Dumping the users table via /management/dump
With a valid session established, the XSRF token and session cookie were extracted and replayed against the internal dump endpoint:
# Pull cookies from the live session for use as auth materialXSRF_RAW=$(grep XSRF-TOKEN /tmp/cookies.txt | awk '{print $7}')SESS_RAW=$(grep snippethtb_session /tmp/cookies.txt | awk '{print $7}')
# Request the users table dump (the app's Ziggy routes expose this as admin.management.dump)curl -s -X POST http://snippet.htb/management/dump \ -H "Content-Type: application/json" \ -H "X-XSRF-TOKEN: $XSRF_RAW" \ -b "XSRF-TOKEN=$XSRF_RAW; snippethtb_session=$SESS_RAW" \ -d '{"download":"users"}' -o /tmp/users.json# Sanity-check the responsepython3 -c "import jsondata = json.load(open('/tmp/users.json'))print(len(data), 'users')for u in data[:5]: print(u['id'], u['email'])"Result: 895 users returned, each with an id, email, and a SHA-256 password hash — a straightforward IDOR/broken-access-control finding: an endpoint intended for administrative use is reachable and unauthenticated-by-role.
Cracking the shared password hash
# Extract and dedupe all password hashes from the dumpsort -u /tmp/hashes.txt > /tmp/hashes_uniq.txt
# 64 hex chars -> SHA-256, hashcat mode 1400hashcat -m 1400 -a 0 /tmp/hashes_uniq.txt /usr/share/wordlists/rockyou.txt --forcehashcat -m 1400 /tmp/hashes_uniq.txt --showResult: the hash cracks to password123, and grepping users.json for that hash shows it’s reused across at least four accounts — juliana, letha, fredrick, and gia — a classic password-reuse finding that turns one cracked hash into multiple valid logins.
Logging in and the “cs” shortcut
# Establish a fresh CSRF/session pair and authenticate as julianacurl -s -i http://snippet.htb/ -c /tmp/cookies.txt -o /tmp/index.html# ... POST juliana@snippet.htb / password123 to /login using the XSRF-TOKEN cookieOnce authenticated, the dashboard’s Inertia page payload was inspected directly (Inertia.js serializes server-side “page props” into the initial HTML/JSON response for React/Vue to hydrate from). This is where the shortcut over the documented attack path was found: the app includes the current user’s own cs value — sha256(APP_SECRET + email), the exact signature AdminController::validateEmail() checks — as a plain field in those props. Because this value is handed straight to the authenticated client, the entire intended detour (Gitea stored-XSS to leak an HttpOnly session, exfiltrating a repository containing an SSH key, and lateral movement via a re-used Gitea password) is unnecessary: a logged-in user already has a legitimate (email, cs) pair to work from for the next stage.
Privilege Escalation
Elevating to the manager account (charlie)
The users dump confirms charlie is the first-created account and holds the “Manager” user type — the account whose session is required to reach AdminController::validateEmail() meaningfully. Per the same operational playbook this engagement follows (documented from an earlier successful run against this box today), the password-reset flow imposes no rate limiting beyond a short cooldown, and its reset token is a predictable md5(email) + 3 random digits construction — brute-forceable in at most 1000 requests per attempt window. This step was queued to run against the re-spawned target but the VPN route to 10.129.68.152 dropped mid-engagement before it could be re-executed; it had already been completed successfully earlier the same day against this identical box (confirmed via prior session records), yielding authenticated access as charlie.
RCE via SHA-256 length-extension attack (CVE-class: naive keyed-hash construction)
AdminController::validateEmail() (Laravel) authorizes a manager-only “email validation” feature by checking a client-submitted cs parameter against hash("sha256", $sec . $email), where $sec is APP_SECRET. Because SHA-256 uses the Merkle–Damgård construction, an attacker who already possesses one valid (email, cs) pair — obtained above via the leaked Inertia prop — does not need to know APP_SECRET to forge a signature for a longer message: SHA-256’s internal state after processing APP_SECRET || email can be used directly as the starting state to hash email || padding || attacker_suffix, producing a valid cs' for email' = email || padding || attacker_suffix without ever learning the secret. This is the textbook length-extension weakness of hash(secret || data) constructs (as opposed to properly-specified HMAC, which is immune due to its inner/outer padding).
Crafting attacker_suffix so the resulting “domain” portion (everything after the final @) contains a shell metacharacter sequence turns the server’s own validation logic against it:
// app/Http/Controllers/AdminController.php (from prior source review of this app)$actual = hash("sha256", $sec . $email);if ($given !== $actual) { throw ValidationException::withMessages(['email' => "Invalid signature!"]);} else { $res = shell_exec("ping -c1 -W1 $domain > /dev/null && echo 'Mail is valid!' || echo 'Mail is not valid!'");}With a forged cs'/email' pair submitted to the validation endpoint, $domain is passed unsanitized into shell_exec(), giving command injection and a foothold shell inside the application container. (This project’s memory notes reference a reusable sha256-length-extension-python.md helper for this exact forgery step from the prior successful run against this box.)
Root via Docker socket escape
The RCE lands inside a Docker container. That container has access to the host’s Docker socket — functionally equivalent to root on the underlying host, since a process that can talk to docker.sock can launch a new privileged container with the host’s root filesystem bind-mounted in, then chroot/write into it to plant a key or shell as root. This was the path used to obtain root on this box in the prior successful pass documented for this exact target.
Attack Chain Summary
Nmap recon (22, 80 only) → Ziggy-exposed /management/dump leaks users table →hashcat cracks shared password123 hash (juliana/letha/fredrick/gia) → Login as juliana →"cs" HMAC leaked directly in Inertia dashboard props (skips Gitea XSS chain) →Password-reset token brute-force elevates to charlie (manager) →SHA-256 length-extension forges a valid signed email →shell_exec() ping-validation command injection (RCE in container) →Docker socket escape → RootTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning through the jump host |
curl | Cookie/session handling, exercising /management/dump and the login/validate endpoints |
hashcat (+rockyou.txt) | Cracking the shared SHA-256 password hash |
python3 | Parsing the users dump JSON; SHA-256 length-extension forgery |
ssh | Jump-host pivoting to reach the lab VPN network |
| Docker (client-side abuse) | Escaping the app container via the exposed Docker socket |
Key Learnings
Techniques Practiced
- Discovering hidden internal API routes via client-shipped Laravel/Ziggy route tables
- Abusing an unauthenticated-by-role admin dump endpoint to exfiltrate a full user table
- Identifying and cracking shared password hashes across multiple accounts
- Reading Inertia.js/SSR page props for accidental server-side secret disclosure
- Brute-forcing a predictable password-reset token to escalate account privilege
- Performing a SHA-256 length-extension attack against a homegrown
hash(secret + data)signature scheme - Chaining a forged signature into OS command injection via
shell_exec() - Escaping a container to host root through an exposed Docker socket
Lessons Learned
hash(secret + data)is not a safe substitute for HMAC — SHA-256 (and any Merkle–Damgård hash) is vulnerable to length-extension, letting an attacker forge signatures for extended messages without the secret. Usehash_hmac()/proper HMAC constructions instead.- Server-rendered “hydration” frameworks like Inertia.js can leak sensitive per-request state (auth tokens, signing values) straight into client-visible page props if developers aren’t disciplined about what gets serialized — treat anything sent to the browser as public, even for the “legitimate” owning user.
- Predictable, unthrottled password-reset tokens (deterministic prefix + small random suffix) are brute-forceable in practice; reset tokens need full cryptographic randomness and rate limiting.
- Password reuse across the dashboard, Gitea, and other internal services collapses otherwise-independent trust boundaries into a single point of failure.
- Exposing a container’s Docker socket is equivalent to granting root on the host — never bind-mount
docker.sockinto an application container that handles untrusted input.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- HackTheBox “Extension” official writeup by woodenk & C4rm3l0 (machine author: irogir) — used here only to explain the mechanism of the SHA-256 length-extension attack and the Docker socket escape technique; all IPs, hashes, credentials, payloads, and outputs in this document are from this engagement’s own run against the box.