ShopXO任意文件读取漏洞CNVD-2021-15822深度解析
1. 这不是“读文件”而是绕过权限边界的系统级失守ShopXO 是国内中小电商项目中出镜率极高的开源系统轻量、模板丰富、部署快很多本地生活类小程序后台、县域特产商城、校园二手平台都用它打底。但就在2021年CNVD公布的编号 CNVD-2021-15822 漏洞里一个看似普通的“文件读取”功能实则暴露了整个系统权限模型的结构性缺陷——它根本不是简单的路径遍历Path Traversal而是一次对框架路由解析机制与文件操作层之间信任链的彻底击穿。我第一次复现这个漏洞时本意只是验证下某客户站点是否受影响结果在 Burp Suite 的 Repeater 里输入?s/index/index/readfilefilename../config/database.php后直接返回了明文数据库账号密码。那一刻我立刻停手没继续试../../etc/passwd因为我知道这不是“能读配置”而是“只要路径合法就能读任意可访问文件”。这个漏洞的危险性不在于技术多炫酷而在于它把开发者默认的信任逻辑——“控制器方法只该处理业务参数不该碰文件系统”——完全架空了。它适合三类人重点掌握一是正在维护 ShopXO 站点的运维或开发必须立刻确认补丁状态二是刚入门 Web 渗透的新手这是理解“参数污染如何穿透多层校验”的绝佳沙盒三是安全方案设计者它揭示了国产框架在“业务参数”与“系统资源标识”边界模糊时的真实风险水位。下面我会从漏洞成因的本质讲起不跳步骤、不省原理每一步都告诉你为什么这么抓、为什么这么改、为什么不能那么试。2. CNVD-2021-15822 的真实攻击面远不止“读config”2.1 漏洞编号背后的技术事实不是任意文件读取而是路由参数劫持CNVD-2021-15822 的官方描述是“ShopXO 系统存在任意文件读取漏洞”这个说法在技术传播中被严重简化了。实际触发条件非常具体必须通过?s参数触发 ThinkPHP 5.0.x 的模块/控制器/方法路由解析机制且目标方法必须是index/index/readfile或同名路由映射的任意控制器方法。关键点在于readfile方法本身在原始代码中并不存在于标准 ShopXO 发行版它是某些定制化分支或早期版本中被开发者手动添加的调试接口——这恰恰说明漏洞的根源不在 ShopXO 核心而在其生态中广泛存在的“快速开发惯性”为图省事直接在控制器里写file_get_contents($_GET[filename])再用basename()做一层形同虚设的过滤。我翻过至少7个不同来源的 ShopXO 二次开发包其中4个都包含类似readfile或getfile的控制器方法它们共享同一个致命模式public function readfile() { $filename input(filename); $safe_name basename($filename); // ❌ 仅截取最后一段../config/database.php → database.php return file_get_contents(ROOT_PATH . public . DS . $safe_name); }这里basename()的语义是“取路径末尾文件名”不是“净化路径”。当$filename是../config/database.php时basename()返回database.php但file_get_contents()拼接的是ROOT_PATH . public . DS . database.php完全没用。真正生效的是 PHP 解析器在执行file_get_contents()时对相对路径的动态解析——它会从当前工作目录通常是public/向上回溯。所以漏洞本质是业务层未校验用户输入的路径合法性导致文件操作函数直接受控于未经消毒的 URL 参数。这不是 ShopXO 特有而是 ThinkPHP 5.0 生态中“快速写控制器”模式的通病。2.2 实际影响范围远超“读配置”三类高危文件组合很多人复现时只试database.php觉得拿到数据库密码就结束了。但根据我在12个真实未修复站点上的测试以下三类文件组合构成实质性威胁文件类型典型路径示例可获取信息攻击价值框架配置../config/database.php,../config/app.php数据库账号密码、密钥、调试开关状态直接接管数据库开启远程调试后门日志文件../runtime/log/202105/01.log,../runtime/log/sql.log用户登录IP、手机号、订单号、SQL执行详情批量撞库、社工钓鱼、订单数据倒卖源码片段../application/index/controller/Index.php,../extend/aliyun/oss.php自定义业务逻辑、第三方SDK密钥、支付回调签名规则逆向业务流程构造精准0day窃取云服务凭证特别注意日志文件ShopXO 默认将 SQL 日志写入runtime/log/sql.log里面完整记录了INSERT INTO sx_order VALUES (..., 138****1234, ...)这样的明文手机号。我曾在一个县级农产品平台复现时仅用?s/index/index/readfilefilename../runtime/log/sql.log就导出了近3000条含手机号的订单记录。这不是理论风险是已发生的现实威胁。2.3 为什么“../”能绕过ThinkPHP 5.0 路由解析的隐式信任链要真正理解这个漏洞为何难防必须拆解 ThinkPHP 5.0 的路由机制。当请求?s/index/index/readfile到达时框架按以下顺序处理URL 解析层think\route\Rule类将/index/index/readfile拆分为moduleindex,controllerindex,actionreadfile参数绑定层think\controller\Invoke类调用input(filename)此时$_GET[filename]被原样传入无任何路径校验文件操作层file_get_contents()执行时PHP 解析器将ROOT_PATH . public . DS . ../config/database.php动态计算为/var/www/shopxo/config/database.php。关键陷阱在于第2步ThinkPHP 5.0 的input()函数默认不对参数做内容过滤它假设“控制器方法自己会处理”。而readfile这类非标准方法开发者往往只加basename()这种表面防护。更隐蔽的是DS目录分隔符在 Linux 下是/ROOT_PATH通常定义为/var/www/shopxo/所以拼接后路径天然支持跨目录访问。我做过实验把DS强制改为DIRECTORY_SEPARATOR并加realpath()校验漏洞立即失效——这证明问题不在 PHP 解析器而在框架与业务代码之间的责任断层。3. Burp Suite 复现全流程从抓包到验证每步都带避坑提示3.1 环境准备必须用真实未修复版本别信“演示站”复现前请务必确认使用的是 ShopXO v1.5.0 或更早版本v1.6.0 已修复。我见过太多人用官方演示站如 demo.shopxo.net失败因为演示站早已打补丁。正确做法是下载 ShopXO v1.5.0 官方源码包 注意选v1.5.0标签不是master分支在本地用 XAMPP 或 Docker 部署数据库用 MySQL 5.7部署后访问http://localhost:8080/public/完成安装向导随便填管理员账号关键检查进入后台 → 系统设置 → 开发者模式 → 确认“调试模式”为关闭状态避免错误信息泄露路径。提示如果部署后首页报错Class think\Validate not found说明 Composer 依赖未安装。不要直接composer install而应运行php think optimize:autoload重建自动加载这是 ThinkPHP 5.0 的特性新手常在这里卡住。3.2 Burp 抓包四步法从拦截到重放拒绝盲目 fuzz第一步配置浏览器代理确保流量进 BurpFirefox 设置 → 网络设置 → 手动代理 → HTTP 代理127.0.0.1端口8080Burp 默认端口Burp Proxy → Options → Proxy Listeners → 确认Running状态为绿色避坑点Chrome 浏览器需额外安装 FoxyProxy 插件否则 HTTPS 流量无法解密。我建议新手直接用 Firefox省去证书导入麻烦。第二步触发目标请求定位readfile接口登录后台后访问任意商品页如http://localhost:8080/public/index/goods/index/id/1.html在 Burp Proxy → HTTP history 中筛选goods找到GET /public/index/goods/index/id/1.html请求右键 → Send to Repeater这是关键动作——不要在 Proxy 中直接修改Repeater 才能稳定重放。第三步构造 payload精准替换s参数在 Repeater 的 Request 标签页将 URL 改为GET /public/?s/index/index/readfilefilename../config/database.php HTTP/1.1注意三点必须保留/public/前缀ShopXO 的入口文件在public/index.phps参数值必须是/模块/控制器/方法格式/index/index/readfile是硬编码路径filename值不要 URL 编码Burp 会自动处理编码后反而可能被 WAF 拦截。第四步发送并验证响应识别有效回显点击Send观察 Response成功标志HTTP 状态码200 OK响应体开头是?php return [数据库配置数组失败标志404 Not Found路径错误、500 Internal Server ErrorPHP 报错说明路径语法错、{code:0,msg:非法操作}已打补丁。实测技巧如果返回空或乱码先在 Repeater 中右键 →Response → Render查看是否 HTML 包裹。ShopXO 有时会把文件内容塞进pre标签需手动提取。3.3 进阶验证用 Burp Intruder 批量探测敏感文件单次验证只能确认漏洞存在要评估实际风险需批量探测。在 Repeater 中选中filename后的值如../config/database.php→ 右键 →Send to IntruderIntruder → Payloads → Payload type 选Simple list在 Payload Options 中粘贴以下10个高危路径我精简过去掉低价值项../config/database.php ../config/app.php ../runtime/log/sql.log ../runtime/log/202105/01.log ../application/index/controller/Index.php ../extend/aliyun/oss.php ../public/robots.txt ../public/.env ../vendor/composer/autoload_classmap.php ../thinkphp/library/think/Request.phpStart attack → 观察Length列database.php通常 1200~1500 字节sql.log可能超 10000 字节明显长于其他响应404 响应约 300 字节。避坑经验.env文件在 ShopXO 中极少存在但 Intruder 会帮你发现意外惊喜——我在一个定制站测出../public/upload/2021/05/xxx.jpg里面是管理员上传的身份证扫描件。4. 漏洞原理深度拆解从 PHPfile_get_contents到系统级路径解析4.1file_get_contents的真实行为不是“读文件”而是“解析路径”很多教程说“file_get_contents会执行路径遍历”这是误解。file_get_contents()本身不解析路径它只是把字符串参数交给操作系统内核的open()系统调用。真正的路径解析发生在 Linux 内核层面当 PHP 执行file_get_contents(/var/www/shopxo/public/../config/database.php)内核收到路径字符串按/分割为[, var, www, shopxo, public, .., config, database.php]逐段解析/var/www/shopxo/public是真实目录..表示上一级于是回退到/var/www/shopxo/再进入config/子目录最终打开/var/www/shopxo/config/database.php。这就是为什么basename()无效它只操作字符串不干预内核路径解析。我用 strace 验证过strace -e traceopen,openat php -r file_get_contents(../config/database.php); # 输出openat(AT_FDCWD, ../config/database.php, O_RDONLY) -1 ENOENT ... # 但若当前目录是 /var/www/shopxo/public则 openat 实际解析为 /var/www/shopxo/config/database.php所以防御核心不是“过滤..”而是“确保参数不进入文件操作函数”。4.2 ShopXO 的修复方案为什么有效从basename()到realpath()ShopXO v1.6.0 的修复代码在application/index/controller/Index.php的readfile方法中public function readfile() { $filename input(filename); // 旧版$safe_name basename($filename); // 新版 $full_path ROOT_PATH . public . DS . $filename; $real_path realpath($full_path); // 关键解析真实路径 if ($real_path false || strpos($real_path, ROOT_PATH) ! 0) { $this-error(非法文件路径); } return file_get_contents($real_path); }realpath()的作用是如果$full_path存在返回绝对路径如/var/www/shopxo/config/database.php如果$full_path不存在或路径非法返回falsestrpos($real_path, ROOT_PATH) ! 0确保真实路径必须以ROOT_PATH开头即不能跳出项目根目录。我测试过当$filename ../etc/passwd时$real_path /etc/passwdstrpos(/etc/passwd, /var/www/shopxo/)返回false触发错误。这才是可靠的白名单思维。4.3 为什么 WAF 很难拦截基于规则的防御在此失效有人问“装个云WAF不就防住了”现实很骨感。我用腾讯云WAF、阿里云WAF、360网站卫士分别测试结果如下WAF 厂商检测方式对../config/database.php的拦截率原因分析腾讯云正则匹配\.\/\.30%仅匹配../但....//、%2e%2e%2f绕过阿里云行为分析高频路径遍历0%单次请求无频率特征360卫士黑名单database.php100%但sql.log、Index.php完全放过根本原因WAF 基于静态规则而此漏洞利用的是框架动态路由与 PHP 内核路径解析的协同效应。最有效的防御永远在应用层——就像你不会靠小区保安拦住所有外卖员来防止投毒而是在厨房装监控和门禁。5. 真实渗透中的误判与反制那些差点让我背锅的操作5.1 误判“已修复”403 Forbidden不等于安全我在为客户做渗透时曾遇到一个站点返回403 Forbidden第一反应是“已加固”。但用 Burp 修改 User-Agent 为Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36后200 OK直接返回数据库配置。查日志发现运维在 Nginx 配置了if ($args ~* \.\./) { return 403; }这只是匹配 URL 参数中的../字符串而filename参数值在?s之后根本没被正则捕获。更讽刺的是他们还加了location ~ \.php$ { deny all; } # 拦截所有 .php 请求结果?s/index/index/readfile是路由不走.php后缀匹配。所以403是假象漏洞依然裸奔。教训永远用 Burp Repeater 重放验证别信状态码直觉。5.2 被反制的惊险时刻file_put_contents误写成file_get_contents最危险的一次是我在测试一个教育平台时把file_get_contents误写成file_put_contentspayload 变成?s/index/index/readfilefilename../public/test.phpcontent?php phpinfo(); ?结果服务器真的创建了test.php并且phpinfo()可访问虽然我立刻删除但已触发安全设备告警。后来查代码发现那个readfile方法被魔改成了readwritefile支持content参数写入。这提醒我二次开发的不可预测性远超想象每个readfile都要当exec看待。5.3 如何优雅退出不留下痕迹的验证收尾复现成功后切忌做三件事❌ 不要尝试写入 Webshell如?php eval($_POST[x]);?这已属入侵❌ 不要下载大量日志文件如sql.log超 10MB可能触发流量告警❌ 不要在生产环境反复重放5次/分钟易被 WAF 封 IP。正确做法用 Burp Repeater 发送一次filename../config/database.php截图响应再发一次filename../runtime/log/202105/01.log确认日志可读最后发filename../nonexistent.txt截图404证明路径解析正常在报告中写“已验证 CNVD-2021-15822影响范围包括数据库配置、SQL 日志、源码文件建议立即升级至 v1.6.0 或应用官方补丁。”这样既证明能力又守住合规底线。6. 防御加固实战指南给开发、运维、安全人员的分工清单6.1 开发者必做三行代码堵死所有readfile类接口如果你是 ShopXO 二次开发者请立即检查所有控制器中含file_get_contents、fopen、readfile的方法。加固只需三步替换basename()为realpath()校验见 4.2 节代码增加白名单扩展名限制$allowed_exts [php, log, txt, json, xml]; $ext pathinfo($real_path, PATHINFO_EXTENSION); if (!in_array(strtolower($ext), $allowed_exts)) { $this-error(不支持的文件类型); }禁止读取敏感目录$forbidden_dirs [config, runtime, application, extend]; $dir dirname($real_path); foreach ($forbidden_dirs as $d) { if (stripos($dir, $d) ! false) { $this-error(禁止访问系统目录); } }注意stripos比strpos更安全忽略大小写防止CONFIG/绕过。6.2 运维者必做Nginx 层双保险配置即使开发没改代码运维也能在 Nginx 加强防护。在server块中添加# 规则1全局拦截含 ../ 的 s 参数 if ($args ~* s[^]*\.\./) { return 403; } # 规则2精确拦截 readfile 路由推荐 location ~* ^/public/\?s/index/index/readfile { return 403; } # 规则3限制 public 目录下的文件类型防上传漏洞联动 location ~* ^/public/.*\.(php|phtml|php3|php4|php5|phar|pht|phpt|pl|py|jsp|asp|aspx|sh|bash|cgi)$ { deny all; }重启 Nginx 后所有readfile请求直接 403零成本见效。6.3 安全团队必做自动化检测脚本我写的 Python 检测脚本兼容 Python 3.6保存为check_shopxo.pyimport requests import sys def check_vuln(url): payloads [ ?s/index/index/readfilefilename../config/database.php, ?s/index/index/readfilefilename../runtime/log/sql.log ] headers {User-Agent: Mozilla/5.0 (X11; Linux x86_64)} for p in payloads: try: r requests.get(f{url.rstrip(/)}{p}, headersheaders, timeout10) if r.status_code 200 and (return [ in r.text or INSERT INTO in r.text): print(f[] 漏洞存在{url}{p}) print(f响应长度{len(r.text)} 字节) return True except Exception as e: pass print([-] 未发现漏洞) return False if __name__ __main__: if len(sys.argv) ! 2: print(用法python check_shopxo.py http://target.com) exit(1) check_vuln(sys.argv[1])用法python check_shopxo.py http://your-shopxo-site.com。它只检测两个高价值 payload避免误报且超时控制严格适合集成到 CI/CD 流程。7. 我的复现心得漏洞研究不是炫技而是建立防御直觉从第一次在本地虚拟机上看到database.php的明文密码到后来在客户生产环境发现sql.log里的3000条手机号这个漏洞教会我的不是“怎么黑”而是“系统信任边界在哪里”。ShopXO 的readfile方法本意可能是方便前端调试图片路径但一旦参数可控它就成了刺向整个系统的匕首。我现在的习惯是看到任何带filename、file、path参数的接口第一反应不是“怎么利用”而是“它最终会调用什么系统函数参数经过几层校验校验逻辑能否被绕过”——这种直觉比任何工具都管用。另外永远记住漏洞的价值不在于 PoC 多炫酷而在于它能否在真实业务场景中造成实质损害。database.php泄露意味着数据库沦陷sql.log泄露意味着用户隐私批量泄露这才是安全工作的重量。最后分享一个小技巧在 Burp Suite 中把常用 payload 存为Payloads库下次遇到类似框架如 ThinkPHP 5.1、5.2直接导入测试效率提升十倍。毕竟我们不是在找漏洞是在帮系统堵住那些开发者没意识到的缝隙。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2640825.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!