Vincent-P-essy/deception-grid

GitHub: Vincent-P-essy/deception-grid

一套低交互 SSH/HTTP/S3 蜜罐系统,通过 HASSH 指纹、凭据关联和 STIX 2.1 导出实现攻击活动追踪,并将白名单与被污染身份保留并透明声明以避免情报污染。

Stars: 0 | Forks: 0

# deception-grid **三个低交互诱饵 —— SSH、HTTP、S3 —— 它们回答了一个蜜罐通常无法回答的问题:*究竟有多少人真正触碰过它,以及是否有任何内容值得发送给其他人?*** [![ci](https://static.pigsec.cn/wp-content/uploads/repos/cas/99/993938d8ce5e902ccfb9d6747725c320d855dea3235ed9a304cedf0d94c9321f.svg)](https://github.com/Vincent-P-essy/deception-grid/actions/workflows/ci.yml) [![python](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12-3776ab)](pyproject.toml) [![licence](https://img.shields.io/badge/licence-MIT-green)](LICENSE) [![tests](https://img.shields.io/badge/tests-98%20passing-brightgreen)](tests/) 诱饵没有真实用户。没有任何合法的行为有理由连接到它,因此从构建原理上讲,每一个事件要么是入侵企图,要么是你自身环境中的误操作。这就是它的全部核心价值:**一个背后没有良性活动基准率的信号。** 这意味着扼杀一个欺骗防御项目的不是被攻击者发现。而是*你自己的漏洞扫描器*。如果每天晚上对诱饵所在的子网进行一次经过身份验证的扫描,它们每年就会产生几百个毫无意义的事件,不到一个月,就没人再看这个事件队列了。诱饵依然在完美运行,但整个项目已经名存实亡。 所以,这个项目把这个数字放在了首位。

decoy report — the benign share, then the identities behind the traffic

## 包装盒内有什么 ``` 203.0.113.45 ─┐ ┌─ ssh ─ version exchange, KEXINIT, HASSH, close 198.51.100.7 ─┼─→ three decoys ───┼─ http ─ 14 probe signatures, raw + URL-decoded 192.0.2.88 ─┤ one event log └─ s3 ─ SigV4 / SigV2 / presigned credentials 203.0.113.99 ─┤ │ 10.20.30.9 ───┘ ▼ (allowlisted) identities ──→ campaigns ──→ STIX 2.1 / MISP what is the who is what may leave same tool? probably one this machine operator? ``` | 命令 | 它的功能 | |---|---| | `decoy run` | 启动监听器。客户端发送的任何内容都不会被执行、打开或写入。 | | `decoy capture` | 通过真实的 socket 驱动诱饵并保存事件。 | | `decoy report` | 先展示良性共享,然后是身份、攻击活动、探测和凭据。 | | `decoy export` | 导出为 STIX 2.1 或 MISP 格式,保留白名单不计入其中,并记录被保留的数量。 | | `decoy selftest` | 在临时端口上对真实的监听器进行 11 项检查。 | ``` pip install -e . decoy selftest # prove the pipeline end to end decoy capture -o captures/mine.jsonl # drive the decoys, save the events decoy report captures/mine.jsonl # read them decoy export -f stix -o bundle.json # publish only what should be published ``` ## 这个项目做对的四件(容易做错的)事 ### 1. 指纹胜过 Banner,而它们之间的分歧胜过这两者 SSH 诱饵完成了版本交换,读取了客户端的 `SSH_MSG_KEXINIT`,然后关闭连接。它从不执行密钥交换,也从不提供身份验证——过了那个点之后的所有操作都意味着要对攻击者提供的输入运行加密算法,而诱饵存在于*你*拥有的网络中。 它在那里停止是因为有趣的情报已经到手了。KEXINIT 按顺序列出了客户端将接受的每一种算法。这种排序是**客户端实现**的属性,而不是目标的属性,对这些列表中的四个进行哈希处理,就能得到一个在源地址改变后依然存在的 [HASSH](https://github.com/salesforce/hassh) 指纹。 一个用户名和密码是一对凭据。而 HASSH 是一种身份:

probes, one HASSH under two banners, and the credentials the actors gave up

内置的捕获结果包含了一个工具从两个地址、在两个不同的 banner 下发起的连接——`SSH-2.0-Go` 和 `SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.5`。算法列表完全相同,因此指纹也相同,所以是同一个身份。版本字符串是操作者随意输入的;而指纹不是,**一个自称是 OpenSSH 的客户端,却提供了一个所有 OpenSSH 构建版本都从未提供过的算法列表,这本身就是调查发现。** ### 2. 一个网格上的三个诱饵不等于三个蜜罐 报告将通常被混为一谈的两个声明区分开来: - 一个 **identity** —— 相同的工具,或相同的凭据。基于 HASSH、AWS access key ID 或扫描器的 wordlist 进行分组。可导出。 - 一个 **campaign** —— 很可能是同一个操作者。从共享的源地址看到的身份。

campaigns joining identities across services, and a contaminated identity withheld

捕获结果中较大的 campaign 跨越了**三个地址和所有三个服务**,将 Web 扫描器的 wordlist、SSH 指纹和 AWS access key 关联了起来。在一个地址将其中两个观测结果联系起来之前,这些观测结果之间没有任何关联——而单服务蜜罐根本无法建立这种联系。 但 campaign 是比其中包含的 identity 更弱的声明,因为一个地址可能是一个 NAT 网关、一个 VPN 出口,或者是一个上周还属于别人的住宅租约。因此,campaign 会被报告,但**绝不会以与其包含的 identity 相同的置信度被导出**。将这两个层级混为一谈,就是一个共享的出口地址如何将一个共享的情报源变成一个巨大的误报。 ### 3. 没有任何断言的置信度会高于它被观测到时的置信度 | 分组依据 | 置信度 | 为什么是这个数值 | |---|---:|---| | AWS access key ID | 90 | 行为者持有的长效凭据标识符。 | | SSH HASSH | 85 | 客户端实现指纹;在源地址改变后依然存在。 | | Path wordlist | 60 | 对于特定工具是稳定的,但在运行相同扫描器的不同操作者之间是共享的。 | | 源地址 | 30 | 被观测到一次。极有可能被重新分配。 |

decoy export --format stix, with the withholding stated in the bundle

一个将所有内容评级为 100 的威胁情报源,就是一个充满了未来误报的情报源,而接收者无法分辨哪个是哪个。这里的每个 indicator 都带有其*分组*所证明的合理置信度——绝不是其事件计数可能暗示的置信度。 ### 4. 被保留(不导出)的内容会被声明,而不是保持沉默 把你自己的漏洞扫描器作为入侵指标发布,只会让共享社区停止信任你的情报源。有两类内容永远不会被导出: - **加入白名单的源。** 声明时必须带有原因(`--benign 10.20.30.=internal vulnerability scanner subnet`),并且这个原因是必填的——一个没有原因就被豁免的源,就是一个谁也无法审查的源。 - **被污染的身份。** 一个同时匹配了白名单流量和恶意流量的身份。见下文;这是最关键的情况。 这两者都会被计入数据包内的 `x_deception_grid` 中,因为如果一个操作者看不到有哪些内容被保留了,他就无法区分这是一个被过滤的情报源还是一个空的情报源。 ## 这个项目在自身中发现的三个缺陷 这三个问题曾经都是真实存在、悄无声息的,并且是通过运行程序而不是阅读代码发现的。它们被记录在这里,是因为修复它们的原因比修复本身更有趣。 ### 原生的 OpenSSH 只有唯一一个 HASSH 捕获结果中故意包含了一个位于 `10.20.30.9` 的内部扫描器,以及一个运行着**相同 Ubuntu OpenSSH 构建版本**的恶意主机 `203.0.113.99`。因此它们共享同一个指纹。 第一个版本将它们分成了一个身份,并将其标记为恶意——因为它的*部分* session 是恶意的——然后将原生 OpenSSH 的 HASSH 作为 `malicious-activity` 导出。该 indicator 会在互联网上的每一台 Ubuntu 主机上触发,首先遭殃的就是发布它的内部环境中的主机。 由此产生了一条规则:**白名单是那些绝对安全的事物的样本,因此匹配到其中任何一个的 identity 都不够明确,根本无法作为一种身份。** 现在,每个身份都会准确地落入三种处置状态之一——可导出、已加入白名单或被污染——第三种状态会被保留并交给人工审查,因为存在两种解读方式,没有任何代码能够在它们之间做出选择: ### wordlist 签名曾是死代码 按扫描器遍历的路径集对它们进行分组是按每个 session 编写的。带有 `Connection: close` 的 HTTP/1.1 会发送**每个连接一个请求**,因此一个 session 只包含一个路径,永远无法达到三个路径的阈值。 它从未触发过一次。它悄无声息造成的影响是,每个 Web 扫描器都退化为通过它可以随时更改的地址来识别——置信度只有 30 而不是 60,并且无法将同一次扫描中来自两个地址的活动归为一组。现在,wordlist 是按每个地址计算的,并且所有请求都会去请求的路径(`/`、`/health`、`/favicon.ico`)会被首先排除,因为通过共享的 `/` 将两个行为者合并在一起,正是该信号最容易引发的误报。 ### SQL 注入签名没有匹配到任何真实内容 `(union\s+select|'\s+or\s+'1'\s*=\s*'1)` 是一个针对根本没人会发送的 payload 的正确匹配模式。`' OR '1'='1` 在到达服务器时变成了 `%27+OR+%271%27%3D%271`,而 `\s` 既不匹配 `+` 也不匹配 `%20`。该探测绕过了所有规则,并被记录为普通的查询字符串。 现在,请求会与原始格式**以及**解码后的格式进行匹配,如果两者不同,还会匹配二次解码后的格式,因为上游的过滤器可能已经解码过一次了。同样的 bug 还隐藏了 `%2e%2e%2f` 路径穿越。另外,`/wp-(admin|login)(/|$)` 遗漏了 `/wp-login.php`——这是互联网上除 `/` 之外被请求次数最多的路径。 ## 这里发布的所有内容都经过了实际执行

decoy selftest — 11 checks against real listeners on ephemeral ports

本代码仓库中没有样本数据。`decoy capture` 会在临时端口上启动真实的监听器,使用真实的 socket 连接到它们,发送真实扫描器会发送的字节,并从日志中读出事件。一个所有测试都 mock 网络的蜜罐,测试的不过是它自己对于网络的主观臆断。 以下数据来自该次运行,并且 CI 会在运行期间针对生成的捕获结果对每一项数据进行断言验证: | | | |---|---:| | 捕获的事件 | 跨越 31 个 session 的 **98 个事件** | | HTTP / SSH / S3 | 69 / 20 / 9 | | 发现的身份 | **8 个身份** —— 3 个 HASSH,2 个 access key,1 个 wordlist,2 个地址 | | 跨多个地址分组的 | **2** | | 关联身份的 Campaign | **2** 个,较大的一个跨越了 3 个地址和 3 个服务 | | 良性共享 | **16%**(98 个事件中的 16 个) | | 分类的探测类型 | 14 个签名中的 **9** 个 | | 恢复的 AWS access key | **2** 个(`AKIA…` 长效凭据,`ASIA…` 临时凭据) | | 作为 **STIX 2.1** indicator 导出 | **6** 个 indicator,置信度分别为 30 / 60 / 85 / 90 | | 被保留不导出 | 1 个已加入白名单,1 个被污染 | ``` python scripts/reproduce.py --check # fails if any figure above has drifted ``` ## 在真实环境中运行 ``` docker compose -f deploy/docker-compose.yml up -d ``` 诱饵在设计上就会接受恶意输入,因此它是你网络上最有可能在与企图攻击它的人进行通信的进程。该容器在构建时基于这样一个假设:其中的代码总有一天会失守: | 限制 | 原因 | |---|---| | `read_only: true` | 诱饵没有理由写入自己的文件系统。无法写入的攻击者就无法暂存(stage)恶意文件。 | | `cap_drop: ALL`,不添加任何权限 | 它以非 root 用户身份绑定高端口,这不需要任何 capability。由宿主机映射 22/80/443 端口。 | | `no-new-privileges:true` | 镜像中的任何 setuid 二进制文件都无法提升权限,无论它是什么。 | | `user: 10001`,无 shell,无 home 目录 | 如果进程被攻破,它是作为一个无法在任何地方登录的用户被攻破的。 | | `mem_limit: 128m`, `pids_limit: 64` | 工作负载仅仅是几个 socket 和一些解析操作。如果进程需要更多资源,说明它已经偏离了正常工作。 | | `tmpfs /tmp` 挂载选项为 `noexec,nosuid` | 镜像中唯一的可写路径,且其中的任何内容都无法执行。 | 监听器本身在设计上就拒绝三件事。**客户端发送的任何内容都不会被执行、打开或写入**——诱饵通过常量来响应,而一个“模拟”文件系统的蜜罐本身就*变成*了一个文件系统。**每次读取都是有边界的,且每个连接都在计时器的控制下。** 此外,**每个连接在被解析之前就已经被记录下来了**——`connect` 事件在 socket 被接受的那一刻就被写入了,因此即使客户端让解析器崩溃了,也会留下它曾来过这里的证据。如果先解析,一个格式错误的探测就会变成一个不可见的探测。 ## 目录结构 ``` src/decoy/ event.py Event, benign_reason, the append-only JSONL log ssh.py RFC 4253 version exchange, binary packet, KEXINIT, HASSH web.py HTTP parsing, 14 probe signatures, SigV4/SigV2/presigned credentials server.py the asyncio listeners and the benign-source policy analyse.py sessions → identities → campaigns, and the three dispositions intel.py STIX 2.1 and MISP, with the exclusions counted selftest.py drives the decoys over real sockets; the source of every figure here cli.py run · capture · report · export · selftest · profiles tests/ 98 tests — parsing, real sockets, grouping, and what may leave deploy/ hardened Dockerfile and compose constraints scripts/ reproduce.py — regenerates every published number results/ expected.json, and the STIX and MISP documents themselves ``` ## 许可证 MIT —— 见 [LICENSE](LICENSE)。 内置捕获结果中的地址来自 RFC 5737 保留的文档范围(`192.0.2.0/24`、`198.51.100.0/24`、`203.0.113.0/24`)。AWS key ID 是 自己发布的示例。这里没有任何内容指向真实的主机或真实的凭据。
标签:BOF, CISA项目, Python, 威胁情报, 安全, 开发者工具, 无后门, 欺骗防御, 版权保护, 蜜罐, 证书利用, 请求拦截, 超时处理, 逆向工具