文章目錄
- 0 前言
- 1 原理
- 2 利用
0 前言
原型鍊污染,是 NodeJs 中常見的漏洞,在做 antCTF 時也遇到的原型鍊污染題目,在此記錄自己學習原型鍊污染的過程。
1 原理
0x01 問:原型鍊有什麼作用?
用來做繼承,也就是基于原有的代碼做一定的修改。下面是一個使用原型鍊實作繼承的案例:
function Parent () {
this.name = 'kevin';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.getName()) // kevin
當「方法」的 prototype 指定對象原型之後,當試圖通路該類的對象屬性時,它不僅僅在該對象上搜尋,還會搜尋該對象的原型,以及該對象的原型的原型,依次層層向上搜尋,直到找到一個名字比對的屬性或到達原型鍊的末尾。下面是一個修改 prototype 的案例:
注意:prototype 是「方法特有的」(需要大概了解,後面需要使用到)
- 方法:類似 C++ 中的類。除了有屬性
, 還有屬性 prototype,prototype 指向該方法的原型對象。propotype 指定其他對象之後,會包含所有原型對象的屬性和方法
__proto__
- 對象:類似 C++ 中的對象。對象隻有屬性
指向該對象的構造函數的原型對象。對象有
__proto__
裡面包含該類的 prototype。
constructor
// 讓我們從一個函數裡建立一個對象o,它自身擁有屬性a和b的:
let f = function () {
this.a = 1;
this.b = 2;
}
/* 這麼寫也一樣
function f() {
this.a = 1;
this.b = 2;
}
*/
let o = new f(); // {a: 1, b: 2}
// 在f函數的原型上定義屬性
f.prototype.b = 3;
f.prototype.c = 4;
// 不要在 f 函數的原型上直接定義 f.prototype = {b:3,c:4};這樣會直接打破原型鍊
// o.[[Prototype]] 有屬性 b 和 c
// (其實就是 o.__proto__ 或者 o.constructor.prototype)
// o.[[Prototype]].[[Prototype]] 是 Object.prototype.
// 最後o.[[Prototype]].[[Prototype]].[[Prototype]]是null
// 這就是原型鍊的末尾,即 null,
// 根據定義,null 就是沒有 [[Prototype]]。
// 綜上,整個原型鍊如下:
// {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null
console.log(o.a); // 1
// a是o的自身屬性嗎?是的,該屬性的值為 1
console.log(o.b); // 2
// b是o的自身屬性嗎?是的,該屬性的值為 2
// 原型上也有一個'b'屬性,但是它不會被通路到。
// 這種情況被稱為"屬性遮蔽 (property shadowing)"
console.log(o.c); // 4
// c是o的自身屬性嗎?不是,那看看它的原型上有沒有
// c是o.[[Prototype]]的屬性嗎?是的,該屬性的值為 4
console.log(o.d); // undefined
// d 是 o 的自身屬性嗎?不是,那看看它的原型上有沒有
// d 是 o.[[Prototype]] 的屬性嗎?不是,那看看它的原型上有沒有
// o.[[Prototype]].[[Prototype]] 為 null,停止搜尋
// 找不到 d 屬性,傳回 undefined
0x02 問:
__proto__
屬性有什麼作用?
每個「對象」都有
__proto__
屬性,指向了建立該對象的構造函數的原型。其實這個屬性指向了 [[prototype]],但是 [[prototype]] 是内部屬性,我們并不能通路到,是以使用
__proto__
來通路。
簡而言之:用 prototype 無法直接通路,需要使用 __proto__
通路。prototype 是一個指針屬性。
這裡有個需要區分的概念:
-
:指向原型對象的構造器。__proto__
-
:指向目前對象的構造器。constructor
(圖檔說明:右下角是圖檔的說明,左圖的
__proto__
的箭頭指向原型對象的構造器)
(圖檔說明:右下角是圖檔的說明,左圖的
constructor
的箭頭指向原型對象的構造器)
0x03 問:原型鍊污染的概念是什麼?
在一個應用中,如果攻擊者控制并修改了一個對象的原型,那麼将可以影響所有和這個對象來自同一個類、父祖類的對象。這種攻擊方式就是原型鍊污染。
基本原理:引用類型的屬性被所有執行個體共享。案例:
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy", "yayu"]
按照 Java 中正常的繼承,child2.names 應該和原對象一樣,數組中隻有 2 個資料。
2 利用
問:怎麼判斷是否有原型鍊污染?
- 字元串可以被解析為方法或對象。例如:Json.parse 進行解析、shvl 庫使用點對屬性操作。
- 對象的鍵和值都可控。target[key] = value,其中 key 和 value 均可控制。
下面是一個原型鍊污染的簡單案例:
function merge(target, source) {
console.log('merge', target, source);
// 周遊 source 中的 key。
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
let o1 = {} // {} 是一個對象,存在 __proto__ 的 key。
console.log(typeof(o1));
console.log(o1); // object
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
console.log(typeof(o2));
console.log(o2); // object
merge(o1, o2)
console.log('o1 proto:', o1.__proto__);
console.log('o2 proto:', o2.__proto__);
console.log('{} proto:', {}.__proto__);
console.log(o1, o2); // 對象 {} 的原型對象變為 { a: 1 }。
o3 = {}
console.log(o3)
實戰1:在 antCTF 中,使用 shvl 庫對鍵值進行操作。
email: async function (req, res) {
let contents = {};
Object.keys(req.body).forEach((key) => {
shvl.set(contents, key, req.body[key]);
});
// 周遊請求參數中所有的 key
// 将鍵和值指派為 contents(shvl 庫的 set 函數)
contents.from = '"admin" <[email protected]>';
try {
await send(contents);
return res.json({
message: "Success."
});
} catch (err) {
return res.status(500).json({
message: err.message
});
}
}
問:常見的原型鍊污染方法有哪些?
-
。function.__proto__.polluted
-
。function.prototype.polluted
-
。例如:shvl 隻禁用了obj.__proto__.pollluted
,傳送門Security Fix for Prototype Pollution - huntr.dev__proto__
-
。obj.constructor.polluted
案例:
function person(fullName) {
this.fullName = fullName;
}
var person1 = new person("Satoshi");
// function:prototype, __prototype
person.prototype.sayHello = 1
console.log(person1.__proto__);
person.prototype.newConstant = 2
console.log(person1.__proto__);
// object: __prototype__, constructor
person1.__proto__.sayHi= 3
console.log(person1.__proto__);
person1.constructor.prototype.oldConstant = 4
console.log(person1.__proto__);
/*
person { sayHello: 1 }
person { sayHello: 1, newConstant: 2 }
person { sayHello: 1, newConstant: 2, sayHi: 3 }
person { sayHello: 1, newConstant: 2, sayHi: 3, oldConstant: 4 }
*/
推薦一個非常 nice 的網站:https://book.hacktricks.xyz/ 。收集 hack 中的 tricks
問:原型鍊污染鍊怎麼挖掘?
- 尋找 JavaScript 中的危險關鍵字(危險函數)。如:
- 子產品:child_process
- 函數:eval, spwn, exec, setTimeout, setInteval, Function
- 尋找調用關系和可控的參數,并且确定如何進行傳參。 (圖檔說明:先确認危險函數和調用方式,也就是頭部和尾部,再去尋找中間過程)
- 充分利用危險函數和能控制的參數。(讀取檔案或者反彈 shell)
- 目标機器環境如果有 bash,可以反彈 shell。
- 目标機器環境如果有 Python 等,可以反彈 shell。
- 目标機器環境如果隻有 sh,将 readflag 執行寫入到其他地方,再利用其他方式讀取。
(拓展閱讀:反彈Shell表)
問:原型鍊污染怎麼防禦?怎麼繞過防禦呢?
- 如果系統中有鍵值的操作,并且鍵和值來自外部輸入。可以考慮進行過濾:
- 禁止操作
constructor
- 禁止操作
prototype
- 禁止操作
__proto__
- 禁止操作
繞過防禦:
- 思考并測試是否過濾完全,具體參考 antCTF 的 8-bit-pub 中的 shvl 庫繞過。
參考教程:
- JavaScript深入之繼承的多種方式和優缺點-Github
- JS中的prototype、__proto__與constructor-Huawei
- 繼承與原型鍊 - JavaScript | MDN
- 深入了解 JavaScript Prototype 污染攻擊-phithon
- NodeJS - proto & prototype Pollution-HackTricks
- AntCTF2021部分WP-HapHp1