原创文章,不得转载。
漏洞原理
在 JavaScript 中,使用原型继承模型与许多其他语言所采用的基于类的继承模型有着明显的区别。
在 JavaScript 中,对象是由一组称为“属性”的键值对构成的集合。举例来说,可以定义一个包含用户名和用户标识的 user 对象:
const user = {
username: "ice",
useridentify: 1001,
addOrder: function() {
// 执行添加订单操作的函数
}
};
在 JavaScript 中,每个对象(如 B)都链接到另一个对象(如 A),A 被称为 B 的原型。当对象 B 分配了原型之后,它会自动继承其指定原型的所有属性。
举个例子,当 myString 对象继承了 String.prototype 原型之后,由于该原型中包含 toLowerCase() 方法,因此 myString 对象也可以直接调用该方法:
let myString = "Ice";
Object.getPrototypeOf(myString); // String.prototype
myString.toLowerCase(); // ice
在 JavaScript 中,当引用对象的属性时,JavaScript 引擎首先尝试直接在对象本身上查找该属性。如果对象本身没有匹配的属性,JavaScript 引擎会在对象的原型链上继续查找。
在 JavaScript 中,由于对象 B 的原型是另一个对象 A,因此对象 A 也可能有自己的原型,以此类推。由于 JavaScript 中几乎所有的东西都是对象,所以原型链最终会回溯到顶层的 Object.prototype
,而其原型则是 null
。
总结来说:通过原型链的机制,JavaScript 实现了对象之间的继承关系,使得对象可以共享和继承其原型链上的属性和方法。这种原型继承模型为 JavaScript 带来了灵活性和强大的功能。
JavaScript 的原型链污染思路即为:指向全局对象原型添加任意属性,然后这些属性可能会被用户定义的对象继承,从而实现敏感操作。
而恰好,由于每个对象都有一个特殊的属性 __proto__
,我们可以使用它来访问其原型及其属性,实现自定义或覆盖内置方法,甚至可以添加新方法。
举例如下:
// 使用 __proto__ 属性回溯原型链
myString.__proto__ // String.prototype
myString.__proto__.__proto__ // Object.prototype
myString.__proto__.__proto__.__proto__ // null
漏洞危害
原型链污染虽然通常无法作为独立漏洞被利用,但它能够让我们控制本来无法访问的对象属性。如果应用程序随后以不安全的方式处理用户控制的属性,则可能与其他漏洞相互影响。在客户端 JavaScript 中,这将导致 DOM XSS,而服务器端原型污染甚至有可能导致远程代码执行。
漏洞利用
我们通常在以下污染源中实现污染:
1、URL 中的查询字符串或片段字符串(哈希)
2、基于 JSON 的输入
3、Web消息
1、通过 URL 进行原型污染
当 URL 中包含像 ?__proto__[evilProperty]=payload
这样的恶意构造的查询字符串时,URL 解析器可能会将 __proto__
解释为字符串:
{
string1: 'ice',
string2: 'JavaScript',
__proto__: {
evilProperty: 'payload'
}
}
但在执行递归合并操作时,URL解析器会使用类似于 targetObject.__proto__.evilProperty = 'payload'
的语句来分配 evilProperty
的值。
在这个赋值过程中,原型对象的evilProperty
属性将被覆盖或添加(如果不存在)。假设目标对象使用默认的 Object.prototype
,那么 JavaScript 运行时,所有对象都会继承这个 evilProperty
属性,除非它们已经拥有了相同名字的属性。
2、通过 JSON 输入造成原型污染
用户可控制的对象通常是通过 JSON 字符串使用 JSON.parse() 方法派生而来的。而JSON.parse() 也将 JSON 对象中的任何键都视为任意字符串,包括像 proto 这样的键,这为原型污染提供了另一个潜在的途径。
假设攻击者通过网页消息注入了以下恶意的 JSON:
jsonCopy Code{
"__proto__": {
"evilProperty": "payload"
}
}
如果这个 JSON 经过 JSON.parse() 方法转换为 JavaScript 对象,得到的对象实际上将具有一个键为 __proto__
的属性:
const objectLiteral = {__proto__: {evilProperty: 'payload'}};
const objectFromJson = JSON.parse('{"__proto__": {"evilProperty": "payload"}}');
objectLiteral.hasOwnProperty('__proto__'); // false
objectFromJson.hasOwnProperty('__proto__'); // true
通过 JSON.parse() 创建的对象随后被合并到现有对象时,可能导致原型污染。
漏洞实例
定义user对象,它从父对象 Object.Prototype 继承了所有属性:
let user = {'name': 'mayank', 'age': 25}
user.name
user.name.toString()
覆盖上层对象的toString()属性:
user.name.__proto__.toString = ()=>{alert('ice')}
访问用户属性:
user.name.toString()
缓解措施
1、确保用户输入不包含影响原型链的 __proto__
、构造函数或原型。
2、使用 Object.freeze(Object.prototype) 冻结 Object.prototype(由于兼容性问题,不推荐使用)