Merge pull request #12 from CZ-NIC/master
Checks for open resolvers in the list of IPs.pull/14/head
commit
04f4797f64
|
@ -0,0 +1,189 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# Tool to check for openresolvers (from list of source IP).
|
||||||
|
#
|
||||||
|
# Dragon Research Group project
|
||||||
|
#
|
||||||
|
# Software is free software released under the "Modified BSD license"
|
||||||
|
#
|
||||||
|
# Copyright (c) 2015 Alexandre Dulaunoy - a@foo.be
|
||||||
|
#
|
||||||
|
# Has been modified by Edvard Rejthar, CSIRT.cz at MISP Hackathon 7 Dec 2016
|
||||||
|
#
|
||||||
|
#
|
||||||
|
import os, sys, argparse, redis, datetime, ipdb, json, threading, jsonpickle
|
||||||
|
from dns.resolver import Resolver, NXDOMAIN, NoNameservers, Timeout
|
||||||
|
from urllib.request import urlopen
|
||||||
|
from netaddr import IPAddress, AddrFormatError
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG, format='%(message)s')
|
||||||
|
logger = logging.getLogger('openresolverchecker')
|
||||||
|
__help__ = """
|
||||||
|
Check the list of IP/domains agains an IP to see where is open resolvers.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
./openresolverchecker.py
|
||||||
|
./openresolverchecker.py --max 2000 --out save.json --include-timeouts
|
||||||
|
./openresolverchecker.py -v -o save.json --type TXT --query resolvertest.switch.ch "resolver test ok"
|
||||||
|
./openresolverchecker.py -v -o save.json --type A --query seznam.cz 77.75.77.53 --input ../list.json --max 100
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_QUERY, DEFAULT_IP = "resolvertest.switch.ch", "resolver test ok"
|
||||||
|
MISP_WARNING_LIST = "https://raw.githubusercontent.com/MISP/misp-warninglists/master/lists/public-dns-v4/list.json"
|
||||||
|
|
||||||
|
class OpenResolverChecker:
|
||||||
|
def __init__(self, ips, query = DEFAULT_QUERY, expected = DEFAULT_IP, threadnum = 500, verbose = 0, recordType = "TXT", includeTimeouts = False ):
|
||||||
|
self.resolver = Resolver()
|
||||||
|
self.resolver.timeout=1
|
||||||
|
self.resolver.lifetime=1
|
||||||
|
self.query = query
|
||||||
|
self.expected = expected
|
||||||
|
self.openresolvers = [] # [address, ...]
|
||||||
|
self.errors = [] # [(address, errcode), ...]
|
||||||
|
self.ips = ips
|
||||||
|
self.threadnum = threadnum
|
||||||
|
self.verbose = verbose
|
||||||
|
self.recordType = recordType
|
||||||
|
self.includeTimeouts = includeTimeouts
|
||||||
|
|
||||||
|
def _worker(cls,num, this):
|
||||||
|
""" Worker thread checks if the IP is an Open resolver. """
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
ip = this.ips.pop()
|
||||||
|
except IndexError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ts = str(datetime.datetime.utcnow())
|
||||||
|
|
||||||
|
try:
|
||||||
|
IPAddress(ip) # check
|
||||||
|
addresses = [ip]
|
||||||
|
except AddrFormatError:
|
||||||
|
addresses = []
|
||||||
|
try:
|
||||||
|
answer = dns.resolver.query(ip, 'AAAA')
|
||||||
|
for data in answer:
|
||||||
|
addresses.append(data)
|
||||||
|
answer = dns.resolver.query(ip, 'A')
|
||||||
|
for data in answer:
|
||||||
|
addresses.append(data)
|
||||||
|
except:
|
||||||
|
#logging.warning("Cant translate to the IP: {}".format(ip))
|
||||||
|
this.errors.append((ip, "untranslatable"))
|
||||||
|
if this.verbose > 0:
|
||||||
|
print("Left:{} (Worker {}) {} {}".format(len(this.ips), num, "untranslatable", ip))
|
||||||
|
continue
|
||||||
|
|
||||||
|
for ip in addresses:
|
||||||
|
passed, msg = this.check(address=ip)
|
||||||
|
#if passed:
|
||||||
|
#this.openresolvers.append(ip)
|
||||||
|
#logline = ts+" - Open resolver at "+ip+" "+str(tst)
|
||||||
|
#else:
|
||||||
|
#logline = "{} closed".format(ip)
|
||||||
|
if this.verbose > 0:
|
||||||
|
print("Left:{} (Worker {}) {} {}".format(len(this.ips), num, msg, ip))
|
||||||
|
|
||||||
|
|
||||||
|
def launch(self):
|
||||||
|
""" Launch analysis in threads. """
|
||||||
|
threads = []
|
||||||
|
for i in range(self.threadnum):
|
||||||
|
th = threading.Thread(target=self._worker, args=(i,self))
|
||||||
|
threads.append(th)
|
||||||
|
th.start()
|
||||||
|
|
||||||
|
for x in threads:
|
||||||
|
x.join()
|
||||||
|
|
||||||
|
return self.openresolvers
|
||||||
|
|
||||||
|
|
||||||
|
def check(self, address=None):
|
||||||
|
""" False : not an openresolver
|
||||||
|
True is an open resolver
|
||||||
|
(True, ip) is an open resolver and includes the non expected A record """
|
||||||
|
if address is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.resolver.nameservers=[address]
|
||||||
|
try:
|
||||||
|
#print("QUERY",self.query, self.recordType)
|
||||||
|
answer = self.resolver.query(self.query, self.recordType)
|
||||||
|
result = next(answer.__iter__()).to_text().strip('"')
|
||||||
|
if result == self.expected:
|
||||||
|
self.openresolvers.append(address)
|
||||||
|
return (True, "*** open resolver")
|
||||||
|
else:
|
||||||
|
self.errors.append((address, "Wrong answer: {}".format(txt)))
|
||||||
|
return (False, "Wrong answer: {}".format(txt))
|
||||||
|
except Timeout as e:
|
||||||
|
if not self.includeTimeouts:
|
||||||
|
return (False, "Timeout")
|
||||||
|
else:
|
||||||
|
ex = e
|
||||||
|
except Exception as e:
|
||||||
|
ex = e
|
||||||
|
# NXDOMAIN = lies that the know domain does not exist
|
||||||
|
# NoNameservers = Refused – they claim to be but are not. Ex ns1.eurodns.com", 80.92.65.2
|
||||||
|
# any other reason
|
||||||
|
pass
|
||||||
|
self.errors.append((address, ex.__class__.__name__))
|
||||||
|
return (False, ex.__class__.__name__)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# command line args
|
||||||
|
parser = argparse.ArgumentParser(description=__help__, formatter_class=argparse.RawTextHelpFormatter)
|
||||||
|
parser.add_argument('-q','--query', help='<query> <expected ip> EX: {} {}'.format(DEFAULT_QUERY, DEFAULT_IP),nargs=2, default=(DEFAULT_QUERY, DEFAULT_IP))
|
||||||
|
parser.add_argument('-o','--out', help='output file to store the list of open resolvers')
|
||||||
|
parser.add_argument('-v','--verbose', help='verbose output', action="count", default = 0)
|
||||||
|
parser.add_argument('--type', help='Specify the type of record we\'re checking. Default is TXT record but you might want to use an A record.', default="TXT")
|
||||||
|
parser.add_argument('--include-timeouts', help='Include timeout exception to error results. Normally, only Nxdomain and NoNameservers exceptions are treated as errors.', action="store_true")
|
||||||
|
parser.add_argument('-m','--max', help='max open resolvers to check (IE. only first 100)', type=int)
|
||||||
|
parser.add_argument('-t','--threadnum', help='thread number (default 500)', type=int, default=500)
|
||||||
|
parser.add_argument('-i','--input', help="Source file in the format: {'list':[ip, domain...]}. If no file is given, MISP "+MISP_WARNING_LIST+" will be used instead.", default=None)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# load resolvers list
|
||||||
|
if args.input:
|
||||||
|
try: # local file
|
||||||
|
with open(args.input) as f:
|
||||||
|
sourceFile = json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
sys.exit("File {} not found.".format(args.input))
|
||||||
|
else: # load from github
|
||||||
|
print("Downloading the list from: {}".format(MISP_WARNING_LIST))
|
||||||
|
response = urlopen(MISP_WARNING_LIST)
|
||||||
|
sourceFile = json.loads(response.read().decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
ips = sourceFile["list"]
|
||||||
|
if args.max:
|
||||||
|
ips = ips[:args.max]
|
||||||
|
count = len(ips)
|
||||||
|
if not count:
|
||||||
|
print("Nothing to be checked, empty list.")
|
||||||
|
quit()
|
||||||
|
|
||||||
|
# launch resolvers
|
||||||
|
orc = OpenResolverChecker(ips, args.query[0], args.query[1], threadnum = args.threadnum, verbose = args.verbose, recordType=args.type, includeTimeouts = args.include_timeouts)
|
||||||
|
orc.launch()
|
||||||
|
|
||||||
|
# work with results
|
||||||
|
if args.out: # save to file
|
||||||
|
with open(args.out, "w") as f:
|
||||||
|
sourceFile.update({"timestamp": str(datetime.datetime.utcnow()),
|
||||||
|
"openresolvers": orc.openresolvers,
|
||||||
|
"ip-checked": count,
|
||||||
|
"open-ratio": (len(orc.openresolvers)/count),
|
||||||
|
"query": args.query[0],
|
||||||
|
"query-expected-ip": args.query[1],
|
||||||
|
"query-record-type": args.type,
|
||||||
|
"errors": orc.errors
|
||||||
|
})
|
||||||
|
json.dump(sourceFile, f, indent=4, sort_keys = True)
|
||||||
|
else: # print to stdout
|
||||||
|
print("Valid resolvers:")
|
||||||
|
print(orc.openresolvers)
|
||||||
|
print("Resolvers checked: {}, opens: {} ({} %), errors {} ({} %)".format(count, len(orc.openresolvers), (len(orc.openresolvers)/count)*100 ,len(orc.errors),(len(orc.errors)/count*100)))
|
Loading…
Reference in New Issue