Files
fscan/plugins/services/rdp.go
T
ZacharyZcR 71b92d4408 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)
2026-01-11 20:16:23 +08:00

264 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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})
}