mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00

- 完全移除FTP、MySQL、SSH、ActiveMQ的利用功能,只保留弱密码扫描 - 重构Redis插件利用方法,严格按参数控制启用: * arbitrary_file_write: 需要-rwp和(-rwc或-rwf)参数 * ssh_key_write: 需要-rf参数 * crontab_injection: 需要-rs参数 - 修复Redis未授权访问时的利用条件检查问题 - 去除所有信息收集类利用,只保留GetShell和文件写入等实际攻击能力 现在利用功能完全参数驱动,只有提供对应参数时才启动相应利用方法
299 lines
8.4 KiB
Go
299 lines
8.4 KiB
Go
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
|
||
} |