前面讲了用户枚举这个漏洞以及一些常规的防护方法,现在再补充讲另一种用户枚举漏洞的存在情况——在注册页面存在的用户枚举
特殊的用户枚举
先看我们之前实现的认证机制中处理注册请求的视图函数,明显存在用户枚举漏洞
之前我们说的一种防护手段是:把服务器回显的错误提示信息修改成一致的。那这里是否也可以应用这种防护?
显然不能。首先这会影响到用户注册功能的服务效果,而我们所需要的安全,是在不妨碍服务正常运行这个前提下的最大安全
其次也是主要的,即使我们把错误提示信息都改成一样,也么得用。这里只有两种异常情况,而第二种异常(两次密码输入不一致)的发生与否是用户能够控制的。所以当有异常发生时,即便服务器反馈的错误提示信息一致,攻击者同样能够确定发生的是哪一种异常
限制用户可控制
对于上面注册页面的用户枚举,一个较安全的防护方法就是,禁止用户自定义用户名
如果在注册时,用户只能输入确认口令和一些其他的用户信息,而由服务器生成一个用户名来完成注册,那么攻击者就无法控制用户名的内容,自然不能进行枚举,显然这会是一个很安全的防护方法
实现如下
# views.py
from django.contrib.auth.hashers import make_password
from django.shortcuts import render
from Blog.models import User
def register(request):
if request.session.get('is_login', None):
request.session.flush()
if request.method == 'POST':
password = request.POST.get('password', '').strip()
re_password = request.POST.get('re_password', '').strip()
if password == re_password:
while True:
username = username_generater()
try:
User.objects.get(username=username)
except User.DoesNotExist:
encrypted_password = make_password(password, None, 'pbkdf2_sha256')
new_user = User(username=username, password=encrypted_password)
new_user.save()
return render(request, 'register.html', {'mess': '用户注册成功', 'username': username})
else:
pass
else:
return render(request, 'register.html', {'mess': '两次密码输入不一致'})
return render(request, 'register.html')
上面的 username_generater( ) 是用户名生成器,具体可实现的方案有很多,这里不复现
一个较好的生成方案应该有几点:生成的用户名不要呈现某种分布或变化规律,否则容易被攻击者逆推猜测出其他的用户名。同时生成的用户名不能出现碰撞,最好还能兼顾一下效率
注册成功后,服务器存储用户数据到数据库,同时把成功提示和用户名返回至客户端
注册页面的模板也需要进行相应修改
<!-- register.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>用户注册</title>
</head>
<body>
<div>
<h2>请输入待注册的用户信息</h2>
<form action="/register/" method="post">
{% csrf_token %}
<p>密码:
<input type="password" name="password">
</p>
<p>再次输入:
<input type="password" name="re_password">
</p>
<br/>
<input type="submit" value="注册用户">
</form>
</div>
<a href="/login/">返回登录</a>
<div>{{ mess }}</div>
<div>
{% if username %}
<p>请记住你的用户名:{{ username }}</p>
{% endif %}
</div>
</body>
</html>
(这样看着,好像有点丑)
凡事有利必有弊。禁止用户名自定义可以完全杜绝用户枚举,但也同样存在一些缺点
比如,用户可能对于服务器分配的用户名不熟悉,容易忘记。又或者,服务器生成的用户名有可能是用户不喜欢的数字或字符。这样用户体验感显然比不上自定义用户名服务来得好
所以,对于一些安全要求不是特别高的网站,我们可以保留自定义用户名的功能,选用其他的方法来进行防护
限制漏洞可利用
如果保留自定义用户名的功能,攻击者就可以通过查询用户是否已被注册来确定该用户的存在情况,漏洞存在且可利用
不能修复漏洞,我们可以选择增加漏洞利用的成本。如果漏洞利用的成本超出了攻击者的预期收益,那么在一定程度上可以说是安全的
前面说过用户枚举的两种攻击情况,明显第二种使用自动化脚本工具进行大量尝试攻击更具有威胁性,我们可以通过限制自动化工具的使用来增加攻击的难度,提高漏洞利用成本
可以增加一个鉴别自动化工具的机制——全自动区分计算机和人类的图灵测试 ,即 Completely Automated Public Turing Test to Tell Computers and Humans Apart,简称 Captcha,也就是我们说的验证码
这里直接使用模块 django-simple-captcha,使用 pip 命令安装
pip instsall django-simple-captcha
在 setting.py 文件中对 captcha 进行配置
# setting.py
# 添加 captcha 到应用注册
INSTALLED_APPS += [
'captcha',
]
# django_simple_captcha 验证码配置
# 输出格式
CAPTCHA_OUTPUT_FORMAT = u'%(text_field)s %(hidden_field)s %(image)s'
# 图片大小
CAPTCHA_IMAGE_SIZE = (110, 25)
# 图片背景颜色
CAPTCHA_BACKGROUND_COLOR = '#ffffff'
# 设置图片中的字符类型
CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge' # 随机英文字母
# 字符个数
CAPTCHA_LENGTH = 4
# 验证码有效时间 (minutes)
CAPTCHA_TIMEOUT = 1
# 噪点样式
CAPTCHA_NOISE_FUNCTIONS = (
'captcha.helpers.noise_null', # 没有样式
# 'captcha.helpers.noise_arcs', # 线
# 'captcha.helpers.noise_dots', # 点
)
迁移数据,在数据库生成 captcha_captchastore 表
python manage.py migrate
修改视图函数
# views.py
from django.contrib.auth.hashers import make_password
from django.shortcuts import render
from Blog.models import User
from captcha.models import CaptchaStore
from captcha.helpers import captcha_image_url
from django.http import JsonResponse
def captcha_generater(request):
hashkey = CaptchaStore.generate_key() # 生成验证码
request.session['hashkey'] = hashkey # 存入 Session
image_url = captcha_image_url(hashkey)
return image_url
def register(request):
if request.session.get('is_login', None):
request.session.flush()
if request.method == 'POST':
username = request.POST.get('username', '').strip()
password = request.POST.get('password', '').strip()
re_password = request.POST.get('re_password', '').strip()
captcha = request.POST.get('captcha', '').strip()
if not captcha:
return render(request, 'register.html', {'mess': '请输入验证码', 'image_url': captcha_generater(request)})
hashkey = request.session.get('hashkey') # 在这里,显然是存在的
try:
mycaptcha = CaptchaStore.objects.get(hashkey=hashkey)
except CaptchaStore.DoesNotExist:
return render(request, 'register.html', {'mess': '服务器故障,请重新提交', 'image_url': captcha_generater(request)})
if captcha.lower() != mycaptcha.response:
return render(request, 'register.html', {'mess': '验证码输入错误', 'image_url': captcha_generater(request)})
if password == re_password:
try:
User.objects.get(username=username)
except User.DoesNotExist:
encrypted_password = make_password(password, None, 'pbkdf2_sha256')
new_user = User(username=username, password=encrypted_password)
new_user.save()
return render(request, 'register.html', {'mess': '用户注册成功', 'image_url': captcha_generater(request)})
else:
return render(request, 'register.html', {'mess': '该用户已被注册', 'image_url': captcha_generater(request)})
else:
return render(request, 'register.html', {'mess': '两次密码输入不一致', 'image_url': captcha_generater(request)})
return render(request, 'register.html', {'image_url': captcha_generater(request)})
e_url, 'mess': mess})
# 动态刷新验证码
def refresh_captcha(request):
return JsonResponse({'status': 1, 'image_url': captcha_generater(request)})
注册页面模板
<!-- register.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>用户注册</title>
{% load static %}
<script src="{% static 'js/jquery-3.4.1.js' %}"></script>
</head>
<body>
<div class="reg">
<h2>请输入待注册的用户信息</h2>
<form action="/register/" method="post">
{% csrf_token %}
<p>账号:
<input type="text" name="username">
</p>
<p>密码:
<input type="password" name="password">
</p>
<p>再次输入:
<input type="password" name="repassword">
</p>
<p>验证码:
<input type="text" name="captcha">
<img src="{{ image_url }}" id="captcha_img">
<!--a id="captcha_refresh">换一张</a-->
<span>(点击图片刷新)</span>
</p>
<br/>
<input type="submit" value="注册用户" class="button">
</form>
</div>
<a href="/login/" class="tologin">返回登录</a>
<div class="mess">{{ mess }}</div>
<script>
$(document).ready(function() {
$('#captcha_img').click(function() {
$.getJSON("/refresh_captcha/", function(result) {
$('#captcha_img').attr('src', result['image_url']);
});
});
});
</script>
</body>
</html>
最后,修改 urls.py 文件
# urls.py
from django.contrib import admin
from django.urls import path, include
from . import views
urlpatterns = [
path('admin/', admin.site.urls),
path('login/', views.login),
path('logout/', views.logout),
path('register/', views.register),
path('index/', views.index),
path('captcha/', include('captcha.urls')), # 添加 captcha 路由
path('refresh_captcha/', views.refresh_captcha), # 异步刷新验证码
]
增加了验证码之后的注册界面
图中的验证码太过简单,很容易被工具识别出来,我们需要更加复杂的验证码
修改 setting.py 文件,给验证码增加噪点
# setting.py
# 噪点样式
CAPTCHA_NOISE_FUNCTIONS = (
#'captcha.helpers.noise_null', 没有样式
'captcha.helpers.noise_arcs', # 线
'captcha.helpers.noise_dots', # 点
)
最终页面