← All posts

Grok Bot found malware on WP.org during a routine check.

Within Grok Bot I have a bot dedicated for WP Beacon. Every morning it reviews top plugin diffs on WP.org. After two weeks of running daily checks I got a ping. While I was working on something else it found malware sitting in trunk of a 40,000-install plugin.

The stable zip never moved, so auto-updates never shipped it to anyone. The plugins team closed the listing that afternoon and restored it the next morning after it was cleaned by the plugin author.

40k
Active installs on instawp-connect
23h
Infected trunk, then reverted
0
Overnight Wordfence hits
0.1.3.8
Zip the directory kept serving

The bot already had a job.

I did not sit down that morning to hunt malware. This particular bot runs every morning. Updates WP Beacon’s website for plugin closures first, then yesterday’s plugin diffs, then last night’s Wordfence results. Not a full Beacon scan. Wordfence runs on my WP Beacon plugin mirror. Not on my laptop.

A full Beacon scan does too much for a morning pass. Closures, detection events, catalog diffs, and a corpus Wordfence run do not belong in the same conversation. You miss the thing that actually shipped yesterday.

So the morning job is four smaller items. Most days it has nothing to say.

The morning job
CLOSURES
Keep the closed-plugin list current
Refresh recent closures. A security-issue close or a big temporary close is worth a look. Quiet author-request, skip.
DetectOnly the closures worth a closer look
SIGNALS
Pull last night, then detect
The mirror already walked SVN. Local Beacon pulls that feed and runs detect. Skip a local crawl. Only ping me on Confirm or Alert.
DetectConfirm and Alert events
DIFFS
Review the diffs Beacon already dumped
This is the one that caught it. WP Beacon dumps a unified diff for every plugin that got a commit yesterday. Grok Bot reviews that output. It does not generate the diffs. Only report stuff that looks like an attack.
DetectSuspicious and malicious only
WORDFENCE
Read last night’s Wordfence CSV
Overnight Wordfence scans new PHP and JS on high-install plugins. The morning job just reads the CSV. Stay quiet if hits=0. Do not start Wordfence on the laptop.
DetectCommodity malware on what shipped

Beacon dumps the diffs. The bot just reads them.

WP Beacon keeps a git copy of the top ~25,000 wordpress.org plugins. Every tagged version, real SVN authors and dates, public at plugin-repo.wpbeacon.io. Local copy is the source of truth. Production at wpbeacon.io is just a read-only push.

Overnight the box rebuilds whatever shipped. In the morning I do not need to talk to plugins.svn.wordpress.org. The history is already on disk.

A script called diff-day.sh walks every repo and dumps yesterday’s net change. One unified diff per plugin. PHP and JS stay, including minified. CSS, images, fonts, and translation files get dropped.

/srv/wporg/bin/diff-day.sh 2026-09-07
# → /srv/wporg/diffs/2026-09-07/_summary.tsv
# → /srv/wporg/diffs/2026-09-07/<slug>.diff

A typical day is a couple hundred plugins. Most of that is version bumps and asset-only releases. Beacon dumps the diffs. Grok Bot just reviews them. Same files I would read with /beacon-audit-diff by hand, except I was doing other work.

Wordfence is a separate pass on the same window. It wants file contents, not a unified diff. Overnight it scans the new PHP and JS from plugins at 10,000 installs and up. Fine for commodity malware. It will not catch the structural stuff Beacon already watches: update-channel hijacks, author flips, dormant backdoors.

The ping from Grok Bot.

After two weeks of quiet, this is what showed up while I was working.

Grok Bot Session
Grok Bot
Morning refresh is live (122,338 / 50,060). Big one first: instawp-connect (40k) has a malicious inject in yesterday’s catalog. tailwind.config.js was padded with a Node Ethereum-oracle C2 dropper. It pulls C2 IPs from txs on 0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a, XOR-decrypts, then eval / detached node spawn. Commit 11b710c by instawp (“Update to version incident-14ypaj0acdw from GitHub”). Live on the mirror (~9.8k vs ~2.2k prior). Only this slug in yesterday’s diffs. Overnight Wordfence was hits=0. Node/build-config payload. It likely never looked.
Grok Bot
Want me to start a full malware audit on instawp-connect? This one’s clearly malicious.
Austin
Sure

Wordfence never saw it. Grok Bot did.

instawp-connect has 40,000 active installs. It was in scope for the overnight Wordfence pass. Still hits=0.

Not a slam on Wordfence. The payload lived in tailwind.config.js. It only runs if someone evaluates that config during a Node build. PHP page loads never touch it. Scanners built for webshells in PHP are looking at a different class of file.

Grok Bot does not need a signature. It reads what Beacon dumped. A 2.2 KB Tailwind config became 9.8 KB. The extra bytes looked up Ethereum transactions, XOR-decrypted a remote payload, and spawned detached Node loaders. That is not a Tailwind change. That is malware.

The payload was sitting in a Tailwind config.

InstaWP Connect is a staging and migration plugin. Normal Tailwind config at the top of the file. Then the module.exports block closes. Then a bunch of padding. Then minified Node.

The dropper sets an Ethereum address, queries public ETH RPCs and Blockscout for recent transactions, decodes the first eight bytes of tx.to as two IPv4s, fetches loaders at /0x/cls and /0x/ls, XOR-decrypts a body or an X-Payload-B64 header, and runs the recovered JavaScript in-process and via detached node -e. Campaign id in the sample: A8-6369-1. Same family as npm malware MAL-2026-12217 (tailwind-animate-plugin).

Indicators from audit #53
Filetailwind.config.jsPadded 2,200 → 9,794 bytes. Legitimate Tailwind, then the dropper.
ETH0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1aOracle address. C2 IPs rotate via new transactions.
IP193.247.144.38On-chain C2 observed 2026-09-08. Do not connect. IPs rotate.
Path/0x/cls /0x/lsDetached Node loaders fetched from the decoded IPs.
HeaderX-Payload-B64XOR-decrypted remote payload, then eval / node -e.
FamilyMAL-2026-12217Same Ethereum-oracle Node dropper as npm tailwind-animate-plugin. Campaign A8-6369-1.

Trunk is not the release.

Trunk on wordpress.org is the staging copy of a plugin. The next release lives there while the author works. It is not what the directory serves. When you hit Install or Update you get a tagged zip. Trunk can be dirty, tagged, reverted, and dirty again without auto-updates moving an inch.

The same commit bumped the readme Stable tag toward 0.1.3.9. No tags/0.1.3.9 was ever published. downloads.wordpress.org kept serving 0.1.3.8.

If you installed from the directory, or you auto-update from the directory, you did not get this payload. The attacker padded trunk and never cut a tag. The plugins team said the same thing later: the attacker was not familiar with SVN. That complexity saved us this time.

Trunk was infected for about 23 hours. Risk is anyone who pulled trunk during that window, or ran Node against the plugin tree, including CI.

Aug 12, 2026 05:38 ET
r3643122. Update to version 0.1.3.8 from GitHub. Clean stable.
Sep 7, 2026 06:38 ET
r3684673 / Forgejo 11b710c. “Update to version incident-14ypaj0acdw from GitHub.” tailwind.config.js padded 2.2 KB → 9.8 KB with the ETH-oracle Node dropper. Readme Stable tag text bumped to 0.1.3.9. A benign Sync DB fix rode along in the same commit.
Sep 7 catalog diff
Beacon dumps yesterday’s diffs. Grok Bot flags this slug. Sole IOC in the day’s catalog for instawp-connect.
Overnight Wordfence-on-diffs
hits=0. Node/build-config payload sits outside the PHP malware scan path.
Sep 8, 2026 05:39 ET
r3686315 by @instawp. “Remove accidental publish; restore stable 0.1.3.8.” Trunk config back to 2.2 KB. Still no 0.1.3.9 tag.
Sep 8, 2026 morning
Grok Bot reviews Beacon’s Sep 7 diffs, publishes audit #53, drafts plugins@wordpress.org. Listing closed the same afternoon, temporary pending review.
Sep 9, 2026
wordpress.org fully restores the listing. Served 0.1.3.8 zip and SVN trunk tailwind.config.js are clean of the dropper IOCs. Audit #53 marked cleaned.
If you only ever installed the wp.org stable zip, you were not auto-updated into this
No 0.1.3.9 tag was published. As of September 9 the listing is open again and Beacon verified the served zip plus trunk config are clean. Optional sanity check: tailwind.config.js should be about 2.2 KB and must not contain the ETH oracle address, X-Payload-B64, /0x/cls, or child_process. Fast check: wp plugin verify-checksums instawp-connect.
If you pulled trunk or ran a Node build against this plugin between Sep 7 06:38 and Sep 8 05:39 ET
Treat that machine as potentially compromised. This dropper does not run on ordinary PHP page loads. It runs when Node evaluates tailwind.config.js. Rotate secrets that were present on that laptop or CI runner. Reinstall from the current directory zip after verifying size. Directory and trunk builds checked on September 9 do not contain the dropper.

Looked like a compromised publish path, not a hostile vendor.

Sole committer the whole time was @instawp. The inject and the revert both came from that identity. No new account showed up the day before. The same commit mixed a benign Sync DB fix under a version label that looked like an incident ticket. Next morning: “Remove accidental publish; restore stable 0.1.3.8.”

Sibling plugins on the same SVN account scanned clean. string-locator at 100,000 installs. instamigrate at 20. Same commit bit, no IOC.

MAL-2026-12217 is a known Ethereum-oracle Node campaign that pads Tailwind configs. That is not how a staging-plugin vendor backdoors their own product. It is how a compromised GitHub-to-SVN publish path gets used by someone who does not know that wordpress.org serves tags, not trunk, to the update API.

Grok Bot Session
Grok Bot
instawp-connect audit is live: malicious · #53. Still open on wp.org at 40k. Stable zip stayed on 0.1.3.8.
Grok Bot
Sole committer @instawp the whole time. Reads more like a compromised GitHub→SVN path than intentional. Siblings string-locator / instamigrate clean. Payload only runs if someone builds with Node, not on PHP page loads. Wordfence overnight still missed it (build-config blind spot).
Austin
Can you draft an email to the WordPress plugins team regarding this finding?

I had Grok Bot draft the email to the WordPress plugins team. I reviewed the email and hit send.

Plugins team closed it that afternoon.

I asked them to review trunk, consider a temporary close, and lock down the GitHub-to-SVN publish path until they were satisfied. I said it looked like a compromised publish path, not intentional malice. Trunk had been infected for roughly 23 hours.

Their read matched the audit. They closed the plugin. They believe the author would have done this much better than that. Luckily the attacker was not familiar with SVN. They also thanked Beacon for the catalog work, which I appreciate.

Vikas Singhal, who builds InstaWP, replied on my post a little after noon. “Yes already fixed for connect plugin.” That matches the early-morning revert.

They put it back the next morning.

September 9, wordpress.org fully reopened the listing. Downloads serve instawp-connect.0.1.3.8.zip again. Beacon rescanned. The served zip and SVN trunk tailwind.config.js (2,200 bytes) contain none of the dropper IOCs. Audit #53 is now marked cleaned.

Most days the bot has nothing to say. After two weeks it did. Now I get where Grok Bot fits into my AI mix of tools. Grok Bot is really good about handling scheduled repeat work. I’ve since started to have and more checks scheduled for Grok Bot to review. If anything needs my attention, I get a message from Grok Bot. This feels like the future.

The public write-up is audit #53
Full timeline, dropper mechanics, sibling fan-out, and the Sep 9 restore: wpbeacon.io/audits/53. Related reading on how these attacks actually land: the six ways in, and the earlier scroll-top side channel.
Austin Ginder
Subscribe