1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4# DNSRecon Data Parser 5# 6# Copyright (C) 2012 Carlos Perez 7# 8# This program is free software; you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published by 10# the Free Software Foundation; Applies version 2 of the License. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program; if not, write to the Free Software 19# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 20 21__version__ = '0.0.7' 22__author__ = 'Carlos Perez, Carlos_Perez@darkoperator.com' 23 24import xml.etree.cElementTree as cElementTree 25import csv 26import os 27import getopt 28import sys 29import re 30 31from netaddr import * 32 33 34# Function Definitions 35# ------------------------------------------------------------------------------ 36 37 38def print_status(message=""): 39 print(f"\033[1;34m[*]\033[1;m {message}") 40 41 42def print_good(message=""): 43 print(f"\033[1;32m[*]\033[1;m {message}") 44 45 46def print_error(message=""): 47 print(f"\033[1;31m[-]\033[1;m {message}") 48 49 50def print_debug(message=""): 51 print(f"\033[1;31m[!]\033[1;m {message}") 52 53 54def print_line(message=""): 55 print(f"{message}") 56 57 58def process_range(arg): 59 """ 60 Function will take a string representation of a range for IPv4 or IPv6 in 61 CIDR or Range format and return a list of IPs. 62 """ 63 try: 64 ip_list = None 65 range_vals = [] 66 if re.match(r'\S*/\S*', arg): 67 ip_list = IPNetwork(arg) 68 69 range_vals.extend(arg.split("-")) 70 if len(range_vals) == 2: 71 ip_list = IPNetwork(IPRange(range_vals[0], range_vals[1])).cidrs()[-1] 72 except Exception: 73 print_error(f"Range provided is not valid: {arg()}") 74 return [] 75 return ip_list 76 77 78def xml_parse(xm_file, ifilter, tfilter, nfilter, list): 79 """ 80 Function for parsing XML files created by DNSRecon and apply filters. 81 """ 82 iplist = [] 83 for event, elem in cElementTree.iterparse(xm_file): 84 # Check if it is a record 85 if elem.tag == "record": 86 # Check that it is a RR Type that has an IP Address 87 if "address" in elem.attrib: 88 # Check if the IP is in the filter list of IPs to ignore 89 if (len(ifilter) == 0 or IPAddress(elem.attrib['address']) in ifilter) and ( 90 elem.attrib['address'] != "no_ip"): 91 # Check if the RR Type against the types 92 if re.match(tfilter, elem.attrib['type'], re.I): 93 # Process A, AAAA and PTR Records 94 if re.search(r'PTR|^[A]$|AAAA', elem.attrib['type']) \ 95 and re.search(nfilter, elem.attrib['name'], re.I): 96 if list: 97 if elem.attrib['address'] not in iplist: 98 print(elem.attrib['address']) 99 else: 100 print_good(f"{elem.attrib['type']} {elem.attrib['name']} {elem.attrib['address']}") 101 102 # Process NS Records 103 elif re.search(r'NS', elem.attrib['type']) and \ 104 re.search(nfilter, elem.attrib['target'], re.I): 105 if list: 106 if elem.attrib['address'] not in iplist: 107 iplist.append(elem.attrib['address']) 108 else: 109 print_good(f"{elem.attrib['type']} {elem.attrib['target']} {elem.attrib['address']}") 110 111 # Process SOA Records 112 elif re.search(r'SOA', elem.attrib['type']) and \ 113 re.search(nfilter, elem.attrib['mname'], re.I): 114 if list: 115 if elem.attrib['address'] not in iplist: 116 iplist.append(elem.attrib['address']) 117 else: 118 print_good(f"{elem.attrib['type']} {elem.attrib['mname']} {elem.attrib['address']}") 119 120 # Process MS Records 121 elif re.search(r'MX', elem.attrib['type']) and \ 122 re.search(nfilter, elem.attrib['exchange'], re.I): 123 if list: 124 if elem.attrib['address'] not in iplist: 125 iplist.append(elem.attrib['address']) 126 else: 127 print_good(f"{elem.attrib['type']} {elem.attrib['exchange']} {elem.attrib['address']}") 128 129 # Process SRV Records 130 elif re.search(r'SRV', elem.attrib['type']) and \ 131 re.search(nfilter, elem.attrib['target'], re.I): 132 if list: 133 if elem.attrib['address'] not in iplist: 134 iplist.append(elem.attrib['address']) 135 else: 136 print_good("{0} {1} {2} {3} {4}".format(elem.attrib['type'], elem.attrib['name'], 137 elem.attrib['address'], elem.attrib['target'], 138 elem.attrib['port'])) 139 else: 140 if re.match(tfilter, elem.attrib['type'], re.I): 141 # Process TXT and SPF Records 142 if re.search(r'TXT|SPF', elem.attrib['type']): 143 if not list: 144 print_good("{0} {1}".format(elem.attrib['type'], elem.attrib['strings'])) 145 # Process IPs in list 146 if len(iplist) > 0: 147 try: 148 for ip in filter(None, iplist): 149 print_line(ip) 150 except IOError: 151 sys.exit(0) 152 153 154def csv_parse(csv_file, ifilter, tfilter, nfilter, list): 155 """ 156 Function for parsing CSV files created by DNSRecon and apply filters. 157 """ 158 iplist = [] 159 reader = csv.reader(open(csv_file, 'r'), delimiter=',') 160 next(reader) 161 for row in reader: 162 # Check if IP is in the filter list of addresses to ignore 163 if ((len(ifilter) == 0) or (IPAddress(row[2]) in ifilter)) and (row[2] != "no_ip"): 164 # Check Host Name regex and type list 165 if re.search(tfilter, row[0], re.I) and re.search(nfilter, row[1], re.I): 166 if list: 167 if row[2] not in iplist: 168 print(row[2]) 169 else: 170 print_good(" ".join(row)) 171 # Process IPs for target list if available 172 # if len(iplist) > 0: 173 # for ip in filter(None, iplist): 174 # print_line(ip) 175 176 177def extract_hostnames(file): 178 host_names = [] 179 hostname_pattern = re.compile("(^[^.]*)") 180 file_type = detect_type(file) 181 if file_type == "xml": 182 for event, elem in cElementTree.iterparse(file): 183 # Check if it is a record 184 if elem.tag == "record": 185 # Check that it is a RR Type that has an IP Address 186 if "address" in elem.attrib: 187 # Process A, AAAA and PTR Records 188 if re.search(r'PTR|^[A]$|AAAA', elem.attrib['type']): 189 host_names.append(re.search(hostname_pattern, elem.attrib['name']).group(1)) 190 191 # Process NS Records 192 elif re.search(r'NS', elem.attrib['type']): 193 host_names.append(re.search(hostname_pattern, elem.attrib['target']).group(1)) 194 195 # Process SOA Records 196 elif re.search(r'SOA', elem.attrib['type']): 197 host_names.append(re.search(hostname_pattern, elem.attrib['mname']).group(1)) 198 199 # Process MX Records 200 elif re.search(r'MX', elem.attrib['type']): 201 host_names.append(re.search(hostname_pattern, elem.attrib['exchange']).group(1)) 202 203 # Process SRV Records 204 elif re.search(r'SRV', elem.attrib['type']): 205 host_names.append(re.search(hostname_pattern, elem.attrib['target']).group(1)) 206 207 elif file_type == "csv": 208 reader = csv.reader(open(file, 'r'), delimiter=',') 209 reader.next() 210 for row in reader: 211 host_names.append(re.search(hostname_pattern, row[1]).group(1)) 212 213 host_names = list(set(host_names)) 214 # Return list with no empty values 215 return filter(None, host_names) 216 217 218def detect_type(file): 219 """ 220 Function for detecting the file type by checking the first line of the file. 221 Returns xml, csv or None. 222 """ 223 ftype = None 224 225 # Get the fist lile of the file for checking 226 with open(file, 'r') as file: 227 firs_line = file.readline() 228 229 # Determine file type based on the fist line content 230 if re.search("(xml version)", firs_line): 231 ftype = "xml" 232 elif re.search(r'\w*,[^,]*,[^,]*', firs_line): 233 ftype = "csv" 234 else: 235 raise Exception("Unsupported File Type") 236 return ftype 237 238 239def usage(): 240 print("Version: {0}".format(__version__)) 241 print("DNSRecon output file parser") 242 print("Usage: parser.py <options>\n") 243 print("Options:") 244 print(" -h, --help Show this help message and exit") 245 print(" -f, --file <file> DNSRecon XML or CSV output file to parse.") 246 print(" -l, --list Output an unique IP List that can be used with other tools.") 247 print(" -i, --ips <ranges> IP Ranges in a comma separated list each in formats (first-last)") 248 print(" or in (range/bitmask) for ranges to be included from output.") 249 print(" For A, AAAA, NS, MX, SOA, SRV and PTR Records.") 250 print(" -t, --type <type> Resource Record Types as a regular expression to filter output.") 251 print(" For A, AAAA, NS, MX, SOA, TXT, SPF, SRV and PTR Records.") 252 print(" -s, --str <regex> Regular expression between quotes for filtering host names on.") 253 print(" For A, AAAA, NS, MX, SOA, SRV and PTR Records.") 254 print(" -n, --name Return list of unique host names.") 255 print(" For A, AAAA, NS, MX, SOA, SRV and PTR Records.") 256 sys.exit(0) 257 258 259def main(): 260 # 261 # Option Variables 262 # 263 ip_filter = [] 264 name_filter = "(.*)" 265 type_filter = "(.*)" 266 target_list = False 267 file = None 268 names = False 269 270 # 271 # Define options 272 # 273 try: 274 options, args = getopt.getopt(sys.argv[1:], 'hi:t:s:lf:n', 275 ['help', 276 'ips=', 277 'type=', 278 'str=', 279 'list', 280 'file=', 281 'name' 282 ]) 283 284 except getopt.GetoptError as error: 285 print_error("Wrong Option Provided!") 286 print_error(error) 287 return 288 289 # 290 # Parse options 291 # 292 for opt, arg in options: 293 if opt in ('-t', '--type'): 294 type_filter = arg 295 296 elif opt in ('-i', '--ips'): 297 ipranges = arg.split(",") 298 for r in ipranges: 299 ip_filter.extend(process_range(r)) 300 301 elif opt in ('-s', '--str'): 302 name_filter = "({0})".format(arg) 303 304 elif opt in ('-l', '--list'): 305 target_list = True 306 307 elif opt in ('-f', '--file'): 308 309 # Check if the dictionary file exists 310 if os.path.isfile(arg): 311 file = arg 312 else: 313 print_error("File {0} does not exist!".format(arg)) 314 exit(1) 315 316 elif opt in ('-r', '--range'): 317 ip_list = [] 318 ip_range = process_range(arg) 319 if len(ip_range) > 0: 320 ip_list.extend(ip_range) 321 else: 322 sys.exit(1) 323 elif opt in ('-n', '--name'): 324 names = True 325 326 elif opt in '-h': 327 usage() 328 329 # start execution based on options 330 if file: 331 if names: 332 try: 333 found_names = extract_hostnames(file) 334 found_names.sort() 335 for n in found_names: 336 print_line(n) 337 except IOError: 338 sys.exit(0) 339 else: 340 file_type = detect_type(file) 341 if file_type == "xml": 342 xml_parse(file, ip_filter, type_filter, name_filter, target_list) 343 elif file_type == "csv": 344 csv_parse(file, ip_filter, type_filter, name_filter, target_list) 345 else: 346 print_error("A DNSRecon XML or CSV output file must be provided to be parsed") 347 usage() 348 349 350if __name__ == "__main__": 351 main() 352