It is common for me to see VPCs that are unable to communicate with the public internet to achieve a “private” network. This is typically done to reduce the attack surface of the network, aiding in its security. For example, making it difficult to establish Command and Control (C2) channels, reducing public exposure of sensitive endpoints, preventing data exfiltration, etc. It is also quite simple to do, just don’t deploy resources such as an Internet Gateway. However, as cloud deployments move to a more serverless nature with extensive use of services such as AWS Lambda, S3, etc, the requirement for systems within the VPC to talk to endpoints outside of the network increase. Luckily, AWS have us covered through the use of VPC endpoints, enabling us to talk to AWS APIs and other services through AWS networking magic. However, even these aren’t foolproof and could lead to unintended exposure to the wider internet.

VPC endpoints are configured with a destination service, commonly an AWS API however it could also be a service deployed in another VPC through PrivateLink. For the purposes of this blog, we are solely looking at the case of AWS APIs. When using AWS APIs, endpoints can also be configured with an endpoint policy, however this is not supported for all AWS services1. This would perform validation at the IAM layer allowing for relevant access controls to be placed on what traffic is permitted through the endpoint. To be clear, this policy doesn’t grant any allow permissions to an entity, but are constraining requests that are made through the endpoint akin to a permissions boundary.

The default VPC endpoint policy can be seen below, and permits all traffic.

{
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "*",
            "Resource": "*"
        }
    ]
}

The main source of risk associated with VPC endpoints is that the destination is multi-tenant in nature. Whilst a fully locked down network control such as a security group limits access to purely the HTTP endpoint, the same endpoint is used to access resources within various AWS accounts, including potential attackers. As such, this can lead to a communication channel between the locked-down VPC and an attackers AWS account.

How I attack them

There are two generic methods that I typically use to communicate to external services through the endpoint, the method used would be dependant on what is restricted within its policy. The first is where the Principal field of the policy is insufficiently restricted, at which point I can use credentials for my own accounts. This can be done by generating IAM Access Keys for an IAM user within my own account, and have injected into the network. This would then have permissions to the service accessible by the endpoint, and I can transmit data out of the network. This is particularly useful when Action is limited to read-only operations, as CloudTrail logs can be leveraged to view the requests being transmitted, and thus data being transferred. For example, if trying to exfiltrate password, I could attempt to list objects in a S3 bucket called password. Whilst the bucket does not exist, the attempt would still be logged and recoverable.

The other method when Principal is locked down, and Resource is not, is making use of resource policies. By updating the resource policies of resources within an attackers account, permissions can directly be granted to IAM entities of the target account, and the principals that are permitted through the endpoint. For example, the following bucket policy could be used to allow any identity, including those within the compromised network, to upload files to the bucket. It should be noted that this requires the endpoint to talk to a service that supports resource policies.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:*",
            ],
            "Resource": "arn:aws:s3:::MYBUCKET/*"
        }
    ]
}

I have used this before to setup a C2 channel through services such as S3. A compromised EC2 was granted permissions to access my control bucket, which it would regularly poll for the command object. Upon detection of this object, it would download and execute the command within. Uploading the results of the output back to an output object in the bucket and deleting the original command. This allowed me to interact with a locked-down VPC from outside the network, and enumerate and exfiltrate data as needed.

%%{init: {'theme':'dark'}}%% sequenceDiagram participant Compromised EC2 participant VPC Endpoint participant S3 Bucket participant Attacker Attacker->>S3 Bucket: Upload "command" file S3 Bucket-->>S3 Bucket: Creates "command" file loop Every minute Compromised EC2->>VPC Endpoint: Request "command" file from bucket VPC Endpoint-->>VPC Endpoint: Check request conforms with VPC Endpoint Policy VPC Endpoint->>S3 Bucket: Forwards request S3 Bucket->>VPC Endpoint: Returns "command" file VPC Endpoint->>Compromised EC2: Returns "command" file Compromised EC2-->>Compromised EC2: Execute command Compromised EC2->>VPC Endpoint: Upload "output" file to bucket and delete "command" flile VPC Endpoint-->>VPC Endpoint: Check request conforms with VPC Endpoint Policy VPC Endpoint->>S3 Bucket: Forwards request S3 Bucket-->>S3 Bucket: Creates/Updates "output" file and deletes "command" file end Attacker->>S3 Bucket: Request "output" file S3 Bucket->>Attacker: Returns "output" file
%%{init: {'theme':'default'}}%% sequenceDiagram participant Compromised EC2 participant VPC Endpoint participant S3 Bucket participant Attacker Attacker->>S3 Bucket: Upload "command" file S3 Bucket-->>S3 Bucket: Creates "command" file loop Every minute Compromised EC2->>VPC Endpoint: Request "command" file from bucket VPC Endpoint-->>VPC Endpoint: Check request conforms with VPC Endpoint Policy VPC Endpoint->>S3 Bucket: Forwards request S3 Bucket->>VPC Endpoint: Returns "command" file VPC Endpoint->>Compromised EC2: Returns "command" file Compromised EC2-->>Compromised EC2: Execute command Compromised EC2->>VPC Endpoint: Upload "output" file to bucket and delete "command" flile VPC Endpoint-->>VPC Endpoint: Check request conforms with VPC Endpoint Policy VPC Endpoint->>S3 Bucket: Forwards request S3 Bucket-->>S3 Bucket: Creates/Updates "output" file and deletes "command" file end Attacker->>S3 Bucket: Request "output" file S3 Bucket->>Attacker: Returns "output" file

What can you do about it?

So, clearly misconfigured VPC endpoints aren’t that great from a security perspective. However, a thoroughly restricted endpoint policy can severely restrict what an attacker could do through it. At minimum, I tend to say the Principal and Resource fields should be restricted either to the same account, the overall AWS Organization, or in some cases specifically allowing certain accounts that are needed. This can be done by adding IAM Conditions to the policy which gives a lot of flexibility on the restrictions applied, for example using aws:PrincipalOrgId or aws:ResourceOrgId to restrict to an Organization. If you are using endpoints extensively, another quick side tip is to use conditions in identity policies as well, conditions such as aws:SourceVpce allow restricting access through specific endpoints. This way were credentials compromised and stolen, their permissions would significantly reduce when not being used within the VPC.

Finally, I know this blog post has been all about the AWS APIs, however thought I’d also add a quick bit of using PrivateLink to communicate with endpoint services. These don’t support endpoint policies, and while most of the time I’ve not found these to communicate with multi-tenant endpoints and thus not readily exploitable. The cases where the endpoint service is multi-tenant, for example a 3rd party SaaS solution, there is no way to stop an attacker making their own account with that SaaS provider and performing a similar style of attack, as far as I know at least.