Skip to main content

Route53 Dangling IP Subdomain Takeover

Overview

This check identifies Route 53 A records that point to IP addresses no longer owned by your AWS account. When a DNS record references an IP that you have released (such as an Elastic IP), attackers can potentially claim that IP and take over traffic intended for your subdomain.

Risk

Severity: High

If an attacker acquires a public IP address that your DNS still points to, they can:

  • Impersonate your service and serve malicious content under your domain
  • Steal user credentials by hosting fake login pages
  • Intercept sensitive data such as cookies, tokens, and form submissions
  • Damage your reputation through phishing or malware distribution

This attack vector is known as "subdomain takeover" and is particularly dangerous because it leverages trust in your domain name.

Remediation Steps

Prerequisites

You need:

  • Access to the AWS Console with permissions to modify Route 53 records
  • Knowledge of which AWS resource (load balancer, CloudFront, etc.) should receive traffic for the affected subdomain
Required IAM permissions

Your IAM user or role needs these permissions:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ChangeResourceRecordSets"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ec2:DescribeAddresses"
],
"Resource": "*"
}
]
}

AWS Console Method

Step 1: Find the problematic record

  1. Open the Route 53 console
  2. Click Hosted zones in the left menu
  3. Select the hosted zone containing the flagged record
  4. Look for A records with a literal IP address (not an alias)

Step 2: Decide on the fix

You have two options:

  • Option A: Delete the record if the subdomain is no longer needed
  • Option B: Convert to an alias record pointing to an active AWS resource

Step 3A: Delete the dangling record

  1. Check the box next to the problematic A record
  2. Click Delete record
  3. Confirm the deletion

Step 3B: Convert to an alias record

  1. Check the box next to the problematic A record
  2. Click Edit record
  3. Toggle Alias to ON
  4. Under Route traffic to, select your target:
    • For a load balancer: Choose Alias to Application and Classic Load Balancer, select us-east-1, then select your load balancer
    • For CloudFront: Choose Alias to CloudFront distribution, then select your distribution
    • For S3 website: Choose Alias to S3 website endpoint, select us-east-1, then select your bucket
  5. Click Save
AWS CLI (optional)

Identify dangling records

First, list A records in your hosted zone:

aws route53 list-resource-record-sets \
--hosted-zone-id Z1234567890ABC \
--region us-east-1 \
--query "ResourceRecordSets[?Type=='A' && ResourceRecords!=null]"

Check if an IP is an Elastic IP you own:

aws ec2 describe-addresses \
--public-ips 203.0.113.50 \
--region us-east-1

If this returns empty or an error, you do not own that IP.

Delete a dangling record

aws route53 change-resource-record-sets \
--hosted-zone-id Z1234567890ABC \
--region us-east-1 \
--change-batch '{
"Comment": "Remove dangling A record",
"Changes": [{
"Action": "DELETE",
"ResourceRecordSet": {
"Name": "app.example.com",
"Type": "A",
"TTL": 300,
"ResourceRecords": [{"Value": "203.0.113.50"}]
}
}]
}'

Replace with an alias record

This requires two steps: delete the old record and create the alias.

aws route53 change-resource-record-sets \
--hosted-zone-id Z1234567890ABC \
--region us-east-1 \
--change-batch '{
"Comment": "Replace dangling IP with ALB alias",
"Changes": [
{
"Action": "DELETE",
"ResourceRecordSet": {
"Name": "app.example.com",
"Type": "A",
"TTL": 300,
"ResourceRecords": [{"Value": "203.0.113.50"}]
}
},
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "app.example.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z35SXDOTRQ7X7K",
"DNSName": "my-alb-123456789.us-east-1.elb.amazonaws.com",
"EvaluateTargetHealth": true
}
}
}
]
}'

Note: The HostedZoneId in AliasTarget is the ALB's hosted zone ID, not your domain's hosted zone. For us-east-1 ALBs, this is Z35SXDOTRQ7X7K. See AWS documentation for other regions.

CloudFormation (optional)

Use an alias record instead of a hardcoded IP to prevent dangling IPs:

AWSTemplateFormatVersion: '2010-09-09'
Description: Route53 Alias A Record pointing to ALB (prevents dangling IP)

Parameters:
HostedZoneId:
Type: AWS::Route53::HostedZone::Id
Description: The Route 53 hosted zone ID
RecordName:
Type: String
Description: The DNS record name (e.g., app.example.com)
ALBHostedZoneId:
Type: String
Description: The hosted zone ID of the ALB (region-specific)
ALBDNSName:
Type: String
Description: The DNS name of the Application Load Balancer

Resources:
AliasARecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref HostedZoneId
Name: !Ref RecordName
Type: A
AliasTarget:
DNSName: !Ref ALBDNSName
HostedZoneId: !Ref ALBHostedZoneId
EvaluateTargetHealth: true

Outputs:
RecordSetName:
Description: The created DNS record
Value: !Ref AliasARecord

Deploy the stack:

aws cloudformation deploy \
--template-file template.yaml \
--stack-name route53-alias-record \
--parameter-overrides \
HostedZoneId=Z1234567890ABC \
RecordName=app.example.com \
ALBHostedZoneId=Z35SXDOTRQ7X7K \
ALBDNSName=my-alb-123456789.us-east-1.elb.amazonaws.com \
--region us-east-1
Terraform (optional)

Use alias records to prevent dangling IPs:

variable "zone_id" {
description = "The Route 53 hosted zone ID"
type = string
}

variable "record_name" {
description = "The DNS record name (e.g., app.example.com)"
type = string
}

variable "alb_dns_name" {
description = "The DNS name of the Application Load Balancer"
type = string
}

variable "alb_zone_id" {
description = "The hosted zone ID of the ALB"
type = string
}

resource "aws_route53_record" "alias_a_record" {
zone_id = var.zone_id
name = var.record_name
type = "A"

alias {
name = var.alb_dns_name
zone_id = var.alb_zone_id
evaluate_target_health = true
}
}

output "record_fqdn" {
description = "The FQDN of the created record"
value = aws_route53_record.alias_a_record.fqdn
}

Example terraform.tfvars:

zone_id      = "Z1234567890ABC"
record_name = "app.example.com"
alb_dns_name = "my-alb-123456789.us-east-1.elb.amazonaws.com"
alb_zone_id = "Z35SXDOTRQ7X7K"

Apply the configuration:

terraform init
terraform plan
terraform apply

Verification

After making changes:

  1. Return to the Route 53 console and confirm the record shows as an Alias (or is deleted)
  2. Test DNS resolution to ensure traffic reaches your intended resource:
    • In your browser, visit the subdomain and verify it loads correctly
    • Or use a DNS lookup tool to confirm the record resolves properly
Advanced verification commands

Check DNS resolution:

dig app.example.com +short

Re-run the Prowler check:

prowler aws --check route53_dangling_ip_subdomain_takeover --region us-east-1

List all A records to audit for other dangling IPs:

aws route53 list-resource-record-sets \
--hosted-zone-id Z1234567890ABC \
--region us-east-1 \
--query "ResourceRecordSets[?Type=='A' && ResourceRecords!=null].{Name:Name,IP:ResourceRecords[0].Value}" \
--output table

Additional Resources

Notes

  • Alias records are strongly preferred over A records with literal IPs. Alias records automatically track changes to AWS resources and cannot become dangling.
  • Audit regularly: Set up periodic reviews of DNS records against your active infrastructure.
  • Elastic IPs: If you release an Elastic IP, update or remove all DNS records pointing to it before releasing.
  • TTL consideration: When replacing records, be aware that old cached DNS entries may persist until the TTL expires. Plan accordingly for time-sensitive changes.
  • Multi-region deployments: If you use multiple regions, ensure you check for dangling records across all regions where you have Route 53 hosted zones.