今天看到两个头注入,一个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/6d7e13da0f998820800ecb3fe9ccee4189aefb54 和 https://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头的时候没有处理换行,导致了头注入。
写一个小的例子
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://cloud.tencent.com/developer/article/1717409 )里进行了介绍,本文不再赘述.
但实际测试的过程中遇到了一个有趣的问题,看看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
法2:使用CSP禁止iframe的跳转
其中的法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这个头注入和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下仍然能够直接触发:
而chrome最新版依旧无法触发,这次是为什么呢?
如上图,我估计是这个Content-Length: 0
,导致Chrome认为这个返回包没有Body,所以并没有解析。
又是一个难题,设置chunk也没有解决,明天再看看吧。
今天在两个Linux上搭了同样的环境,却发现Content-Length
的位置其实不是固定的,有时候会在下面:
但有时又会在上面,和系统是没有关系的。
这个情况下,Chrome是可以触发的:
再深入分析一下,我注入一个Content-Length头进去,你就会发现,Chrome会根据这个头的数值来截取body,如果我注入Content-Length: 5
,此时显示的body如下:
这也就是昨天为什么Chrome下总是触发不了的原因,因为昨天Content-Length头我们无法控制,其值总是为0,导致Chrome不会输出任何内容,也就无法进行XSS。