Fastjson反序列化漏洞史(上)

admin 2021年12月17日10:59:16评论100 views字数 9732阅读32分26秒阅读模式

Fastjson反序列化漏洞史(上)



Fastjson没有cve编号,不太好查找时间线,一开始也不知道咋写,不过还是慢慢写出点东西,幸好fastjson开源以及有师傅们的一路辛勤记录。


文中将给出与Fastjson漏洞相关的比较关键的更新以及漏洞时间线,会对一些比较经典的漏洞进行测试及修复说明,给出一些探测payload,rce payload。



Fastjson解析流程


可以参考下@Lucifaer师傅写的fastjson流程分析,这里不写了,再写篇幅就占用很大了。


文中提到fastjson有使用ASM生成的字节码,由于实际使用中很多类都不是原生类,fastjson序列化/反序列化大多数类时都会用ASM处理。


如果好奇想查看生成的字节码,可以用idea动态调试时保存字节文件:

Fastjson反序列化漏洞史(上)


插入的代码为:

BufferedOutputStream bos =null;FileOutputStream fos =null;File file =null;String filePath ="F:/java/javaproject/fastjsonsrc/target/classes/"+ packageName.replace(".","/")+"/";try{

    File dir =newFile(filePath);

    if(!dir.exists()){

        dir.mkdirs();

    }

    file =newFile(filePath +className +".class");

    fos =newFileOutputStream(file);

    bos =newBufferedOutputStream(fos);

    bos.write(code);}catch(Exception e){

    e.printStackTrace();}finally{

    if(bos !=null){

        try{

            bos.close();

        }catch(IOException e){

            e.printStackTrace();

        }

   }

    if(fos !=null){

        try{

            fos.close();

        }catch(IOException e){

            e.printStackTrace();

        }

    }}


生成的类:

Fastjson反序列化漏洞史(上)


但是这个类并不能用于调试,因为fastjson中用ASM生成的代码没有linenumber、trace等用于调试的信息,所以不能调试。


不过通过在Expression那个窗口重写部分代码,生成可用于调式的bytecode应该也是可行的(我没有测试,如果有时间和兴趣,可以看下ASM怎么生成可用于调试的字节码)。



Fastjson 样例测试


首先用多个版本测试下面这个例子:

//User.javapackage com.longofo.test;

publicclassUser{

    privateString name;//私有属性,有getter、setter方法

    privateintage;//私有属性,有getter、setter方法

    privatebooleanflag;//私有属性,有is、setter方法

    publicString sex;//公有属性,无getter、setter方法

    privateString address;//私有属性,无getter、setter方法

 

    publicUser(){

        System.out.println("call User default Constructor");

    }

 

    publicString getName(){

        System.out.println("call User getName");

        returnname;

    }

 

    publicvoidsetName(String name){

        System.out.println("call User setName");

        this.name=name;

    }

 

    publicintgetAge(){

        System.out.println("call User getAge");

        returnage;

    }

 

    publicvoidsetAge(intage){

        System.out.println("call User setAge");

        this.age=age;

    }

 

    publicbooleanisFlag(){

        System.out.println("call User isFlag");

        returnflag;

    }

 

    publicvoidsetFlag(booleanflag){

        System.out.println("call User setFlag");

        this.flag=flag;

    }

 

    @Override

    publicString toString(){

        return"User{"+

                "name='"+name +'''+

                ", age="+age +

                ", flag="+flag +

                ", sex='"+sex +'''+

                ", address='"+address +'''+

                '}';

    }}

packagecom.longofo.test;

importcom.alibaba.fastjson.JSON;

publicclassTest1{

    publicstaticvoidmain(String[]args){

        //序列化

        String serializedStr ="{"@type":"com.longofo.test.User","name":"lala","age":11, "flag": true,"sex":"boy","address":"china"}";//

        System.out.println("serializedStr="+serializedStr);

 

        System.out.println("-----------------------------------------------nn");

        //通过parse方法进行反序列化,返回的是一个JSONObject]

        System.out.println("JSON.parse(serializedStr):");

        Object obj1 =JSON.parse(serializedStr);

        System.out.println("parse反序列化对象名称:"+obj1.getClass().getName());

        System.out.println("parse反序列化:"+obj1);

        System.out.println("-----------------------------------------------n");

 

        //通过parseObject,不指定类,返回的是一个JSONObject

        System.out.println("JSON.parseObject(serializedStr):");

        Object obj2 =JSON.parseObject(serializedStr);

        System.out.println("parseObject反序列化对象名称:"+obj2.getClass().getName());

        System.out.println("parseObject反序列化:"+obj2);

        System.out.println("-----------------------------------------------n");

 

        //通过parseObject,指定为object.class

        System.out.println("JSON.parseObject(serializedStr, Object.class):");

        Object obj3 =JSON.parseObject(serializedStr,Object.class);

        System.out.println("parseObject反序列化对象名称:"+obj3.getClass().getName());

        System.out.println("parseObject反序列化:"+obj3);

        System.out.println("-----------------------------------------------n");

 

        //通过parseObject,指定为User.class

        System.out.println("JSON.parseObject(serializedStr, User.class):");

        Object obj4 =JSON.parseObject(serializedStr,User.class);

        System.out.println("parseObject反序列化对象名称:"+obj4.getClass().getName());

        System.out.println("parseObject反序列化:"+obj4);

        System.out.println("-----------------------------------------------n");

    }}


说明


· 这里的@type就是对应常说的autotype功能,简单理解为fastjson会自动将json的key:value值映射到@type对应的类中。


·样例User类的几个方法都是比较普通的方法,命名、返回值也都是常规的符合bean要求的写法,所以下面的样例测试有的特殊调用不会覆盖到,但是在漏洞分析中,可以看到一些特殊的情况。


· parse用了四种写法,四种写法都能造成危害(不过实际到底能不能利用,还得看版本和用户是否打开了某些配置开关,具体往后看)。


· 样例测试都使用jdk8u102,代码都是拉的源码测,主要是用样例说明autotype的默认开启、checkautotype的出现、以及黑白名白名单从哪个版本开始出现的过程以及增强手段。


1.1.157测试


这应该是最原始的版本了(tag最早是这个),结果:

serializedStr={"@type":"com.longofo.test.User","name":"lala","age":11, "flag": true,"sex":"boy","address":"china"}

-----------------------------------------------

 

 

JSON.parse(serializedStr):

call User default Constructor

call User setName

call User setAge

call User setFlag


parse反序列化对象名称:

com.longofo.test.User


parse反序列化:

User{name='lala', age=11, flag=true, sex='boy', address='null'}

-----------------------------------------------

 

JSON.parseObject(serializedStr):

call User default Constructor

call User setName

call User setAge

call User setFlag

call User getAge

call User isFlag

call User getName


parseObject反序列化对象名称:

com.alibaba.fastjson.JSONObject


parseObject反序列化:

{"flag":true,"sex":"boy","name":"lala","age":11}

-----------------------------------------------

 

JSON.parseObject(serializedStr, Object.class):

call User default Constructor

call User setName

call User setAge

call User setFlag


parseObject反序列化对象名称:

com.longofo.test.User


parseObject反序列化:

User{name='lala', age=11, flag=true, sex='boy', address='null'}

-----------------------------------------------

 

JSON.parseObject(serializedStr, User.class):

call User default Constructor

call User setName

call User setAge

call User setFlag

parseObject反序列化对象名称:

com.longofo.test.User


parseObject反序列化:

User{name='lala', age=11, flag=true, sex='boy', address='null'}

-----------------------------------------------


下面对每个结果做一个简单的说明:

JSON.parse(serializedStr)

JSON.parse(serializedStr):

call User default Constructor

call User setName

call User setAge

call User setFlag


parse反序列化对象名称:

com.longofo.test.User


parse反序列化:

User{name='lala', age=11, flag=true, sex='boy', address='null'}


在指定了@type的情况下,自动调用了User类默认构造器,User类对应的setter方法(setAge,setName),最终结果是User类的一个实例,不过值得注意的是public sex被成功赋值了,private address没有成功赋值,不过在1.2.22, 1.1.54.android之后,增加了一个SupportNonPublicField特性,如果使用了这个特性,那么private address就算没有setter、getter也能成功赋值,这个特性也与后面的一个漏洞有关。


注意默认构造方法、setter方法调用顺序,默认构造器在前,此时属性值还没有被赋值,所以即使默认构造器中存在危险方法,但是危害值还没有被传入,所以默认构造器按理来说不会成为漏洞利用方法,不过对于内部类那种,外部类先初始化了自己的某些属性值,但是内部类默认构造器使用了父类的属性的某些值,依然可能造成危害。


可以看出,从最原始的版本就开始有autotype功能了,并且autotype默认开启。同时ParserConfig类中还没有黑名单。


JSON.parseObject(serializedStr)

JSON.parseObject(serializedStr):

call User default Constructor

call User setName

call User setAge

call User setFlag

call User getAge

call User isFlag

call User getName


parseObject反序列化对象名称:

com.alibaba.fastjson.JSONObject


parseObject反序列化:

{"flag":true,"sex":"boy","name":"lala","age":11}


在指定了@type的情况下,自动调用了User类默认构造器,User类对应的setter方法(setAge,setName)以及对应的getter方法(getAge,getName),最终结果是一个字符串。


这里还多调用了getter(注意bool类型的是is开头的)方法,是因为parseObject在没有其他参数时,调用了JSON.toJSON(obj),后续会通过gettter方法获取obj。


属性值:

Fastjson反序列化漏洞史(上)

Fastjson反序列化漏洞史(上)


JSON.parseObject(serializedStr, Object.class)

JSON.parseObject(serializedStr, Object.class):

call User default Constructor

call User setName

call User setAge

call User setFlag


parseObject反序列化对象名称:

com.longofo.test.User


parseObject反序列化:

User{name='lala', age=11, flag=true, sex='boy', address='null'}


在指定了@type的情况下,这种写法和第一种JSON.parse(serializedStr)写法其实没有区别的,从结果也能看出。


JSON.parseObject(serializedStr, User.class)

JSON.parseObject(serializedStr, User.class):

call User default Constructor

call User setName

call User setAge

call User setFlag


parseObject反序列化对象名称:

com.longofo.test.User


parseObject反序列化:

User{name='lala', age=11, flag=true, sex='boy', address='null'}


在指定了@type的情况下,自动调用了User类默认构造器,User类对应的setter方法(setAge,setName),最终结果是User类的一个实例。这种写法明确指定了目标对象必须是User类型,如果@type对应的类型不是User类型或其子类,将抛出不匹配异常,但是,就算指定了特定的类型,依然有方式在类型匹配之前来触发漏洞。



1.2.10测试


对于上面User这个类,测试结果和1.1.157一样,这里不写了。


到这个版本autotype依然默认开启。不过从这个版本开始,fastjson在ParserConfig中加入了denyList,一直到1.2.24版本,这个denyList都只有一个类(不过这个java.lang.Thread不是用于漏洞利用的):

Fastjson反序列化漏洞史(上)


1.2.25测试


测试结果是抛出出了异常:

serializedStr={"@type":"com.longofo.test.User","name":"lala","age":11,"flag":true}-----------------------------------------------

 

JSON.parse(serializedStr)Exception in thread "main"com.alibaba.fastjson.JSONException:autoType is not support.com.longofo.test.User

    at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:882)

    at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:322)

    at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1327)

    at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1293)

    at com.alibaba.fastjson.JSON.parse(JSON.java:137)

    at com.alibaba.fastjson.JSON.parse(JSON.java:128)

    at com.longofo.test.Test1.main(Test1.java:14)


从1.2.25开始,autotype默认关闭了,对于autotype开启,后面漏洞分析会涉及到。并且从1.2.25开始,增加了checkAutoType函数,它的主要作用是检测@type指定的类是否在白名单、黑名单(使用的startswith方式)。


以及目标类是否是两个危险类(Classloader、DataSource)的子类或者子接口,其中白名单优先级最高,白名单如果允许就不检测黑名单与危险类,否则继续检测。


黑名单与危险类:

Fastjson反序列化漏洞史(上)


增加了黑名单类、包数量,同时增加了白名单,用户还可以调用相关方法添加黑名。


单/白名单到列表中:

Fastjson反序列化漏洞史(上)


后面的许多漏洞都是对checkAutotype以及本身某些逻辑缺陷导致的漏洞进行修复,以及黑名单的不断增加。



1.2.42测试


与1.2.25一样,默认不开启autotype,所以结果一样,直接抛autotype未开启异常。


从这个版本开始,将denyList、acceptList换成了十进制的hashcode,使得安全研究难度变大了(不过hashcode的计算方法依然是公开的,假如拥有大量的jar包,例如maven仓库可以爬jar包下来,可批量的跑类名、包名,不过对于黑名单是包名的情况,要找到具体可利用的类也会消耗一些时间):

Fastjson反序列化漏洞史(上)

checkAutotype中检测也做了相应的修改:

Fastjson反序列化漏洞史(上)


1.2.61测试


与1.2.25一样,默认不开启autotype,所以结果一样,直接抛autotype未开启异常。


从1.2.25到1.2.61之前其实还发生了很多绕过与黑名单的增加,不过这部分在后面的漏洞版本线在具体写,这里写1.2.61版本主要是说明黑名单防御所做的手段。


在1.2.61版本时,fastjson将hashcode从十进制换成了十六进制:

Fastjson反序列化漏洞史(上)


不过用十六进制表示与十进制表示都一样,同样可以批量跑jar包。在1.2.62版本为了统一又把十六进制大写:

Fastjson反序列化漏洞史(上)


再之后的版本就是黑名单的增加了


(内容过多,无法一下上传,周六继续更文)




关注路劲科技,关注网络安全!


Fastjson反序列化漏洞史(上)
资源分享--书籍分享
RCE远程命令执行和远程代码执行
技术| Linux 运维必备的13款实用工具
XYHCMS 3.6 后台代码执行漏洞(一)

Fastjson反序列化漏洞史(上)
Fastjson反序列化漏洞史(上)

北京路劲科技有限公司

网址:www.lujinsec.com

北京市昌平区南邵镇双营西路78号院2号楼5层504

本文始发于微信公众号(LSCteam):Fastjson 反序列化漏洞史(上)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年12月17日10:59:16
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Fastjson反序列化漏洞史(上)http://cn-sec.com/archives/531922.html

发表评论

匿名网友 填写信息