☰
PHP代码审计入门:用Seay源代码审计系统快速定位危险函数
2026/10/18 8:44:16 网站建设 项目流程

简介: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时误报没清理干净、被同事拿着列表一条条反问之后,我每次代码审计收尾都强制走这三件事:先清理误报,再导出结构化清单,最后在每条记录上写明结论。这个习惯慢慢地比任何工具都可靠。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询