willysnow/wisp

GitHub: willysnow/wisp

单文件网络蜜罐传感器与自托管控制台,用静态二进制简化部署,并覆盖 LLM、Kubernetes、MCP 等新一代攻击面。

Stars: 1 | Forks: 0

# wisp 一个单文件网络蜜罐传感器。将其部署在内网网段中,等待有东西触碰它, 即可收到包含凭据、提示词或入侵者尝试访问路径的告警。 ## 为什么会有这个项目 [OpenCanary](https://github.com/thinkst/opencanary)(BSD-3-Clause,由 Thinkst 开发) 证明了这种模式的有效性,它比本项目更完整,并且是对该领域的一项真正贡献。`wisp` 并非对它的批评——而是坚信有两件事值得重做: 1. **部署。** OpenCanary 需要 Python 3.10+、Twisted、Scapy、pcapy-ng,而且——对于 SMB——需要一个正常工作的 Samba 安装,并通过 `full_audit` VFS 模块写入 syslog 供 OpenCanary 读取。`wisp` 只是一个静态二进制文件。 2. **攻击面。** 2026 年内网上的入侵者会尝试使用 Kubernetes API、Docker socket、云 IMDS,以及任何未经认证就搭建起来的 LLM 基础设施。最后一个正是 `wisp` 的切入点,因为没有其他工具覆盖这一块。 OpenCanary 做的其他所有事情,目前它都做得更好,因为至少它实现了这些功能。 ## 诚实的对比 OpenCanary 提供了 21 个协议模块。`wisp` 重新实现了其中的 **12** 个,外加 3 个 OpenCanary 没有的诱饵。 | | OpenCanary | wisp 目前 | |---|---|---| | 协议模块 | 21 | **12** + 3 个新诱饵 | | 本项目已实现 | — | `ssh`, `http`, `https`, `telnet`, `ftp`, `redis`, `tftp`, `ntp`, `git`, `mongodb`, `llmnr`, TCP banner | | 仍然缺失 | — | SMB, RDP, MySQL, MSSQL, VNC, SNMP, SIP, HTTP 代理, portscan | | SMB | 是,通过外部的 Samba | **未实现** | | LLM / MCP / Kubernetes 诱饵 | 否 | **是**(`ollama`, `mcp`, `k8s`) | | 告警 | 文件, syslog, HPFeeds, 邮件, webhook, + 独立的去重守护进程 | JSONL, syslog, 邮件, LINE, webhook (Slack/Teams/Discord), **内置去重** | | 集群控制台 | 无 | **包含,可自托管** | | 安装 | Python + Twisted + Scapy(SMB 还需要 Samba) | 一个静态二进制文件 | | 平台 | Linux 优先,多个模块需要 root | Go 可交叉编译的任何地方 | 通用的 TCP banner 捕获器可以在连接级别覆盖任何额外的端口—— 足以发现扫描或探测,但无法像真正的模拟器那样捕获凭据。VNC 仍然作为 banner 捕获器提供,并在上表中计为缺失,因为实际情况就是如此。 如果你今天需要全面覆盖,请运行 OpenCanary。如果你想要 LLM 诱饵,或者 Python/Samba 的依赖链是你根本不愿部署蜜罐的原因,那就运行 `wisp`。 ## 目前模拟的功能 | 服务 | 默认端口 | 捕获的内容 | |---|---|---| | `ssh` | 2222/tcp | 用户名、密码、公钥指纹、客户端版本 | | `http` | 8080/tcp | 管理面板凭据、探测的路径、user agents | | `https` | 8443/tcp | 同上,外加 **SNI** ——他们期望在这里找到的名称 | | `telnet` | 2323/tcp | 用户名和密码(去除了 IAC 协商) | | `ftp` | 2121/tcp | 用户名、密码以及登录后的命令意图 | | `redis` | 6379/tcp | AUTH 凭据**以及完整的命令序列** | | `tftp` | 6969/udp | 请求的文件名以及是读取还是写入 | | `ntp` | 1123/udp | 客户端请求,以及 **mode 7 `monlist` 放大侦察** | | `git` | 9418/tcp | 请求的**仓库路径**,以及他们是否打算进行 push | | `mongodb` | 27017/tcp | 用户名和**可离线破解的 SCRAM 证明**、驱动程序和应用程序名称 | | `llmnr` | 出站 | **网段上的主动投毒者**,以及它声称的地址 | | banner | 任意 | 发送的前几个字节,用于没有真正模拟器的端口 | | `ollama` | 11434/tcp | **模型列表侦察,以及发送到你的“GPU”的实际提示词** | | `k8s` | 6443/tls | **被盗的 service-account bearer token**、客户端证书、探测的资源 | | `mcp` | 8931/tcp | **连接的 agent,以及它带参数进行的 tool calls** | ### 其他产品都没有的三个诱饵 `ollama`、`k8s` 和 `mcp` 覆盖了入侵者在 2026 年网络上真正会触及的攻击面,并且每个都捕获到了连接日志无法捕获的内容: - **`ollama`** 记录提示词。未经认证的推理端点是一个免费的 GPU、一个跳板,并且——当连接到内部 RAG 时——一条数据泄露路径。 - **`k8s`** 返回 `403`,而不是 `401`。这就是启用了 anonymous-auth 的集群的行为,两者的区别很重要:`401` 意味着“走错门了”并结束交互;而 `403` 意味着“门是对的,凭据错了”,因此攻击者会用被盗的 token 重试。知道*哪个 service account* 被攻破,比知道有人敲门要有价值的得多。 - **`mcp`** 记录 agent 自己的 `clientInfo`(比 user-agent 强得多的标识符)以及每次 `tools/call` 的参数。公布的工具是故意设下诱饵的,所以他们选择的工具能告诉你他们想要什么: mcp tool_call tool=query_customer_db arguments={"query":"SELECT email, credit_card FROM customers LIMIT 5000"} 这三个诱饵会给出应答而不是拒绝,因为合理的回复会让攻击者继续交流,而随后的每一条消息都是更多的情报: - **redis** 返回 `+OK`,因此脚本化的接管会在你监视的地方运行它的全套 playbook。真实的攻击看起来像 `CONFIG SET dir /root/.ssh` → `CONFIG SET dbfilename authorized_keys` → `SET x "ssh-rsa …"` → `SAVE`,这四条命令都会落到你的日志里。 - **ollama** 返回一个模型列表和一条拒绝消息,因此扫描器会发送真实的提示词。 - **ntp** 回答普通的 mode 3 客户端请求。 实际上什么都不会被授权——没有文件被写入,没有命令被执行,每个凭据都会被拒绝。 **`ntp` 从不回答 mode 7(`monlist`)。** 回答会将传感器变成一个正常工作的 DDoS 放大器,指向攻击者伪造的任何地址。它记录探测并保持沉默;这一属性由测试保障。 **`llmnr` 从不回答查询。** 它是唯一一个不是诱饵的模块:它组播一个对不存在的主机名的请求并等待。正常的网络会用沉默来回答,所以收到回复就是存在投毒者的证据——Responder、 Inveigh 等工具会回答所有请求,因为回答一切正是它们的工作方式。我们自己回答一个本质上就*等于*那种攻击,而一个会实施它所检测到的攻击的检测器根本不是检测器。这一点同样由测试保障。 它默认是关闭的,因为它是唯一一个会自行向网络发送数据包的模块。 **`mongodb` 声称需要身份验证**,而不是假装完全开放,原因与 `k8s` 回答 403 相同。一扇锁着的门会引诱人提供钥匙:驱动程序接着会运行 SCRAM,这会立即交出用户名,并且——一旦客户端发送了其证明——一个可以像捕获的 NetNTLMv2 响应那样被离线破解的值。任何内容都不会被接受;每一次对话都以 `AuthenticationFailed` 结束。 Ollama 传感器是其他产品都没有的。一个未经认证的推理 端点是一个免费的 GPU、一个跳板,并且——当连接到内部 RAG 时——一个数据泄露面。该模拟会合理地回答而不是报错, 因为获得模型列表的扫描器通常接下来会发送真实的提示词, 而该提示词会用攻击者自己的话告诉你他们想要什么。 ## 快速开始 ``` go mod tidy go run ./cmd/wispd ``` 不需要配置文件——它在默认配置下运行。要进行自定义: ``` cp wisp.example.yaml wisp.yaml go run ./cmd/wispd -config wisp.yaml ``` 然后触发它: ``` ssh -p 2222 root@localhost curl -s localhost:11434/api/tags curl -s localhost:11434/api/generate -d '{"model":"llama3.2","prompt":"cat /etc/shadow","stream":false}' ``` ## Docker 一个 `Dockerfile` 构建了两个镜像,并且都是 distroless 的——没有 shell,没有 包管理器,没有任何可以作为跳板的东西。传感器是最有可能被故意攻击的容器,因此它携带的东西最少。 ``` docker build --target sensor -t wisp/sensor . ``` ``` docker build --target console -t wisp/console . ``` 单主机评估栈(在生产环境中,控制台不应该位于你预期会被攻击的网段中): ``` docker compose up --build ``` 关于在容器中运行蜜罐,有两件事比其他事情更重要: - **在 Linux 上使用 host networking。** 使用发布的端口时,Docker 会将 客户端的地址重写为网桥网关,而一条声称每次入侵都来自 `172.17.0.1` 的告警是任何人都无法采取行动的。在 Docker Desktop (macOS/Windows)上,这种重写是不可避免的——在那里映射端口并视 源 IP 为不可靠的。 - **保留数据卷。** 它们保存了 SSH 主机密钥、诱饵 TLS 证书、控制台数据库和操作员账户。每次重启都会改变的密钥材料,对于任何连接两次的人来说,都等同于昭告天下这是一台蜜罐。 两个容器都以非 root 用户身份运行,具有只读的根文件系统, 所有权限被丢弃,并设置了 `no-new-privileges`。端口是非特权 默认端口,因此不需要重新授予任何权限。 控制台镜像没有 shell 来运行健康探测,因此二进制文件探测 自身——`wisp-console healthcheck` 被配置为镜像的 `HEALTHCHECK`, 这也是编排器用来判断控制台是否存活的方式。 ## systemd 两部分的 unit 文件都在 [`deploy/systemd/`](deploy/systemd) 中,沙箱化达到 与容器相同的标准:无权能,除了它们自己的状态目录外没有任何可写 的地方,包含 seccomp 过滤器,并且没有提升至 root 的路径。 传感器在设计上就需要解析恶意输入,而 unit 文件不应该依赖于代码 的绝对正确。 ``` sudo install -m 0755 wispd /usr/local/bin/wispd sudo useradd --system --no-create-home --shell /usr/sbin/nologin wisp sudo install -m 0644 deploy/systemd/wispd.service /etc/systemd/system/ sudo systemctl enable --now wispd ``` `StateDirectory` 和 `ConfigurationDirectory` 会以正确的所有权创建 `/var/lib/wisp` 和 `/etc/wisp`,因此不需要 `chown` 步骤。完整的说明, 包括控制台以及如何在不授予该权能的情况下绑定特权端口,都在 [`deploy/systemd/README.md`](deploy/systemd/README.md) 中。 ## 输出 两个输出端,默认均开启。供人类查看的控制台: ``` 14:22:07 ollama prompt 10.0.3.44:51188 -> :11434 model=llama3.2 path=/api/generate prompt=cat /etc/shadow ``` ……以及供机器读取的 `events.jsonl` ——每行一个 JSON 对象,可供 Vector、 Filebeat 或 SIEM 直接使用: ``` {"time":"2026-07-24T14:22:07.113Z","node":"wisp-01","service":"ollama","kind":"prompt","src_ip":"10.0.3.44","src_port":51188,"dst_port":11434,"data":{"model":"llama3.2","prompt":"cat /etc/shadow"}} ``` ## 控制台 一组传感器写入各自本地文件并不构成一个监控系统。 `wisp-console` 是使其成为监控系统的部分:传感器通过 HTTPS 携带 bearer token 传输数据, 并且每条告警都会汇聚到同一个地方。 它是自托管的,除了一个可写文件外不需要任何其他东西——通过一个纯 Go 实现的驱动使用 SQLite,因此控制台也是一个单一的静态二进制文件。 ``` go build -o wisp-console ./cmd/wisp-console ./wisp-console sensor add sensor-01 ``` 这会打印出一个 token ——仅一次。将其放入传感器的配置中: ``` log: file: events.jsonl console: true remote: url: "https://console.example.com" token: "wisp_..." ``` 然后运行控制台: ``` ./wisp-console -addr :8000 ``` 每个传感器都有自己的 token,并且 **节点名称来源于 token,而不是 来源于请求体**。传感器无法伪装成另一个传感器,因此追查告警的操作人员永远不会被引向错误的机器。使用以下命令管理它们: ``` ./wisp-console sensor list ``` ``` ./wisp-console sensor revoke sensor-01 ``` 为现有节点重新运行 `sensor add` 会签发一个新的 token 并使旧 token 失效——这既是轮换也是“我丢失了 token”。数据库只存储 token 的 SHA-256 哈希值,因此被盗的控制台数据库不会泄露任何有效的凭据。 为了兼容性,仍然可以使用单一共享 token(`-token` 或 `WISP_CONSOLE_TOKEN`),但控制台在启动时会发出警告:使用共享 token 时,节点名称会回退为 payload 中声明的任何内容,任何持有该 token 的人都可以伪造归属于任何传感器的事件。 然后在浏览器中打开控制台。凭据事件(`login_password`、 `auth_attempt`,……)会被高亮显示;点击传感器、服务或源 IP 会过滤视图。 ### 登录 UI 在登录之后,没有任何设置可以关闭它。将 接入限制在 token 之后却仪表板敞开大门将起不到任何保护作用—— 仪表板是所有捕获的密码、提示词和被盗 token 最终汇聚的地方。 在首次启动时,控制台会创建一个操作员账户并打印一次 密码: ``` created console operator "admin" with password: QOavm-F81rHgpHHQuX-Cqv48 ``` 从 CLI 管理账户: ``` ./wisp-console user add alice ``` ``` ./wisp-console user list ``` `user add` 和 `user passwd` 会生成一个密码并打印一次;传递 `-password-stdin` 来提供你自己的密码(至少 12 个字符): ``` printf '%s\n' "$NEW_PASSWORD" | ./wisp-console user passwd alice -password-stdin ``` 更改密码会使该账户拥有的所有会话登出——密码更改通常 *因为*它泄露了,而让旧会话继续保持登录状态将使这一切 毫无意义。默认情况下,会话持续 12 小时(`ui.session_ttl`),存在于 数据库中,因此重启不会让所有人登出,并且以哈希形式存储,因此 被盗的数据库不会泄露有效的访问权限。 传感器 token 和操作员登录是特意分开的凭据:位于 敌对网段的传感器除了写入事件外什么也做不了。它的 token 无法打开 UI,而 UI 会话也无法传输事件。 在反向代理后面,设置 `ui.trust proxy: true`,以便登录速率限制能识别 真实的客户端地址,而不是代理的地址——否则一个攻击者 失败的登录会把所有人都锁在外面。 ### TLS 控制台自行终结 TLS——不需要反向代理。传感器发送 捕获的凭据及其 bearer token,操作人员发送密码; 这些内容都不应该以明文形式跨越网络。 ``` tls: mode: self-signed # off | self-signed | file | acme cert_file: console-cert.pem key_file: console-key.pem domains: ["console.internal"] ``` **`self-signed`** 是大多数控制台的正确选择,它们通常部署在 没有公共 DNS 名称的内网网段中。证书生成一次并 保留——每次重启都更改的证书是无法被固定的(pin)——并且其指纹 在启动时打印: ``` TLS: self-signed certificate SHA-256 51:49:55:A1:64:D0:50:F0:DD:CE:22:40:A2:EA:D3:A6:... ``` 将其提供给传感器,这样就不必使用“跳过验证”(skip verification): ``` log: remote: url: "https://console.internal:8000" token: "wisp_..." fingerprint: "51:49:55:A1:..." # or ca_file: console-cert.pem ``` 带有 pin 的传感器拒绝向任何其他对象传输数据——包括 对连接重新签名的 TLS 代理。(这并非假设:在运行 Norton Web Shield 的机器上,被固定的传感器正确地拒绝与 同一主机上的控制台通信,因为它拿到的是 Norton 的证书,而不是 控制台的。)`insecure_skip_verify` 依然存在,并在文档中作为实验室设置 被记录,它将把捕获的每一个凭据发送给任何响应连接的人。 **`acme`** 为公共名称从 Let's Encrypt 获取证书。它 需要 `accept_tos: true`——代表操作人员同意 CA 的条款 不是软件该做的决定。端口 80 是可选的:如果没有 `http_addr`,颁发 将使用基于 443 端口的 TLS-ALPN-01。设置 `http_addr: ":80"` 以提供 HTTP-01 验证并将纯 HTTP 重定向到 HTTPS。 对于前面已经有代理的部署,**`off`** 依然可用, 但控制台在每次启动时都会发出强烈的警告。 ### 搜索、分页和导出 事件列表每页显示 100 行,搜索框将自由文本与 每一列进行匹配,**包括 JSON 数据 blob**——这是有趣 字符串所在的地方。搜索 `hunter2` 会找到使用它的登录记录;搜索 `/etc/shadow` 会找到请求它的提示词;搜索 `10.0.0.9` 会找到 该地址触碰过的所有记录。点击传感器、服务或源 IP 仍然 会进行过滤,而搜索会在已过滤的内容范围内进一步缩小范围,而不是 替换它。 导出采用与启动它的页面相同的过滤器,格式为 CSV 或 换行符分隔的 JSON——与传感器 `events.jsonl` 的格式相同,因此 任何已经指向该文件的工具也能读取此导出。 ``` /export.csv?q=hunter2&hours=168 ``` 关于 CSV 有一个细节值得了解:其中的每个字段都是攻击者控制的—— 用户名、探测的路径、提示词。当电子表格打开该文件时,以 `=` 开头 的值会被作为公式执行,因此仔细挑选用户名的攻击者 可能会在读取导出文件的分析师的机器上实现代码执行。这些值在输出时会被加上一个前导撇号;payload 仍可作为证据阅读,只是不会被执行。 ### 当传感器变得沉默时 传感器无法报告自己的死亡。如果有人发现并阻止了它, 它所做的最后一件事就是保持沉默——而一个沉默的传感器看起来就像是一个什么都没发生的网络。控制台是唯一能区分 这两者的地方: ``` sensors: silence_after: 30m check_interval: 1m ``` 越过阈值会引发 `sensor_silent` 事件:存储在时间线中, 推送到每个通知器,并在传感器列表中标记为红色。重新上线会 引发 `sensor_returned`,因为凌晨 3 点被叫醒的人不应该 猜测它是否仍然处于宕机状态。每次状态转换时各报告一次,因此 保持宕机状态的传感器不会每分钟都呼叫任何人。 这是 wisp 中唯一因为什么都没发生而触发的告警,也是 最可能至关重要的告警:以杀死传感器为开端的入侵 在其他情况下是不可见的。 ### 保留 如果放任不管,数据库将永远增长,其大小由扫描你传感器的 人决定——磁盘填满的控制台将停止记录导致其填满的入侵。在 `console.yaml` 中设置策略: ``` retention: events: 90d # maximum age; accepts d and w as well as h/m/s max_events: 1000000 # hard cap whatever the age; oldest go first interval: 1h # how often the policy is applied ``` 两个限制均适用,以最先触发的为准。计数上限是年龄限制无法提供的基础保障: 在持续扫描下的传感器一个下午就能产生数月的 事件。清除后的空间将返还给文件系统——SQLite 会重用释放的页面, 但从不会自行缩小文件,因此控制台在足够大规模的清理之后会对其进行重建。 默认设置为无限制,因为静默丢弃操作员的证据 是本末倒置的做法——但控制台在启动时会发出警告,直到设置了策略。 根据设计,交付是尽力而为的:`Emit` 从不阻塞服务 goroutine,并且 当控制台不可达时,事件会排队并最终被丢弃。 因为报告通道停滞而停止响应网络的传感器, 要比丢失遥测数据的传感器更糟——停滞的服务是一个 可被检测到的特征。 ### 通知 没人打开的仪表板不是监控。复制 `console.example.yaml` 到 `console.yaml` 以通过 **电子邮件**、**LINE** 或 **webhook**(JSON、 Slack、Teams、Discord)发送告警。 有两条规则决定了什么内容会被发送,这两条都很重要: **只有有意义的事件类型才会通知。** 提供的凭据(`login_password`、 `auth_attempt`,……)和表达的意图(`prompt`、`tool_call`、`command`,……)。 纯粹的连接和版本探测会被存储但不会被推送——它们 是调查的上下文,而不是在凌晨 3 点叫醒某人的理由。 **重复项会按 `sensor|service|kind|source-IP` 被抑制**,抑制期可配置 (默认 15 分钟)。窗口期结束后的下一次告警会说明有多少相似事件 被折叠了:`ssh login_password from 10.0.0.9 (+47 similar suppressed)`。 不同的源 IP 始终被视为新告警——抑制机制绝不 能掩盖新的攻击者。 在本地端到端运行中,40 个存储的事件产生了 7 条通知——来自 一个源的重复 telnet 暴力破解从 4 条告警合并为 1 条。如果没有这个 机制,单次端口扫描就会让你的通道静音,从而错过下一次 真正的入侵。 提供 LINE 是因为在台湾和日本,运维团队实际 就在使用
标签:AMSI绕过, DLL 劫持, EVTX分析, StruQ, 单二进制, 大语言模型, 威胁检测, 安全, 日志审计, 网络蜜罐, 请求拦截, 超时处理