前言
在Java开发中,如果涉及权限如管理员普通用户操作,则需要在接口调用前对用户的身份进行鉴权。对于权限固定且后期无需变动的情况可在程序中硬编码但是代码的设计规范可扩展性、抽离性等往往与其背道而驰不建议使用,所以权限的判定的代码应该在数据库动态查询。在这样的背景下,Java产生了两款权限判定框架,对接口鉴权、session管理、登录、数据库动态权限查询进行了封装,大大减少了开发人员的工作量。这两款框架就是:
1、SpringSecurity
2、Shiro
而Shiro以其上手难度低,轻量级框架对程序侵入性小而闻名。大大用于主流的Java开发应用,但是这样一款框架却被安全人员在HW中使用的得心应手,今天将从原理性的角度剖析其550漏洞产生的原因.
感谢
https://blog.csdn.net/hackzkaq/article/details/114278891
https://blog.csdn.net/three_feng/article/details/52189559
文章的帮助,才得以让我将Java反序列化漏洞原理剖析系列文章的延续。
序列化与反序列化
既然是反序列化漏洞必然要提起的就是序列化与反序列化,如果还有读者对这个概念不清楚请参考文章《前尘——与君再忆CC链》,在Java反序列化漏洞中,序列化和反序列化是理解这些漏洞的基本条件。
搭建环境
下载地址:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
上文作者已经搭建好shiro的集成环境,在Maven中处理下载依赖问题即可。
漏洞跟踪
分析源码,根据利用漏洞可知漏洞点在于Cookie上的问题。找到Shiro管理Cookie的位置
此为Shiro-core包的类包,但是据了解可知产生问题的类为org.apache.shiro.web.mgt.CookieRememberMeManager路径,但是在包下没找到web包索性直接搜索功能
按住IDEA CTRL+SHIFT+R 键打开搜索页面 搜索类CookieRememberMeManager
getRememberedSerializedIdentity()方法处理cookie的方法,跟进此方法
1.将Request和Response对象传入从而获取Request对象中的cookie
2.将获取到的Base64加密的cookie调用Base64解密方法进行解密
随后进入getRememberedPrincipals方法
跟随convertBytesToPrincipals方法内部
进入decrypt方法
查看167行代码解密时会将this.getDecryptionCipherKey()作为密钥传入进行解密
最终跟进方法发现密钥是一个byte数组decryptionCipherKey,继续跟进
在构造方法内调用方法进行赋值,传参内容为上文base64的字符串。
赋值加密方法,赋值解密方法
进入解密方法赋值,发现其实base64那段写死的值其实赋值给了上文提到的byte数组decryptionCipherKey。
逻辑整理
得到rememberMe的cookie值 --> Base64解码 --> AES解密 --> 反序列化
1.getRememberedSerializedIdentity()方法内部获取cookie的值
2.getRememberedSerializedIdentity()方法内部Base64解密
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
if (!WebUtils.isHttp(subjectContext)) {
if (log.isDebugEnabled()) {
String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " +
"servlet request and response in order to retrieve the rememberMe cookie. Returning " +
"immediately and ignoring rememberMe operation.";
log.debug(msg);
}
return null;
}
WebSubjectContext wsc = (WebSubjectContext) subjectContext;
if (isIdentityRemoved(wsc)) {
return null;
}
HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);
String base64 = getCookie().readValue(request, response);
// Browsers do not always remove cookies immediately (SHIRO-183)
// ignore cookies that are scheduled for removal
if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;
if (base64 != null) {
base64 = ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}
byte[] decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}
return decoded;
} else {
//no cookie set - new site visitor?
return null;
}
}
3.convertBytesToPrincipals()方法调用其aes解密方法跟进发现密钥为byte数组decryptionCipherKey,查看AbstractRememberMeManager的构造方法发现在实例化对象时硬编码的DEFAULT_CIPHER_KEY_BYTES其实分别给加密解密的byte数组进行了赋值
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = this.getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
4.回到主线解密后发现调用了反序列化的方法
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (this.getCipherService() != null) {
bytes = this.decrypt(bytes);
}
return this.deserialize(bytes);
}
5.反序列话的参数可控,并且在aes密钥已知的情况下使用前文提到的《前尘——与君再忆CC链》或者是其他链进行攻击即可
再说说攻击的顺序恶意命令-->序列化-->AES加密-->base64编码-->发送cookie
讨论
这是我讲的第三篇关于反序列化的文章,相信仔细阅读前面的文章到这里都会跟我有一个相同的体会。这个体会就是,所谓的反序列化漏洞并不是反序列化存在的问题,反序列化是一个入口点。好比粮仓着火这个问题,粮仓符合所有的着火因素,着火只是天气干燥恰恰引起的。
总结
这篇文章更完可能会断更一段时间,整理下目标再次出发。
比起找不到方向,停下来 便是一种前进。
Java反序列化一直是一个 老生常谈的问题,理解这些原理性的知识可以更好的帮助我们找到执行链,你我终有一天也会发现理解事物的本质是如此重要。