From deb9847e2b3dada78a2be6b4314489bc51e32aec Mon Sep 17 00:00:00 2001 From: Sergio Garcia <38561120+sergargar@users.noreply.github.com> Date: Tue, 23 May 2023 16:35:13 +0200 Subject: [PATCH] fix(route53_dangling_ip_subdomain_takeover): notify only IPs with AWS IP Ranges (#2396) --- poetry.lock | 14 ++++- .../route53_dangling_ip_subdomain_takeover.py | 10 ++- pyproject.toml | 1 + ...e53_dangling_ip_subdomain_takeover_test.py | 63 ++++++++++++++++++- 4 files changed, 84 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index bd599089..4910e9d4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -67,6 +67,18 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope-interface"] tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +[[package]] +name = "awsipranges" +version = "0.3.3" +description = "Work with the AWS IP address ranges in native Python." +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "awsipranges-0.3.3-py3-none-any.whl", hash = "sha256:f3d7a54aeaf7fe310beb5d377a4034a63a51b72677ae6af3e0967bc4de7eedaf"}, + {file = "awsipranges-0.3.3.tar.gz", hash = "sha256:4f0b3f22a9dc1163c85b513bed812b6c92bdacd674e6a7b68252a3c25b99e2c0"}, +] + [[package]] name = "azure-common" version = "1.1.28" @@ -2891,4 +2903,4 @@ docs = ["mkdocs", "mkdocs-material"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "25f5c9874335d9fe564abc5bd7938c9350fc083dc6074f5de9f75a916ba4d71c" +content-hash = "60d5a4537ef2c46cf9ae54378fe801fda4adec8251c03cf60b815889b433f48f" diff --git a/prowler/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover.py b/prowler/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover.py index e2fa0999..34f8bc88 100644 --- a/prowler/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover.py +++ b/prowler/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover.py @@ -1,5 +1,7 @@ from ipaddress import ip_address +import awsipranges + from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.ec2.ec2_client import ec2_client from prowler.providers.aws.services.route53.route53_client import route53_client @@ -32,8 +34,12 @@ class route53_dangling_ip_subdomain_takeover(Check): report.status_extended = f"Route53 record {record} in Hosted Zone {route53_client.hosted_zones[record_set.hosted_zone_id].name} is not a dangling IP." # If Public IP check if it is in the AWS Account if not ip_address(record).is_private and record not in public_ips: - report.status = "FAIL" - report.status_extended = f"Route53 record {record} in Hosted Zone {route53_client.hosted_zones[record_set.hosted_zone_id].name} is a dangling IP which can lead to a subdomain takeover attack!" + report.status_extended = f"Route53 record {record} in Hosted Zone {route53_client.hosted_zones[record_set.hosted_zone_id].name} does not belong to AWS and it is not a dangling IP." + # Check if potential dangling IP is within AWS Ranges + aws_ip_ranges = awsipranges.get_ranges() + if aws_ip_ranges.get(record): + report.status = "FAIL" + report.status_extended = f"Route53 record {record} in Hosted Zone {route53_client.hosted_zones[record_set.hosted_zone_id].name} is a dangling IP which can lead to a subdomain takeover attack!" findings.append(report) diff --git a/pyproject.toml b/pyproject.toml index b36b7d6d..35255ce4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ version = "3.5.2" [tool.poetry.dependencies] alive-progress = "3.1.1" +awsipranges = "0.3.3" azure-identity = "1.13.0" azure-mgmt-authorization = "3.0.0" azure-mgmt-security = "5.0.0" diff --git a/tests/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover_test.py b/tests/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover_test.py index a60e266b..1fa741c3 100644 --- a/tests/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover_test.py +++ b/tests/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover_test.py @@ -164,7 +164,7 @@ class Test_route53_dangling_ip_subdomain_takeover: @mock_ec2 @mock_route53 - def test_hosted_zone_dangling_public_record(self): + def test_hosted_zone_external_record(self): conn = client("route53", region_name=AWS_REGION) zone_id = conn.create_hosted_zone( @@ -191,6 +191,67 @@ class Test_route53_dangling_ip_subdomain_takeover: audit_info = self.set_mocked_audit_info() + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=audit_info, + ): + with mock.patch( + "prowler.providers.aws.services.route53.route53_dangling_ip_subdomain_takeover.route53_dangling_ip_subdomain_takeover.route53_client", + new=Route53(audit_info), + ): + with mock.patch( + "prowler.providers.aws.services.route53.route53_dangling_ip_subdomain_takeover.route53_dangling_ip_subdomain_takeover.ec2_client", + new=EC2(audit_info), + ): + # Test Check + from prowler.providers.aws.services.route53.route53_dangling_ip_subdomain_takeover.route53_dangling_ip_subdomain_takeover import ( + route53_dangling_ip_subdomain_takeover, + ) + + check = route53_dangling_ip_subdomain_takeover() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert search( + "does not belong to AWS and it is not a dangling IP", + result[0].status_extended, + ) + assert result[0].resource_id == zone_id.replace("/hostedzone/", "") + assert ( + result[0].resource_arn + == f"arn:{audit_info.audited_partition}:route53:::{zone_id.replace('/hostedzone/','')}" + ) + + @mock_ec2 + @mock_route53 + def test_hosted_zone_dangling_public_record(self): + conn = client("route53", region_name=AWS_REGION) + + zone_id = conn.create_hosted_zone( + Name="testdns.aws.com.", CallerReference=str(hash("foo")) + )["HostedZone"]["Id"] + + conn.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch={ + "Changes": [ + { + "Action": "CREATE", + "ResourceRecordSet": { + "Name": "foo.bar.testdns.aws.com", + "Type": "A", + "ResourceRecords": [{"Value": "54.152.12.70"}], + }, + } + ] + }, + ) + from prowler.providers.aws.services.ec2.ec2_service import EC2 + from prowler.providers.aws.services.route53.route53_service import Route53 + + audit_info = self.set_mocked_audit_info() + with mock.patch( "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", new=audit_info,