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 利用链导致反序列化漏洞