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
选择 12.2.1.4 的 Generic 版本进行下载安装即可,安装过程注意JDK版本造成的安装失败(建议JDK 1.8)以及以管理员权限运行安装包。具体安装过程可参照这两篇文章:
- http://www.quiee.com.cn/courses/qui/370984-1429262936779670.html
- https://www.cnblogs.com/xdp-gacl/p/4140683.html
此外,安装过程中,为方便后续其他漏洞利用,安装类型可选择“含示例的完整安装”。
安装完之后,直接运行安装目录下的启动脚本即可,路径结构如下:
$Oracle_Home$\12.2.1.4.0\user_projects\domains\wl_server\startWebLogic.cmd
// Oracle_Home指WebLogic安装的绝对路径
启动脚本后,浏览器访问http://127.0.0.1:7001/console
。
正常显示控制台登录界面,即代表安装成功。
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 协议传输数据
成功弹出计算器,但是此处同一个Payload无法多次执行,原因在于这个类在第一次触发时已经被加载了(static块)。可以考虑通过生成不同的类,或者其他方式进行反复利用。
0x06 漏洞分析
先来看看漏洞的调用栈
整体来看,调用链比较简单,直接定位到com.tangosol.internal.util.invoke.RemoteConstructor
这个类。
可以看到RemoteConstructor
这个类是实现了ExternalizableLite
接口,而ExternalizableLite
接口又继承了Serializable
接口。因此,RemoteConstructor
这个类可被序列化与反序列化,我们来看其反序列化入口readResolve
。
可以看到调用了内部的newInstance
方法,跟进分析一下。
这里,我们需要重点关注RemotableSupport.realize
方法。
我们很容易注意到两个关键点——defineClass
和createInstance
方法,调试过一些 java 漏洞的同学,应该很容易就意识到这里是一个通过加载字节码自定义类,然后进行实例化的过程。但是注意这里一开始会调用registerIfAbsent
方法,我们先来看看该方法。
这里出现Map
类型的f_mapDefinitions
是用于充当一个缓存作用,因此每次调用 realize
时会先在缓存中查找 ClassDefinition
。
往下可以发现会通过getRemotableClass
方法去获取this.m_clz
的值,但是其经过transient
修饰,无法控制其值。注意这里还有一个同样被transient
修饰的m_mhCtor
,为一个MethodHandle
对象。MethodHandle
是 JDK7 的新特性,其引入是为了与已经存在的java.lang.reflect API
相配合,以解决不同情况下的问题。在这里先不讨论,在后面再阐述。
回到realize
方法走入if
分支,这里会调用defineClass
方法然后通过setRemotableClass
方法进行赋值,跟进分析:
我们可以看到RemotableSupport
继承了ClassLoader
并且重写了defineClass
方法,在方法中先对definition
对象进行一些数据处理分割,最后调用原生的ClassLoader.defineClass
加载字节码生成Class
对象。我们先来看看其怎么处理数据的。
可以看到这里的id
实质上就是指ClassIdentity
对象,abClass
就是需要加载的字节码,跟进其getName
方法。
很容易看到,获取的属性都是构造方法中的参数,因此我们只要构造出一个符合条件的ClassIdentity
对象。sPackage
和sBaseName
都比较容易处理,重点在于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
方法将会触发加载字节码操作,继续往下看:
这里调用了constructor.getArguments
方法,返回的是m_aoArgs
属性值,而该属性来自构造方法的参数,因此是可控的。
这里会先获取以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:为什么构造的恶意类名一定要带包名?
不带包名getPackage()
会返回null
,再往下调用就会造成空指针异常。
参考链接:
-
Orcle 7月份安全补丁更新通告
-
weblogic t3脚本及相关poc
-
深入了解序列化writeObject、readObject、readResolve
-
Weblogic 远程命令执行漏洞(CVE-2020-14644)分析
-
一篇图文彻底弄懂Class文件是如何被加载进JVM的 | 类加载器,加载,连接,初始化
https://www.itzhai.com/articles/how-class-file-load-into-jvm.html