Ysoserial Commons Collections 5 Analyst
Ysoserial Commons Collections 5 Analyst
Tổng quan CommonsCollections 5 trong Ysoserial
CommonsCollections là một trong những gadget chain nổi tiếng nhất trong các cuộc tấn công khai thác Java deserialization không an toàn, đặc biệt khi ứng dụng sử dụng thư viện Apache Commons Collections.
Trong bài viết này, chúng ta sẽ tập trung vào CommonsCollections5 (CC5) — một trong các chain được tích hợp sẵn trong công cụ ysoserial . Mình chọn phân tích CC5 vì đây là chain được nhiều người đề xuất để học do tính minh bạch và dễ debug.

Thiết lập môi trường
Mở dự án ysoserial trong IntelliJ IDEA (hoặc IDE tương đương). Yêu cầu:
- JDK 8
- Thư viện commons-collections:3.1
Mở Ysoserial project sau khi tải từ github link ở đây:
https://github.com/frohoff/ysoserial
Ở đây mình sử dụng IntelliJ để tiến hành test và debug.

Khi vừa mở lên thì ta có thể thấy được GadgetChain được giới thiệu sẵn ở đây và kèm với đó là điều kiện để có thể chạy được để nó gen ra payload ở đây ta sẽ dùng JDK 8.

Payload
1 | package ysoserial.payloads; |
Khi ta chạy được lệnh java -jar ysoserial.jar CommonsCollections5 "calc.exe" payload được tạo ra và khi victim deserializes nó, calc.exe sẽ được thực thi.


Vậy là ta chắc chắn rằng nó chạy được không vấn đề gì nên ta sẽ đi vào phân tích và debug gadget.
Phân tích từng phần và Debug
Theo như comment thì ta có đống gadget như sau :
1 | Gadget chain: |
ObjectInputStream.readObject() thì không có gì để nói vì nó chỉ để thực hiện read object đã được serialize nên ta đi vào gadget luôn. Ta tới với gadget 1 –> 2 :
1 | Gadget 1:BadAttributeValueExpException.readObject() --> Gadget 2:TiedMapEntry.toString() |
BadAttributeValueExpException.readObject() –> TiedMapEntry.toString()
Class BadAttributeValueExpException (package javax.management) được thiết kế để ném exception khi giá trị thuộc tính JMX không hợp lệ.
Tuy nhiên, phương thức readObject() của nó ghi đè phương thức mặc định và gọi .toString() trên trường val nếu:
- val != null
- val không phải là String
Và một số điều kiện về SecurityManager hoặc kiểu dữ liệu
Trong payload, ta gán val = new TiedMapEntry(lazyMap, "foo") → không phải String → .toString() được gọi.
Đây chính là điểm khởi phát của toàn bộ chuỗi!


Điều kiện để valObj được gọi lần lượt là :
- valObj
!= null. - valObj
không phải String. - HOẶC
System.getSecurityManager() == null. - HOẶC
valObj là primitive wrapper(Long, Integer, etc.)
Bây giờ ta sẽ đặt breakpoint ngay tại đoạn Object valObj = gf.get("val", null) và val = valObj.toString() và tiến hành debug từng bước xem nó sẽ gọi tới đâu.

Có thể thấy rằng khi đặt breakpoint ở line 72 và thực thi thì valObj bây giờ là một object nằm bên trong TiedMapEntry.


Sau đó sau khi đi từ breakpoint ở dòng 86 thì valObj được gọi đến bên toString của class TiedMapEntry .

TiedMapEntry() –> LazyMap.get()
Ta sẽ tiến hành tiếp tục phân tích gadget thứ 2 là từ TiedMapEntry() tới LazyMap.get().

Ở class TiedMapEntry.java ta có thể thấy rằng nó call 2 giá trị đó là getKey() và getValue().
Đầu tiên ta sẽ thử click rồi follow method getKey() xem thử nó có gì.


Có vẻ không có gì nó chỉ là get key từ object key không có gì để phân tích nên ta sẽ follow method getValue().

Thấy được rằng getValue() có gọi đến map nên ta sẽ set một cái breakpoint nằm ở ngay đoạn getValue().

Ồ ta thấy rằng nó gọi đến class LazyMap là một trong gadget của ta cần tới tiếp tục follow để xem nó có gọi đến LazyMap.get không.

Chuẩn theo gadget ta xác định được nó call đến LazyMap.get .
1 | public Object get(Object key) { |

Luồng xử lý nó đơn giản chỉ là :
- getKey() → trả về “foo” (không có gì đặc biệt)
- getValue() → gọi map.get(key) → đây chính là LazyMap.get(“foo”)
- Vì “foo” chưa tồn tại trong LazyMap, phương thức get() sẽ:
- Gọi factory.transform(“foo”)
- Lưu kết quả vào map
- Trả về giá trị đó
→ factory ở đây là ChainedTransformer, nên transform() sẽ được gọi.
Đã nạp key vào LazyMap bây giờ ta đã đi đến cuối gadget 2 bây giờ đi tiếp gadget tiếp theo.

LazyMap.get() –> ChainedTransformer.transform()
Ta bay vào class LazyMap sau đó tìm được hàm get của nó sau đó tiến hành set thêm 1 cái breakpoint ở đây xem luồng xử lý nó sẽ như thế nào.


Rồi ở đây ta thấy nó đã chạy xuống dưới là hàm :
1 | Object value = factory.transform(key); |
Ở đây giá trị key đã có và được gán là foo nên điều kiện đúng và nó chạy đúng theo gadget. Trong ysoserial, factory được set thành ChainedTransformer và ta thấy rằng ở đây rằng hàm ChainedTransformer đã được gọi lên để ghép vào phần gadget này.
ChainedTransformer.transform() –> Phần còn lại
Như ở phần giới thiệu đã nói ngay sau LazyMap.get() là một chuỗi transform bao gồm :
1 | ChainedTransformer.transform() |
Từ cái breakpoint trước ta tiếp tục chạy nó đưa ta vào hàm public Object transform(Object object).

Ở đây ta có thể thấy trong method transform của chain này nó sẽ tiến hành loop hết các giá trị bên trong i.Transformers sau đó thực hiện gán nó vào Object và return về có thể coi rằng bước loop này chính là chain mọi cái transform lại với nhau.

this.iTransformers[] trong gadgetchain này được set cho các giá trị lần lượt là:
- ConstantTransformer
- InvokerTransformer
- InvokerTransformer
- InvokerTransformer
- ConstantTransformer
Bây giờ ta sẽ test từng vòng loop để xem nó làm những gì.

Ở đây với i=0 thì nó được gán Object là foo ở đây nó dạng this.iTransformers[0] = ConstantTransformer nó gọi ConstantTransformer(object).

ConstantTransformer sẽ có dạng như dưới đây :

Ở đây ta sẽ thấy rằng giá trị của iConstant đã set giá trị của Object trở thành class lang.java.Runtime.


Tới với vòng lặp thứ 2 là i=1 ta có :
this.iTransformer[1].transform(object) = InvokerTransformer.transform(object)
object = class java.lang.Runtime()
InvokerTransformer.transform()

Ở đây ta có các giá trị bao gồm :
- Class cls : nó là class java.lang.Class.

this.iMethodName=cls.getMethodnó đã được set ở trước.

this.iParamTypes = Class[] { String.class, Class[].class }: cũng đã được set giá trị trước.

Method method= Class.getMethod().

this.Args = new Object[]{ “getRuntime”, new Class[0] }.

Cuối cùng là nó thực hiện return method.invoke(input, iArgs) dưới đây là hình minh hoạ dễ hiểu cho method InvokerTransformer.

Ở đây ở dòng
Class cls = input.getClass();có sự khác biệt khá lớn so vớimethod InvokerTransformer.transform()thông thường vì method này thường sẽ input là một object nhưng ở đây nó lại là 1 class cụ thể làclass java.lang.Runtimethật là ảo giác từ đó khi thực hiệngetClassnó sẽ giống như gọiClass.getClass()rồi return nó lạiclass Class.Sau đó đến dòng
Method method = cls.getMethod(iMethodName, iParamTypes);thì nó cũng lú tương tự, ở đây nó sẽ dùngClass.getMethodvà method này sẽ trả vềgetMethodcủaclass Class.Và ở cuối sẽ là
method invoketa đã lấy ở bên trên ở đây nó sử dụngMethod.invoke(object, args)nó là Reflection API ở đây nó sử dụngReflection APInày là để invoke một method của object khi nó không thể cast theo một kiểu đã được xác định trước. Ta có thể hiểu nôm na rằng ở đây ta có mộtprivate class ABCXZYnào đó và mộtpublic method foo()nào đó từ nơi khác nhận đượcobjectcủaclass ABCXYZ. Thì ở đây thường đểinvokeđược methodABCXYZ.foo()này ta sẽ không thể gọi thẳng đếnobject.foo()mà ta phải cast nó sang dạng kiểu ((ABCXYZ)object).foo(). Nhưng cái dở hơi ở đây ta lại khai báo nó làprivate classthì sao mà đưa ra ngoài được thì method Reflection giải quyết cho ta vấn đề trên.return method.invoke(input, this.iArgs);cũng không có gì với vòng lặp thứ 2 này kết quả của invoke này là kết quả của đoạn trước tại đây object của nó làclass Runtimeta có thể xeminput=Runtime.classlà 1objectcủaclass Classsau đó kết quả của vòng lặp thứ 3 này nó làmethod Runtime()bây giờ ta đi đến vòng lặp thứ 4
Tới với vòng lặp thứ 4 tại đây giá trị i = 3.


Tại đây giá trị của iMethodName đã là exec cùng với đó là iArgs nay đã được gán giá trị là calc.exe.

Debug đến đây nó sẽ thực thi invoke chạy hàm exec đến calc.exe tại đây máy tính đã được popup lên.

Vậy là kết thúc phân tích CC5 của Ysoserial.
Nguồn tham khảo phân tích: https://sec.vnpt.vn/2020/02/the-art-of-deserialization-gadget-hunting-part-2