Holiday
Executive summary — Holiday is a seven-link chain against a bespoke Node.js/Express booking application ("Booking Management") sitting behind a User-Agent-filtering WAF, with blacklist-based input validation at every following step. The login form hides a blind SQL injection against SQLite whose own oracle is poisoned by the WAF (it blocks the word
select), forcing the boolean queries to be built without subqueries, leaning onrowidinstead. Once inside, a stored XSS in the booking notes system — approved by an admin bot running PhantomJS, a browser unmaintained since 2018 that ignores theContent-Security-Policyheader — becomes the vector for firing a command injection against the admin panel's export endpoint, already under the admin's authenticated session. The rest is classic lateral movement: a self-planted SSH key installed through the RCE grants access asalgernon, and a misconfiguredsudorule overnpm installhands over a trivial root escalation through npm's lifecycle scripts.
| Platform | Hack The Box |
| Operating system | Linux (Ubuntu 16.04) |
| Difficulty | Hard |
| Status | Retired |
| Target IP | 10.129.75.47 |
| Services | 22 (OpenSSH 7.2p2), 8000 (Node.js Express — "Booking Management") |
Attack map
[8000] curl -> 404 on EVERYTHING ── WAF filters by User-Agent
│ real browser UA (Firefox) ──▶ "Booking Management" app appears (login)
▼
[SQLi] Injectable SQLite login ── the WAF blocks the word "select"
│ boolean oracle built on rowid, no subqueries ──▶ RickA's MD5 hash
▼
[crack] hashcat -m0 + rockyou ──▶ RickA:nevergonnagiveyouup
│ login ──▶ /agent (bookings) ──▶ /agent/addNote form
▼
[XSS] Note with String.fromCharCode (evades the quote filter)
│ the PhantomJS admin bot approves it (~1 min) and ignores CSP
▼
[RCE] XSS (admin session) ──▶ command injection in /admin/export?table=
│ lowercase-only filter; attacker IP in decimal (no dots)
│ installs my public SSH key
▼
[algernon] (user.txt)
│ sudo -l ──▶ NOPASSWD: /usr/bin/npm i *
▼
[ROOT] package.json with a "preinstall" script ──▶ chmod +s /bin/bash (root.txt)
1. Reconnaissance and the User-Agent WAF
nmap -sCV -p22,8000 10.129.75.47
# 22 OpenSSH 7.2p2 Ubuntu
# 8000 Node.js Express framework (http-title: Error)
The "Error" title on port 8000 is the first hint: every normal curl request returns a generic 404.
curl -s http://10.129.75.47:8000/
404 Cannot GET …
And not just the root path: an exhaustive fuzz of over 62,000 routes and method combinations turned up nothing but that same 404.
When a massive fuzz fails to tell any route apart from the rest, the most likely explanation isn't an empty site — it's that something is intercepting the request before it reaches the application and always answering the same way: a web application firewall, a proxy, or middleware filtering on some property of the request itself. The next logical move is to vary that property, not the paths.
The obvious suspect is the User-Agent: curl advertises itself as curl/7.x, a trivially detectable and blockable value. Repeating the same request with a real browser UA:
UA="Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0"
curl -s -A "$UA" http://10.129.75.47:8000/
# → "Booking Management" (login)
Why it works: the application's middleware checks whether the
User-Agent"looks like" a browser (likely a simple substring check such ascontains("Mozilla")) and returns the generic 404 to any client that fails it —curl,nmap, and most scanners/fuzzers by default. This is a security-by-obscurity control (CWE-807): it authenticates nothing, it just hides the attack surface from anything that doesn't impersonate a browser. With the right UA, the real app shows up: a login form for "Booking Management", with/login,/agentand/adminroutes.
Resumen ejecutivo — Holiday es una cadena de siete eslabones sobre una aplicación Node.js/Express de reservas ("Booking Management") construida a medida, protegida por un WAF que filtra por User-Agent y por una validación de entrada basada en blacklists en cada paso siguiente. El login esconde una inyección SQL ciega sobre SQLite cuyo propio oráculo está envenenado por el WAF (bloquea la palabra
select), lo que obliga a construir las consultas booleanas sin subqueries, apoyándose enrowid. Una vez dentro, un XSS almacenado en el sistema de notas de reservas —aprobado por un bot administrador que corre PhantomJS, un navegador sin soporte desde 2018 que ignora la cabeceraContent-Security-Policy— se convierte en el vector para disparar una command injection en el endpoint de exportación del panel de administración, ya con la sesión autenticada del admin. El resto es movimiento lateral clásico: una clave SSH propia instalada por RCE da acceso comoalgernon, y unsudomal configurado sobrenpm installentrega una escalada a root trivial a través de los lifecycle scripts de npm.
| Plataforma | Hack The Box |
| Sistema operativo | Linux (Ubuntu 16.04) |
| Dificultad | Hard |
| Estado | Retired |
| IP objetivo | 10.129.75.47 |
| Servicios | 22 (OpenSSH 7.2p2), 8000 (Node.js Express — "Booking Management") |
Mapa del ataque
[8000] curl -> 404 en TODO ── WAF filtra por User-Agent
│ UA de navegador real (Firefox) ──▶ aparece "Booking Management" (login)
▼
[SQLi] Login SQLite inyectable ── el WAF bloquea la palabra "select"
│ oráculo booleano con rowid, sin subqueries ──▶ hash MD5 de RickA
▼
[crack] hashcat -m0 + rockyou ──▶ RickA:nevergonnagiveyouup
│ login ──▶ /agent (reservas) ──▶ formulario /agent/addNote
▼
[XSS] Nota con String.fromCharCode (evade filtro de comillas)
│ bot admin PhantomJS la aprueba (~1 min) e ignora la CSP
▼
[RCE] XSS (sesión admin) ──▶ command injection en /admin/export?table=
│ filtro solo-minúsculas; IP del atacante en decimal (sin puntos)
│ instala mi clave SSH pública
▼
[algernon] (user.txt)
│ sudo -l ──▶ NOPASSWD: /usr/bin/npm i *
▼
[ROOT] package.json con script "preinstall" ──▶ chmod +s /bin/bash (root.txt)
1. Reconocimiento y el WAF por User-Agent
nmap -sCV -p22,8000 10.129.75.47
# 22 OpenSSH 7.2p2 Ubuntu
# 8000 Node.js Express framework (http-title: Error)
El título "Error" en el 8000 ya es la primera pista: toda petición normal con curl devuelve un 404 genérico.
curl -s http://10.129.75.47:8000/
404 Cannot GET …
Y no solo la raíz: un fuzzing exhaustivo de más de 62.000 rutas y combinaciones de método no encontró nada distinto de ese mismo 404.
Cuando un fuzzing masivo no distingue ninguna ruta del resto, lo más probable no es que el sitio esté vacío, sino que algo está interceptando la petición antes de que llegue a la aplicación y respondiendo siempre igual — un firewall de aplicación web (WAF), un proxy, o un middleware que filtra por alguna característica de la petición en sí. El siguiente paso lógico es variar esa característica, no las rutas.
La variable sospechosa es el User-Agent: curl lo anuncia como curl/7.x, un valor trivial de detectar y bloquear. Repitiendo la misma petición con un UA de navegador real:
UA="Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0"
curl -s -A "$UA" http://10.129.75.47:8000/
# → "Booking Management" (login)
Por qué funciona: el middleware de la aplicación comprueba si el
User-Agent"parece" un navegador (probablemente una comprobación simple de substring, tipocontains("Mozilla")) y devuelve el 404 genérico a cualquier cliente que no lo cumpla —curl,nmap, la mayoría de escáneres y fuzzers por defecto. Es un control de seguridad por oscuridad (CWE-807): no autentica nada, solo oculta la superficie de ataque a quien no imite un navegador. Con el UA correcto aparece la aplicación real: un formulario de login para "Booking Management", con rutas/login,/agenty/admin.
Root Access unlocks the content you can't see — everything else on the site is already open.
See Root Access plansNo account? Create one free then upgrade from your console.
Root Access desbloquea el contenido que no puedes ver — todo lo demás de la web ya está abierto.
Ver planes de Root Access¿Sin cuenta? Crea una gratis y luego mejora desde tu consola.