EX-D:代码审计
2.1 六阶总纲与顺序铁律
结论:六阶顺序不可颠倒。每一阶的产出是下一阶的输入。跳过任一阶,结论不可信。
顺序:测绘 → 技术栈推导 → 攻击面推导 → 数据流追踪 → 成链 → 伤害定级。
| 阶 | 动作 | 产出 | 退出判据 |
|---|---|---|---|
| 1 测绘 | 列入口、列资产、列依赖、列配置 | 入口清单、资产边界表 | 能说出全部外部可达入口,无"应该走这里"的猜测 |
| 2 栈推导 | 版本落包、部署形态落文件 | 技术栈表 | 每个版本能落到具体包名与版本号 |
| 3 攻击面 | 标可控性、鉴权要求、数据去向 | 攻击面清单(排序) | 每行都有可控性与业务价值判断 |
| 4 数据流 | 脏标记传播、收敛点标注 | 传播路径图 | 每个收敛点能回溯到具体入口参数 |
| 5 成链 | 补最小增量、验证跨请求状态 | 链路草案 | 链路能过五问法前四问 |
| 6 定级 | 按整链最大可达定级 | 定级结论、修复建议 | 有证据、有范围、有整改方案 |
铁律的现实理由:先测绘再推导,避免用"猜的版本"去查组件漏洞;先攻击面再数据流,避免把三个月时间花在无外部入口的模块上;先成链再定级,避免按单点定级造成的高估或低估。
倒着走的审计,一定会变成"危险函数清单"。
2.2 第一阶 源码情报测绘:找入口,不找漏洞
结论:这一阶的任务只有一个:画出"外部请求 → 具体文件"的完整映射。
六个观察面:
目录结构。 找入口目录(controller、api、route、handler、servlet)、危险目录(upload、template、cmd、exec、export)、遗留目录(backup、old、test、demo、bak)。遗留目录是二次开发项目的高发区,接口还在,鉴权常已失效。
配置文件。 连接串、密钥、开关、白名单、上传限制、调试开关、跨域策略。配置错误常是链路第一环,比代码缺陷更容易被忽略。
路由注册。 全量导出路由表,逐条标注鉴权装饰器、注解、拦截器、白名单例外。导出方式按框架而定,Java 看注解扫描结果,PHP 看路由文件,Python 看装饰器注册与 OpenAPI 描述。
依赖清单。 优先锁文件:composer.lock、pom.xml、build.gradle、requirements.txt、poetry.lock。声明文件给范围,锁文件给事实。
非代码资产。 .git 残留、.env、备份压缩包、CI 脚本、Dockerfile、部署文档、测试用例与默认账号。这些不在代码里,但在源码包里。
日志与任务。 定时任务、消息消费、CLI 入口、健康检查、内部回调。这些入口多数无鉴权,是横向移动的稳定落点。
判据:能在纸上画出全部映射。做不到就继续测绘,不要进入下一阶。
2.3 第二阶 技术栈推导:把版本落到具体包
结论:版本必须落到包名与版本号。推不到就写"未知",不要用"大概是 PHP 7"往下推。
五个推导方向:
- 锁文件优先。声明是范围,锁是事实。同一份
composer.json在不同环境可能装出两个版本。 - 路由写法反推框架。 注解式、配置文件式、约定式,三种写法对应三代框架风格。框架确定后,参数绑定规则、默认过滤、异常处理全部可预期。
- 模板语法反推引擎。 这是模板注入类链路的前置条件。不同引擎的表达式语义不同,危害边界也不同。
- 响应特征反推部署。 响应头、错误页、Cookie 键名、默认路径,能定位容器与语言,部分框架的会话键名长期固定。
- 部署文件反推形态。 php-fpm、mod_php、gunicorn、uwsgi、JVM 单进程,五种形态的权限天花板完全不同。
版本与形态一旦确定,攻击面立即收敛。例如 fpm 形态下,文件包含与命令执行的权限边界,是 fpm worker 的 uid,不是 web 目录的属主。假设错这一层,定级就错一档。
2.4 第三阶 攻击面推导:可控性 × 可达性 × 业务价值
结论:攻击面推导是给入口排序,不是给入口定性。
三要素:
可控性指参数值是否由外部决定,以及外部能决定到什么程度。路径参数、查询串、请求体能决定完整值,属高;请求头、文件名能决定部分值,属中;服务端生成的值属低。
可达性指需要什么身份。匿名可达最高,普通用户次之,特定角色再次之,内部服务最低。
业务价值指汇聚点后面是什么。碰到批量用户数据、资金操作、权限变更,价值最高;只碰展示串,价值最低。
| 参数来源 | 可控性 | 典型入口 |
|---|---|---|
| 路径段 / 资源 ID | 高 | REST 详情、导出、下载 |
| 查询串 | 高 | 列表、搜索、排序、分页 |
| 请求体(JSON / form) | 高 | 业务提交、批量导入 |
| 请求头(Cookie / UA / 自定义头) | 中 | 会话、日志、限流、审计、回调签名 |
| 文件名 / MIME / 文件内容 | 高 | 头像、附件、导入解析 |
| 服务端会话值 | 低 | 除非存在会话固定或反序列化 |
排序规则:可控性高 × 匿名可达 × 直连汇聚点,排最前。排序是时间分配,不是结论。排在前面的先追,不等于后面的没洞。
2.5 第四阶 数据流追踪:脏标记与收敛点
结论:每个收敛点必须能写出一条从入口参数到该行的完整路径,路径上每一层校验都要判定有效性。
三个概念定死:
脏标记。 外部可控的值标脏。传播发生在赋值、拼接、容器传递、跨函数参数、跨文件返回。
收敛点。 脏值进入汇聚函数的那一行。这一行是审计靶心,其余都是路径。
脱脏。 只有三种形态算真脱脏:白名单枚举(允许集有限且闭合)、类型强转(值不再进入字符串语法)、参数化(值不参与语法构建)。其余一律算部分脱脏,按未脱脏处理。
追踪三条规则:
- 同函数内按语句顺序走,遇分支分开走,不合流。
- 跨函数按参数映射,不按调用顺序猜。同名参数不等于同一个值。
- 跨中间件与过滤器看执行顺序。过滤在解包之前还是之后,结论完全不同。
// 入口:GET /order/detail
public function detail(Request $r) {
$id = $r->input('id'); // ① 入口参数,外部可控,标脏
$id = (int)$id; // ② 类型强转,真脱脏(数字上下文)
$row = Order::where('id', $id)->first(); // ③ 收敛点,参数化,值不参与语法
return view('order.detail', ['row' => $row]);
}
同一个入口,把 ② 换成 trim($id),把 ③ 换成字符串拼接,链路立刻成立。审计看的是 ② 有没有改变值的类型,不是"有没有过滤函数"。
这也是 1.1 讲的检索法失效的具体形态:Order::where 不在任何危险函数表里,但它就是收敛点。
2.6 第五阶 成链:补最小增量
结论:单点升级为链路,缺哪一环补哪一环。补的那一环叫最小增量。
链的三种形态:
同请求链。 一个请求内完成全部环节。最短、最稳、最容易复现,优先构造这一种。
跨请求链。 写入与触发分离,典型是存储型、缓存投毒、任务队列。需要证明两次请求之间的状态保持:写入落在哪张表、哪个缓存、哪个队列,读取方什么时候消费。
跨主体链。 借用他人权限或借用服务端权限,典型是越权、SSRF、模板注入回显。需要证明执行主体不是你:要么服务端替你发请求,要么别人的会话替你读数据。
四种最小增量:
- 补一个可控点。当前入口参数不可控,去别的入口找同名参数,或找可覆盖的参数。
- 补一段回显。汇聚点执行了但看不见结果,找同数据的另一个读出侧。
- 补一次权限跨越。当前身份不够,找未校验主体的接口,或找服务端代发的出口。
- 补一个持久化载体。需要跨请求生效,找未被清理的落点。
判据:链上每一环都要有代码位置或报文位置。出现"通常可以""一般会""大概率能",这一环就是没补上。
2.7 第六阶 伤害定级:按整链最大可达
结论:定级对象是整条链最终可达的最大业务伤害,不是链上最危险的那一行代码。
四档:
| 档 | 判据 | 抽象示例 |
|---|---|---|
| 信息级 | 仅泄露非敏感元信息,或前置条件极高 | 内部版本号、公开配置项 |
| 中 | 泄露受限数据,或可做低影响操作 | 单用户非敏感资料、有限枚举 |
| 高 | 可跨主体获取数据,或执行敏感操作 | 批量用户数据、关键业务操作、权限提升 |
| 严重 | 可直达或接近最高权限,或造成不可逆伤害 | 管理员接管、全量数据、审计链破坏 |
定级必须写三段:
可达范围。碰到哪些数据、哪些操作、多少个主体。 前置条件。需要什么身份、什么环境、什么开关状态。 可复现性。证据形态是什么,换环境是否成立。
定级前回五问法。第 1 至 3 问任一不过,降档或撤案。第 4 问不过,标"未证实",不进正式结论。