fscan/Plugins/services/ftp/exploiter.go
ZacharyZcR 4b482b603d fix: 修复FTP插件注册和利用功能问题
- 在Core/Registry.go中添加FTP插件导入,确保插件正确注册
- 完善FTP插件的i18n消息支持,添加完整的中英文消息
- 修复FTP利用器错误处理逻辑,改进错误报告机制
- 添加FTP测试环境docker-compose配置文件

修复后FTP插件支持:
- 服务识别和版本检测
- 弱密码扫描和匿名登录检测
- 目录枚举、文件上传下载测试等利用功能
2025-08-08 08:58:36 +08:00

325 lines
9.4 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 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 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
}