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