From 7ad930e5fb907ca407a1deadb02c795d479a45b9 Mon Sep 17 00:00:00 2001 From: Edvard Rejthar Date: Wed, 7 Dec 2016 18:30:52 +0100 Subject: [PATCH] Checks for open resolvers in the list of IPs. Is able to fetch the MISP warning list a say if there are some resolvers. --- tools/openresolverchecker.py | 189 +++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 tools/openresolverchecker.py diff --git a/tools/openresolverchecker.py b/tools/openresolverchecker.py new file mode 100644 index 0000000..eefd2e9 --- /dev/null +++ b/tools/openresolverchecker.py @@ -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=' 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)))