Durant le confinement, j’étais particulièrement actif sur HackTheBox. Ayant fait toutes les boxs que je jugeait “à ma portée” à un moment je m’étais lancé dans la quête (qui me paraissait impossible) d’une box notée Insane !
Ainsi j’ai réussi Fatty non sans difficulté. J’ai trouvé l’expérience assez intéréssante pour en faire un Write-Up que vous pouvez lire juste en dessous !
Trouver un point d’entrée
|
|
On peut voir :
- 1 service FTP (21)
- 1 service SSH (22)
- 3 services inconnus (1337,1338,1339)
Le FTP
Dans un premier temps, l’option la plus logique est de tester une connexion anonyme au FTP.
On a en effet un accès et quelques fichiers potentiellement intéréssants
- 3 fichiers textes
- 1 fichier executable jar
On s’empresse de récupérer tous ces fichiers puis on lit les notes.
Note 1
Dear members,
because of some security issues we moved the port of our fatty java server from 8000 to the hidden and undocumented port 1337. Furthermore, we created two new instances of the server on port 1338 and 1339. They offer exactly the same server and it would be nice if you use different servers from day to day to balance the server load.
We were too lazy to fix the default port in the ‘.jar’ file, but since you are all senior java developers you should be capable of doing it yourself ;)
Best regards, qtc
Note 2
Dear members,
we are currently experimenting with new java layouts. The new client uses a static layout. If your are using a tiling window manager or only have a limited screen size, try to resize the client window until you see the login from.
Furthermore, for compatibility reasons we still rely on Java 8. Since our company workstations ship Java 11 per default, you may need to install it manually.
Best regards, qtc
Note 3
Dear members,
We had to remove all other user accounts because of some seucrity issues. Until we have fixed these issues, you can use my account:
User: qtc Pass: clarabibi
Best regards, qtc
Bien… La suite se passe donc sur le client jar.
Le client jar
La première chose à faire c’est d’essayer d’executer le client normalement. Pour cela, rien de plus simple !
|
|
On arrive sur une page de login, on test les identifiants donnés dans la 3ème note ( qtc / clarabibi ) et… Ça ne fonctionne pas. On obient un Connection Error !
Réussir à se connecter
Si on se réfère à la première note : le port du serveur à changé, il est passé de 8000 à 1337 (et 1338, 1339 sans doute pour éviter les problèmes s’il y a trop de joueurs à la fois). Il faut donc trouver un moyen d’arranger tout ça.
On a besoin d’un peu plus d’informations. On va donc regarder dans le jar.
Basiquement, les jar ne sont que des archives de projet java.
|
|
Donc on va juste decompresser l’archive et regarder son contenu.
|
|
On y trouve un fichier beans.xml, celui-ci sera utilisé par Spring pour la construction d’objets. Ce fichier contient dans notre cas les informations de connexion au serveur :
|
|
Je vais donc tout simplement les modifier pour qu’ils collent au serveur auquel je veux me connecter.
|
|
Ensuite, je n’ai plus qu’à recréer l’archive et à l’executer
|
|
On retente la connexion avec les identifiants trouvés plus tôt et… On obtient une erreur.
Exception in thread “AWT-EventQueue-0” org.springframework.beans.factory.BeanDefinitionStoreException: Unexpected exception parsing XML document from class path resource [beans.xml]; nested exception is java.lang.SecurityException: SHA-256 digest error for beans.xml
En réalité, ce n’est pas très grave. Le jar vérifie juste son intégrité via les signatures de fichiers et puisqu’on a modifié beans.xml, sa signature a changé. On va donc supprimer les fichiers permettant cette vérification.
|
|
On relance le jar, et cette fois la connexion fonctionne \o/
Obtenir le code du serveur
Une fois connecté au serveur via le client, on a plusieurs fonctionnalités qui s’offrent à nous :
- Whoami - Pour avoir notre login et notre role (dans notre cas qtc et user)
- Connection Test - Pour tester la connexion avec le serveur
- File Browser - Pour sélectionner un dossier et pouvoir ensuite lire les fichiers qu’il contient
Ainsi que des fonctionnalités “grisées”, sans doute accessible uniquement aux admins :
- ChangePassword
- Uname
- Users
- Netstat
- IpConfig
Pour commencer, on essaie de lire un fichier du serveur. On sait qu’il s’agit d’un Linux, on va donc tenter un Path Traversal via le File Browser pour lire le fichier /etc/passwd.
Visiblement, ça ne fonctionne pas, des caractères sont supprimés… Même en essayant de “bypass” ce nettoyage, impossible d’effectuer un Path Traversal.
Il faudrait avoir accès au code du client afin de comprendre comment il fonctionne. Et un outil existe pour décompiler du java : jd-gui !
On va tout simplement utiliser jd-gui pour récupérer l’intégralité des fichiers sources du projet. Pour cela on ouvre le projet (le modifié afin de toujours avoir le beans.xml voulu).
|
|
Puis on sauvegarde les sources (File > Save All Sources).
On obtient alors un magnifique zip que l’on va décompresser.
|
|
Le dossier org contient apparemment des librairies importées. On va donc se concentrer sur le dossier htb/fatty qui a l’air de contenir du code propre à notre client. On va donc dans ce dossier et on recherche les fonctions qui nous intéressent.
Dans notre cas on veut voir comment le File Browser fonctionne, on va donc faire une recherche par rapport à certains mots clefs. Par exemple le mot clef configs qui correspond à l’un des dossiers que l’on peut explorer.
|
|
On fouille un peu le code java et on retrouve très vite la partie qui nous intéresse : Comment est configuré le dossier avec le File Browser.
Sachant que toutes les options de File Browser nous emmènent dans /opt/fatty/files/<OPTION CHOISIE> on va essayer de modifier les strings configs de cette fonction pas .. voir si on peut accéder à /opt/fatty.
On retourne dans la racine des sources (ici client_sources) et on compile tout ça.
|
|
Ici on a une erreur avec la compilation de spring ! Il suffit de récupérer le contenu déjà compilé que l’on a décompressé tout à l’heure.
|
|
On relance le client, se connecte et va dans File Browser > Configs et là… Le contenu affiché diffère de la dernière fois.
Visiblement logs, tar et files sont des dossiers. On va alors lire start.sh.
Apparemment, nous sommes dans un docker. On va malgré tout continuer et essayer de récupérer fatty-server.jar.
On se frotte à un problème : Le client ne nous permet que d’afficher les fichiers… Il faut donc trouver un moyen de télécharger ce fichier. En réfléchissant, j’ai décider d’utiliser une méthode “sale” mais efficace : écrire le retour du Open directement dans un fichier.
Il faut donc modifier le code (encore).
Donc là encore il faut trouver le moment de reception du contenu des fichiers lus. On va se baser sur le Open dans le GUI. On trouve le nom openFileButton auquel est accroché une action et pour obtenir la réponse cette action fait this.invoker.open().
Malheureusement, cela nous sort un String, je veux être sur de ne pas avoir de problème, je veux le byte-string correspondant. On va remonter cette fonction open.
Pour ça, on voit que invoker vient d’un autre fichier java : htb/fatty/client/methods/Invoker.java et on retrouve le code de la fonction open.
On voit bien le moment où il récupère le contenu de la réponse. Ici il fait un getContentAsString(). On va plutôt utiliser la méthode getContent() qui va récupérer un objet de type byte[] et le rediriger vers un fichier.
On a donc rajouté les lignes suivantes
|
|
En oubliant pas d’importer les bibliothèques java nécessaires
|
|
Donc à chaque fois que l’on va utiliser open, le contenu de la réponse sera écrit dans ce fichier.
On recompile, recréé le jar et on va utiliser la fonction open sur fatty-server.jar. Puis on regarde si le fichier a bien été créé.
|
|
Super ! On a plus qu’à le récupérer et surtout… Récupérer ses sources, là encore avec jd-gui.
Maintenant la question est… Qu’est-ce qu’on en fait ?
On a vu qu’il y avait des options “grisée”, on en a déduit qu’elles n’étaient pas accessible au role user. On va donc essayer d’obtenir un role admin. On part donc en analyse statique du code du serveur.
Obtenir plus de droits
En regardant rapidement le code du serveur, on voit qu’il y a un code pour gérer la base de données (htb/fatty/server/databaseF/attyDbSession.java) et dans ce code se trouve une requête SQL.
Et surtout, fait intéressant, la requête est vulnérable aux injections ! Et elle récupère 5 données que l’on peut facilement comprendre :
- id : l’ID de l’utilisateur (sans doute une clef primaire de la table)
- username : le nom de l’utilisateur (jusque là on utilisait qtc)
- email : le mail de l’utilisateur
- password : le mot de passe de l’utilisateur (jusque là clarabibi)
- role : le rôle de l’utilisateur (jusque là user)
Il faut donc forger une requête avec le même username, le même hash de mot de passe mais un rôle admin.
On peut voir que pour la vérification du mot de passe, celle-ci se fait en vérifiant le mot de passe de l’utilisateur requêté et celui qui a été envoyé pas le client. On va donc faire une modification dans le client pour envoyer un “hash” connu.
Et pour ça, ça se passe dans htb/fatty/shared/resources/User.java avec la fonction setPassword()
On va tout simplement modifier la dernière ligne de ce code par :
|
|
On compile, recréé le jar et pour se connecter on met en guise de username :
pouet’ UNION SELECT 1, ‘qtc’, ‘qtc@mail.com’, ‘ABCD’, ‘admin
Et nous avons une connexion en tant que qtc avec un rôle admin !
Obtenir un accès sur le serveur
On a donc maintenant accès à des commandes “admin”. Il y a sans doute faire quelque chose à faire avec.
Dans la liste on a :
- Uname - Effectue un uname sur le serveur
- Users - Liste apparemment le contenu de /home
- Netstat - Effectue un netstat
- IpConfig - Effectue un ipconfig
- ChangePassword - Permet de changer le mot de passe, mais n’est pas implémenté encore
Après vérification dans le code serveur, on peut vori qu’on a tout bon pour les 4 premiers et il n’y a aucune injection de commande possible. L’idée est donc de se concentrer sur le ChangePassword qui pour le coup est un peu plus particulier.
On regarde donc le code au niveau du serveur.
Et on voit une désérialisation vers un object User. On cherche un peu d’information sur les désérialisation en java et on tombe vite sur le github Java Deserialization Cheat Sheet. Et il parle de CommonsCollections, une bibliothèque Java qui est justement utilisée pas notre client et notre serveur.
On regarde un peu plus en détail notre code et on voit que cet objet sérialisé vient d’un Base64 qui a été envoyé par le client. On va donc voir au niveau du code client, dans le fichier htb/fatty/client/methods/Invoker.java (Fonction changePW() là aussi).
Dans un premier temps, on va faire en sorte qu’il soit “implémenté” (C’est à dire que cette fonction soit appelée). Pour ça, ça se passe dans le code du GUI, ligne 560.
On voit que lorsque l’on appuie sur le bouton, le message “Not implemented yet.” est affiché dans tous les cas. On va supprimer l’affichage de ce message et copier le fonctionnement des autre boutons pour appeler changePW(). On va donc mettre ce code (avec des valeurs “poubelles” dans l’appel de la fonction puisqu’on ne s’en servira pas).
|
|
Et maintenant, on modifie le code de changePW(). Dans un premier temps il nous faut une payload.
Pour générer la payload, on va utiliser ysoserial, d’après le fichier META-INF/maven/fatty-server/fatty-server/pom.xml le serveur utilise la version 3.1 de commons-collections. Il y a donc 5 payloads possibles via ysoserial, on les essayer une par une.
|
|
Ici, X est à remplacer par le numéro de payload. On obtient un base64 que l’on va utiliser dans notre code. Pour cela il n’y a qu’à modifier la ligne
|
|
par
|
|
Comme d’habitude on passe par les étapes compilations & création du jar puis on ouvre un listener sur notre machine.
|
|
On lance le client, se connecte en admin, va dans Change Password puis sans même remplir le formulaire on clique sur Change.
Et là, avec la payload CommonsCollections5 on obtient un reverse shell sur la machine.
Le fichier user.txt n’a pas les droits de lecture (sans doute pour obliger les joueurs à avoir un shell pour le lire), on lui donne et le lit.
|
|
Devenir root
Obtenir un meilleur shell
Avant de devenir root, une première chose à faire est d’avoir un shell digne de ce nom. Problème : Comme on l’a vu on est dans un docker, il n’y a donc quasiment aucun binaire.
Celui dont nous avons le plus besoin est python. Pour ça, un dépot git existe : static-binaries !
On va donc le récupérer via un serveur HTTP fait en python sur ma machine.
|
|
Puis on effectue notre suite de commande habituelle pour avoir un beau shell intéractif
|
|
Trouver le point d’entrée
Maintenant, il faut trouver comment accéder au root de la machine sachant que nous sommes dans un docker.
J’utilise donc pspy pour voir ce qu’il se passe (ici encore récupéré via wget). Et on voit chaque minute une commande est executée par notre utilisateur.
|
|
L’option -f de scp est non-documentée. En réalité c’est un serveur distant qui demande à notre docker de lui envoyer le fichier /opt/fatty/tar/logs.tar. A partir de là on peut établir une hypothèse sur ce qui est executé sur le serveur que l’on veut atteindre.
|
|
On peut ensuite supposer que ces commandes seront executées en tant que root.
Si ces hypothèses savèrent vraies, alors on peut facilement obtenir le shell root.
Obtenir un shell root
Pour obtenir un shell root en exploitant scp et tar, il faut utiliser un lien symbolique.
En effet, une archive peut contenir un lien symbolique. En décompressant celle-ci le lien symbolique est gardé intacte. Par exemple :
-
Je créé un lien symbolique
1
ln -s /tmp/fichier_test symlink
-
Je le met dans une archive
1
tar -cvf archive.tar symlink
-
J’envoie ce tar sur un autre machine
-
Je décompresse le tar
1
tar -xvf archive.tar
-
J’écris dans le fichier symlink
1
echo "Test test" > symlink
-
Le fichier /tmp/fichier_test existe et contient notre test
1 2
cat /etc/fichier_test Test test
Dans notre cas, nous que la décompression sur le serveur cible transforme logs.tar en lien symbolique vers /root/.ssh/authorized_keys afin de pouvoir, lors d’un second scp, écrire sur celui-ci.
Donc on fait comme suit :
|
|
On attend ensuite un scp (une minute, on peut voir son déclenchement avec pspy). Puis on remplace notre lien symbolique par un fichier contenant notre clef publique SSH
|
|
Là encore on attend le scp puis sur notre machine on fait
|
|
Et nous avons un accès SSH direct en root. Il ne nous reste plus qu’à lire le root.txt.
|
|
Conclusion
J’ai trouvé cette box très intéréssante, c’était ma première de niveau “Insane” et je pense que celui-ci n’est pas volé. J’ai appris quelques trucs en la faisant (notamment sur CommonsCollections) et ça m’a permis de me remettre légèrement au Java.
Par contre, si ça n’avait pas été dans un contexte CTF/HackTheBox je pense que je n’aurais jamais trouvé la vulnérabilité de désérialisation. Dans ce genre de contexte quasiment tout a un sens et il existait forcément un chemin vers un accès serveur. En cas réel j’aurais probablement mis ça de côté.
Même si les clients lourds se font de plus en plus rare dans l’informatique, j’ai trouvé ça réaliste à ce niveau avec peut-être un bémol sur la privesc qui restait sympathique.