How Simple Cloud Misconfigurations Lead to Total Compromise
What an intentionally vulnerable AWS environment teaches us about where our security boundaries actually end
Every security review operates on assumptions about what is exposed to the outside world. The gap between what is theoretically secure and what actually ships is where real breaches happen. Cloud security failures are rarely zero-day exploits; they are far more likely to be a storage bucket left open, a firewall rule set too wide, or a trusted internal request sent to the wrong address.
This is a walkthrough of the flAWS.cloud and flAWS2.cloud environments, tracking how six distinct, seemingly minor misconfigurations chain together to yield total system compromise.
flAWS.cloud Level 1: Enumerating a Public Bucket
Misconfiguration
The bucket flaws.cloud has public read access enabled, allowing anyone to list its full contents without any AWS credentials.
Steps Taken
- Visited http://flaws.cloud and confirmed the page states the site is hosted on a public S3 bucket.
- Ran the following command to list the bucket contents with no credentials:
aws s3 ls s3://flaws.cloud — no-sign-request - The listing returned a file named
secret-dd02c7c.html, which is not a normal site file. - Opened the file directly in the browser:
http://flaws.cloud/secret-dd02c7c.html - The file revealed the Level 2 bucket address:
http://level2-c8b217a33fcf1f839f6f1f73a00a9ae7.flaws.cloud
Real World Fix
Enable S3 Block Public Access on the bucket. AWS has enabled this by default for all new buckets since April 2023, so a publicly exposed bucket today reflects a deliberate override of that safer default.
flAWS.cloud Level 2: Authenticated-Read Is Not Private
Misconfiguration
The bucket’s policy allows read access to any authenticated AWS user, not just users belonging to the target organization. Since AWS accounts are free and instant to create, this is functionally close to fully public.
Steps Taken
- Created a personal AWS IAM user with no special permissions, then attached the
AmazonS3FullAccesspolicy, which was required for the CLI to make the S3 call at all. - Configured a named profile with that user’s credentials:
aws configure — profile For-Insecure-S3bucket - Listed the Level 2 bucket using that profile instead of
-no-sign-request:
aws s3 ls s3://level2-c8b217a33fcf1f839f6f1f73a00a9ae7.flaws.cloud — region us-west-2 — profile For-Insecure-S3bucket - The listing revealed a file named secret-e4443fc.html, opened in the browser:
http://level2-c8b217a33fcf1f839f6f1f73a00a9ae7.flaws.cloud/secret-e4443fc.html - This revealed the Level 3 bucket address:
http://level3-9afd3927f195e10225021a578e6f78df.flaws.cloud
Real World Fix
Never grant bucket access to “any authenticated AWS user.” Bucket policies should restrict access to specific, named AWS account IDs or roles that genuinely need it.
flAWS.cloud Level 3: An Exposed .git Folder
Misconfiguration
The developer’s local .git folder, containing the site's full commit history, was uploaded to the public bucket along with the website files. A secret removed from the current version of a file was still recoverable from an earlier commit.
Steps Taken
- Synced the entire bucket to a local folder:
aws s3 sync s3://level3–9afd3927f195e10225021a578e6f78df.flaws.cloud/ ./level3 — no-sign-request - Viewed the commit history: Output showed two commits, the most recent one titled “Oops, accidentally added something I shouldn’t have.”
cd level3git log — oneline - Inspected the earlier commit, before the cleanup:
git show f52ec03 - This revealed a live AWS access key and secret key that had been removed from the current version of the code but still existed in the earlier commit.
- Configured a new named profile with the recovered credentials:
aws configure — profile flaws
Real World Fix
Never upload a .git folder to a public location, always exclude it from deployment. If a secret is ever committed to source control, treat it as permanently exposed and rotate it immediately, since deleting it later does not remove it from history.
.git folder to a public location, and always exclude it from deployment pipelines. If a sensitive credential is ever committed to source control, it must be considered permanently compromised and rotated immediately.flAWS.cloud Level 4: An Exposed EC2 Snapshot
Misconfiguration
An EC2 snapshot, a full backup of a server’s storage, was shared with public permissions (createVolumePermission set to "all"), exposing the entire filesystem of that server to anyone with an AWS account.
Steps Taken
- Using the leaked credentials from Level 3, listed EC2 snapshots owned by that account:
aws — profile flaws ec2 describe-snapshots — owner-ids self - Retrieved the snapshot ID from the output.
- Checked the snapshot’s sharing permission:
aws — profile flaws ec2 describe-snapshot-attribute — snapshot-id <snapshot-id> — attribute createVolumePermission - The result confirmed
"Group": "all", meaning the snapshot is shared publicly.
Real World Fix
Never set createVolumePermission to public. Only share snapshots with specific, named AWS account IDs when sharing is genuinely required, and audit existing snapshots regularly for accidental public sharing.
createVolumePermission to public. Restrict sharing to explicitly defined AWS account IDs when necessary, and audit existing snapshots regularly for accidental exposure.flAWS2.cloud Level 1: Client-Side Validation Is Not Security
Misconfiguration
PIN validation for this level happened entirely in client-side JavaScript, meaning the check could be read and bypassed by anyone. Additionally, the server-side Lambda function backing the form was not built to handle invalid input safely, and crashed on a non-numeric PIN, leaking its internal environment variables, including live AWS credentials.
Steps Taken
- Opened the page and inspected the JavaScript using browser developer tools, finding that the
validateForm()function only checked whether the input was a number, not the actual PIN. - Found the form’s real submission target inside the page source:
https://2rfismmoo8.execute-api.us-east-1.amazonaws.com/default/level1 - Sent a request directly to that address with a non-numeric code, bypassing the JavaScript check entirely:
https://2rfismmoo8.execute-api.us-east-1.amazonaws.com/default/level1?code=a - The server crashed and returned its full environment variables in the response, including AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN.
- Configured a new profile with the leaked temporary credentials:
aws configure — profile flaws2aws configure set aws_session_token <token> — profile flaws2 - Used the credentials to list the Level 1 bucket:
aws s3 ls s3://level1.flaws2.cloud — profile flaws2 - Found and opened the secret file, revealing the Level 2 address.
Real World Fix
Never rely on client-side validation for security, it only serves usability, since any visitor can read or bypass it. Never let unhandled server errors expose environment variables or credentials in a response returned to the client.
flAWS2.cloud Level 2: SSRF to a Container’s Metadata Endpoint
Misconfiguration
The container ran a built-in proxy that would fetch any address supplied to it, including its own internal files and its own private metadata endpoint. This allowed a full Server-Side Request Forgery (SSRF) attack, letting an outside request reach a private address only the container itself should have been able to access.
Steps Taken
- Confirmed the proxy would fetch an arbitrary external address:
curl “http://container.target.flaws2.cloud/proxy/http://neverssl.com" - Used the proxy to read the container’s own hidden environment variables:
curl “http://container.target.flaws2.cloud/proxy/file:///proc/self/environ" — output - - Found the hidden credential path inside the output:
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/v2/credentials/5d8244e4–78fd-4ee7–9e4f-4e3b4643b2a4 - Used the proxy again, this time pointed at the container’s metadata endpoint with that exact path:
curl “http://container.target.flaws2.cloud/proxy/http://169.254.170.2/v2/credentials/5d8244e4-78fd-4ee7-9e4f-4e3b4643b2a4" - This returned live, temporary IAM credentials, an access key, secret key, and session token, tied to a role named
role/level3.
Real World Fix
Restrict what addresses a proxy or fetch feature is allowed to reach, using an allow list of approved destinations. Explicitly block requests to internal metadata addresses (169.254.169.254, 169.254.170.2, and other private ranges). Enforce IMDSv2 wherever possible, since it requires a PUT request with a custom header before answering, a step a basic SSRF vulnerability cannot perform on the application's behalf.
169.254.169.254 or 169.254.170.2).Conclusion
The container-level SSRF exploited in flAWS2.cloud is a direct, mechanical replica of the attack vector utilized in the 2019 Capital One breach. In both scenarios, a trusted internal application, a proxy here, a Web Application Firewall in Capital One’s case, is manipulated into forwarding a request to the cloud provider’s internal metadata endpoint.
Because the request originates from inside the environment, the metadata service trusts it implicitly, returning temporary IAM credentials without requiring a password or further authentication. The attacker then extracts these credentials and uses them to access external resources, such as S3 buckets containing the data of over 100 million customers.
This highlights why AWS introduced Instance Metadata Service Version 2 (IMDSv2) as a mandatory defense. While IMDSv1 readily answers simple GET requests (which basic SSRF vulnerabilities can easily generate), IMDSv2 requires the client to first fetch a session token via a PUT request containing a custom header. Because most standard SSRF vulnerabilities cannot force an application to inject custom HTTP headers, enforcing IMDSv2 fundamentally breaks the exploit chain before the attacker can ever reach the credentials.
Get P.A’s stories in your inbox
Join Medium for free to get updates from this writer.
Till we meet again.




