Two weeks ago, a Windows trojan slipped into my MCP (Model Context Protocol) marketplace. The malware was Trojan:Win64/Lazy.PGPK!MTB
, hidden inside a nested zip in a skill package.
This is the technical writeup of what happened, what I built to prevent it, and the architecture of the 8-layer defense pipeline now running in production.
Vector: Typosquatting GitHub repository
A legitimate MCP server prospector-mcp-email-finder
existed. An attacker created a typosquatting copy at JuanquiFortuny/prospector-mcp-email-finder
(now taken down) with an identical README β but with a "Download Latest Release" badge linking to a malicious zip on raw.githubusercontent.com
.
The payload:
Application.cmd β 'start unit.exe package.txt'
package.txt β 298 KB of obfuscated Lua bytecode
unit.exe β 872 KB PE32+ Windows executable
Windows Defender flagged unit.exe
as Trojan:Win64/Lazy.PGPK!MTB
. The Application.cmd β unit.exe + package.txt
pattern is a classic staged launcher β small CMD wrapper executes the binary with the bytecode payload as input.
How it got in: My import script (which fetches MCP servers from GitHub) downloaded the zip and committed it to dist/skills/
without scanning inside it. My existing audit pipeline (Sentinel L1.5 + L1.6) only checked skill metadata β name, description, system_prompt. It never looked inside the actual package.
Cheapest layer. Runs on every skill's metadata:
ignore previous instructions
)Access-Control-Allow-Origin: *
(warning)18 Semgrep-equivalent rules implemented as JS regex (no Semgrep binary needed):
18 secret detection patterns:
sk_live_*
, sk_test_*
)ghp_*
, gho_*
, ghs_*
)AKIA*
, aws_secret_access_key
)OSV API for known vulnerable dependencies.
Bug fix (from peer review): process.env.X
references were triggering MCP-SL-001 (hardcoded API key). Fixed by stripping process.env.*
patterns before running secret detection.
This is the layer I built after the trojan incident. It opens the package zip (recursively β zips inside zips) and scans for:
const BINARY_EXTENSIONS = ['.exe', '.dll', '.scr', '.msi'];
const LAUNCHER_EXTENSIONS = ['.bat', '.cmd', '.vbs', '.ps1'];
Plus 8 regex patterns for:
start X.exe Y.txt
(the exact prospector signature)function(o,R,F,U,b,p,E,M,Z,W,...)
(high-arity function signature)raw.githubusercontent.com/.../...zip
-encodedcommand
with long base64eval(atob(...))
obfuscationQuarantine flow: Any critical finding β score 0, risk_level critical
, status quarantined
, removed from catalog, listed at /api/security?view=quarantine
.
YARA-equivalent rules for specific malware families, each with MITRE ATT&CK technique IDs:
| Family | MITRE | What it does |
|---|---|---|
| Win64/Lazy.PGPK | T1027.002 | The trojan that hit us |
| Emotet | T1027.011 | Banking trojan, Office macro delivery |
| Cobalt Strike | T1071.001 | Post-exploitation beacon |
| Mimikatz | T1003.001 | LSASS credential dumper |
| QakBot | T1027 | Banking trojan |
| TrickBot | T1027 | Modular trojan, ransomware precursor |
| Agent Tesla | T1056.001 | Keylogger |
| RedLine Stealer | T1555 | Browser credential theft |
| Vidar Stealer | T1555 | Browser + crypto wallet theft |
| Raccoon Stealer | T1555 | MaaS stealer |
| LummaC2 Stealer | T1555 | Telegram-sold stealer |
| AsyncRAT | T1071.001 | Open-source RAT |
| njRAT | T1071.001 | .NET RAT |
| Remcos RAT | T1071.001 | Commercial RAT |
| SolarMarker | T1027 | SEO-poisoned backdoor |
| Lokibot | T1555 | Credential stealer |
| DoS tools | T1499 | hping3, slowloris, goldeneye |
Any match β instant quarantine.
Inspects every incoming HTTP request:
SQLi (7): UNION SELECT, OR 1=1, stacked, time-based, info_schema, hex
XSS (7): script, event handlers, javascript:, data:, vbscript:, img/svg
Path traversal (5): ../, encoded, /etc/passwd, /proc/self, Windows
SSRF (6): AWS/GCP/Azure metadata, file://, gopher://, dict://
Command injection (5): backticks, $(), chained, pipe, && ||
NoSQL (3): $where, $ne, $gt
Prototype pollution: __proto__, constructor.prototype
SSTI: Jinja2 {{ }}, Twig {% %}, JS ${ }
Log injection: \n \r header injection
Auto-ban after 5 WAF hits in 10 minutes (1-hour ban).
Fake vulnerable paths that auto-ban scanners for 24 hours:
/.env
β serves fake env file with canary tokens/admin
β fake admin login form/wp-admin
β fake WordPress login/.git/config
β fake git config/.aws/credentials
β fake AWS credentials/.ssh/id_rsa
β fake SSH key/phpmyadmin
, /backup.sql
, /server-status
, /.DS_Store
, etc.Each hit: IP banned 24h + logged publicly at /api/security?view=honeypot
.
Real-time IOC feeds from abuse.ch (free, no API key):
Used to check skill source URLs and file hashes.
If any layer flags a skill as critical/high:
_data/quarantine/
/api/security?view=quarantine
for transparencyRe-audited all 7,063 skills with the 8 layers. 0 in quarantine. The catalog was clean except for the one we already removed.
Request β Honeypot check β WAF (40 rules) β Handler
β
Skill import β L1.5 (6) β L1.6 (36) β L1.7 (8) β L1.8 (17) β Quarantine?
β no
Catalog (public)
β yes
_data/quarantine/
(publicly listed)
Stack: Vercel Hobby ($0/mo), GitHub Actions (free), Docker + gVisor for L2 sandbox, abuse.ch feeds.
β Edison Flores, AliceLabs LLC