>
av.Amit VijayanCYBERSECURITY & RESEARCHLet\u2019s connect
Back to the library

Threat Hunting Playbooks

Hunting Golden SAML: Forged Federation Tokens in ADFS Sign-In Logs

Thumbnail for: Hunting Golden SAML: Forged Federation Tokens in ADFS Sign-In Logs

Why this matters now

Golden SAML is the federation kill chain with the longest shelf life in the attacker's playbook. Steal one ADFS token-signing certificate and its private key, and you can mint your own SAML assertions offline — any user, any group membership, any privileges — against every application that trusts that federation server: Microsoft 365, Azure, SharePoint, Teams, any SAML-enabled SaaS. The technique, named by CyberArk in 2017, went from theory to nation-state tradecraft during the SolarWinds / Solorigate intrusions of December 2020, when forged SAML tokens were used to move into Microsoft 365 tenants.

The signature move of Golden SAML is absence. A forged token never touches your ADFS server, so the legitimate provider issues nothing and logs nothing — no 1200 token-issuance events, no 1202 credential-validation events. Password resets don't stop it, and MFA doesn't stop it, because the forged token skips the normal authentication flow entirely. These hunts close that gap: catch the certificate theft that makes it possible, read the anomaly in the federated sign-in itself, and prove the issuance gap that a forgery leaves behind.

How the federation golden ticket works

1. Steal the token-signing certificate

Every SAML identity provider signs its assertions with a token-signing certificate. Whoever holds that certificate's private key is the identity provider, cryptographically. To get it, the attacker first needs the ADFS server itself: a privileged compromise followed by export of the certificate store or direct access to the ADFS configuration database (WID or SQL). Windows records these moments: CAPI2's Operational log fires event 70 when a private key is exported, the Certificate Services Client log fires event 1007 on certificate export, and event 4662 on the domain side marks reconnaissance of the ADFS DKM keys (CN=CryptoPolicy,CN=ADFS,... with the thumbnailPhoto GUID {8d3bca50-1d7e-11d0-a081-00aa006c33ed}). Hunt 1 targets exactly this window.

2. Forge any assertion, offline

With the certificate and key in hand, tools — mimikatz ships a golden_saml module, and purpose-built scripts do the same — craft a SAML response with attacker-chosen claims: the NameID of any target user, group SIDs for elevated roles, long token lifetimes, and whatever issuer string the forgery needs to look legitimate. This happens on the attacker's machine, not yours. Nothing about it touches your network, your logs, or your SIEM. The forgery is then POSTed straight to the service provider's login endpoint as if it came from your ADFS server.

3. Bypass the identity provider entirely

The service provider validates the signature against the federation metadata you published — the same public key it has always trusted — and lets the attacker in. Because no request ever reached ADFS, there are no corresponding issuance events on the federation farm. The tell is the gap: a successful federated sign-in on the provider side with no matching token-issuance event on the ADFS side. The other tells live inside the sign-in record itself: forged assertions carry attacker-chosen session characteristics — an unfamiliar UserAgent, an unexpected source IP, single-factor authentication on an account your Conditional Access policy should never let in without MFA.

The KQL hunts

These queries assume federated users signing into Microsoft 365 / Entra, queried through Microsoft Defender XDR advanced hunting. They run in AADSignInEventsBeta, where federated sign-ins are marked inside the AuthenticationProcessingDetails JSON with an IsFederatedUser entry. ADFS-server queries run in DeviceProcessEvents and DeviceFileEvents — replace the example server names with your federation farm.

Hunt 1 — The key theft: cert export and DKM access on ADFS servers

Golden SAML starts at the federation server. Any interactive access to the certificate store or export tooling on an ADFS node is abnormal — these servers should never be touched by anything but your admins and the ADFS service itself:

// Hunt: token-signing certificate theft activity on ADFS servers
let ADFSservers = dynamic(["ADFS01", "ADFS02"]); // your federation farm names
DeviceProcessEvents
| where Timestamp > ago(14d)
| where DeviceName in (ADFSservers)
| where InitiatingProcessAccountName !~ "system"
    and ProcessCommandLine has_any (
        "certutil", "Export-PfxCertificate", "Export-Certificate",
        "mimikatz", "sekurlsa", "Get-ADFS", "certificate"
    )
    or (InitiatingProcessFileName =~ "mmc.exe"
        and ProcessCommandLine has "certmgr")
| project Timestamp, DeviceName, InitiatingProcessAccountName,
    InitiatingProcessFileName, FileName, ProcessCommandLine
| order by Timestamp desc

Complement this with the Windows events, which are the authoritative record even when endpoint telemetry is gappy: event 70 (CAPI2 private-key export), event 1007 (certificate exported), and event 307 (AD FS configuration changed) on the federation servers. A 70 or 1007 fired by anything other than your documented automation is containment-grade evidence.

Hunt 2 — The anomalous federated sign-in: MFA policy that never engaged

A forged token carries the attacker's claims, not the user's normal session profile. The strongest signal in Entra is a federated sign-in that skipped the step your policy should have enforced: a single-factor success on an account that sits behind an MFA-requiring policy. Federated claims arrive pre-authenticated, so an attacker can assert a clean logon while your Conditional Access never saw the user:

// Hunt: federated sign-ins that satisfied single-factor only
AADSignInEventsBeta
| where Timestamp > ago(14d)
| where tostring(AuthenticationProcessingDetails) has "IsFederatedUser"
| where ErrorCode == 0
| where AuthenticationRequirement == "singleFactorAuthentication"
| summarize Successes = count(),
    Resources = make_set(ResourceDisplayName, 10),
    SourceIPs = make_set(IPAddress, 10),
    Countries = make_set(tostring(parse_json(Location).countryOrRegion), 5),
    Agents = make_set(UserAgent, 5),
    FirstSeen = min(Timestamp), LastSeen = max(Timestamp)
    by AccountUpn
| project AccountUpn, Successes, Countries, SourceIPs, Resources,
    Agents, FirstSeen, LastSeen
| order by Successes desc

Tune this against your Conditional Access reality: accounts that are legitimately MFA-exempt (service accounts, break-glass) belong on an allowlist. What you are hunting is a human federated account that should have done MFA but didn't — cross-check each hit against the policy that covers it. A forged assertion asserting a high-privilege account (global admin, AAD role holders) with a single-factor requirement is the shape of the Solorigate-era token replay.

Hunt 3 — The forged session fingerprint: foreign UserAgents on federated accounts

Forged assertions are delivered from the attacker's machine, so the browser fingerprint of the session belongs to the attacker. A federated user suddenly signing in with a UserAgent that never appears in their history — a generic script UA, an older browser build, a mismatched OS string — while the rest of the tenant's sign-ins look normal, is a classic token-replay tell:

// Hunt: federated accounts with more than one UserAgent this week
AADSignInEventsBeta
| where Timestamp > ago(7d)
| where tostring(AuthenticationProcessingDetails) has "IsFederatedUser"
| where ErrorCode == 0
| where isnotempty(AccountUpn) and isnotempty(UserAgent)
| summarize Agents = make_set(UserAgent), SignIns = count(),
    IPs = make_set(IPAddress, 10)
    by AccountUpn
| where array_length(Agents) > 1
| project AccountUpn, SignIns, Agents, IPs
| order by SignIns desc

Work each hit like a timeline: take the foreign UserAgent, list every session it touched and everything those sessions accessed in the following hour. Forged sessions from token theft often strike first at mail and then at admin APIs — the same escalation pattern documented in the December 2020 forged-SAML campaigns against Microsoft 365.

Hunt 4 — The issuance gap: federated success with no ADFS token event

This is the definitive Golden SAML test, and it requires two log sources joined: your ADFS token-issuance events (event 1200, or 1202 for fresh-credential validation) and Entra federated sign-ins over the same window. A successful federated sign-in for an account with no corresponding issuance event on the federation farm means the assertion did not come from your IdP — it was forged:

// Hunt: federated Entra successes with no matching ADFS issuance event
// Requires ADFS 'AD FS/Admin' audit events (1200/1202) shipped to Sentinel
let ADFSIssued = SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID in (1200, 1202)
| where tostring(TargetUserName) != ""
| extend Upn = tolower(TargetUserName)
| summarize ADFSIssuedCount = count() by Upn;
AADSignInEventsBeta
| where Timestamp > ago(7d)
| where tostring(AuthenticationProcessingDetails) has "IsFederatedUser"
| where ErrorCode == 0
| extend Upn = tolower(AccountUpn)
| join kind=leftanti (ADFSIssued) on Upn
| summarize SignIns = count(),
    IPs = make_set(IPAddress, 10),
    FirstSeen = min(Timestamp), LastSeen = max(Timestamp)
    by AccountUpn
| project AccountUpn, SignIns, IPs, FirstSeen, LastSeen
| order by SignIns desc

This only works if ADFS security auditing is enabled (Set-AdfsProperties -AuditLevel Verbose on the farm) and the 'AD FS/Admin' log is shipping to your SIEM — neither is on by default, which is exactly why the gap goes unnoticed. Time-skew allowances matter: give the join a few minutes of slack before treating a miss as forgery, and remember that SSO-refresh paths legitimately skip fresh issuance.

Splunk equivalents

These assume the Splunk Add-on for Microsoft Cloud Services (sourcetype="azure:aad:signin") plus ADFS server events shipped via the Universal Forwarder with renderXml=true. The ADFS event log stanza is WinEventLog://AD FS/Admin; the certificate-export events come from Microsoft-Windows-CertificateServicesClient-Lifecycle-System/Operational (1007) and Microsoft-Windows-CAPI2/Operational (70).

SPL 1 — Certificate export and ADFS config changes on federation servers

(EventCode=70 OR EventCode=1007 OR EventCode=307) host IN (adfs01, adfs02)
| eval event_type=case(EventCode=70, "private_key_export",
    EventCode=1007, "cert_export", EventCode=307, "adfs_config_change")
| stats earliest(_time) AS first_seen, latest(_time) AS last_seen,
    values(user) AS users, values(src) AS sources BY event_type, host
| sort first_seen

SPL 2 — Federated sign-ins skipping MFA

sourcetype="azure:aad:signin" earliest=-14d
| spath input='properties.authenticationProcessingDetails' | search "IsFederatedUser"
'properties.status.errorCode'=0
'properties.authenticationRequirement'="singleFactorAuthentication"
| stats count AS successes, values('properties.ipAddress') AS ips,
    values('properties.location.countryOrRegion') AS countries,
    values('properties.userAgent') AS agents,
    earliest(_time) AS first_seen, latest(_time) AS last_seen
    BY 'properties.userPrincipalName'
| sort - successes

SPL 3 — Foreign UserAgents on federated accounts

sourcetype="azure:aad:signin" earliest=-7d
| spath input='properties.authenticationProcessingDetails' | search "IsFederatedUser"
'properties.status.errorCode'=0
| stats dc('properties.userAgent') AS agent_count,
    values('properties.userAgent') AS agents,
    values('properties.ipAddress') AS ips, count AS signins
    BY 'properties.userPrincipalName'
| where agent_count>1
| sort - signins

Triage: what to do when a hunt hits

  • Treat any certificate-export event on an ADFS server as containment-grade. Events 70 or 1007 outside your documented automation are not warnings — the private key may already be gone. Isolate the server before you start reading logs; forensic images are easier to take from a powered-on host than from a wiped one.
  • Check whether the forged tokens are already in use. For every privileged federated account, pull the last 30 days of sign-ins and look for the session fingerprint: new IPs, new UserAgents, single-factor successes against MFA policies. The mail audit log (Search-MailboxAuditLog or M365 compliance search) shows what a forged session actually read.
  • Score the account, not the event. One foreign UserAgent on a help-desk account is noise; a single-factor global-admin sign-in from an ASN the tenant has never seen is a breach. Prioritize admin and service accounts first — forgers aim at the claims that unlock everything.
  • Do not rely on the ADFS log to clear a hit. If auditing was off, the gap query in Hunt 4 proves nothing either way. Correlate against provider-side session evidence instead: impossible travel, session revocations, and app-level audit events.
  • Remember the persistence. Forged tokens work until the signing certificate is replaced — password resets, MFA re-enrollment, and even account disables of the user do not invalidate them. Scope the incident window from the earliest export evidence, not from the first suspicious sign-in.

Remediation and hardening

  • Rotate the token-signing certificates immediately — primary and secondary — on every ADFS farm where export is confirmed or suspected, then confirm the new trust metadata reaches every relying party. Until the certificates change, every forged token still validates.
  • Revoke all Entra sessions and refresh tokens for affected users (Revoke-AzureADUserAllRefreshToken in a hybrid environment), then force re-authentication. Do this after the certificate rotation, not before — revocation against a still-valid signing key does not stop re-forgery.
  • Rotate the ADFS service account and DKM keys if the configuration database was accessed; assume the whole federation farm is compromised and rebuild at least one server from a known-good source to verify.
  • Enable ADFS security auditing permanently: Set-AdfsProperties -AuditLevel Verbose, and ship the 'AD FS/Admin' log (1200, 1202, 307) plus the CAPI2 and CertificateServicesClient operational logs to your SIEM. You cannot hunt the issuance gap without them.
  • Reduce the blast radius of any single certificate: keep a secondary signing certificate ready, automate rollover drills, and maintain an inventory of every relying party that trusts each federation domain — so a compromise can be re-keyed in hours, not weeks.

Keep exploring.

All articles