Skip to content

[Security] 基於 JavaScript 的攻擊手法:Prototype Pollution 初探

· 20 min

前言#

承接上篇 [Security] 最新 XSS 防禦 Trusted Types & Sanitizer API,以及 XSS 防禦繞過手法,此篇主要敘述的是《Beyond XSS:探索網頁前端資安宇宙》 3–1 章節的筆記,若有錯誤歡迎大家回覆告訴我~
(本來想要一次讀書會進度寫一篇文章,但這次 3–1 內容有點多只好拆開…)

Web Application Firewall(WAF):防護 XSS 手段之一#

在前面幾篇文章中,我們都在探討 XSS,且範例程式碼都是以「能注入 HTML」為前提,再想辦法轉為 XSS 來達成攻擊,也提了許多實際 XSS 案例,這乍看之下好像很容易就能做到 XSS 攻擊,但其實並不容易,因為 Web Application Firewall(WAF)可作為 XSS 的防線之一。

Web Application Firewall(WAF)是應用程式專用的防火牆,它會利用一些規則來阻擋「看似不安全」的 payload。舉例來說,Medium 就有用 Cloudflare 的 WAF,訪問 https://medium.com/?a=%3Cscript%3E 時會被阻擋如下:

知名 WAF 例如 ModSecurity,提供基礎建設,可自己撰寫阻擋規則,也可用他人寫的。如果不知如何寫阻擋規則,可參考開源規則集合 OWASP ModSecurity Core Rule Set (CRS),在這些阻擋規則中,有的規則會以 Regex <script[^>]*>[\s\S]*? 找出含有 <script 的程式碼並阻擋,讓 <script>alert(1)</script> 無法被執行。因此即使漏洞存在,若有 WAF 阻擋,攻擊者依然無法達成攻擊。

然而,並不是要直接執行程式碼才能達成攻擊,實際上還有很多「間接攻擊」方式,接下來會逐步從「間接影響 JavaScript 執行」到「不需要 JavaScript」,再到「不只 JavaScript,連 CSS 都不需要」,逐漸探索前端攻擊極限。而首先要介紹的是 Prototype Pollution 的攻擊方式,Prototype Pollution 可間接影響程式執行的流程,若搭配其他現有程式碼,也能達到具破壞力的攻擊效果。

JavaScript 原型鏈#

介紹 Prototype Pollution 之前,先來看看什麼是 JavaScript 的 prototype (原型繼承)。以以下範例來說,當我們在用字串內建函式時,這些函式從何而來? 又為何兩個字串的 toUpperCase 方法是同一個 function?

var str = "hello";
var str2 = "world";
// toUpperCase 從何而來?
console.log(str.toUpperCase()); // "HELLO"
console.log(str2.toUpperCase()); // "WORLD"
// 確認兩個字串的 toUpperCase 是同一個 function
console.log(str.toUpperCase === str2.toUpperCase); // true

toUpperCase 在 MDN 文件上被稱為 String.prototype.toUpperCase,而非 toUpperCase,又是為什麼呢?

這些都和 prototype 有關,當我們呼叫 str.toUpperCase 時,不是 str instance 上真的有 toUpperCase方法,而是 JavaScript 引擎透過 prototype chain(原型鏈)找到的方法。

prototype chain 的概念和 scope chain 類似,當 JavaScript 引擎在 str 本身找不到該屬性或方法時, JavaScript 引擎就會沿著 JavaScript 隱藏屬性 __proto__ 逐步往上找,直到頂端才停下(沿__proto__逐步往上找的最頂端是 null)。而 str.__proto__ 指向的就是 String.prototype

var str = ""
console.log(str.__proto__) // String.prototype

因此 String.prototype.toUpperCase 才是 toUpperCase function 全名,toUpperCase是存在String.prototype 物件上的方法,當我們呼叫 str.toUpperCase 時,JavaScript 引擎在 str 上找不到 toUpperCase 函式,所以到 str.__proto__ 找(也就是到 String.prototype 上找),找到 String.prototype.toUpperCase並呼叫它。示意圖如下。

物件也和字串同理,如下範例,obj 是空物件,但還是找得到 toString,因為 JavaScript 引擎在 obj 找不到 toString時會去 obj.__proto__ 找,obj.__proto__ 指向的是 Object.prototype,而 Object.prototypetoString方法,因此最後找到的是 Object.prototype.toString

var obj = {}
console.log(obj.a) // undefined
console.log(obj.toString) // ƒ toString() { [native code] }

補充:
scope chain:如果我們呼叫某變數,JavaScript 引擎在 local scope 找不到,就會向上一層 scope 找,直到找到 global scope 為止。JavaScript 引擎沿著 scope 這條鏈往上找,直到頂端才停下,因此稱為 scope chain。

改變預設 prototype 上的屬性#

型別各自的__proto__已預設好關聯,以讓這些類別的東西可共用同一 function:

透過 prototype,可讓每個字串都呼叫到同函式、共用同函式,節省記憶體空間。而當字串都呼叫同一個 prototype 函式,函式要如何區分是不同的字串在呼叫呢?JavaScript 引擎會用 this來區分。

String.prototype.first = function() {
return this[0]
}
console.log("".first()) // undefined
console.log("abc".first()) // a

執行 "".first() 時,JavaScript 引擎沿__proto__找到String.prototype,呼叫到 String.prototype.first"".first()first 拿到的 this"";而"abc".first()first 拿到的 this"abc"

上面的 String.prototype.first 就是直接修改 String 原型並加上新方法,所有字串皆可共用新方法,然而這種修改原型的做法是不推薦的,「Don’t modify objects you don’t own」。過去曾因 MooTools 做了類似修改 prototype 的事情,而導致 Array 的 method 要從 flatten 改名成 flat,可參考 Don’t break the Web:以 SmooshGate 以及 keygen 為例

String.prototype.first 是在修改 String 原型,同理 Object.prototype 也可修改,範例如下。

Object.prototype.a = 123
var obj = {}
console.log(obj.a) // 在 obj 找不到 a 屬性,到 obj.__proto__(也就是 Object.prototype)找,拿到 a 的值 123

當程式有漏洞,攻擊者可拿去改變原型鏈上的屬性,就稱為 prototype pollution。稱為 pollution (汙染)是因為Object.prototype.a = 123 就是在「污染」物件原型上的 a 屬性,導致後續存取物件時可能有意想不到的行為。不過除了污染,還須找到可影響的地方,加在一起才能形成完整攻擊。

污染了屬性,然後呢?#

汙染屬性後可以達到什麼效果?假設網站有搜尋的功能,會從 query string 拿 q 的值放到畫面。

var qs = new URLSearchParams(location.search.slice(1))
// 放上畫面,為避免 XSS 用 innerText
document.body.appendChild(createElement({
tag: 'h2',
innerText: `Search result for ${qs.get('q')}`
}))
function createElement(config){
const element = document.createElement(config.tag)
if (config.innerHTML) {
element.innerHTML = config.innerHTML
} else {
element.innerText = config.innerText
}
return element
}

程式碼乍看沒問題,但如果在執行這段程式碼之前有 prototype pollution 漏洞…

// 先假設有 prototype pollution 漏洞,可污染原型屬性
Object.prototype.innerHTML = '<img src=x onerror=alert(1)>'
// 底下都跟剛剛一樣
// 略...
function createElement(config){
const element = document.createElement(config.tag)
// 因為原型鏈被污染,if(config.innerHTML) 結果會是 true
if (config.innerHTML) {
element.innerHTML = config.innerHTML
} else {
element.innerText = config.innerText
}
return element
}

因為污染了物件原型 innerHTML 屬性,導致 if (config.innerHTML) { 判斷成立,攻擊就可用 query string 放入惡意 payload 然後被拿去 innerHTML 執行,達成 XSS。

Prototype pollution 是怎麼發生的?#

什麼程式碼、什麼情況下會有漏洞,能讓攻擊者改變原型鏈上的屬性呢?

1. 解析 query string

有些函式庫在解析 query string 時,支援陣列或物件格式。例如 ?a=1&a=2 或 ?b[]=1&b[]=2 可能被解析為陣列,?c[b][a]=1 會被解析為 {c: {b: {a: 1}}} 物件。像 qs 套件就有支援物件的解析。

來看看解析 query string 並支援物件格式的簡單版程式碼:

function parseQs(qs) {
let result = {}
let arr = qs.split('&') // 將 query string 用 `&` 分割成每個 key-value 項目
for(let item of arr) {
let [key, value] = item.split('=') // 遍歷分割後的 key-value 項目
if (!key.endsWith(']')) { // 判斷 key 是否以 `]` 結尾,來區分一般的 key-value 和物件結構
result[key] = value
continue
}
// 針對物件
let items = key.split('[')
let obj = result
for(let i = 0; i < items.length; i++) {
let objKey = items[i].replace(/]$/g, '')
if (i === items.length - 1) { // 若是最內層,則直接設定 obj[objKey] = value
obj[objKey] = value
} else {
if (typeof obj[objKey] !== 'object') { // 若不是最內層,檢查 obj[objKey] 是否為物件
obj[objKey] = {} // 若非物件,則初始化為 {},準備進一步嵌套
}
obj = obj[objKey] // 將 obj 指向 obj[objKey],這樣下一層迴圈會繼續在該層建立嵌套物件
}
}
}
return result
}
var qs = parseQs('test=1&a[b][c]=2')
console.log(qs)
// { test: '1', a: { b: { c: '2' } } }

以上程式碼乍看沒問題,但如果 query string 長這樣…?

var qs = parseQs('__proto__[a]=3')
console.log(qs) // {}
var obj = {}
// 空物件的 a 屬性被污染
console.log(obj.a) // 3

可看到parseQs 改變了 obj.__proto__.a 的值,造成 prototype pollution 的發生。

有些解析 query string 的 library 曾遇過類似 prototype pollution 問題,如:jquery-deparambackbone-query-parameters

2. 合併物件

合併物件的簡單版程式碼如下:

function merge(a, b) {
for(let prop in b) { // 遍歷 b 中所有屬性
if (typeof a[prop] === 'object') { // 如果 a 中有跟 b 同屬性的 prop 且 a 的該屬性值是物件,就遞歸合併
merge(a[prop], b[prop])
} else {
a[prop] = b[prop] // 如果 a[prop] 不是物件,或者屬性在 a 中不存在,則直接將 b[prop] 賦值給 a[prop]
}
}
}
var config = {
a: 1,
b: {
c: 2
}
}
var customConfig = {
b: {
d: 3
}
}
merge(config, customConfig)
console.log(config)
// { a: 1, b: { c: 2, d: 3 } }

問題在於,如果 customConfig 可以控制如下,物件的原型就被汙染了:

var config = {
a: 1,
b: {
c: 2
}
}
var customConfig = JSON.parse('{"__proto__": {"a": 1}}')
merge(config, customConfig)
var obj = {}
console.log(obj.a) // 1

在這裡用 JSON.parse 建立物件的原因是為了讓 __proto__ 作為普通屬性,且能被 for...in 遍歷。若用物件字面值(object literal)指定 __proto__ 屬性,customConfig 只會是一個空物件,__proto__ 會被視為在設定物件原型,因此在 merge 的 for...in__proto__ 不會被遍歷:

var customConfig = {
__proto__: {
a: 1
}
}
console.log(customConfig); // {}

JSON.parse 才能製造「key 是 __proto__」的物件,__proto__ 被視為一般屬性,可以被 for...in 遍歷:

var obj1 = {
__proto__: {
a: 1
}
}
var obj2 = JSON.parse('{"__proto__": {"a": 1}}')
console.log(obj1) // {}
console.log(obj2) // { __proto__: { a: 1 } }

有些實作 merge 的 library 曾有上述此類漏洞,例如:mergelodash.merge

Prototype pollution script gadgets#

在汙染原型屬性後,還需要找到能影響的地方,才能達到攻擊。而那些「只要污染了 prototype,就可以拿來利用的程式碼」稱為 script gadget,在 Client-Side Prototype Pollution 有蒐集各類的 gadget。

舉一個 gadget 範例如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/vue@2.7.14/dist/vue.js"></script>
</head>
<body>
<div id="app">
{{ message }}
</div>
<script>
// 污染 template
Object.prototype.template = '<svg onload=alert(1)></svg>';
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
});
</script>
</body>
</html>

在污染 Object.prototype.template 後,就達成了 XSS,因為在 Vue 中,template 是一個特殊屬性,用來定義元件的 HTML 結構,Vue 會嘗試從元件或實例中獲取 template 屬性,這template 可能來自於實例或元件上直接定義的 template 屬性,若沒有找到,Vue 可能會透過 JavaScript 原型鏈嘗試讀取,因此讀取到被汙染的Object.prototype.template

另一個 server side 的 gadget 範例則是:

const child_process = require('child_process')
const params = ['123']
// child_process.spawnSync(command, args) 用於同步執行命令
// command 是要執行的命令(如 'echo')
// args 是命令的參數陣列
const result = child_process.spawnSync(
'echo', params
);
console.log(result.stdout.toString()) // 123

上述程式碼中,echo 只是將參數內容輸出,不解析特殊字元,不會觸發 Command Injection,例如以下,特殊字元只是單純的字串輸入值:

const child_process = require('child_process')
const params = ['123 && ls']
const result = child_process.spawnSync(
'echo', params // echo "123 && ls"
);
console.log(result.stdout.toString()) // 123 && ls

然而,若有 prototype pollution 漏洞,就可變 RCE(Remote code execution):

const child_process = require('child_process')
const params = ['123 && ls']
Object.prototype.shell = true // 只多了這行,參數的解析就會不一樣
const result = child_process.spawnSync(
'echo', params, {timeout: 1000}
);
console.log(result.stdout.toString())
// Object.prototype.shell 被污染,spawnSync 的選項 shell 被自動設定為 true
// 實際執行的命令類似於:/bin/sh -c "echo 123 && ls"
// 導致執行 echo 123,然後執行 ls,輸出當前目錄內容:
/*
123
index.js
node_modules
package-lock.json
package.json
*/

原因是child_process.spawn 第三個參數 options 中的 shell 選項設為 true 後的行為不同,shelltrue 時,spawnSync 會將命令與參數組裝成字串並交給 shell執行,如果輸入參數包含特殊字符(如 &&, ; 等),這些字符會被 shell 解釋為命令組合符,可能導致執行額外的命令。因此上述就是 prototype pollution 搭配 script gadget(child_process.spawn),達成了 RCE 攻擊。

補充:
Command Injection:使用者的惡意輸入被解釋為命令,導致執行未預期的命令。

防禦方式#

當程式中某功能能讓攻擊者污染 prototype 上的屬性,有漏洞的這功能稱為 prototype pollution,但 prototype pollution 仍需結合其他程式碼才能達成攻擊,而那些可以和 prototype pollution 結合的程式碼稱為 script gadget。兩者關係如下:

舉例來說,透過 prototype pollution 污染了 Object.prototype.template,又因為 Vue 會依據物件 template 屬性渲染東西(script gadget),因而達成攻擊。簡單講的話,就是先透過 prototype pollution 污染到 Object.prototype.a,而同時原有程式碼會拿物件的 a 屬性做某些事(script gadget),因而影響到原有執行流程,達成攻擊。也因此攻擊者在污染原型時,會汙染某些特定、會影響後續流程的屬性。

而 prototype pollution 防禦方式並非修復可利用的 script gadget,而是要杜絕 prototype pollution,從一開始、最根源就不該讓 prototype 被污染,也就不會有後續的事了。

常見防禦方式#

1. 操作物件時,阻止原型相關的 key#

阻止原型相關的 key 包含阻止 __proto__constructor.prototype。不只__proto__constructor.prototype也可污染原型鏈屬性,範例如下:

// constructor.prototype 也可污染原型鏈屬性
var obj = {}
obj['constructor']['prototype']['a'] = 1
var obj2 = {}
console.log(obj2.a) // 1

有使用此方式的例如 lodash.merge,當 key 是 __proto__prototype 時會做特殊處理。

2. 不要用有 prototype 的 object#

改用 Object.create(null) 來建立物件,Object.create(null) 可建立無 __proto__ 屬性的空物件,空物件內無任何屬性、方法。

var obj = Object.create(null)
obj['__proto__']['a'] = 1 // 根本沒有 __proto__ 這個屬性
// Uncaught TypeError: Cannot set properties of undefined (setting 'a')

有使用此方式的例如 query-string library,在解析 query string 後回傳的物件是用 Object.create(null) 建立的,文件上這樣寫:「.parse(string, options?) Parse a query string into an object. Leading ? or # are ignored, so you can pass location.search or location.hash directly.
The returned object is created with Object.create(null) and thus does not have a prototype.」。

3. 以 Map 取代 {}#

不過多數人仍習慣用 object,Object.create(null) 會比 Map 好用。

4. 以 Object.freeze(Object.prototype) 凍結 prototype、防止修改#

Object.freeze(Object.prototype)
var obj = {}
obj['__proto__']['a'] = 1
var obj2 = {}
console.log(obj2.a) // undefined

缺點是若第三方套件有修改 Object.prototype,會較難除錯,因為使用Object.freeze(Object.prototype) 後再去修改 Object.prototype 不會噴錯,只是無法修改成功,因此很難找到套件壞掉的原因;另外,若是要加 polyfill,也會因 Object.freeze 而失效。

5. 使用 Node.js 的 --disable-proto option#

參考官方文件--disable-proto 可把 Object.prototype.__proto__關掉。

6. 未來可能可用 document policy 處理#

可參考 Feature proposal: Mitigation for Client-Side Prototype Pollution 的討論。

補充:
polyfill:指為某些舊版瀏覽器或環境提供對新功能的支援,當這些環境本身不支援該功能時,開發者可用 JavaScript 實現相似的功能作為替代。這種替代實現就稱為 polyfill。polyfill 的實現方式通常是通過修改 prototype,將缺少的功能手動增加到原型鏈上。

實際案例#

書中有提到兩個 prototype pollution 的實際案例,分別是 bug bounty 平台本身漏洞以及 Kibana 的漏洞,就不詳細描述,有興趣可點進連結了解。

Web API 與 Prototype Pollution#

瀏覽器 Web API 也會被 prototype pollution 影響。舉例來說,以下只是一個單純 GET 請求:

fetch('https://example.com', {
mode: 'cors'
})

但如果有prototype pollution 漏洞後,就可變成 POST 請求:

Object.prototype.body = 'a=1'
Object.prototype.method = 'POST'
fetch('https://example.com', {
mode: 'cors'
})

而 Web API 的 prototype pollution 問題曾被討論過,但這其實是符合 spec,不須特別處理。

由此可看出,script gadget 會一直存在,因為 JavaScript 原型鏈是自身特色,很難特地處理,因此防禦應從源頭避免原型鏈被汙染,而非去調整 script gadget 或 JavaScript 原型鏈的存取方式。

小結#

prototype pollution 單看影響不大,但結合其他程式碼就可能破壞現有流程,進而達成 XSS 或 RCE。若想了解更多 prototype pollution 資訊,也可參考自動化方式檢測 prototype pollution 漏洞的這篇研究:A tale of making internet pollution free — Exploiting Client-Side Prototype Pollution in the wild


Reference:#

如有任何問題歡迎聯絡、不吝指教✍