New Technique Used by Attackers in NPM to Avoid Detection

Checkmarx SCS team recently detected several malicious NPM packages using a new evasion technique, enhancing dependency confusion attacks to help malicious packages avoid detection.

This novel technique tries to avoid being detected by security scanners or AppSec platforms, which sometimes only look at the latest version of a package. It does so by publishing a benign latest version after a burst of malicious versions with high version number.

Intro

In the past few weeks, the Checkmarx SCS team has detected 14 malicious packages exhibiting a new behavior. They all have a benign latest version along with multiple malicious versions trying to exfiltrate the machine’s environment variables.

At first glance, it seems that this kind of technique will defeat the purpose of the attack, as the malicious functionality won’t be installed by end-users using the “npm install” command, which automatically serves the package’s latest version. However, these end-users are not the attackers’ targets.

It seems the attackers’ intent is to target automatic updates of “patch” and “minor” versions as part of dependency confusion attacks.

How NPM Versioning Works

To better understand this nuance, some background on NPM and semantic versioning (semver) is required. Great resource about semver is https://semver.org/ and is highly recommended to checkout.

In a nutshell, a valid semver version is constructed from 3 parts: “major”, “minor”, and “patch”

  • Major – Promoted when making significant version changes, usually known breaking changes
  • Minor – Promoted when adding new features
  • Patch – Promoted when fixing issues, either functionality or security

As previously mentioned, using the “npm install” command will install the latest version of the package even if the version number is not the highest among the versions that were released; however, most package installations are performed as a dependency of other packages. These dependencies are specified in the package.json file of the package:

...
"dependencies": {
  "dep": "3.3.3",
  "latest_dep": "*",
  "my_dep": "^1.0.0",
  "another_dep": "~2.2.0",
},...

The example above shows 4 options for consuming dependencies:

  1. The “dep” package will install precisely the “3.3.3” version of the package and no other (commonly known as version pinning).
  2. The “latest_dep” will install the package’s latest version available.
  3. The “my_dep” includes the Caret sign (^) prior to the version number, in which case NPM will pull “patch” and “minor” updates to the specified version. For instance, version 1.1.0 or 1.0.1 but not 2.0.0.
  4. The “another_dep” includes the Tilda sign (~) prior to the version number, in which case NPM will only pull “patch” updates to the specified version. For instance, version 2.2.1 but not 2.3.0 or 3.0.0.

The Technique

With this understanding of NPM versioning mechanism and the original dependency confusion technique, it is easier to understand the attackers’ intent.

The attacker releases a burst of versions for each package, almost all of them with low “major” version number and high “minor” and “patch” version numbers. The low “major” component of the version is intended to match the one used by the victim and the high “minor” and “patch” components are made to ensure automatic update in case the dependency version is not pinned to a specific one.

Finally, the attacker releases one last version with low version number to function as the latest version of the package. This version contains legitimate code that in many cases is even related to the package’s name and seems to be borrowed from a related package.

Releasing a package in this manner, as seen in the image above for example, will result in the following outcomes:

  • The dependency confusion attack will work – a package dependent on the malicious package without version pinning, such as ‘: “^3.1.0”’,  will download and install the malicious version “3.758.11”
  • A security scanner that scans only the latest version of a package will deem it legitimate.

The Attack

Aside from using this novel technique, this cluster of 14 malicious packages discover in the last few weeks has several other characteristics in its package:

1 – The objective of these attacks is to exfiltrate all the victim machine’s environment variables to the attacker via a webhook of the legitimate service “pipedream”:

2 – Sandbox detection

Another evasion technique the attacker employed is trying to avoid running on environments suspected as sandboxes. This attempt was implemented by filtering out specific values of environment variables.

var filter = [
  { key: ['npm', 'config', 'registry'].join('_'), val: ['taobao', 'org'].join('.') },
  { key: ['npm', 'config', 'registry'].join('_'), val: ['registry', 'npmmirror', 'com'].join('.') },
  { key: 'USERNAME', val: ['daas', 'admin'].join('') },
  { key: '_', val: '/usr/bin/python' },
  { key: 'npm_config_metrics_registry', val: ['mirrors', 'tencent', 'com'].join('.') },
  [
    { key: 'MAIL', val: ['', 'var', 'mail', 'app'].join('/') },
    { key: 'HOME', val: ['', 'home', 'app'].join('/') },
    { key: 'USER', val: 'app' },
  ],
  [
    { key: 'EDITOR', val: 'vi' },
    { key: 'PROBE_USERNAME', val: '*' },
    { key: 'SHELL', val: '/bin/bash' },
    { key: 'SHLVL', val: '2' },
    { key: 'npm_command', val: 'run-script' },
    { key: 'NVM_CD_FLAGS', val: '' },
    { key: 'npm_config_fund', val: '' },
  ],
  [
    { key: 'HOME', val: '/home/username' },
    { key: 'USER', val: 'username' },
    { key: 'LOGNAME', val: 'username' },
  ],
  [
    { key: 'PWD', val: '/my-app' },
    { key: 'DEBIAN_FRONTEND', val: 'noninteractive' },
    { key: 'HOME', val: '/root' },
  ],
  [
    { key: 'INIT_CWD', val: '/analysis'},
    { key: 'APPDATA', val: '/analysis/bait'}
  ]
];

Should one of these values appears in the environment variable the code breaks from its malicious functionality and won’t exfiltrate this sensitive information.

var data = process.env || {};
  if (
    filter.some((entry) =>
      [].concat(entry).every((item) => (data[item.key] || '').includes(item.val) || item.val === '*')
    ) ||
    Object.keys(data).length < 10
  ) {
    return;
  }

During the past few weeks, we observed that the “filter” variable grows bigger to include more environment variables, identifying more scanners that the attackers want to avoid, which indicates that they are learning and improving their capabilities.

3 – obfuscation

Some of the packages in these attacks were also using obfuscation and double obfuscation to hinder researchers from studying the packages’ code.

IOC’s

  • bd43527e019ae8efb47eb39bb9313ac5.m.pipedream[.]net
  • eol8qq3niztjn4p.m.pipedream[.]net
  • eolock03iutvcf7.m.pipedream[.]net
  • c2e7a0cfd4f45f811ede4b077ae48dfa.m.pipedream[.]net
  • eoxb1lay4m45mms.m.pipedream[.]net
  • eof0n35fe1w7o9j.m.pipedream[.]net
  • eobn5xcv41edv52.m.pipedream[.]net
  • 1268384ff816fb002f65302287b639ce.m.pipedream[.]net
  • dbky7khu4dz2.m.pipedream[.]net
  • eorthox7nn7e4fg.m.pipedream[.]net
  • eovsmsusn4979sc.m.pipedream[.]net
  • eo9jtk2svcyyaus.m.pipedream[.]net
  • eojea1m1h2bi4nw.m.pipedream[.]net

Conclusion

Dependency confusion attacks continue to flood NPM ecosystem and introduce new techniques to make it harder for defenders to detect and block them. The technique described above demonstrates the advantages of “version pinning” while writing a new package. This approach has its own shortcomings, such as a potentially longer timeframe in which your package could be exploitable by new vulnerabilities, but these should be considered against the alternative of leaving your package susceptible to these kinds of attacks.  

Packages

Skip to content