环境搭建
下载源码
- 从链接https://codeload.github.com/apache/shiro/zip/shiro-root-1.4.1下载
- 使用git下载
- git clone https://github.com/apache/shiro.git
- git checkout shiro-root-1.2.4
构建war包
- 进入下载的源码目录
- mvn cd ./shiro/samples/web
- mvn clean
- mvn package
- 从链接https://github.com/jas502n/SHIRO-721下载war包
使用IDEA打开环境
使用IDEA打开下载源码的根目录,配置tomcat服务器,添加依赖,选择Exrernal Source(外部源),选择第二部生产的war包。
SDK版本选择的11
漏洞分析
密钥生成方式对比
shiro550漏洞的主要成因是shiro使用的密钥是硬编码的,在1.25<=shiro<=1.41中,shiro使用随机生成的密钥。我们看一下相关代码。
在shiro<=1.24中,可以看到shiro使用setCipherKey函数将常量DEFAULT_CIPHER_KEY_BYTES设为Key。
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
……
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
在1.25<=shiro<=1.41中,shiro使用generateNewKey()函数生成了一个新的Key
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
AesCipherService cipherService = new AesCipherService();
this.cipherService = cipherService;
setCipherKey(cipherService.generateNewKey().getEncoded());
}
跟进,调用了重载函数generateNewKey(getKeySize())
public Key generateNewKey() {
return generateNewKey(getKeySize());
}
public int getKeySize() {
return keySize;
}
再跟进,生成了一个KeyGenerator对象并调用init函数进行初始化。
查看init函数,调用了重载的双参数函数,多的那个参数是个random随机数
再调用generayeKey()函数生成密钥,跟进后发现调用engineGenerateKey()函数,生成了一个128位的随机aesKey,再调用getEncoded()方法获取key
密钥序列
shiro721解密流程
AbstractRememberMeManager类的getRememberedPrincipals方法获取subjectContext对象。
跟进,getCookie获取base64编码的RemerbeMe值,判断值不为DELETED_COOKIE_VALUE,即deleteMe后,接着处理,先是调用ensurePadding方法填充“=”号,再进行base64解码。
若RemeberMe不为null且长度大于0,调用convertBytesToPrincipals将subjectContext对象转换为principals对象。
跟进,
//这是一个公共方法,接受SubjectContext对象作为参数,并返回PrincipalCollection对象。
//PrincipalCollection是Shiro框架中用于存储主体身份信息的集合。
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
//创建一个PrincipalCollection对象,初始值为null。
PrincipalCollection principals = null;
try {
// 调用getRememberedSerializedIdentity方法获取记住的身份信息的字节数组。
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
// SHIRO-138 - 只有当字节数组存在时才调用convertBytesToPrincipals方法
if (bytes != null && bytes.length > 0) {
// 如果字节数组存在且长度大于0,则调用convertBytesToPrincipals方法将字节数组
//转换为PrincipalCollection对象,即将记住的身份信息还原为主体的身份集合。
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
// onRememberedPrincipalFailure方法是一个用于处理记住身份信息失败的回调方法。
//在这里,它将被调用,并将运行时异常和SubjectContext对象作为参数传递给它,
//以获取处理后的PrincipalCollection对象。
principals = onRememberedPrincipalFailure(re, subjectContext);
}
// 返回获取到的PrincipalCollection对象
return principals;
}
SubjectContext是Apache Shiro中的一个接口,用于在创建或更新Subject(主体)时传递和存储相关的上下文信息。它提供了一种统一的方式来传递Subject的创建或更新所需的各种参数和属性。
SubjectContext接口定义了一些用于存储和获取Subject相关信息的方法,例如设置或获取主体的身份标识、设置或获取主体的认证状态、设置或获取主体的会话等。通过SubjectContext,可以将这些上下文信息传递给Shiro的各个组件,以便进行相应的操作和处理。
SubjectContext的具体实现通常是由Shiro的各个组件提供的,例如在Web应用中,可以使用WebSubjectContext来存储与Web请求相关的信息。
在上述代码中,SubjectContext对象作为参数传递给getRememberedPrincipals
方法,用于获取记住的主体信息。通过SubjectContext,可以传递和访问获取记住身份信息所需的上下文信息,如请求数据、配置参数等。
PrincipalCollection是Apache Shiro中的一个接口,用于表示主体(Subject)的身份集合。它是一个包含多个身份信息的容器。
在Shiro中,Principal(身份)是指代表主体的标识信息,可以是用户名、邮箱、手机号等唯一标识主体的信息。PrincipalCollection则是存储一个主体可能拥有的多个身份信息的容器,例如一个用户可能同时拥有多个角色或权限。
PrincipalCollection接口提供了一些方法来处理和操作主体的身份集合,例如添加、移除、获取身份信息等。通过PrincipalCollection,可以访问主体拥有的各种身份信息,并在安全认证和授权过程中进行判断和处理。
PrincipalCollection的具体实现通常由Shiro的各个组件提供,如DefaultPrincipalCollection等。
在上述代码中,getRememberedPrincipals
方法返回的就是一个PrincipalCollection对象,它代表了通过记住我(RememberMe)功能获取到的主体的身份集合。通过PrincipalCollection,可以获取和操作这些身份信息,以供后续的认证和授权操作使用。
- SubjectContext(主体上下文):SubjectContext是一个上下文接口,用于在创建或更新Subject(主体)时传递和存储相关的上下文信息。它提供了一种统一的方式来传递Subject的创建或更新所需的各种参数和属性。SubjectContext通常用于在主体创建或认证之前传递必要的信息,例如主体的身份标识、认证状态、会话等。
- PrincipalCollection(身份集合):PrincipalCollection是一个表示主体的身份集合的接口。它用于存储主体拥有的多个身份信息,例如用户可能同时拥有多个角色或权限。PrincipalCollection提供了一些方法来处理和操作主体的身份集合,例如添加、移除、获取身份信息等。PrincipalCollection主要用于表示主体的身份信息,在认证和授权过程中使用。
总结起来,SubjectContext是用于传递主体创建或更新所需的上下文信息的接口,而PrincipalCollection是用于表示主体的身份集合的接口。SubjectContext包含更广泛的上下文信息,而PrincipalCollection更专注于主体的身份信息。在不同的场景和目的下,它们扮演着不同的角色。
调用decrypt()方法解密。生成一个cipherService对象,调用它的decrypt方法解密。
cipherService是一个接口,具体实现在JcaCipherService中
调用decrypt(encrypted, key, iv)函数进行解密
再跟进,调用了crypt(ciphertext, key, iv, javax.crypto.Cipher.DECRYPT_MODE)方法
调用crypt(cipher,bytes)方法处理
解密完成后,调用deserialize()方法进行反序列化处理,与shiro550类似。
Padding Orcale Attcak
首先要学习下前置知识,CBC翻转攻击:https://goodapple.top/2022/01/06/6db157fde87a6bae/
在解密最后一个函数crypt(cipher,bytes)中有个doFinal()方法,它对密文进行异常处理,doFinal()方法有IllegalBlockSizeException和BadPaddingException这两个异常,分别用于捕获块大小异常和填充错误异常。
无论是Padding错误还是反序列化处理错误,异常都会被getRememberedPrincipals()方法捕获,并执行onRememberedPrincipalFailure()方法。onRememberedPrincipalFailure()方法调用了forgetIdentity()。该方法会调用removeFrom(),在response头部添加字段Set-Cookie: rememberMe=deleteMe。
即,Padding结果不正确的话,响应包就会返回 Set-Cookie: rememberMe=deleteMe
可以得到布尔条件:
- Padding正确,服务器正常响应
- Padding错误,服务器返回Set-Cookie: rememberMe=deleteMe
通过该布尔条件,可以通过不断改变“前一个Block”,改变“后1个Block密文”的明文。通过返回结果,来判断解密是否成功。进而获取“后一个Block”解密出的“MediumValue”,得到“MediumValue”就能解密密文,加密明文。大于2组的密文,攻击者可以按规则选取其中2组block进行尝试,从而达到加密解密所有Block的效果
利用 Padding Oracle Attack,我们已经可以得知明文对应的密文以及密文对应的明文,相当于知道了key值,我们又知道在 CBC 加密模式中,第 n 个密文分组可以影响第 n+1 个明文分组,那我们已经知道了密文对应的明文,这里修改第 n 个密文分组,就相当于控制第 n+1 个明文分组。
即可构造我们的恶意数据,在数据解密后通过验证,执行反序列化时触发我们的恶意数据。
利用工具