一、防御分层模型

反序列化防御可以按作用域分五层:

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 本质都是同一个设计思想——把"允许哪些类型被还原"的决定权收回到开发者手里。永远记住:任何能反序列化不可信数据的代码都是漏洞