package services import ( "context" "fmt" "net" "net/smtp" "strings" "time" "github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/plugins" ) type SMTPPlugin struct { plugins.BasePlugin } func NewSMTPPlugin() *SMTPPlugin { return &SMTPPlugin{ BasePlugin: plugins.NewBasePlugin("smtp"), } } func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) if common.DisableBrute { return p.identifyService(ctx, info) } // 首先测试匿名访问(最重要!) if p.testCredential(ctx, info, plugins.Credential{Username: "", Password: ""}) { common.LogSuccess(fmt.Sprintf("SMTP服务 %s 允许匿名访问", target)) return &plugins.Result{ Success: true, Service: "smtp", Banner: "允许匿名访问", } } // 检查开放中继 if result := p.testOpenRelay(ctx, info); result != nil && result.Success { common.LogSuccess(fmt.Sprintf("SMTP %s 开放中继", target)) return result } credentials := plugins.GenerateCredentials("smtp") if len(credentials) == 0 { return &plugins.Result{ Success: false, Service: "smtp", Error: fmt.Errorf("没有可用的测试凭据"), } } for _, cred := range credentials { // 检查上下文是否已取消 select { case <-ctx.Done(): return &plugins.Result{ Success: false, Service: "smtp", Error: ctx.Err(), } default: } if p.testCredential(ctx, info, cred) { common.LogSuccess(fmt.Sprintf("SMTP %s %s:%s", target, cred.Username, cred.Password)) return &plugins.Result{ Success: true, Service: "smtp", Username: cred.Username, Password: cred.Password, } } } return &plugins.Result{ Success: false, Service: "smtp", Error: fmt.Errorf("未发现弱密码"), } } func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo) *plugins.Result { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) // 检查发包限制 if canSend, reason := common.CanSendPacket(); !canSend { common.LogError(fmt.Sprintf("SMTP连接 %s 受限: %s", target, reason)) return nil } // 设置超时的Dialer dialer := &net.Dialer{ Timeout: time.Duration(common.Timeout) * time.Second, } conn, err := dialer.DialContext(ctx, "tcp", target) if err == nil { common.IncrementTCPSuccessPacketCount() } else { common.IncrementTCPFailedPacketCount() } if err != nil { return nil } defer conn.Close() client, err := smtp.NewClient(conn, info.Host) if err != nil { return nil } defer client.Quit() if err := client.Hello("fscan.test"); err != nil { return nil } if err := client.Mail("test@fscan.test"); err != nil { return nil } if err := client.Rcpt("external@example.com"); err != nil { return nil } return &plugins.Result{ Success: true, Service: "smtp", Banner: "开放中继", } } func (p *SMTPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred plugins.Credential) bool { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) timeout := time.Duration(common.Timeout) * time.Second common.LogDebug(fmt.Sprintf("SMTP测试凭据: %s:%s", cred.Username, cred.Password)) // 检查发包限制 if canSend, reason := common.CanSendPacket(); !canSend { common.LogError(fmt.Sprintf("SMTP凭据测试 %s 受限: %s", target, reason)) return false } // 设置连接超时 dialer := &net.Dialer{ Timeout: timeout, } conn, err := dialer.DialContext(ctx, "tcp", target) if err == nil { common.IncrementTCPSuccessPacketCount() } else { common.IncrementTCPFailedPacketCount() } if err != nil { common.LogDebug(fmt.Sprintf("SMTP连接失败: %v", err)) return false } defer conn.Close() // 设置读写超时 conn.SetDeadline(time.Now().Add(timeout)) client, err := smtp.NewClient(conn, info.Host) if err != nil { common.LogDebug(fmt.Sprintf("SMTP客户端创建失败: %v", err)) return false } defer client.Close() // 如果有用户名密码,则尝试认证 if cred.Username != "" { auth := smtp.PlainAuth("", cred.Username, cred.Password, info.Host) if err := client.Auth(auth); err != nil { common.LogDebug(fmt.Sprintf("SMTP认证失败 %s:%s - %v", cred.Username, cred.Password, err)) return false } } // 尝试发送邮件测试权限(仿照原版) if err := client.Mail("test@test.com"); err != nil { common.LogDebug(fmt.Sprintf("SMTP Mail命令失败 %s:%s - %v", cred.Username, cred.Password, err)) return false } common.LogDebug(fmt.Sprintf("SMTP认证成功: %s:%s", cred.Username, cred.Password)) return true } func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo) string { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) // 使用统一TCP包装器 conn, err := common.SafeTCPDial(target, time.Duration(common.Timeout)*time.Second) if err != nil { return "" } defer conn.Close() conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) buffer := make([]byte, 1024) n, err := conn.Read(buffer) if err != nil { return "" } welcome := strings.TrimSpace(string(buffer[:n])) if strings.HasPrefix(welcome, "220") { return strings.TrimPrefix(welcome, "220 ") } return welcome } func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) serverInfo := p.getServerInfo(ctx, info) var banner string if serverInfo != "" { banner = fmt.Sprintf("SMTP邮件服务 (%s)", serverInfo) } else { // 使用统一TCP包装器进行简单连接测试 conn, err := common.SafeTCPDial(target, time.Duration(common.Timeout)*time.Second) if err != nil { return &plugins.Result{ Success: false, Service: "smtp", Error: err, } } defer conn.Close() banner = "SMTP邮件服务" } common.LogSuccess(fmt.Sprintf("SMTP %s %s", target, banner)) return &plugins.Result{ Success: true, Service: "smtp", Banner: banner, } } func init() { plugins.RegisterWithPorts("smtp", func() plugins.Plugin { return NewSMTPPlugin() }, []int{25, 465, 587, 2525}) }