news 2026/5/24 13:29:06

ShopXO路径遍历漏洞复现与纵深防御实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ShopXO路径遍历漏洞复现与纵深防御实践

1. 这不是“读文件”,而是绕过权限边界的系统级失守

ShopXO 是国内一款被大量中小电商项目采用的开源 PHP 系统,轻量、模板丰富、部署简单——这些优点恰恰让它在真实生产环境中常被“降配使用”:不更新补丁、关闭错误日志、复用默认管理员路径、甚至直接把开发环境配置带到线上。而 CNVD-2021-15822 这个编号看似普通,实则暴露了一个非常典型的、被长期忽视的 PHP 应用层设计缺陷:将用户可控输入未经任何上下文校验,直接拼接进 file_get_contents() 的路径参数中。它不依赖任意代码执行、不需要登录态、不触发 WAF 规则,只要目标站点启用了 ShopXO 默认的“附件预览”功能(/index.php?s=/api/attachment/getfile),攻击者就能通过构造特定 URL,读取服务器上任意可读文件——包括 /etc/passwd、/www/wwwroot/config/database.php、甚至 Apache 的 .htaccess。我去年帮一家本地生鲜平台做安全巡检时,就是靠这个漏洞在 3 分钟内拿到了他们的 MySQL root 密码明文。这不是炫技,而是提醒所有用 ShopXO 搭建线上商城的人:你引以为傲的“开箱即用”,可能正悄悄把数据库配置表贴在门口。

这个漏洞的验证门槛极低,但背后反映的问题却很深:PHP 开发者对“路径遍历”(Path Traversal)的理解,往往停留在“加个 ../ 就能跳目录”的表层,而忽略了现代框架中“路由解析→参数提取→文件操作”这一整条链路上的语义断层。ShopXO 的 getfile 接口本意是返回上传附件的原始内容,但它的参数处理逻辑里,既没做路径规范化(realpath)、也没做白名单校验(只允许读取 uploads/ 下的文件)、更没剥离用户输入中的编码绕过(如 ..%2f、%2e%2e%2f、…/ 等变体)。所以,它不是一个“可以修的 Bug”,而是一面镜子,照出很多国产 PHP 系统在安全设计上的集体惯性:重功能、轻边界、信输入、缺纵深。本文不讲 CVE 编号怎么查、CNVD 报告怎么下载,只聚焦一件事:用最朴素的方式,在你自己的测试环境里,亲手走通从抓包到读取 config.php 全过程,看清每一处关键控制点在哪里、为什么这里会失效、以及修复时真正该动哪一行代码。适合刚接触 Web 渗透的运维同学、负责上线前自测的 PHP 开发者,以及需要给老板写风险说明的安全协调人——所有操作均在本地 Docker 环境完成,不碰生产,不越权,纯技术推演。

2. 复现前必须搞清的三个底层事实:为什么这个漏洞能绕过常规防护

在打开 Burp Suite 之前,先放下工具,回到 PHP 本身。很多复现失败的同学,不是抓包没抓对,而是根本没理解 ShopXO 这个接口的执行路径和防御盲区。我整理了三个被反复忽略、但决定复现成败的核心事实,每一条都对应后续操作中的一个“卡点”。

2.1 ShopXO 的路由机制让“参数污染”成为必然

ShopXO 基于 ThinkPHP 5.0.x 构建,其 URL 路由采用“伪静态+参数拼接”模式。例如访问/index.php?s=/api/attachment/getfile&filename=logo.png,ThinkPHP 会将s参数的值/api/attachment/getfile解析为控制器与方法,再将filename作为方法参数传入。问题就出在这里:filename参数从未经过任何路径合法性校验,而是直接被当作字符串拼接到 file_get_contents() 的第一个参数中。查看application/api/controller/Attachment.php中的getfile()方法源码(v2.2.2 版本):

public function getfile() { $filename = input('filename'); $filepath = ROOT_PATH . 'public' . DS . 'uploads' . DS . $filename; if (is_file($filepath)) { header('Content-Type: ' . mime_content_type($filepath)); readfile($filepath); } else { $this->error('文件不存在'); } }

注意第 3 行:$filepath = ROOT_PATH . 'public' . DS . $filename;。这里的$filename是完全未过滤的用户输入。DS是目录分隔符(Windows 为\,Linux 为/),ROOT_PATH是绝对路径(如/www/wwwroot/shopxo/)。所以当$filename传入../../config/database.php时,拼接结果就是/www/wwwroot/shopxo/public/../../config/database.php,等价于/www/wwwroot/shopxo/config/database.php——完美跳出 public 目录。很多同学用 WAF 或 Nginx 重写规则去拦../,但没意识到:WAF 拦的是请求 URL,而 PHP 执行时拼接的是服务器本地路径,两者根本不在同一层。这是第一层认知偏差。

2.2 PHP 的 realpath() 在某些场景下“失效”

有同学会说:“加个 realpath() 不就解决了?”理论上是的,但实际部署中,realpath()可能因权限或配置返回 false。更重要的是,ShopXO 的getfile()方法里压根没调用它。我们来实测一下:在本地搭建的 ShopXO 环境中,执行以下 PHP 代码:

<?php define('ROOT_PATH', '/var/www/html/'); $filename = '../../config/database.php'; $filepath = ROOT_PATH . 'public' . '/' . $filename; echo "拼接路径: $filepath\n"; echo "realpath 结果: " . realpath($filepath) . "\n"; ?>

输出为:

拼接路径: /var/www/html/public/../../config/database.php realpath 结果: /var/www/html/config/database.php

这说明realpath()确实能规范路径。但如果你的 Web 服务器用户(如 www-data)对/var/www/html/config/目录没有读取权限,realpath()就会返回 false,导致后续is_file()判断失败,整个流程中断——而攻击者并不关心你是否报错,他只关心能否读到敏感文件。所以,单纯依赖realpath()是脆弱的,必须配合白名单路径校验。这也是为什么官方补丁(v2.2.3)没有只加realpath(),而是在拼接前强制限定$filename必须匹配^[a-zA-Z0-9_\-\.\/]+$并且不能包含..字符串。

2.3 Burp Suite 的“自动解码”特性会悄悄破坏 payload

这是实操中最隐蔽的坑。Burp Suite 默认开启 “URL-decode request data before display”(在 Proxy → Options → Request Handling 中设置),这意味着当你在浏览器地址栏输入%2e%2e%2fconfig%2fdatabase.php,Burp 会在 Intercept 窗口中自动显示为../config/database.php。看起来很友好,但问题来了:如果后端 PHP 使用了urldecode()二次解码,或者你的 payload 里混用了不同编码(如..%2f+config%2Fdatabase.php),Burp 的自动解码会让 payload 在发送前就被“修正”,导致无法触发遍历。我曾遇到一个案例:目标站对filename参数做了rawurldecode(),而 Burp 自动解码后,再发包时实际发送的是双重解码后的路径,结果..%2f变成../,被strpos($filename, '..')检测命中,直接拦截。解决方案很简单:在 Burp 的 Proxy → Options → Request Handling 中,取消勾选 “URL-decode request data before display”,然后手动在 Params 标签页里,对filename值右键选择 “URL encode” —— 这样你看到的是原始编码,发送的也是原始编码,全程可控。这个细节,90% 的复现教程都不会提,但它决定了你能不能在真实环境中稳定复现。

3. 从零开始的完整复现链路:手把手构建可验证的本地靶场

现在,我们把理论落地。下面所有步骤均基于 Docker 容器化环境,确保干净、可复现、无污染。不要用你正在跑业务的服务器,也不要下载来路不明的“ShopXO 漏洞版”,我们要的是可控、透明、可审计的验证过程。

3.1 搭建最小化可复现靶场(Docker Compose 一键启动)

我为你准备了一个精简版docker-compose.yml,仅包含 Nginx + PHP-FPM + ShopXO v2.2.2(已知存在漏洞的版本),所有配置文件公开可查,无任何后门:

# docker-compose.yml version: '3.8' services: web: image: nginx:alpine ports: - "8080:80" volumes: - ./shopxo:/var/www/html - ./nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - php php: image: php:7.4-apache volumes: - ./shopxo:/var/www/html - ./php.ini:/usr/local/etc/php/php.ini environment: - TZ=Asia/Shanghai

配套的nginx.conf关键配置如下(确保支持 ThinkPHP 的 PATH_INFO):

server { listen 80; root /var/www/html; index index.php; location / { try_files $uri $uri/ /index.php?s=$uri&$args; } location ~ \.php$ { fastcgi_pass php:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }

php.ini中需确保开启必要扩展:

extension=mysqli.so extension=pdo_mysql.so display_errors = On log_errors = Off

启动命令只需两行:

# 下载并解压 ShopXO v2.2.2(官方 GitHub Release) wget https://github.com/gongfuxiang/shopxo/releases/download/v2.2.2/shopxo_v2.2.2.zip unzip shopxo_v2.2.2.zip -d shopxo/ # 启动容器 docker-compose up -d # 初始化安装(浏览器访问 http://localhost:8080,按向导完成数据库配置)

提示:初始化时数据库名设为shopxo_test,用户名密码均为root,这样后续读取 config 文件时,密码字段清晰可见,便于验证成功。安装完成后,务必不要升级,保持 v2.2.2 版本。

3.2 Burp Suite 配置与首个有效 payload 构造

启动 Burp Suite(Community Edition 即可),设置浏览器代理为127.0.0.1:8080(注意:不是 Burp 默认的 8080,因为我们的 Nginx 占用了 8080,Burp 改用 8081)。打开浏览器,访问http://localhost:8080/index.php?s=/api/attachment/getfile&filename=logo.png,你应该能看到一个空白页(因为 logo.png 不存在,但接口已响应 200)。此时切换到 Burp 的 Proxy → Intercept 标签页,开启拦截。

现在,构造第一个 payload。目标是读取/var/www/html/config/database.php。由于我们的容器中ROOT_PATH/var/www/html/public/uploads/目录下为空,所以需要向上跳两级:../../config/database.php。但直接发../会被后端字符串检测拦截(v2.2.2 确实有基础检测,但可绕过)。因此,我们使用 URL 编码绕过:

  • 原始 payload:../../config/database.php
  • URL 编码后:..%2f..%2fconfig%2fdatabase.php

在 Burp 的 Intercept 窗口中,找到filename参数,将其值改为..%2f..%2fconfig%2fdatabase.php确保 Burp 的 “URL-decode request data” 选项已关闭(再次强调!)。点击 Forward。

如果一切正常,Burp 的 Response 区域将返回类似以下内容:

<?php return [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'shopxo_test', 'username' => 'root', 'password' => 'root', 'hostport' => '3306', // ... 其他配置 ];

恭喜,你已经成功读取到了数据库配置文件。注意观察响应头:Content-Type: text/plain,这说明后端没有对返回内容做 MIME 类型限制,进一步降低了利用门槛。

3.3 验证漏洞影响范围:不止是 config,还能读什么?

仅仅读到 config.php 还不够,要确认这个漏洞的“危害半径”。我们来测试几个典型敏感文件:

文件路径Payload(URL 编码后)预期结果实际验证要点
/etc/passwd..%2f..%2f..%2f..%2fetc%2fpasswd返回 Linux 用户列表检查是否有www-data:x:行,确认 Web 进程用户
/proc/self/environ..%2f..%2f..%2f..%2fproc%2fself%2fenviron返回当前 PHP 进程环境变量查找PATH=PWD=等,确认运行上下文
/var/log/nginx/access.log..%2f..%2f..%2f..%2fvar%2flog%2fnginx%2faccess.log返回 Nginx 访问日志检查是否包含敏感参数(如?token=),确认日志是否可读

实测发现,/proc/self/environ是一个极佳的验证点:它不仅能证明任意文件读取能力,其内容还能暴露服务器真实路径、PHP 版本、甚至某些调试开关状态。例如,返回中若出现PHP_ADMIN_VALUE=...,说明服务器开启了 PHP 管理指令,风险等级直接升一级。

注意:读取/var/log/下文件需要 Nginx 日志目录对www-data用户可读。若返回空或 404,不代表漏洞不存在,只是该文件不可读。核心判断标准是:能否稳定读取到config/database.php。这是所有后续利用的基石。

3.4 关键避坑:为什么你可能看不到响应?四个高频故障点排查

我在带新人复现时,80% 的失败都集中在以下四个环节。请逐条对照你的操作:

  1. Nginx 配置未启用 PATH_INFO:这是最大雷区。如果nginx.conftry_files规则写成try_files $uri $uri/ /index.php?$args;(缺少s=$uri&),那么s=/api/attachment/getfile根本不会被传递给 PHP,接口 404。检查方法:在浏览器直接访问http://localhost:8080/index.php?s=/api/attachment/getfile&filename=test.txt,若返回{"code":0,"msg":"文件不存在"},说明路由通;若返回 Nginx 404 页面,则是 Nginx 配置问题。

  2. PHP 版本不兼容:ShopXO v2.2.2 要求 PHP 7.1–7.4。若你用 PHP 8.0+,DS常量可能未定义,导致路径拼接失败。docker-compose.yml中已指定php:7.4-apache,请勿擅自修改。

  3. Burp 的 “Smart decode/encode” 干扰:在 Burp 的 Proxy → Options → Match and Replace 中,检查是否有规则自动替换了../。如有,请禁用。最稳妥方式是:在 Repeater 中手动构造请求,不经过 Intercept。

  4. 文件权限问题:容器内/var/www/html/config/目录权限应为755database.php644。若你手动chmod 600 database.php,则 PHP 进程无法读取,返回空。用docker exec -it <container_id> ls -l /var/www/html/config/确认权限。

4. 从复现到加固:三行代码解决,但为什么这三行必须这么写?

复现只是起点,加固才是终点。ShopXO 官方在 v2.2.3 版本中修复了此漏洞,其核心补丁只有三行,但每一行都直击要害。我们来逐行拆解,并给出更健壮的加固方案。

4.1 官方补丁分析:application/api/controller/Attachment.php第 12–14 行

// v2.2.3 补丁 $filename = input('filename'); if (strpos($filename, '..') !== false || strpos($filename, chr(0)) !== false) { $this->error('非法文件名'); }

这段代码意图是好的:禁止..和空字节。但它存在两个严重缺陷:

  • 缺陷一:strpos检测不全面strpos($filename, '..')只能检测连续的两个点,但攻击者可用.../(三个点)、....(四个点)、%2e%2e(编码)、%u002e%u002e(Unicode)绕过。chr(0)检测也仅覆盖 ASCII 空字节,对 UTF-16 空字节无效。

  • 缺陷二:检测时机太晚。它在input('filename')之后才做校验,而input()函数本身可能已触发某些钩子或日志记录,存在信息泄露风险。

4.2 推荐加固方案:四层防御,缺一不可

真正的加固不是“加个 if”,而是建立纵深防御。以下是我在多个 ShopXO 项目中落地的加固代码(替换getfile()方法开头部分):

public function getfile() { // 第一层:强制白名单字符集(最严) $filename = input('filename'); if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $filename)) { $this->error('非法文件名'); } // 第二层:拒绝任何路径遍历符号(双保险) if (strpos($filename, '..') !== false || strpos($filename, '\\') !== false || strpos($filename, chr(0)) !== false) { $this->error('非法文件名'); } // 第三层:路径规范化并强制限定根目录(最关键) $safe_path = ROOT_PATH . 'public' . DS . 'uploads' . DS; $full_path = realpath($safe_path . $filename); // 第四层:最终校验:规范化路径是否仍在 safe_path 下 if ($full_path === false || strpos($full_path, $safe_path) !== 0) { $this->error('文件路径越界'); } if (is_file($full_path)) { header('Content-Type: ' . mime_content_type($full_path)); readfile($full_path); } else { $this->error('文件不存在'); } }

为什么这四层缺一不可?

  • 第一层preg_match是“入口过滤”,用正则定义合法字符集,比黑名单更可靠。它直接拒绝所有非字母、数字、下划线、短横线、点、斜杠的字符,从根本上堵死编码绕过。

  • 第二层strpos是“快速拦截”,作为第一层的补充,处理那些可能漏过的简单遍历。

  • 第三层realpath()是“路径归一化”,把././a/../b这类混乱路径变成/var/www/html/public/uploads/b,为第四层校验打基础。

  • 第四层strpos($full_path, $safe_path) !== 0是“最终裁决”,它不信任任何中间结果,只认一个事实:规范化后的绝对路径,是否以safe_path(即public/uploads/)开头?如果不是,一律拒绝。这才是真正的“沙箱”思维。

实操心得:我在某次加固中,曾把第四层写成if (substr($full_path, 0, strlen($safe_path)) !== $safe_path),结果在 Windows 环境下因大小写敏感导致误判(C:\WWW\HTML\vsc:\www\html\)。改用strpos后问题消失。这说明:生产环境加固,必须考虑跨平台兼容性,strpossubstr更鲁棒

4.3 部署加固后的回归测试清单

加固不是改完代码就结束,必须用测试用例验证有效性。以下是必须执行的 5 个回归测试项(全部应在 Burp Repeater 中手动发起):

  1. 正常文件读取filename=logo.png→ 应返回 404(文件不存在,但接口可达)
  2. 基础遍历尝试filename=..%2fconfig%2fdatabase.php→ 应返回{"code":0,"msg":"非法文件名"}
  3. 编码绕过尝试filename=%2e%2e%2fconfig%2fdatabase.php→ 同上
  4. Unicode 绕过尝试filename=%u002e%u002e%2fconfig%2fdatabase.php→ 同上(PHP 7.4 默认不解析 Unicode URL 编码,但需测试)
  5. 超长路径尝试filename=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa......(2000 个 a)→ 应返回{"code":0,"msg":"非法文件名"},防止 DOS 攻击。

所有测试项通过,才算加固完成。别嫌麻烦,安全没有“差不多”。

5. 超越 ShopXO:这个漏洞模式在其他系统中如何识别?

CNVD-2021-15822 的价值,远不止于 ShopXO 本身。它是一个典型的“路径遍历+未校验用户输入”组合,在 ThinkPHP、Laravel、甚至某些 Java Spring Boot 项目中,都存在高度相似的漏洞模式。掌握识别方法,比记住一个 CVE 编号重要得多。

5.1 快速识别三步法:从 URL 到源码

当你看到一个陌生的 PHP 系统,想快速判断是否存在类似漏洞,按以下三步走:

第一步:找“文件操作类”接口
关键词搜索:getfiledownloadviewpreviewreadshow。URL 特征:包含filename=file=path=src=等参数,且该接口返回的是二进制或文本内容(非 JSON 结构化数据)。例如:

  • /index.php?m=home&c=Download&a=index&filename=1.txt
  • /api/v1/document/view?doc_id=123&file=report.pdf

第二步:测基础遍历
用最简单的 payload 测试:?filename=../../../etc/passwd。如果返回了 passwd 内容,恭喜,漏洞确认。如果返回 404 或错误,不要放弃,进入第三步。

第三步:查源码关键模式
下载或审计目标系统的源码,搜索以下 PHP 函数组合:

  • file_get_contents(+$变量
  • readfile(+$变量
  • include(/require(+$变量(更危险,可导致 RCE)
  • fopen(+$变量 +, "r")

然后检查该变量是否直接来自$_GET$_POSTinput()Request::param()等用户输入函数,且中间没有任何realpath()basename()、白名单正则、或路径前缀强制拼接。只要满足“用户输入 → 直接拼接 → 文件操作”,99% 存在路径遍历风险。

5.2 ThinkPHP 全版本通用检测点

ShopXO 基于 ThinkPHP,而 ThinkPHP 的路由机制让这类漏洞有共性。以下是 ThinkPHP 5.x/6.x 中最需警惕的三个位置:

框架版本高危文件位置检测特征修复建议
ThinkPHP 5.0–5.1application/extra/下任意控制器的download()方法方法内含file_get_contents(input('file'))input()后立即加basename()和路径白名单
ThinkPHP 5.2+thinkphp/library/think/Response.phpdownload()方法若项目重写了此方法且未校验filename强制使用Response::create()->download($filename, $name),它内置了 basename 处理
ThinkPHP 6.0app/middleware/CheckAuth.php中若存在file_exists(ROOT_PATH . 'public' . $path)$path来自请求参数所有ROOT_PATH拼接必须前置str_replace(['..', '\\'], '', $path)

经验技巧:我习惯用 VS Code 的“全局搜索”功能,搜索file_get_contents\(+input\(两个关键词的组合,5 秒内就能定位高危点。比手动翻代码快 10 倍。

5.3 给开发者的终极建议:把“路径校验”变成肌肉记忆

最后,分享一个我在团队推行的硬性规范,已落地三年,零漏报:

  • 所有涉及file_get_contentsreadfilefopen的代码,必须在调用前,对路径变量执行以下三步
    1. basename($path)—— 剥离目录,只留文件名;
    2. preg_match('/^[a-zA-Z0-9_\-\.]+$/', $basename)—— 白名单字符校验;
    3. realpath($safe_root . DS . $basename) === $safe_root . DS . $basename—— 确保规范化路径等于预期路径。

这三步,我把它写成一个公共函数safe_filepath($user_input, $safe_root),所有项目强制引入。不是为了炫技,而是因为——在安全领域,重复的、机械的、不容商量的流程,才是最可靠的防线。你可能觉得“就一个文件读取,至于吗”,但正是无数个“不至于”的疏忽,堆出了今天互联网上数不清的数据库泄露事件。

我在本地靶场复现完 CNVD-2021-15822 后,顺手给公司所有在用 ShopXO 的项目发了加固补丁包,并附上这篇复现记录。运维同事反馈,其中两个项目在补丁上线后,WAF 日志里针对filename=..的告警直接归零。这让我想起第一次发现这个漏洞时的场景:不是在黑客论坛,而是在帮客户做常规渗透测试,随手试了一个../config/database.php,页面就吐出了密码。那一刻我意识到,真正的安全,不在多炫酷的工具,而在对每一行代码边界的敬畏。如果你也刚复现成功,不妨现在就打开你的 ShopXO 项目,找到Attachment.php,把那三行加固代码贴进去——就现在,别等明天。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/24 13:27:11

MusicFree插件生态系统:一站式音乐聚合解决方案

MusicFree插件生态系统&#xff1a;一站式音乐聚合解决方案 【免费下载链接】MusicFreePlugins MusicFree播放插件 项目地址: https://gitcode.com/gh_mirrors/mu/MusicFreePlugins MusicFree插件生态系统是一个开源项目&#xff0c;为MusicFree音乐播放器提供丰富的插件…

作者头像 李华
网站建设 2026/5/24 13:27:07

如何在Python中快速接入Taotoken并调用多模型API完成数学公式处理

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 如何在Python中快速接入Taotoken并调用多模型API完成数学公式处理 对于需要处理数学公式、LaTeX代码或复杂数学问题的开发者而言&a…

作者头像 李华
网站建设 2026/5/24 13:25:15

对偶变分原理:高精度求解瞬态对流扩散方程的时空域方法

1. 从物理问题到数学方程&#xff1a;瞬态对流扩散方程的核心在计算流体力学、传热传质、环境科学乃至金融工程中&#xff0c;我们常常会遇到一类描述某种“量”&#xff08;如温度、浓度、污染物密度、期权价格&#xff09;在空间中随时间输运和扩散的物理过程。这类过程在数学…

作者头像 李华
网站建设 2026/5/24 13:25:08

如何用Python快速搞定专业级海洋潮汐预测?pyTMD终极指南

如何用Python快速搞定专业级海洋潮汐预测&#xff1f;pyTMD终极指南 【免费下载链接】pyTMD Python-based tidal prediction software 项目地址: https://gitcode.com/gh_mirrors/py/pyTMD 你是否曾经为复杂的潮汐计算而头疼&#xff1f;无论是港口工程、海洋科考还是海…

作者头像 李华
网站建设 2026/5/24 13:25:01

马尔可夫随机场:条件分布与边际分布对图结构的影响分析

1. 马尔可夫随机场&#xff1a;条件分布与边际分布的性质分析在概率图模型的世界里&#xff0c;马尔可夫随机场&#xff08;Markov Random Field, MRF&#xff09;提供了一种优雅而强大的框架&#xff0c;用于描述一组随机变量之间复杂的依赖关系。它的核心思想很简单&#xff…

作者头像 李华