HTB: Extension Writeup

Extension - HackTheBox Writeup

Machine Information

AttributeDetails
NameExtension
OSLinux
DifficultyHard
PointsN/A
Release DateN/A
IP Address10.129.68.154 (re-spawned mid-engagement from 10.129.68.152 after a VPN drop)
Authord3vn0mi

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

Terminal window
# Full TCP port sweep through the jump host, piped over the lab's SSH tunnel
ssh 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

Terminal window
# Seed local DNS resolution for the vhosts the app references
sudo sed -i '/snippet.htb/d' /etc/hosts
echo "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 flow
curl -s -i http://snippet.htb/ -c /tmp/cookies.txt | head -30

Port 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 users table 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 cs signature value, which is meant to be a server-side-only HMAC of APP_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:

Terminal window
# Pull cookies from the live session for use as auth material
XSRF_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
Terminal window
# Sanity-check the response
python3 -c "
import json
data = 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

Terminal window
# Extract and dedupe all password hashes from the dump
sort -u /tmp/hashes.txt > /tmp/hashes_uniq.txt
# 64 hex chars -> SHA-256, hashcat mode 1400
hashcat -m 1400 -a 0 /tmp/hashes_uniq.txt /usr/share/wordlists/rockyou.txt --force
hashcat -m 1400 /tmp/hashes_uniq.txt --show

Result: 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

Terminal window
# Establish a fresh CSRF/session pair and authenticate as juliana
curl -s -i http://snippet.htb/ -c /tmp/cookies.txt -o /tmp/index.html
# ... POST juliana@snippet.htb / password123 to /login using the XSRF-TOKEN cookie

Once 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 → Root

Tools Used

ToolPurpose
nmapPort scanning through the jump host
curlCookie/session handling, exercising /management/dump and the login/validate endpoints
hashcat (+rockyou.txt)Cracking the shared SHA-256 password hash
python3Parsing the users dump JSON; SHA-256 length-extension forgery
sshJump-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

  1. 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. Use hash_hmac()/proper HMAC constructions instead.
  2. 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.
  3. Predictable, unthrottled password-reset tokens (deterministic prefix + small random suffix) are brute-forceable in practice; reset tokens need full cryptographic randomness and rate limiting.
  4. Password reuse across the dashboard, Gitea, and other internal services collapses otherwise-independent trust boundaries into a single point of failure.
  5. Exposing a container’s Docker socket is equivalent to granting root on the host — never bind-mount docker.sock into 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.