Skip to content

flaws2.cloud — Attacker Path — Level 1

Vulnerability: Client-side-only input validation on a form → API callable directly with arbitrary input → Lambda crashes and dumps all environment variables including live AWS credentials


The Vulnerability Explained

The web page has a form with a JavaScript validation function that checks the input is a number before submitting. The developer thought: "I've validated the input." But JavaScript validation only runs in the user's browser — it has zero effect on what you send to the server directly.

The actual API endpoint is just a URL. Nothing stops you from hitting it with any input you want, bypassing the browser entirely.


Step 1 — The JavaScript "Validation"

function validateForm() {
    var code = document.forms["myForm"]["code"].value;
    if (!(!isNaN(parseFloat(code)) && isFinite(code))) {
        alert("Code must be a number");
        return false;
    }
}

What this does:

Part What it means
document.forms["myForm"]["code"].value Get the value from the form field named "code"
parseFloat(code) Try to parse it as a decimal number
isNaN(...) Is it "Not a Number"?
isFinite(code) Is it a finite number (not Infinity)?
return false If validation fails, prevent form submission

Why this is not security:

Client-side validation runs in your browser. You can disable JavaScript entirely, open the browser dev tools and delete the function, or just ignore the form and call the API URL directly. The server never enforces this — it just receives whatever you send.

Rule: Client-side validation = UX convenience only. Never security. Any validation that matters must happen on the server.


Step 2 — The API Is Just a URL

https://2rfismmoo8.execute-api.us-east-1.amazonaws.com/default/level1?code=1234

This is an API Gateway URL (same pattern as flaws.cloud level 6):

https://<api-id>.execute-api.<region>.amazonaws.com/<stage>/<resource>?<params>

The form submits to this URL with ?code=1234 as a query parameter. Since it's a GET request, you can just type this URL directly in a browser or use curl.

What GET 301 means: A 301 Moved Permanently redirect — the server is telling you the resource has moved to another URL and you should follow the redirect. The browser follows it automatically. In this context it's just the API routing — not an error.

Bypass: send non-numeric input directly

https://2rfismmoo8.execute-api.us-east-1.amazonaws.com/default/level1?code=undefined

The JavaScript would have blocked undefined in the browser. The API has no such check — it passes undefined to the Lambda function's code. The Lambda code tries to process it, fails, crashes, and returns an error message containing its full environment state.


Step 3 — The Error Response Leaks Everything

When the Lambda crashes with malformed input, it returns:

Error, malformed input
{"AWS_REGION":"us-east-1","AWS_SESSION_TOKEN":"IQoJb3Jp...","AWS_ACCESS_KEY_ID":"<REDACTED_ACCESS_KEY_ID>","AWS_SECRET_ACCESS_KEY":"<REDACTED_SECRET_KEY>", ...}

Why does Lambda have AWS credentials in environment variables?

When a Lambda function runs, AWS automatically injects the credentials for the function's attached IAM role directly as environment variables. The function uses these to make AWS API calls (access S3, call other services, etc.). The developer doesn't explicitly set them — AWS puts them there automatically every time the function is invoked.

The full set of auto-injected credential env vars: - AWS_ACCESS_KEY_ID — the key ID (starts with ASIA = temporary/role credential) - AWS_SECRET_ACCESS_KEY — the secret key - AWS_SESSION_TOKEN — the session token (required alongside the above two for temporary creds)

The flaw: The error handler printed the entire process.env (all environment variables) in the error response. The developer wrote a debug error handler that was never removed from production. Credentials that were only supposed to be used internally by the Lambda are now fully visible to anyone.

Other interesting environment variables in the dump:

Variable Value / What it reveals
AWS_REGION us-east-1 — the region
AWS_EXECUTION_ENV AWS_Lambda_nodejs8.10 — Node.js 8.10 runtime (EOL)
AWS_LAMBDA_FUNCTION_NAME level1 — the function name
AWS_LAMBDA_FUNCTION_VERSION $LATEST — not a pinned version
AWS_LAMBDA_FUNCTION_MEMORY_SIZE 128 — 128MB RAM
AWS_LAMBDA_LOG_GROUP_NAME /aws/lambda/level1 — CloudWatch log group
AWS_XRAY_DAEMON_ADDRESS 169.254.79.129:2000 — X-Ray tracing daemon (link-local again)
LAMBDA_TASK_ROOT /var/task — where the function code lives on the container
_HANDLER index.handler — entry point: file index.js, function handler
NODE_PATH Runtime module paths

LAMBDA_TASK_ROOT: /var/task is worth noting — if you ever get code execution on a Lambda, that's where the source code is.


Step 4 — Configure the Stolen Credentials

What each command does and why

aws configure --profile flaws2-level1
# AccessKeyId: <REDACTED_ACCESS_KEY_ID>
# SecretAccessKey: <REDACTED_SECRET_KEY>
# region: us-east-1

aws configure is an interactive wizard that writes credentials to ~/.aws/credentials and config to ~/.aws/config under the profile name you specify. Without --profile, it writes to the [default] block. With --profile flaws2-level1, it creates a named block [flaws2-level1].

What it writes to ~/.aws/credentials:

[flaws2-level1]
aws_access_key_id = <REDACTED_ACCESS_KEY_ID>
aws_secret_access_key = <REDACTED_SECRET_KEY>


aws configure set aws_session_token <token> --profile flaws2-level1

Why a separate command? aws configure only asks for 4 things: Access Key ID, Secret Access Key, region, and output format. It has no field for a session token. So you add it separately with configure set.

What is a session token and why does it exist?

Session tokens are the third piece of temporary credentials. They exist because of how AWS's temporary credential system works:

Permanent IAM user credentials (AKIA...): - Just two pieces: Access Key ID + Secret Access Key - Never expire (until manually deleted) - Long-lived — the access key IS the identity

Temporary credentials (ASIA...): - Three pieces: Access Key ID + Secret Access Key + Session Token - Always expire (minutes to hours) - The session token is a cryptographically signed assertion from STS that says: "these temporary keys are valid until this expiry time, for this role" - AWS validates the token server-side on every API call

Why use temporary credentials at all? They're more secure than permanent ones because they expire. Even if an attacker steals them (like we just did), they stop working after the Expiration time. Permanent keys work until someone manually rotates or deletes them.

The session token is essentially the proof. Without it, the Access Key ID and Secret Key are useless — AWS sees ASIA... and knows it must be a temporary credential, so it requires the token to validate.


What is STS?

STS = Security Token Service. The AWS service that issues temporary credentials. When a Lambda function starts, it calls STS behind the scenes to get temporary credentials for its IAM role. Those credentials come back with an expiry time. That's what's in the Lambda's environment variables.

STS is also how you explicitly assume roles, federate with external identity providers, and implement cross-account access.


aws sts get-caller-identity --profile flaws2-level1

What it does: Asks AWS "who do these credentials belong to?" Returns a guaranteed-accurate answer from AWS itself — not cached, not guessed.

Output:

{
    "UserId": "AROAIBATWWYQXZTTALNCE:level1",
    "Account": "653711331788",
    "Arn": "arn:aws:sts::653711331788:assumed-role/level1/level1"
}

Field Value What it means
UserId AROAIBATWWYQXZTTALNCE:level1 Role ID : session name. The session name is set when the role is assumed — here it's level1 (the Lambda set it)
Account 653711331788 The AWS account ID these credentials belong to
Arn arn:aws:sts::653711331788:assumed-role/level1/level1 Full identity ARN

How to Read an ARN to Determine Credential Type

The ARN structure tells you everything:

arn:aws:sts::653711331788:assumed-role/level1/level1
     ↑↑↑                  ↑↑↑↑↑↑↑↑↑↑↑
     STS service           assumed-role = temporary, from a role

Compare to a permanent IAM user from flaws.cloud level 6:

arn:aws:iam::975426262029:user/Level6
     ↑↑↑                  ↑↑↑↑
     IAM service           user = permanent IAM user identity

ARN contains Means
iam:...:user/ Permanent IAM user — two-piece credentials, no session token needed
sts:...:assumed-role/ Temporary credentials assumed by a service/role — three-piece credentials, session token required
sts:...:federated-user/ Temporary credentials issued via web identity federation

How you know it's Lambda specifically: Lambda functions don't have permanent credentials. They always assume an IAM role at runtime. So assumed-role in the ARN = almost certainly a service (Lambda, EC2, ECS task, etc.) rather than a human. Combined with the environment variables (AWS_EXECUTION_ENV: AWS_Lambda_nodejs8.10, AWS_LAMBDA_FUNCTION_NAME: level1), it's confirmed.


Step 5 — Enumerate What This Role Can Do

aws s3 ls --profile flaws2-level1

Result: AccessDenieds3:ListAllMyBuckets not allowed. This role can't enumerate all S3 buckets.

Lesson: Just because you have credentials doesn't mean you have full access. Always enumerate permissions first.

Check the role's permissions

aws iam get-role --role-name level1 --profile flaws2-level1
aws iam list-attached-role-policies --role-name level1 --profile flaws2-level1

get-role — returns the role's trust policy (which services/identities are allowed to assume this role) and metadata.

list-attached-role-policies — returns which IAM policies are attached to the role. The policies define what the role can actually do.

What is IAM?

IAM = Identity and Access Management. The AWS service that controls who can do what. Everything in AWS goes through IAM.

IAM Concept What it is
User A permanent human identity with long-lived credentials
Role An identity meant to be assumed temporarily — by services, cross-account users, or other roles. No permanent credentials
Policy A JSON document listing allowed/denied actions on resources
Attached policy A managed policy linked to a user/role/group
Inline policy A policy embedded directly in a user/role (not reusable)
Trust policy Controls who can assume a role (e.g., lambda.amazonaws.com can assume this role)
Permission policy Controls what the role can do once assumed

get-role vs list-attached-role-policies: - get-role → who is allowed to assume this role (the trust policy) - list-attached-role-policies → what the role can do (permission policies)

You need both to understand a role fully.


Enumerate other services

aws lambda list-functions --profile flaws2-level1 --region us-east-1
aws sns list-topics --profile flaws2-level1 --region us-east-1
aws sqs list-queues --profile flaws2-level1 --region us-east-1

Broad enumeration — try everything the SecurityAudit-style approach suggests. See what errors you get (AccessDenied) vs what returns data.

What is ECR?

aws ecr describe-repositories --profile flaws2-level1 --region us-east-1

ECR = Elastic Container Registry. AWS's private Docker image registry — like Docker Hub but hosted in your AWS account. If a team is running containerised workloads (ECS, Fargate, EKS), their Docker images live in ECR.

Why check ECR? Docker images often contain: - Hardcoded credentials left in the image layers - Application source code - Config files with secrets - A buildable version of the app you can inspect

If the role has ecr:GetAuthorizationToken and ecr:BatchGetImage, you can pull images and inspect every layer.


Step 6 — Access the Level S3 Bucket

aws s3 ls s3://level1.flaws2.cloud --profile flaws2-level1 --region us-east-1

Even though the role can't list all buckets (s3:ListAllMyBuckets), it can list a specific bucket (s3:ListBucket) if the policy allows it — or if the bucket is publicly readable.

The distinction: - s3:ListAllMyBuckets — list every bucket in the account. This is what aws s3 ls (no bucket name) does. - s3:ListBucket — list the contents of one specific bucket. This is what aws s3 ls s3://bucketname does.

These are separate permissions. A role can have one without the other.

Result: bucket listing succeeds → you see secret-ppxVFdwV4DDtZm8vbQRvhxL8mE6wxNco.html → that's the next level.


The Full AWS Credential Flow for Lambda (Mental Model)

Lambda function is invoked
AWS Lambda service calls STS:AssumeRole for the function's attached IAM role
STS issues temporary credentials (AccessKeyId + SecretAccessKey + SessionToken)
with an expiry time (typically 12 hours for Lambda)
AWS injects all three into the Lambda container as environment variables:
    AWS_ACCESS_KEY_ID
    AWS_SECRET_ACCESS_KEY
    AWS_SESSION_TOKEN
Lambda code can now make AWS API calls — automatically authenticated
When the function crashes and dumps process.env... you have all three

Vulnerability Summary

The flaws (two separate issues combined):

  1. Client-side-only validation: Input filtering runs in JavaScript in the browser. Easily bypassed by calling the API directly with any value.

  2. Unhandled errors expose environment variables: The Lambda's error handler printed process.env — all environment variables — in the HTTP response. Environment variables in Lambda always contain the function's IAM role credentials.

The attack chain: 1. Observe the form — it submits ?code=<number> to an API Gateway URL 2. Call the API directly with non-numeric input (?code=undefined) 3. Lambda crashes → error response dumps all environment variables 4. Extract AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN 5. aws configure --profile flaws2-level1 → enter the keys 6. aws configure set aws_session_token <token> --profile flaws2-level1 7. aws sts get-caller-identity → confirm identity (Lambda's IAM role) 8. Enumerate what the role can access → find s3://level1.flaws2.cloud 9. aws s3 ls s3://level1.flaws2.cloud → find the secret HTML file → level 2

The fix: - Validate input on the server, not (only) the client - Never print environment variables in error responses — use structured error messages that reveal nothing about the server environment - Scope Lambda IAM roles to the minimum permissions needed - In production: use structured exception handling with a generic error response; log details to CloudWatch only - Enable Lambda error alerting — a crash that returns credentials in the body should trigger an immediate incident