dubbo反序列化问题-Hessian2安全加固和修复

0x01 前言

前段时间在安全客发了篇Dubbo的反序列化利用文章《dubbo源码浅析:默认反序列化利用之hessian2》,讲述了其部分源码并着重分析了其反序列化部分,最后以一个Remo依赖的反序列化gadget结尾。

大部分公司,在业务扩张的情况下,为了缓解数据库压力、服务器压力等,采取了分库分表、多级缓存等架构,或对业务进行划分,做成分布式,那么分布式环境下,多个系统间的协作通讯一般使用RPC、HTTP、MQ等,而我相信大部分中小公司、阿里系公司、阿里输送人才的公司..一般都使用到了Dubbo,据我对Dubbo源码的微末了解,其Dubbo协议默认的Hessian2反序列化,并没有什么所谓的安全保护机制(有个瓜,ruilin湿敷一堆gadget,官方不给CVE)。

在我写那篇文章前,我发现国内貌似也没人写过Dubbo相关的漏洞利用,这几天,应该很多用到Dubbo的公司,都在排查其安全受影响情况,因此,我打算写这篇文章,讲讲如何去对Dubbo进行反序列化安全加固,一般情况下,大概有这几种安全加固方案:

  1. 修改反序列化类型(不推荐,就算你改成原生Java、Fastjson等等反序列化,依然存在问题,与其不熟悉的情况下对dubbo进行修改可能会导致业务受损风险,还不如不改)
  2. RPC改成HTTP API(业务开发量太大了)
  3. 加固Hessian2(推荐,也是这篇文章主要讲的)

0x02 Java SPI和Spring SPI

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

经常使用Java语言开发一些框架的人都清楚,SPI的机制带来了很大的便利,使用SPI,我们就可以开发多种实现,分别打包到不同的jar包中去,用户根据所需选择实现的jar包,我们得核心程序就能根据用户引入的jar包,使用SPI去加载其实现。也就是说,如果我提供一个序列化工具,然后把每种序列化实现都分别打包到不同的jar包中去,用户就可以根据引入的jar包选择序列化实现。

Java SPI

下面以一个例子讲解Java SPI使用原理:

  1. core.jar
1
2
3
4
5
package my.threedr3am.fruit;

public interface Fruit {
String name();
}
  1. apple.jar
1
2
3
4
5
6
7
package my.threedr3am.fruit;

public class Apple implememt Fruit {
public String name() {
return "apple";
}
}

META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 my.threedr3am.fruit.Fruit。文件内容为实现类的全限定的类名,如下:

1
my.threedr3am.fruit.Apple
  1. mango.jar
1
2
3
4
5
6
7
package my.threedr3am.fruit;

public class Mango implememt Fruit {
public String name() {
return "mango";
}
}

META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 my.threedr3am.fruit.Fruit。文件内容为实现类的全限定的类名,如下:

1
my.threedr3am.fruit.Mango
  1. 使用

我们引入core.jar包以及Spring依赖,运行:

1
2
3
4
5
6
public static void main(String[] args) {
ServiceLoader<Fruit> fruits = ServiceLoader.load(Fruit.class);
fruits.forEach(fruit -> {
System.out.println(fruit.name());
});
}

若我们引入了apple.jar,main方法的执行就会输出apple,若引入的是mango.jar,则输出的是mango。

Spring SPI

与Java原生的SPI不一样,Spring SPI配置文件并不在

1
2
3
4

下面以一个例子讲解Spring SPI使用原理:

1. core.jar

package my.threedr3am.fruit;

public interface Fruit {
String name();
}

1
2

2. apple.jar

package my.threedr3am.fruit;

public class Apple implememt Fruit {
public String name() {
return “apple”;
}
}

1
spring.factories文件(文件在可以打包到classes目录下的地方,例:resources):

my.threedr3am.fruit=my.threedr3am.fruit.Apple

1
2
3
4



3. mango.jar

package my.threedr3am.fruit;

public class Mango implememt Fruit {
public String name() {
return “mango”;
}
}

1
spring.factories文件(文件在可以打包到classes目录下的地方,例:resources):

my.threedr3am.fruit=my.threedr3am.fruit.Mango

1
2
3
4

4. 使用

我们引入core.jar包以及Spring依赖,运行:

public static void main(String[] args) {
List fruits = SpringFactoriesLoader.loadFactories(Fruit.class, null);
fruits.forEach(fruit -> {
System.out.println(fruit.name());
});
}

1
2
3
4
5
6
7
8
9
10
11
12
13
若我们引入了apple.jar,main方法的执行就会输出apple,若引入的是mango.jar,则输出的是mango。



### 0x03 dubbo序列化SPI原理

Dubbo的SPI和Java SPI以及Spring SPI都不一样,Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。

以下是dubbo官方对其SPI功能的一个小简介:

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。

Dubbo SPI的相关逻辑在ExtensionLoader类中,通过ExtensionLoader类,我们就可以根据参数配置、依赖选择需要的实现类,Dubbo SPI 所需的配置文件通常放置在 META-INF/dubbo 路径下,但是Dubbo对其做了一定的兼容处理:

private static final String SERVICES_DIRECTORY = “META-INF/services/“;

private static final String DUBBO_DIRECTORY = “META-INF/dubbo/“;

private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + “internal/“;

private Map<String, Class<?>> loadExtensionClasses() {
cacheDefaultExtensionName();

Map<String, Class<?>> extensionClasses = new HashMap<>();
// internal extension load from ExtensionLoader's ClassLoader first
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(), true);
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"), true);

loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
return extensionClasses;

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
可以看到,放在这些目录下也是没问题的。



### 0x04 hessian2反序列化安全加固

通过前面的讲解,我相信大家都学会了Dubbo的SPI原理了,那么,我们如果要对Hessian2进行修改,只有两个方法:
1. 创建新的Hessian2序列化工厂,引入我们自定义的反序列化类,通过Dubbo SPI注册我们创建的Hessian2序列化工厂
2. 修改Dubbo源码(不现实)

也就是说,我现在最推荐的做法,就是加Hessian2反序列化黑名单,具体做法,看下面:

1. 新增三个自定义的Hessian2序列化类:

MyHessian2Serialization:

package com.threedr3am.learn.server.boot.serialize;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.serialize.ObjectInput;
import org.apache.dubbo.common.serialize.ObjectOutput;
import org.apache.dubbo.common.serialize.Serialization;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput;

public class MyHessian2Serialization implements Serialization {

@Override
public byte getContentTypeId() {
    return 22;
}

@Override
public String getContentType() {
    return "x-application/hessian2";
}

@Override
public ObjectOutput serialize(URL url, OutputStream out) throws IOException {
    return new Hessian2ObjectOutput(out);
}

@Override
public ObjectInput deserialize(URL url, InputStream is) throws IOException {
    return new MyHessian2ObjectInput(is);
}

}

1
MyHessian2ObjectInput:

package com.threedr3am.learn.server.boot.serialize;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import org.apache.dubbo.common.serialize.ObjectInput;
import org.apache.dubbo.common.serialize.hessian2.Hessian2SerializerFactory;

public class MyHessian2ObjectInput implements ObjectInput {
private final MyHessian2Input mH2i;

public MyHessian2ObjectInput(InputStream is) {
    mH2i = new MyHessian2Input(is);
    mH2i.setSerializerFactory(Hessian2SerializerFactory.SERIALIZER_FACTORY);
}

@Override
public boolean readBool() throws IOException {
    return mH2i.readBoolean();
}

@Override
public byte readByte() throws IOException {
    return (byte) mH2i.readInt();
}

@Override
public short readShort() throws IOException {
    return (short) mH2i.readInt();
}

@Override
public int readInt() throws IOException {
    return mH2i.readInt();
}

@Override
public long readLong() throws IOException {
    return mH2i.readLong();
}

@Override
public float readFloat() throws IOException {
    return (float) mH2i.readDouble();
}

@Override
public double readDouble() throws IOException {
    return mH2i.readDouble();
}

@Override
public byte[] readBytes() throws IOException {
    return mH2i.readBytes();
}

@Override
public String readUTF() throws IOException {
    return mH2i.readString();
}

@Override
public Object readObject() throws IOException {
    return mH2i.readObject();
}

@Override
@SuppressWarnings("unchecked")
public <T> T readObject(Class<T> cls) throws IOException,
        ClassNotFoundException {
    return (T) mH2i.readObject(cls);
}

@Override
public <T> T readObject(Class<T> cls, Type type) throws IOException, ClassNotFoundException {
    return readObject(cls);
}

}

1
MyHessian2Input:

package com.threedr3am.learn.server.boot.serialize;

import com.alibaba.com.caucho.hessian.io.Hessian2Input;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class MyHessian2Input extends Hessian2Input {

private static final Set blackList = new HashSet<>();

static {
blackList.add(“com.threedr3am.learn.server.boot.A”);
}

public MyHessian2Input(InputStream is) {
super(is);
}

@Override
public Object readObject(Class cl) throws IOException {
checkClassDef();
return super.readObject(cl);
}

@Override
public Object readObject(Class expectedClass, Class<?>… expectedTypes) throws IOException {
checkClassDef();
return super.readObject(expectedClass, expectedTypes);
}

@Override
public Object readObject() throws IOException {
checkClassDef();
return super.readObject();
}

@Override
public Object readObject(List<Class<?>> expectedTypes) throws IOException {
checkClassDef();
return super.readObject(expectedTypes);
}

void checkClassDef() {
if (_classDefs == null || _classDefs.isEmpty())
return;
for (Object c : _classDefs) {
Field[] fields = c.getClass().getDeclaredFields();
if (fields.length == 2) {
fields[0].setAccessible(true);
try {
String type = (String) fields[0].get(c);
if (blackList.contains(type))
_classDefs = new ArrayList();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}

1
2
3
以上三个文件,必须Dubbo的服务和消费者双方两端都存在

2. 在resources目录下,新增目录META-INF/dubbo,并创建文件org.apache.dubbo.common.serialize.Serialization,内容:

MyHessian2=com.threedr3am.learn.server.boot.serialize.MyHessian2Serialization

1
2
3
4
5
和上面一样,也是必须Dubbo的服务和消费者双方两端都存在

3. 服务端配置序列化方式

application.properties:

dubbo.provider.serialization=MyHessian2

1
2
3
4

4. 加入反序列化黑名单类

只要给com.threedr3am.learn.server.boot.serialize.MyHessian2Input#blackList集合添加黑名单类即可,我这里列出一些已存在的gadget

org.apache.xbean.naming.context.ContextUtil.ReadOnlyBinding
org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor
com.rometools.rome.feed.impl.EqualsBean
com.caucho.naming.QName
`