未完待续

环境配置

本人近几天一直头疼于怎么配置这个shiro550的源码环境,这里我给出详细的步骤,以免后人再踩进我踩过的大坑…

首先,下载p神的shiro源码:

https://github.com/phith0n/JavaThings/

image-20250308162441449

下载好之后用IDEA打开这个shirodemo。

我的JDK版本:8u66,正常来说pom.xml里面是不会爆红的。

接着去配置Tomcat,这个步骤比较简单,网上教程也很多,这里不多赘述,推荐:

IntelliJ IDEA中配置Tomcat(超详细)_intellij idea配置tomcat-CSDN博客

我下载的是9.0.100版本。

在项目中配置

image-20250308163827383

接着来到项目结构的工件这里:

image-20250308163905643

可以看到已经有两个部署好的工件,选择下面这个,点应用。

编辑配置里头新建本地Tomcat:

image-20250308164008459

image-20250308164028242

点击应用后直接运行login.jsp,记得修改下它的端口号。

然后我们点击这里:

image-20250308164359419

即可启动。

image-20250308164415898

漏洞分析

数据包特点

在登录框中我们先简单地抓个包(点击了Remember me):

image-20250308164824611

没点击的包:

image-20250308165108882

相比之下得出结论:

勾选了remember me之后,在返回的数据包上多了Set-Cookie字段,第一个值为deleteMe,第二个像是一串Base64。

plaintext
1
6nTT7Ep5Grk1dN3I5KgZ1iTjjdZnS2+zSoEcYP69yZ7n+r2mYNzMXMXY1WvN99zuOQlrEsv1uHH9UOuTO0fyQjg0v0rh4RVzw4V4SANKcQSkSsFX7V8CSelOgTmiBX+3goGAsbhFGonZIE0nSlqprPgLsybCTW5hdJf/FbE6hC2g1xnoNHbuJ6Lj+BBgRY4IYmSm7kz2AhvtioQi6k6AsDAayvnZKwQlSIzH8Y4s2lEt6vxndTjQcHIngWQTxGZxjmXnQuOrsu6IeLtlRLpNzYSRw0RtXe3T5SifdcBXs5JC15ljEoRJqkO8GBT/8u1RfUNAXW9+stYrDbxBoQRO5bOZWZq/AHUXkACTiGJsiL17WjGYkT/Ec022fmdiZPGnvrLWeaOQeDXCFfg4IqSCH4tnHzt1kpwncCdSwpEbkbWb9/5/v1ZM3Ejz1j/KQI4Winhwytrz2cZq83CcITV1D1YeUGW4c0kVLPFu8NWVwM8hvxtmIphLYQgKU3xbe8tK

试图解码一下发现并不是base64,下面我们来看看Shiro框架(550版本)的加解密过程。

过程分析

我们先定位到CookieRememberMeManager类:

CookieRememberMeManager类

构造方法:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* CookieRememberMeManager 的默认构造方法。
* 该方法初始化了一个默认的 Cookie,用于存储 RememberMe 功能的数据。
*/
public CookieRememberMeManager() {
// 创建一个名为 "rememberMe" 的 Cookie 实例。
Cookie cookie = new SimpleCookie("rememberMe");

// 设置 Cookie 的 HttpOnly 属性为 true。
// 这表示该 Cookie 只能通过 HTTP(S) 协议访问,无法被 JavaScript 操作,从而提高安全性,
// 防止跨站脚本攻击(XSS)读取用户的 Cookie 数据。
cookie.setHttpOnly(true);

// 设置 Cookie 的最大存活时间(MaxAge),单位为秒。
cookie.setMaxAge(31536000);

// 将初始化的 Cookie 对象赋值给当前类的 cookie 属性,供后续使用。
this.cookie = cookie;
}

getRememberedSerializedIdentity()

image-20250308170912989

长话短说:

  1. 先判断是否为 HTTP 请求
  2. 如果是的话,获取 cookie 中 rememberMe 的值
  3. 然后判断是否是 deleteMe,如果存在,则返回null。
  4. 如果不是,则判断是否是符合 base64 的编码长度,然后再对其进行 base64 解码,将解码结果返回。

AbstractRememberMeManager类

现在我们去寻找一下谁会调用CookieRememberMeManager#getRememberedSerializedIdentity()

getRememberedPrincipals()

image-20250308171615315

  1. 首先是将 HTTP Requests 里面的 Cookie 拿出来,赋值给 bytes 数组;
  2. 随后将 bytes 数组的东西进行 convertBytesToPrincipals() 转换成principals(这是什么?)

我们跟进到神秘的convertBytesToPrincipals():

convertBytesToPrincipals()

image-20250308171857004

正常情况下这里调用了decrypt()deserialize()

看看decrypt()

decrypt()
java
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
/**
* 解密方法,用于对加密后的字节数组进行解密操作。
*
* @param encrypted 加密后的字节数组
* @return 解密后的字节数组(即原始的序列化数据)
*/
protected byte[] decrypt(byte[] encrypted) {
// 默认将输入的加密数据赋值给 serialized,防止未使用加密服务时直接返回原始数据。
byte[] serialized = encrypted;

// 获取 CipherService(加密服务)的实例。
// 如果配置了加密服务,则使用该服务进行解密操作。
CipherService cipherService = this.getCipherService();

if (cipherService != null) {
// 调用加密服务的 decrypt 方法对数据进行解密。
// 解密时需要提供加密数据(encrypted)和解密密钥(getDecryptionCipherKey())。
ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());

// 将解密后的数据转换为字节数组,赋值给 serialized。
serialized = byteSource.getBytes();
}

// 返回解密后的字节数组(如果未使用加密服务,则返回原始输入数据)。
return serialized;
}

既然有decrypt函数,肯定也有encrypt函数。我们把断点下在encrypt函数:

在单步跟的过程中可以知道encrypt采用的是AES加密:

image-20250308173312190

而且AES的key是一个固定值,写在了本类的源码中:

image-20250308174108474

deserialize()

image-20250308174154461

java
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 反序列化方法,将字节数组转换为 PrincipalCollection 对象。
*
* @param serializedIdentity 序列化后的字节数组
* @return 反序列化后的 PrincipalCollection 对象
*/
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
// 获取序列化器实例,用于进行反序列化操作。
// this.getSerializer() 方法返回当前对象配置的序列化器实例,通常是一个 Serializer 对象。
return (PrincipalCollection)this.getSerializer().deserialize(serializedIdentity);
}