防护思路
点击放大

![CI]()

![Node.js]()
KC2 is an educational research lab demonstrating an indirect
command-and-control (C2) architecture that uses **npm dist-tags as the
communication mailbox**,
inspired by the npm-c2 research. Built for understanding C2 channel design and
defender detection — not for offensive use.
> Scope / ethics: the agent executes only hard-coded mock tasks
> (26 allowlisted operations — see src/common/ops.js: echo, ping, time,
> sysinfo, whoami, env, netinfo, ps, df, pwd, cd, ls,
> stat, find, hash, getfile, geolocate, plus eight
> /audible fun ops, plus the opt-in exec demo operation).
> The opt-in exec demo operation can run an explicitly requested program
> without shell parsing; there is no persistence, pri escalation,
> obfuscation, or encryption. Path operations are not confined to a
> filesystem root, and
> getfile reads one file under a small size cap. Desktop-affecting
> operations and en disclosure are disabled by default.
> Payloads are deliberately plain base64 so
> every artifact is readable and analyzable. The required workflow uses a
> public npm test package you own.
> Do not point this at packages or accounts you do not own.
The two sides ne to each other. The attacker writes command
dist-tags; the polls the package's dist-tags, executes the mock
task, and writes result dist-tags back. All payloads travel in tag
names (tag must be real , so they are pinned to 1.0.0).
Package files are ne after the initial publish.
Deep dives:
docs/protocol.md — tag grammar, encoding, chunking, sequencing, state machinedocs/architecture.md — components, why are immutable / dist-tags mutable, ad & limitationsdocs/detection.md — defender's view: IoCs, beha detections, Splunk/Elastic/Sigma/YARA examplesUse a public, throwaway npm test package that you own. Do not use an existing
or shared package: package dist-tags and their payloads are world-readable.
Both sides read the package-scoped token from env.sh or the NPM_C2_TOKEN
en . env.sh.example includes the required npm registry and
public-registry opt-in, and explicitly disables risky capabilities so it
o any permissive config.json . Replace the package-name
placeholder and token before starting either process.
In the CLI:
The CLI polls in the background while you type: agent disco and task
completions/failures print as li — no watch needed.
To reset the lab: stop the and attacker, delete the local state files
and chains.json, then manually remove the test package's KC2 dist-tags withnpm dist-tag rm. Keep the published 1.0.0 placeholder .
The public npm test package workflow is required. Before running it:
1.0.0 once, e.g. scoped under/c2-lab-test);NPM_C2_TOKEN;docs/detection.md;Set your package name and token in env.sh (see below), which keeps secrets
out of config files entirely. NPM_C2_ALLOW_PUBLIC_REGISTRY=true is required;
startup otherwise fails closed. Do not enable insecure HTTP for this workflow.
Copy config.example.json to config.json (both sides read it) and update
only the authorized features needed for the exercise, or o with
en . Keep the npm token out of config.json.
Recommended: env.sh. Both sides auto-load an env.sh file in the
project root at startup (KEY=VALUE or export KEY=VALUE lines). It is
git-ignored and is the intended place for the token — no pasting secrets
into your shell history, and both npm run and npm run attacker
pick it up no matter which terminal they run in:
Real en always win over env.sh . UseNPM_C2_EN=/path/to/file to load a different file. On POSIX systems,
group- or world-readable en are refused.
The table lists built-in runtime defaults. The local-registry fallback
exist for backward compatibility and are not the supported workflow; use the
public npm in config.example.json or env.sh.example. Replace the
package-name placeholder before use. The examples keep risky demo capabilities
disabled; enable them only for a specific, authorized exercise. SetNPM_C2_LOG_FILE= to disable file logs.
| Key / env var | Meaning | Default |
|---|---|---|
| registryUrl / NPM_C2_REGISTRY_URL | registry base URL | |
| packageName / NPM_C2_PACKAGE_NAME | mailbox package | my-package |
| pollInter / NPM_C2_POLL_INTERVAL | poll interval | 10 |
| agentId / NPM_C2_AGENT_ID | id (generated + persisted if empty) | "" |
| logFile / NPM_C2_LOG_FILE | append-only log file ("" disables) | "" |
| stateFile / NPM_C2_STATE_FILE | state file path (role default if empty) | "" |
| maxFileBytes / NPM_C2_MAX_FILE_BYTES | max bytes returned by getfile (hard cap 1 MiB) | 32768 |
| re / NPM_C2_RE | let env return real instead of <redacted> (true/1/yes) | false |
| downloadDir / NPM_C2_DOWNLOAD_DIR | attacker destination for recei | downloads |
| enableFunOps / NPM_C2_ENABLE_FUN_OPS | enable attended-host desktop actions | false |
| enableGeolocate / NPM_C2_ENABLE_GEOLOCATE | enable the geolocate task (WiFi positioning demo; discloses host location) | false |
| geolocateSer / NPM_C2_GEOLOCATE_URL | MLS/Ichnaea-compatible WPS endpoint (recommended: beaconDB, no key — see table below); empty = WiFi-scan-only mode | "" |
| geolocateSer / NPM_C2_GEOLOCATE_KEY | API key appended as ?key= to the WPS endpoint | "" |
| allowPublicRegistry / NPM_C2_ALLOW_PUBLIC_REGISTRY | required opt in to registry.npmjs.org | false |
| allowInsecureHttp / NPM_C2_ALLOW_INSECURE_HTTP | permit token use o HTTP | false |
| logLevel / NPM_C2_LOG_LEVEL | debug/info/warn/error | info |
| requestTimeoutMs / NPM_C2_REQUEST_TIMEOUT_MS | per-request timeout, 100–120000 ms | 10000 |
| maxRetries / NPM_C2_MAX_RETRIES | retries after the first attempt, 0–10 | 3 |
| retryBaseDelayMs / NPM_C2_RETRY_BASE_DELAY_MS | exponential-backoff base, 10–60000 ms | 500 |
| — / NPM_C2_TOKEN | registry bearer token (env / env.sh only, never config.json) | — |
| — / NPM_C2_EN | explicit env.sh path | ./env.sh |
| — / NPM_C2_CONFIG | explicit config file path | ./config.json |
State files: .json (agent id, command-baseline , and last
processed seq per target), attacker-state.json (last issued seq per target,
with new sequences allocated globally, seen results, discovered agent ids, request/
response history, stats). Delete them to reset a side.
| Command | Description |
|---|---|
| agents | list historically discovered agents and result counts; no li claim |
| task <agentId\|all> <op> [args...] | task one known agent or publish one broadcast for all agents |
| attach <agentId> | attach the prompt to one known agent; bare task operations target it |
| detach | lea mode and restore the normal prompt |
| rename <agentId> <name> | assign a durable local display name; registry identity is unchanged |
| chain list\|add\|delete\|run | named, agent-agnostic task sequences (chains.json) |
| history [n] | show the last n requests/responses (default 20, persisted) |
| poll | fetch new results; show locally pending direct tasks while waiting |
| clean | delete all x-cmd-*/x-res-*/x-ann-* tags (leaves only latest) |
| stats | local counters: sent, received, per-agent |
| help, exit | — |
Command payloads that do not fit in one 214-character dist-tag (roughly
anything past ~90 chars of arguments) are split automatically into<chunk>of<total> command tags; the buffers the parts and executes
the command only once every chunk is , so long commands are safe to
type or paste.
attach <agentId> is an in-memory routing shortcut for a known agent: while
attached, entering a bare KC2 task operation (for example, pwd) dispatches
it to that agent. Local CLI commands still take precedence. Use detach to
restore the kc2> prompt and normal routing. Attached mode does not change
delivery semantics; results still arri through the registry.
chain manages named, reusable task sequences stored in chains.json
(owner-only, next to the attacker state file). Each chain has a short
description and a list of bare task ops — no target agent baked in — so the same sequence can be run
against any agent, chosen at run time:
Flags: -n/--name, -a/--agent/--agentId, and repeatable-s/--step; quote steps that contain spaces, and --flag=value works
too. Steps are when added (known op, correct arguments), and
running a chain dispatches each step through the normal task path, so
agent checks stay identical to typing the commands by hand. A legacyplaybooks.json from the playbook era is migrated automatically on firstchain use — its task <agentId> prefixes are stripped — and kept asplaybooks.json.bak.
Ops without arguments: echo <text>, ping, time, sysinfo, whoami,env (names listed, all redacted unless the opts in withre), netinfo, ps, df, pwd. Ops
taking a path: cd, stat, hash, getfile — the path is **absolute, or
relative to the agent's current working directory** (use pwd to see the cwd
and cd to change it).ls [path] lists a directory (default: the agent's cwd)
and find <dir> <text> searches file names recursively. geolocate demonstrates the classic WiFi
positioning attack: the agent scans the BSSIDs its radio can hear with OS
built-in tools (airport/system_profiler on macOS, nmcli on Linux,netsh wlan on Windows), then — when geolocateSer is configured —
resolves them against a WiFi Positioning System (WPS) database for
coordinates with an accuracy estimate. This is how real-world implants
locate hosts that have no GPS: BSSIDs are worldwide index keys in
wardri databases. Any MLS/Ichnaea-compatible endpoint works —
the request shape is the shared Google/Mozilla wifiAccessPoints JSON, the
client identifies itself with a real User-Agent (required by beaconDB), and
the response's fallback field is surfaced so students can contrast a true
WiFi fix (tens of meters) with a coarse IP estimate (tens of kilometers):
| Service | Endpoint | Key | Status (2026) |
|---|---|---|---|
| beaconDB (recommended) | | none | works — community MLS successor; experimental, sparse co outside mapped areas, falls back to IP estimate |
| Google Geolocation API | | required (geolocateSer) | works — best co, billed |
| Mozilla Location Service | location.services.mozilla.com | — | retired 2024 — historical reference only |
| Apple | — | — | no public API (private endpoint, used by macOS/iOS) |
| WiGLE | wigle.net API | free account | different per-BSSID API — good manual-lookup exercise, not MLS-compatible |
Note that IP-geolocation services like ip-api.com use a different
response shape and are not compatible with the WPS client. Without a
service URL the task returns the scan-only reconnaissance stage, which is
itself the teachable artifact. It requires enableGeolocate=true on the
. The allowlist is
data-driven: src/common/ops.js holds each op's name, usage, argument spec,
and help summary — adding an op is one entry there plus one handler undersrc//.
Fun ops are shown in their own CLI help section but requireenableFunOps=true on the : openurl <http(s)-url>,say <text>, notify <text>, beep, bounce, <0-100>, rickroll,
and party. They use built-in macOS/Windows facilities and common Linux
desktop utilities. Minimal/headless Linux systems report a clear error when
the required browser, speech, notification, or audio utility is una.
While the prompt is idle, the CLI polls in the background (everymin(pollInter, 5)s) and prints li when an agent is
first discovered and when a task completes or fails. Each publishes
the deterministic announcement payload { "v": 1 }; the same agent id always
produces the same x-ann-* tag, including across restarts. The tag is a
historical discovery marker, not a heartbeat: known does not mean online,
and host, cwd, leases, and online/offline status are intentionally absent.
Direct tasks require a pre discovered agent id. task all writes onex-cmd-all-* broadcast tag instead of per-agent fan-out. On the first
successful poll after installing or upgrading this , a records
the highest existing direct and broadcast sequences and does not execute those
pre-existing commands. Commands published later may remain queued and execute
after a reconnect; there is no lease- or clock-based expiry.
To disconnect a , press Ctrl-C once: the agent sa and exits (a
second Ctrl-C force-exits). Its stable announcement remains so restarts do not
create additional discovery tags.
On npmjs.org, sensiti DELETE operations require
interactive 2FA and cannot be automated with a bypass-2FA granular token.
Consequently clean may fail; use interactive npm dist-tag rm ... for
manual cleanup. Command and result tags otherwise remain and accumulate with
task activity.
getfile transfers bytes, so small images and short video samples work the
same way as text files. The attacker sa downloads underdownloads/.
GitHub Actions tests Node.js 22 and 24. Seedocs/operations.md for deployment, monitoring,
incidents, upgrades, and releases, and SECURITY.md for the
security boundaries and reporting policy.
Project layout:
MIT — for educational and defensi use only.
开发语言:JavaScript
Star 数:1
该项目属于防护思路方向的开源资源,代码结构清晰,适合安全研究人员学习参考。项目围绕Kapital C2: Educational defensive-security lab for studying indirect C2 communication over npm dist-tags and the detection artifacts it creates.展开,提供了可运行的实现,对理解相关安全机制、工具原理与攻防技术具有较高的研究价值。
从技术实现角度看,此类项目的核心在于对底层原理的深入理解与工程化落地。开发者在研究过程中可以结合官方文档与社区讨论,梳理项目的初始化流程、核心模块职责与数据流转,逐步掌握其中的技术细节与设计思路。
1. 下载源码压缩包,解压到本地环境。
2. 根据项目类型准备对应的运行环境与依赖。
3. 按照 README 中的步骤完成配置与部署。
4. 在隔离的测试环境中运行验证,避免影响生产系统。
本资源来源于开源社区,仅供安全研究、技术学习与合法授权范围内的攻防演练使用,请勿用于任何非法或侵害他人权益的用途。使用本项目产生的一切后果由使用者自行承担。
| 资源标题 | kc2 - Kapital C2: Educational defensive-security lab for |
| 所属分类 | 防护思路 |
| 发布作者 | admin |
| 发布时间 | 2026-10-06 00:30:24 |
| 下载权限 | 免费下载 |
| 附件数量 | 2 个下载链接 |
| 浏览次数 | 157 |
| 讨论数量 | 0 |
| 主题状态 | 正常讨论中 |