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.
GHSL-2021-023/app.js - vulnerable server code.
GHSL-2021-023/index.squirrelly - template.
Start the vulnerable application server.
PROOF OF CONCEPT
Start a Netcat listener on port 443.
Send the crafted payload via curl.
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.
ANALYSIS
Send a request to start the analysis.
The ExpressJS calls renderFile function from the SquirrellyJS engine after the request has been made (view.js line 135).
this.engine variable is the renderFile function.
renderFile(filename, data, cb)
definition (file-handlers.ts line 113).
Parameters
filename is the template path.
data is the template data that contains the request query.
cb is a callback function that is defined in another scope.
renderFile function calls getConfig function (line 3).
getConfig(override, baseConfig)
definition (config.ts line 101).
override parameter is the template data that contains the request query.
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:
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)
definition (file-handlers.ts line 69).
Parameters
options is the set of compilation options.
data is the template data that contains the request query.
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)
definition (file-handlers.ts line 43).
options parameter is the set of compilation options.
handleCache function gets the template (index.squirrelly) content from a file, then handleCache function calls compile function (line 18).
compile(str, env)
definition (compile.ts line 14).
Parameters
str is the content of the template (index.squirrelly)
env is the set of compilation options.
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:
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)
definition (compile-string.ts line 12).
Parameters
str is the content of the template file (index.squirrelly).
env is the set of compilation options.
compileToString function defines buffer and calls parse function (line 2) to prase the template content and its variables
compileToString function defines res variable for the ctor function body (line 3)
useWith is false
compileToString function calls compileScope function to append its return value to res variable.
compileScope(buff, env)
definition (compile-string.ts line 101).
Parameters
buff is an array that contains a parsed template content.
env is the set of compilation options.
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
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
More code is appended to filtered (line 26).
compileScope function returns returnStr (line 39), which is a part of the function body
res variable in compileToString function scope is
The anonymous function that is going to be executed
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:
The condition (line 21) in compileScope function must be true to append the defaultFilter to the content.
The appended code syntax must be correct to get code execution.
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
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.
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.
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.
4. defaultFilter=e')); console.log('Remote Code Execution') //
Add a single-line comment to remove the next portion that comes after your injected code.
REFERENCES
Last updated