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 |
|
File SerializationTest.java
1 |
|
File DeserializationTest.java
1 |
|
Đầ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.txt1
2ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
oos.writeObject(obj); - DeserializationTest.java
1
2ObjectInputStream 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:
Gõ 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 | Gadget Chain: |
Để 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 | public static void main(String[] args) throws IOException { |
Ở đâ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 | Gadget Chain: |
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ệnreturn hashCode
bởi vì nó đã được set rồi. - Nếu không thì
hashCode
sẽ được gán bằnghandler.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ànhCtrl+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
Biến handler ở đây là một object của lớp1
transient URLStreamHandler handler;
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ànhCtrl+Chuột trái
rồi click vàoURLStreamHandler
, 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 | package BasicChain; |
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 | package BasicChain; |
file: Deser.java
1 | package BasicChain; |
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~