Nghiên cứu về Hessian Deserialization trong một buổi chiều đầu thu
Thời tiết chuyển mùa mấy hôm nay khá dễ chịu, mưa gió nhiều mát mẻ không như những ngày nắng nóng trước đó. Rất thích hợp để viết thêm một bài.
Bài viết này mình sẽ trình bày về giao thức Hessian, cách khai thác Hessian Deserialization.
1. Giao thức Hessian
1.1. Khái niệm
Giao thức dịch vụ web nhị phân Hessian (Hessian binary web service protocol) là một giao thức làm cho các dịch vụ web có thể sử dụng được mà không yêu cầu một framework lớn và không cần học một bộ giao thức mới. Bởi vì nó là một giao thức nhị phân, nó rất phù hợp để gửi dữ liệu nhị phân mà không cần mở rộng giao thức với các tệp đính kèm.
Hessian được phát triển bởi công ty Caucho, một công ty có thể nổi tiếng nhưng mình không biết tới.
Hessian thường được sử dụng để giao tiếp giữa các ứng dụng Java từ xa, truyền tải dữ liệu qua mạng. Hessian sử dụng cả hai phương thức truyền tải là HTTP và HTTPS để gửi và nhận dữ liệu.
1.2. Khác biệt giữa Hessian và RMI
Hessian và RMI (Remote Method Invocation) là cả hai công nghệ được sử dụng để thực hiện giao tiếp giữa các ứng dụng Java từ xa, nhưng chúng có một số điểm khác biệt quan trọng:
- Giao thức sử dụng:
Hessian sử dụng giao thức HTTP hoặc HTTPS để truyền tải dữ liệu. Điều này có nghĩa là Hessian có thể hoạt động qua các cơ chế proxy và tường lửa mà không gặp vấn đề nhiều.
RMI sử dụng giao thức JRMP (Java Remote Method Protocol) hoặc IIOP (Internet Inter-ORB Protocol) để truyền tải dữ liệu. JRMP yêu cầu một cổng duy nhất để giao tiếp và có thể bị hạn chế bởi các tường lửa và cơ chế bảo mật.
- Môi trường của ứng dụng:
Hessian thường được sử dụng cho các ứng dụng hoạt động ở xa như giữa máy khách và máy chủ trên mạng.
RMI thường được sử dụng trong các môi trường có cấu trúc phức tạp hơn, chẳng hạn như các ứng dụng Enterprise Java (JEE), trong đó có các dịch vụ Web và các máy chủ ứng dụng.
- Cấu hình:
Hessian thường ít phức tạp hơn so với RMI.
RMI yêu cầu cấu hình khá lằng nhằng, bao gồm việc tạo RMI Registry, cấu hình bảo mật và xác thực.
- Dữ liệu đóng gói:
Hessian sử dụng định dạng nhị phân để đóng gói dữ liệu. Điều này có thể làm cho Hessian nhanh hơn trong việc truyền tải dữ liệu.
RMI sử dụng đối tượng Java Serialization để đóng gói dữ liệu, có thể tốn kém về hiệu suất so với Hessian.
Tóm lại, cả Hessian và RMI đều là các công nghệ cho phép giao tiếp giữa các ứng dụng Java từ xa, nhưng chúng có các đặc điểm riêng biệt phù hợp với các tình huống sử dụng khác nhau.
1.3. Ví dụ về giao thức Hessian
Trong ví dụ này mình sẽ thực hiện xây dựng một client-server cơ bản giao tiếp với nhau bằng Hessian.
Dựng một Servlet app cơ bản, với một service là HelloService
, một implementation HelloServiceImpl
extend HessianServlet
.
Add route
Lúc này khi truy cập tới http://localhost:8081/hessian
ta được kết quả
Dựng một Hessian Client như sau:
Kết quả:
Hessian cũng có triển khai trong Spring và các framework khác.
1.4. Phân tích giao thức Hessian
Lớp key trong Hessian Servlet là com.caucho.hessian.server.HessianServlet
Vì extend HttpServlet
nên nó cũng sử dụng hàm init
để thực hiện khởi tạo một số thuộc tính và invoke
để thực hiện tác vụ.
Các thuộc tính được tạo trong init
Tiếp theo, trong service
Nếu nó là POST request, nó sẽ lấy ra objectId
, sau đó chuyển cho invoke
. Hàm invoke
sẽ quyết định gọi phương thức nào dựa trên việc objectId
có rỗng hay không
Tiếp theo, chúng ta xem tới com.caucho.hessian.server.HessianSkeleton
. Đây là lớp con của AbstractSkeleton
, được sử dụng để đóng gói các dịch vụ do Hessian cung cấp.
Khi AbstractSkeleton
được khởi tạo, nó sẽ nhận các interface và lưu trữ các phương thức trong interface theo logic của riêng nó trong _methodMap
, bao gồm methodName
, methodName__paramLength
và các loại định dạng tùy chỉnh khác.
Khi HessianSkeleton
được khởi tạo, lớp triển khai được lưu trong biến _service
Ngoài ra còn có hai biến trong HessianSkeleton
, HessianFactory
được sử dụng để tạo luồng HessianInput/HessianOutput và HessianInputFactory
được sử dụng để đọc và tạo luồng HessianInput/Hessian2Input, sẽ được mô tả chi tiết khi sử dụng.
Sau khi đã hiểu sơ qua, chúng ta đi vào phương thức invoke
được gọi trong HessianServlet
. Nó được gọi để khởi tạo AbstractHessianInput
và AbstractHessianOutput
.
Rồi chủ yếu là tìm kiếm phương thức và deserialize các tham số.
Sau đó, reflection được thực hiện và kết quả được ghi lại.
1.5. Serialize và Deserialize process
Hessian serialization và deserialization process có một vài key class, bao gồm input và output stream, serializer/deserializer, các factory class liên quan. Hãy cùng tìm hiểu chúng.
Đầu tiên là input và output stream. Hessian định nghĩa 2 abstract class AbstractHessianInput/AbstractHessianOutput
dùng để read và write serialized data. Chuẩn Hessian/Hessian2/Burlap
sẽ có các implementation riêng để thực hiện logic riêng biệt.
Xét quá trình serialization, với các lớp con liên quan đến AbstractHessianOutput
, class chính của output stream, những class này cung cấp method call
để thực hiện các method call, các method writeX
để thực hiện write các serialize data. Lấy chuẩn Hessian2
làm ví dụ. Ngoài các kiểu dữ liệu cơ bản, mối quan tâm chính là phương thức writeObject
.
Method này nhận implementation class của serializer
tùy thuộc vào loại của Serializer
và gọi writeObject
của Serializer
đó.
Với version mình sử dụng là 4.0.65 thì có 29 implementations của Serializer
. Với custom type, nó sẽ sử dụng JavaSerializer/UnsafeSerializer/JavaUnsharedSerializer
, mặc định UnsafeSerializer
.
Method UnsafeSerializer#writeObject
sẽ tương thích với 2 protocol Hessian và Hessian2 và sẽ gọi writeObjectBegin
để bắt đầu write data.
Method đó là AbstractHessianOutput#writeObjectBegin
.
Hessian2Output
sẽ viết lại method này, nhưng với Hessian1 và Burlap thì nó không viết lại, vì vậy với Hessian và Burlap, phương thức writeMapBegin
sẽ được gọi để đánh dấu nó là một Map type, còn Hessian2 không phải là Map nữa.
Tiếp theo là quá trình deserialization. Và đương nhiên, key class sẽ là Hessian2Input
, một implementation của AbstractHessianInput
.
Phương thức readObject
thực hiện deserialize data với 1 đoạn switch/case dài 255 trường hợp, kiểm tra data type và thực hiện các logic tương ứng.
Tương tự với serialize, Hessian cũng định nghĩa Deserializer
interface tạo các implementation khác nhau cho từng type. Ở đây chúng ta tập trung vào việc read các custom type.
Trong HessianInput
của Hessian 1 không có hàm read cho Object, nhưng có readMap
. Trong quá trình serialization, chúng ta đã nói khi write một custom type, nó sẽ được đánh dấu là Map type.
Method MapDeserializer#readMap
cung cấp logic cho Map type.
Trong Hessian 2.0, nó được cung cấp UnsafeDeserializer
để deserialize custom data, đi cùng phương thức readObject
Method instantiate
tạo instance của class một cách trực tiếp bằng cách sử dụng unsafe instance allocateInstance
1.6. Remote call
Khi gọi giao thức Hessian từ xa, code của chúng ta
String url = "http://127.0.0.1:8081/hessian";
HessianProxyFactory factory = new HessianProxyFactory();
HelloService helloService = (HelloService) factory.create(HelloService.class, url);
System.out.println("Hessian call: " + helloService.sayHello());
Có thể thấy rằng một instance của HessianProxyFactory
đã được khởi tạo và method create
được gọi. Thực tế, HessianProxy
được sử dụng để tạo một dynamic proxy cho interface được gọi và HessianRemoteObject
.
Chún ta biết rằng bất kể đối tượng dynamic proxy gọi phương thức nào, nó sẽ sử dụng method invoke
của InvocationHandler
Gửi request và nhận kết quả sau đó deserialize nó.
Send request sử dụng HessianURLConnection#sendRequest
.
Logic rất đơn giản, chỉ cần thực hiện một yêu cầu HTTP và deserialize dữ liệu.
Như đã đề cập, phiên bảo Hessian đã được nâng cấp từ 1.0 lên 2.0, tuy nhiên trong package mới nó vẫn support cả 2 phiên bản và việc server sử dụng phiên bản nào để serialize và format nào để trả lại data lại phụ thuộc hoàn toàn vào flag bit từ request.
Cài đặt này nằm trong HessianProxyFactory
với 2 variable là _isHessian2Request
và _isHessian2Reply
Để dùng Hessian 2 làm transmission protocol, chúng ta có thể set
HessianProxyFactory factory = new HessianProxyFactory();
factory.setHessian2Request(true);
Nói thêm về Hessian Deserialization. Như chúng ta đã biết, với Java native deserialization, chỉ có các class extend từ java.io.Serializable
là có thể được deserialize. Hessian có hỗ trợ các đặc điểm này.
Khi có default serializer, nó sẽ đánh giá xem class này có implement Serializable
hay không
Tuy nhiên, đồng thời Hessian lại kiểm tra thêm 1 biến _isAllowNonSerializable
để phá vỡ sự kiểm tra này. Biến này có thể sử dụng SerializerFactory#setAllowNonSerializable
để đặt thành true
. Vì vậy các class không implement Serializable
cũng có thể serialize và deserialize như thường.
Điều này thực sự kì diệu. Việc kiểm tra thực hiện trong quá trình serialization chứ không xảy ra trong deserialization, vì vậy tự nhiên có thể bị bypass. Nói cách khác, Hessian hỗ trợ deserialize cho mọi class mà không cần implement Serializable
.
Tiếp theo là vấn đề về transient
và static
. Trong quá trình serialize, method UnsafeSerializer#introspect
được gọi để kiểm tra xem các định danh biến thành viên có phải là transient
và static
hay không. Nếu có, nó sẽ không can thiệp vào quá trình serialize và deserialize
Trong Java native, nếu sử dụng transient
có nghĩa là chúng ta không muốn serialize hoặc deserialize object, developer có thể sử dụng readObject/writeObject
của riêng họ để thực hiện các logic. Tuy nhiên không có cơ chế như vậy trong Hessian, vì vậy trường được đánh dấu là transient
không đóng vai trò gì trong quá trình deserialize.
2. Gadget chain với Hessian Deserialization
Lý thuyết thì hơi khó hiểu và khô khan, chúng ta sẽ đi vào các gadget chain sử dụng với Hessian Deserialization.
Như ta có thể thấy, giao thức Hessian sử dụng Unsafe để tạo các class instance, sử dụng reflection để ghi các giá trị và không có logic nào như gọi các method nhất định sau khi ghi đè chúng. Vì vậy, bất kể constructor method, getter/setter, readObject và các method khác sẽ không được kích hoạt trong quá trình Hessian Deserialization. Vậy lỗ hổng có thể nằm ở đâu?
Câu trả lời nằm ở quá trình Hessian xử lý Map type. Như đã đề cập trong phần phân tích trước, MapDeserializer#readMap
của dữ liệu Map sẽ tạo ra Map object tương ứng, deserialize key-value pari và sử dụng put method data input. Khi implementation của Map không được chỉ định, HashMap
sẽ được dùng theo mặc định và TreeMap
sẽ được dùng cho SortedMap
.
Chúng ta đã biết, khi HashMap
add một key-value pair, nó sẽ check hashcode của key để xem nó có duplicate không
Với TreeMap
, bởi vì nó cần được sắp xếp, nó sẽ compare các key, và method compareTo
sẽ được gọi.
Điều đó có nghĩa là Hessian sẽ có một số hạn chế so với gadget sử dụng trong Native Deserialization:
- Phương thức bắt đầu gadget chỉ có thể là method
hashCode/equals/compareTo
- Member variable được gọi trong gadget không thể modify thành
transient
- Tất cả các method call không phụ thuộc vào logic của readObject trong lớp cũng như logic của getter/setter
Những hạn chế này khiến rất nhiều gadget trong Java native không sử dụng được trong Hessian Deserialization, thậm chí một số chuỗi trong ysoserial
rõ ràng được kích hoạt bởi hashCode/equals/compareTo
cũng không thể được sử dụng trực tiếp.
Hiện tại có 5 gadget chain thường được sử dụng trong marshalsec
:
Rome
XBean
Resin
SpringPartiallyComparableAdvisorHolder
SpringAbstractBeanFactoryPointcutAdvisor
Hãy cùng phân tích gadget nổi bật nhất của nó Rome
.
Rome
Core của Rome
gadget chain là ToStringBean
. Phương thức toString
của class này sẽ gọi tất cả các getter
method không có tham số của lớp đóng gói của nó.
Trong số các method, có một method là getDatabaseMetaData
trong com.sun.rowset.JdbcRowSetImpl
có JNDI lookup
Lớp bên ngoài được đóng gói bằng EqualsBean
và HashMap
, và deserialization call đã trigger EqualsBean#hashCode
Đây là điểm trigger ROME gadget, cũng có thể thấy điều đó trong ysoserial
Vấn đề của gadget này đó là nó phải có JNDI lookup. Vì vậy chúng ta cần tìm một phương pháp để sử dụng khi không có internet. Một trong những phương pháp thông thường là sử dụng java.security.SignedObject
cho quá trình deserialization thứ cấp.
Class này có phương thức getObject
thực hiện read data từ native deserialization, dẫn đến deserialization thứ cấp
Như vậy, chỉ cần đóng gói ROME gadget vào trong lớp này là được.
Một bài viết cực hay của Orange Tsai về khai thác Hessian Deserialization, ai quan tâm có thể theo dõi tại https://blog.orange.tw/2020/09/how-i-hacked-facebook-again-mobileiron-mdm-rce.html
Tài liệu tham khảo
https://paper.seebug.org/1137/
https://securitylab.github.com/research/hessian-java-deserialization-castor-vulnerabilities/
All rights reserved