mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +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