Python-渗透测试实用指南-一-

73 阅读1小时+

Python 渗透测试实用指南(一)

原文:annas-archive.org/md5/4B796839472BFAAEE214CCEDB240AE18

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在网络安全和 Python 编程领域有这么多优秀的书籍,都是由聪明的人写成的,那么这本书有什么不同的特点呢?这是一个非常合理的问题,现在让我们来试着回答一下。

这本书试图捕捉我在过去几年中使用 Python 和渗透测试领域所积累的实践经验。它是 Python、渗透测试/攻击性安全、防御性安全和机器学习在渗透测试生态系统中独特的融合。本书以温和的方式开始,涵盖了 Python 的所有关键概念,使读者在前四章结束时能够对 Python 有相当不错的掌握,然后深入研究渗透测试和网络安全用例的自动化。读者将了解如何从头开始开发符合行业标准的漏洞扫描器,与 Nessus 和 Qualys 相同。本书还探讨了有关 Web 应用程序漏洞、它们的利用以及如何使用定制的利用程序自动化 Web 利用的概念。它还深入探讨了反向工程、模糊测试和在 Windows 和 Linux 环境中的缓冲区溢出漏洞,以 Python 作为核心。书中有一个专门讨论自定义利用程序开发的部分,重点是规避反病毒检测。本书还有一个专门讨论开发网络爬虫及其在网络安全领域中的利用的章节。本书还对防御性安全概念提供了相当深入的见解,讨论了网络威胁情报,以及如何开发自定义威胁评分算法。本书最后还介绍了 Python 的许多其他有益用例,比如开发自定义键盘记录器。

这本书适合谁

如果你是一名安全顾问、开发人员或者对 Python 知之甚少的网络安全爱好者,并且需要深入了解渗透测试生态系统和 Python 如何结合创建攻击工具、利用漏洞、自动化网络安全用例等等,那么这本书适合你。《Python 实战渗透测试》指导你深入了解 Python 在网络安全和渗透测试中的高级用法,帮助你更好地了解基础设施中的安全漏洞。

这本书涵盖了什么

第一章,Python 简介,介绍了 Python 的基础知识,主要关注 Python 使用的数据类型、变量、表达式和程序结构。其目标是让读者熟悉 Python 编程语言的基础知识,以便在接下来的章节中使用和利用它。

第二章,构建 Python 脚本,涵盖了 Python 的进一步概念,这些概念构成了编写 Python 脚本的基础,同时探讨了函数、模块、循环、包和导入等概念。

第三章,概念处理,向读者介绍了其他与 Python 相关的概念,包括类、对象、IO 和目录访问、正则表达式、异常处理以及 CSV、JSON 和 XML 文件的解析。

第四章,高级 Python 模块,将学习过程提升到一个高级水平,探索了 Python 的强大之处,理解了多进程和多线程概念,以及套接字编程。

第五章,“漏洞扫描器 Python-第 1 部分”,探讨了制作迷你漏洞扫描引擎所需的高级概念,该引擎将使用自定义端口扫描程序构建在 Nmap 上的端口扫描结果,并应用各种开源脚本和 Metasploit 模块,以及 Python、Ruby 和 NSE 脚本。结果将被汇总,最终将为分析师起草报告。这一章在复杂性和代码行数方面非常庞大,分为两部分。本部分侧重于使用 Python 自动化端口扫描。

第六章,“漏洞扫描器 Python-第 2 部分”,探讨了制作迷你漏洞扫描引擎所需的高级概念。这一章是前一章的延续,读者将学习如何协调各种 Kali Linux 工具,以便自动化服务枚举阶段的漏洞评估,从而完成定制漏洞扫描器的开发。

第七章,“机器学习和网络安全”,试图将网络安全领域与数据科学联系起来,并阐明我们如何使用机器学习和自然语言处理来自动化渗透测试的手动报告分析阶段。本章还将把之前的所有部分联系在一起,基于我们迄今所学的知识,制作一个迷你渗透测试工具包。

第八章,“自动化 Web 应用程序扫描-第 1 部分”,向读者解释了他们如何使用 Python 自动化各种 Web 应用程序攻击类型,其中一些最知名的包括 SQL 注入、XSS、CSRF 和点击劫持。

第九章,“自动化 Web 应用程序扫描-第 2 部分”,是前一章的延续。在这里,读者将了解如何使用 Python 开发自定义利用程序,利用 Web 应用程序最终为用户提供使用 Python 的 shell 访问权限。

第十章,“构建自定义爬虫”,解释了如何使用 Python 构建自定义爬虫,以便在应用程序中进行爬取,无论是否有身份验证,同时列出被测试应用程序的注入点和网页。爬虫的功能可以根据需求进行扩展和定制。

第十一章,“逆向工程 Linux 应用程序和缓冲区溢出”,解释了如何对 Linux 应用程序进行逆向工程。读者还将了解 Python 如何在帮助 Linux 环境中的缓冲区溢出漏洞方面发挥作用。该章还指导读者针对缓冲区溢出漏洞进行自定义利用程序开发。

第十二章,“逆向工程 Windows 应用程序”,解释了如何对 Windows 应用程序进行逆向工程,以及 Python 如何在帮助 Windows 环境中的缓冲区溢出漏洞方面发挥作用。该章还指导读者针对缓冲区溢出漏洞进行自定义利用程序开发。

第十三章,“利用开发”,解释了读者如何使用 Python 编写自己的利用程序,这些利用程序可以作为 Metasploit 模块进行扩展,并且还涵盖了编码 shell 以避免检测。

第十四章,网络威胁情报,指导读者如何使用 Python 进行网络威胁情报和威胁信息的收集、威胁评分,最后,如何利用获得的信息,使 SIEM、IPS 和 IDS 系统能够利用最新的威胁信息进行早期检测。

第十五章,Python 的其他奇迹,介绍了如何使用 Python 提取 Google 浏览器保存的密码,开发自定义键盘记录器,解析 Nessus 和 Nmap 报告文件等。

为了充分利用本书

为了充分利用本书,只需要有一个继续前进并详细理解每个概念的愿望。

下载示例代码文件

您可以从您在www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. www.packt.com上登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载和勘误。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保您使用最新版本的解压或提取文件夹:

  • Windows 的 WinRAR/7-Zip

  • Mac 的 Zipeg/iZip/UnRarX

  • Linux 的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Penetration-Testing-with-Python。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还提供了来自我们丰富书籍和视频目录的其他代码包,可以在**github.com/PacktPublishing/**上找到。请查看!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781788990820_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:"要使用 Python 终端,只需在终端提示符中键入python3命令。"

代码块设置如下:

a=44
b=33
if a > b:
    print("a is greater")
print("End") 

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

my_list=[1,"a",[1,2,3],{"k1":"v1"}]
my_list[0] -> 1
my_List[1] -> "a"
my_list[2] -> [1,2,3]
my_list[2][0] -> 1
my_list[2][2] -> 3
my_list[3] -> {"k1":"v1"}
my_list[3]["k1"] -> "v1"
my_list[3].get("k1") -> "v1 

任何命令行输入或输出都以以下方式编写:

import threading
>>> class a(threading.Thread):
... def __init__(self):
... threading.Thread.__init__(self)
... def run(self):
... print("Thread started")
... 

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中出现。例如:"点击开始爬取按钮。"

警告或重要说明会出现在这样。

技巧和窍门会出现在这样。

第一章:Python 简介

本章将介绍 Python,主要关注 Python 编程语言遵循的数据类型,变量,表达式和程序结构。本章的目标是使读者熟悉 Python 的基础知识,以便他们可以在接下来的章节中使用它。本章将涵盖 Python 的安装及其依赖管理器。我们还将开始研究 Python 脚本。

在本章中,我们将涵盖以下主题:

  • Python 简介(包括安装和设置)

  • 基本数据类型

  • 序列数据类型 - 列表,字典,元组

  • 变量和关键字

  • 操作和表达式

技术要求

在继续本章之前,请确保您已准备好以下设置:

  • 一台工作的计算机或笔记本电脑

  • Ubuntu 操作系统,最好是 16.04 版本

  • Python 3.x

  • 一个可用的互联网连接

为什么选择 Python?

当我们考虑探索一种新的编程语言或技术时,我们经常会想到新技术的范围以及它可能给我们带来的好处。让我们从思考为什么我们可能想要使用 Python 以及它可能给我们带来的优势开始这一章。

为了回答这个问题,我们将考虑当前的技术趋势,而不会涉及更多的语言特定功能,比如它是面向对象的,功能性的,可移植的和解释性的。我们以前听过这些术语。让我们试着思考为什么我们可能会从严格的工业角度使用 Python,这种语言的现在和未来的景观可能是什么样的,以及这种语言如何为我们服务。我们将首先提到一些计算机科学相关人员可能选择的职业选项:

  • 程序员或软件开发人员

  • Web 开发人员

  • 数据库工程师

  • 网络安全专业人员(渗透测试员,事件响应者,SOC 分析师,恶意软件分析师,安全研究员等)

  • 数据科学家

  • 网络工程师

还有许多其他角色,但我们暂时只关注最通用的选项,看看 Python 如何适用于它们。让我们从程序员或软件开发人员的角色开始。截至 2018 年,Python 被记录为招聘广告中列出的第二受欢迎的语言(www.codingdojo.com/blog/7-most-in-demand-programming-languages-of-2018/)。程序员的角色可能因公司而异,但作为 Python 程序员,您可能会编写 Python 软件产品,开发用 Python 编写的网络安全工具(已经存在大量这样的工具可以在 GitHub 和网络安全社区的其他地方找到),原型设计一个可以模仿人类的机器人,设计智能家居自动化产品或实用工具等。Python 的范围涵盖了软件开发的各个方面,从典型的软件应用到强大的硬件产品。这是因为这种语言易于理解,具有出色的库支持,由庞大的社区支持,并且当然,它是开源的美丽之处。

让我们转向网络。近年来,Python 在成熟作为 Web 开发语言方面表现出色。最受欢迎的全栈基于 Web 的框架,如 Django、Flask 和 CherryPy,使得使用 Python 进行 Web 开发成为一种无缝和清晰的体验,学习、定制和灵活性都很强。我个人最喜欢 Django,因为它提供了非常清晰的 MVC 架构,业务逻辑和表示层完全隔离,使得开发代码更加清晰和易于管理。Django 装备齐全,支持 ORM 和使用 celery 进行后台任务处理,实现了其他任何 Web 框架能够做到的一切,同时保持了 Python 的本地代码。Flask 和 CherryPy 也是 Web 开发的绝佳选择,可以对数据流和定制性进行大量控制。

网络安全是一个离开 Python 就不完整的领域。网络安全领域的每个行业都与 Python 有一定的关联,大多数网络安全工具都是用 Python 编写的。从渗透测试到监控安全运营中心,Python 被广泛使用和需要。Python 通过为渗透测试人员提供出色的工具和自动化支持,使他们能够为各种渗透测试活动编写快速而强大的脚本,从侦察到利用都可以。我们将在本书的课程中详细学习这一点。

机器学习ML)和人工智能AI)是科技行业中我们经常遇到的热门词汇。Python 对所有 ML 和 AI 模型都有出色的支持。在大多数情况下,Python 是任何想学习 ML 和 AI 的人的首选。这个领域中另一个著名的语言是 R,但由于 Python 在其他技术和软件开发领域的出色覆盖,将用 Python 编写的机器学习解决方案与现有或新产品结合起来比用 R 编写的解决方案更容易。Python 拥有惊人的机器学习库和 API,如 sciket-learn、NumPy、Pandas、matplotlib、NLTK 和 TensorFlow。Pandas 和 NumPy 使得科学计算变得非常容易,给用户提供了在内存中处理大型数据集的灵活性,具有出色的抽象层,使开发人员可以忘记背景细节,干净高效地完成工作。

几年前,一个典型的数据库工程师可能会被期望了解关系型数据库,比如MySQLSQL ServerOraclePostgreSQL等等。然而,在过去的几年里,技术领域已经完全改变。虽然一个典型的数据库工程师仍然应该了解并熟练掌握这些数据库技术栈,但这已经不够了。随着数据量的增加,当我们进入大数据时代时,传统数据库必须与 Hadoop 或 Spark 等大数据解决方案配合工作。话虽如此,数据库工程师的角色已经演变成包括数据分析师的技能集。现在,数据不再需要从本地数据库服务器中获取和处理 - 它需要从异构来源收集,预处理,跨分布式集群或并行核心进行处理,然后再存储回分布式节点集群中。我们在这里谈论的是大数据分析和分布式计算。我们之前提到了 Hadoop 这个词。如果你对它不熟悉,Hadoop 是一个引擎,能够通过在计算机集群中生成文件块来处理大文件,然后对处理结果集进行聚合,这在业界被称为 map-reduce 操作。Apache Spark 是分析领域的一个新热词,它声称比 Hadoop 生态系统快 100 倍。Apache Spark 有一个名为pyspark的 Python API,使用它我们可以用本地 Python 代码运行 Apache Spark。它非常强大,熟悉 Python 使得设置变得简单和无缝。

提到前面的几点的目的是为了突出 Python 在当前技术领域和未来的重要性。机器学习和人工智能很可能会成为主导产业,而这两者都主要由 Python 驱动。因此,现在开始阅读和探索 Python 和机器学习的网络安全将是一个更好的时机。让我们通过了解一些基础知识来开始我们的 Python 之旅。

关于 Python - 编译还是解释

编译器通过将用高级编程语言编写的人类可读的代码转换为机器代码,然后由底层架构或机器运行。如果你不想运行代码,编译后的版本可以保存并以后执行。值得注意的是,编译器首先检查语法错误,只有在没有发现错误的情况下才会创建程序的编译版本。如果你使用过 C 语言,你可能会遇到.out文件,这些是编译后的文件的例子。

然而,在解释器的情况下,程序的每一行都是在运行时从源代码中解释并转换为机器代码进行执行。Python 属于解释的字节码类别。这意味着 Python 代码首先被翻译成中间字节码(一个.pyc文件)。然后,这个字节码由解释器逐行解释并在底层架构上执行。

安装 Python

在本书的过程中,所有的练习都将在 Linux 操作系统上展示。在我的情况下,我使用的是 Ubuntu 16.04。你可以选择任何你喜欢的变种。我们将使用python3来进行练习,可以按照以下方式安装:

sudo apt-get install python3
sudo apt-get install python3-pip

第二个命令安装了pip,它是 Python 的包管理器。所有不包括在标准安装中的开源 Python 库都可以通过pip来安装。我们将在接下来的部分中探讨如何使用 pip。

开始

在本书的过程中,我们将致力于涵盖 Python、网络安全、渗透测试和数据科学领域的先进和著名的行业标准。然而,正如他们所说,每段非凡的旅程都始于小步。让我们开始我们的旅程,先了解 Python 的基础知识。

变量和关键字

变量,顾名思义,是保存值的占位符。Python 变量只是在 Python 程序或脚本的范围内保存用户定义值的名称。如果我们将 Python 变量与其他传统语言(如 C、C++、Java 等)进行比较,我们会发现它们有些不同。在其他语言中,我们必须将数据类型与变量的名称关联起来。例如,在 C 或 Java 中声明整数,我们必须声明为int a=2,编译器将立即在 C 中保留两个字节的内存,在 Java 中保留四个字节。然后将内存位置命名为a,程序将引用其中存储的值2。然而,Python 是一种动态类型语言,这意味着我们不需要将数据类型与我们在程序中声明或使用的变量关联起来。

整数的典型 Python 声明可能如a=20。这只是创建一个名为a的变量,并将值20放入其中。即使我们在下一行将值更改为a="hello world",它也会将字符串hello world与变量a关联起来。让我们在 Python 终端上看看它的运行情况:

使用 Python 终端,只需在终端提示符中键入python3命令。让我们思考一下这是如何工作的。看一下下面的图表,比较静态类型语言和动态类型语言:

正如您在前面的图表中看到的,在 Python 的情况下,变量实际上保存对实际对象的引用。每次更改值时,都会在内存中创建一个新对象,并且变量指向这个新对象。以前的对象由垃圾收集器声明。

在讨论 Python 是一种动态类型语言之后,我们不应该将其与弱类型语言混淆。尽管 Python 是动态类型的,但它也是一种强类型语言,就像 Java、C 或 C++一样。

在下面的示例中,我们声明一个字符串类型的变量a和一个整数类型的变量b

当我们执行操作c=a+b时,在弱类型语言中可能发生的是将b的整数值转换为字符串,并将存储在变量c中的结果为hello world22。然而,由于 Python 是强类型的,该函数遵循与变量关联的类型。我们需要显式进行转换才能执行这种操作。

让我们看看下面的示例,以了解强类型语言的含义;我们在运行时明确更改变量b的类型并将其转换为字符串类型:

变量命名约定

在了解了如何声明和使用变量的基础知识之后,让我们尝试了解它们遵循的命名约定。变量,也称为标识符,可以以 A-Z、a-z 或下划线之间的任何字母开头命名。然后可以跟随任意数量的数字或字母数字字符。

必须注意的是,某些特殊字符,如%,@,#,-和!在 Python 中是保留的,不能与变量一起使用。

Python 关键字

关键字,顾名思义,是某种语言实现中具有预定义含义的特定保留字。在其他语言中,我们通常不能使用与关键字相同的名称来命名我们的变量,但 Python 是一个略有不同的情况。尽管我们不应该使用与关键字保留相同的名称来命名变量或标识符,即使我们这样做,程序也不会抛出任何错误,我们仍然会得到一个输出。让我们尝试通过传统的 C 程序和等效的 Python 脚本来理解这一点:

应该注意的是,这是一个简单的 C 程序,我们在其中声明了一个整数,并使用int标识符来标识它,随后我们简单地打印hello world

然而,当我们尝试编译程序时,它会抛出编译错误,如下面的屏幕截图所示:

让我们尝试在 Python shell 中做同样的事情,看看会发生什么:

可以看到,当我们用名称intstr声明变量时,程序没有抛出任何错误。尽管intstr都是 Python 关键字,在前面的情况下,我们看到用名称int声明的变量保存了一个字符串值,而用str类型声明的变量保存了一个int值。我们还看到了一个普通变量a,它是从int类型转换为string类型。由此可以确定,我们可以在 Python 中使用保留字作为变量。这样做的缺点是,如果我们要使用关键字作为变量或标识符,我们将覆盖这些保留字所具有的实际功能。当我们在程序范围内覆盖它们的实际行为时,它们将遵循更新或覆盖的功能,这是非常危险的,因为这将使我们的代码违反 Python 的约定。这应该始终被避免。

让我们扩展前面的例子。我们知道str()是一个内置的 Python 函数,其目的是将数值数据类型转换为字符串类型,就像我们对变量a所看到的那样。然而,后来我们重写了它的功能,并且在我们的程序范围内,我们将其分配给了一个整数类型。现在,在程序范围内的任何时间点,如果我们尝试使用str函数将数值类型转换为string,解释器将抛出一个错误,说int类型变量不能用作方法,或者它们不可调用,如下面的屏幕截图所示:

对于int方法也是如此,我们将不再能够使用它将字符串转换为其等效的整数。

现在,让我们看看 Python 中还有哪些类型的关键字,我们应该尽量避免将它们用作我们的变量名。有一种很酷的方法可以通过 Python 代码本身来做到这一点,这让我们可以在终端窗口中打印 Python 关键字:

import语句用于在 Python 中导入库,就像我们在 Java 中导入包时一样。我们将在以后的章节中详细介绍使用导入和循环。现在,我们将看看不同的 Python 关键字的含义:

  • false: 布尔false运算符。

  • none: 这相当于其他语言中的Null

  • true: 布尔true运算符。

  • and: 逻辑and,可以与条件和循环一起使用。

  • as: 这用于为我们导入的模块分配别名。

  • assert: 这用于调试代码的目的。

  • break: 这会退出循环。

  • class: 这用于声明一个类。

  • continue: 这是传统的continue语句,用于循环,可以用于继续执行循环。

  • def:用于定义函数。每个 Python 函数都需要在def关键字之前。

  • del:用于删除对象

  • elif:条件else...if语句。

  • else:条件else语句。

  • except:用于捕获异常。

  • finally:与异常处理一起使用,作为我们清理资源的最终代码块的一部分。

  • for:传统的 for 循环声明关键字。

  • global:用于声明和使用全局变量。

  • if:条件if语句。

  • import:用于导入 Python 库、包和模块。

  • in:用于在 Python 字符串、列表和其他对象之间进行搜索。

  • is:用于测试对象的标识。

  • lambda:与 Lambda 函数一起使用。

  • nonlocal:用于声明嵌套函数中不是其本地变量的变量。

  • not:条件运算符。

  • or:另一个条件运算符。

  • pass:在 Python 中用作占位符。

  • raise:用于在 Python 中引发异常。

  • return:用于从函数返回。

  • try:与异常处理一起使用的传统try关键字。

  • while:与while循环一起使用。

  • with:用于文件打开等。

  • yield:与生成器一起使用。

  • from:与相对导入一起使用。

在本书中,我们将学习此列表中提到的所有关键字。

Python 数据类型

像任何其他编程语言一样,Python 也有标准数据类型。在本节中,我们将探讨 Python 提供给我们使用的各种强大的数据类型。

数字

数字,顾名思义,涵盖了所有数字数据类型,包括整数和浮点数据类型。在本章的前面,我们看到要使用整数或浮点数,我们可以简单地声明变量并赋予整数或浮点值。现在,让我们编写一个适当的 Python 脚本,并探索如何使用数字。将脚本命名为numbers.py,如下所示:

前面的屏幕截图显示了一个简单的 Python 脚本,该脚本将整数与浮点数相加,然后打印总和。要运行脚本,我们可以输入python3 numbers.py命令,如下所示:

您可能已经注意到脚本开头的命令是#! /usr/bin/python。这行的作用是使您的代码可执行。在脚本的权限已更改并且已被设置为可执行之后,命令表示如果尝试执行此脚本,则我们应该继续使用/usr/bin/python3路径中放置的python3来执行它。可以在以下示例中看到这一点:

如果我们观察print命令,我们可以看到字符串格式化程序是%s。要用实际值填充它,需要将第二个参数传递给print函数:

要将字符串转换为其等效的整数或浮点值,我们可以使用内置的int()float()函数。

字符串类型

我们知道字符串是字符的集合。在 Python 中,字符串类型属于序列类别。字符串非常强大,有许多方法可用于执行字符串操作。让我们看一下下面的代码片段,它向我们介绍了 Python 中的字符串。在 Python 中,字符串可以在单引号和双引号中声明:

在上面的代码中,我们只是声明了一个名为my_str的字符串,并将其打印在控制台窗口上。

字符串索引

必须注意的是,在 Python 中可以将字符串视为字符序列。字符串可以被视为字符列表。让我们尝试打印字符串的各个索引处的字符,如下面的屏幕截图所示:

在索引 0 处,字符 0 被打印。在索引 10 处,我们有一个空格,而在索引 5 处,我们有字母 m。需要注意的是,序列在 Python 中以起始索引 0 存储,字符串类型也是如此。

通过方法和内置函数进行字符串操作

在本节中,我们将看看如何比较两个字符串,连接字符串,将一个字符串复制到另一个字符串,并使用一些方法执行各种字符串操作。

replace( ) 方法

replace 方法用于执行字符串替换。它返回一个带有适当替换的新字符串。replace 方法的第一个参数是要在字符串中替换的字符串或字符,而第二个参数是要替换的字符串或字符:

在前面的例子中,我们可以看到原始字符串中的 !@ 替换,并返回一个带有替换的新字符串。需要注意的是,这些更改实际上并没有应用到原始字符串上,而是返回了一个带有适当更改的新字符串。这可以在下一行中验证,我们打印原始字符串,旧的未更改值 Welcome to python strings ! 被打印出来。这背后的原因是 Python 中的字符串是不可变的,就像在 Java 中一样。这意味着一旦声明了一个字符串,通常就不能修改。然而,并非总是如此。让我们尝试更改字符串,并这次尝试捕获最初声明的字符串 my_str 中的修改,如下所示:

在前面的代码中,我们能够修改原始字符串,因为我们从我们之前声明的字符串 my_str 中的 replace 方法中得到了新返回的字符串。这可能与我们之前说的相矛盾。让我们看看在调用 replace 方法之前和之后发生了什么:

! 替换为 @ 后,结果如下:

在前面的两个示例中可以看到,在调用 replace 方法之前,my_str 字符串引用指向包含 ! 的实际对象。一旦 replace() 方法返回一个新字符串,并且我们用新返回的对象更新了现有的字符串变量,旧的内存对象并没有被覆盖,而是创建了一个新的对象。程序引用现在指向新创建的对象。早期的对象在内存中,并没有任何引用指向它。这将在以后的阶段由垃圾收集器清理。

另一件我们可以做的事情是尝试改变原始字符串中任何位置的任何字符。我们已经看到字符串字符可以通过它们的索引访问,但是如果我们尝试在任何特定索引处更新或更改字符,就会抛出异常,并且不允许进行操作,如下面的屏幕截图所示:

默认情况下,replace() 方法会替换目标字符串中替换字符串的所有出现。然而,如果我们只想替换目标字符串中的一个或两个出现,我们可以向 replace() 方法传递第三个参数,并指定我们想要进行的替换次数。假设我们有以下字符串:

如果我们只想要!字符的第一个出现变成@,并且我们希望其余部分保持不变,可以按照以下方式实现:

子字符串或字符串切片

获取字符串的一部分是我们在日常字符串操作中经常遇到的常见练习。诸如 C 或 Java 之类的语言为我们提供了专用方法,如substr(st_index,end_index)subString(st_index,end_index)。在 Python 中执行子字符串操作时,没有专用方法,但我们可以使用切片。例如,如果我们希望获得原始my_str字符串的前四个字符,我们可以通过使用my_str[0:4]等操作来实现,如下面的屏幕截图所示:

同样,切片操作返回一个新的字符串,而不会对原始字符串进行更改。此外,值得在这里理解的是,切片发生在 n-1 个字符上,其中n是作为第二个参数指定的上限,即在我们的例子中是四。因此,实际的子字符串操作将从索引0开始,到索引3结束,从而返回字符串Welc

让我们看一些切片的更多例子:

  • 要从索引4获取整个字符串,按照以下方式操作:

  • 要从开头获取到索引4的字符串,请执行以下操作:

  • 要使用切片打印整个字符串,请执行以下操作:

  • 要打印步长为2的字符,按照以下方式操作:

  • 要打印字符串的反向,请执行以下操作:

  • 打印字符串的一部分以相反的顺序,如下所示:

字符串连接和复制

+是 Python 中用于连接两个字符串的连接运算符。与往常一样,连接的结果是一个新的字符串,除非我们获得更新后的字符串,否则更新将不会反映在原始字符串对象上。+运算符在用于字符串类型时内部被重载以执行对象的连接。当它用于数值数据类型时,也用于两个数字的加法,如下所示:

有趣的是,Python 还支持另一个操作符,当与字符串数据类型一起使用时会被重载。它不是执行常规操作,而是执行原始操作的变体,以便可以在字符串数据类型之间复制功能。在这里,我们谈论的是乘法操作符*。它通常用于执行数值数据类型的乘法,但当它用于字符串数据类型时,它执行的是复制操作。这在以下代码片段中显示:

在前面的情况下,乘法运算符实际上将存储在变量c中的Hello world字符串复制了五次,正如我们在表达式中指定的那样。这是一个非常方便的操作,可以用来生成模糊负载,我们将在本书的后面章节中看到。

strip(),lstrip()和 rstrip()方法

strip方法实际上是用于从输入字符串中去除空格。默认情况下,strip方法将从字符串的左右两侧去除空格,并返回一个新的字符串,其中前导和尾随两侧都没有空格,如下面的屏幕截图所示:

然而,如果我们只想去掉左边的空格,我们可以使用lstrip()方法。同样,如果我们只想去掉右边的空格,我们可以使用rstrip()方法。如下所示:

split()方法

split方法,顾名思义,用于在特定分隔符上拆分输入字符串,并返回包含已拆分单词的列表。我们将很快更详细地了解列表。现在,让我们看一下以下示例,其中我们有员工的姓名、年龄和工资,用逗号分隔在一个字符串中。如果我们希望分别获取这些信息,我们可以在,上执行拆分。split函数将第一个参数作为要执行split操作的分隔符:

默认情况下,split操作是在空格上执行的,即,如果未指定分隔符。可以如下所示:

find()、index()、upper()、lower()、len()和 count()方法

find()函数用于在目标字符串中搜索字符或字符串。如果找到匹配,此函数返回字符串的第一个索引。如果找不到匹配,则返回-1

index()方法与find()方法相同。如果找到匹配,它返回字符串的第一个索引,并在找不到匹配时引发异常:

upper()方法用于将输入字符串转换为大写字母,lower()方法用于将给定字符串转换为小写字母:

len()方法返回给定字符串的长度:

count()方法返回我们希望在目标字符串中计算的任何字符或字符串的出现次数:

innot in方法

innot in方法非常方便,因为它们让我们可以快速在序列上进行搜索。如果我们希望检查目标字符串中是否存在或不存在某个字符或单词,我们可以使用innot in方法。这将返回True(如果单词存在)和False(如果不存在):

endswith()、isdigit()、isalpha()、islower()、isupper()和 capitalize()方法

endswith()方法检查给定字符串是否以我们传递的特定字符或单词结尾:

isdigit()方法检查给定的字符串是否为数字类型:

isalpha()方法检查给定的字符串是否为字母字符类型:

islower()方法检查字符串是否为小写,而isupper()方法检查字符串是否为大写。capitalize()方法将给定字符串转换为句子大小写:

列表类型

Python 没有数组类型,而是提供了列表数据类型。Python 列表也属于序列类,并提供了广泛的功能。如果你来自 Java、C 或 C++背景,你可能会发现 Python 列表与这些语言提供的数组和列表类型略有不同。在 C、C++或 Java 中,数组是相似数据类型的元素集合,Java 数组列表也是如此。但在 Python 中情况不同。在 Python 中,列表是可以是同质和异质数据类型的元素集合。这是使 Python 列表强大、健壮且易于使用的特点之一。在声明时,我们也不需要指定 Python 列表的大小。它可以动态增长以匹配它包含的元素数量。让我们看一个使用列表的基本示例:

Python 中的列表从索引0开始,可以根据索引访问任何项,如前面的屏幕截图所示。前面的列表是同质的,因为所有元素都是字符串类型。我们也可以有一个异质列表,如下所示:

目前,我们正在手动打印列表元素。我们可以很容易地用循环迭代它们,稍后我们将探讨这一点。现在,让我们试着理解 Python 中可以对列表结构执行哪些操作。

切片列表

切片是一种允许我们从序列和列表中提取元素的操作。我们可以对列表进行切片,以提取我们感兴趣的部分。需要再次注意的是,切片的索引是基于 0 的,并且最后一个索引始终被视为n-1,其中 n 是指定的最后一个索引值。要从列表中切片出前五个和后五个元素,我们可以执行以下操作:

让我们看一些列表切片的示例及其结果:

  • 要获取从索引4开始的列表,请执行以下操作:

  • 要获取从开头到索引4的列表元素,请执行以下操作:

  • 要使用切片打印整个列表,请执行以下操作:

  • 要打印步长为2的列表元素,请执行以下操作:

  • 要打印列表的反向,请执行以下操作:

  • 要以相反的顺序打印列表的一部分,请执行以下操作:

  • list-append()添加新元素:append()方法用于向列表添加元素,要添加的元素作为参数传递给append()方法。要添加的这些元素可以是任何类型。除了数字或字符串之外,元素本身可以是一个列表:

我们可以看到在前面的例子中,我们使用append()方法向原始列表添加了三个元素678。然后,我们实际上添加了另一个包含三个字符的列表,这个列表会完整地存储在原始列表中。可以通过指定my_list[8]索引来访问它们。在前面的例子中,新列表完整地添加到原始列表中,但没有合并。

合并和更新列表

在 Python 中,可以通过两种方式进行列表合并。首先,我们可以使用传统的+运算符,之前我们用来连接两个字符串。当用于列表对象类型时,它也是一样的。另一种方法是使用extend方法,它将新列表作为要与现有列表合并的参数。这在以下示例中显示:

要更新列表中的元素,我们可以访问其索引,并为我们希望更新的任何元素添加更新后的值。例如,如果我们希望将字符串Hello作为列表的第 0 个元素,可以通过将第 0 个元素分配给Hello值来实现merged[0]="hello"

复制列表

我们已经看到 Python 变量只是对实际对象的引用。对于列表也是如此。因此,操作列表会有点棘手。默认情况下,如果我们通过简单地使用=运算符将一个列表变量复制到另一个列表变量,它实际上不会创建列表的副本或本地副本 - 相反,它只会创建另一个引用,并将新创建的引用指向相同的内存位置。因此,当我们对复制的变量进行更改时,原始列表中也会反映相同的更改。在下面的示例中,我们将创建新的隔离副本,其中对复制的变量的更改不会反映在原始列表中:

现在,让我们看看如何创建现有列表的新副本,以便对新列表的更改不会对现有列表造成任何更改:

创建原始列表的隔离副本的另一种方法是利用 Python 中提供的copydeepcopy函数。浅复制构造一个新对象,然后将该对象的引用插入到原始列表中找到的对象中。另一方面,深复制构造一个新的复合对象,然后递归地插入到原始列表中找到的对象的副本

从列表中删除元素

我们可以使用del命令删除列表中的元素或整个列表。del命令不返回任何内容。我们也可以使用pop方法从列表中删除元素。pop方法将要删除的元素的索引作为参数:

整个列表结构可以被删除如下:

使用 len()、max()和 min()进行复制

乘法运算符*,当应用于列表时,会导致列表元素的复制效果。列表的内容将根据传递给复制运算符的数字重复多次:

len()方法给出了 Python 列表的长度。max()方法返回列表的最大元素,而min()方法返回列表的最小元素:

我们也可以在字符类型上使用maxmin方法,但是不能在包含混合或异构类型的列表上使用它们。如果这样做,将会得到一个异常,说明我们正在尝试比较数字和字符:

in 和 not in

innot in方法是 Python 中的基本操作,可以用于任何序列类型。我们之前看到了它们如何与字符串一起使用,我们用它们来搜索目标字符串中的字符串或字符。in方法返回true,如果搜索成功则返回falsenot in方法则相反。执行如下所示:

Python 中的元组

Python 元组与 Python 列表非常相似。不同之处在于它是一个只读结构,因此一旦声明,就不能对元组的元素进行修改。Python 元组可以用如下方式使用:

在前面的代码中,我们可以看到我们可以像访问列表一样访问元组,但是当我们尝试更改元组的任何元素时,它会抛出一个异常,因为元组是只读结构。如果我们执行我们在列表上执行的操作,我们会发现它们与元组的工作方式完全相同:

如果元组中只有一个元素,则必须使用尾随逗号声明。如果在声明时不添加逗号,则将根据元组的元素将其解释为数字或字符串数据类型。以下示例更好地解释了这一点:

元组可以转换为列表,然后可以进行如下操作:

Python 中的字典

字典是非常强大的结构,在 Python 中被广泛使用。字典是一种键值对结构。字典键可以是唯一的数字或字符串,值可以是任何 Python 对象。字典是可变的,可以就地更改。以下示例演示了 Python 中字典的基础知识:

Python 字典可以在花括号内声明。每个键值对之间用逗号分隔。应该注意,键必须是唯一的;如果我们尝试重复键,旧的键值对将被新的键值对覆盖。从前面的例子中,我们可以确定字典键可以是字符串或数字类型。让我们尝试在 Python 中对字典进行各种操作:

  • 使用键检索字典值:可以通过字典键的名称访问字典值。如果不知道键的名称,可以使用循环来遍历整个字典结构。我们将在本书的下一章中介绍这一点:

这是打印字典值的许多方法之一。但是,如果我们要打印值的键在字典中不存在,我们将收到一个找不到键的异常,如下截图所示:

有一种更好的方法来处理这个问题,避免这种类型的异常。我们可以使用字典类提供的get()方法。get()方法将键名作为第一个参数,如果键不存在,则将默认值作为第二个参数。然后,如果找不到键,将返回默认值,而不是抛出异常。如下截图所示:

在前面的例子中,当实际字典dict1中存在k1键时,将返回k1键的值,即v1。然后,搜索了k0键,但最初不存在。在这种情况下,不会引发异常,而是返回False值,表明实际上不存在这样的键K0。请记住,我们可以将任何占位符作为get()方法的第二个参数,以指示我们要搜索的键的缺失。

  • 向字典添加键和值:一旦声明了字典,在代码的过程中可能会有许多情况,我们希望修改字典键或添加新的字典键和值。可以通过以下方式实现。如前所述,字典值可以是任何 Python 对象,因此我们可以在字典中的值中有元组、列表和字典类型:

现在,让我们将更复杂的类型添加为值:

可以通过它们的键正常检索这些值,如下所示:

  • 扩展字典内容: 在前面的例子中,我们将一个字典添加为现有字典的值。我们现在将看到如何将两个字典合并为一个公共或新字典。可以使用update()方法来实现这一点:

  • Keys():要获取所有字典键,我们可以使用keys()方法。这将返回字典键的类实例:

我们可以看到,keys 方法返回一个dict_keys类的实例,它保存了字典键的列表。我们可以将其强制转换为列表类型,如下所示:

  • values()values()方法返回字典中存在的所有值:

  • Items():这个方法实际上是用来遍历字典键值对的,因为它返回一个包含元组列表的列表类实例。每个元组有两个条目,第一个是键,第二个是值:

我们也可以将返回的类实例转换为元组、列表元组或列表类型。这样做的理想方式是遍历项目,我们稍后将在循环时看到:

  • innot ininnot in方法用于查看字典中是否存在键。默认情况下,innot in子句将搜索字典键,而不是值。看下面的例子:

  • 存储顺序:默认情况下,Python 字典是无序的,这意味着它们在内部存储的顺序与我们定义的顺序不同。这是因为字典存储在称为哈希表的动态表中。由于这些表是动态的,它们的大小可以增加和缩小。内部发生的情况是计算键的哈希值并将其存储在表中。键进入第一列,而第二列保存实际值。让我们看下面的例子来更好地解释这一点:

在前面的例子中,我们声明了一个名为a的字典,第一个键为abc,第二个键为abcd。然而,当我们打印值时,我们可以看到abcdabc之前存储。为了解释这一点,让我们假设字典内部存储的动态表或哈希表的大小为8

正如我们之前提到的,键将被存储为哈希值。当我们计算abc字符串的哈希并以模 8 的方式进行除法时,即表大小为8,我们得到结果7。如果我们对abcd做同样的操作,我们得到结果4。这意味着哈希abcd将被存储在索引4,而哈希abc将被存储在索引7。因此,在列表中,我们得到abcdabc之前列出的原因是这样的:

hash(key)%table_size操作后,可能会出现两个键到达相同值的情况,这称为冲突。在这种情况下,首先插槽的键是先存储的键。

  • sorted():如果我们希望字典根据键排序,可以使用内置的 sorted 方法。这可以调整为返回一个元组列表,每个元组在第 0 个索引处有一个键,第 1 个索引处有一个值:

  • 删除元素:我们可以使用传统的del语句来删除任何字典项。当我们说删除时,我们指的是删除键和值。字典项成对工作,因此删除键也会删除值。删除条目的另一种方法是使用pop()方法并将键作为参数传递。这在以下代码片段中显示:

Python 运算符

Python 中的运算符是可以对表达式进行算术或逻辑操作的东西。运算符操作的变量称为操作数。让我们试着了解 Python 中提供的各种运算符:

  • 算术
函数示例
加法a + b
减法a - b
否定-a
乘法a * b
除法a / b
取模a % b
指数a ** b
地板除法a // b
  • 赋值

  • a = 0评估为a=0

  • a +=1评估为a = a + 1

  • a -= 1评估为a = a + 1

  • a *= 2评估为a = a * 2

  • a /= 5评估为a = a / 5

  • a **= 3评估为a = a ** 3

  • a //= 2评估为a= a // 2(地板除法 2)

  • a %= 5评估为a= a % 5

  • 逻辑运算符

  • andTrue:如果两个操作数都为true,则条件变为true。例如,(a and b)true

  • orTrue:如果两个操作数中有任何一个非零,则条件变为true。例如,(a or b)true

  • notTrue:用于颠倒其操作数的逻辑状态。例如,not (a and b)false

  • 位运算符

函数示例
anda & b
ora | b
xora ^ b
反转~ a
右移a >> b
左移a << b

总结

在本章中,我们讨论了 Python 的基础知识,并探索了该语言的语法。这与您以往可能学过的语言并没有太大不同,例如 C、C++或 Java。但是,与同行相比,它更容易使用,并且在网络安全领域非常强大。本章阐述了 Python 的基础知识,并将帮助我们进步,因为一些数据类型,如列表、字典、元组和字符串在本书的整个过程中都被大量使用。

在下一章中,我们将学习条件和循环,并看看循环如何与我们迄今为止学习的数据类型一起使用。

问题

  1. Python 是开源的吗?如果是,它与其他开源语言有何不同?

  2. 谁管理 Python 并致力于进一步的功能增强?

  3. Python 比 Java 快吗?

  4. Python 是面向对象的还是函数式的?

  5. 如果我对任何编程语言几乎没有经验,我能快速学会 Python 吗?

  6. Python 对我有什么好处,作为一名网络安全工程师?

  7. 我是一名渗透测试员-为什么我需要了解人工智能和机器学习?

第二章:构建 Python 脚本

本章将涵盖所有编程语言的核心概念。这包括条件语句、循环、函数和包。我们将看到这些概念在 Python 中与其他编程语言中基本相同,只是在一些语法上有所不同。但语法只需要练习;其他一切都会自动顺利进行。本章我们将要涵盖的主题如下:

  • 条件语句

  • 循环

  • 函数

  • 模块和包

  • 理解和生成器

技术要求

确保你具备以下继续学习所需的先决条件:

  • 一台工作的计算机或笔记本电脑

  • Ubuntu 操作系统(最好是 16.04)

  • Python 3.x

  • 一个工作的互联网连接

缩进

如果你来自 Java、C 或 C++等语言的背景,你可能熟悉使用花括号来分组逻辑连接语句的概念。然而,在 Python 中情况并非如此。相反,逻辑连接的语句,包括类、函数、条件语句和循环,都是使用缩进来分组的。缩进可以使代码保持清晰易读。我们将在接下来的部分中更详细地探讨这一点。但现在,让我们和花括号说再见。我建议你使用制表符进行缩进,因为在每一行输入相同数量的空格会非常耗时。

条件语句

与所有其他语言一样,Python 使用条件语句来执行条件操作。Python 支持的条件语句如下:

  • if条件

  • if...else条件

  • else...if条件梯,在 Python 中称为elif

Python 不支持switch语句。

if 条件

if条件或if语句接受一个语句,并在评估该语句后返回布尔值TrueFalse。如果条件返回True,则执行if语句后面的代码(同样缩进)。如果语句/条件评估为False,那么如果有else代码块,则执行else代码块,否则执行if块后面的代码,因此if块实际上被跳过。让我们看看if代码的运行情况。

从现在开始,我们将看一下脚本是如何工作的。我们将要么创建脚本文件,要么进行练习。因此,请继续在 gedit 或你选择的任何编辑器上创建一个名为if_condition.py的文件。或者,我们可以在终端中输入gedit if_condition.py

然后我们输入以下代码:

a=44
b=33
if a > b:
    print("a is greater") 
print("End")

现在,为了运行这个脚本,我们可以在终端中简单地输入python3.5 if_condition.py

Python 的print方法默认会在要打印的字符串后添加\n,这样我们可以看到两个不同行的输出。请注意if语句的语法如下:

if <条件>:然后缩进的代码

我们是否使用括号与条件是由我们决定的。正如你所看到的,条件评估为True,所以打印了a is greater。对于 Python 中的if条件,任何不评估为零(0)、FalseNone的东西都会被视为True,并且执行if语句后面的代码。

让我们看一个if条件与and...orand...not逻辑运算符结合的另一个例子。

让我们创建另一个名为if_detailed.py的文件,并输入以下代码:

你可能已经注意到,在文件的开头,我们有一个语句,写着#! /usr/bin/python3.5。这意味着我们不必每次执行代码时都输入python3.5。它指示代码使用位于/usr/bin/python3.5的程序来执行它,每次作为可执行文件执行时。我们需要改变文件的权限使其可执行。这样做,然后按照以下方式执行代码:

产生的输出是不言自明的。正如我之前提到的,任何不等于0FalseNoneempty的东西都被视为True,并且执行if块。这解释了为什么前三个if条件被评估为True并且消息被打印出来,但第四个消息没有被打印。从第 19 行开始,我们使用了逻辑运算符。在 Python 中,合取操作由and运算符执行,这与我们在 C、C++和 Java 中使用的&&相同。对于短路布尔运算符,在 Python 中我们有or关键字,它与 C、C++和 Java 中的||相同。最后,not关键字在 Python 中提供否定,就像其他语言中的!一样。

应该注意,在 Python 中,null字节字符由保留关键字None表示,这与 Java 或 C#等语言中的null相同。

if...else条件

if...else条件在任何其他语言中基本上是一样的。如果if条件评估为True值,那么缩进在if下面的代码块将被执行。否则,缩进在else块下面的代码块将被执行:

a=44
b=66
if a > b:
    print("a is Greater") 
else:
    print("B is either Greater or Equal")
print("End")

让我们创建一个名为if_else.py的文件,并看看如何使用它:

这里的输出也是不言自明的。在这段代码中,我们探讨了一些位运算符与if...else代码结构一起使用的情况。我们还使用了变量,这些变量将被打印出来。%s是一个占位符,并指定%s的值应该被字符串变量替换,其值将在字符串结束后立即出现。如果我们有多个值要替换,它们可以作为一个元组传递,如%(val1,val2,val3)

if...elif条件

if...elif梯,在其他编程语言中如 C、C++和 Java 中被称为if...else if,在 Python 中具有相同的功能。if条件让我们在代码的else部分旁边指定一个条件。只有条件为true时,才会执行条件语句后面的部分:

a=44
b=66
if a > b:
    print("a is Greater") 
elif b > a:
    print("B is either Greater or Equal")
else:
    print("A and B are equal")
print("End")

必须注意的是,前面代码片段中的第三个else是可选的。即使我们不指定它,代码也能正常工作:

让我们创建一个名为if_el_if.py的文件,并看看它如何使用:

循环

循环是每种编程语言都具有的实用工具。借助循环,我们可以执行重复性的任务或语句,如果没有循环,将需要大量的代码行。这在某种程度上违背了首先拥有编程语言的目的。如果你熟悉 Java、C 或 C++,你可能已经遇到了whilefordo...while循环。Python 基本上是一样的,只是它不支持do...while循环。因此,我们将在下一节中学习的 Python 中的循环是以下的:

  • while循环

  • for循环

while 循环

请记住,当我们在书的第一章讨论列表时,我们提到在 Python 中列表实际上可以包含异构数据类型。列表可以包含整数、字符串、字典、元组,甚至是嵌套列表。这个特性使得列表非常强大,非常容易和直观地使用。让我们看下面的例子:

my_list=[1,"a",[1,2,3],{"k1":"v1"}]
my_list[0] -> 1
my_List[1] -> "a"
my_list[2] -> [1,2,3]
my_list[2][0] -> 1
my_list[2][2] -> 3
my_list[3] -> {"k1":"v1"}
my_list[3]["k1"] -> "v1"
my_list[3].get("k1") -> "v1

让我们通过以下代码更仔细地了解while循环,我们将其称为while_loops.py。我们还将看到如何使用while循环迭代列表:

代码的第一部分,第 2 到 6 行,描述了while循环的简单用法,我们在其中打印了一个语句五次。请注意,为了执行循环指定的条件可以放在括号内或括号外,如第 7 到 10 行所示。

在第 12 行,我们声明了一个包含数字、字符串、浮点数和嵌套列表的列表。然后,在从第 14 行开始的最后一个while循环中,我们通过将循环控制变量设置为小于列表长度来迭代列表的元素。在循环中,我们检查列表变量的类型。if类型(1)返回一个整数类,类型(a)返回一个字符串类,类型([])返回一个列表类。当类型是列表时,我们再次在嵌套的while循环中迭代它的元素,并打印每一个,如第 19 到 24 行所示:

for 循环

for循环在 Python 中被广泛使用,每当我们需要迭代不可改变的列表时,它都是默认选择。在继续使用for循环之前,让我们更仔细地了解 Python 中的迭代可迭代迭代器这些术语的含义。

迭代、可迭代和迭代器

迭代:迭代是一个过程,其中一组指令或结构按顺序重复指定次数,或直到满足条件。每次循环体执行时,都称为完成一次迭代。

可迭代:可迭代是一个具有__iter__方法的对象,它返回一个迭代器。迭代器是任何包含可以迭代的元素序列的对象,然后可以执行操作。Python 字符串、列表、元组、字典和集合都是可迭代的,因为它们实现了__iter__方法。看下面的代码片段,看一个例子:

在上面的代码片段中,我们声明了一个字符串a,并将值hello放入其中。要查看 Python 中任何对象的所有内置方法,我们可以使用dir(<object>)方法。对于字符串,这将返回可以在字符串类型上执行的所有操作和方法。在第二行,第 5 个操作是我们之前提到的iter方法。可以看到iter(a)返回一个字符串迭代器。

同样,列表对象的iter方法将返回一个列表迭代器,如前所示。

迭代器:迭代器是一个具有__next__方法的对象。next方法始终返回调用原始iter()方法的序列的next元素,从索引 0 开始。下面的代码片段中展示了这一点:

正如在字符串和列表的示例中所看到的,迭代器上的next方法总是返回我们迭代的序列或对象中的next元素。必须注意的是,迭代器只能向前移动,如果我们想让iter_alist_itr返回到任何元素,我们必须重新将迭代器初始化为原始对象或序列:

更仔细地看一下 for 循环

Python 中的for循环超出了其他编程语言中for循环的能力。当调用诸如字符串、元组、列表、集合或字典等可迭代对象时,for循环内部调用iter来获取迭代器。然后,它调用next方法来获取可迭代对象中的实际元素。然后,它重复调用 next 直到引发StopIteration异常,然后它会在内部处理并将我们从循环中取出。for循环的语法如下所示:

for var in iterable:
    statement 1
    statement 2
    statement n

让我们创建一个名为for_loops.py的文件,它将解释for循环的基本用法:

在前面的示例中,我们使用了 Python 的 range 函数/方法,它帮助我们实现了传统的for循环,我们在其他编程语言(如 C、C++或 Java)中学到的。这可能看起来像for i =0 ;i < 5 ;i ++。Python 中的 range 函数需要一个必需参数和两个默认参数。必需参数指定迭代的限制,并且从索引0开始,返回数字,直到达到限制,就像代码的第 3 和第 4 行所示的那样。当使用两个参数调用时,第一个参数作为范围的起点,最后一个作为终点,就像我们代码的第 7 和第 8 行所示的那样。最后,当使用三个参数调用range函数时,第三个参数作为步长,默认为 1。这在下面的输出和示例代码的第 12 和第 13 行中显示:

让我们看看另一个for循环的例子,我们将用它来迭代 Python 定义的所有可迭代对象。这将使我们能够探索for循环的真正威力。让我们创建一个名为for_loops_ad.py的文件:

之前,我们看到了如何从列表、字符串和元组中读取值。在前面的示例中,我们使用for循环枚举字符串、列表和字典。我们之前了解到,for循环实际上调用可迭代对象的iter方法,然后为每次迭代调用next方法。这在下面的示例中显示:

当我们使用for循环迭代 Python 字典时,默认情况下会将字典键返回给我们。当我们在字典上使用.items()时,每次迭代都会返回一个元组,其中键在元组的第 0 个索引处,值在第一个索引处。

Python 中的函数和方法

函数和方法用于设计或制作可以在脚本或其他脚本的整个过程中重复使用的逻辑代码单元。函数实际上构成了代码重用的基础,并为代码结构带来了模块化。它们使代码更清晰,更容易修改。

建议我们总是尝试将逻辑分解为小的代码单元,每个单元都是一个函数。我们应该尽量保持方法的大小在代码行方面尽可能小。

以下代码代表了在 Python 中定义方法的基本语法:

def print_message(message):
    print(message)
    statement 2
    statement 

Python 方法在其定义中没有返回类型,就像您在 C、C++或 Java 中看到的那样,例如voidinfloat等。Python 方法可能返回值,也可能不返回值,但我们不需要明确指定。方法在 Python 中非常强大和灵活。

应该注意到每个 Python 脚本的默认名称是main,并且它被放置在一个全局变量中,可以在整个 Python 上下文中访问,称为__name__。我们将在接下来的示例中使用它。

让我们探索使用我们的method_basics.py脚本调用方法的各种方式:

现在让我们将其分解成更小的部分,并尝试理解发生了什么:

  • print_msg1(): 这是一个基本的方法,只是在控制台上打印一个字符串。它在第 2 行定义,在第 19 行调用。

  • print_msg2(): 这是一个方法,接受变量消息作为参数,然后在屏幕上打印该变量的值。请记住,Python 变量不需要指定类型,因此我们可以将任何数据传递给message变量。这是一个接受单个参数的 Python 方法的示例。请记住,参数的类型是 Python 对象,它可以接受传递给它的任何值。输出可以在以下截图中看到:

  • print_msg3(): 这是一个 Python 方法,接受两个参数。它类似于我们之前看到的print_msg2()方法。不同之处在于它有时可能会返回一个值。它的调用方式也不同。请注意,在第 22 行,我们通过将第二个参数传递为True来调用此方法。这意味着它返回一个值为True,但是我们在第 26 行不使用True作为第二个参数调用它,因此它不返回任何值。因此,我们在屏幕上得到None。在其他编程语言中,如 C、C++或 Java,调用方法时参数的顺序非常重要。这是因为我们传递参数的顺序应该与传递给方法的顺序相同。然而,在 Python 中,我们可以调用方法并在调用过程中传递命名参数。这意味着顺序并不重要,只要名称与方法参数的名称匹配即可。这在第 29 行中得到了体现,我们将消息作为第二个参数传递,即使它在方法定义中是第一个参数。这样做完全有效,如输出所示。

  • print_msg4(): 这是我们熟悉 Python 默认参数以及它们如何与方法一起使用的地方。默认参数是在声明方法时分配默认值的变量。如果调用者为此参数或变量传递了一个值,则默认值将被调用者传递的值覆盖。如果在调用过程中没有为默认参数传递值,则变量将保持其初始化的默认值。print_msg4()方法有一个必填参数m,和两个可选参数op1op2

  • print_msg4('Test Mandatory'): 这在第 31 行被调用。这表示必填参数应传递Test mandatory字符串,另外两个op1op2变量将被初始化为默认值,如输出所示。

  • print_msg4(1,2): 这在第 32 行被调用。这表示必填参数应传递一个带有value=1的整数,另一个带有value=2的整数应传递给op1。因此,op1的默认值将被覆盖。op2将保留默认值,因为没有传递值。

  • print_msg4(2,3,2): 这在第 33 行被调用。这表示必填参数应传递一个带有value=2的整数,另一个带有value=3的整数应传递给op1,因此op1op2的默认值将被覆盖。

  • print_msg4(1,op2='Test'): 这在第 34 行被调用。必填参数接收一个带有value=1的整数。对于第二个参数,在调用过程中我们指定了一个命名参数,因此Test的顺序对op2不重要,它将被复制到调用者的op2

  • print_msg4(1,op2=33,op1=44): 这在第 35 行被调用。必填参数接收value=1。对于第二个参数,我们指定了一个命名参数op2,对于第三个参数,我们传递了op1。同样,我们可以在输出中看到顺序并不重要。

  • print_msg5(): 通常,在其他编程语言中,函数或方法总是可以返回一个值。如果需要返回多个值,必须将这些值放入数组或另一个结构中,然后返回它们。Python 为我们抽象地处理了这种情况。如果你阅读代码,你可能会认为该方法返回了多个值,而实际上它返回的是一个元组,其中每个值都乘以了二。这可以从输出中验证。

让我们现在探索一些更进一步的方法和传递参数的方式,使用以下示例methods_adv.py。以下代码片段表示 Python 中的可变参数类型方法。从输出中可以验证,method_1接受任意大小的普通序列作为输入,这意味着我们可以向方法传递任意数量的参数。当方法声明为由*符号前缀的参数时,所有传递的参数都被转换为序列,并且一个元组对象被放置在args中。另一方面,当在调用方法时使用*与参数一起使用时,参数类型从序列中更改,内部将每个元素if序列作为单个参数传递给调用者,如method_1_rev中所示。

此外,当在方法声明中使用if与参数一起使用时,它会将所有命名参数内部转换为 Python 字典,键为名称,值为=运算符后的值。这可以在method_2中看到。最后,当**与调用者参数一起使用时,该参数会从 Python 字典内部转换为命名参数。这可以通过method_2_rev进行验证:

模块和包

每个 Python 脚本都被称为一个模块。Python 被设计为可重用和易于编码。因此,我们创建的每个 Python 文件都成为 Python 模块,并有资格在任何其他文件或脚本中被调用或使用。你可能已经学过在 Java 中如何导入类并与其他类一起重用。这里的想法基本上是一样的,只是我们将整个文件作为模块导入,我们可以重用导入文件的任何方法、类或变量。让我们看一个例子。我们将创建两个文件child.pyparent.py,并在每个文件中放置以下代码:

前五行属于child.py,最后八行属于parent.py。我们将运行父文件,如输出所示。应该注意的是,导入的文件可以被赋予别名。在我们的例子中,我们导入了 child 并给它起了别名 C。最后,我们从父 Python 脚本中调用了该模块的child_method()类。

让我们现在尝试探索 Python 包以及它们如何被使用。在 Java 中,包只是收集 Java 中逻辑连接的类文件的文件夹或目录。包在 Python 中也是如此;它们收集逻辑连接的 Python 模块。始终建议使用包,因为这样可以保持代码整洁,使其可重用和模块化。

如前所述,Python 包是一个普通的目录。唯一的区别是,为了使普通目录像 Python 包一样运行,我们必须在目录中放置一个空的__init__.py文件。这告诉 Python 应该使用哪些目录作为包。让我们继续创建一个名为shapes的包。我们将放置一个空的 Python 文件__init__.py和另一个名为area_finder.py的文件在其中:

让我们现在把以下代码放在area_finder.py文件中。我们还要创建另一个名为invoker.py的文件,并将其放在我们创建的 shapes 文件夹之外。调用者的代码在下图的右侧,而area_finder的代码在左侧:

上面的代码是 Python 中如何使用包的一个简单示例。我们创建了一个名为shapes的包,并在其中放置了一个名为area_finder的文件,用于计算形状的面积。然后,我们继续创建了一个名为invoker.py的文件,放在shapes文件夹外,并以多种方式导入了包中的area_finder脚本(仅用于演示目的)。最后,我们使用其中一个别名来调用find_area()方法。

生成器和推导式

生成器是 Python 中一种特殊的迭代器。换句话说,Python 生成器是通过发出yield命令返回生成器迭代器的函数,可以进行迭代。可能会有一些情况,我们希望一个方法或函数返回一系列值,而不仅仅是一个值。例如,我们可能希望我们的方法部分执行任务,将部分结果返回给调用者,然后从上次返回最后一个值的地方恢复工作。通常,当方法终止或返回一个值时,它的执行会从头开始。这就是生成器试图解决的问题。生成器方法返回一个值和一个控制给调用者,然后从离开的地方继续执行。生成器方法是一个带有 yield 语句的普通 Python 方法。以下代码片段generators.py解释了如何使用生成器:

请注意,由于genMethod中有一个 yield 语句,它变成了一个生成器。每次执行 yield 语句时,"a"的值都会作为控制返回给调用者(记住生成器返回一系列值)。每次对生成器方法进行next()调用时,它都会从之前离开的地方恢复执行。

我们知道,每次执行 yield 时,生成器方法都会返回一个生成器迭代器。因此,与任何迭代器一样,我们可以使用for循环来迭代生成器方法。这个for循环会一直持续,直到它到达方法中的 yield 操作。使用for循环的相同示例如下:

你可能会想为什么我们要使用生成器,当相同的结果可以通过列表实现。生成器非常节省内存和空间。如果需要大量处理来生成值,使用生成器是有意义的,因为我们只根据需求生成值。

生成器表达式是可以产生生成器对象的一行表达式,可以进行迭代。这意味着可以实现相同的内存和处理优化。以下代码片段显示了如何使用生成器表达式:

推导式

Python 推导式,通常称为列表推导式,是 Python 中非常强大的实用工具,如果我们需要对列表的所有或部分元素执行一些操作,它会很方便。列表推导式将返回一个带有应用修改的新列表。假设我们有一个数字列表,我们想要对列表中的每个数字进行平方。

让我们看看解决这个问题的两种不同方法:

左侧的代码片段是更传统的方法,需要九行。使用推导式的相同代码只需要三行。列表推导式在方括号内声明,并对列表的每个元素执行任何操作。然后返回带有修改的新列表。让我们看另一个推导式的例子。这次,我们将使用一个if条件(称为推导式过滤器),以及带有推导式的嵌套循环。我们将命名文件为list_comp_adv.py,并输入以下代码:

前面的代码片段是不言自明的。它向我们展示了如何在推导式中使用if条件(第 4 行)。它还向我们展示了如何使用嵌套循环来累加两个列表(第 5 行)。最后,它向我们展示了如何在推导式中使用字典(第 6 行)。

Map、Lambda、zip 和 filters

在本节中,我们将了解一些非常方便的 Python 函数。这些函数允许我们对 Python 可迭代对象(如列表)进行快速处理操作。

  • Map(): 正如我们之前看到的,当我们需要对列表中的所有或部分元素执行操作时,列表推导式非常方便。同样的操作也可以通过map函数实现。它接受两个参数,第一个是将对列表元素执行操作的函数,第二个是列表本身。以下示例map_usage.py演示了这一点:

  • Lambda(): Lambda 函数是小巧但功能强大的内联函数,可用于数据操作。它们对于小的操作非常有用,因为实现它们所需的代码很少。让我们再次看同一个示例,但这次我们将使用 Lambda 函数代替普通的 Python 函数:

  • Zip(): zip方法接受两个列表或可迭代对象,并在多个可迭代对象之间聚合元素。最后,它返回一个包含聚合的元组迭代器。让我们使用一个简单的代码zip_.py来演示这个函数:

  • Filter(): filter方法用于过滤出列表中满足特定条件的元素。filter方法接受两个参数,第一个是返回特定元素为truefalse的方法或 Lambda 函数,第二个是该元素所属的列表或可迭代对象。它返回一个包含条件评估为true的元素的列表。让我们创建一个名为filter_usage.py的文件,并添加以下内容:

摘要

在本章中,我们讨论了条件、循环、方法、迭代器、包、生成器和推导式。所有这些在 Python 中被广泛使用。我们之所以涵盖这些主题,是因为当我们进入后面的自动化渗透测试和网络安全测试用例时,我们将看到这些概念在我们的代码文件中被广泛使用。在下一章中,我们将探讨 Python 的面向对象特性。我们将探讨如何在 Python 中处理 XML、CSV 和 JSON 数据。我们还将了解有关文件、IO 和正则表达式的内容。

问题

  1. 举一个现实生活中使用生成器的用例。

  2. 我们可以将函数名称存储在变量中,然后通过变量调用它吗?

  3. 我们可以将模块名称存储在变量中吗?

进一步阅读

第三章:概念处理

这一章将使我们熟悉 Python 中的各种面向对象的概念。我们将看到 Python 不仅可以用作脚本语言,而且还支持各种面向对象的原则,并且因此可以用来设计可重用和可扩展的软件组件。此外,我们还将探讨正则表达式、文件和其他基于 I/O 的访问,包括 JSON、CSV 和 XML。最后,我们将讨论异常处理。在本章中,我们将涵盖以下主题:

  • Python 中的面向对象编程

  • 文件、目录和其他基于 I/O 的访问类型

  • Python 中的正则表达式

  • 使用 XML、JSON 和 CSV 数据进行数据操作和解析

  • 异常处理

Python 中的面向对象编程

任何编程语言的面向对象特性都教会我们如何处理类和对象。对于 Python 也是如此。我们将要涵盖的一般面向对象特性包括:

  • 类和对象

  • 类关系:继承、组合、关联和聚合

  • 抽象类

  • 多态

  • 静态、实例和类方法和变量

类和对象

可以被认为是一个包含了方法和变量定义的模板或蓝图,用于与该类的对象一起使用。对象只是类的一个实例,其中包含实际值而不是变量。一个类也可以被定义为对象的集合。

简单来说,一个类是变量和方法的集合。方法实际上定义了类执行的行为或操作,而变量是操作所针对的实体。在 Python 中,使用 class 关键字声明类,后跟类名。以下示例显示了如何声明一个基本的员工类,以及一些方法和操作。让我们创建一个名为Classes.py的 Python 脚本:

以下项目符号解释了前面的代码及其结构:

  • class Id_Generator():为了在 Python 中声明一个类,我们需要将其与 class 关键字关联起来,这就是我们在代码的第 2 行所做的。在等同缩进的情况下,Id_Generator类的内容是类的一部分。这个类的目的是为每个新创建的员工生成一个员工 ID。它使用generate()方法来实现这一点。

  • 每个 Python 或任何其他编程语言中的类都有一个构造函数。这要么是显式声明的,要么没有声明,隐式地采用默认构造函数。如果你来自使用 Java 或 C++的背景,你可能习惯于构造函数的名称与类名相同,但这并不总是这样。在 Python 中,类构造方法是使用__init__单词定义的,并且它总是以self作为参数。

  • selfself类似于关键字。在 Python 中,self表示类的当前实例,并且在 Python 中,每个类方法都必须将self作为其第一个参数。这也适用于构造函数。值得注意的是,在调用实例方法时,我们不需要显式地将类的实例作为参数传递;Python 会隐式地为我们处理这个问题。任何实例级变量都必须使用self关键字声明。这可以在构造函数中看到——我们已经声明了一个实例变量 ID 为self.id并将其初始化为0

  • def generate(self)generate是一个实例方法,它递增 ID 并返回递增的 ID。

  • class Employee()employee类是一个用于创建员工的类,它具有构造函数。它使用printDetails方法打印员工的详细信息。

  • def __init__(self,Name,id_gen):构造函数有两种类型——带参数和不带参数。任何带参数的构造函数都是带参数的构造函数。在这里,employee类的构造函数是带参数的,因为它接受两个参数:要创建的员工的姓名和Id_Generator类的实例。在这个方法中,我们只是调用了Id_Generator类的generate方法,它会返回员工 ID。构造函数还将传递给self类实例变量的员工姓名name进行初始化。它还将其他变量D_idSalary初始化为None

  • def printDetails(self):这是一个打印员工详细信息的方法。

  • 24-32 行:在代码的这一部分,我们首先创建了Id_Generator类的实例并命名为Id_gen。然后,我们创建了Employee类的一个实例。请记住,类的构造函数在我们创建类的实例时被调用。由于在这种情况下构造函数是带参数的,我们必须创建一个带两个参数的实例,第一个参数是员工姓名,第二个参数是Id_Generator类的实例。这就是我们在第 25 行所做的:emp1=Employee('Emp1',Id_gen)。正如前面提到的,我们不需要显式传递self;Python 会隐式处理这个问题。之后,我们为Emp1实例的SalaryD_id实例变量赋一些值。我们还创建了另一个名为Emp2的员工,如第 28 行所示。最后,我们通过调用emp1.printDetails()emp2.printDetails()来打印两个员工的详细信息。

类关系

面向对象编程语言最大的优势之一是代码重用。这种可重用性是由类之间存在的关系所支持的。面向对象编程通常支持四种关系:继承、关联、组合和聚合。所有这些关系都基于is-ahas-apart-of关系。

继承

类继承是一个功能,我们可以使用它来扩展类的功能,通过重用另一个类的能力。继承强烈促进了代码的重用。举个简单的继承例子,假设我们有一个Car类。车辆类的一般属性包括category(如 SUV、运动型、轿车或掀背车)、mileagecapacitybrand。现在假设我们有另一个名为Ferrari的类,除了普通汽车的特征外,还具有特定于跑车的额外特征,如HorsepowerTopspeedAccelerationPowerOutput。在这种情况下,我们在两个类之间建立了继承关系。这种关系是子类和基类之间的is-a关系。我们知道 Ferrari 是一辆车。在这种情况下,汽车是基类,Ferrari 是子类,它从父类继承了通用汽车属性,并具有自己的扩展特征。让我们扩展我们之前讨论的例子,我们创建了一个Employee类。现在我们将创建另一个名为Programmer的类,并看看如何在两者之间建立继承关系:

以下几点解释了前面的代码及其结构:

  • Class Programmer(Employee):在前面的情况下,我们创建了另一个名为Programmer的类,它继承自Employee基类。ProgrammerEmployee之间存在is a关系。除了Employee类的所有变量和方法外,Programmer类还定义了一些自己的变量和方法,如语言、数据库、项目和其他技能。

  • def __init__(self,name,id_gen,lang,db,projects,**add_skills)Programmer类的init方法接受一些自解释的参数。请注意(Employee类) super().__init__() 超类构造函数的调用,位于第 32 行。在其他高级语言如 Java 和 C++中,我们知道基类或超类构造函数会自动从子类构造函数中调用,当没有指定时,这是隐式从子类构造函数中执行的第一个语句。但在 Python 中并非如此。基类构造函数不会从子类构造函数中隐式调用,我们必须使用 super 关键字显式调用它,就像在第 32 行中看到的那样。

  • def printSkillDetails(self):这是帮助我们探索继承力量的方法。我们在这个方法中使用了基类的变量(iDnamesalary),以及一些特定于Programmer类的变量。这显示了如何使用继承来重用代码并得到一个是一个关系。

  • 第 52-62 行:最后,我们创建了Programmer类的一个实例并调用了printSkillDetails方法。

Python 中的访问修饰符

在 Python 中,我们没有像 Java 和 C++中那样的访问修饰符。然而,有一种部分解决方法可以用来指示哪些变量是公共的受保护的私有的。这里的指示一词很重要;Python 并不阻止使用受保护或私有成员,它只是指示成员是哪个。让我们看一个例子。创建一个名为AccessSpecifiers.py的类:

上面的例子向我们展示了如何在 Python 中使用访问限定符。在类中简单声明的任何变量默认为公共,就像我们声明的self.public一样。Python 中的受保护变量是通过在它们前面加下划线(_)来声明的,就像第 5 行中看到的self._protected一样。但必须注意的是,这并不能阻止任何人使用它们,就像在第 23 行中看到的那样,我们在类外部使用了受保护成员。Python 中的私有成员是通过在它们前面加双下划线(__)来声明的,就像第 6 行中看到的self.__private一样。然而,同样地,没有任何东西可以阻止这个成员在类外部被使用。然而,访问它们的方式有点不同;对于私有成员,如果它们要在类外部被访问,会遵循一个特定的约定:instance._<className><memberName>。这被称为名称修饰

我们在这里学到的关于 Python 中访问修饰符的知识是,Python 确实有符号来表示类的公共、私有和受保护成员,但它没有任何方式让成员在其范围之外被使用,因此这仅仅是用于标识目的。

组合

面向对象编程中的组合表示类之间的部分关系。在这种关系中,一个类是另一个类的一部分。让我们考虑以下示例Composition.py,以了解类之间的组合关系:

在上面的例子中,法拉利汽车和发动机之间的关系是组合类型。这是因为发动机是汽车的一部分,而汽车是法拉利类型的。

关联

关联关系维护了类的对象之间的拥有关系。拥有关系可以是一对一,也可以是一对多。在下面的例子中,我们可以看到EmployeeManager类之间存在一对一的关联关系,因为一个Employee只会有一个Manager类。我们还有一个EmployeeDepartment之间的一对一关联关系。这些关系的反向将是一对多的关系,因为一个Department类可能有很多员工,一个经理可能有很多员工报告给他们。以下代码片段描述了关联关系:

聚合

聚合关系是一种特殊的拥有关系,它总是单向的。它也被称为单向关联关系。例如,EmployeeAddress之间的关系是单向关联,因为员工总是有地址,但反过来并不总是成立。以下示例描述了EmployeeAddress之间的聚合关系:

抽象类

有许多情况下,我们可能希望部分实现一个类,使得该类通过模板定义其目标,并且还定义了它必须如何通过一些已实现的方法获取其目标的一部分。类目标的剩余部分可以留给子类来实现,这是强制性的。为了实现这样的用例,我们使用抽象类。抽象基类,通常称为abc类,是一个包含抽象方法的类。抽象方法是一种没有实现的方法。它只包含声明,并且应该在实现或继承抽象类的类中实现。

关于抽象类的一些重要要点包括以下内容:

  • 在 Python 中,抽象方法是使用@abstractmethod装饰器声明的。

  • 虽然抽象类可以包含抽象方法,但没有任何阻止抽象类同时拥有普通或非抽象方法的限制。

  • 抽象类不能被实例化。

  • 抽象类的子类必须实现基类的所有抽象方法。如果没有这样做,它就无法被实例化。

  • 如果抽象类的子类没有实现抽象方法,它将自动成为一个抽象类,然后可以由另一个类进一步扩展。

  • Python 中的抽象类是使用abc模块实现的。

让我们创建一个名为Abstract.py的类,并看看如何在 Python 中使用抽象类:

在上面的例子中,我们创建了一个名为QueueAbs的抽象类,它继承自名为ABC的抽象基类。该类有两个抽象方法,enqueuedequeue,还有一个名为printItems()的具体方法。然后,我们创建了一个名为Queue的类,它是QueueAbs抽象基类的子类,并实现了enqueuedequeue方法。最后,我们创建了Queue类的实例并调用了方法,如前所示。

值得记住的一件事是,在 Java 和 C#中,抽象类不能实现抽象方法。但在 Python 中不是这样。在 Python 中,抽象方法可能有默认实现,也可能没有,但这并不妨碍子类对其进行重写。无论抽象类方法是否有实现,子类都必须对其进行重写。

多态

多态性是指一个实体可以存在多种形式的属性。在编程方面,它指的是创建一个可以与多个对象或实体一起使用的结构或方法。在 Python 中,多态性可以通过以下方式实现:

  • 函数多态性

  • 类多态性(抽象类)

函数多态性

让我们考虑两个类,FerrariMcLaren。假设两者都有一个返回汽车最高速度的Speed()方法。让我们思考在这种情况下如何使用函数多态性。让我们创建一个名为Poly_functions.py的文件:

我们可以看到我们有两个类,FerrariMcLaren。两者都有一个打印两辆车速度的共同速度方法。一种方法是创建两个类的实例,并使用每个实例调用打印速度方法。另一种方法是创建一个接受类实例并在接收到的实例上调用速度方法的公共方法。这就是我们在第 10 行定义的多态printSpeed(carType)函数。

类多态性(抽象类)

也许有时我们希望根据类必须做什么来定义类的模板,而不是如何做到这一点 - 我们希望将这留给类的实现。这就是我们可以使用抽象类的地方。让我们创建一个名为Poly_class.py的脚本,并添加以下代码:

可以看到我们有一个名为Shape的抽象类,它有一个area方法。area方法在这个类中没有实现,但会在子类中实现。SquareCircle子类重写了area方法。area方法是多态的,这意味着如果一个正方形重写它,它实现了正方形的面积,当Circle类重写它时,它实现了圆的面积。

Python 中的静态、实例和类方法

在 Python 类中可以定义三种方法。到目前为止,我们大部分时间都在处理实例方法,我们已经使用我们的 Python 类实例调用了它们:

  • 实例方法和变量: 在 Python 类中定义的任何方法,使用类的实例调用,以 self 作为其第一个位置参数,被称为实例方法。实例方法能够访问类的实例变量和其他实例方法。使用self.__class__构造,它也能够访问类级别的变量和方法。另一方面,实例变量是在 Python 类中使用self关键字声明的任何变量。

  • 类方法和变量: 使用@classmethod Python 装饰器声明的任何方法都被称为类方法。类方法也可以在没有@classmethod装饰器的情况下声明。如果是这种情况,必须使用类名调用它。类方法只能访问在类级别标记或声明的变量,并且不能访问对象或实例级别的类变量。另一方面,类变量可以在任何方法之外声明。在类内部,我们必须在不使用 self 关键字的情况下声明变量。因此,类变量和方法在某种程度上类似于我们在 Java 中学习的静态方法和变量,但有一个陷阱,如下所述:

在 Java 和 C#中,我们知道静态变量不能通过类的实例访问。在 Python 中,静态变量是类级变量,实际上可以通过类的实例访问。但是访问是只读访问,这意味着每当使用类的实例访问类级变量并且实例尝试修改或更新它时,Python 会自动创建一个同名的变量副本并将其分配给类的这个实例。这意味着下次使用相同实例访问变量时,它将隐藏类级变量,并提供对新创建的实例级副本的访问。

  • 静态方法: 在 Python 类中使用@staticmethod装饰器声明的任何方法都被称为静态方法。Python 中的静态方法与我们在 Java 和 C#中看到的不同。静态级别的方法无法访问实例或对象级别的变量,也无法访问类级别的变量。

让我们以一个名为Class_methods.py的示例来进一步解释:

以下是前面代码的延续:

前面的代码片段解释了静态、实例和类方法的用法。每当类方法由类的实例调用时,Python 会在内部自动将实例类型转换为类类型,这可以在第 42 行看到。

输出如下截图所示:

文件、目录和 I/O 访问

与其他编程语言一样,Python 提供了一个强大且易于使用的接口来处理 I/O、文件和目录。我们将在接下来的章节中更详细地探讨这些内容。

文件访问和操作

我们可以在 Python 中读取、写入和更新文件。Python 有一个open结构,可以用来提供文件操作。当我们打开一个文件时,可以以各种模式打开该文件,如下所示:

  • r:读取模式,这以文本模式读取文件(默认)。

  • rb:这以二进制模式读取文件。

  • r+:这以读取和写入模式读取文件。

  • rb:这以二进制模式打开文件进行读取和写入。

  • w:这仅以写入模式打开文件。它会覆盖现有文件。

  • wb:这以二进制模式打开文件进行写入。它会覆盖现有文件。

  • w+:这以写入和读取模式打开文件。它会覆盖现有文件。

  • wb+:这以二进制模式打开文件进行读取和写入。它会覆盖现有文件。

  • a:这以追加模式打开文件,并在文件不存在时创建文件。

  • ab:这以追加二进制模式打开文件,并在文件不存在时创建文件。

  • a+:这以追加和读取模式打开文件,并在文件不存在时创建文件。

  • ab+:这以追加读取二进制模式打开文件,并在文件不存在时创建文件。

在以下代码块中,open方法调用的第一个参数是要打开的文件的路径。第二个参数是文件打开的mode,第三个是可选的缓冲参数,指定文件的期望buffer大小:0表示无缓冲,1表示行缓冲,任何其他正值表示使用大约该大小的缓冲(以字节为单位)。负缓冲表示应使用系统默认值。对于 tty 设备,通常是行缓冲,对于其他文件,通常是完全缓冲。如果省略,将使用系统默认值。

open("filepath","mode",buffer)

通过缓冲,我们不是直接从操作系统的原始文件表示中读取(这样会有很高的延迟),而是将文件读入操作系统缓冲区,然后从那里读取。这样做的好处是,如果我们有一个文件存在于共享网络上,并且我们的目标是每 10 毫秒读取一次文件。我们可以将其加载到缓冲区中,然后从那里读取,而不是每次都从网络中读取,这将是昂贵的。

看一下File_access.py文件中的以下片段以了解更多:

前面截图中的代码片段来自File_access.py文件,解释了如何在 Python 中使用文件。File类的read()方法接受文件路径,如果没有给出整个路径,则假定当前工作目录是起始路径。在文件实例上调用的read()方法将整个文件读入程序变量。read(20)将从当前文件指针位置加载 20 个字节的文件。当我们处理大文件时,这非常方便。

readlines()方法返回一个列表,每个条目都指向文件的一行。readline()方法返回文件的当前行。seek()方法将文件指针移到参数中指定的位置。因此,每当我们执行seek(0)时,文件指针都会指向文件的开头:

重命名和删除文件以及访问目录

在 Python 中,对文件目录和各种其他操作系统命令的系统级访问是由os模块提供的。os模块是一个非常强大的实用程序。在本节中,我们将看到它在重命名、删除、创建和访问目录方面的一些用法,借助os_directories.py文件中的以下片段:

前面截图中的代码片段展示了在 Python 中使用os模块与文件和目录一起使用的各种方式,以重命名和删除文件以及创建和更改目录。它还向我们展示了如何重命名和遍历所有文件(包括嵌套文件)从一个子文件夹。需要注意的是,如果我们想要删除一个文件夹,我们可以使用os.rmdir()方法,但是文件夹中的所有文件都应该被显式删除才能使其工作:

  • 以下输出显示了文件在创建前后的变化:

  • 以下输出显示了文件名的更改:

  • 以下输出显示了文件被删除后的变化:

控制台 I/O

到目前为止,我们已经处理了大部分以硬编码数据作为输入的 Python 程序。让我们看看如何在 Python 中从用户那里获取输入并在我们的代码中使用。我们将创建一个名为user_input.py的文件:

这是相当不言自明的。为了获取用户输入,我们使用input()方法,它会暂停屏幕,直到用户提供输入。它总是返回一个字符串:

Python 中的正则表达式

正则表达式非常强大,在网络安全领域被广泛用于模式匹配,无论是处理解析日志文件、Qualys 或 Nessus 报告,还是 Metasploit、NSE 或任何其他服务扫描或利用脚本生成的输出。Python 中提供对正则表达式支持的模块是re。我们将使用 Python 正则表达式(re模块)的一些重要方法,如下所述:

match()这确定正则表达式是否在字符串开头找到匹配项re.match(pattern,string,Flag=0)。标志可以用&#124;或操作符指定。最常用的标志是re.Ignore-Casere.Multilinere.DOTALL。这些标志可以用或操作符指定为(re.M&#124; re.I)。
search()与 match 不同,search 不仅在字符串开头寻找匹配项,而是在整个字符串中搜索或遍历以寻找给定的搜索字符串/正则表达式,可以指定为re.search(pattern,string,Flag=0)
findall()这在字符串中搜索正则表达式匹配项,并返回所有子字符串作为列表,无论它在哪里找到匹配项。
group()如果找到匹配项,则group()返回正则表达式匹配的字符串。
start()如果找到匹配项,则start()返回匹配项的起始位置。
end()如果找到匹配项,则end()返回匹配项的结束位置。
span()如果找到匹配项,则span()返回一个包含匹配项的起始和结束位置的元组。
split()这根据正则表达式匹配来拆分字符串,并返回一个列表。
sub()这用于字符串替换。它会替换所有子字符串的匹配项。如果找不到匹配项,则返回一个新字符串。
subn()这用于字符串替换。它会替换所有子字符串的匹配项。返回类型是一个元组,新字符串在索引 0 处,替换的数量在索引 1 处。

现在我们将尝试通过regular_expressions.py脚本中的以下片段来理解正则表达式:

matchsearch之间的区别在于,match只在字符串开头搜索模式,而search则在整个输入字符串中搜索。代码行 42 和 50 产生的输出将说明这一点:

在前面的屏幕中,可以看到当输入Hello时,matchsearch都能够定位到字符串。然而,当输入为\d时,表示任何十进制数,match无法定位,但search可以。这是因为search方法在整个字符串中搜索,而不仅仅是开头。

同样,可以从以下截图中看到,match没有返回数字和非数字的分组,但search有。

在以下输出中,搜索了Reg关键字,因此matchsearch都返回了结果:

注意,在下面的截图中,findall()matchsearch不同:

这些例子已经展示了match()search()的不同操作方式,以及search()在执行搜索操作时更加强大:

让我们来看一下 Python 中一些重要的正则表达式:

正则表达式描述
\d这匹配字符串中的零到九的数字。
(\D\d)这匹配了\D非数字和\d数字,它们被分组在一起。括号(())用于分组。
.*string.*如果在字符串中找到一个单词,不管它前面和后面是什么,都会返回一个匹配项。.*符号表示任何东西。
^尖号符号表示它匹配字符串的开头。
[a-zA-Z0-9][...]用于匹配放在大括号内的任何内容。例如,[12345]表示应该找到介于一和五之间的任何数字的匹配项。[a-zA-Z0-9]表示应该将所有字母数字字符视为匹配项。
\w\w[a-zA-Z0-9_]相同,匹配所有字母数字字符。
\W\W\w的否定形式,匹配所有非字母数字字符。
\D\D\d的否定形式,匹配所有不是数字的字符。
[^a-z]^,当放置在[]内时,作为否定形式。在这种情况下,它意味着匹配除了az之间的字母以外的任何内容。
re{n}这意味着精确匹配前面表达式的n次出现。
re{n ,}这意味着匹配前面表达式的n次或更多次出现。
re {n,m}这意味着匹配前面表达式的最少n次和最多m次出现。
\s这意味着匹配空格字符。
[T&#124;t]est这意味着匹配Testtest
re*这意味着匹配*后面的表达式的任何出现。
re?这意味着匹配?后面的表达式的任何出现。
re+这意味着匹配+后面的表达式的任何出现。

使用 XML、JSON 和 CSV 数据进行数据操作和解析

在本节中,我们将首先看看如何在 Python 中操作 XML 数据,然后看看如何操作 JSON 数据。之后,我们将重点介绍 CSV 的 pandas Python 实用程序。

XML 数据操作

在本节中,我们将看看如何在 Python 中操作 XML 数据。虽然有许多方法可以解析 Python 中的 XML 文档,但简单且最常用的方法是使用XML.etree模块。让我们看看以下示例,它将说明在 Python 中解析 XML 文档和字符串是多么简单和容易。创建一个名为xml_parser.py的脚本。我们将使用一个名为exmployees.xml的 XML 文档:

正如前面的例子中所示,我们简单地使用xml.etree.ElementTree模块,并将其别名为 ET。在类的解析方法中,我们通过调用parse方法(在前一种情况下)或fromstring方法(在后一种情况下)来提取 XML 文档或 XML 字符串的根。这将返回<class 'xml.etree.ElementTree.Element'> ET 元素类的实例。我们可以遍历它以获取所有子节点,如从第 21 行到第 26 行所示。如果我们不知道节点的属性名称,类的attrib属性返回一个字典,其中包含属性名称和其值的键值映射。如果我们知道子节点的名称,我们可以遵循第二种方法,如从第 29 行到第 36 行所示,其中我们指定节点的名称。

如果我们传递的是 XML 字符串而不是文件,则唯一的变化在于初始化根元素的方式;其余部分保持不变。关于此脚本的另一点要注意的是,我们使用了命令行参数。sys.argv[]用于访问这些命令行参数,文件的 0 索引具有脚本本身的名称,从索引 1 开始是参数。在我们的示例中,XML 文件的名称作为命令行参数传递给脚本,并使用sys.argv[1]属性进行访问。如下所示:

JSON 数据操作

现在让我们看看如何使用 Python 操作 JSON 数据。 JSON(JavaScript 对象表示)是一种非常广泛使用的数据存储和交换格式。随着互联网的成熟,它变得越来越受欢迎,并成为基于 REST 的 API 或服务中信息交换的标准。

Python 为我们提供了一个用于 JSON 数据操作的 JSON 模块。让我们创建一个名为employees.json的 JSON 文件,并查看如何使用 JSON 模块访问 JSON 内容。假设我们的目标是读取员工数据,然后找出工资超过 30,000 的员工,并用A级标记他们。然后我们将那些工资低于 30,000 的员工标记为B级:

获得的输出如下截图所示:

从前面的代码可以推断出,JSON 文件被加载为 Python 字典,可以通过json.load()命令实现。load()方法期望提供 JSON 文件路径作为参数。如果 JSON 数据不是作为外部文件而是作为 Python 字符串存在,我们可以使用json.loads()方法,并将 JSON 字符串作为参数传递。这将再次将字符串转换为 Python 本机类型,可能是列表或字典。如下所示:

>>> a='{"k1":"v1"}'
>>> d=json.loads(a)
>>> type(d)
<class 'dict'

json_parse.py文件中,第 10 到 20 行简单地迭代 Python 字典和内部列表,并显示员工详细信息。这是我们之前见过的。脚本的目标实际上是更新员工的工资档次,这是在process()方法中实现的。我们再次打开并加载 JSON 文件到 Python 本机类型(第 23 行)。然后,我们迭代 Python 字典。在第 27 行,我们检查员工的工资是否大于或等于 30,000。如果是,我们修改员工的档次,通过修改加载所有详细信息的原始json_data对象。json_data["employees"]["data"][index]["slab"]语句将指向当前员工的档次,确定他们的工资是多还是少于 30,000,并将其设置为AB。最后,我们将在json_data对象中得到修改后的员工详细信息,并使用json.dump()方法覆盖原始 JSON 文件的内容。这将把 Python 本机对象(列表、字典或元组)转换为其 JSON 等效形式。它将file_object作为第二个参数,指示 JSON 数据必须放在哪里。它还接受格式选项,如indentsort_keys等。同样,我们还有一个json.dumps()方法,它将 Python 本机类型转换为其 JSON 字符串等效形式。如下所示:

>>> json.dumps({"k1":"v1"})
'{"k1": "v1"}'

应该记住,外部 JSON 文件不能在原地修改。换句话说,我们不能修改外部 JSON 文件的一部分,然后保持其余部分不变。在这种情况下,我们需要用新内容覆盖整个文件。

CSV

CSV 数据在网络安全和数据科学领域被广泛使用,无论是作为日志文件的形式,作为 Nessus 或 Qualys 报告的输出(以 Excel 格式),还是用于机器学习的大型数据集。Python 提供了内置的 CSV 模块对 CSV 文件提供了出色的支持。在本节中,我们将探讨这个模块,并关注 CSV 的 pandas Python 实用程序。

让我们首先看一下 Python 提供的内置 CSV 模块。下面的代码片段,名为csv_parser.py,演示了这个模块:

前面的代码帮助我们了解如何使用 CSV 模块在 Python 中读取 CSV 文件。建议始终使用 CSV 模块,因为它内部处理分隔符、换行符和字符。有两种从 CSV 文件中读取数据的方法,第一种是使用csv.reader()方法(第 10-25 行),它返回一个 CSV 字符串列表。列表的每一行或项将是表示 CSV 文件一行的字符串列表,可以通过索引访问每个项。另一种读取 CSV 文件的方法是使用csv.DictReader()(第 29-38 行),它返回一个字典列表。每个字典将具有一个键值对,键表示 CSV 列,值是实际的行值。

产生的输出如下所示:

为了写入 CSV 文件,有两种不同的方法。一种方法是使用csv.DictWriter()指令,它返回一个 writer 对象,并且具有将 Python 列表或字典直接推送到 CSV 文件的能力。当我们在列表或字典上调用writerows()方法时,这将在内部将 Python 列表或字典转换为 CSV 格式。这在第 40-53 行中展示:我们检查员工的薪水,将适当的分级与之关联,最后使用writerows()方法覆盖修改后的 CSV 文件。csv.DictWriter()支持writerows()write row()方法。writerows()方法将简单地获取一个字典并将其写入 CSV 文件。

写入 CSV 文件的第二种方法是使用csv.Writer()方法。这将返回一个 writer 对象,该对象将以列表的形式作为writerows()方法的参数,并将结构写入外部 CSV 文件。这两种方法的示例如下屏幕截图所示:

虽然前面介绍的访问和处理 CSV 文件的方法很好,但如果 CSV 文件非常大,这些方法就不适用了。如果 CSV 文件大小为 10GB,系统的 RAM 只有 4GB,那么csv.reader()csv.DictReader()都无法很好地工作。这是因为reader()DictReader()都会将外部 CSV 文件完全读入变量程序内存中,也就是 RAM。对于一个巨大的文件,直接使用 CSV 模块是不可取的。

另一种方法是使用迭代器或按字节块读取文件,如下面的屏幕截图所示:

前面的代码片段不会将整个文件加载到内存中,而是一次读取一行。这样,我们可以处理和存储该行到数据库中,或执行任何相关操作。由于文件是逐行读取的,如果我们有多行的 CSV 数据,这将会造成麻烦。正如我们在前面的示例中看到的,Emp1的第一条记录没有完全读取;它被分成两行,第二行只包含Description字段的一部分。这意味着以前的方法对于大型或多行的 CSV 文件是行不通的。

如果我们试图按块或字节来读取,就像我们之前看到的那样,我们将不知道多少块或字节对应于一行,因此这也会导致不一致的结果。为了解决这个问题,我们将使用 Pandas,这是一个强大的 Python 数据分析工具包。

有关 Pandas 的详细信息,请参阅以下链接:pandas.pydata.org/pandas-docs/stable/

首先,我们需要安装 pandas,可以按照以下步骤进行:

pip3.5 install pandas

以下代码片段解释了如何使用 pandas 以小块读取巨大的 CSV 文件,从而减少内存使用:

如前面的代码片段所示,我们声明块大小为 100,000 条记录,假设我们有一个非常大的 CSV 文件要处理。块大小是上限;如果实际记录少于块大小,程序将只获取两者中较小的值。然后,我们使用pd.read_csv()加载 CSV 文件,指定块大小作为参数。chunk.rename()方法实际上会从列名中删除换行符(如果有的话),chunk.fillna('')将填充 CSV 文件返回的空值。最后,我们使用iterrows()方法迭代行,该方法返回一个元组,然后按照所示打印值。应该注意的是,pd.read_csv()返回一个 pandas DataFrame,可以被视为内存中的关系表。

异常处理

异常,我们都知道,是意想不到的条件。它们可能在运行时出现并导致程序崩溃。因此,建议将可疑代码(可能导致异常)放在异常处理代码块中。然后,即使发生异常,我们的代码也会适当地处理它并采取所需的操作。与 Java 和 C#一样,Python 也支持用于处理异常的传统 try 和 catch 块。然而,有一个小改变,就是 Python 中的 catch 块被称为 except。

以下代码片段显示了我们如何在 Python 中进行基本的异常处理:

前面的代码是不言自明的。Python 使用tryexcept,而不是trycatch。我们使用raise命令来手动抛出异常。最终块的工作方式与其他语言相同,核心条件是无论异常是否发生,最终块都应该被执行。

应该注意,在前面的例子中,我们在 except 块中处理异常时使用了一个通用的 Exception 类。如果我们确定代码可能引发什么样的异常,我们可以使用特定的异常处理程序,比如IOErrorImportErrorValueErrorKeyboardINteruptEOFError。最后,还应该记住,在 Python 中,我们可以在try块旁边使用一个 else 块。

摘要

在本章中,我们讨论了 Python 的 OOP、文件、目录、IO、XML、JSON、CSV 和异常处理。这些是 Python 的核心构造,被广泛使用。当我们转向使用 Python 实现渗透测试和网络安全时,我们将经常使用所有这些结构和概念,因此我们对它们有很好的理解是很重要的。在下一章中,我们将讨论更高级的概念,如 Python 中的多线程、多进程、子进程和套接字编程。通过那一章,我们将完成对 Python 先决条件的探索,这将进而引导我们学习有关 Python 的渗透测试和网络安全生态系统。

问题

  1. 我们经常听说 Python 是一种脚本语言。将其用作面向对象的语言的典型优势是什么?你能想到任何特定的产品或用例吗?

  2. 列举一些解析 XML 和 CSV 文件的方法。

  3. 我们能否在不看类结构的情况下检测到类的所有属性?

  4. 什么是方法装饰器?

进一步阅读

第四章:高级 Python 模块

本章将使我们熟悉一些高级 Python 模块,当涉及到响应时间、处理速度、互操作性和通过网络发送数据等参数时非常有用。我们将研究如何使用线程和进程在 Python 中进行并行处理。我们还将了解如何使用 IPC 和子进程在进程之间建立通信。之后,我们将探讨 Python 中的套接字编程,并通过实现反向 TCP shell 进入网络安全领域。本章将涵盖以下主题:

  • 使用线程进行多任务处理

  • 使用进程进行多任务处理

  • 子进程

  • 套接字编程的基础

  • 使用 Python 实现反向 TCP shell

使用线程进行多任务处理

线程是一个轻量级的进程,与其父进程共享相同的地址和内存空间。它在处理器核心上并行运行,从而为我们提供了并行性和多任务处理能力。它与父进程共享相同的地址和内存空间的事实使得整个多任务处理操作非常轻量级,因为没有涉及上下文切换开销。在上下文切换中,当调度新进程以执行时,操作系统需要保存前一个进程的状态,包括进程 ID、指令指针、返回地址等。

这是一个耗时的活动。由于使用线程进行多任务处理不涉及创建新进程来实现并行性,线程在多任务处理活动中提供了非常好的性能。就像在 Java 中我们有Thread类或可运行接口来实现线程一样,在 Python 中我们可以使用Thread模块来实现线程。通常有两种在 Python 中实现线程的方法:一种是 Java 风格的,另一种更符合 Python 的风格。让我们一起来看看这两种方法。

以下代码显示了类似于 Java 的实现,我们在其中对线程类进行子类化并覆盖run()方法。我们将希望与线程并行运行的逻辑或任务放在run()方法内:

import threading
>>> class a(threading.Thread):
... def __init__(self):
... threading.Thread.__init__(self)
... def run(self):
... print("Thread started")
... 
>>> obj=a()
>>> obj.start()
Thread started

在这里,我们有一个方法(run()),在这种情况下,它被设计为并行执行。这就是 Python 探索的另一种线程方法,在这种方法中,我们可以使用线程使任何方法并行执行。我们可以使用我们选择的任何方法,该方法可以接受任何参数。

以下代码片段显示了使用线程的另一种方式。在这里,我们可以看到我们通常定义了一个add(num1,num2)方法,然后在线程中使用它:

>>> import threading
>>> def add(num1,num2):
...     print(num1 + num2)
... 
>>> for i in range(5):
...     t=threading.Thread(target=add,args=(i,i+1))
...     t.start()
... 
1
3
5
7
9

for循环创建了一个线程对象t。在调用start()方法时,会调用在创建线程对象时指定的目标参数中的方法。在前面的例子中,我们将add()方法传递给了线程实例。要传递给使用线程调用的方法的参数作为元组传递给args参数。add()方法通过线程调用了五次,并且输出显示在屏幕上,如前面的例子所示。

恶魔线程和非恶魔线程

必须注意的是,线程是从主程序中调用的,主程序不会退出(默认情况下)直到线程完全执行。原因是主程序默认以非恶魔模式调用线程,这使得线程在前台运行,而不是等待它在后台运行。因此,非恶魔线程是在前台运行的,导致主程序等待运行线程完成执行。另一方面,恶魔线程是在后台运行的,因此不会导致主程序等待其完成执行。请看下面的例子:

从前面的代码片段可以看出,当我们创建和执行一个非恶魔线程(默认情况下)时,在打印Main Ended后,终端窗口会暂停 4 秒,等待ND线程完成执行。当它完成时,我们会得到一个Exit Non Demonic的消息,这时主程序退出。在此之前,主程序不会退出。

让我们看看这在恶魔线程中如何改变,它在后台运行:

在前面的代码片段中,我们看到了如何使用一个恶魔线程。有趣的是,主程序并没有等待恶魔线程完成执行。恶魔线程在后台运行,当它完成时,主线程已经从内存中退出,因此我们没有在屏幕上看到Exit: Daemonic的消息。在这种情况下,我们正在使用日志记录模块。默认情况下,日志记录模块将记录到stdout,在我们的情况下,这是终端。

线程加入和枚举

正如我们在前一节中看到的,主线程默认情况下会等待线程执行。尽管如此,主方法的代码仍将被执行,因为主线程将在不同的处理器核心上运行,与子线程不同。有时我们可能希望控制主线程的执行,与子线程的执行周期一致。假设我们希望在子线程执行后仅执行主线程的一部分代码。这可以通过join()方法实现。如果我们在主线程 M 的第 X 行调用线程 T 上的join(),那么主线程的 X+1 行将在 T 线程完成执行之前不会被执行。换句话说,我们将主线程的尾部与线程 T 连接起来,因此主线程的执行将在 T 完成之前暂停。让我们看下面的例子,我们在其中使用线程枚举和join()来批量执行线程。

主程序必须在退出之前验证所有线程是否已执行:

以下截图描述了前面代码的输出:

线程之间的互通

尽管线程应该独立执行,但有许多情况下需要线程之间进行通信,例如如果一个线程需要在另一个线程达到某个特定点时才开始任务。假设我们正在处理生产者和消费者问题,其中一个线程(生产者)负责将项目放入队列。生产者线程需要向消费者线程发送消息,以便它知道可以从队列中消费数据。这可以通过 Python 中的线程事件来实现。调用threading.event()返回一个事件实例,可以使用set()方法设置,使用clear()方法重置。

在下面的代码块中,我们将看到一个示例,其中一个线程将递增一个计数器。另一个线程需要在计数器值达到 5 时执行一个动作。必须注意,事件还有一个wait()方法,它会等待事件被阻塞或设置。事件可以等待一个超时间隔,或者可以无限期等待,但一旦设置标志为truewait()方法将不会实际阻塞线程的执行。这在下面的代码中有所体现:

线程并发控制

有许多情况下,多个线程需要共享资源。我们希望确保如果一个线程正在改变对象的状态,另一个线程必须等待。为了避免不一致的结果,在改变其状态之前必须锁定共享资源。状态改变后,应释放锁。Python 提供了线程锁来做到这一点。看一下下面的代码片段Thread_locking.py,它演示了线程锁定和并发控制:

前面的代码片段显示了线程锁定。在这里,count是一个多个线程尝试更新的共享变量。第一个输出没有锁定机制(第 16 行和第 22 行被注释掉)。当没有锁定时,可以看到thread_3在获取锁时将值读为 1,thread_4也是一样。每个线程将计数的值增加 1,但到thread_4结束时,计数的值为 3。当我们使用锁定时,可以从第二个输出中看到,当共享资源counter被更新时,没有其他线程实际上可以读取它,因此获得的结果是一致的。

使用进程进行多任务处理

与线程模块一样,多进程模块也用于提供多任务处理能力。线程模块实际上有点误导:它在 Python 中的实现实际上不是用于并行处理,而是用于在单个核心上进行时间共享的处理。默认的 Python 实现CPython在解释器级别上不是线程安全的。每当使用线程时,都会在 Python 线程中访问的对象上放置一个全局解释器锁GIL)。这个锁以时间共享的方式执行线程,给每个线程一小部分时间,因此我们的程序中没有性能增益。因此,多进程模块被开发出来,以提供并行处理给 Python 生态系统。这通过将负载分布到多个处理器核心上来减少执行时间。看一下下面的代码,它使用了多进程:

>>> import multiprocessing
>>> def process_me(id):
... print("Process " +str(id))
... 
>>> for i in range(5):
... p=multiprocessing.Process(target=process_me,args=(i,))
... p.start()
>>> Process 0
>>> Process 1
>>> Process 2
>>> Process 3
>>> Process 4
import multiprocessing as mp
>>> class a(mp.Process):
... def __init__(self):
... threading.Thread.__init__(self)
... def run(self):
... print("Process started")
... 
>>> obj=a()
>>> obj.start()
Process started

前面的代码片段表示了两种多进程的实现:一种简单的方法和一种基于类的方法。

恶魔和非恶魔进程

我们已经学习了什么是恶魔和非恶魔线程。同样的原则也适用于进程。恶魔进程在后台运行而不阻塞主进程,而非恶魔进程在前台运行。这在下面的示例中显示出来:

可以从前面的代码片段中看到,当我们创建和执行一个非恶魔进程(默认选项)时,如输出 1 和第 20 行所示,在打印Main Ended后,终端窗口会在等待非恶魔进程完成执行时停顿 4 秒。当它完成时,我们会得到Exit Non Daemonic的消息,这时主程序退出。在第二种情况下(如输出 2 所示),主程序不会等待恶魔进程完成执行。恶魔进程在后台运行,当它完成时,主线程已经从内存中退出。因此,我们没有在屏幕上看到Exit :Daemonic的消息打印出来。

进程连接、枚举和终止

关于线程连接和枚举的相同理论也可以应用于进程。进程可以连接到主线程或另一个进程,以便另一个线程在连接的进程完成之前不会退出。除了连接和枚举之外,我们还可以在 Python 中显式终止进程。

看一下以下代码片段,演示了上述概念。以下代码的目标是生成一些进程,并使主进程等待 10 秒,以便生成的进程完成执行。如果它们没有完成,那些仍在运行的进程将在退出之前被终止:

上述代码Join_enumerate_terminate.py非常简单;我们所做的与之前的线程相同。这里唯一的区别是我们仅应用了 3 秒的加入操作,以便故意获得一些仍在运行的进程。然后我们通过对它们应用terminate()来终止这些进程。

多进程池

多进程库中最酷的功能之一是池化。这让我们可以将任务均匀分配到所有处理器核心上,而不必担心同时运行的进程数量。这意味着该模块有能力批量生成一组进程。假设我们将批处理大小定义为 4,这是我们可能拥有的处理器核心数量。这意味着,任何时候,可以执行的最大进程数量为四个,如果其中一个进程完成执行,也就是说现在有三个正在运行的进程,模块会自动选择下一组进程,使批处理大小再次等于四。该过程将持续进行,直到我们完成分布式任务或明确定义条件。

看一下以下示例,我们需要在八个不同的文件中写入 800 万条记录(每个文件中有 100 万条记录)。我们有一个四核处理器来执行此任务。理想情况下,我们需要两次生成一个批处理大小为四的进程,以便每个进程在文件中写入 100 万条记录。由于我们有四个核心,我们希望每个核心执行我们任务的不同部分。如果我们选择一次生成八个进程,我们将在上下文切换中浪费一些时间,因此我们需要明智地使用我们的处理器和处理能力,以获得最大的吞吐量:

在上述代码Multiprocess_pool.py中,我们在第 30 行创建了一个多进程池。我们将池的大小定义为size=mp.cpu_count(),在我们的情况下是4,因此我们定义了一个大小为四的池。我们需要创建八个文件,每个文件包含 100 万条记录。我们使用for循环来定义八个进程,这些进程将通过在创建的池对象上调用apply_async()来发送到池对象。apply_async()方法期望我们希望使用多进程模块作为参数执行的方法的名称。第二个参数是传递给我们希望执行的方法的参数。请注意,当使用池模块执行进程时,进程还具有从方法返回数据的能力。

从输出中可以看到,没有同时执行的进程超过四个。还可以验证,第一个完成的进程是Forkpoolworker4。当批处理大小为 3 时,模块会立即生成另一个进程。这可以通过输出来验证,输出中在第一部分的第六行中声明了Started process Poolworker4

请注意,两个批次是并行执行的。每个进程花费了 13 到 14 秒,但由于它们是并行执行的,每个批次的整体执行时间为 14 秒。因此,两个批次的总时间为 28 秒。很明显,通过使用并行处理,我们在短短 28 秒内解决了问题。如果我们选择顺序或线程方法,总时间将接近*(138) = 104秒。作为练习,您可以自己尝试。

现在让我们举另一个例子,展示池模块的另一个强大功能。假设作为我们的要求的一部分,我们需要解析创建的 800 万个文件中的四个文件,其 ID%1700的结果为零。然后我们必须将所有四个文件的结果合并到另一个文件中。这是分布式处理和结果聚合的一个很好的例子:这些进程不仅应该并行读取文件,还必须聚合结果。这与 Hadoop 的映射-减少问题有些类似。在典型的映射-减少问题中,有两组操作:

  • 映射:这涉及将一个巨大的数据集分布在分布式系统的各个节点上。每个节点处理它接收到的数据块。

  • 减少:这是聚合操作,其中来自每个节点的映射阶段的输出被返回,并且根据逻辑,最终聚合并返回结果。

我们在这里做的是相同的事情,唯一的区别是我们使用处理器核心代替节点:

如前面的代码片段所示,借助Pool模块的map()方法,我们可以让多个进程并行处理不同的文件,然后将所有结果组合并作为单个结构发送。这些进程是并行执行的,对于record_id%1700返回零的记录将被返回。最后,我们将聚合结果保存在Modulo_1700_agg文件中。这是多进程模块的一个非常强大的特性,如果使用正确,可以大大减少处理时间和聚合时间。

子进程

从另一个进程调用外部进程称为子处理。在这种情况下,进程之间的通信是通过操作系统管道进行的。换句话说,如果进程 A 被进程 B 作为子进程调用,那么进程 B 可以通过操作系统管道向其传递输入,也可以通过操作系统管道从中读取输出。在自动化渗透测试和使用 Python 调用其他工具和实用程序时,该模块至关重要。Python 提供了一个非常强大的模块,称为subprocess来处理子处理。看一下下面的代码片段Subprocessing.py,它展示了如何使用子处理来调用一个名为ls的系统命令:

在前面的代码片段中,我们使用了subprocess.Popen()方法来调用subprocess。还有一些其他调用或调用subprocess的方法,比如call(),但我们在这里讨论的是Popen。这是因为Popen方法返回将要生成的进程的进程 ID,从而使我们对该进程有很好的控制。Popen方法接受许多参数,其中第一个实际上是要在操作系统级别执行的命令。命名参数包括stderr=subprocess.PIPE,这意味着如果外部程序或脚本产生错误,该错误必须重定向到操作系统管道,父进程必须从中读取错误。stdout=subprocess.PIPE表示子进程产生的输出也必须发送到管道到父进程。shell=True表示无论给出什么命令,第一个参数都必须被视为shell命令,如果有一些参数,它们必须作为要调用的进程的参数传递。最后,如果我们希望父进程读取子进程产生的输出和错误,我们必须在调用的subprocess上调用communicate()方法。communicate()方法打开subprocess管道,通信从子进程向管道的一端写入开始,父进程从另一端读取。必须注意communicate()方法将使父进程等待子进程完成。该方法返回一个元组,其中 0 号索引处是输出,1 号索引处是标准错误。

应该注意的是,我们在现实世界的示例中不应该使用shell=True,因为这会使应用程序容易受到 shell 注入的攻击。避免使用以下行:

>>> subprocess.Popen(command, shell=True) #这将删除所有内容!!

看一下以下示例,我们将使用shell=False。使用shell=False,我们调用的进程/命令的命令和参数必须作为列表分开传递。让我们尝试使用shell=False执行ls -l

这就是我们如何使用 Python 执行外部进程的方式,借助于 subprocess 模块。

套接字编程基础

当我们谈论套接字时,我们指的是 TCP 套接字和 UDP 套接字。套接字连接只是 IP 地址和端口号的组合。我们可以想到的每个在端口上运行的服务都在内部实现和使用套接字。

例如,我们的 Web 服务器总是在端口80(默认情况下)上监听,它打开一个套接字连接到外部世界,并绑定到具有 IP 地址和端口80的套接字。套接字连接可以以以下两种模式使用:

  • 服务器

  • 客户端

当套接字用作服务器时,服务器执行的步骤顺序如下:

  1. 创建一个套接字。

  2. 绑定到套接字。

  3. 在套接字上监听。

  4. 接受连接。

  5. 接收和发送数据。

另一方面,当套接字连接用作客户端连接到服务器套接字时,步骤顺序如下:

  1. 创建一个套接字。

  2. 连接到套接字。

  3. 接收和发送数据。

看一下以下代码片段server_socket.py,它在端口80实现了一个 TCP 服务器套接字:

在前面的案例中,我们使用socket.socket语句创建了一个套接字。在这里,socket.AF_INET表示 IPv4 协议,socket.SOCK_STREAM表示使用基于流的套接字数据包,这些数据包仅是 TCP 流。bind()方法以元组作为参数,第一个参数是本地 IP 地址。您应该将其替换为您的个人 IP,或127.0.0.1。传递给元组的第二个参数是端口,然后调用bind()方法。然后我们开始监听套接字,最后开始一个循环,我们接受客户端连接。请注意,该方法创建了一个单线程服务器,这意味着如果任何其他客户端连接,它必须等到活动客户端断开连接。send()recv()方法是不言自明的。

现在让我们创建一个基本的客户端套接字代码client_socket.py,连接到之前创建的服务器并向其传递消息:

客户端和服务器套接字产生的输出如下:

这是我们如何使用 UDP 进行套接字连接的方式:

sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

使用 Python 进行反向 TCP shell

现在我们已经了解了子进程、多进程等基础知识,使用 Python 实现基本的 TCP 反向 shell 非常简单。在这个例子rev_tcp.py中,我们将使用基于 bash 的反向 TCP shell。在本书的后面章节中,我们将看到如何完全使用 Python 传递反向 shell:

需要注意的是,OS.dup2用于在 Python 中创建文件描述符的副本。stdin被定义为文件描述符0stdout被定义为文件描述符1stderr被定义为文件描述符2。代码行OS.dup2(s.fileno(),0)表示我们应该创建stdin的副本并将流量重定向到套接字文件,该套接字文件恰好位于本地主机和端口1234(Netcat 正在监听的地方)。最后,我们以交互模式调用 shell,由于我们没有指定stderrstdinstdout参数,默认情况下,这些参数将被发送到系统级的stdinstdout,再次映射到程序的范围内的套接字。因此,前面的代码片段将以交互模式打开 shell,并将其传递给套接字。所有输入都从套接字作为stdin接收,所有输出都通过stdout传递到套接字。可以通过查看生成的输出来验证这一点。

总结

在本章中,我们讨论了一些更高级的 Python 概念,这些概念可以帮助我们增加吞吐量。我们讨论了多进程 Python 模块以及它们如何用于减少所需时间并增加我们的处理能力。通过本章,我们基本上涵盖了我们进入渗透测试、自动化和各种网络安全用例所需的 Python 的一切。需要注意的是,从现在开始,我们的重点将是应用我们到目前为止所学的概念,而不是解释它们的工作原理。因此,如果您有任何疑问,我强烈建议您在继续之前澄清这些疑问。在下一章中,我们将讨论如何使用 Python 解析 PCAP 文件,自动化 Nmap 扫描等等。对于所有的安全爱好者,让我们开始吧。

问题

  1. 我们可以使用 Python 的其他多进程库吗?

  2. 在 Python 中,线程在哪些情况下会变得有用,考虑到它们实际上在同一个核心上执行?

进一步阅读

第五章:漏洞扫描器 Python - 第 1 部分

当我们谈论端口扫描时,自动想到的工具就是 Nmap。Nmap 有良好的声誉,可以说是最好的开源端口扫描器。它具有大量功能,允许您在网络上执行各种扫描,以发现哪些主机是活动的,哪些端口是开放的,以及主机上运行的服务和服务版本。它还有一个引擎(Nmap 扫描引擎),可以扫描用于发现运行服务的常见漏洞的 NSE 脚本。在本章中,我们将使用 Python 来自动执行端口扫描的过程。本章将为我们的自动化漏洞扫描器奠定基础,并将补充下一章,该章将专注于自动化服务扫描和枚举。

本章涵盖以下主题:

  • 介绍 Nmap

  • 使用 Python 构建网络扫描器

介绍 Nmap

我们的端口扫描器将基于 Nmap 构建,具有额外的功能和能力,例如并行端口扫描目标,暂停和恢复扫描。它还将具有一个 Web GUI,我们可以用来进行扫描。

让我们来看看 Nmap 的各种属性:

  • 以下截图显示了 Nmap 可用的不同扫描技术:

  • 以下截图显示了主机发现和端口规范,以及一些示例:

  • 以下截图显示了服务和版本检测以及操作系统检测,以及一些示例:

  • 以下截图显示了时间和性能,以及一些示例:

  • 以下截图显示了 NSE 脚本以及一些示例:

  • 以下截图显示了防火墙/IDS 回避和欺骗,以及一些示例:

  • 以下截图显示了一些有用的 Nmap 输出示例:

前面的截图提供了我们在日常操作中经常使用的 Nmap 命令的全面列表。我们将不会涵盖如何在终端上运行 Nmap 命令,因为这被认为是直接的。

需要注意的是,从现在开始,我们将使用 Kali Linux 作为我们的渗透测试实验室操作系统。因此,我们将在 Kali Linux 上实施所有 Python 自动化。要安装 Kali Linux VM/VirtualBox 镜像,请参考www.osboxes.org/Kali-linux/。要下载 VirtualBox,请参考www.virtualbox.org/wiki/Downloads。下载后,执行以下截图中显示的步骤。

首先,输入新虚拟机的名称,类型和版本;在我们的案例中,这是 Linux 和 Debian(64 位)。之后,分配内存大小:

接下来,选择虚拟硬盘文件,如下截图所示:

使用 Python 构建网络扫描器

现在我们已经设置好了 VirtualBox 镜像,让我们来看一个简单的 Python 脚本,它将帮助我们调用 Nmap 并启动扫描。稍后,我们将优化此脚本以使其更好。最后,我们将使其成为一个功能齐全的端口扫描 Python 引擎,具有暂停,恢复和多进程能力:

前面脚本产生的信息对 Python 代码来说很难过滤和存储。如果我们想要将所有打开的端口和服务存储在字典中,使用前面的方法会很困难。让我们考虑另一种方法,可以解析并处理脚本产生的信息。我们知道oX标志用于以 XML 格式生成输出。我们将使用oX标志将 XML 字符串转换为 Python 字典,如下节所示。

使用脚本控制 Nmap 输出

在下面的示例中,我们重复使用了之前学习的相同概念。我们将 Nmap 输出重定向为 XML 格式显示在屏幕上。然后,我们将产生的输出作为字符串收集起来,并使用import xml.Etree.elementTree Python 模块作为ET,以将 XML 输出转换为 Python 字典。使用以下代码,我们可以使用我们的程序控制 Nmap 并过滤出所有有用的信息:

然后,我们可以将这些信息存储在数据库表中:

接下来,运行以下命令:

Nmap=NmapPy(["Nmap","-Pn","-sV","-oX","-","127.0.0.1"])
Nmap.scan()

虽然前面的方法很好,并且让我们对 Nmap 输出有细粒度的控制,但它涉及处理和解析代码,这可能是我们每次使用 Nmap 进行扫描时都不想编写的。另一种更好的方法是使用 Python 内置的 Nmap 包装模块。我们可以使用pip install安装 Python 的 Nmap 模块,它几乎与我们之前做的事情一样,但允许我们避免编写所有处理和子处理逻辑。这使得代码更清晰、更可读。每当我们希望有更细粒度的控制时,我们总是可以回到前面的方法。

使用 Nmap 模块进行 Nmap 端口扫描

现在让我们继续安装 Python Nmap 模块,如下所示:

pip install Nmap

上述命令将安装Nmap实用程序。以下部分提供了有关如何使用该库的概述:

import Nmap # import Nmap.py module
 Nmap_obj = Nmap.PortScanner() # instantiate Nmap.PortScanner object
 Nmap_obj.scan('192.168.0.143', '1-1024') # scan host 192.1680.143, ports from 1-1024
 Nmap_obj.command_line() # get command line used for the scan : Nmap -oX - -p 1-1024 192.1680.143
 Nmap_obj.scaninfo() # get Nmap scan informations {'tcp': {'services': '1-1024', 'method': 'connect'}}
 Nmap_obj.all_hosts() # get all hosts that were scanned
 Nmap_obj['192.1680.143'].hostname() # get one hostname for host 192.1680.143, usualy the user record
 Nmap_obj['192.1680.143'].hostnames() # get list of hostnames for host 192.1680.143 as a list of dict
 # [{'name':'hostname1', 'type':'PTR'}, {'name':'hostname2', 'type':'user'}]
 Nmap_obj['192.1680.143'].hostname() # get hostname for host 192.1680.143
 Nmap_obj['192.1680.143'].state() # get state of host 192.1680.143 (up|down|unknown|skipped)
 Nmap_obj['192.1680.143'].all_protocols() # get all scanned protocols ['tcp', 'udp'] in (ip|tcp|udp|sctp)
 Nmap_obj['192.1680.143']['tcp'].keys() # get all ports for tcp protocol
 Nmap_obj['192.1680.143'].all_tcp() # get all ports for tcp protocol (sorted version)
 Nmap_obj['192.1680.143'].all_udp() # get all ports for udp protocol (sorted version)
 Nmap_obj['192.1680.143'].all_ip() # get all ports for ip protocol (sorted version)
 Nmap_obj['192.1680.143'].all_sctp() # get all ports for sctp protocol (sorted version)
 Nmap_obj['192.1680.143'].has_tcp(22) # is there any information for port 22/tcp on host 192.1680.143
 Nmap_obj['192.1680.143']['tcp'][22] # get infos about port 22 in tcp on host 192.1680.143
 Nmap_obj['192.1680.143'].tcp(22) # get infos about port 22 in tcp on host 192.1680.143
 Nmap_obj['192.1680.143']['tcp'][22]['state'] # get state of port 22/tcp on host 192.1680.143

这为 Alexandre Norman 编写的出色实用程序提供了一个快速入门。有关此模块的更多详细信息,请访问pypi.org/project/python-Nmap/。我们将使用相同的模块来进行 Nmap 的并行端口扫描,并具有暂停和恢复扫描的附加功能。

目标和架构概述

在深入了解代码细节之前,重要的是我们理解我们在做什么以及为什么这样做。默认情况下,Nmap 非常强大并且具有大量功能。在使用操作系统工具进行典型的网络渗透测试时,采用的方法是使用 Nmap 进行端口扫描以获取打开的端口、运行的服务和服务版本。根据端口扫描结果,测试人员通常使用各种服务扫描脚本来获取服务版本和相关的 CVE ID(如果有的话),然后再根据这些,测试人员可以使用 Metasploit 来利用这些漏洞。对于服务扫描,测试人员使用各种开源技术,如 NSE、Ruby、Python、Java、bash 脚本,或者诸如 Metasploit、w3af、nikto、Wireshark 等工具。整个周期形成了一个需要每次遵循的流程,但它非常分散。我们在这里尝试提出的想法是,在接下来的部分中,我们将编排渗透测试人员需要执行的所有活动,并使用 Python 自动化所有这些活动,以便所有需要运行的工具和脚本都可以预先配置并一次性运行。我们不仅仅是编排和自动化活动,还使代码优化以利用多进程和多线程来减少扫描时间。

代码的架构可以分为以下几部分:

  • 端口扫描(服务/端口发现)

  • 服务扫描

端口扫描

端口扫描部分是指我们将如何在 Python 代码中实现它。想法是使用线程和多进程的组合。如果我们想要扫描 10 个主机,我们将把它分成 5 个批次。每个批次有两个主机(批次大小可以根据实验室机器的 RAM 和处理器能力增加)。对于四核处理器和 2GB RAM,批次大小应为 2。在任何时候,我们将处理一个批次,并为每个主机分配一个单独的线程。因此,将有两个线程并行运行,扫描两个主机。一旦一个主机被分配给一个线程,线程将选择要扫描该主机的端口范围(假设在 1 到 65535 之间)。逻辑不是顺序扫描端口,而是将整个范围分成三个大小为 21,845 的块。现在,单个主机的三个块并行扫描。如果处理器核心数量更多,块大小可以增加。对于四核处理器和 2GB RAM,建议使用三个块:

总之,主机被分成大小为 2 的批次,并专门用于单个主机。进一步的端口被分成块,并且一个多进程过程被专门用于扫描每个块,使得端口扫描可以并行进行。因此,在任何时候,将有两个线程和六个进程用于端口扫描活动。如果用户想要暂停扫描,他们可以在终端窗口使用Ctrl + C来暂停。当他们重新运行代码时,他们将被提示选择启动新的扫描或恢复先前暂停的扫描。

服务扫描

当端口扫描活动结束时,我们将所有结果保存在我们的 MySQL 数据库表中。根据发现的服务,我们有一个配置好的脚本列表,如果找到特定的服务,我们需要执行这些脚本。我们使用 JSON 文件来映射服务和相应的脚本以执行。用户将得到端口扫描结果,并有选择重新配置或更改结果的选项,以减少误报。一旦最终配置完成,服务扫描就开始了。我们从数据库中逐个选择一个主机,并根据发现的服务,从 JSON 文件中读取适当的脚本,为这个特定的主机执行它们,并将结果保存在数据库中。这将持续到所有主机的服务都被扫描。最后,生成一个包含格式化结果和屏幕截图的 HTML 报告,以附加到概念验证(POC)报告中。

服务扫描的架构图如下:

以下屏幕截图显示了 JSON 文件如何配置以执行脚本:

如前面的屏幕截图所示,JSON 文件中有各种类别的命令。Metasploit 模板显示了用于执行 Metasploit 模块的命令。单行命令用于执行 NSE 脚本和所有非交互式的模块或脚本。其他类别包括interactive_commandssingle_line_sniffing,在这里我们需要嗅探流量并执行脚本。JSON 文件的一般模板如下:

键是服务的名称。标题有文件描述。method_id是应调用的实际 Python 方法,以调用要执行的外部脚本。请注意,对于单行命令,我们还在args参数下的第一个参数中指定了以秒为单位的超时参数。

代码的更详细查看

让我们来看一下我们将使用 Python 构建网络扫描器所需的基本文件和方法的概述:

  • Driver_main_class.py:这是提示用户输入信息的 Python 类、文件或模块,例如项目名称、要扫描的 IP 地址、要扫描的端口范围、要使用的扫描开关和扫描类型。

  • main_class_based_backup.py:这是包含我们之前讨论的所有端口扫描主要逻辑的 Python 类、文件或模块。它从Driver_main_class.py获取输入并将输入存储在数据库中。最后,它使用线程和多进程在我们的目标上启动端口扫描。

  • Driver_scanner.py:端口扫描结束后,下一步是执行服务扫描,这个 Python 类调用另一个类driver_meta.py,该类获取要执行服务扫描的项目名称或 ID。

  • driver_meta.py:这个类显示端口扫描的默认结果,并给用户重新配置结果的选项。重新配置后,该类从数据库表中读取当前项目的主机,为其执行服务扫描。对于每个主机,它然后读取 JSON 文件以获取要执行的命令,并对于要执行的每个命令,它将控制传递给另一个文件auto_comamnds.py

  • auto_commands.py:这个文件从driver_meta.py获取参数,并调用外部技术,如 NSE、Ruby、Python、Java、bash 脚本,或者工具,如 Metasploit、Wireshark 和 Nikto。然后用于执行所选服务、主机和端口的服务扫描。命令执行结束后,它将结果返回给driver_meta.py以保存在数据库中。

  • IPtable.py:这是将端口扫描结果存储在数据库表中的类。它代表了我们的漏洞扫描器的数据层。

  • IPexploits.py:这是将服务扫描结果存储在数据库表中的类。它还代表了我们的漏洞扫描器的数据层。

入门

整个代码库可以在以下 GitHub 存储库中找到。安装说明在主页上指定。我们将查看代码部分和具有实现扫描器的中心逻辑的文件。请随时从存储库下载代码并按照执行部分中指定的方式执行。或者,我创建了一个即插即用的 Kali VM 映像,其中包含所有先决条件安装和开箱即用的代码库。可以从 URLdrive.google.com/file/d/1e0W…下载并无忧地执行。默认用户名为:PTO_root,密码为:PTO_root

如前所述,我们将讨论代码的中心逻辑,该逻辑由以下代码片段表示:

整个类可以在 URLgithub.com/FurqanKhan1…找到Driver_main_class.py。该类的构造函数声明了在main_class_based_backup.py中找到的NmapScan类的对象。**(1)(2)**标记的行是在收集所有输入后触发实际逻辑的地方,包括项目名称、IP、端口范围、扫描开关和扫描类型。扫描类型 1 表示新扫描,而扫描类型 2 表示恢复先前暂停的现有扫描。self.scanbanner()方法提示用户输入用户希望使用的 Nmap 扫描开关。有七种开关类型在日常扫描中最常用。以下截图显示了配置文件Nmap.cfg中配置的扫描开关:

以下代码片段代表了main_class_based_backup.py类的流程:

这个截图代表了主要的NmapScan类。该类的构造函数包含了我们将在整个类的执行流程中使用的各种变量。如前所述,IPtable是一个用于将数据推送到后端数据库的 Python 类。数据库的结构将在db_structure部分讨论。目前,我们应该理解,通过使用 MySQLdb db 连接器/Python 模块,我们将通过IPtable类将所有端口扫描的详细信息推送到后端表中。此外,textable是一个用于在终端窗口上绘制表格以表示数据的 Python 模块。Simple_Logger是一个用于在文件中记录调试和错误消息的 Python 模块。

正如我们之前所看到的,当我们查看Driver_main_class.py时,实际执行流程始于NmapScan类的driver_main方法(在Driver_main_class.py类的代码片段**(1)(2)**中突出显示)。以下截图更详细地显示了这个方法:

前面的代码片段很简单。该方法接收来自调用者的所有参数。我们将扫描的开始时间保存在一个名为start的变量中。突出显示的代码片段**(1)**调用了同一类的另一个main方法,并将所有接收到的参数传递给它。这是启动所有主机的端口扫描的方法。一旦调用的self.main方法完成执行,如代码片段(2)所示,我们需要检查所有主机是否成功扫描。这可以从一个后台表中推断出,该表维护了所有正在扫描的主机的status_code,由当前项目 ID 引用。如果主机成功扫描,状态将是 complete,否则将是 processing 或 incomplete。如果当前项目不处于暂停状态,并且仍有一些主机的状态是 incomplete 或 processing,我们需要再次处理这些主机,这是代码片段(3)所突出显示的内容。如果所有主机的处理状态都是 complete,我们将最终项目状态更新为 complete,由self.IPtable.clearLogs方法指定。最后,我们显示执行时间(以秒为单位)。在下一个代码片段中,我们将看一下NmapScan类的main方法,让事情开始运行:

main方法开始检查scan_type。必须注意scan_type="1"表示新扫描,scan_type="2"表示恢复先前暂停的扫描。代码还检查扫描模式。注意c代表命令行模式。我们正在制作的漏洞扫描器在 GUI 模式和命令行模式下都可以操作,我们稍后会讨论。我们现在可以忽略g-initg-start模式。

在第 6 行,代码将当前项目名称存储在后端数据库中。代码的逻辑由self.db_projectname方法处理。该方法接受项目名称,将其存储在数据库表中,返回一个唯一的项目 ID,并将其存储在名为self.CURRENT_PROJECT_ID的类变量中。它还在父项目文件夹的根目录下的Results文件夹下创建一个名为Results_project_id的文件夹。该方法的完整细节可以在以下路径找到:github.com/FurqanKhan1…

高亮显示的代码片段**(2)调用了一个名为self.numofips(targethosts)的方法,该方法返回要扫描的主机数量。如果有多个主机,它们应该被输入为逗号分隔(例如192.168.250.143192.168.250.144)或 CIDR 表示法(例如192.168.250.140/16)。如果它们是逗号分隔的,那么targethosts.split(',')将分割输入并返回 IP 列表给listip变量。如果是 CIDR 表示法,代码片段(3)**将把 CIDR IP 列表转换为本机 Python IP 列表并返回结果,结果将再次存储在listip变量中。

高亮显示的代码片段**(4)**负责将端口分成小块并将它们存储在数据库中,与之前讨论的当前项目 ID 相关。假设我们有两个要扫描的主机,192.168.250.143192.168.250.136,并且我们想要扫描主机的整个端口范围(从 1 到 65,535)。在这种情况下,方法的调用将是self.makeBulkEntries([192.168.250.143,192.168.250.136], "1-65535")。该方法处理输入并将其转换为以下内容:

[[192.168.250.143,"1-21845"],[192.168.250.143,"21845-43690"],[192.168.250.143,"43690-65535"],[192.168.250.144,"1-21845"],[192.168.250.144,"21845-43690"],[192.168.250.144,"43690-65535"]]

前面的列表被插入到数据库表中,共有六行,每行的扫描状态为不完整。

在下一行,threading.enumurate()返回当前运行线程的数量。它应该返回一个值为 1,因为只有主线程在运行。

高亮显示的代码片段**(5)**调用了startProcessing方法。这个方法从后端数据库表中读取一批不完整状态的不同主机,然后为这些主机分配一个线程进行扫描。必须注意的是,self.N表示批处理大小,我们已经讨论过它是 2,并且在类的构造函数中初始化。我们可以增加这个数字以获得更高的处理器数量。

startProcessing方法会生成线程并为每个未扫描的主机分配一个线程,但必须有一些逻辑来检查主机何时完全扫描,例如,如果批处理大小为2,并且扫描了 1 个主机,它会提取另一个未扫描的主机并为其分配一个线程。该方法还需要检查所有主机是否完全扫描。如果是这种情况,扫描必须结束。这段逻辑由start_Polling()方法处理,如标有**(6)**的代码片段所示。

高亮显示的代码片段**(7)**将调用一个方法来恢复暂停的扫描。因此,它将加载所有处于暂停状态的扫描的项目 ID。用户可以选择任何有效的项目 ID 来恢复扫描。

最后,代码片段**(8)**提到了Start_Polling(),它具有与之前讨论的相同功能,但在这种情况下是为恢复的扫描。

在下面的代码片段中,startProcessing()方法简单地从数据库表中提取所有不完整状态的不同主机,并将它们放入本机 Python 列表All_hosts中。对于当前示例,它将返回以下列表:[192.168.250.143, 192.168.250.144]。之后,高亮显示的代码片段**(1)**将调用startThreads方法,其中一个线程将被分配给一个主机:

startThreads()方法很简单。我们遍历主机列表并为每个主机分配一个线程,通过调用obj.simplescanner方法并将当前 IP 列表传递给它。对于我们当前的示例,simplescanner方法将被调用两次。首先,它将为线程 1 调用,该线程具有 IP 地址192.168.250.143,然后它将为线程 2 调用,该线程具有 IP 地址192.168.250.144。这由代码片段**(1)**突出显示。

simpleScanner()方法也很简单,使用了我们之前学习的多进程概念。首先,它读取调用它的当前主机的所有记录或端口块。例如,当它针对主机192.168.250.143调用时,它会读取数据库行[[192.168.250.143,"1-21845"],[192.168.250.143,"21845-43690"]和[192.168.250.143,"43690-65535"]]。之后,它将更新所有这些记录的状态,并将它们标记为:处理中,因为我们将要专门处理端口块的进程。最后,我们遍历端口列表,并为当前 IP 和当前端口块调用多进程进程,如**(1)**部分所示。根据当前示例,我们将为 Thread 1 运行三个并行进程,为 Thread 2 运行三个并行进程:

  • 进程 1(方法=端口扫描器(),IP=192.168.250.143,portx=1-21845,rec_id=100)

  • 进程 2(方法=端口扫描器(),IP=192.168.250.143,portx=21845-43690,rec_id=101)

  • 进程 3(方法=端口扫描器(),IP=192.168.250.143,portx=43690-65535,rec_id=102)

  • 进程 4(方法=端口扫描器(),IP=192.168.250.144,portx=1-21845,rec_id=103)

  • 进程 5(方法=端口扫描器(),IP=192.168.250.144,portx=21845-43690,rec_id=104)

  • 进程 6(方法=端口扫描器(),IP=192.168.250.144,portx=43690-65535,rec_id=105)

理想情况下,每个进程将在处理器核心上执行。拥有七个核心的处理器将是很棒的。在这种情况下,主程序将利用一个核心,其余六个核心将在前面的六个进程之间并行分配。然而,在我们的情况下,我们有一个四核处理器,其中一个核心被主线程使用,其余三个核心被生成的六个进程共享。这将涉及由于上下文切换而产生一定的延迟。还要注意,我们正在使用多进程库的 mp.Process 实用程序。请随时使用批处理模块,如我们在前几章中讨论的,批处理大小为 3,看看扫描时间是否有任何差异。最后,我们希望 Thread 1 线程保持活动状态,直到所有主机块都被扫描,因为我们的轮询逻辑表明,如果一个线程完成,那么主机扫描就结束了。因此,我们在当前线程上调用join()方法。这确保了 Thread 1 和 Thread 2 在所有进程完成之前都保持活动状态;换句话说,所有块都被扫描。

以下代码是不言自明的。我们使用 Python 的内置 Nmap 实用程序来扫描主机和端口块。如果扫描成功,我们只需解析结果并分别提取 TCP 和 UDP 结果。提取结果后,我们只需使用self.IPtable .Update()方法将结果保存在后端数据库表中。我们将状态标记为完成,并保存发现为开放的端口和服务的结果。另一方面,如果端口扫描结果和 IP 返回任何异常,我们将尝试进行三次重复扫描:

经过三次重试,如果扫描不成功,那么对于该记录(Iport-chunkproject_id),我们将更新状态为错误完成,如下截图所示:

start_Polling方法不断监视活动线程的数量,如**(1)(2)所示。如果发现只有一个正在运行的线程,然后它检查后端表,看是否所有主机都标记为complete状态。如果只有一个正在运行的线程(main)并且所有主机都标记为 complete,则它会跳出无限轮询循环。另一方面,如果发现当前运行的线程数量小于最大允许的批处理大小,并且数据库表中还有一些未扫描的主机,它会选择一个未扫描的主机,并通过调用startProcessing()方法为其分配一个线程。这在以下代码片段的(3)(4)**部分中得到了突出显示:

以下代码处理了如何恢复暂停的扫描。self.IPtable.MakeUpdate方法将未扫描主机的状态更新为incomplete。当有主机的状态从processing更改为incomplete时,返回 1。如果在将主机放入数据库表之前扫描被暂停,则返回状态2。在这种情况下,我们需要重新进行批量输入。其余代码很简单;我们调用startProcessing()方法来委派一个线程来扫描主机:

必须注意的是,为了暂停扫描,我们只需在控制台或终端窗口上按下Ctrl + C。当前扫描将被暂停,并在后端数据库中针对当前项目 ID 适当地更新状态。还应该注意,正如前面提到的,上述方法构成了我们漏洞扫描器的端口扫描部分的核心逻辑。确切的代码还有一些其他功能,详细信息可以在 GitHub 存储库github.com/FurqanKhan1…中找到。

执行代码

在执行代码之前,请参考 GitHub URL github.com/FurqanKhan1…上的安装和设置说明。安装指南还介绍了如何设置后端数据库和表。或者,您可以下载预先安装和预配置了所有内容的即插即用的虚拟机。

要运行代码,请转到/root/Django_project/Dictator/Dictator_Servicepath并运行driver_main_class.py代码文件,命令为python Driver_main_class.py

以下屏幕截图显示了程序正在进行扫描的过程:

以下屏幕截图显示了日志详情:

可以看到在前面的屏幕截图中,为一个主机生成了三个子进程并创建了一个线程。

漏洞扫描器端口扫描部分的数据库架构

让我们试着了解我们正在使用的后端数据库以及数据库中各种表的结构。使用show databases命令列出 MySQL 中存在的所有数据库:

为了使用当前数据库,也就是我们的漏洞扫描器相关的数据库,我们使用nmapscan命令。此外,要查看当前数据库中的所有表,我们使用show tables命令:

为了查看将保存所有扫描项目的表的结构或模式,我们使用desc project命令。要查看我们扫描的项目的数据,我们发出以下 SQL 查询:

IPtable 是保存我们目标端口扫描结果的表。以下命令 desc IPtable 显示了表的模式:

以下截图显示了当前项目744IPtable中的数据。我们可以看到所有的服务扫描结果都以 CSV 格式放在表中:

一旦项目的端口扫描成功完成,项目的所有细节都将从 IPtable 移动到 IPtable_history。这是为了在 IPtable 上快速进行查找操作。因此,IPtable_history 表的模式将与 IPtable 完全相同。这可以在以下截图中验证:

总结

在本章中,我们讨论了如何使用 Python 内置的 Nmap 实用程序来进行和自动化端口扫描,同时具有暂停和恢复扫描的附加功能,并使用线程和多进程添加了优化层。在下一章中,我们将继续使用我们的漏洞扫描程序,了解如何现在可以使用端口扫描结果来进一步自动化和编排服务扫描。我们还将讨论我们的漏洞扫描程序的 GUI 版本,它具有大量功能和非常直观的仪表板。

问题

  1. 为什么我们要使用线程和多进程的组合来自动化端口扫描?

  2. 我们可能如何进一步优化吞吐量?

  3. 是否有其他 Python 模块或库可以用来自动化 Nmap?

  4. 我们可以使用其他扫描程序,如 Angry-IP 或 Mass Scan,使用相同的方法吗?

进一步阅读

第六章:漏洞扫描器 Python - 第 2 部分

当我们谈论使用开源脚本进行服务扫描时,首先想到的是利用各种 NSE 脚本获取配置的服务的服务版本和相关漏洞。在典型的手动网络渗透测试中,我们不仅使用 NSE 脚本来完成工作,还使用各种 Ruby、Perl 和 Bash 脚本,以及 Java 类文件。我们还运行 Metasploit 辅助模块进行服务扫描和利用模块来利用漏洞并创建 POC。我们还可能运行各种 Kali 工具,比如用于 Web 扫描的 Nikto,或者用于捕获未正确配置的 FTP 或 SSH 服务的明文用户名和密码的 SQLmap、w3af 和 Wireshark。所有这些工具和脚本产生了大量信息,测试人员需要手动枚举和整合。还必须消除误报,以得出哪些服务存在哪些漏洞的结论。手动服务扫描的另一个方面是缺乏标准化,更多地依赖于个人的专业知识和所使用的脚本的选择。重要的是要记住,要使用的脚本大多是相互分离的,以至于一个人必须按顺序运行所有所需的脚本和模块。我们可以实现有限的并行性。

在本章中,我们将看到我们的漏洞扫描器如何自动化所有这些活动,并为整个生态系统带来标准化。我们还将看到自动化扫描器如何调用和编排所有 Kali 工具,以为渗透测试人员生成一个集成报告,供其快速分析使用。我们还将研究漏洞扫描器的图形用户界面版本,该版本具有更高级的功能,并补充了现有的漏洞扫描器,如 Nessus。必须指出的是,当我使用 补充 这个词时,我绝不是在将我们的扫描器与 Nessus 或 Qualys 进行比较。它们都是经过多年研发的优秀商业产品,并有一些优秀的工程师在其中工作。然而,我们将构建出一个运行非常出色的东西;了解代码可以让您有机会为扫描器做出贡献,从而帮助它随着时间的推移变得更好更大。

架构概述

我们已经在第五章 漏洞扫描器 Python - 第 1 部分 中看过了扫描器的架构。让我们重新审视扫描器的服务扫描部分,并思考整个生态系统是如何工作的。以下图表显示了我们的服务扫描架构:

项目 ID 将与使用 Nmap 端口扫描完成的所有扫描相关联。用户可以选择要进行服务扫描的项目 ID,并且还可以查看已成功完成端口扫描的所有项目 ID。应该注意,只有已完成的项目的项目 ID 将被显示;暂停端口扫描的项目将不会被显示。

一旦选择了项目 ID,代码就会读取数据库表 IPtable_history,显示开放端口和默认配置,这指的是开放端口和相关脚本(取决于服务名称)。用户可以重新配置扫描结果,包括手动添加任何被忽略的开放端口或删除任何显示为开放但实际上不可访问的条目。一旦用户重新配置了结果,我们就可以运行服务扫描了。应该注意,如果用户发现端口扫描结果都正确,可以跳过重新配置步骤。

扫描活动结束后,我们将把所有结果保存在我们的 MySQL 数据库表中。在服务扫描的情况下,根据发现的服务,我们将得到一个配置好的脚本列表,如果找到特定的服务,我们需要执行这些脚本。我们使用一个 JSON 文件来映射服务和相应的要执行的脚本。

在端口扫描的情况下,用户将收到端口扫描结果,并有选择重新配置结果(以减少误报)。最终配置设置后,将开始服务扫描。逻辑是从数据库中逐个选择一个主机,并根据发现的服务,从 JSON 文件中读取适当的脚本,并为该特定主机执行它们。最后,在执行脚本后,结果应保存在数据库中。这将持续到所有主机都扫描其服务为止。最后,将生成一个包含格式化结果和 POC 截图的 HTML 报告。以下截图显示了如何配置 JSON 文件以执行脚本:

从前面的截图可以看出,JSON 文件中包含各种类别的命令。Metasploit 模板包含用于执行 Metasploit 模块的命令。单行命令用于执行 NSE 脚本以及所有非交互式的模块和脚本,可以用单个命令触发。其他类别包括interactive_commandssingle_line_sniffing(需要在执行脚本的同时嗅探流量)。JSON 文件的一般模板如下:

key是服务的名称。标题包含文件的描述。method_id是应调用的实际 Python 方法,以调用要执行的外部脚本。请注意,对于单行命令,我们还在args参数下的第一个参数中指定了一个timeout参数,单位为秒。

代码的更详细查看

应该注意到整个代码库可以在 GitHub 上找到github.com/FurqanKhan1/Dictator。我们将查看所有构成服务扫描器核心逻辑的基本代码文件。或者,我创建了一个即插即用的 Kali VM 镜像,其中包含所有必需的安装和开箱即用的代码库。可以从以下 URLdrive.google.com/file/d/1e0Wwc1r_7XtL0uCLJXeLstMgJR68wNLF/view?usp=sharing下载并无忧地执行。默认用户名是PTO_root,密码是PTO_root

让我们概览一下我们将使用的基本文件和方法,来构建我们的服务扫描引擎,使用 Python。

Driver_scanner.py

端口扫描结束后,下一步是执行服务扫描。这个 Python 类调用另一个类driver_meta.py,它接受要执行服务扫描的项目名称/ID,如下面的代码片段所示:

driver_meta.py

这个类显示了端口扫描的默认结果,并给用户重新配置结果的选项。重新配置后,这个类从数据库表中读取要执行服务扫描的项目的主机。对于每个主机,它然后从 JSON 文件中读取要执行的命令,对于要执行的每个命令,它将控制传递给另一个文件auto_comamnds.py

前面的类代表了这个 Python 模块的主要父类。正如我们所看到的,我们已经导入了其他各种 Python 模块,如 JSON、SYS 和 psutil,以便与这个类一起使用。我们还可以看到,我们已经在这个模块中使用了其他类,如auto_commandsAuto_loggerIPexploitsIPtable。这些不是 Python 的内置模块,而是我们自己的类,用于执行我们服务扫描引擎的不同功能。我们将在稍后更详细地讨论这些。

main()

看一下这个类的main()方法,从这里实际上开始执行循环:

main()方法是用于 CLI 版本和 GUI 版本的相同代码片段,因此有许多参数只有在以 GUI 模式调用时才相关。我们将在本节讨论在 CLI 模式下需要的参数。我们可以看到mode变量在main()方法的定义中初始化为c

在下面的屏幕截图中标记为**(1)**的部分中,我们为texttable() Python 模块初始化了一个对象,该模块将用于在控制台窗口上绘制一个表,以显示可以执行服务扫描的项目 ID。第二部分从数据库中收集了所有已完成的项目,第三部分将检索到的行添加到程序变量中,以在屏幕上显示。随后的代码很简单。在第四部分,功能实际上删除了先前已完成服务扫描的项目的详细信息,以便用户可以用新的服务扫描操作覆盖结果:

第五部分创建了一个名为<project_id>的目录,位于results文件夹下。例如,如果当前项目 ID 是744,则命令init_project_directory()将在<parent_folder_code_base>/results/<744_data>下创建一个子文件夹。所有日志文件、扫描配置和最终报告都将放在这个文件夹中。正如我们已经讨论过的,我们有一个预配置的 JSON 文件,其中包含服务名称和要针对该服务执行的测试用例之间的映射。

以下部分显示了 JSON 文件的配置方式。让我们以http服务为例,看看如何配置要针对 HTTP 服务执行的测试用例:

从前面的分叉中可以看出并分类,名为http的服务的所有测试用例将放在一个 JSON 列表中,其键为CommandsCommands列表中的每个条目都将是一个 JSON 字典,其中包含以下条目:{"args":[],"id":"","method":"","include":"","title":""}。每个字典构成一个要执行的测试用例。让我们试着理解每个条目:

  • argsargs参数实际上是一个包含要针对目标执行的实际命令和 NSE 脚本的列表。要执行的所有命令/脚本被分类为我们将在方法部分中看到的五个不同类别。现在,了解args包含要在 Kali 控制台上用 Python 执行的实际命令就足够了。

  • id:给定要执行的每个命令都有一个唯一的 ID,这使得枚举变得容易。对于所有基于 HTTP 的命令,我们可以看到 ID 是http_1http_2http_3等等。

  • method: 这个特定的条目非常重要,因为它指的是应该调用的实际 Python 方法来执行这个测试用例。这些方法位于一个名为 auto_commands.py 的 Python 文件/模块中,该类别有不同的方法与 JSON 文件进行了映射。通常,要执行的所有脚本被分成五类/类别,并且每个类别都有一个相应的方法与之关联。脚本的类别及其相应的方法如下:

  • Single_line_comamnds_timeout: 所有需要一次性调用并为您生成输出的命令/脚本,而不需要在其间进行任何交互的命令/脚本都属于这一分类。例如,可以执行一个 NSE 脚本,命令如下:nmap -p80 --script <scriptname.nse> 10.0.2.15;它不需要任何其他输入,只需执行并给出最终输出。或者,可以如下调用一个用于执行目录枚举的 Perl 脚本:perl http-dir-enum.pl http://10.0.2.15:8000。同样,所有 Python 脚本、Bash 命令和 Kali 工具,如 Nikto 或 Hoppy,都属于这一类别。所有这些脚本都由一个名为 singleLineCommands_timeout() 的 Python 方法处理,该方法位于 auto_comamnds.py 模块中。需要注意的是,所有这些脚本还需要一个额外的 timeout 参数。有时单个脚本由于某些原因而挂起(主机可能无响应,或者可能遇到未经测试的意外情况),脚本的挂起将导致队列中的其他脚本处于等待状态。为了解决这种情况,我们在 args[] 列表中指定一个阈值参数作为第一个参数,这是我们希望脚本执行的最长时间(以秒为单位)。因此,从先前的配置中,我们可以看到为 ID 为 http_5 的 NSE 脚本指定了 500 秒的超时时间。如果脚本在 500 秒内未执行完毕,操作将被中止,并执行队列中的下一个脚本。

  • General_interactive: 除了需要执行单行命令并执行的脚本外,我们还有其他需要在执行后进行一些交互的 Bash 命令、Kali 工具和开源脚本。一个典型的例子是 SSH 到远程服务器,通常我们需要传递两组命令。这可以一次完成,但为了更好地理解,让我们举个例子:

  • ssh root@192.168.250.143 [Command 1]

  • password:<my_password> [Command 2]

另一个例子可能是工具,如 SQLmap 或 w3af_console,需要一定程度的用户交互。请注意,通过这种自动化/扫描引擎,我们可以通过自动调用 Python 来解决脚本的问题。所有需要交互的脚本或测试用例都由一个名为 general_interactive() 的方法处理,该方法位于 Python 模块 auto_comamnds.py 中。

    • General_commands_timeout_sniff: 有许多情况下,我们需要执行一个脚本或一个 Bash 命令,同时我们希望 Wireshark 在接口上嗅探流量,以便我们可以找出凭据是否以明文传递。在执行此类别中的脚本时,流量必须被嗅探。它们可以是单行脚本,如 NSE,也可以是交互式命令,如 ssh root@<target_ip> 作为第一个命令,password:<my_password> 作为第二个命令。所有需要这种调用的脚本都由 Python 方法 generalCommands_Tout_Sniff() 处理,该方法同样位于 auto_comamnds.py 模块中。
  • Metasploit_Modules:这是执行和处理所有 Metasploit 模块的类别。每当我们需要执行任何 Metasploit 模块时,该模块(无论是辅助还是利用)都将放置在此分类中。执行委托的方法称为custom_meta(),放置在auto_commands.py下。

  • HTTP_BASED:最终类别包含所有需要在目标上发布 HTTP GET/POST 请求进行测试的测试用例,并且这些情况由名为http_based()的方法处理,该方法再次放置在auto_commands.py模块中。

  • include**: **include参数有两个值:TrueFalse)如果我们不希望将测试用例/脚本包含在要执行的测试用例列表中,我们可以设置include=False。在选择扫描配置文件时,此功能非常有用。有时我们不希望在目标上运行耗时的测试用例,例如 Nikto 或 Hoppy,并且更喜欢仅运行某些强制性检查或脚本。为了具有该功能,引入了包含参数。我们将在查看我们的扫描仪的 GUI 版本时进一步讨论这一点。

  • title:这是一个信息字段,提供有关要执行的基础脚本的信息。

现在我们对将加载到我们的self.commandsJSON类变量中的 JSON 文件有了很好的理解,让我们继续进行我们的代码。

突出显示的部分**(6)读取我们的all_config_file程序变量中的 JSON 文件,最终进入self.commandsJSON类变量。突出显示的代码部分(7),(8)(9)**加载要与扫描一起使用的扫描配置文件:

默认情况下,我们的代码的命令行版本的扫描配置文件是强制性配置文件。该配置文件基本上包含应针对目标执行的所有测试用例;它只删除了一些耗时的测试用例。但是,如果我们希望更改mandatory_profile的定义,以添加或减去测试用例,我们可以编辑mandatory.json文件,该文件位于与我们的代码文件driver_meta.py相同的路径上。

以下是mandatory.json文件中为http服务存在的条目:

突出显示的部分(9)将加载项目 ID744的端口扫描获得的所有结果,结果将保存在数据库表IPtable_history中,以下屏幕截图给出了将加载的记录的想法:

我们可以从前面的屏幕截图中看到,基本上有三条记录对应于我们的 ID744的扫描。表列的模式是(record_id,IP,port_range,status,project_id,Services_detected[CSV_format])

后端执行的实际查询如下:

返回的结果将是一个可以迭代的列表。第一个内部列表的第 0 个索引将包含以 CSV 格式加载的检测到的服务。格式将是(主机;协议;端口;名称;状态;产品;额外信息;原因;版本;配置;cpe),可以从前面的屏幕截图中验证。所有这些信息将放在results_列表中。

在第**(10)**部分中,如下片段所示,我们正在遍历results_列表,并将字符串数据拆分为新行\n。我们进一步将返回的列表拆分为,最后将所有结果放在一个列表lst1 []中:

对于当前示例,在第(11)部分之后,lst1将包含以下数据:

lst1=[
[10.0.2.15,tcp,22,ssh,open,OpenSSH,protocol 2.0,syn-ack,OpenSSH-7.2p2 Debian 5,10,cpe:/o:linux:linux_kernel],                                                                    [10.0.2.15,tcp,80,http,open,nginx,,syn-ack,nginx-1.10.2,10,cpe:/a:igor_sysoev:nginx:1.10.2],
  [10.0.2.15,tcp,111,rpcbind,open,,RPC #100000,syn-ack,-2-4,10,],
  [10.0.2.15,tcp,443,https,open,nginx,,syn-ack,nginx-1.10.2,10,cpe:/a:igor_sysoev:nginx:1.10.2],
  [10.0.2.15,tcp,8000,http,open,nginx,,syn-ack,nginx-1.10.2,10,cpe:/a:igor_sysoev:nginx:1.10.2],
  [10.0.2.15,tcp,8002,rtsp,open,,,syn-ack,-,10,]
]

因此,lst1[0][0]将给我们10.0.2.15lst1[2][2]=111等等。

在代码的第**(12)节中,我们正在按服务类型对lst1中的数据进行排序。我们声明了一个字典lst={},并希望根据它们的服务类型对所有主机和端口进行分组,以便第(12)(13)**节的输出如下:

lst = {
"ssh":[[10.0.2.15,22,open,OpenSSH-7.2p2 Debian 5;10]],
"http":[[10.0.2.15,80,open,nginx-1.10.2],[10.0.2.15,8000,open,nginx-1.10.2]],
"rcpbind":[[10.0.2.15,111,open,-2-4,10]],
"https":[[10.0.2.15,443,open,nginx-1.10.2]],
"rtsp":[[10.0.2.15,8002,open,-]]
}

在第**(15)节中,ss = set(lst_temp).intersection(set(lst_pre)),我们对包含字典键的两个结构进行了交集运算。一个结构包含来自字典lst的键,该字典包含我们的端口扫描程序发现的所有服务。另一个包含从预配置的 JSON 文件中加载的键。这样做的目的是让我们看到所有已映射测试用例的发现服务。所有已发现和映射的服务键/名称都放在列表SS**中,代表要扫描的服务。

在第**(16)节中,ms=list(set(lst_temp) - set(lst_pre)),我们正在比较未在 JSON 文件中配置的服务与发现的服务。我们的 JSON 文件在常见服务方面非常详尽,但仍然有时 Nmap 可能会在端口扫描期间发现未在 JSON 文件中预先配置的服务。在本节中,我们试图识别 Nmap 发现但在我们的 JSON 文件中没有针对它们映射测试用例的服务。为此,我们对这两种结构进行了集合差异。我们将标记这些服务为new,用户可以对其进行配置测试用例,或者离线分析以执行自定义测试用例。所有这些服务将被放在一个名为ms的列表中,其中ms代表未发现的服务**。

在代码片段中显示的第**(17)(18)**节中,我们再次将两个未发现和映射的服务重新构建为两个不同的字典,格式如前所述:{"ssh":[[10.0.2.15,22,open,OpenSSH-7.2p2 Debian 5;10]],...}。发现的服务将放在dic字典中,然后放入self.processed_services类变量中。未发现的服务将放入ms_dic,最终放入self.missed_services中。

最后,在第**(19)**节中,我们调用parse_and_process()方法,该方法将调用显示发现和未发现服务的逻辑,并为用户提供必要时执行任何重新配置的选项。

重新配置完成后,parse_and_process()将调用另一个方法launchExploits(),该方法将实际从 JSON 配置文件中读取method_name,用发现的适当主机 IP 和端口替换<host><port>,并将控制传递给auto_command.py模块的相关方法(根据读取的method_name)。

一旦对所有发现的主机和端口执行了所有测试用例,就该生成包含屏幕截图和相关数据的报告了。这部分由第**(20)(21)**节处理,如下面的代码片段所示:

解析和处理()

在接下来的部分中,我们将了解parse_and_process()方法的工作原理。值得注意的是,对于 CLI 版本,mode 变量的值为c,我们将只关注通向mode=c的代码部分。代码的其他分支将用于 GUI 模式,如果您想了解更多,可以自由阅读。

在第**(1),(2),(3)(4)**节中的parse_and_process()方法开始执行,通过迭代self.missed_servicesself.processed_services。这里的迭代思想是将这些发现的服务、主机、端口和command_template放入不同的数据库表IPexploits。我们将稍后讨论command_template。对于当前的示例,self.processed_services将包含以下数据:

self.processed_services= {
"ssh":[[10.0.2.15,22,open,OpenSSH-7.2p2 Debian 5;10]],
"http":[[10.0.2.15,80,open,nginx-1.10.2],[10.0.2.15,8000,open,nginx-1.10.2]],
"rcpbind":[[10.0.2.15,111,open,-2-4,10]],
"https":[[10.0.2.15,443,open,nginx-1.10.2]],
}
self.missed_services ={
"rtsp":[[10.0.2.15,8002,open,-]]
}

这是因为除了rtsp之外,所有发现的服务都在 JSON 文件中映射了。

代码的第**(5)**部分遍历此字典,并尝试获取诸如getTemplate(k)的内容,其中k是当前正在迭代的服务。getTemplate()是一个读取 JSON 文件并返回要执行的测试用例的命令 ID 的方法。

以下示例将说明这一点。假设getTemplatehttp上被调用,如getTemplate('http')。这将返回以下结构:

entries= {"Entries": {"http_5": [true, "0", "0"], "http_trace_1": [true, "0", "0"], "http_trace_2": [true, "0", "0"], "http_trace_3": [true, "0", "0"], "http_banner_1": [true, "0", "0"], "http_banner_2": [true, "0", "0"], "http_robots_1": [true, "0", "0"], "http_robots_2": [true, "0", "0"], "http_headers_1": [true, "0", "0"], "http_headers_2": [true, "0", "0"], "http_methods_1": [true, "0", "0"], "http_methods_2": [true, "0", "0"], "http_web_dev_1": [true, "0", "0"], "http_web_dev_2": [true, "0", "0"]}}

结构如下:{"http_5":['include_command,commands_executed,results_obtained]}。如果http_5是键,那么值是一个包含三个条目的列表。第一个条目表示命令是要包含还是执行(取决于所选择的扫描配置文件)。第二个条目保存在终端上执行的实际命令。最初它设置为 0,但一旦执行,http_50将被替换为nmap -Pn --script=banner.nse -p 80 10.0.2.15。第三个0实际上将被执行命令产生的结果所替换。

代码entries=getTemplate(k)将为每种服务类型返回一个类似上述的条目。我们准备一个名为rows的列表,其中放置主机、端口、服务、开/关状态和条目/command_template。执行该活动的代码片段是self.rows.append((self.project_id, str(h_p[0]), str(h_p[1]), str(k), 'init', entries, service_status, str(h_p[2]), str(h_p[3])))

type=new的服务或未映射的服务将由代码部分**(2)**处理。这将在我们的示例条目中放置以下内容:

entries={"Entries": {"new": true, "unknown": false}}

代码部分**(6)检查诸如if(is_custom==True)之类的内容。这意味着有一些服务可以与其他服务多次使用。例如,ssl的测试用例可以与https一起使用,如[http +ssl]ftps作为[ftp + ssl]ssh作为[ssh + ssl]。因此,诸如httpsftps等服务被标记为custom,当发现https时,我们应该加载httpssl的两个模板。这就是在第(6)**部分中所做的。

在第(6)部分结束时,self.rows将为所有主机和端口的所有服务类型保存类似[project_id,host,port,service,project_status,command_template,service_type,port_state,version]的条目。在我们当前的示例中,它将为所有服务类型保存六行。

在第**(7)**部分,self.IPexploit.insertIPexploits(self.rows),我们一次性将self.rows的所有数据推送到后端数据库表IPexploits中。必须记住,后端数据库中command_template/entries的数据类型也标记为 JSON。因此,我们需要 MySQL 版本 5.7 或更高版本,支持 JSON 数据类型。

执行此命令后,我们当前项目744的后端数据库将如下所示:

必须注意的是,我没有加载command_template(在后端命名为Exploits),因为数据会变得混乱。让我们尝试加载两个服务的模板,如rtspssh

同样,我们还将有httpsslrcpbind的条目。应该注意的是,我们预计表中有六行,但实际上有七行。这是因为https服务被分为两类httpssl,因此,在端口443上,我们不是有https,而是有两个条目:http-443ssl-443

在下一部分,项目的默认配置(主机、端口、要执行的测试用例)从同一数据库表中获取,并显示给用户。第八部分调用代码使用launchConfiguration()

launchConfiguration()

在这一节中,让我们来看一下launchConfiguration()方法,它加载默认配置,并且还允许用户进行微调或重新配置。此外,它调用了文件的中心逻辑,实际上会启动脚本执行,即launchExploits()

对于 CLI 版本,launchExploits()是由launchConfiguiration()调用的。然而,在 GUI 版本中,launchExploits()只能由parse_and_process()方法调用。有关此方法的更多信息可以从前面的截图中看到。

以下代码片段的第 1 节加载了放置在当前项目的IPexploits表中的所有细节。我们已经看到了将被拉出并放置在IPexploits列表下的七行。请记住,在后端表中,我们只有命令 ID,例如http_1http_2放在Template下,但是为了显示所选的配置和要执行的命令,我们拉出实际的脚本,它将映射到http-1等等。这就是第 2 节在做什么。它读取 JSON 文件以获取实际命令。

在第 3 节中,我们将拉取的细节放在tab_draw变量中,它将在控制台窗口上绘制一个表,并表示加载的配置:

第 4 节是不言自明的;我们将所有拉取的细节放在一个名为config_entry的字典中。这将被保存到一个文件中,因为最终选择的配置与扫描将被启动:

最后,在第 6 节下,我们调用launchExploits()。如果需要执行重新配置,第 7 节调用self.reconfigure()方法,该方法很简单,可以从代码库或以下 URL github.com/FurqanKhan1… 中找到:

第 5 节将如下显示屏幕上的配置:

launchExploits()

接下来的部分将讨论launchExploits()方法。

以下代码的第 9 节加载了放置在当前项目的IPexploits表中的所有细节。我们已经看到了将被拉出并放置在IPexploits_data列表下的七行。我们不需要关注if(concurrent=False)else块,因为那是指在 GUI 版本中调用的代码。现在,让我们只考虑if块,因为对于 CLI 版本,concurrent=False。接下来,我们遍历IPexploits_data: "for exploit in IPexploits_data:"结构:

在第 10 节中,我们从当前正在迭代的服务的 JSON 结构中加载细节。请记住,self.commandsJSON保存了整个 JSON 文件数据,我们在其中映射了服务和测试用例。然后,我们加载该特定服务的所有命令和测试用例,并将它们放在一个名为meta的列表下。例如,如果service = http,那么 meta 将包含[http_1,http_2,http_3,http_4,http_5 ...]。现在,请记住,在最后一节中,对于七条记录中的每条记录,project_status都是init。在下一行(第 11 节),我们将当前记录的(host,port,service,record_id)组合的状态更新为processing。因为我们已经选择了执行此服务,我们希望更改数据库状态。

在第 12 节中,我们加载了为项目选择的扫描配置所执行的特定服务用例的所有启用服务用例。

还有一些项目/扫描可能需要一些用户定义的参数,例如要使用的用户名、密码等。所有这些参数都放在一个Project_params.json文件中,第**(13)**节将要执行的命令中的项目特定用户名和密码替换为适用的项目特定用户名和密码:

Self.commandObj保存了auto_commands.pl类的对象。第**(14)**节初始化了与要执行的当前记录集相关的类的实例变量(主机、端口、服务等)。正如我们之前讨论的,JSON 文件中的args参数包含要执行的实际命令。我们将args的值加载到程序变量 args 中。我们知道,这是一个包含命令的列表。我们遍历这个列表,并将诸如<host>之类的条目替换为要扫描的实际 IP,将<port>替换为要扫描的实际端口。我们将逐个为所有测试用例重复这个活动。对于当前示例,如果我们假设http是要扫描的当前服务,代码将遍历所有命令[http_1,http_2..]。最后,http_5和端口80final_args列表将被指定为[500, nmap -Pn --script=banner.nse -P80 10.0.2.5]

在第**(16)**节中,我们实际上是从auto_comamnds.py模块中调用适当的方法。让我们思考一下这是如何工作的。getattr(object, name[, default])返回object的命名属性的值。如果字符串是对象属性之一的名称,则结果是该属性的值。例如,getattr(x,'Method_name')等同于x. Method_name

正如我们已经讨论过的,要执行脚本/模块的方法的名称在 JSON 文件中预先配置,并且在前面的代码中它被读入变量方法。func = getattr(self.commandObj,method_name)将返回该方法的引用,并且可以被调用,比如func(args)。这就是第**(18)节中所做的:func(final_args,grep_commands)。当执行该方法时,它将自动将结果保存在数据库中。一旦一个服务的所有测试用例都执行完毕,我们希望将该行的状态从processing更新为complete,这就是第(20)**节所做的。相同的操作会重复,直到所有主机的所有发现的服务都被扫描。让我们看一下当执行一个测试用例时数据库表是什么样子的。我们将从不同的项目 ID 中取一些例子:

从前面的屏幕截图可以看出,项目 ID 736 的这一行在服务扫描之前的数据如下:Pid=736,Service='ssl',Exploits={"Entries" :{"ssl_1":[true,0,0]} ... }。然而,一旦执行结束,第一个 0 将被一个包含执行的命令的列表所替换。第二个 0 的位置,我们有最终结果的字符串形式。

自动 _commands.py

在下一节中,我们将看一下实际工作的方式,即调用的方法如何自动化服务扫描的过程。我们将探索 Python 模块或文件auto_commands.py。必须记住,在本节中,我们将涵盖该类的基本方法。除此之外,还有一些其他方法是为特定用例定制的。您可以在 GitHub 存储库的确切代码文件中查看github.com/FurqanKhan1…。让我们首先看一下这个类是什么样子的:

我们导入的模块之一是pexpect。在接下来的部分中,让我们试着理解这个模块的作用以及它为什么重要。

Pexpect

Pexpect 是一个类似 Unix 的 expect 库的 Python 模块。这个库的主要目的是自动化交互式控制台命令和实用程序。Pexpect 是一个纯 Python 模块,用于生成子应用程序、控制它们,并响应其输出中的预期模式。Pexpect 允许您的脚本生成子应用程序并控制它,就像一个人在键入命令一样。Pexpect 可用于自动化交互式应用程序,如 SSH、FTP、passwd、telnet 等。

我们将使用 Pexpect 来使用 Python 自动化 Metasploit,并且还将调用需要用户交互的终端自动化的各种用例。必须注意的是,还有另外两种用 Python 代码调用 Metasploit 的方法:"msfrpc",它调用了建立在 Metasploit 之上的服务 API,以及".rc"脚本。然而,我们观察到使用 Pexpect 模块的成功率最高。

Pexpect 模块有一个 spawn 类,用于生成任何终端命令、进程或工具。生成的工具应作为代码的子进程生成。

spawn 类构造函数的语法如下:

pexpect.spawn(command, args=[], timeout=30, maxread=2000, searchwindowsize=None, logfile=None, cwd=None, env=None, ignore_sighup=False, echo=True, preexec_fn=None, encoding=None, codec_errors='strict', dimensions=None, use_poll=False)

spawn类构造函数有许多参数,但强制参数是commandcommand是我们希望在 Unix 终端上执行的实际命令。如果我们希望传递参数给调用的命令,我们可以在命令本身中指定参数,用空格分隔,或者将参数作为 Python 列表传递到第二个参数args下。第三个参数是timeout,默认为 30 秒。这意味着如果在 30 秒内未生成进程,整个操作将被终止。如果我们的服务器负载很高,或者我们有性能问题,我们可以增加timeout参数。以下代码表示如何使用 Pexpect 调用 SSH 会话:

child = pexpect.spawn('/usr/bin/ftp')
child = pexpect.spawn('/usr/bin/ssh user@example.com')

我们还可以使用参数列表构造它,如下所示:

child = pexpect.spawn('/usr/bin/ftp', [])
child = pexpect.spawn('/usr/bin/ssh', ['user@example.com'])

当在终端上执行命令时,会创建一个会话,并通过返回的进程进行控制,该进程被放置在child变量下,如前面的示例所示。

pexpect的另一个重要类是expect。如其名称所示,Expect 规定了在成功执行spawn命令时可能产生的预期输出或输出。例如,如果spawn命令是pexpect.spawn('/usr/bin/ssh',['user@example.com']),我们通常期望 ssh 服务器要求我们输入密码。从先前指定的命令中可能期望的所有可能模式或字符串都作为参数传递给pexpect.expect类,如果任何模式匹配,我们可以根据匹配定义要发送到终端的下一个命令。如果没有匹配,我们可以中止操作并尝试调试。

以下语法查找流,直到匹配模式。模式是重载的,可能有多种类型。模式可以是字符串类型、EOF、编译的正则表达式,或者是任何这些类型的列表:

pexpect.expect(pattern, timeout=-1, searchwindowsize=-1, async_=False, **kw)

如果传递了模式列表,并且有多个匹配项,则流中选择第一个匹配项。如果此时有多个模式匹配,则选择模式列表中最左边的模式。例如:

# the input is 'foobar'
index = p.expect(['bar', 'foo', 'foobar'])
# returns 1('foo') even though 'foobar' is a "better" match

child.sendLine(command)是一个方法,它接受要发送到终端的命令,假设一切都按预期模式工作:

child = pexpect.spawn('scp foo user@example.com:.')
child.expect('Password:')
child.sendline(mypassword)

让我们通过使用 Pexpect 进行 SSH 自动化的小例子来更清楚地说明问题:

child = pexpect.spawn(ssh root@192.168.250.143)
i=child.expect(['.*Permission denied.*', 'root@.* password:.*','.* Connection refused','.*(yes/no).*',pexpect.TIMEOUT,'[#\$]',pexpect.EOF],timeout=15)
if(i==1):
       child.sendline('root')
       j=child.expect(['root@.* password:.*', '[#\$] ','Permission denied'],timeout=15)
       if(j==1):   
           self.print_Log( "Login Successful with password root")
       else:
           self.print_Log("No login with pw root")

在前面的代码中,我们只考虑成功的情况。必须注意,如果终端期望输入列表的第 1 个索引'root@.* password:.',那么我们将使用sendline方法将密码作为 root 传递。注意'root@.* password:.'表示 root 后面的任何 IP 地址,因为它是一个正则表达式模式。根据匹配的字符串/正则表达式模式的索引,我们可以制定逻辑来指示接下来应该做什么。

自定义 _meta()

现在让我们来看一下custom_meta方法,它负责处理所有的 Metasploit 模块。它借助 Pexpect 库完成这一工作。

正如在以下片段的第**(1)**部分中所示,我们使用pexpect.spawn在我们的终端上调用"msfconsole -q"。这将在虚拟终端上调用一个 Metasploit 进程,并将该进程的控制返回给声明为 child 的变量:

每当我们调用 msfconole 时,如果没有错误,我们将得到一个 Metasploit 提示符,如msf>。这就是我们在第**(2)**部分中指定的,[.*>, .., ..],作为第 0 个索引。这里暗示的是,我们期望任何>之前的内容都能成功执行,因此我们将传递运行 Metasploit 模块所需的命令。如果 child.expect 返回的索引为 0,我们将遍历 JSON 文件的命令列表,并将每个命令发送到我们的 Metasploit 控制台。对于我们的 projectID 744http服务,我们配置了一些 Metasploit 模块。其中一个如下所示:

在前面的 JSON 结构的args键中的任何内容都将作为列表传递给custom_meta方法,并存储在 commands 列表中。在第**(3)**部分,我们遍历 commands 列表,并且,正如我们之前学过的那样,<host><port>实际上将被实际主机和正在扫描的端口替换。

在这一部分中,每个命令都会使用child.sendline(cmd)命令逐个发送到 msfconsole 终端。发送每个命令后,我们需要检查控制台是否符合我们的预期,也就是说它应该包含msf>提示符。我们调用pexpect.expect并将".*>"指定为我们输入列表的第 0 个索引。注意,索引 0 定义了我们继续的成功标准。只要我们得到与索引 0 匹配的输出,我们就继续,如第**(4)**部分所指定的那样。如果我们在任何时候观察到除索引 0 之外的任何内容(超时或文件结束-EOF),我们意识到某些事情并没有按预期发生,因此我们将布尔变量设置为 false:

当我们退出这个迭代循环时,我们转到第**(9)部分,检查 run == True。如果为真,我们假设所有参数都已正确设置以执行 Metasploit 模块。我们使用sendline发出'run'命令,如第(10)**部分所示。

最后,如果一切顺利,模块成功执行,那么现在是收集结果的时候了。在第**(11)部分,如果一切如预期那样进行,我们将在exploits_results变量中收集结果,并在commands_launched变量中收集命令。如果出现错误,我们将在第(12)**部分中收集错误详情:

最后,在第**(14)**部分,我们通过调用saveDetails()方法将结果保存在数据库表中。必须注意,结果将以与之前讨论的相同的 JSON 结构保存在"http_headers_2"键下,这是脚本的 ID。saveDetails方法的定义如下。请注意,它将被应用于我们将讨论的所有不同方法:

**(1)**部分调用了放置在类文件IPexploits.py中的方法,该方法将在数据库中插入详细信息。整个代码文件可以在 GitHub 存储库中找到。

singleLineCommands_Timeout()

在本节中,我们将看到singleLineCommands_Timeout方法的定义。这部分代码解释了线程和多进程的强大之处。我们之前学习了所有概念,但在本节中,我们将看到如何应用线程和进程的概念来解决现实世界的问题。

手头的问题是执行所有可以通过在控制台上输入一行命令来执行的命令和脚本的所有类别。这些产生输出。这可能看起来很简单,但有一个问题。请记住,我们讨论过脚本的执行可能因为某些不可预见的原因而需要很长时间,我们应该设计我们的解决方案,以便所有可能出现这种情况的脚本类别都有一个关联的超时。在这里,我们将使用线程来实现超时功能。线程和进程的组合将帮助我们实现我们的目标。

核心思想是调用一个线程并将其绑定到一个方法"x"。我们在调用的线程上调用join()join()的持续时间将是 JSON 文件中指定的超时时间。正如我们之前学过的,当从主线程'm'上的线程't'上调用join()方法时,将导致主线程'm'等待,直到't'完成其执行。如果我们在主线程'm'上的线程't'上调用join(20),这将导致主线程'm'等待 20 秒,直到't'完成。20 秒后,主线程将继续执行并退出。我们可以使用相同的类比来实现我们的任务:

在**(1)(2)**部分,我们正在创建一个thread对象,并将其附加到"execute_singleLine"方法。应该注意的是,有时我们希望从最终输出中提取出一些内容,这就是为什么我们要检查grep参数是否设置。如果设置了,我们将grep字符串作为参数发送到线程方法;否则,我们只发送方法应该调用的控制台脚本/命令。现在我们不需要担心 grep 条件。

在**(3)部分,我们可以看到我们正在收集超时参数,该参数始终位于命令列表的索引 0 处,或者位于 JSON 文件的 args 的第 0 个索引处。我们在线程上调用 start 方法,该方法将调用"execute_singleLine"方法,并将要执行的命令作为参数传递。之后,我们在调用的线程上调用join(timeout),代码将在那里暂停,直到超时指定的秒数为止。在(3)**部分之后不会执行任何行,直到"execute_singleLine"方法完成或时间超过超时。在继续之前,让我们更仔细地看看"execute_singleLine"方法中发生了什么:

"execute_singleLine()"方法的**(1)**部分所述,我们正在利用 Python 的 subprocess 模块来生成一个子进程。进程将由cmd变量中的命令指定。因此,如果cmd包含"nmap -Pn --script=banner.nse -p 80 192.168.250.143",则相同的命令将在终端上执行,这只是操作系统级别的一个进程。进程类的实例将被返回并放置在self.process类变量下。该实例具有各种属性,如"id""is_alive()"等,这些属性给我们提供了有关进程状态的信息。

由于我们确定了传递给进程的参数(因为它们不是直接来自用户),我们可以继续进行。但是,最好使用shell=False并将参数指定为列表[],或者使用 Python 的shelx实用程序自动将字符串参数转换为列表并使用shell=False

我们希望父进程等待子进程执行,我们也希望子进程将其产生的所有数据返回给父进程。我们可以通过在调用的进程上调用communicate()来实现这一点。communicate()方法将返回一个元组,其中包含来自进程的输出的第 0 个索引和产生的错误的第一个索引。由于我们指定了output=subprocess.PIPEerror=subprocess.PIPE,输出和错误都将通过 OS 管道传输到父进程,这就是我们实现进程间通信的方式。这在第(2)部分中有所强调。

我们的下一个挑战是将控制台输出转换为标准的 ASCII 格式,以便我们可以将数据干净地保存在数据库中。需要注意的是,不同的工具和脚本以不同的格式和编码生成数据,这些格式和编码适合控制台显示。控制台支持各种编码,但我们需要将输出保存在数据库表中,因此在推送数据之前,我们需要将其从控制台编码转换为 ASCII 格式。这就是我们在第(3)部分所做的事情。

在第(4)部分中,我们通过调用process = psutil.Process(self.process.pid).来控制父进程。

在第(5)部分中,清理数据后,我们通过调用saveDetails()方法将执行的两个命令和生成的数据推送到数据库表中。

在第(3)部分之后,我们通过调用thread.is_alive()来检查线程是否仍然活动。如果返回false,这意味着线程已经成功在指定的时间内执行,通过内部调用subprocess.Process命令,并且详细信息也保存在数据库表中。但是,如果thread.is_alive()返回true,这意味着外部脚本仍在运行,因此我们需要强制将其终止,以免影响其他要执行的脚本的执行。请记住,调用的进程会将我们保存在self.process类变量下的进程实例返回给我们。我们将在这里使用该变量来终止进程。Python 有一个非常强大的实用程序叫做"psutil",我们可以使用它来不仅终止进程,还可以终止该进程调用的所有子进程。我们还需要终止子进程,因为我们不希望它们在后台运行并消耗我们的 CPU。例如,诸如 Nikto 之类的工具会调用许多子进程来加快整个操作,我们希望终止所有这些进程,以确保父进程被终止并且所有系统资源都被释放供其他进程使用。一旦我们获取了父进程,我们使用for循环迭代其每个子进程,for proc in process.children(recursive=True):,并通过发出命令proc.kill()来终止每个子进程。这在第(5)部分中有所强调。最后,在第(6)部分,我们通过调用self.process.kill()确保终止父进程。

general_interactive()

在这一部分,我们将了解general_interactive()方法的工作原理。虽然我们也可以使用这种方法实现 Metasploit 命令,但为了保持类别的分离,我们单独实现了 Metasploit。

general_interactive的目标是自动化交互式工具和 Bash 命令。这意味着 JSON 文件包含了定义执行工作流程的成功模式和失败模式。我们将使用 Pexpect 来实现这一点,如下所示:

让我们通过进行干运行来更仔细地研究这个方法,如下所示:

正如我们在args[]中看到的,第一个参数是超时时间。第二个索引保存我们希望使用一般交互方法自动化的命令。对于这个类别,第一个参数将始终是超时时间,第二个参数将是要执行的命令。从这里开始,定义了一个交替模式。第三个索引将保存预期输出列表和成功标准。如果满足成功标准,第四个索引将保存要发送到控制台的下一个命令。第五个索引将再次保存基于第四个索引发送的命令的预期输出列表,并且它还保存成功标准。模式很简单,根据我们计划自动化的底层命令或工具所需的,同样的交替序列将继续进行。

成功标准在预期输出列表的第一个索引处定义。如果有多个成功结果或索引,它们可以作为逗号分隔的输入给出在第一个索引处。让我们以rlogin的上述示例为例,我们正在尝试使用 root 作为用户名和密码进行远程登录,并尝试理解预期输出列表的内容和意义。索引 3 处的列表包含['0,1','.* password: .*","[$,#]",".*No route.*"]。在这里,第 0 个索引“0,1”定义了成功标准。这意味着如果终端期望".* password: .*""[$,#]"中的任何一个,我们就假设输出符合预期,因此我们将下一个命令发送到控制台,这在我们的情况下是"root"。如果我们得到的不是索引 0 或 1,我们就假设工具或脚本的行为不符合预期,因此中止操作。

要配置属于此类别的命令和脚本,测试人员需要知道脚本在成功和失败条件下的执行方式,并制定配置文件。前面的代码很简单,实现了我们之前讨论的相同逻辑。

generalCommands_Tout_Sniff()

这里的想法类似于我们如何使用线程实现singleLineComamnd()方法。请注意,要执行的命令的类别要么是interactive,要么是"singleLineCommand_Timeout",还有一个嗅探操作。我们将创建一个线程,并将嗅探任务委托给它,通过将它附加到start_sniffing方法。我们还将重用我们之前创建的方法。我们要么按照**(1)指定的方式调用singleLineCommands_Timeout(),要么按照(2)**指定的方式调用general_interactive()

在第**(3)(4)**节中,我们检查嗅探进程是否仍然存活,如果是,则将其终止:

start_sniffing()

我们通常使用 Wireshark 来捕获接口上的所有流量。然而,由于 Wireshark 是一个桌面应用程序,在这种情况下,我们将使用Tshark。Tshark 代表终端 shark,是 Wireshark 的 CLI 版本。Tshark 调用命令在第(2)部分中指定,我们指定要嗅探流量的端口。我们还指定需要嗅探流量的主机,或目标主机。我们指定主机和端口的原因是我们想要保持结果的完整性;工具的 GUI 版本可以部署在服务器上,并且多个用户可以使用它进行扫描。如果我们指定它应该在接口上嗅探,那么其他用户的其他运行会话的数据也会被嗅探。为了避免这种情况,我们对主机和端口非常具体。我们还指定了它嗅探的超时持续时间。我们将输出保存在指定的文件中"project_id_host_port_capture-output.pcap"

在第(2)部分,我们使用子进程模块调用tshark进程,这是我们之前讨论过的:

HTTP_based()

以下的http_based方法很简单。我们使用 Python 的请求库向目标发送 GET 请求,捕获响应,并将其保存在数据库中。目前,我们只是发送 GET 请求,但您可以在自己的时间内调整代码以处理 GET 和 POST。我们将在下一章节中更多地介绍 Python 请求和抓取:

IPexploits.py

服务扫描引擎的数据库层处理另一个重要的代码文件是IPexploits.py。这个文件很简单;它包含各种方法,每个方法的目的要么是从数据库表中获取数据,要么是将数据放入数据库表中。我们不会在这里讨论这个模块,但我建议你看一下可以在 GitHub 存储库github.com/FurqanKhan1/Dictator/blob/master/Dictator_service/IPexploits.py找到的代码。

执行代码

在执行代码之前,请仔细参考 GitHub 存储库github.com/FurqanKhan1/Dictator/wiki中的安装和设置说明。安装指南还讨论了如何设置后端数据库和表。或者,您可以下载预先安装和预配置了所有内容的即插即用的虚拟机。

要运行代码,请转到以下路径:/root/Django_project/Dictator/Dictator_Service。运行代码文件driver_main_class.py,如:python Driver_scanner.py。必须注意的是,结果是使用 Python 库生成的,该库将控制台输出转换为其 HTML 等效。更多细节可以在以下代码文件github.com/PacktPublishing/Hands-On-Penetration-Testing-with-Pythongenerate_results()方法中找到。

漏洞扫描器的服务扫描部分的数据库模式

要扫描服务扫描的扫描结果,请转到 IPexploits 表,其模式如下:

漏洞扫描器的 GUI 版本

先前讨论的相同代码库可以进行增强,以开发一个基于 Web 的漏洞扫描仪版本,具有端口扫描和服务扫描功能。该工具具有许多不同的功能,包括四层架构,其中包括 Web 层呈现、Web 层服务器、API 层和 DB 层。从 GitHub 存储库github.com/FurqanKhan1/Dictator/wiki下载并安装工具的 Web 版本。或者,您可以使用即插即用的虚拟机,只需登录并在https://127.0.0.1:8888上打开浏览器即可访问该工具。

扫描仪 GUI 版本的各种功能包括以下内容:

  • 并行端口扫描

  • 暂停和恢复端口扫描

  • 服务扫描

  • 所有测试用例自动化

  • 暂停和恢复服务扫描 (不在 CLI 中)

  • 并行服务扫描 (不在 CLI 中)

  • Nmap 报告上传和解析 Qualys 和 Nessus 报告

使用[PTO-GUI]

以下部分将介绍扫描仪的 GUI 版本的用法。

扫描模块

基于正在进行的基础设施上的扫描类型和性质,渗透测试人员有多种可用选项,并且可以选择最适合被测试基础设施的选项。可用的各种使用模式在以下部分中进行了介绍。

顺序模式

在顺序模式中,工具将从发现开始,然后重新配置,然后开始服务扫描。因此,这是一个三步过程。请注意,在顺序模式中

  • 在所有主机都被扫描之前,无法开始服务扫描

  • 一旦服务扫描开始,就无法重新配置

  • 一旦开始服务扫描,所有服务都将开始扫描。用户无法控制先扫描哪个服务,后扫描哪个服务

发现完成后重新配置

为了减少误报和漏报,请分析端口扫描结果,如果需要,重新配置/更改它们。如果有任何服务/端口被遗漏,您还可以额外添加测试用例。

在上述截图中,我们将类型为状态的服务更改为ftp类型。因此,测试用例将为ftp运行。注意:只有在确定发现的服务不正确或类型为Unknown时才这样做。我们将很快了解服务类型。

如果 nmap 错过了主机/端口/服务,可以手动添加,如下所示:

添加测试用例后,我们可以点击“开始扫描”选项开始服务扫描。我们可以选择启用线程选项以加快结果的速度,也可以选择不使用线程选项开始服务扫描,如下图所示:

查看中间结果:当用户点击“开始扫描”时,他/她将被重定向到扫描页面。每次执行一个测试用例,UI 都会更新,并且一个蓝色的图标会出现在正在扫描的服务前面的屏幕上。用户可以点击该图标查看测试用例的结果。

当服务的所有“测试用例”都被执行时,图标将变为绿色。

以下图显示了中间测试用例的结果:

在任何时候,用户都可以离开 UI 而不会影响正在运行的扫描。如果用户希望查看当前正在运行的扫描,可以从顶部的“扫描状态”选项卡中选择正在运行的扫描。将显示以下屏幕:

根据扫描的状态,它将显示适当的操作。如果扫描正在进行中,操作列将显示进行中。用户可以点击此按钮以获取其扫描当前状态的 UI 屏幕。

用户可以点击扫描名称以查看扫描最初启动时的配置(主机、端口、开关)。

并发模式

在顺序模式中,直到所有端口的端口扫描结果可用并且主机已经扫描完毕,服务扫描才能开始。因此,渗透测试人员可能需要等待获取这些结果。此外,在此模式下,渗透测试人员无法控制哪些服务可以先扫描,哪些可以稍后扫描。所有服务将一次性扫描,限制了对服务扫描的控制粒度。这些是并发模式处理的顺序模式的限制。

并发模式提供了在服务发现完成后立即启动服务扫描的灵活性,并进一步提供了根据渗透测试人员选择启动选择性服务扫描的选项。

  1. 点击扫描选项卡下的新扫描选项卡。

  2. 填写扫描参数,并选择并发扫描模式:

  1. 其余步骤将相同,唯一的例外是在此扫描模式中,用户无需等待所有主机和端口都被扫描才能开始服务扫描。此外,用户可以选择希望扫描哪些服务。如下图所示:

如前面的屏幕截图所示,用户可以选择先扫描http,而不立即扫描 ssh。用户可以决定何时扫描哪项服务。

并发模式也具有重新配置、查看结果等所有功能。

顺序默认模式

使用此模式,服务扫描将在发现完成后立即开始,从而跳过重新配置阶段。此模式的实用性在于调度扫描的情况下更为相关,渗透测试人员可以安排扫描在其他时间开始,并且可能无法进行重新配置,同时希望继续使用默认的端口扫描结果进行服务扫描。因此,此扫描模式跳过重新配置阶段,并在获取默认的nmap端口扫描结果后直接启动服务扫描。

  1. 点击扫描选项卡下的新扫描选项卡

  2. 填写扫描参数,并选择顺序默认扫描模式

当端口扫描结果完成后,它将自行开始服务扫描,无论用户当前是否已登录。

暂停和恢复扫描

无论扫描模式如何,任何处于发现或服务扫描状态的扫描都可以暂停。中间结果将被保存,用户可以随时在将来恢复扫描。

必须注意,如果在发现过程中暂停扫描(端口扫描可能正在进行),则已经扫描的端口的端口扫描结果将被保存;用户恢复后,将对未扫描的端口进行扫描。同样,如果在服务扫描过程中暂停扫描,则已经扫描的服务的结果将被保存,用户可以灵活分析将要扫描的服务的结果。扫描恢复后,将对未扫描的服务进行服务扫描。

以下屏幕截图显示了如何暂停正在进行的扫描:

要恢复扫描,可以转到当前扫描选项卡或暂停的扫描选项卡。默认情况下,操作列会有两个按钮:

  • 恢复:这将从暂停的状态恢复扫描。

  • 分析:如果扫描在扫描时暂停,渗透测试人员可以分析已经扫描的服务的结果。如果您希望恢复扫描,那么他/她可以选择分析选项。通过这个选项,用户可以看到已完成服务的中间测试用例结果。

如果扫描在端口扫描期间暂停,分析选项可能不会出现,因为如果端口扫描正在进行并且模式不是并发的话,就不会执行test_cases来分析。分析选项不会出现在并发扫描中,恢复按钮将执行并发模式中的恢复和分析扫描的联合功能。

下载报告或分析扫描何时完成

当扫描完成时,用户将在 UI 上获得全部下载的选项。如果用户访问当前扫描选项卡,对于所有发现和服务扫描状态为完成的扫描,操作列将默认具有下载结果的选项,以进行离线分析或在线分析结果,如下图所示:

点击全部下载,将下载一个压缩文件夹。它将包括:

  • 包含所有测试用例结果的最终 HTML 报告。

  • Pcap 文件可以嗅探需要嗅探的某些服务。Pcap 文件可以用 Wireshark 打开并分析文本/凭据是以明文还是加密格式传递的。注意:Pcap 文件的名称将类似于<project_id>_capture_output.pcap。因此,如果在host1上对端口21和项目 ID100进行嗅探,Pcap 文件名称将是100_host1_21_capture_output.pcap

  • 下载的文件夹还将包含最终选择的配置(服务-测试用例),用于启动扫描(JSON 格式)

  • 另一方面,点击分析测试将带我们到用户界面,我们可以在那里看到所有test_cases的结果。

报告

要上传 Nmap 报告,请转到上传报告并选择 Nmap 报告。这是一个结果导入模块,可以读取现有的Nmap.xml报告文件中的结果,并将这些发现导入到我们的自定义数据库中,并进一步使用这些发现来启动测试用例/服务扫描。因此,这使用户可以在两种模式下使用我们的工具:

  • 发现和服务扫描一起

  • 仅服务扫描模式

点击上传,报告将被解析和上传。用户可以转到当前扫描选项卡,会发现已上传的项目test_upload_nmap列在那里,其发现状态完成服务扫描状态为未完成。用户可以点击操作选项卡进行中,重新配置结果,然后开始服务扫描。

  • Qualys 和 Nessus 报告解析器

要使用此选项,请转到上传报告选项卡,并选择Qualys/Nessus报告。我们有一个报告合并模块,可以合并从 Qualys、Nessus 和手动测试用例获得的结果。为了合并报告,它们必须首先被解析。我们有 Qualys、Nmap 和 Nessus 报告解析器。它们都将以 XML 格式接收报告,并解析报告并将其放置在本地存储中,以便查询和将结果与其他报告集成变得更容易:

在这里上传报告的目的是将其与某个手动项目合并。因此,从下拉列表中选择要将 Nessus/Qualys 报告合并的项目。

  • 报告合并:

要使用此选项,请转到合并报告选项卡,并选择您希望将 Qualys 和 Nessus 结果集成的手动项目的ID/名称

它假定 Nessus 和 Qualys 报告已经被上传并链接到它们应该合并的项目。

该模块合并了手动测试用例、解析的 Qualys 报告、解析的 Nessus 报告,并将 CVE 映射到利用,最后,将为用户提供下载集成报告的选项,格式包括(XML、HTML、CSV、JSON),从而提供一个统一的分析视图。

最终可下载的报告有四种格式(HTML、CSV、JSON、XML)。

合并报告将根据 Nessus/Qualys 和手动测试用例中发现的共同结果进行合并。它将共同的主机和端口聚合到一组中,以便分析变得更容易。

摘要

在本章中,我们讨论了如何使用各种 Python 模块来实现服务扫描自动化的任务。我们还研究了如何使用线程和多进程的组合来解决现实世界的问题。本章讨论的所有概念在前几章中都有所提及。通过本章的学习,读者应该对 Python 在网络安全领域有多么强大以及我们如何使用它来创建自己的扫描器有了很好的理解。我们还在 GUI 模式下概述了漏洞扫描器。

在下一章中,我们将看到如何使用机器学习和自然语言处理来自动化渗透测试阶段的手动报告分析。

问题

  1. 为什么我们不使用 msfrpc 来自动化 Metasploit?

  2. 我们可能还可以做些什么来进一步优化吞吐量?

  3. 使用 JSON 文件是强制性的吗?我们可以使用数据库吗?

  4. 我们还可以将哪些其他工具与扫描仪集成?

进一步阅读