I. Tổng quan
Xem sơ đồ luồng thực thi của readObject bên dưới. Đây là sơ đồ mô tả quá trình thực thi Deserialization trong WebLogic.
Có thể tạm thời bỏ qua lần gọi readObject đầu tiên; phần này sẽ được giải thích chi tiết trong bài viết tiếp theo về WebLogic.
Tạo 2 class để debug quá trình deserialization
Class User:
1 | package org.example; |
Class ReadObjectAnalysis:
1 | package org.example; |
II. Phân tích method readObject
Sau khi chuẩn bị. Ta đi đến quá trình phân tích.
Đặt breakpoint tại method ObjectInputStream.readObject(), rồi chạy class ReadObjectAnalysis để debug.
Tại đây có một đoạn kiểm tra biến enableOverride. Nếu giá trị của nó không phải false, chương trình sẽ trả về kết quả từ method readObjectOverride. Trong constructor, giá trị này đã được định nghĩa là false.
Tiếp theo chương trình sẽ gọi method readObject0
Ta tiếp tục follow vào bên trong phương thức readObject0 để xem cách implement.
Ở đây, chương trình sẽ lấy byte đầu tiên trong dữ liệu serialized. Nếu byte này là TC_RESET thì sẽ gọi:
1 | bin.readByte(); |
Kiểm tra giá trị của `TC_RESET.
Sau khi chuyển sang kiểu Byte, giá trị này là 121. Trong khi đó, byte đầu tiên trong dữ liệu serialized của chúng ta là 115, nên đoạn xử lý này sẽ bị bỏ qua.
Tiếp theo, trong code có một câu lệnh switch để kiểm tra loại dữ liệu. Giá trị của TC_OBJECT sau khi chuyển đổi vừa đúng bằng 115, vì vậy chương trình sẽ đi vào nhánh này.
Trong nhánh này, nó sẽ gọi phương thức readOrdinaryObject. Tiếp tục follow vào trong.
Trong phương thức này, chương trình tiếp tục gọi readClassDesc. Tiếp tục follow.
Đến đây khá thú vị: chương trình lấy byte thứ hai trong dữ liệu serialized, sau đó lại thực hiện một câu lệnh switch. Lần này luồng thực thi đi vào phương thức readNonProxyDesc. Tiếp tục follow.
Trong phương thức này, nó lại gọi resolveClass và truyền vào tham số readDesc. Tiếp tục follow vào phương thức này.
Tại đây, phương thức trả về:
1 | Class.forName(name, false, latestUserDefinedLoader()); |
Phương thức latestUserDefinedLoader() trả về sun.misc.VM.latestUserDefinedLoader(). Điều này cho thấy class loader được chỉ định tại đây.
Sau đó quay lại phương thức readOrdinaryObject để tiếp tục phân tích.
Đây là nơi thực hiện thao tác deserialization. Đi vào hàm readSerialData để phân tích.
Phương thức slotDesc.hasReadObjectMethod() sẽ lấy thuộc tính readObjectMethod.
Nếu class đang được deserialization không override readObject(), thì thuộc tính readObjectMethod sẽ là null.
Nếu class này có override readObject(), chương trình sẽ đi vào nhánh if bên trong và gọi slotDesc.invokeReadObject(obj, this);
Nói cách khác, nếu phương thức readObject() bị override thì luồng xử lý sẽ đi đến được điều kiện else này trong readSerialData.
III. Vì sao trong các bản patch thường thực hiện tại method resolveClass
Phương thức resolveClass có nhiệm vụ chuyển đổi serialization descriptor của một class thành đối tượng Class tương ứng.
Có thể sử dụng sơ đồ trên để minh họa quá trình gọi hàm bên trong phương thức readObject nguyên bản (dù có 1 vài bước liên quan tới quá trình readObject Weblogic nhưng ko cần để tâm).
Bên trong hàm resolveClass, chương trình lấy fully qualified class name từ phần mô tả của class. Sau đó, nó sử dụng Reflection để tìm và trả về đối tượng Class tương ứng với tên đầy đủ đó.
Vì vậy, khi override phương thức resolveClass, chúng ta có thể thêm một blacklist chứa tên các class không được phép sử dụng. Nếu phát hiện class đang được xử lý nằm trong blacklist, chương trình sẽ ném ra một exception.
Qua đó ta có thể ngăn chặn các cuộc tấn công Deserialization Attack ở một mức độ nhất định.