Detecting Abuse of SSPR and Voluntary Password Resets in Azure.
Kloudynet Technologies
Experts in Cloud Governance | Cybersecurity | Compliance | SOC | MDR | Microsoft's Advanced Security Solutions Partner
Summary:
In this article, we explore how sophisticated adversaries, such as well-funded or well-equipped Advanced Persistent Threat (APT) groups, abuse Azure's Self-Service Password Reset (SSPR) service in tandem with SIM swapping to compromise users in Azure cloud environments.
Additionally, we detail detection methods for identifying this abuse at each stage of the attack lifecycle using KQL queries and mitigation steps. We also explore another potential vector: leveraging leaked credentials in conjunction with SIM swapping to compromise user accounts and reset the user's password. Furthermore, we provide insight into how the password reset process operates and methods for identifying various password reset activities from AuditLogs.
Background:
Before we dive into the actual method of attack, let's first understand how password reset works in Azure. In Azure, a user's password can be reset using three methods. Even though we categorize them into three methods, the concept of SSPR and Voluntary Password Reset shares the same goal: allowing the user to reset their password themselves.
Admin Password Reset
The traditional method involves administrators resetting a user's password in scenarios where the password is forgotten, the user is locked out of a device, or the user never received a password initially. This is typically initiated either through a helpdesk ticket or by directly contacting an administrator for assistance.
Voluntary Password Reset:
In this method, a user can change their password on their own if they successfully log into the My accounts portal. Furthermore, they can change their password using the "Change password" feature from the portal, as shown.
Also, users can update their password from the My Sign-ins Security Info Page. Based on your company’s authentication method policies, if you sign in with a password and multi-factor authentication to My Security Info, you will be able to update your password without entering your current password.
Note: This method only works when the user isn't facing account lockout issues or other sign-in problems. Hence, we can say it's more of a password update (change password) feature rather than a password reset feature.
Self-service password reset (SSPR):
This method surpasses the previous methods. Microsoft Entra self-service password reset (SSPR) gives users the ability to change or reset their password, with no administrator or help desk involvement. If a user's account is locked or they forget their password, they can follow prompts to unblock themselves and get back to work. This ability reduces help desk calls and loss of productivity when a user can't sign in to their device or an application. You can also access this page from the "Forgot password" option on any Azure application sign-in page, such as Azure Portal, Office 365, OneDrive, and Outlook login pages.
Here is a rough, high-level overview of how the SSPR (Self-Service Password Reset) password reset flow works:?
User Initiates Password Reset: The user clicks on the "Forgot Password" link on the login page and verifies his e-mail.
Identity Verification: Once the user's email is verified the system prompts the user to verify their identity using one or more of the configured auth methods (e.g., receiving a code via SMS or email or via Authenticator app notification etc).?
Reset Password: Once the user’s identity is verified, they are allowed to create a new password.?
Password Update: The new password is updated in the system, and the user can log in with the new credentials.?
Now, let's see how to identify each of the above mentioned methods in the AuditLogs table.
Detect reset methods from AuditLogs
Password Reset by admin:
Firstly, an admin password reset is uniquely identified by the "Reset password (by admin)" operation in the logs. Here's the KQL query to find password reset operations performed by admins on behalf of a user:
AuditLogs
| where TimeGenerated >ago(1d)
//| where LoggedByService=~"Self-service Password Management"
| where OperationName == "Reset password (by admin)"
| where Result == "success"
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| extend Initiatedby=InitiatedBy.user.userPrincipalName
| extend IpAddress=tostring(InitiatedBy.user.ipAddress)
| project TimeGenerated, OperationName, Initiatedby,TargetUser,IpAddress, AdditionalDetails,Result, CorrelationId
Voluntary, or forced (due to expiry) password change.
Our next method is the voluntary method, which generates the "Change password (self-service)" operation in the logs. Interestingly, this operation name is also generated for other scenarios, such as...
AuditLogs
| where TimeGenerated >ago(1d)
| where LoggedByService=="Self-service Password Management"
| where OperationName=="Change password (self-service)"
| where Result == "success"
| extend TargetUser=TargetResources[0].userPrincipalName
| extend Actor=InitiatedBy.user.userPrincipalName
| extend IpAddress=tostring(InitiatedBy.user.ipAddress)
| project TimeGenerated, OperationName,ActivityDisplayName, Actor,IpAddress, TargetUser, LoggedByService, Result, ResultDescription, CorrelationId, AdditionalDetails
Self Service Password Reset(SSPR)
Finally, our SSPR password reset, which we focus on more in this article, is uniquely identified by the "Reset password (self-service)" operation in the logs.
AuditLogs
| where TimeGenerated >ago(90d)
//| where LoggedByService=="Self-service Password Management"
| where OperationName contains "Reset password (self-service)"
| where ResultDescription=="Successfully completed reset."
| where Result=="success"
| extend User=InitiatedBy.user.userPrincipalName
(Or)
AuditLogs
| where TimeGenerated >ago(90d)
//| where LoggedByService=="Self-service Password Management"
| where OperationName=="Self-service password reset flow activity progress"
| where ResultDescription=="User successfully reset password"
| where Result=="success"
| extend User=InitiatedBy.user.userPrincipalName
Alright, enough of the basics. Let's dive into the actual attack vector of abusing the SSPR password reset flow along with SIM swapping.
Compromising User Accounts: Abusing SSPR Flow with SIM Swapping
This technique primarily abuses the SSPR flow method, which was discovered by the Obsidian threat research team as part of their SaaS Investigations.
The attack sequence typically unfolds as follows:?
Reconnaissance:?
SIM Swapping:
Account Compromise
MFA Manipulation
Detection
Now, lets see how to detect this attack method in each phase of the above-mentioned attack lifecycle.
Reconnaissance:
Firstly, during the reconnaissance phase, attackers will attempt to verify how many verification methods the SSPR flow requires to perform the password reset of a user account. To do this, attackers may stop at the verification stage(Identity verification stage mentioned in previous sspr flow) without completing verification steps and the entire SSPR flow. This page clearly indicates whether one or two verification methods are required, making it easier for attackers to identify and select vulnerable target accounts.
Here's a KQL query to detect SSPR reconnaissance activities where attackers stop at the verification options page without completing the entire process:
AuditLogs
| where TimeGenerated >ago(1d)
| where LoggedByService=="Self-service Password Management"
| where OperationName=="Self-service password reset flow activity progress"
| where ResultDescription=="User was presented with verification options"
//where ResultDescription=="User cancelled before passing the required authentication methods" //optional also attacker can close browser tab instead of cancelling it. So not accurate but a worthwhile option.
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| extend Initiatedby=tostring(InitiatedBy.user.userPrincipalName)
| extend IpAddress=tostring(InitiatedBy.user.ipAddress)
| join kind=leftanti (
AuditLogs
| where TimeGenerated >ago(1d)
| where LoggedByService=="Self-service Password Management"
| where OperationName=="Self-service password reset flow activity progress"
| where ResultDescription has_any ("User started the","verification option")) on CorrelationId
| project TimeGenerated, OperationName, Initiatedby,TargetUser,IpAddress, AdditionalDetails,Result, CorrelationId
A sudden increase in such activities, targeting multiple accounts within the organization, is a significant detection indicator of reconnaisance.
let threshold=3;
let reconSSPR=AuditLogs
| where TimeGenerated >ago(1d)
| where LoggedByService=="Self-service Password Management"
| where OperationName=="Self-service password reset flow activity progress"
| where ResultDescription=="User was presented with verification options"
//where ResultDescription=="User cancelled before passing the required authentication methods" //optional also attacker can close browser tab instead of cancelling it. So not accurate but a worthwhile option.
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| extend Initiatedby=tostring(InitiatedBy.user.userPrincipalName)
| extend IpAddress=tostring(InitiatedBy.user.ipAddress)
| join kind=leftanti (
AuditLogs
| where TimeGenerated >ago(1d)
| where LoggedByService=="Self-service Password Management"
| where OperationName=="Self-service password reset flow activity progress"
| where ResultDescription has_any ("User started the","verification option")) on CorrelationId
| project TimeGenerated, OperationName, Initiatedby,TargetUser,IpAddress, AdditionalDetails,Result, CorrelationId;
reconSSPR
| summarize Count=make_set(TargetUser)
| where array_length(Count)>threshold
KQL query to detect incomplete SSPR flow initiations from TOR IPs:
let TorExitNodes=externaldata(ipAddress:string)[
"https://check.torproject.org/torbulkexitlist"];
AuditLogs
| where TimeGenerated >ago(1d)
| where LoggedByService=="Self-service Password Management"
| where OperationName=="Self-service password reset flow activity progress"
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| extend Initiatedby=tostring(InitiatedBy.user.userPrincipalName)
| extend IpAddress=tostring(InitiatedBy.user.ipAddress)
//| project TimeGenerated, OperationName, Initiatedby,TargetUser,IpAddress, AdditionalDetails,Result, CorrelationId, ResultDescription
| summarize Result=make_list(ResultDescription),InitiatedUsers=make_list(Initiatedby) by TargetUser, IpAddress, CorrelationId
| where Result !has "User successfully reset password"
| where IpAddress in (TorExitNodes)
Compromise:
Query for detecting SSPR flows completed via SMS or phone call options from rare IP.
If a SSPR flow is completed via SMS or phone call options from a rare and suspicious IP, it may indicate a potential SIM Swap attack that was then used to perform SSPR.
Query for detecting SSPR flows completed via SMS or phone call options from rare IP.
AuditLogs
| where TimeGenerated >ago(90d)
| where LoggedByService=="Self-service Password Management"
| where OperationName=="Self-service password reset flow activity progress"
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| extend Initiatedby=tostring(InitiatedBy.user.userPrincipalName)
| extend IPAddress=tostring(InitiatedBy.user.ipAddress)
//Only Verifications should present
// User started the mobile SMS verification option
// User completed the mobile SMS verification option
// User completed the mobile voice call verification option
// User started the mobile voice call verification option
| summarize StartTime=min(TimeGenerated),EndTime=max(TimeGenerated),SSPRFlowEvents=make_set(ResultReason),count() by CorrelationId, TargetUser,IPAddress
| where SSPRFlowEvents has_any ("User successfully reset password","Successfully completed reset") //Successfull password reset.
//Security Questions
// User started the security questions verification option
// User completed the security questions verification option
//Email-Verification
// User started the email verification option
// User completed the email verification option
//Authenticator App
// User started the mobile app notification verification option
// User completed the mobile app notification verification option
// User started the mobile app code verification option
// User started the mobile app notification verification option
| where not(SSPRFlowEvents has_any ("mobile app notification","mobile app code verification","email verification option","security questions verification option"))
| join kind=leftanti (
SigninLogs
| where TimeGenerated > ago(90d)
| where ResultType == 0 )
on IPAddress
Query for detecting SSPR flows completed via SMS or phone call options from TOR IP.
let TorExitNodes=externaldata(ipAddress:string)[
"https://check.torproject.org/torbulkexitlist"];
AuditLogs
| where TimeGenerated >ago(90d)
| where LoggedByService=="Self-service Password Management"
| where OperationName=="Self-service password reset flow activity progress"
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| extend Initiatedby=tostring(InitiatedBy.user.userPrincipalName)
| extend IPAddress=tostring(InitiatedBy.user.ipAddress)
| where IPAddress in (TorExitNodes)
//Only Verifications should present
// User started the mobile SMS verification option
// User completed the mobile SMS verification option
// User completed the mobile voice call verification option
// User started the mobile voice call verification option
| summarize StartTime=min(TimeGenerated),EndTime=max(TimeGenerated),SSPRFlowEvents=make_set(ResultReason),count() by CorrelationId, TargetUser,IPAddress
| where SSPRFlowEvents has_any ("User successfully reset password","Successfully completed reset") //Successfull password reset.
//Security Questions
// User started the security questions verification option
// User completed the security questions verification option
//Email-Verification
// User started the email verification option
// User completed the email verification option
//Authenticator App
// User started the mobile app notification verification option
// User completed the mobile app notification verification option
// User started the mobile app code verification option
// User started the mobile app notification verification option
| where not(SSPRFlowEvents has_any ("mobile app notification","mobile app code verification","email verification option","security questions verification option"))
Multiple user password resets via SSPR method performed from a single IP address.
The query below identifies multiple user password resets via the Self-Service Password Reset (SSPR) method performed from a single IP address.
Expected FPs:
领英推荐
let threshold=2;
AuditLogs
| where TimeGenerated >ago(90d)
| where LoggedByService=="Self-service Password Management"
| where OperationName=="Reset password (self-service)"
| where ResultDescription=="Successfully completed reset."
| where Result=="success"
| extend TargetUser=tostring(TargetResources[0].userPrincipalName)
| extend Actor=tostring(InitiatedBy.user.userPrincipalName)
| extend IpAddress=tostring(InitiatedBy.user.ipAddress)
| extend User=InitiatedBy.user.userPrincipalName
| summarize min(TimeGenerated),max(TimeGenerated),UserList=make_set(TargetUser), count() by IpAddress
| where array_length(UserList)>threshold
Post-Compromise - Persistence:
As mentioned above attackers update or create new MFA methods of thier own for persistence.
KQL query for detecting MFA registration or MFA update followed by SSPR password reset.
AuditLogs
| where TimeGenerated >ago(1d)
| where OperationName =~"Reset password (self-service)"
| where ResultDescription=="Successfully completed reset."
| where Result=="success"
| extend InitiatedUser=tostring(InitiatedBy.user.userPrincipalName)
| extend TargetUser=tostring(TargetResources[0].userPrincipalName)
| project ResetTime=TimeGenerated, InitiatedUser, TargetUser, OperationName, Result
| join kind=inner (
AuditLogs
| where TimeGenerated >ago(1d)
| where OperationName in~ ("User registered all required security info","User registered security info","Admin registered security info","User changed default security info","Admin updated security info","User updated security info")
//| where Result=="success"
| extend InitiatedUser=tostring(InitiatedBy.user.userPrincipalName)
| extend TargetUser=tostring(TargetResources[0].userPrincipalName)
| project ["MFARegistration/Update Time"]=TimeGenerated, InitiatedUser, TargetUser, Result, OperationName) on TargetUser
| where ['MFARegistration/Update Time']>ResetTime
| extend ['Reset to MFA change TimeGap']=datetime_diff('minute',["MFARegistration/Update Time"],ResetTime)
Post-Compromise - Removing MFA methods:
As mentioned earlier, attackers may delete existing MFA methods to inhibit legitimate user access.
MITRE Technique: T1531- Account Access Removal
KQL query for detecting MFA method deletion followed by password reset.
AuditLogs
| where TimeGenerated >ago(1d)
| where OperationName =~"Reset password (self-service)"
| where ResultDescription=="Successfully completed reset."
| where Result=="success"
| extend InitiatedUser=tostring(InitiatedBy.user.userPrincipalName)
| extend TargetUser=tostring(TargetResources[0].userPrincipalName)
| project ResetTime=TimeGenerated, InitiatedUser, TargetUser, OperationName, Result
| join kind=inner (
AuditLogs
| where TimeGenerated >ago(1d)
| where OperationName in~ ("Admin deleted security info","User deleted security info")
//| where Result=="success"
| extend InitiatedUser=tostring(InitiatedBy.user.userPrincipalName)
| extend TargetUser=tostring(TargetResources[0].userPrincipalName)
| project ['MFA Method Deletion Time']=TimeGenerated, InitiatedUser, TargetUser, Result, OperationName) on TargetUser
| where ['MFA Method Deletion Time']>ResetTime
| extend ['Reset to MFA deletion TimeGap']=datetime_diff('minute',['MFA Method Deletion Time'],ResetTime)
If the attacker compromises an administrator account with permissions to delete, update, or add other users' MFA methods, they can make changes to MFA settings for multiple users. The below query finds the same.
let threshold=2;
AuditLogs
| where TimeGenerated >ago(90d)
| where OperationName has_any ("Admin deleted security info","Admin updated security info","Admin registered security info")
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| extend Initiatedby=tostring(InitiatedBy.user.userPrincipalName)
| extend IPAddress=tostring(InitiatedBy.user.ipAddress)
| where Result=="success"
| project TimeGenerated, OperationName, Result, ResultReason,Initiatedby, TargetUser, IPAddress
| summarize min(TimeGenerated), ResultReason=make_list(ResultReason), TargetUser=make_set(TargetUser), IpAddress=make_list(IPAddress) by Initiatedby, OperationName
| where array_length(TargetUser)>threshold
If the attacker compromises an administrator account with permissions to delete, update, or add other users' MFA methods, they can perform password resets for multiple users.
KQL query to find password reset operations performed by admins on behalf of multiple users in Entra Id
let threshold=2;
AuditLogs
| where TimeGenerated >ago(1d)
//| where LoggedByService=~"Self-service Password Management"
| where OperationName == "Reset password (by admin)"
| where Result == "success"
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| extend Initiatedby=tostring(InitiatedBy.user.userPrincipalName)
| extend IpAddress=tostring(InitiatedBy.user.ipAddress)
| project TimeGenerated, OperationName, Initiatedby,TargetUser,IpAddress, AdditionalDetails,Result, CorrelationId
| summarize TargetUsers=make_set(TargetUser),count() by Initiatedby
| where array_length(TargetUsers)>threshold
Abusing voluntary password reset(update) method via SIM swapping and compromised credentials.
This is a new attack vector we discovered, where the following prerequisites should be met for the attack to succeed:
If the above prerequisites are met, an attacker can sign in to the user account without needing to reset the password, using the leaked credentials and the SMS-based MFA method.
Step1: Attacker uses compromised credentials for login.
Step2: MFA method completion
If the user's default MFA method is mobile SMS, the attacker will be prompted to enter an SMS code during authentication.
If the default method is not Mobile SMS as shown attacker can select I can't use Microsoft Authenticator App right now. This action will trigger an alert to the client, possibly through an Authenticator app notification.
And further he select the mobile SMS method and completes MFA.
Step3: Password Reset
Once the attacker logged into the account he can simple change password of the user via My Sign-ins Security Info Page method where he don't even need to provide the old password. Also he can start adding new MFA methods as like previous attack vector.
Detection Ideas:
We are providing some key KQL queries for detection here:
MFA registration followed by voluntary password reset.
AuditLogs
| where TimeGenerated >ago(1d)
| where OperationName=="Change password (self-service)"
| where Result == "success"
| extend InitiatedUser=tostring(InitiatedBy.user.userPrincipalName)
| extend TargetUser=tostring(TargetResources[0].userPrincipalName)
| project ResetTime=TimeGenerated, InitiatedUser, TargetUser, OperationName, Result
| join kind=inner (
AuditLogs
| where TimeGenerated >ago(1d)
| where OperationName in~ ("User registered all required security info","User registered security info","Admin registered security info","User changed default security info","Admin updated security info","User updated security info")
//| where Result=="success"
| extend InitiatedUser=tostring(InitiatedBy.user.userPrincipalName)
| extend TargetUser=tostring(TargetResources[0].userPrincipalName)
| project ["MFARegistration/Update Time"]=TimeGenerated, InitiatedUser, TargetUser, Result, OperationName) on TargetUser
| where ['MFARegistration/Update Time']>ResetTime
| extend ['Reset to MFA change TimeGap']=datetime_diff('minute',["MFARegistration/Update Time"],ResetTime)
MFA method deletion followed by voluntary password reset.
AuditLogs
| where TimeGenerated >ago(1d)
| where OperationName=="Change password (self-service)"
| where Result == "success"
| extend InitiatedUser=tostring(InitiatedBy.user.userPrincipalName)
| extend TargetUser=tostring(TargetResources[0].userPrincipalName)
| project ResetTime=TimeGenerated, InitiatedUser, TargetUser, OperationName, Result
| join kind=inner (
AuditLogs
| where TimeGenerated >ago(1d)
| where OperationName in~ ("Admin deleted security info","User deleted security info")
//| where Result=="success"
| extend InitiatedUser=tostring(InitiatedBy.user.userPrincipalName)
| extend TargetUser=tostring(TargetResources[0].userPrincipalName)
| project ['MFA Method Deletion Time']=TimeGenerated, InitiatedUser, TargetUser, Result, OperationName) on TargetUser
| where ['MFA Method Deletion Time']>ResetTime
| extend ['Reset to MFA deletion TimeGap']=datetime_diff('minute',['MFA Method Deletion Time'],ResetTime)
Collecting SSPR Settings Data via Compromised User
MITRE Technique: T1087-Account Discovery
If an attacker compromises a user who has "Reports.Read.All" delegated permission, they can retrieve details about SSPR settings for users.
The following powershell commands can be used to retrieve the details of sspr
First connect to microsoft graph powershell and authenticate with the scope "Reports.Read.All"
Install-Module Microsoft.Graph.Beta
Connect-MgGraph -Scopes "Reports.Read.All"
Filter for users who have registered for self-service password reset (SSPR).
Get-MgBetaReportCredentialUserRegistrationDetail -All | Where-Object {$_.IsRegistered -eq $true}
Filter for users who have been enabled for SSPR.
Get-MgBetaReportCredentialUserRegistrationDetail -All| Where-Object {$_.IsEnabled -eq $true}
Filter for users who are ready to perform password reset or multi-factor authentication (MFA).
Get-MgBetaReportCredentialUserRegistrationDetail | Where-Object {$_.isCapable -eq $true}
Filter for users who are registered for MFA.
Get-MgBetaReportCredentialUserRegistrationDetail -All | Where-Object {$_.isMfaRegistered -eq $true}
Filter users who has only mobile based security verifications(auth method):
Get-MgBetaReportCredentialUserRegistrationDetail | Where-Object {$_.AuthMethods.Count -ne 0 -and $_.AuthMethods -notcontains "appNotification" -and $_.AuthMethods -notcontains "appCode" -and $_.AuthMethods -notcontains "securityQuestion"} | Where-Object { $_.AuthMethods -contains "mobileSMS" -or $_.AuthMethods -contains "officePhone" -or $_.AuthMethods -contains "mobilePhone" -or $_.AuthMethods -contains "mobileSMS"}
Filter users without having authenticator app verification auth method:
Get-MgBetaReportCredentialUserRegistrationDetail | Where-Object {$_.AuthMethods.Count -ne 0 -and $_.AuthMethods -notcontains "appNotification" -and $_.AuthMethods -notcontains "appCode"}
Detection:
The above methods generate microsoft graph logs with api endpoint "reports/credentialUserRegistrationDetails". The same you can detect via below query.
MicrosoftGraphActivityLogs
| where TimeGenerated >ago(50m)
| where RequestUri has_all ("reports/credentialUserRegistrationDetails","graph.microsoft.com")
| where RequestMethod=="GET"
| where ResponseStatusCode==200
Mitigations:
Here are three important mitigation steps we discussed. For the remaining steps, you can refer to the Obsidian Threat Research article.
You can further restrict based on client device whether its compliant or not.
Author: Ashok Krishna Vemuri
information Security enthusiast with expertise for various areas of infosec
8 个月Awesome article ,great insights