Lambda Configuration Updated with Layers by User


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.

Query · python

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

Analyst notes

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

Raw source Lambda Configuration Updated with Layers by User · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
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

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.