threatintel
actor tracker
All briefs

Threat-hunting brief: Golden SAML token forgery in Azure AD / ADFS environments

Author: Thomas Malinowski Published: 2026-05-18 Status: Hunt-methodology brief, citation-disciplined Tags: APT29, Midnight Blizzard, Azure AD, Microsoft 365, Golden SAML, T1606.002, T1550.001, threat-hunting, KQL


Bottom line up front

APT29 / Midnight Blizzard demonstrated in the SolarWinds post-compromise phase (Dec 2020 – Jan 2021) that an attacker who extracts an on-prem ADFS token-signing certificate can forge SAML 2.0 assertions for any identity in the federated tenant, bypassing MFA and Conditional Access entirely. The technique is MITRE T1606.002 / T1550.001. Telemetry target: Microsoft Sentinel SigninLogs, AADServicePrincipalSignInLogs, and AuditLogs. Recommended hunt window: 90 days. Deliverable: a triage queue of sign-in events bearing the forged-token signature (no MFA claim, anomalous source IP, unconstrained audience) joined to subsequent privilege escalation in the same tenant.


Threat-model context

Cloud-first and AI-lab organisations sit in the highest-value target set. Three properties make this particularly acute:

  • Federated identity is the control plane. A single ADFS signing key underpins trust for every SAML-integrated application — M365, Azure ARM, SaaS tooling, and inference infrastructure. Owning the key is equivalent to owning every account in the tenant.
  • Service-principal abuse is a force multiplier. CISA AA21-008A documents APT29 using forged tokens specifically to authenticate as application service principals, which typically lack MFA and carry standing Graph API delegations.
  • Multiple actors now use this playbook. Storm-0558 (2023) forged Entra ID tokens using a stolen Microsoft MSA signing key — different acquisition path, same downstream observable. See MSRC September 2023.

The hunt hypothesis

If an attacker has forged a SAML token from an on-prem ADFS signing key, then we expect:

  • SigninLogs entries with AuthenticationRequirement == "singleFactorAuthentication" and TokenIssuerType indicating federation, against a high-privilege resource (Azure ARM, Graph, Exchange Online).
  • The authenticating IP falls outside the organisation's known ADFS Web Application Proxy ranges — a forged token can originate from any host.
  • AuditLogs show Set domain authentication or Set federation settings on domain in the access window — attackers may embed a second signing certificate in the trust.
  • Within hours of the anomalous sign-in: a directory role assignment or application credential addition on the same principal.

Queries

Q1 — SAML sign-ins without MFA (T1606.002)

SigninLogs
| where TimeGenerated > ago(90d)
| where AuthenticationRequirement == "singleFactorAuthentication"
| where TokenIssuerType == "AzureAD"
| where AuthenticationDetails has_any ("SAML", "Federation")
| where ResultType == 0
| project TimeGenerated, UserPrincipalName, IPAddress,
    ResourceDisplayName, ConditionalAccessStatus, Location
| order by TimeGenerated desc

Q2 — Service-principal sign-ins from anomalous country (T1550.001)

let baseline = ago(30d);
let hunt = ago(7d);
let sp_geo =
    AADServicePrincipalSignInLogs
    | where TimeGenerated between (ago(37d) .. baseline)
    | where ResultType == 0
    | summarize BaselineCountries = make_set(Location) by ServicePrincipalId;
AADServicePrincipalSignInLogs
| where TimeGenerated > hunt
| where ResultType == 0
| join kind=leftouter sp_geo on ServicePrincipalId
| where not(Location in (BaselineCountries))
| project TimeGenerated, ServicePrincipalName, IPAddress, Location, ResourceDisplayName

Q3 — Federated domain or trust modification (T1484.002)

AuditLogs
| where TimeGenerated > ago(90d)
| where OperationName in (
    "Set domain authentication",
    "Set federation settings on domain",
    "Add unverified domain to company",
    "Verify domain")
| extend InitiatedByUPN = tostring(InitiatedBy.user.userPrincipalName)
| extend TargetDomain  = tostring(TargetResources[0].displayName)
| project TimeGenerated, OperationName, InitiatedByUPN, TargetDomain, Result

Q4 — Privilege escalation within 2 h of a SAML sign-in (T1606.002 → T1098.003)

let saml =
    SigninLogs
    | where TimeGenerated > ago(90d)
    | where AuthenticationRequirement == "singleFactorAuthentication"
    | where ResultType == 0
    | where AuthenticationDetails has_any ("SAML", "Federation")
    | project SigninTime = TimeGenerated, UserPrincipalName, IPAddress;
AuditLogs
| where TimeGenerated > ago(90d)
| where OperationName in (
    "Add member to role",
    "Add eligible member to role",
    "Add app role assignment to service principal")
| extend TargetUPN = tostring(TargetResources[0].userPrincipalName)
| join kind=inner saml on $left.TargetUPN == $right.UserPrincipalName
| where TimeGenerated between (SigninTime .. (SigninTime + 2h))
| project TimeGenerated, OperationName, TargetUPN, IPAddress

Tuning — FP vs TP

Common FP sources:

  • Service accounts on SAML with no MFA — CI/CD pipelines, monitoring agents, legacy apps. Build an allow-list and filter them from Q1.
  • M&A / partner B2B onboarding — Q3 fires routinely when adding a federated partner domain. Cross-reference an open change ticket; if one exists, it's a FP.
  • Cloud-NAT egress — managed workloads doing cross-cloud API calls appear as anomalous geolocation in Q2. Maintain an allow-list of cloud-provider NAT ranges.

TP indicators that raise confidence:

  • Q1 and Q4 fire on the same UPN within the same hour with no change record.
  • The IP in Q1 is not an ADFS proxy or WAP server address from your inventory.
  • Q3 fires on an unrecognised domain or is initiated by a service principal (service principals should not be adding federated domains).
  • The resource in Q1 is Azure ARM, Graph (graph.microsoft.com), or Exchange Online.

From hypothesis to durable detection

Alert on (high-precision): Q4 hits where the SAML sign-in IP is outside known ADFS proxy ranges and no matching change record exists. Once proxy IPs are baselined, the benign rate should approach zero.

Keep as hunt (high-recall, analyst-triaged): Q1 and Q2 — both produce volume that requires context to evaluate. Run weekly; triage against service-account allow-list; escalate residual.

Integration with the existing Sigma ruleset: The Q4 pattern — anomalous SAML sign-in preceding a role assignment — extends t1098.001_azure_app_credential_addition.yml. That rule fires on credential additions to service principals. The full composite alert should chain: anomalous SAML → app credential addition → role assignment, each within a two-hour sliding window.


Pivots from a single finding

Given one confirmed suspicious SAML sign-in, fetch in this order:

  1. Token claims. Inspect AuthenticationDetails for the amr field. A legitimate MFA-satisfied session carries ["mfa", "rsa"] or equivalent; a forged token typically carries only ["pwd"] against a resource with a CA policy requiring MFA.
  2. App-consent grants. Query AuditLogs for Add app role assignment and Consent to application on the same principal in the 48 h following the sign-in. APT29 used this to add OAuth permissions that survive password resets (AA21-008A).
  3. CA policy mutations. Update conditional access policy / Delete conditional access policy in AuditLogs — an attacker with sufficient privilege weakens CA to reduce friction on subsequent access.
  4. Mailbox access via Graph. MailItemsAccessed in the M365 Unified Audit Log tied to the same application confirms collection impact.
  5. ADFS on-prem events (if in scope). Windows Event ID 1202 (token issuance) on the ADFS farm for the same time window — a token accepted by Azure AD with no corresponding ADFS 1202 is a near-certain forgery.

Limitations

  • Stolen refresh tokens. An attacker replaying a legitimate refresh token never touches ADFS. The sign-in carries a full MFA claim and a plausible IP. Q1–Q4 do not fire. Detection shifts to endpoint telemetry.
  • Azure managed identities. Workloads using managed identities authenticate through Azure's internal IMDS endpoint — no ADFS, no SigninLogs SAML event. This hunt is blind to that path.
  • ADFS trust lateral move. An attacker who adds a new ADFS server to the trust (rather than exporting the key) may not surface in Entra audit logs if the federation settings are not modified in Azure. Q3 is only effective if the attacker updates the Azure-side trust.
  • Slow-and-low token use. Infrequent token use from a geolocation within the tenant's plausible range defeats Q2's baseline. Q4 remains the most durable signal because it is activity-based rather than attribute-based.

References