T1528 Elastic Security · Elastic

Detect Steal Application Access Token in Elastic Security

Adversaries may steal application access tokens as a means of acquiring credentials to access remote systems and resources. Application access tokens — including OAuth 2.0 tokens, Kubernetes service account tokens, cloud provider temporary credentials (Azure Managed Identity via IMDS, AWS STS instance role credentials, GCP service account tokens), and CI/CD pipeline secrets — authorize API requests on behalf of users or services. Token theft enables adversaries to impersonate legitimate identities, access cloud resources and SaaS platforms with the victim's permissions, and move laterally without requiring plaintext passwords. Real-world examples include APT29 stealing OAuth tokens via malicious application consent phishing, APT28 creating fraudulent OAuth apps masquerading as Google services, and threat actors exploiting compromised containers to extract Kubernetes service account tokens via the pod filesystem.

MITRE ATT&CK

Tactic
Credential Access
Technique
T1528 Steal Application Access Token
Canonical reference
https://attack.mitre.org/techniques/T1528/

Elastic Detection Query

Elastic Security (Elastic)
eql
any where
  (
    // Vector 1: Cloud IMDS token requests from non-native processes
    event.category == "network" and
    destination.ip == "169.254.169.254" and
    (
      url.full like~ "*metadata/identity*" or
      url.full like~ "*iam/security-credentials*" or
      url.full like~ "*computeMetadata/v1/instance/service-accounts*" or
      url.full like~ "*metadata/instance*"
    ) and
    not process.name in~ (
      "waagent.exe", "WindowsAzureGuestAgent.exe", "WaAppAgent.exe",
      "MonAgentCore.exe", "HealthService.exe", "MMAExtensionHeartbeatService.exe",
      "AzureAttestService.exe", "azd.exe", "AzureCLI.exe"
    )
  ) or
  (
    // Vector 2: Kubernetes service account token file reads by unexpected processes
    event.category == "file" and
    (
      file.path like~ "*/var/run/secrets/kubernetes.io/serviceaccount/*" or
      file.path like~ "*/run/secrets/kubernetes.io/*" or
      file.path like~ "*/var/run/secrets/tokens/*"
    ) and
    file.name in~ ("token", "ca.crt") and
    not process.name in~ (
      "kubelet", "pause", "containerd-shim", "containerd-shim-runc-v2",
      "runc", "cri-o", "dockerd"
    )
  ) or
  (
    // Vector 3: OAuth and cloud CLI token cache access by unexpected processes
    event.category == "file" and
    (
      file.name in~ (
        "application_default_credentials.json", "accessTokens.json",
        "azureProfile.json", "msal_token_cache.json", "msal_token_cache.bin",
        "TokenCache.dat", ".git-credentials"
      ) or
      (file.path like~ "*/.config/gcloud/*" and file.extension == "json") or
      (file.path like~ "*/.azure/*" and file.extension == "json") or
      file.path like~ "*/TokenCache/*"
    ) and
    not process.name in~ (
      "gcloud", "gcloud.exe", "aws", "aws.exe", "az", "az.exe",
      "gh", "gh.exe", "git", "git.exe", "Code.exe", "code",
      "terraform", "terraform.exe", "kubectl", "kubectl.exe"
    )
  )
high severity high confidence

Detects T1528 application access token theft via three behavioral vectors using Elastic Common Schema (ECS) fields: (1) network connections to the cloud IMDS link-local address 169.254.169.254 from processes not in the native agent allowlist, targeting Azure Managed Identity, AWS IAM instance role, or GCP service account credential paths; (2) file reads of Kubernetes service account tokens from /var/run/secrets/kubernetes.io/serviceaccount/ by non-orchestration processes; and (3) access to OAuth and cloud CLI token cache files (MSAL, GCP ADC, Azure CLI, GitHub credentials) by processes outside the expected toolchain. Covers both Windows and Linux endpoints via Elastic Agent endpoint integration.

Data Sources

Elastic Endpoint Security agent (network and file telemetry)Elastic Agent with System integration (Linux auditd)Elastic Agent with Windows integration (Sysmon via winlogbeat)Elastic Agent with Azure or GCP integration for cloud audit logs

Required Tables

logs-endpoint.events.network-*logs-endpoint.events.file-*logs-system.syslog-*logs-windows.sysmon_operational-*

False Positives & Tuning

  • Azure VM Agent (waagent.exe), AWS Systems Manager Agent (amazon-ssm-agent), and GCP OS Config daemon legitimately query IMDS on a regular schedule — tune the process exclusion list to reflect all cloud platform native agents deployed in your environment, including versioned paths
  • Container init processes, sidecar containers, and Kubernetes admission webhooks may read service account tokens during pod startup or cluster API authentication flows — correlate alerts with known Kubernetes node hostnames and system namespace pods before escalating
  • Developer workstations with VS Code remote extensions, IntelliJ cloud plugins, or multi-cloud CLI sessions may trigger token cache file reads from unexpected parent processes — validate with process ancestry chain and user identity context

Other platforms for T1528


Testing Methodology

Validate this detection against 4 adversary techniques from Atomic Red Team. Each test below lists the behaviour to exercise and the telemetry you should expect to see. Executable commands and cleanup steps are available with Pro.

  1. Test 1 Query Azure IMDS Endpoint for Managed Identity Token

    Expected signal: Sysmon Event ID 3: Network Connection from powershell.exe to 169.254.169.254:80. DeviceNetworkEvents in MDE: RemoteIP=169.254.169.254, InitiatingProcessFileName=powershell.exe. The RemoteUrl field will contain the metadata identity path.

  2. Test 2 Read Kubernetes Service Account Token from Pod Filesystem

    Expected signal: Linux auditd syscall audit event for openat/read on /var/run/secrets/kubernetes.io/serviceaccount/token with the process name (cat or the shell). Sysmon for Linux Event ID 11 (if deployed) for file access. The token value (JWT) will be visible in any memory or command output capture.

  3. Test 3 Enumerate and Exfiltrate Azure CLI Token Cache

    Expected signal: Sysmon Event ID 11: File access/creation event with TargetFilename matching *msal_token_cache.json and Image=powershell.exe. DeviceFileEvents in MDE: FileName=msal_token_cache.json, ActionType=FileRead or FileAccessed, InitiatingProcessFileName=powershell.exe.

  4. Test 4 Register Malicious OAuth App and Simulate Consent Phishing Link

    Expected signal: Azure AD AuditLogs OperationName='Add application' followed by 'Update application' with permissions modification. The registered app will appear in AuditLogs with the requesting user's UPN and source IP. If a test user clicks the generated consent URL, AuditLogs will show OperationName='Consent to application' with the scopes granted.


Response Playbook

Triage

  1. Identify the vector: IMDS query, Kubernetes token read, OAuth cache access, or OAuth consent grant. Each requires a different investigation path — IMDS and K8s are host-based; OAuth consent is identity-centric.
  2. For IMDS hits: identify the process that made the request (InitiatingProcessFileName / Image). Is this a known agent, an admin tool, or a suspicious binary? Check the process parent chain — was it spawned from a web server, a deserialization framework, or a container workload?
  3. For Kubernetes token reads: determine the container and namespace involved. Run: kubectl get pod <pod-name> -n <namespace> -o yaml to check the service account bound to the pod and its RBAC permissions (kubectl get clusterrolebindings,rolebindings -o wide). A token with cluster-admin is critical.
  4. For OAuth token cache access: determine if the process is a known credential-harvesting tool (LaZagne, SharpCloud, Mimikatz cloud module). Check VirusTotal and EDR telemetry for the initiating binary hash.
  5. For OAuth consent grants: review the granted application — is it registered in your tenant or external? Check the app's redirect URIs for suspicious domains. Query AuditLogs for the full consent event and map the user's location against their sign-in baseline in AADSignInLogs.
  6. Correlate the suspicious token access event with subsequent API calls: did the process or user account make cloud API calls (Azure Resource Manager, AWS API, Kubernetes API) immediately after the token access? This confirms successful theft and active use.
  7. Check whether the access token has already been used: query CloudAppEvents or SigninLogs for the affected application/identity in the 30 minutes following the token access event.

Containment

  1. For Kubernetes token theft: immediately revoke the service account token by deleting and recreating the ServiceAccount secret: kubectl delete secret <token-secret-name> -n <namespace>. Rotate the service account to generate a new token. If cluster-admin scope was exposed, treat the entire cluster as compromised.
  2. For IMDS-based token theft: immediately revoke the Managed Identity's role assignments in Azure IAM (az role assignment delete) or AWS IAM (aws iam remove-role-from-instance-profile). Detach and re-attach the identity after forensic capture to generate new credentials.
  3. For OAuth consent grant abuse: immediately revoke the OAuth consent in Azure AD portal (Enterprise Applications > [App] > Permissions > Revoke admin consent), or via PowerShell: Revoke-AzureADUserAllRefreshToken -ObjectId <user-id>. Block the malicious app's client ID via Conditional Access policy.
  4. For cloud CLI token cache theft: revoke all active sessions for the affected user (az ad user revoke-sign-in-sessions --id <upn>), rotate any service principal credentials associated with the tokens, and force re-authentication. For AWS, deactivate the affected IAM instance profile role temporarily.
  5. Isolate the compromised host if process-based token theft is confirmed — the endpoint is likely compromised and token theft may be a secondary action after initial access.
  6. Audit all actions taken with the stolen token by reviewing cloud provider activity logs (Azure Activity Log, AWS CloudTrail, GCP Cloud Audit Logs) for the identity in the time window following the theft event.

Evidence Collection

  1. Kubernetes token content and RBAC permissions: kubectl auth can-i --list --as=system:serviceaccount:<namespace>:<serviceaccount> — enumerate what the stolen token could do
  2. Azure Managed Identity token request log: check Azure IMDS access via Azure Monitor VM diagnostics or Defender for Cloud alert timeline for the affected VM
  3. OAuth app registration details: az ad app show --id <client-id> — capture redirect URIs, required permissions, and owner UPN for the consented application
  4. Cloud provider activity logs for the stolen identity: az monitor activity-log list --caller <client-id> (Azure), aws cloudtrail lookup-events --lookup-attributes AttributeKey=Username,AttributeValue=<role-name> (AWS)
  5. Process memory dump of the process that accessed the token (if still running) — may contain the raw token value: procdump.exe -ma <PID> C:\forensics\token_process.dmp
  6. Sysmon Event ID 1 parent process chain for the token-accessing process — reconstruct the full execution tree to identify initial access vector
  7. Network connections made by the process immediately after token access: Sysmon Event ID 3 or DeviceNetworkEvents correlated by ProcessId and Timestamp within a 5-minute window
  8. File system artifacts: %APPDATA%\.azure\accessTokens.json, ~/.config/gcloud/application_default_credentials.json, ~/.aws/credentials — preserve timestamps and content for forensic review

Escalation Criteria

  • !Kubernetes service account token with cluster-admin or high-privilege RBAC role (edit, admin on sensitive namespaces) was accessed — this represents full cluster compromise potential
  • !Azure Managed Identity token requested from a process running inside a container or web application context (e.g., nginx, java, python workers) — indicates server-side exploitation enabling cloud lateral movement
  • !OAuth consent grant for offline_access scope (refresh token issued) — adversary can maintain long-term access that survives password resets without recurring phishing
  • !Stolen token subsequently used from a geographically anomalous IP — confirmed token exfiltration and active use in a separate environment
  • !CI/CD pipeline token accessed during off-hours by an unexpected process — indicates supply chain compromise with potential to affect all deployment targets
  • !Multiple token vectors triggered on the same host within a short window (e.g., IMDS request + token cache access) — indicates systematic credential harvesting, not a single misconfiguration

Investigation Guide

Related Techniques

Forensic Artifacts

  • >Linux: /var/run/secrets/kubernetes.io/serviceaccount/token — the actual JWT service account token; decode with: cat token | cut -d. -f2 | base64 -d | python3 -m json.tool to inspect claims and expiration
  • >Linux: /proc/<PID>/environ — environment variables of the token-stealing process; may contain KUBERNETES_SERVICE_TOKEN, AZURE_CLIENT_SECRET, or similar injected credentials
  • >Windows: %USERPROFILE%\.azure\accessTokens.json and %USERPROFILE%\.azure\msal_token_cache.json — Azure CLI token caches including access and refresh tokens
  • >Windows: %USERPROFILE%\.aws\credentials and %USERPROFILE%\.aws\config — AWS credential files potentially containing STS temporary tokens
  • >Windows: %APPDATA%\GitHub CLI\hosts.yml — GitHub personal access tokens stored by gh CLI
  • >Cloud: Azure Activity Log (az monitor activity-log list) — all Azure Resource Manager operations performed by the stolen Managed Identity
  • >Cloud: AWS CloudTrail (aws cloudtrail lookup-events) — all API calls made using stolen instance role credentials, with source IP addresses
  • >Azure AD: AuditLogs table OperationName='Add OAuth2PermissionGrant' — full history of OAuth consent events including granted scopes and consenting user
  • >Azure AD: SigninLogs with AuthenticationDetails — token-based sign-ins lack password authentication step, identifiable by authentication method = 'oauth2 token' or 'service principal'

Tuning Guidance

Start by building an allowlist of legitimate IMDS-querying processes for each VM role in your environment. Azure VMs will have waagent.exe, WindowsAzureGuestAgent.exe, and monitoring agents; document these per image type. For Kubernetes, the set of legitimate token-reading processes is determined by your container runtime (containerd, CRI-O) and any operators deployed. For OAuth consent, configure Azure AD to require admin consent for all applications requesting high-privilege delegated permissions — this eliminates the user-consent phishing vector entirely and converts all consent grants into auditable admin actions. For CI/CD environments, exclude known pipeline agent processes (runner, agent, executor) by combining process name with parent process name for higher specificity. The IMDS detection will have a higher false positive rate on developer machines where engineers run tools like terraform, pulumi, or ansible that legitimately call IMDS — consider scoping this detection to production server and container workloads only, excluding developer workstation collections. For the OAuth hunt, set a 7-day rolling window and exclude known first-party Microsoft applications (by verifying the app's verified publisher status). Adjust ClusterCIDR in the K8s hunting query to match your actual pod/node network range.


Hunting Queries

Hunt for processes querying the cloud IMDS endpoint for the first time on a given device. A first-time IMDS caller that is not a known cloud agent is highly suspicious and may indicate an attacker attempting to obtain a managed identity token post-compromise. Compares last 24 hours against a 30-day baseline of known callers per device.

Hunting — KQL
kql
// Hunt: Processes querying IMDS that have never done so before on this device (first-time IMDS callers)
let HistoricalIMDS = DeviceNetworkEvents
| where Timestamp between (ago(30d) .. ago(24h))
| where RemoteIP == "169.254.169.254"
| summarize KnownCallers=make_set(InitiatingProcessFileName) by DeviceName;
DeviceNetworkEvents
| where Timestamp > ago(24h)
| where RemoteIP == "169.254.169.254"
| join kind=leftanti HistoricalIMDS on $left.DeviceName == $right.DeviceName, $left.InitiatingProcessFileName == $right.KnownCallers
| project Timestamp, DeviceName, AccountName, InitiatingProcessFileName, InitiatingProcessCommandLine, RemoteUrl
| sort by Timestamp desc
Hunting — SPL
spl
index=wineventlog sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=3 DestinationIp="169.254.169.254" earliest=-30d latest=-24h
| stats values(Image) as HistoricalCallers by host
| join type=left host [
    search index=wineventlog sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=3 DestinationIp="169.254.169.254" earliest=-24h
    | table host, Image, CommandLine, _time ]
| eval IsNew=if(NOT mvfind(HistoricalCallers, Image) >= 0, "NEW", "KNOWN")
| where IsNew="NEW"
| table _time, host, Image, CommandLine, IsNew

Hunt for OAuth applications that have received high-privilege consent from multiple users or from multiple source IPs. OAuth phishing campaigns (APT28/APT29 style) typically trick many users into granting the same malicious app access. A single app collecting consents from multiple users, especially across different IPs or short time windows, is a strong indicator of a coordinated OAuth phishing operation.

Hunting — KQL
kql
// Hunt: OAuth apps with high-privilege permissions consented to by multiple users or from anomalous IPs
AuditLogs
| where TimeGenerated > ago(7d)
| where OperationName has_any ("Consent to application", "Add OAuth2PermissionGrant")
| where Result == "success"
| extend User = tostring(InitiatedBy.user.userPrincipalName)
| extend IP = tostring(InitiatedBy.user.ipAddress)
| extend AppName = tostring(TargetResources[0].displayName)
| extend Scopes = tostring(TargetResources[0].modifiedProperties)
| where Scopes has_any ("Mail.Read", "offline_access", "Files.ReadWrite", "User.Read.All", "Directory.ReadWrite")
| summarize ConsentCount=count(), UniqueUsers=dcount(User), UniqueIPs=dcount(IP), UserList=make_set(User), IPList=make_set(IP) by AppName, Scopes
| where ConsentCount > 1 or UniqueIPs > 1
| sort by ConsentCount desc
Hunting — SPL
spl
index=o365 sourcetype="o365:management:activity" (Operation="Consent to application" OR Operation="Add OAuth2PermissionGrant")
  (ModifiedProperties="*Mail.Read*" OR ModifiedProperties="*offline_access*" OR ModifiedProperties="*Files.ReadWrite*" OR ModifiedProperties="*User.Read.All*")
earliest=-7d
| stats count as ConsentCount, dc(UserId) as UniqueUsers, values(UserId) as UserList, dc(ClientIP) as UniqueIPs, values(ClientIP) as IPList by AppDisplayName, ModifiedProperties
| where ConsentCount > 1 OR UniqueIPs > 1
| sort - ConsentCount

Hunt for Kubernetes service account tokens being accessed frequently or across multiple hosts, and for token-based authentication to Kubernetes APIs originating from outside the expected cluster IP range. Legitimate in-cluster service account usage is bounded by the pod network CIDR; token reuse from external IPs indicates the token has been extracted and is being used by an adversary from outside the cluster.

Hunting — KQL
kql
// Hunt: Kubernetes service account tokens used from outside the cluster IP range
// Identifies JWT tokens being used from IPs not associated with your cluster nodes
let ClusterCIDR = "10.0.0.0/8"; // Adjust to your cluster network range
AuditLogs
| where TimeGenerated > ago(7d)
| where Category == "ServicePrincipalSignInLogs" or OperationName has "kubernetes"
| extend CallerIP = tostring(InitiatedBy.app.ipAddress)
| extend ServicePrincipal = tostring(InitiatedBy.app.displayName)
| where isnotempty(CallerIP)
| where ipv4_is_in_range(CallerIP, ClusterCIDR) == false
| where ServicePrincipal has_any ("system:serviceaccount", "kubernetes")
| project TimeGenerated, ServicePrincipal, CallerIP, OperationName, Result
| sort by TimeGenerated desc
Hunting — SPL
spl
index=linux_audit type=SYSCALL syscall=openat
  (key="k8s_token_access" OR comm!="kubelet")
  name="/var/run/secrets/kubernetes.io/serviceaccount/token"
earliest=-7d
| stats count as AccessCount, dc(host) as UniqueHosts, values(comm) as Processes, values(pid) as PIDs by name, auid
| where AccessCount > 5 OR UniqueHosts > 1
| sort - AccessCount

Atomic Red Team Tests

Test 1 Query Azure IMDS Endpoint for Managed Identity Token
windows

Simulates an adversary who has compromised an Azure VM or container and queries the Instance Metadata Service (IMDS) to obtain a Managed Identity access token for the Azure Resource Manager API. This is a primary technique used in cloud lateral movement after initial access to a compute resource. On a machine without an assigned Managed Identity, the request will return a 400 error, but the network connection event is still generated for detection testing.

Command

powershell
powershell.exe -Command "$headers = @{'Metadata'='true'}; Invoke-RestMethod -Uri 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/' -Headers $headers -Method GET | ConvertTo-Json"

Expected Telemetry

Sysmon Event ID 3: Network Connection from powershell.exe to 169.254.169.254:80. DeviceNetworkEvents in MDE: RemoteIP=169.254.169.254, InitiatingProcessFileName=powershell.exe. The RemoteUrl field will contain the metadata identity path.

Expected Detection

Alert fires on Vector='IMDS Token Request'. KQL: IMDSTokenRequests sub-query matches powershell.exe querying 169.254.169.254 with identity path. SPL: Sysmon Event ID 3 with DestinationIp=169.254.169.254 and Image containing powershell.exe triggers the union branch.

Test 2 Read Kubernetes Service Account Token from Pod Filesystem
linux

Simulates an adversary who has gained execution within a Kubernetes pod and reads the auto-mounted service account token to use for direct Kubernetes API calls. This is a standard technique in container escape and lateral movement playbooks. The token is a JWT that can be decoded and used with kubectl or direct API calls.

Command

bash
cat /var/run/secrets/kubernetes.io/serviceaccount/token && echo '' && cat /var/run/secrets/kubernetes.io/serviceaccount/namespace

Expected Telemetry

Linux auditd syscall audit event for openat/read on /var/run/secrets/kubernetes.io/serviceaccount/token with the process name (cat or the shell). Sysmon for Linux Event ID 11 (if deployed) for file access. The token value (JWT) will be visible in any memory or command output capture.

Expected Detection

K8sTokenAccess sub-query matches FolderPath containing /var/run/secrets/kubernetes.io/serviceaccount and FileName=token where InitiatingProcessFileName=cat. SPL Sysmon Event ID 11 branch fires on TargetFilename matching kubernetes.io token path.

Test 3 Enumerate and Exfiltrate Azure CLI Token Cache
windows

Simulates a credential harvester reading the Azure CLI OAuth token cache from a compromised Windows workstation. LaZagne and similar tools perform this exact operation. The access token in this file can be used directly with az CLI commands or raw REST API calls without re-authenticating. This test only reads the file path — no token is transmitted anywhere.

Command

powershell
powershell.exe -Command "$tokenPath = "$env:USERPROFILE\.azure\msal_token_cache.json"; if (Test-Path $tokenPath) { $content = Get-Content $tokenPath -Raw; Write-Output "Token cache found: $($content.Length) bytes"; $content | Select-String -Pattern '\"secret\"' | Select-Object -First 3 } else { Write-Output 'Azure CLI token cache not found on this system' }"

Expected Telemetry

Sysmon Event ID 11: File access/creation event with TargetFilename matching *msal_token_cache.json and Image=powershell.exe. DeviceFileEvents in MDE: FileName=msal_token_cache.json, ActionType=FileRead or FileAccessed, InitiatingProcessFileName=powershell.exe.

Expected Detection

TokenCacheAccess sub-query fires on FileName matching msal_token_cache.json accessed by powershell.exe (not in LegitTokenOwners list). Vector='Azure OAuth Token Cache'. SPL Sysmon Event ID 11 branch fires on TargetFilename matching msal_token_cache. SuspicionScore=2.

Test 4 Register Malicious OAuth App and Simulate Consent Phishing Link
windows

Simulates the preparatory steps an adversary takes before an OAuth phishing campaign: registering an application in Azure AD, setting required permissions, and generating a consent URL that would be sent to target users. This tests the AuditLogs detection for new app registrations with high-privilege scopes. Requires az CLI authenticated with sufficient Azure AD permissions. IMPORTANT: Run only in a non-production test tenant.

Command

powershell
az ad app create --display-name "Test-Detection-App-T1528" --required-resource-accesses "[{\"resourceAppId\": \"00000003-0000-0000-c000-000000000000\", \"resourceAccess\": [{\"id\": \"570282fd-fa5c-430d-a7fd-fc8dc98a9dca\", \"type\": \"Scope\"}]}]" --reply-urls "https://localhost" 2>&1

Cleanup

powershell
az ad app delete --id $(az ad app list --display-name 'Test-Detection-App-T1528' --query '[0].appId' -o tsv) 2>&1

Expected Telemetry

Azure AD AuditLogs OperationName='Add application' followed by 'Update application' with permissions modification. The registered app will appear in AuditLogs with the requesting user's UPN and source IP. If a test user clicks the generated consent URL, AuditLogs will show OperationName='Consent to application' with the scopes granted.

Expected Detection

OAuthConsentGrants sub-query fires when consent is granted for the registered app if it includes high-privilege scopes. The app registration itself fires related detections in T1136 (Create Account) and application monitoring rules. SPL o365:management:activity branch fires on Operation='Consent to application'.

Related Detections

Detection Variants (1)

Different telemetry and tradecraft for the same technique — pick the one that matches the data you collect.