Web登陆防爆破的原理和实现
0、写在前面
本来只想做个验证码的功能实现,后来想想光要验证码也不行,干脆写整个防爆破的实现吧
1、防护软件/硬件Waf/Web服务器限制单IP固定时间段的登陆频率
1.1 作用
通过WAF可以实现某一个IP访问频率过高时则将此IP加入黑名单一段时间
通过Nginx等Web服务器可以实现限制单IP固定时间段的登陆频率,也就是限制流量
可以防爆破的同时一定程度上防止DDos攻击
1.2 实现
1 http{ 2 ... 3 4 #定义一个名为allips的limit_req_zone用来存储session,大小是10M内存, 5 #以$binary_remote_addr 为key,限制平均每秒的请求为20个, 6 #1M能存储16000个状态,rete的值必须为整数, 7 #如果限制两秒钟一个请求,可以设置成30r/m 8 9 limit_req_zone $binary_remote_addr zone=allips:10m rate=20r/s; 10 ... 11 server{ 12 ... 13 location { 14 ... 15 16 #限制每ip每秒不超过20个请求,漏桶数burst为5 17 #brust的意思就是,如果第1秒、2,3,4秒请求为19个, 18 #第5秒的请求为25个是被允许的。 19 #但是如果你第1秒就25个请求,第2秒超过20的请求返回503错误。 20 #nodelay,如果不设置该选项,严格使用平均速率限制请求数, 21 #第1秒25个请求时,5个请求放到第2秒执行, 22 #设置nodelay,25个请求将在第1秒执行。 23 24 limit_req zone=allips burst=5 nodelay; 25 ... 26 } 27 ... 28 } 29 ... 30 } 31
1.3 问题
攻击者可以通过代理池的方式来绕过
2、WebApp限制单用户固定时间段的登陆频率
2.1 作用
极大的拖慢爆破的速度,通常一小时错误6次就要锁账号
而且可以被锁定时发邮件提醒用户(感觉最多也就是让人心里有个数,但用没什么卵用)
2.2 实现
用户名密码等信息为了简化都保存在字典中,正常都应在数据库中
1 # -*- coding: utf-8 -*- 2 from flask import Flask,render_template,request,flash,redirect,url_for 3 import datetime 4 5 app = Flask(__name__) 6 app.secret_key = 'zz' 7 8 user_dict = { 9 'admin':{ 10 'username': 'admin', 11 'password': 'admin', 12 'count': 0, 13 'last_time': '', 14 'new_time': '', 15 'locked': False, 16 }, 17 'test': { 18 'username': 'test', 19 'password': 'test', 20 'count': 0, 21 'last_time': '', 22 'new_time': '', 23 'locked': False, 24 } 25 } 26 27 28 29 @app.route('/',methods=['POST','GET']) 30 def hello_world(): 31 if request.method == 'POST': 32 username = request.form.get('username') 33 password = request.form.get('password') 34 #验证用户是否存在,存在则继续,不存在则返回用户不存在 35 if user_dict.get(username): 36 #验证用户是否被锁 37 if user_dict[username]['locked']: 38 #验证被锁时间是否达到3600秒,达到则用户登陆计数清零,锁定状态变为未锁定 39 if (datetime.datetime.now()-user_dict[username]['last_time']).seconds>3600: 40 user_dict[username]['count'] = 0 41 user_dict[username]['locked'] = False 42 flash('you are unlocked') 43 return redirect(url_for('hello_world')) 44 flash('you are locked') 45 return redirect(url_for('hello_world')) 46 else: 47 if username==user_dict[username]['username'] and password==user_dict[username]['password']: 48 return 'ok' 49 else: 50 if user_dict[username]['count'] == 0: 51 user_dict[username]['count']+=1 52 user_dict[username]['last_time'] = datetime.datetime.now() 53 user_dict[username]['new_time'] = datetime.datetime.now() 54 flash('password is error') 55 elif user_dict[username]['count'] == 6: 56 user_dict[username]['new_time'] = datetime.datetime.now() 57 user_dict[username]['last_time'] = datetime.datetime.now() 58 user_dict[username]['locked'] = True 59 flash('you tried 6 times and you are locked 3600 seconds') 60 else: 61 user_dict[username]['count'] += 1 62 user_dict[username]['last_time'] = user_dict[username]['new_time'] 63 user_dict[username]['new_time'] = datetime.datetime.now() 64 flash('password is error') 65 return redirect(url_for('hello_world')) 66 else: 67 flash('username errors') 68 return redirect(url_for('hello_world')) 69 return render_template('index.html') 70 71 72 73 74 if __name__ == '__main__': 75 app.run()
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body> 8 <form action="" method="post"> 9 username: <input type="text" name="username"><br> 10 password: <input type="password" name="password"><br> 11 <input type="submit"><label>count:{{ count }}</label> 12 </form> 13 <hr> 14 {% for msg in get_flashed_messages() %} 15 <p>{{ msg }}</p> 16 {% endfor %} 17 </body> 18 </html>
2.3 问题
可以故意输错密码让原账户登陆不了,造成原用户也无法使用
3、扭曲的图片验证码
3.1、作用
为了防止重放攻击
不能每次post和get都改变的验证码没有意义
3.2、实现
验证码的生成、获取、更新、核对全部在后端进行
这里为了简化用户名密码没有加密保存在数据库中,验证码也没有生成图片显示
1 from flask import Flask,render_template,request,session,flash,redirect,url_for 2 import random 3 4 app = Flask(__name__) 5 app.secret_key = 'zz' 6 7 user_dict = { 8 'username':'admin', 9 'password':'admin', 10 } 11 12 check_code_now = { 13 'check_code':'' 14 } 15 16 def get_code(): 17 ascii_num = [48,49,50,51,52,53,54,55,56,57] 18 ascii_chr1 = [i for i in range(65,91)] 19 ascii_chr2 = [i for i in range(97,123)] 20 ascii = ascii_chr1+ascii_chr2+ascii_num 21 random_pool = [chr(i) for i in ascii] 22 check_code = random.choice(random_pool)+random.choice(random_pool)+random.choice(random_pool)+random.choice(random_pool) 23 check_code_now['check_code'] = check_code 24 return check_code 25 26 27 @app.route('/',methods=['POST','GET']) 28 def hello_world(): 29 if request.method == 'POST': 30 username = request.form.get('username') 31 password = request.form.get('password') 32 check_code = request.form.get('check_code') 33 if check_code_now.get('check_code')!= check_code: 34 flash('check_code error') 35 get_code() 36 return redirect(url_for('hello_world')) 37 if username == user_dict.get('username') and password == user_dict.get('password'): 38 return 'ok' 39 else: 40 flash('username or password error') 41 get_code() 42 return redirect(url_for('hello_world')) 43 check_code = get_code() 44 return render_template('index.html',check_code = check_code) 45 46 47 48 49 if __name__ == '__main__': 50 app.run()
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body> 8 <form action="" method="post"> 9 username: <input type="text" name="username"><br> 10 password: <input type="password" name="password"><br> 11 checkcode:<input type="text" name="check_code"><label style="color: red">{{ check_code }}</label><br> 12 <input type="submit"> 13 </form> 14 <hr> 15 {% for msg in get_flashed_messages() %} 16 <p>{{ msg }}</p> 17 {% endfor %} 18 </body> 19 </html>
3.3、问题
不要将验证码保存在session中
不管是Flask(将session加密存储在浏览器cooikes中),还是Django(将session保存在数据库中,cooikes只保存sessionID)
重放攻击中每次的session都不变,去session中核对验证码每次都是不变的,那验证码将没有意义
另外扭曲的验证码会让用户都看不明白,而且某些图片识别还是能识别、、
4、IP段黑白名单
4.1 作用
通过Web服务器(Nginx等)实现
黑名单可以把某些IP直接Ban掉
对内网的WebApp白名单很有用
4.2 实现
1 location / { 2 deny 192.168.1.1; 3 allow 192.168.1.0/24; 4 allow 10.1.1.0/1 5 }
4.3 问题
有些用户不知情的情况下被人当肉鸡,然后被黑名单Ban掉,影响正常使用、、
5、写在最后
还是那句老话,安全没有银弹
这些措施加在一起也并不能防止代理池+图片识别工具,但可以大幅度增加爆破的时间
然而很可能对用户正常使用产生影响、、