首页 / 资讯中心 / 文章详情

PHP代码审计入门:用Seay源代码审计系统快速定位危险函数

PHP代码审计入门:用Seay源代码审计系统快速定位危险函数 ★ FEATURED ARTICLE
简介Seay源代码审计系统是一套面向开发者和安全工程师的自动化代码审计工具旨在帮助用户快速定位PHP等源码中的安全漏洞与隐患。该压缩包共包含25个文件以exe主程序、dll依赖库、bin规则与配置、php示例文件等为主另有报告模板与调试记录整体大小约14.06MB解压即可部署使用。已有970人学习下载。包内集成一键自动审计、函数查询、代码高亮编辑器、自定义审计规则、代码调试与报告生成等核心功能适合Web安全测试、代码审查及安全培训场景。借助内置规则库和插件可对项目进行深度扫描并将问题定位到具体代码行结合调试工具与详细报告能有效提升漏洞发现和修复效率是开展PHP代码安全审计的实用工具。1. Seay源代码审计系统PHP代码审计老工具为什么还在用接手一套别人写的PHP系统时最怕的不是功能看不懂而是藏在几千个文件里的危险函数你来不及一个个翻。Seay源代码审计系统就是为这个场景准备的把源码目录丢进去它按正则规则把eval、include、mysql_query这类高危调用全部捞出来按文件、函数、行号排列好剩下的事才是人工判断这条路径是否真正可控。这套工具在代码审计圈子里属于“老古董”但它对PHP项目的集中扫描能力至今没有多少轻量替代品能把步骤压缩到这个程度。适合的人很明确刚接手遗留代码的程序员、做二次开发前需要安全摸底的人以及常年跟漏洞列表打交道的测试人员。它不负责替你思考漏洞是否成立它负责替你省掉“从几千个文件中找出可疑点”的那几个小时。2. 跑通环境是第一步依赖、源码目录与首次索引2.1 先确认它是Windows下的白盒审计工具很多第一次拿到这个压缩包的人会以为它需要装PHP环境才能跑实际上这是个理解偏差。它不执行你的业务代码只做静态文本扫描因此核心依赖不在PHP而在.NET框架。解压rar后直接运行主程序如果系统缺少对应版本的.NET依赖程序会直接报错或者闪退连界面都看不到。我一般先确认两件事操作系统是不是Windows.NET环境是否完整。你可以打开“运行”输入winver看一下系统版本再在控制面板的“程序和功能”里翻一下已安装的.NET项这一步熟练的话三十秒就够。环境确认清单可以做成一张表方便对照检查检查项要求失败时的表现操作系统Windows 7及以上运行无反应或提示兼容性问题.NET环境常见3.5和4.x组合程序闪退日志无记录源码目录本地可读路径无法加载项目或扫描列表为空源码格式PHP/JS/HTML等文本文件二进制文件被跳过不算错源码目录不一定非得在开发机上压缩包拷贝到一台干净的Windows机器上也能工作只要目录可读就能建立索引。这一点在实际工作中很管用客户给一个打包好的源码zip我通常本地解压后直接用这个工具扫不搭任何运行环境。如果扫描时提示某个文件无法访问十有八九是路径权限问题右键以管理员身份运行程序再试一次这类问题基本就消失了。2.2 新建审计项目与索引动作首次打开界面会看到一个项目列表区域默认是空的。我的习惯是先点新建项目给这次审计起一个能区分批次的名字比如“某跨平台系统客户端代码审计”然后选择源码根目录。这里有个容易忽略的细节根目录选择要落在最外层也就是包含index.php、config、application这些内容的顶级目录而不是某个子模块目录。选太深会导致扫描范围缺漏选成父级目录又会出现vendor那类第三方依赖里的误报堆成山后续筛选反而浪费时间。选定目录后程序会建立文件与函数位置的索引。这个索引是扫描动作的基础它先读取目录下的文件类型再记录每个文件里符合规则的特征位置最后呈现到结果列表中。我实际操作时的顺序大致是这样# 在源码根目录先确认文件结构再启动软件建立审计项目 find ./ -type f -name *.php | wc -l # 统计PHP文件数量便于判断索引耗时数量上千时建议分批扫描统计文件数量这一步不是必须的但对大项目有预判作用。索引期间界面可能短暂无响应这不代表卡死只是在读取文件树。等待索引完成后打开“扫描”相关按钮程序会用当前匹配规则跑一遍全目录结果区会按函数名分组展示命中记录。首次使用我建议保持默认规则先扫一轮不要一上来就添加自定义规则因为默认规则能覆盖大部分常见高危函数后续再按项目特性缩窄范围更高效。2.3 第一次扫描与结果列表三处重点扫描完成后结果列表一般包含三列核心信息命中的函数或表达式、所在文件路径、行号。这三项信息构成一次告警的基本要素也是后续人工验证的起点。很多人只盯着函数名看只看eval、include却忽略了行号列导致复核时翻文件效率极低。正确做法是双击一条结果程序会跳到对应文件的对应行直接在上下文里看这个函数是否接收外部输入。还有一个常被忽略的功能是扫描文件类型配置。默认情况下它可能会把JS、HTML也纳入正则匹配导致某些JS中字符串命中也显示为风险。实际审计时我一般把范围限定为PHP相关后缀只保留php、inc、phtml这类与服务端逻辑相关的类型。这样能减少噪声也让结果更贴近真实漏洞分布。文件类型过滤配置保存后会一直生效下次新建项目时也沿用同一套选择属于那种设一次就长期受益的参数。首次扫描的目的不是立刻找到漏洞而是摸清这个项目的危险面在哪里。结果数量多不代表漏洞多可能只是这个项目用了大量file_put_contents写日志这是正常操作结果数量少也不代表绝对安全因为正则规则识别不了逻辑漏洞。理解了这一层后面的复核工作才有重心。3. 从危险函数到有数据流的告警正则规则与自定义特征3.1 默认规则到底能帮你筛出什么这个工具内置的检测规则本质上是一组正则表达式匹配的是危险函数调用和超全局变量使用点。放到真实场景里它们对应的是SQL注入、命令执行、文件操作、代码执行这些常见漏洞类型的入口点。比如PHP项目里mysql_query($sql)拼接用户输入最终导致注入工具做的是先把这个函数从几千个文件里拎出来让你看见“这里发生了数据库查询”而不是直接告诉你“这里可注入”。理解这层差异很重要它决定了你后续复核时的工作方式。默认规则覆盖的高危函数大致有这些风险类型常见匹配点典型函数名SQL注入数据库查询拼接mysql_query、mysqli_query、PDO::query代码执行动态执行字符串eval、assert、call_user_func文件包含变量拼接路径include、require、include_once文件操作写入/删除文件file_put_contents、fwrite、unlink命令执行系统命令调用system、exec、shell_exec这张表应该被当作记忆索引而不是全部规则因为默认规则远不止这五类。初次拿到陌生项目时我习惯先看结果列表里哪一类函数出现最频繁、分布最广再决定先深入哪条链。比如CMS类项目通常include和file_put_contents命中多接口类项目则mysqli_query占大头。优先处理最密集的类型一次修一类复核效率会明显提升。3.2 新增一条自定义规则的做法项目中有时候会出现一些业务自封装的函数比如项目里全用DB::select那么内置规则是不认识它的。这时候需要手动加一条特征规则把“调用DB类的select方法”的模式匹配出来。正则表达式可以写成这样DB\s*::\s*select\s*\(这条正则干了这么几件事先匹配字面量DB然后用\s*允许类名与双冒号之间有空白::是PHP的静态调用分隔符select为方法名\(表示匹配函数左括号。核心逻辑是“定位所有静态调用select方法的位置”至于生成SQL时是否拼接了变量那是下一步人工确认的事。加规则时我一般先选忽略大小写模式毕竟代码规范不严的项目里db::SELECT这种写法并不罕见。更复杂的场景可能是函数命名带动态前缀比如同一套逻辑同时存在insertData和updateData。这时可以加一条分组正则匹配多个方法名::\s*(?:insertData|updateData)\s*\(这里?:表示非捕获分组两个方法名之间用|分隔整个模式展开说明就是“匹配双冒号后跟insertData或updateData再接一个左括号”。这类规则的适用性不局限于当前项目往后审计其他系统时如果遇到同风格的封装也能复用建议按业务方法名整理一套属于你自己的常用规则库。3.3 给规则做分层先粗扫再精排自定义规则加多了以后命中数会直线上升这时候需要建立分层思路。我一般把规则分成三层第一层是全量粗扫用内置默认规则跑出所有危险函数第二层加业务规则把自定义封装方法纳入范围这个阶段命中数最多第三层开始做减法逐个确认后把误报记录标记为已处理或者临时禁用某些规则。分层的目的不是精确而是控制工作节奏。禁用规则不等于删除规则程序里通常有启用开关。把一个规则关闭后重新扫描时对应函数就不再显示这适合那些“本项目中确定不需要关注”的函数。比如项目完全不涉及文件上传功能那么把move_uploaded_file的规则临时关掉结果列表会清爽很多。要注意的是禁用的规则会影响下一次扫描想恢复就在规则列表里重新勾选别真把它删了。4. 审计误报避坑五种翻车现象与一眼识破的方法4.1 误报源头多样先学会分类在做一个模拟项目X的代码审计时扫描结果里出现了三百多条eval命中费了一整个下午逐条复核最后真正由用户输入流到达eval的可能只有五条。这个比例不夸张正则扫描的误报率就是居高不下。翻车翻多了之后我把常见误报归成五类每一类的特征都有迹可循这里按出现频率排序记录如下。第一类注释文本中的函数名。很多开发者会写// 修复eval注入问题这样一行注释正则匹配时不会区分注释代码与业务代码于是这条注释也成了待审计记录。现象就是结果列表里函数名在注释行肉眼一看上下文全是说明性文字。解决方式是在复核时用编辑器过滤掉行首带//、#或注释块内的记录不需要修改平台行为。第二类字符串里的函数名。日志类代码经常出现error_log(call eval failed)或者数组里存了array(eval 1)这类字符串中的函数名并不代表实际调用。原因很简单正则在匹配时没有上下文判断能力看到文本就命中。解决方法是复核时优先看函数名后面是否直接跟左括号如果跟的是字符串结束符或者数组赋值符号基本就是误报。第三类变量名包含关键字的干扰。比如变量名$eval_id命中eval规则实际只是业务里的一个普通变量。这种误报在命名不规范的项目里特别多。解决方式是我在自定义规则里给函数名加上词边界用正则的\b限定单词边界从根源减少无意义命中。第四类同志愿者的引用。项目里多处require config.php这类固定路径包含会每次扫描都出现但它是正常项目骨架。原因是规则匹配到include/require关键字时不关心参数是常量还是变量。解决方式是先把文件路径为固定值的记录统一复核一次确认不是变量拼接后直接标记为已处理。第五类重复扫描导致的堆积记录。同一个项目扫描多次结果列表里会累积多次记录新旧混杂使复核结果无法准确核对。原因是工具可能在重复扫描时不做去重。解决方式是在二次扫描前清空上一次的结果记录或者单独创建新的审计项目存档保持每次扫描的独立性。4.2 批量复核四步流程面对几百条扫描结果时逐条点开文件复核的效率其实很低。我后来把流程固定成四步每步的处理量都做了划分。先把所有结果按函数名排序同一函数名的记录归到一起处理一个函数类型时上下文保持一致不需要频繁切换思维然后浏览文件路径过滤掉vendor、lib这类第三方库目录这些目录里的命中与项目自身代码风险无关。如果工具结果列表支持按路径排序这一步会非常直观。接着逐条双击记录只核对函数名前后十行代码判断是否存在变量拼接。这一步是要点中的要点复核目的不是读懂整个文件而是看入口是否可控。最后把确认过的记录在界面上标记为已处理未处理的自然成为下一步动态调试的目标。这个流程走完一遍一个上万行代码的项目通常只需要半天多。相比最早拿着结果列表从头点到尾的方式节约了大约一半时间。而且因为每类误报都有了明确的处置方式重新翻查时也不会遗漏真正可疑的调用点。对熟手来说这四步几乎是肌肉记忆对新手来说最大的帮助是别在无用记录上消耗太多耐心。5. 从告警到实锤用本地环境验证数据流5.1 把源码放进本地运行环境静态扫描只能告诉你“这里有一个危险函数”但它无法说明这个函数接收的数据是不是用户可控。想要把一条告警从“疑似”变成“确定”我通常会把源码跑在本地集成环境里顺着入口参数实际走一遍数据流。这个过程中不修改源码只观察参数如何流转确认危险函数是否确实接收了外部输入。假设扫描结果里有一条mysqli_query命中对应的代码片段是?php // 模拟项目X中的用户登录逻辑 $username $_POST[username]; $password $_POST[password]; $sql SELECT * FROM users WHERE username $username AND password $password; mysqli_query($conn, $sql); ?先把源码放进本地集成环境用PHP内置服务器快速启动# 在项目根目录启动开发服务器端口选8080避免冲突 php -S 127.0.0.1:8080 -t ./启动后路径下如果存在入口文件访问对应地址即可触发登录逻辑。这个过程的作用是让$_POST[username]真正接收一次请求验证这个数据最终是否原样进入了mysqli_query。如果请求后数据库查询执行则说明这条告警是一条“可控执行路径”属于真实风险如果请求根本没有经过这条代码或参数在中间被强制转成了整型则这条告警可以被降级处理。5.2 追踪一次SQL注入的完整链路承接上面的代码片段用户提交一个包含单引号的用户名直接在查询语句里改变SQL结构。这里常常被忽视的关键节点是$sql变量从赋值到执行的中间没有任何过滤函数整条链路是$_POST[username] -- $sql -- mysqli_query($conn, $sql)这个链路里只需要一个addslashes($username)整条风险就会消失。而静态扫描不会替你做这层判断它会识别出mysqli_query并报告但不会分析$_POST是否安全进入SQL。所以要完成审计闭环必须回到代码本身把入口参数到危险函数的路径完整读一遍。读的时候关注三类问题参数有没有经过过滤函数比如intval、htmlspecialchars、addslashes参数有没有经过类型判断比如要求整型的地方用字符串拼接以及参数流经多个函数时有没有可能被二次解码改变原有过滤效果。这三类问题凡是有一处模糊这条告警就值得继续深入。没有环境的场景下我会用IDE的全局搜索功能把$_POST、$_GET、$_REQUEST作为前驱节点反向追踪它们流向的每一个变量。这种纯静态追踪方式虽然慢一点但验证结果同样可靠。5.3 动态调试补充静态扫描的三个经验本地环境跑起来之后不要只看页面返回结果。遇到疑似代码执行或命令执行的场景我会在危险函数调用的前一行加断点用xdebug观察传递进来的变量是否带着可控制的内容。实践中有三条经验反复被验证有效。第一var_dump和exit是最快的确认工具断点都不需要直接在函数前打印变量值一眼就能看出参数是否来自请求。打印完记得删除调试代码别把狼狈留在交付代码里。第二浏览器开发者工具的网络面板能帮你确认请求头参数是否真的传入了后端比如JSON请求里某些字段会被框架自动解析但这层解析过程在静态扫描中完全不可见。第三动态调试确认过的结果一定要在原记录上注明日期和验证状态。否则周一批复记录后周三再来看同样的告警又会产生一次重复验证浪费时间。6. 把结果整理成报告两次导出习惯与固定收尾流程复核完一轮代码误报被逐条排除、真风险被逐条确认之后还缺一个能拿走的记录。我一般会把结果整理成两类一份是给同事看的待修复问题清单一份是给自己留底的审计存板。待修复清单按文件路径排序保留函数名、行号和风险等级每行都注明是否可控这样的清单在后续开发修复时可以直接当作任务分配依据。留底则记录扫描时间、项目目录、当时使用的规则集合方便日后代码迭代后对比新旧结果。导出动作我固定有两个习惯。第一个习惯是清空“已标记为安全”的记录后再导出保证交付出去的清单只包含待办问题不给阅读者增加筛选负担。第二步是把结果列表复制到表格工具中整理成下面这样的结构文件路径行号命中函数风险等级复核结论/app/controllers/Login.php18mysqli_query高已确认可控参数来自POST/app/library/Cache.php102file_put_contents中已确认不可控路径为常量/app/helper/io.php45shell_exec高待复核疑似命令拼接这种结构化记录最大的好处是可追溯。别人拿着这份表格去代码里找对应行不会出现“你说的风险在哪”的误解。工具自身的记录如果支持保存我也会打一个项目存档包连同源码版本号、扫描时间一起放进文件夹作为这个版本的审计基线。从那次审计模拟项目X时误报没清理干净、被同事拿着列表一条条反问之后我每次代码审计收尾都强制走这三件事先清理误报再导出结构化清单最后在每条记录上写明结论。这个习惯慢慢地比任何工具都可靠。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站