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

- 删除整个legacy插件系统(7794行代码) - 完成所有插件向单文件架构迁移 - 移除19个插件的虚假Exploit功能,只保留真实利用: * Redis: 文件写入、SSH密钥注入、计划任务 * SSH: 命令执行 * MS17010: EternalBlue漏洞利用 - 统一插件接口,简化架构复杂度 - 清理临时文件和备份文件 重构效果: - 代码行数: -7794行 - 插件文件数: 从3文件架构→单文件架构 - 真实利用插件: 从22个→3个 - 架构复杂度: 大幅简化
235 lines
5.4 KiB
Go
235 lines
5.4 KiB
Go
package services
|
|
|
|
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("未发现弱密码或匿名访问"),
|
|
}
|
|
}
|
|
|
|
|
|
// 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()
|
|
})
|
|
} |