Skip to content

Commit

Permalink
feat: Add support for DigitalOcean provider (#115)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnotbrandon authored Sep 1, 2022
1 parent 6eb4606 commit f8673cf
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 9 deletions.
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,32 +78,32 @@ python main.py --help
Scan all your DNS records for subdomain takeovers!
usage:
.\main.py provider [options]
main.py provider [options]
output:
findings output to screen and (by default) results.csv
help:
.\main.py --help
main.py --help
providers:
> aws - Scan multiple domains by fetching them from AWS Route53
> azure - Scan multiple domains by fetching them from Azure DNS services
> bind - Read domains from a dns BIND zone file, or path to multiple
> cloudflare - Scan multiple domains by fetching them from Cloudflare
> file - Read domains from a file, one per line
> digitalocean - Scan multiple domains by fetching them from Digital Ocean
> file - Read domains from a file (or folder of files), one per line
> single - Scan a single domain by providing a domain on the commandline
> zonetransfer - Scan multiple domains by fetching records via DNS zone transfer
positional arguments:
{aws,azure,bind,cloudflare,file,single,zonetransfer}
{aws,azure,bind,cloudflare,digitalocean,file,single,zonetransfer}
options:
-h, --help Show this help message and exit
--out OUT Output file (default: results) - use 'stdout' to stream out
--out-format {csv,json}
--resolver RESOLVER
Provide a custom DNS resolver (or multiple seperated by commas)
--resolver RESOLVER Provide a custom DNS resolver (or multiple seperated by commas)
--parallelism PARALLELISM
Number of domains to test in parallel - too high and you may see odd DNS results (default: 30)
--disable-probable Do not check for probable conditions
Expand Down Expand Up @@ -148,8 +148,16 @@ cloudflare:
--cloudflare-token CLOUDFLARE_TOKEN
Required
digitalocean:
Scan multiple domains by fetching them from Digital Ocean
--do-api-key DO_API_KEY
Required
--do-domains DO_DOMAINS
Optional
file:
Read domains from a file, one per line
Read domains from a file (or folder of files), one per line
--filename FILENAME Required
Expand All @@ -165,4 +173,4 @@ zonetransfer:
Required
--zonetransfer-domain ZONETRANSFER_DOMAIN
Required
```
```
4 changes: 3 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@
- [AWS](aws.md)
- [Azure](azure.md)
- [BIND](bind.md)
- [CloudFlare](cloudflare.md)
- [CloudFlare](cloudflare.md)
- [Digital Ocean](digitalocean.md)
- [Zone Transfer](zonetransfer.md)
16 changes: 16 additions & 0 deletions docs/digitalocean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# DigitalOcean

## Description
The DigitalOcean provider connects to the DigitalOcean API and retrieves domains and records.
It can enumerate all available domains, or alternatively you can supply a comma-separated list of domains to limit
the scope to.

# Usage
The `--do-api-key` option is used to provide your DigitalOcean API Key. API keys are available from the DigitalOcean
control panel (click API in the sidebar, or [here for a direct link](https://cloud.digitalocean.com/account/api/tokens)).

The API key should be limited to read-only access.

The `--do-domains` option is used to limit the domains that are being scanned. Multiple can be provided by separating
each domain with a comma, eg:
`--do-domains first.domain.example,second.domain.example`
16 changes: 16 additions & 0 deletions docs/zonetransfer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Zone Transfer

## Description
The ZoneTransfer provider connects to a DNS Servers and attempts to fetch all records for a given domain.

It requires 2 parameters, and both are mandatory. Zone Transfers must fetch a single DNS zone, there is no mechanism in the spec to enumerate all zones on the DNS server.

The DNS server should permit Zone Transfers to your IP. There is no auth mechanism in the zone transfer spec so it operates on an ip allowlist. You also need TCP Port 53 access to the server, not UDP.

# Usage
zonetransfer_nameserver, zonetransfer_domain
The `--zonetransfer-nameserver` option is used to provide your DNS server fqdn (such as ns1.domain.com) or DNS server IP. ).


The `--zonetransfer-domain` option is used to specify the domain to fetch. This should be the root domain, i.e. a domain of punksecurity.co.uk would be used to fetch all subdomains such as www.punksecurity.co.uk.

113 changes: 113 additions & 0 deletions providers/digitalocean.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import boto3
import logging

import requests

from domain import Domain

description = "Scan multiple domains by fetching them from Digital Ocean"


class DomainNotFoundError(Exception):
def __init__(self, domain):
self.message = "Domain not found: " + domain
super().__init__(self.message)


class DoApi:
def __init__(self, api_key):
self.session = requests.session()
self.session.headers.update(
{"Content-Type": "application/json", "Authorization": "Bearer " + api_key}
)

@staticmethod
def check_response(response: requests.Response):
if response.status_code == 401:
raise ValueError("Invalid API key specified.")

if response.status_code < 200 or response.status_code >= 300:
raise ValueError("Invalid response received from API: " + response.json())

return response

def make_request(self, endpoint):
return self.session.prepare_request(
requests.Request("GET", "https://api.digitalocean.com/v2/" + endpoint)
)

def list_domains(self):
req = self.make_request("domains")

return self.check_response(self.session.send(req))

def get_records(self, domain):
req = self.make_request(f"domains/{domain}/records")
res = self.session.send(req)

if 404 == res.status_code:
raise DomainNotFoundError(domain)

return self.check_response(res)


def convert_records_to_domains(records, root_domain):
buf = {}
for record in records:
if "@" == record["name"]:
continue

record_name = f"{record['name']}.{root_domain}"

if record_name not in buf.keys():
buf[record_name] = {}

if record["type"] not in buf[record_name].keys():
buf[record_name][record["type"]] = []

if "data" in record.keys():
buf[record_name][record["type"]].append(record["data"])

def extract_records(desired_type):
return [r.rstrip(".") for r in buf[subdomain][desired_type]]

for subdomain in buf.keys():
domain = Domain(subdomain.rstrip("."), fetch_standard_records=False)

if "A" in buf[subdomain].keys():
domain.A = extract_records("A")
if "AAAA" in buf[subdomain].keys():
domain.AAAA = extract_records("AAAA")
if "CNAME" in buf[subdomain].keys():
domain.CNAME = extract_records("CNAME")
if "NS" in buf[subdomain].keys():
domain.NS = extract_records("NS")

yield domain


def validate_args(do_api_key: str):
if not do_api_key.startswith("dop_v1"):
raise ValueError("DigitalOcean: Invalid API key specified")


def fetch_domains(do_api_key: str, do_domains: str = None, **args): # NOSONAR
validate_args(do_api_key)
root_domains = []
domains = []
api = DoApi(do_api_key)

if do_domains is not None and len(do_domains):
root_domains = [domain.strip(" ") for domain in do_domains.split(",")]
else:
resp_data = api.list_domains().json()
root_domains = [domain["name"] for domain in resp_data["domains"]]

for domain in root_domains:
if "" == domain or domain is None:
continue

records = api.get_records(domain).json()
domains.extend(convert_records_to_domains(records["domain_records"], domain))

return domains

0 comments on commit f8673cf

Please sign in to comment.