Java là một ngôn ngữ lập trình hướng đối tượng, chính vì thế khi có nhu cầu trao đổi dữ liệu dưới dạng các object thì các format thông thường như JSON hay XML sẽ không thể nào làm tốt, do đó ta cần sử dụng serialized data để thực hiện việc trao đổi dữ liệu của object dưới dạng bytes
Ví dụ code serialize và deserialize
// 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;
}
...
}
// 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 sẽ khởi tạo object của class User
sau đó dùng method writeObject
để serialize và ghi vào file data.ser
Tiếp theo sử dụng method readObject
để deserialize và in ra console
Sau khi đoạn code trên thực thi ta sẽ nhận được kết quả:
Ta có thể thấy format serialized data trong Java khác hoàn toàn so với serialized data trong PHP thông qua file data.ser
<!-- Serialized data trong PHP -->
O:4:"User":2:{s:10:"\0User\0name";N;s:9:"\0User\0age";N;}
Serialized data của Java sẽ có cấu trúc như sau
Note: Kiểu dữ liệu của thuộc tính age là
I
(interger), còn giá trị của thuộc tính age là 20 tương đương với0x14
Ta có thể dùng jdeserialize-1.2.jar
để hiển thị serialized data một cách dễ nhìn hơn
Và cũng tương tự như lỗ hổng Unsafe Deserialize trong các ngôn ngữ khác thì Java Deserialized Vulnerability sẽ xảy ra khi untrusted data rơi vào quá trình serialize và deserialize, attacker có thể kiếm soát Object được deserialized từ đó dẫn đến nhiều hậu quả khác nhau, tùy theo logic code của chương trình
Reflection là một khái niệm quan trọng trong khai thác lỗ hổng Java deser, tưởng tượng ta tìm được một chain có thể dẫn đến RCE, tuy nhiên muốn làm được ta phải thay đổi được giá trị của một property private của một class trong chain đó.
Để làm được vậy ta sẽ dùng đến Reflection
đây là một API cho phép thay đổi giá trị của các thuộc tính trong quá trình Runtime, nó thường được dùng để set giá trị cho các private properties của class
Ví dụ:
Ta có class User như sau
public class User {
public String name;
private boolean isAdmin = false;
public User(String name) {
this.name = name;
}
...
}
Để thay đổi giá trị của isAdmin
mà không cần dùng đến setter ta sẽ dùng Reflection API
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws IllegalAccessException {
User a = new User("endy");
Field f = null;
try {
f = a.getClass().getDeclaredField("isAdmin");
f.setAccessible(true);
f.set(a, true);
}
catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
System.out.println(a.isAdmin());
}
}
Kết quả:
Ngoài ra Reflection còn được sử dụng trong trường hợp ta muốn gọi method của một class nhưng không muốn tạo instant của class đó, ví dụ có một private class XY
và ab
là public method của class XY
. Ta muốn sử dụng ab
nhưng không muốn tạo instant của XY
nên sẽ dùng Reflection
Syntax:
<Object>.class.getMethod("<method name>").invoke(agrs);
Ở trên là những gì cơ bản nhất về serialized/deserialized và Reflection trong java, phần quan trọng nhất là làm sao từ một methods deserialize như readObject
mà dẫn đến RCE, writefile hay bất kỳ thứ gì mà ta muốn.
Ta sẽ sử dụng gadgetchain để làm được điều đó, có thể ví dụ các object, method như là các đoạn ống nước, ta phải nối chúng lại làm sao để nước có thể đi từ source đến sink, khiến untrusted data rơi vào method mà ta muốn thực thi. Đoạn đường ống nước nối từ source đến sink chính là gadgetchain
Nhắc đến Java Deser mà không nhắc đến ysoserial
thì rất thiếu sót, ysoserial
là một tool cung cấp cho ta một số gadgetchain phổ biến trong thư viện của Java
Để sử dụng thì ta chỉ cần gọi tên gadgetchain nó sẽ tự động gen payload cho ta
Tiếp theo mình sẽ phân tích lại một gadgetchain đơn giản là URLDNS
. Cách phân tích và các bước mình tham khảo theo blog này và blog này. Các bạn có thể xem bài viết góc để dễ hiểu hơn, ở đây mình chỉ phân tích lại để phục vụ nhu cầu của mình thôi
Gadgetchain này sẽ gửi DNS query đến URL mà ta truyền vào, thông thường gadgetchain này được sử dụng để test xem target có thể deserialize object được hay không. Đây cũng là gadgetchains đơn giản và dễ phân tích nhất trong ysoserial
Đầu tiên ta sẽ thử gen payload để sử dụng
Vậy thì gadgetchain này bao gồm những gì?
Tại src gen payload của ysoserial
ta biết được chain này bao gồm:
Ta cũng biết được cách ysoserial
tạo payload
Đầu tiên object của class URL
được tạo với url
là input của ta, handler
là object của class URLStreamHandler
Sau đó class u
được put vào hashmap
Và cuối cùng là dùng Reflecton
để thay đổi giá trị của property private hashCode
Sau khi biết được đường đi từ source đến sink, bây giờ tiến hành debug hoii
Vì gadgetchain này lợi dụng class có sẵn trong Java nên ta không cần phải khai báo thêm thư viện gì cả
Sơ đồ gadgetchain là như sau:
HashMap.readObject()
->HashMap.putVal()
->HashMap.hash()
->URL.hashCode()
Gadgetchain này sẽ được trigger khi ta gọi readObject
của clas Hashmap
, do đó để debug ta sẽ bắt đầu từ Hashmap
Tại InteliJ nhấn Ctrl+N
và tìm Hashmap
để xem src của class HashMap
Tại đây nhấn Ctrl+F12
để tìm methods readObject
Note: Java cho phép dev có thể ghi đè lên các methods
readObject
vàwriteObject
, và thông thường dev cũng thích làm thế. Do đó ở một số class methodsreadObject
hoặcwriteObject
sẽ khác so với mặc định của Java
Tại method readObject
của Hashmap
, nó sẽ loop qua từng key của hashmap và thực hiện putVal(hash(key))
Đặt breakpoint tại đây để xem giá trị của key
Ta thấy key
chính là object URL
mà ta truyền vào
Tiếp tục nhấn F7
và chọn method hash
để nhảy vào method này
Method này gọi đến key.hashCode()
trong trường hợp này key chính là object URL
, nghĩa là nó đang gọi đến URL.hashCode()
Tiếp tục nhảy vào method hashCode
Method này sẽ kiểm tra giá trị của property hashCode
có khác -1
hay không, nếu khác thì tiếp tục gọi đến handler.hasCode()
còn nếu không thì gadgetchain của ta bị gãy từ đây.
Tuy nhiên vấn đề nằm ở hashCode
được defined mặc định là -1
Vì đây là property private nên ta không thể thay đổi giá trị của nó lúc khai báo, nhìn lại code gen payload của ysoserial
ta thấy nó sử dụng Reflection
để thay đổi giá trị của hashCode
trong quá trình runtime
Reflection
trên là một class của ysoserial.payloads.util
nên để tái hiện lại mình sẽ dùng cách mặc địch của Java
URL u = new URL(null, "http://ydg1nw3a.requestrepo.com", handler);
Field field = URL.class.getDeclaredField("hashCode");
field.setAccessible(true);
field.set(u, 1337);
Ta có thể test bằng cách dùng Evalute Expression
của InteliJ
Khi comment dòng set
ta thấy giá trị của hashCode
là -1, khi gỡ comment dòng đó thì:
Giá trị của hashCode
đã là 1337
. Vậy thông qua reflection
ta có thể thay đổi giá trị của hashCode
Gadget của ysorial
đến đây là hết, tuy nhiên ta vẫn chưa thấy dòng nào thực hiện DNS query, do đó mình sẽ tiếp tục debug
Sau khi bypass được hashCode = -1
, tiếp tục code gọi đến handler.hashCode
handler
này là class của object URLStreamHandler
F7
để nhảy vào handler.hashCode
thì ta thấy nó gọi đến getHostAddress
với u là class URL
ta truyền vào
method này sẽ gọi đến InetAddress.getByName()
để resolve DNS đến host của ta
Vậy thì gadgetchain đầy đủ sẽ là
HashMap.readObject()
->HashMap.putVal()
->HashMap.hash()
->URL.hashCode()
->URLStreamHandler.getHostAddress()
->InetAddress.getByName()
Đây là gadgetchain đơn giản nhất trong các gadgetchain ysoserial
, nó giúp mình có cái nhìn tổng quát về gadgetchain trong Java cũng như là học được cách debug trong InteliJ. Phần tiếp theo mình sẽ phân tích thêm một gadgetchain nữa trong bộ ysoserial
https://www.infoworld.com/article/2072752/the-java-serialization-algorithm-revealed.html
https://learn.snyk.io/lessons/insecure-deserialization/java/
https://sec.vnpt.vn/2020/02/co-gi-ben-trong-cac-gadgetchain/
https://tsublogs.wordpress.com/2023/02/16/javasecurity101-2-java-deserialization-overview/
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java