53

一次安全测试引发的对Django框架文件上传安全机制的初步分析

 5 years ago
source link: http://www.freebuf.com/articles/web/183125.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

*本文原创作者:ForrestX386,本文属FreeBuf原创奖励计划,未经许可禁止转载

0×00. 起因

我司的堡垒机是基于jumpserver 0.3版本进行二次开发,进行了大量的重构和新功能的添加,基本满足了公司安全运维的需求。在对文件上传接口进行安全审计的时候发现,其对上传文件名没有过滤处理,然后直接写入磁盘(部分代码如下)

RRF7zmN.jpg!web

隐隐觉得可以搞搞任意文件写入漏洞(jumpserver web 控制面板都是以root权限运行,如果可以任意文件写入,危害呵呵,你懂得)。启动burpsuite ,拦截请求,修改文件名(含有目录穿越字符),但是结果没成功,调试发现upload_file.name 已经是../../等目录穿越字符过滤后的结果,有点奇怪,莫非是框架自动帮我过滤掉了,好奇心驱使我必须弄明白其中的原理,于是有了本文。

0×01. 分析过程

切入点就是request.FILES 对象的由来,整个流程涉及到5个模块,如下:

django.core.handlers.wsgi

django.http.request

django.http.multipartparser

django.core.files.uploadhandler
django.core.files.uploadedfile

request.FILES 是一个类似于dict的对象,上传文件输入框name属性的值为键名,键值指向处理后的文件对象(框架会调用指定的文件处理器处理),这个文件对象就是django.core.files.uploadedfile 模块中UploadedFile类的实例。详细分析如下:

访问request.FILES 就是访问 django.core.handlers.wsgi 模块中WSGIRequest(继承至django.http.request模块的HttpRequest类)类的FILES属性

myeUveb.jpg!web

也即访问WSGIRequest._get_files,这个方法会先判断是否已经解析过上传的文件(也即判断是否有_files属性,其实FILES 就是_files,MultiValueDict 类的实例),跟进_load_post_and_files 方法(这是继承至其父类django.http.request模块的HttpRequest类中的方法),如下:

YjMNjan.jpg!web

跟进parse_file_upload方法(这里的data为WSGIRequest 的实例),如下:

bY7Nzui.jpg!web

这里要先说下upload_handlers 成员,如下:

zmmAFzA.jpg!web

初始化upload_handlers的时候会调用django.core.files.uploadhandler模块的load_handler加载系统默认的文件处理器,如下:

Rniyair.jpg!web

settings.FILE_UPLOAD_HANDLERS

默认就是指的红框中的两个文件处理器,大于2.5M的就用TemporaryFileUploadHandler 处理器,否则用MemoryFileUploadHandler。

初始化文件上传处理器之后,就开始调用django.http.multipartparser 模块的MultiPartParser 类的parse 方法对上传文件进行解析处理,在解析处理过程中,会调用 handle_file_complete 对上传后的文件进行再次处理(处理完成后就返回一个django.core.files.uploadedfile.UploadedFile类的实例, 这个实例对象会被添加到_files 对象中,然后由parse 方法返回此对象, 这个过程就包含文件名被过滤掉的过程),如下:

MRBJ7fa.jpg!web

图中的old_filed_name 即为上一个解析完毕的文件,跟进handle_file_complete ,如下:

ANJFfeM.jpg!web

跟进文件处理器的file_complete方法, 这个方法返回的就是处理后的文件对象,也就是0×00 图中upload_file 变量指向的文件对象,这里我们以MemoryFileUploadHandler 文件处理器为例进行说明:

yeqYfyj.jpg!web 也就是说0×00 中的upload_file 也即InMemoryUploadedFile 类的实例,所以调用upload_file.name 即调用InMemoryUploadedFile 的name属性,如下:

JN3auyM.jpg!web 调用InMemoryUploadedFile 的name属性,即调用_get_name方法,在InMemoryUploadedFile  实例话的过程中有name的赋值操作(在其父类__init__方法中)如下:

ZvM3auy.jpg!web

赋值操作就会触发_set_name方法的执行:

UzIFjee.jpg!web

在_set_name中就会对上传的文件进行过滤处理,os.path.basename(name)防止了目录穿越漏洞,所以我们在0×00 图中使用uploadfile.name获取到的是经过os.path.basename 处理后的文件名,当然没法任意文件写入了

0×02. 总结

梳理完成之后,终于对Django 文件上传中的安全机制有了一些了解,解决了我的困惑,像Django 这种现代的web框架对传统的安全漏洞(比如XSS,CSRF、文件上传等)都做了比较好的处理,在开发中,这些都是值得我们借鉴和学习的地方。

*本文原创作者:ForrestX386,本文属FreeBuf原创奖励计划,未经许可禁止转载


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK