1.Kafka反序列化漏洞简介
Apache Kafka 是开源的 Apache 流处理平台,由 Apache编写,采用scala与java。该项目旨在于提供一个统一的、高吞吐量的、低延迟的实时数据处理平台(中间件模型应用广泛)。
2017年7月19日曝出,Apache kafka connect-runtime
包,在执行 FileOffsetBackingStore
类时,可实现一个反序列化漏洞,这将导致远程代码执行。
Kafka开源代码 Github:https://github.com/apache/kafka/
Kafka官方文档:http://kafka.apache.org/
官方漏洞公告:http://seclists.org/oss-sec/2017/q3/184
2.漏洞概述
Kafka 内部实现一个带有 readObject
方法的并且未带有任何校验的危险类,如果用户在使用该框架中,使用了该类的话,通过设置相关参数后实例化该类的时候会导致远程代码执行。
受影响版本:Apache Kafka 0.10.0.0 -> 0.11.0.0(2017/09/13当前最新版本为0.11.0.0)
考虑到实际生产环境这样的代码逻辑并不常见,根据影响,确认为中低危漏洞。
3.漏洞详情
漏洞复现POC
官方漏洞复现POC:http://seclists.org/oss-sec/2017/q3/184
1 | import junit.framework.Test; |
ysoserial简介
ysoserial
是一个构造恶意序列化对象的工具包。
github地址:https://github.com/frohoff/ysoserial/
由于该组件没有入maven中央仓库,因此需要将代码clone到本地,编译,部署到本地maven仓库。
1 | git clone git@github.com:frohoff/ysoserial.git |
恶意序列化代码
其实核心的恶意序列化代码是如下两行
1 | Jdk7u21 jdk7u21 = new Jdk7u21(); |
由于touch是linux命令,在windows环境下,我改为了如下,即执行计算器程序。
1 | Jdk7u21 jdk7u21 = new Jdk7u21(); |
github demo:https://github.com/shiyueqi/kafka-deserialization-bug
漏洞解析
org.apache.kafka.connect.storage.FileOffsetBackingStore
这个class拥有一个反序列化操作,在执行 FileOffsetBackingStore
对象的start方法时候会触发并反序列恶意序列化对象,导致代码执行。
再来看一下上面的漏洞复现POC中,优先调用了FileOffsetBackingStore
对象的 configure
方法。
1 | @Override |
调用configure方法后,对类成员变量this.file赋值,赋值为为传入的文件。然后继续调用FileOffsetBackingStore
对象的start方法。
1 | @Override |
调用start方法时,start()
方法会调用load方法。
1 | @SuppressWarnings("unchecked") |
在load()方法中可以看到通过 ObjectInputStream
类加载,将 this.file
的值读取到 is
中,这里就是我们构造的恶意序列化的对象。而接下来调用的 readObject()
方法正好会反序列化这个对象。
而在 FileOffsetBackingStore
在反序列化过程中,是直接调用了 java.io.ObjectInputStream
类的 readObject()
方法,没有对传入参数进行验证。这将允许攻击者构造序列化对象并执行恶意代码。
4.源码分析
概述
恶意序列化的核心思想是构造恶意的对象,在执行反序列化过程时,执行构造对象的反序列化方法,从而完成远程漏洞执行。
如下代码,构造一个 DeserializationObject
类,实现 readObject()
方法,方法内调用 Runtime.getRuntime().exec("calc.exe");
即通过命令的方式,打开计算器。
完整代码如下:
1 | static class DeserializationObject implements Serializable { |
源码跟踪
FileOffsetBackingStore
关于 FileOffsetBackingStore
类的调用代码如下:
1 | FileOffsetBackingStore restore = new FileOffsetBackingStore(); |
FileOffsetBackingStore类的configure()方法和start()方法源码如下:
1 | @Override |
可以看到,调用configure方法后,对类成员变量this.file赋值,赋值为为传入的文件。调用start方法后,直接进入load方法。而在这里,我们先构造恶意对象,然后进行序列化,保存到文件中,再将该文件作为参数传入FileOffsetBackingStore类的configure()方法中。
load()方法如下:
1 | @SuppressWarnings("unchecked") |
可以看到刚进入load()方法,即构造ObjectInputStream类对象,
1 | ObjectInputStream is = new ObjectInputStream(new FileInputStream(file)); |
FileInputStream类
调用 FileInputStream
类的构造方法,可以看到首先拿到文件的name,再创建一个文件句柄,即 FileDescriptor
类对象,最后调用open方法,将入参file对象内容读入。而open方法已经是调用底层的 native
方法。
该方法的注释中,也声明:对指定的文件,打开一个连接,创建一个文件输入流。一个文件句柄对象会被创建,用来持有该连接。
源码如下:
1 | /** |
ObjectInputStream
调用 ObjectInputStream
构造方法。可以看到,基本都是一些初始化操作。源码如下:
1 | /** |
readObject()
再回到 FileOffsetBackingStore
类的 start()
方法调用的 load()
方法中,在构造完 ObjectInputStream
对象后,调用
1 | ObjectInputStream is = new ObjectInputStream(new FileInputStream(file)); |
实际调用 ObjectInputStream
类中的 readObject()
方法。源码如下
1 | /** |
继续调用 ObjectInputStream
中的 readObject0()
方法。
readObject0()
方法中关键源码如下:
1 | /** |
可以看到会对传入参数进行匹配,因为构造的恶意对象为Object类型,因此会触发 case TC_OBJECT
这个条件。继续调用 readOrdinaryObject()
方法。
源码如下:
1 | /** |
调用 readSerialData(obj, desc)
方法。源码如下,核心代码为 slotDesc.invokeReadObject(obj, this);
:
1 | /** |
ObjectStreamClass 类的反射调用
调用 ObjectStreamClass
类的 invokeReadObject()
方法。源码如下:
1 | /** |
这时候可以看到这行代码,就是反射调用了。
1 | readObjectMethod.invoke(obj, new Object[]{ in }); |
再看一下 readObjectMethod
,这个 ObjectStreamClass
类的成员变量的定义。在 ObjectStreamClass
类的构造方法中,对 readObjectMethod
进行了初始化。 ObjectStreamClass
类的构造方法源码如下:
1 | /** |
可以看到,readObjectMethod
定义如下:
1 | readObjectMethod = getPrivateMethod(cl, "readObject", |
第二个参数为反射调用的方法名称,即 readObject
;第三个参数为反射调用的方法参数,为 ObjectInputStream
类的对象;最后一个参数为反射调用的方法返回值,即 void
类型。
因此,在构造恶意序列化对象时,实现如下的方法即可。
1 | private void readObject(ObjectInputStream input) |
5.解决方案
kafka官方已经修复了该漏洞,如下为commit链接。
https://github.com/apache/kafka/commit/da42977a004dc0c9d29c8c28f0f0cd2c06b889ef
通过fix代码可以看到新加入了自己实现的 org.apache.kafka.connect.util.SafeObjectInputStream
而用来替代原有的JDK中的 java.io.ObjectInputStream
类。
而 SafeObjectInputStream
继承了 ObjectInputStream
,代码里加入一个黑名单列表,并使用resolveClass.isBlacklisted()
过滤掉黑名单类。
预计在0.11.0.1版本推出该功能。
6.JVM 禁止执行外部命令
严格意义说起来,Java相对来说安全性问题比较少,出现的一些问题大部分是利用反射,最终用Runtime.exec(String cmd)函数来执行外部命令的。如果可以禁止JVM执行外部命令,未知漏洞的危害性会大大降低,可以大大提高JVM的安全性。
1 | SecurityManager originalSecurityManager = System.getSecurityManager(); |
如上所示,只要在Java代码里简单加一段程序,就可以禁止执行外部程序了。
禁止JVM执行外部命令,是一个简单有效的提高JVM安全性的办法。可以考虑在代码安全扫描时,加强对Runtime.exec相关代码的检测。
7.参考链接
- https://github.com/apache/kafka/
- http://kafka.apache.org/
- http://seclists.org/oss-sec/2017/q3/184
- http://www.openwall.com/lists/oss-security/2017/07/19/1
- http://bobao.360.cn/learning/detail/4190.html
- http://seclist.us/ysoserial-v-0-0-3-a-proof-of-concept-tool-for-generating-payloads-that-exploit-unsafe-java-object-deserialization.html
- https://github.com/frohoff/ysoserial/
- https://github.com/apache/kafka/commit/da42977a004dc0c9d29c8c28f0f0cd2c06b889ef