什么是原型污染?
原型污染是一个 JavaScript(nodejs) 漏洞,攻击者可以利用它向全局对象原型添加任意属性,然后用户定义的对象可以继承这些属性。
原型链污染又分为客户端原型链污染和服务端原型链污染;在ctf中基本都是服务端污染导致绕过验证,得到管理员权限或者RCE等等;
JavaScript 中的原型是什么?
JavaScript 中的每个对象都链接到某种类型的另一个对象,称为原型。JavaScript 使用原型继承模型,这与许多其他语言使用的基于类的模型有很大不同。默认情况下,JavaScript 会自动将新对象分配给其内置原型之一。例如,字符串会自动分配内置的String.prototype
. 您可以在下面看到这些全局原型的更多示例:
1 | let myObject = {}; |
对象会自动继承其指定原型的所有属性,除非它们已经拥有具有相同键的自己的属性。这使得开发人员能够创建可以重用现有对象的属性和方法的新对象。
内置原型提供了用于处理基本数据类型的有用属性和方法。例如,String.prototype
对象有一个toLowerCase()
方法。因此,所有字符串都会自动有一个现成的方法将它们转换为小写。这使得开发人员不必手动将此行为添加到他们创建的每个新字符串中。
对象继承是如何工作的?
每当您引用对象的属性时,JavaScript 引擎都会首先尝试直接在对象本身上访问该属性。如果对象没有匹配的属性,JavaScript 引擎会在对象的原型上查找它。
原型链
请注意,一个对象的原型只是另一个对象,该对象也应该有自己的原型,依此类推。由于实际上 JavaScript 中的所有内容都是底层的对象,因此这条链最终会回到顶层Object.prototype
,其原型很简单null
。
至关重要的是,对象不仅从其直接原型继承属性,而且从原型链中位于其上方的所有对象继承属性。在上面的示例中,这意味着username对象可以访问String的属性和方法。String.prototype
and Object.prototype
.
使用 __proto__
访问对象的原型
每个对象都有一个特殊的属性,您可以使用它来访问其原型。尽管它没有正式的标准化名称,但__proto__
它是大多数浏览器使用的事实上的标准。您可以使用它来读取原型及其属性,甚至在必要时重新分配它们。
与任何属性一样,您可以__proto__
使用括号或点符号进行访问:
1 | username.__proto__ |
甚至可以链接引用以__proto__
沿着原型链向上工作:
1 | username.__proto__ // String.prototype |
修改原型
修改原型可以像修改任何其他对象一样修改 JavaScript 的内置原型。这意味着开发人员可以自定义或重写内置方法的行为,甚至添加新方法来执行有用的操作。
例如,现代 JavaScript 提供了trim()
字符串方法,使您能够轻松删除任何前导或尾随空格。在引入此内置方法之前,开发人员有时会String.prototype
通过执行以下操作将自己的此行为的自定义实现添加到对象中:
1 | String.prototype.removeWhitespace = function(){ // remove leading and trailing whitespace } |
由于原型继承,所有字符串都可以访问此方法:
1 | let searchTerm = " example "; searchTerm.removeWhitespace(); // "example" |
原型污染漏洞是如何产生的?
当JavaScript函数递归地将包含用户可控属性的对象合并到现有对象时,通常会出现原型污染漏洞。这可以允许攻击者注入带有类似__proto__
的键的属性,以及任意嵌套的属性。
由于__proto__
在JavaScript上下文中的特殊含义,合并操作可能会将嵌套属性分配给对象的原型,而不是目标对象本身。因此,攻击者可以用包含恶意值的属性污染原型,这些属性随后可能被应用程序以危险的方式使用。
prototype
, __proto__
prototype
和 __proto__
是 JavaScript 中与原型(prototype)相关的两个重要概念,但它们有不同的用途和含义。
prototype:
prototype
是函数对象特有的属性。每个函数都有一个prototype
属性,这个prototype属性是一个指向原型对象的指针,它包含了可以由该函数的实例继承的属性和方法。- 当你创建一个新的函数时,JavaScript 自动为该函数创建一个
prototype
对象,并赋值给prototype
属性。 - 通过
prototype
对象,你可以定义函数的共享属性和方法,它将被该函数的所有实例所共享。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 定义构造函数
function Person(name, age) {
// 实例属性
this.name = name;
this.age = age;
}
// 在构造函数的 prototype 上定义共享方法
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
// 创建实例
const person1 = new Person("Alice", 25);
const person2 = new Person("Bob", 30);
// 调用共享方法
person1.sayHello(); // 输出: Hello, my name is Alice and I am 25 years old.
person2.sayHello(); // 输出: Hello, my name is Bob and I am 30 years old.proto:
__proto__
是每个对象都具有的属性,用于指向其构造函数的原型。它是一个指向该对象的原型链上一层的链接。__proto__
属性是非标准的,尽管大多数现代浏览器都支持它,但它已经被 ECMAScript 6 标准中的Object.getPrototypeOf
方法所替代。__proto__
主要用于获取和设置对象的原型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25const myObject= {};
const myObjectPrototype = Object.getPrototypeOf(myObject);
console.log(myObjectPrototype === Object.prototype); //true
console.log(myObject.__proto__ === Object.prototype); //true
const myString = "";
const myStringPrototype = Object.getPrototypeOf(myString);
console.log(myStringPrototype.__proto__ === Object.prototype); //true
const myArray = [];
const myArrayPrototype = Object.getPrototypeOf(myArray);
console.log(myArrayPrototype.__proto__ === Object.prototype); //true
const myNumber = 1;
const myNumberPrototype = Object.getPrototypeOf(myNumber);
console.log(myNumberPrototype.__proto__ === Object.prototype); //true
//======================================================================
console.log(Object.getPrototypeOf(myString)) //{}
console.log(Object.getPrototypeOf(myNumber)) //{}
console.log(Object.getPrototypeOf(myString) ==Object.getPrototypeOf(myNumber)) //false
console.log(Object.getPrototypeOf(myString).__proto__ ==Object.getPrototypeOf(myNumber).__proto__) //true
//下面解释为什么是 false?
//在 JavaScript 中,Object.getPrototypeOf 用于获取对象的原型。在代码中,myString 是一个空字符串,而 myNumber 是一个数字。当调用 Object.getPrototypeOf(myString) 时,它返回的是字符串的原型对象 String.prototype。同样,Object.getPrototypeOf(myNumber) 返回的是数字的原型对象 Number.prototype。虽然输出结果都是 {} 对象,但它们是不同的对象。这是因为 {} 是表示一个空对象字面量的方式,而不是真正的空对象。在 JavaScript 中,每次调用 Object.getPrototypeOf 都会返回一个新的对象,即使它们有着相同的结构和原型链。
总的来说,prototype
用于构造函数,帮助定义共享属性和方法,而 __proto__
是每个对象都有的属性,用于指向其构造函数的原型。Object.getPrototypeOf
方法是更推荐使用的获取原型的方式,而 __proto__
在现代 JavaScript 中已经不再建议使用。
一个简单的案例
1 | const myObject = { a: 1, b: 2 }; |
这也适用于数组,其中for...in
循环首先迭代每个索引(本质上只是底层的数字属性键),然后再继续处理任何继承的属性。
1 | const myArray = ['a','b']; |
通过 JSON 输入造成原型污染
JSON.parse()
在ctf中最常见的莫过于JSON.parse()
函数了;
用户可控的对象通常是使用该JSON.parse()
方法从 JSON 字符串派生的。有趣的是,JSON.parse()
还将 JSON 对象中的任何键视为任意字符串,包括__proto__
. 这为原型污染提供了另一个潜在载体。
假设攻击者通过网络消息注入以下恶意 JSON:
1 | { |
如果通过该方法将其转换为 JavaScript 对象JSON.parse()
,则生成的对象实际上将具有一个带有 key 的属性__proto__
:
1 | const objectLiteral = {__proto__: {evilProperty: 'payload'}}; |
注意:在Node.js中,hasOwnProperty
函数是JavaScript中的一个内置函数,用于检查对象自身是否包含指定的属性(即不包括从原型链继承的属性)。这个函数返回一个布尔值,如果对象包含指定的属性,则返回true,否则返回false。
例如:
1 | let obj = {a: 1, b: 2}; |
Example
1 | function merge(target, source) { |
服务器端原型污染
记录一下portswigger上的实验;漏洞点都是在一个修改个人形象的功能点,后端是基于 Node.js 和 Express 框架构建。它很容易受到服务器端原型污染的影响,因为它将用户可控的输入不安全地合并到服务器端 JavaScript 对象中。
污染进行权限升级
用bp抓包;往请求包中用__proto__
尝试原型污染;然后发送请求。可以看到响应包中出现了"hacker":"zIxyd"
1 | "__proto__":{"hacker":"zIxyd"} |
确认存在原型链污染,发送恶意数据,
1 | "__proto__":{"isAdmin":true} |
检测服务器端原型污染
大多数时候,即使您成功污染了服务器端原型对象,您也不会在响应中看到受影响的属性。也就是说你在一个点上无法判断是否存在原型污染;那该怎么办呢?
一种方法是尝试注入与服务器的潜在配置选项相匹配的属性。然后,您可以比较注入前后服务器的行为,以查看此配置更改是否已生效。如果是这样,这强烈表明您已成功发现服务器端原型污染漏洞。
状态码覆盖
Express 等服务器端 JavaScript 框架允许开发人员设置自定义 HTTP 响应状态。如果出现错误,JavaScript 服务器可能会发出通用 HTTP 响应,但在正文中包含 JSON 格式的错误对象。这是提供有关错误发生原因的附加详细信息的一种方法,这从默认 HTTP 状态中可能并不明显。
Node 的http-errors
模块包含以下用于生成此类错误响应的函数:
1 | function createError () { |
“ //注意下面这一行”显示的下一行试图通过读取传递给函数的对象的status或statusCode属性来分配状态变量。如果网站的开发人员没有明确地为错误设置状态属性,你可以使用它来探测原型污染;
注意:根据代码分析,我们要修改的状态码必须在400和600之间,否则为状态码为500
再次声明,实验环境用的是:portswigger
多加了一个双引号,使得json格式不对,返回报错状态码为400
尝试污染status
的值
1 | "__proto__": { |
再次使得json格式错乱,查看报错状态码为555,说明已经污染成功了
JSON 空格覆盖
Express 框架提供了一个json spaces
选项,使您能够配置用于缩进响应中任何 JSON 数据的空格数。在许多情况下,开发人员不会定义此属性,因为他们对默认值感到满意,这使其容易受到原型链的污染。
如果您可以访问任何类型的 JSON 响应,则可以尝试使用自己的json spaces
属性污染原型,然后重新发出相关请求以查看 JSON 中的缩进是否相应增加。您可以执行相同的步骤来删除缩进以确认漏洞。
这是一项特别有用的技术,因为它不依赖于所反映的特定属性。它也非常安全,因为您只需将属性重置为与默认值相同的值即可有效地打开和关闭污染。
尽管 Express 4.17.4 中已经修复了原型污染问题,但未升级的网站可能仍然容易受到攻击。
1 | "__proto__":{ |
除了上面介绍的俩种;还有字符集覆盖方法等等
绕过过滤造成原型污染
有时会过滤__proto__
,但是这种关键的过滤方法并不是一个强大的长期解决方案,因为有多种方法可能会绕过它。
__proto__
节点应用程序还可以分别使用命令行标志--disable-proto=delete
或来完全 删除或禁用--disable-proto=throw
。然而,这也可以通过使用构造函数技术来绕过。
实验
多次发送json spaces
请求,但是响应包没有反应
每个构造函数(constructor)都有一个原型对象(prototype); 上面已经解释的很清楚了
constructor.prototype === __proto__
1 | let myObject = {} |
尝试使用constructor
;
1 | "constructor": { |
Tools
Server-Side Prototype Pollution Scanner
https://portswigger.net/research/server-side-prototype-pollution