来源:[同程安全应急响应中心](https://mp.weixin.qq.com/s?__biz=MzI4MzI4MDg1NA==&mid=2247483817&idx=1&sn=5a1fd58b65edf4b88d2f455a486b97bd)
作者:**Nearg1e@YSRC**
国外安全研究员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。文档如下:
```python
print(is_safe_url.__doc__)
Return ``True`` if the url is a safe redirection (i.e. it doesn't point to
a different host and uses a safe scheme).
Always returns ``False`` on an empty url.
If ``require_https`` is ``True``, only 'https' will be considered a valid
scheme, as opposed to 'http' and 'https' with the default, ``False``.
```
让我们来看看常规的几个用法:
```python
from django.utils.http import is_safe_url
In [2]: is_safe_url('http://baidu.com')
Out[2]: False
In [3]: is_safe_url('baidu.com')
Out[3]: True
In [5]: is_safe_url('aaaaa')
Out[5]: True
In [8]: is_safe_url('//blog.neargle.com')
Out[8]: False
In [7]: is_safe_url('http://google.com/adadadadad','blog.neargle.com')
Out[7]: False
In [13]: is_safe_url('http://blog.neargle.com/aaaa/bbb', 'blog.neargle.com')
Out[13]: True
```
可见在没有指定第二个参数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`):
```python
def _is_safe_url(url, host):
if url.startswith('///'):
return False
url_info = urlparse(url)
if not url_info.netloc and url_info.scheme:
return False
if unicodedata.category(url[0])[0] == 'C':
return False
return ((not url_info.netloc or url_info.netloc == host) and
(not url_info.scheme or url_info.scheme in ['http', 'https']))
```
我们来看一下urlparse的常规用法及几种urlparse无法处理的特殊情况。
```python
>>> urlparse('http://blog.neargle.com/2017/01/09/chrome-ext-spider-for-probe/')
ParseResult(scheme='http', netloc='blog.neargle.com', path='/2017/01/09/chrome-ext-spider-for-probe/',
params='', query='', fragment='')
>>> urlparse('ftp:99999999')
ParseResult(scheme='', netloc='', path='ftp:99999999', params='', query='', fragment='')
>>> urlparse('http:99999999')
ParseResult(scheme='http', netloc='', path='99999999', params='', query='', fragment='')
>>> urlparse('https:99999999')
ParseResult(scheme='', netloc='', path='https:99999999', params='', query='', fragment='')
>>> urlparse('javascript:222222')
ParseResult(scheme='', netloc='', path='javascript:222222', params='', query='', fragment='')
>>> urlparse('ftp:aaaaaaa')
ParseResult(scheme='ftp', netloc='', path='aaaaaaa', params='', query='', fragment='')
>>> urlparse('ftp:127.0.0.1')
ParseResult(scheme='ftp', netloc='', path='127.0.0.1', params='', query='', fragment='')
>>> urlparse('ftp:127.0.0.1')
ParseResult(scheme='ftp', netloc='', path='127.0.0.1', params='', query='', fragment='')
```
可以发现当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](https://www.djangoproject.com/weblog/2017/apr/04/security-releases/)中提到的poc:”http:99999999″是无法bypass的,在前面的判断`if not url_info.netloc and url_info.scheme:`都过不了。)例如下面几种情况:
```
>>> is_safe_url('http:555555555')
False
>>> is_safe_url('ftp:23333333333')
True
>>> is_safe_url('https:2333333333')
True
```
#### 使用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`(转换器:http://www.ipaddressguide.com/ip ),而'http:2130706433'是在浏览器上是可以访问到对应的ip及服务的,即`'http:2130706433 = http://127.0.0.1/'`。
这里我们选用 `https:1029415385` 作为poc,这是一个google的ip,这个url可以 `bypassis_safe_url` 并跳转到google.com。
![](https://images.seebug.org/content/images/2017/04/92B82897-B6E2-47B2-9CA4-0739D87BD002.png)
#### 漏洞验证与影响
我们来写一个简单的环境:
```python
from 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('/')
```
然后访问: `http://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,退出登录时同样使用到了该函数。
```python
def _get_login_redirect_url(request, redirect_to):
# Ensure the user-originating redirection URL is safe.
if not is_safe_url(url=redirect_to, host=request.get_host()):
return resolve_url(settings.LOGIN_REDIRECT_URL)
return redirect_to
@never_cache
def login(request, template_name='registration/login.html',
redirect_field_name=REDIRECT_FIELD_NAME,
authentication_form=AuthenticationForm,
extra_context=None, redirect_authenticated_user=False):
......
return HttpResponseRedirect(_get_login_redirect_url(request, redirect_to))
......
```
![](https://images.seebug.org/content/images/2017/04/92B82897-B6E2-47B2-9CA4-0739D87BD002-1.png)
#### 修复
django修复了代码,自己重构了一下urlparse函数,修复了urlparse函数的这个漏洞。
```python
# Copied from urllib.parse.urlparse() but uses fixed urlsplit() function.
def _urlparse(url, scheme='', allow_fragments=True):
"""Parse a URL into 6 components:
<scheme>://<netloc>/<path>;<params>?<query>#<fragment>
Return a 6-tuple: (scheme, netloc, path, params, query, fragment).
Note that we don't break the components up in smaller bits
(e.g. netloc is a single string) and we don't expand % escapes."""
url, scheme, _coerce_result = _coerce_args(url, scheme)
splitresult = _urlsplit(url, scheme, allow_fragments)
scheme, netloc, url, query, fragment = splitresult
if scheme in uses_params and ';' in url:
url, params = _splitparams(url)
else:
params = ''
result = ParseResult(scheme, netloc, url, params, query, fragment)
return _coerce_result(result)
```
#### 关于官方提到的 possible XSS attack
django官方News&Event中提到的这个漏洞可能会产生XSS,我认为除非程序员把接受跳转的url插入的到`<script type="text/javascript" src="{{ url }}"></script>`等特殊情况之外,直接使用产生XSS的场景还是比较少的。如果你想到了其他的场景还请赐教,祝好。
暂无评论