前段时间在安全客发了篇Dubbo的反序列化利用文章《dubbo源码浅析:默认反序列化利用之hessian2》,讲述了其部分源码并着重分析了其反序列化部分,最后以一个Remo依赖的反序列化gadget结尾。
大部分公司,在业务扩张的情况下,为了缓解数据库压力、服务器压力等,采取了分库分表、多级缓存等架构,或对业务进行划分,做成分布式,那么分布式环境下,多个系统间的协作通讯一般使用RPC、HTTP、MQ等,而我相信大部分中小公司、阿里系公司、阿里输送人才的公司..一般都使用到了Dubbo,据我对Dubbo源码的微末了解,其Dubbo协议默认的Hessian2反序列化,并没有什么所谓的安全保护机制(有个瓜,ruilin湿敷一堆gadget,官方不给CVE)。
在我写那篇文章前,我发现国内貌似也没人写过Dubbo相关的漏洞利用,这几天,应该很多用到Dubbo的公司,都在排查其安全受影响情况,因此,我打算写这篇文章,讲讲如何去对Dubbo进行反序列化安全加固,一般情况下,大概有这几种安全加固方案:
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
经常使用Java语言开发一些框架的人都清楚,SPI的机制带来了很大的便利,使用SPI,我们就可以开发多种实现,分别打包到不同的jar包中去,用户根据所需选择实现的jar包,我们得核心程序就能根据用户引入的jar包,使用SPI去加载其实现。也就是说,如果我提供一个序列化工具,然后把每种序列化实现都分别打包到不同的jar包中去,用户就可以根据引入的jar包选择序列化实现。
下面以一个例子讲解Java SPI使用原理:
package my.threedr3am.fruit;
public interface Fruit {
String name();
}
package my.threedr3am.fruit;
public class Apple implememt Fruit {
public String name() {
return "apple";
}
}
META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 my.threedr3am.fruit.Fruit。文件内容为实现类的全限定的类名,如下:
my.threedr3am.fruit.Apple
package my.threedr3am.fruit;
public class Mango implememt Fruit {
public String name() {
return "mango";
}
}
META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 my.threedr3am.fruit.Fruit。文件内容为实现类的全限定的类名,如下:
my.threedr3am.fruit.Mango
我们引入core.jar包以及Spring依赖,运行:
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。
与Java原生的SPI不一样,Spring SPI配置文件并不在META-INF/services
目录下,而是META-INF/spring.factories
文件
下面以一个例子讲解Spring SPI使用原理:
package my.threedr3am.fruit;
public interface Fruit {
String name();
}
package my.threedr3am.fruit;
public class Apple implememt Fruit {
public String name() {
return "apple";
}
}
spring.factories文件(文件在可以打包到classes目录下的地方,例:resources):
my.threedr3am.fruit=my.threedr3am.fruit.Apple
package my.threedr3am.fruit;
public class Mango implememt Fruit {
public String name() {
return "mango";
}
}
spring.factories文件(文件在可以打包到classes目录下的地方,例:resources):
my.threedr3am.fruit=my.threedr3am.fruit.Mango
我们引入core.jar包以及Spring依赖,运行:
public static void main(String[] args) {
List<Fruit> fruits = SpringFactoriesLoader.loadFactories(Fruit.class, null);
fruits.forEach(fruit -> {
System.out.println(fruit.name());
});
}
若我们引入了apple.jar,main方法的执行就会输出apple,若引入的是mango.jar,则输出的是mango。
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;
}
可以看到,放在这些目录下也是没问题的。
通过前面的讲解,我相信大家都学会了Dubbo的SPI原理了,那么,我们如果要对Hessian2进行修改,只有两个方法:
也就是说,我现在最推荐的做法,就是加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);
}
}
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);
}
}
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<String> 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();
}
}
}
}
}
以上三个文件,必须Dubbo的服务和消费者双方两端都存在
MyHessian2=com.threedr3am.learn.server.boot.serialize.MyHessian2Serialization
和上面一样,也是必须Dubbo的服务和消费者双方两端都存在
application.properties:
dubbo.provider.serialization=MyHessian2
只要给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