漏洞信息
官方安全公告:https://www.djangoproject.com/weblog/2024/sep/03/security-releases/
漏洞影响:
- Django 5.1 < 5.1.1
- Django 5.0 < 5.0.9
- Django 4.2 < 4.2.16
简介:由于未处理的电子邮件发送失败, django.contrib.auth.forms.PasswordResetForm
类允许远程攻击者通过发出密码重置请求并观察结果来枚举用户电子邮件。
漏洞环境搭建
注意:该漏洞为电子邮件发送失败情况下的用户枚举,所以我们估计设置了一个无效的邮件服务器
复现环境:
- Django:5.0.8版本
- Windows10
- Python3.12
Django环境部署:
一、新建一个Django,我这里叫Helloworld,不会新建参考菜鸟教程
二、编写一下代码
Helloworld/views.py
from django.contrib.auth.forms import PasswordResetForm
from django.http import JsonResponse
def password_reset_api(request):
if request.method == 'POST':
form = PasswordResetForm(request.POST)
if form.is_valid():
# 尝试发送密码重置邮件
form.save(
request=request,
from_email='[email protected]',
email_template_name='registration/password_reset_email.html',
)
# 返回成功响应
return JsonResponse({'message': '密码重置邮件已发送'}, status=200)
else:
# 返回表单错误
return JsonResponse({'errors': form.errors}, status=400)
else:
# 返回方法不允许的错误
return JsonResponse({'error': '仅支持 POST 请求'}, status=405)
Helloworld/urls.py
from django.urls import path
from .views import password_reset_api
urlpatterns = [
path('api/password_reset/', password_reset_api, name='password_reset_api'),
]
Helloworld/settings.py 最下面增加以下内容
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # 或其他数据库引擎
'NAME': BASE_DIR / 'db.sqlite3', # 数据库名称或路径
# 其他数据库配置,如用户名、密码、主机等
}
}
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'invalid.smtp.server' # 故意使用无效的邮件服务器
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = '[email protected]'
EMAIL_HOST_PASSWORD = 'your_email_password'
templates/registration/password_reset_email.html
{% autoescape off %}
要重置您的密码,请点击以下链接(测试用,链接格式随意,对不对都行):
{{ protocol }}://{{ domain }}{{uid}}{{token}}
{% endautoescape %}
三、创建内置用户并启动Django环境
# 生成迁移文件
python manage.py makemigrations
# 应用迁移,创建数据库表
python manage.py migrate
# 创建超级用户
python manage.py createsuperuser
接下来会提示你创建用户,根据提示创建一个用户即可,用于后续的对比回显,我这里创建了[email protected]
# 运行开发服务器
python manage.py runserver
漏洞复现
访问我们编写的api,并发送post请求,直接使用HackBar即可
一、测试用户存在时,邮件发送功能不可用的回显为500
二、测试用户不存在时,邮件发送功能不可用的回显为200
漏洞分析
一、调试用户存在时的代码,发现问题点在于django.contrib.auth.forms.PasswordResetForm
类的save函数,该函数代码如下:
def save(
self,
domain_override=None,
subject_template_name="registration/password_reset_subject.txt",
email_template_name="registration/password_reset_email.html",
use_https=False,
token_generator=default_token_generator,
from_email=None,
request=None,
html_email_template_name=None,
extra_email_context=None,
):
"""
Generate a one-use only link for resetting password and send it to the
user.
"""
email = self.cleaned_data["email"]
if not domain_override:
current_site = get_current_site(request)
site_name = current_site.name
domain = current_site.domain
else:
site_name = domain = domain_override
email_field_name = UserModel.get_email_field_name()
for user in self.get_users(email):
user_email = getattr(user, email_field_name)
context = {
"email": user_email,
"domain": domain,
"site_name": site_name,
"uid": urlsafe_base64_encode(force_bytes(user.pk)),
"user": user,
"token": token_generator.make_token(user),
"protocol": "https" if use_https else "http",
**(extra_email_context or {}),
}
self.send_mail(
subject_template_name,
email_template_name,
context,
from_email,
user_email,
html_email_template_name=html_email_template_name,
)
二、在存在用户时,代码进行到for user in self.get_users(email)时,self.get_users(email)会获取到用户的email,然后最终调用self.send_mail来发送重置密码的邮件,如果发送邮件的功能错误时,就会产生500报错
三、如果用户不存在,代码进行到for user in self.get_users(email)时,self.get_users(email)获取的内容为空,那么也就不会进入for循环的逻辑,从而直接到下一步返回200给客户端
官方修复方法分析
官方给send_mail方法加入了异常处理,使得该方法在邮件服务器错误时不再报错
修复前:
修复后: