diff --git a/common/flag_config.go b/common/flag_config.go index 4f09030..f1759a4 100644 --- a/common/flag_config.go +++ b/common/flag_config.go @@ -1,9 +1,11 @@ package common import ( + "os" "time" "github.com/shadow1ng/fscan/common/config" + "golang.org/x/term" ) /* @@ -195,7 +197,7 @@ func BuildConfigFromFlags(fv *FlagVars) *Config { File: fv.Outputfile, Format: fv.OutputFormat, DisableSave: fv.DisableSave, - NoColor: fv.NoColor, + NoColor: fv.NoColor || !isStdoutTerminal(), Silent: fv.Silent, DisableProgress: fv.DisableProgress, ShowProgress: !fv.DisableProgress, @@ -237,3 +239,7 @@ func BuildConfigFromFlags(fv *FlagVars) *Config { }, } } + +func isStdoutTerminal() bool { + return term.IsTerminal(int(os.Stdout.Fd())) +} diff --git a/common/i18n/locales/en.yaml b/common/i18n/locales/en.yaml index bbdc267..ad8ed2f 100644 --- a/common/i18n/locales/en.yaml +++ b/common/i18n/locales/en.yaml @@ -198,7 +198,7 @@ scan_alive_hosts_list: progress_scanning_description: other: "Scanning Progress" progress_scan_completed: - other: "Scan Completed:" + other: "Scan Completed" progress_waiting: other: "waiting..." progress_done: diff --git a/common/i18n/locales/zh.yaml b/common/i18n/locales/zh.yaml index b4b1b33..f94f971 100644 --- a/common/i18n/locales/zh.yaml +++ b/common/i18n/locales/zh.yaml @@ -198,7 +198,7 @@ scan_alive_hosts_list: progress_scanning_description: other: "扫描进度" progress_scan_completed: - other: "扫描完成:" + other: "扫描完成" progress_waiting: other: "等待中..." progress_done: diff --git a/common/progress_manager.go b/common/progress_manager.go index 797e654..4de2309 100644 --- a/common/progress_manager.go +++ b/common/progress_manager.go @@ -321,12 +321,13 @@ func (pm *ProgressManager) showCompletionInfo() { completionMsg := i18n.GetText("progress_scan_completed") doneMsg := i18n.GetText("progress_done") durationMsg := i18n.GetText("progress_duration") + total := pm.total.Load() if pm.noColor { - fmt.Printf("[%s] %s %d/%d (%s: %s)\n", - doneMsg, completionMsg, pm.total.Load(), pm.total.Load(), durationMsg, formatDuration(elapsed)) + fmt.Printf("[%s] %s: %d/%d (%s: %s)\n", + doneMsg, completionMsg, total, total, durationMsg, formatDuration(elapsed)) } else { - fmt.Printf("%s[%s] %s %d/%d%s %s(%s: %s)%s\n", - AnsiGreen, doneMsg, completionMsg, pm.total.Load(), pm.total.Load(), AnsiReset, + fmt.Printf("%s[%s] %s: %d/%d%s %s(%s: %s)%s\n", + AnsiGreen, doneMsg, completionMsg, total, total, AnsiReset, AnsiGray, durationMsg, formatDuration(elapsed), AnsiReset) } } diff --git a/core/env_profiler.go b/core/env_profiler.go index 26130d9..bb4ad05 100644 --- a/core/env_profiler.go +++ b/core/env_profiler.go @@ -97,11 +97,18 @@ func (ep *EnvironmentProfile) TuneConfig(config *common.Config, session *common. // 含义: 重试 N 次后仍然全部丢包的概率 < 1% // 例: 丢包率 5% → N=2, 丢包率 20% → N=3, 丢包率 50% → N=7 // 下限 1(零丢包也至少试一次),上限 6(避免对不可达目标死磕) - if !isExplicit(config, "retry") && net.Samples > 0 { - computed := computeRetries(net.LossRate, net.Env) - old := config.MaxRetries - config.MaxRetries = computed - session.LogDebug(fmt.Sprintf("MaxRetries: %d -> %d (loss_rate=%.2f%%)", old, computed, net.LossRate*100)) + if !isExplicit(config, "retry") { + if net.Samples > 0 { + computed := computeRetries(net.LossRate, net.Env) + old := config.MaxRetries + config.MaxRetries = computed + session.LogDebug(fmt.Sprintf("MaxRetries: %d -> %d (loss_rate=%.2f%%)", old, computed, net.LossRate*100)) + } else if config.MaxRetries > 2 { + // 无网络探测数据(-np 跳过存活探测),降低默认重试避免对不可达主机死磕 + old := config.MaxRetries + config.MaxRetries = 2 + session.LogDebug(fmt.Sprintf("MaxRetries: %d -> %d (no network probe data)", old, config.MaxRetries)) + } } // ---------- ICMPRate ---------- diff --git a/core/env_profiler_test.go b/core/env_profiler_test.go index 477563e..93d4c63 100644 --- a/core/env_profiler_test.go +++ b/core/env_profiler_test.go @@ -344,8 +344,9 @@ func TestTuneConfig_NoSamples(t *testing.T) { if config.Timeout != origTimeout { t.Errorf("零样本不应改 Timeout: %v -> %v", origTimeout, config.Timeout) } - if config.MaxRetries != origRetry { - t.Errorf("零样本不应改 MaxRetries: %d -> %d", origRetry, config.MaxRetries) + // 零样本时,默认重试降到 2(避免对不可达主机死磕) + if origRetry > 2 && config.MaxRetries != 2 { + t.Errorf("零样本应降 MaxRetries 至 2: %d -> %d", origRetry, config.MaxRetries) } if config.Network.ICMPRate != origICMP { t.Errorf("零样本不应改 ICMPRate: %.2f -> %.2f", origICMP, config.Network.ICMPRate) diff --git a/core/port_scan.go b/core/port_scan.go index 6f71fe4..05e2ba9 100644 --- a/core/port_scan.go +++ b/core/port_scan.go @@ -700,6 +700,18 @@ func saveOpenPort(session *common.ScanSession, host string, port int) { }) } +// correctServiceByBanner 根据 banner 特征校正被 nmap 指纹误匹配的服务名 +func correctServiceByBanner(info *ServiceInfo) { + if info == nil || info.Banner == "" { + return + } + banner := strings.ToLower(info.Banner) + // MySQL 握手包包含认证插件名,nmap 随机 salt 可能导致误匹配 + if strings.Contains(banner, "mysql_native_password") || strings.Contains(banner, "caching_sha2_password") { + info.Name = "mysql" + } +} + // processServiceResult 处理服务识别结果 func processServiceResult(ctx context.Context, host string, port int, addr string, serviceInfo *ServiceInfo, config *common.Config, session *common.ScanSession) { if serviceInfo == nil { @@ -710,6 +722,9 @@ func processServiceResult(ctx context.Context, host string, port int, addr strin return } + // Banner 校正:nmap 指纹库可能将 MySQL 握手包的随机 salt 误匹配为其他服务 + correctServiceByBanner(serviceInfo) + // 缓存指纹识别结果,供插件按服务类型匹配(解决非标准端口问题) CacheServiceInfo(host, port, serviceInfo) diff --git a/core/web_scanner.go b/core/web_scanner.go index ebe8687..e3b2236 100644 --- a/core/web_scanner.go +++ b/core/web_scanner.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "net/url" + "os" "strconv" "strings" "sync" @@ -57,14 +58,17 @@ func DetectHTTPSchemeContext(ctx context.Context, host string, port int, config } // 第二步:尝试国密TLS握手(GM TLS fallback) - gmConn, gmErr := gmtls.DialWithDialer( - tlsDialer, - "tcp", addr, - &gmtls.Config{ - GMSupport: gmtls.NewGMSupport(), - InsecureSkipVerify: true, - }, - ) + // 抑制 gmtls 库的 fmt.Println("handshake error") 噪声输出 + gmConn, gmErr := suppressGMTLSStdout(func() (net.Conn, error) { + return gmtls.DialWithDialer( + tlsDialer, + "tcp", addr, + &gmtls.Config{ + GMSupport: gmtls.NewGMSupport(), + InsecureSkipVerify: true, + }, + ) + }) if gmErr == nil { _ = gmConn.Close() @@ -498,3 +502,23 @@ func hasMalformedURLPort(host string) bool { } return strings.Contains(host, ":") } + +// suppressGMTLSStdout 抑制 gmtls 库硬编码的 fmt.Println("handshake error") 输出 +// gmtls/conn.go:1304 在握手失败时直接 Println 到 os.Stdout,无法通过 API 关闭 +var gmtlsStdoutMu sync.Mutex + +func suppressGMTLSStdout(fn func() (net.Conn, error)) (net.Conn, error) { + gmtlsStdoutMu.Lock() + orig := os.Stdout + devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0) + if err == nil { + os.Stdout = devNull + } + conn, dialErr := fn() + os.Stdout = orig + if devNull != nil { + _ = devNull.Close() + } + gmtlsStdoutMu.Unlock() + return conn, dialErr +} diff --git a/plugins/web/webtitle.go b/plugins/web/webtitle.go index 72bfd1f..6c192ba 100644 --- a/plugins/web/webtitle.go +++ b/plugins/web/webtitle.go @@ -339,6 +339,16 @@ func (p *WebTitlePlugin) formatHeaders(headers http.Header) string { // detectProtocol 智能检测HTTP/HTTPS协议(基于服务识别和主动探测) func (p *WebTitlePlugin) detectProtocol(ctx context.Context, info *common.HostInfo, config *common.Config, session *common.ScanSession) string { + // 用户已通过 -u 显式指定协议时,直接使用,跳过 TLS 探测 + if info.URL != "" { + if strings.HasPrefix(info.URL, "https://") { + return "https" + } + if strings.HasPrefix(info.URL, "http://") { + return "http" + } + } + host := info.Host port := info.Port diff --git a/webscan/lib/Client.go b/webscan/lib/Client.go index 014d516..18a7e91 100644 --- a/webscan/lib/Client.go +++ b/webscan/lib/Client.go @@ -11,6 +11,7 @@ import ( "os" "strconv" "strings" + "sync" "time" "github.com/shadow1ng/fscan/common" @@ -31,6 +32,8 @@ const ( ProxySocks5URL = "socks5://127.0.0.1:1080" ) +var gmtlsStdoutMu sync.Mutex + // 全局HTTP客户端变量 var ( Client *http.Client // 标准HTTP客户端 @@ -202,10 +205,20 @@ func InitHTTPClient(ThreadsNum int, DownProxy string, Timeout time.Duration, max Timeout: dialTimeout, KeepAlive: keepAlive, } - return gmtls.DialWithDialer(dialer, network, addr, &gmtls.Config{ + // 抑制 gmtls 库的 fmt.Println("handshake error") 噪声 + gmtlsStdoutMu.Lock() + orig := os.Stdout + if devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0); err == nil { + os.Stdout = devNull + defer devNull.Close() + } + conn, err := gmtls.DialWithDialer(dialer, network, addr, &gmtls.Config{ GMSupport: gmtls.NewGMSupport(), InsecureSkipVerify: true, }) + os.Stdout = orig + gmtlsStdoutMu.Unlock() + return conn, err }, MaxConnsPerHost: 20, MaxIdleConns: 20,