0x01 前言

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

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

在我写那篇文章前,我发现国内貌似也没人写过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
package my.threedr3am.fruit;

public interface Fruit {
    String name();
}
  1. apple.jar
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
  1. mango.jar
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
  1. 使用

我们引入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。

Spring SPI

与Java原生的SPI不一样,Spring SPI配置文件并不在META-INF/services目录下,而是META-INF/spring.factories文件

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

  1. core.jar
package my.threedr3am.fruit;

public interface Fruit {
    String name();
}
  1. apple.jar
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
  1. mango.jar
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
  1. 使用

我们引入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。

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

可以看到,放在这些目录下也是没问题的。

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

}

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的服务和消费者双方两端都存在

  1. 在resources目录下,新增目录META-INF/dubbo,并创建文件org.apache.dubbo.common.serialize.Serialization,内容:
MyHessian2=com.threedr3am.learn.server.boot.serialize.MyHessian2Serialization

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

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

application.properties:

dubbo.provider.serialization=MyHessian2
  1. 加入反序列化黑名单类

只要给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

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖