前一段时间一直在思考如何学习与发掘java反序列化漏洞,最近有一点小的成果,因此便想借着研究java反序列化利用工具ysoserial的机会来验证下自己的思路。
Java反序列化利用
要想掌握Java反序列化漏洞payload的构造,那就要知道在构造Java反序列化的payload的构造过程中有三个比较特别重要的步骤,在这里简称为两点一链:
输入数据执行点
输入数据执行点是指能够将攻击者控制的数据作为java代码执行的位置。
反序列化传导链
反序列化传导链主要用作链接反序列化利用点和用户数据可控点,能够使用户可控的数据在反序列化过程中被利用。
反序列化利用点
反序列化是指在程序的执行流程中有执行反序列化的操作。该位置一般为程序自身实现,只需定位即可。
CommonsCollections1
介绍
Java Collections Framework 是JDK 1.2中的一个重要组成部分。它增加了许多强大的数据结构,加速了最重要的Java应用程序的开发。从那时起,它已经成为Java中集合处理的公认标准。官网介绍如下:Commons Collections使用场景很广,很多商业,开源项目都使用到了commons-collections.jar。 很多组件,容器,cms(诸如WebLogic、WebSphere、JBoss、Jenkins、OpenNMS等)的rce漏洞都和Commons Collections反序列被披露事件有关。
利用条件
JDK <= 1.8u66
CommonCollections < 3.1
CommonCollections =4.0 (commons-collections-4.0删除了lazyMap的decode方法,所以需要将代码中的Map lazyMap = LazyMap.decorate(innerMap, transformerChain);修改为Map lazyMap = LazyMap.lazyMap(innerMap,transformerChain);)
代码分析
整个分析过程并未完全按照ysoserial中的代码进行分析,而是结合前面的思考结果进行分段分析,但是整个payload的生成流程与ysoserial中一致。
输入数据执行点
1 | final String[] execArgs = new String[] { command }; |
根据上面的代码可以看到输入数据执行点就是利用Java中内置的类,能够将用户输入的数据作为代码执行。其中省略好表示的是反序列化传导链中的代码。
反序列化传导链
在CommonsCollections1中反序列化的传导链有两种构造方式,一种是使用LazyMap一种是使用TransformedMap,下面分别介绍这两种构造反序列化传导链的方法。
LazyMap
1 | Map innerMap = new HashMap(); |
从上面的代码可以看出该利用链条有LazyMap,Proxy代理类和最后的AnnotationInvocationHandler实例。下面结合各个类的源代码,对该链条做一个详细的分析:
LazyMap.java1
2
3
4
5
6
7
8
9
10
11
12public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}
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);
}
从上面可以看出,LazyMap的构造函数中可以传入一个Transformer作为参数,而且当从LazyMap实例中获取数据时,如果map中不含有该key,那么就会调用传入的transformer的transform方法。该步骤实现了LazyMap与前面Transformer的链接。
AnnotationInvocationHandler.java
该类的访问控制权限为包内访问,因此需要采用反射的方式进行访问。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 AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
assert va5.length == 0;
if(var4.equals("toString")){
return this.toStringImpl();
} else if(var4.equals("hashCode")){
return this.hashCodeImpl();
}else if(var4.equals("annonationType")){
return this.type;
}else{
Object var6 = this.memeberValue.get(var4);
…………………… // 由于jdk版本不对,需补充源代码
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);//判断第一个参数是否为AnnotationType,因此使用Retention.class传入较好。
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// memberValues.entrySet 方法触发了反序列化
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) {
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
这里只列出了AnnotationInvocationHandler类中使用到的代码,可以看到AnnotationInvocationHandler类的构造函数使用一个注解类和一个Map类构造实例。并且该类具有readObject方法,即能够进行序列化。但是此时有一个问题就是AnnotationInvocationHandler类在反序列化的过程中并没有直接调用其成员变量memberValues的get方法,查看其invoke方法中调用了memberValues的get方法。
因此,想到一个实现思路为利用AnnotationInvocationHandler实例和Map接口构造出一个代理类,当调用Map中的方法时,便会调用AnnotationInvocationHandler实例的invoke方法,进而调用LazyMap的get方法,最后实现对Transformer的调用,造成代码执行。
这就是为什么在构造传导链时需要两次创建AnnotationInvocationHandler的实例。
调用链条:1
2
3
4AnnotationInvocationHandler.readObject
AnnotationInvocationHandler.invoke
LazyMap.get
Transformer.transform
TransformedMap1
2
3
4
5
6
7
8
9
10
11Map innerMap = new HashMap();
innerMap.put("value","key");
TransformerMap transformerMap = TransformerMap.decorate(innerMap,transformerChain);
String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
// 获取 ANN_INV_HANDLER_CLASS类的构造函数
final Constructor<?> ctor = Class.forName(ANN_INV_HANDLER_CLASS).getDeclaredConstructors()[0];
// 设置构造函数可访问
Permit.setAccessible(ctor);
// 创建InvocationHandler实例
InvocationHandler invocationHandler = ctor.newInstance(Retention.class,transformerMap);
从上面的代码中可以看出利用TransformedMap,只需生成一个TransformedMap实例,然后直接构造AnnotationInvocationHandler实例即可。但是需要注意的是,inner.map中输入值的key必需与构造AnnotationInvocationHandler实例时传入的注解类相对应,如Retention.class,则其值为字符串“value”。下面来分析下该传导链条的传导过程。
TransformedMap.java1
2
3public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
AbstractInputCheckedMapDecorator.java (TransformedMap的父类)1
2
3
4
5
6
7
8public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
从上面可以看出,只要调用TransformedMap的setValue方法,便会调用Transformed链条,从而造成代码执行。分析AnnotationInvocationHandler类。
AnnotationInvocationHandler.java1
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 AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);//判断第一个参数是否为AnnotationType,因此使用Retention.class传入较好。
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
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) || // 获取的值不是annotationType类型,便会触发setValue。这里只需用简单的String即可触发。
value instanceof ExceptionProxy)) {
memberValue.setValue( // 此处触发一些列的Transformer
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
前面已经说过其构造函数会接收一个注解类和一个Map实例作为参数。主要是分析其readObject方法,在readObject方法中调用了memberValue.setValue方法。即只要能够经过判断,到达该方法则就会触发反序列化链条,从而造成代码执行。
分析readObject中的代码,主要是循环TransformedMap(即传入的innerMap),然后获取其键值,之后查看注解类中是否有以该键值命名的类型,如果含有该类型,则比较其类型是否为value的实例化,如果不是则调用memberValue的setValue方法。
因此在选择注解类时,使用了Retention类,并为innerMap添加一个“value”为键的值(Retention类中有一个value方法)。
Retention.java1
2
3
4
5
6
7
8@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
反序列化函数调用链
1 | AnnotationInvocationHandler.readObject |
源代码文件
1.源代码工程文件