From 8da185257b3eb97143c09baaf2785df70ba91682 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Sat, 9 Aug 2025 15:34:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0SNMP=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E7=AE=A1=E7=90=86=E5=8D=8F=E8=AE=AE=E4=B8=93=E4=B8=9A?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增SNMP服务插件,支持UDP协议和community字符串认证 - 实现SNMP连接器、利用器和主插件的完整架构 - 添加UDP端口161的特殊处理机制,解决UDP端口扫描问题 - 支持默认community字符串爆破(public, private, cisco, community) - 集成SNMP系统信息获取和服务识别功能 - 完善国际化消息支持和Docker测试环境配置 --- Common/i18n/messages/plugins.go | 18 ++ Common/i18n/messages/scan.go | 4 + Core/PortDiscoveryService.go | 34 ++++ Core/Registry.go | 1 + Plugins/services/snmp/connector.go | 271 +++++++++++++++++++++++++++++ Plugins/services/snmp/exploiter.go | 31 ++++ Plugins/services/snmp/plugin.go | 240 +++++++++++++++++++++++++ TestDocker/SNMP/docker-compose.yml | 9 + 8 files changed, 608 insertions(+) create mode 100644 Plugins/services/snmp/connector.go create mode 100644 Plugins/services/snmp/exploiter.go create mode 100644 Plugins/services/snmp/plugin.go create mode 100644 TestDocker/SNMP/docker-compose.yml diff --git a/Common/i18n/messages/plugins.go b/Common/i18n/messages/plugins.go index e7227b3..2dc5044 100644 --- a/Common/i18n/messages/plugins.go +++ b/Common/i18n/messages/plugins.go @@ -864,4 +864,22 @@ var PluginMessages = map[string]map[string]string{ LangZH: "SMTP认证失败: %v", LangEN: "SMTP authentication failed: %v", }, + + // ========================= SNMP插件消息 ========================= + "snmp_weak_community_success": { + LangZH: "SNMP弱community: %s [%s]", + LangEN: "SNMP weak community: %s [%s]", + }, + "snmp_service_identified": { + LangZH: "SNMP服务识别成功: %s - %s", + LangEN: "SNMP service identified: %s - %s", + }, + "snmp_connection_failed": { + LangZH: "SNMP连接失败: %v", + LangEN: "SNMP connection failed: %v", + }, + "snmp_auth_failed": { + LangZH: "SNMP认证失败: %v", + LangEN: "SNMP authentication failed: %v", + }, } \ No newline at end of file diff --git a/Common/i18n/messages/scan.go b/Common/i18n/messages/scan.go index b568552..86601ba 100644 --- a/Common/i18n/messages/scan.go +++ b/Common/i18n/messages/scan.go @@ -62,6 +62,10 @@ var ScanMessages = map[string]map[string]string{ LangZH: "存活端口数量: %d", LangEN: "Alive ports count: %d", }, + "scan_snmp_udp_ports_added": { + LangZH: "检测到SNMP端口161,添加UDP端口到扫描目标", + LangEN: "Detected SNMP port 161, adding UDP ports to scan targets", + }, "scan_task_complete": { LangZH: "扫描已完成: %d/%d", LangEN: "Scan completed: %d/%d", diff --git a/Core/PortDiscoveryService.go b/Core/PortDiscoveryService.go index 8a3965f..2607cfd 100644 --- a/Core/PortDiscoveryService.go +++ b/Core/PortDiscoveryService.go @@ -66,6 +66,13 @@ func (p *PortDiscoveryService) discoverAlivePorts(hosts []string) []string { common.LogBase(i18n.GetText("scan_alive_ports_count", len(alivePorts))) } + // UDP端口特殊处理(当前仅支持SNMP的161端口) + udpPorts := p.handleUDPPorts(hosts) + if len(udpPorts) > 0 { + alivePorts = append(alivePorts, udpPorts...) + common.LogBase(i18n.GetText("scan_alive_ports_count", len(alivePorts))) + } + // 合并额外指定的端口 if len(common.HostPort) > 0 { alivePorts = append(alivePorts, common.HostPort...) @@ -95,4 +102,31 @@ func (p *PortDiscoveryService) convertToTargetInfos(ports []string, baseInfo com } return infos +} + +// handleUDPPorts 处理UDP端口的特殊逻辑 +func (p *PortDiscoveryService) handleUDPPorts(hosts []string) []string { + var udpPorts []string + + // 检查是否包含SNMP端口161 + portList := parsers.ParsePort(common.Ports) + hasPort161 := false + for _, port := range portList { + if port == 161 { + hasPort161 = true + break + } + } + + // 如果端口列表包含161,则为每个主机添加UDP 161端口 + if hasPort161 { + for _, host := range hosts { + udpPorts = append(udpPorts, fmt.Sprintf("%s:161", host)) + } + if len(udpPorts) > 0 { + common.LogBase(i18n.GetText("scan_snmp_udp_ports_added")) + } + } + + return udpPorts } \ No newline at end of file diff --git a/Core/Registry.go b/Core/Registry.go index 220a954..4e818a0 100644 --- a/Core/Registry.go +++ b/Core/Registry.go @@ -25,6 +25,7 @@ import ( _ "github.com/shadow1ng/fscan/plugins/services/redis" _ "github.com/shadow1ng/fscan/plugins/services/rsync" _ "github.com/shadow1ng/fscan/plugins/services/smtp" + _ "github.com/shadow1ng/fscan/plugins/services/snmp" _ "github.com/shadow1ng/fscan/plugins/services/ssh" ) diff --git a/Plugins/services/snmp/connector.go b/Plugins/services/snmp/connector.go new file mode 100644 index 0000000..95a1993 --- /dev/null +++ b/Plugins/services/snmp/connector.go @@ -0,0 +1,271 @@ +package snmp + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/gosnmp/gosnmp" + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// SNMPConnector SNMP连接器实现 +type SNMPConnector struct { + host string + port int +} + +// SNMPConnection SNMP连接结构 +type SNMPConnection struct { + client *gosnmp.GoSNMP + community string + sysDesc string + info string +} + +// NewSNMPConnector 创建SNMP连接器 +func NewSNMPConnector() *SNMPConnector { + return &SNMPConnector{} +} + +// Connect 建立SNMP连接 +func (c *SNMPConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) { + // 解析端口 + port, err := strconv.Atoi(info.Ports) + if err != nil { + return nil, fmt.Errorf("无效的端口号: %s", info.Ports) + } + + c.host = info.Host + c.port = port + + timeout := time.Duration(common.Timeout) * time.Second + + // 结果通道 + type connResult struct { + conn *SNMPConnection + err error + banner string + } + resultChan := make(chan connResult, 1) + + // 在协程中尝试连接 + go func() { + // 尝试使用默认的public community进行连接 + client := &gosnmp.GoSNMP{ + Target: info.Host, + Port: uint16(port), + Community: "public", + Version: gosnmp.Version2c, + Timeout: timeout, + Retries: 1, + } + + err := client.Connect() + if err != nil { + select { + case <-ctx.Done(): + case resultChan <- connResult{nil, err, ""}: + } + return + } + + // 尝试获取系统描述信息 + oids := []string{"1.3.6.1.2.1.1.1.0"} // sysDescr OID + result, err := client.Get(oids) + + var sysDesc string + var banner string + + if err == nil && len(result.Variables) > 0 { + if result.Variables[0].Type != gosnmp.NoSuchObject { + switch v := result.Variables[0].Value.(type) { + case []byte: + sysDesc = strings.TrimSpace(string(v)) + case string: + sysDesc = strings.TrimSpace(v) + } + } + } + + if sysDesc != "" { + banner = fmt.Sprintf("SNMP Service (Version: %s, System: %s)", client.Version.String(), sysDesc) + } else { + banner = fmt.Sprintf("SNMP Service (Version: %s)", client.Version.String()) + } + + // 创建连接对象 + snmpConn := &SNMPConnection{ + client: client, + community: "public", + sysDesc: sysDesc, + info: banner, + } + + select { + case <-ctx.Done(): + client.Conn.Close() + case resultChan <- connResult{snmpConn, nil, banner}: + } + }() + + // 等待连接结果 + select { + case result := <-resultChan: + if result.err != nil { + return nil, result.err + } + return result.conn, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +// Authenticate 进行SNMP认证(通过community字符串) +func (c *SNMPConnector) Authenticate(ctx context.Context, conn interface{}, cred *base.Credential) error { + snmpConn, ok := conn.(*SNMPConnection) + if !ok { + return fmt.Errorf("无效的SNMP连接类型") + } + + // 对于SNMP,将用户名作为community字符串使用 + community := cred.Username + if community == "" { + community = cred.Password // 如果用户名为空,尝试使用密码作为community + } + + timeout := time.Duration(common.Timeout) * time.Second + + // 结果通道 + type authResult struct { + client *gosnmp.GoSNMP + sysDesc string + err error + } + resultChan := make(chan authResult, 1) + + // 在协程中尝试认证 + go func() { + // 关闭旧连接 + if snmpConn.client != nil && snmpConn.client.Conn != nil { + snmpConn.client.Conn.Close() + } + + // 创建新的SNMP客户端 + client := &gosnmp.GoSNMP{ + Target: c.host, + Port: uint16(c.port), + Community: community, + Version: gosnmp.Version2c, + Timeout: timeout, + Retries: 1, + } + + err := client.Connect() + if err != nil { + select { + case <-ctx.Done(): + case resultChan <- authResult{nil, "", err}: + } + return + } + + // 尝试获取系统描述信息验证认证 + oids := []string{"1.3.6.1.2.1.1.1.0"} // sysDescr OID + result, err := client.Get(oids) + if err != nil { + client.Conn.Close() + select { + case <-ctx.Done(): + case resultChan <- authResult{nil, "", err}: + } + return + } + + var sysDesc string + if len(result.Variables) > 0 && result.Variables[0].Type != gosnmp.NoSuchObject { + switch v := result.Variables[0].Value.(type) { + case []byte: + sysDesc = strings.TrimSpace(string(v)) + case string: + sysDesc = strings.TrimSpace(v) + } + } + + select { + case <-ctx.Done(): + client.Conn.Close() + case resultChan <- authResult{client, sysDesc, nil}: + } + }() + + // 等待认证结果 + select { + case result := <-resultChan: + if result.err != nil { + return fmt.Errorf(i18n.GetText("snmp_auth_failed"), result.err) + } + + // 更新连接信息 + snmpConn.client = result.client + snmpConn.community = community + snmpConn.sysDesc = result.sysDesc + if result.sysDesc != "" { + snmpConn.info = fmt.Sprintf("SNMP Service (Community: %s, System: %s)", community, result.sysDesc) + } else { + snmpConn.info = fmt.Sprintf("SNMP Service (Community: %s)", community) + } + + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// Close 关闭SNMP连接 +func (c *SNMPConnector) Close(conn interface{}) error { + if snmpConn, ok := conn.(*SNMPConnection); ok { + if snmpConn.client != nil && snmpConn.client.Conn != nil { + snmpConn.client.Conn.Close() + } + return nil + } + return fmt.Errorf("无效的SNMP连接类型") +} + +// GetConnectionInfo 获取连接信息 +func (conn *SNMPConnection) GetConnectionInfo() map[string]interface{} { + info := map[string]interface{}{ + "protocol": "SNMP", + "service": "SNMP", + "info": conn.info, + "community": conn.community, + } + + if conn.sysDesc != "" { + info["system"] = conn.sysDesc + } + + return info +} + +// IsAlive 检查连接是否仍然有效 +func (conn *SNMPConnection) IsAlive() bool { + if conn.client == nil || conn.client.Conn == nil { + return false + } + + // 简单的SNMP GET测试连接 + oids := []string{"1.3.6.1.2.1.1.1.0"} + _, err := conn.client.Get(oids) + return err == nil +} + +// GetServerInfo 获取服务器信息 +func (conn *SNMPConnection) GetServerInfo() string { + return conn.info +} \ No newline at end of file diff --git a/Plugins/services/snmp/exploiter.go b/Plugins/services/snmp/exploiter.go new file mode 100644 index 0000000..77be5d3 --- /dev/null +++ b/Plugins/services/snmp/exploiter.go @@ -0,0 +1,31 @@ +package snmp + +import ( + "context" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" +) + +// SNMPExploiter SNMP利用器实现 +type SNMPExploiter struct{} + +// NewSNMPExploiter 创建SNMP利用器 +func NewSNMPExploiter() *SNMPExploiter { + return &SNMPExploiter{} +} + +// Exploit 执行SNMP利用 +func (e *SNMPExploiter) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return nil, nil +} + +// GetExploitMethods 获取可用的利用方法 +func (e *SNMPExploiter) GetExploitMethods() []base.ExploitMethod { + return []base.ExploitMethod{} +} + +// IsExploitSupported 检查是否支持特定的利用类型 +func (e *SNMPExploiter) IsExploitSupported(method base.ExploitType) bool { + return false +} \ No newline at end of file diff --git a/Plugins/services/snmp/plugin.go b/Plugins/services/snmp/plugin.go new file mode 100644 index 0000000..8aa8221 --- /dev/null +++ b/Plugins/services/snmp/plugin.go @@ -0,0 +1,240 @@ +package snmp + +import ( + "context" + "fmt" + "strings" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// SNMPPlugin SNMP插件实现 +type SNMPPlugin struct { + *base.ServicePlugin + exploiter *SNMPExploiter +} + +// NewSNMPPlugin 创建SNMP插件 +func NewSNMPPlugin() *SNMPPlugin { + // 插件元数据 + metadata := &base.PluginMetadata{ + Name: "snmp", + Version: "2.0.0", + Author: "fscan-team", + Description: "SNMP网络管理协议服务扫描插件", + Category: "service", + Ports: []int{161}, // SNMP默认端口 + Protocols: []string{"udp"}, + Tags: []string{"snmp", "network-management", "weak-community", "information-disclosure"}, + } + + // 创建连接器和服务插件 + connector := NewSNMPConnector() + servicePlugin := base.NewServicePlugin(metadata, connector) + + // 创建SNMP插件 + plugin := &SNMPPlugin{ + ServicePlugin: servicePlugin, + exploiter: NewSNMPExploiter(), + } + + // 设置能力 + plugin.SetCapabilities([]base.Capability{ + base.CapWeakPassword, + base.CapInformationLeak, + }) + + return plugin +} + +// Scan 重写扫描方法,进行SNMP服务扫描 +func (p *SNMPPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + // 如果禁用了暴力破解,只进行服务识别 + if common.DisableBrute { + return p.performServiceIdentification(ctx, info) + } + + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 优先尝试默认community字符串 + defaultCommunities := []*base.Credential{ + {Username: "public", Password: ""}, + {Username: "private", Password: ""}, + {Username: "cisco", Password: ""}, + {Username: "community", Password: ""}, + } + + // 先测试默认community + for _, cred := range defaultCommunities { + result, err := p.ScanCredential(ctx, info, cred) + if err == nil && result.Success { + // 认证成功 + common.LogSuccess(i18n.GetText("snmp_weak_community_success", target, cred.Username)) + + return &base.ScanResult{ + Success: true, + Service: "SNMP", + Credentials: []*base.Credential{cred}, + Banner: result.Banner, + Extra: map[string]interface{}{ + "service": "SNMP", + "port": info.Ports, + "community": cred.Username, + "type": "weak-community", + }, + }, nil + } + } + + // 生成其他凭据进行暴力破解 + credentials := p.generateCredentials() + + // 遍历凭据进行测试 + for _, cred := range credentials { + result, err := p.ScanCredential(ctx, info, cred) + if err == nil && result.Success { + // 认证成功 + common.LogSuccess(i18n.GetText("snmp_weak_community_success", target, cred.Username)) + + return &base.ScanResult{ + Success: true, + Service: "SNMP", + Credentials: []*base.Credential{cred}, + Banner: result.Banner, + Extra: map[string]interface{}{ + "service": "SNMP", + "port": info.Ports, + "community": cred.Username, + "type": "weak-community", + }, + }, nil + } + } + + // 所有凭据都失败,但可能识别到了SNMP服务 + return p.performServiceIdentification(ctx, info) +} + +// generateCredentials 生成SNMP凭据(community字符串) +func (p *SNMPPlugin) generateCredentials() []*base.Credential { + var credentials []*base.Credential + + // 获取SNMP community字典 + communities := common.Userdict["snmp"] + if len(communities) == 0 { + // 常见的SNMP community字符串 + communities = []string{ + "admin", "manager", "secret", "read", "write", "test", + "monitor", "guest", "default", "root", "snmp", "router", + "switch", "network", "public1", "private1", "v1", "v2c", + } + } + + // 生成community凭据 + for _, community := range communities { + credentials = append(credentials, &base.Credential{ + Username: community, + Password: "", + }) + } + + // 如果有密码字典,也作为community使用 + passwords := common.Passwords + if len(passwords) > 0 { + for _, password := range passwords { + // 替换密码中的占位符 + actualPassword := strings.Replace(password, "{user}", "snmp", -1) + if actualPassword != "" && actualPassword != "snmp" { + credentials = append(credentials, &base.Credential{ + Username: actualPassword, + Password: "", + }) + } + } + } + + return credentials +} + +// Exploit 使用exploiter执行利用 +func (p *SNMPPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return p.exploiter.Exploit(ctx, info, creds) +} + +// GetExploitMethods 获取利用方法 +func (p *SNMPPlugin) GetExploitMethods() []base.ExploitMethod { + return p.exploiter.GetExploitMethods() +} + +// IsExploitSupported 检查利用支持 +func (p *SNMPPlugin) IsExploitSupported(method base.ExploitType) bool { + return p.exploiter.IsExploitSupported(method) +} + +// performServiceIdentification 执行SNMP服务识别(-nobr模式) +func (p *SNMPPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 尝试识别SNMP服务 + connector := NewSNMPConnector() + conn, err := connector.Connect(ctx, info) + + if err == nil && conn != nil { + if snmpConn, ok := conn.(*SNMPConnection); ok { + // 记录服务识别成功 + common.LogSuccess(i18n.GetText("snmp_service_identified", target, snmpConn.info)) + + connector.Close(conn) + return &base.ScanResult{ + Success: true, + Service: "SNMP", + Banner: snmpConn.info, + Extra: map[string]interface{}{ + "service": "SNMP", + "port": info.Ports, + "info": snmpConn.info, + "protocol": "SNMP", + "community": snmpConn.community, + }, + }, nil + } + } + + // 如果无法识别为SNMP,返回失败 + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("无法识别为SNMP服务"), + }, nil +} + +// ============================================================================= +// 插件注册 +// ============================================================================= + +// RegisterSNMPPlugin 注册SNMP插件 +func RegisterSNMPPlugin() { + factory := base.NewSimplePluginFactory( + &base.PluginMetadata{ + Name: "snmp", + Version: "2.0.0", + Author: "fscan-team", + Description: "SNMP网络管理协议服务扫描插件", + Category: "service", + Ports: []int{161}, // SNMP默认端口 + Protocols: []string{"udp"}, + Tags: []string{"snmp", "network-management", "weak-community", "information-disclosure"}, + }, + func() base.Plugin { + return NewSNMPPlugin() + }, + ) + + base.GlobalPluginRegistry.Register("snmp", factory) +} + +// 自动注册 +func init() { + RegisterSNMPPlugin() +} \ No newline at end of file diff --git a/TestDocker/SNMP/docker-compose.yml b/TestDocker/SNMP/docker-compose.yml new file mode 100644 index 0000000..ae07672 --- /dev/null +++ b/TestDocker/SNMP/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3.8' + +services: + snmp: + build: . + ports: + - "161:161/udp" + container_name: snmp_test + restart: unless-stopped \ No newline at end of file