docs: 完善参数配置文档并修复插件兼容性

- 添加完整的参数配置表格和使用示例到README.md
- 修复Kafka插件的协议错误识别逻辑
- 修复RabbitMQ插件的AMQP协议检测
- 完成所有核心参数的功能验证测试
This commit is contained in:
ZacharyZcR 2025-09-02 05:41:22 +00:00
parent b7b805874f
commit af2c92a591
3 changed files with 329 additions and 61 deletions

139
README.md
View File

@ -33,25 +33,132 @@
- 扫描结果存储:将所有检测结果保存至文件,便于后续分析 - 扫描结果存储:将所有检测结果保存至文件,便于后续分析
# 0x03 使用说明 # 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 ```bash
# 基础编译 # 基础编译
go build -ldflags="-s -w" -trimpath main.go go build -ldflags="-s -w" -trimpath -o fscan main.go
# UPX压缩可选 # UPX压缩可选
upx -9 fscan upx -9 fscan
@ -65,6 +172,18 @@ yay -S fscan-git
paru -S fscan-git paru -S fscan-git
``` ```
## 官方网站
**https://fscan.club/**
访问官网获取:
- 详细功能文档
- 使用教程
- 最新版本下载
- 常见问题解答
- 技术支持
# 0x04 运行截图 # 0x04 运行截图
`fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)` `fscan.exe -h 192.168.x.x (全功能、ms17010、读取网卡信息)`

View File

@ -3,6 +3,7 @@ package services
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/IBM/sarama" "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) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
if common.DisableBrute { if common.DisableBrute {
return p.identifyService(ctx, info) return p.identifyService(ctx, info)
} }
credentials := GenerateCredentials("kafka") credentials := plugins.GenerateCredentials("kafka")
if len(credentials) == 0 { if len(credentials) == 0 {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "kafka", Service: "kafka",
Error: fmt.Errorf("没有可用的测试凭据"), Error: fmt.Errorf("没有可用的测试凭据"),
@ -39,10 +40,21 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanResu
} }
for _, cred := range credentials { 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 { if client := p.testCredential(ctx, info, cred); client != nil {
client.Close() client.Close()
common.LogSuccess(fmt.Sprintf("Kafka %s %s:%s", target, cred.Username, cred.Password)) common.LogSuccess(fmt.Sprintf("Kafka %s %s:%s", target, cred.Username, cred.Password))
return &ScanResult{ return &plugins.Result{
Success: true, Success: true,
Service: "kafka", Service: "kafka",
Username: cred.Username, 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, Success: false,
Service: "kafka", Service: "kafka",
Error: fmt.Errorf("未发现弱密码"), 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) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
timeout := time.Duration(common.Timeout) * time.Second 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) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
emptyCred := Credential{Username: "", Password: ""} // 尝试无认证连接
emptyCred := plugins.Credential{Username: "", Password: ""}
client := p.testCredential(ctx, info, emptyCred) client := p.testCredential(ctx, info, emptyCred)
if client == nil { if client != nil {
return &ScanResult{ 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, Success: false,
Service: "kafka", Service: "kafka",
Error: fmt.Errorf("无法连接到Kafka服务"), Error: fmt.Errorf("无法识别为Kafka服务: %v", err),
} }
} }
defer client.Close() defer client.Close()
banner := "Kafka" banner := "Kafka"
common.LogSuccess(fmt.Sprintf("Kafka %s %s", target, banner)) common.LogSuccess(fmt.Sprintf("Kafka %s %s", target, banner))
return &ScanResult{ return &plugins.Result{
Success: true, Success: true,
Service: "kafka", Service: "kafka",
Banner: banner, 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() { func init() {
// 使用高效注册方式:直接传递端口信息,避免实例创建 plugins.RegisterWithPorts("kafka", func() plugins.Plugin {
RegisterPluginWithPorts("kafka", func() Plugin {
return NewKafkaPlugin() return NewKafkaPlugin()
}, []int{9092, 9093, 9094}) }, []int{9092, 9093, 9094})
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"strings" "strings"
"time" "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) target := fmt.Sprintf("%s:%s", info.Host, info.Ports)
if common.DisableBrute { if common.DisableBrute {
return p.identifyService(ctx, info) return p.identifyService(ctx, info)
} }
// 对于AMQP端口首先识别服务
if info.Ports == "5672" || info.Ports == "5671" { 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 { if len(credentials) == 0 {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "rabbitmq", Service: "rabbitmq",
Error: fmt.Errorf("没有可用的测试凭据"), Error: fmt.Errorf("没有可用的测试凭据"),
@ -45,10 +56,20 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo) *ScanR
} }
for _, cred := range credentials { 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) { if p.testCredential(ctx, info, cred) {
common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s:%s", target, cred.Username, cred.Password)) common.LogSuccess(fmt.Sprintf("RabbitMQ %s %s:%s", target, cred.Username, cred.Password))
return &plugins.Result{
return &ScanResult{
Success: true, Success: true,
Service: "rabbitmq", Service: "rabbitmq",
Username: cred.Username, 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, Success: false,
Service: "rabbitmq", Service: "rabbitmq",
Error: fmt.Errorf("未发现弱密码"), 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 { // testAMQPProtocol 检测AMQP协议
return &ScanResult{ 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, Success: true,
Service: "rabbitmq", Service: "rabbitmq",
Banner: "RabbitMQ AMQP", 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",
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) baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports)
client := &http.Client{ 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 { func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo) *plugins.Result {
target := fmt.Sprintf("%s:%s", info.Host, info.Ports) // 对于AMQP端口检测AMQP协议
if info.Ports == "5672" || info.Ports == "5671" { if info.Ports == "5672" || info.Ports == "5671" {
return &ScanResult{ return p.testAMQPProtocol(ctx, info)
Success: true,
Service: "rabbitmq",
Banner: "RabbitMQ AMQP",
}
} }
// 对于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) baseURL := fmt.Sprintf("http://%s:%s", info.Host, info.Ports)
client := &http.Client{ 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) req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil)
if err != nil { if err != nil {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "rabbitmq", Service: "rabbitmq",
Error: err, Error: err,
@ -132,7 +234,7 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "rabbitmq", Service: "rabbitmq",
Error: err, Error: err,
@ -146,33 +248,29 @@ func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostI
bodyStr := strings.ToLower(string(body)) bodyStr := strings.ToLower(string(body))
if strings.Contains(bodyStr, "rabbitmq") { if strings.Contains(bodyStr, "rabbitmq") {
banner = "RabbitMQ" banner = "RabbitMQ Management"
} else if strings.Contains(bodyStr, "management") { } else if strings.Contains(bodyStr, "management") {
banner = "RabbitMQ" banner = "RabbitMQ Management"
} else { } 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 { } else {
return &ScanResult{ return &plugins.Result{
Success: false, Success: false,
Service: "rabbitmq", Service: "rabbitmq",
Error: fmt.Errorf("无法识别为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() { func init() {
// 使用高效注册方式:直接传递端口信息,避免实例创建 plugins.RegisterWithPorts("rabbitmq", func() plugins.Plugin {
RegisterPluginWithPorts("rabbitmq", func() Plugin {
return NewRabbitMQPlugin() return NewRabbitMQPlugin()
}, []int{5672, 15672, 5671}) }, []int{5672, 15672, 5671})
} }