黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第五章 WEB黑客(2)匹配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版。尽管文件的布局可能与实际的目标服务器不同,但它为我们查找大多数版本中存在的文件和目录提供了一个合理的起点。
通过mapper.py遍历目录和文件
为了要获取标准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']
TARGET = 'http://boodelyboo.com/wordpress'
THREADS = 10
answers = queue.Queue()
web_paths = queue.Queue()
def gather_paths():
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
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:
yield
finally:
os.chdir(this_dir)
if __name__ == "__main__":
with chdir("/home/kali/Downloads/wordpress"):
gather_paths()
input('Press return to continue.')
我们首先定义了一个远程目标网站,并创建一个对对mapping无实际作用的文件扩展名列表。实际操作时根据目标应用程序的不同,此列表可能有所不同,在本例中,我们将忽略图像和样式表文件,而目标是HTML或文本文件,因为这类文件更可能包含对服务器有害的信息。answers变量是Queue对象,用来存放本地找到的文件路径。web_paths变量是第二个Queue对象,用来存放将要在远程服务器上查找的文件。在gather_paths函数中,我们使用os.walk方法遍历本地web应用程序目录中的所有文件和目录。在遍历文件和目录时,我们构建目标文件的完整路径,并根据FILTERED中存储的列表进行匹配,以确保只查找所需的文件类型。对于在本地找到的每个有效文件,将其添加到web_paths变量的Queue队列中。
上下文管理器
代码中的chdir上下文管理器需要解释一下。上下文管理器提供了一种很酷的编程模式,特别是当我们健忘或有太多东西需要跟踪时。当我们打开某个文件并需要关闭它、锁定某个文件并且需要释放它,或者更改某个文件然后需要重置它时,会发现这种模式很有用。可能我们已经熟悉了内置的文件管理器,例如使用open打开一个文件,或者使用socket来处理套接字。
通常,我们通过使用__enter__和__exit__方法来创建上下文管理器。__enter__方法返回需要管理的资源(如文件或套接字),__exit__方法执行清理操作(如关闭文件)。
但是,在不需要太多控制的情况下,我们可以使用@contextlib.contextmanager创建一个简单的上下文管理器,这会将生成器函数转换为上下文管理器。
chdir函数允许我们在不同的目录中执行代码,并保证在退出时返回到原始目录。chdir生成器函数通过保存原始目录并更改为新目录来初始化上下文,yield控制返回到gather_path,然后恢复到原始目录。
try-finally块
需要注意的是,chdir函数的定义中包含了try-finally块。我们可能经常会遇到try-except语句,但try-finally不太常见。不管是否有异常发生,finally块始终执行。我们在这里需要它,因为无论目录更改是否成功,我们都希望上下文恢复到原始目录。如下的try块示例显示了每种情况下将会发生什么:
try:
something_that_might_cause_an_error()
except SomeError as e:
# show the error on the console
print(e)
# take some alternative action
dosomethingelse()
else:
# this executes only if the try succeeded
everything_is_fine()
finally:
# this executes no matter what
cleanup()
遍历本地安装包路径
回到原来的mapping示例代码,我们可以在__main__块中看到,在with语句中使用了chdir上下文管理器,它使用执行代码的目录的名称调用生成器。在本例中,我们传入解压后的WordPress的ZIP文件的位置(注意,此位置在不同的机器上会有所不同,实际上手操作时要确保正确)。进入chdir函数将保存当前目录名,并将工作目录更改为作为函数参数传入的路径。然后,chdir函数将控制权交还给执行主线程,这是运行gather_path函数的地方。一旦gather_paths函数完成,我们将退出上下文管理器,执行finally子句,并将工作目录恢复到初始的位置。
当然,我们也可以手动使用os.chdir,但如果我们忘记撤消更改,将可能会让程序在意外的地方执行。通过使用上下文管理器,我们将会确保在正确的上下文中自动工作。
说明:在实际操作中,我们可以使用此上下文管理器,花时间编写这种干净、易懂的实用函数,会有很大的好处,因为我们会反复处理这类场景。
下面,我们执行一下程序,遍历WordPress分发版的层次结构,然后查看打印到控制台的完整路径,如下图:
测试远端目标
现在,我们已经有了WordPress文件和目录的路径,是时候测试远程的实际目标了,看看在本地文件系统中找到的哪些文件被实际安装到了远程目标上。这些将会是是我们可以在稍后的阶段中进行攻击的文件(强行登录或调查错误配置)。
test_remote函数
在我们前面的mapper.py脚本中添加test_remote函数:
def test_remote():
while not web_paths.empty():
path = web_paths.get()
url = f'{TARGET}{path}'
# your target may have throttling/lockout
time.sleep(2)
r = requests.get(url)
if r.status_code == 200:
answers.put(url)
sys.stdout.write('+')
else:
sys.stdout.write('x')
sys.stdout.flush()
说明:我个人觉得上面第四行代码可能会有问题,TARGET和path中间可能少了个斜杠(/),待会儿运行试试看。
test_remote函数是mapper脚本的主力。它在一个循环中运行,在web_paths变量的Queue为空之前它将一直执行。在循环的每次迭代中,我们从队列中获取一个路径,将其添加到目标网站的基本路径,然后尝试请求它。如果成功(响应代码200),将该URL放入answer队列,并在控制台上输出一个“+”;否则,在控制台上输出一个“x”,然后并继续循环。
对于某些web服务器来说,如果向其发送大量请求,将会被锁定。因此我们将使用time.sleep方法,在两次请求之间等待2秒钟,这将会使得我们绕过被锁定。
一旦我们知道了目标如何响应,就可以删除写入控制台的行,但当在第一次接触目标时,在控制台上输出+和x字符将有助于我们了解在运行测试时发生了什么。
run函数
最后,我们将编写run函数,作为匹配器的入口:
def run():
mythreads = list()
for i in range(THREADS):
print(f'Spawning thread {i}')
t = threading.Thread(target=test_remote)
mythreads.append(t)
t.start()
for thread in mythreads:
thread.join()
run函数编排匹配过程,调用前面定义的函数。在run函数中,我们启动了10个线程,并让每个线程运行test_remote函数。等待所有10个线程完成(使用thread.join),然后返回。
main函数块
现在,我们可以向__main__块添加更多逻辑,完整的代码块如下:
if __name__ == "__main__":
with chdir("/home/kali/Downloads/wordpress"):
gather_paths()
input('Press return to continue.')
run()
with open('myanswers.txt', 'w') as f:
while not answers.empty():
f.write(f'{answers.get()}\n')
print('done')
在调用gather_path之前,我们使用上下文管理器chdir导航到正确的目录。我们在这里添加了一个暂停(input(‘Press return to continue.’)),以备在继续之前查看控制台输出。到目前为止,我们已经从本地安装包中收集了所有感兴趣的文件路径。然后,我们针对远程目标WordPress程序运行主要的匹配任务,并将匹配结果写入文件。这个过程中我们可能会收到一堆成功的请求,把这些成功的url输出到控制台时,可能会刷新的太快,不容易跟踪。为了避免这种情况,我们将结果写入文件。注意使用上下文管理器方法打开文件,可以确保在完成时关闭文件。
小试牛刀
原书的作者保留了一个测试站点(boodelyboo.com),这就是我们在本例中进行匹配的远程目标;自己进行测试时,可以在虚拟机中安装一个Wordress。
踩坑
在运行mapper.py时,我我本地报了一堆的错误,如下图所示:
从上图的随后三行基本上看出问题所在,连不上目标主机,手工试了一下这个地址“boodelyboo.com”,果真连不上。算了,只能自己安装一个了。
安装wordpress
真是为了打车买了辆车啊,幸好有无所不知的度娘,以及高手云集的CSDN。这里参照某大神的CSDN博客(https://blog.csdn.net/m0_62454521/article/details/125691885)一次性搞定,再次感谢。
说明:在参照上述博客安装wordpress时,在最后的证书配置环节,会碰到如下的错误,提示域名被占用了,其实我试了几次,不管用什么域名,都会报这个错误,所以我最后选择干脆不配证书。
为了节省时间,安装完wordpress之后,发现出现如下的界面,我就没在进行进一步的配置。
运行mapper.py脚本
接下来直接运行脚本试一下,能够正常命中,明显的“+”比“x”多,效果还不错,如下图。
脚本执行结束后,新文件myanswers.txt中会列出匹配成功的路径,cat命令获取一下文件内容,如下图,圆满完成。
完整代码
最后,附上可运行的mapper.py的完整代码。
import contextlib
import os
import queue
import requests
import sys
import threading
import time
FILTERS = [".jpg", ".gif", ".png", ".css"]
# TARGET = "http://boodelyboo.com/wordpress"
TARGET = "http://www.llwordpress.net"
THREADS = 10
answers = queue.Queue()
web_paths = queue.Queue()
def gather_paths():
for root, _, files in os.walk('.'):
for fname in files:
if os.path.splitext(fname)[1] in FILTERS:
continue
path = os.path.join(root, fname)
if path.startswith('.'):
path = path[1:]
print(path)
web_paths.put(path)
@contextlib.contextmanager
def chdir(path):
"""
On enter, change directory to specified path.
On exit, change direcgory to original.
"""
this_dir = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(this_dir)
def test_remote():
while not web_paths.empty():
path = web_paths.get()
url = f'{TARGET}{path}'
time.sleep(2)
r = requests.get(url)
if r.status_code == 200:
answers.put(url)
sys.stdout.write('+')
else:
sys.stdout.write('x')
sys.stdout.flush()
def run():
mythreads = list()
for i in range(THREADS):
print(f'Spawning thread {i}')
t = threading.Thread(target=test_remote)
mythreads.append(t)
t.start()
for thread in mythreads:
thread.join()
if __name__ == '__main__':
with chdir("/home/kali/Downloads/wordpress"):
gather_paths()
input('Press return to continue.')
run()
with open('myanswers.txt', 'w') as f:
while not answers.empty():
f.write(f'{answers.get()}\n')
print('done')