Django CVE-2019-19118 越权漏洞分析

0x00前言

因为各种新洞,还有realworld hackvally的展台导致近来的分析漏洞一直被耽搁,而且最近Django发布出来一个最新的漏洞,是一个admin提权漏洞,但是分析一下感觉漏洞比较鸡肋。。

 

0x01 漏洞分析

首先看看官方是怎么介绍的

Since Django 2.1, a Django model admin displaying a parent model with related model inlines, where the user has view-only permissions to a parent model but edit permissions to the inline model, would display a read-only view of the parent model but editable forms for the inline.

Submitting these forms would not allow direct edits to the parent model, but would trigger the parent model's save() method, and cause pre and post-save signal handlers to be invoked. This is a privilege escalation as a user who lacks permission to edit a model should not be able to trigger its save-related signals.

To resolve this issue, the permission handling code of the Django admin interface has been changed. Now, if a user has only the "view" permission for a parent model, the entire displayed form will not be editable, even if the user has permission to edit models included in inlines.

This is a backwards-incompatible change, and the Django security team is aware that some users of Django were depending on the ability to allow editing of inlines in the admin form of an otherwise view-only parent model.

Given the complexity of the Django admin, and in-particular the permissions related checks, it is the view of the Django security team that this change was necessary: that it is not currently feasible to maintain the existing behavior whilst escaping the potential privilege escalation in a way that would avoid a recurrence of similar issues in the future, and that would be compatible with Django's safe by default philosophy.

For the time being, developers whose applications are affected by this change should replace the use of inlines in read-only parents with custom forms and views that explicitly implement the desired functionality. In the longer term, adding a documented, supported, and properly-tested mechanism for partially-editable multi-model forms to the admin interface may occur in Django itself.

其阐述了如果我当前模块使用的是内联模块,如果我对父模块拥有view的权限,但是我对内敛模块有编辑的权限,那么执行内敛模块编辑的post请求之后其后台会同时也会调用父类型的save()方法和信号,这其实也就造成了一次提高权限的攻击,虽然我们不能对父类型进行编辑但是防止未来在软件升级的时候因为储存问题导致了越权,此漏洞影响Django2.1和2.28以下的版本。先来看看官方怎么修复的

https://github.com/django/django/commit/11c5e0609bcc0db93809de2a08e0dc3d70b393e4

首先看到get_inline_formsets这个函数

    def get_inline_formsets(self, request, formsets, inline_instances, obj=None):
        inline_admin_formsets = []
        for inline, formset in zip(inline_instances, formsets):
            fieldsets = list(inline.get_fieldsets(request, obj))
            readonly = list(inline.get_readonly_fields(request, obj))
            has_add_permission = inline._has_add_permission(request, obj)
            has_change_permission = inline.has_change_permission(request, obj)
            has_delete_permission = inline.has_delete_permission(request, obj)
            has_view_permission = inline.has_view_permission(request, obj)
            prepopulated = dict(inline.get_prepopulated_fields(request, obj))
            inline_admin_formset = helpers.InlineAdminFormSet(
                inline, formset, fieldsets, prepopulated, readonly, model_admin=self,
                has_add_permission=has_add_permission, has_change_permission=has_change_permission,
                has_delete_permission=has_delete_permission, has_view_permission=has_view_permission,
            )
            inline_admin_formsets.append(inline_admin_formset)
        return inline_admin_formsets

该函数在每次进入到后台编辑界面便会触发,如果我们需要进入到for循环里面去,则admin.py文件需要用inline的方法,在对内联模型也就是Test2模型进行权限的检查,检查时候存在读写和查看的权限。但是如果只赋了增加、删除和更改权限则后台会默认赋予查看权限。因此我们使用账号进入到模块的编辑之后其四个权限都是正确的

同时在_changeform_view函数中的if判断函数

      if not self.has_view_or_change_permission(request, obj):
                raise PermissionDenied

if判断在我们传入的数据是否有查看或者编写的权限,同时也可以看到只要账号拥有查看权限便可以直接绕过检查进入到后面的save_model函数中

   if request.method == 'POST':
            form = ModelForm(request.POST, request.FILES, instance=obj)
            form_validated = form.is_valid()
            if form_validated:
                new_object = self.save_form(request, form, change=not add)
            else:
                new_object = form.instance
            formsets, inline_instances = self._create_formsets(request, new_object, change=not add)
            if all_valid(formsets) and form_validated:
                self.save_model(request, new_object, form, not add)
                self.save_related(request, form, formsets, not add)
                change_message = self.construct_change_message(request, form, formsets, add)
                if add:
                    self.log_addition(request, new_object, change_message)
                    return self.response_add(request, new_object)
                else:
                    self.log_change(request, new_object, change_message)
                    return self.response_change(request, new_object)
            else:
                form_validated = False

在if判断之后便进入到了表单有效性检查,检查之后便到达了表单的存储,在save_related方法里面调用save_m2m方法,该方法为存储many-to-many数据

    def _save_m2m(self):
        """
        Save the many-to-many fields and generic relations for this form.
        """
        cleaned_data = self.cleaned_data
        exclude = self._meta.exclude
        fields = self._meta.fields
        opts = self.instance._meta
        # Note that for historical reasons we want to include also
        # private_fields here. (GenericRelation was previously a fake
        # m2m field).
        for f in chain(opts.many_to_many, opts.private_fields):
            if not hasattr(f, 'save_form_data'):
                continue
            if fields and f.name not in fields:
                continue
            if exclude and f.name in exclude:
                continue
            if f.name in cleaned_data:
                f.save_form_data(self.instance, cleaned_data[f.name])

之后该方法便执行form表单的存储

因此进行一下debug调试,先创建一个内联模块

model.py

from django.db import models
from django.contrib.auth.models import User

# Create your models here.



class Test(models.Model):
    num = models.IntegerField(default=0)
    name = models.CharField(max_length=256)

class Test2(models.Model):
    test = models.ForeignKey('Test', on_delete = models.CASCADE)
    enable = models.CharField(max_length=256)

 

admin.py

from django.contrib import admin
from app01.models import Test2,Test

# Register your models here.


class Links(admin.TabularInline):
    model = Test2

class LinkInline(admin.ModelAdmin):
    list_display = ('num', 'name')
    inlines = [Links]


admin.site.register(Test,LinkInline)


 

之后我们给测试账号赋予权限,将Test模块赋予查看的权限,给Test2赋予编辑可写的权限,如图所示

因此使用test账号登陆之后后台则会显示Test2模块可编辑

点击保存激存储过程,执行到new_object = self.save_form(request, form, change=not add)中首次调用了save方法,可以看到在save_model方法的obj中,父模型的name和num被传了进来

 

之后在save_related方法中调用了_save_m2m的方法,对数据进行了一次对象化处理

在这里就导致了漏洞的触发,因为该账号对Test模块是只读的权限,而系统调用了Test的save存储模块,进而造成了权限提升。又在save_related中进行数据存储的for循环存储,此时父模型Test和内联合模型Test2将会进行储存

进行save方法的后续

至此,模块的存储数据库便完成了。

那么官方修复后拥有父模型的查看权限和内联的编辑权限的账号便无法对改模型进行任何的操控

如果post上传修改也只是会爆出403错误

 

最后,虽然现在这个还是一个普通的权限越级的漏洞,但是如果开发人员在接受改save信号并且对信号作出相应的处理的时候,这个漏洞的危害还是显而易见的。顺便来推广一下realworld的比赛,明年一定要去

 

 

0xFF Reference