慕学网强力Django教程例子存在认证逻辑漏洞 可修改任意用户密码

最近在看慕学网《强力Django+xadmin打造在线云课堂》这个教程。

感觉还不错。昨日看到用户模块,修改密码功能的逻辑编写,觉得存在比较大的安全隐患。刚刚按照思路小撸了一下,可以修改任意用户密码。

0x01 漏洞分析

文件apps/users/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 用户进入到重置密码页面
class ResetView(View):
def get(self, request, active_code):
all_records = EmailVerifyRecord.objects.filter(code=active_code)
if all_records:
for records in all_records:
email = records.email
return render(request, 'password_reset.html', {'email': email})
# return render(request, 'active_fail.html')


# 用户在重置密码页面提交新密码
class ModifyPwdView(View):
def post(self, request):
modify_form = ModifyPwdForm(request.POST)
email = request.POST.get('email', '')
if modify_form.is_valid():
pwd1 = request.POST.get('password1', '')
pwd2 = request.POST.get('password2', '')
if pwd1 != pwd2:
return render(request, 'password_reset.html', {'email': email, 'msg': '密码不一致!'})
user = UserProfile.objects.get(email=email)
user.password = make_password(pwd2)
user.save()
return render(request, 'login.html')

首先他逻辑是这样的,通过ResetView来展示修改密码页面,然后修改数据POST给ModifyPwdView修改,其实我觉得大可在ResetView中处理POST动作就好了。

ModifyPwdView主要问题有几处:

  1. 没有校验验证码是否有效,是否存在。。。
  2. 没有鉴定验证码是否属于修改密码的类型的。验证码是同个表。意味着注册认证码也是能进入到修改页面的。
  3. 从用户页面获取email。这个我不是很理解,因为保存验证码的时候,其实有保存了email字段,根据验证码直接取email即可。也就是这里可以在用户页面修改email,达到修改其它账户密码。
  4. 没有删除掉验证码。可以同个验证码无限使用。

0x02利用

开始想着,连校验验证码都没,From只是对密码长度做了限制。直接构造数据post就行了。

但是由于Django CSRF机制存在,没有成功。于是转向了另一种简单粗暴的方式,一个chrome浏览器搞定。

  1. 首先注册用户

它会生成一个验证码存到相应数据库。并给你发送一封激活邮件来激活你的账户。
里面链接格式:http://[网站]/active/[验证码]
比如我收到的激活链接应该是这样的:
http://127.0.0.1:8000/actice/UCZDRMPO7lVErxM5
验证码

  1. 构造修改页面的链接

提取验证码组合出以下重置密码的链接:
http://127.0.0.1:8000/reset/UCZDRMPO7lVErxM5
重置密码页面
通过审查可以看到有个隐藏的input,里面有email。

  1. 修改email和密码

修改email为其它账户的email,密码随便设置一个。
重置密码页面
提交的数据:
重置密码页面

最后用图中所示的账户和密码登录成功.

0x03 修补

方法许多,我自己处理如下:
POST同样在ResetView里面处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ResetView(View):
def get(self, request, reset_code):
records = VerifyRecord.objects.filter(verify_code=reset_code)
for record in records:
if record.send_type == 'forget':
return render(request=request, template_name='password_reset.html', context={'reset_code': reset_code})
else:
return render(request, 'msg_box.html', {"msg": "验证码类型错误"})
else:
return render(request=request, template_name='verify_expire.html')

def post(self, request, reset_code):
reset_form = ResetForm(request.POST)
records = VerifyRecord.objects.filter(verify_code=reset_code)
if not reset_form.is_valid():
return
for record in records:
user = UserProfile.objects.filter(email=record.email)
if user and record.send_type == 'forget':
password1 = request.POST.get('password', '')
password2 = request.POST.get('password2', '')
if password1 == password2:
user.update(password=make_password(password1))
record.delete()
return render(request, 'msg_box.html', {"msg": "修改密码成功"})
else:
return render(request=request, template_name='verify_expire.html')

另外html模板也要修改一下:
把传入的reset_code组合出POST URL。

1
<form id="reset_password_form" action="{% url 'reset_pwd' reset_code %}" method="post">

关注公众号 尹安灿