FastJson反序列化学习-01

ops/2024/12/19 0:00:24/
🌸 FastJson

FastJson是一个由阿里巴巴开发的高性能JSON处理库,支持Java对象与JSON字符串之间的互相转换。

本次漏洞研究基于FastJson1.2.24版本。也就是最早出现FastJson反序列化漏洞的版本。

CVE-2017-18349,FastJson<=1.2.24

🍂 Demo

先来熟悉一下什么是Json

package org.y4y17;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;public class Main {public static void main(String[] args) {String s = "{\"name\":\"Y4y17\",\"age\":17}";JSONObject jsonObject = JSON.parseObject(s);System.out.println(jsonObject);System.out.println(jsonObject.get("name"));System.out.println(jsonObject.get("age"));}
}

这里简单的写了一个DemoString s = "{\"name\":\"Y4y17\",\"age\":17}";创建了一个字符串,然后利用JSON.parseObject方法来将字符串解析为对象。

  • JSON.parseObject,是将Json字符串转化为相应的对象;
  • JSON.toJSONString,是将对象转化为Json字符串。

然而在反序列化的时候,可以指定转化的对象类型!此时JSON.parseObject方法便会将其转化为对应的一个javaBean,比如我们这里存在一个JavaBean

package org.y4y17;public class Person {private String name;private int age;public Person() {System.out.println("调用了constructor方法");}public String getName() {System.out.println("调用了getName方法");return name;}public void setName(String name) {this.name = name;System.out.println("调用了setName方法");}public int getAge() {System.out.println("调用了getAge方法");return age;}public void setAge(int age) {System.out.println("调用了setAge方法");this.age = age;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}';}
}

那么在转化为Java对象的时候可以通过指定要转化的类,来完成对应对象的转化。如下代码:

package org.y4y17;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;public class Main {public static void main(String[] args) {String s = "{\"name\":\"Y4y17\",\"age\":17}";Person person = JSON.parseObject(s, Person.class);System.out.println(person.getAge());}
}

在上面的Person类中的各个setget方法中,打印了相关的方法名,以便更加清晰的看到调用关系。如上代码的执行结果如下:

当我们指定了要转化的类的时候,发现整个转化的过程中,先调用了构造器,然后就是调用相关的set方法和get方法(在这里的get方法是在getAge()调用的时候触发的)。

然而在FastJson反序列化的时候,可以指定一个@type字段,用来表明指定反序列化的目标恶意对象类。比如我们在String字符串里面添加一个@type字段。

String s = "{\"@type\":\"org.y4y17.Person\",\"name\":\"Y4y17\",\"age\":17}"

package org.y4y17;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;public class Main {public static void main(String[] args) {String s = "{\"@type\":\"org.y4y17.Person\",\"name\":\"Y4y17\",\"age\":17}";JSONObject jsonObject = JSON.parseObject(s);System.out.println(jsonObject);}
}

然而在上面的JSON转化为Java对象的时候,通过写入@type字段,实现了指定类的反序列化,成功的调用了Person类中的setget方法以及构造器。

🍂 流程分析

下断点进行调试:

跟进到JSONparseObject方法中:在JSON类中可以看到存在很多种方法,其中他们的参数是不同的:

parseObject方法中:

首先parse主要负责解析我们传递的text,最后便会返回这个Person类对象,在这个过程中就会调用构造器和set方法,而最后的return,将对象强制转化为JSONObject对象,这个过程中会调用到Person类中的get方法!继续跟进到parse方法中:

其中这个parse方法也是一个静态的方法,可以直接在外部进行调用:

在上述的代码中,会创建一个默认的JSON解析器:

DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();

然后利用创建的解析器进行解析,最终返回Person类对象。ParserConfig.getGlobalInstance()是创建了一个解析器,进行初始化,主要就是初始化一些配置,可以跟进一下看:

可以看到这个parserConfig类中的构造器:

放进去了一下默认的反序列化器;比如:derializers.put(Map.class, MapDeserializer.instance);如果是Map类型的话,就使用MapDeserializer反序列化器

同时整个过程中还设置了一些黑名单,也就是当时在这个漏洞出现之前,禁止指定的类:

如这里的java.lang.Thread,线程类。

接下来JSON扫描器,就会从头开始扫描我们传递进来的string

首先就会判断第一个字符是不是一个" { ",如果是的话,也就是代表着一个JSON格式的字符串,进入到if条件里面:

或者是不是一个“[”,如果是的话,其实就是一个数组。由于我们传递的就是一个符合JSON格式的字符串,所以if直接就进入了,其他的都不会进去,此时就会创建好默认的JSON解析器了,下一步就是解析的过程:

跟进到这个parse方法中:

parse方法中通过switch来匹配第一个字符到底是什么,因为我们是一个左大括号,所以继续往下走:

这里便会创建一个JSONObject,他其实是一个Map

这里相当于是创建了一个新的Mapsize0

这里我们继续跟进到parseObject方法中!先是经过一系列的判断:

因为这里我们是“{”,所以这里所有的if条件都不满足,直接过:

接下来经过了一个死循环!必须存在break 或者return才能结束。ifwhile主要是进行了重复的事情,寻找“,”,如果存在的话,就跳过。

然后继续判断是不是双引号和:、}等符号:整个过程中就是取出来了一个key!也就是@type

继续往下走的话,其实会去判断拿到这个key是不是和默认的key一样!默认的可以就是@type

之后就会通过loadClass进行类加载了!继续跟进:

先从缓存中查找是不是已经加载过了,或者有相关的记录(空间换时间),如果存在的话,就直接拿出来用了!

之后便会判断类名的首字符是不是“[”,也就代表着数组,之后又判断是不是以“L”开头,同时以“;”结尾。我们这里都不满足,所以就不管了,继续往下走:

这里就会获取到AppClassLoader,通过AppClassLoader进行加载!然后就会把他放到缓存中,最后return 这个类。此时整个loaderclass也就结束了。

接下来回到了默认的JSON解析器中:

这里if条件都不满足,继续往下走吧:

这里的object就是最开始创建的JSONObject,当时说他是一个Map,整个过程中还没有往里面存放东西,所以他的size就是0,这里的if条件还是不成立,继续往下走就会发现:ObjectDeserializer deserializer = config.getDeserializer(clazz);

config里面获取了一个反序列化器!将我们的类放到里面。而这个获取到的反序列化器就是前面我们看到ParserConfig构造器里面初始化的那些,当然我们这里继续跟进,看看他获取到的反序列化器是什么?

这里从构造器初始化的那些反序列化器中获取对应的,其实是没有的!继续往下走是泛型 然后调用了:getDeserializer

跟进之后,发现还是一直在找这个反序列化器,一直找不到,就各种判断是不是type为空等,是不是自己通过注解的方式写了一个反序列化器,显然我们是没有的!所以这里的if都是进不去的!

之后就开始判断是不是黑名单里面的~ 继续往下走:

然后又去判断了是不是Enum等,到最后一直找不到,就创建了一个JavaBeanDeserializer反序列化器!继续跟进到这个方法中:

这里存在一个变量asmEnable变量,初始化是true

继续走,调用了JavaBeaninfo类中的getBuilderClass方法,由于传递的jsonType是一个null值,最终方法return null。还是继续往下走,superclass复制本身,通过一个死循环,获取这个类的修饰符,判断是不是public。如果不是的话,那么asmEnable就会被设置为false

整个过程中又去判断了是不是参数为空,指定的类是不是一个接口!(反正又是各种不满足!)

最后调用了一个JavaBeaninfobuild方法,跟进看一下:

到这个build方法中,发现获取了Person类中的所有的Filed,以及Public属性修饰的方法,以及默认的构造器!然后判断这个默认的构造器是不是为空,如果不为空的话,就设置一下可访问!

接下来就是三个for循环:

首先第一个就是获取所有的set方法!第二个是获取所有的public属性的变量,第三个就是获取get方法!先看第一个方法:

这里先挨个遍历这些方法,他的方法名字长度是不是小于4,因为set就占了三个字符长度了~ 又判断了这个方法的修饰符是不是静态的,以及方法的返回值是什么,因为set方法一般就是没有返回值的!

当方法是set相关的方法的时候,经过上述的条件判断,一直往下走:

获取到set之后的第一个字母,判断是不是大写的!然后判断了一个静态变量是不是true,如果不是的话,那么就将这个大写字母转换为小写字母!然后将后面的其他字符进行拼接!

之后就是获取这个方法中的变量:

最后通过add方法,把整个获取到信息,全部放入到List里面!

在这个方法中还创建一个FieldInfo,这里我们跟进到这个方法中:

在这个方法中存在一个Feild,对整个逻辑存在相关的影响:

就是这个getOnly变量,正常来说他是一个false,往下走的时候,可以清楚的看到else{ }里面存在着这个变量的覆盖!getOnly=true

正常这个types就是参数的长度,我们这里就是1,如果不是1的话,那就会走到这个else代码里面,然后给getOnly进行赋值!(这里下面也就没有什么其他的东西了,继续往下走就回到了add方法,这里的getOnly有什么用后续再说!)

最终经过上面的for循环,整个List里面就存放了两个值,一个是age,一个是name

之后便是进入到获取类中所有的变量,因为当前的类中是没有Public修饰的变量的,所以就不用看了。

进入到第三个for循环,就是获取get方法,但是这里并不会add(像第一个for循环,寻找set方法时),

因为在这个for循环中,会去判断这个方法的返回值类型是不是Collection或者是不是Map等,如果是的话,才会进行后续的add,还有一个条件就是:

他在找get的时候,会看一下fieldList里面是不是存在这个字段,如果存储过了,那就不会add。所以这里总结一下在寻找get方法的时候,触发add方法的两个条件:

    • 返回值类型需要是Map、Collections等要求的那些
    • 同时fieldList里面没有这个字段(换句话说就是这个字段,只有get方法,而没有set方法!)

最后直接返回了一个JavaBeanInfo,其实在创建反序列化器的整个过程中就是在获取我们这个Person类中的所有的信息!

继续往下走:

下面依然会去通过一些if条件,这个asmEnable还是存在可以修改的情况!比如clazz不是一个接口,默认的构造器是null,同样会修改asmEnablefalse

接下来就是会通过一个for循环!可以遍历Field里面的getonly,如果有一个是true的话,就可以修改asmEnbalefalse了!但是这里并不满足!

最后就会判断这个asmEnable是不是false,如果是的话,就会创建一个JavaBeanDeserializer反序列化器!否则的话,会利用asmFactory去创建一个反序列化器!

所以这里创建的反序列化器并不是默认的那个反序列化器:

而是一个叫FastJsonASMDeserializer的反序列化器!他是一个临时创建的类,所以这里是没办法调试的!回顾整个创建的过程中,其实我们在之前有说到过一个field,就是getonly。当他满足的为true的时候,asmEnable也就变成了false。此时创建的反序列化器就是默认的,此时也就可以进行调试了。

也就是Fieldinfo类中的构造器中,他去获取了方法的参数,判断参数类型的长度是不是1,如果不是的话,就可以进入到else代码中,将getOnly设置为true

那么在往前去找我们从哪里进来这个构造器的:

是在JavaBeanInfo类中的两个for循环中出现的调用!第一个就是寻找set方法,第二个就是寻找get方法!然而在第一个for循环中的FieldInfo里面其实是无法将getOnly设置为true的。原因就是他的参数类型长度肯定是1,所以在第一个for循环中无法设置了!

只能在第二个for循环中进行设置!也就是寻找get方法的for循环。但是之前我们就谈到过这个for循环中的add方法是需要满足条件才能进入的!

他的返回值必须要是if条件里面的才行!所以我们这里需要创建一个返回值是如上类型的set方法!之前还说到一个条件就是 这个field只能有get方法,没有set方法,不然的话,在上面for循环中,将这个field加入到FeildList中就不会再调用get方法了!

所以这里我们定义一个map类型的field

package org.y4y17;import java.util.Map;public class Person {private String name;private int age;private Map map;public Map getMap() {System.out.println("调用了getMap方法");return map;}public Person() {System.out.println("调用了constructor方法");}public String getName() {System.out.println("调用了getName方法");return name;}public void setName(String name) {this.name = name;System.out.println("调用了setName方法");}public int getAge() {System.out.println("调用了getAge方法");return age;}public void setAge(int age) {System.out.println("调用了setAge方法");this.age = age;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", age=" + age +'}';}
}

再次调试,直接断点在第二个for循环那里,因为没有给map变量设置set方法,所以前面的寻找set方法的时候,往FieldList里面加入的还是agename两个field

此时是getMap!我们往下跟进:

这里的if条件满足,所以就会进入到这个if条件里面了!

而且在fieldList里面寻找也没找到,原因就是他根本就没有set方法,所以没找到!

到这里的话就继续跟进到Fieldinfo方法里面:

到这里的时候,getMap方法的参数是空的,所以这里就不满足了,成功的进入到了else代码里面,成功的将getOnly设置成了true

最后return。回到上层,继续看:

此时已经是返回了beaninfo,里面的field就是三个了,分别就是agenamemap;继续代码往下走就会经过for循环,去获取fieldgetonly变量,如果是true的话,就会将asmEnable设置为false!然而上面的mapgetonly就已经被我们设置成了true!所以能进入if条件:

这里可以看到mapgetOnly确实就是true。因此可以设置asmEnablefalse

然后往下走成功进入到if条件,创建了一个默认的JavaBeanDeserializer对象!

然后将成功的创建了一个反序列化器,这个是可以调试的。(上述的目的仅仅是为了调试~ )接下来就是利用反序列化器进行反序列化操作了,继续跟进到deserialze方法中:

发现调用了createInstance方法,其中parser就是默认的JSONparsertype便是指定的类,继续跟进:

最终通过构造器进行了newInstance,也就执行了构造器方法!

接着继续往下走便是setvalue

赋值操作无非就是通过反射或者是通过调用set方法!跟进到这个setValue方法中!

然后就是通过invoke进行调用赋值!这里其实有一个if条件,并没有满足,直接跳到了invoke这里执行了。

这里的getOnly变量的值是false。因为他是一个set方法。最后也就返回了整个调用过程和对象:

可以看到这里并没有执行get方法,最开始的时候,就已经提到了上面是调用set方法,而在toJSON的时候才会进行调用get方法!

继续跟进到toJSON里面:

上面一直都在判断这个clazz是个什么?都不满足,因为他是个Person类!

这里的getobjectWriter就是序列化的方法!

接下来就是创建了一个JSONObject对象(和之前的一样就是一个Map

这里就是获取到了三个键值对!然后往JSONObject里面存放!整个过程中也是通过调用invoke方法来实现的get方法执行:

Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);

javaBeanSerializer.getFieldValuesMap(javaObject)方法中调用了invoke方法:

继续跟进到这个getPropertyValue方法中:

该方法中又调用了get方法!继续跟进到这个get方法中:

get方法中,先是判断了method是不是为空,如果不为空的话,就通过invoke方法来调用get方法。

此时便成功的完成了整个的调用。整个过程中,我们传递的参数,并没有传递map。如果我们传递map的话,在利用反序列化器进行反序列化的时候,也是会调用getMap的(原因是,寻找set方法的时候,mapfield并没有set方法,仅仅有一个get方法!)

🌸 流程总结

整个过程分为三个阶段

    • JSON解析器解析阶段,此时还只是当作JSON字符串来解析
    • Java反序列化器解析阶段,此时是因为在JSON字符串中找到了@type字段,便开始当作是Java对象解析
    • toJSON阶段,此时会调用get方法

思考:整个流程分析完了,那么如何利用这个漏洞/缺陷来进行攻击呢?

    • 只要能找到一个类,该类中的set或者get方法中存在调用链,便可以利用
🍂 本地Demo

尝试创建一个类,实现弹计算器的操作:

package org.y4y17;import java.io.IOException;public class Test {public void setCmd(String cmd) throws IOException {Runtime.getRuntime().exec(cmd);}
}

创建一个Test类!然后我们传递这个类,通过@type,进行指定:

package org.y4y17;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;public class Main {public static void main(String[] args) {String s = "{\"@type\":\"org.y4y17.Test\",\"cmd\":\"open -a calculator\"}";JSONObject jsonObject = JSON.parseObject(s);System.out.println(jsonObject);}
}

运行后成功弹出计算器:


http://www.ppmy.cn/ops/143024.html

相关文章

SQL 标准定义了哪些事务隔离级别?

SQL标准定义了四个事务隔离级别&#xff0c;它们分别是&#xff1a; READ UNCOMMITTED&#xff08;读取未提交&#xff09;&#xff1a; 最低的隔离级别。允许读取尚未提交的数据变更。可能会导致脏读、幻读或不可重复读。脏读是指一个事务可以读取到另一个事务未提交的数据。 …

ADB在浏览器中:ya-webadb项目安装与配置完全指南

ADB在浏览器中&#xff1a;ya-webadb项目安装与配置完全指南 ya-webadb ADB in your browser [这里是图片001] 项目地址: https://gitcode.com/gh_mirrors/ya/ya-webadb 项目基础介绍与编程语言 ya-webadb 是一个由 Yume-chan 开发的开源项目&#xff0c;它实现了ADB&#x…

基于大数据爬虫数据挖掘技术+Python的线上招聘信息分析统计与可视化平台(源码+论文+PPT+部署文档教程等)

博主介绍&#xff1a;CSDN毕设辅导第一人、全网粉丝50W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流✌ 技术范围&#xff1a;SpringB…

Android Studio创建新项目并引入第三方so外部aar库驱动NFC读写器读写IC卡

本示例使用设备&#xff1a;https://item.taobao.com/item.htm?spma21dvs.23580594.0.0.52de2c1bbW3AUC&ftt&id615391857885 一、打开Android Studio,点击 File> New>New project 菜单&#xff0c;选择 要创建的项目模版&#xff0c;点击 Next 二、输入项目名称…

在 Docker 中运行 Golang 应用程序,如何做?

文章精选推荐 1 JetBrains Ai assistant 编程工具让你的工作效率翻倍 2 Extra Icons&#xff1a;JetBrains IDE的图标增强神器 3 IDEA插件推荐-SequenceDiagram&#xff0c;自动生成时序图 4 BashSupport Pro 这个ides插件主要是用来干嘛的 &#xff1f; 5 IDEA必装的插件&…

浅谈Java注解之CachePut

一、CachePut的介绍 Java注解CachePut是Spring框架中用于缓存操作的一部分&#xff0c;主要用于更新缓存中的数据。 功能说明 CachePut注解用于在方法执行后更新缓存中的数据。与Cacheable不同&#xff0c;CachePut注解的方法总是会被执行&#xff0c;并且其返回结果会被放入缓…

概率论得学习和整理27:关于离散的数组 随机变量数组的均值,方差的求法3种公式,思考和细节。

目录 1 例子1&#xff1a;最典型的&#xff0c;最简单的数组的均值&#xff0c;方差的求法 2 例子1的问题&#xff1a;例子1只是1个特例&#xff0c;而不是普遍情况。 2.1 例子1各种默认假设&#xff0c;导致了求均值和方差的特殊性&#xff0c;特别简单。 2.2 我觉得 加权…

[图形渲染]【Unity】【游戏开发】Shader基础9 什么是固定管线渲染?

在图形渲染领域,**固定管线渲染(Fixed-Function Pipeline)**是一种历史悠久的渲染方法,曾是早期图形API(如OpenGL和DirectX)的核心设计思想。尽管它已经逐步被现代的可编程管线取代,但理解固定管线的概念对于学习图形渲染的演进和基础非常重要。 1. 什么是固定管线? …