diff --git a/README.md b/README.md index 470df69..7531f6f 100644 --- a/README.md +++ b/README.md @@ -33,25 +33,132 @@ - 扫描结果存储:将所有检测结果保存至文件,便于后续分析 # 0x03 使用说明 -完整功能介绍、使用说明及最新更新请访问我们的官方网站。 -## 官方网站 +## 参数配置详解 -**https://fscan.club/** +### 目标配置参数 +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `-h` | 无 | 目标主机: IP, IP段, IP段文件, 域名 | +| `-eh` | 无 | 排除主机 | +| `-p` | 1000个常用端口 | 端口: 默认1000个常用端口 | +| `-ep` | 无 | 排除端口 | +| `-hf` | 无 | 主机文件 | +| `-pf` | 无 | 端口文件 | -访问官网获取: +### 扫描控制参数 +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `-m` | all | 扫描模式: all(全部), icmp(存活探测), 或指定插件名称 | +| `-t` | 600 | 端口扫描线程数 | +| `-time` | 3 | 端口扫描超时时间(秒) | +| `-mt` | 50 | 模块线程数 | +| `-gt` | 180 | 全局超时时间(秒) | +| `-np` | false | 禁用ping探测 | +| `-fp` | false | 启用指纹识别 | +| `-local` | 无 | 指定本地插件名称 (如: cleaner, avdetect, keylogger 等) | +| `-ao` | false | 仅进行存活探测 | -- 详细功能文档 -- 使用教程 -- 最新版本下载 -- 常见问题解答 -- 技术支持 +### 认证与凭据参数 +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `-user` | 无 | 用户名 | +| `-pwd` | 无 | 密码 | +| `-usera` | 无 | 额外用户名 | +| `-pwda` | 无 | 额外密码 | +| `-userf` | 无 | 用户名字典文件 | +| `-pwdf` | 无 | 密码字典文件 | +| `-hashf` | 无 | 哈希文件 | +| `-hash` | 无 | 哈希值 | +| `-domain` | 无 | 域名 (SMB扫描用) | +| `-sshkey` | 无 | SSH私钥文件 | + +### Web扫描参数 +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `-u` | 无 | 目标URL | +| `-uf` | 无 | URL文件 | +| `-cookie` | 无 | HTTP Cookie | +| `-wt` | 5 | Web超时时间(秒) | +| `-proxy` | 无 | HTTP代理 | +| `-socks5` | 无 | SOCKS5代理 (如: 127.0.0.1:1080) | + +### POC测试参数 +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `-pocpath` | 无 | POC脚本路径 | +| `-pocname` | 无 | POC名称 | +| `-full` | false | 全量POC扫描 | +| `-dns` | false | DNS日志记录 | +| `-num` | 20 | POC并发数 | +| `-nopoc` | false | 禁用POC扫描 | + +### Redis利用参数 +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `-rf` | 无 | Redis文件 | +| `-rs` | 无 | Redis Shell | +| `-rwp` | 无 | Redis写入路径 | +| `-rwc` | 无 | Redis写入内容 | +| `-rwf` | 无 | Redis写入文件 | + +### 输出与显示控制参数 +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `-o` | result.txt | 输出文件 | +| `-f` | txt | 输出格式: txt, json, csv | +| `-no` | false | 禁用结果保存 | +| `-silent` | false | 静默模式 | +| `-nocolor` | false | 禁用颜色输出 | +| `-log` | info+success | 日志级别 | +| `-nopg` | false | 禁用进度条 | + +### 高级功能参数 +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `-nobr` | false | 禁用暴力破解 | +| `-retry` | 3 | 最大重试次数 | +| `-sc` | 无 | Shellcode | +| `-rsh` | 无 | 反弹Shell目标地址:端口 (如: 192.168.1.100:4444) | +| `-start-socks5` | 0 | 启动SOCKS5代理服务器端口 (如: 1080) | +| `-fsh-port` | 4444 | 启动正向Shell服务器端口 | +| `-persistence-file` | 无 | Linux持久化目标文件路径 (支持.elf/.sh文件) | +| `-win-pe` | 无 | Windows持久化目标PE文件路径 (支持.exe/.dll文件) | +| `-keylog-output` | keylog.txt | 键盘记录输出文件路径 | +| `-download-url` | 无 | 要下载的文件URL | +| `-download-path` | 无 | 下载文件保存路径 | +| `-lang` | zh | 语言: zh, en | + +## 常用示例 + +```bash +# 基本主机扫描 +./fscan -h 192.168.1.1/24 + +# 指定端口扫描 +./fscan -h 192.168.1.1 -p 80,443,22,3389 + +# 仅存活探测 +./fscan -h 192.168.1.1/24 -ao + +# 禁用暴力破解,仅服务识别 +./fscan -h 192.168.1.1/24 -nobr + +# 指定用户名密码 +./fscan -h 192.168.1.1 -user admin -pwd admin123 + +# Web扫描 +./fscan -u http://192.168.1.1 -proxy http://127.0.0.1:8080 + +# 本地插件使用 +./fscan -local cleaner +``` ## 编译说明 ```bash # 基础编译 -go build -ldflags="-s -w" -trimpath main.go +go build -ldflags="-s -w" -trimpath -o fscan main.go # UPX压缩(可选) upx -9 fscan @@ -65,6 +172,18 @@ yay -S fscan-git paru -S fscan-git ``` +## 官方网站 + +**https://fscan.club/** + +访问官网获取: + +- 详细功能文档 +- 使用教程 +- 最新版本下载 +- 常见问题解答 +- 技术支持 + # 0x04 运行截图 `fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)` diff --git a/plugins/services/kafka.go b/plugins/services/kafka.go index e74db88..c229a37 100644 --- a/plugins/services/kafka.go +++ b/plugins/services/kafka.go @@ -3,6 +3,7 @@ package services import ( "context" "fmt" + "strings" "time" "github.com/IBM/sarama" @@ -22,16 +23,16 @@ func NewKafkaPlugin() *KafkaPlugin { -func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult { +func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) if common.DisableBrute { return p.identifyService(ctx, info) } - credentials := GenerateCredentials("kafka") + credentials := plugins.GenerateCredentials("kafka") if len(credentials) == 0 { - return &ScanResult{ + return &plugins.Result{ Success: false, Service: "kafka", Error: fmt.Errorf("没有可用的测试凭据"), @@ -39,10 +40,21 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu } for _, cred := range credentials { + // 检查上下文是否已取消 + select { + case <-ctx.Done(): + return &plugins.Result{ + Success: false, + Service: "kafka", + Error: ctx.Err(), + } + default: + } + if client := p.testCredential(ctx, info, cred); client != nil { client.Close() common.LogSuccess(fmt.Sprintf("Kafka %s %s:%s", target, cred.Username, cred.Password)) - return &ScanResult{ + return &plugins.Result{ Success: true, Service: "kafka", Username: cred.Username, @@ -51,7 +63,7 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu } } - return &ScanResult{ + return &plugins.Result{ Success: false, Service: "kafka", Error: fmt.Errorf("未发现弱密码"), @@ -59,7 +71,7 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu } -func (p *KafkaPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) sarama.Client { +func (p *KafkaPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred plugins.Credential) sarama.Client { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) timeout := time.Duration(common.Timeout) * time.Second @@ -87,32 +99,71 @@ func (p *KafkaPlugin) testCredential(ctx context.Context, info *common.HostInfo, -func (p *KafkaPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult { +func (p *KafkaPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) - emptyCred := Credential{Username: "", Password: ""} + // 尝试无认证连接 + emptyCred := plugins.Credential{Username: "", Password: ""} client := p.testCredential(ctx, info, emptyCred) - if client == nil { - return &ScanResult{ + if client != nil { + defer client.Close() + banner := "Kafka (无认证)" + common.LogSuccess(fmt.Sprintf("Kafka %s %s", target, banner)) + return &plugins.Result{ + Success: true, + Service: "kafka", + Banner: banner, + } + } + + // 尝试简单认证检测 + config := sarama.NewConfig() + config.Net.DialTimeout = time.Duration(common.Timeout) * time.Second + config.Version = sarama.V2_0_0_0 + + brokers := []string{target} + client, err := sarama.NewClient(brokers, config) + if err != nil { + // 如果连接失败,尝试检查是否是Kafka协议错误 + if p.isKafkaProtocolError(err) { + banner := "Kafka (需要认证)" + common.LogSuccess(fmt.Sprintf("Kafka %s %s", target, banner)) + return &plugins.Result{ + Success: true, + Service: "kafka", + Banner: banner, + } + } + return &plugins.Result{ Success: false, Service: "kafka", - Error: fmt.Errorf("无法连接到Kafka服务"), + Error: fmt.Errorf("无法识别为Kafka服务: %v", err), } } defer client.Close() banner := "Kafka" common.LogSuccess(fmt.Sprintf("Kafka %s %s", target, banner)) - return &ScanResult{ + return &plugins.Result{ Success: true, Service: "kafka", Banner: banner, } } +// isKafkaProtocolError 检查错误是否表示Kafka协议响应 +func (p *KafkaPlugin) isKafkaProtocolError(err error) bool { + errStr := strings.ToLower(err.Error()) + // Kafka常见的协议错误模式 + return strings.Contains(errStr, "sasl") || + strings.Contains(errStr, "authentication") || + strings.Contains(errStr, "kafka") || + strings.Contains(errStr, "protocol") || + strings.Contains(errStr, "broker") +} + func init() { - // 使用高效注册方式:直接传递端口信息,避免实例创建 - RegisterPluginWithPorts("kafka", func() Plugin { + plugins.RegisterWithPorts("kafka", func() plugins.Plugin { return NewKafkaPlugin() }, []int{9092, 9093, 9094}) } \ No newline at end of file diff --git a/plugins/services/rabbitmq.go b/plugins/services/rabbitmq.go index e35e558..907d887 100644 --- a/plugins/services/rabbitmq.go +++ b/plugins/services/rabbitmq.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "net" "net/http" "strings" "time" @@ -24,20 +25,30 @@ func NewRabbitMQPlugin() *RabbitMQPlugin { -func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResult { +func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *plugins.Result { target := fmt.Sprintf("%s:%s", info.Host, info.Ports) if common.DisableBrute { return p.identifyService(ctx, info) } + // 对于AMQP端口,首先识别服务 if info.Ports == "5672" || info.Ports == "5671" { - return p.testAMQPProtocol(ctx, info) + if result := p.testAMQPProtocol(ctx, info); result.Success { + // AMQP协议识别成功,尝试HTTP管理接口的密码爆破 + managementResult := p.testManagementInterface(ctx, info) + if managementResult.Success { + return managementResult + } + // 返回服务识别结果 + return result + } } - credentials := GenerateCredentials("rabbitmq") + // HTTP端口的密码爆破 + credentials := plugins.GenerateCredentials("rabbitmq") if len(credentials) == 0 { - return &ScanResult{ + return &plugins.Result{ Success: false, Service: "rabbitmq", Error: fmt.Errorf("没有可用的测试凭据"), @@ -45,10 +56,20 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR } for _, cred := range credentials { + // 检查上下文是否已取消 + select { + case <-ctx.Done(): + return &plugins.Result{ + Success: false, + Service: "rabbitmq", + Error: ctx.Err(), + } + default: + } + if p.testCredential(ctx, info, cred) { common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s:%s", target, cred.Username, cred.Password)) - - return &ScanResult{ + return &plugins.Result{ Success: true, Service: "rabbitmq", Username: cred.Username, @@ -57,7 +78,7 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR } } - return &ScanResult{ + return &plugins.Result{ Success: false, Service: "rabbitmq", Error: fmt.Errorf("未发现弱密码"), @@ -65,15 +86,94 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR } -func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.HostInfo) *ScanResult { - return &ScanResult{ - Success: true, +// testAMQPProtocol 检测AMQP协议 +func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.HostInfo) *plugins.Result { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) + + // 连接到AMQP端口 + conn, err := net.DialTimeout("tcp", target, time.Duration(common.Timeout)*time.Second) + if err != nil { + return &plugins.Result{ + Success: false, + Service: "rabbitmq", + Error: err, + } + } + defer conn.Close() + + // 设置超时 + conn.SetDeadline(time.Now().Add(time.Duration(common.Timeout) * time.Second)) + + // 发送AMQP协议头请求 (AMQP 0-9-1) + amqpHeader := []byte{0x41, 0x4d, 0x51, 0x50, 0x00, 0x00, 0x09, 0x01} + _, err = conn.Write(amqpHeader) + if err != nil { + return &plugins.Result{ + Success: false, + Service: "rabbitmq", + Error: fmt.Errorf("发送AMQP握手失败: %v", err), + } + } + + // 读取服务器响应 + buffer := make([]byte, 32) + n, err := conn.Read(buffer) + if err != nil { + return &plugins.Result{ + Success: false, + Service: "rabbitmq", + Error: fmt.Errorf("读取AMQP响应失败: %v", err), + } + } + + // 调试信息(可选) + common.LogDebug(fmt.Sprintf("RabbitMQ AMQP响应长度: %d, 前8字节: %v", n, buffer[:min(n, 8)])) + + // 检查AMQP协议头或连接开始帧 + if n >= 4 && string(buffer[:4]) == "AMQP" { + // 服务器返回AMQP协议头 + banner := fmt.Sprintf("RabbitMQ AMQP %d.%d.%d", buffer[5], buffer[6], buffer[7]) + common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s", target, banner)) + return &plugins.Result{ + Success: true, + Service: "rabbitmq", + Banner: banner, + } + } else if n >= 8 && buffer[0] == 0x01 && buffer[7] == 0xCE { + // Connection.Start方法帧 (AMQP 0-9-1) + banner := "RabbitMQ AMQP 0-9-1" + common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s", target, banner)) + return &plugins.Result{ + Success: true, + Service: "rabbitmq", + Banner: banner, + } + } else if n >= 8 && buffer[0] == 0x01 { + // 可能是AMQP帧,但格式不同 + banner := "RabbitMQ AMQP" + common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s", target, banner)) + return &plugins.Result{ + Success: true, + Service: "rabbitmq", + Banner: banner, + } + } + + return &plugins.Result{ + Success: false, Service: "rabbitmq", - Banner: "RabbitMQ AMQP", + Error: fmt.Errorf("非AMQP协议响应"), } } -func (p *RabbitMQPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred Credential) bool { +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func (p *RabbitMQPlugin) testCredential(ctx context.Context, info *common.HostInfo, cred plugins.Credential) bool { baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports) client := &http.Client{ @@ -104,17 +204,19 @@ func (p *RabbitMQPlugin) testCredential(ctx context.Context, info *common.HostIn -func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo) *ScanResult { - target := fmt.Sprintf("%s:%s", info.Host, info.Ports) - +func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result { + // 对于AMQP端口,检测AMQP协议 if info.Ports == "5672" || info.Ports == "5671" { - return &ScanResult{ - Success: true, - Service: "rabbitmq", - Banner: "RabbitMQ AMQP", - } + return p.testAMQPProtocol(ctx, info) } + // 对于HTTP端口,检测管理界面 + return p.testManagementInterface(ctx, info) +} + +// testManagementInterface 检测RabbitMQ管理界面 +func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *common.HostInfo) *plugins.Result { + target := fmt.Sprintf("%s:%s", info.Host, info.Ports) baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports) client := &http.Client{ @@ -123,7 +225,7 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil) if err != nil { - return &ScanResult{ + return &plugins.Result{ Success: false, Service: "rabbitmq", Error: err, @@ -132,7 +234,7 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI resp, err := client.Do(req) if err != nil { - return &ScanResult{ + return &plugins.Result{ Success: false, Service: "rabbitmq", Error: err, @@ -146,33 +248,29 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI bodyStr := strings.ToLower(string(body)) if strings.Contains(bodyStr, "rabbitmq") { - banner = "RabbitMQ" + banner = "RabbitMQ Management" } else if strings.Contains(bodyStr, "management") { - banner = "RabbitMQ" + banner = "RabbitMQ Management" } else { - banner = "RabbitMQ" + banner = "RabbitMQ Management" + } + common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s", target, banner)) + return &plugins.Result{ + Success: true, + Service: "rabbitmq", + Banner: banner, } } else { - return &ScanResult{ + return &plugins.Result{ Success: false, Service: "rabbitmq", Error: fmt.Errorf("无法识别为RabbitMQ服务"), } } - - common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s", target, banner)) - - return &ScanResult{ - Success: true, - Service: "rabbitmq", - Banner: banner, - } } -// init 自动注册插件 func init() { - // 使用高效注册方式:直接传递端口信息,避免实例创建 - RegisterPluginWithPorts("rabbitmq", func() Plugin { + plugins.RegisterWithPorts("rabbitmq", func() plugins.Plugin { return NewRabbitMQPlugin() }, []int{5672, 15672, 5671}) } \ No newline at end of file