零、前言

前段时间用Django完整的做了个项目,顺便对Django的安全机制做了些研究。本文的Django版本是2.1.3。

一、XSS

Django的模板机制本身对XSS做了很多防御,比如自动的HTML标签转义,具体来说主要是下面五个字符:

< is converted to <
> is converted to >
' (single quote) is converted to '
" (double quote) is converted to "
& is converted to &

比如如下代码

def index(request):
    if 'data' in request.GET:
        data = request.GET['data']
    else:
        data = 'nothing'
    return render(request, 'security_test/index.html', {'data': data})

它会将GET到的数据输出到浏览器

img

但是这里<script>alert(1)</script>并不会执行,查看源代码,可以看到标签都被编码了。

img

但是如果我们换一种方式返回数据

return HttpResponse('This is %s' % (data))

HttpResponse对象不会调用模板,直接返回数据,那么模板自带的安全机制也就无法发挥作用了,不过一般很少有这种写法。

img

但是Django的防御不是万能的,比如模板里有这样一句

<img src={{ img }}>

如果img参数是来自用户的而又没做检测,那么很可能会造成XSS。

二、SQL

SQL是针对数据库的攻击,Django为数据库操作提供了一个抽象的模型(models)层,通过模型的API可以对数据进行操作。比如在models.py里定义这样的代码,它实际上对应了数据库的一个表,包含两个字段,id和name。

class User(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=200)

使用如下代码调用model的API可以对数据表进行查询

result = models.User.objects.get(id=id)

img

这种情况下是不会发生SQL注入的。如果使用原生的拼接SQL语句则与其他环境的SQL注入别无二致了。比如这样调用

result = models.User.objects.raw('SELECT * FROM security_test_user where id=%s' % id)

或者

cursor.execute('SELECT * FROM security_test_user where id=%s' % id)
result = cursor.fetchone()

那么就可能会发生SQL注入

img

不过传参如果是以列表的形,这样

result = models.User.objects.raw('SELECT * FROM security_test_user where id=%s', [id])

或者这样

cursor.execute('SELECT * FROM security_test_user where id=%s', [id])

Django同样会提供保护。

三、CSRF

Django内置了CSRF的防御机制,在任何有可能发生CSRF攻击的地方使用设置csrf_token即可,也就是在模板中插入

{% csrf_token %}

在django/middleware/csrf.py中可以看到

img

Django是不对GET、HEAD、OPTIONS以及TRACE协议进行检测的,这其中最常用德就是GET请求。也就是在使用GET时要保证GET仅仅是读操作,不能有任何的写(添加、删除、修改等)操作,否则Django也无法提供保护。对于一些AJAX请求,官方建议是将token放到HTTP Header中:

function csrfSafeMethod(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
    beforeSend: function(xhr, settings) {
        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", csrftoken);
        }
    }
});

四、Command(Code) Injection

命令执行一般是一些危险函数使用不当,主要与os、commands以及subprocess三个包有关,在python3中已去除了commands。比如:

import os
import subprocess
import commands

os.system('whoami')
commands.getstatusoutput('whoami')
subprocess.call(['whoami'],shell=True)

任何用户控制的参数进入了上述函数都会造成危险,如果非要使用,正确的方法就是使用subprocess并且设立了参数设置为False。shell=True会将输入字符串解析为shell命令;shell=False时输入必须为list且只会讲将一个元素解析为命令,而后面的元素为命令的参数。如下所示:

>>> import subprocess
>>> subprocess.call('whoami; ls',shell=True)
xman21
=0.1.6  BT  CTF  cuda  DeepLearn  Desktop  Documents  Downloads  examples.desktop  frp  libpcap-1.9.0  Music  Pictures  Public  Templates  Videos  vld
0
>>> subprocess.call('whoami; ls',shell=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.5/subprocess.py", line 557, in call
    with Popen(*popenargs, **kwargs) as p:
  File "/usr/lib/python3.5/subprocess.py", line 947, in __init__
    restore_signals, start_new_session)
  File "/usr/lib/python3.5/subprocess.py", line 1551, in _execute_child
    raise child_exception_type(errno_num, err_msg)
FileNotFoundError: [Errno 2] No such file or directory: 'whoami; ls'
>>> subprocess.call(['whoami; ls'],shell=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.5/subprocess.py", line 557, in call
    with Popen(*popenargs, **kwargs) as p:
  File "/usr/lib/python3.5/subprocess.py", line 947, in __init__
    restore_signals, start_new_session)
  File "/usr/lib/python3.5/subprocess.py", line 1551, in _execute_child
    raise child_exception_type(errno_num, err_msg)
FileNotFoundError: [Errno 2] No such file or directory: 'whoami; ls'
>>> subprocess.call(['whoami','ls'],shell=False)
whoami: extra operand ‘ls’
Try 'whoami --help' for more information.
1

命令执行主要和eval()、exec()以及execfile()函数有关,他们可以执行Python,一旦能够进行代码执行很多时候也就可以进行命令执行了:

>>> exec("print(__import__('os').popen('whoami').read())")
xman21
>>> eval("print(__import__('os').popen('whoami').read())")
xman21

execfile()可以用来执行一个文件,Python3中已经将其去除

>>> execfile('test')
xman21

test文件内容为:

__import__('os').popen('whoami').read()

五、XXE Injection

XML注入是很多语言都存在的问题,Python中一般使用lxml库来解析xml文件,如下所示,xml文件的内容为:

?xml version = "1.0"?>
<!DOCTYPE ANY [
    <!ENTITY f SYSTEM "file:///etc/passwd">
]>
<x>&f;</x>

调用lxml解析:

>>> from lxml import etree
>>> tree = etree.parse('xml')
>>> print(etree.tostring(tree.getroot()))
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
... ...

其中resolve_entities参数默认为True,它会解析外部实体,造成XXE,安全的设置为:etree.parse(xmlSource,etree.XMLParser(resolve_entities=False))

六、SSTI

模板注入最初是从Flask过来的,一位内SSTI一般与jinja2模板引擎有关,Flask默认使用,而Django有自己的模板引擎。所以一般情况下Django不会发生SSTI,这里不详细介绍,给出几个链接:

https://xz.aliyun.com/t/390

https://p0sec.net/index.php/archives/120/

七、Deserialization

Python与序列化数据相关的模块是json、pickle/cPickle(这两者使用规则是一样的,不过cPickle是C写的。速度更快)、yaml、shelve以及marshal。

pickle

pickle模块通常利用reduce函数进行构造,比如下面这个例子

>>> import os
>>> import pickle
>>> class test(object):
...     def __reduce__(self):
...         return (os.system,('whoami',))
... 
>>> a=test()
>>> payload=pickle.dumps(a)
>>> pickle.loads(payload)
xman21

reduce函数是一个关键的魔法函数,它的使用方法如下:

  • 如果返回值是一个字符串,那么将会去当前作用域中查找字符串值对应名字的对象,将其序列化之后返回,例如最后return ‘a’,那么它就会在当前的作用域中寻找名为a的对象然后返回,否则报错。
  • 如果返回值是一个元组,要求是2到5个参数,第一个参数是可调用的对象,第二个是该对象所需的参数元组,剩下三个可选。所以比如最后return (eval,(“os.system(‘ls’)”,)),那么就是执行eval函数,然后元组内的值作为参数,从而达到执行命令或代码的目的,当然也可以return (os.system,(‘ls’,))

yaml

yaml和xml、json等类似,都是标记类语言,有自己的语法格式。各个支持yaml格式的语言都会有自己的实现来进行yaml格式的解析(读取和保存),其中PyYAML就是python的一个yaml库。示例如下:

>>> yaml.load('!!python/object/apply:os.system ["id"]')
uid=1000(xman21) gid=1000(xman21) groups=1000(xman21),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)

而如果使用yaml.safe_load()上面的代码就不会执行

其他的细节可以参考:https://xz.aliyun.com/t/2289https://xz.aliyun.com/t/47

一个有趣的案例:https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.html

八、Clickjacking

点击劫持并不常见,现有的防御方法是在HTTP response header中设置X-Frame-Options,一般来说,它有三种值:

X-Frame-Options: deny
X-Frame-Options: sameorigin
X-Frame-Options: allow-from https://example.com/

deny拒绝一切页面通过<frame><iframe>或者<object>加载;sameorigin允许同源的页面;allow-from允许特定的页面。在Django中只需要在setting中设置django.middleware.clickjacking.XFrameOptionsMiddleware那么所有的响应即可自动添加X-Frame-Options

img

其中默认值为SAMEORIGIN,修改的话也是直接在setting中设置X_FRAME_OPTIONS = ‘DENY’ 或者 X_FRAME_OPTIONS = ‘ALLOW-FROM HTTPS://EXAMPLE.COM/’

九、Upload

Django自带了两个文件上传的属性:FileField以及ImageFeild。它们本身并没有对上传的文件做过多的检测,这部分的安全策略需要开发人员自行考虑。原来的版本中使用ImageField进行图片上传,Django会检查文件内容是否是图片却不会检查后缀名,那就可以上传一个.html文件从而造成一个XSS漏洞,就像这样

img

img

但是在我所使用的这个版本里Django对图片后缀做了检测

img

不过FileField依然存在上述的问题,需要开发人员手动处理,Django给了些建议,比如:

1、限制上传文件大小以防止DOS

2、禁用文件执行权限

3、设置单独的文件服务器

4、设置文件类型白名单

十、Format

格式化字符串漏洞,一个有趣的例子

https://www.leavesongs.com/PENETRATION/python-string-format-vulnerability.html

十一、Directory traversal

危险的代码例如:

open(user_input)
os.fdopen(user_input)

防护思路:

cwd = os.getcwd()
if os.path.abspath(user_input).startswith(cwd) is True:
    open(user_input)

abspath是获取绝对路径,这段代码的意思就是限定用户读取的目录为当前目录。我们可以把os.getcwd()替换成os.path.realpath限定当前目录下的任意子目录。

十二、SECTET_KEY

SECTET_KEY的重要性不言而喻,这个链接中的示例非常形象。

http://xxlegend.com/2015/04/01/%E4%BB%8EDjango%E7%9A%84SECTET_KEY%E5%88%B0%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C/

十三、Additional

1、生成环境下关闭DEBUG

2、修改默认Admin后台链接,或者猥琐点,使用django-admin-honeypot

3、使用manage.py check –deploy检查可能存在安全问题

References

https://landgrey.me/media/gallery/attachment/2017/05/17/Django_%E5%AE%89%E5%85%A8%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.png

https://zhuanlan.zhihu.com/p/26134332