Java安全(6)Fastjson漏洞原理基础+反序列化过程审计

如标题,Fastjson 安全方面最基础的东西,漏洞成因分析以及简单审计一下反序列化过程

我或许应该每一篇多写一些东西,少分几篇?封面常驻的艾姬多娜图片要用完了(

最后还是希望大家都能技术进步事业顺利,下面正文开始

json

  • 现代Web开发的首选数据格式,适合大多数场景,尤其是轻量级和高效的数据交换。
  • 简洁高效:JSON格式简洁,数据体积小,适合网络传输。
  • 复杂性有限:对于非常复杂的数据结构(如嵌套层次很深或需要命名空间),JSON可能不够灵活。
POST /api/user HTTP/1.1
Host: www.example.com
Content-Type: application/xml
Content-Length: 152
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
Accept: application/xml
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

<User>
    <id>1</id>
    <name>John</name>
    <age>30</age>
    <gender>Male</gender>
    <deleted>0</deleted>
    <createTime>2024-07-26T10:00:00Z</createTime>
    <updateTime>2024-07-26T10:00:00Z</updateTime>
</User>

Fastjson

Fastjson是阿里巴巴开源的一个Java库,用于将Java对象转换成JSON格式的字符串,以及将JSON字符串转换成Java对象。这个过程通常被称为序列化和反序列化。Fastjson的特点是速度快、使用广泛,并且可以操作没有源码的Java对象

最大滴特点,就如名字一样:fast,很快啊

示例

导入依赖 pom.xml

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.24</version>
</dependency>
<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat-dbcp</artifactId>
  <version>9.0.20</version>
</dependency>

Person 类

写了一个测试用的类

package org.test;

import java.io.IOException;

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

    public Person() {
        System.out.println("Person constructor 1");
    }

    public Person(String name, int age) {
        System.out.println("Person constructor 2");
        this.name = name;
        this.age = age;
    }


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

    public void setName(String name) throws IOException {
        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() {
        return "人类{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

对象->json

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

public class Tojsoin {
    public static void main(String[] args) {
        
        Person user = new Person("rice", 18);

        String jsonString = JSON.toJSONString(user);
        System.out.println(jsonString);
    }
}

json->对象

package org.test;

import com.alibaba.fastjson.JSON;

public class Toobj {
    public static void main(String[] args) {
        String json = "{\"age\":18,\"name\":\"rice\"}";
        Person user = JSON.parseObject(json, Person.class);
        System.out.println(user.getName());
    }
}

Autotype

Fastjson 的 AutoType 功能 允许在序列化的 JSON 字符串中带上类型信息 ,这样在反序列化时,不需要显式传入目标类型,即可 自动识别 并还原为正确的 Java 对象

示例:

package org.test;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class AutotypeTest {
    public static void main(String[] args) {

        String jsonString = "{\"@type\":\"org.test.Person\",\"name\":\"Bob\",\"age\":25}";

        Object obj = JSON.parseObject(jsonString);

        System.out.println(obj.toString());
    }
}

同上个示例的 json -> 对象 对比发现,传入 parseObject 的参数只剩下 json 本身了,类型在 json 数据中由 @type 进行了指定

头脑风暴(

在 Fastjson 将 json 转化为对象的过程中,发生什么了呢?是怎么完成的这个过程?

Fastjson 在反序列化的过程中并没有利用反射,或者别的方法去操作,而是调用对应的类内部的方法

首先 Fastjson 会创建一个我们对应类的对象,然后需要将我们 json 当中记录的对象属性赋值过去,这样自然而然就产生了两个需求,一个空的构造方法,以及对应着属性的 set 方法

Fastjson 当找到满足下面条件的 set 就会调用这个 set 方法

  • 方法名长度大于 4 (set就已经三个字了,随便跟个单词就够了)
  • 非静态方法
  • 返回值为
  • 返回值为 void 或当前类
  • 以 set 开头且第四个字母为大写
  • 参数个数为 1 个

如果找不着满足条件的 set 呢?那就会去尝试寻找 get,get 方法同样有要求

  • 方法名长度大于 4
  • 非静态方法
  • 以 get 开头且第四个字母为大写
  • 无参数传入
  • 返回值类型继承自 Collection Map AtomicBoolean AtomicInteger AtomicLong
  • 此属性没有 setter 方法

那么一个很明显的安全隐患就摆在眼前了,那假如 @type 指定了一个类,而那个类的 set 或者 get 方法有问题呢?

比如:

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

将 Person 类里面的 setName 修改为如上,添加了一行调用系统命令,弹计算器的操作,这时候,再去让 Fastjson 解析我们的 json(json 数据表面看起来没有变化),会不会触发恶意代码?

如图所示,执行代码,Fastjson 将数据转换为了对象,同时弹了计算器

那扩展一下来看,只要一个类的 set 里面存在危险方法,就可以利用 Fastjson 打开的这个口子去调用到

那 Fastjson 有没有安全校验呢?有的兄弟,有的

在 fastjson 1.2.24 版是做了一点点的,有一个黑名单,在 ParserConfig.java (只有class的话点击右上角弹出来的download source就有源码了)

private String[] denyList = new String[] { "java.lang.Thread" };

大概 147 行的位置,定义的 denyList 作为黑名单,之后每次进行 Autotype 反序列化 json 数据的时候,都会跑一遍这个黑名单:

String className = clazz.getName();
className = className.replace('$', '.');
for (int i = 0; i < denyList.length; ++i) {
    String deny = denyList[i];
    if (className.startsWith(deny)) {
        throw new JSONException("parser deny : " + className);
    }
}

每次都会循环跑一遍黑名单,如果检测到了就报一个错,但是这么一点的安全措施肯定是不够的,黑名单里也只是塞进了一个 Thread 类

Fastjson 漏洞

漏洞大致的原理成因其实刚刚也都说过了,@type + 恶意的 set 或 get

代码上的关键点

autotype 的判断

Object obj = JSON.parseObject(jsonString);

这是刚刚示例里调用的函数的地方,直接跟进一下 parseObject

public static JSONObject parseObject(String text) {
    Object obj = parse(text);
    if (obj instanceof JSONObject) {
        return (JSONObject) obj;
    }

    return (JSONObject) JSON.toJSON(obj);
}

调用 parse 方法了,再跟进

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

可以看到,parse 方法里面又调用了 parse,但是传参不同,并非同一个方法,那就再跟进(其实就在下面写着)

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

这一步中间部分可以看到创建了一个默认的 json 解析器 DefaultJSONParser,之后又调用了 parse,再跟进

public Object parse() {
    return parse(null);
}

再跟进:

public Object parse(Object fieldName) {
    final JSONLexer lexer = this.lexer;
    switch (lexer.token()) {
        case SET:
            lexer.nextToken();
            HashSet<Object> set = new HashSet<Object>();
            parseArray(set, fieldName);
            return set;
        case TREE_SET:
            lexer.nextToken();
            TreeSet<Object> treeSet = new TreeSet<Object>();
            parseArray(treeSet, fieldName);
            return treeSet;
        case LBRACKET:
            JSONArray array = new JSONArray();
            parseArray(array, fieldName);
            if (lexer.isEnabled(Feature.UseObjectArray)) {
                return array.toArray();
            }
            return array;
        case LBRACE:
            JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
            return parseObject(object, fieldName);
        case LITERAL_INT:
            Number intValue = lexer.integerValue();
            lexer.nextToken();
            return intValue;
        case LITERAL_FLOAT:
            Object value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
            lexer.nextToken();
            return value;
        case LITERAL_STRING:
            String stringLiteral = lexer.stringVal();
            lexer.nextToken(JSONToken.COMMA);

            if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
                JSONScanner iso8601Lexer = new JSONScanner(stringLiteral);
                try {
                    if (iso8601Lexer.scanISO8601DateIfMatch()) {
                        return iso8601Lexer.getCalendar().getTime();
                    }
                } finally {
                    iso8601Lexer.close();
                }
            }

            return stringLiteral;
        case NULL:
            lexer.nextToken();
            return null;
        case UNDEFINED:
            lexer.nextToken();
            return null;
        case TRUE:
            lexer.nextToken();
            return Boolean.TRUE;
        case FALSE:
            lexer.nextToken();
            return Boolean.FALSE;
        case NEW:
            lexer.nextToken(JSONToken.IDENTIFIER);

            if (lexer.token() != JSONToken.IDENTIFIER) {
                throw new JSONException("syntax error");
            }
            lexer.nextToken(JSONToken.LPAREN);

            accept(JSONToken.LPAREN);
            long time = ((Number) lexer.integerValue()).longValue();
            accept(JSONToken.LITERAL_INT);

            accept(JSONToken.RPAREN);

            return new Date(time);
        case EOF:
            if (lexer.isBlankInput()) {
                return null;
            }
            throw new JSONException("unterminated json string, " + lexer.info());
        case ERROR:
        default:
            throw new JSONException("syntax error, " + lexer.info());
    }
}

好长啊,功能就是对 json 的结构进行检测,左大括号,右大括号什么的

那检测到左大括号的话,说明 json 里面有我们要反序列化的一个对象,进入这个 case:

 case LBRACE:
            JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
            return parseObject(object, fieldName);

会去 new 一个 JSONObject 对象,然后传入 parseObject

parseObject 会提取 json 的第一个双引号(功能简单就不贴整个方法代码了),然后会提取出来第一个双引号里面的内容 @type

随后进入下面(320行位置)

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

刚才提取出来的内容去判断一下是不是 @type ,随后 loadClass (加载一个类,但不一定初始化),把 @type 的类给加载进来了

那既然类也加载进来了,接下来该着手反序列化了,367行

ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);

获取 clazz (上一步加载的类)的反序列化器

public ObjectDeserializer getDeserializer(Type type) {
    ObjectDeserializer derializer = this.derializers.get(type);
    if (derializer != null) {
        return derializer;
    }

    if (type instanceof Class<?>) {
        return getDeserializer((Class<?>) type, type);
    }

    if (type instanceof ParameterizedType) {
        Type rawType = ((ParameterizedType) type).getRawType();
        if (rawType instanceof Class<?>) {
            return getDeserializer((Class<?>) rawType, type);
        } else {
            return getDeserializer(rawType);
        }
    }

    return JavaObjectDeserializer.instance;
}

先去一个 map 里面找一下,里面存了常见的类,如果找到就直接获取,找不到就会自己自动生成一个:

if (type instanceof Class<?>) {
    return getDeserializer((Class<?>) type, type);
}

那就接着跟进

判断是否恶意类

(太长了,只节选一下关键的)

String className = clazz.getName();
className = className.replace('$', '.');
for (int i = 0; i < denyList.length; ++i) {
    String deny = denyList[i];
    if (className.startsWith(deny)) {
        throw new JSONException("parser deny : " + className);
    }
}

这不就是之前那段跑黑名单过滤的嘛,当然也不是,那就过(后面有一大坨各种判断的流程,就跳过了,看一下正式搞反序列化器的地方罢)

461行

derializer = createJavaBeanDeserializer(clazz, type);

接着跟进,然后找到关键部分(太长了)

JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);

注意到 build,关键,跟进之(从129到539全是这个build)

类和属性的遍历

for (Method method : methods)

for (Field field : clazz.getFields())

for (Method method : clazz.getMethods())

328 433 490这三个方法,分别遍历获取 set、filed(参数)、get

之后里面的一些逻辑判断的功能和效果同前面说的对 set 和 get 的要求

暂无评论

发送评论 编辑评论


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