mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 14:06:44 +08:00
feat: 实现FTP文件传输协议专业扫描插件
• 新增FTP插件支持标准21端口和常见替代端口 • 实现匿名访问检测和弱密码爆破功能 • 支持自动利用:目录枚举、文件下载测试、文件上传测试 • 集成-nobr模式服务识别和-ne自动利用功能 • 完整的Context超时机制和连接数管理 • 添加中英文国际化消息支持 • 基于新插件架构实现模块化设计 功能特性: - 支持jlaffaye/ftp驱动的FTP协议通信 - 智能识别vsFTPd、ProFTPD等多种FTP服务器 - 三种利用方法:目录结构探测、文件操作测试 - 完整的错误处理和连接限制处理机制
This commit is contained in:
parent
516225a11f
commit
83afd0f994
116
Plugins/services/ftp/connector.go
Normal file
116
Plugins/services/ftp/connector.go
Normal file
@ -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
|
||||||
|
}
|
331
Plugins/services/ftp/exploiter.go
Normal file
331
Plugins/services/ftp/exploiter.go
Normal file
@ -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
|
||||||
|
}
|
266
Plugins/services/ftp/plugin.go
Normal file
266
Plugins/services/ftp/plugin.go
Normal file
@ -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()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user