问题不在有没有反序列化调用,在于谁喂给它数据

一、判据是数据流,不是关键词
那篇审计文章把判断标准说得很短:风险不取决于代码里有没有那个把字节还原成对象的调用,而取决于被还原的数据是不是由不可信输入驱动的。这一句决定了整件事的工作方法。如果是前者,审计就是一次文本检索,做完就完了;如果是后者,审计是一次数据流追踪,要回答的是“外部数据能到哪里”。
我们自己在整改项目里见过两种典型误判。一种是检索命中一堆调用点,逐一看下去其实全部只处理本机生成、且从不落库的配置数据,属于噪声;另一种是检索几乎没命中,因为还原动作是框架和组件在某个配置开关打开时代做的,业务代码里根本看不到那个函数名。第二种漏检的代价大得多,因为它的入口在配置里,不在代码里,代码评审永远看不到它。
先把立场写清楚:下面讲入口盘点方法、风险分级和整改顺序,不复述任何利用链、载荷或绕过步骤,也不对任何在用系统是否受影响下结论。来源里提到过一条对应公开漏洞编号的历史链条,本篇不复制它的细节,也不把编号写出来——判断一个系统该做什么,不需要靠转述别人的链条。
二、不可信输入的七类,漏的通常是后三类
盘入口之前,先把“不可信”这个集合定义完整。我们的分类是七类。
- 请求参数:查询串、表单字段、路径参数。
- 请求头与 Cookie:包括被中间层改写过的头部和客户端存的会话标识。
- 上传文件的内容:文件名之外的那部分字节同样来自外部。
- 消息队列的消息体:写入方常常不止一个系统,跨团队时没人把它当外部输入。
- 外部接口的返回:第三方给的字段,格式由对方定。
- 缓存与数据库里的历史值:写进去的那一刻可信,不代表现在仍可信,尤其当写入路径本身已被整改或废弃。
- 日志与审计导出件:会被重新加载或回放的运维文件,往往有共享目录读写权限。
后三类最容易被漏,因为它们都不在“请求进来”这条路上。队列消息体被当作内部信任域,外部返回被当作对方负责,历史值被当作已校验过的存量——三个假设都有一个共同缺陷:它们都不成立,但都不会在任何一次代码评审中被提出。把七类逐条对着系统填一遍“这里有没有对象还原”,比读一百遍函数名有用。
三、倒推三步,以及为什么只检索函数名必然漏
盘点动作我们固定成三步,顺序不能反。第一步列外部可达点:按上面七类,把系统里所有能接收外部字节的位置写成清单,标明是否需要鉴权、鉴权主体是谁。第二步沿每条路径查有没有对象还原:包含框架和组件在配置开关下代做的还原,也包含自定义驱动里手写的还原。第三步看还原之后可能实例化的类图里,有没有能被触发的魔术方法。三步的产出是三张表,互相以标识引用,缺一张就不算盘完。
只检索函数名会漏三种情况:框架与组件在配置开关下替你做还原;自定义的会话、缓存、日志驱动里手写的还原逻辑;把序列化格式当协议用在接口上的那类设计——这时序列化串就是接口契约的一部分,任何能调接口的人都在给你喂对象定义。来源里给的正确定位方式是全局检索加上混淆变体的排查,检索是必要的起手,但它是用来发现候选点的,不是用来下结论的;结论要由可达性判定给。
隐式入口那几个位置要单独立一项检查:缓存配置里把序列化打开的那个开关、仍在用的旧版缓存组件、自定义会话驱动与日志驱动。开关类配置的键名各版本与各实现不同,我们不在文档里写死某一条指令,而是把它做成巡检项:“当前配置下,缓存与会话是否走序列化路径,走的是哪种”——这个问题必须能被回答,答案必须记录在案。
四、魔术方法的四类副作用面
来源给的观察重点很清楚:在析构、唤醒、转字符串和魔术调用这几类会被自动触发的方法里,看有没有出现写文件、执行命令、文件包含和回调式函数这四类动作。这个组合之所以危险,是因为魔术方法不需要有人显式调用它——对象被还原出来,它自己就走。
| 副作用面 | 对应的动作类别 | 最小拦截策略 |
|---|---|---|
| 文件写入 | 写路径、改内容、落临时文件 | 写入路径限定在受控目录白名单内,禁止由对象属性直接拼接 |
| 命令执行 | 调用外壳或外部程序 | 业务类里不允许出现执行入口;确有需要的集中在一个受审计的服务上 |
| 文件包含 | 按变量决定加载哪个文件 | 包含目标不做动态解析,改为固定映射表 |
| 回调调用 | 把属性当函数或方法名使用 | 回调来源限定为常量或注册表,拒收字符串形式的可调用名 |
这里说的拦截策略是设计约束,不是给某个具体缺陷打的补丁。真正落地时,这四类动作在类图里的位置一旦确定,就要和可达性判定合起来看:外部能触达的还原入口,其类图里如果这四类动作一个都没有,风险等级立刻下降;反之,哪怕入口藏在配置里,也要按高危处理。
五、风险分级四条问句与整改顺序
分级我们用四条问句,回答的是是或否:这个入口能否被未授权触达;能否控制被还原的类名与属性;类图里能否触达到上面四类副作用;失败或异常是否对运维可见。四条里命中的越多等级越高。四级对应三种处置时限:最高级要求当日完成入口切断并启动整改,两周内交付替换或校验方案;第二级两周内整改,整改期内必须有临时隔离;第三级纳入下个迭代,同期补上可见性;第四级作为技术债登记,但巡检项照常执行。时限不是仪式,它的作用是让“入口清单上的每一条都有人认领”这件事可以被检查。
整改路径的来源给的顺序很干脆:替换优先于校验,校验优先于隔离。替换是改用数组化的解析方式,或换成不受控对象构造的二进制序列化器,代价在兼容性——已有数据格式要迁移,跨语言的消费方要重新对齐。校验是做类型与类名白名单,代价在遗漏——新加的类、别人复用的类都会成为漏点。隔离是限制可达范围与权限,代价在配置漂移,一次临时放权就把整改抹平了。三条的取舍逻辑是:替换消除问题本身,校验压缩问题面,隔离只让问题暂时够不着。所以我们的默认顺序是能在入口层替换就不做校验,能同时做校验与白名单就不只靠隔离。
白名单的写法有三个细节决定它是否有效:精确匹配而不是前缀匹配;默认拒绝而不是默认允许;未知类名必须记录并告警,而不是静默跳过。第三条尤其重要——静默跳过会让攻击面变成一个“没人知道的分支”,也让误伤变成一起查不出来的故障。
六、存量数据、月度巡检与审计交付物
库里已经存在的序列化值是整改最容易忘的一半。我们的做法是给一个双读窗口:读取时先按新格式解析,解析不了再回退旧格式,回退事件单独计数。转换按业务标识批量做,每批记录成功数、失败数与失败原因分类。转换失败时的降级要提前定:是保留旧值继续回退读,还是标记为需人工处理,取决于该字段是否参与鉴权——参与鉴权的字段一律不允许长期停留在旧格式上。双读窗口的结束要有判据,就是回退计数连续降到接近零。
缓存与会话配置的巡检项固定三条:序列化开关当前状态、驱动组件的版本与是否仍在维护、这些存储有没有对外可达的读写口。三条按月执行并把结果落表,因为这类入口的变化通常来自一次依赖升级或一次临时调试,而不是来自代码评审。
审计交付物我们只认五个部分:入口清单(七类不可信输入逐条对应)、可达性判定(每条入口是否需要鉴权、由谁鉴权)、类图风险点(四类副作用面的位置)、处置建议与优先级(替换、校验、隔离三选一或组合,配上面的时限)、复测方法(正例证明业务不受损,反例证明被拒并留下记录)。缺任何一份,审计结论都只是意见而不是可复查的证据。洹拓把这些交付物当成安全整改的合同附件来签,因为对客户来说,一份能复查的入口清单,价值远大于一句风险评级。