面向渗透测试者的-Bash-脚本编程-一-

84 阅读1小时+

面向渗透测试者的 Bash 脚本编程(一)

原文:annas-archive.org/md5/cf34f1eb5597431cf072cd50824c69f9

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Bash shell 脚本编写是渗透测试人员工具包中的基本技能,可以实现复杂安全评估、漏洞分析和利用任务的自动化。本书提供了一本专门为渗透测试而编写的 Bash 脚本编写的全面指南,涵盖了从基本脚本概念到逃避检测和与人工智能等现代技术集成的高级技术。

本书分为三个部分,引导读者从基础概念,通过实际的渗透测试应用,到高级主题。您将学习如何利用 Bash 进行侦察、Web 应用程序测试、网络基础设施评估、权限提升和维持持久性。本书强调通过实际示例和渗透测试人员在日常工作中遇到的真实场景进行实践学习。

本书适合的读者

本书面向几个关键受众群体设计:

  • 希望使用 Bash 自动化其工作流程的安全专业人员和渗透测试人员

  • 希望增强其安全测试能力的系统管理员

  • 对开发自定义工具和脚本感兴趣的安全研究人员

  • 旨在将安全测试整合到其流水线中的 DevSecOps 从业者

  • 寻求在自动化方面建立坚实基础的学生和渴望成为渗透测试人员的人

对 Linux/Unix 系统和命令行界面的基本熟悉有所帮助,但不是必需的,因为本书从基本概念到高级技术构建。您必须具备创建虚拟机和安装 Kali Linux 操作系统的知识和计算资源。

本书涵盖内容

第一章Bash 命令行 及其 黑客环境,在渗透测试的背景下向您介绍 Bash shell 脚本的基础知识。它涵盖了选择正确的操作系统、配置您的 shell 环境以及设置必要的渗透测试工具。

第二章文件和目录管理,深入探讨了文件和目录的操作,涵盖了导航、操作、权限和文件链接的基本命令 - 这些对于任何渗透测试人员都至关重要的技能。

第三章变量、条件、循环和数组,教授 Bash 中的核心编程概念,包括变量使用、决策结构和数据迭代技术。

第四章正则表达式,提供了一个深入介绍使用正则表达式进行模式匹配和文本操作的内容,这是解析工具输出和自动化数据分析的基本技能。

第五章函数和脚本组织,探讨如何使用函数创建模块化、可维护的脚本,涵盖了从基本函数创建到递归等高级技术。

第六章Bash 网络编程,专注于与网络相关的脚本编写,包括配置、故障排除和网络服务的利用。

第七章并行处理,教授了如何同时运行多个任务的技巧,这对于高效扫描和测试大规模目标环境至关重要。

第八章侦察与信息收集,展示了如何自动化发现目标资产,包括 DNS 枚举、子域发现和 OSINT 收集。

第九章使用 Bash 进行 Web 应用渗透测试,涵盖了自动化 Web 应用测试的技术,包括请求自动化、响应分析和漏洞检测。

第十章使用 Bash 进行网络和基础设施渗透测试,探讨了网络扫描、枚举和漏洞评估自动化。

第十一章Bash Shell 中的特权提升,教授如何使用 Bash 识别和利用特权提升机会的技巧。

第十二章持久化与转移,涵盖了如何保持对受损系统的访问并通过网络转移扩大访问范围。

第十三章使用 Bash 生成渗透测试报告,展示了如何自动化生成专业的渗透测试报告。

第十四章规避与混淆,探讨了在进行渗透测试时规避检测的技术。

第十五章与人工智能接口,展示了如何将 AI 功能集成到渗透测试工作流中。

第十六章Pentesters 的 DevSecOps,最后讨论了如何在 CI/CD 管道中实施安全测试,并在现代开发环境中自动化安全检查。

为了从本书中获得最大的收益

为了最大化从本书中的学习,您应该具备以下条件:

  • 理解基本的安全原理

  • 访问 Linux 环境(Kali Linux)以练习示例

  • 基本虚拟化概念的知识,包括创建和运行虚拟机的能力

  • 拥有足够资源的计算机硬件,能够同时运行两个虚拟机

书中涉及的软件/硬件操作系统要求
Kali LinuxLinux
Bash

拥有创建和运行虚拟机所需的知识和计算机资源是至关重要的。本书不涉及如何创建虚拟机和安装 Linux 的内容。

如果您使用的是本书的数字版本,我们建议您从本书的 GitHub 仓库获取代码(下节会提供链接)。这样可以帮助您避免与复制粘贴代码相关的潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,链接为 github.com/PacktPublishing/Bash-Shell-Scripting-for-Pentesters。如果代码有更新,它将会在 GitHub 仓库中更新。

我们还提供其他来自我们丰富书籍和视频目录的代码包,您可以访问 github.com/PacktPublishing/。快来看看吧!

使用的约定

本书中使用了若干文本约定。

文本中的代码:表示文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账户。以下是一个例子:“现在,每当您需要访问此目录时,您只需输入 cd $MY_DEEP_DIRECTORY,Bash 会立即带您到该目录。”

代码块设置如下:

 #!/usr/bin/env bash
if [ $USER == 'steve' ] && [ -f "/path/to/file.txt" ]; then
  echo "Hello, Steve. File exists." elif [ $USER == 'admin' ] || [ -f "/path/to/admin_file.txt" ]; then
  echo "Admin access granted or admin file exists."

任何命令行输入或输出均按以下方式书写:

 $ cd /home

粗体:表示新术语、重要词汇或屏幕上显示的词汇。例如,菜单或对话框中的词汇会以 粗体 显示。以下是一个例子:“在 模型设置 标签中,确保选择您的模型,并将 自由度 设置为 精确。点击 保存 按钮。”

提示或重要说明

如此显示。

免责声明

本书中的信息仅应以伦理方式使用。如果您没有设备所有者的书面许可,请不要使用书中的任何信息。如果您进行非法行为,您可能会被逮捕并依法追究责任。本书的出版商 Packt Publishing 以及本书的作者不对您滥用书中信息承担任何责任。书中的信息仅应在获得相关责任人书面授权的测试环境中使用。

与我们联系

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何内容有疑问,请通过电子邮件联系我们,地址是 customercare@packtpub.com,并在邮件主题中提及书名。

勘误:尽管我们已尽力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能向我们报告。请访问 www.packtpub.com/support/err… 并填写表格。

盗版:如果您在互联网上发现我们作品的任何形式的非法副本,我们将不胜感激您提供位置地址或网站名称。请通过链接 copyright@packt.com 与我们联系。

如果您有兴趣成为作者:如果您在某个专题上有专业知识,并且有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com

分享您的想法

一旦您阅读了《渗透测试人员的 Bash Shell 脚本编程》,我们很乐意听取您的想法!请 点击这里直接转到亚马逊评论页面 为这本书分享您的反馈。

您的评论对我们和技术社区至关重要,将帮助我们确保我们提供的内容质量卓越。

下载这本书的免费 PDF 副本

感谢您购买这本书!

您喜欢在路上阅读,但无法随身携带印刷书籍吗?

您购买的电子书是否与您选择的设备不兼容?

别担心,现在每本 Packt 书籍您都可以免费获得不带数字版权保护的 PDF 版本。

随时随地,在任何地方,使用任何设备阅读。直接从您喜爱的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

福利不止于此,您可以在每天的收件箱中获得独家折扣、新闻简报和优质免费内容的访问权限

遵循以下简单步骤获取这些福利:

  1. 扫描二维码或访问下面的链接

img

packt.link/free-ebook/9781835880821

  1. 提交您的购买凭证

  2. 就这样!我们将把您的免费 PDF 和其他福利直接发送到您的电子邮件中

第一部分:开始学习 Bash Shell 脚本编写

在本部分中,你将为渗透测试建立扎实的 Bash 脚本基础。从设置合适的黑客环境和配置 Bash Shell 开始,本节逐步讲解进行安全评估所需的文件和目录管理技巧。你将掌握核心编程概念,包括变量、条件语句、循环和数组,然后深入学习使用正则表达式进行模式匹配——这是解析安全工具输出的关键技能。接下来,本节介绍函数创建和脚本组织,确保你能够构建可维护的、专业级别的安全工具。进入网络基础知识后,你将学习 Bash 如何与网络服务和协议交互。本节最后介绍并行处理技术,帮助你开发高效的脚本,能够同时处理多个任务——这是大规模安全评估所必需的能力。在第一部分结束时,你将掌握编写复杂安全导向 Bash 脚本所需的所有基础技能。

本部分包含以下章节:

  • 第一章Bash 命令行黑客环境

  • 第二章文件和目录管理

  • 第三章变量、条件语句、循环和数组

  • 第四章正则表达式

  • 第五章函数和脚本组织

  • 第六章Bash 网络编程

  • 第七章并行处理

第一章:Bash 命令行及其黑客环境

在这一基础章节中,你将开始踏上进入 Bash shell 脚本世界的旅程,特别是面向渗透测试员(pentesters)。你将清楚地了解什么是 Bash,为什么它对渗透测试(pentesting)至关重要,以及如何设置你的脚本环境。通过实际的示例和解释,你将为成为网络安全领域的熟练 Bash 脚本编写者打下基础。

Bash 不仅仅是一个命令解释器——它是一个自动化我们每天在网络安全中遇到的复杂和繁琐任务的工具。在没有训练的人的手中,Bash 就像一根大棒,显得沉重、过于复杂且不舒服。而在那些能够看到其好处并投入时间去学习其细节的人手中,Bash 就像一把外科医生使用的手术刀,你可以像机器人专家一样使用它来切割数据并自动化渗透测试方法。

在本章中,我们将涵盖以下主要内容:

  • Bash 简介

  • 实验室设置

  • 配置你的黑客终端

  • 设置基本的渗透测试工具

技术要求

要跟随本章的练习,你需要一个 Linux 环境。本书假设你具备安装操作系统的能力,并熟悉虚拟机环境的安装与配置。如果你需要帮助来设置实验室环境,可以参考 VirtualBox 在线手册(Oracle VM VirtualBox 用户手册download.virtualbox.org/virtualbox/UserManual.pdf)和一些 YouTube 视频(VirtualBox – YouTube www.youtube.com/results?search_query=virtualbox)。

幸运的是,有许多免费的方式可以配置 Bash 学习环境。所有示例将使用 Kali Linux 展示。然而,任何 Linux 或 macOS 环境都可以使用。

“Kali Linux 是一个开源的、基于 Debian 的 Linux 发行版,旨在进行各种信息安全任务,如渗透测试、安全研究、计算机取证和逆向工程。”Kali Linuxwww.kali.org/

我强烈建议你使用一个全新的 Kali Linux 虚拟机来跟随本书中的练习或进行渗透测试。在本书和你的渗透测试过程中,你将安装很多工具及其依赖项。工具依赖项之间的冲突是常见的,这种情况被称为依赖地狱。如果在安装过程中没有正确地隔离这些工具,可能会对你的主系统造成损害。你也不希望冒险让恶意软件感染你的主系统。

Kali 提供了多种解决方案。它们提供安装镜像、虚拟机、云镜像和Windows 子系统 for Linux(WSL) 包。

你可以从 www.kali.org/get-kali/#kali-platforms 下载 Kali。

本章中将使用的所有命令可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Bash-Shell-Scripting-for-Pentesters/tree/main/Chapter01

Bash 简介

Bash,亦称为 Bourne Again Shell,是一个命令行 shell 解释器和脚本语言。Bash 由 Brian Fox 于 1989 年创建,作为 Bourne shell(一个专有软件)的自由软件替代品。(Bash – GNU 项目 – 自由软件基金会,www.gnu.org/software/bash/)。它是最常见的 Linux shell。Bash 还引入了将多个命令组合成脚本的功能,用户只需输入一个命令即可执行这些脚本。

当你在 Linux 系统中打开终端并输入命令时,你的 Bash shell 管理着与操作系统的交互以及运行可执行文件和脚本。Bash 和 Linux 可执行文件形成了共生关系,彼此增强对方的功能性和效率。Bash 作为用户和脚本与 Linux 内核(操作系统的核心)互动的门户。它解析用户命令,无论是直接输入到终端还是写入文件中的脚本,并在系统中启动相应的操作。另一方面,Linux 可执行文件是执行这些操作的“工作马”。它们是二进制文件,通常使用 C 或 C++ 等编程语言编写,并被编译以在 Linux 系统上高效运行。当用户在 Bash 中发出命令时,通常涉及调用一个或多个可执行文件来执行某项任务。

以下是 Bash 的一些功能:

  • 带参数的命令执行:命令可以是二进制文件、内建的 shell 命令或脚本。

  • 命令补全:这是一个通过按下 Tab 键自动补全部分输入的命令或文件名的功能,帮助用户提高效率。

  • 命令历史:命令历史允许你快速重用先前在 shell 中输入的命令。

  • 作业控制:将命令发送到后台并将其带到前台。

  • Shell 函数和别名:函数将相关代码组织成一个可以在需要时调用的名称。别名允许用户将复杂命令简化为单个名称。

  • 数组:数组将元素存储在一个列表中,我们可以在之后检索和处理这些元素。

  • 命令与大括号扩展:命令扩展使用一个命令的结果作为另一个命令的输入。大括号扩展允许生成字符串。

  • 管道与重定向:一个命令的输出作为另一个命令的输入。

  • 环境变量:动态值被分配到标签名,这些标签通常用于表示系统配置或存储环境相关信息。

  • 文件系统导航:Bash 提供了更改目录、打印当前目录以及查找文件和目录的命令。

  • 帮助man命令,简写为manual,为用户提供有关如何执行命令的信息和示例。

Bash 脚本是我在渗透测试生涯中学到的最重要的技能之一,并且我每天都会使用。当你在开发应用程序时,某一时刻你可能会需要使用其他脚本语言,例如 Python。在大多数情况下,你在终端中想做的任何事情,都可以通过 Bash 完成,Bash 负责协调输入输出以及解析来自多个工具的数据。Bash 与 Linux 操作系统紧密集成,因此在学习其他脚本语言如 Python 或 Ruby 之前,学习 Bash 是非常有意义的。尽管我知道多种脚本和编程语言,但由于 Bash 与 Shell 的紧密集成以及通过一行甚至一个单词的命令快速获得结果的便利性,Bash 仍是我最常使用的工具。

在我作为渗透测试员的工作中,每天我都会使用 Bash 来解析数据或自动化将多个工具串联在一起。当客户给我提供范围数据时,我通常需要从参与规则文档、电子邮件或 Excel 表格中复制一个范围内的 IP 地址或主机名列表,并粘贴到文本文件中。数据中难免会有多余的字符,或者数据的格式不适合用作扫描目标列表。我可以使用 Bash 清理文件数据,并通过在终端中输入一行简单的代码,将其格式化成我需要的样子以便测试。

渗透测试工具接受各种格式的数据,并输出扫描结果或以常见格式如 XML、JSON 或纯文本显示的数据。纯文本输出可能会通过多个空格、制表符或两者的组合进行格式化。我通过管道将源文件的内容传递到 Bash 管道中,以解析、清理、重新格式化并排序数据。我可能会使用一组合适的 Bash 命令,在一个命令的输出和另一个命令的输入之间执行这些操作,形成自动化管道。Bash 确实是渗透测试工具箱中不可或缺的工具。

以下是我在渗透测试工作流中常用的一些 Bash 脚本应用:

  • 自动化网络扫描:我经常处理 Masscan(一个快速的 TCP 扫描器)的输出,并将其输入到 Nmap 中进行深入的服务检测和脚本扫描。

  • 密码破解:我使用一个 Bash 脚本,执行一系列复杂的密码破解功能,涉及破解 Microsoft LM 和 NTLM 哈希值,并将 Hashcat 的输出格式化,以便输入到报告工具中。

  • 文本搜索:在文本中搜索 IP 地址或其他细节。

  • 范围自动化:我使用子域枚举工具和 Bash 脚本,确保发现的子域在渗透测试的规则范围内。

  • 数据格式化:我使用 Bash 解析和重新格式化 Nuclei 扫描的输出,从 TLS 证书中枚举子域和 Web 应用,并重新格式化数据以用于绕过 内容分发网络CDN)绕过 Web 应用防火墙WAF)并直接扫描目标。

  • 搜索和排序 Nmap 报告:在扫描数百或甚至数千个 IP 地址后,我使用 Bash 解析 gnmap 文件,创建包含按 TCP 或 UDP 端口组织的目标的文本文件,以便进行更有针对性的扫描。例如,所有的 SMB 服务器或 HTTP 服务器的 IP 地址会被提取并放入名为 smb.txthttp.txt 的文件中。

  • 排序数据和去重:将唯一的 IP 地址按顺序放入一个文件中进行去重。

  • 数据转换:将姓名转换为多种格式用于密码喷洒。如果我能通过 开源情报OSINT)获取员工姓名列表,我会查看任何可能提示他们的 Active Directory 姓名格式的信息,例如 f.lastfirst.last,然后使用 Bash 按适当的格式转换姓名。

  • 数据过滤:有时,我需要从工具的输出日志文件中删除终端颜色代码,以便用于报告,因为我忘记了包括不显示颜色的命令行标志,或者该工具可能没有此选项。我不想为客户的报告截图时包含颜色代码,这会使数据难以阅读。

  • 数据迭代:我使用 Bash forwhile 循环来遍历文件,并对每一行运行一个命令。一个典型的例子是当你需要使用一个每次只能扫描一个主机的工具,且该工具没有处理多个目标的选项时。

我相信学习 Bash 脚本会让你在工作中更加高效,节省时间。当你能用 Bash 自动化那些时间密集、无聊的任务时,你就可以腾出时间去做更重要的事。难道不是很棒吗?你可以有更多时间用于学习或研究,而不是浪费在那些可以用很少的努力就能自动化的手动任务上。

现在我们对 Bash 有了基本了解,也明白了它在渗透测试中的用处,接下来我们来看看如何搭建一个实验室环境,在这里你可以安全地学习并跟随我的练习。下一节我们将介绍如何搭建实验室环境,以便你能跟着我一起学习。

实验室搭建

Bash 不是 Linux 和 Unix 系统上唯一的 Shell 解释器,但它是最常见的。其他 Shell 受到了 Bash 的影响。你可能还会在 macOS 和 Kali Linux 上遇到 Zsh。

你可能会好奇,尽管一些操作系统已经转向 Zsh,本书为何仍专注于 Bash。虽然 macOS 和 Kali 在新用户账户中已经转为使用 Zsh,但它们依然安装有 Bash。大多数为 Bash 编写的代码在 Zsh 上也能运行,只需做一些小的调整。你可以在脚本中加入 shebang 行,确保在多个 shell 系统中使用 Bash 解释器来执行脚本。在进行安全评估时,你很可能会遇到 Bash 是默认 shell 的 Linux 服务器。对于渗透测试人员来说,理解如何与 Bash 交互以利用应用程序、提升权限和横向移动是至关重要的。

幸运的是,你可以通过多种方式免费访问 Bash shell。本节将探讨在理想环境中访问 Bash shell 的各种方式,这样你可以跟随学习并掌握如何使用 Bash 进行渗透测试。我们还将探索一些易受攻击的实验环境,你可以在这些环境中安全地练习使用 Bash 和渗透测试工具。

虚拟机是跟随本书活动以及进行渗透测试时的首选方式。你可能会想在同一系统上安装渗透测试工具和漏洞利用代码,来处理日常的业务或个人事务。但安装各种工具的软件先决条件很容易破坏你的系统。黑客工具中可能包含恶意软件,感染你日常用来发送邮件或上网的系统。虚拟机提供了一个便捷的沙盒环境,所有你需要的内容都能快速恢复或更换测试环境。我在所有示范中都选择使用 Kali Linux。我们希望避免在日常工作或个人使用的同一系统中安装渗透测试工具和漏洞利用代码。最好使用一个干净的测试环境,以避免产生软件依赖问题。Kali 让安装与渗透测试相关的软件包变得非常简单。

虚拟机

使用 虚拟机 是首选方法。在进行渗透测试时,你很可能会安装大量的工具和漏洞利用概念验证代码。到某个阶段,你还可能会保存客户或目标的敏感数据。虚拟机提供了一个便捷的容器,你可以对其进行快照并恢复,或者在评估后轻松删除和替换。

有许多免费和付费的虚拟化解决方案,可以满足各种需求:

  • Oracle VirtualBox 是一款免费的 x86 虚拟化管理程序。它适用于 Windows、macOS(Intel 芯片组)和 Linux。VirtualBox 用户友好,深受初学者和专业人士的喜爱。它支持广泛的客户操作系统,并提供如快照、无缝模式和共享文件夹等功能。

  • VMware 提供了一款免费的虚拟化软件版本,名为 VMware Workstation Player,仅供非商业使用。它与 Windows 和 Linux 主机兼容。Workstation Player 使用简便,支持 VMware 的 VMDK 虚拟磁盘格式,并且与其他 VMware 产品创建的虚拟机兼容。

  • Microsoft Hyper-V 在 Windows 10 Pro、Enterprise 和 Education 版本中是免费的。虽然它更常用于服务器环境,但 Hyper-V 也可以作为 Microsoft Windows 主机上的桌面虚拟化的良好选择。

提示

对于使用 Apple CPU 的 macOS 用户,您的虚拟化选项有 UTM、Parallels 和 VMWare Fusion。UTM 是唯一免费的选项。

Docker 容器

Docker 容器相比虚拟机提供了一个轻量级的选择。Docker 为 Windows、Linux 和 macOS 提供运行时。由于 Docker 使用主机的内核,它不需要像传统虚拟化软件那样虚拟化硬件,因此在低端硬件上,容器比虚拟机更加轻量和高效。

由于 Docker 使用主机的内核,您只能在与主机相同的操作系统上运行容器。Docker Desktop 是一个替代方案,它使用虚拟机运行与主机操作系统不同的容器。

根据我的经验,使用 Docker 有一些积极和消极的因素需要考虑。

Docker 更加轻量,是传统虚拟机的良好替代方案,尤其是在硬件不够强大的情况下。我为运行 Kali Linux 的虚拟机分配的最小硬件资源是 4 GB 的内存和 40 GB 的磁盘空间。您并不总是会使用到这 4 GB/40 GB 的资源。同时,除非您关闭虚拟机、调整内存并扩展磁盘,否则您会被限制在这些数值范围内。Docker 容器以本地进程运行(不包括 Docker Desktop),因此它只会使用运行容器所需的内存和磁盘空间。

在 Linux 主机上,您可以将容器直接附加到主机网络,并根据需要打开和关闭端口,前提是您包含特定的命令行参数。这使您可以在不停止和启动容器的情况下,动态地打开主机网络适配器上的监听服务器端口。您还可以将容器附加到 USB 或串行端口,以便与硬件设备进行交互。当我需要运行一个与 USB 或串行设备接口的旧 Python2 渗透测试应用程序,用于无线频率和硬件黑客攻击时,我有时会使用这个选项。

使用 Docker Desktop 时,NAT 用于将容器网络端口连接到主机网络,因此,如果需要关闭或打开额外的端口,必须停止并重新启动容器。在 Docker Desktop 中,无法将容器附加到硬件设备。当你在容器中配置了应用程序及其依赖项,之后摧毁容器并启动一个新实例时,你可能会失去工作进度并需要重新开始,特别是当你只是想为反向监听器或服务器应用程序打开另一个 TCP 端口时,这会让人感到沮丧。

总结来说,我更倾向于仅在 Linux 主机上使用 Docker,并且我将其用于三种特定的渗透测试场景:

  • 它提供了一种轻松隔离旧版 Python 2 应用程序并避免依赖地狱的方式。对于所有 Python 2 和 3 版本,Docker 都有官方容器。

  • 我用它来创建和运行通过我的包管理器无法获得的应用程序,并且我想避免浪费时间解决依赖问题。例如,某个黑客工具可以通过 Kali 软件库获得,但在 Ubuntu 中无法使用。我可以创建一个简洁的 Kali 容器,只使用足够的资源来运行其中的应用程序,并在 ~/.bashrc 文件中使用别名,将冗长的 docker run 命令缩短为我可以在终端中输入的单个单词。当我只想运行一个无法或难以在主机系统上运行的应用程序时,这是比沉重的虚拟机更快、更轻量的选择。

  • 当我想练习利用或创建针对最近公布的易受攻击的 web 应用程序的利用工具时,我通常可以找到一个 Docker 容器,让我立即启动这个易受攻击的应用程序,而无需花费宝贵的时间进行安装和配置。

Docker 容器非常适合特定的使用场景。然而,它们不如虚拟机更受青睐。接下来,我们将探讨将 live USB 系统作为虚拟机和容器的替代方案。

Live USB

Live USB 是一种将操作系统镜像写入 USB 硬盘的方式,使其可以启动。Live USB 是当计算机没有足够的硬件资源运行虚拟机时的一个不错选择。你可以使用镜像软件将 Linux ISO 镜像刻录到 USB 并启动 Linux 操作系统。在 Linux 上完成工作后,你只需重新启动计算机并拔掉 USB 驱动器,即可恢复到已安装的操作系统。某些 Linux 发行版允许你在 USB 驱动器上创建持久存储,这样你在重启时就不会丢失更改。

以下是通过 live USB 运行 Linux 发行版的一些常规步骤:

  1. 下载 ISO 镜像。一些受渗透测试者喜爱的 Linux 发行版包括 Kali、Parrot Security OS 和 BlackArch。

  2. 创建一个 live USB 驱动器。常用的工具包括 Rufus、balenaEtcher 和 Linux 的dd命令。

  3. 配置持久性(可选)。这通常涉及在 USB 驱动器上创建一个独立的分区,并配置引导加载程序以识别并使用此分区。你可以在 www.kali.org/docs/usb/usb-persistence/ 找到创建 live USB Kali 系统所需的文档步骤。

使用 live USB 时有一些考虑事项和缺点:

  • USB 存储通常比直接从 SSD 运行要慢。如果你使用 live USB,请确保使用 USB 3.0 或 3.1 标准,以获得最佳性能。

  • 始终从官方来源下载 ISO 镜像,并在信任之前验证其校验和。

  • 如果你计划将其用于生产环境,请务必使用加密的持久性存储,因为存在将驱动器上的敏感数据暴露给未授权人员的风险。

现在,让我们继续讨论基于云的系统。

基于云的系统

许多云平台为访问具有足够资源以应对适度工作负载的 Linux 系统创建了免费层。提供免费层的云服务商包括Google Cloud PlatformGCP)、Microsoft Azure 和 Amazon EC2。请注意,免费层可能不足以提供生产环境所需的 RAM,且不适合运行 Kali Linux 镜像。

Kali Linux 提供了用于在 AWS、Digital Ocean、Linode 和 Azure 上运行的文档和市场镜像(www.kali.org/docs/cloud/)。我有与客户合作的经验,他们在云中配置 Kali 以进行云安全评估,或通过 VPN 连接到其内部网络基础设施以促进内部网络渗透测试。如果客户的内部网络已经通过 VPN 与云服务提供商连接,那么他们可以相对轻松地启动 Kali 镜像并创建防火墙规则,允许从我的 IP 地址进行 SSH 访问。现在我们已经探讨了使用 Bash 运行渗透测试系统的选项,让我们来发现一些可以在实验室中使用的易受攻击的系统,以进行实践。

易受攻击的实验室目标

在后续章节中跟随一些与渗透测试方法论相关的内容时,能够访问易受攻击的目标会对你运行命令并开发 Bash 脚本有很大帮助。你可以从几个很好的来源获取易受攻击的目标,用于在你的实验室中进行实践。

Metasploitable 2 是由 Rapid7 提供的一个易受攻击的虚拟机。它的设计目的是展示 Metasploit 框架的功能。Metasploitable 2 也是一个很好的初学者挑战,帮助你发展黑客方法论,并学习用于渗透测试的 Bash。该项目需要适度的资源来运行虚拟机,并包含有关机器漏洞的文档(Metasploitable 2 | Metasploit Documentationdocs.rapid7.com/metasploit/metasploitable-2/)。

活跃目录游戏(GOAD)也是一个选项。

“GOAD 是一个渗透测试的 Active Directory 实验室项目。该实验室的目的是为渗透测试人员提供一个易受攻击的 Active Directory 环境,供他们练习常见的攻击技术。” (Active Directory 的游戏 – Orange-CyberDefensegithub.com/Orange-Cyberdefense/GOAD)

请注意,GOAD 是免费的,并且使用免费的微软 Windows 许可证,激活期限为 180 天。GOAD 是我找到的用于在内部 Active Directory 网络环境中练习黑客技术的最佳资源。

MayFly 是 GOAD 的创建者。他们的网站上有大量关于如何在不同虚拟机虚拟化平台上设置 GOAD 的文章,以及使用常见渗透测试工具进行 Active Directory 攻击的实验室指南。

提示

MayFly 还发布了一个全面的 Active Directory 渗透测试思维导图。尽管我在黑客攻击 Active Directory 上有多年的经验,但我仍然发现自己有时会用尽测试的项目,并且在遇到卡壳或者想确保自己没有遗漏任何地方时,我会参考这张思维导图。这张思维导图也是我推荐给初学渗透测试的年轻渗透测试员的首要资源,帮助他们学习 Active Directory 黑客技术和工具(你可以在 orange-cyberdefense.github.io/ocd-mindmaps/img/pentest_ad_dark_2022_11.svg 上查看更多细节)。

如果你希望在 web 应用程序上练习 Bash 脚本、工具和方法,OWASP Juice Shop 是一个很好的资源。

“OWASP Juice Shop 可能是最现代、最复杂的不安全 web 应用程序!它可以用于安全培训、意识演示、CTF 以及作为安全工具的试验品!Juice Shop 包含了整个 OWASP 前十漏洞(owasp.org/www-project-top-ten/)的漏洞,以及在现实世界应用程序中发现的许多其他安全缺陷!” (OWASP Juice Shop – OWASP 基金会owasp.org/www-project-juice-shop/)

一个较老但仍然非常相关的脆弱 web 应用程序是 Mutillidae II。

“OWASP Mutillidae II 是一个免费的开源故意设计为脆弱的 web 应用程序,旨在为 web 安全培训提供目标。它包含了数十个漏洞,并提供了帮助用户的提示,是一个易于使用的 web 黑客环境,专为实验室、安全爱好者、课堂、CTF 和漏洞评估工具目标设计。” (OWASP Mutillidae II – OWASP 基金会owasp.org/www-project-mutillidae-ii/)

我喜欢 Mutillidae 的一件事是它在内容中嵌入了提示、教程和视频教程。Mutillidae 是我很多年前作为初级渗透测试员时学习 Web 应用测试的资源。Juice Shop 和 Mutillidae 之间的区别在于,Juice Shop 是一个现代化的 Web 应用,使用了 JavaScript 框架,而 Mutillidae 是一个更传统的 Web 应用。虽然 Juice Shop 有一个排行榜,并且你可以在线找到第三方的教程,Mutillidae 在应用中嵌入了大量的培训文本和视频。

网络安全领域始终在变化,新的漏洞定期被发现。实验室设置是进行研究和开发的理想场所,它让你能够安全地实验这些漏洞。这里是你通过发现新漏洞或改进现有渗透测试方法来为网络安全社区做出贡献的地方。

现在我们已经探索了渗透测试实验室中的易受攻击目标,接下来,我们将讨论如何定制你的 Bash shell,以便它符合你的需求和个人风格。

配置你的黑客 shell

如果你正在使用 Kali Linux 或 macOS,请注意,默认情况下你的终端 shell 使用的是 Zsh,而不是 Bash。Zsh 有更多功能(例如更好的标签补全和主题支持),但 Bash 更加广泛且标准。Bash 自 80 年代末期就已经存在,是 shell 领域的老将。它是大多数 Linux 发行版和 macOS(直到 Catalina 版本,Zsh 才接管)的默认 shell。Bash 的长寿意味着它极为稳定并且得到良好的支持。

Zsh,相比之下,稍晚一些出现。它以比 Bash 更优秀的交互性和更强大的脚本功能而闻名。

你可以通过在终端中输入echo $SHELL命令来确定配置了哪个 shell。书中展示的几乎所有代码都可以在 Bash 和 Zsh 中运行,除非特别注明。在我的日常渗透测试活动中,我很少注意到任何区别。然而,如果你想将 shell 从 Zsh 更改为 Bash,可以在终端中执行chsh -s /bin/bash命令,然后登出并重新登录,以查看更改生效。

Bash 配置文件可以在用户的主目录/home/username 中找到。由于文件名以点(.)开头,它们通常被称为dotfiles。以下配置文件用于配置 Bash shell:

  • ~/.bash_profile : 此文件在交互式登录时执行,用于初始化用户环境。可以把交互式登录看作是通过命令行登录,例如通过 SSH 会话访问一个基于文本的终端。

  • ~/.bashrc : 此文件用于在通过图形用户界面GUI)登录时配置终端。此文件包含别名、函数、提示符定制和环境变量等设置。

  • ~/.bash_logout:当你的会话结束时会执行此文件。它用于执行与注销时清理环境相关的任务。

提示

如果你不理解波浪号(~)字符和点文件名前的斜杠的作用,波浪号字符代表用户的主目录。~/.bashrc 路径相当于 /home/username/.bashrc。这个概念将在 第二章 中讲解。

你最常见的编辑包括添加别名和函数,以及自定义 ~/.bashrc 文件中的命令提示符。别名是一个很好的方法,可以将长的或复杂的命令缩短为一个单词。函数更为复杂,可以把函数看作是一个短脚本,你可以将其包含在 shell 配置中,并通过名字在终端中调用。函数将在稍后的 第五章 中讲解。

下面是我在~/.bashrc文件中用于搜索文本中 IP 地址的别名示例:

 alias grepip="grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}'"

你可以看到这个命令很难记住,所以创建一个别名对于任何你可能需要反复使用的复杂命令是很有帮助的。

重要提示

当你编辑 Bash 配置文件时,你必须退出并重新登录,或者获取文件以使更改生效。

输入以下命令以获取文件并立即生效:

 $ source ~/.bashrc

现在你已经理解了 Bash 点文件的作用,让我们继续了解如何编辑它们来个性化我们的环境。

自定义 Bash 提示符

提示符是你在 Bash 终端中输入命令的地方。你的提示符可以简单或复杂,取决于你的喜好和个性。可以把你的提示符设计选择想象成画家挑选调色板的方式。

你可以通过在 ~/.bashrc 文件中查找以 PS1 开头的行来找到当前配置的提示符。一个常见的 Bash 提示符会使用类似 export PS1="\u@\h \w\$ "PS1 值,它会在提示符处显示为 username@hostname ~$。让我们来拆解一下。以下是每个部分的作用:

  • \u 会被当前用户名替换。

  • @ 是一个字面字符,会出现在用户名之后。

  • \h 会被主机名替换,直到第一个句点为止。

  • \w 会被当前工作目录替换,$HOME 会被缩写为波浪号字符。

  • \$ 显示普通用户的 $ 字符,或显示根用户的 # 字符。

一旦你编辑了 PS1 提示符,记得获取文件,以便看到更改生效。

你也可以非常炫酷地定制你的提示符。我曾经在我的PS1提示符中插入$(ip a show eth0 | grep -m 1 inet | tr -s ' ' | cut -d ' ' -f 3)字符串,用来显示我的 IP 地址,以便在我的日志或报告截图中捕捉,并帮助客户将我的活动与他们的安全信息和事件管理SIEM)警报关联起来。请参见bash-prompt-generator.org/ 了解图形化的 Bash 提示符生成器,或者查阅官方 Bash 手册获取所有选项。

定制 Bash 环境就是让你的终端为你工作。这是一个不断试验和改进的过程,找出什么能让你更高效,什么能为你的命令行会话带来一点乐趣。从小处着手,实验一下,看看一些改变能如何大大改善你每天的任务。

设置基本的渗透测试工具

在这一部分,我们将介绍通过更新系统软件包并安装必要的工具来设置我们的渗透测试环境。大多数所需的工具已经预装在 Kali 中,所以我们只需要再安装几个软件包。

更新包管理器

使用新 Linux 安装时的第一步应该是更新软件包。正如前面所说,我将在所有演示中使用 Kali Linux。Kali 基于 Debian Linux 发行版,使用高级包管理工具APT)包管理器。在其核心,apt简化了软件管理。它自动化了从预定义的仓库中获取、配置和安装软件包的过程。这个自动化不仅节省了时间,还确保在没有人工干预的情况下解决软件依赖关系。

运行sudo apt update可以刷新本地可用软件包及其版本的数据库,确保你从仓库获取到最新的信息。在安装新软件或更新现有软件包之前,这一步是至关重要的,确保你得到的是最新版本。如果你使用的是 Kali、Ubuntu 或 Debian Linux,以下的更新和升级命令将按预期工作,因为它们都使用apt包管理器:

 $ sudo apt update && sudo apt upgrade -y && reboot

在前面的命令中,我们使用sudo提升权限,使用apt更新可用软件包的列表。双重与符号(&&)像逻辑与操作符一样工作;第二个命令用于在不提示的情况下升级软件包(-y),仅在第一个命令成功执行时才会运行。最后,我们重启系统以确保所有服务和内核更新生效。

安装 ProjectDiscovery 工具

ProjectDiscovery提供了一些我推荐用于渗透测试的工具(PDTM – ProjectDiscovery,github.com/projectdiscovery/pdtm)。在安装它们之前,我们必须安装 Go 编程语言运行时和库。按照以下步骤操作:

  1. 在你的网页浏览器中,导航到go.dev/dl/

  2. 下载适用于你 Linux 发行版的正确包。确保仔细查看处理器架构。通常,这将是一个Kind值为archiveOS值为LinuxArch值为x86-64ARM64

  3. 解压下载的压缩包。确保更改包版本,以使其与所下载的版本匹配:

    $ sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
    
  4. /usr/local/go/bin添加到你~/.bashrc文件中的PATH环境变量中。PATH环境变量告诉你的 Bash shell 在你没有在命令前加上路径时在哪里找到可执行程序的完整路径。echo命令会将引号内的文本打印到终端,且大于号(>)将输出重定向到文件。请注意,我们在这里使用了两个大于号来重定向输出。如果只使用一个,它将覆盖文件内容。我们希望通过使用两个符号将内容追加到文件中:

    $ echo "export PATH=$PATH:/usr/local/go/bin" >> ~/.bashrc
    
  5. 获取文件以执行你的更改:

    $ source ~/.bashrc
    
  6. 检查确保/usr/local/go/bin已被添加到你的PATH中(请查看最后一个冒号字符之后的部分):

    $ echo $PATH
    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games:/usr/local/go/bin
    
  7. 验证 Go 是否已正确安装,并且可以在你的PATH中找到。你的版本和架构可能有所不同:

    $ go version
    go version go1.22.0 linux/arm64
    
  8. 从 ProjectDiscovery 安装pdtm。这是一个安装和管理所有 ProjectDiscovery 工具更新的工具:

    $ go install -v github.com/projectdiscovery/pdtm/cmd/pdtm@latest
    
  9. pdtm添加到你的路径中:

    $ echo "export PATH=$PATH:$HOME/.pdtm/go/bin" >> ~/.bashrc
    
  10. 运行以下pdtm命令以安装所有工具:

    $ pdtm -install-all
    
  11. naabu安装libpcap

    $ sudo apt install -y libpcap-dev
    

这就完成了安装所有必需的 ProjectDiscovery 工具。

安装 NetExec

NetExec 是一个网络服务利用工具,帮助自动化评估大型网络的安全性NetExec wikiwww.netexec.wiki/)。

在我看来,NetExec 是内部网络渗透测试中最有用的工具之一。它支持大多数在内部网络渗透测试中需要的网络协议,还支持 Microsoft Active Directory 测试。

这里列举的功能太多,无法一一列出。我使用 NetExec 的一些功能包括以下内容:

  • 扫描漏洞;NetExec 包括一些有用的模块来测试常见漏洞

  • 对身份验证进行暴力破解攻击,以测试弱密码

  • 向服务器喷射密码或密码哈希,以查找提供的凭据在哪些地方具有本地管理员访问权限

  • 命令执行

  • 收集凭据

  • 枚举 SMB 共享的读/写访问权限

输入以下命令来安装 NetExec:

 $ sudo apt install -y pipx git && pipx ensurepath && pipx install git+https://github.com/Pennyw0rth/NetExec

这就完成了安装最常用的渗透测试工具的过程,这些工具默认情况下未安装。

总结

在本章中,您已经了解了不可或缺的 Bash Shell 脚本世界,这是任何有志于在渗透测试中脱颖而出的人必备的核心技能。本章开始时,我们解开了 Bash 的神秘面纱,并强调了它在网络安全任务中的重要性。这不仅仅是记住命令,更是利用 Bash 来自动化重复任务、处理数据,并高效地进行安全评估。接下来的内容提供了如何选择支持 Bash 的操作系统的指导,为成功的脚本编写奠定了基础。然后,我们卷起袖子,配置了我们的黑客 Shell,定制了其外观和行为,以反映个人的品味和偏好。这种定制不仅仅是为了美观,更是为了创建一个功能强大且高效的工作环境。最后,本章介绍了必备的渗透测试工具,带领您完成了它们的安装和基础使用。到此为止,您已经具备了一个准备充分的环境,并对 Bash 脚本如何显著提升您的渗透测试能力有了基础的理解。

下一章将介绍处理文件和目录的技巧。

第二章:文件和目录管理

精通 Bash 文件和目录管理将使你具备高效浏览文件系统、操作文件和目录、通过权限控制访问以及自动化日常任务的技能。这些能力对于任何希望充分发挥 Linux 或 Unix 系统功能的人来说都是必不可少的。通过实践、耐心和一些创造力,你可以将复杂的文件系统转变为你指挥的井然有序的文件和目录集合。

到本章结束时,你将掌握创建、删除、复制和移动文件的技能。你将理解绝对路径和相对路径的重要性。这也将包括对目录结构的介绍,以及如何在 Bash 环境中高效地导航文件系统。你将掌握 Linux 环境中用户和组权限的概念。你将学习硬链接和符号链接symlink或软链接)之间的区别,如何创建它们,以及在何种场景下每种链接类型最为有用。

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

  • 文件和目录的操作

  • 目录导航与操作

  • 文件权限和所有权

  • 文件链接—硬链接和符号链接

技术要求

跟随本章的内容需要访问具有 Bash Shell 的 Linux 系统。本章使用的所有命令都可以在 GitHub 代码库中找到,链接为github.com/PacktPublishing/Bash-Shell-Scripting-for-Pentesters/tree/main/Chapter02

文件和目录的操作

在本节中,我们将介绍用于处理文件和目录的命令以及如何浏览文件系统。我们将从ls命令开始,它用于列出文件、目录及其权限。

Bash 中的ls命令就像是列出目录内容的瑞士军刀。它简单却充满了选项,能够根据你的需要定制输出。让我们深入了解如何使用ls让你的终端使用更加高效和便捷。

基本的ls命令会列出当前目录中的文件和目录,使用如下命令:

 ~ $ ls
Desktop  Documents  Downloads  Music  Pictures

这将显示所有非隐藏文件和目录。隐藏文件(以点开头的文件)不会显示出来。若要查看隐藏文件,请使用-a选项,得到以下输出:

图 2.1 – 使用-a 选项与 ls 命令时,隐藏文件会显示出来

图 2.1 – 使用-a 选项与 ls 命令时,隐藏文件会显示出来

现在,你将看到所有内容,包括如.bashrc这样的文件。

如果你想查看与当前目录不同位置的文件和目录列表,请将目录位置添加到ls命令的末尾,如下所示:

 $ ls /opt

在像ls *.txt这样的 Shell 命令中,星号(*****)被称为glob字符。*字符匹配任何字符序列,因此该命令列出了当前目录下所有扩展名为.txt的文件。你也可以使用 glob 字符列出所有以指定字符串开头,后跟任意字符或字符序列的文件,方法是使用ls sometext*命令。

使用-l选项获取更多详细信息,例如文件权限、链接数、所有者、组、大小和时间戳:

图 2.2 – 使用 ls 命令的-l 选项显示扩展的文件和目录信息

图 2.2 – 使用 ls 命令的-l 选项显示扩展的文件和目录信息

这种长格式对于快速概览文件系统状态非常有用。

使用-l时,默认按字节列出文件大小。添加-h选项可以使文件大小更加易读(例如,KB,MB)。这样可以让你一目了然地判断文件的大小:

图 2.3 – ls 命令的-h 选项以人类可读的格式显示文件大小

图 2.3 – ls 命令的-h 选项以人类可读的格式显示文件大小

若要将最近修改的文件显示在最上面,可以使用-t选项。要将ls -t的输出按逆序排列,可以加入-r选项。将此与-h选项结合使用,可以获得按修改时间排序的详细且易读的文件列表,如下图所示:

图 2.4 – ls 命令选项显示如何根据文件修改时间进行排序

图 2.4 – ls 命令选项显示如何根据文件修改时间进行排序

按文件大小排序可以快速显示目录中最大或最小的文件。以下命令将按文件大小排序ls的输出:

 $ ls -lS

提示

有时你不仅想查看当前目录的内容,还想查看所有子目录的内容。使用-R选项可以递归显示所有子目录的内容。

除了使用ls命令列出文件或目录外,还有一些常见操作你可能需要对文件或目录进行,例如创建、复制和删除它们。

你可以分别使用touchmkdir命令创建新文件或目录。

使用touch命令创建一个新的空文件,如下所示:

 $ touch test.txt

使用mkdir命令创建一个新目录,如下所示:

 $ mkdir [path and name of new directory]

如果你想在路径中创建多个嵌套的目录,请包含-p选项。例如,假设你想创建一个名为first的新目录,并且在first目录中创建一个second目录。以下示例将创建这个新的目录结构:

 $ mkdir -p first/second

你可以使用cp命令复制文件和目录。cp命令的语法如下所示:

 $ cp [source] [destination]

要删除文件,请使用rm命令。请注意此命令,因为删除操作无法恢复。如果要删除目录,请包括-r选项以递归删除目录中包含的文件和子目录。以下命令演示了如何使用rm删除文件:

 $ rm [file]

现在您已经学会了如何列出、创建和删除文件,是时候进入下一节学习如何在文件系统中导航了。

目录导航和操作

在本节中,您将学习 Linux 文件系统目录的布局,常见目录的用途以及如何在系统中导航。到本节结束时,您应该对文件系统的位置和设计决策感到满意,并将使用常见的 Bash 命令像专家一样导航它。

文件系统设计和层次结构

Bash 文件管理的核心是理解文件系统层次结构。在这里,我们将回顾各种文件系统目录及其用途。我们还将回顾对渗透测试感兴趣的特定目录。这将使您在导航文件系统时感到自信。

想象文件系统是一棵树,从树干上延伸出分支。

使用tree命令,您可以找到文件系统的高级概览,如下图所示:

图 2.5 – 文件系统层次结构概览

图 2.5 – 文件系统层次结构概览

让我们按照以下方式理解这个高级概览的要素:

  • /:在这个结构的根部是/目录,简称为根目录。这是起点:一切都从这里延伸出去。想象它是一棵树干,其他所有路径都从这里分叉。以下图演示了运行tree命令而不指定要显示的级别数量以展示文件系统的完整布局:

图 2.6 – 发现树形目录结构的更深入理解

图 2.6 – 发现目录结构的树的更深入理解

  • /bin:直接在根目录下,您会找到/bin,一个充满了基本用户二进制文件或程序的目录。这些是每个用户都可以访问的工具,对于日常操作是必需的。

  • /boot/boot目录包含启动系统所需的文件,如 Linux 内核和初始 RAM 磁盘(initrd)文件。

  • /dev/dev目录包含代表硬件设备和特殊文件的设备文件。

  • /etc/etc目录包含许多对系统操作至关重要的配置文件。作为渗透测试,您可能对/etc目录中的某些文件和目录感兴趣。以下是一些最显著的文件:

    • /etc/passwd:此文件包含有关系统上用户的基本信息,如用户 ID、组 ID、主目录和 shell。

    • /etc/group : 该文件包含系统中所有组的列表,及其组 ID 和成员用户名。

    • /etc/shadow : 该文件存储用户的密码信息,包括哈希后的密码和账户过期日期。

    • /etc/sudoers : 该文件包含允许使用sudo命令的用户和组的列表,以便以提升权限执行命令。

    • /etc/sysconfig : 该目录包含各种系统服务和应用程序的配置文件,例如网络设置、显示管理器配置和防火墙规则。

    • /etc/network : 该目录包含网络接口的配置文件,包括 IP 地址、子网掩码和域名系统(DNS)服务器设置。

    • /etc/hosts : 该文件将主机名映射到 IP 地址,允许系统在不依赖 DNS 服务器的情况下解析主机名为 IP 地址。

    • /etc/services : 该文件列出了系统上可用的服务,及其端口号和协议。

    • /etc/protocols : 该文件列出了系统支持的网络协议,包括其版本号和其他配置信息。

    • /etc/fstab : 该文件包含关于系统上已挂载文件系统的信息,包括挂载点、文件系统类型和选项。

  • /home : 用户特定的数据存储在/home中,这是文件系统内的一组个人空间。每个用户的目录就像他们的家,存储个人文件和设置。

  • /lib : 系统库,即程序运行所需的共享资源,存储在/lib中。

  • /mnt : 用于挂载外部设备或文件系统的有/mnt/media。它们作为外部文件系统的停靠点。

  • /opt : 可选或第三方软件存储在/opt。渗透测试人员常常将git仓库克隆到/opt下的自定义目录,以运行那些没有安装在常规/****bin目录下的工具。

  • /proc** : **/proc目录存储关于正在运行的进程的信息。

  • /root : 根用户的主目录位于/root。由于 root 是超级用户,其文件存储在与其他用户(位于/home下)不同的地方。

  • /run** : **/run目录是一个临时文件系统,用于存储自上次启动以来的瞬态信息。

  • /sbin : 紧邻/bin的是/sbin,用于存放系统二进制文件。这些通常是仅供系统管理员使用的工具。

  • /srv** : **/srv目录存储系统服务使用的数据。

  • /sys** : **/sys目录提供了一个与内核对象及其属性交互的接口。

  • /tmp** : **/tmp目录存储临时文件,这些文件在系统重启时会被删除。

  • /usr/usr目录是一个更广泛的集合,包含用户二进制文件、库文件、文档等。它就像城市的商业区,提供了超出/bin/lib中基本功能之外的各种服务。

  • /var:在 Linux 系统中,/var目录是文件系统层级中的关键组成部分,其主要目的是存储预期会随着时间增长的可变数据、文件和目录。这些数据可能包括日志文件、队列文件、临时文件以及其他随着系统操作而变化或扩展的瞬态或动态数据。/var目录的结构和内容旨在支持跨系统重启存储可变数据,确保数据在会话之间持久存在。以下是/var目录中的一些重要子目录及其典型用途:

    • /var/log:包含系统和运行在系统上的各种应用程序生成的日志文件。这些日志包括系统日志、应用日志和系统事件日志,对于故障排除和监控系统健康状况至关重要。

    • /var/spool:用于排队任务和数据,如打印作业、邮件和其他排队任务。该区域旨在存放等待由某些服务或应用程序处理的数据。

    • /var/tmp:用于存储在系统重启之间保持的临时文件。与/tmp不同,后者也可能存储临时文件,但/var/tmp不会在重启时被删除或清除。

    • /var/cache:存储应用程序的缓存数据。虽然这些数据可以根据需要重新生成,但它们的存储目的是通过减少重新计算或重复获取相同数据来提高性能。

    • /var/mail:在某些配置中存放用户的电子邮件信息。这个目录对于处理本地邮件存储的系统至关重要。

    • /var/www:通常用作 Web 服务器内容的默认目录。它包括托管在服务器上的网站,也是许多 Linux 发行版中存放 Web 文件的标准位置。

    • /var/lib:包含程序在运行时通常会修改的动态状态信息。这些信息可以包括数据库、应用程序状态文件和其他应用程序在操作过程中需要存储和管理的数据。

你可以通过输入hier命令来查看 Linux 文件系统层级的文档,示例如下:

 $ man hier

提示

man命令是 manual(手册)的缩写。当你需要了解命令所需的选项和惯例时,记得使用man命令。

尽管当前工作目录可能会显示在 Bash 提示符中,你也可以使用pwd命令打印当前目录,示例如下:

 ~ $ pwd
/home/steve

现在你已经了解了文件系统的布局,并理解了其层级结构和设计,接下来我们将进入下一部分,了解如何在文件系统中进行导航。

文件系统导航命令

浏览文件系统可以使用各种工具和技术。最常见的方法是使用命令行界面CLI)并通过cd命令在目录之间切换。例如,要切换到/home目录,你可以输入以下命令:

 $ cd /home

之前我们提到过,波浪号(~)字符是一个快捷方式,用于输入用户目录的完整路径,因此你也可以在cd命令后使用波浪号跳转到主目录,如下所示:

 $ cd ~

如果你在个人配置文件中启用了标签补全,你可以使用 Tab 键在输入时自动完成目录名称,从而更容易地浏览文件系统。

除了cd命令,Bash Shell 还提供了若干命令用于浏览目录,包括pushdpopd。这两个命令就像在荒野中留下的面包屑轨迹,帮助你跟踪你去过的地方,从而轻松返回。当你pushd进入一个目录时,Bash 会记住你当前的位置,然后再将你带到新目录。需要返回吗?只需popd,你就会回到上一个目录。这就像在命令行工具箱中拥有一个瞬移设备。下面的命令输出展示了如何使用pushdpopd命令来浏览文件系统:

 ~ $ pushd /var/log
/var/log ~
/var/log $ pushd /etc
/etc /var/log ~
/etc $ popd
/var/log ~
/var/log $ popd
~
~ $

现在是提到绝对路径相对路径的好时机。绝对路径是从驱动器的根目录(/)开始的完整路径。指向你主目录中文件的绝对路径是/home/user/filename。相对路径是相对于你当前所在目录的路径。当前目录由一个点和斜杠(./)表示。目录结构中上一级由../表示。上两级则是../../,以此类推。如果你想从当前目录进入子目录,只需使用目录名。例如,要引用当前目录下两级目录的文件,路径是directory1/directory2/filename

现在,假设你正在一个深层的目录树中工作,并且需要返回多个层级。输入cd ../../..不仅麻烦,而且容易出错。这时,cd -命令登场了,它是一个简单而强大的快捷方式,可以立即将你带回上一个目录。就像拥有一个撤销按钮来修正导航错误。这里我们可以看到它是如何工作的,并将我们带回到之前的位置:

 ~ $ cd /opt
/opt $ cd -
/home/steve
~ $

那如果你能在不记住路径的情况下跳转到常用的目录呢?这时别名就派上用场了。通过在.bashrc文件中添加像alias docs='cd /home/user/documents'这样的行,你可以为那些长路径创建快捷方式。突然之间,进入documents文件夹就像输入docs一样简单。这就像在庞大的城市中设置个人快捷方式一样。

对于那些热衷于提高效率的人来说,Ctrl + R 反向搜索功能简直是游戏规则改变者。按下这些键并开始输入之前使用过的命令的一部分,Bash 会在你的历史记录中搜索并建议匹配的命令。就像为你的命令历史提供了一个搜索引擎,省去了重新输入长命令的麻烦。

最后,别忘了自动补全功能,这是一个几乎神奇的特性。开始输入目录或文件的名称并按下 Tab 键,Bash 会为你自动完成,或者如果有多个匹配项,它会显示可用的补全项。这就像是拥有一个私人助手为你完成句子,但它是用于目录导航的。

总结来说,掌握这些高级的 Bash 导航技巧可以将你的命令行体验从令人沮丧转变为流畅高效。无论是通过 pushdpopd 在目录间跳转,使用别名创建快捷方式,还是利用反向搜索和自动补全的功能,这些技巧都旨在让你的工作更加轻松。所以下次打开终端时,记得运用这些技巧,看看你如何快速穿越文件系统。

到现在为止,你应该已经对文件系统布局有了清晰的了解,并且在系统中游刃有余。接下来,我们将探索文件系统的权限。

文件权限与所有权

在本章早些时候,你可能注意到 ls -l 命令的输出中出现过类似 drwxr-xr-x 的字符串。这表示文件或目录的权限。Linux 文件系统的权限就像游乐场的规则。它们决定了谁可以玩秋千(访问文件),谁可以邀请朋友一起玩(更改权限),以及谁可以制定规则(所有权)。理解这些权限对于任何想要有效管理 Linux 系统的人来说至关重要。我们将用简单的术语来讲解,包括 chownchmodSUIDSGID 的使用。

所有权与用户组

Linux 中的每个文件和目录都有一个与之关联的所有者和用户组。可以将所有者看作是对孩子的玩具拥有控制权的父母,而用户组则是可以在特定条件下与其一起玩耍的朋友。以下描述可能有助于理解:

  • 所有者:拥有文件或目录控制权的用户

  • 用户组:一组共享某些权限的用户

更改所有权 – chown

要更改文件或目录的所有者,我们使用 chown 命令(这可能需要在命令前加上 sudo 前缀):

 $ chown [user]:[group] [file]

该命令同时更改文件的所有者和用户组。如果你只想更改所有者或用户组,可以省略命令中的用户组。但是,如果省略了用户,则用户组必须以冒号(:)符号作为前缀。以下命令演示了如何仅更改文件的用户组所有权:

 $ chown :[group] [file name]

这将保留所有者不变,但会更改文件或目录的用户组权限。

如果你有一个文件,并且想要应用在参考文件上使用的相同权限,包括--reference参数,如下所示:

 $ chown --reference=file1 file2

有两个常见的chown选项,你应该熟悉:

  • -h:影响符号链接而不是任何引用文件

  • -R:递归地操作文件和目录

在学会使用chown更改文件所有权后,在下一节中,您将学习如何使用chmod修改权限。

修改权限 – chmod

权限确定文件或目录上可以执行哪些操作。有三种类型的权限:

  • 读取r ):查看文件或目录的内容

  • 写入w ):修改文件或目录的内容

  • 执行x ):将文件作为程序运行或访问目录

权限设置给三类用户:

  • 拥有者

  • 其他

你可以使用ls -l命令列举文件或目录的权限。以下命令输出演示了如何使用ls列出文件权限:

 $ ls -l .bashrc
-rw-r--r-- 1 steve steve 6115 Feb 21 10:02 .bashrc

先前显示的权限指示以下细节:

  • 第一个字符是-,表示这是一个文件。目录将被表示为d

  • 在初始的**–**字符表示文件后,接下来的三个字符表示用户权限( steve )。这是文件的所有者。这些字符是rw-,这意味着文件所有者具有读取和写入权限,但文件不可执行。

  • 接下来的三个字符代表组( steve )。权限是r--,这意味着steve组可以读取文件,但不能写入或执行它。

  • 最后三个字符是r--。这意味着除了所有者或组成员之外的任何人都可以读取文件,但不能写入或执行它。

让我们可视化文件权限,以便更容易理解。以下图表显示了如何解读读取、写入和执行权限以及它们如何组合:

图 2.7 – 按 rwx 位拆分的文件系统权限

图 2.7 – 按 rwx 位拆分的文件系统权限

如前所述,权限以rwx的三组重复。如读取( r )= 4,写入( w )= 2,执行( x )= 1,您可以将它们相加以用一个数字代表权限,而不是三个字符。以下图表显示了每种可能组合的权限的数字表示:

图 2.8 – 用八进制表示的文件系统权限

图 2.8 – 用八进制表示的文件系统权限

使用chmod,我们可以更改这些权限。例如,以下命令设置了所有者的读取、写入和执行权限,组和其他用户的读取和执行权限:

 $ chmod 755 filename

你也可以使用 chmod 以符号方式修改文件。例如,如果你想使一个文件可执行,可以使用如下命令:

 $ chmod +x filename

在学习了基本的文件权限后,在下一部分,你将学习一些特殊权限,这些权限会在非文件拥有者的用户执行文件时产生影响。

特殊权限 – SUID 和 SGID

SUID(设置用户 ID)和 SGID(设置组 ID)是可以设置在可执行文件上的特殊权限类型。它们允许用户以文件拥有者或组的权限执行文件。当带有 SUID 权限的可执行文件被运行时,它将以文件拥有者的权限运行,而不是以启动它的用户的权限。同样,带有 SGID 权限的可执行文件则以文件组拥有者的权限运行。这种机制允许用户在通常受限的情况下执行需要提升权限的任务。它们可以简要地描述如下:

  • SUID:如果设置在可执行文件上,运行该文件的用户将获得与文件拥有者相同的权限。

  • SGID:与 SUID 类似,但应用于组权限。

要使用 chmod 命令设置 SUID,你可以使用以下命令:

 $ chmod u+s filename

要使用 chmod 命令设置 SGID,你可以使用以下命令:

 $ chmod g+s filename

从系统安全的角度来看,SUIDSGID 是双刃剑。一方面,它们对于需要临时提升权限但不暴露敏感凭证的任务至关重要。例如,passwd 命令允许用户更改密码,它需要访问系统的影子文件,而普通用户无法触及此文件。通过为 passwd 设置 SUID 权限,用户在运行该命令时可以在拥有足够权限的情况下更新密码,从而修改影子文件。

然而,另一方面,如果不小心管理,这种权限可以被利用。黑客对发现设置不当的 SUIDSGID 权限的可执行文件垂涎欲滴。为什么?因为它为他们提供了提升系统权限的机会。设想一个场景,其中一个看似无害的可执行文件具有 SUID 权限并且由 root 拥有。如果这个可执行文件存在任何漏洞,它就会允许任意命令执行;黑客可以利用它以 root 权限执行命令,从而控制系统。

黑客利用各种技术来利用 SUIDSGID 权限。他们可能会扫描系统中所有设置了这些权限的文件,然后尝试利用这些文件中的漏洞。另一种常见的手段是二进制植入攻击,黑客将一个合法的 SUID** / **SGID 文件替换或链接到一个恶意文件,等待毫无防备的用户执行它。

防止此类攻击需要认真管理SUIDSGID权限。定期审计这些权限有助于发现和修复潜在的漏洞。系统管理员应该确保只有绝对必要的文件才具有SUIDSGID权限,并确保这些文件保持最新,以减轻已知的漏洞。此外,采用入侵检测系统IDS)有助于监控与这些权限相关的异常活动。

总之,虽然SUIDSGID是 Linux 中管理特权操作的不可或缺的工具,但它们必须小心处理。滥用或配置错误可能使它们成为黑客武器。通过理解它们的功能及滥用的潜力,系统管理员可以更好地保护系统免受未授权的特权提升,而你作为渗透测试人员,能在审计系统安全时理解其复杂性。

理解 Linux 文件系统权限就像学习一款新游戏的规则。一旦你了解了谁能做什么(权限)、谁拥有什么(所有权),以及如何更改这些(使用chownchmod),你就可以有效地管理 Linux 系统了。记住:权力越大,责任越大。明智地使用这些命令,保持系统的安全和功能。

现在你已经掌握了列出和设置文件系统权限的技巧,让我们继续进入下一部分,了解文件系统符号链接。

文件链接——硬链接和符号链接

硬链接本质上是文件系统中现有文件的另一个名字。假设你有一本最爱的书放在书架上。有一天,你决定它应该同时出现在经典收藏两个区域。你不需要重新买一本,而是简单地在书上贴上另一个标签,指引读者从这两个区域找到它。在 Linux 中,创建硬链接意味着你给文件添加了一个新的引用,但磁盘上的文件依然是同一个。如果你删除了原始文件名,内容仍然可以通过硬链接访问。这就像魔法一样:即使删除了其中一个标签,书仍然会留在书架上。

然而,硬链接有其局限性。它们不能跨越不同的文件系统;一个硬链接不能指向另一个驱动器上的文件,而且它们不能链接到目录,以防止可能在文件系统中创建循环。

介绍符号链接,它们更具灵活性,类似于快捷方式。用我们图书馆的比喻,符号链接就像是在经典区域放置一张指向收藏夹区域某本书位置的便条。这张便条本身并不是书,而是指向书本所在位置的指示。在 Linux 中,符号链接是一个指向另一个文件或目录的独立文件。与硬链接不同,如果你删除原始文件,符号链接会断开,因为它的引用点已经消失了。这就像有人把书从图书馆拿走了,经典区域的便条现在指向空的书架位置。

符号链接以其跨越文件系统边界和链接目录的能力而脱颖而出,使它们在任务中非常灵活,例如创建可访问的路径指向深层嵌套的目录,或保持不同版本文件或程序之间的兼容性。

为什么使用这些链接?效率和方便性是主要原因。硬链接让你可以为单个文件提供多个访问点,而不需要复制其内容,从而节省空间。符号链接(symlink)则提供了一种无需移动或复制文件的方式来创建易于导航的文件结构。

在实践中,管理这些链接非常简单,使用如ln命令来创建硬链接和符号链接(ln 用于硬链接,ln -s 用于符号链接),而使用ls -l命令查看它们。真正的艺术在于知道何时使用每种类型的链接。硬链接非常适合备份系统,或者在一个文件系统内工作时,因为它能够确保文件完整性。符号链接则非常适合创建灵活的路径和快捷方式,特别是在跨不同文件系统或者链接目录时。

总结来说,硬链接和符号链接提供了管理和访问文件的创造性方式,各自有不同的规则和潜在用途。无论是优化工作空间还是构建复杂的系统,理解这些链接能为你打开一片新的可能性天地。

总结

总结来说,掌握 Bash 文件和目录管理技能,可以帮助你高效地浏览文件系统,操作文件和目录,控制访问权限,并自动化常规任务。这些能力对于任何希望充分利用 Linux 或 Unix 系统的人来说都是必不可少的。通过练习、耐心和一些创造力,你可以将文件系统的复杂性转变为你指挥的一个井然有序的文件和目录集合。作为一名渗透测试人员,了解 Linux 文件系统的复杂性对于审计系统和利用它们展示风险至关重要。

在下一章,你将学习正则表达式,没多久,你将像武士挥剑一样,切割文本和命令输出!

第三章:变量、条件语句、循环和数组

在之前的章节中,我们提供了与当前主题相关的信息。我们通过带领你完成系统设置和常用命令的讲解,帮助你了解如何使用 Bash 命令浏览 Linux 文件系统。

在本章中,我们将深入探讨让代码智能高效的编程要素:变量条件语句循环数组。可以把变量看作是指向数据的标签,条件语句是决定程序路径的交叉点,循环则是你可以不断执行某些操作,直到满足某个条件为止。这些概念是构建动态响应程序的基石。无论你是刚开始学习还是想复习基础,理解这些要素对任何编程之旅都是至关重要的。

本章节将涵盖以下主要主题:

  • 介绍变量

  • 使用条件语句进行分支

  • 使用循环重复

  • 使用数组作为数据容器

技术要求

在本章节中,你将需要一个 Linux Bash 终端来跟随教程。你可以在github.com/PacktPublishing/Bash-Shell-Scripting-for-Pentesters/tree/main/Chapter03找到本章的代码。

介绍变量

可以把变量看作是存储数据的标签或容器。你可以将任何数据(例如文本、数字、文件名等)分配给一个简短且易记的变量名。在脚本中,你可以多次引用这个数据,通过变量名来访问或修改其内容。从技术上讲,变量是一种声明,它分配了内存存储空间并给它赋予了一个值。

在接下来的子章节中,我们将把变量这一主题分解成易于消化的小块,以便更好地理解。

声明变量

在 Bash 中声明变量时,只需将一个值赋给一个变量名。声明变量的语法如下:

 variable_name=value

例如,要声明一个名为my_variable的变量,并赋值为Hello, World!,你可以使用以下命令:

 my_variable="Hello, World!"

重要提示

等号=两边不应有空格。此外,最好将字符串值用双引号"括起来,这样可以正确处理空格和特殊字符。使用单引号可以防止扩展;如果希望变量或特殊字符被扩展,则使用双引号。如果必须在双引号字符串中使用特殊字符(如****),可以通过在字符前加反斜杠来使它们作为字面字符显示,而不是被求值,例如:`\`。

Bash 变量的一个强大特性是它们能够存储命令的输出,使用 命令替换。可以通过将命令括在反引号 ``` 中,或者使用 $() 语法来实现。以下是一个示例:

 current_date=`date`

或者,您可以使用以下实现:

 current_date=$(date)

两个命令将把当前日期和时间存储在 current_date 变量中。

在 Bash 中,您可以在从命令行运行脚本时传递参数。这些参数被存储在特殊的变量中,您可以在脚本内使用它们。其工作原理如下:

 ~ $ ./myscript.sh arg1 arg2 arg3

在脚本中,您可以使用以下特殊变量访问这些参数:

  • $0:脚本本身的名称。

  • $n:传递给脚本的第 n 个参数,$1$9。示例包括 $1$2$3

  • ${10}:传递给脚本的第十个参数(对于大于等于 10 的参数,必须使用花括号)。

  • $#:参数的数量。

  • $?:上一个执行命令的退出状态。

  • $$:当前 shell 的进程 ID。

  • $@:包含命令行参数的数组。

  • $*:表示传递给脚本或函数的所有位置参数(参数),作为一个单一的字符串。

访问变量

要访问变量的值,只需使用变量的名称,前面加上美元符号 $

 #!/usr/bin/env bash
my_string="Welcome to Bash Scripting for Pentesters!" echo $my_string

这个示例代码可以在本章文件夹中的 ch03_variables_01.sh 文件中找到。

这将输出以下内容:

 Welcome to Bash Scripting for Pentesters!

以下脚本展示了如何访问命令行参数:

 #!/usr/bin/env bash
name=$1
age=$2
echo "Hello $name, you're $age years old!"

这个示例代码可以在本章文件夹中的 ch03_variables_02.sh 文件中找到。

如果我们运行这个脚本,我们将得到以下输出:

 ~ $ bash ch03_variables_02.sh Steve 25
Hello Steve, you're 25 years old!

如果你输入名字和姓氏时没有用双引号括起来,会发生什么?试试看。

您可以使用 $(()) 语法或 let 命令对变量进行算术运算:

 #!/usr/bin/env bash
a=5
b=3
c=$((a + b))
let d=a+b
let e=a*b
echo "a = $a"
echo "b = $b"
echo "c = $c"
echo "d = $d"
echo "3 = $e"

这个示例代码可以在本章文件夹中的 ch03_variables_03.sh 文件中找到。

在上面的代码块中,我们将值 5 赋给了 a 变量,将值 3 赋给了 b 变量。接下来,我们将 ab 相加,并将结果赋给了 c 变量。最后两行展示了使用 let 命令进行加法和乘法运算。

这是我们运行代码时的输出:

 ~ $ bash ch03_variables_03.sh
a = 5
b = 3
c = 8
d = 8
3 = 15

现在您已经了解了如何创建和访问变量,我们将继续讲解一种特殊类型的变量 —— 环境变量

环境变量

环境变量 本质上是命名的对象,用于存储操作系统进程所需的数据。这些变量通过提供用户环境的信息(如当前用户的主目录或可执行文件的路径),可以影响系统上软件的行为。

默认情况下,在 Bash 脚本中定义的变量是局部的,仅对该脚本有效。要使变量对其他进程(如子 shell 或子进程)可用,需要使用 export 命令导出它:

 my_var="Hello, World!" export my_var

在导出一个变量后,你可以在子 Shell 或子进程中访问其值。

环境变量的美妙之处在于它们能够简化流程。如果没有它们,每次你想运行一个程序或脚本时,你可能都需要输入它的完整路径。使用环境变量后,Bash 知道该去哪里查找某些文件或目录,因为这些路径被存储在像 PATH 这样的变量中。

此外,环境变量确保软件在不同用户环境下的正常运行。例如,HOME 变量告诉应用程序用户的主目录位置,使程序能够在正确的位置保存文件,而不需要每次都明确指示。

让我们通过一些实际的例子来理解这一点。假设你经常访问一个深藏在文件系统中的目录。每次都输入完整的路径可能会很麻烦。通过为这个路径创建一个自定义的环境变量,你可以大大简化这个过程:

 export MY_DEEP_DIRECTORY="/this/is/a/very/long/path/that/I/use/often"

现在,每当你需要访问这个目录时,你只需要输入 cd $MY_DEEP_DIRECTORY ,Bash 会立即带你到那里。

另一个常见用例是修改 PATH 变量。PATH 变量告诉 Bash 去哪里查找可执行文件。如果你安装了一个不在系统默认可执行路径中的程序,你可以将其位置添加到你的 PATH 中:

 export PATH=$PATH:/path/to/my/program

这个添加功能允许你在终端中从任何地方运行程序,而无需指定其完整路径。

注意,你的程序路径前面有 $PATH: 。这样做是将新路径附加到现有路径后面。如果没有这一部分,你将覆盖原有的 PATH,并且在修复或重启计算机之前会出现错误。

重要提示

如果你希望一个环境变量在重启后保持有效,可以将其放入 .bashrc 文件中。为了让对 .bashrc 的更改生效,运行 source ~/.bashrc 命令。

现在你已经牢牢掌握了变量的概念,是时候通过一些实践来巩固所学的知识了。

变量回顾

让我们检查一个包含本章所涵盖内容的脚本。请看下面的脚本:

 #!/usr/bin/env bash
# What is the name of this script? echo "The name of this script is $0." # Assign command line arguments to variables
name=$1
age=$2
# Use the first two parameters. echo "The first argument was $1, the second argument was $2." # How many parameters did the user enter? echo "The number of parameters entered was $#." # What is the current process ID? echo "The current process id of this shell is $$." # Print the array of command line arguments. echo "The array of command line arguments: $@"

这个示例代码可以在本章文件夹中的 ch03_variables_04.sh 文件中找到。

首先,重要的是要指出我在这里介绍了一些新的内容。脚本中的注释以 # 开头,并持续到行尾。# 后面的任何内容都不会被打印,前提是该符号没有被转义。你可能注意到,在某一行中,我们使用了 $# 来打印传递给脚本的参数个数。由于它在双引号内,并且前面有 $ 符号,所以注释行为不适用,且没有被转义。

你必须用注释来记录你的脚本。如果在一段时间后需要编辑脚本,注释会帮助你回忆当时想做什么,如果你与他人共享或发布脚本,注释也能帮助他们理解。

现在,让我们运行脚本。有两种方法可以运行它。我们可以通过输入 bash 后跟脚本名称来运行它,或者我们可以使脚本可执行并加上路径前缀,以下是示例:

 ~ $ bash ch03_variables_04.sh "first arg" 2nd 3rd fourth
The name of this script is ch03_variables_1.sh. The first argument was first arg, the second argument was 2nd. The number of parameters entered was 4. The current process id of this shell is 57275. The array of command line arguments: first arg 2nd 3rd fourth
The first argument is: first arg
The second argument is: 2nd
The first and second arguments are: first arg 2nd

接下来,让我们列出文件权限,正如你在 第二章 中学到的那样:

 ~ $ ls -l ch03_variables_04.sh
-rw-r--r-- 1 author author 714 Mar 20 09:28 ch03_variables_04.sh

在前面的命令中,你可以看到我们使用了 ls 命令的 -l 选项来查看权限。它对所有者可读可写,对组和其他人仅可读。接下来,让我们使用 chmod 命令使其可执行:

 ~ $ chmod +x ch03_variables_04.sh
~ $ ls -l ch03_variables_1.sh
-rwxr-xr-x 1 author author 714 Mar 20 09:28 ch03_variables_04.sh

在这里,你可以看到在输入了带有 +x 参数的 chmod 命令后,文件现在可以被所有者、组和其他人执行。当然,你也可以通过使用 chmod 744 ch03_variables_04.sh 命令,使其仅由所有者执行。如需复习,请参考 第二章 或运行 man chmod 命令。

既然文件已可执行,你可以在文件名之前加上路径来运行它。你可以指定绝对路径或相对路径,正如在 第二章 中所讨论的那样。以下是如何使用相对路径运行它:

 ~ $ ./ch03_variables_04.sh 1 2 3 4

重要提示

shebang#!)是脚本中的第一行,用于指定执行脚本时使用的解释器(程序)。使用 #!/usr/bin/env bash shebang 告诉 shell 使用 Bash 解释器来运行脚本。

如果没有 shebang,以下执行方法可能无法工作,因为 shell 可能不知道使用哪个程序来执行代码。

如果不包括 shebang 并使脚本可执行,你必须在脚本名称前加上 bash 才能运行脚本。

到现在为止,你应该已经很好地掌握了变量的使用。在下一节中,你将学习如何使用条件语句来做出决策并在脚本中进行分支。

使用条件语句进行分支

从本质上讲,Bash 中的条件语句是一种告诉脚本:“嘿,如果这个特定的事情是真的,那就执行这个;否则,执行那个。”它是脚本中做决策的基础。在 Bash 中,你最常遇到的条件语句是 ifelseelif

if 语句

if 语句是最简单的条件语句形式。它检查一个条件,如果条件为真,则执行一段代码。下面是一个简单的示例:

 #!/usr/bin/env bash
USER="$1"
if [ $USER == 'steve' ]; then
  echo "Welcome back, Steve!" fi

这个示例代码可以在本章文件夹中的 ch03_conditionals_01.sh 文件中找到。

在这个示例中,脚本通过匹配第一个命令行参数来检查当前用户是否为steve。如果是,它会向 Steve 打招呼。注意这里的语法:条件周围使用方括号,使用双等号进行比较,then表示条件为真时应该执行的操作开始。fi部分则表示结束当前的if语句块。

需要指出的是,分号(;)字符在这里有特殊含义,用作命令分隔符。没有它,这个if语句块会出错。分号还可以用来将多个命令写在同一行。前面的if语句可以通过更多的分号重新编写,如下所示:

 if [ "$USER" == 'steve' ]; then echo "Welcome back, Steve!"; fi

添加 else

但是如果条件不满足时你想做别的事情呢?这时else就派上用场了。它允许你在条件为假时指定其他的操作。这里是一个示例:

 #!/usr/bin/env bash
USER="$1"
if [ $USER == 'steve' ]; then
  echo "Welcome back, Steve!" else
  echo "Access denied." fi

这个示例代码可以在本章文件夹中的ch03_conditionals_02.sh文件找到。

现在,如果用户不是steve,脚本会返回访问被拒绝

 ~ $ bash ch03_conditionals_02.sh Somebody
Access denied.

elif 的强大

有时,你需要考虑的不止两种可能性。这时elif(即else if的缩写)就变得非常有用。它让你可以逐一检查多个条件:

 #!/usr/bin/env bash
if [ $USER == 'steve' ]; then
  echo "Welcome back, Steve!" elif [ $USER == 'admin' ]; then
  echo "Hello, admin." else
  echo "Access denied." fi

这个示例代码可以在本章文件夹中的ch03_conditionals_03.sh文件找到。

在前面的脚本中,USER变量来自于已登录用户的环境变量。根据需要,修改ifelif语句中的代码,以使其与你的用户名匹配。

当你以Steve身份登录并运行它时,你将得到以下输出:

 $ bash ch03_conditionals_03.sh
Welcome back, Steve!

使用elif,你可以根据需要添加任意多个附加条件,使脚本能够处理各种不同的情况。

现在你知道了如何使用常见的条件语句,让我们深入探讨一些在 Bash 脚本中常用的稍微复杂一点的示例。

超越简单的比较

Bash 条件语句不仅限于检查一个值是否等于另一个值。你可以检查多种条件,包括以下内容:

  • 文件是否存在

  • 变量是否大于某个值

  • 文件是否可写

在 Bash 中,primaries指的是在条件测试中使用的表达式,这些表达式位于[(单括号)、[[(双括号)和 test 命令中。这些原语用于评估不同类型的条件,如文件属性、字符串比较和算术运算。原语是条件语句的基本构建块,允许你测试文件、字符串、数字和逻辑条件。它们通常用于ifwhileuntil结构中,用于根据这些评估结果确定脚本的执行流程。

文件测试操作符 用于检查文件的属性,例如是否存在、是否可读、是否为目录等。以下列表列出了文件测试操作符:

  • -e 文件 : 如果文件存在,则为真

  • -f 文件 : 如果文件存在并且是常规文件,则为真

  • -d 文件 : 如果文件存在并且是一个目录,则为真

  • -r 文件 : 如果文件存在并且可读,则为真

  • -w 文件 : 如果文件存在并且可写,则为真

  • -x 文件 : 如果文件存在并且是可执行的,则为真

  • -s 文件 : 如果文件存在并且不为空,则为真

  • -L 文件 : 如果文件存在并且是符号链接,则为真

例如,在尝试从文件中读取之前检查文件是否存在,可以防止脚本崩溃:

 #!/usr/bin/env bash
if [ -f "/path/to/file.txt" ]; then
  echo "File exists. Proceeding with read operation." else
  echo "File does not exist. Aborting." fi

这个示例代码可以在本章文件夹中的 ch03_conditionals_04.sh 文件中找到。

-f 标志用于测试提供的文件名是否存在并且是常规文件。要测试目录,可以使用 -d 标志。要同时测试文件和目录,可以使用 -e 标志。如果我们没有首先检查文件是否存在,脚本可能会崩溃。使用 if 语句可以优雅地处理这个错误。

要在 Bash 中比较整数变量,你应该使用 -eq-ne-lt-le-gt-ge 操作符:

  • -eq : 如果两个数字相等,则为真

  • -ne : 如果两个数字不相等,则为真

  • -gt : 如果第一个数字大于第二个数字,则为真

  • -ge : 如果第一个数字大于或等于第二个数字,则为真

  • -lt : 如果第一个数字小于第二个数字,则为真

  • -le : 如果第一个数字小于或等于第二个数字,则为真

这里有一些示例演示整数比较:

 #!/usr/bin/env bash
num1=10
num2=20
# Compare if num1 is equal to num2
if [ $num1 -eq $num2 ]; then
    echo "num1 is equal to num2"
else
    echo "num1 is not equal to num2"
fi

这个示例代码可以在本章文件夹中的 ch03_conditionals_05.sh 文件中找到。

运行此代码应输出以下内容:

 num1 is not equal to num2

在上面的代码中,我声明了两个变量。然后,我在 if** - **else 块中使用了 -eq 比较操作符来打印结果。你也可以把它放在一行中,像下面这样:

 num1=10; num2=20; [ $num1 -eq $num2 ] && echo "num1 is greater" || echo "num2 is greater"

在前面的示例中,我声明了两个变量。然后,我将比较放在方括号中。逻辑与(&&)操作符表示 如果前一个命令成功(即返回真或 0),则执行下一个命令。否则,逻辑或(||)操作符表示 如果前一个命令不成功(即返回非零退出代码),则执行下一个命令。尝试在终端中运行此代码并查看输出。你应该会看到以下输出:

 num2 is greater

以下代码演示如何使用小于 -lt 操作符比较整数:

 #!/usr/bin/env bash
num1=10
num2=20
if [ $num1 -lt $num2 ]; then
    echo "num1 is less than num2"
else
    echo "num1 is not less than num2"
fi

这个示例代码可以在本章文件夹中的 ch03_conditionals_06.sh 文件中找到。

运行上述代码应输出以下内容:

 num1 is less than num2

以下代码演示如何使用大于或等于操作符 -ge

 #!/usr/bin/env bash
num1=10
num2=20
if [ $num1 -ge $num2 ]; then
    echo "num1 is greater than or equal to num2"
else
    echo "num1 is not greater than or equal to num2"
fi

这个示例代码可以在本章文件夹中的ch03_conditionals_07.sh文件找到。

这段代码的输出应该是:

 num1 is not greater than or equal to num2

Bash 中的字符串比较使用=!=表示等于和不等于,使用<>表示字典序比较。以下是 Bash 中的字符串基本操作:

  • -z STRING : 如果字符串为空,则为真

  • -n STRING : 如果字符串不为空,则为真

  • STRING1 == STRING2 : 如果字符串相等,则为真

  • STRING1 != STRING2 : 如果字符串不相等,则为真

这是一个演示字符串比较的示例:

 #!/usr/bin/env bash
# Declare string variables
str1="Hello"
str2="World"
str3="Hello"
# Compare if str1 is equal to str2
if [ "$str1" == "$str2" ]; then
    echo "str1 is equal to str2"
else
    echo "str1 is not equal to str2"
fi
# Compare if str1 is not equal to str3
if [ "$str1" != "$str3" ]; then
    echo "str1 is not equal to str3"
else
    echo "str1 is equal to str3"
fi
# Lexicographical comparison if str1 is less than str2
if [[ "$str1" < "$str2" ]]; then
    echo "str1 is less than str2"
else
    echo "str1 is not less than str2"
fi

这个示例代码可以在本章文件夹中的ch03_conditionals_08.sh文件找到。

这是脚本的输出:

 str1 is not equal to str2
str1 is equal to str3
str1 is less than str2

这个示例展示了如何比较字符串中的字节。要提取字符串的第一个字符,可以使用byte="${string:1:1}"。然后像比较其他字符串一样比较byte

到目前为止,我们一直在比较简单的文本和整数。比较UTF-8编码的字符串与比较英文字符相同。Bash 本身没有内置直接比较UTF-16编码字符串的功能,也没有意识到编码的具体细节。然而,你可以使用外部工具,如iconv,来转换并比较这些字符串。不过,这个主题超出了本书的范围。我只是希望你了解这个限制,并知道如果你需要比较 UTF-16 编码的字符串,应该去哪里查找。

在深入讨论条件比较之后,接下来我们将学习如何使用逻辑运算符结合条件。

结合条件

如果你需要同时检查多个条件怎么办?Bash 通过逻辑运算符如&&(与)和||(或)为你提供了便利。这些运算符允许你组合多个条件,使你的脚本更加智能。以下示例展示了如何使用逻辑运算符检查多个条件:

 #!/usr/bin/env bash
if [ $USER == 'steve' ] && [ -f "/path/to/file.txt" ]; then
  echo "Hello, Steve. File exists." elif [ $USER == 'admin' ] || [ -f "/path/to/admin_file.txt" ]; then
  echo "Admin access granted or admin file exists." else
  echo "Access denied or file missing." fi

这个示例代码可以在本章文件夹中的ch03_conditionals_09.sh文件找到。

在这里,我们使用if条件,如果两个条件都为真,则评估结果为TRUE(返回0)。这段代码使用了逻辑与运算符&&。这意味着只有当第一个条件和第二个条件都为真时,结果才为真。

elif条件中,如果任一评估结果为真,则该块返回TRUE。可以将&&理解为“如果 test1 和 test2 都为真,返回TRUE”,而||则是“如果 test1 或 test2 为真,返回TRUE,否则返回FALSE(返回1)”。

逻辑运算符简化了比较操作,并为我们节省了大量的输入!没有它们,我们不得不编写更长且更复杂的代码。Bash 中的逻辑比较就像决策工具,帮助脚本理解并根据不同情况做出反应。就像你可能根据天气决定穿什么一样,Bash 脚本利用逻辑比较来决定根据它处理的数据采取什么操作。

case 语句

让我们来看看 case 语句。它有点像你在其他编程语言中可能知道的 switch 语句。case 语句允许你将一个变量与一系列模式进行匹配,并根据匹配的结果执行命令。当你需要对同一个变量进行多个条件检查时,它非常有用。下面是一个简单的例子:

 #!/bin/bash
read -p "Enter your favorite fruit: " fruit
case $fruit in
  apple) echo "Apple pie is classic!" ;;
  banana) echo "Bananas are full of potassium." ;;
  orange) echo "Orange you glad I didn't say banana?" ;;
  *) echo "Hmm, I don't know much about that fruit." ;;
esac

这个示例代码可以在本章节文件夹中的 ch03_conditionals_10.sh 文件中找到。

在这个脚本中,我们使用 read -p 提示用户输入他们最喜欢的水果,将输入赋值给水果变量,并使用 case 语句根据这个变量返回一个自定义的消息。*) 模式充当了一个通配符,类似于 if 语句中的 else

当我们运行它时,得到如下输出:

 ~ $ bash ch03_conditionals_10.sh
Enter your favorite fruit: pear
Hmm, I don't know much about that fruit.

介绍完 Bash 内建的 read 命令后,让我们回顾一下它的参数及其效果:

  • -p prompt :在读取输入前显示提示符

  • t timeout :为输入设置超时

  • -s :静默模式;不回显输入

  • -r :原始输入;不允许反斜杠转义字符

  • -a array :将输入读入数组

  • -n nchars :只读取 nchars 个字符

  • -d delimiter :读取直到遇到第一个定界符,而不是换行符

Bash 条件语句是你脚本工具箱中的一项强大工具。它们使得你的脚本可以做出决策,并智能地应对不同的情况。通过理解和使用 ifelseelifcase,并结合 &&|| 等逻辑运算符,你可以编写更高效、更灵活的 Bash 脚本。

增加了条件语句后,我们将在下一节探索循环。当循环与条件语句和变量结合时,它们使我们的脚本更强大!

使用循环重复

Bash 循环是迭代语句,是一个过程的重复。假设你有来自日志文件或漏洞扫描的多行数据的输出。手动检查每一行就像是用绑着的双手爬山;虽然可能做到,但不必要地具有挑战性。Bash 循环以其简单的语法和多样的应用,将这座大山变成了小土堆。在这一节中,我们将深入探讨 Bash 循环的本质,了解它们的类型、如何工作,以及它们为何是 Linux 环境中脚本编写不可或缺的一部分。

for 循环

for 循环是你在知道需要重复多少次某个动作时的首选。它就像是在说:“对于列表中的每一项,做这件事。” Bash for 循环会遍历一个项列表或一个值的范围,并为每一项执行命令。下面是基本的 for 循环语法:

 for variable in list
do
  command1
  command2
  ... done

请注意,for循环的语法通常是“在一组项目中对每个项目执行操作”。对于文件来说,这可能是for $line in lines。该语句初始化了循环。接下来是do关键字,后跟循环语句,最后以done结束。

假设你有一个包含一些文本文件的文件夹,并且你想打印出它们的文件名:

 for file in *.txt
do
  echo "Text file: $file"
done

这个循环会遍历当前目录中每个以.txt扩展名结尾的文件,将文件名赋值给file变量,然后通过echo语句将其打印出来。

当你编写一个简单的脚本,如这里所示时,通常通过使用分号将每个部分分隔开,可以更轻松地将其写成一个单行脚本,如下所示:

 ~ $ for file in *.txt;do echo "Text file: $file";done
Text file: example.txt
Text file: sample.txt

请注意,for循环有时会与sequence(序列)一起使用。Bash 的序列表达式生成一个整数或字符的范围。你需要定义整数或字符范围的起始和结束点。一个序列由大括号中的值范围组成。这个序列的形式为{START..END[..INCREMENT]}。如果没有提供INCREMENT,则默认值为1。序列通常与for循环结合使用。这里有一个简单的示例:

 ~ $ for n in {1..5};do echo "Current value of n: $n";done
Current value of n: 1
Current value of n: 2
Current value of n: 3
Current value of n: 4
Current value of n: 5
~ $ for n in {a..d};do echo "Current value of n: $n";done
Current value of n: a
Current value of n: b
Current value of n: c
Current value of n: d

现在你已经了解了for循环,让我们继续学习并探索while循环。

while 循环

当你想重复执行一个任务,直到某个条件不再为真时,使用while循环。这就像在说:“只要这个条件为真,就继续执行。”以下是while循环的基本语法:

 while [ condition ]
do
  command1
  command2
  ... done

这是一个创建从5开始倒计时的例子:

 #!/usr/bin/env bash
count=5
while [ $count -gt 0 ]
do
  echo "Countdown: $count"
  count=$((count-1))
done

这个示例代码可以在本章文件夹中的ch03_loops_01.sh文件中找到。

在这个示例中,我们将count变量初始化为5。然后,我们检查count的值;如果它大于0,我们打印该值,然后将其值减去1。只要count大于0,循环就会继续执行。每次迭代都会将count减去1,直到它变为0

运行这个脚本会得到以下输出:

 ~ $ bash ch03_loops_01.sh
Countdown: 5
Countdown: 4
Countdown: 3
Countdown: 2
Countdown: 1

我使用while循环的最常见方式是从文件中读取主机名或 IP 地址,并对其执行一些操作。有时,渗透测试工具会对单个主机执行某些操作,而我希望对一组主机进行操作。以下是一个简单示例,我使用一行脚本和while循环从文件中读取 IP 地址:

图 3.1 – 演示一行的 while 循环

图 3.1 – 演示一行的 while 循环

我稍后会更详细地解释这一点。

另一个例子是PetitPotam工具,它用于从未修补的 Windows 主机中强制获取密码哈希。你可以从github.com/topotam/PetitPotam获取更多信息并下载此工具。该工具只接受一个目标主机。在这里,我通过以下命令将其应用于包含主机列表的文件:

图 3.2 – 演示使用 PetitPotam 的单行 while 循环

图 3.2 – 演示使用 PetitPotam 的单行 while 循环

上一张截图的内容可以解释如下:

  • while read line** : **while 关键字确保我们会继续执行循环,直到条件不再成立。在这种情况下,我们会继续循环,直到文件结束。read 关键字从标准输入 (stdin) 中读取一行,直到遇到换行符,并将读取到的数据赋值给名为 line 的变量。当 read 命令读取到文件末尾时,会返回一个非零(false)状态,导致循环终止。

  • 在 Bash 脚本中,分号(;)用于在同一行中分隔多个命令。这使得你可以编写简洁的单行脚本,按顺序执行多个命令。

  • do python3 PetitPotam.py 10.2.10.99 $line : 在 Bash 脚本中,do 关键字标志着每次循环迭代中要执行的命令块的开始。在这种情况下,它运行的是 PetitPotam 命令。第一个 IP 地址 10.2.10.99 是我的 Kali 主机的 IP 地址。$line 变量是从文件中读取的一行数据,它成为 PetitPotam 命令的目标 IP 地址。

  • done : 在 Bash 脚本中,done 关键字标志着每次循环迭代中执行的命令块的结束。

  • < ips.txt : 我将 ips.txt 文件的内容重定向到 stdin,以便被 read 命令读取。该文件包含一个 IP 地址列表,每行一个地址。

在运行 PetitPotam 命令之前,我在另一个终端标签中使用 sudo responder -I eth0 命令运行了 Responder。Responder 是一个恶意服务器,旨在从受害者处获取认证信息。如果你正在进行这个练习,请确保将 IP 地址替换为你自己的。在 Responder 输出中,我发现从一个易受攻击的系统捕获了密码哈希:

图 3.3 – 捕获到来自受害者的密码哈希

图 3.3 – 捕获到来自受害者的密码哈希

如果没有 Bash while 循环,我就得为网络中的每个主机手动运行命令。如果我在测试一个大规模的网络,手动操作会非常疲惫,且如果没有利用 Bash 的强大功能,我可能会浪费大量时间!

现在你已经了解了 while 循环的强大功能,让我们来看一下它的替代者,until 循环。

until 循环

until 循环与 while 循环相反。它会一直运行,直到某个条件变为真。可以把它理解为:“直到发生这个,才做那个。”

until 循环的基本语法如下所示:

 until [ condition ]
do
  command1
  command2
  ... done

假设你在等待一个名为 done.txt 的文件出现在当前目录中:

 #!/usr/bin/env bash
until [ -f done.txt ]
do
  echo "Waiting for done.txt..."   sleep 1
done

该示例代码可以在本章文件夹中的 ch03_loops_02.sh 文件中找到。

该循环将持续运行,直到done.txt文件存在,每秒检查一次。

我很少使用until循环;然而,在某些情况下,当你想要做某件事直到某个条件为真时,它非常适用。

接下来,我们将探讨如何使用select来构建交互式菜单!

select — 简化交互式菜单

另一个较不为人知的循环命令是select。它非常适合在脚本中创建简单的交互式菜单。使用select,用户可以从呈现的选项中进行选择,非常适合用作导航或设置菜单:

 #!/usr/bin/env bash
echo "What's your favorite programming language?" select lang in Python Bash Ruby "C/C++" Quit; do
  case $lang in
    Python) echo "Great choice! Python is versatile." ;;
    Bash) echo "Bash is great for shell scripting and automation!" ;;
    Ruby) echo "Ruby is used in the Metasploit Framework." ;;
    "C/C++") echo "C/C++ is powerful for system-level programming." ;;
    Quit) break ;;
    *) echo "Invalid option. Please try again." ;;
  esac
done

这个示例代码可以在本章文件夹中的ch03_loops_03.sh文件中找到。

该脚本展示了一系列编程语言,并根据用户的选择执行相应命令。select命令会自动创建一个编号菜单,用户输入对应的数字来选择。请注意,每个选项后面必须加上两个分号(;;)。*)表达式是一个“穿透”选项,用来捕获那些没有匹配前面选项的输入。

运行时的效果如下:

 ~ $ bash ch03_loops_03.sh
What's your favorite programming language? 1) Python
2) Bash
3) Ruby
4) C/C++
5) Quit
#? 2
Bash is great for shell scripting and automation!

注意,当你运行它时,它会持续循环,直到你输入5以退出,这时使用了代码中的break语句。break语句会跳出循环。break语句可以在任何循环中使用,以退出循环,无论条件语句的返回值是什么。

现在你已经掌握了使用循环的方法,让我们来探索一些高级示例。

高级用法 —— 嵌套循环

你可以将循环嵌套在彼此之间,并使用breakcontinue关键字来更精确地控制流程。下面是一个打印简单图案的示例:

 #!/usr/bin/env bash
for i in {1..3}
do
  for j in {1..3}
  do
    echo -n "$i$j "
  done
  echo "" # New line after each row
done

这个示例代码可以在本章文件夹中的ch03_loops_04.sh文件中找到。

该脚本打印了一个 3x3 的数字网格,展示了嵌套循环的工作原理:

 ~ $ bash ch03_loops_04.sh
11 12 13
21 22 23
31 32 33

接下来,让我们探讨如何使用breakcontinue关键字,帮助我们在嵌套循环中使用高级逻辑。

使用breakcontinue

break命令会完全退出循环,而continue命令会跳过当前循环的其余部分,开始下一次迭代。以下示例结合了breakcontinue来演示这些概念:

 #!/usr/bin/env bash
for i in {1..20}; do
  if ! [[ $(($i%2)) == 0 ]]; then
    continue
  elif [[ $i -eq 10 ]]; then
    break
  else echo $i
  fi
done

这个示例代码可以在本章文件夹中的ch03_loops_05.sh文件中找到。

在前面的示例中,for循环遍历了从120的一个序列。接下来,我介绍了取余运算符%,它返回除法运算的余数。如果余数不为零,循环将继续执行下一次迭代。如果i的值等于10,则退出循环。否则,它会打印出i的值。运行该代码后的结果如下:

 ~ $ bash ch03_loops_05.sh
2
4
6
8

正如你所预期的,它会打印出所有偶数,并在到达10时退出。

Bash 循环是脚本编写的基础部分,可以简化和自动化重复任务。无论是迭代文件、等待条件,还是创建交互式菜单,理解这些循环可以显著提升你的脚本技巧。从小处开始,尝试示例,很快你就能像专家一样进行循环操作!

在下一部分,你将结合之前学到的内容和一个新概念:数组。

使用数组作为数据容器

Bash 脚本的一个强大特性是数组的使用。数组允许你在一个变量中存储多个值,这使得你的脚本更加高效,代码更简洁。让我们深入了解 Bash 数组的基础知识,并通过实际示例探索如何利用它们。

从本质上讲,数组是一个可以通过索引访问的元素集合。可以把它想象成一排邮箱,每个邮箱都有一个唯一的号码。你可以在每个邮箱(元素)中存储不同的邮件(数据),并通过它们的邮箱号码(索引)来取出它们。

在 Bash 中,数组非常灵活。它们不要求你声明类型,并且可以根据需要增长或缩小。这意味着你可以在不必担心数组大小的情况下添加或删除元素。

在 Bash 中声明数组非常直接。你不需要显式地声明变量为数组;只需在数组上下文中为其赋值即可。以下是一个简单的示例:

 my_array=(apple banana cherry)

这一行创建了一个名为my_array的数组,其中包含三个元素:applebananacherry

要访问数组中的元素,必须使用以下语法:

 ${array_name[index]}

记住,Bash 中的数组索引从0开始。所以,要访问my_array中的第一个元素(apple),你可以使用以下语法:

 ${my_array[0]}

向数组添加元素或修改现有元素同样简单。要将元素添加到数组的末尾,可以使用以下语法:

 my_array+=(date)

+=操作符在许多编程语言中都很常见。这个操作表示my_array等于当前的my_array值加上date

现在,my_array包含四个元素:applebananacherrydate。要修改现有元素,必须直接为其赋予新值:

 my_array[1]=blueberry

此命令将第二个元素从banana更改为blueberry

遍历数组

遍历数组是脚本中常见的任务。以下是如何遍历my_array中的每个元素:

 #!/usr/bin/env bash
my_array=(apple banana cherry)
for fruit in "${my_array[@]}"
do
  echo "Fruit: $fruit"
done

这个示例代码可以在本章文件夹中的ch03_arrays_01.sh文件中找到。

这个循环会在新的一行打印数组中的每个元素,如下所示:

 ~ $ bash ch03_arrays_01.sh
Fruit: apple
Fruit: banana
Fruit: cherry

Bash 还支持关联数组(有时称为哈希映射字典),其中每个元素由一个键而不是数字索引来标识。要声明一个关联数组,使用-A标志与declare关键字:

 #!/usr/bin/env bash
# Declare the associative array. declare -A my_assoc_array
# Assign new keys/value pairs to the associative array. my_assoc_array[apple]="green"
my_assoc_array[banana]="yellow"
my_assoc_array[cherry]="red"
# The whole associative array is accessed as follows:
for key in "${!my_assoc_array[@]}"
do
  # A key/value pair is accessed as shown:
  echo "$key: ${my_assoc_array[$key]}"
done

这个示例代码可以在本章文件夹中的ch03_arrays_02.sh文件中找到。

访问和修改关联数组中的元素的方式类似于索引数组,但你使用的是键而非数字索引。

在之前的脚本中,关联数组是通过使用 declare -A 和数组名称来声明的。然后,将键值对添加到关联数组中。接下来,for 循环使用 key 变量来访问数组中的每个循环。

重要提示

你可以通过 "${!my_assoc_array[@]}" 引用整个关联数组。

最后,在每次 for 循环迭代中,当前的键值对将被打印出来:

 ~ $ bash ch03_arrays_02.sh
cherry: red
apple: green
banana: yellow

你可能已经注意到 Bash 中的关联数组并不保持顺序;它们是无序的键值对集合。这就是为什么键值对被打印出来的顺序与它们添加到数组中的顺序不同。

你可以使用以下语法访问特定关联数组键值对的值:

 {my_assoc_array[key]}

以下脚本展示了与前一个脚本相同的代码,只是在最后一行添加了这个概念:

 #!/usr/bin/env bash
# Declare the associative array. declare -A my_assoc_array
# Assign new keys and values to the associative array. my_assoc_array[apple]="green"
my_assoc_array[banana]="yellow"
my_assoc_array[cherry]="red"
# The whole associative array is accessed as follows:
for key in "${!my_assoc_array[@]}"
do
  # A key/value pair is accessed as shown:
  echo "$key: ${my_assoc_array[$key]}"
done
# Access a specific value from the associative array:
echo "The color of an apple is: ${my_assoc_array[apple]}"

这个示例代码可以在本章文件夹中的 ch03_arrays_03.sh 文件中找到。

该脚本的输出如下:

 ~ $ bash ch03_arrays_03.sh
cherry: red
apple: green
banana: yellow
The color of an apple is: green

Bash 数组是一个强大的功能,可以使你的脚本更加高效且易于阅读。无论你是存储一个简单的项目列表,还是处理更复杂的数据结构,如关联数组,了解如何使用数组将显著提升你的脚本能力。记住,实践是掌握 Bash 数组的关键,所以不要犹豫,尽管尝试提供的示例并自行探索更多应用。

摘要

这总结了一组紧密相关的主题。Bash 变量、条件语句、循环和数组是 Bash 脚本中的工具,分别用于存储数据、做出决策、重复任务和处理值列表。

循环是整个过程的明星。就像任何节目的演员阵容一样,循环也需要配角。对于循环,它们需要变量来为数据分配标签,需要条件语句来测试相等性,还需要数组来存储数据。它们齐心协力,使你的 Bash 脚本更加强大和灵活。

在下一章,你将学习 Bash 正则表达式,这是一项宝贵的技能,你需要掌握它才能有效地进行文本搜索和匹配。

第四章:正则表达式

正则表达式,或称regex,刚开始可能看起来让人望而生畏,但它们对于任何处理文本的人来说,尤其是在 Bash 脚本中,是一个非常强大的工具。本章旨在帮助你逐步掌握正则表达式的世界,从基础知识开始,逐步深入到更复杂的模式和技术。无论你是想验证电子邮件地址、搜索日志文件中的特定模式,还是自动化文本处理任务,理解正则表达式将大大改变你的工作方式。我们将探索如何构建正则表达式模式,理解其结构,并将其应用于实际场景。到本章结束时,你不仅能熟练使用正则表达式,还能体会到它们如何使你的脚本任务更加高效和灵活。

本章内容建立在上一章学习的基础上。正则表达式常常与变量和条件语句一起使用。例如,你可能会使用一个while循环从stdin或文件中读取一行数据,并将读取的数据赋值给一个变量。然后,你将对该变量数据执行正则表达式,最后使用条件语句作出决策。

本章我们将讨论以下主要主题:

  • 正则表达式基础

  • 高级正则表达式模式和技巧

  • 演示实际应用

  • 正则表达式技巧和最佳实践

技术要求

第一章所述,能够安装 Kali 虚拟机是有帮助的,但不是必须的。

本章的代码可以在 github.com/PacktPublishing/Bash-Shell-Scripting-for-Pentesters/tree/main/Chapter04 找到。

正则表达式基础

本质上,正则表达式(regex)是一种搜索、匹配和操作文本的方法。可以把它们看作是一个复杂的搜索工具,超越了你在文本编辑器或文字处理软件中标准搜索功能的能力。正则表达式允许你在文本中定义模式,使得进行复杂的搜索和编辑变得相对简单。

正则表达式极其灵活。以下是它们可以用来做的几个例子:

  • 数据验证:确保用户输入符合特定格式,例如电子邮件地址或电话号码

  • 数据提取:从更大的数据集提取特定的信息,例如从网页中提取所有 URL

  • 搜索和替换:基于模式而非精确匹配在文档中查找和替换文本

正则表达式字母表由字符元字符组成。字符就是你想要在文本中查找的字母、数字和符号。另一方面,元字符是正则表达式的特殊“魔法”。它们是具有特殊含义的符号,帮助定义模式。一些常见的元字符包括.*+?^$[]{n}{n,m}{n,}(a|b)=~

在这一节中,我将展示使用grep命令的示例。grep命令用于在文件或管道输入中搜索模式。你可以通过输入man grep命令来了解更多关于grep的信息。

句点(.)元字符匹配除了换行符以外的任何单个字符,换行符表示为\n。我在正则表达式中常用.的一个例子是,当解析程序的输出并且想要去除空白行时。正如.匹配任何字符,当它单独使用时,它会移除所有空白行,因为没有任何内容可以匹配。下图展示了.匹配任何字符的情况,匹配的文本以红色字体突出显示:

图 4.1 – 使用句点元字符匹配非空行

图 4.1 – 使用句点元字符匹配非空行

如您所见,. 元字符不仅匹配任何字符(以红色高亮显示),它还帮助我们仅匹配非空行。

星号(*****)元字符匹配前一个元素出现零次或多次。假设你有一个名为sample.txt的文本文件,里面包含多行文本,你想要查找匹配ho*p模式的行。这个模式应该匹配hophoophooooop等行。sample.txt文件的内容如下所示:

图 4.2 – sample.txt 文件的内容

图 4.2 – sample.txt 文件的内容

小贴士

你必须使用带有-E选项的grep命令来进行扩展正则表达式,这样你才能使用*元字符。

此命令告诉grepsample.txt中搜索匹配ho*p模式的行:grep -E 'ho*p' sample.txt-E选项用于启用扩展正则表达式,支持包括*元字符在内的多种功能。否则,外部正则表达式中,*被称为glob字符,正如在第二章中讨论的那样。

加号(+)元字符匹配前一个元素出现一次或多次。例如,如果你正在分析日志文件中的错误,可以使用Error: +模式帮助你找到Error:后跟一个或多个空格的行,表示错误信息的开始。如果没有+元字符,你就会错过多个空格的情况,或者浪费时间筛选无关的数据。

问号(?)元字符使得前面的元素变为可选。?元字符的核心含义是可选性。它告诉正则表达式引擎匹配前面的元素零次或一次。简单来说,它意味着紧跟在?前面的字符或模式可能出现,也可以不出现。

这个概念通过一个示例更容易理解。假设你需要处理日志文件。这些日志遵循如app-log-2024.txt这样的命名规范,但有时它们会包含一个额外的标识符,如app-log-2024-debug.txt。使用?元字符可以让你的脚本更加灵活。像app-log-2024(-debug)?.txt这样的模式就可以匹配这两个文件名,确保你的脚本在不同日志类型间无缝工作。

插入符号(^)元字符匹配行首。你可能会想,为什么需要指定某个内容必须出现在行的开头?这完全是为了精确。在这个例子中,如果我们不使用^元字符,仅仅搜索DONE,我们会得到文本中任何包含DONE的行——而不仅仅是行首的部分。这可能包括DONE出现在便签或提醒中的行,而不仅仅是任务状态标记。

美元符号($)元字符匹配行尾。

以下是使用$匹配的示例:

图 4.3 – 使用$元字符匹配字符串的结尾

图 4.3 – 使用$元字符匹配字符串的结尾

括号表达式([ ])匹配括号内的任何单个字符。你可以通过将^符号放在列表的第一个字符位置来执行逻辑表达式。这样会匹配列表中不包含的字符。例如,如果你想匹配元音字符,适当的括号表达式是[aeiou],而如果你想匹配辅音字符,可以使用[^aeiou]

范围表达式经常在括号表达式内使用,节省你输入所有后续字符或数字的时间和精力。例如,代替在括号内输入从az的字母,你可以使用[a-z]作为便捷的快捷方式。同样,对于数字,你可以使用类似[1-10]的范围。下图展示了括号表达式的工作原理:

图 4.4 – 使用括号表达式的示例

图 4.4 – 使用括号表达式的示例

括号表达式是一个非常有价值且节省时间的正则表达式功能!

{n}元字符指定前面的元素恰好匹配n次。它也可以写作{n, m}{n,},表示前面的元素匹配nm次,或者恰好匹配n次或更多次。我们来看一下如何使用这个:

图 4.5 – 一个展示如何匹配 n 次或更多次的示例

图 4.5 – 一个示例,展示如何匹配 n 次或更多次

上面的图示显示了我指定必须匹配3次或更多次字符o。单词hoooop是唯一的匹配项。请注意,我在grep中必须包含-E参数,以启用扩展正则表达式功能,并且必须使用反斜杠转义方括号。

(a|b) 元字符匹配ab

=~ 匹配操作符通常用于脚本中。让我们讨论下图所示的基本示例:

图 4.6 – 演示匹配操作符的示例

图 4.6 – 演示匹配操作符的示例

如果左侧的字符串与右侧的正则表达式匹配,表达式的值为true,并且[[ ]]括号表达式的退出状态为 0(零)。在 Bash 脚本中,退出状态为 0 表示成功或true,任何非 0 的退出值表示失败或false

在 Bash 脚本中,&&||逻辑操作符,用于条件表达式中组合多个命令或条件。它们的使用与命令的退出状态相关。应用到前面的示例中,如果匹配模式找到了输入表达式的匹配项,结果将是退出状态 0,或为 true。如果字符串与正则表达式不匹配,表达式值为 false,[[ ]]表达式的退出状态为 1(退出状态 1 表示失败或 false)。&& 操作符将退出状态传递给后续的||表达式,可以认为是truefalse。如果表达式为 true,左侧的语句 echo Match found! 将被执行。如果为 false,右侧的语句 echo "No match" 将被执行。

现在你已经熟悉了元字符,接下来让我们探索字符类,它们在使用我们刚刚介绍的括号表达式时提供了方便的快捷方式。

使用字符类

在括号表达式中使用时,字符类是一个方便的快捷方式,可以简化正则表达式:

  • [:alpha:] : 字母字符

  • [:alnum:] : 字母数字字符

  • [:digit:] : 数字 0 到 9

  • [:blank:] : 空格和制表符

  • [:cntrl:] : 控制字符

  • [:lower:] : 小写字母

  • [:upper:] : 大写字母

  • [:punct:] : 标点符号

  • [:space:] : 空格字符,包括空格、制表符、换行符、垂直制表符、换页符和回车符

提示

字符类必须包含在括号表达式中——例如,[[:alpha:]]

字符类是一种节省时间的简写方法,极大简化了创建正则表达式的过程。

标志 – 修改你的搜索

正则表达式允许你通过标志修改搜索方式。这些通常是单个字母,改变正则引擎如何解释你的模式。以下是一些例子:

  • i : 使搜索不区分大小写

  • g :执行全局搜索(找到所有匹配项,而不是在第一次匹配后停止)

  • m :多行模式(改变 ^$ 的行为,使其匹配行的开始和结束,而不是整个字符串)

这不是一个详尽无遗的列表。更多信息请参见 www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html。这些标志可以单独使用,也可以组合使用,具体取决于正则表达式操作的需求。这些标志的应用方式在不同工具间略有不同,但通常是附加到正则表达式模式中的。由于它们的使用依赖于工具,稍后我在本章的实际示例中会展示它们如何使用。

现在你已经理解了正则表达式的基础,接下来让我们回顾一些示例,展示它们是如何工作的。

应用基础正则表达式示例

这个示例仅使用 grep 来匹配字母 t。默认情况下,grep 执行全局搜索。因此,g 标志不是必需的:

图 4.7 – 对 t 字符的基本 grep

图 4.7 – 对 t 字符的基本 grep

这个示例匹配所有元音:

图 4.8 – 匹配所有元音的模式

图 4.8 – 匹配所有元音的模式

这个示例匹配所有辅音。记住,^ 符号在括号内有不同的含义。这实际上意味着它匹配列表中不包含的任何字符:

图 4.9 – 匹配所有辅音的模式

图 4.9 – 匹配所有辅音的模式

现在,我将展示一个稍微复杂一点的示例。你能发现下面两个示例之间的区别吗?

图 4.10 – 用于演示微妙差异的两种模式

图 4.10 – 用于演示微妙差异的两种模式

第一种模式匹配 t 后跟零个或多个不是 w 的字符。重要的是要注意,* 适用于模式的 [^w] 部分,允许任何不以 w 开头的字符序列紧跟在 t 后面。因此,它匹配所有内容,包括 told 中的空格,并一直匹配到输入的末尾。

第二种模式专门寻找 t 后跟一个不是 w 的字符,然后是零个或多个字母字符。在 [^w] 后包含 [[:alpha:]]* 表示在找到 t 后跟任何非 w 字符时,只有当后续字符是字母时,才匹配。

提示

图 4 .10 中的示例展示了反斜杠字符用于转义星号。少数几个字符具有特殊意义。以下字符必须使用反斜杠进行转义:[\^$.|?*+()

现在你已经理解了基础知识,让我们来体验一些高级正则表达式概念。

高级正则表达式模式和技巧

在正则表达式中,使用捕获分组就像是将模式的一部分放入一个框中。框内的所有内容都被视为一个单元。你可以对其应用量词,查找重复项,甚至提取其中的信息。在 Bash 中,你使用圆括号()来创建这些分组。

分组不仅仅是将模式的部分视为一个单元;它还涉及到捕获信息。当你将正则表达式的一部分分组时,Bash 会记住与该部分模式匹配的文本。这对于从字符串中提取信息非常有用。

假设你正在处理日志文件,并希望提取时间戳。你的日志行可能看起来像这样:2023-04-01 12:00:00 错误:出现问题。匹配时间戳的正则表达式模式可能是(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})。在这里,\d匹配任何数字,而{n}指定该元素应重复的次数。整个时间戳模式是分组的,因此你可以轻松地从字符串中提取出来。

让我们通过一些实际例子,帮助你巩固对捕获分组的理解。

实际示例——使用正则表达式提取数据

假设你需要从列表中提取用户名及其对应的电子邮件地址。列表看起来像这样:

 john_doe: john.doe@example.com
jane_smith: jane.smith@example.com

你可以使用以下正则表达式模式来匹配并提取用户名和电子邮件地址:

 ([a-zA-Z0-9_]+): ([a-zA-Z0-9_.]+@[a-zA-Z0-9_.]+)

在这里,[a-zA-Z0-9_]+匹配一个或多个字母数字字符或下划线(用户名),而[a-zA-Z0-9_.]+@[a-zA-Z0-9_.]+匹配电子邮件地址。通过对它们进行分组,你可以分别提取用户名和电子邮件地址。

例如,假设你有字符串I love apples and I love oranges,并且你想找到每个I love的实例。在正则表达式中,你可以写出这个模式:(I love)。这告诉 Bash 将I love视为一个单元。

在 Bash 中使用正则表达式分组一开始可能显得复杂,但一旦你理解了基础,它会为字符串处理和数据提取打开一扇新的大门。通过将模式分解为可管理的组,你可以简化脚本并提高效率。记住,实践出真知。开始在 Bash 脚本中尝试正则表达式分组,你很快就会发现它的重要性。

接下来,我们将通过展示如何使用选择扩展正则表达式分组,使你的捕获分组更强大、更灵活。

使用选择

正则表达式中的选择通过管道符号(|)表示,它的功能类似于逻辑“或”。它允许你在同一个正则表达式中指定多个模式,从而提供匹配某个或另一个内容的方式。可以将其理解为告诉你的脚本:“嘿,如果你看到这个或那个,认为它是匹配的。”

假设你正在编写一个需要处理特定扩展名文件的脚本。你对.txt.log文件感兴趣,但希望通过一个正则表达式来处理它们。你可以这样做:

 #!/usr/bin/env bash
filename="example.txt"
if [[ $filename =~ \.(txt|log)$ ]]; then
  echo "File is either a .txt or .log file." else
  echo "File is not a .txt or .log file." fi

这个示例代码可以在本章文件夹中的ch04_regex_01.sh文件中找到。

运行这个示例会产生以下输出:

 $ bash ch04_regex_01.sh
File is either a .txt or .log file.

在这个示例中,(txt|log\)$是正则表达式模式。管道符号|将两个选择项txtlog分隔开,而反斜杠\用于转义那些在正则表达式中有特殊意义的字符。美元符号$确保该模式匹配字符串的结尾,防止像example.txt.bak这样的文件出现误匹配。

你可能会想,为什么要使用交替(alternation),而不直接为每个情况编写独立的条件?答案在于简洁性和效率。使用交替,你可以将多个条件合并成一行代码,使你的脚本更加简洁,便于维护。

在需要匹配大量可能性时,交替可以显著减少代码的复杂性。与其拥有冗长的if语句或笨重的case语句,你可以将所有的选项列在一处。

虽然交替非常强大,但必须明智使用,以避免陷入误区。这里有一些提示供你参考:

  • 具体一点:正则表达式模式有时可能会匹配到超出你预期的内容。为了避免意外行为,请尽量使你的模式尽可能具体。

  • 测试:始终使用不同的输入测试你的正则表达式模式,以确保它们按预期行为运行。像grep这样的工具和在线正则表达式测试工具(regex101.com)对于此类测试非常有帮助。

在 Bash 脚本中,正则表达式的交替(alternation)就像是你武器库中的一件秘密武器。它可以通过简化复杂的模式匹配逻辑,帮助你编写更加简洁、易读且易维护的代码。无论你是经验丰富的脚本编写者,还是刚刚入门,新掌握交替用法无疑会让你的脚本编写过程更加顺畅和愉快。

记住,编写有效脚本的关键不仅仅是知道有哪些工具可用,更重要的是理解如何明智地使用它们。使用正则表达式交替,你将能够应对各种字符串匹配的挑战。

现在你已经对正则表达式的工作原理有了不错的掌握,我们来探索一些实际的正则表达式应用。

演示实际应用

在这里,我使用了前面章节中介绍的各种变量和数组。让我们通过以下 Bash 脚本来实践一下:

图 4.11 – 在实际应用中引入 BASH_REMATCH

图 4.11 – 在实际应用中引入 BASH_REMATCH

这个示例代码可以在本章文件夹中的 ch04_regex_02.sh 文件中找到。在这个脚本中,我在 第 3 行 声明了 user_list 变量。在 第 6 行,我声明了 pattern 变量。在 第 8 行,我启动了一个 while 循环,读取来自 $** **user_list 变量的每一行数据。

第 9 行,我使用了匹配操作符 =~,将每一行 ($line) 与我们的正则表达式模式 (**pattern)进行比较。这些通过pattern**) 进行比较。这些通过 `linepattern变量引用,它们已经声明。使用匹配操作符时,左侧的字符串(由pattern` 变量引用,它们已经声明。使用匹配操作符时,左侧的字符串(由 `line` 变量表示)将与右侧的正则表达式模式匹配。如果模式匹配,表达式返回 true(0);否则,返回 false(1)。

首先,模式通过相关的捕获组捕获用户名:([a-zA-Z0-9_]+)。记住,捕获组由圆括号()围绕一个正则表达式组成。在捕获组内,我们有一个字符集表达式,它将匹配所有字母数字字符,并加上下划线以匹配用户名。第二个捕获组匹配一个电子邮件地址。

如果某一行匹配,Bash 会将捕获的组填充到一个名为 BASH_REMATCH 的数组中。在这里,BASH_REMATCH[1] 包含第一个捕获组(用户名),而 BASH_REMATCH[2] 包含第二个组(电子邮件地址)。然后,我们将它们打印出来:

 ~ $ bash ch04_regex_02.sh
Username: john_doe, Email: john.doe@example.com
Username: jane_smith, Email: jane.smith@example.com

你发现我在哪儿可以使捕获组更容易阅读和编写吗?第一个捕获组 ([a-zA-Z0-9_]+) 可以简化为 ([[:alnum:]_]+),而第二个捕获组 ([a-zA-Z0-9_.]+@[a-zA-Z0-9_.]+) 可以简化为 ([[alnum]_.]+@[[:alnum:]_.]+)

使用 grep 匹配 IP 地址

在这个示例中,我们将查看一个实际的案例,涉及端口扫描以定位特定端口开放的 IP 地址。这是一个常见的渗透测试任务,通常用于生成主机列表,以便进行后续的目标扫描,或生成受影响主机的列表,用于渗透测试结果。

由于这涉及到扫描你的本地网络,确保你有权限扫描网络,如果你不拥有该网络。我已在本书的 GitHub 仓库中为方便起见,提供了一个来自我实验室的示例 Nmap 扫描文件:test_nmap.gnmap

使用以下 Nmap 命令扫描网络,将网络地址替换为适合你网络的地址:

 nmap -oG test.gnmap 10.1.0.0/24

扫描命令的选项指定了可以用来过滤的输出 -oG,输出文件名为 test_nmap.gnmap,后面跟着网络地址。

在我的扫描中,从 test_nmap.gnmap 文件输出的一行扫描结果如下所示:

 Host: 10.1.0.1 ()     Ports: 53/open/tcp//domain///, 80/open/tcp//http///, 443/open/tcp//https///

接下来,我们需要识别任何主机 IP 地址,其上开放了 httphttps 服务端口。在与 test_nmap.gnmap 文件位于同一目录下执行以下命令:

 grep /open/tcp//http test_nmap.gnmap | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b"

这个示例代码可以在本章文件夹中的 ch04_regex_03.sh 文件中找到。

上述命令使用 grep 来搜索文字的正则表达式(没有元字符),/open/tcp//http 。该命令的输出是每一行包含该字符串的完整文本行。管道字符 | 只是将第一个进程的输出 (stdout) 与下一个进程的输入 (stdin) 连接起来。然后,-oE 参数与 grep 命令一起提供。-o 选项意味着只输出匹配的文本,而不是整行文本,-E 选项启用扩展正则表达式功能。最后,命令的末尾是一个用于匹配 IP 地址的正则表达式模式。该命令产生以下输出:

 ~ $ grep /open/tcp//http test_nmap.gnmap | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b"
10.1.0.1
10.1.0.4
10.1.0.6
10.1.0.7
10.1.0.13

管道字符用于将输出重定向到另一个进程的输入,这是一个强大的功能,我们将在后面的章节中频繁使用。

使用方便的 grep 标志

虽然这些 grep 标志非常简单,但它们也非常方便。我经常使用它们,并希望与大家分享。

在内部网络渗透测试中,我经常做的一件事是使用我获得的任何凭证来枚举可以使用这些凭证访问的文件共享。在这个示例中,我使用 NetExec 来检查可以使用我拥有的凭证访问的 SMB 文件共享。你可以在 github.com/Pennyw0rth/NetExec 找到 NetExec。

下图显示了 NetExec SMB 文件共享枚举扫描的输出:

图 4.12 – NetExec SMB 共享枚举扫描

图 4.12 – NetExec SMB 共享枚举扫描

扫描结果已保存到文件 nxc.log 。假设我在一个包含数百甚至上千个主机的大型网络上运行了此扫描,并且我希望专注于寻找那些我可以读取或写入的共享,但我不想看到任何 IPC$PRINT$ 共享。

虽然有正则表达式模式可以合理地在这里匹配 READ** / **WRITE 的组合,但我们希望保持简单,以免频繁查阅笔记。以下命令可以完成这个目标:

图 4.13 – 我们的 grep 标志简化了任务

图 4.13 – 我们的 grep 标志简化了任务

此示例代码可以在本章文件夹中的 ch04_regex_04.sh 文件中找到。让我们分解一下这些命令的顺序:

  • cat nxc.log :此命令打印 nxc.log 文件的输出。

  • | :这将 cat 命令的输出连接到 grep 命令的输入。

  • grep -e READ -e WRITEgrep** **-e 标志指定一个模式。如果你包含额外的 -e 标志,可以使用多个模式。如果找到任一或两个单词,这将匹配成功。

  • grep -v …grep** **-v 标志表示反转匹配。这类似于逻辑 NOT 表达式。换句话说,过滤掉任何与此表达式匹配的内容。

在你的渗透测试生涯中,你将经常使用这些模式。

屏蔽 IP 地址

以下示例展示了如何使用 sed(流编辑器)命令编辑 IP 地址,但它也可以用于文件或输入流中的其他批量文本编辑。

假设你想在与别人共享 test_nmap.gnmap 文件之前,先对文件中的 IP 地址进行编辑。我们再次使用 IP 地址的正则表达式。不过,这一次,我们会将输出传递给 sed 并编辑所有的 IP 地址。在终端中运行以下命令:

 sed -E 's/([0-9]{1,3}\.){3}[0-9]{1,3}/REDACTED_IP/g' test_nmap.gnmap

这个示例代码可以在本章文件夹中的 ch04_regex_05.sh 文件中找到。输出应该显示文件中的每个 IP 地址已被编辑。

那么,这条 sed 命令是做什么的呢?

  • -E 选项启用扩展正则表达式。

  • sed 后面的命令被单引号括起来。

  • sed 命令和参数之后,你会看到类似 's/MATCH/REPLACE/g' 的模式。

  • s 选项表示查找下一个 / 字符之间的任何内容(MATCH 文本)。

  • 用下一个斜杠(/)字符之间的模式(REPLACE 文本)替换匹配的文本。

  • g 标志表示进行 全局 查找,并替换每一个匹配项。否则,如果正则表达式或字面字符串在同一行中匹配了两次,它只会对第一个匹配项执行替换。

在这个例子中,我们没有直接编辑原文件,只是编辑了屏幕上的文本输出。我们有两种方法可以编辑并保存文本:一种是包括 sed -i 标志,另一种是将输出重定向到文件。

在第一种情况下,通过添加 sed -** **i 标志可以就地编辑文件:

 sed -iE 's/([0-9]{1,3}\.){3}[0-9]{1,3}/REDACTED_IP/g' test_nmap.gnmap

这个示例代码可以在本章文件夹中的 ch04_regex_06.sh 文件中找到。另一个选项是省略 -i 标志。它会保留原文件,并将编辑后的文本重定向到一个新文件中:

 sed -E 's/([0-9]{1,3}\.){3}[0-9]{1,3}/REDACTED_IP/g' test_nmap.gnmap > new_test_nmap.gnmap

这个示例代码可以在本章文件夹中的 ch04_regex_07.sh 文件中找到。上述命令使用 > 字符将输出重定向到随后的文件名。

提示

当使用 > 字符将输出(stdout)重定向到文件时,如果文件已存在,它将覆盖该文件。如果想要追加内容而不是覆盖,命令中应使用 >>

接下来,让我们来看看如何使用 awk 进行正则表达式匹配。Awk 不仅仅是一个正则表达式工具,它是一个完整的编程语言。它的优势在于当你在处理表格数据(列、制表符或逗号分隔的数据)时,能发挥巨大的作用。在学习 awk 之前,我误以为它太复杂,常常将多个工具链在一起完成同样的工作,结果做了更多的工作,实际上如果直接使用 awk,反而省力。本章我会简要介绍几个快速的示例,更多深入的内容将在下一章中讲解。

Awk 程序可以是一行快速的一次性脚本,尽管它们也可以用于文件以处理更复杂的用例。一行 awk 脚本的格式是awk '模式 {动作}'模式动作可以省略一个,但不能同时省略。

默认字段分隔符是任何空白字符,比如空格或制表符。多个空白字符被视为一个单元。这对我来说非常有帮助,因为在学习 awk 之前我习惯使用tr -s ' '压缩或合并多个空格为一个。

在深入研究我们的第一个 awk 示例之前,让我们花一分钟来了解常见的 awk 术语:

  • 记录:输入文件的每一行被称为一个记录。

  • 字段:每一列都是一个字段。

  • $n:每个字段(列)。整个记录(行)是$0,第一个字段是$1,依此类推。

  • $NF:记录中的字段数。也可以用来指代最后一个字段。

  • $NR:到目前为止的记录数。

  • -F:字段分隔符;默认是一个空格。记住,任意数量的连续空格会被合并。因此,如果前两个字段由一个或多个空格分隔,$1$2仍然指代第一个和第二个字段(列)。

在下图中,您可以看到我系统上ps -ef命令的输出。这是我将在接下来的示例中使用的数据:

图 4.14 – 使用 ps 命令显示系统进程

图 4.14 – 使用 ps 命令显示系统进程

在我们的第一个 awk 示例中,我只是简单地打印每个记录():

图 4.15 – 使用$0 打印整个记录

图 4.15 – 使用$0 打印整个记录

这个例子的代码可以在本章节文件夹中的ch04_regex_08.sh文件中找到。

接下来,我们将看一个更高级的例子。在下图中,我使用了一个模式和动作。这个例子将匹配任何具有author UID 的进程,并打印 CMD($8,或第 8 个字段):

图 4.16 – 打印任何由 author 拥有的进程的 CMD

图 4.16 – 打印任何由 author 拥有的进程的 CMD

这个例子的代码可以在本章节文件夹中的ch04_regex_09.sh文件中找到。

在我们最后一个 awk 示例中,我们将研究如何使用正则表达式并用自定义分隔符打印输出:

图 4.17 – 使用正则表达式并用 awk 打印自定义输出

图 4.17 – 使用正则表达式并用 awk 打印自定义输出

这个例子的代码可以在本章节文件夹中的ch04_regex_10.sh文件中找到。在前面的示例中,模式中的正则表达式匹配第八个字段中以[irq/开头,后跟正好两位数字,后跟-pciehp]的内容。对于任何匹配的记录,动作打印第一个和第八个字段,用--->代替默认的空格分隔符。

我们仅仅触及了使用 awk 的表面。然而,这里展示的概念可以解决最常见的脚本任务。我们将在下一章深入探讨这一主题。

正则表达式技巧与最佳实践

以下技巧将帮助你创建复杂的正则表达式模式:

  • 从小开始:从简单的模式入手,逐渐引入更多的复杂性。

  • 练习:使用在线正则表达式测试工具,尝试不同的模式和标志。

  • 分解:面对复杂的模式时,把它分解成更小的部分,以便理解每个组成部分。

  • 参考文档:保持一份备忘单或参考指南,直到你对常见模式和元字符更熟悉为止。虽然网上有很多正则表达式备忘单,但我建议你在阅读本书并进行实验时自己制作一份。我发现,做笔记的过程有助于我记住难以理解的概念。

总结

在本章中,我们介绍了正则表达式的基本概念,并进一步讲解了更高级的主题,包括元字符和捕获组。最后,我们学习了如何将这些技巧应用于 Bash 脚本的实际应用中,这对于渗透测试非常有用。

正则表达式并不需要让人害怕。通过对字符、元字符和标志的基本理解,你已经在掌握它们的道路上了。无论是编辑文本、分析数据还是验证用户输入,正则表达式都可以成为你工具箱中不可或缺的工具。记住,像任何技能一样,熟练掌握需要练习。所以,尽管去尝试,开始实验,很快你会发现它们变得简单易懂。

在下一章中,我们将把本章学到的正则表达式概念与常见的文本解析工具结合起来,专注于常见的网络安全和渗透测试任务。

第五章:函数与脚本组织

在上一章中,你学习了正则表达式以及如何将其应用到实际应用中。本章将在此基础上,教你如何将之前学到的内容应用到组织代码成函数中。

函数是 Bash 脚本编写中的一个基础概念,它使你能够将代码组织成可重用和模块化的单元。通过掌握函数,你可以编写更高效、易于维护和更具可读性的脚本。本章将深入探讨Bash 函数的世界,探索它们的语法、用法和高级技巧。我们还将讨论函数如何帮助你组织脚本结构,并简化常见的渗透测试任务。最后,我们将比较和对比函数与别名。

本章结束时,你将对如何在 Bash 脚本中定义和使用函数有一个扎实的理解。你将学习如何向函数传递参数,理解函数内变量的作用域和生命周期,并探索递归和回调等高级技巧。最重要的是,你将看到函数如何帮助你编写更简洁、更有组织的脚本,这些脚本更容易维护和扩展,最终简化你的渗透测试工作流。

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

  • Bash 函数简介

  • 向函数传递参数

  • 函数中变量的作用域和生命周期

  • 高级函数技巧

  • 函数与别名

本章的代码可以在github.com/PacktPublishing/Bash-Shell-Scripting-for-Pentesters/tree/main/Chapter05找到。

Bash 函数简介

Bash 函数是任何在 Linux 系统上使用 Bash shell 的人的基本工具。它们允许你将可重用的代码块封装成命名的、带有参数的单元,可以在 Bash 脚本或交互式 shell 会话的任何地方调用。

让我们探索一下 Bash 函数为何如此重要和有用的几个关键原因。

代码重用

Bash 函数的最大好处之一是它们促进了代码重用。如果你发现自己在 Bash 脚本中反复编写相同或非常相似的代码,这通常是一个信号,提示你应该将这些代码提取成一个可重用的函数!

例如,假设你的许多脚本都需要以一致的方式解析命令行参数。与其将参数解析逻辑复制粘贴到每个脚本中,不如定义一个parse_args函数(代码可以在本章文件夹中的书籍 GitHub 仓库中的ch05_parse_args.sh找到):

 parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -h|--help)
        usage
        exit 0
        ;;
      -v|--verbose)
        verbose=true
        ;;
      *)
        echo "Invalid argument: $1"
        usage
        exit 1
        ;;
    esac
    shift
  done
}

现在,任何需要以这种方式解析参数的脚本都可以简单地调用parse_args函数。这使得你的代码更加简洁、易读且易于维护。如果你需要更新参数解析逻辑,只需在一个地方进行更改。

如果你不理解前面的函数在做什么,不用担心,你很快就会理解的。

模块化

Bash 函数使你可以将脚本拆解成更小、更独立、更易管理的部分。每个函数理想情况下应执行一个特定任务,并且能够做到这一点。

通过将脚本分解为模块化的函数,代码变得更易于理解、调试和维护。与包含数百行甚至数千行的单体脚本相比,排查一个特定函数的错误要简单得多。

设计良好的函数还通过为代码块赋予语义化的名称,使脚本更具可读性。例如,一个充满 fetch_dataparse_responseupdate_database 调用的脚本,比一个将所有操作混杂在一起的脚本更容易理解。

封装

函数提供了 封装,即它们为变量和其他资源创建了一个独立的作用域。默认情况下,在函数内部定义的任何变量都是局部变量。它们不会污染全局命名空间,也不会与脚本其他部分的变量发生冲突。

这种封装使得函数比到处使用全局变量更加安全,错误率也更低。它可以防止意外的命名冲突,并清晰地表明哪些变量在何处使用。

当然,有时你确实需要在函数之间或与主脚本共享变量。Bash 通过使用 global 关键字或 upvar-style 引用来允许这种做法。但这些技术应该谨慎使用。一般来说,最好保持函数的独立性和自包含性。

可测试性

Bash 函数的另一个主要好处是,它们使得代码更易于测试。为单个函数编写单元测试比为整个脚本编写测试要容易得多。

你可以编写测试用例,使用不同的参数调用函数,并验证它们是否产生预期的输出或副作用。这能让你更加确信代码是正确的,并且在做修改时帮助防止回归问题。

有几个流行的框架可以用于单元测试 Bash 代码,例如 BatsshUnit2。这些框架允许你以熟悉的 xUnit 风格编写简洁、易读的测试用例。

如果没有函数,你的 Bash 代码将很难以自动化的方式进行测试。你可能不得不采用笨拙的端到端测试,使用不同的参数调用整个脚本。这些测试较慢、更加脆弱,且更难维护。

性能

最后,使用 Bash 函数还可以提高脚本的性能,尤其是当你多次调用相同的代码时。

当你调用一个函数时,Bash 不需要每次都生成一个新进程或重新解析函数定义。函数的代码已经加载到内存中,因此调用函数的开销非常低。

相比之下,如果你将相同的代码放在一个单独的脚本中,并使用bash myscript.sh来调用它,Bash 每次都需要创建一个新进程并从磁盘解析该脚本。对于在紧密循环中调用的代码,这种开销会累积起来。

当然,函数的性能提升在绝对意义上通常是非常小的。Bash 中产生进程的速度已经很快。但是在优先考虑性能的脚本中,使用函数代替单独的脚本可以为你提供一点额外的提升。

现在你已经理解了函数的用途,让我们来探讨如何定义和调用一个函数。

定义和调用函数

在 Bash 中定义一个函数,你可以使用以下语法(代码可以在本章的 GitHub 仓库文件夹中找到,名为ch05_function_definition.sh):

 function_name() {
  # commands go here
}

或者,你可以在函数名之前使用function关键字:

 function function_name {
   # commands go here
}

让我们分解一下函数定义的组件:

  • function_name:这是你给函数起的名字。它应该具有描述性,并遵循与变量相同的命名规则(字母数字字符和下划线,以字母或下划线开头)。

  • ():函数名后的圆括号是必需的。

  • {}:花括号包围了函数体,你在其中放置构成函数的命令。

下面是一个简单的示例函数,它打印问候语:

 greet() {
  echo "Hello, world!" }

以下是解释:

  • greet是函数的名称。函数名后面必须跟圆括号。

  • 花括号{}包围了函数体。

  • 函数体内的echo命令将字符串Hello, world!打印到控制台。

一旦定义了函数,你可以通过简单地使用其名称并加上任何参数(如果需要)来调用它。以下是一个greet函数的定义示例:

 greet() {
  echo "Hello, world!" }

以下是我们如何调用该函数:

 greet

输出如下:

 Hello, world!

这段代码可以在书本的 GitHub 仓库中找到,名为ch05_greet.sh,我们可以如下解释:

  • greet函数是通过echo命令定义的,它将Hello, world!打印出来。

  • 调用函数时,我们只需在新的一行中使用它的名称greet

  • 当脚本执行时,greet函数被调用,输出Hello, world!被打印到控制台。

你可以在脚本中多次调用一个函数。

学会了如何声明和调用函数后,让我们进入下一部分,在这里你将学习如何将参数传递给函数以及如何将其应用于实际应用中。

向函数传递参数

Bash 函数是自动化重复任务和创建可重用代码块的强大工具。它们允许你将一系列命令封装成一个单一的、命名的单元,可以从脚本中的任何地方调用。然而,当你能够向它们传递参数时,函数会变得更加多功能和灵活。

向 Bash 函数传递参数是一种技术,可以让你为函数提供动态输入,使它们在不同场景中更具适应性和可重用性。通过接受参数,函数可以根据传递给它们的特定值执行操作,而不是依赖于函数内部的硬编码或预定义值。

以下是向 Bash 函数传递参数的一些好处:

  • 灵活性:接受参数的函数可以在多种上下文中使用。你可以创建一个单一的函数,根据传递的参数调整其行为,而不是创建多个略有不同的函数。这促进了代码的重用并减少了重复。

  • 参数化:参数允许你对函数进行参数化,这意味着你可以传递不同的值来控制函数的行为。这使你能够根据特定的需求或输入自定义函数的操作,使其更加多才多艺,适用于不同的场景。

  • 模块化:通过接受参数,函数变成了自包含的模块,可以独立于周围的代码运行。它们可以轻松地移动或在其他脚本中重用,而无需进行重大修改。这种模块化提升了代码组织和可维护性。

  • 可读性:当函数接受参数时,它使代码更加易读和自解释。参数清楚地指示了函数期望的值以及如何使用它们。这提高了代码的可理解性,并使其他开发人员更容易理解和维护脚本。

  • 效率:向函数传递参数可以通过避免在函数内部使用全局变量或复杂逻辑来帮助优化代码。函数可以直接通过其参数接收所需的数据,而不依赖于外部变量,从而使代码更加高效和专注。

在本教程中,我们将探索传递参数给 Bash 函数的不同方式,并展示如何在脚本中有效利用这一技术。通过掌握传递参数的技巧,你将能够创建更加灵活、可重用且易于维护的 Bash 函数,从而大大增强你的脚本能力。

所以,让我们深入学习如何利用向 Bash 函数传递参数的强大功能吧!

让我们从一个基本的 Bash 函数示例开始,该函数接受参数:

 greet() {
  echo "Hello, $1!" }
greet "John"

当你运行这个脚本并调用 greet 函数时,它将输出以下内容:

 Hello, John!

Bash 函数可以接受多个参数。让我们修改之前的示例,以处理多个参数(这段代码可以在书籍的 GitHub 仓库中找到,文件名为 ch05_greet_args.sh ):

 greet() {
  echo "Hello, $1 $2!" }
greet "John" "Doe"

以下是相关解释:

  • greet 函数现在期望两个参数。

  • 在函数内部,$1 代表第一个参数,$2 代表第二个参数。

  • 更新后的 echo 命令将两个参数都包括在问候消息中。

  • 我们调用 greet 函数并传入两个参数:JohnDoe

输出将如下所示:

 Hello, John Doe!

学习了基本的参数传递方式后,让我们继续学习一些更高级的参数传递用法。

处理可变数量的参数

有时,您可能希望创建一个可以处理可变数量参数的函数。Bash 提供了一个特殊的变量 $@,它表示传递给函数的所有参数。这里有一个示例,展示如何使用这个概念循环处理用户名(这段代码可以在本书的 GitHub 仓库中找到,文件名为 ch05_variable_args.sh):

 print_arguments() {
  for arg in "$@"
  do
    echo "Argument: $arg"
  done
}
print_arguments "tsmith" "sjones" "mknight"

以下是一个解释:

  • print_arguments 函数定义为处理可变数量的参数。

  • 在函数内部,使用 for 循环迭代通过 $@ 传递给函数的所有参数,$@ 表示参数数组。

  • 使用 echo 命令将每个参数打印在单独的一行。

  • 我们调用 print_arguments 函数并传入三个参数:applebananacherry

输出将如下所示:

 Argument: tsmith
Argument: sjones
Argument: mknight

虽然 $@ 变量表示传递给脚本或函数的参数数组,但了解 $# 变量也很有用,$# 代表参数的个数。如果脚本或函数要求一定数量的参数,您应该始终确保用户输入了正确的参数数量。以下代码展示了这一点,您也可以在本书的 GitHub 仓库中找到它,文件名为 ch05_count_args.sh

 if [ "$#" -ne 2 ]; then
  echo "Usage: $0 <arg1> <arg2>"
  exit 1
fi

这个 if 语句检查参数的数量是否不等于 2。如果条件为真,它将打印一个有用的使用说明并退出。$0 变量表示脚本的名称。

参数的默认值

您可以为函数参数分配默认值,以防在调用函数时未提供这些参数。以下是一个示例(这段代码可以在本书的 GitHub 仓库中找到,文件名为 ch05_default_args.sh):

 greet() {
  local name=${1:-"Guest"}
  echo "Hello, $name!" }
greet
greet "John"

以下是一个解释:

  • greet 函数定义了一个参数。

  • 在函数内部,使用 ${1:-"Guest"} 将第一个参数赋值给局部变量 name。如果没有提供第一个参数,它将默认值为 Guest。这里有进一步的解释:

    • 局部变量将在本章稍后解释。基本上,声明为局部的变量只在函数执行时有效。当局部变量将控制权交还给调用它的主脚本或函数时,局部变量将不再可用。

    • 1 表示第一个参数(1)。第二个参数(1**)。第二个参数(**2)将被称为 2

    • :- 是默认值操作符。

    • Guest 是默认值。

  • 使用 echo 命令打印带有 name 变量的问候消息。

  • 我们调用了greet函数两次:一次没有参数,另一次传入了参数John

输出结果如下:

 Hello, Guest! Hello, John!

通过为函数变量设置默认值,你可以编写更少的代码来处理没有传入参数的情况。

这部分内容全面回顾了如何向函数传递参数。在下一节中,你将了解为什么理解 Bash 代码中变量的作用域和生命周期如此重要。

函数中的变量作用域和生命周期

在编写 Bash 脚本时,理解变量的作用域和生命周期是非常重要的,尤其是在处理函数时。合理管理变量有助于避免错误,使代码更具可维护性,并防止意外的副作用。

变量作用域指的是变量在脚本中的可见性和可访问性。它决定了变量在哪些地方可以被访问和修改。理解变量作用域对于编写简洁、模块化和可重用的代码至关重要。

生命周期指的是变量在脚本执行过程中存在并保持其值的时间长短。不同生命周期的变量可能对资源使用和数据持久性产生不同的影响。

正确管理变量的作用域和生命周期在处理函数时尤其重要。函数允许你封装可重用的代码块,但它们也引入了自己的作用域。理解变量在函数内外的行为对于编写稳健且可维护的 Bash 脚本至关重要。

在本教程中,我们将通过示例探讨 Bash 中函数内变量的作用域和生命周期。

全局变量

默认情况下,在 Bash 脚本中声明的变量具有全局作用域,这意味着它们可以在脚本的任何地方被访问和修改,包括函数内部。

这里是一个示例(该代码可以在本书的 GitHub 仓库中找到,文件名为ch05_global_var.sh):

 #!/bin/bash
name="John"
greet() {
  echo "Hello, $name!" }
greet
echo "Name: $name"

以下是输出结果:

 Hello, John! Name: John

以下是解释内容:

  • 第 3 行:我们声明了一个全局变量name,并为其赋值John

  • 第 5-7 行:我们定义了一个名为greet的函数,使用name变量打印问候信息。

  • 第 9 行:我们调用了greet函数,它访问了全局的name变量并打印了Hello, John!

  • 第 10 行:我们打印了name变量的值,它仍然可以在函数外部访问。

在这个示例中,name变量是全局的,可以在greet函数内和主脚本中访问。

局部变量

为了将变量的作用域限制在特定函数内,可以使用local关键字将其声明为局部变量。局部变量仅在声明它的函数内可访问。如果未使用local关键字,那么该变量就是全局的。以下是一个示例(该代码可以在书籍的 GitHub 仓库中找到,文件名为ch05_local_var.sh):

 #!/bin/bash
greet() {
  local name="Alice"
  echo "Hello, $name!" }
greet
echo "Name: $name"

以下是输出:

 Hello, Alice! Name:

以下是解释:

  • 第 3-6 行:我们定义了一个名为greet的函数,该函数声明了一个局部变量name,使用local关键字,并赋值为Alicename变量仅在greet函数内可访问。

  • 第 5 行:我们使用局部name变量打印了问候信息。

  • 第 8 行:我们调用了greet函数,它打印了Hello, Alice!

  • 第 9 行:我们尝试在函数外打印name变量的值,但它不可访问,因此输出为空。

在这个示例中,name变量是greet函数的局部变量,无法在函数外访问。试图在函数外使用$name会导致空值。

变量生命周期

变量的生命周期取决于其作用域。全局变量的生命周期贯穿整个脚本执行,而局部变量的生命周期仅限于声明它们的函数。

下面是一个展示变量生命周期的示例(该代码可以在书籍的 GitHub 仓库中找到,文件名为ch05_var_lifetime.sh):

 #!/bin/bash
global_var="I'm global"
my_function() {
  local local_var="I'm local"
  echo "Inside function:"
  echo "Global variable: $global_var"
  echo "Local variable: $local_var"
}
my_function
echo "Outside function:"
echo "Global variable: $global_var"
echo "Local variable: $local_var"

以下是输出:

 Inside function:
Global variable: I'm global
Local variable: I'm local
Outside function:
Global variable: I'm global
Local variable:

以下是解释:

  • 第 3 行:我们声明了一个全局变量global_var,并赋值为I'm global

  • 第 5-10 行:我们定义了一个名为my_function的函数,该函数声明了一个局部变量local_var,并赋值为I'm local。在函数内,我们打印了全局变量和局部变量的值。

  • 第 12 行:我们调用了my_function函数。

  • 第 14-16 行:在函数外,我们打印全局变量和局部变量的值。

在这个示例中,全局变量global_var在函数内外都可以访问,展示了它的生命周期贯穿整个脚本。而局部变量local_var仅在my_function函数内可访问,函数外没有值。

修改全局变量

如果你需要在函数内修改全局变量,可以通过直接引用该变量而无需任何特殊声明来实现。由于 Bash 没有global关键字,任何没有使用local关键字的变量实际上都是全局变量。通常建议尽量避免在函数内修改全局变量,以避免意外的副作用并保持代码的清晰性。

下面是一个在函数内修改全局变量的示例(该代码可以在书籍的 GitHub 仓库中找到,文件名为ch05_modify_global_var.sh):

 #!/bin/bash
count=0
increment() {
  count=$((count + 1))
}
echo "Before: count = $count"
increment
echo "After: count = $count"

以下是输出:

 Before: count = 0
After: count = 1

以下是解释:

  • 第 3 行:我们声明一个全局变量,count,并将其初始化为0

  • 第 5-7 行:我们定义了一个名为increment的函数,该函数通过将全局count变量的值增加1来修改它。

  • 第 9 行:我们在调用increment函数之前打印count的值。

  • 第 10 行:我们调用increment函数,它修改了全局的count变量。

  • 第 11 行:我们在调用increment函数后打印count的值。

在这个示例中,increment函数直接修改了全局的count变量,将它的值增加了1。这种修改在函数外部得以反映,从输出结果中可以看到这一点。

理解变量的作用域和生命周期对于编写清晰、可维护和无错误的 Bash 脚本至关重要。全局变量的作用域覆盖整个脚本,而局部变量仅限于声明它们的函数。变量的生命周期取决于它的作用域,全球变量在整个脚本执行过程中都存在,而局部变量仅在其所属的函数内存在。

通过正确管理变量的作用域和生命周期,你可以创建模块化和可重用的代码,避免命名冲突,并更好地控制脚本的行为。通常建议在函数中使用局部变量来封装数据,并防止意外副作用。

修改全局变量时要小心,因为这可能导致意外行为,并使得代码难以推理。尽可能实现关注点的清晰分离,并减少对全局变量的依赖。

在充分理解 Bash 变量作用域和生命周期后,你将能够编写更健壮、更易维护的脚本,使你的 Bash 编程体验更加愉快和高效。

在深入了解函数之后,在接下来的章节中,我们将基于这些知识,探索一些我确信你会在 Bash 脚本中找到有用的高级函数技巧。

高级函数技巧

在本节中,我们将探索一些用于处理 Bash 函数的高级技巧,包括返回值和递归函数。我们将提供代码示例和详细解释,帮助你掌握这些概念。

函数返回值

在 Bash 中,函数的返回值与大多数编程语言中的函数不同。它们返回的是退出状态,也称为返回码,这是一个整数,其中0通常表示成功,任何非零值表示错误或某种类型的失败。

返回退出状态

Bash 函数使用return命令返回退出状态。默认情况下,Bash 函数将返回函数内最后执行命令的退出状态。以下是一个基本示例(此代码在本书的 GitHub 仓库中作为ch05_exit_status.sh提供):

 function check_file {
    ls "$1"
    return $? }
check_file "example.txt"
echo "The function returned with exit code $?"

在这个例子中,check_file函数尝试列出作为函数参数提供的文件。$?特殊变量捕获最后执行命令的退出状态,在本例中是ls。函数调用后,$?将包含函数的返回状态。

你可以使用return命令后跟一个整数来显式设置函数的返回值。以下是一个例子(此代码在本书的 GitHub 仓库中作为ch05_explicit_exit_status.sh提供):

 function is_even {
    local num=$1
    if (( num % 2 == 0 )); then
        return 0  # Success, number is even
    else
        return 1  # Failure, number is odd
    fi
}
is_even 4
result=$? if [ $result -eq 0 ]; then
    echo "Number is even." else
    echo "Number is odd." fi

在上面的脚本中,is_even检查一个数字是否是偶数。如果数字是偶数,它返回0;否则返回1。然后检查函数调用的结果,打印该数字是偶数还是奇数。

使用输出代替返回码

如果你需要捕获函数的输出而不仅仅是退出状态,你可以使用命令替换。以下是通过同时使用变量和echo命令设置返回值的示例(此代码在本书的 GitHub 仓库中作为ch05_command_substitution.sh提供):

 square() {
  local result=$(($1 * $1))
  echo "$result"
}
squared=$(square 5)
echo "The square of 5 is $squared"

以下是输出:

 25
The square of 5 is 25

以下提供了一个解释:

  • 在这个例子中,我们定义了一个名为square的函数,它接受一个参数并计算它的平方。

  • 在函数内部,我们执行计算$1 * $1并将结果赋值给一个名为result的局部变量。

  • 数学表达式$1 * $1通过将因子括起来,用 Bash shell 算术扩展$(($1 * $1))进行封装。

  • 然后我们使用echo输出result的值。

  • 为了捕获函数的返回值,我们在调用函数时使用命令替换$()

  • 我们将square 5的输出赋值给一个名为squared的变量。

  • 最后,我们打印一条包含squared值的消息,值为25

根据你所学的内容,记住以下几点是非常重要的:

  • 退出状态范围:退出状态应该是 0 到 255 之间的整数,任何超出此范围的值可能会绕回(例如,256 变为 0)。

  • 使用输出:函数可以将数据输出到stdout,并通过命令替换捕获该输出。

  • 提前返回:你可以在函数中使用多个返回语句,在不同条件下提前退出函数。

在深入了解如何在 Bash 代码中使用函数后,让我们简要了解如何在代码中递归地使用它们。

递归函数

Bash 支持递归函数,即调用自身的函数。递归函数对于解决可以分解成更小子问题的问题非常有用。这里有一个示例,使用递归计算一个数的阶乘(此代码已在书籍的 GitHub 仓库中提供,文件名为ch05_recursive_function.sh):

 factorial() {
  if [ "$1" -eq 0 ]; then
    echo 1
  else
    local prev=$(factorial $(($1 - 1)))
    echo $(($1 * prev))
  fi
}
result=$(factorial 5)
echo "The factorial of 5 is $result"

以下是输出:

 The factorial of 5 is 120

以下是解释:

  • 在这个示例中,我们定义了一个名为factorial的函数,它接受一个参数,即我们要计算阶乘的数字。该函数使用if语句检查参数是否等于0。如果是,函数返回1,这是递归的基本情况。

  • 如果参数不是0,该函数会以参数减去1的值递归调用自身。这个递归调用将持续进行,直到达到基本情况。每次递归调用的结果会存储在一个名为prev的局部变量中。最后,函数将当前参数与上一次递归调用的结果相乘,并使用echo返回乘积。

  • 要使用阶乘函数,我们用5作为参数调用它,并通过命令替换捕获结果。我们将结果赋值给一个名为result的变量,并打印一条消息,显示5的阶乘结果,即120

递归函数的一个典型应用场景是在 Web 应用中进行文件和目录枚举。你会希望创建一个已发现目录的数组,并在每个目录内重新开始,发现文件。

递归函数可能非常强大,但它们也可能难以理解和调试。确保递归函数有一个明确的基本情况非常重要,以防止出现无限递归,并且需要仔细考虑终止条件。

在接下来的部分中,我们将继续在本章中学到的内容,学习如何导入函数,以减少你编写的代码量并重用代码。

导入函数

我之前提到过,Bash 函数的一个优点是代码重用。你可以通过编写一个函数解决一个问题,并多次调用该函数。在程序员的术语中,这被称为不要重复自己,或DRY(Don't Repeat Yourself)。现在,我们将进一步探讨这一点。

让我们设想一下,你之前通过实现一个函数解决了一个问题,而这个函数可以根据需要多次调用。那么,当你需要在一个新的 Bash 脚本中使用这个函数时,会发生什么呢?你会去翻找你的脚本,找到那个函数然后复制粘贴到新脚本中吗?其实完全不需要这样做。

养成将函数放入一个脚本中的习惯,比如库或模块。当你需要在新脚本中使用一个你之前定义过的函数时,只需在调用该函数之前source它即可。

以下示例代码可以在本章文件夹中的 GitHub 仓库中找到,文件名为ch05_importing_funcs_1.sh

 function greet() {
  echo "Hello, $1!" }

接下来,从另一个脚本中调用函数之前,先加载脚本(ch05_importing_funcs_2.sh):

 source script1.sh
greet "John"

你需要注意,加载一个文件可能会稍微增加启动脚本的时间,因为它必须将被加载的脚本载入内存。这个过程只会进行一次。如果你从一个函数库文件中使用多个函数,那么只需加载一次该文件,因为整个脚本会在加载时一次性载入内存。

学会了如何使用函数,从基础到高级应用后,我想简要讨论函数与别名的区别和使用场景,以帮助你在下一部分选择使用函数还是别名。

函数与别名

函数是编程中的基本构建块,它允许开发人员将一组指令封装成一个可重用的代码块。通过定义函数,程序员可以简化代码,提高可读性,并促进代码的可重用性。函数在被调用时会执行特定的任务,从而使得管理和维护代码变得更加容易。它们是编程语言(如 Python、JavaScript 和 Java)中的一个基本概念,使开发人员能够将复杂问题分解成更小、更易管理的组件。

别名在编程中有着不同的用途。别名是给实体(如变量、函数或命令)指定的符号名称。别名提供了一种为程序中现有元素创建快捷方式或替代名称的方法。它们可以帮助简化命令的语法,或者使代码更加简洁、易于理解。在基于 Unix 的系统中,别名通常用于定义自定义命令或缩短冗长的命令,以便于使用。

虽然函数和别名在编程中都扮演着重要角色,但它们服务于不同的目的,并具有不同的应用。函数主要用于将一组指令封装成一个可重用的代码块,促进模块化和代码组织。而别名则用于为实体创建符号名称,为了方便起见,提供快捷方式或替代名称。了解函数和别名之间的区别,可以帮助你利用这些编程概念来提高代码质量和效率。

现在我们已经深入探讨了函数的使用,我想向你介绍如何在脚本外部使用函数,以简化你的渗透测试工作流程。别名非常适合简化工作流程,因为它们允许你创建一个命名命令,可以输入该命令来替代更复杂的命令。

例如,我在我的~/.bashrc文件中有一个别名,它简化了一个非常长且复杂的命令,用来运行一个提供有关 Web 应用程序信息的 Docker 容器。我在每次进行 Web 应用渗透测试时运行这个命令,以便获取与应用程序使用的框架相关的信息:

 zapit='docker run -it --rm softwaresecurityproject/zap-stable zap.sh -cmd -addonupdate -addoninstall wappalyzer -addoninstall pscanrulesBeta -zapit'

这需要记住很多内容,不是吗?!幸运的是,我们有别名来解决这个问题。

尽管别名非常方便,但它们缺少我们所需的一个关键特性;它们不接受像$1 $2 $3这样的参数。在前面的别名中,当我们在终端中输入别名时,别名名称后附加的任何内容都会被包含在命令中,当 Bash 将别名扩展为完整的命令并在 Shell 中执行时。

本质上,Bash 将zapit www.example.com命令扩展为之前显示的Docker** **run命令,并附加了www.example.com。如果我们想运行一个需要多个参数并按特定顺序排列的命令,那么我们不能简单地在别名名称后追加参数?这时,函数就显得非常有用。

让我们以使用msfvenom生成 shellcode 为例。msfvenom是一个与Metasploit 框架一起包含的命令,用于生成各种格式的 shellcode。这个工具在渗透测试和漏洞开发中非常常用:

 gen_shellcode() {
  if [[ $# -eq 0 ]]; then
    echo "Usage: gen_shellcode [payload] [LPORT] [output format]"
    return 1
  fi
  msfvenom -p $1 LHOST=$(ip -o -4 a show tun0 | awk '{print $4}' | cut -d/ -f1) LPORT=$2 -f $3;
 }

这段代码在本书的 GitHub 仓库中提供,名为ch05_gen_shellcode.sh。我们可以如下解释:

  • 我们声明了一个名为gen_shellcode的函数。

  • 如果参数的数量等于0,则打印用法并退出。

  • msfvenom命令中,第一个参数$1被插入为有效载荷,位于-p之后。

  • LHOST=$(ip -o -4 a show tun0 | awk '{print $4}')代码获取tun0网络接口的 IP 地址,并将其插入到$()的位置。

  • 第二个参数$2被分配给LPORT变量。

  • 第三个参数$3用于输出格式的-f参数。

最后,将这段代码添加到你的~/.bashrc文件的末尾,你就可以在需要使用msfvenom生成 shellcode 时随时调用这个函数。如果你忘记了需要哪些选项,只需输入gen_shellcode而不加任何参数,然后按回车键,它将为你打印用法示例。

总的来说,别名会展开以表示引号中的命令,但你只能在别名名称后追加额外的参数。使用函数时,没有这种限制。除了在脚本中使用函数的巨大价值外,任何有效的 Bash 函数代码都可以放在你的.bashrc文件中,以便在命令行中调用,参数将在执行时插入到函数代码中。想象一下为你的渗透测试工作流程创建自动化的可能性!我们将在后面的章节中深入探讨这个话题。

总结

在本章中,我们深入探讨了 Bash 函数的世界,以及它们如何彻底改变你的脚本编写方式。通过掌握函数,你将编写出更简洁、更有组织、更加高效的脚本,从而节省时间并避免头痛。

我们从基础开始,了解了什么是函数以及它们为何如此有用。接着,我们深入探讨了如何向函数传递参数,使其更加灵活和可重用。我们还研究了函数内部变量的作用域和生命周期,让你完全了解函数内部发生的事情。

当我们进入高级技巧时,事情变得非常激动人心。你学会了如何使用递归优雅地解决复杂问题,如何使用回调函数让你的函数变得更强大。最后,我们将函数与别名进行了对比,并展示了函数在渗透测试工作流中的明显优势。

现在,你的脚本工具箱里有了一些强大的工具。你现在可以编写模块化、结构化的脚本,这些脚本易于阅读、调试和维护。最重要的是,你可以利用函数来简化渗透测试流程,节省宝贵的时间和精力。所以,去吧,像专家一样编写脚本吧!

在下一章,我们将探索如何使用 Bash 命令进行网络操作。