一、防御分层模型
反序列化防御可以按作用域分五层:
Layer 5: 依赖扫描 ← 源头拦截引入漏洞组件 (OWASP Top 10 A06)
Layer 4: 输入校验 / WAF ← 边界拦截已知 payload 特征
Layer 3: RASP / JVM Agent ← 运行时拦截危险类加载
Layer 2: ObjectInputFilter ← JEP 290 原生白名单 (最有效)
Layer 1: 白名单反序列化 ← 根本只反序列化安全类型 (根本解决)
二、Java 原生防御
2.1 JEP 290 ObjectInputFilter
JDK 9 引入,JDK 8u121 和 11.0.8 反向移植。这是目前最有效的防御手段。
方式 A:全局过滤器
ObjectInputFilter.Config.setGlobalFilter(
ObjectInputFilter.Config.createFilter(
"com.example.dto.*;!org.apache.commons.collections.*;!*"));
方式 B:每个 ObjectInputStream 单独设置
ObjectInputStream ois = new ObjectInputStream(in);
ois.setObjectInputFilter(
ObjectInputFilter.Config.createFilter(
"com.example.dto.*;com.example.entities.*;java.util.*;java.lang.*;!*"));
Object obj = ois.readObject();
2.2 JVM 启动参数(JDK 8u261+ / 11.0.8+)
# 拦截 commons-collections gadget
java -Djdk.serialFilter="org\.apache\.commons\.collections\..*;!*" -jar app.jar
# 拦截 TemplatesImpl (Fastjson/Jackson 通用 gadget)
java -Djdk.serialFilter="com\.sun\.org\.apache\.xalan\.internal\.xsltc\.trax\.TemplatesImpl;!*" -jar app.jar
# 拦截 Shiro rememberMe 反序列化
java -Djdk.serialFilter="org\.apache\.commons\.collections\..*;!*" -jar app.jar
| 语法 | 含义 |
|---|---|
!pattern |
拒绝匹配 |
pattern |
允许匹配 |
* |
通配所有 |
!* |
拒绝剩余所有(必须放在最后兜底) |
2.3 自定义 SafeObjectInputStream
import java.io.*;
import java.util.Set;
public class SafeObjectInputStream extends ObjectInputStream {
private static final Set<String> ALLOWED = Set.of(
"com.example.dto.User",
"com.example.dto.Order",
"com.example.dto.OrderItem",
"java.util.ArrayList",
"java.util.HashMap",
"java.util.Date",
"java.lang.String",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Boolean"
);
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String name = desc.getName();
if (!ALLOWED.contains(name)) {
throw new InvalidClassException("Deserialize blocked by policy: " + name);
}
return super.resolveClass(desc);
}
}
使用:
try (SafeObjectInputStream ois = new SafeObjectInputStream(inputStream)) {
Object data = ois.readObject();
}
三、PHP 防御
3.1 严格白名单
// 最严格:不允许反序列化任何类,只还原基本类型
$data = unserialize($payload, ['allowed_classes' => false]);
// 显式白名单
$data = unserialize($payload, ['allowed_classes' => [User::class, Order::class]]);
3.2 php.ini 配置
disable_functions = system,exec,passthru,shell_exec,popen,proc_open,pcntl_exec
allow_url_include = Off
allow_url_fopen = Off
3.3 禁用危险 stream wrapper
stream_wrapper_unregister('phar');
stream_wrapper_unregister('data');
stream_wrapper_unregister('expect');
3.4 升级 PHP 版本
- PHP 8.0:allow_classes 默认行为改变
- PHP 7.4+ 用
__unserialize替代__wakeup,绕过更难
四、Python 防御
4.1 自定义 SafeUnpickler
import pickle, io
class SafeUnpickler(pickle.Unpickler):
ALLOWED = {
'builtins': {'list', 'dict', 'int', 'str', 'float', 'set', 'tuple', 'bool', 'bytes'},
'datetime': {'datetime', 'date', 'time', 'timedelta', 'timezone'},
'decimal': {'Decimal'},
'collections': {'OrderedDict', 'defaultdict'},
}
def find_class(self, module, name):
if module in self.ALLOWED and name in self.ALLOWED[module]:
return super().find_class(module, name)
raise pickle.UnpicklingError(
f"Blocked by policy: {module}.{name} is not in allowlist")
def safe_unpickle(data: bytes) -> object:
return SafeUnpickler(io.BytesIO(data)).load()
4.2 全局替换 pickle.loads
import pickle, functools
_original_loads = pickle.loads
@functools.wraps(_original_loads)
def _safe_loads(*args, **kwargs):
return safe_unpickle(args[0])
pickle.loads = _safe_loads
# 注意:monkey patch 只在进程内有效,子进程无效
4.3 替换为安全序列化格式
import json
# 用 JSON 替代 pickle(推荐)
data_json = json.dumps(python_obj)
restored = json.loads(data_json)
# 或 msgpack / protobuf
import msgpack
data = msgpack.packb(python_obj)
restored = msgpack.unpackb(data)
五、.NET 防御
5.1 SerializationBinder 白名单
public class SafeSerializationBinder : SerializationBinder {
static readonly HashSet<string> ALLOWED = new HashSet<string>(
new[]{
"MyApp.DTOs.User, MyApp",
"MyApp.DTOs.Order, MyApp",
"System.Collections.Generic.List`1, mscorlib",
"System.String, mscorlib",
"System.Int32, mscorlib",
"System.DateTime, mscorlib"
}, StringComparer.OrdinalIgnoreCase);
public override Type BindToType(string assemblyName, string typeName) {
string full = $"{typeName}, {assemblyName}";
if (ALLOWED.Contains(full)) return Type.GetType(full, throwOnError: true);
throw new SerializationException($"Blocked: {full}");
}
}
5.2 替换为安全序列化器
// System.Text.Json (推荐,.NET Core 3.0+)
string json = JsonSerializer.Serialize(obj);
User u = JsonSerializer.Deserialize<User>(json);
// protobuf-net
var ms = new MemoryStream();
Serializer.Serialize(ms, obj);
六、RASP 运行时防护
Java JVM Agent 拦截 ClassLoader
import java.lang.instrument.*;
import java.security.ProtectionDomain;
public class DeserializeRaspAgent {
static final String[] BLOCKED_PREFIXES = {
"org/apache/commons/collections",
"com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl",
"com/sun/rowset/JdbcRowSetImpl",
"org/springframework/aop/config",
"javassist",
"bcel",
"org/apache/shiro"
};
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader, String name,
Class<?> classBeingRedefined, ProtectionDomain pd, byte[] cb) {
for (String p : BLOCKED_PREFIXES) {
if (name.startsWith(p)) {
System.err.println("[RASP] BLOCKED class load attempt: " + name);
return null; // 返回空字节数组 = 抛 ClassFormatError
}
}
return cb;
}
});
}
}
打包并使用:
javac -cp . -d . DeserializeRaspAgent.java
# 在 MANIFEST.MF 加 Premain-Class: DeserializeRaspAgent
jar cf rasp-agent.jar MANIFEST.MF DeserializeRaspAgent.class
# 运行时
java -javaagent:rasp-agent.jar -jar app.jar
七、依赖组件审计
7.1 Java Maven OWASP Dependency-Check
mvn org.owasp:dependency-check-maven:check
# 输出 dependency-check-report.html
7.2 pip-audit
pip install pip-audit
pip-audit
7.3 npm audit / yarn audit
npm audit
npm audit fix
7.4 Trivy(容器扫描)
trivy image myapp:latest
trivy fs /path/to/project
八、WAF / 边界拦截
8.1 ModSecurity 规则
SecRule REQUEST_HEADERS|ARGS|BODY "@rx \xac\xed\x00\x05" \
"id:100001,phase:1,deny,status:403,msg:'Java serialized data detected'"
SecRule ARGS "@rx \{\s*\"@type\"\s*:\s*\"java\." \
"id:100002,phase:2,deny,msg:'Fastjson AutoType attempt'"
SecRule REQUEST_HEADERS \
"Cookie:.*rememberMe=AAEAAAD" \
"id:100003,phase:1,deny,msg:'Shiro rememberMe base64'"
8.2 Nginx Lua
http {
lua_package_path "/etc/nginx/lua/?.lua;;";
access_by_lua_file /etc/nginx/lua/check_deserialize.lua;
}
-- check_deserialize.lua
local body = ngx.var.request_body or ""
local cookies = ngx.var.http_cookie or ""
-- 检测 Java 序列化魔数
if body:find("\236\237\0\5") or body:find("\xAC\xED\x00\x05") then
ngx.log(ngx.WARN, "Java serialized payload detected")
ngx.exit(403)
end
-- 检测 @type AutoType
if body:find("@type") and body:find("java%.") then
ngx.log(ngx.WARN, "Fastjson AutoType detected")
ngx.exit(403)
end
-- 检测 Shiro rememberMe
if cookies:find("rememberMe=AAEAAAD") then
ngx.log(ngx.WARN, "Shiro rememberMe detected")
ngx.exit(403)
end
九、蓝队实战:进程监控脚本
#!/usr/bin/env python3
"""监控 Java 进程加载危险 gadget 类,蓝队应急响应脚本"""
import subprocess, os, signal, time, re
from datetime import datetime
BAD_PATTERNS = [
("org/apache/commons/collections", "CommonsCollections gadget"),
("com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl", "TemplatesImpl gadget"),
("javassist", "Javassist"),
("bcel", "BCEL"),
("org/springframework/aop", "Spring AOP gadget"),
("org/apache/shiro", "Shiro rememberMe"),
]
SUSPICIOUS_JAVA_OPS = [
"exec(",
"Runtime.getRuntime()",
"ProcessBuilder(",
"InitialContext",
"JNDI",
"TemplatesImpl",
]
def find_java_procs():
try:
out = subprocess.run(["jps", "-v"], capture_output=True, text=True).stdout
for line in out.splitlines():
parts = line.strip().split()
if len(parts) >= 2:
yield int(parts[0]), parts[1], line
except:
pass
def check_process(pid, name):
issues = []
try:
# 检查打开的 .jar 文件
lsof = subprocess.run(["lsof", "-p", str(pid)], capture_output=True, text=True).stdout
for pattern, desc in BAD_PATTERNS:
if pattern.replace("/", "-").lower() in lsof.lower():
issues.append(f"[!] PID {pid} ({name}) loads {desc}")
except:
pass
return issues
def main():
print(f"[*] 扫描时间: {datetime.now()}")
for pid, name, full in find_java_procs():
for issue in check_process(pid, name):
print(issue)
if __name__ == "__main__":
main()
十、最佳实践 Checklist
- 升级所有 Jackson / Fastjson / commons-collections / Shiro 到最新版本
- 全局禁用
enableDefaultTyping()和 AutoType - Java:设置 JVM 参数
-Djdk.serialFilter=org\.apache\.commons\.collections\..*;!* - PHP:
unserialize($data, ['allowed_classes' => false]) - Python:用 JSON / msgpack 替代 pickle
- .NET:移除 BinaryFormatter 引用,改用 System.Text.Json
- Shiro:替换默认 cipherKey,禁用 rememberMe
- 添加 OWASP dependency-check 到 CI/CD
- WAF 添加 AC ED 00 05 魔数拦截规则
- 审计代码中所有 ObjectInputStream / unserialize / pickle.loads 调用
十一、总结
反序列化防御没有银弹,但分层防御模型能大幅降低风险。最有效的三件事:用 ObjectInputFilter 设白名单、升级所有依赖、禁用不必要的反序列化入口。JEP 290 / PHP allowed_classes / .NET SerializationBinder 本质都是同一个设计思想——把"允许哪些类型被还原"的决定权收回到开发者手里。永远记住:任何能反序列化不可信数据的代码都是漏洞。