+4

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.

image.png

Add route

image.png

Lúc này khi truy cập tới http://localhost:8081/hessian ta được kết quả

image.png

Dựng một Hessian Client như sau:

image.png

Kết quả:

image.png

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

image.png

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

image.png

Tiếp theo, trong service

image.png

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

image.png

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.

image.png

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

image.png

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 AbstractHessianInputAbstractHessianOutput.

image.png

Rồi chủ yếu là tìm kiếm phương thức và deserialize các tham số.

image.png

Sau đó, reflection được thực hiện và kết quả được ghi lại.

image.png

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.

image.png

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 đó.

image.png

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.

image.png

Method đó là AbstractHessianOutput#writeObjectBegin.

image.png

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.

image.png

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.

image.png

Method MapDeserializer#readMap cung cấp logic cho Map type.

image.png

Trong Hessian 2.0, nó được cung cấp UnsafeDeserializer để deserialize custom data, đi cùng phương thức readObject

image.png

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

image.png

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.

image.png

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

image.png

Gửi request và nhận kết quả sau đó deserialize nó.

image.png

Send request sử dụng HessianURLConnection#sendRequest.

image.png

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_isHessian2Reply

image.png

Để 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

image.png

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ề transientstatic. 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à transientstatic hay không. Nếu có, nó sẽ không can thiệp vào quá trình serialize và deserialize

image.png

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

image.png

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.

image.png

Đ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

image.png

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ó.

image.png

Trong số các method, có một method là getDatabaseMetaData trong com.sun.rowset.JdbcRowSetImpl có JNDI lookup

image.png

image.png

Lớp bên ngoài được đóng gói bằng EqualsBeanHashMap, và deserialization call đã trigger EqualsBean#hashCode

image.png

Đâ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

image.png

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/

https://su18.org/post/hessian/


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí