fscan/Plugins/services/ftp/exploiter.go
ZacharyZcR 60e59f5a78 refactor: 精简利用功能,只保留真正有攻击价值的利用方法
- 完全移除FTP、MySQL、SSH、ActiveMQ的利用功能,只保留弱密码扫描
- 重构Redis插件利用方法,严格按参数控制启用:
  * arbitrary_file_write: 需要-rwp和(-rwc或-rwf)参数
  * ssh_key_write: 需要-rf参数
  * crontab_injection: 需要-rs参数
- 修复Redis未授权访问时的利用条件检查问题
- 去除所有信息收集类利用,只保留GetShell和文件写入等实际攻击能力

现在利用功能完全参数驱动,只有提供对应参数时才启动相应利用方法
2025-08-08 09:40:56 +08:00

299 lines
8.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ftp
import (
"context"
"fmt"
"path/filepath"
"strings"
"time"
ftplib "github.com/jlaffaye/ftp"
"github.com/shadow1ng/fscan/common"
"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() {
// FTP插件不提供利用功能仅进行弱密码扫描
}
// 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
}