mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
1. -pwd 支持逗号分隔多个密码 之前 -pwd "123,456,root" 被当作单个密码,SSH root:123 无法匹配 现在逗号分隔为独立密码,空格保留(可能是密码的一部分) 2. -nobr 禁用爆破时仍检测 Redis 未授权访问 未授权访问是服务探测不是爆破,不应被 -nobr 跳过 将未授权检测移到 DisableBrute 判断之前 3. 指定端口时跳过 UDP 插件调度 -p 80 只扫 HTTP 时不需要 SNMP/BACnet/DNS 等 UDP 探测 仅在默认端口扫描时才分发 UDP 插件 效果: -p 80 从 9 秒降到 3 秒
381 lines
9.4 KiB
Go
381 lines
9.4 KiB
Go
package common
|
||
|
||
import (
|
||
"encoding/hex"
|
||
"fmt"
|
||
"net"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/shadow1ng/fscan/common/config"
|
||
"github.com/shadow1ng/fscan/common/i18n"
|
||
"github.com/shadow1ng/fscan/common/parsers"
|
||
)
|
||
|
||
/*
|
||
config_builder.go - 统一配置构建入口
|
||
|
||
从 FlagVars 直接构建 Config 和 State,消除中间层。
|
||
*/
|
||
|
||
// BuildConfig 从 FlagVars 构建完整的 Config 和 State
|
||
// 这是新的统一入口,替代原来的 Parse() + BuildConfigFromFlags() + updateGlobalVariables()
|
||
func BuildConfig(fv *FlagVars, info *HostInfo) (*Config, *State, error) {
|
||
// 1. 构建基础 Config(从 flag_config.go 的 BuildConfigFromFlags)
|
||
cfg := BuildConfigFromFlags(fv)
|
||
|
||
// 2. 创建 State
|
||
state := NewState()
|
||
|
||
// 3. 解析凭据
|
||
if err := parseCredentials(fv, cfg); err != nil {
|
||
return nil, nil, fmt.Errorf("%s: %w", i18n.GetText("config_credentials_parse_failed"), err)
|
||
}
|
||
|
||
// 4. 解析目标(主机、端口、URL)
|
||
if err := parseTargets(fv, info, cfg, state); err != nil {
|
||
return nil, nil, fmt.Errorf("%s: %w", i18n.GetText("config_targets_parse_failed"), err)
|
||
}
|
||
|
||
// 5. 应用日志级别
|
||
applyLogLevelFromConfig(fv)
|
||
|
||
return cfg, state, nil
|
||
}
|
||
|
||
// =============================================================================
|
||
// 凭据解析
|
||
// =============================================================================
|
||
|
||
func parseCredentials(fv *FlagVars, cfg *Config) error {
|
||
// 解析用户名
|
||
usernames, err := parseUsernames(fv)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(usernames) > 0 {
|
||
for serviceName := range cfg.Credentials.Userdict {
|
||
cfg.Credentials.Userdict[serviceName] = usernames
|
||
}
|
||
}
|
||
|
||
// 解析密码
|
||
passwords, err := parsePasswords(fv)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(passwords) > 0 {
|
||
cfg.Credentials.Passwords = passwords
|
||
}
|
||
|
||
// 解析用户密码对
|
||
pairs, err := parseUserPassPairs(fv)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(pairs) > 0 {
|
||
cfg.Credentials.UserPassPairs = pairs
|
||
}
|
||
|
||
// 解析哈希
|
||
hashValues, hashBytes, err := parseHashes(fv)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(hashValues) > 0 {
|
||
cfg.Credentials.HashValues = hashValues
|
||
cfg.Credentials.HashBytes = hashBytes
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func parseUsernames(fv *FlagVars) ([]string, error) {
|
||
var usernames []string
|
||
|
||
// 命令行用户名
|
||
if fv.Username != "" {
|
||
for _, u := range strings.Split(fv.Username, ",") {
|
||
u = strings.TrimSpace(u)
|
||
if u != "" {
|
||
usernames = append(usernames, u)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 从文件读取
|
||
if fv.UsersFile != "" {
|
||
if lines, err := parsers.ReadLinesFromFile(fv.UsersFile); err == nil {
|
||
usernames = append(usernames, lines...)
|
||
} else {
|
||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_users_failed", fv.UsersFile, err))
|
||
}
|
||
}
|
||
|
||
// 额外用户名
|
||
if fv.AddUsers != "" {
|
||
for _, u := range strings.Split(fv.AddUsers, ",") {
|
||
u = strings.TrimSpace(u)
|
||
if u != "" {
|
||
usernames = append(usernames, u)
|
||
}
|
||
}
|
||
}
|
||
|
||
return removeDuplicate(usernames), nil
|
||
}
|
||
|
||
func parsePasswords(fv *FlagVars) ([]string, error) {
|
||
var passwords []string
|
||
|
||
// 命令行密码(支持逗号分隔多个值,保留空格作为密码的一部分)
|
||
if fv.Password != "" {
|
||
for _, p := range strings.Split(fv.Password, ",") {
|
||
p = strings.TrimSpace(p)
|
||
if p != "" {
|
||
passwords = append(passwords, p)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 从文件读取
|
||
if fv.PasswordsFile != "" {
|
||
if lines, err := parsers.ReadLinesFromFile(fv.PasswordsFile); err == nil {
|
||
passwords = append(passwords, lines...)
|
||
} else {
|
||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_passwords_failed", fv.PasswordsFile, err))
|
||
}
|
||
}
|
||
|
||
// 额外密码
|
||
if fv.AddPasswords != "" {
|
||
passwords = append(passwords, splitCredentialValues(fv.AddPasswords)...)
|
||
}
|
||
|
||
return removeDuplicate(passwords), nil
|
||
}
|
||
|
||
func splitCredentialValues(input string) []string {
|
||
fields := strings.FieldsFunc(input, func(r rune) bool {
|
||
return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||
})
|
||
|
||
values := make([]string, 0, len(fields))
|
||
for _, field := range fields {
|
||
field = strings.TrimSpace(field)
|
||
if field != "" {
|
||
values = append(values, field)
|
||
}
|
||
}
|
||
return values
|
||
}
|
||
|
||
func parseUserPassPairs(fv *FlagVars) ([]config.CredentialPair, error) {
|
||
var pairs []config.CredentialPair
|
||
|
||
// 如果命令行同时指定了单个用户名和单个密码(不是逗号分隔的多个)
|
||
if fv.Username != "" && fv.Password != "" &&
|
||
!strings.Contains(fv.Username, ",") && !strings.Contains(fv.Password, ",") &&
|
||
fv.AddUsers == "" && fv.AddPasswords == "" &&
|
||
fv.UsersFile == "" && fv.PasswordsFile == "" && fv.UserPassFile == "" {
|
||
pairs = append(pairs, config.CredentialPair{
|
||
Username: strings.TrimSpace(fv.Username),
|
||
Password: fv.Password,
|
||
})
|
||
return pairs, nil
|
||
}
|
||
|
||
// 从文件读取用户密码对
|
||
if fv.UserPassFile != "" {
|
||
filePairs, err := parsers.ParseUserPassFile(fv.UserPassFile)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
pairs = append(pairs, filePairs...)
|
||
}
|
||
|
||
return pairs, nil
|
||
}
|
||
|
||
func parseHashes(fv *FlagVars) ([]string, [][]byte, error) {
|
||
var hashValues []string
|
||
var hashBytes [][]byte
|
||
|
||
// 命令行哈希
|
||
if fv.HashValue != "" {
|
||
hash := strings.TrimSpace(fv.HashValue)
|
||
if len(hash) != 32 {
|
||
return nil, nil, fmt.Errorf("invalid hash length: %s", hash)
|
||
}
|
||
hashByte, err := hex.DecodeString(hash)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
hashValues = append(hashValues, hash)
|
||
hashBytes = append(hashBytes, hashByte)
|
||
}
|
||
|
||
// 从文件读取
|
||
if fv.HashFile != "" {
|
||
fileHashes, fileHashBytes, err := parsers.ParseHashFile(fv.HashFile)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
hashValues = append(hashValues, fileHashes...)
|
||
hashBytes = append(hashBytes, fileHashBytes...)
|
||
}
|
||
|
||
return hashValues, hashBytes, nil
|
||
}
|
||
|
||
// =============================================================================
|
||
// 目标解析
|
||
// =============================================================================
|
||
|
||
func parseTargets(fv *FlagVars, info *HostInfo, cfg *Config, state *State) error {
|
||
// 检查是否为 host:port 格式
|
||
ports := fv.Ports
|
||
if info.Host != "" && strings.Contains(info.Host, ":") {
|
||
if _, portStr, err := net.SplitHostPort(info.Host); err == nil {
|
||
if port, portErr := strconv.Atoi(portStr); portErr == nil && port >= 1 && port <= 65535 {
|
||
// 有效的 host:port 格式
|
||
state.SetHostPorts([]string{info.Host})
|
||
info.Host = ""
|
||
ports = "" // 清空端口,避免双重扫描
|
||
}
|
||
}
|
||
}
|
||
|
||
// 解析 URL
|
||
urls, err := parseURLs(fv)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(urls) > 0 {
|
||
state.SetURLs(urls)
|
||
if info.URL == "" && len(urls) == 1 {
|
||
info.URL = urls[0]
|
||
}
|
||
}
|
||
|
||
// 更新端口配置
|
||
if ports != "" {
|
||
cfg.Target.Ports = ports
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func parseURLs(fv *FlagVars) ([]string, error) {
|
||
var urls []string
|
||
|
||
// 命令行 URL
|
||
if fv.TargetURL != "" {
|
||
for _, u := range strings.Split(fv.TargetURL, ",") {
|
||
u = strings.TrimSpace(u)
|
||
if u != "" {
|
||
urls = append(urls, normalizeURL(u))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 从文件读取
|
||
if fv.URLsFile != "" {
|
||
if lines, err := parsers.ReadLinesFromFile(fv.URLsFile); err == nil {
|
||
for _, line := range lines {
|
||
urls = append(urls, normalizeURL(line))
|
||
}
|
||
} else {
|
||
return nil, fmt.Errorf("%s", i18n.Tr("config_read_urls_failed", fv.URLsFile, err))
|
||
}
|
||
}
|
||
|
||
return removeDuplicate(urls), nil
|
||
}
|
||
|
||
func normalizeURL(rawURL string) string {
|
||
rawURL = strings.TrimSpace(rawURL)
|
||
if rawURL == "" {
|
||
return rawURL
|
||
}
|
||
lowerURL := strings.ToLower(rawURL)
|
||
if !strings.HasPrefix(lowerURL, "http://") && !strings.HasPrefix(lowerURL, "https://") {
|
||
return "http://" + normalizeSchemelessURLTarget(rawURL)
|
||
}
|
||
parsed, err := url.Parse(rawURL)
|
||
if err != nil || parsed.Host == "" {
|
||
return rawURL
|
||
}
|
||
normalizedHost := normalizeURLHost(parsed.Host)
|
||
if normalizedHost == parsed.Host {
|
||
return rawURL
|
||
}
|
||
parsed.Host = normalizedHost
|
||
normalized := parsed.String()
|
||
if schemeEnd := strings.Index(rawURL, "://"); schemeEnd >= 0 {
|
||
return rawURL[:schemeEnd] + normalized[len(parsed.Scheme):]
|
||
}
|
||
return normalized
|
||
}
|
||
|
||
func normalizeSchemelessURLTarget(rawURL string) string {
|
||
authority := rawURL
|
||
suffix := ""
|
||
if idx := strings.IndexAny(rawURL, "/?#"); idx >= 0 {
|
||
authority = rawURL[:idx]
|
||
suffix = rawURL[idx:]
|
||
}
|
||
return normalizeURLHost(authority) + suffix
|
||
}
|
||
|
||
func normalizeURLHost(host string) string {
|
||
if strings.HasPrefix(host, "[") {
|
||
return host
|
||
}
|
||
if ip := net.ParseIP(host); ip != nil && strings.Contains(host, ":") {
|
||
return "[" + host + "]"
|
||
}
|
||
return host
|
||
}
|
||
|
||
// =============================================================================
|
||
// 日志级别应用
|
||
// =============================================================================
|
||
|
||
func applyLogLevelFromConfig(fv *FlagVars) {
|
||
if fv.LogLevel == "" {
|
||
return
|
||
}
|
||
// 调用已有的 applyLogLevel 函数
|
||
applyLogLevel()
|
||
}
|
||
|
||
// =============================================================================
|
||
// 辅助函数
|
||
// =============================================================================
|
||
|
||
func removeDuplicate(old []string) []string {
|
||
if len(old) <= 1 {
|
||
return old
|
||
}
|
||
|
||
temp := make(map[string]struct{}, len(old))
|
||
result := make([]string, 0, len(old))
|
||
|
||
for _, item := range old {
|
||
if _, exists := temp[item]; !exists {
|
||
temp[item] = struct{}{}
|
||
result = append(result, item)
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// =============================================================================
|
||
// 保留 BuildConfigFromFlags 的原有实现(从 flag_config.go 移入)
|
||
// =============================================================================
|
||
|
||
// BuildConfigFromFlags 已在 flag_config.go 中定义,这里不重复
|