package ftp import ( "context" "fmt" "path/filepath" "strings" "time" ftplib "github.com/jlaffaye/ftp" "github.com/shadow1ng/fscan/common" "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() { // FTP插件不提供利用功能,仅进行弱密码扫描 } // 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 nil, fmt.Errorf("执行失败: %v", err) } 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 }