PHP代码审计实战:从输入追到漏洞的完整溯源思路

发布时间:2026/10/10 3:31:31
PHP代码审计实战:从输入追到漏洞的完整溯源思路
墨者学院的“PHP代码分析溯源”系列是不少做代码审计入门的师傅反复刷的题。第4题表面上和前面几题差不多都是把源码丢给你让你自己找到问题、打通利用链、把结果交上去。但如果你还按上一题的惯性上来就搜eval、assert、shell_exec大概率会卡在原地。这题真正想考察的是完整的分析能力从入口点开始顺着参数把整条调用链捋清楚判断每个可疑点是否真的可控、真的能到达危险函数最后再把漏洞变成可用的通道。所以这篇东西我不打算写“第4题的标准答案”而是把一套能反复用的审计流程拆开讲顺带说说我在墨者这类平台上踩过的坑。1. 拿到题目包后先搞清楚“溯源”二字指什么1.1 两种常见的题目形态先说一个很多人容易忽略的事题目叫“代码分析溯源”但不同考点的侧重点完全不一样。第一种是“给一段源码分析出漏洞并利用”重点是找可利用点目标通常是拿到flag或者后台权限第二种是“给你一个被入侵后的环境或日志倒推攻击者做了什么”这种更像真实应急响应要你在代码里找后门、找webshell、甚至通过日志还原攻击链。墨者这套题从命名来看偏向前者也就是“源码审计利用”但题面里可能还会藏着一条暗线比如让你通过漏洞拿到数据库配置后再去数据库里溯源某条操作记录。所以我拿到题目后的第一件事不是打开代码而是先把题目描述、提交格式、环境信息完全读一遍把“解题目标”写下来。很多新手在这里栽过跟头。题目明明要求提交某个“溯源结果”他却以为只要getshell就行结果拿到shell也不清楚该去哪个目录找答案白白浪费环境时间。溯源题还有一个特点出题人会把真正要你找的“结论”藏在代码、配置、数据库或日志中的某一层如果你只是验证了一个RCE就收工往往会漏掉下一步。1.2 本地环境准备先能跑起来再谈分析解析这类题我建议先搭一个本地调试环境不要直接在题目网页上试来试去。在线环境有IP限制、并发干扰payload太长很容易被影响而且每个操作都会留下日志你也不想让题目环境一团乱。本地准备清单PHP集成环境phpstudy或者直接用Docker拉一个php:7.4-apache镜像。版本尽量和题目环境一致。墨者这类老题目很多源码是PHP 5.x/7.x时代写的你拿PHP 8跑可能因为函数移除直接跑不起来那分析就无从谈起。代码编辑器VSCode或PhpStorm。我习惯用PhpStorm因为它的Find Usages和Go to Declaration在追调用链时非常有用能直接看一个函数在哪些地方被调用双向跳转很顺手。抓包工具Burp Suite用来改请求、看响应差异、对比各种payload的输出。数据库环境如果源码里带了SQL文件本地建好库和表很多逻辑需要真实数据才能走到。环境装好后第一步是访问一下首页确认能不能正常打开。如果打开就报错或者白屏优先看config.php里的数据库配置、PHP版本、路径设置别急着审代码。跑不起来的代码很多逻辑你是看不全的。常见的坑是数据库密码错误、时区配置导致PHP 8直接报警告以及缺少某个扩展比如mysqli、gd、curl。这类问题在本地版本和题目版本不一致时特别容易出现。2. 整体梳理把审计变成一次有目的的排查2.1 先画文件地图再读业务拿到源码后不要一头扎进去逐行读。我习惯先做一张文件地图目录里有哪些文件哪些是入口哪些是公共引用哪些是配置文件。用命令列一下find . -type f -name *.php | xargs wc -l | sort -rn | head -30这个命令能很快告诉你哪些文件代码量最大。真正复杂的业务逻辑通常集中在少数几个文件里而入口文件一般都很短。比如典型的MVC结构入口index.php可能只有十几行业务逻辑都在controllers和models目录下传统小应用则相反可能十几个页面文件各管一段公共函数都塞在common.php里。文件地图画完之后优先级就很清楚配置文件看是否存在硬编码、可写风险、数据库口令入口与路由看哪些页面是无条件加载的哪些是要登录的公共函数文件看过滤函数和权限校验都做了什么处理上传、下载、搜索、登录等功能的业务文件这些是漏洞高发地。在墨者这类溯源题里有时还会故意在某个目录里放一个不起眼的文件比如include/conn.php、upload/shell.php。它们往往不是正常业务的一部分文件名也能看出端倪。画地图时如果发现某个文件路径和整体结构格格不入请单独标记这很可能就是出题人留下的“题眼”。2.2 全局搜索的顺序与优先级有了地图之后再做一次定向搜索。搜索不是乱搜顺序很重要。我一般分成三轮。第一轮搜“危险终点”也就是直接执行代码、命令、查询数据库、读取文件的函数grep -rn -E eval|assert|system|exec|shell_exec|passthru|popen|proc_open|pcntl_exec|unserialize|include|require|preg_replace|create_function|call_user_func --include*.php .第二轮搜“输入起点”grep -rn -E \$_GET|\$_POST|\$_REQUEST|\$_COOKIE|\$_FILES|getParameter|getHeader --include*.php .第三轮把这两份结果交叉比对重点看“输入起点出现在危险函数所在文件的上一行或同一函数体内”的情况。这个交集越小题目往往越简单交集很大说明这个应用对用户输入做了很多处理你需要继续往下钻。这里插一句工具问题。你可能听说过Seay源代码审计系统、RIPS、Fortify等自动化工具它们确实能扫出大量疑似点。但CTF题目总会故意设置一些绕开简单正则的写法比如函数名拼接、用变量动态调用。所以我只把工具结果当线索真正判断还得靠人。手动审计的核心能力是理解上下文工具替代不了。2.3 容易被忽略的高价值位置有几个位置我做题时一定会额外花时间看因为它们经常是出题人埋点的首选上传点与包含点的组合。如果某个文件上传只校验了MIME或后缀再把上传文件路径交给include那基本就是一条完整的利用链。日志写入与日志包含。很多小应用会把IP、UA、错误信息写入日志文件如果日志路径可控、内容部分可控配合文件包含就能执行代码。配置文件中的parse_str和extract。这两个函数能把数组展开成变量如果参数可控就可能造成变量覆盖进而覆盖掉权限判断、跳转地址等关键值。下载或预览功能里的路径处理。download.php?filexx直接拼文件路径是文件读取/包含漏洞的重灾区。序列化数据落地。比如把用户提交的序列化字符串直接存库或直接unserialize也是常见考点。我把它记为“上传、下载、日志、配置、序列化”五类位置。每拿到一个新项目我会先把这五类位置全部标出再结合输入起点逐一定级。碰到可疑点先别急着试把上下文代码完整复制到一个临时文件里把用户可控的变量标出来再判断能不能走到危险函数。3. 核心高危点的排查思路3.1 文件包含类参数能不能原样到达include文件包含的原理不展开说了关键要看一步include或require的参数是否来自用户输入以及到达之前经过什么处理。我以一个类似常见小应用的代码片段举例?php if (isset($_GET[page])) { $page $_GET[page]; include($page . .php); } ?如果看到这种写法第一反应就是这里存在本地文件包含而且下面有开发者以为“很安全”的处理——强制加上了.php后缀。那我们就得考虑绕过方式用php://filter伪协议读取源码比如php://filter/convert.base64-encode/resourceconfig注意resourceconfig不带.php后缀在某些拼接场景下正好能用用绝对路径尝试包含系统文件拼接.php后大多不成立但别急着否定有些环境对文件名处理很粗糙如果allow_url_includeOn可以尝试data://协议把base64编码的PHP代码传进去直接执行。实际审计中一旦看到include的文件名可控先看这一行前后的上下文比联想N种绕过姿势更有效。比如有些地方在include之前会写$page str_replace(../, , $page)但可能没过滤绝对路径有些地方会拼.php那我们就可以用php://filter读取同目录下的源码或者用data://协议传入含量接执行前提是PHP配置允许。这类题的动态验证也很有讲究。你可以先用一个不存在的页面名访问看报错信息里是否完整暴露了拼接后的路径。如果能看到基本就能确认前面没有加后缀、没有加目录前缀漏洞条件非常干净。如果页面会把文件内容原样输出那大概率还存在文件读取漏洞可以直接读源码。在溯源题里文件包含的价值不只是RCE它还能帮你读取config.php这类敏感文件拿到数据库口令后再去访问后台甚至数据库这样整个利用链就完整了。所以排查文件包含时既要评估“能否命令执行”也要评估“能读到什么敏感信息”。3.2 SQL注入类看拼接、转义和输出点SQL注入在溯源题里非常常见。审计时先搜SQL语句的位置再看参数是怎么进去的。我一般把SQL注入分成两类来看一类是数字型和字符串拼接型一类是经过弱过滤的二次注入。对于最简单的拼接型代码大概长这样$sql SELECT * FROM users WHERE id . $_GET[id];这基本能直接注入。如果是字符串型开发可能写了addslashes或mysql_real_escape_string那就要看有没有宽字节注入的可能以及数据库连接字符集是否是GBK等。现在很多题目环境都是PHP 7宽字节注入的场景比以前少了但也要注意。更隐蔽的是“过滤后的二次注入”。比如一个函数先把单引号替换成\但只执行了一次之后这个字符串又被拼进后续SQL查询就可能出现转义被消费掉的情况。我在墨者这类题目上见过不少类似的设置先存储后拼接结果存储层的转义在拼接层被绕过了。另外判断注入不需要一上来就上sqlmap。先手动确认输入一个引号看是否报错输入1 and 11与1 and 12看返回差异看页面是否有输出点比如查询结果会显示用户名那就可以考虑联合查询没有输出点就用报错注入或盲注。溯源题有一个细节值得注意题目可能不是让你直接读出数据而是让你通过SQL注入把恶意PHP代码写入数据库再通过某个文件包含把数据库内容执行成代码。这种情况我在审计时一定会把“存在SQL写入点”和“存在数据库内容回显或包含点”连起来看因为这是一套完整的利用链。你在代码里看到INSERT或UPDATE语句时别光想着读数据也想想这个数据之后去了哪里。3.3 命令执行与代码执行最直接的入口命令执行类漏洞审计起来最省事但也是最容易看走眼的。危险函数就那么几个只要用户参数能原样到达基本就稳了。举个例子很多小工具都有“ping检测”功能$ip $_POST[ip]; system(ping -c 3 . $ip);这里就能直接拼命令。审计时要重点注意开发人员做的过滤比如只过滤了;和|但没过滤$()、反引号、换行符比如只过滤了空格但可以用${IFS}绕过再比如用正则会过滤cat、ls等关键字那可以用cat、c\at、编码等方式绕过。这些都是老生常谈但在实际做审计时我会把这些过滤函数从头到尾读一遍看它到底把哪些字符列入黑名单哪些字符漏掉了。代码执行类则要格外关注几个函数和写法eval和assert如果参数来自用户输入直接就是RCEpreg_replace的/e修饰符PHP 7以下可以用替换内容会执行create_function通过字符串创建匿名函数参数可控时等同于代码执行call_user_func、call_user_func_array第一个参数是回调函数名如果可控可以直接调用任意函数比如call_user_func(system, whoami)变量函数比如$action $_GET[a]; $action();这也是一种隐性的代码执行。在做题时一旦命中了这些函数我建议立即在PhpStorm中打开该文件然后Find Usages看这个函数是从哪调进来的。如果是从表单、URL参数直接进来那么下一步就直接构造最简单的payload验证不要再继续往后审其他文件。因为这是性价比最高的路径。3.4 反序列化与变量覆盖绕远路但经常是正解墨者这类题目中反序列化是大概率出现的考法因为PHP序列化漏洞利用讲究链条和魔术方法比单纯的危险函数更有“分析”的味道也契合“代码分析溯源”这个标题。先看一个最基础的触发点$data $_COOKIE[user]; $user unserialize(base64_decode($data));如果项目里存在一个__destruct或__wakeup方法并且里面调用了可执行方法我们就可以通过构造序列化对象来触发。这个链条就叫POP链也就是面向属性编程。它不要求一个函数同时包含危险函数和用户输入只要串联起多个类即可。审计反序列化的时候真正难的不是写poc而是确定每个魔术方法的调用条件和属性类型。我一般按以下顺序做全局搜魔术方法__destruct、__wakeup、__toString、__call在方法体里找危险动作文件操作、命令执行、调用外部方法从触发点反向推我的输入怎么变成目标类的属性写一个本地的生成器脚本把对象序列化后放进输入点测试。变量覆盖也类似。extract($_POST)在早年CMS里非常流行。比如下面这种写法extract($_POST); if ($is_admin) { echo welcome; }如果变量$is_admin原本是false但通过POST提交is_admin1直接就被覆盖成1权限判断形同虚设。parse_str也类似它会把字符串解析成变量如果输入可控同样可以覆盖关键变量。这类漏洞的排查技巧在代码里搜$_GET、$_POST、$_REQUEST之后重点找有没有一个extract或parse_str包裹这些超全局变量。如果看到extract($_POST)直接把它标记为“高优先级的变量覆盖点”再往下看哪里用到了可能被覆盖的变量。4. 动态验证与一条完整利用链的落地4.1 静态分析之后必须做动态验证静态审计得出的结论只能算“疑似”能不能用还得看动态表现。我一般拿到题目环境后不会立刻上大招而是先做几个温和的验证请求。比如假设我在本地分析时发现某个参数进入了一个文件包含点我会这么做先正常访问一次记录正常返回内容再访问一个确定不存在的文件名观察报错和响应差异再用php://filter读取当前文件自身看是否能拿到base64编码源码如果读不到考虑路径是否要加前缀、有没有后缀、是否过滤伪协议。这四步走完基本就能确认这个点能不能读文件、能不能包含远程。整个过程不要依赖自动化工具Burp的重放功能就够了。我特别喜欢把同一请求的响应放到一个临时笔记里对比肉眼判断比看工具输出快得多。在做动态验证之前要确认本地PHP版本和题目环境一致。如果条件不允许至少要确认几个关键配置allow_url_include、display_errors、magic_quotes_gpc。这些配置直接决定了payload能不能通。拿allow_url_include举例很多早期的PHP文件包含RCE需要远程包含如果环境是默认配置Off那你就只能走php://input或者本地文件包含加日志投毒。4.2 从回显差异判断漏洞类型做题时payload发过去之后要学会看回显。我总结了一批典型的回显特征遇到这些情况几乎可以直接对号入座页面出现完整的PHP报错信息比如Warning: include(foo.php): failed to open stream:说明文件包含点基本确认页面出现Fatal error: Call to undefined function或class not found说明代码执行类函数被触发了页面输出数据库查询的错误信息比如You have an error in your SQL syntax说明SQL拼接点存在页面空白或500但响应体长度发生了变化多半是代码走到了某个分支需要结合本地代码继续分析回显完全一样那可能需要转盲注或盲打比如时间盲注、外带请求。看回显是动态验证的核心能力。很多人老是问“为什么我的payload没反应”其实多半不是payload写错了而是根本没看懂前后两个响应的差异。我一般先点开对比工具左边是正常请求右边是测试请求先把响应头、状态码、长度都过一遍再去看正文。有些时候题目环境出于防护会把错误显示关掉但状态码和长度会变这本身就能给你线索。以命令执行举例如果你怀疑一个system函数能打先别急着反弹shell。先用最无害的whoami测试看回显是否出现当前用户如果没回显可以用ping配合时间差来确认比如ping -c 5 127.0.0.1再把-c 5改成-c 10观察响应时间差异。确认命令能执行之后再考虑写文件、读取flag或者进一步收集信息。我个人的习惯是能读文件就先读文件尽量避免反弹shell省得被平台盯着。4.3 拿到权限以后的“溯源”动作回到“溯源”两个字。很多题目拿到代码执行权限并不代表结束它还要你继续在系统里找到“攻击源头”。比如源码里可能有一个后台管理员登录功能你通过SQL注入读取到管理员表里的用户名和密码哈希然后需要在后台进一步操作或者找到一条日志记录里面藏着题目要求的flag。我自己的经验是把这类题当成一次“轻量级应急响应”来做拿到shell或代码执行后先看当前目录和Web根目录列目录看有没有特殊文件看数据库配置找连接账号和库名尝试连接数据库看日志目录找访问日志或者错误日志溯源攻击路径全局搜文件名疑似flag的文件比如flag.txt、key.php、隐藏目录里的文件。记住溯源题的最终目标不一定是权限而是“结论”。可能是某个IP、某个参数、某个文件名甚至是一串被加密的数据。所以拿到权限后的每一步操作都要有目的不要漫无目的地翻目录。翻遍整个服务器找flag是最低效的方式。你要学会从题目描述里推断它需要什么再按图索骥。5. 常见问题与避坑实录5.1 卡点速查表把我在做这类题时最容易卡住的现象整理成了表格供你排查时快速对号入座现象可能原因排查方向本地能跑题目环境白屏PHP版本不同、函数被禁用、路径大小写问题查看题目环境响应头、报错输出对比本地配置文件包含点读不了文件被添加了后缀、过滤了../、伪协议被禁尝试绝对路径、php://filter、data://看报错信息include报错但不显示内容display_errors关闭故意访问一个不存在文件观察状态码和长度变化SQL语句报错但返回参数被过滤过滤了关键字未过滤编码尝试URL编码、内联注释、大小写变形命令执行无回显输出被丢弃或函数改为shell_exec使用时间差验证先别上反弹shellpreg_replace利用失败PHP版本过高e修饰符已移除换思路查看是否存在assert或create_function反序列化poc不生效魔术方法没有触发、属性类型不匹配本地用同版本PHP生成序列化数据逐段调试这张表不是标准答案但它覆盖了初学者80%以上的卡点。如果你卡住了先别急着改payload先回到代码里确认环境特性。环境搞不对payload写得再漂亮也白搭。5.2 在在线平台上做审计的独家经验最后分享一些线上线下差异相关的经验。墨者学院这类靶场平台和本地审代码最大的区别在于环境是共享的、不稳定的、有时间限制的。我在上面做过几次题积累了几个比较实用的习惯珍惜“第一次访问”。有些题目环境会记录你的整个操作过程如果你上来就对着题目网站乱试刷出大量异常流量反而会让自己后面利用时被误导。先想清楚payload再一次性发出去。优先看题目页面的源码注释和响应头。有时出题人会在HTML注释里留下提示比如“过滤了单引号”、“flag在数据库”之类的信息比你自己瞎猜高效得多。如果题目环境比较卡减少重复请求。很多验证没必要在线做先本地把payload调试好再放到题目环境一次通过。善用本地的差异比对。同一个payload在本地和在线都通说明环境一致度很高只通一个就要马上检查PHP版本和配置差异。写完利用链后建议截图和记录请求包。这类平台有时候会重置环境你辛苦试出来的路径下一轮可能要重新走。有记录能极大加快复现速度。5.3 一个我常用的“溯源”思维模型做多了这类题之后我发现“溯源”其实可以抽象成一个反向的调用链图。正常代码是“输入 - 处理 - 输出”攻击则是“危险点 - 往回找触发 - 再往回找输入”。所以我建议你在纸上画三列第一列是可控输入第二列是处理函数和过滤函数第三列是危险终点。然后用线把它们连起来能连成一条完整路径的就是解题入口。这个图不一定要画在纸上可以是一个简单的文本笔记。比如$_GET[id] - intval? - SQL拼接 - 报错注入 - 读管理员密码 $_COOKIE[data] - base64_decode - unserialize - __destruct - eval一开始手可能生画过三道题之后就自然形成条件反射了。我强烈建议你在做墨者第4题之前先拿前几题练一练这个思维模型。本身这类题就是练审计手感最好的载体题目短、目的明确、反馈也快。在第4题上我一开始也犯了“只盯着危险函数”的毛病浪费了不少时间。后来想明白一个事代码分析溯源的关键不是“找到eval”而是“理解整条逻辑链”。你把一条攻击路径从输入参数一路追到执行结果把过程中每一个过滤、跳转、拼接都搞清楚答案自己就浮出来了。最后再分享一个很小的技巧看到任何可疑参数先问自己一个问题——“它最远能走到哪一步”顺着这个问题追下去基本不会迷路。希望这篇东西能帮你少走一点弯路有更好的思路也欢迎交流。