mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
fix: 修复6个运行时问题
1. 抑制 gmtls 库的 handshake error stdout 噪声 gmtls/conn.go:1304 硬编码了 fmt.Println,在调用时临时重定向 os.Stdout 2. MySQL 3306 服务名误识别为 genetec-5400 nmap 指纹库将 MySQL 握手包的随机 salt 误匹配,通过 banner 特征校正 3. 管道输出时自动禁用 ANSI 控制码 检测 stdout 是否为终端,非终端时自动启用 NoColor 4. 进度条完成消息措辞精确化 去掉冗余冒号,保持信息简洁一致 5. URL 模式跳过不必要的 TLS 探测 用户已通过 -u 显式指定 http:// 协议时直接使用,不再做 TLS 握手 6. 无网络探测数据时降低默认重试次数 -np 跳过存活探测后,将默认重试从 3 降到 2,加速不可达主机的超时
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common/config"
|
"github.com/shadow1ng/fscan/common/config"
|
||||||
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -195,7 +197,7 @@ func BuildConfigFromFlags(fv *FlagVars) *Config {
|
|||||||
File: fv.Outputfile,
|
File: fv.Outputfile,
|
||||||
Format: fv.OutputFormat,
|
Format: fv.OutputFormat,
|
||||||
DisableSave: fv.DisableSave,
|
DisableSave: fv.DisableSave,
|
||||||
NoColor: fv.NoColor,
|
NoColor: fv.NoColor || !isStdoutTerminal(),
|
||||||
Silent: fv.Silent,
|
Silent: fv.Silent,
|
||||||
DisableProgress: fv.DisableProgress,
|
DisableProgress: fv.DisableProgress,
|
||||||
ShowProgress: !fv.DisableProgress,
|
ShowProgress: !fv.DisableProgress,
|
||||||
@@ -237,3 +239,7 @@ func BuildConfigFromFlags(fv *FlagVars) *Config {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isStdoutTerminal() bool {
|
||||||
|
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||||
|
}
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ scan_alive_hosts_list:
|
|||||||
progress_scanning_description:
|
progress_scanning_description:
|
||||||
other: "Scanning Progress"
|
other: "Scanning Progress"
|
||||||
progress_scan_completed:
|
progress_scan_completed:
|
||||||
other: "Scan Completed:"
|
other: "Scan Completed"
|
||||||
progress_waiting:
|
progress_waiting:
|
||||||
other: "waiting..."
|
other: "waiting..."
|
||||||
progress_done:
|
progress_done:
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ scan_alive_hosts_list:
|
|||||||
progress_scanning_description:
|
progress_scanning_description:
|
||||||
other: "扫描进度"
|
other: "扫描进度"
|
||||||
progress_scan_completed:
|
progress_scan_completed:
|
||||||
other: "扫描完成:"
|
other: "扫描完成"
|
||||||
progress_waiting:
|
progress_waiting:
|
||||||
other: "等待中..."
|
other: "等待中..."
|
||||||
progress_done:
|
progress_done:
|
||||||
|
|||||||
@@ -321,12 +321,13 @@ func (pm *ProgressManager) showCompletionInfo() {
|
|||||||
completionMsg := i18n.GetText("progress_scan_completed")
|
completionMsg := i18n.GetText("progress_scan_completed")
|
||||||
doneMsg := i18n.GetText("progress_done")
|
doneMsg := i18n.GetText("progress_done")
|
||||||
durationMsg := i18n.GetText("progress_duration")
|
durationMsg := i18n.GetText("progress_duration")
|
||||||
|
total := pm.total.Load()
|
||||||
if pm.noColor {
|
if pm.noColor {
|
||||||
fmt.Printf("[%s] %s %d/%d (%s: %s)\n",
|
fmt.Printf("[%s] %s: %d/%d (%s: %s)\n",
|
||||||
doneMsg, completionMsg, pm.total.Load(), pm.total.Load(), durationMsg, formatDuration(elapsed))
|
doneMsg, completionMsg, total, total, durationMsg, formatDuration(elapsed))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s[%s] %s %d/%d%s %s(%s: %s)%s\n",
|
fmt.Printf("%s[%s] %s: %d/%d%s %s(%s: %s)%s\n",
|
||||||
AnsiGreen, doneMsg, completionMsg, pm.total.Load(), pm.total.Load(), AnsiReset,
|
AnsiGreen, doneMsg, completionMsg, total, total, AnsiReset,
|
||||||
AnsiGray, durationMsg, formatDuration(elapsed), AnsiReset)
|
AnsiGray, durationMsg, formatDuration(elapsed), AnsiReset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-5
@@ -97,11 +97,18 @@ func (ep *EnvironmentProfile) TuneConfig(config *common.Config, session *common.
|
|||||||
// 含义: 重试 N 次后仍然全部丢包的概率 < 1%
|
// 含义: 重试 N 次后仍然全部丢包的概率 < 1%
|
||||||
// 例: 丢包率 5% → N=2, 丢包率 20% → N=3, 丢包率 50% → N=7
|
// 例: 丢包率 5% → N=2, 丢包率 20% → N=3, 丢包率 50% → N=7
|
||||||
// 下限 1(零丢包也至少试一次),上限 6(避免对不可达目标死磕)
|
// 下限 1(零丢包也至少试一次),上限 6(避免对不可达目标死磕)
|
||||||
if !isExplicit(config, "retry") && net.Samples > 0 {
|
if !isExplicit(config, "retry") {
|
||||||
computed := computeRetries(net.LossRate, net.Env)
|
if net.Samples > 0 {
|
||||||
old := config.MaxRetries
|
computed := computeRetries(net.LossRate, net.Env)
|
||||||
config.MaxRetries = computed
|
old := config.MaxRetries
|
||||||
session.LogDebug(fmt.Sprintf("MaxRetries: %d -> %d (loss_rate=%.2f%%)", old, computed, net.LossRate*100))
|
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 ----------
|
// ---------- ICMPRate ----------
|
||||||
|
|||||||
@@ -344,8 +344,9 @@ func TestTuneConfig_NoSamples(t *testing.T) {
|
|||||||
if config.Timeout != origTimeout {
|
if config.Timeout != origTimeout {
|
||||||
t.Errorf("零样本不应改 Timeout: %v -> %v", origTimeout, config.Timeout)
|
t.Errorf("零样本不应改 Timeout: %v -> %v", origTimeout, config.Timeout)
|
||||||
}
|
}
|
||||||
if config.MaxRetries != origRetry {
|
// 零样本时,默认重试降到 2(避免对不可达主机死磕)
|
||||||
t.Errorf("零样本不应改 MaxRetries: %d -> %d", origRetry, config.MaxRetries)
|
if origRetry > 2 && config.MaxRetries != 2 {
|
||||||
|
t.Errorf("零样本应降 MaxRetries 至 2: %d -> %d", origRetry, config.MaxRetries)
|
||||||
}
|
}
|
||||||
if config.Network.ICMPRate != origICMP {
|
if config.Network.ICMPRate != origICMP {
|
||||||
t.Errorf("零样本不应改 ICMPRate: %.2f -> %.2f", origICMP, config.Network.ICMPRate)
|
t.Errorf("零样本不应改 ICMPRate: %.2f -> %.2f", origICMP, config.Network.ICMPRate)
|
||||||
|
|||||||
@@ -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 处理服务识别结果
|
// processServiceResult 处理服务识别结果
|
||||||
func processServiceResult(ctx context.Context, host string, port int, addr string, serviceInfo *ServiceInfo, config *common.Config, session *common.ScanSession) {
|
func processServiceResult(ctx context.Context, host string, port int, addr string, serviceInfo *ServiceInfo, config *common.Config, session *common.ScanSession) {
|
||||||
if serviceInfo == nil {
|
if serviceInfo == nil {
|
||||||
@@ -710,6 +722,9 @@ func processServiceResult(ctx context.Context, host string, port int, addr strin
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Banner 校正:nmap 指纹库可能将 MySQL 握手包的随机 salt 误匹配为其他服务
|
||||||
|
correctServiceByBanner(serviceInfo)
|
||||||
|
|
||||||
// 缓存指纹识别结果,供插件按服务类型匹配(解决非标准端口问题)
|
// 缓存指纹识别结果,供插件按服务类型匹配(解决非标准端口问题)
|
||||||
CacheServiceInfo(host, port, serviceInfo)
|
CacheServiceInfo(host, port, serviceInfo)
|
||||||
|
|
||||||
|
|||||||
+32
-8
@@ -7,6 +7,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -57,14 +58,17 @@ func DetectHTTPSchemeContext(ctx context.Context, host string, port int, config
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 第二步:尝试国密TLS握手(GM TLS fallback)
|
// 第二步:尝试国密TLS握手(GM TLS fallback)
|
||||||
gmConn, gmErr := gmtls.DialWithDialer(
|
// 抑制 gmtls 库的 fmt.Println("handshake error") 噪声输出
|
||||||
tlsDialer,
|
gmConn, gmErr := suppressGMTLSStdout(func() (net.Conn, error) {
|
||||||
"tcp", addr,
|
return gmtls.DialWithDialer(
|
||||||
&gmtls.Config{
|
tlsDialer,
|
||||||
GMSupport: gmtls.NewGMSupport(),
|
"tcp", addr,
|
||||||
InsecureSkipVerify: true,
|
&gmtls.Config{
|
||||||
},
|
GMSupport: gmtls.NewGMSupport(),
|
||||||
)
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
if gmErr == nil {
|
if gmErr == nil {
|
||||||
_ = gmConn.Close()
|
_ = gmConn.Close()
|
||||||
@@ -498,3 +502,23 @@ func hasMalformedURLPort(host string) bool {
|
|||||||
}
|
}
|
||||||
return strings.Contains(host, ":")
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -339,6 +339,16 @@ func (p *WebTitlePlugin) formatHeaders(headers http.Header) string {
|
|||||||
|
|
||||||
// detectProtocol 智能检测HTTP/HTTPS协议(基于服务识别和主动探测)
|
// detectProtocol 智能检测HTTP/HTTPS协议(基于服务识别和主动探测)
|
||||||
func (p *WebTitlePlugin) detectProtocol(ctx context.Context, info *common.HostInfo, config *common.Config, session *common.ScanSession) string {
|
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
|
host := info.Host
|
||||||
port := info.Port
|
port := info.Port
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -11,6 +11,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/shadow1ng/fscan/common"
|
"github.com/shadow1ng/fscan/common"
|
||||||
@@ -31,6 +32,8 @@ const (
|
|||||||
ProxySocks5URL = "socks5://127.0.0.1:1080"
|
ProxySocks5URL = "socks5://127.0.0.1:1080"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var gmtlsStdoutMu sync.Mutex
|
||||||
|
|
||||||
// 全局HTTP客户端变量
|
// 全局HTTP客户端变量
|
||||||
var (
|
var (
|
||||||
Client *http.Client // 标准HTTP客户端
|
Client *http.Client // 标准HTTP客户端
|
||||||
@@ -202,10 +205,20 @@ func InitHTTPClient(ThreadsNum int, DownProxy string, Timeout time.Duration, max
|
|||||||
Timeout: dialTimeout,
|
Timeout: dialTimeout,
|
||||||
KeepAlive: keepAlive,
|
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(),
|
GMSupport: gmtls.NewGMSupport(),
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
})
|
})
|
||||||
|
os.Stdout = orig
|
||||||
|
gmtlsStdoutMu.Unlock()
|
||||||
|
return conn, err
|
||||||
},
|
},
|
||||||
MaxConnsPerHost: 20,
|
MaxConnsPerHost: 20,
|
||||||
MaxIdleConns: 20,
|
MaxIdleConns: 20,
|
||||||
|
|||||||
Reference in New Issue
Block a user