Skip to content

flaws.cloud — Level 6

Vulnerability: Over-permissive SecurityAudit policy allows full enumeration → discovers public Lambda via API Gateway → Lambda's IAM role exposes the next level


Core Concepts First


What is Lambda?

AWS Lambda is serverless computing — you write a function (a chunk of code), upload it to AWS, and AWS runs it when triggered. You don't provision servers, you don't manage an OS, you don't pay for idle time. You pay only for the milliseconds your code actually runs.

Traditional server model:

You → rent a server → install OS → install runtime → deploy code → server runs 24/7 (you pay 24/7)

Lambda model:

You → upload function code → Lambda runs it only when triggered → you pay per invocation

What triggers Lambda? Almost anything: an HTTP request (via API Gateway), an S3 file upload, a database change, a scheduled timer (cron), an SNS message, a queue item — and more.

Why use it?

Reason What it means in practice
No server management No OS updates, no capacity planning, no SSH into prod
Scales automatically Goes from 0 to 10,000 concurrent executions with no config
Pay per use Idle = $0. Useful for infrequent tasks — a Lambda that runs 1,000 times/month is essentially free
Fast deployment Upload code, it's live in seconds
Event-driven Reacts to things happening rather than polling

Cons:

Limitation Why it matters
Cold starts First invocation spins up a container — 100ms–2s latency. Bad for real-time apps
15 min max runtime Can't run long jobs
Limited local storage 512MB–10GB ephemeral /tmp only — no persistent local disk
Stateless No memory between invocations — must use external storage (S3, DynamoDB) for state
Debugging is harder You're not SSH-ing into anything — logs go to CloudWatch

The Lambda output here:

{
    "FunctionName": "Level6",
    "Runtime": "python2.7",
    "Role": "arn:aws:iam::975426262029:role/service-role/Level6",
    "Handler": "lambda_function.lambda_handler",
    "Timeout": 3,
    "MemorySize": 128
}

  • Runtime: python2.7 — the code is Python (2.7 specifically, which is EOL — bad practice)
  • Role — the IAM role this Lambda runs with. This is the permissions the function has when executing
  • Handler: lambda_function.lambda_handler — the entry point: file lambda_function.py, function lambda_handler
  • Timeout: 3 — kills the function if it runs more than 3 seconds
  • MemorySize: 128 — 128MB RAM allocated

What is API Gateway?

AWS API Gateway is a fully managed HTTP router/endpoint service. Its job: accept HTTP requests from the internet and route them to a backend (usually Lambda, but also EC2, other AWS services, or a plain HTTP endpoint).

Think of it as the front door of your serverless web application. Lambda is the backend code. API Gateway is the web server that accepts requests and hands them off.

Internet user
    → HTTP GET /level6
    → API Gateway (handles TLS, auth, rate limiting, routing)
    → Triggers Lambda function
    → Lambda returns response
    → API Gateway sends response back to user

Why have API Gateway at all — why not hit Lambda directly?

Lambda isn't directly accessible via HTTP. It's triggered by events, not by raw HTTP connections. API Gateway is the component that turns an HTTP request into a Lambda invocation event. You need it to expose Lambda as a web endpoint.

Also: API Gateway handles things Lambda shouldn't have to care about: SSL certificates, DDoS protection, rate limiting, API keys, CORS headers, request/response transformation.

The structure of an API Gateway:

Concept What it is
REST API The top-level container — has an ID (e.g., s33ppypa75)
Resource A path in the URL (e.g., /level6)
Method HTTP verb on a resource (e.g., GET /level6)
Stage A named deployment of the API (e.g., Prod, Dev, v1) — like a version/environment
Integration What backend handles the request (e.g., Lambda function Level6)

The Serverless Architecture — How It All Connects

You said you understand the pieces individually but can't connect them. Here's the concrete mental model:

Compare to a traditional web app you'd understand:

Traditional web app (e.g., a PHP site on a server):
Browser → nginx (web server, port 80) → PHP script runs → response

Serverless equivalent:
Browser → API Gateway (port 443, HTTPS) → Lambda function runs → response

It's exactly the same architecture, just with AWS brand names: - nginx = API Gateway - PHP/Python/Node script on a server = Lambda function - Server's IAM permissions/user = Lambda's IAM role

The big difference is Lambda doesn't live on a persistent server — AWS spins up a temporary container to run it, then destroys it. But from the HTTP perspective, the pattern is identical.

Full picture for this level:

You (curl/browser)
    ↓  HTTP GET
API Gateway: s33ppypa75.execute-api.us-west-2.amazonaws.com
    ↓  invokes
Lambda function: Level6 (Python code runs)
    ↓  runs as
IAM Role: arn:aws:iam::975426262029:role/service-role/Level6
    ↓  which has permissions to...
[whatever the role policy allows — that's the next level flag]

Step 1 — Configure the Credentials

aws configure --profile level6
# AccessKeyId: redacted
# SecretAccessKey: redacted
# region: us-west-2

These are permanent credentials (AKIA prefix — not temporary). A real IAM user's access key, not a role.

aws sts get-caller-identity --profile level6
{
    "UserId": "AIDAIRMDOSCWGLCDWOG6A",
    "Account": "975426262029",
    "Arn": "arn:aws:iam::975426262029:user/Level6"
}

You're operating as user/Level6 in account 975426262029.


Step 2 — Enumerate Your Own Permissions

aws iam get-user --profile level6
aws iam list-attached-user-policies --user-name Level6 --profile level6

Returns two attached policies: - MySecurityAudit — a custom version of AWS's SecurityAudit policy - list_apigateways — a custom policy, name immediately tips off API Gateway

What is SecurityAudit?

SecurityAudit is a read-only policy that allows Describe*, List*, Get* on almost every AWS service. It's designed for auditors and monitoring tools — they need to see everything but shouldn't be able to change anything.

What it allows (non-exhaustive): - List all S3 buckets and their policies - Describe all EC2 instances, security groups, VPCs - List all IAM users, roles, policies, and their contents - List all Lambda functions and their configs - Describe all RDS databases - List all CloudTrail logs and CloudWatch alarms - Describe all API Gateways

Why read-only can still destroy you:

An attacker with SecurityAudit can map your entire AWS environment — every resource, every permission, every misconfiguration. They can't directly do damage, but they can find the thing that lets them escalate. That's exactly what this level is.

The lesson: treating "read-only" as "harmless" is wrong. Visibility into your environment is incredibly powerful for an attacker.


Step 3 — Read Your Policy Details

aws iam get-policy --policy-arn arn:aws:iam::975426262029:policy/list_apigateways --profile level6

Important: get-policy only returns metadata (name, ARN, version IDs). It doesn't show the actual permission document. To see what the policy actually allows, you need get-policy-version:

# First get the default version ID from get-policy output (e.g., "v1")
aws iam get-policy-version \
  --policy-arn arn:aws:iam::975426262029:policy/list_apigateways \
  --version-id v1 \
  --profile level6

This returns the actual JSON policy document showing which actions are allowed on which resources.


Step 4 — Look at the Lambda Function's Invocation Policy

aws lambda get-policy --function-name Level6 --profile level6 --region us-west-2

This is different from list-functions. list-functions shows the function's configuration. get-policy shows the resource-based policy — who is allowed to invoke this function.

The policy will contain something like:

{
    "Statement": [{
        "Principal": {"Service": "apigateway.amazonaws.com"},
        "Action": "lambda:InvokeFunction",
        "Resource": "arn:aws:lambda:us-west-2:975426262029:function:Level6",
        "Condition": {
            "ArnLike": {
                "AWS:SourceArn": "arn:aws:execute-api:us-west-2:975426262029:s33ppypa75/*/GET/level6"
            }
        }
    }]
}

What this tells you: - apigateway.amazonaws.com can invoke this Lambda — confirms API Gateway is the trigger - s33ppypa75this is the API Gateway ID — you just found it - GET/level6 — the method is GET and the resource path is /level6

You now have the API ID (s33ppypa75) and the resource path (/level6). You're missing one thing: the stage name.


Step 5 — Find the API Gateway and Stage

# List all REST APIs
aws apigateway get-rest-apis --profile level6 --region us-west-2

Returns the API with ID s33ppypa75. Confirms it exists.

# Get the stages for this API
aws apigateway get-stages --rest-api-id s33ppypa75 --profile level6 --region us-west-2

Returns the stage name — e.g., Prod.


Step 6 — Construct and Hit the Invoke URL

How you know the URL formula — AWS URL Standards

This is the question you asked: "how do you know this is the URL?"

The answer: AWS has fixed, documented URL formats for every service. Just like you know an S3 bucket URL is always <bucket>.s3.amazonaws.com, every API Gateway URL is always:

https://<API_ID>.execute-api.<REGION>.amazonaws.com/<STAGE>/<RESOURCE>

You assembled this from the pieces you found:

Piece Value Where you got it
API_ID s33ppypa75 From lambda get-policy (the SourceArn condition)
REGION us-west-2 Known from the beginning
STAGE Prod From apigateway get-stages
RESOURCE level6 From lambda get-policy (GET/level6)

Result:

https://s33ppypa75.execute-api.us-west-2.amazonaws.com/Prod/level6

Hit this in a browser or with curl:

curl https://s33ppypa75.execute-api.us-west-2.amazonaws.com/Prod/level6

The Lambda function runs, and its output contains the next level's information.


AWS URL Standards — Complete Reference

This is what you asked for. Every AWS service has a fixed URL pattern:

S3

# REST API endpoint (for CLI/SDK)
https://<bucket>.s3.amazonaws.com/<key>
https://<bucket>.s3.<region>.amazonaws.com/<key>  # region-specific

# Static website endpoint (for browser access)
http://<bucket>.s3-website-<region>.amazonaws.com
http://<bucket>.s3-website.<region>.amazonaws.com  # newer format

# Path-style (deprecated but still works)
https://s3.amazonaws.com/<bucket>/<key>

API Gateway

# REST API
https://<api-id>.execute-api.<region>.amazonaws.com/<stage>/<resource>

# HTTP API (newer, cheaper API Gateway type)
https://<api-id>.execute-api.<region>.amazonaws.com/<resource>

# Custom domain (if configured)
https://api.yourdomain.com/<resource>

EC2

# Instance public DNS (auto-assigned)
ec2-<public-ip-dashes>.compute-1.amazonaws.com          # us-east-1
ec2-<public-ip-dashes>.<region>.compute.amazonaws.com   # other regions

# Example: IP 52.25.143.249 in us-west-2
ec2-52-25-143-249.us-west-2.compute.amazonaws.com

Lambda

# Lambda has no direct HTTP URL — it's triggered by events
# You invoke it via CLI:
aws lambda invoke --function-name MyFunction ...

# Or via its API endpoint (not a public URL):
https://lambda.<region>.amazonaws.com/2015-03-31/functions/<function-name>/invocations

RDS (Databases)

# Auto-generated endpoint when you create a database instance
<db-instance-identifier>.<unique-id>.<region>.rds.amazonaws.com

# Example:
mydb.c9akciq32.us-east-1.rds.amazonaws.com

ECS / Fargate (containers)

# No fixed public URL — exposed via a Load Balancer
# Load balancer URL (auto-generated):
<alb-name>-<id>.<region>.elb.amazonaws.com

CloudFront (CDN)

# Auto-generated CloudFront domain
<distribution-id>.cloudfront.net

# Custom domain if configured:
cdn.yourdomain.com

Elastic Beanstalk

<environment-name>.<region>.elasticbeanstalk.com

SQS (Message Queue)

https://sqs.<region>.amazonaws.com/<account-id>/<queue-name>

SNS (Notifications)

# SNS doesn't have a browsable URL — ARN-based:
arn:aws:sns:<region>:<account-id>:<topic-name>

IAM (Global service — no region)

# IAM ARN format (not a URL, but the universal identifier):
arn:aws:iam::<account-id>:user/<username>
arn:aws:iam::<account-id>:role/<rolename>
arn:aws:iam::<account-id>:policy/<policyname>

The ARN Pattern (applies everywhere)

ARN = Amazon Resource Name — the universal unique identifier for every AWS resource:

arn:aws:<service>:<region>:<account-id>:<resource-type>/<resource-id>

Examples:

arn:aws:iam::975426262029:user/Level6           # IAM user (no region — IAM is global)
arn:aws:lambda:us-west-2:975426262029:function:Level6
arn:aws:s3:::flaws.cloud                        # S3 (no region, no account — globally unique names)
arn:aws:ec2:us-west-2:975426262029:instance/i-02e76e84fbe738592
arn:aws:execute-api:us-west-2:975426262029:s33ppypa75/*/GET/level6

Fields that can be empty: - Region is empty for global services (IAM, S3 bucket names) - Account ID is empty for S3 (bucket names are globally unique across all accounts)


The Enumeration Logic — How Each Step Leads to the Next

This is the "how does it all connect" answer.

You start with credentials and ask: what can I see? what can I reach? what's misconfigured?

Step 1: sts get-caller-identity
    → You're user/Level6 in account 975426262029

Step 2: iam list-attached-user-policies
    → You have MySecurityAudit + list_apigateways
    → "list_apigateways" = API Gateway is in scope

Step 3: lambda list-functions
    → There's a "Level6" Lambda function
    → It has an IAM role: role/service-role/Level6
    → SecurityAudit lets you see Lambda functions

Step 4: lambda get-policy (the function's resource policy)
    → API Gateway s33ppypa75 can invoke it
    → The path is GET/level6
    → You now have: API ID + method + resource path

Step 5: apigateway get-stages --rest-api-id s33ppypa75
    → Stage name: Prod

Step 6: Assemble the URL
    → https://s33ppypa75.execute-api.us-west-2.amazonaws.com/Prod/level6
    → curl it → Lambda runs → flag appears

Each command's output gives you the input for the next command. This is standard enumeration methodology — you follow the thread until you have something exploitable.


Lambda Function Fields Explained

{
    "FunctionName": "Level6",
    "FunctionArn": "arn:aws:lambda:us-west-2:975426262029:function:Level6",
    "Runtime": "python2.7",
    "Role": "arn:aws:iam::975426262029:role/service-role/Level6",
    "Handler": "lambda_function.lambda_handler",
    "CodeSize": 282,
    "Timeout": 3,
    "MemorySize": 128,
    "Version": "$LATEST"
}
Field What it means
FunctionName How you reference it in CLI commands
FunctionArn The full ARN — unique identifier
Runtime Language/version the code runs in. python2.7 = EOL, bad practice
Role IAM role the function assumes when running — this defines what the function can do
Handler Entry point: file.function. lambda_function.lambda_handler = file lambda_function.py, function named lambda_handler
CodeSize Size in bytes of the deployment package (282 bytes = tiny, just a few lines)
Timeout Max seconds before AWS kills the function. Default 3s, max 900s (15 min)
MemorySize RAM in MB. CPU scales proportionally — more RAM = faster execution
Version: $LATEST Using the latest unpublished version (not a fixed immutable version)

Vulnerability Summary

The flaw: A SecurityAudit policy (designed to be "just read-only") provides enough visibility to enumerate the entire account, find a public Lambda endpoint, and invoke it.

Why this matters:

The developer thought: "SecurityAudit is read-only, so it's safe to hand out." But read-only on an AWS account means: - You can see every Lambda function and its role - You can see every API Gateway endpoint - You can see every IAM policy and what it allows - You can see every S3 bucket and its ACL - You can see every security group and what ports are open - You can map the entire attack surface

An attacker with SecurityAudit doesn't need to compromise anything — they just need to read until they find something misconfigured or over-permissive. This level's Lambda endpoint was publicly invokable, and its IAM role had permissions it shouldn't have.

The attack chain: 1. Use Level 5 credentials to list Level 6 bucket → find Level 6 IAM credentials 2. sts get-caller-identity → confirm identity 3. iam list-attached-user-policies → find SecurityAudit + list_apigateways 4. lambda list-functions → find Level6 function 5. lambda get-policy → find API Gateway ID s33ppypa75 and path GET/level6 6. apigateway get-stages → find stage Prod 7. Construct URL → curl https://s33ppypa75.execute-api.us-west-2.amazonaws.com/Prod/level6 8. Lambda runs → returns the level completion flag

The fix: - Don't give SecurityAudit broadly — scope it to only the services that actually need auditing - Lambda functions should have resource policies that restrict who can invoke them (e.g., specific IPs, authenticated users, specific roles — not the whole internet) - Lambda IAM roles should follow least-privilege — the role should only have exactly the permissions the function needs, nothing more - Treat any read permission as potentially dangerous — what can an attacker learn from it?