Detection rules › Kusto
AWSCloudTrail - Privilege escalation via Lambda policy
Detects inline IAM policy updates that grant broad AWS Lambda permissions, including combinations that can link Lambda with other services such as DynamoDB. These permissions can provide execution control and support privilege escalation paths in the cloud environment.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- AWS IAM Access Key Compromise Detection (Panther)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWSCloudTrail - Privilege escalation via CloudFormation policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD DynamoDB policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD IAM policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD KMS policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD Lambda policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD S3 policy (Kusto)
Rule body
id: 8e01c41d-bd4c-4bbe-aed5-18592735052d
name: AWSCloudTrail - Privilege escalation via Lambda policy
description: |
Detects inline IAM policy updates that grant broad AWS Lambda permissions, including combinations that can
link Lambda with other services such as DynamoDB. These permissions can provide execution control and support
privilege escalation paths in the cloud environment.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1098.003
query: |
AWSCloudTrail
| where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
| 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), Condition = tostring(parse_json(Statement).Condition)
| extend Action = tostring(Action)
| where Effect =~ "Allow" and ((((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "lambda:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "lambda:CreateFunction" and Action contains "lambda:InvokeFunction") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "lambda:Create*" and Action contains "lambda:Invoke*")) or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "lambda:*" and Action contains "dynamodb:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "lambda:CreateFunction" and Action contains "lambda:CreateEventSourceMapping" and Action contains "dynamodb:PutItem" and Action contains "dynamodb:CreateTable") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "lambda:Create*" and Action contains "dynamodb:Put*" and Action contains "dynamodb:Create*")) and Resource == "*" and Condition == ""
| 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]), "")
| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
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
UserIdentityArn: UserIdentityArn
RecipientAccountId: RecipientAccountId
alertDetailsOverride:
alertDisplayNameFormat: 'AWS Lambda privilege escalation policy update by {{AccountName}}'
alertDescriptionFormat: 'Detected {{EventName}} Event, updating inline Lambda escalation policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
AWSCloudTrail
Stage 2: where
| where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
Stage 3: extend
| extend PolicyName = tostring(parse_json(RequestParameters).policyName)
Stage 4: extend
| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
Stage 5: mv-expand
| mvexpand Statement
Stage 6: extend
| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
Stage 7: extend
| extend Action = tostring(Action)
Stage 8: where
| where Effect =~ "Allow" and ((((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "lambda:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "lambda:CreateFunction" and Action contains "lambda:InvokeFunction") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "lambda:Create*" and Action contains "lambda:Invoke*")) or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "lambda:*" and Action contains "dynamodb:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "lambda:CreateFunction" and Action contains "lambda:CreateEventSourceMapping" and Action contains "dynamodb:PutItem" and Action contains "dynamodb:CreateTable") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "lambda:Create*" and Action contains "dynamodb:Put*" and Action contains "dynamodb:Create*")) and Resource == "*" and Condition == ""
Stage 9: extend (4 consecutive steps)
| 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]), "")
Stage 10: distinct
| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | contains |
| field:"Action" kind:contains |
Effect | eq |
| field:"Effect" kind:eq value:"Allow" |
ErrorCode | is_null | field:"aws::errorCode" kind:is_null | |
ErrorMessage | is_null | field:"aws::errorMessage" kind:is_null | |
EventName | in |
| field:"aws::eventName" kind:in |
Resource | eq |
| field:"Resource" kind:eq value:"*" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
PolicyName | extend |
Statement | extend |
Action | extend |
Condition | extend |
Effect | extend |
Resource | extend |
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |