Skip to content

The Art of Deserialization Gadget Hunting [part 2]


The Art of Deserialization Gadget Hunting [part 2]

Như đã hứa hẹn trong bài trước, phần 2 này sẽ tiếp tục nói về một số gadgetchain thông dụng trong bộ tool ysoserial.

Việc phân tích gadgetchain như này nó cũng giống như việc review source code, không thú vị như các writeup về bounty hunting, nhưng nó là bước khởi đầu cơ bản để tìm được các lỗ hổng trong phần mềm.

Những thứ anh làm thường đơn giản, nên không hay được đánh giá cao …”

(Credit: https://www.youtube.com/watch?v=KKc_RMln5UY)

#2. CommonsCollection5

Trong bộ ysoserial có đến 7 gadgetchain liên quan tới library Apache CommonsCollections (https://commons.apache.org/proper/commons-collections/).

Chỉ cần đọc hiểu kỹ 7 cái gadgetchain này là đã có thể thu về rất nhiều kinh nghiệm về Java Deserialization nói riêng, và về cách hoạt động của Java nói chung.

Version hiện tại của CommonsCollections là 3.2.2, 4.4 đã có các phuơng pháp để chống bị lợi dụng thành gadgetchain, mọi chain còn lại đều hoạt động trên version 3.2.1, 4.3 ngoại trừ CC1 và CC3 do version của java đã chặn!

Các gadgetchain này trông thì hơi khác nhau, nhưng đều có đặc điểm chung là lợi dụng class InvokerTransformer và InstantiateTransformer để invoke method.

Sau khi random 1 trong 7 gadgetchain, mình chọn ra CC5 để làm mẫu phân tích cho bài này.

Gadgetchain có dạng như sau:

BadAttributeValueExpException.readObject()
    TiedMapEntry.toString()
        TiedMapEntry.getValue()
            LazyMap.get()
                ChainedTransformer.transform()
                    ConstantTransformer.transform()
                    InvokerTransformer.transform()
                        Method.invoke()
                            Class.getMethod()
                    InvokerTransformer.transform()
                        Method.invoke()
                            Runtime.getRuntime()
                    InvokerTransformer.transform()
                        Method.invoke()

 

Mình thực hiện debug trực tiếp gadget này trên ysoserial,

Gadgetchain được bắt đầu tại BadAttributeValueExpException.readObject() và gọi tới TiedMapEntry.toString(),

Content của method BadAttributeValueExpException.readObject() có dạng như sau:

Breakpoint tại điểm gọi tới Object.toString() và tiến hành debug:

stacktrace

Ta có thể thấy, ngay khi breakpoint dừng tại line 86, chưa cả đi hết gadgetchain nhưng calculator đã bị pop up. Vậy điều gì đã xảy ra??

Đây là một tính năng của IntellIJ Debugger!

Như mình đã nói trong bài trước, về cái nhược điểm khó chịu của Eclipse: đó là khi debug với class không có source code, nó không có show ra local variable của method.

IntellIJ debugger thì lại làm được điều này, cho ra các local variable của method đang debug.

Cùng nhìn rõ hơn vào bảng chứa thông tin Variables của IntellIJ khi debug:

Mình ko tìm hiểu sâu về internal của IntellIJ, nhưng theo kinh nghiệm của mình đến thời điểm hiện tại thì vấn đề có thể được giải thích như thế này,

Khi dừng tại breakpoint, muốn show được ra các variable thì debugger phải bằng cách nào đó lấy được các variable được khai báo trước breakpoint. Theo như suy đoán của mình, sau khi lấy được các variable dưới dạng object, và để show lại cho người dùng dưới dạng tường minh, nghĩa là dạng Human-readable. Dạng Human-readable là gì? Là String chứ còn gì nữa ¯\_(ツ)_/¯.

Ở đây để có thể lấy về Object dưới dạng String, debugger đã “ngầm” invoke method toString() của object variable muốn show ra cho người dùng, như trong trường hợp hiện tại thì debugger đã lén invoke method toString() của valObject và khiến cho các chain về sau được thực thi luôn -> calc pop up!

Khi dừng breakpoint, IntellIJ cũng đã cảnh báo về có gì sai sót ở đây:

Tính năng này như con dao 2 lưỡi của cái IntellIJ Debugger này,

  • Tuyệt đối không bao giờ debug malware trên IntellIJ debugger
  • Phải luôn tỉnh táo khi debug bằng IntellIJ, predict được trước các variable, các flow của chương trình và đặt breakpoint cho chính xác. Đôi khi đặt 1 breakpoint nhầm cũng có thể gây ra sai sót cho việc debug toàn bộ chương trình!

Ngoài việc lén invoke toString() ra, debugger còn lén invoke các setter-getter method nữa, nhưng mình nói rõ hơn khi gặp!


#! Đây cũng là 1 trong những source chain kinh điển, bị lợi dụng rất nhiều để build các gadgetchain gọi tới Object.toString()


Tiếp tục với chain hiện tại, do đã biết là khi break tại BadAttributeValueExpException.readObject() có thể gây ra sai sót, ta nên bỏ qua nó và đặt breakpoint tại chain phía sau đó: TiedMapEntry.toString().

#! Sau khi đặt bp mới, ta phải restart debugger để mọi thứ không bị sai sót!

Có vẻ như vẫn bị vướng cái lỗi của debugger kia, lần này là do khi dừng tại TiedMapEntry.toString(), debugger cần truy xuất tới variable this nên đã invoke this.toString(). Do đó, ta cần tránh việc dừng breakpoint tại TiedMapEntry!

Chain tiếp theo sau TiedMapEntry.toString() là TiedMapEntry.getValue():

Chắc chắn nếu như breakpoint tại đây sẽ gây ra pop calc 1 lần nữa, vì đây vẫn đang trong TiedMapEntry. Mình tiếp tục breakpoint tại đây để lấy variable của this.map:

this.map đang trỏ tới LazyMap, nghĩa là từ TiedMapEntry.getValue() chương trình sẽ tiếp tục nhảy vào LazyMap.get().

Tiếp tục restart debugger và đặt breakpoint tại LazyMap.get() để tránh bị pop calc không mong muốn như đã nói bên trên:

Thật may mắn là tại đây debugger không còn bị thực thi code không mong muốn nữa, ta có thể bình tĩnh debug tiếp!

Từ đây, đọc sơ sơ code của method LazyMap.get() ta có thể thấy luồng xử lý cơ bản của method này như sau:

  • đầu tiên là check xem this.map của Object LazyMap này có chưa key hay không
  • nếu không thì sẽ đi vào nhánh gọi this.factory.transform()

Với this.factory ở đây là object của ChainedTransformer().

Jump vào this.factory.transform() A.K.A ChainedTransformer.transform()!

Đây là chain cuối để lead to RCE.

#! Chain này cũng khá là thú vị khi phân tích :v. Sau này mình có gặp lại 1 chain tương tự cái chain này, hoạt động y chang, chỉ khác là ở 1 loại lib khác, của product khác.

Bộ lib CommonsCollections có chứa nhiều các class Transformer, chức năng của các class sinh ra dường như là để làm backdoor chứ không để làm gì khác cả =))).

Đọc hiểu code của phần này như sau:

  • this.iTransformers[] của ChainedTransformer là 1 mảng chứa các object là abstract của interface Transformer

this.iTransformers[] trong gadgetchain này được set cho các giá trị lần lượt là:

  1. ConstantTransformer
  2. InvokerTransformer
  3. InvokerTransformer
  4. InvokerTransformer
  5. ConstantTransformer

Vòng lặp for bên trong ChainedTransformer.transform() sẽ lấy list này ra, gọi từng method transform() của từng phần tử trong mảng, truyền vào biến object cũ và khi kết thúc sẽ gán kết quả return vào biến object.

# Tại vòng lặp đầu tiên, i=0:

this.iTransformers[0] = ConstantTransformer, sẽ gọi this.iTransformers[0].transform(object) = ConstantTransformer.transform(object), với object = “foo”

ConstantTransformer.transform(object) có dạng như sau:

ConstantTransformer.transform()

Method này chỉ đơn thuần là nhận vào input mà không làm gì, chỉ return lại this.iConstant đã được set từ trước!

Như vậy, khi kết thúc vòng lặp số 0, object được set giá trị mới là class java.lang.Runtime:

# Tại vòng lặp số 2, i=1:

this.iTransformer[1].transform(object) = InvokerTransformer.transform(object)

object = class java.lang.Runtime()

InvokerTransformer.transform() có dạng như sau:

InvokerTransformer.transform()

Với các local variable hiện tại như sau:

  • Class cls = Runtime.class.getClass() => Class.class
  • this.iMethodName = “getMethod” đã được set từ trước
  • this.iParamTypes = Class[] { String.class, Class[].class } cũng đã được set từ trước
  • Method method = Class.getMethod()
  • this.Args = new Object[] { “getRuntime”, new Class[0] }

Cuối cùng, thực hiện method.invoke(input, this.Args).

Trông thì rất là hợp lý và bình thường, nhưng hãy đọc kỹ và sẽ thấy như thể đoạn này được sinh ra để build gadgetchain RCE vậy,

Ở đây đầu vào là 1 class: Runtime.class, hay chính xác hơn là một Object của class java.lang.Class =))). Mỗi 1 class trong java đều là 1 object của class java.lang.Class.

Cùng đọc lại đoạn dòng code get Class của method transform() này:

Class cls = input.getClass();

Đối với cách sử dụng thông thường của method InvokerTransformer.transform() này thì input này là 1 object,

  • Ví dụ như: input là 1 object của class HashMap(). Khi thực thi đoạn code trên, input.getClass() sẽ tương tự như thực thi HashMap.getClass() và trả về class HashMap()

Nhưng với input của chain này lại khác, object input lại là 1 object của Class chứ không phải 1 object thông thường.

Khi thực hiện getClass() sẽ tương tự như việc gọi Class.getClass() và return lại class Class() ¯\_(ツ)_/¯.

Tiếp tục tới đoạn getMethod phía dưới, đoạn code xử lý:

Method method = cls.getMethod(this.iMethodName, this.iParamTypes);

Với this.iMethodName = “getMethod”

Lúc này là chương trình đang thực hiện gọi Class.getMethod(“getMethod”, …) và getMethod() này sẽ trả về method getMethod() của class Class.

¯\_(ツ)_/¯ Nghe hơi rối phải ko, nghĩa là dùng getMethod() để lấy ra cái method tên là getMethod của class Class đó.

Cuối cùng là đoạn invoke method vừa get được:

return method.invoke(input, this.iArgs);

Để hiểu rõ hơn, mình sẽ nói qua về cách dùng của Method.invoke(object, args). Đây là 1 Reflection API cho phép gián tiếp invoke 1 method, Reflection API này sinh ra để invoke method của 1 object không thể cast được vào 1 kiểu xác định nào đó,

Ví dụ như:

  • Trường hợp thông thường, ta có thể khai báo HashMap obj = new HashMap(); và sau đó có thể gọi obj.put(), obj.get() bình thường mà không có vấn đề gì xảy ra.

Nhưng trong trường hợp thực tế, có 1 private class XYZ và public method foo(), từ một nơi nào đó, nhận được 1 (Object)object của class XYZ này.

Để invoke được method XYZ.foo() này, không thể gọi object.foo(), mà phải cast nó sang XYZ -> ((XYZ) object).foo() mới có thể call được.

Vấn đề ở đây là class XYZ được set ở private, nên từ bên ngoài không thể access được class XYZ này, và cast (XYZ) object được.

Và method reflection sinh ra để giải quyết vấn đề này,

Method.invoke(obj, args)
  • obj là object XYZ nhận được
  • Method là method XYZ.foo(), có thể sử dụng XYZ.class.getMethod() để lấy được method này

Quay trở lại với InvokerTransformer.transform(), đoạn code ta đang xem xét là:

return method.invoke(input, this.iArgs);

Với:

  • method lúc này là kết quả của đoạn trước: Class.getMethod()
  • input ở đây là class Runtime
  • this.iArgs = new Object[] {“getRuntime”, new Class[0] }

Ta có thể xem input=Runtime.class là 1 object của class Class, do vậy việc invoke Class.getMethod() với Runtime.class là hoàn toàn hợp lý.

Cuối cùng, có thể xem đoạn invoke này thành:

Runtime.class.getMethod(“getRuntime”, …)

Kết quả trả về của vòng lặp này là Method getRuntime() của class Runtime, => object = method Runtime.getRuntime()

#Tại vòng lặp số 3, i=2:

this.iTransformer[2].transform(object) = InvokerTransformer.transform(object)

object = Method Runtime.getRuntime()

Cũng loằng ngoằng như vòng lặp số 2,

  • input là Method Runtime.getRuntime()
  • method = method Method.invoke() (là cái method Reflection đó)

=> Sau khi loằng ngoằng method.invoke() sẽ thực hiện invoke như này:

Reflection invoke (Method.invoke(Runtime.getRuntime(), …), …)

Là reflection của reflection đó =))).

Và kết quả của vòng lặp này thu được:

object = Runtime object

#Tại vòng lặp số 4, i=3:

this.iTransformer[3].transform(object) = InvokerTransformer.transform(object)

object = Runtime object

  • input của vòng này là 1 Runtime object, không còn là class như trước đó nữa
  • Do object đã là Runtime nên Class cls = Runtime.class.
  • method = Runtime.class.getMethod(“exec”, …) => return Method exec của Runtime.

=> method.invoke() lúc này sẽ là

Runtime.exec(cmd) =>> sink

#! Đoạn invoke này có thể sẽ gây nhiều bối rối ban đầu, mình khuyến khích bạn đọc kết hợp vừa đọc với debug để dễ hiểu hơn!

Sau khi tiếp tục step qua, calc sẽ được pop lên như ý muốn ban đầu:

Vậy là đã xong phần phân tích về CC5, mình cũng ko ngờ là nó dài như vậy, có lẽ phần phân tích gadgetchain sẽ dừng lại ở đây được rồi.

Phần sau mình sẽ viết chi tiết hơn về cách build 1 gadgetchain.

TL;DR:

  • IntellIJ debugger có thể gây ra những lỗi unexpected, do not trust it by 100%
  • CommonsCollections được sinh ra để làm gadgetchain lead to RCE

Một lần nữa cảm ơn IntellIJ đã tài trợ cho bài này!

Happy reading!

__Jang__

293 lượt xem