在FastJson1.2.25以及之后的版本中,fastjson为了防止autoType这一机制带来的安全隐患,增加了一层名为checkAutoType的检测机制。在之后的版本中,随着checkAutoType安全机制被不断绕过,fastjson也进行了一系列例如黑名单防逆向分析、扩展黑名单列表等加固。但是checkAutoType的原理未曾有过大的变化,因此本文将以fastjson 1.2.25版本为例,介绍一下checkAutoType安全机制的原理。
在调试分析fastjson的checkAutoType安全机制之前,发现网上很多fastjson漏洞的分析文章中曾经提到过一个名为autoTypeSupport的开关,且在1.2.25以及之后的版本中默认关闭。在动手调试之前,我曾一度以为autoTypeSupport开关关闭与否直接决定了fastjson是完全摒弃或是使用autotype功能的。但是实际调试中发现,这个开关仅仅是checkAutoType安全机制中的一个选项,这个开关的关闭与否,并不直接作用于fastjson是否使用autoType机制,下文案例中可以看出这个问题。
fastjson在1.2.25以及之后的版本中引入的checkAutoType安全机制,位于com/alibaba/fastjson/parser/ParserConfig.java文件。但并不是所有情况下fastjson都会加载这个机制进行安全监测,让我们下面来看看究竟什么情况下这个安全机制会被触发。
通过调试fastjson 1.2.25代码发现,如果想触发checkAutoType安全机制,需要执行到com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java中下图红框处位置
在分析触发checkAutoType安全机制的情况之前,首先来看下什么情况下不会触发checkAutoType安全机制
不使用checkAutoType安全机制的情况
fastjson将字符串转换为Java对象时,并不是都采用checkAutoType机制进行校验,以下两种情况均为使用checkAutoType机制
- json字符串中未使用@type字段
1 | public static void main(String[] args){ |
待处理字符串中并不包含@type字段。在将这个普通的json字符串转换为java对象方法过程中并不执行到上文JavaBeanDeserializer.java中userType = config.checkAutoType(typeName, expectClass);位置
- Class<T> clazz与@type相同
1 | public static void main(String[] args) { |
待处理的字符串@type指定的类与parseObject(String text, Class<T> clazz)中Class<T> clazz)参数指定的类相同,都是AutoTypeTest.Test1。在这种情况下,程序执行到下图红框处分支中
上图中typeName变量值由@type值传递而来;beanInfo.typeName变量值由parseObject(String text, Class<T> clazz)中Class<T> clazz所指定。在Class<T> clazz值与@type值相同的情况下,程序进入上图红框中的if分支。而在这个分支中,程序不是被break就是被continue,而checkAutoType安全机制加载位置又处在该处if分支后面(上图552行),因此程序无论如何也不会执行到上图552行的checkAutoType安全机制中。
由上文两个例子可见,在1.2.25以及之后的版本中,并不是所有的情况都需要经过checkAutoType这一关卡的。
我们接下来看看如何触发checkAutoType安全机制,以及checkAutoType安全机制的原理。
使用checkAutoType安全机制的情况
通过分析可以发现,checkAutoType安全机制中也是针对不同情况不同处理的:checkAutoType安全机制处理流程受autoTypeSupport、parseObject参数等因素控制,这些因素共同影响checkAutoType安全机制是如何过滤以及处理传入的等待反序列化的json字符串
总得来说,有如下几个元素共同作用影响checkAutoType选择哪种方式处理输入
- autoTypeSupport开关值(True/False)
- 使用parseObject(String text, Class<T> clazz)或是parseObject(String text)(这里Class<T> clazz参数为应与@type字段不一样的值,否则不会触发checkAutoType)
根据这两种条件,我们可以列出如下四种情况的表格
autoTypeSupport值 | parseObject(String text, Class<T> clazz)/ parseObject(String text) | |
---|---|---|
情况一 | False | parseObject(String text) |
情况二 | False | parseObject(String text, Class<T> clazz) |
情况三 | True | parseObject(String text) |
情况四 | True | parseObject(String text, Class<T> clazz) |
接下来我们对上述四种情况逐一进行分析
autoTypeSupport值为False、使用parseObject(String text)
1 | public static void main(String[] args) { |
接下来我们来看一下这种情况下checkAutoType安全机制是如何进行处理的
checkAutoType位于com/alibaba/fastjson/parser/ParserConfig.java中
1 | public Class<?> checkAutoType(String typeName, Class<?> expectClass) { |
由于autoTypeSupport为False,程序进入if (!autoTypeSupport)分支中
1 | if (!autoTypeSupport) { |
程序首先遍历denyList这一黑名单,并判断className与黑名单是否匹配
这里需要说明一下:className变量是由typeName简单变换而来,而typeName即为@type字段值。详见下图代码
接下来看下黑名单中的元素
1 | denyList = |
如果className命中黑名单,程序抛出”autoType is not support.”异常并结束
在黑名单过滤完成后,程序还会将className与白名单匹配一下,下图中红框里的for循环即为白名单匹配过程
程序将className与acceptList白名单进行匹配,通过动态调试查看以下默认白名单中的值
从动态调试的结果可见,acceptList默认情况下是一个空列表。
开发者可以通过ParserConfig.getGlobalInstance().addAccept()自行向acceptList白名单中增加元素。如下代码向白名单中增加了一个AutoTypeTest.Test1类
1 | public static void main(String[] args) { |
在白名单匹配环节,如果className指定的类与白名单中的项相匹成功,程序将该类的类对象赋值与clazz变量并返回
程序接下来将传入的json字符串反序列为该类对象
在程序执行完黑名单与白名单校验后,既没有匹配到黑名单,也没有匹配到白名单的话,程序最终会执行到下图分支
此时程序将抛出异常并结束
由于在1.2.25以及之后的版本中,autoTypeSupport值默认False。所以1.2.25以及之后的版本中假使攻击者构造的payload中的恶意类绕过了黑名单,但如果payload中的类不在白名单上,也是不能成功利用的。值得注意的是,白名单默认情况是空的。在此场景下payload想要执行成功,只有一种可能性:
- @type字段值在不在黑名单中且在白名单中
autoTypeSupport值为False、使用parseObject(String text, Class<T> clazz)
1 | public static void main(String[] args) { |
注意上图,这里parseObject中的Class<T> clazz参数是AutoTypeTest.Test.class,而@type中的是AutoTypeTest.Test1,二者不是一个类。如果是一个类,根据上文checkAutoType触发条件分析,根本不会触发checkAutoType
在这种情况下,我们再来看看checkAutoType会如何处理我们的数据
1 | public Class<?> checkAutoType(String typeName, Class<?> expectClass) { |
此时autoTypeSupport为true,expectClass不为空,程序会首先进入if (autoTypeSupport || expectClass != null)分支
值得注意的是,这个分支中是先匹配白名单,后匹配黑名单,如果@type字段指定的类在白名单中,则直接返回,不需要再经过黑名单过滤了。这一点很有意思,如果开发者因为开发失误,将存在利用的类加到了白名单里,攻击者是可以直接利用的
回归正文,由于上图这里我们没有向白名单中增加AutoTypeTest.Test.class类,程序会接下来检查传入的类是否在黑名单中
如果匹配到黑名单,则直接抛出错误
如果这里既没有匹配到白名单直接返回,也没有匹配到黑名单抛出错误终止,程序则继续向下执行
继续执行到的这个分支与情况一中的完全一致,又匹配了一遍黑名单与白名单。显而易见,这里既不会匹配到白名单,也不会匹配到黑名单
最后程序执行到下图这里
由于我们使用的是parseObject(String text, Class<T> clazz)这种方式,上图代码中872行处的expectClass即为Class<T> clazz传入的AutoTypeTest.Test.class类,而clazz变量为@type字段指定的AutoTypeTest.Test1.class类.程序通过
1 | expectClass.isAssignableFrom(clazz) |
判断Class<T> clazz传入的expectClass对象所表示的类是否与@type字段指定的clazz变量参数表示的类相同,或者是其超类或父接口。本例中AutoTypeTest.Test.class类与AutoTypeTest.Test1.class类所表示的类与接口不同,也不是超类或父类关系。因此程序抛出异常
1 | Exception in thread "main" com.alibaba.fastjson.JSONException: type not match.AutoTypeTest.Test1 -\> AutoTypeTest.Test |
如果二者相同或者是超类、父类关系,程序将@type字段指定的类对象返回,并随后将传入字符串反序列化为该类对象
- 此场景的安全隐患
通过上文的分析,在此场景下程序会先匹配白名单、后匹配黑名单。如果@type字段指定的类在白名单中,则程序是会跳过黑名单校验的,例如下图
即使com.sun.rowset.JdbcRowSetImpl在Fastjson默认黑名单中,但在开发的时候,由于开发安全意识不强或开发疏忽等原因,将com.sun.rowset.JdbcRowSetImpl加入了白名单,此时是可以绕过黑名单直接执行利用的。在此场景下,payload想执行成功,有两种可能性:
没有命中黑名单且Class<T> clazz表示的类或接口是否与指定的@type字段值表示的类或接口相同,或者是其超类或父接口。
@type字段值在白名单中
autoTypeSupport值为True、使用parseObject(String text)
1 | public static void main(String[] args) { |
在此场景中,程序首先进入了与上一场景相同的分支,与上一场景不同的是,这里是由于autoTypeSupport为true而进入此分支,而非上一场景满足expectClass != null这一条件
与上一场景一致,程序先匹配白名单、后匹配黑名单。如果@type字段指定的类在白名单中,则直接返回,不再进行黑名单校验。在白名单未匹配成功后,使用黑名单进行匹配,若匹配到黑名单,直接抛出异常。如果黑白名单都未匹配成功,程序继续向下执行
程序将@type字段指定的类返回
这种情况下要是payload想成功利用有两种办法:
@type字段值只需要不在黑名单中即可成功利用
@type字段值在黑名单中,但是开发的时候在白名单中加入了这个类,payload依然可以成功利用
四、autoTypeSupport值为True、使用parseObject(String text, Class<T> clazz)
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String jsonstr = "{\"@type\":\"AutoTypeTest.Test1\",\"s1\":\"1\"}";
Object obj = JSON.parseObject(jsonstr,AutoTypeTest.Test.class);
System.out.println(obj)
}
与上文第二、三场景相同,程序首先进入了这个分支
此场景进入该分支的原因是autoTypeSupport与expectClass这两个条件都满足。关于这个分支的执行流程这里不再复述了
在没有匹配到黑白名单后,程序执行到了下图这里
由于这里clazz与expectClass所表示的类与接口不同,也不是超类或父类关系。因此程序抛出异常
1 | Exception in thread "main" com.alibaba.fastjson.JSONException: type not match.AutoTypeTest.Test1 -\> AutoTypeTest.Test |
在这种情况下,payload想执行成功,有两种可能性:
没有命中黑名单且Class<T>
clazz表示的类或接口是否与指定的@type字段值表示的类或接口相同,或者是其超类或父接口。@type字段值在白名单中
早期checkAutoType安全机制缺陷
在fastjson 1.2.25版本引入的checkAutoType以及后续的几个版本中存在着一定的缺陷
如上文所分析,程序通常先经过黑名单与白名单的校验后,将满足条件的类对象通过如下代码赋值给clazz
1 | clazz = TypeUtils.loadClass(typeName, defaultClassLoader); |
程序最终将字符串反序列化为clazz所表示的类对象。我们着重分析下TypeUtils.loadClass是如何实现的
TypeUtils.loadClass方法位于com/alibaba/fastjson/util/TypeUtils.java中
1 | public static Class<?> loadClass(String className, ClassLoader classLoader) { |
问题就出在下图代码上:
当传入的className变量以”L”开头,并以”;”结尾,进入该if分支。在这个if分支中,程序将会把开头的”L”与结尾的”;”去掉,并递归调用loadClass加载这个类。因此可以下图这样构造来进行绕过
1 | public static void main(String[] args) { |
loadClass会将”L”与”;”去除后组成newClassName并返回
这一操作在匹配黑白名单之后,Lcom.sun.rowset.JdbcRowSetImpl;恰好可以绕过黑名单中的限制。后续checkAutoType检测机制进行了一系列的安全加固,大体上都是黑名单防逆向分析、扩展黑名单列表等,但checkAutoType检测机制没有太大的改变。受篇幅影响,这里就不再详细分析了。