#!/usr/bin/env python
#
# dnscan copyright (C) 2013-2014 rbsec
# Licensed under GPLv3, see LICENSE for details
#
from __future__ import print_function

import os
import platform
import re
import sys
import threading
import time

try:    # Ugly hack because Python3 decided to rename Queue to queue
    import Queue
except ImportError:
    import queue as Queue

try:    # Python2 and Python3 have different IP address libraries
        from ipaddress import ip_address as ipaddr
except ImportError:
    try:
        from  netaddr import IPAddress as ipaddr
    except ImportError:
        print("FATAL: Module netaddr missing (python-netaddr or python3-netaddr)")
        sys.exit(1)

try:
    import argparse
except:
    print("FATAL: Module argparse missing (python-argparse)")
    sys.exit(1)

try:
    import dns.query
    import dns.resolver
    import dns.zone
except:
    print("FATAL: Module dnspython missing (python-dnspython)")
    sys.exit(1)

# Usage: dnscan.py -d <domain name>

class scanner(threading.Thread):
    def __init__(self, queue):
        global wildcard
        threading.Thread.__init__(self)
        self.queue = queue

    def get_name(self, domain):
            global wildcard, addresses
            try:
                if sys.stdout.isatty():     # Don't spam output if redirected
                    sys.stdout.write(domain + "                              \r")
                    sys.stdout.flush()
                res = lookup(domain, recordtype)
                if args.tld and res:
                    nameservers = sorted(list(res))
                    ns0 = str(nameservers[0])[:-1]  # First nameserver
                    print(domain + " - " + col.brown + ns0 + col.end)
                if args.tld:
                    if res:
                        print(domain + " - " + res)
                    return
                for rdata in res:
                    address = rdata.address
                    if wildcard:
                        for wildcard_ip in wildcard:
                            if address == wildcard_ip:
                                return
                    if args.domain_first:
                        print(domain + " - " + col.brown + address + col.end)
                    else:
                        print(address + " - " + col.brown + domain + col.end)
                    if outfile:
                        if args.domain_first:
                            print(domain + " - " + address, file=outfile)
                        else:
                            print(address + " - " + domain, file=outfile)
                    try:
                        addresses.add(ipaddr(unicode(address)))
                    except NameError:
                        addresses.add(ipaddr(str(address)))

                if domain != target and args.recurse:    # Don't scan root domain twice
                    add_target(domain)  # Recursively scan subdomains
            except:
                pass

    def run(self):
        while True:
            try:
                domain = self.queue.get(timeout=1)
            except:
                return
            self.get_name(domain)
            self.queue.task_done()


class output:
    def status(self, message):
        print(col.blue + "[*] " + col.end + message)
        if outfile:
            print("[*] " + message, file=outfile)

    def good(self, message):
        print(col.green + "[+] " + col.end + message)
        if outfile:
            print("[+] " + message, file=outfile)

    def verbose(self, message):
        if args.verbose:
            print(col.brown + "[v] " + col.end + message)
            if outfile:
                print("[v] " + message, file=outfile)

    def warn(self, message):
        print(col.red + "[-] " + col.end + message)
        if outfile:
            print("[-] " + message, file=outfile)

    def fatal(self, message):
        print("\n" + col.red + "FATAL: " + message + col.end)
        if outfile:
            print("FATAL " + message, file=outfile)


class col:
    if sys.stdout.isatty() and platform.system() != "Windows":
        green = '\033[32m'
        blue = '\033[94m'
        red = '\033[31m'
        brown = '\033[33m'
        end = '\033[0m'
    else:   # Colours mess up redirected output, disable them
        green = ""
        blue = ""
        red = ""
        brown = ""
        end = ""


def lookup(domain, recordtype):
    try:
        res = resolver.query(domain, recordtype)
        return res
    except:
        return

def get_wildcard(target):

    # List of IP's for wildcard DNS
    wildcards = []
    # Use current unix time as a test subdomain
    epochtime = str(int(time.time()))
    # Prepend a letter to work around incompetent companies like CableOne
    # and their stupid attempts at DNS hijacking
    res = lookup("a" + epochtime + "." + target, recordtype)
    if res:
        for res_data in res:
            address = res_data.address
            wildcards.append(address)
            out.good(col.red + "Wildcard" + col.end + " domain found - " + col.brown + address + col.end)
        return wildcards
    else:
        out.verbose("No wildcard domain found")

def get_nameservers(target):
    try:
        ns = resolver.query(target, 'NS')
        return ns
    except:
        return

def get_v6(target):
    out.verbose("Getting IPv6 (AAAA) records")
    try:
        res = lookup(target, "AAAA")
        if res:
            out.good("IPv6 (AAAA) records found. Try running dnscan with the "+ col.green + "-6 " + col.end + "option.")
        for v6 in res:
            print(str(v6) + "\n")
            if outfile:
                print(v6, file=outfile)
    except:
        return

def get_txt(target):
    out.verbose("Getting TXT records")
    try:
        res = lookup(target, "TXT")
        if res:
            out.good("TXT records found")
        for txt in res:
            print(txt)
            if outfile:
                print(txt, file=outfile)
        print("")
    except:
        return

def get_dmarc(target):
    out.verbose("Getting DMARC records")
    try:
        res = lookup("_dmarc." + target, "TXT")
        if res:
            out.good("DMARC records found")
        for dmarc in res:
            print(dmarc)
            if outfile:
                print(dmarc, file=outfile)
        print("")
    except:
        return

def get_mx(target):
    out.verbose("Getting MX records")
    try:
        res = lookup(target, "MX")
    except:
        return
    # Return if we don't get any MX records back
    if not res:
        return
    out.good("MX records found, added to target list")
    for mx in res:
        print(mx.to_text())
        if outfile:
            print(mx.to_text(), file=outfile)
        mxsub = re.search("([a-z0-9\.\-]+)\."+target, mx.to_text(), re.IGNORECASE)
        try:
            if mxsub.group(1) and mxsub.group(1) not in wordlist:
                queue.put(mxsub.group(1) + "." + target)
        except AttributeError:
            pass
    print("")

def zone_transfer(domain, ns):
    out.verbose("Trying zone transfer against " + str(ns))
    try:
        zone = dns.zone.from_xfr(dns.query.xfr(str(ns), domain, relativize=False),
                                 relativize=False)
        out.good("Zone transfer sucessful using nameserver " + col.brown + str(ns) + col.end)
        names = list(zone.nodes.keys())
        names.sort()
        for n in names:
            print(zone[n].to_text(n))    # Print raw zone
            if outfile:
                print(zone[n].to_text(n), file=outfile)
        sys.exit(0)
    except Exception:
        pass

def add_target(domain):
    if '%%' in domain:
        for word in wordlist:
            queue.put(domain.replace(r'%%', word))
    else:
        for word in wordlist:
            queue.put(word + "." + domain)

def add_tlds(domain):
    for tld in wordlist:
        queue.put(domain + "." + tld)

def get_args():
    global args
    
    parser = argparse.ArgumentParser('dnscan.py', formatter_class=lambda prog:argparse.HelpFormatter(prog,max_help_position=40),
            epilog="Specify a custom insertion point with %% in the domain name, such as: dnscan.py -d dev-%%.example.org")
    target = parser.add_mutually_exclusive_group(required=True) # Allow a user to specify a list of target domains
    target.add_argument('-d', '--domain', help='Target domain', dest='domain', required=False)
    target.add_argument('-l', '--list', help='File containing list of target domains', dest='domain_list', required=False)
    parser.add_argument('-w', '--wordlist', help='Wordlist', dest='wordlist', required=False)
    parser.add_argument('-t', '--threads', help='Number of threads', dest='threads', required=False, type=int, default=8)
    parser.add_argument('-6', '--ipv6', help='Scan for AAAA records', action="store_true", dest='ipv6', required=False, default=False)
    parser.add_argument('-z', '--zonetransfer', action="store_true", default=False, help='Only perform zone transfers', dest='zonetransfer', required=False)
    parser.add_argument('-r', '--recursive', action="store_true", default=False, help="Recursively scan subdomains", dest='recurse', required=False)
    parser.add_argument('-R', '--resolver', help="Use the specified resolver instead of the system default", dest='resolver', required=False)
    parser.add_argument('-T', '--tld', action="store_true", default=False, help="Scan for TLDs", dest='tld', required=False)
    parser.add_argument('-o', '--output', help="Write output to a file", dest='output_filename', required=False)
    parser.add_argument('-i', '--output-ips',   help="Write discovered IP addresses to a file", dest='output_ips', required=False)
    parser.add_argument('-D', '--domain-first', action="store_true", default=False, help='Output domain first, rather than IP address', dest='domain_first', required=False)
    parser.add_argument('-v', '--verbose', action="store_true", default=False, help='Verbose mode', dest='verbose', required=False)
    parser.add_argument('-n', '--nocheck', action="store_true", default=False, help='Don\'t check nameservers before scanning', dest='nocheck', required=False)
    args = parser.parse_args()

def setup():
    global targets, wordlist, queue, resolver, recordtype, outfile, outfile_ips
    if args.domain:
        targets = [args.domain]
    if args.tld and not args.wordlist:
        args.wordlist = os.path.join(os.path.dirname(os.path.realpath(__file__)), "tlds.txt")
    else:
        if not args.wordlist:   # Try to use default wordlist if non specified
            args.wordlist = os.path.join(os.path.dirname(os.path.realpath(__file__)), "subdomains.txt")
    try:
        wordlist = open(args.wordlist).read().splitlines()
    except:
        out.fatal("Could not open wordlist " + args.wordlist)
        sys.exit(1)

    # Open file handle for output
    try:
        outfile = open(args.output_filename, "w")
    except TypeError:
        outfile = None
    except IOError:
        out.fatal("Could not open output file: " + args.output_filename)
        sys.exit(1)
    if args.output_ips:
        outfile_ips = open(args.output_ips, "w")
    else:
        outfile_ips = None

    # Number of threads should be between 1 and 32
    if args.threads < 1:
        args.threads = 1
    elif args.threads > 32:
        args.threads = 32
    queue = Queue.Queue()
    resolver = dns.resolver.Resolver()
    resolver.timeout = 1
    resolver.lifetime = 1
    if args.resolver:
        resolver.nameservers = [ args.resolver ]

    # Record type
    if args.ipv6:
        recordtype = 'AAAA'
    elif args.tld:
        recordtype = 'NS'
    else:
        recordtype = 'A'


if __name__ == "__main__":
    global wildcard, addresses, outfile_ips
    addresses = set([])
    out = output()
    get_args()
    setup()
    if args.nocheck == False:
        try:
            resolver.query('.', 'NS')
        except dns.resolver.NoAnswer:
            pass
        except dns.exception.Timeout:
            out.fatal("No valid DNS resolver. Set a custom resolver with -R <resolver>")
            out.fatal("Override with -n --nocheck\n")
            sys.exit(1)

    if args.domain_list:
        out.verbose("Domain list provided, will parse {} for domains.".format(args.domain_list))
        if not os.path.isfile(args.domain_list):
            out.fatal("Domain list {} doesn't exist!".format(args.domain_list))
            sys.exit(1)
        with open(args.domain_list, 'r') as domain_list:
            try:
                targets = list(filter(bool, domain_list.read().split('\n')))
            except Exception as e:
                out.fatal("Couldn't read {}, {}".format(args.domain_list, e))
                sys.exit(1)
    for subtarget in targets:
        global target
        target = subtarget
        out.status("Processing domain {}".format(target))
        if args.resolver:
            out.status("Using specified resolver {}".format(args.resolver))
        else:
            out.status("Using system resolvers {}".format(resolver.nameservers))
        if args.tld and not '%%' in args.domain:
            if "." in target:
                out.warn("Warning: TLD scanning works best with just the domain root")
            out.good("TLD Scan")
            add_tlds(target)
        else:
            queue.put(target)   # Add actual domain as well as subdomains

            # These checks will all fail if we have a custom injection point, so skip them
            if not '%%' in args.domain:
                nameservers = get_nameservers(target)
                out.good("Getting nameservers")
                targetns = []       # NS servers for target
                try:    # Subdomains often don't have NS recoards..
                    for ns in nameservers:
                        ns = str(ns)[:-1]   # Removed trailing dot
                        res = lookup(ns, "A")
                        for rdata in res:
                            targetns.append(rdata.address)
                            print(rdata.address + " - " + col.brown + ns + col.end)
                            if outfile:
                                print(rdata.address + " - " + ns, file=outfile)
                        zone_transfer(target, ns)
                except SystemExit:
                    sys.exit(0)
                except:
                    out.warn("Getting nameservers failed")
        #    resolver.nameservers = targetns     # Use target's NS servers for lokups
        # Missing results using domain's NS - removed for now
                out.warn("Zone transfer failed\n")
                if args.zonetransfer:
                    sys.exit(0)

                get_v6(target)
                get_txt(target)
                get_dmarc(target)
                get_mx(target)
            wildcard = get_wildcard(target)
            if wildcard:
                for wildcard_ip in wildcard:
                    try:
                        addresses.add(ipaddr(unicode(wildcard_ip)))
                    except NameError:
                        addresses.add(ipaddr(str(wildcard_ip)))
            out.status("Scanning " + target + " for " + recordtype + " records")
            add_target(target)

        for i in range(args.threads):
            t = scanner(queue)
            t.setDaemon(True)
            t.start()
        try:
            for i in range(args.threads):
                t.join(1024)       # Timeout needed or threads ignore exceptions
        except KeyboardInterrupt:
            out.fatal("Caught KeyboardInterrupt, quitting...")
            if outfile:
                outfile.close()
            sys.exit(1)
        print("                                        ")
        if outfile_ips:
            for address in sorted(addresses):
                print(address, file=outfile_ips)
    if outfile:
        outfile.close()
    if outfile_ips:
        outfile_ips.close()
