一、WSGI介绍
二、请求走私
三、WSGI server中的走私问题
四、总 结
WSGI是一种规范,描述了web server
如何与web application
通信的规范。
WSGI规范是为python生态定义的,符合WSGI接口的server如下所示:
-
environ
:一个包含所有HTTP
请求信息的dict
对象; -
start_response
:一个发送HTTP
响应的函数。WSGI server负责完成http
请求解析到environ
的映射过程,这样python的Web框架可以专注于业务逻辑,直接使用解析好的http
请求对象。
keep-alive 与 pipeline
为了缓解源站的压力,一般会在用户和后端服务器(源站)之间加设前置服务器,用以缓存、简单校验、负载均衡等,而前置服务器与后端服务器往往是在可靠的网络域中,ip 也是相对固定的,所以可以重用 TCP 连接来减少频繁 TCP 握手带来的开销。这里就用到了 HTTP1.1
中的Keep-Alive
和 Pipeline
特性:
所谓 Keep-Alive,就是在 HTTP 请求中增加一个特殊的请求头 Connection: Keep-Alive,告诉服务器,接收完这次 HTTP 请求后,不要关闭 TCP 链接,后面对相同目标服务器的 HTTP 请求,重用这一个 TCP 链接,这样只需要进行一次 TCP 握手的过程,可以减少服务器的开销,节约资源,还能加快访问速度。这个特性在 HTTP1.1 中是默认开启的。
有了 Keep-Alive 之后,后续就有了 Pipeline,在这里呢,客户端可以像流水线一样发送自己的 HTTP 请求,而不需要等待服务器的响应,服务器那边接收到请求后,需要遵循先入先出机制,将请求和响应严格对应起来,再将响应发送给客户端。现如今,浏览器默认是不启用 Pipeline 的,但是一般的服务器都提供了对 Pipleline 的支持。
http消息处理过程中出现两次http解析就可能出现走私,常见的情景里,容易出现在Content-Length
和Transfer-Encoding
的处理差异中。而WSGI中进行了一次http
请求解析,并且经常置于nginx等中间件后使用,所以也容易出现请求走私问题。
waitress中条件竞争导致的走私问题(CVE-2024-49768)
waitress
是一个流行的纯 Python 实现的 WSGI 服务器,用于在生产环境中部署 Python Web 应用程序。影响版本:>=2.0.0,<3.0.1
当配置文件中channel_request_lookahead
被设置为大于0
时,waitress
中存在http
请求走私漏洞,当攻击者构造特定大小的http
请求时,因为waitress的异步处理机制可导致请求走私。
利用场景
nginx 使用proxy_pass
向后端服务器转发请求,配置如下:
python代码示例如下:
因为nginx只转发/user
对应的请求,正常情况下waitress
应该只能处理针对/user
的请求。但利用下文中提供的Poc,可以观察到日志中将输出Hello, Admin!
,代表可以通过nginx访问到/admin
路由。如果python部分未进行额外的请求校验,将产生请求绕过问题。
漏洞分析
waitress
主要逻辑为:通过socket
读取用户请求,然后交给子线程异步处理。处理socket
时,主流程如下(wasyncore.py->poll
函数):
触发的函数调用顺序为:read->handle_read_event->(HTTPChannel)handle_read->(HTTPChannel)received
,socket
接受的数据最终交给HTTPChannel
中的received
函数处理。同时可以观察到只要channel.readable
为True,就会有新的数据交给received
处理,如果发生request.error
不为空的情况,write函数中会根据channel中的close_when_flushed
关闭HTTPChannel
对象。
在HTTPChannel
中,received
函数负责处理接收到的数据(默认是8192
大小,在adjustments.py
中的recv_bytes = 8192
定义) 同时通过self.request.received(data)
填充request
对象,其返回值代表消耗掉的字节数,当填充出错时,将返回一个比正常值更小的值,同时将request.error
设置为错误原因,然后整个请求被add_task
加入待处理队列,由另一个线程(ThreadedTaskDispatcher
)处理,相关代码如下:
可以看到n
代表消费的字节数,只要data
中还有数据就会重复解析过程,这与pipeline
的行为相符。而self.request.received
处理数据时,只要处理完成都会标记request.completed
为True
,不过对于解析出错的请求,其request.error
属性不为空。
而ThreadedTaskDispatcher
中将异步调用channel
的service
方法,当碰到request.error
不为空的请求时,会将task.close_on_finish
设置为True
以执行以下代码:
代码执行时在设置self.close_when_flushed = True
之前,如果channel_request_lookahead
被设置为大于0
,则:
这时的readable
函数将返回True
, 此时如果handle_read
函数读取了下一个包,因为task.close_on_finish
为True
,self.requests = []
将清空已有的请求队列,handle_read
中刚读取到的bytes
将作为全新的包进行解析(事实上它是上一个包未处理完的部分)。解析后的请求将交由service函数处理,虽然这时self.close_when_flushed = True
,但handle_write
执行前self.connected
仍被设置为True
,所以service
函数仍将处理该请求。
简单总结:在service
执行过程中,self.close_when_flushed = True
之前,handler_read
接收到一个包,然后self.close_when_flushed = True
且self.requests = []
,此时received
函数将handler_read
收到的内容作为新的http
内容处理解析后交给service
异步处理。在handle_write
将self.connected
设置为False
前,service
的异步处理逻辑仍有机会执行传入的http
请求。
PoC构造
首先是一个长度为recv_bytes
(之前提到的8192
)大小的包,其header
中,出现x7f
来使其解析时出错(可根据其解析逻辑选择其它报错逻辑),这样下一个包就有机会被handler_read
处理。第二个包是用来请求走私的报文,两部分一起在nginx中是合法的一个包。在waitress
中,被解析为两个,产生走私问题。因为涉及到条件竞争,使用多线程发包可增大走私成功的几率。
非生产环境server的请求走私问题
python官方库中提供了部分简单的server
实现,如常用的SimpleHttpServer
就有用到。而继承了BaseHTTPRequestHandler
并且没有重写do_GET
方法,或者继承了SimpleHTTPRequestHandler
方法,均会受到请求走私问题的影响。不过官方已经声明不应该在生产环境使用,所以不归类为安全问题。
场景
nginx设置了proxy_set_header Connection ""
(否则默认每个请求结束都会close
),或使用httpd
。这里使用的nginx配置如下:
后端启动的服务为http.server
,高版本中增加了-p
参数可以指定使用的http
协议版本,这里使用的是python3.11
,指定为HTTP/1.1
是为了启用keep-alive
与pipeline
特性。
因为其代码实现中处理GET
请求时没有处理Content-Length
,GET
请求的body
被当做新的请求处理,如果发送以下请求。
可观察到日志如下,收到两个请求:
CGIHTTPRequestHandler
也继承了BaseHTTPRequestHandler
:测试后端执行以下命令:
发送如下请求:
响应如下,可以看到在cgi
模式中,特定的情况下可走私产生RCE效果。
Flask中的修复
部分常用框架如flask也扩展了BaseHTTPRequestHandler
用于开发环境使用,可以看到flask中对于以上提到的走私问题做了修复,将Connection
固定为close
。
gevent中的请求走私问题
其处理未使用的数据时将调用_discard
函数,而如果请求中带有100-continue
请求头,则self.socket
不为None
,未被使用的数据将被作为新的请求包处理,产生走私。
测试服务启动如下,前端为httpd
:
发送如下请求:
日志输出如下:
可以观察到产生了走私,同时可以看到这里的测试服务使用gunicorn
来启动,而gunicorn
是应用最广泛的WSGI服务器,这是因为其gevent_wsgi
模式直接使用了gevent
的对应实现。另外gevent
维护者将此问题归类为非安全问题,因为文档中说明了其WSGI模块是为开发与测试设计的,不过笔者已经提交了对应的补丁,后续版本中将修复此问题。
本文通过几个案例研究了WSGI(Web Server Gateway Interface)中出现的请求走私问题,可以看到传统的Content-Length
与Transfer-Encoding
已经得到了重视,但许多问题仍然源于先前请求的body未被正确丢弃。除了传统的中间件,WSGI服务本身也会出现请求走私问题。鉴于此,类似Python的WSGI实现,以及Perl、Ruby等其他语言的相关实现也值得进一步探索。
1. 浅谈HTTP请求走私
2. Request processing race condition in HTTP pipelining with invalid first request
【版权说明】
本作品著作权归m4yfly所有
未经作者同意,不得转载
m4yfly
天工实验室安全研究员
专注于代码审计、物联网安全。
原文始发于微信公众号(奇安信天工实验室):WSGI中的请求走私问题研究
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论