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. 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. 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: 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: According to the Microsoft Security Response Center, CWD DLL planting is an important security issue, and Microsoft will issue a patch for it. We started our research after we noticed some unusual behavior when running an npm command from the command line: Process Monitor (Sysinternals) with filters for “Process Name” = “node.exe” and “Result” include “NOT FOUND” 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: 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: 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. 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 Files\Common Files\SSL\openssl.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: These conditions will reproduce the vulnerability. 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: For more technical details about the vulnerability and the fix, go to the GitHub documentation. 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. 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.What is DLL hijacking?
Windows search order: Overview
CVE-2022-32223 discovery: Technical details
Getting to the root cause
Why npm CLI searches for providers.dll
Identifying the conditions for CVE-2022-32223
The patch for CVE-2022-32223
Use case for the vulnerability inside a JS package
Mitigation and summary
Discovery timeline