Another Microsoft security vulnerability became known over the past week. It was possible for users and, under certain conditions, external users (guests) to change their so-called "User Principal Name (UPN)." The UPN is the primary sign-in attribute used in the Microsoft world to determine identity.
The Bypass of Intune Compliant Device controls is also still current
⚠️ For hybrid users synchronized from Active Directory, changes were overwritten, but this still opened a window for potential misuse. ⚠️
In the Admin Center, a user could adjust the prefix (Max.Mustermann@…) and the domain (…@Firma.de) in their own profile, thereby also creating additional valid email addresses for themselves. This change could also be done via the API.
Normally, although the field is shown as editable, you would see an error message when attempting to save:
Since at least January 22, 2025, that error message did not appear. The issue was fixed on January 24, 2025, in the afternoon. To date, Microsoft has not disclosed exactly how long the problem existed (status as of January 29, 2025).
When were guest users able to change their sign-in name?
Entra ID offers various ways to manage guests. By default, their access is limited. If, however, guests are granted the same rights as regular users (which I explicitly do not recommend!), those rights included the ability to change their own UPN.
Why is changing the UPN a problem?
The following scenarios are immediately apparent to me, but there are certainly more ways to exploit the ability to change a UPN.
Impersonation
Changing the UPN also changes the email and chat address. This could allow users to conceal their identity (e.g. “CEO@…”), as the display name in the Office contact card is often not visible across organizations. Old UPNs remain associated with the mailbox, meaning unused addresses (e.g. "WorksCouncil@…") could be misused to intercept emails. After an UPN-Change, Guests could also appear as internal employees.
Dynamic Groups
Automatic group rules often rely on email addresses. By changing the UPN, attackers could gain unauthorized access to organization-wide Teams or files. Naming conventions used for security mechanisms (such as Conditional Access) could also be circumvented by clever administrators.
Tip: Administrator controls should use explicit assignments and protected attributes.
Taking over inactive user accounts in connected applications
Even though active UPNs in Entra are unique, departed users’ accounts may still exist in third-party applications. Since those accounts are often not deleted, this could allow extensive access to systems such as HR software or financial accounting.
Searching for Misuse
We’ve established the importance of taking a look at any changes that occured – but how can we find problematic UPNs over an unknown time period?
Microsoft has announced that, in the event of misuse, they will contact the administrators of affected tenants. However, since neither the timing nor the details of this contact are known, it makes sense to proactively look for potential exploitation of this behavior wherever possible.
Before we begin, it’s important to note: without an Entra ID Premium license, Audit Logs are only retained for 7 days. If these logs aren’t forwarded to a Log Analytics Workspace or another archive, it’s very unlikely you will be able to independently identify any misuse.
Adding the storage solutions listed below after the fact will not recover lost data—what’s gone is gone.
Parsing the Audit Log from the Portal
The simplest way to access the Audit Logs is via the Entra admin center. Global Reader or Report Reader permission is sufficient.
With Entra Premium licenses, such as those included in Enterprise plans, changes are retained for 30 days.
While you can filter for user changes in the portal (Activity = "Update User"), even medium-sized tenants quickly become hard to manage in this view.
It’s therefore more practical to export the results and then analyze them with a script. Below is an example script that reads a CSV file of the Audit Logs and filters for suspicious changes:
param (
[parameter(Mandatory = $false)]
[string]$auditCSVPath
)
# If Path not provided or not found, prompt user to select the Audit Log Export
if (-not $(Test-Path "$auditCSVPath") ) {
Add-Type -AssemblyName System.Windows.Forms
$FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{
InitialDirectory = [Environment]::GetFolderPath('Desktop')
Filter = 'CSV Export (*.csv)|*.csv'
}
$null = $FileBrowser.ShowDialog()
$auditCSVPath = $FileBrowser.FileName
}
$auditlogImport = import-csv $auditCSVPath
if ($auditlogImport.Count -eq 250000) {
Write-Warning "Please specify a (more restrictive) filter in the GUI or use the API, you have reached the maximum number of records"
}
# Filter relevant entries
$selfEdits = $auditlogImport | Where-Object {$_.Activity -eq "Update user" -and $_.Target1ObjectId -eq $_.ActorObjectId}
$suspiciousEdits = $selfEdits | Where-Object {$($_ | ConvertTo-Json) -match '"UserPrincipalName",' }
if (-not $suspiciousEdits){ Write-Output "No suspicious changes found"; break }
$Report = [System.Collections.Generic.List[Object]]::new()
forEach ($item in $suspiciousEdits) {
$obj = [PSCustomObject][ordered]@{
"Timestamp" = "$(get-date ($item.'Date (UTC)') -Format 'dd-MMM-yyyy HH:mm')"
"User GUID" = $item.Target1ObjectId
"Old UPN" = $item.ActorUserPrincipalName
"New UPN" = $item.Target1UserPrincipalName
}
$report.Add($obj)
}
$Report | Out-gridview -Title "Please verify, carefully, these UPN changes"
Audit Log via the Graph API
If you have elevated privileges, I recommended setting up an Enterprise App and directly using the Graph API to retrieve the Audit Logs. However, for the initial setup, you’ll need the "Privileged Authentication Administrator" right.
Connect-MgGraph -Scope "AuditLog.Read.All, Directory.Read.All" -NoWelcome
$filter = "activityDateTime le 2025-01-25 and activityDisplayName eq 'Update user'"
$auditLogs = Invoke-MgGraphRequest GET "https://graph.microsoft.com/v1.0/auditLogs/directoryaudits?`$filter=$filter" -OutputType PSObject
# Identify UPN Changes initiated by the user himself
$upnchanges = $auditlogs.value | Where-Object {$_.targetresources.modifiedproperties.displayname -eq "Userprincipalname"}
$suspiciousEdits = $upnchanges | Where-Object {$_.Initiatedby.user.id -eq $_.targetresources.id}
if (-not $suspiciousEdits){ Write-Output "No suspicious changes found"; break }
# Create and show report
$Report = [System.Collections.Generic.List[Object]]::new()
forEach ($item in $suspiciousEdits) {
$modifiedProperty = $item.targetresources.modifiedproperties | Where-Object {$_.displayname -eq "Userprincipalname"}
$obj = [PSCustomObject][ordered]@{
"Timestamp" = "$(get-date ($item.activityDateTime) -Format 'dd-MMM-yyyy HH:mm')"
"User GUID" = $item.Initiatedby.user.id
"Old UPN" = $modifiedProperty.oldValue
"New UPN" = $modifiedProperty.newValue
}
$report.Add($obj)
}
$Report | Out-gridview -Title "Please verify, carefully, these UPN changes"
KQL in an Azure Log Analytics Workspace
If you have set up an export of the Audit Log to an Azure Log Analytics Workspace, you benefit from potentially much longer retention periods and more efficient search options.
Simply open the Log search and use the following KQL to display instances where a user changed their own UPN:
AuditLogs
| where TimeGenerated between (datetime(2024-06-01) .. datetime(2025-01-25) )
| where OperationName == "Update user"
//Filter common Service Identities to reduce costly parsing
| where not(Identity in ("Azure MFA StrongAuthenticationService", "Microsoft Substrate Management", "Azure Credential Configuration Endpoint Service", "Microsoft password reset service", "Microsoft Online Services", "Office 365 SharePoint Online"))
| extend Target = parse_json(TargetResources)[0]
// Check whether the UPN was changed before we expand
| where Target.modifiedProperties has "UserPrincipalName"
| mv-expand Target.modifiedProperties
| where tostring(Target_modifiedProperties.displayName) == "UserPrincipalName"
// Add the Initiator to check whether the user changed himself
| extend Initiatior = parse_json(InitiatedBy)
| where tostring(Initiatior.user.id) == tostring(Target.id)
| extend newValue = tostring(Target_modifiedProperties.newValue)
| extend oldValue = tostring(Target_modifiedProperties.oldValue)
| where newValue != oldValue
| project TimeGenerated, User_GUID = Target.id, newValue, oldValue
For no particular reason I would like to mention Julian Rasmussen.
Purview Audit Log Search
If it turns out the vulnerability has existed for more than 30 days or you only became aware of it late, there is an alternative—even if no Log Analytics Workspace has been set up.
The Purview Audit Log (also called the Exchange Unified Audit Log) stores compliance data for:
- 180 days with an E3 license
- 365 days with an E5 license
This feature is enabled by default but requires specific permissions:
- Assignment of the Exchange role "Audit Logs" or "View-Only Audit Logs" (alternatively, Global Admin, but that’s not recommended).
- A Graph Enterprise App with appropriate permissions.
- Privileged Authentication Administrator is required for initial setup.
With these prerequisites in place, you can search the Unified Audit Log using the PowerShell script below:
Inspired by https://practical365.com/audit-log-query-api/
#Requires -modules Microsoft.Graph.Authentication
# If you have a longer retention you may specify it (Default: 180 days)
param (
[parameter(Mandatory = $false)]
[int]$logRetentionDays = 180
)
Connect-MgGraph -scope "AuditLogsQuery-Entra.Read.All" -NoWelcome
# Function to Handle Graph Pagination
function Get-GraphData {
param (
[Parameter(Mandatory = $true)]
[string]$Uri,
[hashtable]$headers = @{},
[System.Collections.Arraylist]$result
)
$response = Invoke-MgGraphRequest -Method GET -Uri $Uri -Headers $headers -OutputType PSObject
$result.AddRange($response.value)
if ($response.'@odata.nextLink') {
Get-GraphData -Uri $response.'@odata.nextLink' -headers $headers -result $result
}
}
Write-Output "Creating query..."
$SearchName = ("UPNChange_$(Get-Date -format 'dd-MMM-yyyy HH:mm')")
# Subtract retention period, if out of available range API throws an error
[String]$StartDate = (Get-Date).AddDays(-$logRetentionDays).Tostring("yyyy-MM-ddT00:00:00Z")
# End on date the issue was fixed
[String]$EndDate = "2025-01-25T00:00:00Z"
$SearchParameters = @{
displayName = "$SearchName"
filterStartDateTime = "$StartDate"
filterEndDateTime = "$EndDate"
serviceFilter = "AzureActiveDirectory"
operationFilters = @("Update user.")
}
$SearchQuery = Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/beta/security/auditLog/queries" -Body $SearchParameters
$SearchId = $SearchQuery.Id
Write-Output "Waiting for query completion..."
do {
$search = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/beta/security/auditLog/queries/$searchid"
Start-Sleep -Seconds 10
} while ( $search.status -notin @("failed", "succeeded", "cancelled") )
If ( $search.status -in @("failed", "cancelled") ) { Write-Error "The search did not complete successfully. Please check the Security & Compliance Portal - https://security.microsoft.com/auditlogsearch?viewid=Async%20Search" ; break }
Write-Output "Fetching results..."
$result = [System.Collections.Arraylist]::new()
$Uri = ("https://graph.microsoft.com/beta/security/auditLog/queries/{0}/records" -f $SearchId)
Get-GraphData -Uri $Uri -result $result
$suspiciousEdits = $result | Where-Object {$_.auditdata.modifiedproperties.name -eq "userprincipalname" -and
($_.auditData.Actor.id -match "^\w{8}([-]\w{4}){3}-\w{12}$") -eq ($_.auditData.Target.id -match "^\w{8}([-]\w{4}){3}-\w{12}$") }
if (-not $suspiciousEdits) { Write-Output "No suspicious changes found"; break }
$Report = [System.Collections.Generic.List[Object]]::new()
forEach ($item in $suspiciousEdits) {
$modifiedProperty = $item.auditdata.ModifiedProperties | Where-Object {$_.name -eq "userprincipalname"}
$obj = [PSCustomObject][ordered]@{
"Timestamp" = "$(get-date ($item.createdDateTime) -Format 'dd-MMM-yyyy HH:mm')"
"User GUID" = $item.auditData.Actor.id -match "^\w{8}([-]\w{4}){3}-\w{12}$"
"Old UPN" = $modifiedProperty.oldValue
"New UPN" = $modifiedProperty.newValue
}
$report.Add($obj)
}
$Report | Out-gridview -Title "Please verify, carefully, these UPN changes"
What should I do if I find a suspicious UPN change?
-
Disable the affected account immediately to prevent further misuse and limit access.
-
Contact the affected user to clarify the reason for the change. Check whether it was legitimate or if misuse occurred.
-
If it is misuse, correct the UPN and remove all proxy addresses associated with the user account
Closing Remarks
It is good that Microsoft responded quickly and decisively to reports from the community. Nevertheless, it remains clear that independent reviews continue to be an important part of a secure future at Microsoft.
Further articles from colleagues who were involved early on:
Comments