2019-11-18 17:30:04 +01:00
|
|
|
#!/usr/bin/python3
|
|
|
|
|
|
|
|
#
|
|
|
|
# This tool connects to the given Exchange's hostname/IP address and then
|
|
|
|
# by collects various internal information being leaked while interacting
|
|
|
|
# with different Exchange protocols. Exchange may give away following helpful
|
|
|
|
# during OSINT or breach planning stages insights:
|
|
|
|
# - Internal IP address
|
|
|
|
# - Internal Domain Name (ActiveDirectory)
|
|
|
|
# - Exchange Server Version
|
|
|
|
# - support for various SMTP User Enumeration techniques
|
|
|
|
# - Version of underlying software such as ASP.NET, IIS which
|
|
|
|
# may point at OS version indirectly
|
|
|
|
#
|
|
|
|
# This tool will be helpful before mounting social engieering attack against
|
|
|
|
# victim's premises or to aid Password-Spraying efforts against exposed OWA
|
|
|
|
# interface.
|
|
|
|
#
|
|
|
|
# OPSEC:
|
|
|
|
# All of the traffic that this script generates is not invasive and should
|
|
|
|
# not be picked up by SOC/Blue Teams as it closely resembles random usual traffic
|
|
|
|
# directed at both OWA, or Exchange SMTP protocols/interfaces. The only potentially
|
|
|
|
# shady behaviour could be observed on one-shot attempts to perform SMTP user
|
|
|
|
# enumeration, however it is unlikely that single commands would trigger SIEM use cases.
|
|
|
|
#
|
2019-11-20 02:56:17 +01:00
|
|
|
# TODO:
|
|
|
|
# - introduce some fuzzy logic for Exchange version recognition
|
|
|
|
# - Extend SMTP User Enumeration validation capabilities
|
|
|
|
#
|
2019-11-18 17:30:04 +01:00
|
|
|
# Requirements:
|
|
|
|
# - pyOpenSSL
|
2019-11-20 02:56:17 +01:00
|
|
|
# - packaging
|
2019-11-18 17:30:04 +01:00
|
|
|
#
|
|
|
|
# Author:
|
2021-10-24 23:11:42 +02:00
|
|
|
# Mariusz Banach / mgeeky, '19, <mb@binary-offensive.com>
|
2019-11-18 17:30:04 +01:00
|
|
|
#
|
|
|
|
|
|
|
|
import re
|
|
|
|
import sys
|
|
|
|
import ssl
|
|
|
|
import time
|
|
|
|
import base64
|
|
|
|
import struct
|
|
|
|
import string
|
|
|
|
import socket
|
|
|
|
import smtplib
|
2019-11-20 02:56:17 +01:00
|
|
|
import urllib3
|
2019-11-18 17:30:04 +01:00
|
|
|
import requests
|
|
|
|
import argparse
|
|
|
|
import threading
|
|
|
|
import collections
|
2019-11-20 02:56:17 +01:00
|
|
|
import packaging.version
|
2019-11-18 17:30:04 +01:00
|
|
|
from urllib.parse import urlparse
|
|
|
|
import OpenSSL.crypto as crypto
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
VERSION = '0.2'
|
2019-11-18 17:30:04 +01:00
|
|
|
|
|
|
|
config = {
|
|
|
|
'debug' : False,
|
|
|
|
'verbose' : False,
|
|
|
|
'timeout' : 6.0,
|
|
|
|
}
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
found_dns_domain = ''
|
|
|
|
|
2019-11-18 17:30:04 +01:00
|
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
|
|
|
|
class Logger:
|
|
|
|
@staticmethod
|
|
|
|
def _out(x):
|
|
|
|
if config['verbose'] or config['debug']:
|
|
|
|
sys.stdout.write(x + '\n')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def out(x):
|
|
|
|
Logger._out('[.] ' + x)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def info(x):
|
|
|
|
Logger._out('[.] ' + x)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def dbg(x):
|
|
|
|
if config['debug']:
|
|
|
|
Logger._out('[DEBUG] ' + x)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def err(x):
|
|
|
|
sys.stdout.write('[!] ' + x + '\n')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def fail(x):
|
|
|
|
Logger._out('[-] ' + x)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def ok(x):
|
|
|
|
Logger._out('[+] ' + x)
|
|
|
|
|
|
|
|
def hexdump(data):
|
|
|
|
s = ''
|
|
|
|
n = 0
|
|
|
|
lines = []
|
|
|
|
tableline = '-----+' + '-' * 24 + '|' \
|
|
|
|
+ '-' * 25 + '+' + '-' * 18 + '+\n'
|
|
|
|
if isinstance(data, str):
|
|
|
|
data = data.encode()
|
|
|
|
|
|
|
|
if len(data) == 0:
|
|
|
|
return '<empty>'
|
|
|
|
|
|
|
|
for i in range(0, len(data), 16):
|
|
|
|
line = ''
|
|
|
|
line += '%04x | ' % (i)
|
|
|
|
n += 16
|
|
|
|
|
|
|
|
for j in range(n-16, n):
|
|
|
|
if j >= len(data): break
|
|
|
|
line += '%02x' % (data[j] & 0xff)
|
|
|
|
if j % 8 == 7 and j % 16 != 15:
|
|
|
|
line += '-'
|
|
|
|
else:
|
|
|
|
line += ' '
|
|
|
|
|
|
|
|
line += ' ' * (3 * 16 + 7 - len(line)) + ' | '
|
|
|
|
for j in range(n-16, n):
|
|
|
|
if j >= len(data): break
|
|
|
|
c = data[j] if not (data[j] < 0x20 or data[j] > 0x7e) else '.'
|
|
|
|
line += '%c' % c
|
|
|
|
|
|
|
|
line = line.ljust(74, ' ') + ' |'
|
|
|
|
lines.append(line)
|
|
|
|
|
|
|
|
return tableline + '\n'.join(lines) + '\n' + tableline
|
|
|
|
|
|
|
|
|
|
|
|
class NtlmParser:
|
|
|
|
#
|
|
|
|
# Based on:
|
|
|
|
# https://gist.github.com/aseering/829a2270b72345a1dc42
|
|
|
|
#
|
|
|
|
|
|
|
|
VALID_CHRS = set(string.ascii_letters + string.digits + string.punctuation)
|
|
|
|
flags_tbl_str = (
|
|
|
|
(0x00000001, "Negotiate Unicode"),
|
|
|
|
(0x00000002, "Negotiate OEM"),
|
|
|
|
(0x00000004, "Request Target"),
|
|
|
|
(0x00000008, "unknown"),
|
|
|
|
(0x00000010, "Negotiate Sign"),
|
|
|
|
(0x00000020, "Negotiate Seal"),
|
|
|
|
(0x00000040, "Negotiate Datagram Style"),
|
|
|
|
(0x00000080, "Negotiate Lan Manager Key"),
|
|
|
|
(0x00000100, "Negotiate Netware"),
|
|
|
|
(0x00000200, "Negotiate NTLM"),
|
|
|
|
(0x00000400, "unknown"),
|
|
|
|
(0x00000800, "Negotiate Anonymous"),
|
|
|
|
(0x00001000, "Negotiate Domain Supplied"),
|
|
|
|
(0x00002000, "Negotiate Workstation Supplied"),
|
|
|
|
(0x00004000, "Negotiate Local Call"),
|
|
|
|
(0x00008000, "Negotiate Always Sign"),
|
|
|
|
(0x00010000, "Target Type Domain"),
|
|
|
|
(0x00020000, "Target Type Server"),
|
|
|
|
(0x00040000, "Target Type Share"),
|
|
|
|
(0x00080000, "Negotiate NTLM2 Key"),
|
|
|
|
(0x00100000, "Request Init Response"),
|
|
|
|
(0x00200000, "Request Accept Response"),
|
|
|
|
(0x00400000, "Request Non-NT Session Key"),
|
|
|
|
(0x00800000, "Negotiate Target Info"),
|
|
|
|
(0x01000000, "unknown"),
|
|
|
|
(0x02000000, "unknown"),
|
|
|
|
(0x04000000, "unknown"),
|
|
|
|
(0x08000000, "unknown"),
|
|
|
|
(0x10000000, "unknown"),
|
|
|
|
(0x20000000, "Negotiate 128"),
|
|
|
|
(0x40000000, "Negotiate Key Exchange"),
|
|
|
|
(0x80000000, "Negotiate 56")
|
|
|
|
)
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.output = {}
|
|
|
|
self.flags_tbl = NtlmParser.flags_tbl_str
|
|
|
|
|
|
|
|
self.msg_types = collections.defaultdict(lambda: "UNKNOWN")
|
|
|
|
self.msg_types[1] = "Request"
|
|
|
|
self.msg_types[2] = "Challenge"
|
|
|
|
self.msg_types[3] = "Response"
|
|
|
|
|
|
|
|
self.target_field_types = collections.defaultdict(lambda: "UNKNOWN")
|
|
|
|
self.target_field_types[0] = ("TERMINATOR", str)
|
|
|
|
self.target_field_types[1] = ("Server name", str)
|
|
|
|
self.target_field_types[2] = ("AD domain name", str)
|
|
|
|
self.target_field_types[3] = ("FQDN", str)
|
|
|
|
self.target_field_types[4] = ("DNS domain name", str)
|
|
|
|
self.target_field_types[5] = ("Parent DNS domain", str)
|
|
|
|
self.target_field_types[7] = ("Server Timestamp", int)
|
|
|
|
|
|
|
|
def flags_lst(self, flags):
|
|
|
|
return [desc for val, desc in self.flags_tbl if val & flags]
|
|
|
|
|
|
|
|
def flags_str(self, flags):
|
|
|
|
return ['%s' % s for s in self.flags_lst(flags)]
|
|
|
|
|
|
|
|
def clean_str(self, st):
|
|
|
|
return ''.join((s if s in NtlmParser.VALID_CHRS else '?') for s in st)
|
|
|
|
|
|
|
|
class StrStruct(object):
|
|
|
|
def __init__(self, pos_tup, raw):
|
|
|
|
length, alloc, offset = pos_tup
|
|
|
|
self.length = length
|
|
|
|
self.alloc = alloc
|
|
|
|
self.offset = offset
|
|
|
|
self.raw = raw[offset:offset+length]
|
|
|
|
self.utf16 = False
|
|
|
|
|
|
|
|
if len(self.raw) >= 2 and self.raw[1] == 0:
|
|
|
|
try:
|
2022-03-01 12:28:50 +01:00
|
|
|
self.string = self.raw.decode('utf-16', 'ignore')
|
2019-11-18 17:30:04 +01:00
|
|
|
except:
|
2019-11-20 02:56:17 +01:00
|
|
|
self.string = ''.join(filter(lambda x: str(x) != str('\0'), self.raw))
|
2019-11-18 17:30:04 +01:00
|
|
|
self.utf16 = True
|
|
|
|
else:
|
|
|
|
self.string = self.raw
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return ''.join((s if s in NtlmParser.VALID_CHRS else '?') for s in self.string)
|
|
|
|
|
|
|
|
def parse(self, data):
|
|
|
|
st = base64.b64decode(data)
|
|
|
|
if st[:len('NTLMSSP')].decode() == "NTLMSSP":
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
raise Exception("NTLMSSP header not found at start of input string")
|
|
|
|
|
|
|
|
ver = struct.unpack("<i", st[8:12])[0]
|
|
|
|
|
|
|
|
if ver == 1:
|
|
|
|
self.request(st)
|
|
|
|
elif ver == 2:
|
|
|
|
self.challenge(st)
|
|
|
|
elif ver == 3:
|
|
|
|
self.response(st)
|
|
|
|
else:
|
|
|
|
o = "Unknown message structure. Have a raw (hex-encoded) message:"
|
|
|
|
o += st.encode("hex")
|
|
|
|
raise Exception(o)
|
|
|
|
|
|
|
|
return self.output
|
|
|
|
|
|
|
|
def opt_str_struct(self, name, st, offset):
|
|
|
|
nxt = st[offset:offset+8]
|
|
|
|
if len(nxt) == 8:
|
|
|
|
hdr_tup = struct.unpack("<hhi", nxt)
|
|
|
|
self.output[name] = str(NtlmParser.StrStruct(hdr_tup, st))
|
|
|
|
else:
|
|
|
|
self.output[name] = ""
|
|
|
|
|
|
|
|
def opt_inline_str(self, name, st, offset, sz):
|
|
|
|
nxt = st[offset:offset+sz]
|
|
|
|
if len(nxt) == sz:
|
|
|
|
self.output[name] = self.clean_str(nxt)
|
|
|
|
else:
|
|
|
|
self.output[name] = ""
|
|
|
|
|
|
|
|
def request(self, st):
|
|
|
|
hdr_tup = struct.unpack("<i", st[12:16])
|
|
|
|
flags = hdr_tup[0]
|
|
|
|
|
|
|
|
self.opt_str_struct("Domain", st, 16)
|
|
|
|
self.opt_str_struct("Workstation", st, 24)
|
|
|
|
self.opt_inline_str("OS Ver", st, 32, 8)
|
|
|
|
|
|
|
|
self.output['Flags'] = self.flags_str(flags)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def win_file_time_to_datetime(ft):
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
EPOCH_AS_FILETIME = 116444736000000000 # January 1, 1970 as MS file time
|
|
|
|
utc = datetime.utcfromtimestamp((ft - EPOCH_AS_FILETIME) / 10000000)
|
|
|
|
return utc.strftime('%y-%m-%d %a %H:%M:%S UTC')
|
|
|
|
|
|
|
|
|
|
|
|
def challenge(self, st):
|
|
|
|
hdr_tup = struct.unpack("<hhiiQ", st[12:32])
|
|
|
|
|
|
|
|
self.output['Target Name'] = str(NtlmParser.StrStruct(hdr_tup[0:3], st))
|
|
|
|
self.output['Challenge'] = hdr_tup[4]
|
|
|
|
|
|
|
|
flags = hdr_tup[3]
|
|
|
|
|
|
|
|
self.opt_str_struct("Context", st, 32)
|
|
|
|
|
|
|
|
nxt = st[40:48]
|
|
|
|
if len(nxt) == 8:
|
|
|
|
hdr_tup = struct.unpack("<hhi", nxt)
|
|
|
|
tgt = NtlmParser.StrStruct(hdr_tup, st)
|
|
|
|
|
|
|
|
self.output['Target'] = {}
|
|
|
|
|
|
|
|
raw = tgt.raw
|
|
|
|
pos = 0
|
|
|
|
|
|
|
|
while pos+4 < len(raw):
|
|
|
|
rec_hdr = struct.unpack("<hh", raw[pos : pos+4])
|
|
|
|
rec_type_id = rec_hdr[0]
|
|
|
|
rec_type, rec_type_type = self.target_field_types[rec_type_id]
|
|
|
|
rec_sz = rec_hdr[1]
|
|
|
|
subst = raw[pos+4 : pos+4+rec_sz]
|
|
|
|
if rec_type_type == int:
|
|
|
|
if 'Timestamp' in rec_type:
|
|
|
|
self.output['Target'][rec_type] = NtlmParser.win_file_time_to_datetime(
|
|
|
|
struct.unpack("<Q", subst)[0]
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
self.output['Target'][rec_type] = struct.unpack("<Q", subst)[0]
|
|
|
|
elif rec_type_type == str:
|
|
|
|
try:
|
|
|
|
self.output['Target'][rec_type] = subst.decode('utf-16')
|
|
|
|
except:
|
|
|
|
self.output['Target'][rec_type] = subst
|
|
|
|
pos += 4 + rec_sz
|
|
|
|
|
|
|
|
self.opt_inline_str("OS Ver", st, 48, 8)
|
|
|
|
self.output['Flags'] = self.flags_str(flags)
|
|
|
|
|
|
|
|
def response(self, st):
|
|
|
|
hdr_tup = struct.unpack("<hhihhihhihhihhi", st[12:52])
|
|
|
|
|
|
|
|
self.output['LM Resp'] = str(NtlmParser.StrStruct(hdr_tup[0:3], st))
|
|
|
|
self.output['NTLM Resp'] = str(NtlmParser.StrStruct(hdr_tup[3:6], st))
|
|
|
|
self.output['Target Name'] = str(NtlmParser.StrStruct(hdr_tup[6:9], st))
|
|
|
|
self.output['User Name'] = str(NtlmParser.StrStruct(hdr_tup[9:12], st))
|
|
|
|
self.output['Host Name'] = str(NtlmParser.StrStruct(hdr_tup[12:15], st))
|
|
|
|
|
|
|
|
self.opt_str_struct("Session Key", st, 52)
|
|
|
|
self.opt_inline_str("OS Ver", st, 64, 8)
|
|
|
|
|
|
|
|
nxt = st[60:64]
|
|
|
|
if len(nxt) == 4:
|
|
|
|
flg_tup = struct.unpack("<i", nxt)
|
|
|
|
flags = flg_tup[0]
|
|
|
|
self.output['Flags'] = self.flags_str(flags)
|
|
|
|
else:
|
|
|
|
self.output['Flags'] = ""
|
|
|
|
|
|
|
|
|
|
|
|
class ExchangeRecon:
|
|
|
|
COMMON_PORTS = (443, 80, 8080, 8000)
|
|
|
|
MAX_RECONNECTS = 3
|
|
|
|
MAX_REDIRECTS = 10
|
|
|
|
HEADERS = {
|
2022-04-13 16:42:31 +02:00
|
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36',
|
2019-11-18 17:30:04 +01:00
|
|
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
|
|
'Accept-Language': 'en-US,en;q=0.5',
|
|
|
|
'Accept-Encoding': 'gzip, deflate',
|
|
|
|
#'Connection': 'close',
|
|
|
|
}
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
leakedInternalIp = 'Leaked Internal IP address'
|
|
|
|
leakedInternalDomainNTLM = 'Leaked Internal Domain name in NTLM challenge packet'
|
|
|
|
iisVersion = 'IIS Version'
|
|
|
|
aspVersion = 'ASP.Net Version'
|
|
|
|
unusualHeaders = 'Unusual HTTP headers observed'
|
|
|
|
owaVersionInHttpHeader = 'Outlook Web App version HTTP header'
|
|
|
|
legacyMailCapabilities = "Exchange supports legacy SMTP and returned following banner/unusual capabilities"
|
|
|
|
|
|
|
|
usualHeaders = {
|
|
|
|
'accept-ranges',
|
|
|
|
'access-control-allow-origin',
|
|
|
|
'age',
|
|
|
|
'cache',
|
|
|
|
'cache-control',
|
|
|
|
'connection',
|
|
|
|
'content-encoding',
|
|
|
|
'content-length',
|
|
|
|
'content-security-policy',
|
|
|
|
'content-type',
|
|
|
|
'cookie',
|
|
|
|
'date',
|
|
|
|
'etag',
|
|
|
|
'expires',
|
|
|
|
'last-modified',
|
|
|
|
'link',
|
|
|
|
'location',
|
|
|
|
'pragma',
|
|
|
|
'referrer-policy',
|
|
|
|
'request-id',
|
|
|
|
'server',
|
|
|
|
'set-cookie',
|
|
|
|
'status',
|
|
|
|
'strict-transport-security',
|
|
|
|
'vary',
|
|
|
|
'via',
|
|
|
|
'www-authenticate',
|
|
|
|
'x-aspnet-version',
|
|
|
|
'x-content-type-options',
|
|
|
|
'x-frame-options',
|
|
|
|
'x-powered-by',
|
|
|
|
'x-xss-protection',
|
|
|
|
}
|
|
|
|
|
|
|
|
htmlregexes = {
|
|
|
|
'Outlook Web App version leaked in OWA HTML source' : r'/owa/(?:auth/)?((?:\d+\.)+\d+)/(?:themes|scripts)/'
|
|
|
|
}
|
|
|
|
|
|
|
|
class Verstring(object):
|
|
|
|
def __init__(self, name, date, *versions):
|
|
|
|
self.name = name
|
|
|
|
self.date = date
|
|
|
|
self.version = versions[0].split(' ')[0]
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
if isinstance(other, Verstring):
|
|
|
|
return packaging.version.parse(self.version) == packaging.version.parse(other.version) \
|
|
|
|
and self.name == other.name
|
|
|
|
elif isinstance(other, str):
|
|
|
|
return packaging.version.parse(self.version) == packaging.version.parse(other)
|
|
|
|
|
|
|
|
def __lt__(self, other):
|
|
|
|
return packaging.version.parse(self.version) < packaging.version.parse(other.version)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return f'{self.name}; {self.date}; {self.version}'
|
|
|
|
|
|
|
|
# https://docs.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates?view=exchserver-2019
|
|
|
|
exchangeVersions = (
|
|
|
|
Verstring('Exchange Server 4.0 SP5 ', 'May 5, 1998', '4.0.996'),
|
|
|
|
Verstring('Exchange Server 4.0 SP4 ', 'March 28, 1997', '4.0.995'),
|
|
|
|
Verstring('Exchange Server 4.0 SP3 ', 'October 29, 1996', '4.0.994'),
|
|
|
|
Verstring('Exchange Server 4.0 SP2 ', 'July 19, 1996', '4.0.993'),
|
|
|
|
Verstring('Exchange Server 4.0 SP1 ', 'May 1, 1996', '4.0.838'),
|
|
|
|
Verstring('Exchange Server 4.0 Standard Edition', 'June 11, 1996', '4.0.837'),
|
|
|
|
Verstring('Exchange Server 5.0 SP2 ', 'February 19, 1998', '5.0.1460'),
|
|
|
|
Verstring('Exchange Server 5.0 SP1 ', 'June 18, 1997', '5.0.1458'),
|
|
|
|
Verstring('Exchange Server 5.0 ', 'May 23, 1997', '5.0.1457'),
|
|
|
|
Verstring('Exchange Server version 5.5 SP4 ', 'November 1, 2000', '5.5.2653'),
|
|
|
|
Verstring('Exchange Server version 5.5 SP3 ', 'September 9, 1999', '5.5.2650'),
|
|
|
|
Verstring('Exchange Server version 5.5 SP2 ', 'December 23, 1998', '5.5.2448'),
|
|
|
|
Verstring('Exchange Server version 5.5 SP1 ', 'August 5, 1998', '5.5.2232'),
|
|
|
|
Verstring('Exchange Server version 5.5 ', 'February 3, 1998', '5.5.1960'),
|
|
|
|
Verstring('Exchange 2000 Server post-SP3', 'August 2008', '6.0.6620.7'),
|
|
|
|
Verstring('Exchange 2000 Server post-SP3', 'March 2008', '6.0.6620.5'),
|
|
|
|
Verstring('Exchange 2000 Server post-SP3', 'August 2004', '6.0.6603'),
|
|
|
|
Verstring('Exchange 2000 Server post-SP3', 'April 2004', '6.0.6556'),
|
|
|
|
Verstring('Exchange 2000 Server post-SP3', 'September 2003', '6.0.6487'),
|
|
|
|
Verstring('Exchange 2000 Server SP3', 'July 18, 2002', '6.0.6249'),
|
|
|
|
Verstring('Exchange 2000 Server SP2', 'November 29, 2001', '6.0.5762'),
|
|
|
|
Verstring('Exchange 2000 Server SP1', 'June 21, 2001', '6.0.4712'),
|
|
|
|
Verstring('Exchange 2000 Server', 'November 29, 2000', '6.0.4417'),
|
|
|
|
Verstring('Exchange Server 2003 post-SP2', 'August 2008', '6.5.7654.4'),
|
|
|
|
Verstring('Exchange Server 2003 post-SP2', 'March 2008', '6.5.7653.33'),
|
|
|
|
Verstring('Exchange Server 2003 SP2', 'October 19, 2005', '6.5.7683'),
|
|
|
|
Verstring('Exchange Server 2003 SP1', 'May25, 2004', '6.5.7226'),
|
|
|
|
Verstring('Exchange Server 2003', 'September 28, 2003', '6.5.6944'),
|
|
|
|
Verstring('Update Rollup 5 for Exchange Server 2007 SP2', 'December 7, 2010', '8.2.305.3', '8.02.0305.003'),
|
|
|
|
Verstring('Update Rollup 4 for Exchange Server 2007 SP2', 'April 9, 2010', '8.2.254.0', '8.02.0254.000'),
|
|
|
|
Verstring('Update Rollup 3 for Exchange Server 2007 SP2', 'March 17, 2010', '8.2.247.2', '8.02.0247.002'),
|
|
|
|
Verstring('Update Rollup 2 for Exchange Server 2007 SP2', 'January 22, 2010', '8.2.234.1', '8.02.0234.001'),
|
|
|
|
Verstring('Update Rollup 1 for Exchange Server 2007 SP2', 'November 19, 2009', '8.2.217.3', '8.02.0217.003'),
|
|
|
|
Verstring('Exchange Server 2007 SP2', 'August 24, 2009', '8.2.176.2', '8.02.0176.002'),
|
|
|
|
Verstring('Update Rollup 10 for Exchange Server 2007 SP1', 'April 13, 2010', '8.1.436.0', '8.01.0436.000'),
|
|
|
|
Verstring('Update Rollup 9 for Exchange Server 2007 SP1', 'July 16, 2009', '8.1.393.1', '8.01.0393.001'),
|
|
|
|
Verstring('Update Rollup 8 for Exchange Server 2007 SP1', 'May 19, 2009', '8.1.375.2', '8.01.0375.002'),
|
|
|
|
Verstring('Update Rollup 7 for Exchange Server 2007 SP1', 'March 18, 2009', '8.1.359.2', '8.01.0359.002'),
|
|
|
|
Verstring('Update Rollup 6 for Exchange Server 2007 SP1', 'February 10, 2009', '8.1.340.1', '8.01.0340.001'),
|
|
|
|
Verstring('Update Rollup 5 for Exchange Server 2007 SP1', 'November 20, 2008', '8.1.336.1', '8.01.0336.01'),
|
|
|
|
Verstring('Update Rollup 4 for Exchange Server 2007 SP1', 'October 7, 2008', '8.1.311.3', '8.01.0311.003'),
|
|
|
|
Verstring('Update Rollup 3 for Exchange Server 2007 SP1', 'July 8, 2008', '8.1.291.2', '8.01.0291.002'),
|
|
|
|
Verstring('Update Rollup 2 for Exchange Server 2007 SP1', 'May 9, 2008', '8.1.278.2', '8.01.0278.002'),
|
|
|
|
Verstring('Update Rollup 1 for Exchange Server 2007 SP1', 'February 28, 2008', '8.1.263.1', '8.01.0263.001'),
|
|
|
|
Verstring('Exchange Server 2007 SP1', 'November 29, 2007', '8.1.240.6', '8.01.0240.006'),
|
|
|
|
Verstring('Update Rollup 7 for Exchange Server 2007', 'July 8, 2008', '8.0.813.0', '8.00.0813.000'),
|
|
|
|
Verstring('Update Rollup 6 for Exchange Server 2007', 'February 21, 2008', '8.0.783.2', '8.00.0783.002'),
|
|
|
|
Verstring('Update Rollup 5 for Exchange Server 2007', 'October 25, 2007', '8.0.754.0', '8.00.0754.000'),
|
|
|
|
Verstring('Update Rollup 4 for Exchange Server 2007', 'August 23, 2007', '8.0.744.0', '8.00.0744.000'),
|
|
|
|
Verstring('Update Rollup 3 for Exchange Server 2007', 'June 28, 2007', '8.0.730.1', '8.00.0730.001'),
|
|
|
|
Verstring('Update Rollup 2 for Exchange Server 2007', 'May 8, 2007', '8.0.711.2', '8.00.0711.002'),
|
|
|
|
Verstring('Update Rollup 1 for Exchange Server 2007', 'April 17, 2007', '8.0.708.3', '8.00.0708.003'),
|
|
|
|
Verstring('Exchange Server 2007 RTM', 'March 8, 2007', '8.0.685.25 8.00.0685.025'),
|
|
|
|
Verstring('Update Rollup 23 for Exchange Server 2007 SP3', 'March 21, 2017', '8.3.517.0', '8.03.0517.000'),
|
|
|
|
Verstring('Update Rollup 22 for Exchange Server 2007 SP3', 'December 13, 2016', '8.3.502.0', '8.03.0502.000'),
|
|
|
|
Verstring('Update Rollup 21 for Exchange Server 2007 SP3', 'September 20, 2016', '8.3.485.1', '8.03.0485.001'),
|
|
|
|
Verstring('Update Rollup 20 for Exchange Server 2007 SP3', 'June 21, 2016', '8.3.468.0', '8.03.0468.000'),
|
|
|
|
Verstring('Update Rollup 19 forExchange Server 2007 SP3', 'March 15, 2016', '8.3.459.0', '8.03.0459.000'),
|
|
|
|
Verstring('Update Rollup 18 forExchange Server 2007 SP3', 'December, 2015', '8.3.445.0', '8.03.0445.000'),
|
|
|
|
Verstring('Update Rollup 17 forExchange Server 2007 SP3', 'June 17, 2015', '8.3.417.1', '8.03.0417.001'),
|
|
|
|
Verstring('Update Rollup 16 for Exchange Server 2007 SP3', 'March 17, 2015', '8.3.406.0', '8.03.0406.000'),
|
|
|
|
Verstring('Update Rollup 15 for Exchange Server 2007 SP3', 'December 9, 2014', '8.3.389.2', '8.03.0389.002'),
|
|
|
|
Verstring('Update Rollup 14 for Exchange Server 2007 SP3', 'August 26, 2014', '8.3.379.2', '8.03.0379.002'),
|
|
|
|
Verstring('Update Rollup 13 for Exchange Server 2007 SP3', 'February 24, 2014', '8.3.348.2', '8.03.0348.002'),
|
|
|
|
Verstring('Update Rollup 12 for Exchange Server 2007 SP3', 'December 9, 2013', '8.3.342.4', '8.03.0342.004'),
|
|
|
|
Verstring('Update Rollup 11 for Exchange Server 2007 SP3', 'August 13, 2013', '8.3.327.1', '8.03.0327.001'),
|
|
|
|
Verstring('Update Rollup 10 for Exchange Server 2007 SP3', 'February 11, 2013', '8.3.298.3', '8.03.0298.003'),
|
|
|
|
Verstring('Update Rollup 9 for Exchange Server 2007 SP3', 'December 10, 2012', '8.3.297.2', '8.03.0297.002'),
|
|
|
|
Verstring('Update Rollup 8-v3 for Exchange Server 2007 SP3 ', 'November 13, 2012', '8.3.279.6', '8.03.0279.006'),
|
|
|
|
Verstring('Update Rollup 8-v2 for Exchange Server 2007 SP3 ', 'October 9, 2012', '8.3.279.5', '8.03.0279.005'),
|
|
|
|
Verstring('Update Rollup 8 for Exchange Server 2007 SP3', 'August 13, 2012', '8.3.279.3', '8.03.0279.003'),
|
|
|
|
Verstring('Update Rollup 7 for Exchange Server 2007 SP3', 'April 16, 2012', '8.3.264.0', '8.03.0264.000'),
|
|
|
|
Verstring('Update Rollup 6 for Exchange Server 2007 SP3', 'January 26, 2012', '8.3.245.2', '8.03.0245.002'),
|
|
|
|
Verstring('Update Rollup 5 for Exchange Server 2007 SP3', 'September 21, 2011', '8.3.213.1', '8.03.0213.001'),
|
|
|
|
Verstring('Update Rollup 4 for Exchange Server 2007 SP3', 'May 28, 2011', '8.3.192.1', '8.03.0192.001'),
|
|
|
|
Verstring('Update Rollup 3-v2 for Exchange Server 2007 SP3 ', 'March 30, 2011', '8.3.159.2', '8.03.0159.002'),
|
|
|
|
Verstring('Update Rollup 2 for Exchange Server 2007 SP3', 'December 10, 2010', '8.3.137.3', '8.03.0137.003'),
|
|
|
|
Verstring('Update Rollup 1 for Exchange Server 2007 SP3', 'September 9, 2010', '8.3.106.2', '8.03.0106.002'),
|
|
|
|
Verstring('Exchange Server 2007 SP3', 'June 7, 2010', '8.3.83.6', '8.03.0083.006'),
|
|
|
|
Verstring('Update Rollup 8 for Exchange Server 2010 SP2', 'December 9, 2013', '14.2.390.3 14.02.0390.003'),
|
|
|
|
Verstring('Update Rollup 7 for Exchange Server 2010 SP2', 'August 3, 2013', '14.2.375.0 14.02.0375.000'),
|
|
|
|
Verstring('Update Rollup 6 Exchange Server 2010 SP2', 'February 12, 2013', '14.2.342.3 14.02.0342.003'),
|
|
|
|
Verstring('Update Rollup 5 v2 for Exchange Server 2010 SP2 ', 'December 10, 2012', '14.2.328.10 14.02.0328.010'),
|
|
|
|
Verstring('Update Rollup 5 for Exchange Server 2010 SP2', 'November 13, 2012', '14.3.328.5 14.03.0328.005'),
|
|
|
|
Verstring('Update Rollup 4 v2 for Exchange Server 2010 SP2 ', 'October 9, 2012', '14.2.318.4 14.02.0318.004'),
|
|
|
|
Verstring('Update Rollup 4 for Exchange Server 2010 SP2', 'August 13, 2012', '14.2.318.2 14.02.0318.002'),
|
|
|
|
Verstring('Update Rollup 3 for Exchange Server 2010 SP2', 'May 29, 2012', '14.2.309.2 14.02.0309.002'),
|
|
|
|
Verstring('Update Rollup 2 for Exchange Server 2010 SP2', 'April 16, 2012', '14.2.298.4 14.02.0298.004'),
|
|
|
|
Verstring('Update Rollup 1 for Exchange Server 2010 SP2', 'February 13, 2012', '14.2.283.3 14.02.0283.003'),
|
|
|
|
Verstring('Exchange Server 2010 SP2', 'December 4, 2011', '14.2.247.5 14.02.0247.005'),
|
|
|
|
Verstring('Update Rollup 8 for Exchange Server 2010 SP1', 'December 10, 2012', '14.1.438.0 14.01.0438.000'),
|
|
|
|
Verstring('Update Rollup 7 v3 for Exchange Server 2010 SP1 ', 'November 13, 2012', '14.1.421.3 14.01.0421.003'),
|
|
|
|
Verstring('Update Rollup 7 v2 for Exchange Server 2010 SP1 ', 'October 10, 2012', '14.1.421.2 14.01.0421.002'),
|
|
|
|
Verstring('Update Rollup 7 for Exchange Server 2010 SP1', 'August 8, 2012', '14.1.421.0 14.01.0421.000'),
|
|
|
|
Verstring('Update Rollup 6 for Exchange Server 2010 SP1', 'October 27, 2011', '14.1.355.2 14.01.0355.002'),
|
|
|
|
Verstring('Update Rollup 5 for Exchange Server 2010 SP1', 'August 23, 2011', '14.1.339.1 14.01.0339.001'),
|
|
|
|
Verstring('Update Rollup 4 for Exchange Server 2010 SP1', 'July 27, 2011', '14.1.323.6 14.01.0323.006'),
|
|
|
|
Verstring('Update Rollup 3 for Exchange Server 2010 SP1', 'April 6, 2011', '14.1.289.7 14.01.0289.007'),
|
|
|
|
Verstring('Update Rollup 2 for Exchange Server 2010 SP1', 'December 9, 2010', '14.1.270.1 14.01.0270.001'),
|
|
|
|
Verstring('Update Rollup 1 for Exchange Server 2010 SP1', 'October 4, 2010', '14.1.255.2 14.01.0255.002'),
|
|
|
|
Verstring('Exchange Server 2010 SP1', 'August 23, 2010', '14.1.218.15 14.01.0218.015'),
|
|
|
|
Verstring('Update Rollup 5 for Exchange Server 2010', 'December 13, 2010', '14.0.726.0 14.00.0726.000'),
|
|
|
|
Verstring('Update Rollup 4 for Exchange Server 2010', 'June 10, 2010', '14.0.702.1 14.00.0702.001'),
|
|
|
|
Verstring('Update Rollup 3 for Exchange Server 2010', 'April 13, 2010', '14.0.694.0 14.00.0694.000'),
|
|
|
|
Verstring('Update Rollup 2 for Exchange Server 2010', 'March 4, 2010', '14.0.689.0 14.00.0689.000'),
|
|
|
|
Verstring('Update Rollup 1 for Exchange Server 2010', 'December 9, 2009', '14.0.682.1 14.00.0682.001'),
|
|
|
|
Verstring('Exchange Server 2010 RTM', 'November 9, 2009', '14.0.639.21 14.00.0639.021'),
|
|
|
|
Verstring('Update Rollup 29 for Exchange Server 2010 SP3', 'July 9, 2019', '14.3.468.0 14.03.0468.000'),
|
|
|
|
Verstring('Update Rollup 28 for Exchange Server 2010 SP3', 'June 7, 2019', '14.3.461.1 14.03.0461.001'),
|
|
|
|
Verstring('Update Rollup 27 for Exchange Server 2010 SP3', 'April 9, 2019', '14.3.452.0 14.03.0452.000'),
|
|
|
|
Verstring('Update Rollup 26 for Exchange Server 2010 SP3', 'February 12, 2019', '14.3.442.0 14.03.0442.000'),
|
|
|
|
Verstring('Update Rollup 25 for Exchange Server 2010 SP3', 'January 8, 2019', '14.3.435.0 14.03.0435.000'),
|
|
|
|
Verstring('Update Rollup 24 for Exchange Server 2010 SP3', 'September 5, 2018', '14.3.419.0 14.03.0419.000'),
|
|
|
|
Verstring('Update Rollup 23 for Exchange Server 2010 SP3', 'August 13, 2018', '14.3.417.1 14.03.0417.001'),
|
|
|
|
Verstring('Update Rollup 22 for Exchange Server 2010 SP3', 'June 19, 2018', '14.3.411.0 14.03.0411.000'),
|
|
|
|
Verstring('Update Rollup 21 for Exchange Server 2010 SP3', 'May 7, 2018', '14.3.399.2 14.03.0399.002'),
|
|
|
|
Verstring('Update Rollup 20 for Exchange Server 2010 SP3', 'March 5, 2018', '14.3.389.1 14.03.0389.001'),
|
|
|
|
Verstring('Update Rollup 19 for Exchange Server 2010 SP3', 'December 19, 2017', '14.3.382.0 14.03.0382.000'),
|
|
|
|
Verstring('Update Rollup 18 for Exchange Server 2010 SP3', 'July 11, 2017', '14.3.361.1 14.03.0361.001'),
|
|
|
|
Verstring('Update Rollup 17 for Exchange Server 2010 SP3', 'March 21, 2017', '14.3.352.0 14.03.0352.000'),
|
|
|
|
Verstring('Update Rollup 16 for Exchange Server 2010 SP3', 'December 13, 2016', '14.3.336.0 14.03.0336.000'),
|
|
|
|
Verstring('Update Rollup 15 for Exchange Server 2010 SP3', 'September 20, 2016', '14.3.319.2 14.03.0319.002'),
|
|
|
|
Verstring('Update Rollup 14 for Exchange Server 2010 SP3', 'June 21, 2016', '14.3.301.0 14.03.0301.000'),
|
|
|
|
Verstring('Update Rollup 13 for Exchange Server 2010 SP3', 'March 15, 2016', '14.3.294.0 14.03.0294.000'),
|
|
|
|
Verstring('Update Rollup 12 for Exchange Server 2010 SP3', 'December 15, 2015', '14.3.279.2 14.03.0279.002'),
|
|
|
|
Verstring('Update Rollup 11 for Exchange Server 2010 SP3', 'September 15, 2015', '14.3.266.2 14.03.0266.002'),
|
|
|
|
Verstring('Update Rollup 10 for Exchange Server 2010 SP3', 'June 17, 2015', '14.3.248.2 14.03.0248.002'),
|
|
|
|
Verstring('Update Rollup 9 for Exchange Server 2010 SP3', 'March 17, 2015', '14.3.235.1 14.03.0235.001'),
|
|
|
|
Verstring('Update Rollup 8 v2 for Exchange Server 2010 SP3 ', 'December 12, 2014', '14.3.224.2 14.03.0224.002'),
|
|
|
|
Verstring('Update Rollup 8 v1 for Exchange Server 2010 SP3 (recalled) ', 'December 9, 2014', '14.3.224.1 14.03.0224.001'),
|
|
|
|
Verstring('Update Rollup 7 for Exchange Server 2010 SP3', 'August 26, 2014', '14.3.210.2 14.03.0210.002'),
|
|
|
|
Verstring('Update Rollup 6 for Exchange Server 2010 SP3', 'May 27, 2014', '14.3.195.1 14.03.0195.001'),
|
|
|
|
Verstring('Update Rollup 5 for Exchange Server 2010 SP3', 'February 24, 2014', '14.3.181.6 14.03.0181.006'),
|
|
|
|
Verstring('Update Rollup 4 for Exchange Server 2010 SP3', 'December 9, 2013', '14.3.174.1 14.03.0174.001'),
|
|
|
|
Verstring('Update Rollup 3 for Exchange Server 2010 SP3', 'November 25, 2013', '14.3.169.1 14.03.0169.001'),
|
|
|
|
Verstring('Update Rollup 2 for Exchange Server 2010 SP3', 'August 8, 2013', '14.3.158.1 14.03.0158.001'),
|
|
|
|
Verstring('Update Rollup 1 for Exchange Server 2010 SP3', 'May 29, 2013', '14.3.146.0 14.03.0146.000'),
|
|
|
|
Verstring('Exchange Server 2010 SP3', 'February 12, 2013', '14.3.123.4 14.03.0123.004'),
|
|
|
|
Verstring('Exchange Server 2013 CU23', 'June 18, 2019', '15.0.1497.2 15.00.1497.002'),
|
|
|
|
Verstring('Exchange Server 2013 CU22', 'February 12, 2019', '15.0.1473.3 15.00.1473.003'),
|
|
|
|
Verstring('Exchange Server 2013 CU21', 'June 19, 2018', '15.0.1395.4 15.00.1395.004'),
|
|
|
|
Verstring('Exchange Server 2013 CU20', 'March 20, 2018', '15.0.1367.3 15.00.1367.003'),
|
|
|
|
Verstring('Exchange Server 2013 CU19', 'December 19, 2017', '15.0.1365.1 15.00.1365.001'),
|
|
|
|
Verstring('Exchange Server 2013 CU18', 'September 19, 2017', '15.0.1347.2 15.00.1347.002'),
|
|
|
|
Verstring('Exchange Server 2013 CU17', 'June 27, 2017', '15.0.1320.4 15.00.1320.004'),
|
|
|
|
Verstring('Exchange Server 2013 CU16', 'March 21, 2017', '15.0.1293.2 15.00.1293.002'),
|
|
|
|
Verstring('Exchange Server 2013 CU15', 'December 13, 2016', '15.0.1263.5 15.00.1263.005'),
|
|
|
|
Verstring('Exchange Server 2013 CU14', 'September 20, 2016', '15.0.1236.3 15.00.1236.003'),
|
|
|
|
Verstring('Exchange Server 2013 CU13', 'June 21, 2016', '15.0.1210.3 15.00.1210.003'),
|
|
|
|
Verstring('Exchange Server 2013 CU12', 'March 15, 2016', '15.0.1178.4 15.00.1178.004'),
|
|
|
|
Verstring('Exchange Server 2013 CU11', 'December 15, 2015', '15.0.1156.6 15.00.1156.006'),
|
|
|
|
Verstring('Exchange Server 2013 CU10', 'September 15, 2015', '15.0.1130.7 15.00.1130.007'),
|
|
|
|
Verstring('Exchange Server 2013 CU9', 'June 17, 2015', '15.0.1104.5 15.00.1104.005'),
|
|
|
|
Verstring('Exchange Server 2013 CU8', 'March 17, 2015', '15.0.1076.9 15.00.1076.009'),
|
|
|
|
Verstring('Exchange Server 2013 CU7', 'December 9, 2014', '15.0.1044.25', '15.00.1044.025'),
|
|
|
|
Verstring('Exchange Server 2013 CU6', 'August 26, 2014', '15.0.995.29 15.00.0995.029'),
|
|
|
|
Verstring('Exchange Server 2013 CU5', 'May 27, 2014', '15.0.913.22 15.00.0913.022'),
|
|
|
|
Verstring('Exchange Server 2013 SP1', 'February 25, 2014', '15.0.847.32 15.00.0847.032'),
|
|
|
|
Verstring('Exchange Server 2013 CU3', 'November 25, 2013', '15.0.775.38 15.00.0775.038'),
|
|
|
|
Verstring('Exchange Server 2013 CU2', 'July 9, 2013', '15.0.712.24 15.00.0712.024'),
|
|
|
|
Verstring('Exchange Server 2013 CU1', 'April 2, 2013', '15.0.620.29 15.00.0620.029'),
|
|
|
|
Verstring('Exchange Server 2013 RTM', 'December 3, 2012', '15.0.516.32 15.00.0516.03'),
|
|
|
|
Verstring('Exchange Server 2016 CU14', 'September 17, 2019', '15.1.1847.3 15.01.1847.003'),
|
|
|
|
Verstring('Exchange Server 2016 CU13', 'June 18, 2019', '15.1.1779.2 15.01.1779.002'),
|
|
|
|
Verstring('Exchange Server 2016 CU12', 'February 12, 2019', '15.1.1713.5 15.01.1713.005'),
|
|
|
|
Verstring('Exchange Server 2016 CU11', 'October 16, 2018', '15.1.1591.10', '15.01.1591.010'),
|
|
|
|
Verstring('Exchange Server 2016 CU10', 'June 19, 2018', '15.1.1531.3 15.01.1531.003'),
|
|
|
|
Verstring('Exchange Server 2016 CU9', 'March 20, 2018', '15.1.1466.3 15.01.1466.003'),
|
|
|
|
Verstring('Exchange Server 2016 CU8', 'December 19, 2017', '15.1.1415.2 15.01.1415.002'),
|
|
|
|
Verstring('Exchange Server 2016 CU7', 'September 19, 2017', '15.1.1261.35', '15.01.1261.035'),
|
|
|
|
Verstring('Exchange Server 2016 CU6', 'June 27, 2017', '15.1.1034.26', '15.01.1034.026'),
|
|
|
|
Verstring('Exchange Server 2016 CU5', 'March 21, 2017', '15.1.845.34 15.01.0845.034'),
|
|
|
|
Verstring('Exchange Server 2016 CU4', 'December 13, 2016', '15.1.669.32 15.01.0669.032'),
|
|
|
|
Verstring('Exchange Server 2016 CU3', 'September 20, 2016', '15.1.544.27 15.01.0544.027'),
|
|
|
|
Verstring('Exchange Server 2016 CU2', 'June 21, 2016', '15.1.466.34 15.01.0466.034'),
|
|
|
|
Verstring('Exchange Server 2016 CU1', 'March 15, 2016', '15.1.396.30 15.01.0396.030'),
|
|
|
|
Verstring('Exchange Server 2016 RTM', 'October 1, 2015', '15.1.225.42 15.01.0225.042'),
|
|
|
|
Verstring('Exchange Server 2016 Preview', 'July 22, 2015', '15.1.225.16 15.01.0225.016'),
|
|
|
|
Verstring('Exchange Server 2019 CU3', 'September 17, 2019', '15.2.464.5 15.02.0464.005'),
|
|
|
|
Verstring('Exchange Server 2019 CU2', 'June 18, 2019', '15.2.397.3 15.02.0397.003'),
|
|
|
|
Verstring('Exchange Server 2019 CU1', 'February 12, 2019', '15.2.330.5 15.02.0330.005'),
|
|
|
|
Verstring('Exchange Server 2019 RTM', 'October 22, 2018', '15.2.221.12 15.02.0221.012'),
|
2021-10-17 15:27:41 +02:00
|
|
|
Verstring('Exchange Server 2019 Preview', 'July 24, 2018', '15.2.196.0 15.02.0196.000'),
|
|
|
|
Verstring('Exchange Server 2019 CU11', 'October 12, 2021', '15.2.986.9'),
|
|
|
|
Verstring('Exchange Server 2019 CU11', 'September 28, 2021', '15.2.986.5'),
|
|
|
|
Verstring('Exchange Server 2019 CU10', 'October 12, 2021', '15.2.922.14'),
|
|
|
|
Verstring('Exchange Server 2019 CU10', 'July 13, 2021', '15.2.922.13'),
|
|
|
|
Verstring('Exchange Server 2019 CU10', 'June 29, 2021', '15.2.922.7'),
|
|
|
|
Verstring('Exchange Server 2019 CU9', 'July 13, 2021', '15.2.858.15'),
|
|
|
|
Verstring('Exchange Server 2019 CU9', 'May 11, 2021', '15.2.858.12'),
|
|
|
|
Verstring('Exchange Server 2019 CU9', 'April 13, 2021', '15.2.858.10'),
|
|
|
|
Verstring('Exchange Server 2019 CU9', 'March 16, 2021', '15.2.858.5'),
|
|
|
|
Verstring('Exchange Server 2019 CU8', 'May 11, 2021', '15.2.792.15'),
|
|
|
|
Verstring('Exchange Server 2019 CU8', 'April 13, 2021', '15.2.792.13'),
|
|
|
|
Verstring('Exchange Server 2019 CU8', 'March 2, 2021', '15.2.792.10'),
|
|
|
|
Verstring('Exchange Server 2019 CU8', 'December 15, 2020', '15.2.792.3'),
|
|
|
|
Verstring('Exchange Server 2019 CU7', 'March 2, 2021', '15.2.721.13'),
|
|
|
|
Verstring('Exchange Server 2019 CU7', 'September 15, 2020', '15.2.721.2'),
|
|
|
|
Verstring('Exchange Server 2019 CU6', 'March 2, 2021', '15.2.659.12'),
|
|
|
|
Verstring('Exchange Server 2019 CU6', 'June 16, 2020', '15.2.659.4'),
|
|
|
|
Verstring('Exchange Server 2019 CU5', 'March 2, 2021', '15.2.595.8'),
|
|
|
|
Verstring('Exchange Server 2019 CU5', 'March 17, 2020', '15.2.595.3'),
|
|
|
|
Verstring('Exchange Server 2019 CU4', 'March 2, 2021', '15.2.529.13'),
|
|
|
|
Verstring('Exchange Server 2019 CU4', 'December 17, 2019', '15.2.529.5'),
|
|
|
|
Verstring('Exchange Server 2019 CU3', 'March 2, 2021', '15.2.464.15')
|
2019-11-20 02:56:17 +01:00
|
|
|
)
|
|
|
|
|
2019-11-18 17:30:04 +01:00
|
|
|
def __init__(self, hostname):
|
|
|
|
self.socket = None
|
|
|
|
self.server_tls_params = None
|
|
|
|
self.hostname = hostname
|
|
|
|
self.port = None
|
|
|
|
self.reconnect = 0
|
|
|
|
self.results = {}
|
|
|
|
|
|
|
|
def disconnect(self):
|
|
|
|
if self.socket != None:
|
|
|
|
self.socket.close()
|
|
|
|
self.socket = None
|
|
|
|
|
|
|
|
def connect(self, host, port, _ssl = True):
|
|
|
|
try:
|
|
|
|
Logger.dbg(f"Attempting to reach {host}:{port}...")
|
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
|
|
if self.socket != None:
|
|
|
|
self.socket.close()
|
|
|
|
self.socket = None
|
|
|
|
|
|
|
|
sock.settimeout(config['timeout'])
|
|
|
|
if _ssl:
|
|
|
|
context = ssl.create_default_context()
|
|
|
|
|
|
|
|
# Allow unsecure ciphers like SSLv2 and SSLv3
|
|
|
|
context.options &= ~(ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3)
|
|
|
|
context.check_hostname = False
|
|
|
|
context.verify_mode = ssl.CERT_NONE
|
|
|
|
|
|
|
|
conn = context.wrap_socket(sock)
|
|
|
|
conn.connect((host, port))
|
|
|
|
self.socket = conn
|
|
|
|
|
|
|
|
self.server_tls_params = {
|
|
|
|
'cipher' : conn.cipher(),
|
|
|
|
'version': conn.version(),
|
|
|
|
'shared_ciphers': conn.shared_ciphers(),
|
|
|
|
'compression': conn.compression(),
|
|
|
|
'DER_peercert': conn.getpeercert(True),
|
|
|
|
'selected_alpn_protocol': conn.selected_alpn_protocol(),
|
|
|
|
'selected_npn_protocol': conn.selected_npn_protocol(),
|
|
|
|
}
|
|
|
|
|
|
|
|
x509 = crypto.load_certificate(crypto.FILETYPE_ASN1,self.server_tls_params['DER_peercert'])
|
|
|
|
|
|
|
|
out = ''
|
|
|
|
for elem in x509.get_subject().get_components():
|
|
|
|
out += f'\t{elem[0].decode()} = {elem[1].decode()}\n'
|
|
|
|
|
|
|
|
Logger.dbg(out)
|
|
|
|
self.results['SSL Certificate Subject components'] = out[1:-1]
|
|
|
|
|
|
|
|
else:
|
|
|
|
sock.connect((host, port))
|
|
|
|
self.socket = sock
|
|
|
|
|
|
|
|
Logger.dbg("Succeeded.")
|
|
|
|
self.reconnect = 0
|
|
|
|
return True
|
|
|
|
|
|
|
|
except (socket.gaierror,
|
|
|
|
socket.timeout,
|
|
|
|
ConnectionResetError) as e:
|
|
|
|
Logger.dbg(f"Failed.: {e}")
|
|
|
|
return False
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def recvall(the_socket, timeout = 1.0):
|
|
|
|
the_socket.setblocking(0)
|
|
|
|
total_data = []
|
|
|
|
data = ''
|
|
|
|
begin = time.time()
|
|
|
|
|
|
|
|
if not timeout:
|
|
|
|
timeout = 1
|
|
|
|
|
|
|
|
while 1:
|
|
|
|
if total_data and time.time() - begin > timeout:
|
|
|
|
break
|
|
|
|
|
|
|
|
elif time.time() - begin > timeout * 2:
|
|
|
|
break
|
|
|
|
wait = 0
|
|
|
|
try:
|
|
|
|
data = the_socket.recv(4096).decode()
|
|
|
|
if data:
|
|
|
|
total_data.append(data)
|
|
|
|
begin = time.time()
|
|
|
|
data = ''
|
|
|
|
wait = 0
|
|
|
|
else:
|
|
|
|
time.sleep(0.1)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
result = ''.join(total_data)
|
|
|
|
return result
|
|
|
|
|
|
|
|
def send(self, data, dontReconnect = False):
|
|
|
|
Logger.dbg(f"================= [SEND] =================\n{data}\n")
|
|
|
|
|
|
|
|
try:
|
|
|
|
self.socket.send(data.encode())
|
|
|
|
except Exception as e:
|
|
|
|
Logger.fail(f"Could not send data: {e}")
|
|
|
|
|
|
|
|
if not self.reconnect < ExchangeRecon.MAX_RECONNECTS and not dontReconnect:
|
|
|
|
self.reconnect += 1
|
|
|
|
Logger.dbg("Reconnecing...")
|
|
|
|
if self.connect(self.hostname, self.port):
|
|
|
|
return self.send(data, True)
|
|
|
|
else:
|
|
|
|
Logger.err("Could not reconnect with remote host. Failure.")
|
|
|
|
sys.exit(-1)
|
|
|
|
|
|
|
|
out = ExchangeRecon.recvall(self.socket, config['timeout'])
|
|
|
|
|
|
|
|
if not out and self.reconnect < ExchangeRecon.MAX_RECONNECTS and not dontReconnect:
|
|
|
|
Logger.dbg("No data returned. Reconnecting...")
|
|
|
|
self.reconnect += 1
|
|
|
|
if self.connect(self.hostname, self.port):
|
|
|
|
return self.send(data, True)
|
|
|
|
else:
|
|
|
|
Logger.err("Could not reconnect with remote host. Failure.")
|
|
|
|
sys.exit(-1)
|
|
|
|
|
|
|
|
Logger.dbg(f"================= [RECV] =================\n{out}\n")
|
|
|
|
return out
|
|
|
|
|
|
|
|
def http(self,
|
|
|
|
method = 'GET', url = '/', host = None,
|
|
|
|
httpver = 'HTTP/1.1', data = None, headers = None,
|
|
|
|
followRedirect = False, redirect = 0
|
|
|
|
):
|
|
|
|
hdrs = ExchangeRecon.HEADERS.copy()
|
|
|
|
if headers:
|
|
|
|
hdrs.update(headers)
|
|
|
|
|
|
|
|
if host:
|
|
|
|
hdrs['Host'] = host
|
|
|
|
|
|
|
|
headersstr = ''
|
|
|
|
for k, v in hdrs.items():
|
|
|
|
headersstr += f'{k}: {v}\r\n'
|
|
|
|
|
|
|
|
if data:
|
|
|
|
data = f'\r\n{data}'
|
|
|
|
else:
|
|
|
|
data = ''
|
|
|
|
|
|
|
|
packet = f'{method} {url} {httpver}\r\n{headersstr}{data}\r\n\r\n'
|
|
|
|
raw = self.send(packet)
|
|
|
|
resp = ExchangeRecon.response(raw)
|
|
|
|
|
|
|
|
if resp['code'] in [301, 302, 303] and followRedirect:
|
|
|
|
Logger.dbg(f'Following redirect. Depth: {redirect}...')
|
|
|
|
|
2022-04-13 16:42:31 +02:00
|
|
|
if 'location' in resp['headers'].keys():
|
|
|
|
location = urlparse(resp['headers']['location'])
|
|
|
|
port = 80 if location.scheme == 'http' else 443
|
|
|
|
host = location.netloc
|
|
|
|
if not host: host = self.hostname
|
|
|
|
if ':' in location.netloc:
|
|
|
|
port = int(location.netloc.split(':')[1])
|
|
|
|
host = location.netloc.split(':')[0]
|
|
|
|
|
|
|
|
if self.connect(host, port):
|
|
|
|
pos = resp['headers']['location'].find(location.path)
|
|
|
|
return self.http(
|
|
|
|
method = 'GET',
|
|
|
|
url = resp['headers']['location'][pos:],
|
|
|
|
host = host,
|
|
|
|
data = '',
|
|
|
|
headers = headers,
|
|
|
|
followRedirect = redirect < ExchangeRecon.MAX_REDIRECTS,
|
|
|
|
redirect = redirect + 1)
|
2019-11-18 17:30:04 +01:00
|
|
|
|
|
|
|
return resp, raw
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def response(data):
|
|
|
|
resp = {
|
|
|
|
'version' : '',
|
|
|
|
'code' : 0,
|
|
|
|
'message' : '',
|
|
|
|
'headers' : {},
|
|
|
|
'data' : ''
|
|
|
|
}
|
|
|
|
num = 0
|
|
|
|
parsed = 0
|
|
|
|
|
|
|
|
for line in data.split('\r\n'):
|
|
|
|
parsed += len(line) + 2
|
|
|
|
line = line.strip()
|
|
|
|
if not line:
|
|
|
|
break
|
|
|
|
|
|
|
|
if num == 0:
|
|
|
|
splitted = line.split(' ')
|
|
|
|
resp['version'] = splitted[0]
|
|
|
|
resp['code'] = int(splitted[1])
|
|
|
|
resp['message'] = ' '.join(splitted[2:])
|
|
|
|
num += 1
|
|
|
|
continue
|
|
|
|
|
|
|
|
num += 1
|
|
|
|
pos = line.find(':')
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
name = line[:pos]
|
2019-11-18 17:30:04 +01:00
|
|
|
val = line[pos+1:].strip()
|
|
|
|
|
|
|
|
if name in resp['headers'].keys():
|
|
|
|
if isinstance(resp['headers'][name], str):
|
|
|
|
old = resp['headers'][name]
|
|
|
|
resp['headers'][name] = [old]
|
|
|
|
|
|
|
|
if val not in resp['headers'][name]:
|
|
|
|
try:
|
|
|
|
resp['headers'][name].append(int(val))
|
|
|
|
except ValueError:
|
|
|
|
resp['headers'][name].append(val)
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
resp['headers'][name] = int(val)
|
|
|
|
except ValueError:
|
|
|
|
resp['headers'][name] = val
|
|
|
|
|
|
|
|
if parsed > 0 and parsed < len(data):
|
|
|
|
resp['data'] = data[parsed:]
|
|
|
|
|
|
|
|
if 'content-length' in resp['headers'].keys() and len(resp['data']) != resp['headers']['content-length']:
|
|
|
|
Logger.fail(f"Received data is not of declared by server length ({len(resp['data'])} / {resp['headers']['content-length']})!")
|
|
|
|
|
|
|
|
return resp
|
|
|
|
|
|
|
|
def inspect(self, resp):
|
2019-11-20 02:56:17 +01:00
|
|
|
global found_dns_domain
|
|
|
|
|
2019-11-18 17:30:04 +01:00
|
|
|
if resp['code'] == 0:
|
|
|
|
return
|
|
|
|
|
|
|
|
for k, v in resp['headers'].items():
|
|
|
|
vals = []
|
|
|
|
if isinstance(v, str):
|
|
|
|
vals.append(v)
|
|
|
|
elif isinstance(v, int):
|
|
|
|
vals.append(str(v))
|
|
|
|
else:
|
|
|
|
vals.extend(v)
|
|
|
|
lowervals = [x.lower() for x in vals]
|
2019-11-20 02:56:17 +01:00
|
|
|
kl = k.lower()
|
|
|
|
|
|
|
|
if kl == 'x-owa-version':
|
|
|
|
ver = ExchangeRecon.parseVersion(v)
|
|
|
|
if ver:
|
2022-03-01 12:25:13 +01:00
|
|
|
if ExchangeRecon.owaVersionInHttpHeader not in self.results.keys():
|
|
|
|
self.results[ExchangeRecon.owaVersionInHttpHeader] = ''
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
self.results[ExchangeRecon.owaVersionInHttpHeader] += '\n\t({})'.format(str(ver))
|
2019-11-18 17:30:04 +01:00
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
elif kl == 'www-authenticate':
|
2019-11-18 17:30:04 +01:00
|
|
|
realms = list(filter(lambda x: 'basic realm="' in x, lowervals))
|
|
|
|
if len(realms):
|
|
|
|
Logger.dbg(f"Got basic realm.: {str(realms)}")
|
|
|
|
|
|
|
|
m = re.search(r'([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})', realms[0])
|
|
|
|
if m:
|
2019-11-20 02:56:17 +01:00
|
|
|
self.results[ExchangeRecon.leakedInternalIp] = m.group(1)
|
2019-11-18 17:30:04 +01:00
|
|
|
|
|
|
|
negotiates = list(filter(lambda x: 'Negotiate ' in x, vals))
|
|
|
|
if len(negotiates):
|
|
|
|
val = negotiates[0][len('Negotiate '):]
|
|
|
|
Logger.dbg('NTLM Message hex dump:\n' + hexdump(base64.b64decode(val)))
|
|
|
|
parsed = NtlmParser().parse(val)
|
|
|
|
Logger.dbg(f"Parsed NTLM Message:\n{str(parsed)}")
|
|
|
|
|
|
|
|
foo = ''
|
2019-11-20 02:56:17 +01:00
|
|
|
for k2, v2 in parsed.items():
|
|
|
|
if isinstance(v2, str):
|
2019-11-18 17:30:04 +01:00
|
|
|
try:
|
2019-11-20 02:56:17 +01:00
|
|
|
foo += f'\t{k2}:\n\t\t{v2}\n'
|
2019-11-18 17:30:04 +01:00
|
|
|
except: pass
|
2019-11-20 02:56:17 +01:00
|
|
|
elif isinstance(v2, dict):
|
|
|
|
foo += f'\t{k2}:\n'
|
|
|
|
for k3, v3 in v2.items():
|
|
|
|
if k3 == 'DNS domain name':
|
|
|
|
found_dns_domain = v3
|
2019-11-18 17:30:04 +01:00
|
|
|
try:
|
2019-11-20 02:56:17 +01:00
|
|
|
foo += f"\t\t{k3: <18}:\t{v3}\n"
|
2019-11-18 17:30:04 +01:00
|
|
|
except: pass
|
2019-11-20 02:56:17 +01:00
|
|
|
elif isinstance(v2, list):
|
2019-11-18 17:30:04 +01:00
|
|
|
try:
|
2019-11-20 02:56:17 +01:00
|
|
|
foo += f'\t{k2}:\n\t\t- ' + '\n\t\t- '.join(v2) + '\n'
|
2019-11-18 17:30:04 +01:00
|
|
|
except: pass
|
2019-11-20 02:56:17 +01:00
|
|
|
self.results[ExchangeRecon.leakedInternalDomainNTLM] = foo[1:]
|
|
|
|
|
|
|
|
if kl == 'server':
|
|
|
|
self.results[ExchangeRecon.iisVersion] = vals[0]
|
2019-11-18 17:30:04 +01:00
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
if kl == 'x-aspnet-version':
|
|
|
|
self.results[ExchangeRecon.aspVersion] = vals[0]
|
2019-11-18 17:30:04 +01:00
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
if kl not in ExchangeRecon.usualHeaders:
|
|
|
|
l = f'{k}: {v}'
|
|
|
|
if ExchangeRecon.unusualHeaders not in self.results.keys() or \
|
|
|
|
l not in self.results[ExchangeRecon.unusualHeaders]:
|
|
|
|
Logger.info("Came across unusual HTTP header: " + l)
|
|
|
|
if ExchangeRecon.unusualHeaders not in self.results:
|
|
|
|
self.results[ExchangeRecon.unusualHeaders] = set()
|
|
|
|
self.results[ExchangeRecon.unusualHeaders].add(l)
|
2019-11-18 17:30:04 +01:00
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
for name, rex in ExchangeRecon.htmlregexes.items():
|
2019-11-18 17:30:04 +01:00
|
|
|
m = re.search(rex, resp['data'])
|
|
|
|
if m:
|
|
|
|
self.results[name] = m.group(1)
|
|
|
|
if 'Outlook Web App version leaked' in name:
|
|
|
|
ver = ExchangeRecon.parseVersion(m.group(1))
|
|
|
|
if ver:
|
2019-11-20 02:56:17 +01:00
|
|
|
self.results[name] += '\n\t({})'.format(str(ver))
|
2019-11-18 17:30:04 +01:00
|
|
|
|
|
|
|
@staticmethod
|
2019-11-20 02:56:17 +01:00
|
|
|
def parseVersion(lookup):
|
|
|
|
|
|
|
|
# Try strict matching
|
|
|
|
for ver in ExchangeRecon.exchangeVersions:
|
|
|
|
if ver.version == lookup:
|
|
|
|
return ver
|
2019-11-18 17:30:04 +01:00
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
lookupparsed = packaging.version.parse(lookup)
|
|
|
|
|
|
|
|
# Go with version-wise comparison to fuzzily find proper version name
|
|
|
|
sortedversions = sorted(ExchangeRecon.exchangeVersions)
|
|
|
|
|
|
|
|
for i in range(len(sortedversions)):
|
|
|
|
if sortedversions[i].version.startswith(lookup):
|
|
|
|
sortedversions[i].name = 'fuzzy match: ' + sortedversions[i].name
|
|
|
|
return sortedversions[i]
|
|
|
|
|
|
|
|
for i in range(len(sortedversions)):
|
|
|
|
prevver = packaging.version.parse('0.0')
|
|
|
|
nextver = packaging.version.parse('99999.0')
|
|
|
|
if i > 0:
|
|
|
|
prevver = packaging.version.parse(sortedversions[i-1].version)
|
|
|
|
thisver = packaging.version.parse(sortedversions[i].version)
|
|
|
|
if i + 1 < len(sortedversions):
|
|
|
|
nextver = packaging.version.parse(sortedversions[i+1].version)
|
|
|
|
|
|
|
|
if lookupparsed >= thisver and lookupparsed < nextver:
|
|
|
|
sortedversions[i].name = 'fuzzy match: ' + sortedversions[i].name
|
|
|
|
return sortedversions[i]
|
2019-11-18 17:30:04 +01:00
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def verifyExchange(self):
|
|
|
|
# Fetching these paths as unauthorized must result in 401
|
|
|
|
verificationPaths = (
|
|
|
|
# (path, redirect, sendHostHeader /* HTTP/1.1 */)
|
|
|
|
('/owa', True, True),
|
|
|
|
('/autodiscover/autodiscover.xml', True, False),
|
|
|
|
('/Microsoft-Server-ActiveSync', True, False),
|
|
|
|
('/EWS/Exchange.asmx', True, False),
|
|
|
|
('/ecp/?ExchClientVer=15', False, False),
|
|
|
|
)
|
|
|
|
|
|
|
|
definitiveMarks = (
|
|
|
|
'<title>Outlook Web App</title>',
|
|
|
|
'<!-- OwaPage = ASP.auth_logon_aspx -->',
|
|
|
|
'Set-Cookie: exchangecookie=',
|
|
|
|
'Set-Cookie: OutlookSession=',
|
|
|
|
'/owa/auth/logon.aspx?url=https://',
|
|
|
|
'{57A118C6-2DA9-419d-BE9A-F92B0F9A418B}',
|
2019-11-20 02:56:17 +01:00
|
|
|
'To use Outlook Web App, browser settings must allow scripts to run. For ' +\
|
|
|
|
'information about how to allow scripts'
|
2019-11-18 17:30:04 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
otherMarks = (
|
|
|
|
'Location: /owa/',
|
|
|
|
'Microsoft-IIS/',
|
|
|
|
'Negotiate TlRM',
|
|
|
|
'WWW-Authenticate: Negotiate',
|
|
|
|
'ASP.NET'
|
|
|
|
)
|
|
|
|
|
|
|
|
score = 0
|
|
|
|
definitive = False
|
|
|
|
|
|
|
|
for path, redirect, sendHostHeader in verificationPaths:
|
|
|
|
if not sendHostHeader:
|
|
|
|
resp, raw = self.http(url = path, httpver = 'HTTP/1.0', followRedirect = redirect)
|
|
|
|
else:
|
|
|
|
r = requests.get(f'https://{self.hostname}{path}', verify = False, allow_redirects = True)
|
|
|
|
resp = {
|
|
|
|
'version' : 'HTTP/1.1',
|
|
|
|
'code' : r.status_code,
|
|
|
|
'message' : r.reason,
|
|
|
|
'headers' : r.headers,
|
|
|
|
'data' : r.text
|
|
|
|
}
|
|
|
|
raw = r.text
|
|
|
|
|
|
|
|
Logger.info(f"Got HTTP Code={resp['code']} on access to ({path})")
|
|
|
|
|
|
|
|
if resp['code'] in [301, 302]:
|
|
|
|
loc = f'https://{self.hostname}/owa/auth/logon.aspx?url=https://{self.hostname}/owa/&reason=0'
|
|
|
|
if loc in raw:
|
|
|
|
definitive = True
|
|
|
|
score += 2
|
|
|
|
|
|
|
|
if resp['code'] == 401: score += 1
|
|
|
|
|
|
|
|
for mark in otherMarks:
|
|
|
|
if mark in str(raw):
|
|
|
|
score += 1
|
|
|
|
|
|
|
|
for mark in definitiveMarks:
|
|
|
|
if mark in str(raw):
|
|
|
|
score += 2
|
|
|
|
definitive = True
|
|
|
|
|
|
|
|
self.inspect(resp)
|
|
|
|
|
|
|
|
Logger.info(f"Exchange scored with: {score}. Definitively sure it's an Exchange? {definitive}")
|
|
|
|
return score > 15 or definitive
|
|
|
|
|
|
|
|
def tryToTriggerNtlmAuthentication(self):
|
|
|
|
verificationPaths = (
|
|
|
|
'/autodiscover/autodiscover.xml',
|
|
|
|
)
|
|
|
|
|
|
|
|
for path in verificationPaths:
|
|
|
|
auth = {
|
|
|
|
'Authorization': 'Negotiate TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==',
|
|
|
|
'X-Nego-Capability': 'Negotiate, Kerberos, NTLM',
|
|
|
|
'X-User-Identity': 'john.doe@example.com',
|
|
|
|
'Content-Length': '0',
|
|
|
|
}
|
|
|
|
resp, raw = self.http(method = 'POST', host = self.hostname, url = path, headers = auth)
|
|
|
|
self.inspect(resp)
|
|
|
|
|
|
|
|
def process(self):
|
|
|
|
for port in ExchangeRecon.COMMON_PORTS:
|
|
|
|
if self.connect(self.hostname, port):
|
|
|
|
self.port = port
|
|
|
|
Logger.ok(f"Connected with {self.hostname}:{port}\n")
|
|
|
|
break
|
|
|
|
|
|
|
|
if not self.port:
|
|
|
|
Logger.err(f"Could not contact {self.hostname}. Failure.\n")
|
|
|
|
return False
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
print("[.] Probing for Exchange fingerprints...")
|
2019-11-18 17:30:04 +01:00
|
|
|
if not self.verifyExchange():
|
|
|
|
Logger.err("Specified target hostname is not an Exchange server.")
|
|
|
|
return False
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
print("[.] Triggering NTLM authentication...")
|
2019-11-18 17:30:04 +01:00
|
|
|
self.tryToTriggerNtlmAuthentication()
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
print("[.] Probing support for legacy mail protocols and their capabilities...")
|
2019-11-18 17:30:04 +01:00
|
|
|
self.legacyMailFingerprint()
|
|
|
|
|
|
|
|
def legacyMailFingerprint(self):
|
|
|
|
self.socket.close()
|
|
|
|
self.socket = None
|
|
|
|
|
|
|
|
for port in (25, 465, 587):
|
|
|
|
try:
|
|
|
|
Logger.dbg(f"Trying smtp on port {port}...")
|
|
|
|
if self.smtpInteract(self.hostname, port, _ssl = False):
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
Logger.dbg(f"Trying smtp SSL on port {port}...")
|
2019-11-20 02:56:17 +01:00
|
|
|
if self.smtpInteract(self.hostname, port, _ssl = True):
|
|
|
|
break
|
2019-11-18 17:30:04 +01:00
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
Logger.dbg(f"Failed fetching SMTP replies: {e}")
|
|
|
|
raise
|
|
|
|
continue
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _smtpconnect(host, port, _ssl):
|
|
|
|
server = None
|
|
|
|
try:
|
|
|
|
if _ssl:
|
|
|
|
server = smtplib.SMTP_SSL(host = host, port = port,
|
|
|
|
local_hostname = 'smtp.gmail.com', timeout = config['timeout'])
|
|
|
|
else:
|
|
|
|
server = smtplib.SMTP(host = host, port = port,
|
|
|
|
local_hostname = 'smtp.gmail.com', timeout = config['timeout'])
|
|
|
|
|
|
|
|
if config['debug']:
|
|
|
|
server.set_debuglevel(True)
|
|
|
|
|
|
|
|
return server
|
|
|
|
except Exception as e:
|
|
|
|
Logger.dbg(f"Could not connect to SMTP server on SSL={_ssl} port={port}. Error: {e}")
|
|
|
|
return None
|
|
|
|
|
|
|
|
def smtpInteract(self, host, port, _ssl):
|
|
|
|
server = ExchangeRecon._smtpconnect(host, port, _ssl)
|
|
|
|
if not server:
|
|
|
|
return None
|
|
|
|
|
|
|
|
capabilities = []
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
|
|
sock.connect((host, port))
|
|
|
|
banner = ExchangeRecon.recvall(sock)
|
|
|
|
Logger.info(f"SMTP server returned following banner:\n\t{banner}")
|
|
|
|
capabilities.append(banner.strip())
|
|
|
|
|
2019-11-18 17:30:04 +01:00
|
|
|
try:
|
|
|
|
code, msg = server.ehlo()
|
|
|
|
except Exception:
|
|
|
|
server = ExchangeRecon._smtpconnect(host, port, _ssl)
|
|
|
|
if not server:
|
2022-04-13 16:42:31 +02:00
|
|
|
Logger.info('Could not interact with SMTP.')
|
2019-11-18 17:30:04 +01:00
|
|
|
return None
|
|
|
|
code, msg = server.ehlo()
|
|
|
|
|
|
|
|
msg = msg.decode()
|
|
|
|
for line in msg.split('\n'):
|
|
|
|
capabilities.append(line.strip())
|
|
|
|
|
|
|
|
try:
|
|
|
|
server.starttls()
|
|
|
|
code, msg = server.ehlo()
|
|
|
|
except Exception:
|
|
|
|
server = ExchangeRecon._smtpconnect(host, port, _ssl)
|
|
|
|
if not server:
|
|
|
|
return None
|
|
|
|
server.ehlo()
|
|
|
|
server.starttls()
|
|
|
|
code, msg = server.ehlo()
|
|
|
|
|
|
|
|
msg = msg.decode()
|
|
|
|
Logger.info(f"SMTP server banner & capabilities:\n-------\n{msg}\n-------\n")
|
|
|
|
for line in msg.split('\n'):
|
|
|
|
capabilities.append(line.strip())
|
|
|
|
|
|
|
|
try:
|
|
|
|
msg = server.help()
|
|
|
|
except Exception:
|
|
|
|
server = ExchangeRecon._smtpconnect(host, port, _ssl)
|
|
|
|
if not server:
|
|
|
|
return None
|
|
|
|
server.ehlo()
|
|
|
|
try:
|
|
|
|
server.starttls()
|
|
|
|
server.ehlo()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
msg = server.help()
|
|
|
|
|
|
|
|
msg = msg.decode()
|
|
|
|
for line in msg.split('\n'):
|
|
|
|
capabilities.append(line.strip())
|
|
|
|
|
|
|
|
skipThese = (
|
|
|
|
'8BITMIME',
|
|
|
|
'STARTTLS',
|
|
|
|
'PIPELINING',
|
2019-11-20 02:56:17 +01:00
|
|
|
'AUTH',
|
2019-11-18 17:30:04 +01:00
|
|
|
'CHUNKING',
|
|
|
|
'SIZE ',
|
|
|
|
'ENHANCEDSTATUSCODES',
|
|
|
|
'SMTPUTF8',
|
|
|
|
'DSN',
|
|
|
|
'BINARYMIME',
|
|
|
|
'HELP',
|
|
|
|
'QUIT',
|
|
|
|
'DATA',
|
|
|
|
'EHLO',
|
|
|
|
'HELO',
|
2019-11-20 02:56:17 +01:00
|
|
|
#'GSSAPI',
|
|
|
|
#'X-EXPS',
|
|
|
|
#'X-ANONYMOUSTLS',
|
2019-11-20 00:32:33 +01:00
|
|
|
'This server supports the following commands'
|
2019-11-18 17:30:04 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
unfiltered = set()
|
|
|
|
for line in capabilities:
|
|
|
|
skip = False
|
|
|
|
for n in skipThese:
|
|
|
|
if n in line:
|
|
|
|
skip = True
|
|
|
|
break
|
|
|
|
if not skip:
|
|
|
|
unfiltered.add(line)
|
|
|
|
|
|
|
|
|
|
|
|
if len(unfiltered):
|
2019-11-20 02:56:17 +01:00
|
|
|
self.results[ExchangeRecon.legacyMailCapabilities] = \
|
|
|
|
'\t- ' + '\n\t- '.join(unfiltered)
|
2019-11-18 17:30:04 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
server.quit()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
self.verifyEnumerationOpportunities(host, port, _ssl)
|
2019-11-20 02:56:17 +01:00
|
|
|
return True
|
2019-11-18 17:30:04 +01:00
|
|
|
|
|
|
|
def verifyEnumerationOpportunities(self, host, port, _ssl):
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
def _reconnect(host, port, _ssl):
|
|
|
|
server = ExchangeRecon._smtpconnect(host, port, _ssl)
|
|
|
|
server.ehlo()
|
|
|
|
try:
|
|
|
|
server.starttls()
|
|
|
|
server.ehlo()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
return server
|
|
|
|
|
2019-11-18 17:30:04 +01:00
|
|
|
Logger.info("Examining potential methods for SMTP user enumeration...")
|
|
|
|
server = ExchangeRecon._smtpconnect(host, port, _ssl)
|
|
|
|
if not server:
|
|
|
|
return None
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
ip = '[{}]'.format(socket.gethostbyname(self.hostname))
|
|
|
|
if found_dns_domain:
|
|
|
|
ip = found_dns_domain
|
|
|
|
|
2019-11-18 17:30:04 +01:00
|
|
|
techniques = {
|
|
|
|
f'VRFY root' : None,
|
|
|
|
f'EXPN root' : None,
|
2019-11-20 02:56:17 +01:00
|
|
|
f'MAIL FROM:<test@{ip}>' : None,
|
|
|
|
f'RCPT TO:<test@{ip}>' : None,
|
2019-11-18 17:30:04 +01:00
|
|
|
}
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
server = _reconnect(host, port, _ssl)
|
|
|
|
|
2019-11-18 17:30:04 +01:00
|
|
|
likely = 0
|
|
|
|
for data in techniques.keys():
|
|
|
|
for i in range(3):
|
|
|
|
try:
|
|
|
|
code, msg = server.docmd(data)
|
|
|
|
msg = msg.decode()
|
|
|
|
techniques[data] = f'({code}, "{msg}")'
|
|
|
|
|
|
|
|
Logger.dbg(f"Attempted user enumeration using: ({data}). Result: {techniques[data]}")
|
|
|
|
|
|
|
|
if code >= 200 and code <= 299:
|
|
|
|
Logger.ok(f"Method {data} may allow SMTP user enumeration.")
|
|
|
|
likely += 1
|
|
|
|
else:
|
|
|
|
Logger.fail(f"Method {data} is unlikely to allow SMTP user enumeration.")
|
|
|
|
break
|
|
|
|
except Exception as e:
|
|
|
|
Logger.dbg(f"Exception occured during SMTP User enumeration attempt: {e}")
|
2019-11-20 02:56:17 +01:00
|
|
|
server.quit()
|
|
|
|
server = _reconnect(host, port, _ssl)
|
2019-11-18 17:30:04 +01:00
|
|
|
continue
|
|
|
|
|
|
|
|
out = ''
|
|
|
|
for k, v in techniques.items():
|
|
|
|
code = eval(v)[0]
|
|
|
|
c = '?'
|
|
|
|
if code >= 200 and code <= 299: c = '+'
|
|
|
|
if code >= 500 and code <= 599: c = '-'
|
|
|
|
|
2019-11-20 02:56:17 +01:00
|
|
|
out += f'\n\t- [{c}] {k: <40} returned: {v}'
|
2019-11-18 17:30:04 +01:00
|
|
|
|
|
|
|
self.results["Results for SMTP User Enumeration attempts"] = out[2:]
|
|
|
|
|
|
|
|
def parseOptions(argv):
|
|
|
|
global config
|
|
|
|
|
|
|
|
print('''
|
2019-11-20 02:56:17 +01:00
|
|
|
:: Exchange Fingerprinter
|
2019-11-18 17:30:04 +01:00
|
|
|
Tries to obtain internal IP address, Domain name and other clues by talking to Exchange
|
2021-10-24 23:11:42 +02:00
|
|
|
Mariusz Banach / mgeeky '19, <mb@binary-offensive.com>
|
2019-11-18 17:30:04 +01:00
|
|
|
v{}
|
|
|
|
'''.format(VERSION))
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(prog = argv[0], usage='%(prog)s [options] <hostname>')
|
2019-11-20 02:56:17 +01:00
|
|
|
parser.add_argument('hostname', metavar='<domain|ip>', type=str,
|
|
|
|
help='Hostname of the Exchange server (or IP address).')
|
2019-11-18 17:30:04 +01:00
|
|
|
|
|
|
|
parser.add_argument('-v', '--verbose', action='store_true', help='Display verbose output.')
|
|
|
|
parser.add_argument('-d', '--debug', action='store_true', help='Display debug output.')
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
if not 'hostname' in args:
|
|
|
|
Logger.err('You must specify a hostname to launch!')
|
|
|
|
return False
|
|
|
|
|
|
|
|
config['verbose'] = args.verbose
|
|
|
|
config['debug'] = args.debug
|
|
|
|
|
|
|
|
return args
|
|
|
|
|
|
|
|
def output(hostname, out):
|
|
|
|
print("\n======[ Leaked clues about internal environment ]======\n")
|
|
|
|
print(f"\nHostname: {hostname}\n")
|
|
|
|
|
|
|
|
for k, v in out.items():
|
2019-11-20 02:56:17 +01:00
|
|
|
if not v: continue
|
|
|
|
if isinstance(v, str):
|
|
|
|
print(f"*) {k}:\n\t{v.strip()}\n")
|
|
|
|
elif isinstance(v, list) or isinstance(v, set):
|
|
|
|
v2 = '\n\t- '.join(v)
|
|
|
|
print(f"*) {k}:\n\t- {v2}\n")
|
2019-11-18 17:30:04 +01:00
|
|
|
|
|
|
|
def main(argv):
|
|
|
|
opts = parseOptions(argv)
|
|
|
|
if not opts:
|
|
|
|
Logger.err('Options parsing failed.')
|
|
|
|
return False
|
|
|
|
|
|
|
|
recon = ExchangeRecon(opts.hostname)
|
|
|
|
|
|
|
|
try:
|
|
|
|
t = threading.Thread(target = recon.process)
|
|
|
|
t.setDaemon(True)
|
|
|
|
t.start()
|
|
|
|
|
|
|
|
while t.is_alive():
|
|
|
|
t.join(3.0)
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
Logger.fail("Interrupted by user.")
|
|
|
|
|
|
|
|
if len(recon.results) > 1:
|
|
|
|
output(opts.hostname, recon.results)
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main(sys.argv)
|