rmi_1

rmi_1

版权申明:本文为原创文章,转载请注明原文出处

原文链接:http://example.com/post/a787da62.html

rmi_1

初识RMI

  • 概念:Java RMI (Remote Method Invocation) 是一种用于构建分布式应用程序的机制,允许Java对象调用远程服务器上的方法。
  • demo

    RMIClient:

    1
    2
    3
    4
    5
    6
    //  客户端通过LocateRegistry.getRegistry方法获取Registry对象
    Registry registry = LocateRegistry.getRegistry("localhost", 1099);
    // 客户端通过lookup方法获取远程对象
    IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
    // 客户端调用远程对象的方法
    System.out.println(remoteObj.sayHello("Hello, World!"));

    RMIServer:

    1
    2
    3
    4
    5
    6
    // 服务端创建远程对象
    IRemoteObj remoteObj = new RemoteObjImpl();
    // 服务端创建Registry对象
    Registry registry = LocateRegistry.createRegistry(1099);
    // 服务端将远程对象绑定到Registry对象
    registry.bind("remoteObj", remoteObj);
  • 安全问题

    1. RMI允许动态加载类,这可能被滥用进行代码注入攻击。恶意服务器可以发送恶意的类文件,导致客户端执行任意代码。
    2. RMI依赖Java的序列化机制,将对象序列化为字节流进行传输。反序列化过程中如果处理不当,可能导致反序列化漏洞,使攻击者可以发送恶意数据,从而在目标系统上执行任意代码。
  • 组成

    • 注册中心:哈希表,Naming->远程对象
    • 服务端:用代理Skeleton(进行网络请求)
    • 客户端:连接注册中心,用代理Stub(进行网络请求)
  • 通信

    • 服务端->注册中心:绑定、重绑定
    • 注册中心->服务端:RMI注册服务反向生成
    • 客户端->注册中心:Naming
    • 注册中心->客户端:获取Stub
    • 服务端<->客户端:JRMP通信

服务端

服务端创建远程服务

服务端攻击注册中心:一句话逻辑解释:服务端调用bind(name,obj)注册远程对象,其中name,obj会以序列化方式发送给registry,registry反序列化它们,触发boom💣。

创建代码为 IRemoteObj remoteObj = new RemoteObjImpl(); ,实现类继承了UnicastRemoteObject类,如果服务端要将对象发布,也就是导出到注册端上去给客户端使用的话,那么该导出对象就需要继承UnicastRemoteObject,此时的端口设置为0

UnicastRemoteObject 是 Java RMI(Remote Method Invocation)中用于创建单播远程对象的一个类。单播远程对象是指在某一时刻只能由一个客户端调用的远程对象。这个类提供了基本的功能来导出远程对象,使其可以被远程访问。

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

此处的exportobject在指定端口上导出一个远程对象

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

此处的UnicastServerRef是服务端对于网络请求的封装对象,实际上服务端发起的请求先是通过UnicastServerRef操作,而UnicastServerRef中操作是通过StreamRemoteCall来进行请求,继承的是LiveRef类的方法

1
2
3
public UnicastServerRef(int port) {
super(new LiveRef(port));
}

LiveRef类中新建了对象ID,并传入端口号

1
2
3
public LiveRef(int port) {
this((new ObjID()), port);
}

这里port传入了TCPEndpoint类的getLocalEndpoint方法,猜测这里是建立连接

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

getLocalEndpoint方法中增加了两个参数调用了同名方法,调用的getLocalEndpoint是获取本地IP值和传入的端口号,此时端口号还是0

1
2
3
public static TCPEndpoint getLocalEndpoint(int port) {
return getLocalEndpoint(port, null, null);
}

返回到上面LiveRefthis方法中,此时LiveRef对象中的参数如下

image-20240614141242918

返回到上面的UnicastServerRefsuper方法中,此处UnicastServerRef对象的参数如下

image-20240614141424469

返回到UnicastRemoteObjectexportObject方法中,这里会调用传入的UnicastServerRef对象的exportObject方法

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

UnicastServerRef对象的exportObject方法中实现了创建RemoteObjImpl对象的动态代理stub

1
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);

这里的getClientRef()会新建一个UnicastRef对象,而这里的ref就是创建的LiveRef对象,UnicastRef是客户端对于网络请求的封装对象,实际上客户端发起的请求先是通过UnicastRef操作,而UnicastRef中操作是通过StreamRemoteCall来进行请求

1
2
3
4
5
6
7
protected RemoteRef getClientRef() {
return new UnicastRef(ref);
}
// ------------------------------------
public UnicastRef(LiveRef liveRef) {
ref = liveRef;
}

上面的stub就是为getClientRef()的返回创建一个代理对象,stub的数据如下

image-20240614142318346

然后新建target对象,传入参数分别为RemoteObjImpl实现类(包含p~ort和UnicastServerRef对象),UnicastServerRef对象本身,UnicastRef代理对象,LiveRefobjIDpermanentfalse,代表非永久

1
2
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);

然后调用LiveRef对象的exportObject方法

1
ref.exportObject(target);

然后调用ep(TCPEndpoint对象)的exportObject方法

1
2
3
public void exportObject(Target target) throws RemoteException {
ep.exportObject(target);
}

然后调用TCPTransport的方法,TCPTransport是在上面TCPEndpointgetLocalEndpoint定义的,定义的参数包括ip和端口号,应该是用于建立连接的

1
2
3
public void exportObject(Target target) throws RemoteException {
transport.exportObject(target);
}

这是TCPTransport对象的参数信息

image-20240614143127357

TCPTransportexportObject方法中实现了listen进行端口监听

image-20240614143326427

listen函数中实现了创建服务端socket,创建新进程

image-20240614143425352

TCPTransport对象的变量更新如下

image-20240614143618651

至此,服务端远程服务创建完成,总的来说先确定IP,然后逐步创建客户端代理Stub(为了客户端能通过该代理和服务端通信),然后创建连接socket

创建注册中心+绑定

服务端创建注册中心和创建远程访问的流程类似,都是在某端口开启网络通信服务,个别参数有一定区别,创建注册中心指定端口号为1099(默认)

1
2
// 服务端创建Registry对象
Registry registry = LocateRegistry.createRegistry(1099);

LocateRegistry对象的createRegistry会创建RegistryImpl接口对象,并传入端口号1099

1
2
3
public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}

RegistryImpl类的创建方法中,会创建LiveRef对象,该对象的创建中会通过getLocalEndpoint方法确定IP和端口号,以及初始化后面会使用的TCPTransport对象(无socket

image-20240614151130924

返回的lref的返回如下

image-20240614151343681

接着将lref作为参数传递到UnicastServerRef的新建方法中,和服务端创建远程服务类似,主要看此处的setup方法,在该方法中调用了UnicastServerRef对象的exportObject函数,此处的permanent参数传递为true代表永久

1
2
3
4
5
6
7
8
9
private void setup(UnicastServerRef uref)
throws RemoteException
{
/* Server ref must be created and assigned before remote
* object 'this' can be exported.
*/
ref = uref;
uref.exportObject(this, null, true);
}

UnicastServerRef对象的exportObject函数中创建代理,getClientRef()返回的是UnicastRef对象(包含创建的LiveRef对象)

image-20240614151900691

但是和创建远程服务不同,此处的stub返回的并不是一个代理,在createProxy方法中有一个if语句

1
2
3
4
5
if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}

如果remoteClass类作为参数传递的stubClassExists中判断remoteClass_stub类存在,就不通过代理创建方式创建stub,但是注册接口类RegistryImpl_stub是存在的,因此就会进入此处的if循环,进入此处的createStub方法,然后就会获取RegistryImpl_stub类的实例对象,而非代理对象,并以创建的UnicastRef对象作为实例创建参数

最后stub返回的是RegistryImpl_stub实例对象,接着又会进入此处if函数,执行setSkeleton函数,以RegistryImpl对象作为参数传递

1
2
3
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

接着又调用UtilcreateSkeleton方法

image-20240614152708191

相当于就是创建RegistryImpl_Skel的实例对象

image-20240614152808214

然后将设置好的

  • impl(RegistryImpl类,包含Hashtable类的bindings参数和UnicastServerRef类的ref参数,其中ref包含创建好的skel(RegistryImpl_Skel对象),ref(LiveRef对象)),

  • this(UnicastServerRef类,包含创建好的skel(RegistryImpl_Skel对象),ref(LiveRef对象)),

  • stub(ResitryImpl_Stub类,包含UnicastRef类的ref参数,ref包含创建的LiveRef对象),

作为参数新建Target变量,然后通过LiveRef类的exportObject方法建立socket

1
2
3
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);

然后就是绑定方法bind的使用,这里的registry就是创建的RegistryImpl_Stub类的实例对象

1
2
// 服务端将远程对象绑定到Registry对象
registry.bind("remoteObj", remoteObj);

此处的binding初始化为空,因此会将传入的对象和name加入到binding列表中

1
2
3
4
5
6
7
8
9
10
11
public void bind(String name, Remote obj)
throws RemoteException, AlreadyBoundException, AccessException
{
checkAccess("Registry.bind");
synchronized (bindings) {
Remote curr = bindings.get(name);
if (curr != null)
throw new AlreadyBoundException(name);
bindings.put(name, obj);
}
}

至此,服务端代码介绍完毕

客户端

客户端请求注册中心

首先指定hostport获取Registry对象

1
2
//  客户端通过LocateRegistry.getRegistry方法获取Registry对象
Registry registry = LocateRegistry.getRegistry("localhost", 1099);

再次调用同名函数getRegistry

1
2
3
4
5
public static Registry getRegistry(String host, int port)
throws RemoteException
{
return getRegistry(host, port, null);
}

然后新建LiveRef对象,传入IPport,再以创建的LiveRef作为参数创建UnicastRef对象,然后创建代理对象,相当于这里的获取实际上并不是获取,而是通过传入的参数在本地创建好RegistryImpl_Stub类的实例对象

1
2
3
4
5
6
7
8
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);

接着通过lookup寻找远程对象

1
2
//  客户端通过lookup方法获取远程对象
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");

lookup中会调用UnicastRefnewCall,在newCall方法中会根据传入的参数创立连接,同时在lookup中将传入的远程对象名字作为参数传递到建立的连接对象的序列化的函数中,意味着是将该参数进行序列化传递给注册中心,注册中心会负责进行反序列化

image-20240614163338442

返回的参数var2如下

image-20240614163025294

然后会调用invoke方法,invoke方法中会调用 call.executeCall(); ,该方法就是处理客户端网络请求的方法

接着客户端会接受注册中心的返回结果,同样,要进行反序列化才能获取

image-20240614163547092

因此攻击手段可以考虑,模拟注册中心,向客户端发送有危害的序列化代码,客户端在进行反序列化时则会产生漏洞

另外在invoke方法中,当产生异常TransportConstants.ExceptionalReturn时也会调用readObject方法

1
2
3
4
case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject();

因此只要有invoke方法或者readObject方法都有可能产生威胁

客户端请求服务端

此处客户端通过lookup方法获取远程对象remoteObj是一个动态代理对象,因此会执行UnicastRefinvoke方法

1
2
//  客户端调用远程对象的方法
System.out.println(remoteObj.sayHello("Hello, World!"));

invoke方法中,首先创建连接

image-20240614164948844

然后创建远程调用方法对象,然后将要传递的变量通过marshaValue进行序列化

image-20240614165259985

marshaValue中就是对参数进行序列化

image-20240614165327093

然后又执行网络请求(这里主要是处理客户端和服务端通信的JRMP协议,当出现指定异常时可利用)

接着获取返回值然后通过unmarshalValue进行反序列化

image-20240614165519789

客户端-服务端

客户端请求注册中心时的服务端

服务端在创建注册中心时实际上会将创建的所有东西都放在target中,然后创建开启网络监听,创建新线程,会新建skel实例,因此该实例应是服务端用于接收客户端的请求的,当客户端通过lookup寻找远程服务时,服务端应收到请求并返回远程服务对象,因此,主要查看RegistryImpl_Skel类定义的函数,dispatch 函数的主要作用是接收并处理从客户端发送过来的请求。根据请求的内容,调用相应的方法来执行

image-20240615143550267

现在查看服务端创建远程对象时socket创建新进程的流程,也就是TCPTransport的listen函数,在这里面会创建新进程,执行类为AcceptLoop

image-20240616101922514

查看AcceptLoop的run方法

image-20240616102003153

查看executeAcceptLoop方法,这里面又执行了ConnectionHandler类

image-20240616102039718

ConnectionHandler类的run方法执行了run0方法,run0方法中有一个读取input messages的函数handleMessages,这里面应该就是读取客户端信息的函数,咋子handleMessages函数中会根据传输的不同字段值调用不同函数,默认是serviceCall函数

image-20240616102632213

在serviceCall函数中会获取创建过的target,此时的target

image-20240616131547037

在此处增加断点,并发起客户端请求,接着会获取target里面的分发器,这时候获取到的分发器就是有skel的UnicastServerRef类对象,应该是处理客户端请求

image-20240616102946645

然后会调用disp(分发器)的dispatch方法

image-20240616103126724

在dispatch中如果skel并不是null就会调用oldDispatch方法,oldDispatch方法中会调用skel的dispatch方法

1
2
3
if (skel != null) {
oldDispatch(obj, call, num);
return;

在skel的dispatch方法中,执行语句如下,当var3为0,代表绑定命令,1代表list命令,2代表lookup(从注册中心寻找对应的远程对象)...传入lookup的参数var7是通过反序列化读出来的,该处有被攻击可能性,此处所有的case都有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
switch (var3) {
case 0:
//..
var6.bind(var7, var8);
//...
case 1:
//...
String[] var97 = var6.list();
//...
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);
//...
case 3:
//...
var6.rebind(var7, var8);
//...
case 4:
//...
var6.unbind(var7);
//...
default:
throw new UnmarshalException("invalid method number");
}

客户端请求服务端时服务端

还是一样的运行流程,不同的是此时获取的target是如下信息,其中的stub变成了动态代理,也就是服务端创建远程服务建立的动态代理,前面的target是服务器创建注册中心时创建的RegistryImpl_stub类的新实例

image-20240616131710853

此时的skel为null,此处不执行oldDispatch函数

image-20240616132405085

然后获取method方法,并调用logCall记录

image-20240616132501602

接着获取参数类型,并获取传递过来的参数,此处的unmarshalValue和上面分析的一样,是对参数进行反序列化(可利用)

image-20240616132558249

然后调用invoke函数执行方法`

1
2
3
4
5
try {
result = method.invoke(obj, params);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}

最后把返回值序列化后传递回去

1
2
3
4
5
ObjectOutput out = call.getResultStream(true);
Class<?> rtype = method.getReturnType();
if (rtype != void.class) {
marshalValue(rtype, result, out);
}

DGC(客户端请求服务端)

在前面的过程中建立了DGCImpl 类,这是 Java RMI 框架中的一个类,代表分布式垃圾回收 (Distributed Garbage Collection, DGC) 的实现。dispatch 函数在 DGCImpl_Skel 类中主要负责处理与分布式垃圾回收相关的远程方法调用。

分布式垃圾回收用于管理在分布式系统中引用对象的生命周期。为了实现这一点,DGCImpl 类通常提供的方法包括:

  1. dirty:用于注册一个对象的引用。
  2. clean:用于注销一个对象的引用。

特点:自动生成,只要创建了远程对象就一定会创建DGC

创建完远程对象,会把所有的变量放在target中,然后放在一个静态表中

1
2
3
4
5
6
7
8
9
10
public void exportObject(Target target) throws RemoteException {
target.setExportedTransport(this);
ObjectTable.putTarget(target);
}

static void putTarget(Target target) throws ExportException {
//...
objTable.put(oe, target);
implTable.put(weakImpl, target);
//...

但在put之前,objTable表中已经有了一个Target类,也有一个stub(DGCImpl_Stub),说明在这之前已经创建了DGCImpl_Stub实例对象

image-20240617211152942

回过头看DGCImpl_Stub实例对象的创建过程,事实上,在putTarget函数中就有该过程

image-20240617211342125

这里的dgcLog是DGCImpl类的静态变量,当引用静态变量时,会执行静态代码块static{}里的内容,查看DGCImpl的static{}里的内容,也就是会执行此处的run函数

image-20240617211647714

在run函数后面,会新建DGCImpl类对象,并新建LiveRef类对象,接着新建UnicastServerRef对象,并设置stub,设置skel,新建target,并通过put将target放入静态表中,这里创建的DGCImpl的功能和注册中心是一样的,也是有一个端口,建立的是实例对象而非动态代理对象,用于远程回收服务,调用时也是一样的,会调用其disp的dispatch方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dgc = new DGCImpl();
ObjID dgcID = new ObjID(ObjID.DGC_ID);
LiveRef ref = new LiveRef(dgcID, 0);
UnicastServerRef disp = new UnicastServerRef(ref);
Remote stub =
Util.createProxy(DGCImpl.class,
new UnicastRef(ref), true);
disp.setSkeleton(dgc);

Permissions perms = new Permissions();
perms.add(new SocketPermission("*", "accept,resolve"));
ProtectionDomain[] pd = { new ProtectionDomain(null, perms) };
AccessControlContext acceptAcc = new AccessControlContext(pd);

Target target = AccessController.doPrivileged(
new PrivilegedAction<Target>() {
public Target run() {
return new Target(dgc, disp, stub, dgcID, true);
}
}, acceptAcc);

ObjectTable.putTarget(target);

DGCImpl_stub(客户端)

dirty函数中,调用了invoke(存在风险)

image-20240617212457265

除此之外还有反序列化漏洞

image-20240617212619027

clean函数中,也调用了invoke(存在风险)

image-20240617212533978

DGCImpl_skel(服务端)

同样存在漏洞

image-20240617212724556image-20240617212745327

JDK高版本绕过

约束输入流类型,只有符合白名单的才允许被反序列化,在RegistryImpl类中定义了registryFilter方法

image-20240617213238832

在DGC中也有了限制

1
2
3
4
5
6
return (clazz == ObjID.class ||
clazz == UID.class ||
clazz == VMID.class ||
clazz == Lease.class)
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;

考虑利用此处的UnicastRef类,存在invoke,存在jrmp client反序列化攻击,所有rmi客户端

image-20240617213734213

思考目标:在服务端开启客户端请求,导致服务端被攻击,也就是怎么在服务端调用UnicastRef类的invoke函数

image-20240622154350020

调用invoke的对象大多是Registry_stub和DGCImpl_stub里,stub对象是在createPorxy函数中调用的,而前者的createproxy是在exportObject创建注册中心时创建的,后者在创建远程服务就已经创建了,在DGC的静态代码块和DGCClient类的EndPointEntry函数中都有调用

image-20240617215553337

我们要找到一个反序列化点,通过反序列化能够实现调用此处的函数创建DGCImpl_stub对象,并调用invoke方法

  1. 在服务端上创建DGCClient对象,创建dgc对象
  2. 让dgc对象在服务端上发起客户端请求,执行invoke函数

创建dgc对象

首先找调用DGCClient类的EndPointEntry函数的位置,回溯到StreamRemoteCall的releaseInputStream方法,而这个函数是在skel里被调用,所以可利用,下图是releaseInputStream里调用的registerRefs方法,首先需要保证!incomingRefTable.isEmpty()结果为真,也就是incomingRefTable不为空

image-20240622161211181

incomingRefTable是在saveRef中被赋值,saveRef在LiveRef的read中被调用,read方法被UnicastRef的readExternal方法调用,readExternal和readObject类似,在反序列化的时候会被调用,只要实现反序列化UnicastRef对象,就可以实现创建DGC对象,将序列化后的UnicastRef对象传递到RegistyImpl中,由于UnicastRef在白名单,因此可以被正常反序列化,最终实现DGC对象的创建

调用invoke

在DGCClient中创建dgc对象之后,进入了新的线程函数RenewCleanThread

image-20240622162542059

在RenewCleanThread中调用了makeDirtyCall函数,makeDirtyCall函数中调用了dirty函数,由此来触发一个客户端请求,从而导致反序列化攻击

Author

yyyyyyxnp

Posted on

2024-06-13

Updated on

2024-09-29

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.