作者:平安科技銀河實驗室

1、漏洞描述

在 Nginx 的 range filter 中存在整數溢出漏洞,可以通過帶有特殊構造的 range 的 HTTP 頭的惡意請求引發這個整數溢出漏洞,并導致信息泄露。

2、影響程度

攻擊成本:低
危害程度:低
影響范圍:Nginx 0.5.6 – 1.13.2

3 、漏洞原理

3.1 HTTP 斷點續傳:Range

HTTP 的 Range 允許客戶端分批次請求資源的一部分,如果服務端資源較大,可以通過 Range 來并發下載;如果訪問資源時網絡中斷,可以斷點續傳。

Range 設置在 HTTP 請求頭中,它是多個 byte-range-spec (或 suffix-byte-range-spec )的集合;

byte-range-set  = ( byte-range-spec | suffix-byte-range-spec )*N
byte-range-spec = first-byte-pos "-" [last-byte-pos]
suffix-byte-range-spec = "-" suffix-length

其中,first-bytes-pos 指定了訪問的第一個字節,last-byte-pos 指定了最后一個字節,suffix-length 則表示要訪問資源的最后 suffix-length 個字節的內容;例如:

Range:bytes=0-1024 表示訪問第 0 到第 1024 字節;

Range:bytes=500-600,601-999,-300 表示分三塊訪問,分別是 500 到 600 字節,601 到 600 字節,最后的 300 字節;

在 Response 頭中設置:

Accept-Ranges:bytes 表示接受部分資源的請求;

Content-Range: bytes START-END/SIZE 表示返回的資源位置;其中 SIZE 等于 Content-Length ;如:Content-Range: bytes 500-600/1000

3.2 Nginx Range Multipart

如果一次請求有多個 range,返回的數據需要 multipart 來組織;格式如下:

HTTP/1.1 206 Partial Content
Date: Wed, 15 Nov 1995 06:25:24 GMT
Last-Modified: Wed, 15 Nov 1995 04:58:08 GMT
Content-type: multipart/byteranges; boundary=THIS_STRING_SEPARATES

--THIS_STRING_SEPARATES
Content-type: application/pdf
Content-range: bytes 500-999/8000

...the first range...
--THIS_STRING_SEPARATES
Content-type: application/pdf
Content-range: bytes 7000-7999/8000

...the second range
--THIS_STRING_SEPARATES--

Nginx 對 Range 的支持包括 header 處理和 body 處理,分別用來解析客戶端發送過來的 Range header 和裁剪返回給客戶端的請求數據 Body。其實現分別由 ngx_http_range_header_filter_modulengx_http_range_body_filter_module 兩個過濾模塊完成。

ngx_http_range_header_filter_module 中調用了 ngx_http_range_header_filter 函數,而該函數進一步調用了 ngx_http_range_parse 函數來解析 header 中的 Range 字段;分別調用 ngx_http_range_singlepart_headerngx_http_range_multipart_header 來生成 single range 和 multi ranges 的 Response Header;

這次的問題就出現在多個 range 時, ngx_http_range_parse 函數對 suffix-length 的處理;

3.3 Nginx Cache

Nginx 可以作為緩存服務器,將 Web 應用服務器返回的內容緩存起來。如果客戶端請求的內容已經被緩存,那么就可以直接將緩存內容返回,而無需再次請求應用服務器。由此,可降低應用服務器的負載,并提高服務的響應性能。

下面是使用 Nginx 作為緩存服務器的一個示例。假設 Nginx 監聽本地80端口,反向代理百度,那么就有如下配置:

此時,我們訪問 http://127.0.0.1 ,即可得到百度的返回:

檢查頁面資源,存在一個靜態圖片文件。由于這類靜態文件一般不會發生變化,我們可以將其緩存起來。

Nginx 配置緩存主要由以下命令完成:

proxy_cache_key 用于區分 cache 文件。

proxy_cache_path 設置 cache 文件的路徑和參數。

  • cache 文件會保存在指定的目錄下面,文件名是 cache key 的 MD5 值
  • 通過 level 參數將 cache 文件分多層目錄保存,以避免某個目錄下存在大量文件造成的性能開銷
  • 通過 keys_zone 參數指定 cache key 在內存中的區域及其大小,1M 的區域大概可以保存 8000 條 key 的信息

proxy_cache_valid 對不同返回狀態值設定 cache 有效時間

例如,下面這條配置:

指定了以下信息:

使用協議、請求方法、域名、URI 作為 cache key

cache文件保存在目錄 /tmp/Nginx/ 下,采取兩層目錄,keys_zone 名稱為 my_zone,大小為 10M

對于返回狀態值為 200 的內容,cache 有效時間為 10 分鐘

現在,我們配置好了名為 my_zone 的 cache,接下來選擇對目錄 www.baidu.com/img/ 下的圖片做緩存。首先,仍然是設置反向代理:

接下來,我們使用下列命令對 img 目錄下的文件進行緩存:

配置命令解釋如下:

proxy_cache 指定使用的 keys_zone 名稱,就是之前的 my_zone

add_header 在 Nginx 返回的 HTTP 頭中,增加一項 X-Proxy-Cache,如果緩存命中其值為 HIT,未命中則為 MISS

proxy_ignore_headers 由于百度對圖片的請求也會 Set-Cookie 設置,而 Nginx 不會緩存帶有 Set-Cookie 的返回,因此我們這里設置忽略該 HTTP 頭

現在,對圖片的緩存配置就完成了,完整的配置內容如下

我們使用 curl 命令進行實驗,訪問 http://127.0.0.1/img/bd_logo1.png 。由于是第一次訪問,可以看到返回內容中 X-Proxy-Cache 的值為 MISS:

再次訪問時,此時緩存命中,X-Proxy-Cache 的值為 HIT 了

那么現在的 Cache 文件是什么樣的呢?我們檢查設置的緩存目錄 /tmp/Nginx,發現存在以下 Cache 文件:

可見,確實使用了2層目錄保存了 Cache 文件。Cache 文件保存了 Nginx 請求得到的返回內容:

可以看到,cache key 的內容保存在了 Cache 文件的頭部,此外還有 Nginx 請求后端返回的 HTTP 頭,如后端(這里是 www.baidu.com )的服務器為 Apache。正常情況下,這些信息是不會返回給客戶端的。而本次的的漏洞,就是由于負數偏移量,導致 Cache 文件的頭部信息也被返回,從而造成信息泄漏。

4 、漏洞原理

首先,我們看這次漏洞修復的 commit:

可以看到,在 ngx_http_range_filter_module.cngx_http_range_parse 函數中做了兩處修復:

  • 進一步檢測了 size 的溢出情況,防止 size 溢出后造成小于 content-length 這條判斷的繞過
  • 則直接限定了使用后綴的情況下,start 不能為負的,最小只能是 0,也就是說使用 “-xxx” 的方式對 Cache 文件的讀取只能從 0 開始到結束。

根據漏洞修復 commit 的注釋,我們知道這次漏洞的主要成因就是 bytes-range 讀取的起始范圍可能為負數,從而讀取緩存文件頭部。

首先,如果傳入完整的 range 參數,如 start-end,則在 ngx_http_range_parse() 中會檢查 start,確保其不會溢出為負值:

因此,如果需要將 start 解析為負數,只能通過 -end 這類后綴型 range 參數實現:

此時的 start 等于 content-length 減去讀入的 end 值,所以如果傳入的 end 比實際長度還要長,就可以使 start 變為負數,而這就是第二處修復所處理的情形:

同時注意到,在這類情況下,最終 end 的值會被設定為 content-length-1。所以這塊 range 的總長度就超過了 content-length。而 Nginx 對 range 總長度會有檢查:

一般來說,range 作為原始文件的一部分,其長度應該是小于 content-length 的。所以一旦計算得到的 size 比 content-length 還大,那就直接將原始文件返回了,不再進行 range 處理。為了繞過這一限制,我們就需要利用到第一處修復所處理的情形。

具體而言,檢查用到的 size 是將 multipart 的全部 range 長度相加得到的:

因此,一個 range 是不夠的,我們至少需要兩個 range,其長度之和溢出為負數,就可以繞過總長度的檢查了。

要得到一個很大長度的 range,同樣可以采用 -end 這種后綴型,將 end 設置為一個非常大的數即可。此處的 start, end, size 均為 64 位有符號整形,所以只需要最終相加得到的 size 為 0x8000000000000000 即可。

5 、漏洞利用

本次復現利用使用 Nginx-1.12.0 作為緩存服務器,緩存配置同上文,訪問的目標文件仍然是http://www.baidu.com/img/bd_logo1.png 。

首先,我們不指定 range,得到該圖片文件的長度為 7877:

設置第一段 range 為 -8500,此時的 start 為 7877-8500=-623,即圖片在 Cache 文件偏移之前的 623 bytes 也會被返回,而這 623 bytes 中就包含了 Cache 文件頭部。

下一步,按照上文所說,第二段 range 的長度需要將總長度溢出。我們的目標總和 size 為 0x8000000000000000,第一段 range 長度為 8500,故第二段 range 長度為 0x8000000000000000-8500=9223372036854767308。

于是,使用 curl 命令,配合 -r 參數指定 bytes range:

可以看到返回內容中,第一段即為 -8500 的 range,而這一段中我們就看到了 Cache 文件頭部,例如 cache key 以及后端服務器返回的 HTTP 頭。

6、漏洞修復

綜合來看,這個漏洞就是整數溢出漏洞的利用,能夠從 Cache 文件中獲取 Cache 頭的信息。在某些配置的情況下 Cache 頭中會存在 IP 地址信息,造成信息泄露。

就 Nginx 模塊以及常用的第三方模塊本身來說,無法通過這個整數溢出來對內存進行操作或者遠程執行。

建議升級到 1.13.3 和 1.12.1 版本;如果不能升級,可以在 Nginx 配置文件中添加 max_ranges 1,從而禁用 multipart range。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/353/