diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a1795244..3fe03eee 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 6c516149..4721ccb1 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -11,7 +11,7 @@ jobs: pull-requests: write # Required for Publish Test Results steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python 3.11 uses: actions/setup-python@v4 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 2a437c76..69577a1a 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -11,7 +11,7 @@ jobs: pull-requests: write # Required for Publish Test Results steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python 3.11 uses: actions/setup-python@v4 diff --git a/README.md b/README.md index 31103f4e..1bf04363 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # OWASP Domain Protect -![Release version](https://img.shields.io/badge/release-v0.4.2-blue.svg) +![Release version](https://img.shields.io/badge/release-v0.4.4-blue.svg) [![Python 3.x](https://img.shields.io/badge/Python-3.x-blue.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) ![OWASP Maturity](https://img.shields.io/badge/owasp-incubator%20project-53AAE5.svg) diff --git a/aws-iam-policies/domain-protect-deploy.json b/aws-iam-policies/domain-protect-deploy.json index 0fd3b0c8..7b60383e 100644 --- a/aws-iam-policies/domain-protect-deploy.json +++ b/aws-iam-policies/domain-protect-deploy.json @@ -45,6 +45,7 @@ "iam:CreateRole", "iam:CreateServiceLinkedRole", "iam:DeleteRole", + "iam:DeleteRolePermissionsBoundary", "iam:DeleteServiceLinkedRole", "iam:DetachRolePolicy", "iam:DeleteRolePolicy", @@ -53,6 +54,7 @@ "iam:ListAttachedRolePolicies", "iam:ListInstanceProfilesForRole", "iam:ListRolePolicies", + "iam:PutRolePermissionsBoundary", "iam:PutRolePolicy", "iam:PassRole" ], diff --git a/docs/integration-tests.md b/docs/integration-tests.md index 7f887ba5..4b0a7bdc 100644 --- a/docs/integration-tests.md +++ b/docs/integration-tests.md @@ -52,7 +52,6 @@ We are using the moto python module for mocking out AWS, and setting these up us Then in the test you require the mock you can use the function name (e.g. `moto_route53`) as the parameter. You can then use the mock as if it was the boto3 library to create the resources you need for testing, which will be created in a mocked out aws account. -Because we use aws profiles we require the profile name to be set up in boto3 for testing. A "mocked" aws profile is set up in `integration_tests/conftest.py` in the `aws_credentials` fixture (this is why the other moto fixtures have the `aws_credentials` parameter). If the code under test needs an aws profile name, please use "mocked". [back to Automated Tests](automated-tests.md)
[back to README](../README.md) diff --git a/integration_tests/manual_scans/aws/test_aws_alias_s3.py b/integration_tests/manual_scans/aws/test_aws_alias_s3.py index 2bb09d6f..8f6a5c5d 100644 --- a/integration_tests/manual_scans/aws/test_aws_alias_s3.py +++ b/integration_tests/manual_scans/aws/test_aws_alias_s3.py @@ -41,8 +41,6 @@ def test_main_detects_vulnerable_domains(arg_parse_mock, print_list_mock, moto_r requests_mock.get("http://vulnerable.domain-protect.com.", status_code=404, text="Code: NoSuchBucket") - arg_parse_mock.return_value.parse_args.return_value.profile = "mocked" - main() expected_vulnerable_call = call(["vulnerable.domain-protect.com."], "INSECURE_WS") @@ -57,8 +55,6 @@ def test_main_ignores_non_vulnerable_domains(arg_parse_mock, print_list_mock, mo requests_mock.get("http://vulnerable.domain-protect.com.", status_code=200, text="All good here") - arg_parse_mock.return_value.parse_args.return_value.profile = "mocked" - main() print_list_mock.assert_not_called() @@ -71,8 +67,6 @@ def test_main_ignores_non_s3_domains(arg_parse_mock, print_list_mock, moto_route requests_mock.get("http://vulnerable.domain-protect.com.", status_code=404, text="Code: NoSuchBucket") - arg_parse_mock.return_value.parse_args.return_value.profile = "mocked" - main() print_list_mock.assert_not_called() @@ -85,8 +79,6 @@ def test_main_ignores_domains_with_connection_error(arg_parse_mock, print_list_m requests_mock.get("http://vulnerable.domain-protect.com.", exc=requests.exceptions.ConnectionError) - arg_parse_mock.return_value.parse_args.return_value.profile = "mocked" - main() print_list_mock.assert_not_called() diff --git a/lambda_code/cloudflare_scan/cloudflare_scan.py b/lambda_code/cloudflare_scan/cloudflare_scan.py index 1b998e79..6b82600f 100644 --- a/lambda_code/cloudflare_scan/cloudflare_scan.py +++ b/lambda_code/cloudflare_scan/cloudflare_scan.py @@ -2,6 +2,7 @@ import json import os +from utils.utils_aws import eb_susceptible from utils.utils_aws import publish_to_sns from utils.utils_bugcrowd import bugcrowd_create_issue from utils.utils_cloudflare import list_cloudflare_records @@ -182,14 +183,7 @@ def cf_s3(account_name, zone_name, records): def cf_eb(account_name, zone_name, records): - - vulnerability_list = [".elasticbeanstalk.com"] - - records_filtered = [ - r - for r in records - if r["Type"] in ["CNAME"] and any(vulnerability in r["Value"] for vulnerability in vulnerability_list) - ] + records_filtered = [r for r in records if r["Type"] in ["CNAME"] and eb_susceptible(r["Value"])] for record in records_filtered: diff --git a/lambda_code/scan/scan.py b/lambda_code/scan/scan.py index 7698f65a..44684059 100644 --- a/lambda_code/scan/scan.py +++ b/lambda_code/scan/scan.py @@ -2,6 +2,7 @@ import json import os +from utils.utils_aws import eb_susceptible from utils.utils_aws import get_cloudfront_origin from utils.utils_aws import list_domains from utils.utils_aws import list_hosted_zones @@ -120,7 +121,7 @@ def alias_cloudfront_s3(account_name, record_sets, account_id): def alias_eb(account_name, record_sets): record_sets_filtered = [ - r for r in record_sets if "AliasTarget" in r and "elasticbeanstalk.com" in r["AliasTarget"]["DNSName"] + r for r in record_sets if "AliasTarget" in r and eb_susceptible(r["AliasTarget"]["DNSName"]) ] for record in record_sets_filtered: @@ -192,9 +193,7 @@ def cname_eb(account_name, record_sets): record_sets_filtered = [ r for r in record_sets - if r["Type"] in ["CNAME"] - and "ResourceRecords" in r - and "elasticbeanstalk.com" in r["ResourceRecords"][0]["Value"] + if r["Type"] in ["CNAME"] and "ResourceRecords" in r and eb_susceptible(r["ResourceRecords"][0]["Value"]) ] for record in record_sets_filtered: diff --git a/main.tf b/main.tf index 659e352b..e1c1657f 100644 --- a/main.tf +++ b/main.tf @@ -10,6 +10,7 @@ module "lambda-role" { region = var.region security_audit_role_name = var.security_audit_role_name kms_arn = module.kms.kms_arn + permissions_boundary_arn = var.permissions_boundary_arn } module "lambda-slack" { @@ -72,6 +73,7 @@ module "accounts-role" { kms_arn = module.kms.kms_arn state_machine_arn = module.step-function.state_machine_arn policy = "accounts" + permissions_boundary_arn = var.permissions_boundary_arn } module "lambda-scan" { @@ -118,6 +120,7 @@ module "takeover-role" { kms_arn = module.kms.kms_arn takeover = local.takeover policy = "takeover" + permissions_boundary_arn = var.permissions_boundary_arn } module "lambda-resources" { @@ -141,6 +144,7 @@ module "resources-role" { security_audit_role_name = var.security_audit_role_name kms_arn = module.kms.kms_arn policy = "resources" + permissions_boundary_arn = var.permissions_boundary_arn } module "cloudwatch-event" { @@ -248,6 +252,7 @@ module "step-function-role" { kms_arn = module.kms.kms_arn policy = "state" assume_role_policy = "state" + permissions_boundary_arn = var.permissions_boundary_arn } module "step-function" { @@ -284,6 +289,7 @@ module "lambda-role-ips" { kms_arn = module.kms.kms_arn policy = "lambda" role_name = "lambda-ips" + permissions_boundary_arn = var.permissions_boundary_arn } module "lambda-scan-ips" { @@ -321,6 +327,7 @@ module "accounts-role-ips" { state_machine_arn = module.step-function-ips[0].state_machine_arn policy = "accounts" role_name = "accounts-ips" + permissions_boundary_arn = var.permissions_boundary_arn } module "lambda-accounts-ips" { diff --git a/manual_scans/aws/README.md b/manual_scans/aws/README.md index 7694bd9f..d30509e6 100644 --- a/manual_scans/aws/README.md +++ b/manual_scans/aws/README.md @@ -32,65 +32,66 @@ $ export PYTHONPATH="${PYTHONPATH}:/Users/paul/src/github.com/domain-protect/dom * run manual scans from root of domain-protect folder ## CloudFront Alias with missing S3 origin -* replace PROFILE_NAME by your AWS CLI profile name + + ``` -python manual_scans/aws/aws-alias-cloudfront-s3.py --profile PROFILE_NAME +python manual_scans/aws/aws-alias-cloudfront-s3.py ``` ![Alt text](images/aws-cloudfront-s3-alias.png?raw=true "CloudFront Alias with missing S3 origin") ## CloudFront CNAME with missing S3 origin -* replace PROFILE_NAME by your AWS CLI profile name + ``` -python manual_scans/aws/aws-cname-cloudfront-s3.py --profile PROFILE_NAME +python manual_scans/aws/aws-cname-cloudfront-s3.py ``` ![Alt text](images/aws-cloudfront-s3-cname.png?raw=true "CloudFront CNAME with missing S3 origin") ## ElasticBeanstalk Alias -* replace PROFILE_NAME by your AWS CLI profile name + ``` -python manual_scans/aws/aws-alias-eb.py --profile PROFILE_NAME +python manual_scans/aws/aws-alias-eb.py ``` ![Alt text](images/aws-eb-alias.png?raw=true "Detect vulnerable S3 Aliases") ## ElasticBeanstalk CNAMES -* replace PROFILE_NAME by your AWS CLI profile name + ``` -python manual_scans/aws/aws-cname-eb.py --profile PROFILE_NAME +python manual_scans/aws/aws-cname-eb.py ``` ![Alt text](images/aws-eb-cnames.png?raw=true "Detect vulnerable ElasticBeanstalk CNAMEs") ## S3 Alias -* replace PROFILE_NAME by your AWS CLI profile name + ``` -python manual_scans/aws/aws_alias_s3.py --profile PROFILE_NAME +python manual_scans/aws/aws_alias_s3.py ``` ![Alt text](images/aws-s3-alias.png?raw=true "Detect vulnerable S3 Aliases") ## S3 CNAMES -* replace PROFILE_NAME by your AWS CLI profile name + ``` -python manual_scans/aws/aws-cname-s3.py --profile PROFILE_NAME +python manual_scans/aws/aws-cname-s3.py ``` ![Alt text](images/aws-s3-cnames.png?raw=true "Detect vulnerable S3 CNAMEs") ## registered domains with missing hosted zone -* replace PROFILE_NAME by your AWS CLI profile name + ``` -python manual_scans/aws/aws-ns-domain.py --profile PROFILE_NAME +python manual_scans/aws/aws-ns-domain.py ``` ![Alt text](images/aws-ns-domain.png?raw=true "Detect vulnerable subdomains") ## subdomain NS delegations -* replace PROFILE_NAME by your AWS CLI profile name + ``` -python manual_scans/aws/aws-ns-subdomain.py --profile PROFILE_NAME +python manual_scans/aws/aws-ns-subdomain.py ``` ![Alt text](images/aws-ns-subdomain.png?raw=true "Detect vulnerable subdomains") @@ -103,26 +104,18 @@ python manual_scans/aws/aws-ns-subdomain.py --profile PROFILE_NAME ``` aws sts assume-role --role-arn arn:aws:iam::012345678901:role/securityaudit --role-session-name domainprotect ``` -* copy and paste the returned temporary credentials to your desktop -* create AWS cli credentials in CloudShell -``` -vi .aws/credentials -``` -* enter details in the following format -``` -[profile_name] -aws_access_key_id = XXXXXXXXXXXXXXXXXXXXXX -aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -aws_session_token = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -``` -* save and exit vi -``` -:wq! +* set the returned temporary credentials in the environmebt variables of your local machine: + +```bash +export AWS_ACCESS_KEY_ID=... +export AWS_SECRET_ACCESS_KEY=... +export AWS_SESSION_TOKEN=... ``` + * install dependencies and proceed with the scans, e.g. ``` sudo pip3 install dnspython -python3 manual_scans/aws/aws-ns-domain.py --profile profile_name +python3 manual_scans/aws/aws-ns-domain.py ``` [back to README](../../README.md) diff --git a/manual_scans/aws/aws-alias-cloudfront-s3.py b/manual_scans/aws/aws-alias-cloudfront-s3.py index 37564059..1833825e 100644 --- a/manual_scans/aws/aws-alias-cloudfront-s3.py +++ b/manual_scans/aws/aws-alias-cloudfront-s3.py @@ -27,14 +27,14 @@ def vulnerable_alias_cloudfront_s3(domain_name): return False -def route53(profile): +def route53(): print("Searching for Route53 hosted zones") - session = boto3.Session(profile_name=profile) + session = boto3.Session() route53 = session.client("route53") - hosted_zones = list_hosted_zones_manual_scan(profile) + hosted_zones = list_hosted_zones_manual_scan() for hosted_zone in hosted_zones: print(f"Searching for CloudFront Alias records in {hosted_zone['Name']}") paginator_records = route53.get_paginator("list_resource_record_sets") @@ -64,11 +64,8 @@ def route53(profile): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Prevent Subdomain Takeover") - parser.add_argument("--profile", required=True) - args = parser.parse_args() - profile = args.profile - route53(profile) + route53() count = len(vulnerable_domains) my_print(f"\nTotal Vulnerable Domains Found: {str(count)}", "INFOB") diff --git a/manual_scans/aws/aws-alias-eb.py b/manual_scans/aws/aws-alias-eb.py index a5387b98..abb71af2 100644 --- a/manual_scans/aws/aws-alias-eb.py +++ b/manual_scans/aws/aws-alias-eb.py @@ -3,6 +3,7 @@ import boto3 +from utils.utils_aws import eb_susceptible from utils.utils_aws_manual import list_hosted_zones_manual_scan from utils.utils_dns import firewall_test from utils.utils_dns import vulnerable_alias @@ -13,14 +14,14 @@ missing_resources = [] -def route53(profile): +def route53(): print("Searching for Route53 hosted zones") - session = boto3.Session(profile_name=profile) + session = boto3.Session() route53 = session.client("route53") - hosted_zones = list_hosted_zones_manual_scan(profile) + hosted_zones = list_hosted_zones_manual_scan() for hosted_zone in hosted_zones: print(f"Searching for ElasticBeanststalk Alias records in hosted zone {hosted_zone['Name']}") paginator_records = route53.get_paginator("list_resource_record_sets") @@ -34,7 +35,7 @@ def route53(profile): record_sets = [ r for r in page_records["ResourceRecordSets"] - if "AliasTarget" in r and "elasticbeanstalk.com" in r["AliasTarget"]["DNSName"] + if "AliasTarget" in r and eb_susceptible(r["AliasTarget"]["DNSName"]) ] for record in record_sets: @@ -52,12 +53,9 @@ def route53(profile): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Prevent Subdomain Takeover") - parser.add_argument("--profile", required=True) - args = parser.parse_args() - profile = args.profile firewall_test() - route53(profile) + route53() count = len(vulnerable_domains) my_print("\nTotal Vulnerable Domains Found: " + str(count), "INFOB") diff --git a/manual_scans/aws/aws-cname-cloudfront-s3.py b/manual_scans/aws/aws-cname-cloudfront-s3.py index dc9b068b..05f4b980 100644 --- a/manual_scans/aws/aws-cname-cloudfront-s3.py +++ b/manual_scans/aws/aws-cname-cloudfront-s3.py @@ -27,14 +27,14 @@ def vulnerable_cname_cloudfront_s3(domain_name): return False -def route53(profile): +def route53(): print("Searching for Route53 hosted zones") - session = boto3.Session(profile_name=profile) + session = boto3.Session() route53 = session.client("route53") - hosted_zones = list_hosted_zones_manual_scan(profile) + hosted_zones = list_hosted_zones_manual_scan() for hosted_zone in hosted_zones: print(f"Searching for CloudFront CNAME records in hosted zone {hosted_zone['Name']}") @@ -51,7 +51,9 @@ def route53(profile): record_sets = [ r for r in page_records["ResourceRecordSets"] - if r["Type"] == "CNAME" and "cloudfront.net" in r["ResourceRecords"][0]["Value"] + if r["Type"] == "CNAME" + and r.get("ResourceRecords") + and "cloudfront.net" in r["ResourceRecords"][0]["Value"] ] for record in record_sets: @@ -68,12 +70,9 @@ def route53(profile): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Prevent Subdomain Takeover") - parser.add_argument("--profile", required=True) - args = parser.parse_args() - profile = args.profile firewall_test() - route53(profile) + route53() count = len(vulnerable_domains) my_print("\nTotal Vulnerable Domains Found: " + str(count), "INFOB") diff --git a/manual_scans/aws/aws-cname-eb.py b/manual_scans/aws/aws-cname-eb.py index 932848a2..3ecfe822 100644 --- a/manual_scans/aws/aws-cname-eb.py +++ b/manual_scans/aws/aws-cname-eb.py @@ -4,6 +4,7 @@ import boto3 import dns.resolver +from utils.utils_aws import eb_susceptible from utils.utils_aws_manual import list_hosted_zones_manual_scan from utils.utils_dns import firewall_test from utils.utils_dns import vulnerable_cname @@ -13,14 +14,14 @@ vulnerable_domains = [] -def route53(profile): +def route53(): print("Searching for Route53 hosted zones") - session = boto3.Session(profile_name=profile) + session = boto3.Session() route53 = session.client("route53") - hosted_zones = list_hosted_zones_manual_scan(profile) + hosted_zones = list_hosted_zones_manual_scan() for hosted_zone in hosted_zones: print(f"Searching for ElasticBeanstalk CNAME records in hosted zone {hosted_zone['Name']}") paginator_records = route53.get_paginator("list_resource_record_sets") @@ -34,7 +35,9 @@ def route53(profile): record_sets = [ r for r in page_records["ResourceRecordSets"] - if r["Type"] in ["CNAME"] and "elasticbeanstalk.com" in r["ResourceRecords"][0]["Value"] + if r["Type"] in ["CNAME"] + and r.get("ResourceRecords") + and eb_susceptible(r["ResourceRecords"][0]["Value"]) ] for record in record_sets: i = i + 1 @@ -49,12 +52,9 @@ def route53(profile): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Prevent Subdomain Takeover") - parser.add_argument("--profile", required=True) - args = parser.parse_args() - profile = args.profile firewall_test() - route53(profile) + route53() count = len(vulnerable_domains) my_print(f"\nTotal Vulnerable Domains Found: {str(count)}", "INFOB") diff --git a/manual_scans/aws/aws-cname-s3.py b/manual_scans/aws/aws-cname-s3.py index 6cd73f76..7903e697 100644 --- a/manual_scans/aws/aws-cname-s3.py +++ b/manual_scans/aws/aws-cname-s3.py @@ -27,14 +27,14 @@ def vulnerable_cname_s3(domain_name): return False -def route53(profile): +def route53(): print("Searching for Route53 hosted zones") - session = boto3.Session(profile_name=profile) + session = boto3.Session() route53 = session.client("route53") - hosted_zones = list_hosted_zones_manual_scan(profile) + hosted_zones = list_hosted_zones_manual_scan() for hosted_zone in hosted_zones: print(f"Searching for S3 CNAME records in hosted zone {hosted_zone['Name']}") paginator_records = route53.get_paginator("list_resource_record_sets") @@ -49,8 +49,9 @@ def route53(profile): r for r in page_records["ResourceRecordSets"] if r["Type"] in ["CNAME"] + and r.get("ResourceRecords") and "amazonaws.com" in r["ResourceRecords"][0]["Value"] - and ".s3-website." in r["ResourceRecords"][0]["Value"] + and ".s3-website" in r["ResourceRecords"][0]["Value"] ] for record in record_sets: print(f"checking if {record['Name']} is vulnerable to takeover") @@ -66,12 +67,9 @@ def route53(profile): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Prevent Subdomain Takeover") - parser.add_argument("--profile", required=True) - args = parser.parse_args() - profile = args.profile firewall_test() - route53(profile) + route53() count = len(vulnerable_domains) my_print("\nTotal Vulnerable Domains Found: " + str(count), "INFOB") diff --git a/manual_scans/aws/aws-ns-domain.py b/manual_scans/aws/aws-ns-domain.py index 4c9d6a62..461b9fec 100644 --- a/manual_scans/aws/aws-ns-domain.py +++ b/manual_scans/aws/aws-ns-domain.py @@ -11,11 +11,11 @@ vulnerable_domains = [] -def route53domains(profile): +def route53domains(): print("Searching for Route53 registered domains") - session = boto3.Session(profile_name=profile, region_name="us-east-1") + session = boto3.Session(region_name="us-east-1") route53domains = session.client("route53domains") paginator_domains = route53domains.get_paginator("list_domains") @@ -41,12 +41,9 @@ def route53domains(profile): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Prevent Subdomain Takeover") - parser.add_argument("--profile", required=True) - args = parser.parse_args() - profile = args.profile firewall_test() - route53domains(profile) + route53domains() count = len(vulnerable_domains) my_print("\nTotal Vulnerable Domains Found: " + str(count), "INFOB") diff --git a/manual_scans/aws/aws-ns-subdomain.py b/manual_scans/aws/aws-ns-subdomain.py index 3b53507d..0cddbd23 100644 --- a/manual_scans/aws/aws-ns-subdomain.py +++ b/manual_scans/aws/aws-ns-subdomain.py @@ -13,13 +13,13 @@ vulnerable_domains = [] -def route53(profile): +def route53(): - session = boto3.Session(profile_name=profile) + session = boto3.Session() route53 = session.client("route53") print("Searching for Route53 hosted zones") - hosted_zones = list_hosted_zones_manual_scan(profile) + hosted_zones = list_hosted_zones_manual_scan() for hosted_zone in hosted_zones: print(f"Searching for subdomain NS records in hosted zone {hosted_zone['Name']}") paginator_records = route53.get_paginator("list_resource_record_sets") @@ -45,12 +45,9 @@ def route53(profile): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Prevent Subdomain Takeover") - parser.add_argument("--profile", required=True) - args = parser.parse_args() - profile = args.profile firewall_test() - route53(profile) + route53() count = len(vulnerable_domains) my_print("\nTotal Vulnerable Domains Found: " + str(count), "INFOB") diff --git a/manual_scans/aws/aws_alias_s3.py b/manual_scans/aws/aws_alias_s3.py index b5a0cbc1..29c3dbc1 100644 --- a/manual_scans/aws/aws_alias_s3.py +++ b/manual_scans/aws/aws_alias_s3.py @@ -23,16 +23,16 @@ def vulnerable_alias_s3(domain_name): return False -def route53(profile): +def route53(): vulnerable_domains = [] missing_resources = [] print("Searching for Route53 hosted zones") - session = boto3.Session(profile_name=profile) + session = boto3.Session() route53 = session.client("route53") - hosted_zones = list_hosted_zones_manual_scan(profile) + hosted_zones = list_hosted_zones_manual_scan() for hosted_zone in hosted_zones: print(f"Searching for S3 Alias records in hosted zone {hosted_zone['Name']}") @@ -65,11 +65,9 @@ def route53(profile): def main(): parser = argparse.ArgumentParser(description="Prevent Subdomain Takeover") - parser.add_argument("--profile", required=True) args = parser.parse_args() - profile = args.profile - vulnerable_domains, missing_resources = route53(profile) + vulnerable_domains, missing_resources = route53() count = len(vulnerable_domains) my_print(f"\nTotal Vulnerable Domains Found: {str(count)}", "INFOB") diff --git a/manual_scans/aws/images/aws-cloudfront-s3-alias.png b/manual_scans/aws/images/aws-cloudfront-s3-alias.png index 03ca8778..bde12c2b 100644 Binary files a/manual_scans/aws/images/aws-cloudfront-s3-alias.png and b/manual_scans/aws/images/aws-cloudfront-s3-alias.png differ diff --git a/manual_scans/aws/images/aws-cloudfront-s3-cname.png b/manual_scans/aws/images/aws-cloudfront-s3-cname.png index b4b31928..36a1ee06 100644 Binary files a/manual_scans/aws/images/aws-cloudfront-s3-cname.png and b/manual_scans/aws/images/aws-cloudfront-s3-cname.png differ diff --git a/manual_scans/aws/images/aws-eb-alias.png b/manual_scans/aws/images/aws-eb-alias.png index 4133d228..0db40bf6 100644 Binary files a/manual_scans/aws/images/aws-eb-alias.png and b/manual_scans/aws/images/aws-eb-alias.png differ diff --git a/manual_scans/aws/images/aws-eb-cnames.png b/manual_scans/aws/images/aws-eb-cnames.png index aefbef45..5edb3339 100644 Binary files a/manual_scans/aws/images/aws-eb-cnames.png and b/manual_scans/aws/images/aws-eb-cnames.png differ diff --git a/manual_scans/aws/images/aws-ns-domain.png b/manual_scans/aws/images/aws-ns-domain.png index bb24e1c9..afe92ae6 100644 Binary files a/manual_scans/aws/images/aws-ns-domain.png and b/manual_scans/aws/images/aws-ns-domain.png differ diff --git a/manual_scans/aws/images/aws-ns-subdomain.png b/manual_scans/aws/images/aws-ns-subdomain.png index 732790b6..b47d1a4c 100644 Binary files a/manual_scans/aws/images/aws-ns-subdomain.png and b/manual_scans/aws/images/aws-ns-subdomain.png differ diff --git a/manual_scans/aws/images/aws-s3-alias.png b/manual_scans/aws/images/aws-s3-alias.png index f3ed30b1..b8227b15 100644 Binary files a/manual_scans/aws/images/aws-s3-alias.png and b/manual_scans/aws/images/aws-s3-alias.png differ diff --git a/manual_scans/aws/images/aws-s3-cnames.png b/manual_scans/aws/images/aws-s3-cnames.png index ddeae229..5b779a57 100644 Binary files a/manual_scans/aws/images/aws-s3-cnames.png and b/manual_scans/aws/images/aws-s3-cnames.png differ diff --git a/terraform-modules/iam/main.tf b/terraform-modules/iam/main.tf index 43b2becd..4168d89a 100644 --- a/terraform-modules/iam/main.tf +++ b/terraform-modules/iam/main.tf @@ -1,7 +1,8 @@ resource "aws_iam_role" "lambda" { - name = "${var.project}-${local.role_name}-${local.env}" - assume_role_policy = templatefile("${path.module}/templates/${var.assume_role_policy}_role.json.tpl", { project = var.project }) - managed_policy_arns = var.takeover ? ["arn:aws:iam::aws:policy/AmazonVPCFullAccess", "arn:aws:iam::aws:policy/AdministratorAccess-AWSElasticBeanstalk", "arn:aws:iam::aws:policy/AmazonS3FullAccess", "arn:aws:iam::aws:policy/AWSCloudFormationFullAccess"] : [] + name = "${var.project}-${local.role_name}-${local.env}" + assume_role_policy = templatefile("${path.module}/templates/${var.assume_role_policy}_role.json.tpl", { project = var.project }) + managed_policy_arns = var.takeover ? ["arn:aws:iam::aws:policy/AmazonVPCFullAccess", "arn:aws:iam::aws:policy/AdministratorAccess-AWSElasticBeanstalk", "arn:aws:iam::aws:policy/AmazonS3FullAccess", "arn:aws:iam::aws:policy/AWSCloudFormationFullAccess"] : [] + permissions_boundary = var.permissions_boundary_arn } resource "aws_iam_role_policy" "lambda" { diff --git a/terraform-modules/iam/variables.tf b/terraform-modules/iam/variables.tf index 018e03c6..96c44a08 100644 --- a/terraform-modules/iam/variables.tf +++ b/terraform-modules/iam/variables.tf @@ -27,3 +27,8 @@ variable "role_name" { description = "role name if different from policy name" default = "policyname" } + +variable "permissions_boundary_arn" { + description = "permissions boundary ARN" + default = "" +} diff --git a/unittests/utils/test_utils_aws.py b/unittests/utils/test_utils_aws.py index 56740167..4a652fe8 100644 --- a/unittests/utils/test_utils_aws.py +++ b/unittests/utils/test_utils_aws.py @@ -5,6 +5,7 @@ from utils.utils_aws import assume_role from utils.utils_aws import create_session +from utils.utils_aws import eb_susceptible from utils.utils_aws import generate_role_arn from utils.utils_aws import generate_temporary_credentials @@ -218,3 +219,33 @@ def test_assume_role_logs_message_on_exception(os_environ_mock, generate_temp_cr _ = assume_role("some_account") log_mock.assert_called_once_with("ERROR: Failed to assume test_role role in AWS account some_account") + + +def test_eb_susceptible_returns_true_with_user_chosen_name(): + result = eb_susceptible("myapp.eu-west-1.elasticbeanstalk.com") + + assert_that(result).is_true() + + +def test_eb_susceptible_returns_true_with_domain_ending_in_period(): + result = eb_susceptible("myapp.eu-west-1.elasticbeanstalk.com.") + + assert_that(result).is_true() + + +def test_eb_susceptible_returns_true_with_auto_generated_name(): + result = eb_susceptible("myapp.7890hw48u596.eu-west-1.elasticbeanstalk.com") + + assert_that(result).is_false() + + +def test_eb_susceptible_returns_false_with_reserved_prefix(): + result = eb_susceptible("eba-myapp.eu-west-1.elasticbeanstalk.com") + + assert_that(result).is_false() + + +def test_eb_susceptible_returns_false_with_non_eb_domain(): + result = eb_susceptible("eba-myapp.eu-west-1.azurewebsites.net") + + assert_that(result).is_false() diff --git a/utils/utils_aws.py b/utils/utils_aws.py index 58345c7d..ff9b3d1a 100644 --- a/utils/utils_aws.py +++ b/utils/utils_aws.py @@ -257,3 +257,27 @@ def domain_deleted(domain, account_name): print(f"{domain} no longer in Route53 registered domains") return True + + +def eb_susceptible(domain): + """Returns a value of True if the domain is susceptible to EB hijacking""" + # remove trailing dot if present + if domain.endswith("."): + domain = domain[:-1] + + # identify if Elastic Beanstalk name has been auto created by AWS + if domain.endswith(".elasticbeanstalk.com"): + if len(domain.split(".")) == 5: + print(f"ignoring {domain} as auto-generated by AWS so not susceptible to takeover") + return False + + # don't include Elastic Beanstalk domains starting eba- as this prefix is reserved by AWS + if domain.startswith("eba-"): + print(f"ignoring {domain} as prefix eba- is reserved by AWS so not susceptible to takeover") + return False + + # the Elastic Beanstalk is vulnerable to hijacking if neither of the above conditions are met + return True + + # domain is not an Elastic Beanstalk domain + return False diff --git a/utils/utils_aws_manual.py b/utils/utils_aws_manual.py index 42e97b27..32372fcd 100644 --- a/utils/utils_aws_manual.py +++ b/utils/utils_aws_manual.py @@ -1,8 +1,8 @@ import boto3 -def list_hosted_zones_manual_scan(profile): - session = boto3.Session(profile_name=profile) +def list_hosted_zones_manual_scan(): + session = boto3.Session() route53 = session.client("route53") hosted_zones_list = [] diff --git a/variables.tf b/variables.tf index 5b164b7f..dd9b6a4e 100644 --- a/variables.tf +++ b/variables.tf @@ -218,3 +218,8 @@ variable "allowed_regions" { description = "If SCPs block certain regions across all accounts, optionally replace with string formatted list of allowed regions" default = "['all']" # example "['eu-west-1', 'us-east-1']" } + +variable "permissions_boundary_arn" { + description = "permissions boundary ARN to attach to every IAM role" + default = "" +}