漏洞描述

setting中配置了django.middleware.common.CommonMiddlewareAPPEND_SLASH=True时会触发URL跳转漏洞,而这两个配置默认存在,且APPEND_SLASH不需显式写在setting配置文件。CommonMiddlewareDjango的一个通用中间件,其实质是一个位于site-packages/django/middleware/common.py的类,用于执行一些HTTP请求的基础操作:

- Forbid access to User-Agents in settings.DISALLOWED_USER_AGENTS

- URL rewriting: Based on the APPEND_SLASH and PREPEND_WWW settings,
append missing slashes and/or prepends missing "www."s.

  - If APPEND_SLASH is set and the initial URL doesn't end with a
    slash, and it is not found in urlpatterns, form a new URL by
    appending a slash at the end. If this new URL is found in
    urlpatterns, return an HTTP redirect to this new URL; otherwise
    process the initial URL as usual.

This behavior can be customized by subclassing CommonMiddleware and
overriding the response_redirect_class attribute.

- ETags: If the USE_ETAGS setting is set, ETags will be calculated from
the entire page content and Not Modified responses will be returned
appropriately. USE_ETAGS is deprecated in favor of
ConditionalGetMiddleware.

漏洞成因与URL rewriting有关,若设置了APPEND_SLASH=True且初始URL未以斜杠结尾,在urlpatterns中无法找到,则能够在末尾添加斜杠形成新的URL。若在urlpatterns中找到了新的URL,则会将HTTP重定向到新URL

举例:

# 页面未以/结尾
http:<ip>:8000

# 构造新URL,页面会重定向至新URL
http:<ip>:8000//baidu.com

当发出类似上文的请求时,程序会进行设定好的跳转,首先执行process_request()函数,进入get_full_path_with_slash()函数:

# Check if a slash should be appended
        if self.should_redirect_with_slash(request):
            path = self.get_full_path_with_slash(request)
        else:
            path = request.get_full_path()

get_full_path_with_slash()函数,给path末尾加斜杠:

def get_full_path_with_slash(self, request):
        """
        Return the full path of the request with a trailing slash appended.
        Raise a RuntimeError if settings.DEBUG is True and request.method is
        POST, PUT, or PATCH.
        """
        new_path = request.get_full_path(force_append_slash=True)
        if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'):
            raise RuntimeError(
                "You called this URL via %(method)s, but the URL doesn't end "
                "in a slash and you have APPEND_SLASH set. Django can't "
                "redirect to the slash URL while maintaining %(method)s data. "
                "Change your form to point to %(url)s (note the trailing "
                "slash), or set APPEND_SLASH=False in your Django settings." % {
                    'method': request.method,
                    'url': request.get_host() + new_path,
                }
            )
        return new_path

返回的new_path就是之前构造的//baidu.com,然后进入HTTP跳转的HttpResponseRedirectBase基类:

# Return a redirect if necessary
        if redirect_url or path != request.get_full_path():
            redirect_url += path
            return self.response_redirect_class(redirect_url)

虽然类的初始化函数有对协议进行检查,但schema不存在,所以会跳过判断:

class HttpResponseRedirectBase(HttpResponse):
    allowed_schemes = ['http', 'https', 'ftp']

    def __init__(self, redirect_to, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self['Location'] = iri_to_uri(redirect_to)
        parsed = urlparse(str(redirect_to))
        if parsed.scheme and parsed.scheme not in self.allowed_schemes:
            raise DisallowedRedirect("Unsafe redirect to URL with protocol '%s'" % parsed.scheme)

之所以使用双斜杠是为了告知浏览器这是一个绝对路径,否则浏览器会跳转到http://<ip>:8000/baidu.com

影响版本

1.11.0<Django<1.11.5

2.0.0<Django<2.0.8

漏洞复现

访问http://<ip>:8000,页面回显:

Hello, world.

访问http://<ip>:8000//baidu.com,页面跳转到https://www.baidu.com/

文章许可:本文采用CC BY-NC-SA 4.0许可协议,转载请注明出处。