WebLogic反序列化(CVE-2020-14644)漏洞分析

0x01 简介

WebLogic是美国Oracle公司出品的一个application server,确切的说是一个基于JAVAEE架构的中间件,WebLogic是用于开发、集成、部署和管理大型分布式Web应用、网络应用和数据库应用的Java应用服务器。

0x02 漏洞概述

Oracle官方在2020年7月份发布的最新安全补丁中披露此漏洞。该漏洞允许未经身份验证的攻击者通过IIOP,T3进行网络访问,未经身份验证的攻击者成功利用此漏洞可能接管Oracle WebLogic Server。CVSS评分9.8。

0x03 影响版本

  • Oracle WebLogic Server 12.2.1.3.0
  • Oracle WebLogic Server 12.2.1.4.0
  • Oracle WebLogic Server 14.1.1.0.0

0x04 环境搭建

WebLogic 安装

由于之前复现 CVE-2020-14645 漏洞时已经搭建完成,因此在此不过多阐述过程,仅记录一些信息。

WebLogic 下载链接:https://www.oracle.com/middleware/technologies/weblogic-server-installers-downloads.html

image-20200928152012609

选择 12.2.1.4 的 Generic 版本进行下载安装即可,安装过程注意JDK版本造成的安装失败(建议JDK 1.8)以及以管理员权限运行安装包。具体安装过程可参照这两篇文章:

此外,安装过程中,为方便后续其他漏洞利用,安装类型可选择“含示例的完整安装”。

安装完之后,直接运行安装目录下的启动脚本即可,路径结构如下:

$Oracle_Home$\12.2.1.4.0\user_projects\domains\wl_server\startWebLogic.cmd
// Oracle_Home指WebLogic安装的绝对路径

启动脚本后,浏览器访问http://127.0.0.1:7001/console

image-20200928162057678

正常显示控制台登录界面,即代表安装成功。

0x05 漏洞复现

使用 WebLogic 12.2.1.4.0 版本进行复现

poc 文件见:https://github.com/rufherg/WebLogic_Basic_Poc/blob/master/poc/CVE_2020_14644.java

T3 协议 python 脚本见:https://github.com/rufherg/WebLogic_Basic_Poc

运行poc文件生成反序列化数据文件,随后通过 t3 协议传输数据

image-20201028193738062

成功弹出计算器,但是此处同一个Payload无法多次执行,原因在于这个类在第一次触发时已经被加载了(static块)。可以考虑通过生成不同的类,或者其他方式进行反复利用。

0x06 漏洞分析

先来看看漏洞的调用栈

image-20201028204456037

整体来看,调用链比较简单,直接定位到com.tangosol.internal.util.invoke.RemoteConstructor这个类。

image-20201028205202425

可以看到RemoteConstructor这个类是实现了ExternalizableLite接口,而ExternalizableLite接口又继承了Serializable接口。因此,RemoteConstructor这个类可被序列化与反序列化,我们来看其反序列化入口readResolve

image-20201028210621900

可以看到调用了内部的newInstance方法,跟进分析一下。

image-20201028210708766

这里,我们需要重点关注RemotableSupport.realize方法。

image-20201030155959115

我们很容易注意到两个关键点——defineClasscreateInstance方法,调试过一些 java 漏洞的同学,应该很容易就意识到这里是一个通过加载字节码自定义类,然后进行实例化的过程。但是注意这里一开始会调用registerIfAbsent方法,我们先来看看该方法。

image-20201030232320066

image-20201030232431602

这里出现Map类型的f_mapDefinitions是用于充当一个缓存作用,因此每次调用 realize 时会先在缓存中查找 ClassDefinition

image-20201030233310025

image-20201031011955280

往下可以发现会通过getRemotableClass方法去获取this.m_clz的值,但是其经过transient修饰,无法控制其值。注意这里还有一个同样被transient修饰的m_mhCtor,为一个MethodHandle对象。MethodHandle是 JDK7 的新特性,其引入是为了与已经存在的java.lang.reflect API相配合,以解决不同情况下的问题。在这里先不讨论,在后面再阐述。

image-20201030233633169

回到realize方法走入if分支,这里会调用defineClass方法然后通过setRemotableClass方法进行赋值,跟进分析:

image-20201030165200616

image-20201030165248576

我们可以看到RemotableSupport继承了ClassLoader并且重写了defineClass方法,在方法中先对definition对象进行一些数据处理分割,最后调用原生的ClassLoader.defineClass加载字节码生成Class对象。我们先来看看其怎么处理数据的。

image-20201030200451753

可以看到这里的id实质上就是指ClassIdentity对象,abClass就是需要加载的字节码,跟进其getName方法。

image-20201030200855017

image-20201030205934130

很容易看到,获取的属性都是构造方法中的参数,因此我们只要构造出一个符合条件的ClassIdentity对象。sPackagesBaseName都比较容易处理,重点在于sVersion的获取,这里采用的方式是Base.toHex(md5(clazz)))(图片篇幅受限没显示出来)。所以最终defineClass方法中获取的sClassName内容形式应该是xxx.xxx.xxx$md5(clazz),构造对象时我们利用一个存在的class对象,然后生成一个名字为xxx$md5(clazz)的类(字节码的类名需与sClassName一致)。例如:

String classname = "com.tangosol.internal.util.invoke.lambda.LambdaIdentity$" + Base.toHex(md5(LambdaIdentity.class));
ClassPool pool = ClassPool.getDefault();
CtClass ct = pool.makeClass(classname);

所以defineClass方法将会触发加载字节码操作,继续往下看:

image-20201030231037293

image-20201031012931451

这里调用了constructor.getArguments方法,返回的是m_aoArgs属性值,而该属性来自构造方法的参数,因此是可控的。

image-20201031013349628

image-20201031013317022

这里会先获取以aoArgs为参数的构造器,并执行invokeWithArguments方法(即调用构造器生成实例对象)。可以看到m_mhCtor在前面是可以进行赋值的(加载的类只有一个构造器时),并且是对m_clz即加载字节码所生成的类对象进行构造器寻找。如果没赋值,此处会重新寻找构造方法生成MethodHandle。此处,由于 POC 中生成的类是无参的构造方法,所以aoArgs为空的Object[]。至此,便可以通过反序列化漏洞实例化字节码定义的类。

0x07 修复建议

1、安装官方补丁

https://www.oracle.com/security-alerts/cpujul2020.html

2、限制T3访问来源

漏洞产生于WebLogic默认启用的T3协议,因此可通过限制T3访问来源来阻止攻击。

3、禁用IIOP协议

可以查看下面官方文章进行关闭IIOP协议。

https://docs.oracle.com/middleware/1213/wls/WLACH/taskhelp/channels/EnableAndConfigureIIOP.html

0x08 总结

本来已经快写完了,在createInstance打算跳过然后停笔。但是想到了 static 静态块是在类初始化时会自动执行,但是为什么调试时是在createInstance后执行。顺着这个问题,加上对MethodHandle的不了解,翻阅很多资料文章,才纠正了一个很严重的误区。在这里给大家指出来,也希望一些学习 Java 安全的同学不要陷入这样的一个误区,另外再注明几个调试分析中的疑问点。学习与实践是一个相辅相成的过程,通过实践深刻知识,通过学习弥补短板。Java 知识面很广,需要不断多想多学多试,去理解其中的奥秘,像类的加载机制等等底层特性都值得我们去深究。随着你的理解愈发深刻,你会产生更多的新奇思路,不管是漏洞利用方式还是挖洞思维,都格外见效。

类的初始化

引用一张图来理解

很多同学可能以为defineClass之后生成Class对象就是成功加载了一个类并且初始化了,但是事情并不是这样。“加载”通俗来讲就是:将一个类的二进制字节流转化为 Class 对象,例如常见的 .class 文件加载、defineClass方法,而初始化是在链接后才进行的。

以下五种情况将触发类的初始化:

1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

另外还有两个需要对比区分的—— Class.forName 以及 Classloder.loadClass,这两者也经常容易被混淆。Class.forName("类名")默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器),而ClassLoader.loadClass默认不会初始化类方法(未链接状态)。

疑问1:为什么payload只能用一次?

实质上这个问题,在上面已经有了解答。在realize方法中,会先在缓存中查看是否存在对象,不存在时才会调用defineClass方法,而 static 块对于同一个类对象,只会执行一次。要打破这种禁锢,可以尝试将恶意代码写在恶意类的构造方法中,当然这个得自行测试一下了。另外一个方式,就是生成不同的类,可以看到ClassIdentity类的另外一个构造方法是protected修饰,因此我们可以通过反射去构造,POC相关部分改写如下:

//用于生成不同payload
String packagename = "com/payload";
String basename = "DEADF1SH_CAT";
String version = "" + System.nanoTime();
String classname = "com.payload.DEADF1SH_CAT$" + version;
       
//通过反射构造
Class clazz = ClassIdentity.class;
Constructor IdCon = clazz.getDeclaredConstructor(String.class,String.class,String.class);
IdCon.setAccessible(true);//构造方法为protected
       
ClassIdentity ClassId = (ClassIdentity) IdCon.newInstance(packagename, basename, version);

疑问2:为什么构造的恶意类名一定要带包名?

image-20201031022743080

不带包名getPackage()会返回null,再往下调用就会造成空指针异常。

参考链接:

posted @ 2021-01-05 09:31  DEADF1SH_CAT  阅读(762)  评论(0编辑  收藏  举报