python2.x 3.x 的urllib/urllib2从数据解析到发包的整个流程中,均未对URL提供安全性过滤或检查.导致换行符可被插入到HTTP数据流,使攻击者可以注入额外的HTTP头和请求方法.
本文将重点介绍: HTTP 请求头基础 Python urllib/urllib2 核心代码逻辑 漏洞验证过程及代码分析 1 位置Python 2.x urllib2.py httplib.pyPython 3.x urllib.py http/client.py
2 HTTP请求头&换行符为什么注入换行符就可以修改HTTP的请求头呢.
抓个GET请求看下
![[CVE-2016-5699] Python HTTP header injection in urllib/urllib2](http://www.codesec.net/app_attach/201606/27/20160627_422_434849_0.png!web)
图片中可以看到在HTTP报文是连续且可读的,每行以 \r\n 结束,分别对应 %0d , %0a .
2.1 使用nc敲一个请求nc www.cdxy.me 80
然后输入以下两行:
GET HTTP/1.1Host: www.cdxy.me
就可以看到服务器解析这个GET请求并返回的结果:
![[CVE-2016-5699] Python HTTP header injection in urllib/urllib2](http://www.codesec.net/app_attach/201606/27/20160627_422_434849_1.png!web)
3 漏洞PoC 3.1 脚本 #!/usr/bin/env python3
# test.py
import sys
import urllib
import urllib.error
import urllib.request
url = sys.argv[1]
try:
info = urllib.request.urlopen(url).info()
print(info)
except urllib.error.URLError as e:
print(e) 3.2 正常的请求头
nc -l -v 127.0.0.1 -p 12345 监听本机12345端口
python3 test.py http://127.0.0.1:12345/index.html 发出请求
![[CVE-2016-5699] Python HTTP header injection in urllib/urllib2](http://www.codesec.net/app_attach/201606/27/20160627_422_434849_2.png!web)
左边可以看到本机12345端口收到的请求头,也就是我们Python脚本发出的请求头.
3.3 注入换行符后使用恶意构造的URL:
http://127.0.0.1%0d%0aX-injected:%20header%0d%0ax-leftover:%20:12345/foo![[CVE-2016-5699] Python HTTP header injection in urllib/urllib2](http://www.codesec.net/app_attach/201606/27/20160627_422_434849_3.png!web)
可以看到我们在URL构造的 %0d%0a 起了作用,成功插入两个自定义字段.
针对域名也是可以的,需要在域名后插一个 %00 ,下图为 localhost
![[CVE-2016-5699] Python HTTP header injection in urllib/urllib2](http://www.codesec.net/app_attach/201606/27/20160627_422_434849_4.png!web)
4 show me the code 4.1 urllib/request.py
urllib/request.py - line 138 - def urlopen()
urlopen()办了三件事:
实例化OpenerDirector 对象 (选择功能) 实例化Request对象 (添加数据) 发起请求 (执行) 4.1.1 实例化OpenerDirector对象urlopen()先通过build_opener()建立一个OpenerDirector 对象,该对象中可以添加不同的handler
elif _opener is None:_opener = opener = build_opener()
![[CVE-2016-5699] Python HTTP header injection in urllib/urllib2](http://www.codesec.net/app_attach/201606/27/20160627_422_434849_5.png!web)
以下四个重要的变量均通过key-value的形式,将多种handler内部的方法根据功能归类,方便后续调用.
* handle_error 处理连接状态码
* handle_open 发起连接
* process_response 处理响应
* process_request 处理请求
4.1.2 实例化Request对象urlopen() 通过 return 进入 open 函数:
return opener.open(url, data, timeout)open 函数:实例化 Request 对象为 req .
if isinstance(fullurl, str):req = Request(fullurl, data)
其中调用 urlparse.py 对原始URL进行解析.这一步之后,我们构造的恶意代码被带入 req.host 字段,如下图:
![[CVE-2016-5699] Python HTTP header injection in urllib/urllib2](http://www.codesec.net/app_attach/201606/27/20160627_422_434849_6.png!web)
4.1.3 发起请求
需要清楚的一点是
urllib包和urllib2是基于httplib之上,提供更高层次的抽象.
也就是说,它们发送数据的核心代码,仍要经过httplib之手.
整理一下代码:
urlopen() 在实例化了 OpenerDirector 对象.将该实例传入 opener.open() .
opener.open() 在实例化了Request对象之后,将该实例传入self._open()函数,以执行"发起连接"的动作.
在这里 req 被带入opener.open()的子函数: _open()
response = self._open(req, data)接着, _open() 函数调用 _call_chain() 方法,从 OpenerDirector 对象的 handle_open key-value中加载指定的handler方法.
def _open(self, req, data=None):result = self._call_chain(self.handle_open, 'default',
'default_open', req)
if result:
return result
protocol = req.type
result = self._call_chain(self.handle_open, protocol, protocol +
'_open', req)
if result:
return result
return self._call_chain(self.handle_open, 'unknown',
'unknown_open', req)
这里要发送请求,被加载的handler是 HTTPHandler ,代码如下:
class HTTPHandler(AbstractHTTPHandler):def http_open(self, req):
return self.do_open(http.client.HTTPConnection, req)
http_request = AbstractHTTPHandler.do_request_
可以看到, req (携带恶意代码的 Request 实例)已经通过 HTTPHandler 进入了 http.client.HTTPConnection 函数.
这个函数在 http/client.py ,也就是Python2.x之中的 httplib.py 4.2 httplib/http.clienthttplib/http.client对请求头的处理函数如下(漏洞修复之前的代码):
def putheader(self, header, *values):values = list(values)
for i, one_value in enumerate(values):
if hasattr(one_value, 'encode'):
values[i] = one_value.encode('latin-1')
elif isinstance(one_value, int):
values[i] = str(one_value).encode('ascii')
value = b'\r\n\t'.join(values)
header = header + b': ' + value
self._output(header)
代码一目了然,这个函数将所有的header连接成一个长字符串,不同的header之间以 \r\n\t 进行连接.
当然,我们的恶意代码也在其中,现在它返回的header字符串是这样的:
b'GET /foo HTTP/1.1\r\nAccept-Encoding: identity\r\nConnection: close\r\nUser-Agent: Python-urllib/3.4\r\nHost: 127.0.0.1\r\nX-injected: header\r\nx-leftover: :12345\r\n\r\n'前文已经叙述HTTP请求头相关的基本知识,nc收获的请求头如下:
GET /foo HTTP/1.1Accept-Encoding: identity
Connection: close
User-Agent: Python-urllib/3.4
Host: 127.0.0.1
X-injected: header
x-leftover: :12345
本漏洞的原理介绍到此结束
5 官方修复方案官方针对Python2/Python3更新的两个版本代码,均在出口处的 putheader() 函数里添加了一个检验,发现不合法URL会报一个error.
3.4 / 3.5 : revision 94952
2.7 : revision 94951
6 参考关于漏洞危害可参考以下链接
http://drops.wooyun.org/papers/16905 http://blog.blindspotsecurity.com/2016/06/advisory-http-header-injection-in.html