Aqua Team Nautilus recently discovered that all Node.js versions earlier than 16.16.0 (LTS) and 14.20.0 on Windows are vulnerable to dynamic link library (DLL) hijacking if OpenSSL is installed on the host. Attackers can exploit this vulnerability to escalate their privileges and establish persistence in a target environment. The vulnerability can also provide another way to embed malicious code into packages.
We reported this vulnerability to the Node.js team, who patched it in their latest security release. To mitigate the risk, Windows users of Node.js must upgrade to the latest version.
In this blog, we examine CVE-2022-32223, what DLL hijacking is, how we discovered the new vulnerability, and the way binary artifacts in your repository can be dangerous.
What is DLL hijacking?
DLL hijacking attacks, also known as binary planting or preloading attacks, are common to all operating systems that support dynamically loaded shared libraries or shared objects. Let’s take a look at how these attacks work.
Whenever an application loads a DLL without specifying a fully qualified path, Windows searches in a specific order from well-defined directories paths to locate the desired DLL.
If attackers gain control of one of the directories, they can force the application to use a malicious copy of the DLL instead of the DLL that’s expected. In that sense, DLL hijacking can allow an attacker to execute code under the context of the user who runs the application.
When an application is run as an administrator or with high privileges, this can lead to local privilege escalation. In addition, bad actors might use DLL hijacking to evade restrictions on file execution or to establish persistence in the environment.
For example, the Crutch backdoor used by Turla, a Russian-based threat group, can persist via DLL hijacking on Google Chrome, Mozilla Firefox, and Microsoft OneDrive.
Windows search order: Overview
Consider that an application tries to load a DLL. If the safe DLL search mode is enabled (SafeDllSearchMode is set to true by default), and the application isn’t using an alternative search order, the following directories will be searched:
- The directory from which the application is loaded.
- The system directories
- The Windows directory
- The current working directory (CWD)
- The directories that are listed in the PATH environment
DLL hijacking occurs when an attacker can plant a malicious DLL in any of the directories searched during the search order (write access is required) and the desired DLL wasn’t found during the previous searches.
DLL hijacking is convenient for an attacker: it provides easy code execution because the DllMain() gets called immediately after the DLL gets loaded. An attacker doesn’t have to worry about bypassing mitigation if the application allows loading of unsigned binaries.
Based on where the malicious DLL can be planted in the DLL search order, the vulnerability falls into one of the three categories:
- Application directory (App Dir) DLL planting
- Current working directory (CWD) DLL planting (the vulnerability we discovered falls under this category)
- PATH directories DLL planting (requires admin privileges)
According to the Microsoft Security Response Center, CWD DLL planting is an important security issue, and Microsoft will issue a patch for it.
CVE-2022-32223 discovery: Technical details
We started our research after we noticed some unusual behavior when running an npm command from the command line:
In the figure above, node.exe searches for a DLL called providers.dll after we ran the npm command.
Because providers.dll doesn’t exist in any of the paths of the DLL search order we outlined above, we can plant it inside the current working directory and wait until the application will load the “malicious” providers.dll.
We prepared a proof of concept (PoC) to demonstrate our findings. We created a malicious DLL file (providers.dll) and complied it. In our case, it opens calc.exe. You can see the code below:
If we run the npm command (for example, npm –version) again, the code from the “malicious” DLL runs:
Getting to the root cause
So, we introduced the new vulnerability and showed how it can be reproduced. However, what exactly is causing this vulnerability remained unclear. At this point, we had a couple of unanswered questions:
- What causes the npm command line interface (CLI) to search for providers.dll?
- Are there any prerequisites for the providers.dll to be looked for?
Why npm CLI searches for providers.dll
The first question arose when we used the Node.js engine and couldn’t reproduce the vulnerability. For example, when we typed a simple command, let’s say console.log(“*”), Node.js doesn’t search for the providers.dll file. This led us to believe that Node.js is not the root cause of this vulnerability.
Next, we assumed that this vulnerability is caused by the npm CLI. We further investigated this idea by analyzing the flow of dependencies for the npm CLI. We found that the npm CLI was trying to load three built-in Node.js modules called crypto, https, and tls.
It turned out that each one of those three Node.js built-in modules was looking for the providers.dll file. This means that the vulnerability doesn’t stem from the npm CLI, but these modules in the Node.js engine cause the npm CLI to be vulnerable.
Below you can see our trace analysis of the npm CLI dependencies:
What do all these modules have in common? They all depend on OpenSSL.
To sum it up, we found out that what causes the npm CLI to look for providers.dll is three built-in modules of Node.js related to OpenSSL.
Identifying the conditions for CVE-2022-32223
The second question came up since the vulnerability didn’t recur when using a clean installation of a Windows image with only Node.js.
We returned to the same Windows machine where we found this behavior and started x64dbg, an open source debugger for Windows, to debug the Node.js after requiring the crypto module from the CLI.
After this, we set breakpoints on LoadLibraryA of kernel32.dll to catch all the attempts to load providers.dll.
Next, we returned to node.exe by viewing the call stack. Here, we can see two interesting values. One of them is provider_sect and the second is the path C:Program FilesCommon FilesSSLopenssl.cn.
We then took a closer look at the flow of node.exe. The program reads the openssl.conf file and looks for the value that is defined for provider_sect. In our case, this value is providers, and therefore it looks for the file providers.dll by default.
In addition, the value of the provider_sect can be changed to any value you want, and when node.exe is triggered, it will attempt to load the custom DLL name. Editing openssl.cnf requires administrative privileges, so it’s more relevant for a persistence scenario.
Based on the findings above, we determined the prerequisites for the vulnerability whenever Node.js is running:
- Node.js below version 16.16.0 (LTS) or 14.20.0 has been installed.
- One of the three built-in modules, crypto, https, or tls, is loaded.
- The default OpenSSL configuration file (openssl.cnf) is located in the path C:/Program Files/Common Files/openssl.cnf (this assumption seems reasonable because most of the time these are developer computers that use Node.js, in which the above program is likely to be installed)
These conditions will reproduce the vulnerability.
The patch for CVE-2022-32223
To address the vulnerability, the patched version has been released. From now on, Node.js can use an OpenSSL configuration file by specifying the environment variable OPENSSL_CONF or by using the command line option —openssl-conf. If none of those are specified, it will default to reading the default OpenSSL configuration file openssl.cnf. Node.js will only read a section that is by default named nodejs_conf.
If your installation was using the default openssl.cnf file and is affected by this change, you can go back to the previous behavior by:
- Adding –openssl-shared-config to the command line (Node.js 18.5.0 only); or
- Creating a new nodejs_conf section in that file and copying the contents of the default section into the new nodejs_conf.
For more technical details about the vulnerability and the fix, go to the GitHub documentation.
Use case for the vulnerability inside a JS package
Consider the following scenario in which an attacker uploads a package that contains the malicious DLL to npm or other package managers.
First, we’ll create package.json with a postinstall command that includes an unsuspecting npm command, such as npm -version, npm bug, or npm audit.
We’ll also copy the “malicious” DLL to the same folder and publish the package.
Then, we’ll install the providers-win-package in a new project folder. As you can see, the code from the DLL is running:
Obviously, malicious code can always be written in one of the js files, or a calling to a remote malicious DLL /native add-on in the malicious package code.
In addition, we need to remember that the security model of npm — or any other package registry — implies that you explicitly trust whatever you ask to be installed.
However, this vulnerability can provide another way for attackers to embed malicious code into packages, which can go unnoticed by developers who aren’t familiar with DLL hijacking.
Therefore, it might be hard to detect threats in packages containing this DLL, which isn’t referenced at all by any code in the package, so developers might assume this DLL won’t be loaded.
Mitigation and summary
In this blog, we examined the recently discovered vulnerability CVE-2022-32223 in Node.js. This issue has been fixed. Therefore, we highly recommend that Node.js Windows users upgrade to the patched version of Node.js.
These days, many repositories contain binary artifacts. We need to remember that developers often will use executables directly if they’re included in the source repository or package.
Developers can’t easily view the code in these binary artifacts, and in some cases it’s not provided in the repository, so some of these artifacts might contain malicious code.
To ensure the safety of your users, we strongly recommend that you avoid embedding binary artifacts in your repositories or packages.
Ultimately, developers are responsible for what open source projects and packages they use when building applications. To that end, it’s important to use trusted sources for any third-party components and to secure your environment with solutions that can detect software supply chain threats, including suspicious binary artifacts in repositories and packages.
- January 12, 2022: The issue was reported to the Node.js program at HackerOne.
- January 13, 2022: Initial response received from Node.js that they started looking into the issue.
- February 7, 2022: Vulnerability was confirmed by the Node.js security team.
- July 7, 2022: Vulnerability was patched in the Node.js July 7th Security Releases.