fDarkShadow/noctis
GitHub: fDarkShadow/noctis
一款 Rust 实现的 YAML 驱动黑盒漏洞扫描器,支持 CVE 检测、配置审计、OOB 盲打及可复现的端到端测试验证。
Stars: 0 | Forks: 0
# noctis
由 YAML 驱动的 Rust 漏洞扫描器。作为轻量级的 OpenVAS 替代方案,提供可复现的测试、Podman 隔离以及 REST API。
## 核心概念
- **独立的 YAML feed** — 每个 CVE 或配置错误对应一个文件,无共享缓存
- **基于服务的扫描** — feed 声明它们所针对的服务(`http`、`https`、`ssh`…),引擎将它们映射到由 nmap 发现的端口
- **分级置信度** — 每个发现都有一个逐步构建的 0–1 置信度分数(兼容 OpenVAS QoD)
- **原始 TCP** — `tcp_connect` 原样发送字节以处理编码的 payload(路径遍历、注入),绕过 HTTP 标准化
- **集成 OOB** — 用于盲检测的 HTTP 回调服务器(Log4Shell、SSRF、XXE)
- **协议** — HTTP/HTTPS、原始 TCP、TLS、SSH
- **可复现的测试** — Ansible + rootless Podman、动态端口分配、自动化的 TP/TN 断言
## 环境要求
- Rust ≥ 1.75
- Ansible-core ≥ 2.15(仅用于 e2e 测试)
- Podman ≥ 4.0(仅用于 e2e 测试)
- [Task](https://taskfile.dev)(仅用于 e2e 测试)
## 安装说明
```
git clone
cd noctis
cargo build --release
# 二进制文件: target/release/noctis
```
## 用法
### CLI 模式(一次性扫描)
```
noctis scan \
--host \
--service http:80 \
--service https:443 \
--tests tests/cve/
# 单一 service,单一 feed
noctis scan --host 10.0.0.1 --service ssh:22 --tests tests/cve/CVE-2023-48795.yaml
# 详细级别
noctis -v scan ... # INFO
noctis -vv scan ... # DEBUG
```
标准输出为 JSON 格式:
```
{
"id": "550e8400-...",
"status": "completed",
"target": "10.0.0.1",
"findings": [
{
"test_id": "a3c7f4e2-...",
"cve": "CVE-2021-41773",
"severity": "critical",
"confidence": 0.95,
"qod": 75,
"evidence": "LFI confirmed — /etc/passwd via path traversal"
}
],
"error": null
}
```
执行错误时退出码为 `1`,其他情况为 `0`(即使有发现结果)。
### Daemon 模式(REST API)
```
noctis serve --host 0.0.0.0 --port 8080
# 使用 OOB server 进行 blind detections
noctis serve --oob --oob-host --oob-port 9090
```
**Endpoint:**
| 方法 | 路由 | 描述 |
|--------|-------|-------------|
| `GET` | `/health` | 健康检查 |
| `POST` | `/scans` | 启动扫描 |
| `GET` | `/scans` | 列出扫描任务 |
| `GET` | `/scans/{id}` | 扫描状态 |
| `DELETE` | `/scans/{id}` | 取消扫描 |
| `GET` | `/scans/{id}/findings` | 扫描的发现结果 |
**POST /scans 示例:**
```
{
"host": "10.0.0.1",
"services": [
{ "port": 80, "service": "http", "protocol": "tcp" },
{ "port": 443, "service": "https", "protocol": "tcp" }
],
"tests": ["tests/cve/", "tests/misconfig/"],
"concurrency": 5
}
```
## YAML feed 格式
### 完整结构
```
uid: a3c7f4e2-1b9d-4f6a-8e3c-2d5a0f1e9b4c # stable UUID v4 — never change
name: "Apache httpd 2.4.49 Path Traversal / RCE (CVE-2021-41773)"
type: cve # cve | misconfig
cve: CVE-2021-41773
cvss: 9.8
severity: critical # info | low | medium | high | critical
confidence_base: 0.30 # floor before any step runs
tags: [apache, path-traversal, rce]
services: [http, https] # nmap service names to target (empty = all)
author: noctis
version: "1.0.0"
references:
- "https://nvd.nist.gov/vuln/detail/CVE-2021-41773"
steps:
- id: probe
action: tcp_connect
port: "{{port}}"
send: "GET /icons/.%2e/.%2e/etc/passwd HTTP/1.0\r\nHost: {{target_host}}\r\n\r\n"
store_as: resp
- id: match
action: match
source: resp.banner
pattern: "root:[x*!]:0:0"
on_match:
finding:
confidence_delta: 0.65
qod: 75
evidence: "LFI confirmed — /etc/passwd readable"
stop: true
```
### 服务 → 端口匹配
引擎会计算需要对每个 feed 运行哪些端口:
- `services: []` → 在**所有**发现的端口上运行
- `services: [http]` → 仅在 nmap 服务为 `http` 的端口上运行
- `services: [https]` → 仅在 nmap 服务为 `https` 的端口上运行
`{{port}}` 会根据匹配的服务自动注入。**切勿在 `vars:` 中重新定义它。**
### 自动变量
| 变量 | 值 |
|----------|-------|
| `{{target_host}}` | 目标主机 |
| `{{port}}` | 当前服务端口(由引擎注入) |
| `{{scheme}}` | `http` 或 `https` — 根据匹配的服务名称派生 |
| `{{oob_token}}` | 本次运行的唯一 UUID |
| `{{oob_url}}` | 完整的 OOB 服务器 URL |
| `{{oob_host}}` | OOB 服务器主机 |
| `{{oob_port}}` | OOB 服务器端口 |
| `{{oob_enabled}}` | 如果配置了 OOB 则为 `true`,否则为 `false` |
### 可用操作
| 操作 | 描述 |
|--------|-------------|
| `http_request` | 通过 reqwest 发起 HTTP/HTTPS 请求 |
| `tcp_connect` | 原始 TCP socket + banner 抓取(无 URL 标准化) |
| `tls_check` | TLS 检查 — 版本、密码套件、证书 |
| `ssh_check` | SSH banner + 身份验证方法 |
| `match` | 对上下文变量进行正则模式匹配 |
| `script` | 任意 Rhai 脚本 |
| `wait_oob` | 等待 OOB HTTP 回调 |
| `set_var` | 在上下文中分配变量 |
### `tcp_connect` 与 `http_request` 的对比
**对于任何在路径中编码的 payload(路径遍历、`%2e`、`%2f` 等),请使用 `tcp_connect`**。
Reqwest 在发送前会对 URL 进行标准化:先将 `%2e` 转换为 `.`,然后解析 `../`,这会破坏漏洞利用过程。
`tcp_connect` 会原样发送字节。
```
# 正确 — payload 保留
- action: tcp_connect
send: "GET /cgi-bin/.%2e/.%2e/etc/passwd HTTP/1.0\r\nHost: {{target_host}}\r\n\r\n"
store_as: resp
# 结果 — 使用 .banner(而非 .data)
- action: match
source: resp.banner
pattern: "root:.*:0:0"
```
### 置信度级别(QoD)
| QoD | 含义 |
|-----|---------|
| 50 | 常规检测(banner、版本) |
| 70 | 特定的 banner 匹配 |
| 75 | 功能性证明(LFI /etc/passwd、应用程序响应) |
| 97 | 收到 OOB 回调,或确认的 RCE |
| 100 | 具有已验证输出的完整漏洞利用 |
### 条件和 Rhai 脚本
```
- id: conditional-step
action: match
source: resp.banner
pattern: "Apache"
condition: "resp_status >= 200 && resp_status < 300"
on_match:
condition: "resp_banner.len > 100"
finding:
confidence_delta: 0.20
```
### 循环
```
steps:
- id: probe-paths
action: http_request
path: "{{current_path}}"
loop:
over: [/admin, /.git/config, /actuator/env]
var: current_path
store_as: resp
on_success:
condition: "resp_status == 200"
finding:
title: "Exposed path: {{current_path}}"
confidence_delta: 0.10
```
## 可用的 feed
| Feed | 产品 | 服务 | 检测方法 |
|------|---------|----------|-----------------|
| `CVE-2014-6271.yaml` | GNU Bash (Shellshock) | http, https | `() {:;};` header 注入 + RCE 模式 |
| `CVE-2017-9841.yaml` | PHPUnit | http, https | `eval-stdin.php` + MD5 RCE 证明 |
| `CVE-2019-11510.yaml` | Pulse Connect Secure | http, https | `%2F` 路径遍历 + `/etc/passwd` |
| `CVE-2021-26855.yaml` | Exchange ProxyLogon | http, https | SSRF cookie → `X-CalculatedBETarget` header |
| `CVE-2021-41773.yaml` | Apache 2.4.49 | http, https | LFI `/icons/` + 通过 mod_cgi 的 RCE |
| `CVE-2021-44228.yaml` | Log4j2 (Log4Shell) | http, https | 通过 `pom.properties` 获取版本(QoD 75)+ OOB JNDI(QoD 97,需要 `--oob`) |
| `CVE-2022-1388.yaml` | F5 BIG-IP | http, https | iControl REST 身份验证绕过 |
| `CVE-2022-26134.yaml` | Confluence | http, https | OGNL 注入 RCE |
| `CVE-2023-48795.yaml` | SSH (Terrapin) | ssh | ChaCha20-Poly1305 协商 + banner |
## Feed 验证
`schemas/feed.schema.json` 为所有 YAML feed 提供 JSON Schema draft-07 验证和自动补全。Red Hat YAML 扩展会通过 `.vscode/settings.json` 自动识别它。
```
# CLI 验证
npx ajv-cli validate -s schemas/feed.schema.json -d "tests/cve/*.yaml" --spec=draft7 --allow-union-types
```
## 测试基础设施(infra/)
可复现的端到端测试:Podman 容器(漏洞环境 + 已修复环境)、动态端口、自动断言。
### 命令
```
cd infra
# 构建本地镜像(proprietary mocks)
task build
# 测试单个 CVE(TP + TN)
task test CVE=CVE-2021-41773
# 顺序测试所有 CVE
task test-all
# 检查前提条件
task check-deps
```
### CVE 测试结构
每个 CVE 包含:
- `infra/inventories//hosts.yml` — 两台主机:`_vuln`(TP)和 `_patched`(TN)
- `infra/playbooks/.yml` — 委托给 `common_docker` 角色的 playbook
- `infra/docker//Dockerfile.vuln` + `Dockerfile.patched` — 镜像
`common_docker` 角色:
1. 动态分配空闲端口(Python socket)
2. 启动容器
3. 等待服务响应
4. 运行 `noctis scan --service :`
5. 检查发现结果数量(断言 TP 或 TN)
6. 销毁并清理容器
### Inventory 模板
```
# infra/inventories/CVE-XXXX-XXXXX/hosts.yml
all:
children:
cve_xxxx:
hosts:
app_vuln:
ansible_host: localhost
ansible_connection: local
target_host: "127.0.0.1"
target_service: http # nmap service — must match the feed
container_name: noctis_cve_xxxx_vuln
docker_image: "noctis/app-cve-xxxx:vuln"
expected_result: vulnerable # feed MUST produce a finding
app_vuln_https:
ansible_host: localhost
ansible_connection: local
target_host: "127.0.0.1"
target_service: https
container_port: 443 # container port to map (HTTP uses 80 by default)
container_name: noctis_cve_xxxx_vuln_https
docker_image: "noctis/app-cve-xxxx:vuln"
expected_result: vulnerable
app_patched:
ansible_host: localhost
ansible_connection: local
target_host: "127.0.0.1"
target_service: http
container_name: noctis_cve_xxxx_patched
docker_image: "noctis/app-cve-xxxx:patched"
expected_result: clean # feed MUST NOT produce a finding
app_patched_https:
ansible_host: localhost
ansible_connection: local
target_host: "127.0.0.1"
target_service: https
container_port: 443
container_name: noctis_cve_xxxx_patched_https
docker_image: "noctis/app-cve-xxxx:patched"
expected_result: clean
```
**`target_port` 不得出现在 inventory 中** — 它是在每次运行时动态分配的。
`container_port` 默认为 `80`;对于 HTTPS 主机,请将其设置为 `443`。
### 添加新的 CVE
1. `tests/cve/CVE-XXXX-XXXXX.yaml` — 包含稳定 UUID v4 uid 并设置了 `services:` 的 feed
2. `infra/inventories/CVE-XXXX-XXXXX/hosts.yml` — 两台没有 `target_port` 的主机
3. `infra/docker//Dockerfile.vuln` + `Dockerfile.patched`(或者用于专有设备的 Flask mock)
4. `infra/playbooks/CVE-XXXX-XXXXX.yml` — 复制现有的 playbook
5. `infra/site.yml` — 添加 `import_playbook: playbooks/CVE-XXXX-XXXXX.yml`
6. `infra/Taskfile.yml` — 将 CVE 添加到 `vars.INVENTORIES` 中
标签:REST API, Rust, 加密, 可视化界面, 插件系统, 漏洞扫描器, 系统提示词, 网络流量审计, 网络测绘, 通知系统, 黑盒测试