mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00

- 在Core/Registry.go中添加FTP插件导入,确保插件正确注册 - 完善FTP插件的i18n消息支持,添加完整的中英文消息 - 修复FTP利用器错误处理逻辑,改进错误报告机制 - 添加FTP测试环境docker-compose配置文件 修复后FTP插件支持: - 服务识别和版本检测 - 弱密码扫描和匿名登录检测 - 目录枚举、文件上传下载测试等利用功能
325 lines
9.4 KiB
Go
325 lines
9.4 KiB
Go
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
|
||
} |