Skip to content

Latest commit

 

History

History
557 lines (418 loc) · 36.7 KB

File metadata and controls

557 lines (418 loc) · 36.7 KB

九、使用 Python 自动化报告和任务

在前面的章节中,我们介绍了大量的信息,这些信息强调了 Python 可以在哪些方面帮助优化技术现场工作。我们甚至展示了一些方法,可以使用 Python 将后续任务从一个流程自动化到另一个流程。每一项都将帮助你更好地将时间花在优先任务上。这一点很重要,因为有三个因素可能会限制渗透测试的成功完成:评估员完成评估的时间、渗透测试范围的限制以及评估员的技能。在本章中,我们将向您展示如何自动化任务,例如解析可扩展标记语言XML)以从工具数据生成报告。

了解如何解析报告的 XML 文件

我们将使用nmapXMLs 作为示例,展示如何将数据解析为可用格式。我们的最终目标是将数据放在一个具有唯一结果的 Python 字典中。然后,我们可以使用这些数据构建我们认为有用的结构化输出。首先,我们需要一个可以解析和查看的 XML 文件。使用nmap -oX test 127.0.0.1命令对本地主机进行nmap扫描。

这将生成一个使用 XML 标记语言突出显示两个打开端口的文件,如下所示:

Understanding how to parse XML files for reports

通过实际的 XML 文件,我们可以查看数据结构的组件。了解 XML 文件是如何设计的,可以更好地为生成读取 XML 文件的代码做好准备。具体来说,这里的描述基于etree库将 XML 文件的组件分类为什么。etree库在概念上像树一样处理 XML 数据,包含相关的分支、子分支,甚至细枝。用计算机科学的术语来说,我们称之为亲子关系。

使用etree库,您将把数据加载到变量中。这些变量将在其内部保存复合数据块。这些元素被称为元素,可以对其进行进一步剖析以找到有用的信息。例如,如果您将 XML nmap 结构的根加载到一个变量中,然后打印它,您将看到引用和一个标记,该标记描述了其中的元素和数据,如以下屏幕截图所示:

Understanding how to parse XML files for reports

有关etree库的更多详细信息,请访问https://docs.python.org/2/library/xml.etree.elementtree.html

每个元素都可以与其他节点甚至子节点(称为孙子节点)具有父子关系。每个节点都保存我们试图解析的信息。一个节点通常有一个标记,它是它所持有数据的描述,还有一个属性,它是实际数据。为了更好地突出显示这些信息是如何以 XML 表示的,我们捕获了 nmap XML 的一个元素、主机名的节点和一个生成的子节点,如下所示:

Understanding how to parse XML files for reports

当您查看 XML 文件时,您可能会注意到在一个元素中可以有多个节点。例如,由于多个引用,一台主机可能有多个不同的主机名用于相同的互联网协议IP地址)。因此,要迭代元素的所有节点,需要使用 for 循环来捕获所有可能的数据组件。此数据的解析用于生成输出,输出效果与您拥有的数据样本一样好。

这意味着您应该获取多个示例 XML 文件,以获得更好的信息横截面。关键是要获得大多数可能的数据组合。即使样本应该涵盖您将遇到的大多数问题,也会有一些未考虑的示例。所以,如果你的脚本在使用过程中中断,不要泄气。跟踪错误并确定需要调整的内容。

对于我们的测试,我们将使用多个nmap扫描和我们的 Kali 实例,并将细节输出到 XML 文件。

提示

Python 有一个奇妙的库,称为libnmap,可以用来运行和安排扫描,甚至帮助解析输出文件以生成报告。有关的更多详细信息,请访问https://libnmap.readthedocs.org/en/latest/ 。我们可以使用该库解析输出并生成报告,但该库仅适用于nmap。如果您希望解析来自其他工具的其他 XML 输出,以将详细信息添加到更易于管理的格式,则此库将不会帮助您。

当我们准备编写解析器时,第一阶段是映射要解析的文件。因此,我们记录下脚本与输出交互的可能方式。映射文件后,我们在整个文件中放置几个print语句,以显示脚本停止或中断处理的元素。为了更好地理解每个元素,应该将示例 XML 加载到允许正确查看 XML 的工具中。如果您安装了 XML 工具插件,Notepad++工作得非常好。

将文件加载到 Notepad++后,应该将 XML 树向下折叠到其根目录。以下截图显示此树的根为nmaprun

Understanding how to parse XML files for reports

展开一次后,将得到许多子节点,这些子节点可以进一步展开和分解。

Understanding how to parse XML files for reports

从这些细节中,我们看到必须将 XML 文件加载到处理程序中,然后遍历主机元素。但是,我们应该考虑这是一个单一的主机,所以只有一个主机元素。因此,我们应该使用for循环遍历 host 元素,以捕获在未来迭代中将被扫描的其他主机。

当主机元素展开时,我们可以发现地址、主机名、端口和时间都有节点。我们感兴趣的节点是地址、主机名和端口。主机名和端口节点都是可扩展的,这意味着它们可能也需要迭代。

提示

即使只有一个条目,也可以使用 for 循环遍历任何节点。这样可以确保捕获子节点中的所有信息,并防止解析器中断。

此屏幕截图突出显示了扩展的 XML 树的细节,以及我们关心的细节:

Understanding how to parse XML files for reports

对于地址,我们可以看到有不同的地址类型,如addrtype标记突出显示的。在 nmap XML 输出中,您将找到ipv4ipv6mac地址。如果您希望在输出中使用不同的地址类型,可以通过使用简单的if-then语句提取数据,然后将其加载到相应的变量中来获得它们。如果只想将地址加载到变量中,而不考虑其类型,则必须创建优先顺序。

nmap工具可能会也可能不会找到每个扫描目标的主机名。这取决于扫描仪试图检索信息的方式。例如,如果启用了域名服务DNS请求)或针对本地主机进行扫描,则可能已识别主机名。其他扫描实例可能无法识别实际主机名。我们必须构建脚本,以考虑根据扫描可能提供的不同输出。我们的本地主机扫描(如以下屏幕截图所示)确实提供了主机名,因此我们可以在本例中提取信息:

Understanding how to parse XML files for reports

因此,我们决定将主机名和地址加载到变量中。我们将查看ports元素,以确定要提取的父节点和子节点数据。树的此区域中的 XML 节点有大量数据,因为它们必须由许多标记和属性表示,如此屏幕截图所示:

Understanding how to parse XML files for reports

在查看这些节点的细节时,我们应该考虑什么是我们想要提取的组件 T6。我们知道,我们必须迭代所有的端口,并且我们可以唯一地标识由端口 T0 标签标记的端口,它代表端口号,但是我们必须考虑哪些数据对我们是有用的。端口的协议传输控制协议****TCP用户数据报协议****UDP是有用的。此外,港口的状态以及是openclosedfiltered还是open|filtered也很重要。最后,可能已确定的服务名称最好在报告中进行分类。

提示

请记住,服务名称可能不准确,具体取决于扫描类型。如果没有服务检测,nmap 将使用 Linux 的/etc/services文件中描述的默认端口。因此,如果作为示意图练习的一部分为客户机生成报告,请确保启用某种形式的服务检测。否则,您提供的数据可能会被视为不准确。

在查看 XML 文件之后,我们确定,除了地址和主机名之外,我们还将捕获每个端口号、协议、附加到它的服务以及状态。有了这些细节,我们可以考虑如何格式化我们的报告。正如前面的图片所示,来自 nmap XMLs 的数据不是叙述性的格式,因此 Microsoft Word 文档可能不如电子表格那么有用。

因此,我们必须考虑数据将在报告中表示的方式:每个主机的行或每个端口的行。每种表述都有好处和权衡。逐行主机表示法意味着复合信息很容易表示,但是如果我们想过滤数据,我们只能过滤关于主机或端口组的唯一信息,而不能过滤单个端口。

为了使其更有用,电子表格中的每一行都将代表一个端口,这意味着可以在一行上表示每个端口的详细信息。这可以帮助我们的客户机过滤从 XML 提取的每个项目,包括主机名、地址、端口、服务名称、协议和端口状态。以下屏幕截图显示了我们将努力实现的目标:

Understanding how to parse XML files for reports

因为我们正在编写解析器和报告生成器,所以最好创建两个单独的类来处理这些信息。附加的好处是可以实例化 XML 解析器,这意味着我们可以使用解析器运行多个 XML 文件,然后将每个迭代组合成整体和唯一的结果。这对我们极为有利,因为在交战期间,我们通常会进行一次以上的nmap扫描,合并结果和消除重复可能是一个相当费力的过程。同样,这是一个理想的例子,脚本可以让我们的生活更轻松。

了解如何创建 Python 类

对于如何生成 Python 类,新的 Python 爱好者有很多误解。Python 处理类和实例变量的方式与许多其他语言略有不同。这不是一件坏事;事实上,一旦您习惯了该语言的工作方式,您就可以开始理解类定义方式的原因了。

如果您在 Internet 上搜索 Python 和 self 主题,您将发现关于 Python 类中非静态函数开头的定义变量的使用的广泛意见,您将看到关于它的广泛意见。这些问题包括为什么它是一个让生活更轻松的伟大概念,以及它很难处理并使创建多线程脚本成为一件烦琐的事情。通常,困惑源于从另一种语言转向 Python 的开发人员。无论您将站在哪一边,本章中提供的示例都是构建 Python 类的一种方法。

在下一章中,我们将重点介绍脚本的多线程处理,这需要对 Python 类如何工作有一个基本的了解。

Python 的创建者 Guido van Rossum 在一篇博客文章中回应了一些与 self 相关的批评,该文章可在上找到 http://neopythonic.blogspot.com/2008/10/why-explicit-self-has-to-stay.html 。为了帮助您继续关注本书的这一部分,Python 类、导入和对象的广泛定义将不再重复,因为它们已经定义得很好了。如果您想了解有关 Python 类的更多详细信息,请访问http://learnpythonthehardway.org/book 。具体来说,练习 40 到 44 很好地解释了关于类和面向对象原则的“Pythonic”概念,其中包括继承和组合。

之前,我们描述了如何为 Pythonic 类编写命名约定,因此在此不再重复。相反,我们将重点关注脚本中需要的几个项目。首先,我们将定义我们的类和我们的第一个函数__init__函数。

__init__函数是类实例化期间使用的函数。这意味着调用一个类来创建一个对象,该对象可以作为变量通过正在运行的脚本引用。__init__函数帮助定义该对象的初始细节,它基本上充当 Python 类的构造函数。为了更好地理解这一点,__del__函数正好相反,因为它是 Python 中的析构函数。

如果函数要使用实例的详细信息,则传递的第一个参数必须是一致的变量,通常称为self。如果你愿意,你可以叫它别的名字,但那不是Python。如果函数没有此变量,则不能在该函数中直接使用实例化的值。__init__函数中self变量后面的所有值都是在实例化过程中直接传递给类的值。其他语言通过隐藏参数传递这些值;Python 使用self实现这一点。现在您已经了解了 Python 脚本的基本知识,我们可以开始构建解析脚本了。

创建 Python 脚本解析 Nmap XML

我们为这个示例定义的类本质上非常简单。它只有三个函数:__init__,一个处理传递数据的函数,最后一个返回处理数据的函数。我们将设置该类以接受 nmap XML 文件和详细级别,如果没有传递任何内容,则默认为0。以下是 nmap 解析器的实际类和__init__函数的定义:

class Nmap_parser:
    def __init__(self, nmap_xml, verbose=0):
        self.nmap_xml = nmap_xml
        self.verbose = verbose
        self.hosts = {}
        try:
            self.run()
        except Exception, e:
            print("[!] There was an error %s") % (str(e))
            sys.exit(1)

现在,我们将定义一个函数,它将为这个类完成工作。您会注意到,我们不需要在函数中传递任何变量,因为它们包含在self中。在较大的脚本中,我个人会在函数的开头添加注释,以解释正在执行的操作。这样,几年后,当我不得不在它们中添加更多功能时,我就不必浪费时间破译数百行代码。

与前面的章节一样,完整脚本可以在 GitHub 页面的上找到 https://raw.githubusercontent.com/funkandwagnalls/pythonpentest/master/nmap_parser.py

运行函数测试以确保它可以打开 XML 文件,然后使用etree库的parse函数将其加载到变量中。然后,该函数定义初始必需变量并获取 XML 树的根:

def run(self):
    if not self.nmap_xml:
        sys.exit("[!] Cannot open Nmap XML file: %s \n[-] Ensure that your are passing the correct file and format" % (self.nmap_xml))
    try:
        tree = etree.parse(self.nmap_xml)
    except:
        sys.exit("[!] Cannot open Nmap XML file: %s \n[-] Ensure that your are passing the correct file and format" % (self.nmap_xml))
    hosts={}
    services=[]
    hostname_list=[]
    root = tree.getroot()
    hostname_node = None
    if self.verbose> 0:
        print ("[*] Parsing the Nmap XML file: %s") % (self.nmap_xml)

接下来,我们构建一个for循环,该循环迭代每个主机,最初将每个循环的主机名定义为Unknown hostname。这样做是为了防止一台主机的主机名被另一台主机记录。在尝试检索地址之前,对地址进行类似的消隐。您可以在下面的代码中看到嵌套的for循环在主机地址节点中迭代。

每个addrtype标记的每个属性都加载到temp变量中。然后测试该值以查看将提取的地址类型。接下来,将addr标记的属性加载到适合其地址类型的变量中,例如hwaddress、针对互联网协议版本 4(IPv4)、针对**IP 版本 6(IPv6)**的address

for host in root.iter('host'):
    hostname = "Unknown hostname"
    for addresses in host.iter('address'):
        hwaddress = "No MAC Address ID'd"
        ipv4 = "No IPv4 Address ID'd"
        addressv6 = "No IPv6 Address ID'd"
        temp = addresses.get('addrtype')
        if "mac" in temp:
            hwaddress = addresses.get('addr')
            if self.verbose> 2:
                print("[*] The host was on the same broadcast domain")
        if "ipv4" in temp:
            address = addresses.get('addr')
            if self.verbose> 2:
                print("[*] The host had an IPv4 address")
        if "ipv6" in temp:
            addressv6 = addresses.get('addr')
            if self.verbose> 2:
                print("[*] The host had an IPv6 address")

对于主机名,我们做了一些稍微不同的事情。我们本可以创建另一个for循环来尝试识别每个主机的所有可用主机名,但大多数扫描只有一个或没有主机名。为了展示从 XML 文件中获取数据的另一种方式,您可以看到,hostname节点通过首先识别父元素hostnames,然后识别子元素hostname加载到适当命名的变量中。如果脚本没有找到hostname,我们再次将变量设置为Unknown hostname

这个脚本是作为一个教学概念建立的,但我们也希望为将来的变化做好准备,如果必要的话。记住这一点,如果我们希望以后将提取主机名直接节点提取的方式更改为for循环,我们可以。这是在脚本中准备的,方法是在下一个代码部分之前将已识别的主机名加载到主机名列表中。通常,我们提取主机名的方式不需要这样做。在这里为将来的更改准备脚本要比在代码的其余部分返回并更改与加载属性相关的所有内容更容易。

            try:
                hostname_node = host.find('hostnames').find('hostname')
            except:
                if self.verbose > 1:
                    print ("[!] No hostname found")
            if hostname_node is not None:
                hostname = hostname_node.get('name')
            else:
                hostname = "Unknown hostname"
                if self.verbose > 1:
                    print("[*] The hosts hostname is %s") % (str(hostname_node))
            hostname_list.append(hostname)+--

现在我们已经捕获了如何识别主机名,我们将尝试捕获每个主机的所有端口。我们通过迭代所有的port节点并将它们加载到 item 变量中来实现这一点。接下来,我们从节点中提取stateservicenameprotocolportid的属性。然后,将这些值加载到services列表中:

            for item in host.iter('port'):
                state = item.find('state').get('state')
                #if state.lower() == 'open':
                service = item.find('service').get('name')
                protocol = item.get('protocol')
                port = item.get('portid')
                services.append([hostname_list, address, protocol, port, service, hwaddress, state])

现在,有一个值列表,其中包含每个主机的所有服务。我们打算把它编成一本词典,以便参考。因此,我们生成一个for循环,该循环遍历列表的长度,将每个services值重新加载到一个临时变量中,然后使用迭代的值作为键将其加载到实例的self.hosts字典中:

        hostname_list=[]
        for i in range(0, len(services)):
            service = services[i]
            index = len(service) - 1
            hostname = str1 = ''.join(service[0])
            address = service[1]
            protocol = service[2]
            port = service[3]
            serv_name = service[4]
            hwaddress = service[5]
            state = service[6]
            self.hosts[i] = [hostname, address, protocol, port, serv_name, hwaddress, state]
            if self.verbose > 2:
                print ("[+] Adding %s with an IP of %s:%s with the service %s")%(hostname,address,port,serv_name)

在这个函数的末尾,我们添加了一个简单的测试用例来验证数据是否被发现,并且如果详细程度被调低,它可以被显示出来:

        if self.hosts:
            if self.verbose > 4:
                print ("[*] Results from NMAP XML import: ")
                for key, entry in self.hosts.iteritems():
                    print("[*] %s") % (str(entry))
            if self.verbose > 0:
                print ("[+] Parsed and imported unique ports %s") % (str(i+1))
        else:
            if self.verbose > 0:
                print ("[-] No ports were discovered in the NMAP XML file")

主处理函数完成后,下一步是创建一个可以返回特定实例hosts数据的函数。调用此函数时,只返回self.hosts的值:

    def hosts_return(self):
        # A controlled return method
        # Input: None
        # Returned: The processed hosts
        try:
             return self.hosts
        except Exception as e:
            print("[!] There was an error returning the data %s") % (e)

我们已经通过参数和选项反复展示了的基本变量值设置,为了节省空间,nmap_parser.py脚本中的这段代码的细节在此不再赘述;他们可以在网上找到。相反,我们将展示如何通过类实例处理多个 XML 文件。

一开始很简单。我们测试由参数加载的 XML 文件在变量xml中是否有逗号。如果他们这样做了,则意味着用户提供了一个逗号分隔的要处理的 XML 文件列表。因此,我们将按逗号分割,并将值加载到xml_list中进行处理。然后,我们将测试每个 XML 文件,并通过将 XML 文件加载到带有etree.parse的变量中,获取文件的根,然后检查scanner标记的属性值来验证它是否为nmapXML 文件。

如果我们得到nmap,我们就知道该文件是一个 nmap XML。如果没有,我们将退出脚本并显示相应的错误消息。如果没有错误,我们调用Nmap_parser类并将其实例化为具有当前 XML 文件和详细级别的对象。然后,我们将其附加到一个列表中。因此,基本上,XML 文件被传递到Nmap_parser类,对象本身存储在 hosts 列表中。这使我们能够轻松地处理多个 XML 文件并存储对象,以便在必要时进行后续操作:

    if "," in xml:
        xml_list = xml.split(',')
    else:
        xml_list.append(xml)
    for x in xml_list:
        try:
            tree_temp = etree.parse(x)
        except:
            sys.exit("[!] Cannot open XML file: %s \n[-] Ensure that your are passing the correct file and format" % (x))
        try:
            root = tree_temp.getroot()
            name = root.get("scanner")
            if name is not None and "nmap" in name:
                if verbose > 1:
                    print ("[*] File being processed is an NMAP XML")
                hosts.append(Nmap_parser(x, verbose))
            else:
                print("[!] File % is not an NMAP XML") % (str(x))
                sys.exit(1)
        except Exception, e:
            print("[!] Processing of file %s failed %s") % (str(x), str(e))
            sys.exit(1)

加载到字典中的每个实例的数据可能中都有重复的信息。试想一下在渗透测试中会是什么样子;当您扫描特定的弱点时,您通常会查看相同的 IP 地址。每次运行扫描时,您可能会发现相同的端口和服务以及相关状态。为了使数据标准化,需要对其进行组合,并消除重复数据。

当然,当处理典型的内部 IP 地址或征求意见RFC)1918 地址时,10.0.0.1地址可能位于许多不同的内部网络中。因此,如果使用此脚本来组合来自多个网络的结果,则可能会组合实际不重复的结果。在实际执行脚本时,请记住这一点。

现在,我们用for循环中的每个数据实例加载一个临时变量。这将创建字典中所有值的count,并反过来将其用作每个值集的参考。一个名为hosts_dict的新字典用于存储此数据:

    if not hosts:
        sys.exit("[!] There was an issue processing the data")
    for inst in hosts:
        hosts_temp = inst.hosts_return()
        if hosts_temp is not None:
            for k, v in hosts_temp.iteritems():
                hosts_dict[count] = v
                count+=1
            hosts_temp.clear()

现在我们有了一个字典,其中的数据按简单引用排序,我们可以使用它来消除重复项。我们现在要做的是迭代新形成的字典,并在元组中创建键值对。然后将每个元组加载到列表中,以便对数据进行排序。

我们再次遍历列表,它将元组中存储的两个值分解为一个新的键值对。从功能上讲,我们正在操纵通常在 Python 数据结构中存储数据的方式,以便轻松删除重复项。

然后,我们直接比较当前值,即端口数据列表和processed_hosts字典值。这是新的和最终的字典,其中包含从所有 XML 文件中发现的经过验证的唯一值。

此端口数据列表作为嵌套在temp列表中的元组中的第二个值存储。

如果在processed_hosts字典中已经找到一个值,我们将继续使用continue循环,而不将细节加载到字典中。如果该值不在字典中,我们将使用新计数器key将其添加到字典中:

    if verbose > 3:
        for key, value in hosts_dict.iteritems():
            print("[*] Key: %s Value: %s") % (key,value)
    temp = [(k, hosts_dict[k]) for k in hosts_dict]
    temp.sort()
    key = 0
    for k, v in temp:
        compare = lambda x, y: collections.Counter(x) == collections.Counter(y)
        if str(v) in str(processed_hosts.values()):
            continue
        else:
            key+=1
            processed_hosts[key] = v

现在,我们测试并确保数据在新的数据结构中正确排序和显示:

    if verbose > 0:
        for key, target in processed_hosts.iteritems():
            print("[*] Hostname: %s IP: %s Protocol: %s Port: %s Service: %s State: %s MAC address: %s" % (target[0],target[1],target[2],target[3],target[4],target[6],target[5]))

运行脚本会产生以下结果,这表明我们成功地提取了数据并将其格式化为有用的结构:

Creating a Python script to parse an Nmap XML

现在,我们可以注释输出数据的循环,并使用我们的数据结构创建 Excel 电子表格。为此,我们将创建自己的本地模块,然后可以在这个脚本中使用它。将调用该脚本以生成 Excel 电子表格。要做到这一点,我们需要知道我们将要使用的名称,以及我们希望如何引用它。然后,我们在 Python 模块的nmap_parser.py顶部创建相关的import语句,我们称之为nmap_doc_generator.py

try:
    import nmap_doc_generator as gen
except Exception as e:
    print(e)
    sys.exit("[!] Please download the nmap_doc_generator.py script")

接下来,我们用以下代码替换nmap_parser.py脚本底部的字典打印:

gen.Nmap_doc_generator(verbose, processed_hosts, filename, simple)

简单标志被添加到选项列表中,以允许电子表格以不同格式输出,如果您愿意的话。该工具可用于实际渗透测试和最终报告。对于哪种输出更容易阅读,哪种颜色适合他们工作的任何组织的报告品牌,每个人都有自己的偏好。

创建 Python 脚本生成 Excel 电子表格

现在我们创建新的模块。可以将导入nmap_parser.py脚本。脚本非常简单,感谢xlsxwriter库,我们可以使用pip再次安装该库。以下代码通过设置必要的库来生成脚本,以便生成 Excel 电子表格:

import sys
try:
    import xlsxwriter
except:
    sys.exit("[!] Install the xlsx writer library as root or through sudo: pip install xlsxwriter")

接下来,我们为Nmap_doc_generator创建类和构造函数:

class Nmap_doc_generator():
    def __init__(self, verbose, hosts_dict, filename, simple):
        self.hosts_dict = hosts_dict
        self.filename = filename
        self.verbose = verbose
        self.simple = simple
        try:
            self.run()
        except Exception as e:
            print(e)

然后我们创建将为实例执行的函数。从该函数执行一个名为generate_xlsx的辅助函数。此函数是以这种方式创建的,以便我们将来可以将此模块用于其他报告类型(如果需要)。我们所要做的就是创建额外的函数,当nmap_parser.py脚本运行时,可以使用提供的选项调用这些函数。但是,这超出了本例的范围,run函数的范围如下:

    def run(self):
        # Run the appropriate module
        if self.verbose > 0:
            print ("[*] Building %s.xlsx") % (self.filename)
            self.generate_xlsx()

我们定义的下一个函数是generate_xlsx,它包含生成 Excel 电子表格所需的所有功能。我们需要做的第一件事是定义实际的工作簿、工作表以及其中的格式。我们首先设置实际的文件扩展名(如果不存在):

    def generate_xlsx(self):
        if "xls" or "xlsx" not in self.filename:
            self.filename = self.filename + ".xlsx"
        workbook = xlsxwriter.Workbook(self.filename)

然后我们开始创建实际的行格式,从标题行开始。根据是否设置了简单标志,我们将其突出显示为一个粗体行,带有两种不同的可能颜色:

        # Row one formatting
        format1 = workbook.add_format({'bold': True})
    # Header color
    # Find colors: http://www.w3schools.com/tags/ref_colorpicker.asp
  if self.simple:
            format1.set_bg_color('#538DD5')
  else:
      format1.set_bg_color('#33CC33') # Report Format

您可以使用类似 Microsoft 的颜色选择工具在电子表格中识别所需的实际颜色编号。可在找到 http://www.w3schools.com/tags/ref_colorpicker.asp

由于我们希望将其配置为电子表格,以便它可以具有交替的颜色,因此我们将设置两个额外的格式配置。与前面的格式化配置一样,这将保存为变量,可以根据行是偶数还是奇数轻松引用。偶数行将为白色,因为标题行具有颜色填充,而奇数行具有颜色填充。因此,当simple变量被设置时,我们将改变奇数行的颜色。以下代码突出显示了此逻辑结构:

        # Even row formatting
        format2 = workbook.add_format({'text_wrap': True})
        format2.set_align('left')
        format2.set_align('top')
        format2.set_border(1)
        # Odd row formatting
        format3 = workbook.add_format({'text_wrap': True})
        format3.set_align('left')
        format3.set_align('top')
    # Row color
  if self.simple:
      format3.set_bg_color('#C5D9F1') 
  else:
      format3.set_bg_color('#99FF33') # Report Format 
        format3.set_border(1)

定义了格式后,我们现在必须设置列宽和标题,这些将在电子表格的其余部分中使用。这里有一些尝试和错误,因为列宽应该足够宽,以容纳将在电子表格中填充的数据,并正确地表示标题,而不必在屏幕上进行不必要的缩放。通过范围、起始列号、结束列号以及列宽的大小来定义列宽。这三个逗号分隔的值放在set_column函数参数中:

        if self.verbose > 0:
            print ("[*] Creating Workbook: %s") % (self.filename)
        # Generate Worksheet 1
        worksheet = workbook.add_worksheet("All Ports")
        # Column width for worksheet 1
        worksheet.set_column(0, 0, 20)
        worksheet.set_column(1, 1, 17)
        worksheet.set_column(2, 2, 22)
        worksheet.set_column(3, 3, 8)
        worksheet.set_column(4, 4, 26)
        worksheet.set_column(5, 5, 13)
        worksheet.set_column(6, 6, 12)

定义列后,设置行和列的起始位置,填充标题行,并使其中的数据可过滤。想想看,查找具有开放 JBoss 端口的主机有多有用,或者如果客户端想知道已被外围防火墙成功过滤的端口有多有用:

        # Define starting location for Worksheet one
        row = 1
        col = 0
        # Generate Row 1 for worksheet one
        worksheet.write('A1', "Hostname", format1)
        worksheet.write('B1', "Address", format1)
        worksheet.write('C1', "Hardware Address", format1)
        worksheet.write('D1', "Port", format1)
        worksheet.write('E1', "Service Name", format1)
        worksheet.write('F1', "Protocol", format1)
        worksheet.write('G1', "Port State", format1)
        worksheet.autofilter('A1:G1')

因此,定义了格式,我们就可以开始用相关数据填充电子表格了。为此,我们创建一个for循环来填充keyvalue变量。在此报表生成实例中,键对电子表格不有用,因为其中的任何数据都不用于生成电子表格。另一方面,value变量包含来自nmap_parser.py脚本的结果列表。因此,我们在位置变量中填充六个相关的值表示:

        # Populate Worksheet 1
        for key, value in self.hosts_dict.items():
            try:
                hostname = value[0]
                address = value[1]
                protocol = value[2]
                port = value[3]
                service_name = value[4]
                hwaddress = value[5]
                state = value[6]
            except:
                if self.verbose > 3:
                    print("[!] An error occurred parsing host ID: %s for Worksheet 1") % (key)

在每次迭代结束时,我们将增加行计数器。否则,如果我们在开始时这样做,我们将在数据行之间写入空行。要开始处理,我们需要确定行是偶数还是奇数,因为这会更改格式,如前所述。最简单的方法是使用模运算符或%,它将左操作数除以右操作数并返回余数。

如果没有余数,我们知道它是偶数,因此,行也是偶数。否则,行是奇数的,我们需要使用必要的格式。我们不再两次写入整个函数行写入操作,而是再次使用一个临时变量来保存当前行格式,称为temp_format,如下所示:

                    print("[!] An error occurred parsing host ID: %s for Worksheet 1") % (key)
            try:
                if row % 2 != 0:
                    temp_format = format2
                else:
                    temp_format = format3

现在,我们可以从左向右写入数据。数据的每个组成部分进入下一列,这意味着我们每次向行写入数据时,都会取0的列值并向其添加1。这使我们能够轻松地从左到右跨越电子表格,而无需操纵多个值:

                worksheet.write(row, col,     hostname, temp_format)
                worksheet.write(row, col + 1, address, temp_format)
                worksheet.write(row, col + 2, hwaddress, temp_format)
                worksheet.write(row, col + 3, port, temp_format)
                worksheet.write(row, col + 4, service_name, temp_format)
                worksheet.write(row, col + 5, protocol, temp_format)
                worksheet.write(row, col + 6, state, temp_format)
                row += 1
            except:
                if self.verbose > 3:
                    print("[!] An error occurred writing data for Worksheet 1")

最后,关闭将文件写入当前工作目录的工作簿:

        try:
            workbook.close()
        except:
            sys.exit("[!] Permission to write to the file or location provided was denied")

所有必要的脚本组件和模块都已创建,这意味着我们可以从nmapXML 输出生成 Excel 电子表格。在nmap_parser.py脚本的参数中,我们将默认文件名设置为xml_output,但我们可以根据需要传递其他值。以下是nmap_parser.py脚本帮助下的输出:

Creating a Python script to generate Excel spreadsheets

有了这些详细信息,我们现在可以针对我们创建的四个不同的nmap扫描 XML 执行脚本,如以下屏幕截图所示:

Creating a Python script to generate Excel spreadsheets

脚本的输出是这个 Excel 电子表格:

Creating a Python script to generate Excel spreadsheets

相反,如果我们设置 simple 标志并创建一个具有不同文件名的新电子表格,则会得到以下输出:

Creating a Python script to generate Excel spreadsheets

这将创建新的电子表格xml_output2.xlsx,其格式为,如下所示:

Creating a Python script to generate Excel spreadsheets

此模块的代码可在中找到 https://raw.githubusercontent.com/funkandwagnalls/pythonpentest/master/nmap_doc_generator.py

总结

解析 NMAP XML 是非常有用的,但是考虑一下这种能力对于读取和组织其他安全工具输出有多大的帮助。我们向您展示了如何创建 Python 类、解析 XML 结构和生成唯一的数据集。在这一切结束时,我们能够创建一个 Excel 电子表格,它可以以可过滤的格式表示数据。在下一章中,我们将重点介绍如何将多线程功能和持久性添加到 Python 脚本中。