weblogicT3协议及T3反序列化漏洞简介


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://nosec.org/home/detail/2859.html
来源:奇安信
本文版权归原作者所有,如有侵权请联系我及时删除

前言

在研究WebLogic相关的漏洞的时候大家一定见过JNDI、RMI、JRMP、T3这些概念,简单的说,T3是WebLogic RMI调用时的通信协议,RMI又和JNDI有关系,JRMP是Java远程方法协议。我曾经很不清晰这些概念,甚至混淆。因此在我真正开始介绍T3协议及其反序列化漏洞之前,我会对这些概念进行一一介绍。

JNDI

JNDI(Java Naming and Directory Interface)是Java提供的Java 命名和目录接口。通过调用JNDIAPI应用程序可以定位资源和其他程序对象。JNDIJava EE的重要部分,需要注意的是它并不只是包含了DataSource(JDBC 数据源)JNDI可访问的现有的目录及服务有:JDBCLDAPRMIDNSNISCORBA

Naming Service 命名服务:

命名服务将名称和对象进行关联,提供通过名称找到对象的操作,例如:DNS系统将计算机名和IP地址进行关联、文件系统将文件名和文件句柄进行关联等等。

Directory Service 目录服务:

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。

Reference 引用:

在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。

image-20210622142434793

看一下常见的JNDI的例子:

1
jdbc://<domain>:<port>rmi://<domain>:<port>ldap://<domain>:<port>

JNDI的查找一般使用lookup()方法如registry.lookup(name)

RMI

RMI(Remote Method Invocation)即远程方法调用。能够让在某个Java虚拟机上的对象像调用本地对象一样调用另一个Java虚拟机中的对象上的方法。它支持序列化的Java类的直接传输和分布垃圾收集。

Java RMI的默认基础通信协议为JRMP,但其也支持开发其他的协议用来优化RMI的传输,或者兼容非JVM,如WebLogic的T3和兼容CORBA的IIOP,其中T3协议为本文重点,后面会详细说。

举例说明:

假设A公司是某个行业的翘楚,开发了一系列行业上领先的软件。B公司想利用A公司的行业优势进行一些数据上的交换和处理。但A公司不可能把其全部软件都部署到B公司,也不能给B公司全部数据的访问权限。于是A公司在现有的软件结构体系不变的前提下开发了一些RMI方法。B公司调用A公司的RMI方法来实现对A公司数据的访问和操作,而所有数据和权限都在A公司的控制范围内,不用担心B公司窃取其数据或者商业机密。

这种设计和实现很像当今流行的Web API,只不过RMI只支持Java原生调用,程序员在写代码的时候和调用本地方法并无太大差别,也不用关心数据格式的转换和网络上的传输。类似的做法在ASP.NET中也有同样的实现叫WebServices。

RMI远程方法调用通常由以下几个部分组成:

  • 客户端对象
  • 服务端对象
  • 客户端代理对象(stub)
  • 服务端代理对象(skeleton)

下面来看一下最简单的Java RMI要如何实现:

首先创建服务端对象类,先创建一个接口继承java.rmi.Remote:

1
2
3
4
5
6
// IHello.java
import java.rmi.*;

public interface IHello extends Remote {
public String sayHello() throws RemoteException;
}

然后创建服务端对象类,实现这个接口:

1
2
3
4
5
6
7
// Hello.java
public class Hello implements IHello{
public Hello() {}
public String sayHello() {
return "Hello, world!";
}
}

创建服务端远程对象骨架并绑定在JNDI Registry上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Server.java
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class Server{
public Server() throws RemoteException{}
public static void main(String args[]) {
try {
// 实例化服务端远程对象
Hello obj = new Hello();
// 创建服务端远程对象的骨架(skeleton)
IHello skeleton = (IHello) UnicastRemoteObject.exportObject(obj, 0);
// 将服务端远程对象的骨架绑定到Registry上
Registry registry = LocateRegistry.getRegistry();
registry.bind("Hello", skeleton);
System.err.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}

RMI的服务端已经构建完成,继续关注客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Client.java
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
private Client() {}
public static void main(String[] args) {
String host = (args.length < 1) ? "127.0.0.1" : args[0];
try {
Registry registry = LocateRegistry.getRegistry(host);
// 创建客户端对象stub(存根)
IHello stub = (IHello) registry.lookup("Hello");
// 使用存根调用服务端对象中的方法
String response = stub.sayHello();
System.out.println("response: " + response);
} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}

至此,简单的RMI服务和客户端已经构建完成,我们来看一下执行效果:

1
2
3
4
5
6
7
$ rmiregistry &
[1] 80849
$ java Server &
[2] 80935
Server ready
$ java Client
response: Hello, world!

简单解释一下RMI的整个调用流程:

image-20210622150725246

  1. 客户端通过客户端的Stub对象欲调用远程主机对象上的方法
  2. Stub代理客户端处理远程对象调用请求,并且序列化调用请求后发送网络传输
  3. 服务端远程调用Skeleton对象收到客户端发来的请求,代理服务端反序列化请求,传给服务端
  4. 服务端接收到请求,方法在服务端执行然后将返回的结果对象传给Skeleton对象
  5. Skeleton接收到结果对象,代理服务端将结果序列化,发送给客户端
  6. 客户端Stub对象拿到结果对象,代理客户端反序列化结果对象传给客户端

WebLogic RMI

WebLogic RMI和T3反序列化漏洞有很大关系,因为T3就是WebLogic RMI所使用的协议。网上关于漏洞的PoC很多,但是我们通过那些PoC只能看到它不正常(漏洞触发)的样子,却很少能看到它正常工作的样子。那么我们就从WebLogic RMI入手,一起看看它应该是什么样的。

WebLogic RMI就是WebLogic对Java RMI的实现,它和我刚才讲过的Java RMI大体一致,在功能和实现方式上稍有不同。我们来细数一下WebLogic RMI和Java RMI的不同之处。

  • WebLogic RMI支持集群部署和负载均衡
    因为WebLogic本身就是为分布式系统设计的,因此WebLogic RMI支持集群部署和负载均衡也不难理解了。
  • WebLogic RMI的服务端会使用字节码生成(Hot Code Generation)功能生成代理对象
    WebLogic的字节码生成功能会自动生成服务端的字节码到内存。不再生成Skeleton骨架对象,也不需要使用UnicastRemoteobject对象。
  • WebLogic RMI客户端使用动态代理
    在WebLogic RMI 客户端中,字节码生成功能会自动为客户端生成代理对象,因此Stub也不再需要。
  • WebLogic RMI主要使用T3协议(还有基于CORBA的IIOP协议)进行客户端到服务端的数据传输

T3传输协议是WebLogic的自有协议,它有如下特点:

  1. 服务端可以持续追踪监控客户端是否存活(心跳机制),通常心跳的间隔为60秒,服务端在超过240秒未收到心跳即判定与客户端的连接丢失。
  2. 通过建立一次连接可以将全部数据包传输完成,优化了数据包大小和网络消耗。
    下面我再简单的实现一下WebLogic RMI,实现依据Oracle的WebLogic 12.2.1的官方文档,但是官方文档有诸多错误,所以我下面的实现和官方文档不尽相同但保证可以运行起来。

首先依然是创建服务端对象类,先创建一个接口继承java.rmi.Remote:

1
2
3
4
5
6
7
8
// IHello.java

package examples.rmi.hello;
import java.rmi.RemoteException;

public interface IHello extends java.rmi.Remote {
String sayHello() throws RemoteException;
}

创建服务端对象类,实现这个接口:

1
2
3
4
5
6
7
// HelloImpl.java

public class HelloImpl implements IHello {
public String sayHello() {
return "Hello Remote World!!";
}
}

创建服务端远程对象,此时已不需要Skeleton对象和UnicastRemoteobject对象:

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
// HelloImpl.java

package examples.rmi.hello;
import javax.naming.*;
import java.rmi.RemoteException;

public class HelloImpl implements IHello {
private String name;
public HelloImpl(String s) throws RemoteException {
super();
name = s;
}

public String sayHello() throws java.rmi.RemoteException {
return "Hello World!";
}

public static void main(String args[]) throws Exception {
try {
HelloImpl obj = new HelloImpl("HelloServer");
Context ctx = new InitialContext();
ctx.bind("HelloServer", obj);
System.out.println("HelloImpl created and bound in the registry " +
"to the name HelloServer");
} catch (Exception e) {
System.err.println("HelloImpl.main: an exception occurred:");
System.err.println(e.getMessage());
throw e;
}
}
}

WebLogic RMI的服务端已经构建完成,客户端也不再需要Stub对象:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// HelloClient.java

package examples.rmi.hello;

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

public class HelloClient {
// Defines the JNDI context factory.
public final static String JNDI_FACTORY = "weblogic.jndi.WLInitialContextFactory";
int port;
String host;
private static void usage() {
System.err.println("Usage: java examples.rmi.hello.HelloClient " +
"<hostname> <port number>");
}
public HelloClient() {
}

public static void main(String[] argv) throws Exception {
if (argv.length < 2) {
usage();
return;
}
String host = argv[0];
int port = 0;
try {
port = Integer.parseInt(argv[1]);
} catch (NumberFormatException nfe) {
usage();
throw nfe;
}
try {
InitialContext ic = getInitialContext("t3://" + host + ":" + port);
IHello obj = (IHello) ic.lookup("HelloServer");
System.out.println("Successfully connected to HelloServer on " +
host + " at port " +
port + ": " + obj.sayHello());
} catch (Exception ex) {
System.err.println("An exception occurred: " + ex.getMessage());
throw ex;
}
}
private static InitialContext getInitialContext(String url) throws NamingException {
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env.put(Context.PROVIDER_URL, url);
return new InitialContext(env);
}
}

最后记得项目中引入wlthint3client.jar这个jar包供客户端调用时可以找到weblogic.jndi.WLInitialContextFactory

来看一下WebLogic RMI的调用流程

image-20210622152342092

前置知识讲完了,小结一下这些概念的关系,Java RMI即远程方法调用,默认使用JRMP协议通信。WebLogic RMI是WebLogic对Java RMI的实现,其使用T3或IIOP协议作为通信协议。无论是Java RMI还是WebLogic RMI,都需要使用JNDI去发现远端的RMI服务。

image-20210622152514752

T3协议及反序列化漏洞

T3协议简介

WebLogic Server 中的 RMI 通信使用 T3 协议在 WebLogic Server 和其他 Java 程序(包括客户端及其他 WebLogic Server 实例)间传输数据。T3协议包括

  1. 请求包头
  2. 请求主体

因此,在T3数据包构造过程中,需要发送两部分的数据。

2099765-20210307153843180-97941712.png

我们通过部署好的环境,以及现成的payload,去看看这个协议包情况

T3协议

2099765-20210307154238105-472242707.png

1
2
3
4
5
t3 12.2.1
AS:255
HL:19
MS:10000000
PU:t3://us-l-breens:7001

可以看出这是它的请求头。本文测试时发送的T3协议头为

1
2
3
4
5
6
#第一行为“t3”加weblogic客户端的版本号。
t3 12.2.1
AS:255
HL:19
MS:10000000
PU:t3://us-l-breens:7001

weblogic服务器的返回数据为

1
2
3
4
#第一行为“HELO:”加weblogic服务器的版本号
HELO:10.3.6.0.false
AS:2048
HL:19

weblogic客户端与服务器发送的数据均以“\n\n”结尾。

2099765-20210307160852667-465177409.png

可能会问这个地方和其他地方的T3协议怎么不一样?因为我用的exp中,它是伪造自定义了请求包的。可参考文章

也就是说,如何判断对方是否使用T3协议等等,可以对服务器进行发包,发送请求头,对方则会返回weblogic服务器版本
前人经验:使用t3 9.2.0nAS:255nHL:19nn字符串作为T3的协议头发送给weblogic9、weblogic10g、weblogic11g、weblogic12c均合法。
检测代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os
import socket
import time

hello = b't3 10.3.6\nAS:255\nHL:19\n\n'

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect(('192.168.22.137',7001))
sock.send(hello)
time.sleep(1)
resp1 = sock.recv(1024)

print(resp1)

T3攻击方式

  • 第一种:将weblogic发送的java序列化数据的地2到第7部分的反序列化数据进行替换

2099765-20210307161834472-667120481.png

  • 第二种:将weblogic发送的JAVA序列化数据的第一部分与恶意的序列化数据进行拼接。也就是替换第一部分的数据

2099765-20210307162400442-528006101.png

2099765-20210307163345406-1392976202.png

以上来自网上文献:http://drops.xmd5.com/static/drops/web-13470.html

T3反序列化漏洞

上面,我详细解释了WebLogic RMI的调用过程,我们初窥了一下T3协议。那么现在我们来仔细看一下刚才抓到的正常WebLogic RMI调用时T3协议握手后的第一个数据包,有几点值得注意的是:

  • 我们发现每个数据包里不止包含一个序列化魔术头(0xac 0xed 0x00 0x05)
  • 每个序列化数据包前面都有相同的二进制串(0xfe 0x01 0x00 0x00)
  • 每个数据包上面都包含了一个T3协议头
  • 仔细看协议头部分,我们又发现数据包的前4个字节正好对应着数据包长度
  • 以及我们也能发现包长度后面的“01”代表请求,“02”代表返回

64.png

这些点说明了T3协议由协议头包裹,且数据包中包含多个序列化的对象。那么我们就可以尝试构造恶意对象并封装到数据包中重新发送了。流程如下:

65.png

替换序列化对象示意图如下:

66.png

剩下的事情就是找到合适的利用链了(通常也是最难的事)。

参考文章

-------- 本文结束 感谢阅读 --------

本文标题:weblogicT3协议及T3反序列化漏洞简介

文章作者:FunctFan

发布时间:2021年06月22日 - 04:05:28

最后更新:2021年06月22日 - 21:25:43

原始链接:https://functfan.github.io/posts/914084023/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。