When looking through the Squirrelly source code, OwOverflow pointed out this segment of code in
src/compile-string.ts
to me:
(env is the options parameter passed to the render function)
After following the complete function call chain (render -> handleCache -> compile -> compileString), I found that the
env.varName parameter is indeed not sanitized. At first glance, this seems like an easy way to inject arbitrary JS and
get RCE server-side. In order to get this exploit to work, the attacker should set env.useWith so that env.varName
is injected.
The with expression is an almost-deprecated Javascript feature that allows you to execute a block of code, appending
an object to the global scope chain. This is demonstrated below:
However, what is important is that we can inject an arbitrary expression into the with argument. This can easily allow
us to get RCE! A simple payload would be:
This should execute the console.log("x") statement and give us RCE.
Testing
We can set up a simple server to test this out.
Visiting the following URL to execute our payload:
However, if we try to do this, we get the following error:
What does this mean? Searching for the error message, we can see the error being raised at
src/compile.ts:44:
The options.varName parameter is being passed to the ctor function (which is just a Function constructor). The
Function constructor takes in n+1 arguments, where the first n arguments are parameters of the function to be
created, and the last argument is the function body. That’s what’s causing our error, since we’re passing
console.log("x") into the parameter field. This means our injection occurs in the first parameter of the new function
being created, and should be a valid JS expression that can be passed in a parameter declaration. And our payload should
still execute as per normal in the with expression.
First payload
Javascript allows for parameters to be destructured in the function definition like so:
This is done through object destructuring. It’s quite flexible and also allows for default values to be set:
This is the vector for our RCE. When setting the default value, the expression has to be evaluated, which we can use for
RCE. Something like this should work:
However, there’s a small problem - our payload is also being injected in the with expression, and this syntax is
invalid there since it’s not a valid object initializer.
Indeed, when we try this payload, it crashes as well:
Luckily, according to the
docs, the destructuring syntax is quite flexible and allows for a lot more kinds of syntax:
The most interesting syntax is this:
It takes the value corresponding to the a1 key in obj, and assigns it to a. If a1 is not defined then it
evaluates aDefault
With this syntax, we can satisfy the with expression since it’s now a valid object initializer. It also satisfies the
Function constructor since it’s simply assigning the value to a different name. So we can use this for RCE!
Let’s test out our payload with a reverse shell:
And we have RCE!
Better payloads
In the process of writing the report, I realized that there are easier payloads that can be used:
This payload is valid in both the with expression and the Function constructor.
With this payload, we can just bypass the with expression entirely, and just inject the payload into the Function
constructor.
Timeline
Note: When I first requested the CVE ID, I put my Gmail as the contact email, but didn’t receive any response. I’m
pretty sure that Gmail is blocking emails from mitre.org which can’t be disabled, so I re-requested the CVE with
Outlook.