feat: 实现SNMP网络管理协议专业扫描插件

- 新增SNMP服务插件,支持UDP协议和community字符串认证
- 实现SNMP连接器、利用器和主插件的完整架构
- 添加UDP端口161的特殊处理机制,解决UDP端口扫描问题
- 支持默认community字符串爆破(public, private, cisco, community)
- 集成SNMP系统信息获取和服务识别功能
- 完善国际化消息支持和Docker测试环境配置
This commit is contained in:
ZacharyZcR 2025-08-09 15:34:05 +08:00
parent fbc75bb709
commit 8da185257b
8 changed files with 608 additions and 0 deletions

View File

@ -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",
},
}

View File

@ -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",

View File

@ -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
}

View File

@ -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"
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -0,0 +1,9 @@
version: '3.8'
services:
snmp:
build: .
ports:
- "161:161/udp"
container_name: snmp_test
restart: unless-stopped