前言
这几天在一直研究fastjson反序列化漏洞,从1.2.24版本开始一直到1.2.68版本,其漏洞分析及其利用在网上还是很多的,但不知是大佬们有意为之还是怎样,各博客上写的payload大致差不多,但其实在1.2.68的漏洞中,能用的思路不只有AutoCloseable这一个,还有其他方式也有可能导致RCE,我相信大佬们的手中依然有没有放出的payload。
目前最新1.2.75版本的漏洞(在我看来确实是漏洞,因为确实执行了反序列化攻击)的原因是我在分析1.2.68中找到的,我认为作者应该是知道的,因为原理和AutoCloseable是一样的,可能作者认为利用条件比较多,难以构成威胁,但作为学习之用还是很好的素材。
旧版本漏洞回顾
关于这个话题知乎上有答主做了高质量分析
fastjson到底做错了什么?为什么会被频繁爆出漏洞?
这个帖子很详细的总结了先前fastjson网上公开的漏洞细节,不过唯一不敢苟同的是作者在提到1.2.68版本中,主要利用方式是利用异常进行攻击,并且要重写getMessage方法。
其实在1.2.68版本中,利用最广泛的是使用AutoCloseable这个接口绕过checkAutoType,网上大多也以这种方式展开分析。因为AutoCloseable构成的payload所需要的条件可能没有那么复杂。文章中说1.2.69已经修复了异常攻击漏洞,但是经过测试似乎并没有。
AutoCloseable比较完整的利用链:
fastjson 1.2.68 反序列化漏洞 gadgets 挖掘笔记
1.2.68的漏网之鱼
截止这篇文章发出,fastjson在github上的最新版本为1.2.75。
要分析1.2.75漏洞产生原因,我们还是要回到1.2.68版本的漏洞分析中去,本文不会去重提网上提到的一些常见的1.2.68的分析,会稍微提一下被人没有提出来的。
“系统的白名单”
fastjson1.2.68当初产生的主要原因在于,fastjson为了再次避免用户用其AutoType
机制进行反序列化而产生漏洞,加入了checkAutoType
方法,AutoType
机制的标志就是“@type
”这个标签。checkAutoType
这个方法本意是不想让用户使用AutoType
,除非用户自己开启AutoType
。然而在checkAutoType
方法实现过程中还是没忍住偷偷用了几下(标志是解析了"@type
"标志),但加强了限制。在代码审计过程中,我发现了一行非常重要的代码。
这行代码位置在com.alibaba.fastjson.parser.ParserConfig
第1326行
clazz = TypeUtils.getClassFromMapping(typeName);
(这行代码在fastjson宏观意义上有何作用暂不知晓)
其中typeName
变量是用户输入的序列化之后的JSON格式数据中,含有类名的字符串,比如{"@type","com.demo.test"}
这个json数据,其typeName
就是com.demo.test
。这行代码的意思就是从TypeUtils
这个类中尝试获取json中提到的类。
在TypeUtils
的静态方法中,执行了addBaseClassMappings
函数,其函数代码如下(注意其中含有AutoCloseable
接口):
private static void addBaseClassMappings(){
mappings.put("byte", byte.class);
mappings.put("short", short.class);
mappings.put("int", int.class);
mappings.put("long", long.class);
mappings.put("float", float.class);
mappings.put("double", double.class);
mappings.put("boolean", boolean.class);
mappings.put("char", char.class);
mappings.put("[byte", byte[].class);
mappings.put("[short", short[].class);
mappings.put("[int", int[].class);
mappings.put("[long", long[].class);
mappings.put("[float", float[].class);
mappings.put("[double", double[].class);
mappings.put("[boolean", boolean[].class);
mappings.put("[char", char[].class);
mappings.put("[B", byte[].class);
mappings.put("[S", short[].class);
mappings.put("[I", int[].class);
mappings.put("[J", long[].class);
mappings.put("[F", float[].class);
mappings.put("[D", double[].class);
mappings.put("[C", char[].class);
mappings.put("[Z", boolean[].class);
Class<?>[] classes = new Class[]{
Object.class,
java.lang.Cloneable.class,
loadClass("java.lang.AutoCloseable"),
java.lang.Exception.class,
java.lang.RuntimeException.class,
java.lang.IllegalAccessError.class,
java.lang.IllegalAccessException.class,
java.lang.IllegalArgumentException.class,
java.lang.IllegalMonitorStateException.class,
java.lang.IllegalStateException.class,
java.lang.IllegalThreadStateException.class,
java.lang.IndexOutOfBoundsException.class,
java.lang.InstantiationError.class,
java.lang.InstantiationException.class,
java.lang.InternalError.class,
java.lang.InterruptedException.class,
java.lang.LinkageError.class,
java.lang.NegativeArraySizeException.class,
java.lang.NoClassDefFoundError.class,
java.lang.NoSuchFieldError.class,
java.lang.NoSuchFieldException.class,
java.lang.NoSuchMethodError.class,
java.lang.NoSuchMethodException.class,
java.lang.NullPointerException.class,
java.lang.NumberFormatException.class,
java.lang.OutOfMemoryError.class,
java.lang.SecurityException.class,
java.lang.StackOverflowError.class,
java.lang.StringIndexOutOfBoundsException.class,
java.lang.TypeNotPresentException.class,
java.lang.VerifyError.class,
java.lang.StackTraceElement.class,
java.util.HashMap.class,
java.util.Hashtable.class,
java.util.TreeMap.class,
java.util.IdentityHashMap.class,
java.util.WeakHashMap.class,
java.util.LinkedHashMap.class,
java.util.HashSet.class,
java.util.LinkedHashSet.class,
java.util.TreeSet.class,
java.util.ArrayList.class,
java.util.concurrent.TimeUnit.class,
java.util.concurrent.ConcurrentHashMap.class,
java.util.concurrent.atomic.AtomicInteger.class,
java.util.concurrent.atomic.AtomicLong.class,
java.util.Collections.EMPTY_MAP.getClass(),
java.lang.Boolean.class,
java.lang.Character.class,
java.lang.Byte.class,
java.lang.Short.class,
java.lang.Integer.class,
java.lang.Long.class,
java.lang.Float.class,
java.lang.Double.class,
java.lang.Number.class,
java.lang.String.class,
java.math.BigDecimal.class,
java.math.BigInteger.class,
java.util.BitSet.class,
java.util.Calendar.class,
java.util.Date.class,
java.util.Locale.class,
java.util.UUID.class,
java.sql.Time.class,
java.sql.Date.class,
java.sql.Timestamp.class,
java.text.SimpleDateFormat.class,
com.alibaba.fastjson.JSONObject.class,
com.alibaba.fastjson.JSONPObject.class,
com.alibaba.fastjson.JSONArray.class,
};
for(Class clazz : classes){
if(clazz == null){
continue;
}
mappings.put(clazz.getName(), clazz);
}
}
可以发现是系统将这些类加入了mappings
中,之前提到的
clazz = TypeUtils.getClassFromMapping(typeName);
也就一目了然了,就是尝试在TypeUtils
类中尝试获取typeName
值,如果没有获取到那么clazz
为null,若expectClass
此时也为null,就会触发异常,告诉用户AutoType
不可用。正常情况下AutoType
就是被禁止了。
然而还有另一种情况,若clazz
不为空,则就存在绕过限制的可能,绕过的关键就是TypeUtils.mappings
中所包含的类,它们就相当于是“系统的白名单”。
“通行证”
Debug过程就不描述了,只说结论。
- fastjson会首先从左至右寻找JSON格式中带“
@type
”的键,若存在,则将其键对应的值(即typeName
)通过checkAutoType
方法检查是否在"系统的白名单中"和用户自定义的白名单中,当然系统还有自带的黑名单,typeName
还不能在黑名单中。
以下为1.2.68系统设定的黑名单。
if (expectClass == null) {
expectClassFlag = false;
} else {
if (expectClass == Object.class
|| expectClass == Serializable.class
|| expectClass == Cloneable.class
|| expectClass == Closeable.class
|| expectClass == EventListener.class
|| expectClass == Iterable.class
|| expectClass == Collection.class
) {
expectClassFlag = false;
} else {
expectClassFlag = true;
}
}
- 在确定
typeName
不在黑名单而又在白名单后,会将typeName
赋值为expectClass
,expectClass
可是个好东西啊,相当于一个强大的通行证,他会允许expectClass
的儿子们(实现类或子类)不需要通过系统白名单检查直接通过。1.2.68漏洞也因这个“通行证”而产生。
“绕过”
基于以上两个条件,当年的黑客们选择了AutoCloseable的实现类,AutoCloseable也是手持"通行证"的用户,至于如何利用之前分享的文章已经提到了,这里再次贴出:
fastjson 1.2.68 反序列化漏洞 gadgets 挖掘笔记
当然,如果是作为学习之用,可以用个简易的demo:
fastjson<=1.2.68的漏洞分析
除了AutoCloseable在这个系统的白名单外,还有其他的类也在其中,选择其他类也可以绕过checkAutoType
,但仅仅是绕过,有些无法触发子类反序列化。
通过跟踪debug发现,当父类为异常类时,fastjson会调用ThrowableDeserializer
进行处理,在处理中,该类会继续向后处理JSON格式字符串,若出现@type
,会继续调用checkAutoType
检查,但此时因为expectClass
已被赋值为异常类,不为空,"通行证"拿到手,所以子类可以"跳过"系统白名单检查(也就是clazz
为空也没关系),直接进行反序列化。
关于这一点我和知乎那篇文章的作者观点一样,知乎的文章原文如下:
在fastjson中, 如果,@type 指定的类为 Throwable 的子类,那对应的反序列化处理类就会使用到ThrowableDeserializer, 而在ThrowableDeserializer#deserialze的方法中,当有一个字段的key也是 @type时,就会把这个 value当做类名,然后进行一次 checkAutoType 检测。并且指定了expectClass为Throwable.class,但是在checkAutoType中,有这样一约定,那就是如果指定了expectClass,那么也会通过校验。
如果父类或者接口是异常类以外的,可能就不一定能让子类或实现类反序列化了,比如当父类为SimpleDateFormat
时,会向后检查有无"val"字符串,并不会检查@type
,因此也就失去了反序列化的条件。
1.2.75的问题
上一节已经说了1.2.68的问题,回到现在的版本。
AutoCloseable
产生的反序列化漏洞已经在1.2.69中修复了,修复的手段是将AutoCloseable
加入黑名单,但是没有对异常类进行处理,所以仍然存在漏洞,该漏洞的利用条件如下:
- 危险类必须继承“系统白名单”中任意一个异常类
- 危险类中的危险方法必须为构造方法或者setter方法,参数可控是最好的。
关于为何setter方法也可以,fastjson到底做错了什么?为什么会被频繁爆出漏洞?知乎一文中已经说得很清楚了,此处不再重复。
测试Demo
测试环境:
- Web框架为springboot
- fastjson1.2.75
危险类:
package com.fastjson.demo.poc;
import java.io.IOException;
public class poc extends Exception {
public void setS(String a){
try {
Runtime.getRuntime().exec(a);
} catch (IOException e) {
e.printStackTrace();
}
}
}
payload:
'{"@type":"java.lang.Exception","@type":"com.fastjson.demo.poc.poc","s":"calc"}'
测试Demo分享:
链接:https://pan.baidu.com/s/141HLCd-IVIe0jTYkhvgr-Q
提取码:ewra
演示弹出计算器GIF:
小结
虽然这也是漏洞,但是和1.2.68版本已公开的漏洞利用一样,利用难度都要比1.2.24的大,但是仍然具有研究意义。
1.2.24版本的漏洞利用才算是真正的完美。