简介:Seay源代码审计系统是一款基于C#开发、主要运行于Windows平台的PHP代码安全审计工具,面向网络安全初学者、代码审计人员及AWD参赛选手,用于快速发现SQL注入、命令执行、文件包含、文件上传、XSS、信息泄露与任意URL跳转等常见漏洞。压缩包共30个文件,以11个dll、6个exe等程序组件为主,辅以rar、zip源码与资料包,以及ini、md、php、html等配置和说明文件,整体约42.16MB,解压后可直接部署使用。目前已有805人学习下载,适合搭建本地审计环境。其价值在于支持一键审计、函数定位、插件扩展、自定义规则、代码高亮、编码转换和数据库执行监控等功能,并附带插件示例与2.1版本源码资料,便于理解工具机制、练习漏洞定位和复盘AWD审计思路。
1. 代码审计入门的第一把尺:Seay 能替你看什么
很多做 AWD 的团队在拿到源码后的前十分钟,真正卡住的往往不是漏洞会不会挖,而是“从哪个文件下手”。一个压缩包解出来动辄几千个 PHP 文件,靠grep一页页翻,翻到第三个目录就精神涣散了。Seay 源代码审计系统解决的正是这个阶段的问题:它是一个运行在本地的代码审计工具,把源码目录一键导入,自动做危险函数定位、变量覆盖排查、敏感配置提取,并把可疑文件按风险归类成一张清单。你不需要先把整份代码读懂,先看它给出的可疑点,再回头对着源码确认,这个流程对刚接触代码审计、正在准备 AWD 的新手特别顺。
它适合三类人:一是想入门代码审计但不知道从哪切进去的开发;二是打 AWD 需要赛前快速摸一遍靶场源码的选手;三是接手了历史遗留 PHP 项目、想快速了解哪里存在高危写法的维护者。它不做的事情也很明确——它不是 SAST 平台,不替代人工验证,很多结果需要你看源码二次判断。把它当成“可疑点的筛子”,而不是“漏洞的判决书”,心态就对了。
2. 环境与上手:从解压到跑出第一份审计清单
2.1 运行环境与启动方式
Seay 是桌面端的 Windows 应用,不需要装 Web 容器,也不需要配数据库,解压后双击主程序即可。这个设计在当年的审计工具里算很省事的:不用先折腾 PHP 环境、不用起 Apache,省掉的这部分时间正好用来读代码。实际使用中我一般把它放在一台干净的 Windows 机器或虚拟机里跑,原因后面避坑章节会细说。
启动后的界面大致分三块:左侧是项目与文件树,中间是代码查看区,下侧或右侧是审计结果列表。第一次打开是空的,需要先建项目。
用命令行解压和校验压缩包的完整性是个好习惯,尤其从网盘拖下来的时候:
# 校验下载到的包是否完整,先看文件大小和类型 file SeaySourceAudit.zip unzip -l SeaySourceAudit.zip | head -30 # 解压到独立目录,避免和系统盘其他工具混在一起 unzip -q SeaySourceAudit.zip -d ./seay_toolfile用来确认是不是真的 zip 而不是被加了壳的可执行文件;unzip -l只列目录不解压,先看清楚里面有没有可疑的附加文件;-d指定解压目录,让工具和被测代码分开放在不同路径下,这是后面做隔离的基础。
2.2 新建项目并导入源码
进来第一件事是新建项目。菜单里选新建,填项目名,选中被测源码的根目录。这里有个细节:一定要选源码根目录,而不是它的上一层或子目录。选错层级会直接影响后续的文件树展示和函数调用链还原。
导入时通常有几个可选项,我一般这样勾:
| 选项 | 建议 | 原因 |
|---|---|---|
| 文件类型过滤 | 只保留 php、inc、phtml 等 | 减少图片、字体等无关文件干扰检索 |
| 自动备份 | 关闭 | 审计是只读操作,不需要改动源码 |
| 递归子目录 | 开启 | 否则只扫根目录一层,会漏掉大半入口文件 |
| 索引缓存 | 开启 | 二次打开同一项目时省掉重复解析时间 |
导入完成后,左侧文件树会按目录展开。工具会先建一份索引,文件量大时这一步要等一会儿,别以为卡死了,去看任务栏的进度提示。
2.3 用危险函数规则跑第一轮扫描
导入完成就直奔核心功能:危险函数扫描。它的原理是把源码里的函数调用和内置规则表做匹配,命中的位置在结果区列出来,点击可以直接跳到对应代码行。
<?php // 一段典型的、会被规则命中的代码片段,用来理解工具在找什么 $cmd = $_GET['cmd']; // 外部可控输入 system($cmd); // 危险函数:命令执行 $file = $_POST['file']; include($file); // 危险函数:文件包含 $sql = "select * from t where id=" . $_GET['id']; mysql_query($sql); // 危险函数:SQL 拼接工具不会替你做数据流分析到每一层,它的匹配逻辑接近“函数名 + 参数形态”的组合判定。上面三段会被分别标记为命令执行、文件包含、SQL 注入风险。看到结果之后的关键动作是逐个回源码确认:入参是否真的来自外部、中间有没有过滤函数、过滤是否可绕过。这一步没有捷径,工具的命中是起点不是结论。
规则表本身是可维护的,一般放在工具的规则目录下,按函数名或正则组织。想加公司内部框架的自定义危险函数(比如某个自己封装过的执行方法),往里追加一条就行,重启生效。
2.4 变量覆盖与敏感信息提取
除了危险函数,Seay 还有两块很实用的功能。一块是变量覆盖检测,专门找extract()、parse_str()、$$var这类可以把外部输入写成同名变量的写法——在 PHP 里这类代码极容易造成绕过,人工翻的时候眼睛容易滑过去。
<?php // 变量覆盖的典型形态 foreach ($_GET as $k => $v) { $$k = $v; // 动态变量名,覆盖作用域内已有变量 } extract($_POST); // 直接把表单字段铺成变量 parse_str($_SERVER['QUERY_STRING']); // 不传第二参数,同样铺变量这三行的共同点是“变量名由外部决定”。工具会把它们单独归类,因为这类问题不像命令执行那样一眼看到system,但危害一点不小,常常能绕过前置的鉴权判断。
另一块是敏感配置提取,扫配置文件里的数据库账号、密钥、调试开关之类。这在 AWD 赛前尤其省事:靶场源码里经常留着默认管理员密码或者调试模式没关,直接看结果列表比翻config.php快得多。
3. 核心模块拆解:规则引擎、解析器与结果列表怎么配合
3.1 规则引擎:从函数名匹配到正则匹配
理解规则引擎的分层,决定了你后面能不能自己加规则。它的匹配大致分两层:第一层是函数名精确匹配,速度快、误报低,适合system、eval、assert这类确定的高危函数;第二层是正则匹配,用于捕捉参数形态特征,比如“include后面跟的不是字符串字面量”这种更细的条件。
打个比方,第一层像海关的黑名单,名字对上就拦;第二层像行为识别,看的是形态。两层配合能覆盖大部分常见写法,但也意味着层与层之间没有强制关联——它不会自动判断“这个$_GET是否流到了这个system”。这个数据流追踪的活儿,得你自己做。
自定义规则一般长这样,写在一个配置或规则文件里:
// 规则条目示意:函数名 + 危险等级 + 说明 [ 'name' => 'dangerous_call', 'pattern' => '/\b(system|exec|shell_exec|passthru|popen)\s*\(/i', 'level' => 'high', 'desc' => '命令执行类函数调用,需确认参数是否外部可控' ] // 正则型规则:匹配未加引号的 include 参数 [ 'name' => 'dynamic_include', 'pattern' => '/\b(include|require)(_once)?\s*\(\s*\$[a-zA-Z_]/i', 'level' => 'medium', 'desc' => '包含参数为变量,检查是否可控且未做白名单校验' ]pattern是核心,写正则时把\b边界带上,能显著减少误命中;level决定结果列表里的排序和颜色,方便你先看高危;desc建议写清“要确认什么”,因为过几周再回来看,没说明的规则你自己都不敢信。加规则之后一定要拿几个已知的安全写法测一遍,确认不会大面积误报再全量跑。
3.2 源码解析:文件树与语法层面的轻量处理
Seay 的另一块骨架是解析器,负责把源码读进来、切出 token、还原文件之间的引用关系。它做的是轻量语法处理,不会构建完整的 AST 调用图,但对审计来说够用:能识别函数定义位置、能定位变量在文件内的出现行、能把include的目标文件关联出来。
这个能力直接体现在两个交互上。一是点击函数名可以跳到定义处,翻大型项目时不用手搜;二是文件树里能按目录结构看谁被谁引用。我一般先看入口文件(index.php、admin/、api/这类),因为外部流量总是从这里进来,从入口往下的调用链才是攻击面最集中的地方。
需要提醒的是解析器的边界:它对动态拼接的文件名、vendor里的第三方库、被混淆过的代码基本无能为力。遇到这类文件,工具给出的结果会特别少,这时候别以为是安全的,往往说明它没看懂。
3.3 结果列表:怎么读才能不当误报收割机
结果列表是整个工具的出口,也是新手最容易翻车的地方——把所有命中当成漏洞,逐条写报告,最后发现一半是误报。正确读法是三档过滤:
第一档:高危 + 参数明确来自 $_GET/$_POST/$_REQUEST → 立即人工确认 第二档:高危 + 参数是变量但来源未明 → 往上追两层赋值 第三档:中危 + 参数为字面量或来自配置 → 记下,低优先级第一档通常能在几分钟内就能定位到真问题,这也是它作为 AWD 赛前工具的价值——比赛前时间紧,你需要的是快速缩小范围,而不是完整覆盖。第三档不是不重要,而是投入产出比在赛前不划算,赛后复盘时再补。
3.4 一份可复用的审计流程
把上面的模块串起来,我通常按这个顺序走,一个中型项目大概两小时内能出第一版结论:
# 1. 先确认源码指纹,判断框架类型(框架决定审计重点) grep -rl "ThinkPHP\|Laravel\|CodeIgniter" ./src --include="*.php" | head -5 # 2. 找入口文件,确定外部可访问的路径 find ./src -maxdepth 2 -name "index.php" -o -name "api.php" | head -20 # 3. 把高危函数全量列出来,和工具结果互相印证 grep -rn "system\|eval\|assert\|include(\$\|require(\$" ./src --include="*.php" | wc -l第一步用框架特征定位技术栈,ThinkPHP 和原生代码的审计重点完全不同;第二步收敛攻击面,只访问不到的代码优先级低;第三步用命令行做交叉验证,工具漏掉的地方命令行有时能补上,两边结果对不上的地方恰好是最值得看的。这套动作跑熟之后,再打开 Seay 导入,你对结果的判断会快很多。
4. 避坑与排查:五条真实踩过的坑
4.1 导入后文件树是空的
现象:选了目录,进度条走完,左侧树里什么都没有。原因多半是选了源码的上层目录,工具按扩展名过滤后没匹配到任何 PHP 文件,或者路径里带了中文和空格导致解析异常。解决:确认选中的目录下直接就有.php文件,用ls看一眼;路径改成纯英文、无空格再试一次。
4.2 扫描结果少得离谱
现象:一个几万行的项目只命中十几条。原因有两个常见来源:一是过滤规则里排除了vendor和inc目录,而项目的核心逻辑正好写在这两类目录里;二是源码用了大量动态调用(call_user_func、变量函数$fn()),规则表匹配不到。解决:临时放开过滤目录重扫一次,同时用grep -rn "call_user_func"手动补一遍动态调用点。
4.3 中文注释导致结果行号偏移
现象:点击结果跳到的行和实际代码对不上,差了几行。原因是文件编码是 GBK,工具按 UTF-8 读,中文注释被拆成多字节后行计数出错。解决:先把源码统一转成 UTF-8(iconv -f gbk -t utf-8),再重新导入。这个坑在对老项目时特别常见。
4.4 工具被杀软拦截或提示风险
现象:解压后主程序打不开,或者被安全软件直接隔离。原因是这类审计工具的二进制文件没有签名,行为特征又容易被误判。解决:在隔离环境里给它加白名单,别在主力办公机上跑;同时确认下载来源,只从可信渠道取包。
提示:审计工具本身接触的是别人的源码,跑在隔离的虚拟机里比跑在开发机上稳妥得多,快照也能随时回滚。
4.5 把工具结果直接当漏洞报告
现象:交上去的清单被逐条打回,说大部分不可利用。原因是工具只做模式匹配,不判断可达性和过滤有效性。解决:每条结果都回源码确认三件事——参数是否外部可达、中间有无过滤、过滤能否绕过。确认不了的标为“待验证”,不要写成“确认漏洞”。这条是血泪经验,报告的可信度比数量重要得多。
5. 二次开发与规则扩展:让 Seay 适配你自己的代码库
工具自带规则覆盖的是通用写法,真正落到具体项目上,一定要做扩展,否则你会一直被大量误报和漏报夹击。我在改造上的做法分三步,优先级从高到低。
第一步是补自定义危险函数。内部框架通常会把执行、查询、文件操作封装一层,比如Db::query()、Util::run()。这类封装在通用规则里根本不在名单上,但危险程度和原生函数一样。做法是把这些方法名加进规则表,正则写成能匹配静态调用和实例调用两种形态:
// 覆盖静态调用 Db::query(...) 与实例调用 $db->query(...) [ 'name' => 'inner_db_query', 'pattern' => '/(?:[A-Za-z_\\\\]+::|\$[a-zA-Z_][a-zA-Z0-9_]*\s*->)\s*query\s*\(/i', 'level' => 'high', 'desc' => '内部数据库封装,检查 SQL 是否拼接了外部输入' ](?:...)是非捕获分组,用来把两种调用形态并列;\\\\是为了匹配命名空间里的反斜杠。写完先在一份已知安全的代码上跑,误报率高就收紧正则,别急着全量。
第二步是给结果列表加自己的优先级维度。通用规则只有高中低三档,但实际项目里“未授权访问”比“命令执行”更值得先看,因为它不需要构造复杂 payload。我的做法是给规则加一个自定义标签,比如auth_bypass、file_read,结果区按标签再筛一遍,先看未授权和数据读取类,再看执行类。这一步带来的效率提升,比多加一百条规则都明显。
第三步是固化一套验证脚本。规则改完不能只靠肉眼看,我一般写个小脚本做回归:
| 验证项 | 方法 | 通过标准 |
|---|---|---|
| 已知漏洞样本 | 放几个故意写坏的 PHP 文件进测试目录 | 全部被命中 |
| 已知安全样本 | 放参数化查询、白名单包含的写法 | 命中数为 0 |
| 编码兼容 | 同一份代码分别用 UTF-8 和 GBK 存一遍 | 结果一致、行号正确 |
| 大文件性能 | 塞一个 5000 行的单文件 | 扫描不卡死、耗时在可接受范围 |
# 回归脚本的核心逻辑:对比两次扫描结果数量 scan() { ./seay_cli --project "$1" --rule ./rules/custom.rule --output out.txt; } scan ./samples/vuln && grep -c "high" out.txt # 期望 > 0 scan ./samples/safe && grep -c "high" out.txt # 期望 = 0--rule指向自定义规则文件,--output把结果落到文本便于比对。这里要注意:命令行接口在不同版本上可能不存在或参数名不同,先确认你的版本支持哪些参数,别照抄。没有命令行接口的话,就退化成手动导入两个样本目录做对比,结论一样成立。
从那以后我每次动规则表,都会先把上面这四个验证项走一遍再提交,因为规则这东西改一处很容易影响全局,没回归的话过两周你都不知道误报是从哪条规则冒出来的。希望这套流程能帮你在自己的项目上把工具用顺。
本文还有配套的精品资源,点击获取