Source: Infobyte Blog

Infobyte Blog Bypassing a restrictive JS sandbox

Commonly when one audits a software, it usually first understands what features it has and how they are implemented.It is so that one is running into certain behaviors that lead to a deeper level of analysis.There is no single criterion in which one must stop and inquire, sometimes it is intuition other concerns or simply follow a methodology.It was so I came across with a very interesting functionality: it allowed me to filter some data based on a user-controlled expression. I could put something like book.price > 100 to make it only show the books that are more expensive than $100. Using true as filter showed me all the books, and false didn't show anything. So I was able to know whether the expression I used was evaluating to true or false.That functionality caught my attention so I tried passing it more complex expressions, like (1+1).toString()==="2" (evaluated to true) and(1+1).toString()===5 (evaluated to false). This is clearly JavaScript code, so I guessed that the expression was being used as an argument to a function similar to eval, inside a NodeJS server. It seemed like I was close to find a Remote Code Execution vulnerability. However, when I used more complex expressions, I was getting an error indicating that they were invalid. I guessed that it wasn't the eval function that parsed the expression, but a kind of sandbox system for JavaScript.Sandbox systems used to execute untrusted code inside a restricted environment are usually hard to get right. In most cases there exist ways to bypass this protections to be able to execute code with normal privileges. This is specially true if they try to limit the usage of complex, feature bloated languages like JavaScript. The problem had already caught my attention, so I decided to spend an important time trying to break this sandbox system. I would learn about JavaScript internals, and gain some bucks in case of finding and exploiting the RCE.The first thing I did was identify what library the site was using to implement the sandbox, given that the NodeJS ecosystem is known for having tens of libraries that do the same thing.. Maybe it was a custom sandbox library used only for the target site, but I discarded this possibility because it was really unlikely that the developers spent their time doing this kind of things.Finally, by analyzing the app error messages I concluded that they were using static-eval, a not very known library (but written by substack, somebody well known in the NodeJS community). Even if the original purpose of the library wasn't to be used as a sandbox (I still don't understand what it was created for), its documentation suggests that. In the case of the site I was testing, it certainly was being used as a sandbox.Breaking static-evalThe idea of static-eval is to use the Esprima library to parse the JS expression and convert it to an AST (Abstract Syntax Tree). Given this AST and an object with the variables I want to be available inside the sandbox, it tries to evaluate the expression. If it finds something strange, the function fails and my code isn't executed. At first I was a bit demotivated because of this, since I realized that the sandbox system was very restrictive with what it accepted. I wasn't even able to use a for or while statement inside my expression, so doing something that required an iterative algorithm was almost impossible.I did not find any bug at first sight, so I looked at the commits and pull requests of the static-eval GitHub project. I found that the pull request #18 fixed two bugs that allowed a sandbox escape in the library, exactly what I was looking for. I also found a blog post of the pull request author that explained this vulnerabilities in depth. I immediately tried using this techniques in the site I was testing, but unfortunately to me, they were using a newer static-eval version that already patched this vulns. However, knowing that somebody has already been able to break this library made me more confident so I kept looking for new ways to bypass it.Then, I analyzed this two vulns in depth, hoping this could inspire me to find new vulnerabilities in the library.Analysis of the first vulnerabilityThe first vuln used the function constructor to make a malicious function. This technique is frequently used to bypass sandboxes. For example, most of the ways to bypass the angular.js sandbox to get an XSS use payloads that end up accessing and calling the function constructor. It was also used to bypass libraries similar to static-eval, like vm2. The following expression shows the existence of the vulnerability by printing the system environment variables (this shouldn't be possible because the sandbox should block it):"".sub.constructor("console.log(process.env)")()In this code, "".sub is a short way to obtain a function ((function(){}) would also work). Then it access to the constructor of that function. That is a function that when called returns a new function whose code is the string passed as argument. This is like the eval function, but instead of executing the code immediately, it returns a function that will execute the code when called. That explains the () at the end of the payload, that calls the created function.Result of executing the previous payloadYou can do more interesting things than showing the environment variables. For example, you can use the execSync function of the child_process NodeJS module to execute operating system commands and return its output. This payload will return the output of running the id command:"".sub.constructor("console.log(global.process.mainModule.constructor._load(\"child_process\").execSync(\"id\").toString())")()The payload is similar to the previous one, except for the created function's body. In this case, global.process.mainModule.constructor._load does the same as the require function of NodeJS. _load in the lib is similar to require of NodeJS.Result of execute the payload that executes the command idThe fix for this vulnerability consisted in blocking the access to properties of objects that are a function (this is done with typeof obj == 'function'):else if (node.type === 'MemberExpression') { var obj = walk(node.object); // do not allow access to methods on Function if((obj === FAIL) || (typeof obj == 'function')){ return FAIL; }This was a very simple fix, but it worked surprisingly well. The function constructor is available, naturally, only in functions. So I can't get access to it. An object's typeof can't be modified, so anything that is a function will have its typeof set to a function. I didn't find a way to bypass this protection, so I looked at the second vuln.Analysis of the second vulnerabilityThis vuln was way more simple and easy to detect than the first one: the problem was that the sandbox allowed the creation of anonymous functions, but it didn't check their body to forbid malicious code. Instead, the body of the function was being directly passed to the function constructor. The following code has the same effect than the first payload of the blog post:(function(){console.log(process.env)})()You can also change the body of the anonymous function so it uses execSync to show the output of executing a system command.One possible fix for this vulnerability would be to forbid all anonymous function declarations inside static-eval expressions. However, this would block the legitimate use cases of anonymous functions (for example, use it to map over an array). Because of this, the fix would have to allow the usage of benign anonymous functions, but to block the usage of malicious ones. This is done by analyzing the body of the function when it is defined, to check it won't perform any malicious actions, like accessing the function constructor.This fix turned out to be more complex than the first one. Also, Matt Austin (the author of the fix) said he wasn't sure it would work perfectly. So I decided to find a bypass to this fix.Finding a new vulnerabilityOne thing that caught my attention was that static-eval decided whether the function was malicious or not at definition time, and not when it was being called. So it didn't consider the value of the function arguments, because that would require to make the check when the function is called instead.My idea was always trying to access the function constructor, in a way that bypasses the first fix that forbids that (because I'm not able to access properties of functions). However, what would happen if I try to access the constructor of a function parameter? Since its value isn't known at definition time, maybe this could confuse the system and make it allow that. To test my theory, I used this expression:(function(something){return algo.constructor})("".sub)If that returned the function constructor, I would have a working bypass. Sadly for me, it wasn't the case. static-eval will block the function if it accesses a property of something with an unknown type at function definition time (in this case, the something argument).One useful feature of static-eval that is used in almost all cases, is allowing to specify some variables you want to be available inside the static-eval expression. For example, in the beginning of the blog post I used the expression book.price > 100. In this case, the code calling static eval will pass it the value of the book variable so it can be used inside the expression.This gave me another idea: what would happen if I make an anonymous function with an argument whose name is the same as an already defined variable? Since it can't know the value of the argument at definition time, maybe it uses the initial value of the variable. That would be very useful to me. Suppose I have a variable book and its initial value is an object. Then, the following expression:(function(book){return book.constructor})("".sub)would have a very satisfactory result: when the function is defined, static-eval would check if book.constructor is a valid expression.

Read full article »
Est. Annual Revenue
$25-100M
Est. Employees
100-250
CEO Avatar

CEO

Update CEO

CEO Approval Rating

- -/100