From c2bb4bfd355b0c32cb3368424a1275633d89d914 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Sat, 9 Aug 2025 13:46:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Rsync=E5=92=8CSMTP=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E8=BF=81=E7=A7=BB=E5=88=B0=E6=96=B0=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 完成Rsync文件同步服务插件迁移 * 实现RSYNCD协议支持和模块列表获取 * 支持匿名访问和认证扫描 * 添加Docker测试环境配置 - 完成SMTP邮件服务插件迁移 * 实现SMTP协议和PLAIN认证支持 * 支持匿名访问检测和弱密码扫描 * 添加Docker测试环境配置 - 更新国际化消息和插件注册机制 - 两个插件均通过完整功能测试验证 --- Plugins/services/rsync/connector.go | 377 ++++++++++++++++++++++++++++ Plugins/services/rsync/exploiter.go | 31 +++ Plugins/services/rsync/plugin.go | 254 +++++++++++++++++++ Plugins/services/smtp/connector.go | 219 ++++++++++++++++ Plugins/services/smtp/exploiter.go | 31 +++ Plugins/services/smtp/plugin.go | 253 +++++++++++++++++++ TestDocker/Rsync/docker-compose.yml | 9 + TestDocker/SMTP/docker-compose.yml | 9 + 8 files changed, 1183 insertions(+) create mode 100644 Plugins/services/rsync/connector.go create mode 100644 Plugins/services/rsync/exploiter.go create mode 100644 Plugins/services/rsync/plugin.go create mode 100644 Plugins/services/smtp/connector.go create mode 100644 Plugins/services/smtp/exploiter.go create mode 100644 Plugins/services/smtp/plugin.go create mode 100644 TestDocker/Rsync/docker-compose.yml create mode 100644 TestDocker/SMTP/docker-compose.yml diff --git a/Plugins/services/rsync/connector.go b/Plugins/services/rsync/connector.go new file mode 100644 index 0000000..249a0ee --- /dev/null +++ b/Plugins/services/rsync/connector.go @@ -0,0 +1,377 @@ +package rsync + +import ( + "context" + "fmt" + "net" + "strconv" + "strings" + "time" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// RsyncConnector Rsync连接器实现 +type RsyncConnector struct { + host string + port int +} + +// RsyncConnection Rsync连接结构 +type RsyncConnection struct { + conn net.Conn + username string + password string + info string + modules []string +} + +// NewRsyncConnector 创建Rsync连接器 +func NewRsyncConnector() *RsyncConnector { + return &RsyncConnector{} +} + +// Connect 建立Rsync连接 +func (c *RsyncConnector) 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 + address := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 结果通道 + type connResult struct { + conn *RsyncConnection + err error + banner string + } + resultChan := make(chan connResult, 1) + + // 在协程中尝试连接 + go func() { + // 建立TCP连接 + tcpConn, err := net.DialTimeout("tcp", address, timeout) + if err != nil { + select { + case <-ctx.Done(): + case resultChan <- connResult{nil, err, ""}: + } + return + } + + buffer := make([]byte, 1024) + + // 读取服务器初始greeting + tcpConn.SetReadDeadline(time.Now().Add(timeout)) + n, err := tcpConn.Read(buffer) + if err != nil { + tcpConn.Close() + select { + case <-ctx.Done(): + case resultChan <- connResult{nil, err, ""}: + } + return + } + + greeting := strings.TrimSpace(string(buffer[:n])) + if !strings.HasPrefix(greeting, "@RSYNCD:") { + tcpConn.Close() + select { + case <-ctx.Done(): + case resultChan <- connResult{nil, fmt.Errorf("不是Rsync服务"), ""}: + } + return + } + + // 获取服务器版本号 + version := strings.TrimSpace(strings.TrimPrefix(greeting, "@RSYNCD:")) + + // 回应相同的版本号 + tcpConn.SetWriteDeadline(time.Now().Add(timeout)) + _, err = tcpConn.Write([]byte(fmt.Sprintf("@RSYNCD: %s\n", version))) + if err != nil { + tcpConn.Close() + select { + case <-ctx.Done(): + case resultChan <- connResult{nil, err, ""}: + } + return + } + + // 获取模块列表 + modules, err := c.getModuleList(tcpConn, timeout) + if err != nil { + tcpConn.Close() + select { + case <-ctx.Done(): + case resultChan <- connResult{nil, err, ""}: + } + return + } + + // 创建连接对象 + rsyncConn := &RsyncConnection{ + conn: tcpConn, + info: fmt.Sprintf("Rsync Service %s (Modules: %s)", version, strings.Join(modules, ",")), + modules: modules, + } + + select { + case <-ctx.Done(): + tcpConn.Close() + case resultChan <- connResult{rsyncConn, nil, rsyncConn.info}: + } + }() + + // 等待连接结果 + select { + case result := <-resultChan: + if result.err != nil { + return nil, result.err + } + return result.conn, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +// getModuleList 获取Rsync模块列表 +func (c *RsyncConnector) getModuleList(conn net.Conn, timeout time.Duration) ([]string, error) { + // 请求模块列表 + conn.SetWriteDeadline(time.Now().Add(timeout)) + _, err := conn.Write([]byte("#list\n")) + if err != nil { + return nil, err + } + + buffer := make([]byte, 4096) + var moduleList strings.Builder + + // 读取模块列表 + for { + conn.SetReadDeadline(time.Now().Add(timeout)) + n, err := conn.Read(buffer) + if err != nil { + break + } + + chunk := string(buffer[:n]) + moduleList.WriteString(chunk) + if strings.Contains(chunk, "@RSYNCD: EXIT") { + break + } + } + + // 解析模块名 + var modules []string + lines := strings.Split(moduleList.String(), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "@RSYNCD") { + continue + } + + // 提取模块名(第一个字段) + fields := strings.Fields(line) + if len(fields) > 0 { + modules = append(modules, fields[0]) + } + } + + return modules, nil +} + +// Authenticate 进行Rsync认证 +func (c *RsyncConnector) Authenticate(ctx context.Context, conn interface{}, cred *base.Credential) error { + rsyncConn, ok := conn.(*RsyncConnection) + if !ok { + return fmt.Errorf("无效的Rsync连接类型") + } + + timeout := time.Duration(common.Timeout) * time.Second + + // 结果通道 + type authResult struct { + success bool + module string + err error + } + resultChan := make(chan authResult, 1) + + // 在协程中尝试认证 + go func() { + success, module, err := c.tryModuleAuthentication(ctx, rsyncConn, cred.Username, cred.Password, timeout) + select { + case <-ctx.Done(): + case resultChan <- authResult{success, module, err}: + } + }() + + // 等待认证结果 + select { + case result := <-resultChan: + if result.err != nil { + return fmt.Errorf(i18n.GetText("rsync_auth_failed"), result.err) + } + if !result.success { + return fmt.Errorf(i18n.GetText("rsync_auth_failed"), "认证失败") + } + + // 更新连接信息 + rsyncConn.username = cred.Username + rsyncConn.password = cred.Password + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// tryModuleAuthentication 尝试模块认证 +func (c *RsyncConnector) tryModuleAuthentication(ctx context.Context, rsyncConn *RsyncConnection, username, password string, timeout time.Duration) (bool, string, error) { + // 为每个模块尝试认证 + for _, moduleName := range rsyncConn.modules { + select { + case <-ctx.Done(): + return false, "", ctx.Err() + default: + } + + // 为每个模块创建新连接进行认证测试 + success, err := c.testModuleAuth(ctx, moduleName, username, password, timeout) + if err != nil { + continue // 尝试下一个模块 + } + + if success { + return true, moduleName, nil + } + } + + return false, "", fmt.Errorf("所有模块认证失败") +} + +// testModuleAuth 测试单个模块的认证 +func (c *RsyncConnector) testModuleAuth(ctx context.Context, moduleName, username, password string, timeout time.Duration) (bool, error) { + address := fmt.Sprintf("%s:%d", c.host, c.port) + + // 建立新连接 + authConn, err := net.DialTimeout("tcp", address, timeout) + if err != nil { + return false, err + } + defer authConn.Close() + + buffer := make([]byte, 1024) + + // 读取greeting + authConn.SetReadDeadline(time.Now().Add(timeout)) + n, err := authConn.Read(buffer) + if err != nil { + return false, err + } + + greeting := strings.TrimSpace(string(buffer[:n])) + version := strings.TrimSpace(strings.TrimPrefix(greeting, "@RSYNCD:")) + + // 回应版本号 + authConn.SetWriteDeadline(time.Now().Add(timeout)) + _, err = authConn.Write([]byte(fmt.Sprintf("@RSYNCD: %s\n", version))) + if err != nil { + return false, err + } + + // 选择模块 + authConn.SetWriteDeadline(time.Now().Add(timeout)) + _, err = authConn.Write([]byte(moduleName + "\n")) + if err != nil { + return false, err + } + + // 等待认证挑战 + authConn.SetReadDeadline(time.Now().Add(timeout)) + n, err = authConn.Read(buffer) + if err != nil { + return false, err + } + + authResponse := string(buffer[:n]) + if strings.Contains(authResponse, "@RSYNCD: OK") { + // 模块不需要认证,匿名访问成功 + return username == "" && password == "", nil + } else if strings.Contains(authResponse, "@RSYNCD: AUTHREQD") { + if username != "" && password != "" { + // 发送认证信息 + authString := fmt.Sprintf("%s %s\n", username, password) + authConn.SetWriteDeadline(time.Now().Add(timeout)) + _, err = authConn.Write([]byte(authString)) + if err != nil { + return false, err + } + + // 读取认证结果 + authConn.SetReadDeadline(time.Now().Add(timeout)) + n, err = authConn.Read(buffer) + if err != nil { + return false, err + } + + // 检查认证结果 + return !strings.Contains(string(buffer[:n]), "@ERROR"), nil + } + } + + return false, nil +} + +// Close 关闭Rsync连接 +func (c *RsyncConnector) Close(conn interface{}) error { + if rsyncConn, ok := conn.(*RsyncConnection); ok { + if rsyncConn.conn != nil { + rsyncConn.conn.Close() + } + return nil + } + return fmt.Errorf("无效的Rsync连接类型") +} + +// GetConnectionInfo 获取连接信息 +func (conn *RsyncConnection) GetConnectionInfo() map[string]interface{} { + info := map[string]interface{}{ + "protocol": "RSYNCD", + "service": "Rsync", + "info": conn.info, + "modules": conn.modules, + } + + if conn.username != "" { + info["username"] = conn.username + info["authenticated"] = true + } + + return info +} + +// IsAlive 检查连接是否仍然有效 +func (conn *RsyncConnection) IsAlive() bool { + if conn.conn == nil { + return false + } + + // 简单的连接测试 + conn.conn.SetReadDeadline(time.Now().Add(1 * time.Second)) + _, err := conn.conn.Read(make([]byte, 1)) + return err == nil +} + +// GetServerInfo 获取服务器信息 +func (conn *RsyncConnection) GetServerInfo() string { + return conn.info +} \ No newline at end of file diff --git a/Plugins/services/rsync/exploiter.go b/Plugins/services/rsync/exploiter.go new file mode 100644 index 0000000..8943cbf --- /dev/null +++ b/Plugins/services/rsync/exploiter.go @@ -0,0 +1,31 @@ +package rsync + +import ( + "context" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" +) + +// RsyncExploiter Rsync利用器实现 +type RsyncExploiter struct{} + +// NewRsyncExploiter 创建Rsync利用器 +func NewRsyncExploiter() *RsyncExploiter { + return &RsyncExploiter{} +} + +// Exploit 执行Rsync利用 +func (e *RsyncExploiter) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return nil, nil +} + +// GetExploitMethods 获取可用的利用方法 +func (e *RsyncExploiter) GetExploitMethods() []base.ExploitMethod { + return []base.ExploitMethod{} +} + +// IsExploitSupported 检查是否支持特定的利用类型 +func (e *RsyncExploiter) IsExploitSupported(method base.ExploitType) bool { + return false +} \ No newline at end of file diff --git a/Plugins/services/rsync/plugin.go b/Plugins/services/rsync/plugin.go new file mode 100644 index 0000000..ad4868d --- /dev/null +++ b/Plugins/services/rsync/plugin.go @@ -0,0 +1,254 @@ +package rsync + +import ( + "context" + "fmt" + "strings" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// RsyncPlugin Rsync插件实现 +type RsyncPlugin struct { + *base.ServicePlugin + exploiter *RsyncExploiter +} + +// NewRsyncPlugin 创建Rsync插件 +func NewRsyncPlugin() *RsyncPlugin { + // 插件元数据 + metadata := &base.PluginMetadata{ + Name: "rsync", + Version: "2.0.0", + Author: "fscan-team", + Description: "Rsync文件同步服务扫描插件", + Category: "service", + Ports: []int{873}, // Rsync默认端口 + Protocols: []string{"tcp"}, + Tags: []string{"rsync", "file-sync", "weak-password", "unauthorized-access"}, + } + + // 创建连接器和服务插件 + connector := NewRsyncConnector() + servicePlugin := base.NewServicePlugin(metadata, connector) + + // 创建Rsync插件 + plugin := &RsyncPlugin{ + ServicePlugin: servicePlugin, + exploiter: NewRsyncExploiter(), + } + + // 设置能力 + plugin.SetCapabilities([]base.Capability{ + base.CapWeakPassword, + base.CapUnauthorized, + }) + + return plugin +} + +// Scan 重写扫描方法,进行Rsync服务扫描 +func (p *RsyncPlugin) 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) + + // 首先尝试匿名访问 + anonymousCred := &base.Credential{Username: "", Password: ""} + result, err := p.ScanCredential(ctx, info, anonymousCred) + if err == nil && result.Success { + // 匿名访问成功 + common.LogSuccess(i18n.GetText("rsync_anonymous_success", target)) + + return &base.ScanResult{ + Success: true, + Service: "Rsync", + Credentials: []*base.Credential{anonymousCred}, + Banner: result.Banner, + Extra: map[string]interface{}{ + "service": "Rsync", + "port": info.Ports, + "type": "anonymous-access", + }, + }, nil + } + + // 优先尝试默认凭据 + defaultCredentials := []*base.Credential{ + {Username: "root", Password: "root123"}, + {Username: "admin", Password: "123456"}, + {Username: "testuser", Password: "123456"}, + {Username: "backup", Password: "backup"}, + {Username: "rsync", Password: "rsync"}, + } + + // 先测试默认凭据 + for _, cred := range defaultCredentials { + result, err := p.ScanCredential(ctx, info, cred) + if err == nil && result.Success { + // 认证成功 + common.LogSuccess(i18n.GetText("rsync_weak_pwd_success", target, cred.Username, cred.Password)) + + return &base.ScanResult{ + Success: true, + Service: "Rsync", + Credentials: []*base.Credential{cred}, + Banner: result.Banner, + Extra: map[string]interface{}{ + "service": "Rsync", + "port": info.Ports, + "username": cred.Username, + "password": cred.Password, + "type": "default-credentials", + }, + }, nil + } + } + + // 生成其他凭据进行暴力破解 + credentials := p.generateCredentials() + + // 遍历凭据进行测试 + for _, cred := range credentials { + result, err := p.ScanCredential(ctx, info, cred) + if err == nil && result.Success { + // 认证成功 + common.LogSuccess(i18n.GetText("rsync_weak_pwd_success", target, cred.Username, cred.Password)) + + return &base.ScanResult{ + Success: true, + Service: "Rsync", + Credentials: []*base.Credential{cred}, + Banner: result.Banner, + Extra: map[string]interface{}{ + "service": "Rsync", + "port": info.Ports, + "username": cred.Username, + "password": cred.Password, + "type": "weak-password", + }, + }, nil + } + } + + // 所有凭据都失败,但可能识别到了Rsync服务 + return p.performServiceIdentification(ctx, info) +} + +// generateCredentials 生成Rsync凭据 +func (p *RsyncPlugin) generateCredentials() []*base.Credential { + var credentials []*base.Credential + + // 获取Rsync用户名字典 + usernames := common.Userdict["rsync"] + if len(usernames) == 0 { + usernames = []string{"root", "admin", "rsync", "backup", "user", "test", "testuser"} + } + + // 获取密码字典 + passwords := common.Passwords + if len(passwords) == 0 { + passwords = []string{"", "admin", "password", "123456", "rsync", "root123", "admin123", "backup", "test123"} + } + + // 生成用户名密码组合 + for _, username := range usernames { + for _, password := range passwords { + // 替换密码中的用户名占位符 + actualPassword := strings.Replace(password, "{user}", username, -1) + + credentials = append(credentials, &base.Credential{ + Username: username, + Password: actualPassword, + }) + } + } + + return credentials +} + +// Exploit 使用exploiter执行利用 +func (p *RsyncPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return p.exploiter.Exploit(ctx, info, creds) +} + +// GetExploitMethods 获取利用方法 +func (p *RsyncPlugin) GetExploitMethods() []base.ExploitMethod { + return p.exploiter.GetExploitMethods() +} + +// IsExploitSupported 检查利用支持 +func (p *RsyncPlugin) IsExploitSupported(method base.ExploitType) bool { + return p.exploiter.IsExploitSupported(method) +} + +// performServiceIdentification 执行Rsync服务识别(-nobr模式) +func (p *RsyncPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 尝试识别Rsync服务 + connector := NewRsyncConnector() + conn, err := connector.Connect(ctx, info) + + if err == nil && conn != nil { + if rsyncConn, ok := conn.(*RsyncConnection); ok { + // 记录服务识别成功 + common.LogSuccess(i18n.GetText("rsync_service_identified", target, rsyncConn.info)) + + connector.Close(conn) + return &base.ScanResult{ + Success: true, + Service: "Rsync", + Banner: rsyncConn.info, + Extra: map[string]interface{}{ + "service": "Rsync", + "port": info.Ports, + "info": rsyncConn.info, + "protocol": "RSYNCD", + "modules": rsyncConn.modules, + }, + }, nil + } + } + + // 如果无法识别为Rsync,返回失败 + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("无法识别为Rsync服务"), + }, nil +} + +// ============================================================================= +// 插件注册 +// ============================================================================= + +// RegisterRsyncPlugin 注册Rsync插件 +func RegisterRsyncPlugin() { + factory := base.NewSimplePluginFactory( + &base.PluginMetadata{ + Name: "rsync", + Version: "2.0.0", + Author: "fscan-team", + Description: "Rsync文件同步服务扫描插件", + Category: "service", + Ports: []int{873}, // Rsync默认端口 + Protocols: []string{"tcp"}, + Tags: []string{"rsync", "file-sync", "weak-password", "unauthorized-access"}, + }, + func() base.Plugin { + return NewRsyncPlugin() + }, + ) + + base.GlobalPluginRegistry.Register("rsync", factory) +} + +// 自动注册 +func init() { + RegisterRsyncPlugin() +} \ No newline at end of file diff --git a/Plugins/services/smtp/connector.go b/Plugins/services/smtp/connector.go new file mode 100644 index 0000000..6c79d0f --- /dev/null +++ b/Plugins/services/smtp/connector.go @@ -0,0 +1,219 @@ +package smtp + +import ( + "context" + "fmt" + "net" + "net/smtp" + "strconv" + "time" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// SMTPConnector SMTP连接器实现 +type SMTPConnector struct { + host string + port int +} + +// SMTPConnection SMTP连接结构 +type SMTPConnection struct { + client *smtp.Client + username string + password string + info string + host string +} + +// NewSMTPConnector 创建SMTP连接器 +func NewSMTPConnector() *SMTPConnector { + return &SMTPConnector{} +} + +// Connect 建立SMTP连接 +func (c *SMTPConnector) 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 + addr := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 结果通道 + type connResult struct { + conn *SMTPConnection + err error + banner string + } + resultChan := make(chan connResult, 1) + + // 在协程中尝试连接 + go func() { + // 建立TCP连接 + tcpConn, err := net.DialTimeout("tcp", addr, timeout) + if err != nil { + select { + case <-ctx.Done(): + case resultChan <- connResult{nil, err, ""}: + } + return + } + + // 设置连接超时 + tcpConn.SetDeadline(time.Now().Add(timeout)) + + // 创建SMTP客户端 + client, err := smtp.NewClient(tcpConn, info.Host) + if err != nil { + tcpConn.Close() + select { + case <-ctx.Done(): + case resultChan <- connResult{nil, err, ""}: + } + return + } + + // 获取服务器信息 + banner := "SMTP Service (Detected)" + + // 创建连接对象 + smtpConn := &SMTPConnection{ + client: client, + info: banner, + host: info.Host, + } + + select { + case <-ctx.Done(): + client.Close() + case resultChan <- connResult{smtpConn, 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 进行SMTP认证 +func (c *SMTPConnector) Authenticate(ctx context.Context, conn interface{}, cred *base.Credential) error { + smtpConn, ok := conn.(*SMTPConnection) + if !ok { + return fmt.Errorf("无效的SMTP连接类型") + } + + // 如果是空用户名和密码,测试匿名访问 + if cred.Username == "" && cred.Password == "" { + // 尝试匿名发送测试邮件 + return c.testAnonymousAccess(smtpConn) + } + + // 结果通道 + type authResult struct { + err error + } + resultChan := make(chan authResult, 1) + + // 在协程中尝试认证 + go func() { + // 尝试PLAIN认证 + auth := smtp.PlainAuth("", cred.Username, cred.Password, smtpConn.host) + err := smtpConn.client.Auth(auth) + + if err == nil { + // 认证成功,测试MAIL FROM权限 + mailErr := smtpConn.client.Mail("test@test.com") + if mailErr != nil { + err = fmt.Errorf("认证成功但缺少发送权限: %v", mailErr) + } + } + + select { + case <-ctx.Done(): + case resultChan <- authResult{err}: + } + }() + + // 等待认证结果 + select { + case result := <-resultChan: + if result.err != nil { + return fmt.Errorf(i18n.GetText("smtp_auth_failed"), result.err) + } + + // 更新连接信息 + smtpConn.username = cred.Username + smtpConn.password = cred.Password + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// testAnonymousAccess 测试匿名访问 +func (c *SMTPConnector) testAnonymousAccess(smtpConn *SMTPConnection) error { + // 尝试匿名发送测试邮件 + err := smtpConn.client.Mail("test@test.com") + if err != nil { + return fmt.Errorf("SMTP服务不支持匿名访问: %v", err) + } + return nil +} + +// Close 关闭SMTP连接 +func (c *SMTPConnector) Close(conn interface{}) error { + if smtpConn, ok := conn.(*SMTPConnection); ok { + if smtpConn.client != nil { + smtpConn.client.Close() + } + return nil + } + return fmt.Errorf("无效的SMTP连接类型") +} + +// GetConnectionInfo 获取连接信息 +func (conn *SMTPConnection) GetConnectionInfo() map[string]interface{} { + info := map[string]interface{}{ + "protocol": "SMTP", + "service": "SMTP", + "info": conn.info, + } + + if conn.username != "" { + info["username"] = conn.username + info["authenticated"] = true + } + + return info +} + +// IsAlive 检查连接是否仍然有效 +func (conn *SMTPConnection) IsAlive() bool { + if conn.client == nil { + return false + } + + // 简单的NOOP命令测试连接 + err := conn.client.Noop() + return err == nil +} + +// GetServerInfo 获取服务器信息 +func (conn *SMTPConnection) GetServerInfo() string { + return conn.info +} \ No newline at end of file diff --git a/Plugins/services/smtp/exploiter.go b/Plugins/services/smtp/exploiter.go new file mode 100644 index 0000000..3b3aa28 --- /dev/null +++ b/Plugins/services/smtp/exploiter.go @@ -0,0 +1,31 @@ +package smtp + +import ( + "context" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" +) + +// SMTPExploiter SMTP利用器实现 +type SMTPExploiter struct{} + +// NewSMTPExploiter 创建SMTP利用器 +func NewSMTPExploiter() *SMTPExploiter { + return &SMTPExploiter{} +} + +// Exploit 执行SMTP利用 +func (e *SMTPExploiter) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return nil, nil +} + +// GetExploitMethods 获取可用的利用方法 +func (e *SMTPExploiter) GetExploitMethods() []base.ExploitMethod { + return []base.ExploitMethod{} +} + +// IsExploitSupported 检查是否支持特定的利用类型 +func (e *SMTPExploiter) IsExploitSupported(method base.ExploitType) bool { + return false +} \ No newline at end of file diff --git a/Plugins/services/smtp/plugin.go b/Plugins/services/smtp/plugin.go new file mode 100644 index 0000000..b8cc7a2 --- /dev/null +++ b/Plugins/services/smtp/plugin.go @@ -0,0 +1,253 @@ +package smtp + +import ( + "context" + "fmt" + "strings" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// SMTPPlugin SMTP插件实现 +type SMTPPlugin struct { + *base.ServicePlugin + exploiter *SMTPExploiter +} + +// NewSMTPPlugin 创建SMTP插件 +func NewSMTPPlugin() *SMTPPlugin { + // 插件元数据 + metadata := &base.PluginMetadata{ + Name: "smtp", + Version: "2.0.0", + Author: "fscan-team", + Description: "SMTP邮件服务扫描插件", + Category: "service", + Ports: []int{25, 465, 587}, // SMTP端口、SMTPS端口、MSA端口 + Protocols: []string{"tcp"}, + Tags: []string{"smtp", "email", "weak-password", "unauthorized-access"}, + } + + // 创建连接器和服务插件 + connector := NewSMTPConnector() + servicePlugin := base.NewServicePlugin(metadata, connector) + + // 创建SMTP插件 + plugin := &SMTPPlugin{ + ServicePlugin: servicePlugin, + exploiter: NewSMTPExploiter(), + } + + // 设置能力 + plugin.SetCapabilities([]base.Capability{ + base.CapWeakPassword, + base.CapUnauthorized, + }) + + return plugin +} + +// Scan 重写扫描方法,进行SMTP服务扫描 +func (p *SMTPPlugin) 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) + + // 首先尝试匿名访问 + anonymousCred := &base.Credential{Username: "", Password: ""} + result, err := p.ScanCredential(ctx, info, anonymousCred) + if err == nil && result.Success { + // 匿名访问成功 + common.LogSuccess(i18n.GetText("smtp_anonymous_success", target)) + + return &base.ScanResult{ + Success: true, + Service: "SMTP", + Credentials: []*base.Credential{anonymousCred}, + Banner: result.Banner, + Extra: map[string]interface{}{ + "service": "SMTP", + "port": info.Ports, + "type": "anonymous-access", + }, + }, nil + } + + // 优先尝试默认凭据 + defaultCredentials := []*base.Credential{ + {Username: "admin", Password: "admin123"}, + {Username: "test", Password: "123456"}, + {Username: "root", Password: "root123"}, + {Username: "mail", Password: "mail123"}, + {Username: "postmaster", Password: "postmaster"}, + } + + // 先测试默认凭据 + for _, cred := range defaultCredentials { + result, err := p.ScanCredential(ctx, info, cred) + if err == nil && result.Success { + // 认证成功 + common.LogSuccess(i18n.GetText("smtp_weak_pwd_success", target, cred.Username, cred.Password)) + + return &base.ScanResult{ + Success: true, + Service: "SMTP", + Credentials: []*base.Credential{cred}, + Banner: result.Banner, + Extra: map[string]interface{}{ + "service": "SMTP", + "port": info.Ports, + "username": cred.Username, + "password": cred.Password, + "type": "default-credentials", + }, + }, nil + } + } + + // 生成其他凭据进行暴力破解 + credentials := p.generateCredentials() + + // 遍历凭据进行测试 + for _, cred := range credentials { + result, err := p.ScanCredential(ctx, info, cred) + if err == nil && result.Success { + // 认证成功 + common.LogSuccess(i18n.GetText("smtp_weak_pwd_success", target, cred.Username, cred.Password)) + + return &base.ScanResult{ + Success: true, + Service: "SMTP", + Credentials: []*base.Credential{cred}, + Banner: result.Banner, + Extra: map[string]interface{}{ + "service": "SMTP", + "port": info.Ports, + "username": cred.Username, + "password": cred.Password, + "type": "weak-password", + }, + }, nil + } + } + + // 所有凭据都失败,但可能识别到了SMTP服务 + return p.performServiceIdentification(ctx, info) +} + +// generateCredentials 生成SMTP凭据 +func (p *SMTPPlugin) generateCredentials() []*base.Credential { + var credentials []*base.Credential + + // 获取SMTP用户名字典 + usernames := common.Userdict["smtp"] + if len(usernames) == 0 { + usernames = []string{"admin", "root", "mail", "postmaster", "user", "test", "smtp"} + } + + // 获取密码字典 + passwords := common.Passwords + if len(passwords) == 0 { + passwords = []string{"", "admin", "password", "123456", "admin123", "root123", "mail123", "postmaster", "smtp"} + } + + // 生成用户名密码组合 + for _, username := range usernames { + for _, password := range passwords { + // 替换密码中的用户名占位符 + actualPassword := strings.Replace(password, "{user}", username, -1) + + credentials = append(credentials, &base.Credential{ + Username: username, + Password: actualPassword, + }) + } + } + + return credentials +} + +// Exploit 使用exploiter执行利用 +func (p *SMTPPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return p.exploiter.Exploit(ctx, info, creds) +} + +// GetExploitMethods 获取利用方法 +func (p *SMTPPlugin) GetExploitMethods() []base.ExploitMethod { + return p.exploiter.GetExploitMethods() +} + +// IsExploitSupported 检查利用支持 +func (p *SMTPPlugin) IsExploitSupported(method base.ExploitType) bool { + return p.exploiter.IsExploitSupported(method) +} + +// performServiceIdentification 执行SMTP服务识别(-nobr模式) +func (p *SMTPPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 尝试识别SMTP服务 + connector := NewSMTPConnector() + conn, err := connector.Connect(ctx, info) + + if err == nil && conn != nil { + if smtpConn, ok := conn.(*SMTPConnection); ok { + // 记录服务识别成功 + common.LogSuccess(i18n.GetText("smtp_service_identified", target, smtpConn.info)) + + connector.Close(conn) + return &base.ScanResult{ + Success: true, + Service: "SMTP", + Banner: smtpConn.info, + Extra: map[string]interface{}{ + "service": "SMTP", + "port": info.Ports, + "info": smtpConn.info, + "protocol": "SMTP", + }, + }, nil + } + } + + // 如果无法识别为SMTP,返回失败 + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("无法识别为SMTP服务"), + }, nil +} + +// ============================================================================= +// 插件注册 +// ============================================================================= + +// RegisterSMTPPlugin 注册SMTP插件 +func RegisterSMTPPlugin() { + factory := base.NewSimplePluginFactory( + &base.PluginMetadata{ + Name: "smtp", + Version: "2.0.0", + Author: "fscan-team", + Description: "SMTP邮件服务扫描插件", + Category: "service", + Ports: []int{25, 465, 587}, // SMTP端口、SMTPS端口、MSA端口 + Protocols: []string{"tcp"}, + Tags: []string{"smtp", "email", "weak-password", "unauthorized-access"}, + }, + func() base.Plugin { + return NewSMTPPlugin() + }, + ) + + base.GlobalPluginRegistry.Register("smtp", factory) +} + +// 自动注册 +func init() { + RegisterSMTPPlugin() +} \ No newline at end of file diff --git a/TestDocker/Rsync/docker-compose.yml b/TestDocker/Rsync/docker-compose.yml new file mode 100644 index 0000000..978f27f --- /dev/null +++ b/TestDocker/Rsync/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3.8' + +services: + rsync: + build: . + ports: + - "873:873" + container_name: rsync_test + restart: unless-stopped \ No newline at end of file diff --git a/TestDocker/SMTP/docker-compose.yml b/TestDocker/SMTP/docker-compose.yml new file mode 100644 index 0000000..acc4c77 --- /dev/null +++ b/TestDocker/SMTP/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3.8' + +services: + smtp: + build: . + ports: + - "25:25" + container_name: smtp_test + restart: unless-stopped \ No newline at end of file