fastjson
fastjson是阿里巴巴提供的高性能json数据解析库 可以高效的将java对象转换为json数据
也可以接收json数据转换为原来的javaBean
虽然是一个很好用的json数据解析库 但是漏洞频出 java安全必学的一个组件
fastjson原理
配置依赖
1 | <dependency> |
先简单使用上手一下
1 | package com.test; |

根据输出结果可以得到 在转换为json数据时 会调用对象中的getter方法去获取具体属性的值
调试一下对象 to json的源码

进过多层方法调用

获取SerializeWriter out 用来装我们的json数据的

JSONSerializer用来序列化我们的Object对象 重点来到这个write方法

write方法中会获取对象的class类 再获取这个class的对象writer
跟进this.getObjectWriter(clazz)

层层调用到这个方法里

上面的逻辑是要找可以序列化这个类的对象writer
由于是我们自己写的 找不到这个class类的writer 最后直接将他放入SerializeConfig里面

最后再次获取writer
writer 包含了之前的java对象的大部分信息 还获取了所有的getter方法

回到这里 开始序列化

序列化结束就直接输出

总的来说 这里的序列化是使用了对象中的getter方法 然后获取到对象属性值
这里的序列化和java原生的序列化不同 并没有使用read方法
反序列化 json to javabean对象
反序列化的时候有3个方法
1 | JSON.parse() |

获取到的json数据 直接原样输出了 从结果上来看并没有进行正确的反序列化
因为这里解析器不知道要转化为什么对象 所以默认会转换为JSONObject对象

给定了要转换的class类 成功转换
从输出上可以知道转换的时候调用了setter和getter方法
先来调试一下parse方法


创建DefaultJSONParser实例

会先调用上面的方法 JSONScanner会去扫描我们json数据 返回一个JSONLexer

这里会判断json数据里面第一个字符是什么 然后next 换到下一个

开始尝试反序列化

根据当前字符判断 应该干什么
这里的前面基本都是去解析json数据格式的逻辑 还没到我们想要的

这里会创建一个JSONObject对象 是用来后面进行对象转换用的

这里返回的对象是空的

这里的parseObject也是判断json数据格式用的

这里会获取key 记住lexer.ScanSymbol方法 会获取一个键名

重点来到这里

这里的key和DEFAULT_TYPE_KEY比较重要
key的值是我们的一个键的名字 DEFAULT_TYPE_KEY是@type

这里默认就会是true 也就是说只要前面的key成立就会执行
如果判断成立就会获取对象类型 typeName 也就是我们想转换为的对象
1 | String typeName = lexer.scanSymbol(this.symbolTable, '"'); |
这里会获取第一个键值并loadclass
这么来看 在源码中存在一个自定义加载类的功能 前提是你的第一个键名要是**@type** 并且值为你要转换的类的全限定包名
我们这里就没有设置 所以就会掉过了
最后就直接输出原来的json数据了

后续会搞成JSONObject对象 (但是这都不重要了
总结

反序列化的时候 我们可以在json数据里面 第一个键的位置写一个@type并写入任意一个类 这样在反序列化的时候解析器会自动的解析成这个类
前面调用的时候 又知道会调用setter或者getter方法 那么基本了解为什么会导致漏洞了
可以加载任意一个类 并且还会调用getter setter方法
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl(类加载) com.sun.rowset.JdbcRowSetImpl(jndi)这些类都在fsatjson反序列化的时候触发
其他
私有属性 共有属性
在没有setter方法的时候 并且属性是私有属性 不会被赋值


但是getter方法仍然被调用了
当属性为共有属性的时候并且也没有getter的时候 仍然可以赋值

私有属性仍然想被赋值该怎么办
添加参数 Feature.SupportNonPublicField
JSON.parse(jsonstring,Feature.SupportNonPublicField)
即可被赋值

getter setter
然后在反序列化的时候会自动调用这些 setter 与 getter 方法的调用,注意!并不是所有的 setter 和 getter 方法。
下面直接引用结论,Fastjson会对满足下列要求的setter/getter方法进行调用:
满足条件的setter:
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter:
- 非静态方法
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
具体源码可以看这位师傅的调试过程
如果是parseObject就都会触发getter和setter getter触发就没上述要求
parse 与 parseObject区别
基本没什么区别 但是后者parseObject最后返回的是JSONObject
所以他一定会触发getter与 setter
parse 只会触发setter 没有setter在触发getter
fastjson漏洞分析
说了这么多 直接来试试漏洞利用
1.2.0~1.2.24
对于@type加载类没有任何限制 所以可以直接打
1 | String payload = "{\n" + |

分析一下

直接来到这里


根据前面的分析 这里最为重要

判断第一个键名为@type 进入判断语句
获取typeName
开始加载这个类

下面有2个奇怪的判断语句 在这个版本区间还没用

这里的逻辑是通过查找mappings中有点class类去加载 显然这里是不可能有的

获取当前线程的类加载器 去加载

加载成功 并把这个class类放入mapping中 返回加载的class类

也是获取deserializer然后去进行反序列化
反序列化的时候就触发了getter方法然后触发漏洞了
总结一下
这个版本区间没有做任何的限制 可以随便打
1.2.25~1.2.41
从这个版本开始 就添加了黑名单和白名单和AutoType的限制
分析一下
我们在拿之前的payload打一下
1 | String payload = "{\n" + |

提示我们autoType不支持这个类
我们继续调试一下

重点关注这个地方 之前就是在这里把我们想要加载的类给放到mapping中的
现在这里多了个checkAutoType的函数
跟进一下

这里面需要autoTypeSupport为true 后面还有白名单和黑名单的检测
expectClass默认也是null 所以要想成功加载类 就必须要使得autoTypeSupport为true
我们看看这个类

默认情况autoTypeSuppory为false
并且denylist里面有很多的包 acceptlist里面默认是空的

后续是先黑名单检测然后再白名单 但是黑名单检测就过不了就直接抛出异常了
黑名单里面的包有这些
1 | this.denyList = |
我们先把这个autoTypeSuppory参数设置为true在打打看


函数checkAutoType函数这里 如果autoTypeSuppory为true 是先白名单之后再黑名单 如果白名单过了就直接加载这个类
但是默认情况白名单就是空的 所以得尝试绕过黑名单
我们先修改一下我们的payload
1 | String payload = "{\n" + |
这里@type的值变为acom.sun.rowset.JdbcRowSetImpl 显然是不可能在黑名单里面的

继续来到这里 显然是可以绕过黑名单的 我们看看后续逻辑

来到这里的loadClass 之前也是在这个地方把我们的类给加到mapping中 成功加载类
继续跟进一下

在这里面有2个奇怪的判断
第一个判断是检测第一个字符是不是[ 左中括号 如果是的话会加载这个类 最后返回一个数组 里面有我们要加载的class对象
第二个判断是检测首尾字符分别是否是L和; 如果是的话就去到首尾字符 加载这个类
在之前的那个版本里面 这里的判断我们没有去管

因为后面也会将我们的类加入这个mappings里面 后面就会加载这个类
但是在现在 我们这个类实际没法被加载 所以得通过上面的2个判断语句去加载我们的类
于是现在的payload就变成了这样
1 | String payload = "{\n" + |
1 | String payload = "{\n" + |
先试第一种

继续来到这里

通过类第二个检测 开始加载类了 这里的效果就是重新一次loadClass这个函数
后续就是成功将这个类加入mappings里面 后面就和之前的一样了 反序列化之后就成功触发getter从而触发漏洞



成功执行
我们使用第二个payload试试

理论这里应该就可以了 但是这里报错提示第43个字符需要是[
我们继续修改payload
1 | String payload = "{\n" + |

有提示要是{ 而不是,
继续修改
1 | String payload = "{\n" + |

这下成功了 成功实现数组绕过
其实这里很奇怪 为什么要这么畸形的json数据他会去加载这个类 虽然根据报错修改了payload最后也成功执行了
后面也看了一下源代码 就是在分析的时候不符合fastjson对数据格式的要求就是了 感觉这个形式就很奇怪
可以自己动手去调试一下
但是也确实绕过去了
总结一下
这个版本添加了autoTypeSuppory 和 黑白名单
可以通过在类名单首尾添加L 和 ;绕过
也可以通过构造畸形的数组形式绕过
这里的前提就是autoTypeSuppory 要为true
1.2.42
1 | String payload = "{\n" + |
继续直接打 然后看报错信息是个什么情况

分析一下
提示不支持 调试看看

现在ParserConfig里面的黑名单直接玩起hash表了 没法直接看懂黑名单里面有哪些包名

现在checkAutoType方法里面都是各种hash值
目前来看 确实看不懂和之前有什么区别
我们继续调试下去

第一个判断首尾的hash 我们这里的payload过了 猜测实际逻辑和之前没区别

这个h3获取了前3个字符的hash

开始判断白 黑名单 通过判断第四个字符之后的字符 去判断是否和黑名单匹配或者和白名单匹配
先白后黑

最后也是抛出异常了
这个版本他提前判断并删去首尾字符 在去判断白黑名单了
那么我们试试双写绕过

现在是只有一对L和;
显然是可以绕过黑名单的

依旧进入这个loadclass

这下看懂了
依旧是原来的工具类里面的loadclass

注意这里的className是最开始的 也就是有2对的形式 但是没关系 实际情况是会迭代调用 也就是会迭代的删去首尾字符
最后仍然可以加载 所以说一开始的payload至少是要有一对以上的L和;就可以绕过

依旧弹窗了
数组的话是没有限制的 所以可以直接打
1 | String payload22 = "{\n" + |

总结一下
这个版本开始 判断都用到hash了 但是实际逻辑却没有改变 只是搞了hash当幌子罢了
双写绕过或者数组绕过
1.2.43
1 | String payload22 = "{\n" + |
依旧双写

依旧是报错了
分析一下

直接看checkAutoType方法
现在第一个判断是嵌套了2个判断 只要双写就直接报错了
修改的也是简单粗暴 依然没有对数组的限制可以打数组绕过
1 | String payload22 = "{\n" + |

总结一下
只能打数组绕过了
1.2.44

这下是数组也没法绕过了

现在只要开头是[ 或者首尾是L和;就直接报错
至此到这个版本开始 没法再通过在@type名加点小东西去绕过任意类加载了
分析一下
我们可以根据之前的情况想想还能怎么打
其实解析的工作可以简单概括为:
接收json数据判断@type类型 -> 判断@type中的类是否可以被加载 -> 如果可以的话会放入一个mappings中 (可以被加载的类的hash表) -> 之后就是获取这个类的反序列化者 然后反序列化
在判断类的时候有黑白名单 如果可以直接绕过黑名单就可以直接打
也就是找后续版本不在黑名单里面的类去打
另一种思路是可以尝试直接在mappings里面放我们想要加载的类
当然这些的前提都是建立在AutoTypeSupport为true的前提下
其实AutoTypeSupport的情况更为重要
因为默认情况AutoTypeSupport为false 所以没有在AutoTypeSupport为false的情况下的打法的话 没有实际意义
这个版本先跳过
<=1.2.47
这个版本直接报了个很严重的cve cve2017-18349
无需AutoTypeSupport为true就可以打
我们拿大佬的payload去调试看看 具体是个什么情况打出来的
看之前先看一下这个payload
1 | {"@type":"java.net.Inet4Address","val":"dnslog.com"} |
这个类是在可以被默认的反序列化者的findclass找到 也就是说默认就支持
但是它的第二个键值对是val
实际上Inet4Address就没有val这个属性对应的方法
那么为什么可以打呢
我们也来调试一下

依旧来到checkAutoType方法

上面肯定是可以过的

来到这个方法这

指定的类可以被找到 虽然不是在mappings中

后续获取deserializer 类名叫MiscCodec是fastjson提供的
进入deserialize方法

这里要求我们的json数据里面 得有一个键为val 并且得是先@type 再是val
后续就简单了

获取val的值 放到objVal中 后面又放到strVal中

上面检测一堆类名 最后直接是调用了getByName这个方法 会解析域名

成功解析
也就是说 如果是可以允许的类 还必须要有val这个键
分析一下
接下来分析大佬的payload
1 | String payload3 = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," |
这里是map里面又有map 嵌套的形式外层的map里面有2个内层map 因此会解析2次

继续调试

这里的上面会判断是不是嵌套的形式 发现是会解析内层的map 于是开始解析内存map
也就是
1 | {"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}} |
从现在开始就是递归调用parseObject去解析数据了

依旧是来到这个地方 可以看到typename是java.lang.Class 显然是一个可以被允许的类
直接进入checkAutoType


返回java.lang.Class的class对象

来到这里 根据之前的分析 应该也会让val的值做些什么

成功来到loadClass 直接会加载val里面的值

val里面是我们指定的类 在cache为true时可以直接将指定的类放到mappings里面 后续解析的时候就可以成功被加载

后续轮到b中数据做解析 后续就成功加载指定类了

成功解析
总结一下
1 | String payload3 = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," |
这次的绕过方式是通过 java.lang.Class 反序列化器在反序列化的时候 会自动的在mappings中添加我们的指定类的方法去加载类
并且是不需要AutoTypeSupport的
第一次解析将类注入mappings中
第二次解析正式触发漏洞
verison <= 1.2.47 payload总结
由于这个版本可以无限制加载类 所有值得总结一下
打JdbcRowSetImpl 实质是打jndi 得看jdk版本在打
1 | { |
1.2.48
分析
继续如法炮制
依旧报错


在第一次将类加入mappings中 cache默认的设置为了false
现在好了又给他修了
要么在饶黑名单 要么就是找一个其他的类 进行二次解析
也先跳过这个版本
<= 1.2.68
分析一下
在这个版本的时候 又有大佬找到了其他思路
1 | String payloaddd = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.example.VulAutoCloseable\",\"cmd\":\"calc\"}"; |
1 | package org.example; |
就不用java.lang.Class 使用其他类org.example.VulAutoCloseable
这里的绕过方式和之前的也有点不一样了 使用了exceptclass这个参数
依旧调试

跟进checkAutoType方法
这里的expectClass为null

上面会根据expectClass来判断expectClassFlag应该是什么 这里是false

这里获取到这个AutoCloseable接口的class对象

最终返回

获取clazz的deserializer

返回JavaBeanDeserializer

现在开始反序列化

没有匹配到数据的时候 会判断当前的key

上面会判断当前键值也就是org.example.VulAutoCloseable 是否能找到他的deserializer 显然是找不到的
如果当前的key是@type 又会进入checkAutoType方法 这次带上了expectClass和键值
跟进checkAutoType

expectClass为AutoCloseable的class对象 因此会判定为true

会进入一次白黑名单检测 这里也没啥用

尝试获取 class对象 显然是没有的

又检查了一次

重点来自这里
这里除了expectClassFlag其他都是false 因此可以进入判断
我们的目标类也被成功loadclass

这里被当前的ClassLoader加载了 成功返回class对象
由于cache为false没有将这个类加入mappings中

payload最巧妙的地方就在于这里 当expectClass的类是clazz的父类的时候 clazz就可以被添加到mappings中

至此clazz的deserializer也被成功找到 后续反序列化就成功执行代码

总结一下
1 | String payloaddd = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.example.VulAutoCloseable\",\"cmd\":\"calc\"}"; |
我们可以试一下 换一个其他父类会怎么样 为什么一定要是java.lang.AutoCloseable呢
其实理论来说 这里可以是其他符合条件的类
不在黑名单中并且不是这些类的类
1 | else if (expectClass != Object.class && expectClass != Serializable.class && expectClass != Cloneable.class && expectClass != Closeable.class && expectClass != EventListener.class && expectClass != Iterable.class && expectClass != Collection.class) { |
这里的绕过方式是通过expectClass绕过AutoType方法
=> 1.2.68
在调试的时候可以明显看到一个东西

自从1.2.68开始 添加了一个safemode 直接把@type功能给砍掉了
至此 只要safemode添加为true @type就无法生效
也就没法再加载任意类了
后续的版本都是在safemode没有为true的前提下打的
但是默认情况下safemode就是为false 提供给开发者一个口子 看开发是否配置了
1.2.69
之后的版本
1 | String payloaddd = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.example.VulAutoCloseable\",\"cmd\":\"calc\"}"; |
1 | if (expectHash != -8024746738719829346L && expectHash != 3247277300971823414L && expectHash != -5811778396720452501L && expectHash != -1368967840069965882L && expectHash != 2980334044947851925L && expectHash != 5183404141909004468L && expectHash != 7222019943667248779L && expectHash != -2027296626235911549L && expectHash != -2114196234051346931L && expectHash != -2939497380989775398L) { |
这个又打不了了 因为在判断expectClass时 不让是java.lang.AutoCloseable了 但是这里也类似于黑名单
1.2.83
1.2.83是fastjson1最后的版本
从1.2.68开始 只需要你safemode设为true 就不会再有fastjson反序列化rce的攻击了
fastjson2
从第二代开始 至少现在没有爆出可以rce的cve 最多只有dos的cve
因为他就不支持@type加载类了
总的来看 fastjson漏洞的万恶之源就是 @type可以指定要加载的类 开始的
其他
从通过黑白名单绕过限制之后
AutoType实际上好像没有什么用 看起来好像不是true也没什么问题
从绕过手法上来看确实是这样
但是如果还想从其他类里面找一些不是黑名单的类 依旧得通过 AutoType为true的方式去加载类

绕过思路基本就是绕黑名单了 要么直接硬刚denylist里面的黑名单 但是这个绕过方法前提还得AutoTypeSupport为true
要么就是 找expectClass 绕过那个expectHash检测
payload大全
这里收录了一些具有实战意义的payload
万能payload 出网就能用
1 | { |
JdbcRowSetImpl
1 | { |
TemplatesImpl
1 | { |
JndiDataSourceFactory
1 | { |
SimpleJndiBeanFactory
1 | { |
DefaultBeanFactoryPointcutAdvisor
1 | { |
WrapperConnectionPoolDataSource
1 | { |
JndiRefForwardingDataSource
1 | { |
InetAddress
1 | { |
Inet6Address
1 | { |
URL
1 | { |
JSONObject
1 | { |
URLReader
1 | { |
AutoCloseable 任意文件写入
1 | { |
BasicDataSource
1 | { |
JndiConverter
1 | { |
JtaTransactionConfig
1 | { |
JndiObjectFactory
1 | { |
AnterosDBCPConfig
1 | { |
AnterosDBCPConfig2
1 | { |
CacheJndiTmLookup
1 | { |
AutoCloseable 清空指定文件
1 | { |
AutoCloseable 清空指定文件
1 | { |
AutoCloseable 任意文件写入
1 | { |
BasicDataSource
1 | { |
HikariConfig
1 | { |
HikariConfig
1 | { |
HikariConfig
1 | { |
HikariConfig
1 | { |
SessionBeanProvider
1 | { |
JMSContentInterceptor
1 | { |
ContextClassLoaderSwitcher
1 | { |
OracleManagedConnectionFactory
1 | { |
JNDIConfiguration
1 | { |
< 1.2.68
无需AutoTypeSupport
1 | { |
其他payload
小结
fastjson应该是我第一个搞了这么久的java组件
每次版本的更新就像每道更难的ctf题一样
这里感谢一些师傅的文章分享 没有这些文章 我是不可能搞明白这些的
说些什么吧!