#!/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. # # TODO: # - introduce some fuzzy logic for Exchange version recognition # - Extend SMTP User Enumeration validation capabilities # # Requirements: # - pyOpenSSL # - packaging # # Author: # Mariusz B. / mgeeky, '19, <mb@binary-offensive.com> # import re import sys import ssl import time import base64 import struct import string import socket import smtplib import urllib3 import requests import argparse import threading import collections import packaging.version from urllib.parse import urlparse import OpenSSL.crypto as crypto VERSION = '0.2' config = { 'debug' : False, 'verbose' : False, 'timeout' : 6.0, } found_dns_domain = '' 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: self.string = self.raw.decode('utf-16') except: self.string = ''.join(filter(lambda x: str(x) != str('\0'), self.raw)) 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 = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0', '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', } 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'), Verstring('Exchange Server 2019 Preview', 'July 24, 2018', '15.2.196.0 15.02.0196.000') ) 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}...') 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) 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(':') name = line[:pos] 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): global found_dns_domain 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] kl = k.lower() if kl == 'x-owa-version': ver = ExchangeRecon.parseVersion(v) if ver: self.results[ExchangeRecon.owaVersionInHttpHeader] += '\n\t({})'.format(str(ver)) elif kl == 'www-authenticate': 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: self.results[ExchangeRecon.leakedInternalIp] = m.group(1) 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 = '' for k2, v2 in parsed.items(): if isinstance(v2, str): try: foo += f'\t{k2}:\n\t\t{v2}\n' except: pass elif isinstance(v2, dict): foo += f'\t{k2}:\n' for k3, v3 in v2.items(): if k3 == 'DNS domain name': found_dns_domain = v3 try: foo += f"\t\t{k3: <18}:\t{v3}\n" except: pass elif isinstance(v2, list): try: foo += f'\t{k2}:\n\t\t- ' + '\n\t\t- '.join(v2) + '\n' except: pass self.results[ExchangeRecon.leakedInternalDomainNTLM] = foo[1:] if kl == 'server': self.results[ExchangeRecon.iisVersion] = vals[0] if kl == 'x-aspnet-version': self.results[ExchangeRecon.aspVersion] = vals[0] 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) for name, rex in ExchangeRecon.htmlregexes.items(): 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: self.results[name] += '\n\t({})'.format(str(ver)) @staticmethod def parseVersion(lookup): # Try strict matching for ver in ExchangeRecon.exchangeVersions: if ver.version == lookup: return ver 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] 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}', 'To use Outlook Web App, browser settings must allow scripts to run. For ' +\ 'information about how to allow scripts' ) 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 print("[.] Probing for Exchange fingerprints...") if not self.verifyExchange(): Logger.err("Specified target hostname is not an Exchange server.") return False print("[.] Triggering NTLM authentication...") self.tryToTriggerNtlmAuthentication() print("[.] Probing support for legacy mail protocols and their capabilities...") 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}...") if self.smtpInteract(self.hostname, port, _ssl = True): break 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 = [] 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()) try: code, msg = server.ehlo() except Exception: server = ExchangeRecon._smtpconnect(host, port, _ssl) if not server: 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', 'AUTH', 'CHUNKING', 'SIZE ', 'ENHANCEDSTATUSCODES', 'SMTPUTF8', 'DSN', 'BINARYMIME', 'HELP', 'QUIT', 'DATA', 'EHLO', 'HELO', #'GSSAPI', #'X-EXPS', #'X-ANONYMOUSTLS', 'This server supports the following commands' ) 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): self.results[ExchangeRecon.legacyMailCapabilities] = \ '\t- ' + '\n\t- '.join(unfiltered) try: server.quit() except: pass self.verifyEnumerationOpportunities(host, port, _ssl) return True def verifyEnumerationOpportunities(self, host, port, _ssl): def _reconnect(host, port, _ssl): server = ExchangeRecon._smtpconnect(host, port, _ssl) server.ehlo() try: server.starttls() server.ehlo() except: pass return server Logger.info("Examining potential methods for SMTP user enumeration...") server = ExchangeRecon._smtpconnect(host, port, _ssl) if not server: return None ip = '[{}]'.format(socket.gethostbyname(self.hostname)) if found_dns_domain: ip = found_dns_domain techniques = { f'VRFY root' : None, f'EXPN root' : None, f'MAIL FROM:<test@{ip}>' : None, f'RCPT TO:<test@{ip}>' : None, } server = _reconnect(host, port, _ssl) 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}") server.quit() server = _reconnect(host, port, _ssl) 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 = '-' out += f'\n\t- [{c}] {k: <40} returned: {v}' self.results["Results for SMTP User Enumeration attempts"] = out[2:] def parseOptions(argv): global config print(''' :: Exchange Fingerprinter Tries to obtain internal IP address, Domain name and other clues by talking to Exchange Mariusz B. / mgeeky '19, <mb@binary-offensive.com> v{} '''.format(VERSION)) parser = argparse.ArgumentParser(prog = argv[0], usage='%(prog)s [options] <hostname>') parser.add_argument('hostname', metavar='<domain|ip>', type=str, help='Hostname of the Exchange server (or IP address).') 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(): 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") 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)