Search code examples
pythonpython-3.xdnsdnspython

Why is DNSPython zone transfer sporadically failing


So, I'm still at the noob level when it comes to python. I know... I know... there's probably a more efficient way to do what I'm trying but still learning and hopefully, I'll get better with practice.

For a training project, I'm writing a script to do various DNS operations against a domain. I found DNSPython and it seemed to be exactly what I needed to use and I thought I was done with it but when I tried it against a different domain it keeps failing at the zone transfer.

I've got two domains hardcoded right now for testing. The megacorpone domain iw was working as I expected however, now it's failing (with no code change) in order to get it to work I had to filter the first record '@' that was returned otherwise it failed as well.

However, the zonetransfer.me domain sometimes completes the script with error but fails errors sporadically as well, but it never displays the host records for some reason and I've not been able to figure out how to fix it yet, been banging my head against it for a while now.

The megacoprone run was working every time earlier now it's not working at all. The only thing I can think of so far is that it may be a timing issue.

Run with megacoprpone

Attempting zone transfers for megacorpone.com
Traceback (most recent call last):
  File "/home/kali/Exercises/Module_7/dns-axfer.py", line 56, in zoneXFR
    zone = dns.zone.from_xfr(dns.query.xfr(str(server).rstrip('.'), domain))
  File "/usr/lib/python3/dist-packages/dns/zone.py", line 1106, in from_xfr
    for r in xfr:
  File "/usr/lib/python3/dist-packages/dns/query.py", line 627, in xfr
    raise TransferError(rcode)
dns.query.TransferError: Zone transfer error: REFUSED

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/kali/Exercises/Module_7/dns-axfer.py", line 73, in <module>
    zoneXFR()
  File "/home/kali/Exercises/Module_7/dns-axfer.py", line 66, in zoneXFR
    print ("\nResults for",server, "\nZone origin:", str(zone.origin).rstrip('.'))
UnboundLocalError: local variable 'zone' referenced before assignment

Run 1 with zonetransfer.me

Attempting zone transfers for zonetransfer.me

Results for nsztm1.digi.ninja. 
Zone origin: zonetransfer.me
---------------------------------------------------------------------------

Results for nsztm1.digi.ninja. 
Zone origin: zonetransfer.me
---------------------------------------------------------------------------
[*]  Error: <class 'dns.resolver.NoAnswer'> The DNS response does not contain an answer to the question: _acme-challenge.zonetransfer.me. IN A

Results for nsztm2.digi.ninja. 
Zone origin: zonetransfer.me
---------------------------------------------------------------------------

Results for nsztm2.digi.ninja. 
Zone origin: zonetransfer.me
---------------------------------------------------------------------------
[*]  Error: <class 'dns.resolver.NoAnswer'> The DNS response does not contain an answer to the question: _acme-challenge.zonetransfer.me. IN A

Run 2 with no code change (zonetransfer.me)

Attempting zone transfers for zonetransfer.me
Traceback (most recent call last):
  File "/home/kali/Exercises/Module_7/dns-axfer.py", line 56, in zoneXFR
    zone = dns.zone.from_xfr(dns.query.xfr(str(server).rstrip('.'), domain))
  File "/usr/lib/python3/dist-packages/dns/zone.py", line 1106, in from_xfr
    for r in xfr:
  File "/usr/lib/python3/dist-packages/dns/query.py", line 596, in xfr
    _net_write(s, tcpmsg, expiration)
  File "/usr/lib/python3/dist-packages/dns/query.py", line 364, in _net_write
    current += sock.send(data[current:])
ConnectionRefusedError: [Errno 111] Connection refused

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/kali/Exercises/Module_7/dns-axfer.py", line 73, in <module>
    zoneXFR()
  File "/home/kali/Exercises/Module_7/dns-axfer.py", line 66, in zoneXFR
    print ("\nResults for",server, "\nZone origin:", str(zone.origin).rstrip('.'))
UnboundLocalError: local variable 'zone' referenced before assignment

My script: bash away... I can always take constructive criticism.

#!/usr/bin/python3

import sys, argparse
import dns.query
import dns.zone
import dns.resolver
from colorama import Fore, Style

bracket = f"{Fore.BLUE}[{Fore.GREEN}*{Fore.BLUE}]{Style.RESET_ALL} "
bracket_err = f"{Fore.BLUE}[{Fore.RED}*{Fore.BLUE}]{Style.RESET_ALL} "
'''
parser = argparse.ArgumentParser()
parser.add_argument('domain')
args = parser.parse_args()
'''
# domain = (sys.argv[1])
domain = 'megacorpone.com'
#domain = 'zonetransfer.me'

def line():
    print ('-' * 75)
    return None

def resolveDNS(system):
    resolver = dns.resolver.Resolver()
    results = resolver.query(system , "A")
    return results

def getNS ():
    name_servers = dns.resolver.query(domain, 'NS')
    print ("\nThe name servers for " + domain + " are:")
    line()
    for system in name_servers:
        A_records = resolveDNS(str(system))
        for item in A_records:
            answer = ','.join([str(item)])
        print (bracket, "{:30}".format(str(system).rstrip('.')), "{:15}".format(answer))
    return name_servers

def getMX():
    mail_server = dns.resolver.query(domain, 'MX')
    print("\nMail servers for", domain)
    line()    
    for system in mail_server:
        A_records = resolveDNS(str(system.exchange))
        for item in A_records:
            answer = ','.join([str(item)])          
        print(bracket, "{:30}".format(str(system.exchange).rstrip('.')), "{:15}".format(str(answer)), '\t', "{:5}".format("Preference:"), str(system.preference))
    return None

def zoneXFR():
    print ("\nAttempting zone transfers for", domain,)

    for server in name_servers:
        try:
            zone = dns.zone.from_xfr(dns.query.xfr(str(server).rstrip('.'), domain))
            print ("\nResults for",server, "\nZone origin:", str(zone.origin).rstrip('.'))
            line()
            for host in zone:
                if str(host) != '@':
                    A_records = resolveDNS(str(host) + "." + domain)
                    for item in A_records:
                        answer = ','.join([str(item)])   
                    print(bracket, "{:30}".format(str(host) + "." + domain), answer)
        except Exception as e:
            print ("\nResults for",server, "\nZone origin:", str(zone.origin).rstrip('.'))
            line()
            print (bracket_err, f"{Fore.RED}Error:{Style.RESET_ALL}", e.__class__, e)


name_servers = getNS()
getMX()
zoneXFR()
print("\n")

Solution

  • I see that you are trying well-known name servers that are specifically set up for testing. However, for the benefit of other readers, I will add a couple explanations.

    As you are probably aware, most name servers will not allow zone transfers nowadays. That being said, it is possible that each of the name servers for a given domain name will behave differently (they could have different configurations and even be running different software).

    In the case of megacorpone.com there are 3 name servers listed:

    • ns2.megacorpone.com.
    • ns3.megacorpone.com.
    • ns1.megacorpone.com.

    ns2.megacorpone.com is the only one that did allow a zone transfer.

    This message

    dns.query.TransferError: Zone transfer error: REFUSED
    

    means what it means: your query was refused. Probably you talked to the wrong name server.

    Then you have another error which suggest a variable scoping issue:

    UnboundLocalError: local variable 'zone' referenced before assignment
    

    You are calling functions in this order:

    name_servers = getNS()
    getMX()
    zoneXFR()
    

    If name_servers fails, then the subsequent call to zoneXFR will fail too. Because this code:

    for server in name_servers:
    

    will try to iterate over an empty list.

    Intermittent DNS resolution failures are common so a few checks are required here. At the very least, make sure that the list of NS is not empty.

    Another issue: you start a for loop outside of the try block so your control structure is broken right in the middle:

    for server in name_servers:
        try:
            zone = dns.zone.from_xfr(dns.query.xfr(str(server).rstrip('.'), domain))
            print ("\nResults for",server, "\nZone origin:", str(zone.origin).rstrip('.'))
            line()
    

    Do this instead:

    try:
        for server in name_servers:
            zone = dns.zone.from_xfr(dns.query.xfr(str(server).rstrip('.'), domain))
            print ("\nResults for",server, "\nZone origin:", str(zone.origin).rstrip('.'))
            ...
    

    I suspect that your script fails intermittently because the list of name servers is not always returned in the same order. If the first NS returned is ns1.megacorpone.com. or ns3.megacorpone.com. then the code crashes. If the scripts starts with ns2.megacorpone.com (the sole NS allowing zone transfers) then it seems to work OK.

    When this code fails (AXFR denied):

    zone = dns.zone.from_xfr(dns.query.xfr(str(server).rstrip('.'), domain))
    

    then zone is not defined, and that's why you cannot print it in your exception block. Instead, show the domain name or some other variables that you know are defined and valid.

    So if the AXFR is denied, your script should handle this exception dns.query.TransferError, and quietly move on to the next NS if any, until the list has been exhausted.

    Another bit of advice: you try to resolve resources names that are different than '@'. Instead, look at the record type. You should only resolve CNAME, MX or NS. The other common types are TXT, A, AAAA, SOA. The rest are more exotic such as NAPTR, LOC or SRV. Nothing that should be resolved I think.