AWS Stolen Credential Permission Enumeration

Capability that context does not already settle → relevant AWS API request → AWS service returns an authorization outcome

Discovery T1580 T1526 Detection difficulty: MEDIUM Prevalence: HIGH

An operator who obtains AWS credentials — from a leaked .env file, a public repository, an exposed CI secret, SSRF against the instance metadata service, or a compromised container — usually does not know what those credentials are permitted to do. AWS exposes no API returning a principal's effective permissions. Identity-based and resource-based allows are generally unioned; permissions boundaries, Organizations SCPs and RCPs, and session policies constrain the result, and any explicit deny overrides. Visibility into those layers varies by principal and account, so context or a readable policy may settle part of the effective set but not reliably all of it. Whatever it does not settle has to be tested: the operator calls the relevant API and the AWS service returns an authorization outcome. CloudTrail visibility is conditional: the service and operation must be supported, the relevant management or data event category and selector must be enabled, and that integration must record denied requests. Where CloudTrail emits an event, most services populate top-level errorCode, but AWS documents errorCode and errorMessage as optional and some report failures inside responseElements, so top-level-only ingestion has blind spots. The chokepoint sits in that conditional testing requirement rather than in the tooling or the speed, and holds whether the operator runs Pacu, enumerate-iam, CloudFox, the AWS CLI by hand, or a bespoke SDK script. How much testing it takes is situational — context, a readable identity policy, or a correct first guess can narrow it to a single call — so cross-namespace breadth and a denied-then-allowed transition are treated here as Hunt and Analyst heuristics for undirected enumeration rather than steps the operator cannot skip. T1580 and T1526 apply where the List, Get, and Describe calls actually discover services or infrastructure; failed permission tests alone do not instantiate either technique. Unit 42 observed the pattern directly in the 2024 large-scale .env extortion campaign: "At the beginning of every operation for the discovery phase in this campaign, attackers ran the GetCallerIdentity API call to verify the identity of the user or role assigned to the exposed IAM credential."

Attack Chokepoints 3 invariant stages

Applying the chokepoint framework to AWS Stolen Credential Permission Enumeration. Learn the framework →

Attacker controls (variables)
  • Tooling — Pacu, enumerate-iam, CloudFox, WeirdAAL, ScoutSuite/Prowler repurposed as offensive enumerators, the aws CLI driven by hand, or raw boto3/SDK scripts
  • User-Agent string — attacker-controlled. AWS SDKs and tools commonly expose application-ID or custom user-agent metadata, so tool-name matches such as "Pacu" or "Boto3" are not a durable signal
  • Source IP — commercial VPN, Tor, residential proxy, an attacker-controlled EC2 instance, or an instance inside the victim's own account (making the traffic AWS-internal)
  • Call pacing and jitter — probes can be spread over hours or days to defeat short windows
  • Region selection and ordering
  • Which service namespaces to probe, and in what sequence
  • Whether enumeration is driven through the API directly or through a hijacked console session
  • Read-only probing versus attempting state-changing calls
Attacker cannot control (chokepoints)
  • Effective permissions are not computable from outside AWS. Identity-based and resource-based allows are generally unioned; permissions boundaries, Organizations SCPs and RCPs, and session policies constrain the result, and any explicit deny overrides. A principal generally cannot read the SCPs applied to it or resource policies it has no grant on, and visibility into these layers varies by principal and account, so context may settle part of the effective set while anything it leaves open can only be established by attempting the action
  • Permission discovery has no quiet IAM oracle at the service boundary. iam:SimulatePrincipalPolicy requires its own permission and can itself be denied, while a direct probe still requires an AWS API request and service authorization outcome
  • For CloudTrail events that are emitted, errorCode, errorMessage, eventSource, eventTime, and the userIdentity block are authored by AWS rather than the caller. Event visibility is conditional because it depends on support for the service and operation, the selected event category, and whether that integration records denied requests
  • Each capability that context leaves unresolved costs at least one AWS API request and service outcome. CloudTrail is a conditional detection surface for that request, not part of the invariant itself
  • sts:GetCallerIdentity requires no IAM permission and therefore always succeeds for any valid credential — the one call in the sequence that cannot be denied, and therefore cannot be hidden behind a permission failure
1 Identity Orientation (Optional Research Anchor)
  • Operator holds valid, unexpired AWS credentials (long-term AKIA* key, temporary ASIA* session credentials, or a hijacked console session)
  • Operator lacks prior knowledge of the credential's effective permission set
  • Network path to AWS API endpoints (public internet; no inbound access to the victim environment is required)
  • CloudTrail coverage for the supported service operations targeted by the rules. Console event history automatically retains supported management events for 90 days, but the Athena queries need an explicitly created trail delivering to S3 plus an Athena table over that data. Management events are selected by default when a trail is created; S3 delivery, the table, and any required data-event selectors are not automatic
Input Valid credentials held; owning identity unknown
Chokepoint Identity-aware enumeration benefits from resolving whether the credential belongs to an IAM user or assumed role, because that changes which policy and trust APIs are relevant. sts:GetCallerIdentity is a common permissionless sequence anchor, but this stage is optional: generic permission probing can proceed without first resolving the identity.
Observable CloudTrail: eventSource sts.amazonaws.com, eventName GetCallerIdentity, appearing as the first-ever event for a given principal, or the first event from a sourceIPAddress not previously associated with that principal. Key the principal on useridentity.sessioncontext.sessionissuer.arn for assumed-role sessions and on useridentity.arn or useridentity.principalid otherwise, with accessKeyId as a last fallback: the ASIA* key rotates per session and an assumed-role ARN repeats only when its RoleSessionName is reused, so neither is a dependable long-term baseline identity and a history keyed on either looks empty. Frequently followed within seconds by iam:GetUser, iam:ListAttachedUserPolicies, iam:ListGroupsForUser, iam:ListUserPolicies, or iam:GetRole. On its own GetCallerIdentity is extremely common in legitimate SDK and CI workflows, so treat it as a sequence anchor for the later stages.
Why unavoidable
This stage can be bypassed. Identity-aware policy or trust enumeration needs the principal type, while a generic brute-force routine can probe blindly. Skipping orientation may increase the number of calls and denials, but it does not prevent permission testing.
  • AWS CloudTrail management events (eventSource sts.amazonaws.com)
  • AWS CloudTrail management events (eventSource iam.amazonaws.com)
Bypass risk: Operators who already know the identity, or tools that probe blindly, skip this stage. Treat it only as a Research-tier sequence anchor and never deploy it on its own.
CloudTrail — GetCallerIdentity as first observed call for a leaked access key
{ "eventVersion": "1.09", "userIdentity": { "type": "IAMUser", "principalId": "AIDAEXAMPLE1234ABCD", "arn": "arn:aws:iam::123456789012:user/svc-backup", "accountId": "123456789012", "accessKeyId": "AKIAEXAMPLE1234ABCD" }, "eventTime": "2026-08-09T14:22:03Z", "eventSource": "sts.amazonaws.com", "eventName": "GetCallerIdentity", "awsRegion": "us-east-1", "sourceIPAddress": "203.0.113.45", "userAgent": "aws-cli/2.15.30 Python/3.11.8 Linux/6.5.0 exe/x86_64", "readOnly": true, "eventType": "AwsApiCall", "managementEvent": true, "recipientAccountId": "123456789012" } Key signal: The svc-backup service account has a stable historical sourceIPAddress inside its own VPC NAT range. This call arrives from 203.0.113.45, never previously seen for this principal, and is the first event in a burst producing authorization failures across six service namespaces in the following ninety seconds.
View rule →
Capability unknown; identity may also be unresolved
2 Cross-Namespace Permission Probing
Input Capability unknown; identity may also be unresolved
Chokepoint Any capability the operator has not already established from context or a readable policy MUST be tested by calling the relevant AWS API and reading the outcome. Grants are per-service and per-action and the constraining layers are frequently invisible from the principal side, so there is no offline inference for the untested part of the set. How far that testing spreads is situational: context or a correct first guess may reduce it to a few calls, while an operator starting blind samples across namespaces because the grant could sit in any of several hundred services. This tier scores breadth as a heuristic for that undirected case.
Observable CloudTrail, aggregated per principal over a rolling window, where the principal is useridentity.sessioncontext.sessionissuer.arn for assumed-role sessions and useridentity.arn or useridentity.principalid otherwise, with accessKeyId as a last fallback: - errorCode in (AccessDenied, AccessDeniedException, UnauthorizedOperation, Client.UnauthorizedOperation, AuthorizationError, AuthFailure) — only these authorization and authentication failures count as denials, since throttling, validation, and missing-resource errors also populate errorCode - count of DISTINCT eventSource values carrying those errors >= 5 - denial ratio (authorization failures / all events by the principal in the window) >= 0.5 Legitimate automation runs a known, stable permission set and sits near a zero percent denial rate; where it does generate denials, they repeat on the same few API calls rather than spreading across namespaces, so the spread discriminates better than the raw count.
Why unavoidable
A capability the operator cannot establish from context has to be attempted, and the AWS service returns an authorization outcome. CloudTrail observes that request only when the service and operation are supported, the relevant event category is selected, and the integration records denied requests. What the operator influences is how much testing is needed, so breadth indicates undirected enumeration rather than being an unavoidable step: an operator starting blind cannot narrow the search without testing.
  • AWS CloudTrail management events (all eventSource values)
Bypass risk: Pacing defeats a short window. Running the same breadth-and-ratio logic over a twenty-four hour window helps at query cost, and the Analyst tier's novelty scoring helps against a low call rate inside whatever window is configured — but that window is itself one day, so multi-day pacing evades it too unless it is widened. Two gaps limit denial coverage: some List operations return HTTP 200 with an empty result set rather than AccessDenied when constrained by resource policy, and services reporting authorization failures in responseElements are invisible to a query reading errorCode alone.
CloudTrail — AccessDenied across six namespaces for one access key in 90 seconds
{ "eventVersion": "1.09", "userIdentity": { "type": "IAMUser", "principalId": "AIDAEXAMPLE1234ABCD", "arn": "arn:aws:iam::123456789012:user/svc-backup", "accountId": "123456789012", "accessKeyId": "AKIAEXAMPLE1234ABCD" }, "eventTime": "2026-08-09T14:22:31Z", "eventSource": "s3.amazonaws.com", "eventName": "ListBuckets", "awsRegion": "us-east-1", "sourceIPAddress": "203.0.113.45", "userAgent": "aws-cli/2.15.30 Python/3.11.8 Linux/6.5.0 exe/x86_64", "errorCode": "AccessDenied", "errorMessage": "User: arn:aws:iam::123456789012:user/svc-backup is not authorized to perform: s3:ListAllMyBuckets because no identity-based policy allows the s3:ListAllMyBuckets action", "readOnly": true, "eventType": "AwsApiCall", "managementEvent": true, "recipientAccountId": "123456789012" } Key signal: This is one of thirty-one authorization failures for principal arn:aws:iam::123456789012:user/svc-backup within ninety seconds, spanning s3, ec2, iam, lambda, secretsmanager, and rds, accounting for 0.94 of all events by the principal in the window. The same principal averaged zero denials per day across the preceding thirty days, because its production workload calls only s3:PutObject against one bucket.
View rule →
Probing has returned both allow and deny responses
3 Capability Confirmation
Input Probing has returned both allow and deny responses
Chokepoint Any capability the operator goes on to use MUST first have been established, and where context did not establish it that means a successful API call. A success in a namespace that has just denied the same actor is the clearest evidence enumeration located the exploitable path, so this tier scores that transition as follow-on confirmation. It remains a heuristic: an operator can stop after the denials, or succeed first with no prior denial.
Observable CloudTrail: one or more events with no error code (successful) in an eventSource that produced an authorization failure earlier in the window for the same principal (session issuer ARN, else caller ARN or principal ID) and the same actor, keyed more tightly on useridentity.accesskeyid so one session's denial under a shared role cannot join another session's success. Highest fidelity when the successful call is itself sensitive: iam:CreateAccessKey, iam:AttachUserPolicy, iam:UpdateAssumeRolePolicy, secretsmanager:GetSecretValue, ssm:GetParameter with WithDecryption true, or ec2:CreateSnapshot, all management events. s3:GetObject against a bucket never previously read by that principal is an equally strong signal, but it is an object-level data event and is absent unless a data-event selector covers that bucket, so treat it as optional coverage. The correlation stands on management events alone.
Why unavoidable
Enumeration can finish with only denials, but any capability the operator actually uses still requires a successful AWS API request and service outcome. CloudTrail visibility for that action depends on service and operation support, event-category selection, and whether the integration records the relevant result. The denied-then-allowed ordering is not required to complete permission enumeration; it is scored here as the highest-fidelity confirmation available when it occurs, and an operator who already knew where the grant was skips it.
  • AWS CloudTrail management events (all eventSource values)
  • AWS CloudTrail S3 data events (optional — adds object-level successes such as s3:GetObject where a data-event selector covers the bucket)
Bypass risk: A credential whose very first probed namespace succeeds produces no preceding denial there and will not match a strict same-namespace correlation; widen it to "any denial by this actor in the window" to catch that case, at some cost in precision. A service reporting authorization failure in responseElements rather than errorCode is also read as a success by an errorCode IS NULL test, so normalize those first.
View rule →

Variations 4 variants tracked

Tools and methods that exploit this chokepoint. The list grows. The chokepoint doesn't change.

Pacu 2018-Q3 Active
Rhino Security Labs' AWS exploitation framework. The iam__enum_permissions module reads the principal's own policies where permitted; iam__bruteforce_permissions falls back to calling large numbers of read-only APIs and recording which ones are refused. Observed deployed on compromised containers in Sysdig's SCARLETEEL 2.0 reporting, where operators installed the AWS CLI and Pacu onto exploited workloads and configured them with stolen keys.
Executed after set_keys with the stolen credential. The brute-force module exists precisely because the enumeration path is blocked when the credential lacks iam:Get* on itself — it is the empirical fallback the chokepoint predicts.
run iam__bruteforce_permissions
  • CloudTrail: high-volume errorCode AccessDenied spanning many eventSource values for one principal
  • CloudTrail: sts:GetCallerIdentity preceding the burst
Same chokepoint: set_keys and GetCallerIdentity (orientation) → iam__bruteforce_permissions issuing read-only calls across namespaces (cross-namespace probing) → operator pivots to the namespace that returned success (capability confirmation)
Source: github.com →
enumerate-iam 2019-Q1 Active
Andres Riancho's tool does nothing but the chokepoint behavior: it iterates a generated list of non-destructive API calls across the AWS SDK surface and reports which ones the supplied credential is permitted to make. With no context to work from it carries no inference logic at all, which is the undirected case this chokepoint describes.
Run directly against a leaked long-term key, typically immediately after the key is recovered from a public repository, exposed .env file, or CI log.
python enumerate-iam.py --access-key AKIAEXAMPLE1234ABCD --secret-key <redacted>
  • CloudTrail: hundreds to thousands of denied read-only calls in a short window from one principal
  • CloudTrail: authorization-failure ratio approaching 1.0 across dozens of distinct eventSource values
Same chokepoint: GetCallerIdentity (orientation) → bulk read-only API sweep generating mass denials (cross-namespace probing) → tool reports the permitted call list (capability confirmation)
Source: github.com →
CloudFox 2022-Q4 Active
Bishop Fox's tool is oriented toward answering "what can I do with this access" rather than producing an asset inventory, so its call pattern is broad by design. Denials are expected and tolerated by the tool, which continues sweeping other namespaces rather than stopping on failure — producing exactly the breadth signature this chokepoint targets.
Run against a configured profile holding stolen credentials, typically as the first broad sweep before targeted exploitation.
cloudfox aws --profile stolen all-checks
  • CloudTrail: Describe*/List*/Get* calls across compute, storage, identity, and secrets namespaces
  • CloudTrail: mixed success and AccessDenied from a single principal in one window
Same chokepoint: GetCallerIdentity (orientation) → all-checks sweep across namespaces (cross-namespace probing) → operator targets the namespaces that returned data (capability confirmation)
Source: github.com →
Manual AWS CLI enumeration 2015-Q1 Active
The lowest-sophistication variant and the one that defeats every tool-signature detection, because the only binary involved is the official AWS CLI. Unit 42's 2024 extortion campaign reporting documents operators running GetCallerIdentity at the start of every discovery phase and then enumerating across IAM, STS, S3, and SES to establish what the exposed credential permitted.
Typed by hand or wrapped in a short shell loop immediately after harvesting keys from an exposed .env file or public repository.
aws sts get-caller-identity && aws s3 ls && aws iam list-users && aws ec2 describe-instances
  • CloudTrail: userAgent shows the stock aws-cli string — indistinguishable from legitimate administration
  • CloudTrail: denials spread across namespaces despite low total call volume
Same chokepoint: aws sts get-caller-identity (orientation) → sequential per-service list/describe calls generating denials (cross-namespace probing) → operator repeats the call that succeeded (capability confirmation)
Source: unit42.paloaltonetworks.com →

Detection Strategy

Rules organized by the chokepoint stage they detect. Each stage has one or more rules at different maturity levels.

1 Identity Orientation (Optional Research Anchor)
Establish baseline visibility into identity-orientation calls and denial rate...
Research High FP
Goal
Establish baseline visibility into identity-orientation calls and denial rates per principal
Log Sources
  • AWS CloudTrail management events
FP Rate
High
Use Case
Threat research and baselining before deploying the Hunt tier
Surface every sts:GetCallerIdentity event alongside a per-principal count of authorization failures, grouping on the session issuer ARN for assumed-role sessions so a role's history survives session rotation. Only the authorization and authentication error codes count as failures. Characterises what normal looks like — which principals call GetCallerIdentity routinely, which service accounts carry a chronic denial rate, and what the baseline distinct-namespace spread is — so use it for baselining rather than alerting.
Sigma Rule - Research Level
title: AWS Identity Orientation and Authorization Failure Baseline
id: 3c1f7a92-6b04-4d55-9e88-2a0c5f31b7de
status: experimental
description: >
  Baselining rule for visibility. Surfaces sts:GetCallerIdentity and CloudTrail
  authorization failures so the Hunt and Analyst thresholds can be derived from local data.
  Deploy it to characterise the environment, then tune the higher tiers from what it shows.
  Answer three questions before deploying the higher tiers:
    1. Which principals call GetCallerIdentity routinely, and from which source IPs?
    2. Which principals carry chronic authorization failures, and on which
       (eventSource, eventName) pairs? That list becomes the Hunt tier allowlist.
    3. What is the normal distinct-namespace denial spread per principal? Most legitimate
       principals sit at 0-1. Re-derive the Hunt threshold from this, do not assume 5.
  Choosing the principal key: baseline on the identity that issued the session. A temporary
  ASIA* access key rotates per session, and an assumed-role ARN embeds a RoleSessionName that
  repeats only when the caller reuses it, so neither is a dependable long-term baseline
  identity. All three tiers key on COALESCE(useridentity.sessioncontext.sessionissuer.arn,
  useridentity.arn, useridentity.principalid, useridentity.accesskeyid) — the session issuer
  ARN (the role) for AssumedRole sessions, the caller ARN or principal ID otherwise, with
  accessKeyId as a last fallback. Keep the expression identical across tiers so baselines,
  aggregations, and joins agree on what a principal is.
  Only authorization and authentication failures count as denials — AccessDenied,
  AccessDeniedException, UnauthorizedOperation, Client.UnauthorizedOperation,
  AuthorizationError, AuthFailure. Throttling, validation, and missing-resource errors are
  also written to errorCode and say nothing about the caller's permissions, so counting every
  non-empty errorCode as probing inflates the denial ratio with application noise. Total event
  counts stay as the denominator. AWS documents errorCode and errorMessage as optional record
  fields, and some services report authorization failures inside responseElements rather
  than the top-level errorcode column this query reads. CloudTrail visibility also depends on
  support for the service and operation, the selected event category, and whether that
  integration records denied requests. Confirm those conditions before relying on the count.
  Avoid building detection on userAgent. AWS SDKs and tools commonly expose application-ID or
  custom user-agent metadata, so tool-name matches such as "Pacu" or "Boto3" are not durable.
  Baseline query (Athena). It needs a CloudTrail trail delivering management events to S3 and
  an Athena table over that data. Management events are selected by default when a trail is
  created, but the S3 delivery and the Athena table are not created automatically; console
  event history is automatic and covers 90 days, but is not queryable this way.
    WITH ct AS (
      SELECT COALESCE(useridentity.sessioncontext.sessionissuer.arn,
                      useridentity.arn,
                      useridentity.principalid,
                      useridentity.accesskeyid)                       AS principal,
             eventsource,
             eventname,
             sourceipaddress,
             useridentity.accesskeyid                                 AS access_key,
             COALESCE(errorcode, '') IN (
               'AccessDenied', 'AccessDeniedException', 'UnauthorizedOperation',
               'Client.UnauthorizedOperation', 'AuthorizationError', 'AuthFailure'
             )                                                        AS auth_denied
      FROM cloudtrail_logs
      WHERE from_iso8601_timestamp(eventtime) > now() - INTERVAL '30' DAY
    )
    SELECT principal,
           count(*)                                                   AS total_events,
           count_if(auth_denied)                                      AS auth_denials,
           count(DISTINCT CASE WHEN auth_denied THEN eventsource END) AS denied_namespaces,
           array_agg(DISTINCT concat(eventsource, ':', eventname))
             FILTER (WHERE auth_denied)                               AS denied_pairs,
           count_if(eventname = 'GetCallerIdentity')                  AS orientation_calls,
           count(DISTINCT sourceipaddress)                            AS src_ips,
           count(DISTINCT access_key)                                 AS session_keys
    FROM ct
    GROUP BY 1
    ORDER BY total_events DESC
  This tier keeps service and chronic-denial principals in the results, since identifying them
  is the point, and nothing here needs data-event logging.
references:
  - https://unit42.paloaltonetworks.com/large-scale-cloud-extortion-operation/
  - https://attack.mitre.org/techniques/T1580/
  - https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
  - https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-record-contents.html
  - https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-unsupported-aws-services.html
  - https://docs.aws.amazon.com/athena/latest/ug/cloudtrail-logs.html
author: '@NovaSky0x1'
date: 2026-08-09
tags:
  - attack.discovery
  - attack.t1580
  - attack.t1526
  - detection.maturity.research
logsource:
  product: aws
  service: cloudtrail
detection:
  selection_orientation:
    eventSource: 'sts.amazonaws.com'
    eventName: 'GetCallerIdentity'
  selection_denied:
    errorCode:
      - 'AccessDenied'
      - 'AccessDeniedException'
      - 'UnauthorizedOperation'
      - 'Client.UnauthorizedOperation'
      - 'AuthorizationError'
      - 'AuthFailure'
  condition: selection_orientation or selection_denied
falsepositives:
  - Expected at this tier. GetCallerIdentity is routine SDK, CI/CD, and Terraform behaviour
  - AWS Config rules and drift detection generating chronic denials on a fixed API set
  - Backup and inventory agents probing services they are not permitted to read
  - Developers running exploratory CLI commands against accounts they legitimately hold
level: informational
2 Cross-Namespace Permission Probing
Denial breadth across service namespaces for a single principal in a rolling ...
Hunt Med FP
Goal
Denial breadth across service namespaces for a single principal in a rolling window
Log Sources
  • AWS CloudTrail management events
FP Rate
Medium
Use Case
Scheduled hunt over CloudTrail, or a tuned detection once the chronic-denial allowlist is built
Aggregate authorization failures per principal over a rolling window, keyed on the session issuer ARN for assumed-role sessions since an ASIA* key rotates per session and an assumed-role ARN repeats only when the RoleSessionName is reused. Alert where those failures span five or more distinct eventSource values and account for at least 0.5 of the principal's events in the window, counting only AccessDenied, AccessDeniedException, UnauthorizedOperation, Client.UnauthorizedOperation, AuthorizationError, and AuthFailure. Exclude principals on a documented chronic-denial allowlist (drift detection, Config rules, backup agents), applying it inside the Athena query too since the Sigma placeholder does not affect it. Breadth is a heuristic for undirected enumeration rather than a requirement of the technique: a principal denied a hundred times on one API is usually misconfigured, while one denied five times across five namespaces is mapping its own permissions.
Sigma Rule - Hunt Level
title: AWS Credential Permission Probing — Denial Breadth Across Service Namespaces
id: 8d4b21e6-70af-4c19-b3a6-9f5c2e6188a4
status: experimental
description: >
  Detects one principal generating authorization failures across five or more distinct
  service namespaces in a rolling window, with an authorization-failure ratio of at least
  0.5 against all events by that principal in the window. That is the shape of an operator
  mapping a credential whose scope they do not know.
  Breadth is a heuristic rather than an invariant: an operator with useful context, a readable
  identity policy, or a correct first guess can settle capability in a handful of calls, so
  this tier targets the generic, undirected case. Within it the distinct-namespace count
  discriminates better than raw denial volume, since a principal denied 200 times on one API
  is almost always misconfigured while one denied five times across five namespaces is mapping
  its own permissions.
  Choosing the principal key: aggregate on the identity that issued the session, using
  COALESCE(useridentity.sessioncontext.sessionissuer.arn, useridentity.arn,
  useridentity.principalid, useridentity.accesskeyid) as the Research and Analyst tiers do, so
  Research-tier allowlists apply unchanged. A temporary ASIA* key rotates per session and an
  assumed-role ARN repeats only when its RoleSessionName is reused, so grouping on either can
  split one operator's probing into single-event groups that never reach the threshold of 5.
  Populate filter_chronic_principals with session issuer ARNs for the same reason.
  Only authorization and authentication failures count toward the namespace breadth and the
  ratio numerator — AccessDenied, AccessDeniedException, UnauthorizedOperation,
  Client.UnauthorizedOperation, AuthorizationError, AuthFailure. Throttling, validation, and
  missing-resource errors also populate errorCode and carry no permission information, so
  counting every non-empty errorCode inflates both measures with application noise. The
  denominator stays every event by the principal in the window, successful or not. Services
  that report authorization failures inside responseElements rather than the top-level
  errorcode column are a blind spot. CloudTrail visibility also depends on support for the
  service and operation, the selected event category, and whether that integration records
  denied requests.
  Distinct-namespace counts and ratios cannot be expressed in Sigma, so the detection block
  below is a partial stub firing on any single authorization failure; deploy the query for
  production. It needs a CloudTrail trail delivering management events to S3 and an Athena
  table over that data — management events are selected by default when a trail is created,
  but the S3 delivery and the table are not. No data-event logging is needed at this tier.
  Athena:
    WITH ct AS (
      SELECT COALESCE(useridentity.sessioncontext.sessionissuer.arn,
                      useridentity.arn,
                      useridentity.principalid,
                      useridentity.accesskeyid)             AS principal,
             eventsource,
             COALESCE(errorcode, '') IN (
               'AccessDenied', 'AccessDeniedException', 'UnauthorizedOperation',
               'Client.UnauthorizedOperation', 'AuthorizationError', 'AuthFailure'
             )                                              AS auth_denied
      FROM cloudtrail_logs
      WHERE from_iso8601_timestamp(eventtime) > now() - INTERVAL '15' MINUTE
        AND useridentity.type <> 'AWSService'   -- matches filter_service_principals below
    )
    SELECT principal,
           count(*)                                                   AS total_events,
           count_if(auth_denied)                                      AS auth_denials,
           count(DISTINCT CASE WHEN auth_denied THEN eventsource END) AS denied_namespaces
    FROM ct
    GROUP BY 1
    HAVING count(DISTINCT CASE WHEN auth_denied THEN eventsource END) >= 5
       AND count_if(auth_denied) * 1.0 / count(*) >= 0.5
  Before production: re-derive both thresholds from the Research tier, build the chronic-denial
  allowlist, and add those principal exclusions to the Athena WHERE clause by hand, since
  filter_chronic_principals below constrains only the Sigma stub. Config rules, drift
  detection, and backup agents are the dominant false positives and they are stable and
  enumerable.
  Pacing defeats a 15-minute window; widening it to 24 hours catches slower operators at
  query cost. The Analyst tier's novelty scoring helps against a low call rate inside its own
  window, but that window is one day, so multi-day pacing evades it too.
references:
  - https://unit42.paloaltonetworks.com/large-scale-cloud-extortion-operation/
  - https://sysdig.com/blog/scarleteel-2-0/
  - https://github.com/andresriancho/enumerate-iam
  - https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic.html
  - https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-record-contents.html
  - https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-unsupported-aws-services.html
  - https://docs.aws.amazon.com/athena/latest/ug/cloudtrail-logs.html
author: '@NovaSky0x1'
date: 2026-08-09
tags:
  - attack.discovery
  - attack.t1580
  - attack.t1526
  - detection.maturity.hunt
logsource:
  product: aws
  service: cloudtrail
detection:
  # Distinct-namespace and ratio aggregation cannot be expressed in Sigma.
  # Use the Athena query above for production. This stub is a partial signal only.
  selection:
    errorCode:
      - 'AccessDenied'
      - 'AccessDeniedException'
      - 'UnauthorizedOperation'
      - 'Client.UnauthorizedOperation'
      - 'AuthorizationError'
      - 'AuthFailure'
  filter_chronic_principals:
    # PENDING: populate from the Research-tier baseline before production, and mirror the
    # same exclusions into the Athena CTE. Use session issuer ARNs (the role), not session
    # ARNs or ASIA* keys.
    # userIdentity.sessionContext.sessionIssuer.arn|contains:
    #   - 'role/aws-config-role'
    #   - 'role/backup-inventory-agent'
    userIdentity.arn: ''  # placeholder — remove and populate before production
  filter_service_principals:
    userIdentity.type: 'AWSService'
  condition: selection and not filter_chronic_principals and not filter_service_principals
falsepositives:
  - AWS Config rules and drift detection probing resources they cannot read — chronic, stable, allowlistable
  - Backup and inventory agents sweeping services outside their grant
  - Terraform or CloudFormation against a partially-provisioned role, especially on first apply
  - Newly deployed service accounts before policies are finalised — expect a burst, then silence
  - CSPM and security scanners operating with intentionally reduced permissions
level: medium
3 Capability Confirmation
Novel-denial breadth followed by a first success in a just-denied namespace
Analyst Low FP
Goal
Novel-denial breadth followed by a first success in a just-denied namespace
Log Sources
  • AWS CloudTrail management events
  • AWS CloudTrail S3 data events (optional — extends the success half to object-level reads where a data-event selector covers the bucket)
FP Rate
Low
Use Case
Production alerting in environments with a stable thirty-day CloudTrail baseline
Correlate the denied-to-allowed transition and score on novelty. Fire where a principal generates authorization failures on (eventSource, eventName) tuples never previously observed for it — successful or failed — within a thirty-day baseline, across three or more distinct namespaces, and subsequently issues a successful call in a namespace that denied it earlier in the same window. Counting prior failures in the baseline keeps a routinely denied tuple from reading as novel every day. Baseline on the session issuer ARN, since a baseline keyed on the ASIA* key is empty on every new session, and join denial to success on a tighter actor key (access key first) so one session's denial under a shared role cannot join another session's success. Novelty helps against a low call rate inside the observation window, which is one day here, so multi-day pacing still evades it unless that window is widened at query cost. Apply the chronic-principal allowlist inside the Athena query as well as the Sigma stub. Breadth and the transition are heuristics, and an operator can skip either.
Sigma Rule - Analyst Level
title: AWS Capability Confirmation — Novel Denial Breadth Followed by Success in a Denied Namespace
id: b6e09f34-25cd-4a71-8c02-71d4a3e5f9b8
status: experimental
description: >
  Fires when a principal is denied on (eventSource, eventName) pairs it has never been observed
  calling in 30 days, across three or more namespaces, and then succeeds in a namespace that
  just denied it — the clearest available evidence that an operator located an exploitable path.
  Both signals are heuristics rather than invariants. Permission enumeration does not require
  cross-namespace breadth when context or a readable policy narrows the search, and it does not
  require a denied-then-allowed transition, since an operator can stop after the denials or
  succeed on the first call attempted. What holds is that any capability not already known from
  context or a readable policy has to be tested through the relevant AWS API, and the AWS
  service returns an authorization outcome. CloudTrail sees the request only when the service
  and operation are supported, the relevant event category is selected, and that integration
  records denied requests.
  The baseline holds every (principal, eventSource, eventName) tuple observed in the preceding
  30 days, successful or failed. Restricting it to successes would make a routinely denied
  tuple — a Config rule probing a resource it can never read — look novel every day.
  Two keys, deliberately. The 30-day baseline uses a stable principal (session issuer ARN
  first, then caller ARN, principal ID, access key) so a role's history survives session
  turnover, while the denial-to-success correlation additionally joins on an actor key (access
  key first, since a temporary credential's ASIA* key is unique to one session, then caller ARN
  or principal ID, then the stable principal) so a denial in one session under a shared role
  cannot join a different session's success. Neither an ASIA* key nor an assumed-role ARN is a
  dependable long-term baseline identity: the key rotates per session, and the ARN embeds a
  RoleSessionName that repeats only when the caller reuses it.
  A novel denial means an authorization or authentication failure — AccessDenied,
  AccessDeniedException, UnauthorizedOperation, Client.UnauthorizedOperation,
  AuthorizationError, AuthFailure. Throttling, validation, and missing-resource errors carry no
  permission information and do not count toward the three-namespace breadth. Success is the
  absence of any error code, and errorcode is NULL on success in the AWS sample CloudTrail
  table, so it must be tested with IS NULL rather than = ''. Services reporting authorization
  failures inside responseElements rather than the top-level errorcode column are a blind spot
  for the novel-denial half and can also be misread as successes by that test, so normalize
  them into errorcode first.
  Fidelity rises sharply when the successful call is itself sensitive — CreateAccessKey,
  AttachUserPolicy, UpdateAssumeRolePolicy, GetSecretValue, or CreateSnapshot, all management
  events. s3:GetObject against a bucket this principal has never read is a strong signal too,
  but it is an object-level data event, present only where a data-event selector covers that
  bucket, so treat it as optional coverage and otherwise leave it out of the selection below.
  Baseline comparison and cross-event correlation cannot be expressed in Sigma, so the stub
  below covers only the sensitive-success half; deploy the query for production. It needs a
  CloudTrail trail delivering management events to S3 and an Athena table over that data —
  management events are selected by default when a trail is created; the S3 delivery and the
  table are not. Athena:
    WITH ct AS (                             -- both keys and the denial test applied once
      SELECT COALESCE(useridentity.sessioncontext.sessionissuer.arn,
                      useridentity.arn,
                      useridentity.principalid,
                      useridentity.accesskeyid)             AS principal,
             COALESCE(useridentity.accesskeyid,
                      useridentity.arn,
                      useridentity.principalid,
                      useridentity.sessioncontext.sessionissuer.arn) AS actor_key,
             useridentity.arn AS identity_arn, eventsource, eventname,
             sourceipaddress, errorcode,
             from_iso8601_timestamp(eventtime)              AS ts,
             COALESCE(errorcode, '') IN (
               'AccessDenied', 'AccessDeniedException', 'UnauthorizedOperation',
               'Client.UnauthorizedOperation', 'AuthorizationError', 'AuthFailure'
             )                                              AS auth_denied
      FROM cloudtrail_logs
      WHERE from_iso8601_timestamp(eventtime) > now() - INTERVAL '31' DAY
        AND useridentity.type <> 'AWSService'  -- matches filter_service_principals below
    ),
    baseline AS (            -- every tuple seen for this principal, successful or denied
      SELECT DISTINCT principal, eventsource, eventname, 1 AS seen
      FROM ct
      WHERE ts BETWEEN now() - INTERVAL '31' DAY AND now() - INTERVAL '1' DAY
    ),
    novel_denials AS (                       -- auth failures on never-before-seen tuples
      SELECT c.principal, c.actor_key, c.eventsource, min(c.ts) AS first_denial
      FROM ct c
      LEFT JOIN baseline b
        ON  b.principal = c.principal AND b.eventsource = c.eventsource
        AND b.eventname = c.eventname
      WHERE c.ts > now() - INTERVAL '1' DAY AND c.auth_denied AND b.seen IS NULL
      GROUP BY 1, 2, 3
    ),
    probing AS (                             -- actors novel-denied in >= 3 namespaces
      SELECT principal, actor_key FROM novel_denials
      GROUP BY 1, 2
      HAVING count(DISTINCT eventsource) >= 3
    )
    SELECT s.ts, s.identity_arn, s.principal, s.actor_key, s.eventsource, s.eventname,
           s.sourceipaddress, d.first_denial
    FROM ct s
    JOIN novel_denials d          -- same actor, same namespace that denied it
      ON  d.principal = s.principal AND d.actor_key = s.actor_key
      AND d.eventsource = s.eventsource
    JOIN probing p
      ON  p.principal = s.principal AND p.actor_key = s.actor_key
    WHERE s.ts > now() - INTERVAL '1' DAY
      AND s.ts > d.first_denial              -- success after the denial
      AND (s.errorcode IS NULL OR s.errorcode = '')
    ORDER BY s.ts
  Requires a stable 30-day CloudTrail history; expect elevated false positives in younger
  environments or right after a large deployment change. Novelty helps against a low call rate
  inside the observation window, but that window is one day here, so multi-day pacing evades it
  unless the window is widened at query cost. Apply the environment's chronic and
  legitimate-principal allowlist inside the Athena CTE before production, since
  filter_legit_software below constrains only the Sigma stub. Avoid keying on userAgent.
references:
  - https://unit42.paloaltonetworks.com/large-scale-cloud-extortion-operation/
  - https://sysdig.com/blog/scarleteel-2-0/
  - https://github.com/RhinoSecurityLabs/pacu
  - https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic.html
  - https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-record-contents.html
  - https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-unsupported-aws-services.html
  - https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html
  - https://docs.aws.amazon.com/athena/latest/ug/cloudtrail-logs.html
author: '@NovaSky0x1'
date: 2026-08-09
tags:
  - attack.discovery
  - attack.t1580
  - attack.t1526
  - detection.maturity.analyst
logsource:
  product: aws
  service: cloudtrail
detection:
  # Baseline anti-join and denied-to-allowed correlation cannot be expressed in Sigma.
  # Use the Athena query above for production. This stub covers the sensitive-success
  # half only and must be correlated with the novel-denial condition to reach the
  # stated fidelity. Every eventName below is a management event; adding data-event
  # names such as GetObject requires an S3 object-level data-event selector.
  selection_sensitive_success:
    eventName:
      - 'CreateAccessKey'
      - 'AttachUserPolicy'
      - 'AttachRolePolicy'
      - 'PutUserPolicy'
      - 'UpdateAssumeRolePolicy'
      - 'CreateLoginProfile'
      - 'UpdateLoginProfile'
      - 'GetSecretValue'
      - 'CreateSnapshot'
      - 'ModifySnapshotAttribute'
  filter_failed:
    errorCode|exists: true
  filter_service_principals:
    userIdentity.type: 'AWSService'
  filter_legit_software:
    # PENDING: populate with the approved administrative and automation principals that
    # legitimately call these APIs — IaC deployment roles, break-glass roles, IAM
    # provisioning pipelines, backup services taking snapshots — and mirror the same
    # exclusions into the Athena CTE. Key on the session issuer ARN (the role), not on
    # session ARNs or ASIA* access keys.
    # userIdentity.sessionContext.sessionIssuer.arn|contains:
    #   - 'role/terraform-deploy'
    #   - 'role/iam-provisioning-pipeline'
    userIdentity.arn: ''  # placeholder — remove and populate before production
  condition: selection_sensitive_success and not filter_failed and not filter_service_principals and not filter_legit_software
falsepositives:
  - Break-glass or IR administrators exploring an unfamiliar account under legitimate authority
  - Newly onboarded engineers whose principal has no 30-day baseline yet
  - Migration and refactoring work legitimately calling APIs a principal has never called
  - Penetration tests and red-team engagements — check the engagement window before escalating
  - Environments with less than 30 days of CloudTrail history
level: high

OSINT Pivots

URLScan page.url:"/.env"
Finds .env files served publicly over HTTP — the exact harvest vector behind the 2024 Unit 42 extortion campaign, where operators pulled AWS keys straight from exposed environment files and began enumerating within minutes. Pivot on your own domains to find the exposure before an operator does.
Shodan http.html:"AWS_SECRET_ACCESS_KEY"
Surfaces hosts serving AWS secret material directly in HTTP responses — misconfigured app servers, directory listings, and debug endpoints that leak environment content. Complements the URLScan pivot by finding exposure Shodan crawled but no one submitted.
GitHub Code Search "aws_secret_access_key" path:.env
Committed .env files carrying credential material — the dominant public-repo exposure path, and the one that feeds opportunistic mass enumeration. High volume but mixed signal: many hits are empty placeholders (AWS_SECRET_ACCESS_KEY=) in example files. Use the .tfvars pivot below for higher live-credential density.
GitHub Code Search "AWS_SECRET_ACCESS_KEY" path:.github/workflows
Hardcoded credentials in CI workflow definitions rather than repository secrets. These leak with the repo and typically belong to build roles with unusually broad permissions, making them high-value enumeration targets.
GitHub Code Search "AKIA" path:*.tfvars
Terraform variable files committed with live access key IDs. The highest-signal pivot of this set — hits are overwhelmingly real key material rather than placeholders, and IaC credentials generally hold provisioning rights, so a compromised one has an enumeration surface spanning most of the account.

References