Detection rules › Kusto

AWSCloudTrail - Full Admin policy created and then attached to Roles, Users or Groups

Status
available
Severity
medium
Time window
14d
Group by
AccountName, AccountUPNSuffix, EventName, EventSource, PolicyName, RecipientAccountId, SourceIpAddress, UserIdentityArn, UserIdentityType
Source
github.com/Azure/Azure-Sentinel

Identifies creation of IAM policies that grant full administrative access and subsequent attachment to a role, user, or group. This sequence can be used to elevate a low-privilege identity to administrative access and should be investigated immediately. AWS IAM Policy Grammar: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html and AWS IAM API at https://docs.aws.amazon.com/IAM/latest/APIReference/API_Operations.html

MITRE ATT&CK coverage

TacticTechniques
Privilege EscalationT1484 Domain or Tenant Policy Modification

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body kusto

id: 826bb2f8-7894-4785-9a6b-a8a855d8366f
name: AWSCloudTrail - Full Admin policy created and then attached to Roles, Users or Groups
description: |
  Identifies creation of IAM policies that grant full administrative access and subsequent attachment to a role, user, or group. This sequence can be used to elevate a low-privilege identity to administrative access and should be investigated immediately.
  AWS IAM Policy Grammar: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html 
  and AWS IAM API at https://docs.aws.amazon.com/IAM/latest/APIReference/API_Operations.html
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: AWSS3
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1484
query: |
  let EventNameList = dynamic(["AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy"]);
  let createPolicy = dynamic(["CreatePolicy", "CreatePolicyVersion"]);
  let timeframe = 1d;
  let lookback = 14d;
  // Creating Master table with all the events to use with materialize for better performance
  let EventInfo = AWSCloudTrail
  | where TimeGenerated >= ago(lookback)
  | where EventName in (EventNameList) or EventName in (createPolicy)
  | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
  | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
  | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
  | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "");
  //Checking for Policy creation event with Full Admin Privileges since lookback period.
  let FullAdminPolicyEvents =  materialize(  EventInfo
  | where TimeGenerated >= ago(lookback)
  | where EventName in (createPolicy)
  | extend PolicyName = tostring(parse_json(RequestParameters).policyName)
  | extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
  | mvexpand Statement
  | extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource)
  | mvexpand Action
  | extend Action = tostring(Action)
  | where Effect =~ "Allow" and Action == "*" and Resource == "*"
  | distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, UserIdentityArn, UserIdentityUserName, RecipientAccountId, AccountName, AccountUPNSuffix
  | extend UserIdentityUserName = iff(isnotempty(UserIdentityUserName), UserIdentityUserName, tostring(split(UserIdentityArn,'/')[-1]))
  | project-rename StartTime = TimeGenerated  );
  let PolicyAttach = materialize(  EventInfo
  | where TimeGenerated >= ago(timeframe)
  | where EventName in (EventNameList)
  | extend PolicyName = tostring(split(tostring(parse_json(RequestParameters).policyArn),"/")[1])
  | summarize AttachEventCount=count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventSource, EventName,   UserIdentityType , UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, PolicyName
  | extend AttachEvent = pack("StartTime", StartTime, "EndTime", EndTime, "EventName", EventName, "UserIdentityType",   UserIdentityType, "AccountName", AccountName, "AccountUPNSuffix", AccountUPNSuffix, "RecipientAccountId", RecipientAccountId, "UserIdentityArn", UserIdentityArn, "SourceIpAddress", SourceIpAddress)
  | project EventSource, PolicyName, AttachEvent, RecipientAccountId, AccountName, AccountUPNSuffix, AttachEventCount
  );
  // Joining the list of PolicyNames and checking if it has been attached to any Roles/Users/Groups.
  // These Roles/Users/Groups will be Privileged and can be used by adversaries as pivot point for privilege escalation via multiple ways.
  FullAdminPolicyEvents
  | join kind=leftouter
  (
      PolicyAttach
  )
  on PolicyName
  | project-away PolicyName1
  | extend timestamp = StartTime
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  PolicyName: PolicyName
  EventName: EventName
  RecipientAccountId: RecipientAccountId
  UserIdentityArn: UserIdentityArn
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS Full Admin privilege escalation policy activity: {{PolicyName}} by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} for policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.5
kind: Scheduled

Stages and Predicates

Parameters

let EventNameList = dynamic(["AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy"]);
let createPolicy = dynamic(["CreatePolicy", "CreatePolicyVersion"]);
let timeframe = 1d;
let lookback = 14d;

Let binding: PolicyAttach

let PolicyAttach = materialize(  EventInfo
| where TimeGenerated >= ago(timeframe)
| where EventName in (EventNameList)
| extend PolicyName = tostring(split(tostring(parse_json(RequestParameters).policyArn),"/")[1])
| summarize AttachEventCount=count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventSource, EventName,   UserIdentityType , UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, PolicyName
| extend AttachEvent = pack("StartTime", StartTime, "EndTime", EndTime, "EventName", EventName, "UserIdentityType",   UserIdentityType, "AccountName", AccountName, "AccountUPNSuffix", AccountUPNSuffix, "RecipientAccountId", RecipientAccountId, "UserIdentityArn", UserIdentityArn, "SourceIpAddress", SourceIpAddress)
| project EventSource, PolicyName, AttachEvent, RecipientAccountId, AccountName, AccountUPNSuffix, AttachEventCount
);

Derived from EventNameList, timeframe, EventInfo.

Stage 1: source

EventInfo

Stage 2: where

where ...

Stage 3: where

where EventName in~ ("CreatePolicy", "CreatePolicyVersion")

Stage 4: extend

extend PolicyName

Stage 5: extend

extend Statement

Stage 6: mv-expand

mv-expand Statement

Stage 7: extend

extend Action, Effect, Resource

Stage 8: mv-expand

mv-expand Action

Stage 9: extend

extend Action

Stage 10: where

where Action == "*" and Effect =~ "Allow" and Resource == "*"

Stage 11: distinct

distinct AccountName, AccountUPNSuffix, EventName, PolicyName, RecipientAccountId, SourceIpAddress, TimeGenerated, UserIdentityArn, UserIdentityUserName

Stage 12: extend

extend UserIdentityUserName
UserIdentityUserName =
ifisnotempty(UserIdentityUserName)UserIdentityUserName
elsetostring(split(UserIdentityArn, '/')[(- 1)])

Stage 13: project-rename

project-rename

Stage 14: join

join kind=leftouter (...)

Stage 15: project-away

project-away PolicyName1

Stage 16: extend

extend timestamp

Stage 17: summarize

summarize by EventSource, EventName, UserIdentityType, UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, PolicyName

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
Actioneq
  • * transforms: cased
Effecteq
  • Allow
EventNamein
  • AttachGroupPolicy transforms: cased
  • AttachRolePolicy transforms: cased
  • AttachUserPolicy transforms: cased
  • CreatePolicy transforms: cased
  • CreatePolicyVersion transforms: cased
Resourceeq
  • * transforms: cased

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
AccountNamesummarize
AccountUPNSuffixsummarize
EventNamesummarize
EventSourcesummarize
PolicyNamesummarize
RecipientAccountIdsummarize
SourceIpAddresssummarize
UserIdentityArnsummarize
UserIdentityTypesummarize