mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-25 04:31:52 +08:00
feat: v2.1.0 核心重构与功能增强
## 架构重构
- 全局变量消除,迁移至 Config/State 对象
- SMB 插件融合(smb/smb2/smbghost/smbinfo)
- 服务探测重构,实现 Nmap 风格 fallback 机制
- 输出系统重构,TXT 实时刷盘 + 双写机制
- i18n 框架升级至 go-i18n
## 性能优化
- 正则表达式预编译
- 内存优化 map[string]struct{}
- 并发指纹匹配
- SOCKS5 连接复用
- 滑动窗口调度 + 自适应线程池
## 新功能
- Web 管理界面
- 多格式 POC 适配(xray/afrog)
- 增强指纹库(3139条)
- Favicon hash 指纹识别
- 插件选择性编译(Build Tags)
- fscan-lab 靶场环境
- 默认端口扩展(62→133)
## 构建系统
- 添加 no_local tag 支持排除本地插件
- 多版本构建:fscan/fscan-nolocal/fscan-web
- CI 添加 snapshot 模式支持仅测试构建
## Bug 修复
- 修复 120+ 个问题,包括 RDP panic、批量扫描漏报、
JSON 输出格式、Redis 检测、Context 超时等
## 测试增强
- 单元测试覆盖率 74-100%
- 并发安全测试
- 集成测试(Web/端口/服务/SSH/ICMP)
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
//go:build plugin_rdp || !plugin_selective
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
"github.com/shadow1ng/fscan/common/i18n"
|
||||
"github.com/shadow1ng/fscan/mylib/grdp/glog"
|
||||
"github.com/shadow1ng/fscan/mylib/grdp/login"
|
||||
"github.com/shadow1ng/fscan/mylib/grdp/protocol/x224"
|
||||
"github.com/shadow1ng/fscan/plugins"
|
||||
)
|
||||
|
||||
// RDPPlugin RDP远程桌面服务扫描插件 - 真实RDP认证和系统指纹识别
|
||||
type RDPPlugin struct {
|
||||
plugins.BasePlugin
|
||||
}
|
||||
|
||||
// NewRDPPlugin 创建RDP插件
|
||||
func NewRDPPlugin() *RDPPlugin {
|
||||
return &RDPPlugin{
|
||||
BasePlugin: plugins.NewBasePlugin("rdp"),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan 执行RDP扫描 - 系统指纹识别 + 真实暴力破解
|
||||
func (p *RDPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
|
||||
target := info.Target()
|
||||
|
||||
// 配置grdp日志级别
|
||||
login.LogLever = glog.NONE // 静默模式,避免干扰输出
|
||||
|
||||
// 配置代理
|
||||
if config.Network.Socks5Proxy != "" {
|
||||
login.Socks5Proxy = config.Network.Socks5Proxy
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 第一阶段:系统指纹识别(无需密码)
|
||||
// ============================================
|
||||
osInfo := p.probeOSInfo(target, config, state)
|
||||
if len(osInfo) > 0 {
|
||||
p.logOSInfo(target, osInfo)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 第二阶段:暴力破解
|
||||
// ============================================
|
||||
if config.DisableBrute {
|
||||
// 禁用暴力破解,仅返回服务识别结果
|
||||
banner := p.buildBanner(osInfo)
|
||||
common.LogSuccess(i18n.Tr("rdp_service", target, banner))
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeService,
|
||||
Service: "rdp",
|
||||
Banner: banner,
|
||||
}
|
||||
}
|
||||
|
||||
// 生成测试凭据
|
||||
credentials := GenerateCredentials("rdp", config)
|
||||
if len(credentials) == 0 {
|
||||
credentials = []Credential{
|
||||
{Username: "administrator", Password: ""},
|
||||
{Username: "administrator", Password: "administrator"},
|
||||
{Username: "administrator", Password: "password"},
|
||||
{Username: "administrator", Password: "123456"},
|
||||
{Username: "admin", Password: "admin"},
|
||||
{Username: "admin", Password: "123456"},
|
||||
{Username: "user", Password: "user"},
|
||||
{Username: "test", Password: "test"},
|
||||
}
|
||||
}
|
||||
|
||||
// 获取域名
|
||||
domain := config.Credentials.Domain
|
||||
if domain == "" {
|
||||
// 尝试从OSInfo中提取域名
|
||||
if osInfo != nil {
|
||||
if val, ok := osInfo["NetBIOSDomainName"].(string); ok && val != "" {
|
||||
domain = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 逐个测试凭据
|
||||
for _, cred := range credentials {
|
||||
// 检查Context是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rdp",
|
||||
Error: ctx.Err(),
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 真实RDP认证
|
||||
success, err := p.rdpCrack(target, domain, cred.Username, cred.Password, config, state)
|
||||
if success {
|
||||
displayDomain := domain
|
||||
if displayDomain == "" {
|
||||
displayDomain = "WORKGROUP"
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("RDP %s %s\\%s %s", target, displayDomain, cred.Username, cred.Password)
|
||||
common.LogSuccess(result)
|
||||
|
||||
return &ScanResult{
|
||||
Success: true,
|
||||
Type: plugins.ResultTypeCredential,
|
||||
Service: "rdp",
|
||||
Username: cred.Username,
|
||||
Password: cred.Password,
|
||||
Banner: p.buildBanner(osInfo),
|
||||
}
|
||||
}
|
||||
|
||||
// 记录失败(仅调试时)
|
||||
if err != nil && strings.Contains(err.Error(), "dial err") {
|
||||
// 端口未开放,直接返回
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rdp",
|
||||
Error: fmt.Errorf("RDP端口未开放"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有凭据都失败
|
||||
return &ScanResult{
|
||||
Success: false,
|
||||
Service: "rdp",
|
||||
Error: fmt.Errorf("RDP认证失败"),
|
||||
}
|
||||
}
|
||||
|
||||
// rdpCrack 使用grdp库进行真实RDP认证
|
||||
func (p *RDPPlugin) rdpCrack(host, domain, user, password string, config *common.Config, state *common.State) (bool, error) {
|
||||
timeout := int64(config.Timeout.Seconds())
|
||||
|
||||
// 优先尝试 SSL 协议
|
||||
success, err := login.RdpCrack(host, domain, user, password, timeout, x224.PROTOCOL_SSL)
|
||||
if success {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SSL失败,grdp会自动尝试协议降级(PROTOCOL_RDP)
|
||||
// 这里的err包含了自动重连后的结果
|
||||
if err != nil && strings.Contains(err.Error(), "dial err") {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return false, err
|
||||
}
|
||||
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
return false, err
|
||||
}
|
||||
|
||||
// probeOSInfo 通过NLA协商获取系统信息(无需密码)
|
||||
func (p *RDPPlugin) probeOSInfo(host string, config *common.Config, state *common.State) map[string]any {
|
||||
timeout := int64(config.Timeout.Seconds())
|
||||
client := login.NewClient(host, glog.NONE)
|
||||
|
||||
// 使用 PROTOCOL_HYBRID 协议探测系统信息
|
||||
// NLA握手阶段会返回系统信息,无需完整认证
|
||||
osInfo := client.ProbeOSInfo(host, "", "", "", timeout, x224.PROTOCOL_HYBRID)
|
||||
|
||||
if len(osInfo) > 0 {
|
||||
state.IncrementTCPSuccessPacketCount()
|
||||
} else {
|
||||
state.IncrementTCPFailedPacketCount()
|
||||
}
|
||||
|
||||
return osInfo
|
||||
}
|
||||
|
||||
// logOSInfo 输出系统信息
|
||||
func (p *RDPPlugin) logOSInfo(target string, osInfo map[string]any) {
|
||||
var parts []string
|
||||
|
||||
// 提取关键信息
|
||||
hostname := p.extractStringField(osInfo, "NetBIOSComputerName")
|
||||
dnsDomain := p.extractStringField(osInfo, "DNSDomainName")
|
||||
fqdn := p.extractStringField(osInfo, "FQDN")
|
||||
netbiosDomain := p.extractStringField(osInfo, "NetBIOSDomainName")
|
||||
productVersion := p.extractStringField(osInfo, "ProductVersion")
|
||||
osVersion := p.extractStringField(osInfo, "OsVerion")
|
||||
|
||||
// 检查是否获取到有效信息
|
||||
if hostname == "" && dnsDomain == "" && fqdn == "" && netbiosDomain == "" && productVersion == "" && osVersion == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// 构造输出
|
||||
if osVersion != "" {
|
||||
parts = append(parts, fmt.Sprintf("OS:%s", osVersion))
|
||||
}
|
||||
if productVersion != "" {
|
||||
parts = append(parts, fmt.Sprintf("Build:Windows %s", productVersion))
|
||||
}
|
||||
if hostname != "" {
|
||||
parts = append(parts, fmt.Sprintf("Hostname:%s", hostname))
|
||||
}
|
||||
if dnsDomain != "" {
|
||||
parts = append(parts, fmt.Sprintf("DNSDomain:%s", dnsDomain))
|
||||
}
|
||||
if fqdn != "" {
|
||||
parts = append(parts, fmt.Sprintf("FQDN:%s", fqdn))
|
||||
}
|
||||
if netbiosDomain != "" {
|
||||
parts = append(parts, fmt.Sprintf("NetBIOSDomain:%s", netbiosDomain))
|
||||
}
|
||||
|
||||
if len(parts) > 0 {
|
||||
info := fmt.Sprintf("RDP %s [%s]", target, strings.Join(parts, ", "))
|
||||
common.LogSuccess(info)
|
||||
}
|
||||
}
|
||||
|
||||
// buildBanner 构建服务识别Banner
|
||||
func (p *RDPPlugin) buildBanner(osInfo map[string]any) string {
|
||||
if len(osInfo) == 0 {
|
||||
return "RDP远程桌面服务"
|
||||
}
|
||||
|
||||
osVersion := p.extractStringField(osInfo, "OsVerion")
|
||||
hostname := p.extractStringField(osInfo, "NetBIOSComputerName")
|
||||
|
||||
if osVersion != "" && hostname != "" {
|
||||
return fmt.Sprintf("RDP (%s, %s)", osVersion, hostname)
|
||||
} else if osVersion != "" {
|
||||
return fmt.Sprintf("RDP (%s)", osVersion)
|
||||
} else if hostname != "" {
|
||||
return fmt.Sprintf("RDP (Hostname:%s)", hostname)
|
||||
}
|
||||
|
||||
return "RDP远程桌面服务"
|
||||
}
|
||||
|
||||
// extractStringField 安全提取字符串字段
|
||||
func (p *RDPPlugin) extractStringField(osInfo map[string]any, key string) string {
|
||||
if value, exists := osInfo[key]; exists {
|
||||
if strValue, ok := value.(string); ok {
|
||||
return strValue
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// init 自动注册插件
|
||||
func init() {
|
||||
// 使用高效注册方式:直接传递端口信息,避免实例创建
|
||||
RegisterPluginWithPorts("rdp", func() Plugin {
|
||||
return NewRDPPlugin()
|
||||
}, []int{3389})
|
||||
}
|
||||
Reference in New Issue
Block a user