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  
nginx.conf

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()
app.py
 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>
index.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()
app.py
 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>
index.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 }
nginx.conf

4.3 问题

有些用户不知情的情况下被人当肉鸡,然后被黑名单Ban掉,影响正常使用、、

 

5、写在最后

还是那句老话,安全没有银弹

这些措施加在一起也并不能防止代理池+图片识别工具,但可以大幅度增加爆破的时间

然而很可能对用户正常使用产生影响、、

posted @ 2019-02-21 10:17  隔壁古二蛋  阅读(3875)  评论(0编辑  收藏  举报