Skip to content

Latest commit

 

History

History
576 lines (440 loc) · 37.2 KB

File metadata and controls

576 lines (440 loc) · 37.2 KB

四、使用 Python 执行凭证攻击

有多种形式的凭证攻击,但通常情况下,它们被视为渗透测试中的最后一步,而其他一切都失败了。这是因为大多数新的评估员以错误的方式进行评估。在讨论全新的评估员使用什么进行凭据攻击时,最常见的两种攻击是在线字典攻击和暴力攻击。他们通过下载包含密码和广泛用户名列表的巨大单词列表并在接口上运行来执行凭据攻击。当攻击失败时,评估员跟进并执行暴力攻击。

此攻击使用相同的用户名列表或超级用户(root)或本地管理员帐户。大多数情况下,这也会失败,因此最终字典攻击会受到不好的批评,并移动到约定的末尾。这是非常错误的,因为在大多数约会中,尤其是在面对互联网的姿势中,如果操作正确,凭证攻击将让您访问。第 1 章理解渗透测试方法第 3 章使用 Nmap、Scapy 和 Python 识别目标介绍了一些基本的字典攻击概念,本章将以它们为基础,帮助您理解如何以及何时使用它们。在我们开始了解如何执行这些攻击之前,您需要对攻击类型有一个明确的了解。

凭证攻击的类型

在讨论凭证攻击时,会立即被密码攻击所吸引。请记住,对资源的身份验证和授权通常需要两个组件:密码和用户名。如果你不知道密码所属的用户名,拥有世界上使用最广泛的密码对你没有好处。因此,凭证攻击是我们使用用户名和密码评估资源的方式。稍后将介绍有针对性的用户名来源,但目前我们必须定义在线和离线密码攻击的主要类型。

定义在线凭证攻击

在线凭证攻击是指当您针对接口或资源进行强制身份验证时所进行的攻击。这意味着您可能不知道用户名和/或密码,并且正在尝试确定授予您访问权限的正确信息。这些攻击是在您无法访问可为您提供哈希、明文密码或其他受保护形式数据的资源时执行的。相反,您正试图根据您所做的研究对资源进行有根据的猜测。在线攻击的类型包括字典攻击、暴力攻击和密码喷射攻击。请记住,资源可以是联邦或集中式系统的一部分,如Active DirectoryAD)或主机本身的本地帐户。

提示

为你尖叫混合动力车呢?大多数评估者认为这是一种字典攻击形式,因为它只是一个排列的单词列表。今天,你很少能找到一本字典不包含混合词。在 20 世纪 90 年代,这种情况更为罕见,但随着教育水平的提高和更强大的密码要求系统的完善,这种情况已经发生了改变。

定义离线凭证攻击

脱机凭证攻击是指您已经破解了资源并提取了散列等数据,现在正试图猜测这些数据。这可以通过多种方式完成,具体取决于哈希类型和可用的资源,一些示例包括脱机字典、基于规则的攻击、暴力或彩虹表攻击。我们将这种脱机凭据攻击称为脱机密码攻击而不是脱机密码攻击的原因之一,是因为您试图猜测密码的明文版本不是源于系统。

这些密码散列可能已经用随机信息或已知组件(如用户名)进行了 salt,以创建 salt。因此,您可能仍然需要知道用户名才能破解哈希,因为 salt 是增加随机性的一个组成部分。现在,我看到了一些使用用户名作为散列算法盐的实现,这是一个非常糟糕的想法。你会听到的论点是,这是一个好主意,它来自于这样一个事实,即 salt 与用户名一样使用密码存储,那么这又有什么关系呢?在破坏系统之前,已知的用户名在 root、administrator 和 admin 等系统中广泛使用,并且已知的加密方法会打开一个主要漏洞。

这意味着 salt 基于用户名,意味着在访问环境之前和参与开始之前就知道了。因此,这意味着,您已经有效地击败了使破解密码更难包括使用 rainbow 表的机制。如果您有一个可以处理数据的工具,那么在订婚之前让 salt 被知晓意味着彩虹表对于 salt 密码同样有用。

提示

糟糕的 satting 方法和自定义加密方法可能会使组织面临妥协。

离线攻击的前提是使用相同的保护方法,获取一个单词并创建与受保护的密码相同格式的哈希。如果受保护的值与新创建的值相同,则您有一个等效的字并授予访问权限。大多数密码保护方法使用散列来隐藏值,这是一个单向函数,或者换句话说,它不能是单向函数,因此无法反转该方法以生成原始值。

因此,当系统通过其身份验证方法接受密码时,它会使用相同的方法对密码进行散列,并将存储的散列值与新计算的散列值进行比较。如果它们彼此相等,则您可以合理地保证密码相同,并且将授予访问权限。合理的级别保证取决于哈希算法的强度。一些散列算法被认为是弱的或坏的,例如消息摘要 5MD5)和安全散列算法 1SHA-1)。原因是它们容易受到碰撞的影响。

冲突意味着它所保护的数据的数学可能性没有足够的熵来保证不同的散列值不等于相同的值。事实上,两个完全不同的单词被同一个断开的算法散列,可能会产生相同的散列值。因此,这直接影响系统身份验证方法。

当有人访问系统时,密码输入将以与存储在系统上的密码相同的方法进行散列。如果两个值匹配,这意味着密码理论上是相同的,除非散列算法很弱。因此,在评估系统时,您只需找到一个与原始值创建相同哈希的值。如果发生这种情况,您将被授予访问系统的权限,这就是已知冲突的哈希的弱点所在。您不需要知道创建散列的实际值,只需要知道将创建相同散列的等效值。

提示

在撰写本文时,MD5 用于验证文件系统和数据的完整性,以便进行取证。即使 MD5 被认为是一个坏的散列,它仍然被认为是足够好的取证和文件系统完整性。这样做的原因是,要用大量的数据集(如文件系统)来愚弄算法,将需要不可行的工作量。在数据被调整或提取后操作文件系统以创建相同的完整性标记是不现实的。

现在您已经了解了离线和在线凭证攻击的区别,我们需要开始生成用于它们的数据。首先生成用户名,然后将其作为组织的一部分进行验证。这似乎是一个很小的步骤,但它非常重要,因为它可以减少您的目标列表,减少您产生的噪音,并提高您损害组织的机会。

识别目标

我们将使用 Metasploitable 作为示例,因为它将允许您在安全合法的环境中测试这些概念。首先,让我们使用服务检测对系统进行简单的nmap扫描。下面的命令突出显示了特定的参数和选项,它执行 SYN 扫描以查找系统上的已知端口。

nmap -sS -vvv -Pn -sV<targetIP>

从结果中可以看出,主机被标识为 Metasploitable,并且在端口 25 处打开了许多端口,包括简单邮件传输协议SMTP)。

Identifying the target

创建目标用户名

当以组织为目标时,尤其是在周边地区,最简单的方法是妥协账户。这意味着您至少可以获得此人的基本访问权限,并且可以找到提升权限的方法。要做到这一点,您需要为组织识别真实的用户名。实现这一点的多种方法包括通过等网站对为该组织工作的人员进行调查 http://www.data.com/https://www.facebook.com/https://www.linkedin.com/hp/http://vault.com/ 。您可以使用类似于Harvester.pyRecon-ng的工具来自动化其中的一些内容,这些工具可以提供互联网曝光和存储库。

这项初步研究很好,但与恶意参与者不同,您通常需要的时间是有限的。因此,您可以通过生成用户名来补充找到的数据,然后通过启用 VRFY 或 Finger 的 SMTP 等服务端口对其进行验证。如果您发现这些端口打开,特别是在目标组织的 Internet 上,我要做的第一件事就是验证我的用户名列表。这意味着我可以减少下一步的攻击列表,我们将在第 5 章使用 Python开发服务中介绍。

在美国人口普查局的帮助下生成和验证用户名

多年来,美国政府和其他国家都在调查各国民众的详细情况。这些信息可供守法公民以及恶意行为者使用。这些细节可以用于任何事情,从社会工程攻击,销售研究,甚至电话营销。有些细节比其他更难找到,但我们最喜欢的是姓氏列表。这份名单产生于 2000 年,为我们提供了美国人口中排名前 1000 位的姓氏。

如果您曾经查看过大多数组织用户名的组成部分,那么它是他们名字的第一个字母和整个姓氏。当这两个组件组合在一起时,它将创建一个用户名。使用美国人口普查前 1000 名名单,我们可以通过下载名单、提取姓氏并在字母表中的每个字母前加前缀,为每个姓氏创建 26 个用户名,从而欺骗创建方法。这一过程将产生 26000 个用户名的列表,其中不包括公开来源信息的详细信息。

当您结合搜索社交媒体创建的用户名列表,并使用工具识别电子邮件地址时,您可能会有一个大量的列表。所以你需要把它剪下来。在本例中,我们将向您展示如何使用 Python 从 Excel 电子表格中提取详细信息,然后根据运行 VRFY 的 SMTP 服务验证由其他列表创建和组合的用户名。

提示

西化的政府通常会列出类似的清单,因此请确保您查看您试图评估和使用与组织所在地相关的信息的位置。除此之外,美国领土、阿拉斯加和夏威夷等州的姓氏与美国大陆其他州的姓氏大不相同。请列出您的姓氏列表,以弥补这些差异。

生成用户名

流程的第一步是下载 excel 电子表格,可在中找到 http://www.census.gov/topics/population/genealogy/data/2000_surnames.html 。您可以使用wget直接从控制台下载具体文件,如下图所示。记住,你应该只下载文件;除非获得许可,否则不要评估组织或网站。以下命令相当于访问站点并单击链接下载文件:

wget http://www2.census.gov/topics/genealogy/2000surnames/Top1000.xls

现在打开 Excel 文件,看看它是如何格式化的,这样我们就知道如何开发脚本来提取细节。

Generating the usernames

如您所见,共有 11 列定义了电子表格的功能。我们关心的两个问题是姓名和级别。名称是我们将创建用户名列表的姓氏,排名是美国的发生顺序。在我们构建解析人口普查文件的函数之前,我们需要开发一种将数据输入脚本的方法。

argparser库允许您快速有效地开发命令行选项和参数。xlrd库将用于分析 Excel 电子表格,字符串库将用于开发字母字符列表。os库将确认运行脚本所使用的操作系统操作系统),因此可以在内部处理文件名格式设置。最后,集合库将提供组织从 Excel 电子表格中提取的内存中的数据的方法。唯一不是 Python 实例本机的库是xlrd库,它可以与pip一起安装。

#!/usr/bin/env python
import sys, string, arparse, os
from collections import namedtuple
try:
    import xlrd
except:
    sys.exit("[!] Please install the xlrd library: pip install xlrd")

现在您已经有了库,现在可以构建函数来完成这项工作。该脚本还包括增加或减少详细程度的功能。这是一个相对容易包含的特性,通过将 verbose 变量设置为整数值来实现;值越高,越详细。默认值为 1,最多支持值 3。任何超过此值的内容都将被视为 3。此函数也将接受正在传递的文件名,因为您永远不知道它将来可能会更改。

我们将使用一种称为命名元组的元组形式来接受电子表格的每一行。命名元组允许您根据坐标或字段名引用详细信息,具体取决于它的定义方式。正如您所猜测的,这非常适合电子表格或数据库数据。为了使这对我们来说更容易,我们将以与电子表格相同的方式来定义它。

defcensus_parser(filename, verbose):
    # Create the named tuple
    CensusTuple = namedtuple('Census', 'name, rank, count, prop100k, cum_prop100k, pctwhite, pctblack, pctapi, pctaian, pct2prace, pcthispanic')

现在,开发变量来保存工作簿、电子表格的名称以及电子表格的总行和初始行。

    worksheet_name = "top1000"
    #Define work book and work sheet variables
    workbook = xlrd.open_workbook(filename)
    spreadsheet = workbook.sheet_by_name(worksheet_name)
    total_rows = spreadsheet.nrows - 1
    current_row = -1

然后,开发初始变量以保存结果值和实际字母表。

    # Define holder for details
    username_dict = {}
    surname_dict = {}
    alphabet = list(string.ascii_lowercase)

接下来,将迭代电子表格的每一行。surname_dict保存来自电子表格单元格的原始数据。username_dict将保存用户名和转换为字符串的排名。每次在秩值中未检测到一个点时,表示该值不是一个float,因此为空。这意味着行本身不包含实际数据,应该跳过它。

    while current_row<total_rows:
        row = spreadsheet.row(current_row)
        current_row += 1
        entry = CensusTuple(*tuple(row)) #Passing the values of the row as a tuple into the namedtuple
        surname_dict[entry.rank] = entry
        cellname = entry.name
        cellrank = entry.rank
        for letter in alphabet:
            if "." not in str(cellrank.value):
                if verbose > 1:
                    print("[-] Eliminating table headers")
                break
            username = letter + str(cellname.value.lower())
            rank = str(cellrank.value)
            username_dict[username] = rank

请记住,字典存储键引用的值,但没有顺序。所以我们可以做的是取字典中存储的值并按键排序,键是值或姓氏的等级。为此,我们将获取一个列表,并让它接受函数返回的排序细节。由于这是一个相对简单的函数,我们可以使用lambda创建一个无名函数,它在处理代码时使用可选的排序参数键来调用它。实际上,sorted 根据字典键为字典中的每个值创建一个有序列表。最后,如果将来需要,此函数将返回username_list和两个字典。

    username_list = sorted(username_dict, key=lambda key: username_dict[key])
    return(surname_dict, username_dict, username_list)

好消息是这是整个脚本中最复杂的函数。下一个函数是一个众所周知的设计,它接受列表并删除重复项。该函数使用列表理解,它减少了用于创建有序列表的简单循环的大小。函数中的此表达式可以写成以下形式:

for item in liste_sort:
    if not noted.count(item):
        noted.append(item)

为了减小此简单执行的大小并提高可读性,我们将其改为列表理解,如以下摘录所示:

defunique_list(list_sort, verbose):
    noted = []
    if verbose > 0:
        print("[*] Removing duplicates while maintaining order")
    [noted.append(item) for item in list_sort if not noted.count(item)] # List comprehension
    return noted

此脚本的目标之一是将来自其他来源的研究合并到包含用户名的同一文件中。用户可以传递一个文件,该文件可以预先添加或附加到普查文件输出的详细信息中。运行此脚本时,用户可以将文件作为前置值或附加值提供。脚本确定它是哪一个,然后在每一行中读取,从每个条目中剥离新行字符。然后,脚本确定是否需要将其添加到普查用户名列表的末尾或前面,并为put_where设置变量值。最后,返回put_where的列表和值。

defusername_file_parser(prepend_file, append_file, verbose):
    if prepend_file:
        put_where = "begin"
        filename = prepend_file
    elif append_file:
        put_where = "end"
        filename = append_file
    else:
        sys.exit("[!] There was an error in processing the supplemental username list!")
    with open(filename) as file:
        lines = [line.rstrip('\n') for line in file]
    if verbose > 1:
        if "end" in put_where:
            print("[*] Appending %d entries to the username list") % (len(lines))
        else:
            print("[*] Prepending %d entries to the username list") % (len(lines))
    return(lines, put_where)

所需要的只是一个将两个用户列表组合在一起的函数。此函数将使用一个简单的拆分在数据之前添加,该拆分将新用户列表粘贴在普查列表之前,或者使用扩展函数将数据添加到后面。然后,该函数将调用先前创建的函数,从而将非唯一值减少为唯一值。如果知道某个函数的密码锁定限制,然后多次调用同一个用户帐户,锁定该帐户,那就不好了。返回的最后一项是新的组合用户名列表。

defcombine_usernames(supplemental_list, put_where, username_list, verbose):
    if "begin" in put_where:
        username_list[:0] = supplemental_list #Prepend with a slice
    if "end" in put_where:
    username_list.extend(supplemental_list)
    username_list = unique_list(username_list, verbose)
    return(username_list)

此脚本中的最后一个函数将详细信息写入文件。为了进一步改进此脚本的功能,我们可以创建两种不同类型的用户名文件:一种包括域(如电子邮件地址)和另一种标准用户名列表。域的补充用户名列表将被视为可选。

此函数根据需要删除文件的内容,并遍历列表。如果列表是一个域列表,则在将其写入文件时,只需将@和域名应用于每个用户名。

defwrite_username_file(username_list, filename, domain, verbose):
    open(filename, 'w').close() #Delete contents of file name
    if domain:
        domain_filename = filename + "_" + domain
        email_list = []
        open(domain_filename, 'w').close()
    if verbose > 1:
        print("[*] Writing to %s") % (filename)
    with open(filename, 'w') as file:
         file.write('\n'.join(username_list))
    if domain:
        if verbose > 1:
            print("[*] Writing domain supported list to %s") % (domain_filename)
        for line in username_list:
            email_address = line + "@" + domain
            email_list.append(email_address)
        with open(domain_filename, 'w') as file:
            file.write('\n'.join(email_list))
    return

现在已经定义了函数,我们可以开发脚本的主要部分,并适当地引入参数和选项。

argparse库取代了optparse库,后者提供了类似的功能。应该注意的是,这个库很好地解决了脚本语言中与选项和参数相关的许多弱点。

argparse库为您提供了设置短选项和长选项的能力,这些选项可以接受types定义的许多值。然后,这些变量将显示在您用dest定义的变量中。

这些参数中的每一个都可以具有使用 action 参数定义的特定功能,以包括值存储、计数和其他功能。此外,这些参数中的每一个都可以根据需要使用default参数设置default值。另一个有用的特性是help参数,它提供使用反馈并改进文档。我们不会使用我们在每次订婚或每天创建的每个脚本。有关如何为census文件添加参数,请参见以下示例。

parser.add_argument("-c", "--census", type=str, help="The census file that will be used to create usernames, this can be retrieved like so:\n wget http://www2.census.gov/topics/genealogy/2000surnames/Top1000.xls", action="store", dest="census_file")

理解了这些简单的功能后,我们就可以开发传递给脚本的参数的需求。首先,我们验证这是主函数的一部分,然后将argeparse实例化为解析器。simple usage 语句显示了执行脚本需要调用什么。%(prog)s在功能上等同于在argv中放置0,因为它代表脚本名称。

if __name__ == '__main__':
    # If script is executed at the CLI
    usage = '''usage: %(prog)s [-c census.xlsx] [-f output_filename] [-a append_filename] [-p prepend_filename] [-ddomain_name] -q -v -vv -vvv'''
    parser = argparse.ArgumentParser(usage=usage)

现在我们已经在解析器中定义了实例,我们需要将每个参数添加到解析器中。然后,我们定义变量args,它将保存每个存储参数或选项的公共引用值。

    parser.add_argument("-c", "--census", type=str, help="The census file that will be used to create usernames, this can be retrieved like so:\n wget http://www2.census.gov/topics/genealogy/2000surnames/Top1000.xls", action="store", dest="census_file")
    parser.add_argument("-f", "--filename", type=str, help="Filename for output the usernames", action="store", dest="filename")
    parser.add_argument("-a","--append", type=str, action="store", help="A username list to append to the list generated from the census", dest="append_file")
    parser.add_argument("-p","--prepend", type=str, action="store", help="A username list to prepend to the list generated from the census", dest="prepend_file")
    parser.add_argument("-d","--domain", type=str, action="store", help="The domain to append to usernames", dest="domain_name")
    parser.add_argument("-v", action="count", dest="verbose", default=1, help="Verbosity level, defaults to one, this outputs each command and result")
    parser.add_argument("-q", action="store_const", dest="verbose", const=0, help="Sets the results to be quiet")
    parser.add_argument('--version', action='version', version='%(prog)s 0.42b')
    args = parser.parse_args()

定义了参数后,您将需要验证这些参数是否由用户设置,以及它们是否易于通过脚本引用。

    # Set Constructors
    census_file = args.census_file   # Census
    filename = args.filename         # Filename for outputs
    verbose = args.verbose           # Verbosity level
    append_file = args.append_file   # Filename for the appending usernames to the output file
    prepend_file = args.prepend_file # Filename to prepend to the usernames to the output file
    domain_name = args.domain_name   # The name of the domain to be appended to the username list
    dir = os.getcwd()                # Get current working directory
    # Argument Validator
    if len(sys.argv)==1:
        parser.print_help()
        sys.exit(1)
  if append_file and prepend_file:
      sys.exit("[!] Please select either prepend or append for a file not both")

与参数验证器类似,您需要确保设置了输出文件。如果未设置,则可以根据需要使用默认值。您将希望不依赖操作系统,因此需要将其设置为在 Linux/UNIX 系统或 Windows 系统中运行。最简单的确定方法是通过\/的方向。记住,\是用来转义脚本中的字符的,所以一定要放两个来抵消效果。

    if not filename:
        if os.name != "nt":
             filename = dir + "/census_username_list"
        else:
             filename = dir + "\\census_username_list"
    else:
        if filename:
            if "\\" or "/" in filename:
                if verbose > 1:
                    print("[*] Using filename: %s") % (filename)
        else:
            if os.name != "nt":
                filename = dir + "/" + filename
            else:
                filename = dir + "\\" + filename
                if verbose > 1:
                    print("[*] Using filename: %s") % (filename)

其余需要定义的组件是调用函数时的工作变量。

    # Define working variables
    sur_dict = {}
    user_dict = {}
    user_list = []
    sup_username = []
    target = []
    combined_users = []

按照所有这些细节,您最终可以进入脚本的核心部分,即调用活动来创建用户名文件:

    # Process census file
    if not census_file:
        sys.exit("[!] You did not provide a census file!")
    else:
        sur_dict, user_dict, user_list = census_parser(census_file, verbose)
    # Process supplemental username file
    if append_file or prepend_file:
        sup_username, target = username_file_parser(prepend_file, append_file, verbose)
        combined_users = combine_usernames(sup_username, target, user_list, verbose)
    else:
        combined_users = user_list
    write_username_file(combined_users, filename, domain_name, verbose)

以下屏幕截图演示了脚本如何输出帮助文件:

Generating the usernames

在这里可以找到一个如何运行脚本和输出的示例,前面是一个带有用户名msfadminusername.lst

Generating the usernames

提示

此脚本可从下载 https://raw.githubusercontent.com/funkandwagnalls/pythonpentest/master/username_generator.py

我们有我们的用户名生成器,我们包括了名称msfadmin,因为我们已经对测试框 Metasploitable 做了一些初步研究。我们知道这是一个标准的默认帐户,我们将要验证它是否确实在系统中。当您最初扫描一个系统并确定打开的端口和服务,然后验证您准备攻击的内容时,这是研究的正常部分。该研究还应包括寻找违约账户和已知账户。

提示

在执行这些类型的攻击时,通常会排除诸如 root 之类的系统的内置帐户。在 Windows 系统上,仍应测试管理员帐户,因为该帐户可能会被重命名。首先,您还应该避免在双盲或红队练习期间测试 root 登录。这通常会引起安全管理人员的警惕。

对使用 SMTP VRFY 的用户进行测试

现在我们有了一个用户名列表,并且我们知道 SMTP 已打开,我们需要查看VRFY是否已启用。这非常简单,您只需将 telnet 连接到端口 25,然后执行命令VRFY,然后输入一个单词,然后按 enter 键。通过这种方式检查用户名的一个重要方面是,如果启用了VRFY,安全部署实践就会出问题,如果是面向 Internet 的,他们可能不会监控它。减少针对接口的在线凭据攻击中的凭据攻击猜测次数将减少被捕获的机会。执行此操作的简单命令如下图所示:

Testing for users using SMTP VRFY

我们没有得到史密斯的攻击,但也许其他人会在这次攻击中确认。在编写脚本之前,您需要了解大多数 SMTP 系统中可能产生的不同错误或控制消息。这些可能会有所不同,您应该充分设计脚本,以便针对该环境进行修改。

|

返回码

|

意思

| | --- | --- | | 252 | 用户名在系统上。 | | 550 | 用户名不在系统上。 | | 503 | 该服务需要身份验证才能使用。 | | 500 | 该服务不支持 VRFY。 |

现在您已经知道了基本的代码响应,可以编写一个脚本来利用这个弱点。

您可能想知道,当 Metasploit 和其他工具已经为此内置了模块时,我们为什么要编写一个脚本来利用这一点。在许多系统上,这一弱点有特殊的超时和/或节流要求可利用。当您试图绕过这些障碍时,包含 Metasploit 模块的大多数其他工具都会失败,因此 Python 确实是您最好的答案。

创建 SMTP VRFY 脚本

由于 Meta SasPIT 和其他的 To0T0 攻击工具没有考虑到会话尝试的超时和每次尝试之间的延迟,所以我们需要考虑通过合并这些任务来使脚本更有用。如前所述,工具非常好,它们通常适合 80%的情况,但作为一名专业人员,意味着适应工具可能不适合的情况。

到目前为止,所使用的库已经很常见,但我们从第 2 章Python 脚本基础知识——用于网络接口控制和超时控制的套接字库中添加了一个。

#/usr/bin/env python
import socket, time, argparse, os, sys

下一个函数将文件读入一个列表,用于测试用户名。

defread_file(filename):
    with open(filename) as file:
        lines = file.read().splitlines()
    return lines

接下来,修改username_generator.py脚本函数,将数据写入组合用户名文件。这将为有用的输出格式提供一个已确认的用户名列表。

defwrite_username_file(username_list, filename, verbose):
    open(filename, 'w').close() #Delete contents of file name
    if verbose > 1:
        print("[*] Writing to %s") % (filename)
    with open(filename, 'w') as file:
        file.write('\n'.join(username_list))
    return

最后一个函数也是最复杂的函数称为verify_smtp,它针对 SMTPVRFY漏洞验证用户名。首先,它加载从read_file函数返回的用户名,并确认参数数据。

defverify_smtp(verbose, filename, ip, timeout_value, sleep_value, port=25):
    if port is None:
        port=int(25)
    elif port is "":
        port=int(25)
    else:
        port=int(port)
    if verbose > 0:
        print "[*] Connecting to %s on port %s to execute the test" % (ip, port)
    valid_users=[]
    username_list = read_file(filename)

然后,脚本从列表中取出每个用户名,并使用条件测试尝试在指定的 IP 和端口创建与系统的连接。我们在横幅连接时捕获它,使用用户名构建命令,然后发送命令。返回的数据存储在 results 变量中,该变量针对以前记录的响应代码进行测试。如果收到 252 响应,用户名将附加到valid_users列表中。

    for user in username_list:
        try:
            sys.stdout.flush()
            s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.settimeout(timeout_value)
            connect=s.connect((ip,port))
            banner=s.recv(1024)
            if verbose > 0:
                print("[*] The system banner is: '%s'") % (str(banner))
            command='VRFY ' + user + '\n'
            if verbose > 0:
                print("[*] Executing: %s") % (command)
                print("[*] Testing entry %s of %s") % (str(username_list.index(user)),str( len(username_list)))
            s.send(command)
            result=s.recv(1024)
            if "252" in result:
                valid_users.append(user)
                if verbose > 1:
                    print("[+] Username %s is valid") % (user)
            if "550" in result:
                if verbose > 1:
                    print "[-] 550 Username does not exist"
            if "503" in result:
                print("[!] The server requires authentication")
                break
            if "500" in result:
                print("[!] The VRFY command is not supported")
                break

如果满足需要结束测试的条件,则设置特定的中断条件以使脚本相对优雅地结束。需要注意的是,每个用户名都有一个单独的正在建立的连接,以防止连接被打开太长时间,减少错误,并提高将来将该脚本制作成多线程脚本的可能性,如第 10 章所述为 Python 工具添加永久性。

该脚本的最后两个组件是异常错误处理,最后一个条件操作关闭连接,必要时延迟下一次执行并清除标准输出。

        except IOError as e:
            if verbose > 1:
                print("[!] The following error occured: '%s'") % (str(e))
            if 'Operation now in progress' in e:
                print("[!] The connection to SMTP failed")
                break
        finally:
            if valid_users and verbose > 0:
                print("[+] %d User(s) are Valid" % (len(valid_users)))
            elif verbose > 0 and not valid_users:
                print("[!] No valid users were found")
            s.close()
            if sleep_value is not 0:
                time.sleep(sleep_value)
            sys.stdout.flush()
    return valid_users

以前的许多脚本组件在这里被重用,它们只是为了新脚本而进行了调整。看一看,为自己确定不同的组件。然后了解如何将更改合并到未来的更改中。

if __name__ == '__main__':
    # If script is executed at the CLI
    usage = '''usage: %(prog)s [-u username_file] [-f output_filename] [-iip address] [-p port_number] [-t timeout] [-s sleep] -q -v -vv -vvv'''
    parser = argparse.ArgumentParser(usage=usage)
    parser.add_argument("-u", "--usernames", type=str, help="The usernames that are to be read", action="store", dest="username_file")
    parser.add_argument("-f", "--filename", type=str, help="Filename for output the confirmed usernames", action="store", dest="filename")
    parser.add_argument("-i", "--ip", type=str, help="The IP address of the target system", action="store", dest="ip")
    parser.add_argument("-p","--port", type=int, default=25, action="store", help="The port of the target system's SMTP service", dest="port")
    parser.add_argument("-t","--timeout", type=float, default=1, action="store", help="The timeout value for service responses in seconds", dest="timeout_value")
    parser.add_argument("-s","--sleep", type=float, default=0.0, action="store", help="The wait time between each request in seconds", dest="sleep_value")
    parser.add_argument("-v", action="count", dest="verbose", default=1, help="Verbosity level, defaults to one, this outputs each command and result")
    parser.add_argument("-q", action="store_const", dest="verbose", const=0, help="Sets the results to be quiet")
    parser.add_argument('--version', action='version', version='%(prog)s 0.42b')
args = parser.parse_args()
    # Set Constructors
    username_file = args.username_file   # Usernames to test
    filename = args.filename             # Filename for outputs
    verbose = args.verbose               # Verbosity level
    ip = args.ip                         # IP Address to test
    port = args.port                     # Port for the service to test
    timeout_value = args.timeout_value   # Timeout value for service connections
    sleep_value = args.sleep_value       # Sleep value between requests
    dir = os.getcwd()                    # Get current working directory
    username_list =[]  
    # Argument Validator
    if len(sys.argv)==1:
        parser.print_help()
        sys.exit(1)
    if not filename:
        if os.name != "nt":
            filename = dir + "/confirmed_username_list"
        else:
             filename = dir + "\\confirmed_username_list"
    else:
        if filename:
            if "\\" or "/" in filename:
                if verbose > 1:
                    print(" [*] Using filename: %s") % (filename)
        else:
            if os.name != "nt":
                filename = dir + "/" + filename
            else:
                filename = dir + "\\" + filename
                if verbose > 1:
                    print("[*] Using filename: %s") % (filename)

脚本的最后一个组件是调用特定函数来执行脚本。

username_list = verify_smtp(verbose, username_file, ip, timeout_value, sleep_value, port)
if len(username_list) > 0:
    write_username_file(username_list, filename, verbose)

该脚本具有默认的帮助功能,就像username_generator.py脚本一样,如下图所示:

Creating the SMTP VRFY script

此脚本的最终版本将产生如下输出:

Creating the SMTP VRFY script

执行以下命令后,脚本的默认睡眠值为0.0,默认超时值为1秒。该命令将向脚本传递一个用户名平面文件、目标的 IP 地址、SMTP 服务的端口和输出文件。如果通过 Internet 进行测试,则可能必须增加此值。

Creating the SMTP VRFY script

毫无疑问,我们在系统上验证的一个用户是msfadmin帐户。如果这是一个真正的系统,您已经减少了需要测试的帐户数量,有效地缩小了凭证攻击等式的一半。现在,你需要做的就是找到一个你想要测试的服务。

提示

此脚本可从下载 https://raw.githubusercontent.com/funkandwagnalls/pythonpentest/master/smtp_vrfy.py

总结

本章详细介绍了如何从外部源操作文件,以在较低级别连接到资源。最终的结果是能够识别潜在的用户帐户并验证它们。这些活动还强调了在argparse库中正确使用参数和选项,以及脚本的使用可以满足开发工具无法满足的需求。所有这些都是为了利用这些服务而构建的,我们将在下一章介绍这些服务。