diff --git a/README.md b/README.md index 8c29b8c..2564245 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 内网综合扫描工具,一键自动化漏扫。 -**版本**: 2.2.0-rc +**版本**: 2.2.0-rc.1 ## 功能特性 @@ -186,10 +186,10 @@ ```bash # 标准编译 -go build -ldflags="-s -w" -trimpath -o fscan main.go +go build -ldflags="-s -w" -trimpath -o fscan . # 带Web管理界面 -go build -tags web -ldflags="-s -w" -trimpath -o fscan main.go +go build -tags web -ldflags="-s -w" -trimpath -o fscan-web . ``` ## 安装 diff --git a/README_EN.md b/README_EN.md index 7d0104a..d55b95d 100644 --- a/README_EN.md +++ b/README_EN.md @@ -4,7 +4,7 @@ Comprehensive intranet scanning tool for automated vulnerability assessment. -**Version**: 2.2.0-rc +**Version**: 2.2.0-rc.1.1 ## Features @@ -185,10 +185,10 @@ Comprehensive intranet scanning tool for automated vulnerability assessment. ```bash # Standard build -go build -ldflags="-s -w" -trimpath -o fscan main.go +go build -ldflags="-s -w" -trimpath -o fscan . # With Web UI -go build -tags web -ldflags="-s -w" -trimpath -o fscan main.go +go build -tags web -ldflags="-s -w" -trimpath -o fscan-web . ``` ## Install diff --git a/common/flag.go b/common/flag.go index 4ea91d5..f6bfd2e 100644 --- a/common/flag.go +++ b/common/flag.go @@ -137,6 +137,7 @@ func Flag(Info *HostInfo) error { flag.StringVar(&fv.TargetURL, "u", "", i18n.GetText("flag_target_url")) flag.StringVar(&fv.URLsFile, "uf", "", i18n.GetText("flag_urls_file")) flag.StringVar(&fv.Cookie, "cookie", "", i18n.GetText("flag_cookie")) + flag.StringVar(&fv.UserAgent, "ua", "", i18n.GetText("flag_user_agent")) flag.Int64Var(&fv.WebTimeout, "wt", 5, i18n.GetText("flag_web_timeout")) flag.IntVar(&fv.MaxRedirects, "max-redirect", 10, i18n.GetText("flag_max_redirects")) flag.StringVar(&fv.HTTPProxy, "proxy", "", i18n.GetText("flag_http_proxy")) diff --git a/common/flag_config.go b/common/flag_config.go index ea28d5a..b2b4d3a 100644 --- a/common/flag_config.go +++ b/common/flag_config.go @@ -27,7 +27,6 @@ type FlagVars struct { ExcludeHostsFile string Ports string ExcludePorts string - AddPorts string HostsFile string PortsFile string @@ -226,7 +225,7 @@ func BuildConfigFromFlags(fv *FlagVars) *Config { }, HTTP: HTTPConfig{ Cookie: fv.Cookie, - UserAgent: fv.UserAgent, + UserAgent: defaultUserAgent(fv.UserAgent), Accept: fv.Accept, }, LocalExploit: LocalExploitConfig{ @@ -246,3 +245,11 @@ func BuildConfigFromFlags(fv *FlagVars) *Config { func isStdoutTerminal() bool { return term.IsTerminal(int(os.Stdout.Fd())) } + +// defaultUserAgent 用户未通过 -ua 指定时回退到默认 UA,避免发送空 User-Agent 被 WAF 识别 +func defaultUserAgent(ua string) string { + if ua != "" { + return ua + } + return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" +} diff --git a/common/flag_test.go b/common/flag_test.go index a4a9b3c..3e02dd7 100644 --- a/common/flag_test.go +++ b/common/flag_test.go @@ -955,8 +955,9 @@ func TestBuildConfigFromFlags_BoundaryValues(t *testing.T) { if cfg.HTTP.Cookie != "" { t.Errorf("Cookie 应该为空") } - if cfg.HTTP.UserAgent != "" { - t.Errorf("UserAgent 应该为空") + // 空输入回退到默认 UA,避免发送空 User-Agent + if cfg.HTTP.UserAgent == "" { + t.Errorf("UserAgent 空输入应回退到默认 UA") } }, }, diff --git a/common/i18n/locales/en.yaml b/common/i18n/locales/en.yaml index ad8ed2f..c308cf6 100644 --- a/common/i18n/locales/en.yaml +++ b/common/i18n/locales/en.yaml @@ -64,6 +64,8 @@ flag_urls_file: other: "URLs file" flag_cookie: other: "HTTP Cookie" +flag_user_agent: + other: "Custom User-Agent header" flag_web_timeout: other: "Web timeout" flag_max_redirects: diff --git a/common/i18n/locales/zh.yaml b/common/i18n/locales/zh.yaml index f94f971..94eb728 100644 --- a/common/i18n/locales/zh.yaml +++ b/common/i18n/locales/zh.yaml @@ -64,6 +64,8 @@ flag_urls_file: other: "URL文件" flag_cookie: other: "HTTP Cookie" +flag_user_agent: + other: "自定义 User-Agent 请求头" flag_web_timeout: other: "Web超时时间" flag_max_redirects: diff --git a/core/web_scanner.go b/core/web_scanner.go index 24e372b..b73811e 100644 --- a/core/web_scanner.go +++ b/core/web_scanner.go @@ -56,19 +56,21 @@ func DetectHTTPSchemeContext(ctx context.Context, host string, port int, config return "https" } - // 第二步:尝试国密TLS握手(GM TLS fallback) - gmConn, gmErr := gmtls.DialWithDialer( - tlsDialer, - "tcp", addr, - &gmtls.Config{ - GMSupport: gmtls.NewGMSupport(), - InsecureSkipVerify: true, - }, - ) - - if gmErr == nil { - _ = gmConn.Close() - return "https-gm" + // 第二步:仅在标准 TLS 握手级别失败(cipher/protocol 不兼容)时尝试国密 + // 连接级别失败(timeout/refused/非 TLS 端口)不需要尝试 + if maybeGMTLS(err) { + gmConn, gmErr := gmtls.DialWithDialer( + tlsDialer, + "tcp", addr, + &gmtls.Config{ + GMSupport: gmtls.NewGMSupport(), + InsecureSkipVerify: true, + }, + ) + if gmErr == nil { + _ = gmConn.Close() + return "https-gm" + } } // TLS和GM TLS都失败,尝试HTTP @@ -491,6 +493,20 @@ func (s *WebScanStrategy) createTargetFromURLWithSession(baseInfo common.HostInf return &urlInfo } +// maybeGMTLS 判断标准 TLS 握手错误是否可能是国密服务端 +// 只有 cipher/protocol 层面的不兼容才值得尝试国密回退 +// 连接超时、拒绝、非 TLS 端口等连接级错误直接跳过 +func maybeGMTLS(err error) bool { + if err == nil { + return false + } + s := err.Error() + return strings.Contains(s, "handshake failure") || + strings.Contains(s, "protocol version") || + strings.Contains(s, "no mutual") || + strings.Contains(s, "cipher suite") +} + func hasMalformedURLPort(host string) bool { if strings.HasPrefix(host, "[") { end := strings.LastIndexByte(host, ']') diff --git a/plugins/services/ms17010_exp.go b/plugins/services/ms17010_exp.go index 37703f1..56ccb36 100644 --- a/plugins/services/ms17010_exp.go +++ b/plugins/services/ms17010_exp.go @@ -198,6 +198,10 @@ func smb1GetResponse(conn net.Conn) ([]byte, *smbHeader, error) { sizeBuf := make([]byte, 4) copy(sizeBuf[1:], buf[1:]) size := int(binary.BigEndian.Uint32(sizeBuf)) + // 畸形响应(size < SMB 头长度)会导致后续 buf[:smbHeaderSize] 越界 panic + if size < smbHeaderSize { + return nil, nil, fmt.Errorf("SMB1 response too short: %d bytes", size) + } // SMB buf = make([]byte, size) _, err = io.ReadFull(conn, buf) diff --git a/plugins/services/ssh.go b/plugins/services/ssh.go index ed22cf1..ca01af7 100644 --- a/plugins/services/ssh.go +++ b/plugins/services/ssh.go @@ -118,7 +118,7 @@ func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred C } // 建立TCP连接 - conn, err := session.DialTCP(ctx, "tcp", target, config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, moduleTimeout) if err != nil { return &AuthResult{ Success: false, @@ -277,7 +277,7 @@ func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo, sess func (p *SSHPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { target := info.Target() - conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout) + conn, err := session.DialTCP(ctx, "tcp", target, session.Config.ModuleTimeout()) if err != nil { return &ScanResult{ Success: false, diff --git a/webscan/lib/Client.go b/webscan/lib/Client.go index 18a7e91..014d516 100644 --- a/webscan/lib/Client.go +++ b/webscan/lib/Client.go @@ -11,7 +11,6 @@ import ( "os" "strconv" "strings" - "sync" "time" "github.com/shadow1ng/fscan/common" @@ -32,8 +31,6 @@ const ( ProxySocks5URL = "socks5://127.0.0.1:1080" ) -var gmtlsStdoutMu sync.Mutex - // 全局HTTP客户端变量 var ( Client *http.Client // 标准HTTP客户端 @@ -205,20 +202,10 @@ func InitHTTPClient(ThreadsNum int, DownProxy string, Timeout time.Duration, max Timeout: dialTimeout, KeepAlive: keepAlive, } - // 抑制 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{ + return gmtls.DialWithDialer(dialer, network, addr, &gmtls.Config{ GMSupport: gmtls.NewGMSupport(), InsecureSkipVerify: true, }) - os.Stdout = orig - gmtlsStdoutMu.Unlock() - return conn, err }, MaxConnsPerHost: 20, MaxIdleConns: 20, diff --git a/webscan/lib/Eval.go b/webscan/lib/Eval.go index d62dd4e..51398fa 100644 --- a/webscan/lib/Eval.go +++ b/webscan/lib/Eval.go @@ -508,8 +508,9 @@ func DoRequest(req *http.Request, redirect bool, session *common.ScanSession) (* oResp, err = requestClient(false).Do(req) } - // 标准TLS连接失败时,尝试国密TLS客户端 - if err != nil && req.URL.Scheme == "https" { + // 标准TLS握手级别失败时,尝试国密TLS客户端 + // 跳过连接超时、拒绝等非 TLS 相关错误,避免无意义的国密握手尝试 + if err != nil && req.URL.Scheme == "https" && maybeGMTLSError(err) { if req.GetBody != nil { if body, bodyErr := req.GetBody(); bodyErr == nil { req.Body = body @@ -682,3 +683,14 @@ func getRespBody(oResp *http.Response) ([]byte, error) { return body, nil } + +func maybeGMTLSError(err error) bool { + if err == nil { + return false + } + s := err.Error() + return strings.Contains(s, "handshake failure") || + strings.Contains(s, "protocol version") || + strings.Contains(s, "no mutual") || + strings.Contains(s, "cipher suite") +} diff --git a/webscan/lib/eval_test.go b/webscan/lib/eval_test.go index 8013a02..9097ac5 100644 --- a/webscan/lib/eval_test.go +++ b/webscan/lib/eval_test.go @@ -1238,7 +1238,7 @@ func TestDoRequestSkipsNilGMTLSFallback(t *testing.T) { }() ClientNoRedirect = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { - return nil, errors.New("standard tls failed") + return nil, errors.New("tls: handshake failure") })} ClientNoRedirectGM = nil @@ -1261,7 +1261,7 @@ func TestDoRequestReplaysBodyForGMTLSFallback(t *testing.T) { ClientNoRedirect = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { _, _ = io.ReadAll(req.Body) - return nil, errors.New("standard tls failed") + return nil, errors.New("tls: handshake failure") })} var gotBody string