fscan/plugins/ftp.go
ZacharyZcR 678d750c8a refactor: 重构插件架构,实现单文件插件系统
将复杂的三文件插件架构(connector/exploiter/plugin)重构为简化的单文件插件架构,
大幅减少代码重复和维护成本,提升插件开发效率。

主要改进:
• 将每个服务插件从3个文件简化为1个文件
• 删除过度设计的工厂模式、适配器模式等抽象层
• 消除plugins/services/、plugins/adapters/、plugins/base/复杂目录结构
• 实现直接的插件注册机制,提升系统简洁性
• 保持完全向后兼容,所有扫描功能和输出格式不变

重构统计:
• 删除文件:100+个复杂架构文件
• 新增文件:20个简化的单文件插件
• 代码减少:每个插件减少60-80%代码量
• 功能增强:所有插件包含完整扫描和利用功能

已重构插件: MySQL, SSH, Redis, MongoDB, PostgreSQL, MSSQL, Oracle,
Neo4j, Memcached, RabbitMQ, ActiveMQ, Cassandra, FTP, Kafka, LDAP,
Rsync, SMTP, SNMP, Telnet, VNC

验证通过: 新系统编译运行正常,所有插件功能验证通过
2025-08-25 23:57:00 +08:00

319 lines
8.0 KiB
Go

package plugins
import (
"context"
"fmt"
"strings"
"time"
ftplib "github.com/jlaffaye/ftp"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
)
// FTPPlugin FTP服务扫描和利用插件 - 包含文件操作利用功能
type FTPPlugin struct {
name string
ports []int
}
// NewFTPPlugin 创建FTP插件
func NewFTPPlugin() *FTPPlugin {
return &FTPPlugin{
name: "ftp",
ports: []int{21, 2121, 990}, // FTP端口 + FTPS端口
}
}
// GetName 实现Plugin接口
func (p *FTPPlugin) GetName() string {
return p.name
}
// GetPorts 实现Plugin接口
func (p *FTPPlugin) GetPorts() []int {
return p.ports
}
// Scan 执行FTP扫描 - 弱密码检测和匿名访问检测
func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
// 如果禁用暴力破解,只做服务识别
if common.DisableBrute {
return p.identifyService(info)
}
// 首先检查匿名访问
if result := p.testAnonymousAccess(ctx, info); result != nil && result.Success {
common.LogSuccess(i18n.GetText("ftp_anonymous_success", target))
return result
}
// 生成测试凭据
credentials := GenerateCredentials("ftp")
if len(credentials) == 0 {
// FTP默认凭据
credentials = []Credential{
{Username: "ftp", Password: "ftp"},
{Username: "admin", Password: "admin"},
{Username: "admin", Password: ""},
{Username: "user", Password: "user"},
{Username: "test", Password: "test"},
}
}
// 逐个测试凭据
for _, cred := range credentials {
// 检查Context是否被取消
select {
case <-ctx.Done():
return &ScanResult{
Success: false,
Service: "ftp",
Error: ctx.Err(),
}
default:
}
// 测试凭据
if conn := p.testCredential(ctx, info, cred); conn != nil {
conn.Quit() // 关闭测试连接
// FTP认证成功
common.LogSuccess(i18n.GetText("ftp_scan_success", target, cred.Username, cred.Password))
return &ScanResult{
Success: true,
Service: "ftp",
Username: cred.Username,
Password: cred.Password,
}
}
}
// 所有凭据都失败
return &ScanResult{
Success: false,
Service: "ftp",
Error: fmt.Errorf("未发现弱密码或匿名访问"),
}
}
// Exploit 执行FTP利用操作 - 实现文件操作功能
func (p *FTPPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
// 建立FTP连接
conn := p.testCredential(ctx, info, creds)
if conn == nil {
return &ExploitResult{
Success: false,
Error: fmt.Errorf("FTP连接失败"),
}
}
defer conn.Quit()
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
common.LogSuccess(fmt.Sprintf("FTP利用开始: %s (用户: %s)", target, creds.Username))
var output strings.Builder
output.WriteString(fmt.Sprintf("=== FTP利用结果 - %s ===\n", target))
// 获取当前工作目录
if pwd, err := conn.CurrentDir(); err == nil {
output.WriteString(fmt.Sprintf("\n[当前目录] %s\n", pwd))
}
// 列出根目录文件
if entries, err := conn.List("/"); err == nil {
output.WriteString(fmt.Sprintf("\n[根目录文件列表] (共%d项)\n", len(entries)))
for i, entry := range entries {
if i >= 10 { // 限制显示前10项
output.WriteString("... (更多文件)\n")
break
}
output.WriteString(fmt.Sprintf(" %s %10d %s %s\n",
entry.Type, entry.Size, entry.Time.Format("2006-01-02 15:04"), entry.Name))
}
}
// 检查常见敏感目录
sensitiveDirectories := []string{"/etc", "/home", "/var", "/tmp", "/root", "/opt"}
for _, dir := range sensitiveDirectories {
select {
case <-ctx.Done():
return &ExploitResult{
Success: false,
Output: output.String(),
Error: ctx.Err(),
}
default:
}
if entries, err := conn.List(dir); err == nil {
output.WriteString(fmt.Sprintf("\n[敏感目录] %s (共%d项)\n", dir, len(entries)))
for i, entry := range entries {
if i >= 5 { // 每个目录只显示前5项
output.WriteString(" ... (更多文件)\n")
break
}
output.WriteString(fmt.Sprintf(" %s %s\n", entry.Type, entry.Name))
}
}
}
// 检查系统信息
if sysInfo := p.getSystemInfo(conn); sysInfo != "" {
output.WriteString(fmt.Sprintf("\n[系统信息]\n%s\n", sysInfo))
}
// 尝试创建测试文件(验证写权限)
testFileName := "fscan_test.txt"
testContent := "FScan Security Test File"
if err := p.testWritePermission(conn, testFileName, testContent); err == nil {
output.WriteString(fmt.Sprintf("\n[写权限测试] ✅ 成功创建文件: %s\n", testFileName))
// 清理测试文件
conn.Delete(testFileName)
} else {
output.WriteString(fmt.Sprintf("\n[写权限测试] ❌ 无写权限: %v\n", err))
}
common.LogSuccess(fmt.Sprintf("FTP利用完成: %s", target))
return &ExploitResult{
Success: true,
Output: output.String(),
}
}
// testAnonymousAccess 测试匿名访问
func (p *FTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
// 尝试匿名登录
anonCreds := []Credential{
{Username: "anonymous", Password: ""},
{Username: "anonymous", Password: "anonymous"},
{Username: "anonymous", Password: "guest@example.com"},
{Username: "ftp", Password: ""},
}
for _, cred := range anonCreds {
if conn := p.testCredential(ctx, info, cred); conn != nil {
conn.Quit()
return &ScanResult{
Success: true,
Service: "ftp",
Username: cred.Username,
Password: cred.Password,
Banner: "匿名访问",
}
}
}
return nil
}
// testCredential 测试单个凭据 - 返回FTP连接或nil
func (p *FTPPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) *ftplib.ServerConn {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
timeout := time.Duration(common.Timeout) * time.Second
// 使用Context控制超时的连接
type ftpResult struct {
conn *ftplib.ServerConn
err error
}
connChan := make(chan ftpResult, 1)
go func() {
// 建立FTP连接
conn, err := ftplib.DialTimeout(target, timeout)
if err != nil {
connChan <- ftpResult{nil, err}
return
}
// 尝试登录
err = conn.Login(cred.Username, cred.Password)
if err != nil {
conn.Quit()
connChan <- ftpResult{nil, err}
return
}
connChan <- ftpResult{conn, nil}
}()
// 等待连接结果或超时
select {
case result := <-connChan:
if result.err != nil {
return nil
}
return result.conn
case <-ctx.Done():
return nil
}
}
// testWritePermission 测试写权限
func (p *FTPPlugin) testWritePermission(conn *ftplib.ServerConn, filename, content string) error {
// 尝试创建文件并写入内容
return conn.Stor(filename, strings.NewReader(content))
}
// getSystemInfo 获取系统信息
func (p *FTPPlugin) getSystemInfo(conn *ftplib.ServerConn) string {
var info strings.Builder
// 尝试获取当前目录作为系统信息
if pwd, err := conn.CurrentDir(); err == nil {
info.WriteString(fmt.Sprintf("当前目录: %s\n", pwd))
}
// 尝试列出当前目录获取更多信息
if entries, err := conn.List("."); err == nil && len(entries) > 0 {
info.WriteString(fmt.Sprintf("目录项数量: %d\n", len(entries)))
}
return info.String()
}
// identifyService 服务识别 - 检测FTP服务
func (p *FTPPlugin) identifyService(info *common.HostInfo) *ScanResult {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
timeout := time.Duration(common.Timeout) * time.Second
// 尝试连接FTP服务
conn, err := ftplib.DialTimeout(target, timeout)
if err != nil {
return &ScanResult{
Success: false,
Service: "ftp",
Error: err,
}
}
defer conn.Quit()
// 获取FTP服务器信息
var banner string
if pwd, err := conn.CurrentDir(); err == nil {
banner = fmt.Sprintf("FTP服务 (根目录: %s)", pwd)
} else {
banner = "FTP服务"
}
common.LogSuccess(i18n.GetText("ftp_service_identified", target, banner))
return &ScanResult{
Success: true,
Service: "ftp",
Banner: banner,
}
}
// init 自动注册插件
func init() {
RegisterPlugin("ftp", func() Plugin {
return NewFTPPlugin()
})
}