fscan/plugins/services/smtp.go
ZacharyZcR e082e2bb59 refactor: 重组插件目录结构,提升管理直观性
将所有服务插件移动到plugins/services/目录下,使目录结构更加清晰直观:
• 创建plugins/services/目录统一管理服务扫描插件
• 添加init.go提供类型别名和函数导出
• 更新main.go导入路径
• 所有20个服务插件功能验证正常

新的目录结构更便于插件管理和维护。
2025-08-26 00:02:13 +08:00

366 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package services
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/smtp"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
)
// SMTPPlugin SMTP邮件服务扫描和利用插件 - 包含用户枚举利用功能
type SMTPPlugin struct {
name string
ports []int
}
// NewSMTPPlugin 创建SMTP插件
func NewSMTPPlugin() *SMTPPlugin {
return &SMTPPlugin{
name: "smtp",
ports: []int{25, 465, 587, 2525}, // SMTP端口
}
}
// GetName 实现Plugin接口
func (p *SMTPPlugin) GetName() string {
return p.name
}
// GetPorts 实现Plugin接口
func (p *SMTPPlugin) GetPorts() []int {
return p.ports
}
// Scan 执行SMTP扫描 - 弱密码检测和开放中继检测
func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 如果禁用暴力破解,只做服务识别
if common.DisableBrute {
return p.identifyService(ctx, info)
}
// 首先检查开放中继
if result := p.testOpenRelay(ctx, info); result != nil && result.Success {
common.LogSuccess(i18n.GetText("smtp_open_relay_success", target))
return result
}
// 生成测试凭据
credentials := GenerateCredentials("smtp")
if len(credentials) == 0 {
// SMTP默认凭据
credentials = []Credential{
{Username: "admin", Password: "admin"},
{Username: "admin", Password: "password"},
{Username: "admin", Password: "123456"},
{Username: "test", Password: "test"},
{Username: "user", Password: "user"},
{Username: "mail", Password: "mail"},
{Username: "postmaster", Password: "postmaster"},
}
}
// 逐个测试凭据
for _, cred := range credentials {
// 检查Context是否被取消
select {
case <-ctx.Done():
return &ScanResult{
Success: false,
Service: "smtp",
Error: ctx.Err(),
}
default:
}
// 测试凭据
if p.testCredential(ctx, info, cred) {
// SMTP认证成功
common.LogSuccess(i18n.GetText("smtp_scan_success", target, cred.Username, cred.Password))
return &ScanResult{
Success: true,
Service: "smtp",
Username: cred.Username,
Password: cred.Password,
}
}
}
// 所有凭据都失败
return &ScanResult{
Success: false,
Service: "smtp",
Error: fmt.Errorf("未发现弱密码或开放中继"),
}
}
// Exploit 执行SMTP利用操作 - 实现用户枚举功能
func (p *SMTPPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
common.LogSuccess(fmt.Sprintf("SMTP利用开始: %s", target))
var output strings.Builder
output.WriteString(fmt.Sprintf("=== SMTP利用结果 - %s ===\n", target))
// 获取服务器信息
if serverInfo := p.getServerInfo(ctx, info); serverInfo != "" {
output.WriteString(fmt.Sprintf("\n[服务器信息]\n%s\n", serverInfo))
}
// 获取支持的扩展
if extensions := p.getExtensions(ctx, info); len(extensions) > 0 {
output.WriteString(fmt.Sprintf("\n[支持的扩展] (共%d个)\n", len(extensions)))
for _, ext := range extensions {
output.WriteString(fmt.Sprintf(" %s\n", ext))
}
}
// 用户枚举
if users := p.enumerateUsers(ctx, info); len(users) > 0 {
output.WriteString(fmt.Sprintf("\n[用户枚举结果] (共%d个)\n", len(users)))
for _, user := range users {
output.WriteString(fmt.Sprintf(" %s\n", user))
}
}
// 检查开放中继
if relayTest := p.checkOpenRelay(ctx, info); relayTest != "" {
output.WriteString(fmt.Sprintf("\n[开放中继检测]\n%s\n", relayTest))
}
common.LogSuccess(fmt.Sprintf("SMTP利用完成: %s", target))
return &ExploitResult{
Success: true,
Output: output.String(),
}
}
// testOpenRelay 测试开放中继
func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo) *ScanResult {
if p.checkOpenRelay(ctx, info) != "" {
return &ScanResult{
Success: true,
Service: "smtp",
Banner: "开放中继",
}
}
return nil
}
// testCredential 测试单个凭据
func (p *SMTPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 根据端口选择连接方式
var client *smtp.Client
var err error
if info.Ports == "465" { // SMTPS
conn, err := tls.Dial("tcp", target, &tls.Config{InsecureSkipVerify: true})
if err != nil {
return false
}
defer conn.Close()
client, err = smtp.NewClient(conn, info.Host)
if err != nil {
return false
}
} else {
client, err = smtp.Dial(target)
if err != nil {
return false
}
}
defer client.Quit()
// 尝试STARTTLS
if ok, _ := client.Extension("STARTTLS"); ok {
if err := client.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil {
// STARTTLS失败继续明文认证
}
}
// 尝试认证
auth := smtp.PlainAuth("", cred.Username, cred.Password, info.Host)
if err := client.Auth(auth); err != nil {
return false
}
return true
}
// getServerInfo 获取服务器信息
func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo) string {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
conn, err := net.DialTimeout("tcp", 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
}
// getExtensions 获取SMTP扩展
func (p *SMTPPlugin) getExtensions(ctx context.Context, info *common.HostInfo) []string {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
client, err := smtp.Dial(target)
if err != nil {
return nil
}
defer client.Quit()
// 发送EHLO获取扩展
if err := client.Hello("fscan.test"); err != nil {
return nil
}
var extensions []string
if exts, _ := client.Extension("AUTH"); exts {
extensions = append(extensions, "AUTH (认证支持)")
}
if exts, _ := client.Extension("STARTTLS"); exts {
extensions = append(extensions, "STARTTLS (TLS支持)")
}
if exts, _ := client.Extension("SIZE"); exts {
extensions = append(extensions, "SIZE (邮件大小限制)")
}
if exts, _ := client.Extension("PIPELINING"); exts {
extensions = append(extensions, "PIPELINING (管道支持)")
}
if exts, _ := client.Extension("8BITMIME"); exts {
extensions = append(extensions, "8BITMIME (8位MIME)")
}
return extensions
}
// enumerateUsers 枚举用户
func (p *SMTPPlugin) enumerateUsers(ctx context.Context, info *common.HostInfo) []string {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
client, err := smtp.Dial(target)
if err != nil {
return nil
}
defer client.Quit()
if err := client.Hello("fscan.test"); err != nil {
return nil
}
// 常见用户名列表
commonUsers := []string{"admin", "administrator", "root", "user", "test", "mail", "postmaster", "webmaster", "info", "support", "sales", "marketing"}
var validUsers []string
for _, user := range commonUsers {
// 使用VRFY命令验证用户
if err := p.sendRawCommand(client, fmt.Sprintf("VRFY %s", user)); err == nil {
validUsers = append(validUsers, user)
}
// 限制数量
if len(validUsers) >= 10 {
break
}
}
return validUsers
}
// sendRawCommand 发送原始SMTP命令
func (p *SMTPPlugin) sendRawCommand(client *smtp.Client, command string) error {
// 这里简化实现,实际需要直接操作连接
// smtp包没有直接的原始命令接口
return fmt.Errorf("VRFY command not supported")
}
// checkOpenRelay 检查开放中继
func (p *SMTPPlugin) checkOpenRelay(ctx context.Context, info *common.HostInfo) string {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
client, err := smtp.Dial(target)
if err != nil {
return ""
}
defer client.Quit()
if err := client.Hello("fscan.test"); err != nil {
return ""
}
// 尝试发送外部邮件测试开放中继
if err := client.Mail("test@fscan.test"); err != nil {
return "❌ 邮件发送测试失败"
}
if err := client.Rcpt("external@example.com"); err != nil {
return "❌ 不是开放中继"
}
return "⚠️ 可能存在开放中继风险"
}
// identifyService 服务识别 - 检测SMTP服务
func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
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 {
// 尝试简单连接测试
conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second)
if err != nil {
return &ScanResult{
Success: false,
Service: "smtp",
Error: err,
}
}
defer conn.Close()
banner = "SMTP邮件服务"
}
common.LogSuccess(i18n.GetText("smtp_service_identified", target, banner))
return &ScanResult{
Success: true,
Service: "smtp",
Banner: banner,
}
}
// init 自动注册插件
func init() {
RegisterPlugin("smtp", func() Plugin {
return NewSMTPPlugin()
})
}