文章目录
0x00 介绍
本文介绍的是《Web安全技术》的大作业在线服务限制绕过
中,限制同一个账户一段时间内登录次数及绕过脚本的设计与实现。
本模块包含的功能如下:
限制账户在一段时间内登录失败的次数
:如果在该段时间内登录失败的次数超过某个阈值,则禁止登录。这里有几个需要理清楚的:如何定义一段时间
,如何确保账户被封禁的状态的更新,一段时间的起始时间的更新等绕过
:慢速撞库
的方法,对于同一个账户,可以在短时间内只尝试1次,或者少于网站上限的次数,然后不停的尝试不同的账户,直到尝试完账户字典中所有的账户,再进入下一轮撞库。
0x01 限制账户在一段时间内登录失败的次数
1.1 思路
-
数据库新增
login_error
表,记录账户登录失败的信息:- user: auth_user表的外键
- first_err_time:记录第一次登录失败的时间
- err_login_times:记录一段时间内登录失败的次数
- block_state:记录账户是否被封禁,True表示被封禁,False表示账户正常
-
思路:
-
从first_err_time开始的10min内为需要判断当前账户登陆失败次数的时间段。
|___________________________|
first_err_time first_err_time+10min
- 在这段时间内,如果失败次数err_login_times超过3次,则block_state置为True
-
1.2 实现
- 代码如下:
def check_account_state(request):
"""
检查账户是否在封禁状态
Args:
request: request对象
Returns:
state: 1表示该账户状态正常,0表示该账户还在被封禁状态,-1表示用户名或密码错误
"""
state = 1
username = request.POST.get('username', "")
password = request.POST.get('password', "")
auth_res = auth.authenticate(username=username, password=password)
default_first_err_time = datetime.datetime(2019, 10, 24, 12, 30, 11).replace(tzinfo=pytz.timezone('Asia/Shanghai'))
default_first_err_time = timestamp_to_1970(default_first_err_time)
curr_time = datetime.datetime.now().replace(tzinfo=pytz.timezone('Asia/Shanghai'))
curr_time = timestamp_to_1970(curr_time)
# auth_user表中查找该用户
user_obj = User.objects.filter(username=username).first()
if not user_obj: # 该用户不存在
state = -1
return state # 直接返回True,交给Form进一步判断
# 据user id查询login_err表
login_err = LoginError.objects.filter(user=user_obj).first()
if (not login_err) and (not auth_res): # login_err表中没有该账户的记录并且登录失败
err_login_times = 1
block_state = False
login_err_obj = LoginError(user=user_obj, first_err_time=curr_time, err_login_times=err_login_times, block_state=block_state)
login_err_obj.save()
elif login_err: # login_err表中存在该账户的记录
first_err_time = login_err.first_err_time
err_login_times = login_err.err_login_times
block_state = login_err.block_state
time_interval = curr_time - first_err_time
if time_interval > ACCOUNT_LIMIT_TIME: # 超过10min
if not auth_res: # 用户名密码错误,重置login_err对象,并将错误次数置为1
login_err = set_login_err(login_err, curr_time, 1, False)
login_err.save()
else: # 用户名密码正确,登录成功
login_err = set_login_err(login_err, default_first_err_time, 0, False)
login_err.save()
elif (time_interval <= ACCOUNT_LIMIT_TIME) and block_state: # 10min内,并且账户已被封禁
state = 0
return state
elif (time_interval <= ACCOUNT_LIMIT_TIME) and (not block_state): # 10min内,并且账户未被封禁
if not auth_res: # 用户名和密码错误
err_login_times = err_login_times + 1
login_err = set_login_err(login_err, first_err_time, err_login_times, False)
if err_login_times == MAX_ERR_LOGIN_TIMES:
login_err = set_login_err(login_err, first_err_time, 0, True) # 这里的时间必须是数据库中取出来的第一次登录失败的时间
login_err.save()
else: # 用户名和密码正确
login_err = set_login_err(login_err, default_first_err_time, 0, False)
login_err.save()
elif auth_res:
return state
return state
def set_login_err(login_err, first_err_time, err_login_times, block_state):
"""
更新 login_err 对象
"""
login_err.first_err_time = first_err_time
login_err.err_login_times = err_login_times
login_err.block_state = block_state
return login_err
def timestamp_to_1970(timestamp):
"""
当前时间距离1970的秒数
"""
return time.mktime(timestamp.timetuple())
- datetime获取的时间存在时区不一致的问题:在debug时候打印出来的时间与系统的时间一致,但是存储到数据库以后是差了8h的时间,即使我在代码中设置时区的替换也没法解决,settings.py中设置了时区也不行。最后我只想得到两种方法解决:
- 数据库中存储时间戳字符串
>>> from datetime import datetime >>> a = str(datetime.now()) >>> a '2019-10-26 15:57:51.582710' >>> b = datetime.strptime(a, '%Y-%m-%d %H:%M:%S') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "D:\ProgramFiles\Anaconda3\lib\_strptime.py", line 577, in _strptime_datetime tt, fraction, gmtoff_fraction = _strptime(data_string, format) File "D:\ProgramFiles\Anaconda3\lib\_strptime.py", line 362, in _strptime data_string[found.end():]) ValueError: unconverted data remains: .582710 >>> b = datetime.strptime(a.split('.')[0], '%Y-%m-%d %H:%M:%S') >>> b datetime.datetime(2019, 10, 26, 15, 57, 51)
- 数据库中存储的是当前时间距离1970年的秒数(
FloatField()
),这样避免了时区处理的问题,通过代码中的timestamp_to_1970
函数进行转换。
1.3 改进
可以根据账户登录失败的频率、或是该账户使用频率来动态设置封账号的时间
0x02 绕过
2.1 思路
为了尽量不触发目标网站对单个账户一段时间内的登录失败次数的限制,利用慢速撞库
的方法。
针对单个账户,短时间内只进行一次撞库尝试,在一轮里遍历所有的账户;然后进行中场休息,再继续下一轮尝试。如果在过程中已经有撞库成功的账户,则跳过该账户。
2.2 实现
- 详细的代码在
iansmith123
开源的bruteforce Repository中的文件:block_account_bypass.py
和ba_bypass_bruteforce.py
- 主要的代码:
def get_dict(dict_user, dict_pass):
"""
生成字典队列
:return:
"""
with open("dict/{}".format(dict_user)) as f:
username = [line.strip() for line in f.readlines()]
with open('dict/{}'.format(dict_pass)) as f:
passwords = [line.strip() for line in f.readlines()]
count = 0
for u in username:
# 每一轮都换下一个密码
p = passwords[curr_round % len(passwords)]
count += 1
pair = (u, p)
dict_queue.put(pair)
print("字典生成完成,长度 {}".format(count))
2.3 改进
- 测试得到目标网站的账户封禁时间time_interval,该段时间内的封禁次数n
- 在绕过脚本中,对于同一个账户可以直接尝试前n-1次,记录第一次错误时间first_time,然后程序sleep,直到first_time + time_interval
0x03 Tools and Tricks
- 进入sqlite3命令行:
sqlite3 db_file_path
-
pycharm连接sqlite3:
- 侧边栏的Database中,点击
+
新建一个sqlite数据库 - 记得点击窗口下方的安装相关的驱动
- 然后在
File
里选择数据库文件即可
- 侧边栏的Database中,点击
-
django自带的auth_user表的引用:
from django.contrib.auth.models import User
-
删除数据库的表中所有数据:
delete from user_loginerror;