From 83afd0f994ba09da7d7bfbad7311da0f91b51d91 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Fri, 8 Aug 2025 04:46:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0FTP=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=BC=A0=E8=BE=93=E5=8D=8F=E8=AE=AE=E4=B8=93=E4=B8=9A=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • 新增FTP插件支持标准21端口和常见替代端口 • 实现匿名访问检测和弱密码爆破功能 • 支持自动利用:目录枚举、文件下载测试、文件上传测试 • 集成-nobr模式服务识别和-ne自动利用功能 • 完整的Context超时机制和连接数管理 • 添加中英文国际化消息支持 • 基于新插件架构实现模块化设计 功能特性: - 支持jlaffaye/ftp驱动的FTP协议通信 - 智能识别vsFTPd、ProFTPD等多种FTP服务器 - 三种利用方法:目录结构探测、文件操作测试 - 完整的错误处理和连接限制处理机制 --- Plugins/services/ftp/connector.go | 116 +++++++++++ Plugins/services/ftp/exploiter.go | 331 ++++++++++++++++++++++++++++++ Plugins/services/ftp/plugin.go | 266 ++++++++++++++++++++++++ 3 files changed, 713 insertions(+) create mode 100644 Plugins/services/ftp/connector.go create mode 100644 Plugins/services/ftp/exploiter.go create mode 100644 Plugins/services/ftp/plugin.go diff --git a/Plugins/services/ftp/connector.go b/Plugins/services/ftp/connector.go new file mode 100644 index 0000000..e9cc295 --- /dev/null +++ b/Plugins/services/ftp/connector.go @@ -0,0 +1,116 @@ +package ftp + +import ( + "context" + "fmt" + "time" + + ftplib "github.com/jlaffaye/ftp" + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/plugins/base" +) + +// FTPConnector FTP连接器实现 +type FTPConnector struct { + host string + port string +} + +// NewFTPConnector 创建FTP连接器 +func NewFTPConnector() *FTPConnector { + return &FTPConnector{} +} + +// Connect 连接到FTP服务 +func (c *FTPConnector) Connect(ctx context.Context, info *common.HostInfo) (interface{}, error) { + c.host = info.Host + c.port = info.Ports + + // 构建连接地址 + target := fmt.Sprintf("%s:%s", c.host, c.port) + + // 创建FTP连接配置 + config := &FTPConfig{ + Target: target, + Timeout: time.Duration(common.Timeout) * time.Second, + } + + return config, nil +} + +// Authenticate 认证 +func (c *FTPConnector) Authenticate(ctx context.Context, conn interface{}, cred *base.Credential) error { + config, ok := conn.(*FTPConfig) + if !ok { + return fmt.Errorf("无效的连接类型") + } + + // 在goroutine中建立FTP连接,支持Context取消 + resultChan := make(chan struct { + ftpConn *ftplib.ServerConn + err error + }, 1) + + go func() { + ftpConn, err := ftplib.DialTimeout(config.Target, config.Timeout) + select { + case <-ctx.Done(): + if ftpConn != nil { + ftpConn.Quit() + } + case resultChan <- struct { + ftpConn *ftplib.ServerConn + err error + }{ftpConn, err}: + } + }() + + // 等待连接结果或Context取消 + var ftpConn *ftplib.ServerConn + var err error + select { + case result := <-resultChan: + ftpConn, err = result.ftpConn, result.err + if err != nil { + return fmt.Errorf("FTP连接失败: %v", err) + } + case <-ctx.Done(): + return fmt.Errorf("FTP连接超时: %v", ctx.Err()) + } + + defer ftpConn.Quit() + + // 在goroutine中进行登录认证 + loginChan := make(chan error, 1) + + go func() { + err := ftpConn.Login(cred.Username, cred.Password) + select { + case <-ctx.Done(): + case loginChan <- err: + } + }() + + // 等待登录结果或Context取消 + select { + case err := <-loginChan: + if err != nil { + return fmt.Errorf("FTP认证失败: %v", err) + } + return nil + case <-ctx.Done(): + return fmt.Errorf("FTP认证超时: %v", ctx.Err()) + } +} + +// Close 关闭连接 +func (c *FTPConnector) Close(conn interface{}) error { + // FTP配置无需显式关闭 + return nil +} + +// FTPConfig FTP连接配置 +type FTPConfig struct { + Target string + Timeout time.Duration +} \ No newline at end of file diff --git a/Plugins/services/ftp/exploiter.go b/Plugins/services/ftp/exploiter.go new file mode 100644 index 0000000..add03c2 --- /dev/null +++ b/Plugins/services/ftp/exploiter.go @@ -0,0 +1,331 @@ +package ftp + +import ( + "context" + "fmt" + "path/filepath" + "strings" + "time" + + ftplib "github.com/jlaffaye/ftp" + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// FTPExploiter FTP利用器 +type FTPExploiter struct { + *base.BaseExploiter + connector *FTPConnector +} + +// NewFTPExploiter 创建FTP利用器 +func NewFTPExploiter() *FTPExploiter { + exploiter := &FTPExploiter{ + BaseExploiter: base.NewBaseExploiter("ftp"), + connector: NewFTPConnector(), + } + + // 添加利用方法 + exploiter.setupExploitMethods() + + return exploiter +} + +// setupExploitMethods 设置利用方法 +func (e *FTPExploiter) setupExploitMethods() { + // 1. 目录枚举 + dirMethod := base.NewExploitMethod(base.ExploitDataExtraction, "directory_enumeration"). + WithDescription(i18n.GetText("exploit_method_name_directory_enumeration")). + WithPriority(9). + WithConditions("has_credentials"). + WithHandler(e.exploitDirectoryEnumeration). + Build() + e.AddExploitMethod(dirMethod) + + // 2. 文件下载测试 + downloadMethod := base.NewExploitMethod(base.ExploitDataExtraction, "file_download_test"). + WithDescription(i18n.GetText("exploit_method_name_file_read")). + WithPriority(8). + WithConditions("has_credentials"). + WithHandler(e.exploitFileDownloadTest). + Build() + e.AddExploitMethod(downloadMethod) + + // 3. 文件上传测试 + uploadMethod := base.NewExploitMethod(base.ExploitFileWrite, "file_upload_test"). + WithDescription(i18n.GetText("exploit_method_name_file_write")). + WithPriority(7). + WithConditions("has_credentials"). + WithHandler(e.exploitFileUploadTest). + Build() + e.AddExploitMethod(uploadMethod) +} + +// exploitDirectoryEnumeration 目录枚举 +func (e *FTPExploiter) exploitDirectoryEnumeration(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return e.executeWithConnection(ctx, info, creds, "directory_enumeration", e.directoryEnumeration) +} + +// exploitFileDownloadTest 文件下载测试 +func (e *FTPExploiter) exploitFileDownloadTest(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return e.executeWithConnection(ctx, info, creds, "file_download_test", e.fileDownloadTest) +} + +// exploitFileUploadTest 文件上传测试 +func (e *FTPExploiter) exploitFileUploadTest(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return e.executeWithConnection(ctx, info, creds, "file_upload_test", e.fileUploadTest) +} + +// executeWithConnection 使用FTP连接执行利用方法 +func (e *FTPExploiter) executeWithConnection(ctx context.Context, info *common.HostInfo, creds *base.Credential, methodName string, method func(context.Context, *ftplib.ServerConn, string) ([]string, error)) (*base.ExploitResult, error) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 建立FTP连接 + ftpConn, err := ftplib.DialTimeout(target, time.Duration(common.Timeout)*time.Second) + if err != nil { + return nil, fmt.Errorf("连接失败: %v", err) + } + defer ftpConn.Quit() + + // 登录认证 + err = ftpConn.Login(creds.Username, creds.Password) + if err != nil { + return nil, fmt.Errorf("认证失败: %v", err) + } + + // 执行方法 + output, err := method(ctx, ftpConn, target) + if err != nil { + return &base.ExploitResult{ + Success: false, + Error: err, + Type: base.ExploitDataExtraction, + Method: methodName, + Output: fmt.Sprintf("执行失败: %v", err), + }, nil + } + + return &base.ExploitResult{ + Success: true, + Type: base.ExploitDataExtraction, + Method: methodName, + Output: strings.Join(output, "\n"), + }, nil +} + +// directoryEnumeration 目录枚举 +func (e *FTPExploiter) directoryEnumeration(ctx context.Context, ftpConn *ftplib.ServerConn, target string) ([]string, error) { + var output []string + + // 获取当前工作目录 + currentDir, err := ftpConn.CurrentDir() + if err != nil { + currentDir = "/" + } + output = append(output, fmt.Sprintf("当前工作目录: %s", currentDir)) + + // 列举根目录内容 + entries, err := ftpConn.List("") + if err != nil { + return nil, fmt.Errorf("列举目录失败: %v", err) + } + + if len(entries) == 0 { + output = append(output, "目录为空") + return output, nil + } + + // 显示目录内容(限制显示数量) + maxDisplay := 10 + if len(entries) > maxDisplay { + output = append(output, fmt.Sprintf("发现 %d 个条目,显示前 %d 个:", len(entries), maxDisplay)) + } else { + output = append(output, fmt.Sprintf("发现 %d 个条目:", len(entries))) + } + + dirCount := 0 + fileCount := 0 + for i, entry := range entries { + if i >= maxDisplay { + break + } + + // 格式化文件/目录信息 + entryType := "文件" + if entry.Type == ftplib.EntryTypeFolder { + entryType = "目录" + dirCount++ + } else { + fileCount++ + } + + // 格式化文件大小 + sizeInfo := "" + if entry.Type != ftplib.EntryTypeFolder { + if entry.Size > 1024*1024 { + sizeInfo = fmt.Sprintf("%.2fMB", float64(entry.Size)/(1024*1024)) + } else if entry.Size > 1024 { + sizeInfo = fmt.Sprintf("%.2fKB", float64(entry.Size)/1024) + } else { + sizeInfo = fmt.Sprintf("%dB", entry.Size) + } + } + + // 截断过长的文件名 + name := entry.Name + if len(name) > 50 { + name = name[:50] + "..." + } + + if sizeInfo != "" { + output = append(output, fmt.Sprintf(" [%s] %s (%s)", entryType, name, sizeInfo)) + } else { + output = append(output, fmt.Sprintf(" [%s] %s", entryType, name)) + } + } + + // 添加统计信息 + if len(entries) > maxDisplay { + output = append(output, fmt.Sprintf("... 还有 %d 个条目未显示", len(entries)-maxDisplay)) + } + output = append(output, fmt.Sprintf("统计: %d个目录, %d个文件", dirCount, fileCount)) + + return output, nil +} + +// fileDownloadTest 文件下载测试 +func (e *FTPExploiter) fileDownloadTest(ctx context.Context, ftpConn *ftplib.ServerConn, target string) ([]string, error) { + var output []string + + // 获取目录列表 + entries, err := ftpConn.List("") + if err != nil { + return nil, fmt.Errorf("获取文件列表失败: %v", err) + } + + // 寻找可下载的小文件进行测试 + var testFile *ftplib.Entry + for _, entry := range entries { + if entry.Type == ftplib.EntryTypeFile && entry.Size < 10240 { // 小于10KB的文件 + // 过滤一些常见的安全文件 + name := strings.ToLower(entry.Name) + if !strings.Contains(name, "passwd") && + !strings.Contains(name, "shadow") && + !strings.Contains(name, "key") && + !strings.Contains(name, "config") { + testFile = entry + break + } + } + } + + if testFile == nil { + output = append(output, "未找到合适的测试文件(寻找小于10KB的普通文件)") + return output, nil + } + + // 尝试下载测试文件 + response, err := ftpConn.Retr(testFile.Name) + if err != nil { + return nil, fmt.Errorf("下载文件失败: %v", err) + } + defer response.Close() + + // 读取前1024字节作为测试 + buffer := make([]byte, 1024) + n, _ := response.Read(buffer) + + output = append(output, fmt.Sprintf("成功下载测试文件: %s", testFile.Name)) + output = append(output, fmt.Sprintf("文件大小: %d 字节", testFile.Size)) + if n > 0 { + // 检查是否为文本内容 + if isPrintableText(buffer[:n]) { + preview := string(buffer[:n]) + if len(preview) > 200 { + preview = preview[:200] + "..." + } + output = append(output, fmt.Sprintf("文件内容预览: %s", preview)) + } else { + output = append(output, "文件为二进制内容") + } + } + + return output, nil +} + +// fileUploadTest 文件上传测试 +func (e *FTPExploiter) fileUploadTest(ctx context.Context, ftpConn *ftplib.ServerConn, target string) ([]string, error) { + var output []string + + // 测试文件内容 + testFileName := "fscan_test.txt" + testContent := "This is a test file created by fscan for FTP upload testing." + + // 尝试上传测试文件 + err := ftpConn.Stor(testFileName, strings.NewReader(testContent)) + if err != nil { + // 尝试上传到常见的上传目录 + uploadDirs := []string{"upload", "uploads", "tmp", "temp", "public"} + uploaded := false + + for _, dir := range uploadDirs { + testPath := filepath.Join(dir, testFileName) + err = ftpConn.Stor(testPath, strings.NewReader(testContent)) + if err == nil { + output = append(output, fmt.Sprintf("文件上传成功: %s", testPath)) + + // 尝试删除测试文件 + if deleteErr := ftpConn.Delete(testPath); deleteErr == nil { + output = append(output, "测试文件已清理") + } + uploaded = true + break + } + } + + if !uploaded { + return nil, fmt.Errorf("文件上传失败: %v", err) + } + } else { + output = append(output, fmt.Sprintf("文件上传成功: %s", testFileName)) + + // 验证文件是否存在 + entries, err := ftpConn.List("") + if err == nil { + for _, entry := range entries { + if entry.Name == testFileName { + output = append(output, fmt.Sprintf("验证文件存在,大小: %d 字节", entry.Size)) + break + } + } + } + + // 尝试删除测试文件 + if deleteErr := ftpConn.Delete(testFileName); deleteErr == nil { + output = append(output, "测试文件已清理") + } else { + output = append(output, "警告: 无法删除测试文件,请手动清理") + } + } + + output = append(output, "FTP上传功能可用") + return output, nil +} + +// isPrintableText 检查字节数组是否为可打印文本 +func isPrintableText(data []byte) bool { + if len(data) == 0 { + return false + } + + printableCount := 0 + for _, b := range data { + if (b >= 32 && b <= 126) || b == '\n' || b == '\r' || b == '\t' { + printableCount++ + } + } + + // 如果80%以上是可打印字符,认为是文本 + return float64(printableCount)/float64(len(data)) > 0.8 +} \ No newline at end of file diff --git a/Plugins/services/ftp/plugin.go b/Plugins/services/ftp/plugin.go new file mode 100644 index 0000000..36adb44 --- /dev/null +++ b/Plugins/services/ftp/plugin.go @@ -0,0 +1,266 @@ +package ftp + +import ( + "context" + "fmt" + "net" + "strings" + "time" + + "github.com/shadow1ng/fscan/common" + "github.com/shadow1ng/fscan/common/i18n" + "github.com/shadow1ng/fscan/plugins/base" +) + +// FTPPlugin FTP插件实现 +type FTPPlugin struct { + *base.ServicePlugin + exploiter *FTPExploiter +} + +// NewFTPPlugin 创建FTP插件 +func NewFTPPlugin() *FTPPlugin { + // 插件元数据 + metadata := &base.PluginMetadata{ + Name: "ftp", + Version: "2.0.0", + Author: "fscan-team", + Description: "FTP文件传输协议扫描和利用插件", + Category: "service", + Ports: []int{21, 2121}, // 21: 标准FTP端口, 2121: 常见替代端口 + Protocols: []string{"tcp"}, + Tags: []string{"ftp", "file_transfer", "bruteforce", "anonymous"}, + } + + // 创建连接器和服务插件 + connector := NewFTPConnector() + servicePlugin := base.NewServicePlugin(metadata, connector) + + // 创建FTP插件 + plugin := &FTPPlugin{ + ServicePlugin: servicePlugin, + exploiter: NewFTPExploiter(), + } + + // 设置能力 + plugin.SetCapabilities([]base.Capability{ + base.CapWeakPassword, + base.CapUnauthorized, + base.CapDataExtraction, + base.CapFileUpload, + base.CapFileWrite, + }) + + return plugin +} + +// Scan 重写扫描方法以支持匿名登录检测和自动利用 +func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + // 如果禁用暴力破解,只进行服务识别 + if common.DisableBrute { + return p.performServiceIdentification(ctx, info) + } + + // 首先尝试匿名登录 + anonymousCred := &base.Credential{ + Username: "anonymous", + Password: "", + } + + result, err := p.ScanCredential(ctx, info, anonymousCred) + if err == nil && result.Success { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + common.LogSuccess(i18n.GetText("ftp_anonymous_success", target)) + + // 自动利用匿名访问 + if !common.DisableExploit { + p.autoExploit(context.Background(), info, anonymousCred) + } + + return result, nil + } + + // 执行基础的密码扫描 + result, err = p.ServicePlugin.Scan(ctx, info) + if err != nil || !result.Success { + return result, err + } + + // 记录成功的弱密码发现 + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + cred := result.Credentials[0] + common.LogSuccess(i18n.GetText("ftp_weak_pwd_success", target, cred.Username, cred.Password)) + + // 自动利用功能(可通过-ne参数禁用) + if result.Success && len(result.Credentials) > 0 && !common.DisableExploit { + p.autoExploit(context.Background(), info, result.Credentials[0]) + } + + return result, nil +} + +// generateCredentials 生成FTP凭据 +func (p *FTPPlugin) generateCredentials() []*base.Credential { + // 获取FTP专用的用户名字典 + usernames := common.Userdict["ftp"] + if len(usernames) == 0 { + // 默认FTP用户名 + usernames = []string{"ftp", "ftpuser", "admin", "test", "user", "guest"} + } + + return base.GenerateCredentials(usernames, common.Passwords) +} + +// autoExploit 自动利用功能 +func (p *FTPPlugin) autoExploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + common.LogDebug(i18n.GetText("plugin_exploit_start", "FTP", target)) + + // 执行利用操作 + result, err := p.exploiter.Exploit(ctx, info, creds) + if err != nil { + common.LogError(i18n.GetText("plugin_exploit_failed", "FTP", err)) + return + } + + // 处理利用结果 + if result != nil && result.Success { + // SaveExploitResult会自动使用LogSuccess显示红色利用成功消息 + base.SaveExploitResult(info, result, "FTP") + } +} + +// Exploit 使用exploiter执行利用 +func (p *FTPPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds *base.Credential) (*base.ExploitResult, error) { + return p.exploiter.Exploit(ctx, info, creds) +} + +// GetExploitMethods 获取利用方法 +func (p *FTPPlugin) GetExploitMethods() []base.ExploitMethod { + return p.exploiter.GetExploitMethods() +} + +// IsExploitSupported 检查利用支持 +func (p *FTPPlugin) IsExploitSupported(method base.ExploitType) bool { + return p.exploiter.IsExploitSupported(method) +} + +// performServiceIdentification 执行FTP服务识别(-nobr模式) +func (p *FTPPlugin) performServiceIdentification(ctx context.Context, info *common.HostInfo) (*base.ScanResult, error) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 尝试连接到FTP服务获取Banner + ftpInfo, isFTP := p.identifyFTPService(ctx, info) + if isFTP { + // 记录服务识别成功 + common.LogSuccess(i18n.GetText("ftp_service_identified", target, ftpInfo)) + + return &base.ScanResult{ + Success: true, + Service: "FTP", + Banner: ftpInfo, + Extra: map[string]interface{}{ + "service": "FTP", + "port": info.Ports, + "info": ftpInfo, + }, + }, nil + } + + // 如果无法识别为FTP,返回失败 + return &base.ScanResult{ + Success: false, + Error: fmt.Errorf("无法识别为FTP服务"), + }, nil +} + +// identifyFTPService 通过Banner识别FTP服务 +func (p *FTPPlugin) identifyFTPService(ctx context.Context, info *common.HostInfo) (string, bool) { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 尝试建立TCP连接 + conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second) + if err != nil { + return "", false + } + defer conn.Close() + + // 设置读取超时 + conn.SetReadDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) + + // FTP服务器在连接后会发送Welcome Banner + banner := make([]byte, 1024) + n, err := conn.Read(banner) + if err != nil || n < 3 { + return "", false + } + + bannerStr := strings.TrimSpace(string(banner[:n])) + + // 检查FTP协议标识 + if strings.HasPrefix(bannerStr, "220") { + // FTP服务器通常以220开头发送welcome消息 + + // 提取FTP服务器信息 + lines := strings.Split(bannerStr, "\n") + if len(lines) > 0 { + firstLine := strings.TrimSpace(lines[0]) + // 移除状态码 + if len(firstLine) > 4 && firstLine[:3] == "220" { + serverInfo := strings.TrimSpace(firstLine[3:]) + // 移除可能的连字符 + if len(serverInfo) > 0 && serverInfo[0] == '-' { + serverInfo = strings.TrimSpace(serverInfo[1:]) + } + + if serverInfo != "" { + return fmt.Sprintf("FTP服务: %s", serverInfo), true + } + } + } + + return "FTP服务", true + } + + // 检查其他可能的FTP响应 + lowerBanner := strings.ToLower(bannerStr) + if strings.Contains(lowerBanner, "ftp") || + strings.Contains(lowerBanner, "file transfer") || + strings.Contains(lowerBanner, "vsftpd") || + strings.Contains(lowerBanner, "proftpd") || + strings.Contains(lowerBanner, "pure-ftpd") { + return fmt.Sprintf("FTP服务: %s", bannerStr), true + } + + return "", false +} + +// ============================================================================= +// 插件注册 +// ============================================================================= + +// RegisterFTPPlugin 注册FTP插件 +func RegisterFTPPlugin() { + factory := base.NewSimplePluginFactory( + &base.PluginMetadata{ + Name: "ftp", + Version: "2.0.0", + Author: "fscan-team", + Description: "FTP文件传输协议扫描和利用插件", + Category: "service", + Ports: []int{21, 2121}, // 21: 标准FTP端口, 2121: 常见替代端口 + Protocols: []string{"tcp"}, + Tags: []string{"ftp", "file_transfer", "bruteforce", "anonymous"}, + }, + func() base.Plugin { + return NewFTPPlugin() + }, + ) + + base.GlobalPluginRegistry.Register("ftp", factory) +} + +// 自动注册 +func init() { + RegisterFTPPlugin() +} \ No newline at end of file