面向 Linux network namespace 的资产扫描和漏洞验证工具。输入一个 namespace 和一组 IP,工具先用 fscan SDK 做端口、服务、Web 指纹识别,再把目标交给 nuclei SDK 做漏洞模板验证。
适合的使用环境:
- 云平台、容器平台、虚拟化平台中,每个租户或网络隔离域对应一个 Linux network namespace。
- 运维侧已经拿到某个 namespace 下的一批内网 IP,需要快速识别资产和做定向漏洞验证。
- 希望以 CLI 临时扫描,或以 HTTP 服务方式接收扫描任务并持久化结果。
扫描流程:
namespace + IP 列表
-> ip netns exec 进入目标网络命名空间
-> fscan SDK 识别端口、服务、Web 信息
-> 轻量 HTTP 指纹补充
-> 可选 poc_map 按指纹缩小 nuclei 模板范围
-> nuclei SDK 执行漏洞验证
-> 输出 JSON 或写入 SQLite
目录:
准备二进制:
VERSION=v0.1.0
BASE_URL=https://github.com/systemime/fast_scan_tool/releases/download/${VERSION}case"$(uname -m)"in
x86_64) ASSET=vulnscan-wrapper-linux-amd64 ;;
aarch64|arm64) ASSET=vulnscan-wrapper-linux-arm64 ;;
armv7l|armv7*) ASSET=vulnscan-wrapper-linux-armv7 ;;
*) echo"unsupported arch: $(uname -m)">&2;exit 1 ;;
esac
curl -L -o /tmp/vulnscan-wrapper "${BASE_URL}/${ASSET}"
chmod +x /tmp/vulnscan-wrapper准备 nuclei 模板:
sudo git clone https://github.com/adysec/nuclei_poc.git /opt/nuclei_poc最小扫描命令:
VST_POC_DIR=/opt/nuclei_poc/poc_high_quality \
/tmp/vulnscan-wrapper scan \
-namespace ns1 \
-ips 10.0.0.1,10.0.0.2 \
-out scan-result.json这条命令表示:在 ns1 这个 network namespace 内扫描 10.0.0.1 和 10.0.0.2,nuclei 模板从 /opt/nuclei_poc/poc_high_quality 加载,结果写入 scan-result.json。
CLI 模式不启动 HTTP 服务,不写 SQLite。单个 IP 扫描失败时,错误会写到该 host 的 error 字段。
查看帮助:
/tmp/vulnscan-wrapper --help
/tmp/vulnscan-wrapper scan -h安装二进制:
sudo install -d -m 0755 /opt/vulnscan /etc/vulnscan /var/lib/vulnscan /var/log/vulnscan
sudo install -m 0755 /tmp/vulnscan-wrapper /opt/vulnscan/vulnscan-wrapper准备环境文件:
sudo tee /etc/vulnscan/vulnscan.env >/dev/null <<'EOF'VST_ADDR=127.0.0.1:8080VST_DB=/var/lib/vulnscan/tasks.dbVST_POC_DIR=/opt/nuclei_poc/poc_high_qualityVST_WORKERS=6EOF创建 systemd 服务:
sudo tee /etc/systemd/system/vulnscan-wrapper.service >/dev/null <<'EOF'[Unit]Description=Vuln Scan ToolAfter=network-online.targetWants=network-online.target[Service]Type=simpleUser=rootGroup=rootWorkingDirectory=/opt/vulnscanEnvironmentFile=/etc/vulnscan/vulnscan.envExecStart=/opt/vulnscan/vulnscan-wrapperRestart=on-failureRestartSec=5s[Install]WantedBy=multi-user.targetEOF
sudo systemctl daemon-reload
sudo systemctl enable --now vulnscan-wrapper最小 API 调用:
curl -sS http://127.0.0.1:8080/scan \
-H 'Content-Type: application/json' \
-d '{"namespace":"ns1","ip_hosts":["10.0.0.1","10.0.0.2"],"timeout":600}'查询结果:
curl -sS http://127.0.0.1:8080/scan/ns1
curl -sS 'http://127.0.0.1:8080/scan?namespace=ns1'批量提交多个 namespace:
curl -sS http://127.0.0.1:8080/scan \
-H 'Content-Type: application/json' \
-d '[{"namespace":"ns1","ip_hosts":["10.0.0.1"],"timeout":600},{"namespace":"ns2","ip_hosts":["10.0.1.1"],"timeout":600}]'HTTP API 只允许本机访问。远程调用用 SSH tunnel:
ssh -L 8080:127.0.0.1:8080 root@target-serverCLI 扫描结果写入 -out 指定的 JSON 文件:
{
"namespace": "ns1",
"hosts": [
{
"ip": "10.0.0.1",
"assets": [
{
"type": "service",
"target": "10.0.0.1:80",
"port": 80,
"service": "http",
"url": "http://10.0.0.1:80",
"title": "Example",
"server": "nginx",
"status_code": 200,
"is_web": true
}
],
"vulnerabilities": 1,
"vulnerability_ids": ["CVE-2024-0001"]
},
{
"ip": "10.0.0.2",
"assets": [],
"vulnerabilities": 0,
"vulnerability_ids": [],
"error": "context deadline exceeded"
}
]
}HTTP 提交任务返回新增和移除的 host。提交单个 namespace 返回对象,批量提交返回对象数组:
{
"namespace": "ns1",
"host_count": 2,
"timeout": 600,
"added": ["10.0.0.1", "10.0.0.2"],
"removed": []
}HTTP 查询结果从 SQLite 读取,host 状态为 pending、running、completed 或 timeout:
{
"namespace": "ns1",
"host_count": 2,
"timeout": 600,
"hosts": [
{
"ip": "10.0.0.1",
"status": "completed",
"timeout": 600,
"assets": [],
"vulnerabilities": 0,
"vulnerability_ids": [],
"started_at": "2026-07-03T10:00:00Z",
"finished_at": "2026-07-03T10:02:00Z"
}
]
}HTTP 错误统一返回:
{"error":"namespace not found"}CLI 参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
-namespace | 空 | Linux network namespace 名称,必填 |
-ips | 空 | 逗号分隔的 IP/host 列表,必填 |
-out | scan-result.json | JSON 输出文件 |
-timeout | 600 | 单 host 超时,秒 |
-workers | 6 | CLI 模式下同时扫描的 host 数 |
-total-timeout | 1800 | CLI 总超时,秒;设为 0 关闭 |
HTTP 请求字段:
| 字段 | 类型 | 说明 |
|---|---|---|
namespace | string | Linux network namespace 名称 |
ip_hosts | string array | 要扫描的 IP/host,会去重并排序;不能为空 |
timeout | int | 单 host 超时,秒;小于等于 0 时使用 600 |
环境变量和 JSON 配置字段:
| 环境变量 | JSON 字段 | 默认值 | 说明 |
|---|---|---|---|
VST_CONFIG / CONFIG | - | 空 | JSON 配置文件路径;环境变量优先 |
VST_ADDR / ADDR | addr | 127.0.0.1:8080 | HTTP 监听地址 |
VST_DB / DB_PATH | db_path | tasks.db | SQLite 数据库路径 |
VST_POC_DIR / POC_DIR | poc_dir | 程序同级 poc | nuclei 模板目录 |
VST_POC_MAP / POC_MAP | poc_map | 空 | 可选,资产指纹到模板路径的映射 |
VST_FSCAN_PATH / FSCAN_PATH | fscan_path | fscan | 兼容旧配置;SDK 路径不调用外部二进制 |
VST_FSCAN_THREADS / FSCAN_THREADS | fscan_threads | 256 | fscan SDK 并发线程 |
VST_FSCAN_TIMEOUT / FSCAN_TIMEOUT | fscan_timeout | 3 | fscan 单探测超时,秒 |
VST_FSCAN_PORTS / FSCAN_PORTS | fscan_ports | fscan 默认端口 | 逗号分隔端口列表 |
VST_WORKERS / WORKERS | workers | CPU * 1.5 向上取整 | HTTP 服务模式下 host 并发 |
VST_NUCLEI_CONCURRENCY / NUCLEI_CONCURRENCY | nuclei_concurrency | 25 | 单 host 内 nuclei 模板并发 |
VST_NUCLEI_HOST_CONCURRENCY / NUCLEI_HOST_CONCURRENCY | nuclei_host_concurrency | 1 | 单 host 内 nuclei target 并发 |
VST_NUCLEI_TIMEOUT / NUCLEI_TIMEOUT | nuclei_timeout | 5 | nuclei 单请求/连接超时,秒 |
VST_HTTP_FINGERPRINT_TIMEOUT / HTTP_FINGERPRINT_TIMEOUT | http_fingerprint_timeout | 2 | fscan 后追加 HTTP 指纹超时,秒;设为 0 关闭 |
示例 JSON 配置:
{
"addr": "127.0.0.1:8080",
"db_path": "/var/lib/vulnscan/tasks.db",
"poc_dir": "/opt/nuclei_poc/poc_high_quality",
"poc_map": "/etc/vulnscan/poc-map.json",
"fscan_path": "fscan",
"workers": 6,
"fscan_threads": 256,
"fscan_timeout": 3,
"fscan_ports": [80, 443, 8080, 8443, 22, 3306, 5432, 6379],
"nuclei_concurrency": 50,
"nuclei_host_concurrency": 10,
"nuclei_timeout": 2,
"http_fingerprint_timeout": 2
}推荐使用:
sudo git clone https://github.com/adysec/nuclei_poc.git /opt/nuclei_poc常用目录:
/opt/nuclei_poc/poc_high_quality
/opt/nuclei_poc/poc_all
poc_high_quality 模板数量更少,适合控制扫描时长;poc_all 更全,但耗时更长。
也可以使用官方模板仓库:
sudo git clone https://github.com/projectdiscovery/nuclei-templates.git /opt/nuclei-templates此时设置:
VST_POC_DIR=/opt/nuclei-templates只要目录下是 nuclei 支持的 .yaml / .yml 模板,就可以直接作为 VST_POC_DIR 使用,不需要转换。
poc_map.json 不是 POC 仓库。它是可选加速文件,用资产指纹命中模板目录,减少 nuclei 实际加载的模板数量。
格式:
{
"http": ["http/"],
"https": ["http/"],
"port:80": ["http/"],
"port:443": ["http/"],
"nginx": ["nginx/"],
"tomcat": ["tomcat/"],
"jenkins": ["jenkins/"],
"redis": ["redis/"],
"_baseline": ["http/"]
}规则:
- key 是指纹关键词,会和 fscan/HTTP 识别出的
service、protocol、server、title、banner、fingerprints、port:<端口>做包含匹配。 - value 是
VST_POC_DIR下的相对目录或模板文件。 - 未配置、读取失败或解析失败
poc_map时,直接加载整个VST_POC_DIR。 - 配置了
poc_map且命中指纹时,只加载命中路径和_baseline/baseline。 - 配置了
poc_map但未命中指纹时,只加载_fallback/fallback/_baseline/baseline;这些也没有时跳过 nuclei。 - value 越出
VST_POC_DIR会被忽略。
从 nuclei_poc 自动生成 poc_map.json:
POC_DIR=/opt/nuclei_poc/poc_high_quality
OUT=/etc/vulnscan/poc-map.json
sudo install -d -m 0755 "$(dirname "$OUT")"
POC_DIR="$POC_DIR" OUT=/tmp/poc-map.json python3 - <<'PY'import jsonimport osimport refrom pathlib import Pathpoc_dir = Path(os.environ["POC_DIR"]).resolve()out = Path(os.environ["OUT"])keywords = [ "http", "https", "nginx", "apache", "iis", "tomcat", "jenkins", "weblogic", "nacos", "spring", "thinkphp", "wordpress", "gitlab", "grafana", "prometheus", "kibana", "elasticsearch", "elastic", "redis", "mysql", "postgres", "mongodb", "minio", "harbor", "ssh", "ftp", "smb", "rdp",]mapping = {}def add(key, rel): mapping.setdefault(key, set()).add(rel)for path in poc_dir.rglob("*"): if path.suffix.lower() not in {".yaml", ".yml"}: continue rel = path.relative_to(poc_dir).as_posix() text = rel.lower() tokens = set(re.split(r"[^a-z0-9]+", text)) for key in keywords: if key in text or key in tokens: parent = path.parent.relative_to(poc_dir).as_posix() add(key, parent + "/" if parent != "." else rel)for key in ("http", "https", "port:80", "port:443"): for candidate in ("http/", "web/", "network/"): if (poc_dir / candidate).exists(): add(key, candidate)common_ports = { "ssh": ["port:22"], "ftp": ["port:21"], "smb": ["port:445"], "rdp": ["port:3389"], "redis": ["port:6379"], "mysql": ["port:3306"], "postgres": ["port:5432"], "mongodb": ["port:27017"], "elasticsearch": ["port:9200", "port:9300"],}for service, ports in common_ports.items(): for value in mapping.get(service, set()): for port in ports: add(port, value)if "http" in mapping: mapping["_baseline"] = set(mapping["http"])serializable = {key: sorted(values) for key, values in sorted(mapping.items())}out.write_text(json.dumps(serializable, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")print(f"wrote {out} with {len(serializable)} keys")PY
sudo install -m 0644 /tmp/poc-map.json "$OUT"校验生成结果:
POC_DIR=/opt/nuclei_poc/poc_high_quality
OUT=/etc/vulnscan/poc-map.json
POC_DIR="$POC_DIR" OUT="$OUT" python3 - <<'PY'import jsonimport osfrom pathlib import Pathpoc_dir = Path(os.environ["POC_DIR"]).resolve()mapping = json.load(open(os.environ["OUT"], encoding="utf-8"))errors = []for key, values in mapping.items(): if not isinstance(values, list): errors.append(f"{key}: value must be list") continue for value in values: path = (poc_dir / value).resolve() if poc_dir not in (path, *path.parents): errors.append(f"{key}: out of poc_dir: {value}") elif not path.exists(): errors.append(f"{key}: missing: {value}")if errors: print("\n".join(errors)) raise SystemExit(1)print("poc-map ok")PY自动生成的 map 只是按路径关键词归类,适合作为起点。上线前看日志里的 templates_selected、poc_map_hit_rate,再删掉误命中的 key 或补充业务常见组件。
使用 map:
VST_POC_DIR=/opt/nuclei_poc/poc_high_quality \
VST_POC_MAP=/etc/vulnscan/poc-map.json \
/tmp/vulnscan-wrapper scan \
-namespace ns1 \
-ips 10.0.0.1 \
-out scan-result.json源码布局:
/opt/project/
fast_scan_tool/
fscan/
go.mod 使用本地 replace:
replace github.com/shadow1ng/fscan => ../fscan
本地构建:
go build -trimpath -o vulnscan-wrapper .交叉编译:
mkdir -p dist
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOAMD64=v1 \
go build -trimpath -ldflags='-s -w' -o dist/vulnscan-wrapper-linux-amd64 .
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
go build -trimpath -ldflags='-s -w' -o dist/vulnscan-wrapper-linux-arm64 .
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 \
go build -trimpath -ldflags='-s -w' -o dist/vulnscan-wrapper-linux-armv7 .
sha256sum dist/vulnscan-wrapper-linux-* poc-map-budget.example.json > dist/SHA256SUMSSQLite 使用纯 Go 驱动 modernc.org/sqlite,不需要 CGO 或系统 sqlite 开发库。
日志里关注这一行:
nuclei templates_total=34840 templates_selected=111 template_reduction_ratio=99.7% poc_map_hit_rate=100.0% poc_map_assets_matched=4/4 fallback_full_scan=false skip_nuclei=false fingerprint_hits=http,port:80
字段含义:
| 字段 | 说明 |
|---|---|
templates_total | VST_POC_DIR 下模板总数 |
templates_selected | 本次实际加载的模板数 |
poc_map_hit_rate | 资产命中 poc_map 的比例 |
fallback_full_scan=true | 没有可用 map,回退全量模板 |
skip_nuclei=true | 配了 map 但未命中且没有 fallback/baseline |
资源抖动时先降 VST_WORKERS,再降 VST_NUCLEI_CONCURRENCY。排查目标 9100 端口时,用目标 namespace 和目标 IP:
ip netns exec<namespace> curl http://<目标IP>:9100/metrics