Shiro 1.2.4 Unserialize
Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能。Shiro 框架直观、易用,同时也能提供健壮的安全性。
漏洞分析
存在两个过程,一个是生成 remenberMe,一个是将 rememberMe 解密
加密
在 Idea 中打开 Shiro 1.2.4 项目,等待 Maven 同步之后,全局搜索remenberMe
,找到CookieRememberMeManager
类。该类继承了AbstractRememberMeManager
,跟进发现定义了一个硬编码KEY
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
继续看代码,发现
/**
* Remembers a subject-unique identity for retrieval later. This implementation first
* {@link #getIdentityToRemember resolves} the exact
* {@link PrincipalCollection principals} to remember. It then remembers the principals by calling
* {@link #rememberIdentity(org.apache.shiro.subject.Subject, org.apache.shiro.subject.PrincipalCollection)}.
* <p/>
* This implementation ignores the {@link AuthenticationToken} argument, but it is available to subclasses if
* necessary for custom logic.
*
* @param subject the subject for which the principals are being remembered.
* @param token the token that resulted in a successful authentication attempt.
* @param authcInfo the authentication info resulting from the successful authentication attempt.
*/
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
PrincipalCollection principals = getIdentityToRemember(subject, authcInfo);
rememberIdentity(subject, principals);
}
跟进 rememberIdentity
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}
继续跟进 convertPrincipalsToBytes
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}
首先将 principals 序列化,接着判断加密服务是否开启,若开启则进行加密

也就是说默认使用的是 AES 加密,而密钥 getDecryptionCipherKey() 就是一开始的 DEFAULT_CIPHER_KEY_BYTES
整体的流程就是,将传入的序列化信息AES加密之后形成 bytes,继续跟进 rememberSerializedIdentity
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
if (!WebUtils.isHttp(subject)) {
if (log.isDebugEnabled()) {
String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " +
"request and response in order to set the rememberMe cookie. Returning immediately and " +
"ignoring rememberMe operation.";
log.debug(msg);
}
return;
}
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
//base 64 encode it and store as a cookie:
String base64 = Base64.encodeToString(serialized);
Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}
将信息经过 base64 编码之后形成最终的 rememberMe
解密
定位到 getRememberedPrincipals 方法
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}
return principals;
}
首先进行 getRememberedSerializedIdentity,跟进(这边截取了部分代码)
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);
String base64 = getCookie().readValue(request, response);
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;
}
}
返回了 base64 解码的数据,回到 getRememberedPrincipals 执行了 convertBytesToPrincipals,跟进
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
对已经解码的数据再用 AES 密钥解密,得到原始数据后进行反序列化 deserialize,跟进
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, e);
}
}
注意到这边调用了 ois.readObject(),触发 apache.commons 利用链导致反序列化漏洞