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:
Lambda model:
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 executingHandler: lambda_function.lambda_handler— the entry point: filelambda_function.py, functionlambda_handlerTimeout: 3— kills the function if it runs more than 3 secondsMemorySize: 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.
{
"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¶
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¶
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
- s33ppypa75 — this 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¶
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:
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:
Hit this in a browser or with curl:
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¶
SQS (Message Queue)¶
SNS (Notifications)¶
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:
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?