PHP代码审计实战:eval函数引发的敏感信息泄露漏洞全解析 做PHP代码审计这几年eval函数几乎每次都会出现在高危名单里。eval的作用是把字符串当作PHP代码来执行听起来很方便但审计时看到它第一反应应该是“这里有没有可能被外部输入污染”。以“eval函数引发的敏感信息泄露漏洞”来说问题往往不是eval本身有多神秘而是动态执行给了攻击者一个合法的代码入口他可以把file_get_contents、include、readfile这些文件读取动作直接写进来把应用的配置、密钥、数据库口令、业务数据一股脑带走。这篇分享不聊教科书理论我从代码审计的实操视角出发拆解eval泄露敏感信息的完整链路给出定位、验证、修复、排查的一整套方法。适合需要处理PHP代码安全的开发人员和安全测试人员也适合刚入门代码审计、想快速建立危险函数排查思路的朋友。安全提示文中的所有验证动作都请限定在你自己有权限的系统或模拟项目里进行不要对未授权的目标尝试任何payload。1. eval导致敏感信息泄露的逻辑起点1.1 一个看上去合理却把入口打开的高危写法很多开发写eval不是为了炫技而是想让业务“灵活”一点。比如做表单联动、动态报表、自定义计算公式最容易出现这种代码?php // 接收前端提交的公式或模板片段直接执行 $code $_POST[code]; eval($code); ?这段代码的问题一眼就能看出来攻击者只要POST一个echo file_get_contents(config.php);配置内容就会回到响应里。更常见的是从一个数据库字段或缓存文件里读出“可执行片段”再传给eval。这类写法初看没有那么危险因为开发者会告诉自己“内容来自后台配置用户碰不到”。但后台配置如果存在导入功能、编辑器漏洞或者员工账号被盗外部输入照样能顺着链路摸进来。我平时审计的第一步就是先分清eval出现在哪个场景是纯代码执行入口还是被包在业务逻辑里做参数拼接。场景不同病毒的传染面积完全不同。纯入口好定位拼接场景才容易藏着绕过点。1.2 eval不是函数它的危害在于拿到了整个执行环境eval在PHP里是语言构造和include、isset、list是一类不是普通函数。这意味着两件事第一disable_functions禁不掉它很多运维以为把eval写进禁用列表就安全了实际上对语言构造并没有效果第二eval在执行时共享当前脚本的变量范围也就是攻击者不只在字符串里“写代码”而是直接进入了这个PHP进程的角色。为什么要强调这一点因为敏感信息泄露未必是攻击者自己写一句file_get_contents那么直接他可以使用当前上下文里已经存在的变量、数据库连接对象、全局配置数组。比如脚本开头就加载了$dbConfigeval里写一句var_export($dbConfig);数据库账密直接就出来了。代码审计时如果只盯最终执行点不看执行环境里已经有哪些敏感对象就会低估危害。1.3 敏感信息一般藏在哪些位置审计过程中确认eval可被利用之后要快速评估“能读到什么”。我通常会把目标文件按危害级别分成几类文件类型常见路径潜在泄露内容应用配置config.php、config/database.php、.env数据库账号密码、缓存密码、日志等级、调试开关密钥与证书cert/private.key、config/jwt/secret签名密钥、支付回调密钥、HTTPS私钥数据库备份backup/.sql、dump/.sql全量业务数据、管理员口令Hash会话文件/tmp/sess_、runtime/session/管理员登录态、用户会话缓存与日志runtime/cache/.php、logs/.log内部接口返回、调试堆栈、部分参数这些文件不一定都在web根目录里有些部署会把配置放在项目更上层。可一旦eval能执行任意PHP代码路径遍历就只是小事readfile、include、file都可以顺着相对路径往上翻。审计验证时优先读配置文件和备份文件通常几分钟就能确认漏洞的真实危害到底有多大。2. 代码审计中定位eval的几种实用姿势2.1 正则粗扫加AST精查把eval调用点从代码堆里捞出来面对一个几十万行代码的老项目不可能一行一行肉眼找eval。最直接的粗扫方式是命令枚举比如在Linux环境里执行grep -rn \beval\s*( --include*.php 项目目录这个方式能快速得到一批文件列表但会产生两类误报一类是注释里提到eval另一类是用变量拼出来的eval_abc标识符。更严谨一些应该借助AST语法解析工具来定位真正的Expr_Eval节点然后输出文件路径和行号。实际用的时候我往往先用正则缩小范围再对结果文件用AST工具二次确认这样误报率能压到很低。如果你所在的团队没有审计工具链也可以在IDE里对“eval”做全局搜索配合函数列表看上下文。重要的是别只看eval(这种标准形态还要注意assert、create_function、preg_replace/e修饰符这些同类执行点它们的危害和eval基本等同。2.2 沿参数回溯污点判断eval内容是否真的可控定位到eval之后核心问题是“内容从哪里来”。这就要做数据流回溯我习惯用污点分析的方式把问题串起来。先简化成三段$input $_REQUEST[action]; // 入口外部可控 $code base64_decode($input); // 变换编码转换 eval($code); // 汇点代码执行在这个链路里外部数据经过base64解码后进入eval攻击者只要把自己的代码先做一次编码发送就能绕过一些简单的关键字拦截。审计时要特别留意中间层的“清洗函数”addslashes、htmlspecialchars、mysqli_real_escape_string对eval几乎没用因为它们处理的是字符串内容而eval需要的是PHP代码结构攻击者照样可以构造phpinfo();这种纯代码intval、(int)、preg_match(/^\d$/)这类约束是有效的但前提是eval拼接的业务确实只允许数字场景黑名单过滤file_get_contents之类关键字时攻击者可以用字符串拼接、十六进制转义、注释符拆分等方法绕过。所以判断“是否可控”时我会把入口到eval之间的每一层变换都列出来评估它到底是“约束了代码语义”还是“只做了表面转义”。前者可能还能接受后者基本就等于裸奔。2.3 和文件操作函数做交叉关联评估泄露影响面eval只是执行点真正把敏感信息带出去的还是文件读取或网络请求。审计时我会把eval当前文件里的函数调用关系画一遍重点关注有没有现成的“可利用操作器”。也就是看eval所在作用域里有没有已经定义好的、能够接收路径参数并输出文件内容的函数。举个例子项目里可能本来就有这样一个函数用于后端模板读取function render_template($path){ echo file_get_contents($path); }如果eval执行时能够调用render_template()那攻击者不需要自己写完整的文件读取语句只需写一行render_template(../config.php);就完成了信息泄露。这个交叉关联视角在审计里非常重要因为很多开发者会认为“eval没有直接调用危险函数就不严重”实际上代码上下文里到处是顺手可用的杠杆。另外还要看eval执行点是否在循环或异步任务里那意味着攻击者可以在一次请求里读取多个文件把整个应用目录结构翻一遍。即使最终没有拿到WebShell光是这份目录和配置泄露就足够支撑后续定向攻击了。3. 从执行点到数据离场的利用链怎么看3.1 验证传一段读文件的代码确认危害验证eval敏感信息泄露不需要一开始就构造很复杂的payload。我会先读取一个必然存在且能证实“代码已执行”的文件比如应用的配置文件echo file_get_contents(../../config/database.php);在授权环境下把这个文件内容作为请求值提交然后观察响应里是否出现了配置内容。如果没有回显还可以尝试把内容写进日志、写入临时文件再用另外的接口读取甚至用include引入目标文件利用PHP语法错误信息把部分内容带出来。要提醒的是前端页面对输入框往往有长度限制、编码转换或富文本过滤器这些都会干扰测试。遇到这种情况我建议直接走接口层把请求体内容设置为原始字符串并且关闭前端的自动转码功能。数据发送过程中如果做了URL编码先用日志确认服务端拿到的是解码前还是解码后的内容避免误判漏洞不存在。3.2 沿着上传点做组合提升影响敏感信息泄露很少孤立存在它经常会和其他功能组合成更大的风险。eval能执行任意PHP代码本质上已经具备写入能力常见组合是先通过eval读取目录结构或日志文件找到web可写目录然后利用eval执行文件写入把一段持久化后门落到可写目录再请求落地的文件建立稳定访问通道。这套路径里信息泄露是第一步也是帮助攻击者判断“写哪里不会被发现”的关键。审计时评估组合风险不能只看eval一个点要问三个问题当前应用有没有文件上传或写入接口web目录是否对PHP进程可写eval作用域里有没有可用的文件操作函数三个问题里有肯定的答案漏洞评级就至少要往高危走。反之如果php进程被限制在只读目录、可写目录不允许执行PHP那么单纯的信息泄露影响范围会小很多。3.3 泄露出的数据能干什么决定了漏洞评级同样是敏感信息泄露泄露内容不同危害完全不一样。数据库口令泄露意味着攻击者可以直接连库拖取完整业务数据调试开关为true意味着可能会暴露堆栈和内部路径第三方接口密钥泄露意味着攻击者可以伪造请求以应用身份调用支付、短信等服务。审计报告里对漏洞定级通常就是看这些数据离核心资产有多近。我还习惯把泄露数据分两类一类是直接能用的比如账密、Token、私钥另一类是间接帮助的比如目录结构、组件版本、内网地址。后者单独看危害不高但如果配合某个已知框架漏洞就能把攻击链往前走一大步。所以不要因为在验证阶段只读到了日志、没拿到数据库密码就降低漏洞等级信息泄露的评估要结合整个系统的暴露面来定。4. 修复和加固不能只盯着eval4.1 代码级修复消除动态执行用映射表代替字符串代码最彻底的修复方式是让“动态执行”彻底消失。很多业务功能看起来一定要eval其实是在用执行代码来替代本应枚举的分支逻辑。以动态调用方法为例与其写$func $_POST[method]; eval($result . $func . (););不如用映射表$action $_POST[action] ?? default; $handlers [ default handleDefault, detail handleDetail, export handleExport, ]; if (isset($handlers[$action])) { $result call_user_func($handlers[$action]); }这样一来外部输入的取值只能落在固定的三个键里即使攻击者传入execute、file_get_contents也匹配不到任何方法名。对于计算公式类场景建议引入成熟的表达式解析器或公式库把自由度限制在表达式语法范围内而不是让任意PHP代码进入执行环境。修复之后还要把旧数据里以“代码片段”形式存储的那部分内容做迁移或清理否则历史库里还会有可执行字符串。4.2 如果不得不保留eval如何做输入和沙箱约束有些遗留系统短期内无法大改那只能在eval入口前做约束。首先是格式白名单如果业务只允许数字和运算符就用正则把内容严格锁定到字符集如果允许调用函数就限定函数名和参数长度。这部分要做得很细因为一旦字符集允许字母和括号就等于允许调用任意函数。更接近安全的方案是在eval之前用token_get_all解析代码结构拒绝包含文件操作、命令执行、网络请求这类危险节点。这个方法能防住一部分直接利用但并不能完全避免绕过因为PHP语法灵活性很高字符拼接、匿名函数、反射调用都可以把代码拆散。所以我一般会把这种方案作为临时加固目标仍然是后续改造消灭eval。如果运行环境允许尽量给PHP进程开启目录限制能力让它只能访问业务需要的目录。这个限制对eval读取配置内容会产生直接阻断效果前提是配置文件不在可访问范围之内。4.3 加固运行环境让信息就算被读到也传不动即使eval暂时除不掉也可以在系统层面把伤害压到最低。我常用的几个加固项包括配置文件移出web根目录数据库账密从配置中心或环境变量读取为应用设置最小可用PHP进程权限明确哪些目录可读、哪些目录绝对禁止触碰文件系统层面收紧权限日志和备份文件不要放在应用代码同一层级数据库账号按最小权限分配生产环境不用root级账号连接避免一次泄露导致全库可拖严格限制出网和网络隔离即使拿到内网地址也没有跳板可用。这些措施的本质是纵深防御攻击者通过eval已经获得了代码执行能力我们不能阻止他执行但可以让他执行之后发现“读不到想要的、传不出去”。实际加固时一边要做到目录分级一边要验证业务功能仍然正常。尤其open_basedir配置如果限制太严图片上传、日志写入都可能会出问题需要逐项测试。4.4 检测响应不把全部希望放在修复上修复代码需要周期攻击探测却可能随时发生。这段时间里要有监测手段兜底。常见做法是给关键应用加运行期告警当eval参数长度异常、包含文件读取函数特征或者连续出现多个请求时就把请求Log下来并通知负责人。很多规则引擎都抽象了“代码执行函数敏感文件访问”的模式可以用来做实战监测。代码仓库层面可以把危险函数扫描放进发布流水线用正则或AST工具自动检查每次提交一旦出现eval、assert、system、exec、文件上传后的代码落盘直接阻塞合并。开发同学看到报错后必须写清楚使用原因再由安全负责人人工确认。这个门禁看起来多了一道工序但能避免很多人为遗漏。5. 实战排查中的踩坑与确认清单5.1 一个模拟项目排查的完整流程我去年评估过一个后台带模板配置功能的系统功能大概是根据不同渠道动态加载不同页面模板。后端一个文件里写了类似这样的逻辑$tplCode get_config(template_logic); eval($tplCode);从代码静态看get_config()读的是数据库配置外部用户碰不到所以我们起初判断“虽然危险但利用条件苛刻”。后来继续追数据流发现后台有一个配置导入功能允许管理员通过上传JSON文件批量更新配置项而导入功能没有校验字段白名单。这就形成了一条完整链路上传恶意JSON - 写入template_logic字段 - eval处执行。复现的时候我用导入功能写入了一段读取环境变量和部署路径的代码然后请求触发页面配置信息直接在错误信息里回显。修复方案分了两步第一步去掉配置里的template_logic字段把模板选择改成固定枚举值第二步清理历史数据里所有被写入的代码片段。整个过程最大的教训是一个看似来自数据库、来自后台的输入如果上游存在导入或同步机制依然可能被间接污染审计不能只看eval的直接上层。5.2 容易漏掉的变异写法排查eval时最怕盯着标准写法不放漏掉各种变形。比如拼接写法$reader file._get_contents; eval(echo .$reader.(config.php););这类代码在编辑器里看到的是变量赋值加eval直接用关键字搜索file_get_contents不一定能命中。再比如经过多重编码的处理先用gzdecode解压、再用base64_decode还原最后才进入eval。审计时如果只看最终eval参数的原文会误判为“加密字符串不可控”。还有一类变异是把执行能力藏在别的结构里assert($code)在部分PHP版本中可以执行表达式preg_replace加/e修饰符能导致代码执行call_user_func配合外部传入函数名再加上可控参数也是半个执行点。处理这些变体时我建议把“能执行代码”和“能读取文件”两类能力分开画再找交叉点比单纯搜eval更不容易漏。5.3 修完以后别急着上线回验清单一次修复是否有效不能靠“把eval删了”就下结论。我习惯按下面的清单过一遍确认安全性和业务可用性都满足用原始攻击路径重新提交同一payload确认响应不再包含敏感数据对同一入口做大小写混写、编码绕过、注释拆分的变体测试确认加固规则没有明显漏口检查代码库里是否还有第二个eval、第三个动态执行点同类问题往往不止一处回归测试业务功能特别是模板解析、配置读取相关页面确认替代方案行为与旧逻辑一致清理历史数据里的恶意代码片段、临时文件或者备份文件避免复活更新修复说明和审计报告记录漏洞时间线、利用条件和验证结果。回验过程中如果发现绕过不要急着把正则规则调严先回头看业务本身能不能去掉动态执行这个设计。规则补丁只会让猫鼠游戏越来越复杂源头上的简化才是长期有效的。最后分享一个屡试不爽的小习惯每次修完像eval这种危险执行点我会回头把同一个方法里的函数列表重新看一遍把file_get_contents、file_put_contents、include这些常跟eval搭伙的函数在脑海中连成一条线。很多高危漏洞的形态虽然千变万化但底层逻辑无非是可控输入穿过了执行或读取的节点只是换了个样子。把这条经验放进日常评审里比临时翻手册要有用得多。