Django的两则url跳转漏洞:CVE-2017-7233和CVE-2017-7234分析
Django官方News&Event在4月4日发布了一个安全更新,修复了两个URL跳转的漏洞,一个是urlparse的锅,另一个来自国内的安全研究员 [email protected]长亭,都非常漂亮。因为有复现Django漏洞的习惯,晚上抽了点时间复现了一下。有趣的点还挺多。把两个漏洞的分析整合在一起,凑了篇文章。(还是研究漏洞有趣啊,泪流满面QAQ)
CVE-2017-7233分析
国外安全研究员roks0n提供给Django官方的一个漏洞。
关于is_safe_url函数
Django自带一个函数:django.utils.http.is_safe_url(url, host=None, allowed_hosts=None, require_https=False)
,用于过滤需要进行跳转的url。如果url安全则返回ture,不安全则返回false。文档如下:
1 | print(is_safe_url.__doc__) |
让我们来看看常规的几个用法:
1 | from django.utils.http import is_safe_url |
可见在没有指定第二个参数host的情况下,url如果非相对路径,即HttpResponseRedirect
函数会跳往别的站点的情况,is_safe_url
就判断其为不安全的url,如果指定了host为blog.neargle.com
,则is_safe_url
会判断url是否属于’blog.neargle.com’,如果url是’blog.neargle.com’或相对路径的url,则判断其url是安全的。
urllib.parse.urlparse的特殊情况
问题就出在该函数对域名和方法的判断,是基于urllib.parse.urlparse
的,源码如下(django/utils/http.py):
1 | def _is_safe_url(url, host): |
我们来看一下urlparse的常规用法及几种urlparse无法处理的特殊情况。
1 | 'https://blog.neargle.com/2017/01/09/chrome-ext-spider-for-probe/') urlparse( |
可以发现当scheme不等于http,且path为纯数字的时候,urlparse处理例如aaaa:2222222223
的情况是不能正常分割开的,会全部归为path。这时url_info.netloc == url_info.scheme == ""
,则((not url_info.netloc or url_info.netloc == host) and (not url_info.scheme or url_info.scheme in ['http', 'https']))
为true。(这里顺便提一下,django官方News&Event中提到的poc:”http:99999999”是无法bypass的,在前面的判断if not url_info.netloc and url_info.scheme:
都过不了。)例如下面几种情况:
1 | 'http:555555555') is_safe_url( |
使用IP Decimal Bypass is_safe_url
但是既然是url跳转漏洞,我们就需要让其跳转到指定的url里,https:2333333333
这样的url明显是无法访问的,而冒号之后必须纯数字,http:127.0.0.1
是无法pypass的。有什么方法呢?其实ip不仅只有常见的点分十进制表示法,纯十进制数字也可以表示一个ip地址,浏览器也同样支持。例如: 127.0.0.1 == 2130706433
, 8.8.8.8 == 134744072
(转换器:https://www.ipaddressguide.com/ip),而'http:2130706433'是在浏览器上是可以访问到对应的ip及服务的,即'http:2130706433 = https://127.0.0.1/'。
这里我们选用https:1029415385
作为poc,这是一个google的ip,这个url可以bypassis_safe_url
并跳转到google.com。
漏洞验证与影响
我们来写一个简单的环境:1
2
3
4
5
6
7
8
9from django.http import HttpResponseRedirect
from django.utils.http import is_safe_url
def BypassIsUrlSafeCheck(request):
url = request.GET.get("url", '')
if is_safe_url(url, host="blog.neargle.com"):
return HttpResponseRedirect(url)
else:
return HttpResponseRedirect('/')
然后访问:https://127.0.0.1:8000/bypassIsUrlSafeCheck?url=https:1029415385
, 如图,url被重定向到了google.com。
并非只有开发者自己使用is_safe_url
会受到影响,Django默认自带的admin也使用了这个函数来处理next GET | POST参数,当用户访问/admin/login/?next=https:1029415385
进行登录时,登录后同样会跳转到google.com,退出登录时同样使用到了该函数。
1 | def _get_login_redirect_url(request, redirect_to): |
修复
django修复了代码,自己重构了一下urlparse
函数,修复了urlparse
函数的这个漏洞。
1 | def _urlparse(url, scheme='', allow_fragments=True): |
关于官方提到的 possible XSS attack
django官方News&Event中提到的这个漏洞可能会产生XSS,我认为除非程序员把接受跳转的url插入的到<script type="text/javascript" src=""></script>
等特殊情况之外,直接使用产生XSS的场景还是比较少的。如果你想到了其他的场景还请赐教,祝好。
CVE-2017-7234 分析
漏洞详情
来自 @Phithon 的一个漏洞。
问题出现在:django.views.static.serve()
函数上。该函数可以用来指定web站点的静态文件目录。如:
1 | urlpatterns = [ |
这样django项目根目录下staticpath中的所有文件,就可以在staticp/目录中访问。e.g. https://127.0.0.1:8000/staticp/test.css
这种方法是不被django官方推荐在生成环境使用的,对安全性和性能都有一定影响。
问题代码如下 (django/views/static.py):
1 | path = posixpath.normpath(unquote(path)) |
path既我们传入的路径,如果传入的路径为staticp/path.css
,则path=path.css
。跟踪代码可知,path经过了unquote进行url解码,后来又replace('\\', '/')
,进入HttpResponseRedirect,很诡异的逻辑看起来很有问题。一般遇到这类型的函数我们会先试着找看看,任意文件读漏洞,但是这个对'.'
和'..'
进行了过滤,所以这边这个HttpResponseRedirect函数就成了帅的人的目标。
我们的最终目的是HttpResponseRedirect('//evil.neargle.com')
或者HttpResponseRedirect('https://evil.neargle.com')
,那么就要使path != newpath
,那么path里面就必须带有’\‘,好的现在的我们传入'/staticp/%5C%5Cblog.neargle.com'
,则path='\\\\blog.neargle.com';newpath='//blog.neargle.com'
,HttpResponseRedirect就会跳转到'blog.neargle.com'
造成跳转漏洞。
修复
嗯,官方表示自己也不知道为什么要写这串代码,删了这一串代码然后用safe_url函数代替。
ps.
浏览器不仅仅支持十进制来代替点分十进制的IP,也可以使用十六进制和8进制来代替。https://点分十进制 == https://十进制 == https://0x十六进制 == https://0八进制
(例如:https://127.0.0.1 == https://2130706433 == https://0x7F000001 == https://017700000001
),十六进制非纯数字所以不可用来bypass urlparse,但是八进制还是可以的。
urls
- https://github.com/django/django/commit/5ea48a70afac5e5684b504f09286e7defdd1a81a
- https://www.djangoproject.com/weblog/2017/apr/04/security-releases/
- https://docs.python.org/3/library/urllib.parse.html
本博客所有内容只用于安全研究,请勿用于恶意攻击。
本文URL: "https://blog.neargle.com/2017/04/12/Django-CVE-2017-7233-bypass-is-safe-url-and-CVE-2017-7233-serve-open-url/index.html"