星巡研究院
本册目录

第1章 · EX-D

传统代码审计的致命误区

免费章节
4299 字约 10 分钟本章 3 小节

1.1危险函数检索法在真实项目里失效

结论:危险函数检索法给出的是候选位置,不是漏洞。它在真实项目里的命中率接近噪声。

该方法的前提假设是"危险函数等于缺陷"。真实缺陷的成立条件是四个同时满足:数据可控、路径可达、过滤缺失、汇聚点有害。函数只是第四项里的一个候选集合,连第四项都不完整。

grep 只能看见字面。它看不见四类真实路径。

第一类,别名调用。函数名是数据,字面里没有这个函数名。

// 源码里没有 eval,没有 system,没有 assert
$func = $_GET['action'];    // 值可以是任意已存在的函数名
$func();                    // 动态调用,sink 是一对括号
// 源码里没有直接的方法名,只有字符串拼接
Class<?> c = Class.forName("com.app.handler." + name);
c.getMethod(name).invoke(c.getDeclaredConstructor().newInstance());

正则检索在第一个例子里一次都不会命中,因为源码里根本不存在 eval 这个 token。它存在的是"变量加括号"。这类调用在 PHP 编译后落到 ZEND_INIT_FCALL,函数名在运行时才确定;Java 走反射,方法名在字符串里。

危险的是调用能力,不是函数名。 检索表搜的是名字,攻击者用的是能力。

第二类,封装下沉。危险在包装层,业务代码里一个都搜不到。

// 业务层:看起来只是一次普通查询
$this->db->query("select * from t where id = " . $id);

// 底层 Db 类:拼接发生在这里
class Db {
    public function query($sql) {
        return $this->conn->query($sql);   // 收敛点,业务代码看不见
    }
}

grep 业务目录,命中 0 次。grep 到 Db 类,命中全部业务——这时命中量太大,反而被当成噪声忽略。

判据是看包装层的参数语义,不是看函数名。 query() 接受字符串,就存在拼接位;接受结构化条件,才谈得上参数化。函数名干净不代表链路干净。

第三类,间接汇聚。写入侧无害,读出侧有害。

用户备注写进数据库时,做的可能是最严格的过滤;危险发生在另一个模块把它读出来的那一刻——导出 CSV 时没做公式转义,后台渲染时没做上下文转义,缓存反序列化时没做类型校验。

危险函数的检索只能覆盖写入侧,而链的终点常在读出侧。 同一份数据被几个模块消费,就有几个收敛点,检索表只认得其中一个。

第四类,配置驱动。代码是白名单比较,漏洞在配置项。

// 代码:严格比较,看起来毫无问题
if (!in_array($ext, $config['allowed_ext'], true)) {
    return reject();
}

// 配置:白名单里多了一个后缀,整条链就成立
// config/upload.php
'allowed_ext' => ['jpg', 'png', 'php'],

这段代码静态看是安全的,因为白名单比较本身没有缺陷。缺陷在配置与代码的组合:白名单的值由配置文件决定,配置里多一项,判断就整体失效。

脱离配置文件读代码,等于盲人摸象。 审计白名单时,必须读它的来源,不能只读调用点。

更麻烦的是可达性。现代框架把外部请求收口到统一 router,危险函数会不会被执行,由中间件、路由守卫、参数绑定、容器生命周期共同决定。静态表无法判断可达性,只能判断存在性。

危险函数表还会制造盲区。团队越依赖表,越不看表外路径。定时任务、消息消费、CLI 入口、健康检查、内部回调,这些入口通常无鉴权,也通常不在检索表里。

审计动作:

  • grep 结果只写进"候选清单",不写进"漏洞清单"。
  • 每个候选必须回答三问:入口在哪、参数是否可控、到汇聚点之间有几层校验。
  • 三问无法回答的候选直接丢弃,不留"疑似"。疑似项会在报告里变成假阳性。

深度剖析:为什么静态分析工具(SAST)总会产生海量误报

SAST 的模型是三件东西:抽象语法树、有限的调用图、污点传播。它不理解业务,只理解语法结构。三个结构性缺陷由此而来。

其一,污点源不完整。 工具的"可控输入"集合通常只覆盖框架默认取参方式。参数经 @ModelAttribute 绑定、经 Depends 注入、经 Pydantic 转换、经网关透传、经配置注入,这些值在工具的源集合里往往不存在。源没有标脏,下游一切干净。

其二,传播建模不到。 反射、动态派发、依赖注入容器、AOP 代理、ORM 代理、模板上下文,这六处一断,图就断。图断了之后工具只有两个选择:按干净处理,或按脏处理。前者漏报,后者误报,两个方向都错。

其三,净化判定只看函数名。 工具把"出现在转义函数里"当成净化,不看上下文。htmlspecialchars 对 SQL 无效,addslashes 对宽字节无效,basename 对符号链接无效——工具全按脱脏处理。5.5 里那条"部分脱脏等于未脱脏"的判据,工具一条都不认。

再加两条它做不到的事:没有可达性判断,不看路由是否注册、中间件是否拦截、代码是否在死分支;没有业务语义,分不清"读到一条"与"读到全量",所以严重度算不出来,只能一律标高危。

后果是一条正反馈回路:误报率高,团队建立"批量忽略"的习惯,真阳性被一起忽略。SAST 真正的代价不是漏报,是把"存在"当成了"可达"。

正确用法只有一种:当候选生成器。输出必须过 1.1 的三问,规则只拦新增代码(7.10)。指望它清理存量,结果是规则被静音。

常见说法问题所在
这里有 eval,高危无可控入口,是死代码
用了 ORM,没有注入order by、like、动态表名仍是拼接
加了 addslashes,安全字符集未指定,数字型上下文无效
有 WAF,打不进来WAF 管边界,管不了内部任务与横向
扫描器报了 200 条200 条都是候选,一条结论都没有

1.2单点不等于漏洞:成链五问法

结论:一个可控输入到达一个有缺陷的汇聚点,只是技术可能性。它不是漏洞。

成链五问是「三个问题」在审计场景的展开。一问我能不能多做本不该做的,二问我碰得到什么业务目标,三问我造成的伤害稳不稳。前两问与三个问题同源,第三问是审计特有的——复现稳定性。后两问(证据与整改)是审计场景的增量。

同一套判断,分三处讲,不是三套体系。

把这个状态叫单点。单点是审计的中间产物,不是结论。

靶场与企业环境的差别,全在这个"中间产物"上。

// 靶场形态:语法特征,一眼可辨
eval($_POST[1]);
// 企业形态:同一个 sink,前面隔着三层
public function run(Request $r) {
    if (!$this->auth->isAdmin($r)) return;                    // ① 身份
    if (!in_array($r->rule, $this->allowedRules, true)) return; // ② 白名单
    if (strlen($r->rule) > 200) return;                        // ③ 长度
    eval($this->compile($r->rule));                            // sink 在最后一公里
}

两段代码的 sink 完全相同,成立率可能差一个量级。差别不在 sink,在路径。

维度靶场里的 eval企业里的 eval
入口匿名可达需管理员会话
前置校验无身份、白名单、长度三层
可达性默认成立取决于三条判断同时放行
成立判据执行即成功需证明三条判断均可绕过
审计产出语法识别链路与证据

靶场练的是语法特征,真实审计审的是可达性(Reachability)。 靶场里次次成立的 payload,在企业里可能一次都不成立;反过来,企业里那条白名单只要多放一个规则名,同一个 sink 就必定成立。判断依据是路径,不是函数。

成链五问,逐条过:

  1. 能否多做本不该做的操作。相比较正常权限,多出来的那部分操作是什么。
  2. 能否靠近业务目标。碰得到的是数据、账户、资金、审计记录,还是只有内部元信息。
  3. 能否造成可复现的业务伤害。换环境、换会话、换主体,行为是否稳定成立。
  4. 有无报文或日志证据。能不能留下可复核的位置与响应。
  5. 有无整改价值。修一次能不能堵住这一类,而不是只堵这一行。

前四问全过,判技术漏洞。第五问决定定级与交付优先级。

五问不是流程,是减法。 每一问都在砍掉一类推理:砍掉不可达、砍掉无伤害、砍掉不可复现、砍掉无证据。砍到最后还站着的,才是能写进报告的。

假阳性三个来源,对应三种丢弃理由:

  • 不可达。代码存在,入口未注册,类未装配,开关关闭,方法被注释。写报告前必须确认调用链上有真实外部请求。
  • 被截断。前置中间件、参数绑定、类型强转、签名校验把可控性抹掉了。此时汇聚点再有缺陷也打不到。
  • 汇聚点无害。看着危险,实际只写日志、只拼展示串、只在错误分支执行、只操作隔离目录。

判据:同一行为至少在两个不同入口、两次独立会话复现,才算稳定信号。单次成功先怀疑请求构造。

1.3黑白盒链路的统一模型

结论:黑盒给入口,白盒给路径。用黑盒的入口驱动白盒取证,再用白盒结论回黑盒验证。

黑盒给四样:真实入口、真实参数形态、真实响应、真实权限边界。它决定起点,避免在死代码上耗时间。

白盒给四样:数据流、汇聚点、校验层位置、代码意图。它决定终点,避免把"能打通"误判成"能打穿"。

两者是同一条链路的两个视图。统一模型固定五段:

段观察对象典型缺陷位置
入口路由注册、参数绑定、鉴权注解、拦截器未挂鉴权的内部接口、参数名可被覆盖
解包反序列化、格式解析、类型转换弱类型比较、隐式转换、多余的反序列化入口
清洗过滤函数、白名单、字符集、长度过滤与消费不同层、二次解码、字符集不一致
汇聚SQL、命令、文件、模板、反射、表达式字符串拼接、路径未归一化、表达式求值
后果返回体、状态码、落库、外部请求数据范围未校验、执行主体未校验

任何一段缺失,链路不成立。写"存在 SQL 注入",实际是在说"第二段到第四段成立,第五段未验证"。这种结论不能交付。每段必须标出代码位置或报文位置,缺一段就标"未证实"。