├── csp.gif ├── Pipfile ├── LICENSE ├── README.md ├── Pipfile.lock └── csp_parser.py /csp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xbharath/domains-from-csp/HEAD/csp.gif -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "*" 10 | click = "*" 11 | tldextract = "*" 12 | pythonwhois = "*" 13 | 14 | [requires] 15 | python_version = "2.7" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bharath 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # domains-from-csp 2 | 3 | A Python script to parse domain names from CSP header 4 | 5 | - A script to extract domain names from Content Security Policy(CSP) headers 6 | - The output is print to stdout for now 7 | 8 | ## Demo 9 | 10 | ![script-in-action](csp.gif) 11 | 12 | ## Screenshots 13 | 14 | 15 | ## 3rd party package dependency 16 | 17 | [**requests**](https://docs.python-requests.org) 18 | 19 | [**click**](https://pypi.python.org/pypi/click) 20 | 21 | ## Setup 22 | 23 | - Clone this repo 24 | 25 | ```bash 26 | $ git clone git@github.com:yamakira/censys-enumeration.git 27 | ``` 28 | 29 | - Install dependencies 30 | 31 | ```bash 32 | $ pipenv install 33 | ``` 34 | 35 | 36 | - Check help menu 37 | 38 | 39 | ```bash 40 | $ python csp_parser.py --help 41 | 2 ↵ 42 | Usage: csp_parser.py [OPTIONS] URL 43 | 44 | Options: 45 | -r, --resolve / --no-resolve Enable/Disable DNS resolution 46 | --help Show this message and exit. 47 | 48 | ``` 49 | 50 | ## Usage 51 | 52 | - Parse the CSP header for domain names but don't do DNS resolution 53 | 54 | 55 | ``` 56 | $ python csp_parser.py target_url 57 | ``` 58 | 59 | - Parse the CSP header for domain names and also do DNS resolution 60 | 61 | ``` 62 | $ python csp_parser.py target_url --resolve 63 | ``` 64 | 65 | ``` 66 | $ python csp_parser.py target_url -r 67 | ``` -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7ef2df7549e46a73c7a23cc9ac635a9d41208fc50148506c0fc3bf1645264ae1" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "2.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", 22 | "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" 23 | ], 24 | "version": "==2018.4.16" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 29 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 30 | ], 31 | "version": "==3.0.4" 32 | }, 33 | "click": { 34 | "hashes": [ 35 | "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", 36 | "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" 37 | ], 38 | "index": "pypi", 39 | "version": "==6.7" 40 | }, 41 | "idna": { 42 | "hashes": [ 43 | "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", 44 | "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" 45 | ], 46 | "version": "==2.6" 47 | }, 48 | "requests": { 49 | "hashes": [ 50 | "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", 51 | "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" 52 | ], 53 | "index": "pypi", 54 | "version": "==2.18.4" 55 | }, 56 | "urllib3": { 57 | "hashes": [ 58 | "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", 59 | "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" 60 | ], 61 | "version": "==1.22" 62 | } 63 | }, 64 | "develop": {} 65 | } 66 | -------------------------------------------------------------------------------- /csp_parser.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from requests import get, exceptions 4 | import click 5 | from socket import gethostbyname, gaierror 6 | from sys import version_info, exit 7 | 8 | if version_info[0] == 2: 9 | from urlparse import urlparse 10 | elif version_info[0] == 3: 11 | import urllib.parse.urlsplit as urlparse 12 | 13 | import logging 14 | import tldextract 15 | import json 16 | import pythonwhois 17 | 18 | logging.basicConfig( 19 | level=logging.INFO, 20 | format="%(message)s" 21 | ) 22 | 23 | logger = logging.getLogger('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 24 | 25 | __author__ = "Bharath(github.com/yamakira)" 26 | __version__ = "0.0.1" 27 | __purpose__ = '''Parse and print domain names from Content Security Policy(CSP) header''' 28 | 29 | 30 | class Domain: 31 | def __init__(self, domain=None, apex_domain=None, available=None, ip=None, raw_csp_url=None): 32 | self.domain = domain 33 | self.apex_domain = apex_domain 34 | self.available = available 35 | self.ip = ip 36 | self.raw_csp_url = raw_csp_url 37 | 38 | 39 | def clean_domains(domains): 40 | for domain in domains: 41 | ext = tldextract.extract(str(domain.raw_csp_url)) 42 | # If subdomain is wildcard or empty 43 | if ext[0] in ['*', '']: 44 | # Join all but the subdomain (a wildcard or empty) 45 | domain.domain = '.'.join(ext[1:]) 46 | else: 47 | domain.domain = '.'.join(ext) 48 | domain.apex_domain = ".".join(tldextract.extract(domain.domain)[1:]) 49 | return domains 50 | 51 | 52 | def get_csp_header(url): 53 | try: 54 | logger.info("[+] Fetching headers for {}".format(url)) 55 | r = get(url) 56 | except exceptions.RequestException as e: 57 | print(e) 58 | exit(1) 59 | 60 | if 'Content-Security-Policy' in r.headers: 61 | csp_header = r.headers['Content-Security-Policy'] 62 | return csp_header 63 | elif 'content-security-policy-report-only' in r.headers: 64 | csp_header = r.headers['content-security-policy-report-only:'] 65 | return csp_header 66 | else: 67 | logger.info("[+] {} doesn't support CSP header".format(url)) 68 | exit(1) 69 | 70 | 71 | def get_domains(csp_header): 72 | domains = [] 73 | csp_header_values = csp_header.split(" ") 74 | for line in csp_header_values: 75 | if "." in line: 76 | line = line.replace(";", "") 77 | domains.append(Domain(raw_csp_url=line)) 78 | else: 79 | pass 80 | return clean_domains(domains) 81 | 82 | 83 | def resolve_domains(domains): 84 | # To resolve the domains, we need to clean them 85 | for domain in clean_domains(domains): 86 | try: 87 | ip_address = gethostbyname(domain.domain) 88 | domain.ip = ip_address 89 | print("\033[92m{0:<30} - {1:20}\033[1;m".format(domain.domain, ip_address.rstrip("\n\r"))) 90 | except gaierror as e: 91 | print("\033[93m{0:<30} - {1:20}\033[1;m".format(domain.domain, "No A record exists"), end=''), 92 | print(e.message) 93 | return domains 94 | 95 | 96 | def check_whois_domains(domains): 97 | # TODO - Check apex domains once instead of for each domain stored (the same apex domain may appear several times) 98 | for domain in domains: 99 | details = pythonwhois.get_whois(domain.apex_domain) 100 | if details.get('status') is None: 101 | print("[!] Domain available for registering: {}".format(domain.apex_domain)) 102 | domain.available = True 103 | else: 104 | print("[i] Domain registered: {}".format(domain.apex_domain)) 105 | domain.available = False 106 | return domains 107 | 108 | 109 | @click.command() 110 | @click.option('--url', '-u', required=True, 111 | help='Url to retrieve the CSP header from') 112 | @click.option('--resolve/--no-resolve', '-r', default=False, 113 | help='Enable/Disable DNS resolution') 114 | @click.option('--check-whois/--no-check-whois', '--whois', default=False, 115 | help='Check for domain availability') 116 | @click.option('--output', '-o', default=False, 117 | help='Save results into a json file') 118 | def main(url, resolve, check_whois, output): 119 | csp_header = get_csp_header(url) 120 | # Retrieve list of domains "clean" or not 121 | domains = get_domains(csp_header) 122 | if resolve: 123 | domains = resolve_domains(domains) 124 | if check_whois: 125 | domains = check_whois_domains(domains) 126 | if output: 127 | with open(output, 'w') as outfile: 128 | json.dump(dict(domains=[ob.__dict__ for ob in domains]), outfile, sort_keys=True, indent=4) 129 | 130 | 131 | if __name__ == '__main__': 132 | main() 133 | --------------------------------------------------------------------------------