0%

从头开始挖掘Java RMI漏洞

引言

关于Java RMI 的内容反反复复也是看了很多到了,它在我的日常工作中更多的是作为一个知识点而存在,时不时就得拿出来温习一下,时间久了就会被遗忘掉具体的细节。
但这个概念也算是Java漏洞利用中的一个明星概念以及有一定复杂度与难度的知识点了,也是面试中的常客(虽然已经很久没有参加面试了,不过我想应该是这样的,毕竟Java安全的东西说破天也就那么多)

RMI介绍

RMI 顾名思义即远程方法调用,用户通过一些指定查找远程服务器上的对象方法并实施调用,概念是很清楚的,但其实现的机制却不简单。
在Java的RMI实现中涉及到三方成员,即调用客户端,远程注册中心以及远程服务提供者。调用客户端很容易理解即RMI调用的发起者,远程服务提供者也不难理解即提供RMI服务的主体,
远程注册中心想必也并不复杂即远程服务提供者将其能够提供的服务注册到注册中心的注册表中供调用客户端进行查询。我们换个更加通俗一点的说法可能更易理解。
假如我是一个中间商,我提供的服务是联系消费者与供应商,我这里有一张表,这张表上记录了每个供应商拥有的商品的类型以及价格等信息,这些信息是我的核心商业秘密,一般消费者是无法获取到这些信息的。
你是一个小市民,当牛做马10多年终于凑够的房子的首付想要购入一套新房但是你不知道要向谁购买,你机缘巧合下找到了我这个中间商,我告诉你我这里有很多房地产商的联系方式,你只需要付给我10%房价的佣金(黑心)
我就把他们的联系方式告诉你,你虽然万般不愿但也没有更好的办法只能接受我的剥削,我们的交易最终达成,你拿着从我这里买来的联系方式找到房地产开发商购买商品房。
最终我们实现了三赢,你买到了房子,我赚了佣金,房地产开发商得到了现金流。
在这个例子中,你就是远程调用者,我就是注册中心,房地产开发商就是服务提供者,我们的关系也就是RMI中三个关键概念的关系。

RMI调用实例

首先创建一个远程对象,其需要继承UnicastRemoteObject类并实现一个Remote类的子接口。
继承UnicastRemoteObject类是为了在创建该远程对象实例的时候能够调用到继承UnicastRemoteObject的构造方法从而完成远程对象服务器的监听以及对象的导出,当然这都是后话,后面会进行详细介绍。
实现Remote子接口是因为该接口起到一个标志作用,表明该实现类是可以被远程调用的。
而远程对象类为什么实现的是Remote的子类而不是直接实现Remote接口则是因为调用客户端在获得远程对象引用的存根对象时得到的是一个动态代理对象,该对象会被客户端映射为远程的对象直接调用其对应的方法,
Remote类默认是不存在这些方法的,所以我们需要一个接口先将这些方法进行生命以便调用客户端能够对获得的远程对象存根进行强制类型转换,这一点我们在客户端的创建代码中可以看到。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class BindObject extends UnicastRemoteObject implements CustomRemote {
protected BindObject() throws RemoteException {
super();
}
public String sayHello() throws RemoteException {
return "Hello, world!";
}
}

创建Remote接口的子接口CustomRemote
该接口定义了方法sayHello,该方法也就是可以被客户端调用的方法。

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

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

创建注册中心

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

public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
// 创建远程对象
BindObject bindObject = new BindObject();
// 创建注册中心
Registry registry = LocateRegistry.createRegistry(1099);
// 绑定远程对象
registry.bind("test", bindObject);
}
}

创建调用客户端

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

public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
// 获取到注册中心的存根
Registry registry = LocateRegistry.getRegistry(1099);
// 通过注册中心存根据远程对象名称查询远程对象获取到远程对象的引用
// 获取到的是一个动态代理对象 每一个方法的调用都要通过invocationHandler的invoke方法 这很重要
CustomRemote test = (CustomRemote) registry.lookup("test");
// 调用远程对象方法
String s = test.sayHello();
}
}

远程服务提供者是如何启动的

我们在 RMI调用实例 中提到过远程服务提供者需要继承UnicastRemoteObject类,其目的是为了能够调用该类的构造方法从而完成远程对象服务器的监听以及对象导出,我们分析的起点也就是该类的构造方法。
此处调用了UnicastRemoteObject的有参构造方法并传入了一个参数0,该参数表示监听的端口,0即由系统自动分配一个端口。

1
2
3
4
protected UnicastRemoteObject() throws RemoteException
{
this(0);
}

接下来调用了exportObject方法,顾名思义这是一个与远程对象导出有关的方法,其接收两个参数,第一个参数传入我们实例化的远程对象本身,第二个参数为端口。

1
2
3
4
5
protected UnicastRemoteObject(int port) throws RemoteException
{
this.port = port;
exportObject((Remote) this, port);
}

exportObject方法继续调用了其重载方法,该重载方法接受两个参数,第一个参数由外层方法进行原样传递,第二个参数被封装到UnicastServerRef对象中。
UnicastServerRef的实例对象是对远程对象服务器的启动极为重要的一个类,包括对象导出的具体逻辑、接受消息的消息分发都与该类有关。
此处在进行实例化的时候主要作用是将port参数封装到LiveRef实例对象中,并将该LiveRef实例赋值给UnicastServerRefref成员变量。
LiveRef对象中则主要是通过port参数创建了服务器监听的Endpoint对象并实例化了一个ObjID用于唯一标识正在创建的远程对象。
具体的代码就不放了,就是一些new以及赋值操作。

1
2
3
4
5
public static Remote exportObject(Remote obj, int port)
throws RemoteException
{
return exportObject(obj, new UnicastServerRef(port));
}

在重载的exportObject方法中,首先会对输入的远程对象进行类型判断,如果其继承了UnicastRemoteObject类则将刚才创建的单播服务引用UnicastServerRef
设置到该对象的ref成员变量中。 然后会调用UnicastServerRefexportObject方法。

1
2
3
4
5
6
7
8
9
private static Remote exportObject(Remote obj, UnicastServerRef sref)
throws RemoteException
{
// if obj extends UnicastRemoteObject, set its ref.
if (obj instanceof UnicastRemoteObject) {
((UnicastRemoteObject) obj).ref = sref;
}
return sref.exportObject(obj, null, false);
}

UnicastServerRefexportObject方法中,会获取到正在创建的远程对象的Class对象,然后调用Util.createProxy方法创建一个当前正在创建的远程对象的动态代理对象。
然后判断该代理对象是否是RemoteStub的子类,如果是则会为当前正在创建的远程对象创建一个存根。接着将获取到的代理对象封装到Target对象中,并继续执行导出动作。
在进入到后面到导出操作前,这里我们需要重点关注动态代理对象的创建、存根对象的创建、Target对象的创建以及在导出后的方法hash计算的过程。

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;
}

首先我们关注动态代理对象的创建过程,这里会调用Util.createProxy方法,该方接受三个参数,第一个参数为当前正在创建的远程对象的Class对象,第二个参数由getClientRef方法获取,
该方法将当前UnicastServerRefref成员变量封装到一个UnicastRef对象中,从名字中可以看出如果UnicastServerRef表示服务端的引用那么UnicastRef则表示客户端的引用,
第三个参数表示是否强制使用存根,默认为false
进入到createProxy方法中,该方法首先调用getRemoteClass方法获取传入参数的所有父类中直接实现了Remote的子接口的类,这也是为什么我们前面不直接实现Remote接口而是先创建其子接口再实现的原因。
在调用getRemoteClass方法时我们传入的是BindObjectClass对象,因为BindObject实现了Remote的子接口,所以这里会直接返回BindObjectClass对象。
紧接着会遇到一个判断结构,这里重点关注stubClassExists方法的运行结构,因为ignoreStubClasses默认为false,根据运算符的优先级and会优先于or执行,所以这里的判断结果取决于stubClassExists的运算结果。
stubClassExists方法会判断传入的var3的类名拼接_Stub作为新的类名的类是否存在,若存在则返回true,若不存在则返回false
此处我们传入的是自定义的类,很明显其存根类是不存在的,所以这里会返回false,代码也会进入到else的逻辑中。那么当var3为什么时会进入到if紧跟着的代码块中呢。
var3表示RegistryImplClass对象时会出现这样的情况,这将发生在注册中心的创建过程中,我们后面会介绍到的。
else紧跟着的代码块中,我们创建了UnicastRef对象的动态代理对象,使用的invocationHandlerRemoteObjectInvocationHandler,这一点是极为重要的,
因为在动态代理对象执行过程中invocationHandlerinvoke方法会影响原本对象方法执行的行为,请记住这一点。

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);
}
}
}

对于用户创建的用户注册的远程对象来说,调用createProxy方法后获得的是一个UnicastRef的动态代理对象,该对象封装了一个LiveRef对象,
而这个LiveRef对象中又封装了一个ObjID对象,这个ObjID正是当前创建远程对象的唯一标识符,在后面的操作中调用客户端需要以来这个ObjID来从导出表中查找对应的远程对象。
setSkeleton方法的调用需要特殊的时机,即createProxy创建的存根对象实现了RemoteStub类,很明显我们自行创建的远程对象的存根对象是没有实现这个类的,那么也只有前面提到的RegistryImpl_Stub对象会实现这个类了。
setSkeleton方法中,其会创建RegistryImpl的骨架,即创建RegistryImpl_Skel对象并存储到UnicastServerRefskeleton成员变量中。

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
public void setSkeleton(Remote var1) throws RemoteException {
if (!withoutSkeletons.containsKey(var1.getClass())) {
try {
this.skel = Util.createSkeleton(var1);
} catch (SkeletonNotFoundException var3) {
withoutSkeletons.put(var1.getClass(), (Object)null);
}
}

}
static Skeleton createSkeleton(Remote var0) throws SkeletonNotFoundException {
Class var1;
try {
var1 = getRemoteClass(var0.getClass());
} catch (ClassNotFoundException var8) {
throw new SkeletonNotFoundException("object does not implement a remote interface: " + var0.getClass().getName());
}

String var2 = var1.getName() + "_Skel";

try {
Class var3 = Class.forName(var2, false, var1.getClassLoader());
return (Skeleton)var3.newInstance();
} catch (ClassNotFoundException var4) {
throw new SkeletonNotFoundException("Skeleton class not found: " + var2, var4);
} catch (InstantiationException var5) {
throw new SkeletonNotFoundException("Can't create skeleton: " + var2, var5);
} catch (IllegalAccessException var6) {
throw new SkeletonNotFoundException("No public constructor: " + var2, var6);
} catch (ClassCastException var7) {
throw new SkeletonNotFoundException("Skeleton not of correct class: " + var2, var7);
}
}

BindObject对象即是我们长在创建的远程对象,后面不在使用正在创建的远程对象这个说法而直接使用BindObject对象代替。
在进行Target对象的封装时,首先会创建BindObject对象的弱引用, 创建时传入的第二个参数是一个队列,当BindObject对象被GC回收时,会将弱引用对象加入到这个队列中,
通过对这个队列的监听可以定制某个对象被回收时的行为,这是与DGC有关的内容了这里不进行详述,后面会再开一篇专门介绍RMI中的DGC
传入的第二个参数为UnicastServerRef对象本身,其此时作为消息的分发器被传递,后面当服务器接受到客户端的消息时将由该对象对消息进行分发与处理。
传入的第三个参数作为BindObject对象的存根,其本身是一个UnicastRef的动态代理对象。
传入的第四个参数是ObjID对象,其表示远程对象的唯一标识符。
传入的第五个参数表示远程对象是否是持久的,如果为true则表示远程对象是持久的,否则表示远程对象是临时的。我们自行创建的远程对象该参数均为false,即不是持久的对象。
其作为弱引用被Target对象引用,当没有其他对象引用时将被GC回收掉,而持久存在的对象则只有系统创建的RegistryImpl以及DGCImpl对象。
pinImpl方法就是用来进行对象持久化的方法,其本质就是创建了一个使用=号的赋值操作来对远程对象进行引用,只要Target对象不被回收,该远程对象将一直存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();
}

}

尾巴处理完了,我们接着看对象的导出操作是什么样的。
ep以及transport都是再LiveRef实例化过程中创建的对象,需要了解的可以倒回去看看。

1
2
3
4
5
6
7
8
9
public void exportObject(Target var1) throws RemoteException {
this.ep.exportObject(var1);
}

public void exportObject(Target var1) throws RemoteException {
this.transport.exportObject(var1);
}


下面就是最为核心的对象导出方法了,该方法包含重要的两个步骤,其一是完成serverSocket服务的建立,其二是进行真正的对象导出。

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();
}
}

}

首先关注serverSocket的建立。

注册中心是如何启动的

客户端获取注册中心存根

客户端查询远程对象

客户端调用远程对象方法

DGC

Buy me a coffee.

欢迎关注我的其它发布渠道