前言

前置知识(?)。


RememberMe

先把shiro依赖版本改成1.2.4。

漏洞发生在shiro的rememberMe功能,因为这项功能只能允许访问user权限的路径,所以先改改配置文件中的权限配置:

map.put("/logout", "user");
map.put("/user", "user");

再改改控制器代码,开启这项功能:

UsernamePasswordToken token = new UsernamePasswordToken("Twings", "123456");
token.setRememberMe(true);
subject.login(token);

之后shiro会将身份信息加密,base64编码保存在cookie中,用于下一次身份认证:

RememberMe解密

开始找相关的代码,直接搜索名字相关的类可以找到CookieRememberMeManager类及其父类AbstractRememberMeManager,但是换了几个点,断了几次都没有断下来,后来发现浏览器重启后第一次访问才会触发断点,原因不明,可能跟session有关,后面再来研究。最后的断点打在了getRememberedPrincipals函数,此时的调用栈如下:

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;
}

这段代码以前似乎还出现过代号为SHIRO-138的问题,这里通过getRememberedSerializedIdentity函数获取了base64解码后的cookie中保存的加密数据:

protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
    ...
    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);
        ...
        byte[] decoded = Base64.decode(base64);
        ...
        return decoded;
    } 
    ...
}

然后调用convertBytesToPrincipals开始解密:

protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
    if (getCipherService() != null) {
        bytes = decrypt(bytes);
    }
    return deserialize(bytes);
}

这里的处理分为两部分,decrypt函数负责解密:

protected byte[] decrypt(byte[] encrypted) {
    byte[] serialized = encrypted;
    CipherService cipherService = getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
        serialized = byteSource.getBytes();
    }
    return serialized;
}

public byte[] getDecryptionCipherKey() {
    return decryptionCipherKey;
}

解密的key来自该对象中的decryptionCipherKey属性,通过调试可以发现该属性通过setter赋值,而在不特别设置的情况下,该属性在实例化时赋值:

private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

public AbstractRememberMeManager() {
    this.serializer = new DefaultSerializer<PrincipalCollection>();
    this.cipherService = new AesCipherService();
    setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

是一个固定值,而cipherService是一个JcaCipherService对象,其在解密时会从密文中读取iv:

System.arraycopy(ciphertext, 0, iv, 0, ivByteSize);

第二部分的deserialize函数负责反序列化:

public T deserialize(byte[] serialized) throws SerializationException {
    ...
    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);
    }
}

一个没有做安全检验的反序列化点,如果依赖中存在可利用的反序列化链就会导致一个反序列化漏洞。

后续

尝试复现漏洞时发现出现这么一个错误:

org.apache.shiro.io.SerializationException: Unable to deserialze argument byte array.

看起来是不能处理数组类型的对象?不过读读代码就会发现反序列化中只要捕捉到Exception就会抛出这种错误,晚点再看看具体原因。

commons-beanutils

shiro自带的commons-beanutils依赖是1.8.3版本的:

1.9.3版本下打得通的利用链在这个版本生成时会出现一点问题:

问题发生在类的初始化函数中:

public BeanComparator( String property ) {
    this( property, ComparableComparator.getInstance() );
}

简单来说就是BeanComparator对象的实例化依赖于commons-collections中的一个类org.apache.commons.collections.comparators.ComparableComparator,而由于依赖中不存在commons-collections就导致了这个问题。按照参考文章的说法就是:

所以如果想在这个版本下实现RCE,就要通过反射的方式实例化BeanComparator类再给它的属性赋值,Comparator找不找倒是无所谓:

BeanComparator beanComparator = (BeanComparator)Utils.createWithoutConstructor("org.apache.commons.beanutils.BeanComparator");
Utils.setField(beanComparator, "property", "outputProperties");

要注意的一点是,最好用byte[]类型来存放Java序列化数据,后面通过base64编码输出,不然转换为String类型时可能会有编码问题,序列化数据头会被弄成两个问号,一开始没注意踩了一会的坑,生成序列化数据后用python写个脚本发送就行了。

commons-collections

CommonsCollections的利用链可以直接打通,看参考文章似乎在tomcat+shiro环境下,会因为数组形式存在加载问题而无法利用。

JRMP

本地Java版本为8u281,这个版本已经有了对JRMP利用的限制,反序列化时会触发安全检查:

2021-10-20 21:49:54.771  INFO 42580 --- [127.0.0.1:1099]] java.io.serialization                    : ObjectInputFilter REJECTED: class javax.management.BadAttributeValueExpException, array length: -1, nRefs: 2, depth: 1, bytes: 110, ex: n/a

ObjectInputStream类在反序列化JRMPClient传输的数据时,会调用filterCheck函数:

// Call filterCheck on the class before reading anything else
filterCheck(cl, -1);

其检验代码如下:

try {
    status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,
                                                      totalObjectRefs, depth, bytesRead));
}

serialFilter是一个DGCImpl_Stub对象,其检验代码如下:

if (var1.isPrimitive()) {
    return Status.ALLOWED;
} else {
    return var1 != UID.class && var1 != VMID.class && var1 != Lease.class && (var1.getPackage() == null || !Throwable.class.isAssignableFrom(var1) || !"java.lang".equals(var1.getPackage().getName()) && !"java.rmi".equals(var1.getPackage().getName())) && var1 != StackTraceElement.class && var1 != ArrayList.class && var1 != Object.class && !var1.getName().equals("java.util.Collections$UnmodifiableList") && !var1.getName().equals("java.util.Collections$UnmodifiableCollection") && !var1.getName().equals("java.util.Collections$UnmodifiableRandomAccessList") && !var1.getName().equals("java.util.Collections$EmptyList") ? Status.REJECTED : Status.ALLOWED;
}

对要反序列化的类做了白名单检测,按照文章所说,应该是8u231版本做的安全检验,然后因为出现了绕过所以8u241做了进一步的修复。

tomcat+shiro

留坑。


参考

https://www.freebuf.com/vuls/178014.html

https://cloud.tencent.com/developer/article/1816604

https://blog.zsxsoft.com/post/35

https://www.cnblogs.com/W4nder/p/14508817.html


Web Java Shiro 反序列化

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

反序列化中的JRMP末路(?)
shiro2