最近在看慕学网《强力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})
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
主要问题有几处:
- 没有校验验证码是否有效,是否存在。。。
- 没有鉴定验证码是否属于修改密码的类型的。验证码是同个表。意味着注册认证码也是能进入到修改页面的。
- 从用户页面获取email。这个我不是很理解,因为保存验证码的时候,其实有保存了email字段,根据验证码直接取email即可。也就是这里可以在用户页面修改email,达到修改其它账户密码。
- 没有删除掉验证码。可以同个验证码无限使用。
0x02利用
开始想着,连校验验证码都没,From只是对密码长度做了限制。直接构造数据post就行了。
但是由于Django CSRF机制存在,没有成功。于是转向了另一种简单粗暴的方式,一个chrome浏览器搞定。
- 首先注册用户
它会生成一个验证码存到相应数据库。并给你发送一封激活邮件来激活你的账户。
里面链接格式:http://[网站]/active/[验证码]
比如我收到的激活链接应该是这样的:
http://127.0.0.1:8000/actice/UCZDRMPO7lVErxM5
- 构造修改页面的链接
提取验证码组合出以下重置密码的链接:
http://127.0.0.1:8000/reset/UCZDRMPO7lVErxM5
通过审查可以看到有个隐藏的input,里面有email。
- 修改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">
|