白盒审计实战:揭秘PHP老项目SQL注入与后台越权漏洞挖掘

发布时间:2026/10/19 10:59:12
白盒审计实战:揭秘PHP老项目SQL注入与后台越权漏洞挖掘
前段时间接手了一套房屋租赁管理系统V1.0的白盒审计任务。解压源码之后第一反应就是典型的“老PHP项目”没有框架、没有模板引擎、目录结构一眼看到底连数据库连接都直接写在公共文件里。这种代码对我来说已经不新鲜了但恰恰是最练手、最容易出成果的类型。CVE-2025-12598这个编号对应的核心SQL注入漏洞就是从这套系统的登录接口一路追出来的中间还连带发现后台越权和上传绕过最终串成了一条完整的攻击链。这篇文章我会按实际审计的顺序完整复盘一遍怎么搭环境、怎么看目录、怎么定位危险函数、怎么手工验证以及最后怎么给研发讲清楚修复方案。适合正在学代码审计的人、刚入行的安全测试人员还有想知道自己系统为什么总有漏洞的开发者。整套流程没有什么玄学每一步都是可以复现的。1. 审计前的准备工作先让老代码跑起来1.1 环境版本选择老代码要用老环境很多人拿到一套PHP源码第一件事就是用最新版XAMPP跑起来结果不报错、白屏、函数不存在轮番上阵。这套房屋租赁管理系统就是典型例子源码到处是mysql_connect()、mysql_query()这类老函数在PHP 7里面全被移除了直接跑必挂。我当时选了phpStudy切换成PHP 5.6 Apache MySQL 5.5的经典组合。为什么不是Nginx因为老项目经常带.htaccess伪静态规则用Apache兼容性最好少给自己找麻烦。数据库方面直接创建一个hrms库把源码包里的hrms.sql导入进去几分钟就能把环境拉起来。还有个小细节本地访问域名需要单独配一个比如hrms.testhosts映射到127.0.0.1。这不是为了好看而是很多老代码会用$_SERVER[HTTP_HOST]拼URL你用localhost和自定义域名访问生成的绝对路径完全不同后面测上传、测下载会踩坑。1.2 目录漫游与初始信息收集代码跑起来之后先别急着点功能打开目录结构看一遍。这套系统的骨架大概是这样的admin/后台管理端登录、新品管理、订单管理都在这里includes/公共函数、数据库连接config/配置文件uploads/上传目录根目录下散落几个PHP文件多半是首页、用户登录、留言板优先级最高的是配置文件。打开includes/db.php数据库账号密码大摇大摆躺在那里全局包含这个文件的页面全部有信息泄露风险。接下来看有没有install目录、README.txt、.bak、.zip备份文件。这些玩意儿只要存在一个漏洞等级就直接拉高安装脚本没删就可能被重装重置管理员密码备份文件更不用说了下载下来就是全套源码。我翻的时候发现uploads/目录里躺着不少历史遗留文件明显是之前有人测试过上传功能。这种痕迹非常有用它告诉你这个功能点以前被攻击过审计的时候要重点照顾。1.3 建立输入输出清单目录摸完我会手工建一张表的习惯。别小看这个动作后面所有漏洞追踪都靠它。入口文件参数功能过滤情况admin/login.phpusername, password管理员登录无后端过滤admin/search.phpkeyword房源搜索无过滤直接拼接index.phpid房源详情直接拼入WHEREmessage.phpcontent用户留言仅前端限制长度admin/upload.phpfile图片上传仅检查Content-Type这张表一列问题初步就浮出来了登录和搜索是SQL注入的重灾区上传是getshell的热门路径。接下来要做的就是沿着这些入口往下挖。2. 白盒审计核心方法论两条追踪主线2.1 从业务功能点出发正向追踪正向追踪的意思是把自己当用户把系统的功能点全部点一遍每一步都观察请求参数然后去代码里找对应的处理逻辑。登录页是最值得先看的。原因很简单登录是唯一不需要任何前置权限就能触达的敏感功能而且登录逻辑往往直接拼接SQL。我当时打开admin/login.php果然看到了最经典的写法?php include ../includes/db.php; $username $_POST[username]; $password $_POST[password]; $sql SELECT * FROM admin WHERE username $username AND password $password; $result mysqli_query($conn, $sql); $row mysqli_fetch_assoc($result); if ($row) { $_SESSION[admin] $row[username]; header(Location: index.php); } else { echo Login failed; } ?这段代码至少在三个层面有硬伤$_POST参数直接进SQL语句没有转义密码明文存储、明文比对登录成功后仅仅设置了$_SESSION[admin]后续页面是否校验尚未可知。在审计过程中我习惯于把这种代码当作“疑似漏洞点”记录到表格里然后继续看其他功能。因为一个系统中往往不止一个漏洞你需要最后再统一评估优先级。2.2 从危险函数出发反向追踪反向追踪就是先找危险函数再看有没有参数能触达它。这一步我一般用正则全量扫描源码把可疑行全部揪出来。grep -rn mysqli_query\|mysql_query --include*.php . grep -rn select\|insert\|update\|delete --include*.php -i grep -rn include\|require\|include_once\|require_once --include*.php . grep -rn move_uploaded_file\|file_put_contents\|system\|exec\|eval --include*.php .不需要什么高端工具VSCode全局搜索也能胜任。关键在于搜索结果需要人工逐条看判断参数来源是不是用户可控。反向追踪最适合找出容易被忽略的“隐藏入口”。比如我在message.php看到留言插入用的mysqli_querySQL语句里直接拼了$_POST[content]而前端只做了maxlength限制——这种就是典型的前端校验型漏洞浏览器随手改一下请求就能绕过。2.3 参数溯源与调用链分析找到危险函数只是万里长征第一步你要证明“用户输入能一路到达这个函数”。我的做法是穷追参数当看到一个变量进了SQL语句就在编辑器里点“查找所有引用”看它来自哪里、经过了哪些函数、被哪些过滤手段处理过。比如登录页的$username追踪路径是$_POST[username]赋值给$username直接拼入$sql字符串作为参数传给mysqli_query()中间没有任何清洗函数。这种情况下注入基本是板上钉钉的事实接下来就是手工验证的事了。还有一种情况是参数经历了两次编码、一次加解密、一次替换这种时候我会在关键位置临时加var_dump()看实际值。审计不是看戏你的目的是拿证据。2.4 过滤机制的常见误区这套系统里其实有一套看上去不错的过滤逻辑includes/common.php里定义了clean()函数它对字符串做了addslashes()处理也用了htmlspecialchars()。但问题在于这个函数不是全局自动调用的只有部分功能主动调用它。登录、上传这类入口压根没include这个文件。从审计角度讲这提醒我们一件事“看到过滤函数不要立刻放心要看它有没有被覆盖到所有入口看到输入点不要立刻下结论要看它能不能绕过层层过滤到达危险函数。”很多开发者的误区在于认为“我在公共文件里加了一个过滤函数全站安全了”实际上公共文件本身没被引用等于零。3. CVE-2025-12598 漏洞点完整复盘3.1 定位登录页SQL注入这就是CVE-2025-12598的核心点。前面那段admin/login.php代码已经充分说明$username拼接进SQL用户名和密码没有参数化查询后端也没有任何长度限制、类型限制。这不是设计缺陷这是教科书级漏洞。如果只看到这里就止步了那审计还是做得太浅。我的习惯是继续往下想这个漏洞能造成什么后果登录页注入的直接后果是绕过管理员认证拿到后台权限。后台又有什么要看下一步。3.2 手工验证与抓包细节验证SQL注入我的顺序永远是先手工、再工具。上来就跑sqlmap等于跳过理解了而且很多复杂环境工具不一定成功。手工第一步在用户名框输入admin密码随便填。系统返回了数据库语法错误页面直接爆出SQL片段You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near admin AND password123 at line 1这一步的意义不仅是确认注入更重要的是看到了拼接语句的完整结构。接下来构造闭合用户名填admin OR 11 -- -密码任意。整个SQL变成SELECT * FROM admin WHERE username admin OR 11 -- - AND password 123--后面的内容全部被注释掉OR 11让条件恒成立于是查询返回了第一行管理员记录登录成功。我一般会用Burp Suite把这些操作完整录下来放进Repeater反复调试。注意一点Burp里面URL编码要正确空格、引号、注释符都各有对应编码。工具跑的时候也要控制payload的格式否则很容易因为编码问题误判为不存在SQL注入。3.3 注入点的后续扩展与工具验证手工确认过基于布尔的盲注、报错注入、联合注入都可用之后我才上sqlmap做批量验证和数据提取sqlmap -u http://hrms.test/admin/login.php --datausernameadminpassword123 --batchsqlmap会自动识别注入类型但它观察到的只是“症状”最终还是要回到代码层面确认哪个参数、哪条语句、哪种注入手法。在我这几年的经验里sqlmap更适合辅助提取数据真正的漏洞理解得靠白盒代码阅读。登录注入搞定后我继续摸后台。结果发现admin/index.php压根没有验证session。直接访问http://hrms.test/admin/index.php后台管理页就这么打开了连登录步骤都省了。这意味着即使没有上面那个SQL注入后台也是裸奔状态。越权漏洞和SQL注入叠加风险等级直接抬升。再往下走后台的上传功能admin/upload.php只检查了$_FILES[file][type]这个值浏览器端完全可控。Burp抓包把Content-Type改成image/jpeg文件后缀改成.php放行之后直接落盘在uploads/目录。Apache配好解析规则时这就是一个完整的webshell链路。3.4 影响面与风险评级这一整套攻击链总结下来是登录接口SQL注入远程绕过认证获取管理员身份后台未鉴权认证机制形同虚设后台页面裸奔上传类型校验失效已有后台权限可写webshell无权限时可配合越权直接攻击评级不用纠结理论框架直接看现实危害这套系统托管着租客的个人信息、管理员账号、订单数据攻击者拿到DBA权限后不仅可拖库还能结合文件上传拿到服务器权限。对于一个小型业务系统来说这种危害已经是“致命打击”级别。4. 自动化工具与人工审计的配合4.1 静态代码审计工具选型市面上常见的PHP审计工具有这些工具定位适合场景Seay源码审计系统PHP专用轻量快速扫危险函数适合小项目Fortify SCA企业级规则全大项目、深度数据流分析RIPS老牌PHP分析器历史项目但更新较慢VSCode grep手工审计辅助一切项目我最常用的组合对于这套项目Seay跑一遍大约几十秒能列出所有调用了危险函数的文件。但工具只是缩小搜索范围它不会告诉你“这个漏洞能不能实际被远程达到”。比如Seay报了file_get_contents在某个模板文件里但那个文件根本不能被外部访问这就是典型的误报。我拿到工具报告后依然要回到源码里手工确认每条路径。4.2 工具误报与人工确认工具误报主要来自三方面第三方依赖库里的旧函数、全局配置里的安全动作、以及那些写了但从未被调用的函数。如果完全照搬工具报告去提交漏洞研发会把你当成只会跑工具的脚本小子。我的做法是给每条报告打标签可达、不可达、需进一步验证。比如Seay报了admin/edit.php里有eval($_POST[code])这种神仙代码我查了一遍发现这个文件根本不存在于路由表里是历史遗留备份文件那就先放一边。真正能被外部请求触达的文件才值得重点分析。4.3 动态调试技巧让错误信息开口说话审计代码不能只看静态得让代码跑起来拿第一手证据。我常用的几个手段第一临时开启错误显示。在入口文件顶部加ini_set(display_errors, 1); error_reporting(E_ALL);这样PHP会把SQL报错直接吐到页面上省了猜SQL语句结构的功夫。第二在关键查询前加一行echo $sql;看拼接结果。这个方法土但极其高效尤其是在追踪二次拼接、编码转换时直接看最终执行的SQL长什么样比脑子里空推可靠一百倍。第三用Xdebug设置断点观察变量在每一层的值。这个适合逻辑复杂的越权判断比如某个权限标志位到底被哪个文件覆盖了一步步跟下来就清楚了。4.4 修复方案的落地写法漏洞审计只报问题不写方案研发的配合度会大打折扣。针对这套系统我直接给出能落地的修复代码。SQL注入修复必须用参数化查询不要依赖任何转义函数?php include ../includes/db.php; $username $_POST[username]; $password $_POST[password]; $stmt $conn-prepare(SELECT * FROM admin WHERE username ? AND password ?); $stmt-bind_param(ss, $username, $password); $stmt-execute(); $result $stmt-get_result(); $row $result-fetch_assoc(); if ($row) { session_start(); $_SESSION[admin] $row[username]; header(Location: index.php); } else { echo Login failed; } ?输出到页面的内容必须转义echo htmlspecialchars($row[title], ENT_QUOTES, UTF-8);上传功能改成白名单校验加随机文件名$allowed [jpg, jpeg, png, gif]; $ext strtolower(pathinfo($_FILES[file][name], PATHINFO_EXTENSION)); if (!in_array($ext, $allowed)) { die(File type not allowed); } $newName uniqid() . . . $ext; move_uploaded_file($_FILES[file][tmp_name], ../uploads/ . $newName);给所有后台页面加统一鉴权底部引入一个check_login.php里面检查$_SESSION[admin]不存在就跳回登录页。至此这套系统的致命问题才算封堵住。5. 常见问题与排查技巧实录5.1 为什么黑盒扫描器扫不到这类漏洞很多人会问这种SQL注入那么明显为什么开着扫描器跑了一整晚都扫不出东西原因在于黑盒测试只看到HTTP请求和响应看不到代码里的拼接逻辑。登录接口的注入点参数值往往要满足特定业务语义扫描器泛泛地填一堆随机payload很容易被基础校验拦掉而白盒审计可以直接看到“这个参数百分之百拼进SQL了”验证起来目标明确。所以黑盒和白盒从来不是替代关系而是互补关系。黑盒擅长发现已知类型的常规问题白盒擅长发现业务逻辑和代码层面的深层问题。如果只能选一种老PHP项目我肯定选白盒。5.2 PHP版本差异造成的复现失败本地复现这套漏洞时有个问题第二天才注意到第一次在PHP 7.4环境访问后台页面直接500因为mysql_connect()函数不存在。换到PHP 5.6后才正常。这里给个经验收到老代码先看phpinfo()跑出来的版本再看官方文档要求不要默认最新版本就通用。还有一次遇到登录页报错但SQL语句被注释掉了调试半天才发现是PHP配置文件里开了magic_quotes_gpc传入的单引号被自动转义了。后面测试时条件允许的话我都会在审计环境里关掉它模拟现在的主流配置不然会漏判注入。5.3 一种“看起来安全”但实际被绕过的场景这套系统里有一处搜索接口用了addslashes()做了转义我一开始以为注入很难成功。后来看代码才发现转义发生在拼接SQL之前但数据是先从数据库里查出来再拼进第二条SQL的。第一次查询没啥问题第二次查询把数据库里已有的内容直接拼SQL如果那条内容是攻击者可控的就形成了二次注入。这种问题在黑盒层面几乎测不出来因为请求里根本没有特殊字符。白盒审计的价值在这里体现得淋漓尽致——你能看到数据流经过了哪里哪些“安全”只是纸面安全。5.4 漏洞报告怎么写才能让研发服气最后说下报告输出。我的模板是这样的漏洞名称、所在文件、具体参数、请求报文的截图、影响范围、复现步骤、修复建议。最关键的是说清楚“为什么会出事”而不是简单甩一句“这里有问题”。登录页SQL注入不是“有注入风险”而是“攻击者可以直接用admin账号登录后台无需知道密码后续还能拖走全部租客信息”。给研发讲危害的时候结合他们的业务场景会更有说服力。你告诉对方“这套系统里面有租客身份证照片”“账单可以查到所有历史订单”比说几百字的CVSS评分更有效。评分辅助决策但业务影响才是真正的痛点。最后想说的话白盒审计这件事说白了是个耐心活。很多人问我挖CVE有没有什么捷径我的答案从来都是把用户输入当成一条流动的数据从入口到数据库到页面输出每一步都别放过。不要急着上工具先让代码跑起来、目录摸熟、入口列全剩下的就是按图索骥。如果你刚入门找一套老PHP项目按这篇文章的流程走一遍比看十篇理论文章强得多。等你把登录、上传、越权这三类问题都能独立挖出来再去看那些框架完善的现代代码心里会踏实很多。技术更新换代但审计的思路永远是那些。