2

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")
digital_alchemy
  • 663
  • 4
  • 9
  • 19
  • There are multiple problems in your code but one of the main thing you have to remember for the DNS world is that for whatever query you get, you may get back something else than the record you expected, like you query for `A` but you get `CNAME` in return. You have to deal with that properly otherwise you will have many assumptions that can work in some cases but not in others. – Patrick Mevzek Mar 24 '20 at 03:56

2 Answers2

4

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.

Kate
  • 1,809
  • 1
  • 8
  • 7
  • That all makes sense. I've not had time to change and test it but I believe it should work. – digital_alchemy Mar 22 '20 at 09:29
  • "the list of name servers is not always returned in the same order." This is by design of the DNS. It does not deal with lists (ordered) but with sets (unordered) of records. And recursive nameservers are often returning records in a different order each time you query them, exactly to make sure that clients do not stick to the same one. – Patrick Mevzek Mar 24 '20 at 03:54
0

Fixed up your code, doesn't look great yet, but it works

#!/usr/bin/python3
# you might want to run python3 -m pip install dnspython before running this script

import sys
import dns.query
import dns.zone
import dns.resolver

# formatting setup
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} "
def drawLine():
    print ('-' * 75)
    
# read arguments    
try:
    domain = (sys.argv[1])
except:
    print("[!] USAGE: python3 zt.py DOMAIN_NAME")
    sys.exit(0)

# DNS functions
def resolveDNS(name):
    resolver = dns.resolver.Resolver()
    results = resolver.query(name , "A")
    return results

def getNS (domain):
    mapping = {}
    name_servers = dns.resolver.query(domain, 'NS')
    print ("\nThe name servers for " + domain + " are:")
    drawLine()
    for name_server in name_servers:
        A_records = resolveDNS(str(name_server))
        for item in A_records:
            answer = ','.join([str(item)])
        mapping[str(name_server)] = answer
        print (bracket, "{:30}".format(str(name_server).rstrip('.')), "{:15}".format(answer))       
    return mapping
    
def zoneXFR(server):
    try:
        zone = dns.zone.from_xfr(dns.query.xfr(str(server).rstrip('.'), domain))
    except Exception as e:
        print (bracket_err, f"{Fore.RED}Error:{Style.RESET_ALL}", e.__class__, e)
    else:
        print ("\nResults for",server, "\nZone origin:", str(zone.origin).rstrip('.'))
        drawLine()
        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)
        drawLine()
        
name_servers = getNS(domain)
for server in name_servers:
    print ("\nAttempting zone transfers for", server,name_servers[server])
    zoneXFR(name_servers[server])