Skip to content

The Art of Deserialization Gadget Hunting

Bài viết này phần nào đó là chia sẻ lại những kiến thức riêng về Java Deserialization mình đã nghiên cứu trong thời gian qua cho mọi người, phần nữa là để giúp chính mình nhớ lại những gì đã làm, dạo này tự dưng bị bệnh hay quên <@_@>, (có hôm đọc lại chính blog của mình mà cứ như đọc bài của người ta =)) ).

Khái niệm cơ bản về Java Deserialization (Deser) mình không nhắc lại nữa mà sẽ nói về cách làm sao mà từ readObject() lead to RCE, hay còn được gọi là gadgetchain.

Có lẽ cũng giống mình hồi mới biết đến Java Deser, cách nhìn của những người chỉ sử dụng các payload đó ở mức khách quan, chưa thấy được cái hay trong cái gadgetchain mình đang sử dụng. Sau khi nghiên cứu một thời gian mới thấy thực sự nể phục công sức của những người đi trước, không cần dùng công cụ hỗ trợ mà vẫn tìm ra được nhiều gadgetchain hữu ích để góp vào bộ payload của ysoserial!

Một số khái niệm mình cần nêu trước khi vào bài:

  • Source: là điểm bắt đầu của gadgetchain. Khi deser, java sẽ gọi method readObject() của source Object.
  • Sink: sau khi ghép nối các chain thì sẽ dẫn tới sink, hay cách khác, sink chính là điểm cuối của gadgetchain. Thường sink sẽ có các metod hữu ích như Reflection method invoke, file write, …
  • Reflection in java: đây là 1 API của java, cho phép truy xuất, sửa đổi hành vi của method, field, class, interfaces. Do vậy việc tìm hiểu và viết được gadgetchain phụ thuộc rất nhiều vào cái Java Reflection. (for more detail: https://www.geeksforgeeks.org/reflection-in-java/)
  • Method call: method A gọi method B, method A -> method B

IDE mình sử dụng chủ yếu là IntellIJ IDEA, lý do là vì nó free for student và tiện …

Các công cụ mình cần dùng: git, svn, các framework, mvn, gradle … được tích hợp tất tần tật vào. Bạn sẽ không phải bận tâm đến chuyện setup thêm gì nhiều, mà chỉ cần focus vào công việc thôi. Một lựa chọn khác đó là Eclipse, nhưng điểm yếu của eclipse là khi debug sẽ không show ra các local variable nếu như không có source của class đó, cách eval của eclipse cũng hơi bất tiện nên mình khuyên dùng IntellIJ cho việc đọc và debug các loại code liên quan tới Java.

License của IntellIJ thì các bạn có thể sử dụng mail edu của trường hoặc dùng gmail cũng được, apply vào student program của họ là bạn sẽ có hẳn mấy năm sử dụng Ultimate version thoải mái. Nếu không có chương trình này thì ko biết giờ này mình đang chật vật dùng bản crack ở đâu đó nữa ¯\_(ツ)_/¯.

Note: OS mình đang dùng là windows nên tổ hợp phím tắt sử dụng trong bài sẽ khác so với những bạn sử dụng OS khác

_______________________________

Về cơ bản, gadgetchain là kết quả của việc lắp ghép các method call, gán ghép các field làm sao cho nó đi theo luồng mà mình mong muốn: RCE, write file, …

Tưởng tượng các method call như các đoạn ống nước riêng lẻ thì công việc tìm gadgetchain chính là quá trình lắp ghép các đoạn ống thích hợp lại với nhau, và kết quả là có thể dẫn nước từ nguồn (source) tới đích (~sink).

Lý thuyết suông về một vấn đề nào đó thì có thể sẽ hơi khô khan và khó hiểu cho người mới tìm hiểu (nhất là với giọng văn nửa mùa của mình). Do vậy trong phần này mình sẽ đi vào phân tích luồng hoạt động của 1 số gadgetchain có trong bộ ysoserial, cũng như nói vềcác source, sink thường gặp trong các gadgetchain.

#1. URLDNS

Cách hoạt động của chain này khá đơn giản:

  HashMap.readObject()
    HashMap.hash()
      URL.hashCode()
        URLStreamHandler.hashCode()
          URLStreamHandler.getHostAddress()
            InetAddress.getByName()

Gadgetchain này tồn tại ngay bên trong rt.jar của java, mục đích cuối cùng của gadgetchain này là tạo 1 request dns lookup tới domain tùy ý. Từ đó có thể được sử dụng để kiểm tra xem target có thể deserialize object được hay không!

Bắt đầu phân tích từ source của chain này, đó là tại HashMap.readObject().

java.util.HashMap là 1 class có sẵn trong runtime machine của java, để đi đến class này từ IntellIJ IDEA, ta sử dụng tổ hợp phím: Ctrl + N rồi gõ HashMap vào, được kết quả như sau:

Chọn class mà IntellIJ tìm được ở phía dưới, ta sẽ mở được đúng class đang cần đi tới java.util.HashMap.

  • Chức năng này áp dụng được kể cả với các class không có source nằm trong classpath của project.

Di chuyển từ ô tìm kiếm class HashMap sẽ dẫn bạn tới phần đầu của class như thế này:

Tiếp tục sử dụng tổ hợp phím: Ctrl + F12 để list ra các method nằm trong class này:

Method mình cần đi tới bây giờ là readObject(), gõ trực tiếp chữ “readobject” ngay sau khi Ctrl + F12 để tìm method này (lưu ý là ko có ô để gõ chữ đâu), được kết quả:

Click vào kết quả hoặc Enter để chuyển đến method này luôn:

Fact: Việc sử dụng quen và thành thạo các tổ hợp phím tắt của IntellIJ sẽ giúp cho quá trình phân tích gadget được thuận lợi và nhanh hơn rất nhiều!

Khi đã navigate tới HashMap.readObject(), ta cần tìm tới đoạn mà method này sẽ gọi tới chain tiếp theo của nó (ở đây là gọi tới HashMap.hash() )

Như trong trường hợp của mình, tại dòng #1413, method này gọi tới HashMap.hash(). Set breakpoint tại điểm này để xem các giá trị biến local của method trước khi nhảy vào hash(), sử dụng tổ hợp phím Ctrl + F8 để đặt breakpoint tại dòng này.

Bắt đầu debug mode bằng cách click vào hình con bọ hoặc dùng tổ hợp Shift + F9:

Nếu bạn chưa chạy file test lần nào thì chuyển tab sang file cần chạy, chuột phải vào bất cứ đâu trong editor và chọn Debug …

Debugger dừng tại breakpoint đã đặt trước trong HashMap.readObject(), khi đó màn hình debugger sẽ có dạng như thế này:

  • Project File: là cửa sổ show ra các source file java, khi chạy các file java trong IntellIJ sẽ lấy working directory tại đây luôn.
  • Project ‘s Library: hiển thị các library có trong classpath của project
  • Stack Trace: cửa sổ này hiển thị ra backtrace, hay còn được hiểu là show ra các method đã gọi tới method hiện tại.
  • Local Variable: đây là một điểm mạnh mà mình rất thích ở IntellIJ. Khi debug, IntellIJ sẽ cố gắng lấy hết giá trị các biến để show ra cửa sổ này, giúp cho việc debug được dễ dàng hơn, nhưng đôi khi nó lại gây ra những sai sót và hiểu lầm cho người dùng (mình sẽ nói chi tiết ở phía dưới).

Ngoài ra, trong cửa sổ editor, ngay bên cạnh các dòng code cũng có show giá trị của các biến rất trực quan.

Tiếp tục với việc debug, bấm F7 để đi vào HashMap.hash()

Lúc này IDE cho ta nhiều lựa chọn, là muốn đi vào putVal() hoặc đi vào hash(). Lựa chọn bằng cách đơn giản là click chuột vào 1 trong 2 method. Tại dòng 1413, từ method readObject() gọi tới method hash() và có truyền vào biến key, giá trị của key chính là URL Object của target mình cần resolve DNS:

Đoạn code xử lý bên trong method hash() như sau:

Tại đây, method này gọi tới method hashCode() của object key vừa được truyền vào. Như trong trường hợp này, chính là gọi tới URL.hashCode().

Fact: Do nó nằm ngay trong runtime library của java và rất dễ control cho nên đây là 1 trong những source chain bị lợi dụng rất nhiều để viết các gadgetchain có call tới <Object>.hashCode()

Tiếp tục F7 để đi vào URL.hashCode():

Method hashCode() của class URL thực hiện kiểm tra xem có hashCode nào được cache không, nếu có bị cache rồi thì sẽ không đi tiếp nữa, mà trả về giá trị luôn. Do vậy, để chain hoạt động được thì giá trị hashCode này phải = -1.

! Có lẽ hơi phi logic theo luồng văn hiện tại, nhưng là cần thiết nên mình sẽ nói qua về cách sử dụng Reflection API một chút. Chi tiết hơn nữa sẽ nằm trong loạt bài viết về cách xây dựng gadgetchain.

*********

Ở trường hợp này, hashCode được khai báo là 1 private field của class URL.

Để set được giá trị của 1 private field trong java, ta cần sử dụng tới Reflection API. Dưới đây là đoạn code nhỏ mình dựng lại cái URLDNS chain này:

Trong đó object u là URL object, ht HashMap object.

Sau khi put URL object vào ht, HashMap sẽ thay đổi field hashCode của URL object thành giá trị mới “!= -1”. Nếu cứ tiếp tục và không sửa giá trị này, khi deserialize, chương trình sẽ trả về giá trị hashCode luôn, không đi theo nhánh mà ta mong muốn ban đầu.

Ngay sau dòng ht.put()

Field f1 = URL.class.getDeclaredField("hashCode");

Đoạn này được sử dụng để lấy field hashCode từ class URL, cách sử dụng Reflection Field này rất đơn giản:

Field f1 = <Some Class>.getDeclaredField(<Field Name>);

Với <Some Class> có thể là URL.class hoặc gián tiếp Class.forName(“java.net.URL”) (cần lưu ý nếu sử dụng chính tên class đó thì phải có .class nữa).

Field hashCode được khai báo với dạng private, ta không thể thao tác trực tiếp ngay với nó được, mà phải setAccessible() trước:

f1.setAccessible(true);

Như vậy là field này đã có thể được set giá trị dù nó được khai báo là private.

Cú pháp sử dụng để set giá trị:

f1.set(u, -1);
  • f1 là field hashCode của class URL
  • u OBJECT của class URL
  • -1 là giá trị cần set

Vậy là xong, private field hashCode của object u đã được set = -1.

***********

Quay trở lại với việc debug, ta đang dừng lại tại việc set hashCode = -1.

Chương trình chạy tiếp tục, URL.hashCode() gọi tới handler.hashCode()

Field handler này là object của 1 class nào đó, ta cần kiểm tra nơi nào khai báo nó bằng cách dùng tổ hợp Ctrl + B để nhảy tới:

handler là 1 transient field, là object của URLStreamHandler.

! Đặc điểm của transient field này là khi serialize, nó không được serialize cùng object!

Tiếp tục F7 để nhảy vào handler.hashCode() hay cách khác chính là nhảy vào URLStreamHandler.hashCode():

Method này gọi tới URLStreamHandler.getHostAddress() với URL object ban đầu.

Nội dung của method này như sau:

Với InetAddress.getByName() chính là method thực hiện resolve dns với domain ta truyền vào.

Đây cũng được chính là sink của gadgetchain này.

____________

Vậy là mình đã giới thiệu sơ qua về 1 số khái niệm cơ bản về gadgetchain, cách sử dụng IntellIJ cũng như debug gadgetchain URLDNS từng bước.

Phần 1 của loạt bài sẽ dừng lại tại đây, trong phần 2 mình sẽ tiếp tục phân tích về 1 số gadgetchain khác trong bộ ysoserial.

Sau khi đã đầy đủ kiến thức mình sẽ chia sẻ tiếp về 1 số 0day gần đây mình có tìm được liên quan tới các loại gadgetchain này.

Cảm ơn các bạn đã đọc đến đây.

__Jang__

102 lượt xem