AnalysisType: rule
Filename: aws_add_malicious_lambda_extension.py
RuleID: "AWS.Lambda.UpdateFunctionConfiguration"
DisplayName: "Lambda Configuration Updated with Layers by User"
Enabled: true
LogTypes:
- AWS.CloudTrail
Reports:
MITRE ATT&CK:
- TA0007:T1078
Severity: High
CreateAlert: true
Description: >
Detects when Lambda function configuration is updated with layers by an IAM user,
federated user, or AWS SSO user. Lambda layers can execute code before the main function,
making them a stealthy persistence mechanism. This may indicate compromised credentials,
a developer bypassing CI/CD guardrails, or insider threat activity.
Runbook: |
Verify the user identity and layer ARNs are authorized.
Check if layers are from trusted sources (same account, known publishers).
If unauthorized:
- Disable the user's access key or revoke the SSO session
- Revert Lambda configuration to previous version
- Investigate credential compromise
Reference: https://stratus-red-team.cloud/attack-techniques/AWS/aws.persistence.lambda-layer-extension/
Tests:
- Name: IAM User Updates Lambda Configuration with Layers
ExpectedResult: true
Log:
{
"eventVersion": "1.05",
"userIdentity": {
"type": "IAMUser",
"principalId": "AIDAI123456789EXAMPLE",
"arn": "arn:aws:iam::123456789012:user/john.developer",
"accountId": "123456789012",
"accessKeyId": "AKIAI123456789EXAMPLE",
"userName": "john.developer"
},
"eventTime": "2019-01-01T00:00:00Z",
"eventSource": "lambda.amazonaws.com",
"eventName": "UpdateFunctionConfiguration20150331v2",
"awsRegion": "us-west-2",
"sourceIPAddress": "203.0.113.50",
"userAgent": "aws-cli/2.13.5 Python/3.11.4",
"requestParameters": {
"functionName": "production-api"
},
"responseElements": {
"functionName": "production-api",
"layers": [
{
"arn": "arn:aws:lambda:us-west-2:123456789012:layer:malicious-layer:1"
}
]
},
"requestID": "1",
"eventID": "1",
"readOnly": false,
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"p_log_type": "AWS.CloudTrail"
}
- Name: AssumedRole Updates Lambda Configuration with Layers (Normal CI/CD)
ExpectedResult: false
Log:
{
"eventVersion": "1.05",
"userIdentity": {
"type": "AssumedRole",
"principalId": "tester",
"arn": "arn:aws:sts::123456789012:assumed-role/tester",
"accountId": "123456789012",
"accessKeyId": "1",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "1111",
"arn": "arn:aws:iam::123456789012:role/tester",
"accountId": "123456789012",
"userName": "Tester"
},
"webIdFederationData": {},
"attributes": {
"mfaAuthenticated": "true",
"creationDate": "2019-01-01T00:00:00Z"
}
}
},
"eventTime": "2019-01-01T00:00:00Z",
"eventSource": "lambda.amazonaws.com",
"eventName": "UpdateFunctionConfiguration20150331v2",
"awsRegion": "us-west-2",
"sourceIPAddress": "111.111.111.111",
"userAgent": "console.amazonaws.com",
"requestParameters": {
"functionName": "my-lambda-function"
},
"responseElements": {
"layers": [
{
"arn": "arn:aws:lambda:us-west-2:123456789012:layer:my-layer:1"
}
]
},
"requestID": "1",
"eventID": "1",
"readOnly": false,
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"p_log_type": "AWS.CloudTrail"
}
- Name: SSO AssumedRole Updates Lambda Configuration with Layers
ExpectedResult: true
Log:
{
"eventVersion": "1.05",
"userIdentity": {
"type": "AssumedRole",
"principalId": "AROA123456789EXAMPLE:bob.ross",
"arn": "arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_DevAdmin_abc123/bob.ross",
"accountId": "123456789012",
"accessKeyId": "ASIA123456789EXAMPLE",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "AROA123456789EXAMPLE",
"arn": "arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_DevAdmin_abc123",
"accountId": "123456789012",
"userName": "AWSReservedSSO_DevAdmin_abc123"
},
"webIdFederationData": {},
"attributes": {
"mfaAuthenticated": "false",
"creationDate": "2019-01-01T00:00:00Z"
}
}
},
"eventTime": "2019-01-01T00:00:00Z",
"eventSource": "lambda.amazonaws.com",
"eventName": "UpdateFunctionConfiguration20150331v2",
"awsRegion": "us-west-2",
"sourceIPAddress": "203.0.113.50",
"userAgent": "aws-cli/2.13.5 Python/3.11.4",
"requestParameters": {
"functionName": "production-api"
},
"responseElements": {
"functionName": "production-api",
"layers": [
{
"arn": "arn:aws:lambda:us-west-2:123456789012:layer:suspicious-layer:1"
}
]
},
"requestID": "1",
"eventID": "1",
"readOnly": false,
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"p_log_type": "AWS.CloudTrail"
}
- Name: FederatedUser Updates Lambda Configuration with Layers
ExpectedResult: true
Log:
{
"eventVersion": "1.05",
"userIdentity": {
"type": "FederatedUser",
"principalId": "123456789012:federated-user",
"arn": "arn:aws:sts::123456789012:federated-user/federated-user",
"accountId": "123456789012",
"accessKeyId": "ASIA123456789EXAMPLE"
},
"eventTime": "2019-01-01T00:00:00Z",
"eventSource": "lambda.amazonaws.com",
"eventName": "UpdateFunctionConfiguration20150331v2",
"awsRegion": "us-west-2",
"sourceIPAddress": "203.0.113.50",
"userAgent": "aws-cli/2.13.5 Python/3.11.4",
"requestParameters": {
"functionName": "production-api"
},
"responseElements": {
"functionName": "production-api",
"layers": [
{
"arn": "arn:aws:lambda:us-west-2:123456789012:layer:some-layer:1"
}
]
},
"requestID": "1",
"eventID": "1",
"readOnly": false,
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"p_log_type": "AWS.CloudTrail"
}
- Name: Lambda Update Function Configuration without Layers
ExpectedResult: false
Log:
{
"eventVersion": "1.05",
"userIdentity": {
"type": "AssumedRole",
"principalId": "tester",
"arn": "arn:aws:sts::123456789012:assumed-role/tester",
"accountId": "123456789012",
"accessKeyId": "1",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "1111",
"arn": "arn:aws:iam::123456789012:role/tester",
"accountId": "123456789012",
"userName": "Tester"
},
"webIdFederationData": {},
"attributes": {
"mfaAuthenticated": "true",
"creationDate": "2019-01-01T00:00:00Z"
}
}
},
"eventTime": "2019-01-01T00:00:00Z",
"eventSource": "lambda.amazonaws.com",
"eventName": "UpdateFunctionConfiguration20150331v2",
"awsRegion": "us-west-2",
"sourceIPAddress": "111.111.111.111",
"userAgent": "console.amazonaws.com",
"requestParameters": {
"functionName": "my-lambda-function"
},
"responseElements": {},
"requestID": "1",
"eventID": "1",
"readOnly": false,
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"p_log_type": "AWS.CloudTrail"
}
# ------ paired body: aws_add_malicious_lambda_extension.py ------
from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
def rule(event):
if not (
aws_cloudtrail_success(event)
and event.get("eventSource") == "lambda.amazonaws.com"
and event.get("eventName") == "UpdateFunctionConfiguration20150331v2"
and event.deep_get("responseElements", "layers")
):
return False
identity_type = event.deep_get("userIdentity", "type", default="")
if identity_type in ("IAMUser", "FederatedUser"):
return True
if identity_type == "AssumedRole":
role_name = event.deep_get(
"userIdentity", "sessionContext", "sessionIssuer", "userName", default=""
)
return role_name.startswith("AWSReservedSSO_")
return False
def title(event):
lambda_name = event.deep_get(
"responseElements", "functionName", default="LAMBDA_NAME_NOT_FOUND"
)
return (
f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
f"updated Lambda function configuration with layers for [{lambda_name}]"
)
def alert_context(event):
context = aws_rule_context(event)
context["identity_type"] = event.deep_get("userIdentity", "type")
context["user_arn"] = event.deep_get("userIdentity", "arn")
layers = event.deep_get("responseElements", "layers", default=[])
context["layer_arns"] = [layer.get("arn") for layer in layers]
return context