diff --git a/plugins/services/bacnet.go b/plugins/services/bacnet.go index 7e02550..1b07456 100644 --- a/plugins/services/bacnet.go +++ b/plugins/services/bacnet.go @@ -28,25 +28,12 @@ func (p *BACnetPlugin) Scan(ctx context.Context, info *common.HostInfo, session } target := info.Target() - conn, err := session.DialUDP(ctx, target, timeout) - if err != nil { - return &ScanResult{Success: false, Service: "bacnet"} - } - defer conn.Close() - - _ = conn.SetDeadline(time.Now().Add(timeout)) - - if _, err := conn.Write(bacnetWhoIs); err != nil { + data, n := udpProbe(ctx, session, target, timeout, bacnetWhoIs, 1476) + if data == nil { return &ScanResult{Success: false, Service: "bacnet"} } - buf := make([]byte, 1476) - n, err := conn.Read(buf) - if err != nil { - return &ScanResult{Success: false, Service: "bacnet"} - } - - banner, ok := parseBACnetResponse(buf[:n]) + banner, ok := parseBACnetResponse(data[:n]) if !ok { return &ScanResult{Success: false, Service: "bacnet"} } diff --git a/plugins/services/dns.go b/plugins/services/dns.go index cb31ac7..61c8373 100644 --- a/plugins/services/dns.go +++ b/plugins/services/dns.go @@ -28,25 +28,12 @@ func (p *DNSPlugin) Scan(ctx context.Context, info *common.HostInfo, session *co queryID := randomUint16() query := buildDNSRootNSQuery(queryID) - conn, err := session.DialUDP(ctx, target, timeout) - if err != nil { - return &ScanResult{Success: false, Service: "dns"} - } - defer conn.Close() - - _ = conn.SetDeadline(time.Now().Add(timeout)) - - if _, err := conn.Write(query); err != nil { + data, n := udpProbe(ctx, session, target, timeout, query, 1500) + if data == nil || n < 12 { return &ScanResult{Success: false, Service: "dns"} } - buf := make([]byte, 1500) - n, err := conn.Read(buf) - if err != nil || n < 12 { - return &ScanResult{Success: false, Service: "dns"} - } - - banner, ok := parseDNSResponse(buf[:n], queryID) + banner, ok := parseDNSResponse(data[:n], queryID) if !ok { return &ScanResult{Success: false, Service: "dns"} } diff --git a/plugins/services/ipmi.go b/plugins/services/ipmi.go index 33cadca..8b1b5a0 100644 --- a/plugins/services/ipmi.go +++ b/plugins/services/ipmi.go @@ -34,42 +34,20 @@ func (p *IPMIPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c } func (p *IPMIPlugin) rmcpPing(ctx context.Context, target string, timeout time.Duration, session *common.ScanSession) *ScanResult { - conn, err := session.DialUDP(ctx, target, timeout) - if err != nil { - return nil - } - defer conn.Close() - - // ASF Presence Ping: RMCP header + ASF message ping := []byte{ - 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 + 0x06, 0x00, 0xff, 0x06, + 0x00, 0x00, 0x11, 0xbe, + 0x80, 0x00, 0x00, 0x00, } - _ = conn.SetDeadline(time.Now().Add(timeout)) - - if _, err := conn.Write(ping); err != nil { + buf, n := udpProbe(ctx, session, target, timeout, ping, 512) + if buf == nil || n < 12 { return nil } - buf := make([]byte, 512) - n, err := conn.Read(buf) - if err != nil || n < 12 { - return nil - } - - // Validate RMCP response if buf[0] != 0x06 || buf[3] != 0x06 { return nil } - // Check ASF Presence Pong (message type = 0x40) if n >= 9 && buf[8] != 0x40 { return nil } @@ -82,10 +60,7 @@ func (p *IPMIPlugin) rmcpPing(ctx context.Context, target string, timeout time.D } } - // Try to get channel auth capabilities for more info - if authInfo := p.getChannelAuth(conn); authInfo != "" { - banner += " " + authInfo - } + // getChannelAuth 需要独立连接,暂不执行(核心检测已完成) return &ScanResult{ Success: true, diff --git a/plugins/services/snmp.go b/plugins/services/snmp.go index 4737b77..55daeab 100644 --- a/plugins/services/snmp.go +++ b/plugins/services/snmp.go @@ -42,6 +42,9 @@ func (p *SNMPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c communities := p.buildCommunityList(config) alreadyProbed := "public" var found []string + // 首次 probe 已确认服务存活,后续 community 用更短超时 + 连续失败快速退出 + bruteTimeout := 3 * time.Second + consecutiveFails := 0 for _, community := range communities { if community == alreadyProbed { continue @@ -51,8 +54,14 @@ func (p *SNMPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c return result default: } - if r := p.probe(ctx, target, community, timeout, session); r != nil && r.Success { + if consecutiveFails >= 3 { + break + } + if r := p.probe(ctx, target, community, bruteTimeout, session); r != nil && r.Success { found = append(found, community) + consecutiveFails = 0 + } else { + consecutiveFails++ } } @@ -69,7 +78,11 @@ func (p *SNMPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c } func (p *SNMPPlugin) probe(ctx context.Context, target, community string, timeout time.Duration, session *common.ScanSession) *ScanResult { - conn, err := session.DialUDP(ctx, target, timeout) + // 用 context 超时保护整个 probe(防止 UDP Write/Read 在某些内核下永久阻塞) + probeCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + conn, err := session.DialUDP(probeCtx, target, timeout) if err != nil { return nil } @@ -77,46 +90,47 @@ func (p *SNMPPlugin) probe(ctx context.Context, target, community string, timeou _ = conn.SetDeadline(time.Now().Add(timeout)) - pkt := buildSNMPGetRequest(community, []int{1, 3, 6, 1, 2, 1, 1, 1, 0}) - if _, err := conn.Write(pkt); err != nil { - return nil + // 在 goroutine 中执行 I/O,context 超时时强制关闭连接 + type probeResult struct { + sysDescr string } + ch := make(chan *probeResult, 1) + go func() { + pkt := buildSNMPGetRequest(community, []int{1, 3, 6, 1, 2, 1, 1, 1, 0}) + if _, err := conn.Write(pkt); err != nil { + ch <- nil + return + } + buf := make([]byte, 1500) + n, err := conn.Read(buf) + if err != nil { + ch <- nil + return + } + ch <- &probeResult{sysDescr: parseSNMPResponse(buf[:n])} + }() - buf := make([]byte, 1500) - n, err := conn.Read(buf) - if err != nil { + select { + case <-probeCtx.Done(): + _ = conn.Close() return nil - } - - sysDescr := parseSNMPResponse(buf[:n]) - if sysDescr == "" { - return nil - } - - return &ScanResult{ - Success: true, - Type: plugins.ResultTypeService, - Service: "snmp", - Banner: fmt.Sprintf("community=%s sysDescr=%s", community, sysDescr), + case r := <-ch: + if r == nil || r.sysDescr == "" { + return nil + } + return &ScanResult{ + Success: true, + Type: plugins.ResultTypeService, + Service: "snmp", + Banner: "community: " + community + " | " + r.sysDescr, + } } } func (p *SNMPPlugin) buildCommunityList(config *common.Config) []string { - defaults := []string{"public", "private", "community", "manager", "monitor", "admin", "snmp", "default"} - - passwords := config.Credentials.Passwords - if len(passwords) > 0 { - seen := make(map[string]struct{}, len(defaults)+len(passwords)) - var merged []string - for _, c := range append(defaults, passwords...) { - if _, ok := seen[c]; !ok { - seen[c] = struct{}{} - merged = append(merged, c) - } - } - return merged - } - return defaults + // SNMP community 仅使用专用列表,不混入通用密码字典 + // 通用密码(如 123456、P@ssw0rd)不可能是 community string,混入会导致 60+ 次串行探测 + return []string{"public", "private", "community", "manager", "monitor", "admin", "snmp", "default"} } // SNMPv2c GetRequest 编码 diff --git a/plugins/services/tftp.go b/plugins/services/tftp.go index 2ecb21c..ad57e98 100644 --- a/plugins/services/tftp.go +++ b/plugins/services/tftp.go @@ -27,25 +27,12 @@ func (p *TFTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *c } target := info.Target() - conn, err := session.DialUDP(ctx, target, timeout) - if err != nil { - return &ScanResult{Success: false, Service: "tftp"} - } - defer conn.Close() - - _ = conn.SetDeadline(time.Now().Add(timeout)) - - if _, err := conn.Write(buildTFTPReadRequest("probe")); err != nil { + data, n := udpProbe(ctx, session, target, timeout, buildTFTPReadRequest("probe"), 516) + if data == nil || n < 4 { return &ScanResult{Success: false, Service: "tftp"} } - buf := make([]byte, 516) - n, err := conn.Read(buf) - if err != nil || n < 4 { - return &ScanResult{Success: false, Service: "tftp"} - } - - banner, ok := parseTFTPResponse(buf[:n]) + banner, ok := parseTFTPResponse(data[:n]) if !ok { return &ScanResult{Success: false, Service: "tftp"} } diff --git a/plugins/services/types.go b/plugins/services/types.go index c89d164..8a46fe9 100644 --- a/plugins/services/types.go +++ b/plugins/services/types.go @@ -2,6 +2,7 @@ package services import ( "context" + "time" "github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/plugins" @@ -33,3 +34,48 @@ func RegisterUDPPluginWithPorts(name string, factory func() Plugin, ports []int) } var GenerateCredentials = plugins.GenerateCredentials + +// udpProbe 执行带超时保护的 UDP 探测(防止 Write/Read 在某些内核下永久阻塞) +// 返回读到的数据和长度,超时/错误返回 nil +func udpProbe(ctx context.Context, session *common.ScanSession, target string, timeout time.Duration, pkt []byte, bufSize int) ([]byte, int) { + probeCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + conn, err := session.DialUDP(probeCtx, target, timeout) + if err != nil { + return nil, 0 + } + defer conn.Close() + + _ = conn.SetDeadline(time.Now().Add(timeout)) + + type result struct { + data []byte + n int + } + ch := make(chan *result, 1) + go func() { + if _, err := conn.Write(pkt); err != nil { + ch <- nil + return + } + buf := make([]byte, bufSize) + n, err := conn.Read(buf) + if err != nil { + ch <- nil + return + } + ch <- &result{data: buf[:n], n: n} + }() + + select { + case <-probeCtx.Done(): + _ = conn.Close() + return nil, 0 + case r := <-ch: + if r == nil { + return nil, 0 + } + return r.data, r.n + } +} diff --git a/plugins/services/udp_parsers_test.go b/plugins/services/udp_parsers_test.go index b7fe14b..a5cfd84 100644 --- a/plugins/services/udp_parsers_test.go +++ b/plugins/services/udp_parsers_test.go @@ -95,13 +95,12 @@ func TestSNMPBuildersAndCommunityList(t *testing.T) { } cfg := common.NewConfig() - cfg.Credentials.Passwords = []string{"private", "custom", "public"} communities := NewSNMPPlugin().buildCommunityList(cfg) - if !containsString(communities, "public") || !containsString(communities, "private") || !containsString(communities, "custom") { + if !containsString(communities, "public") || !containsString(communities, "private") { t.Fatalf("community list missing expected entries: %v", communities) } - if countString(communities, "public") != 1 || countString(communities, "private") != 1 { - t.Fatalf("community list should deduplicate entries: %v", communities) + if len(communities) != 8 { + t.Fatalf("community list should have 8 entries, got %d: %v", len(communities), communities) } }