diff --git a/mylib/grdp/login/screen.go b/mylib/grdp/login/screen.go index e911116..e8fb2f7 100644 --- a/mylib/grdp/login/screen.go +++ b/mylib/grdp/login/screen.go @@ -67,6 +67,13 @@ func RdpCrack(host, domain, user, password string, timeout int64, rdpProtocol ui } } +// NlaAuth 仅进行NLA认证验证,不建立RDP会话,不会挤掉已登录用户 +// 返回: (认证成功, 错误信息) +func NlaAuth(host, domain, user, password string, timeout int64) (bool, error) { + g := NewClient(host, LogLever) + return g.NlaAuthOnly(domain, user, password, timeout) +} + type Client struct { Host string // ip:port tpkt *tpkt.TPKT @@ -213,6 +220,55 @@ func ToRGBA(pixel int, i int, data []byte) (r, g, b, a uint8) { return } +// NlaAuthOnly 仅进行NLA认证验证凭据,不建立RDP会话 +// 这样不会挤掉已登录的用户 +func (g *Client) NlaAuthOnly(domain, user, pwd string, timeout int64) (bool, error) { + conn, err := WrapperTcpWithTimeout("tcp", g.Host, time.Duration(timeout)*time.Second) + if err != nil { + return false, fmt.Errorf("[dial err] %v", err) + } + defer conn.Close() + + g.tpkt = tpkt.New(core.NewSocketLayer(conn), nla.NewNTLMv2(domain, user, pwd)) + g.x224 = x224.New(g.tpkt) + + // 设置NLA仅验证模式 + g.tpkt.SetNLAAuthOnly(true) + + // 使用 PROTOCOL_HYBRID (NLA) 协议 + g.x224.SetRequestedProtocol(x224.PROTOCOL_HYBRID) + + // 用于接收结果的通道 + resultChan := make(chan error, 1) + + // 监听错误事件(包括 ErrNLAAuthSuccess) + g.x224.On("error", func(err error) { + resultChan <- err + }) + + // 监听连接事件(不应该发生在 auth-only 模式) + g.x224.On("connect", func(protocol uint32) { + resultChan <- fmt.Errorf("unexpected connect in auth-only mode") + }) + + // 发起连接 + err = g.x224.Connect() + if err != nil { + return false, err + } + + // 等待结果或超时 + select { + case err := <-resultChan: + if err == tpkt.ErrNLAAuthSuccess { + return true, nil + } + return false, err + case <-time.After(time.Duration(timeout*3) * time.Second): + return false, fmt.Errorf("NLA auth timeout") + } +} + func (g *Client) ProbeOSInfo(host, domain, user, pwd string, timeout int64, rdpProtocol uint32) (info map[string]any) { start := time.Now() exitFlag := make(chan bool) diff --git a/mylib/grdp/protocol/tpkt/tpkt.go b/mylib/grdp/protocol/tpkt/tpkt.go index 3bd2a9d..34e9569 100644 --- a/mylib/grdp/protocol/tpkt/tpkt.go +++ b/mylib/grdp/protocol/tpkt/tpkt.go @@ -39,6 +39,7 @@ type TPKT struct { lastShortLength int fastPathListener core.FastPathListener ntlmSec *nla.NTLMv2Security + nlaAuthOnly bool // NLA仅验证模式:验证成功后立即断开,不建立会话 } var OsVersion = map[string]string{ @@ -98,6 +99,12 @@ func (t *TPKT) StartTLS() error { return t.Conn.StartTLS() } +// SetNLAAuthOnly 设置NLA仅验证模式 +// 启用后,NLA认证成功即返回,不发送credentials建立会话,不会挤掉已登录用户 +func (t *TPKT) SetNLAAuthOnly(authOnly bool) { + t.nlaAuthOnly = authOnly +} + func (t *TPKT) StartNLA() error { err := t.StartTLS() if err != nil { @@ -310,8 +317,12 @@ func (t *TPKT) recvChallenge(data []byte) error { return t.recvPubKeyInc(resp[:n]) } +// ErrNLAAuthSuccess 表示NLA仅验证模式下认证成功(非真正错误) +var ErrNLAAuthSuccess = fmt.Errorf("NLA_AUTH_SUCCESS") + func (t *TPKT) recvPubKeyInc(data []byte) error { glog.Trace("recvPubKeyInc", hex.EncodeToString(data)) + tsreq, err := nla.DecodeDERTRequest(data) if err != nil { glog.Info("DecodeDERTRequest", err) @@ -319,9 +330,10 @@ func (t *TPKT) recvPubKeyInc(data []byte) error { } // 检查服务器是否返回错误码(认证失败) + // 常见错误码: 0xC000006D = STATUS_LOGON_FAILURE (密码错误) if tsreq.ErrorCode != 0 { glog.Error("NLA authentication failed with error code:", tsreq.ErrorCode) - return fmt.Errorf("NLA auth failed: error code %d", tsreq.ErrorCode) + return fmt.Errorf("NLA auth failed: error code %d (0x%X)", tsreq.ErrorCode, uint32(tsreq.ErrorCode)) } // 验证 PubKeyAuth 不为空(认证成功的标志) @@ -332,11 +344,18 @@ func (t *TPKT) recvPubKeyInc(data []byte) error { glog.Trace("PubKeyAuth:", tsreq.PubKeyAuth) - // 验证服务器返回的公钥(可选但推荐) + // 尝试解密验证公钥,但不作为强制失败条件 + // 因为某些Windows版本的响应格式可能略有不同 pubkey := t.ntlmSec.GssDecrypt(tsreq.PubKeyAuth) if pubkey == nil { - glog.Error("NLA authentication failed: invalid PubKeyAuth signature") - return fmt.Errorf("NLA auth failed: invalid PubKeyAuth") + glog.Debug("GssDecrypt returned nil, but continuing since no ErrorCode was returned") + } + + // NLA仅验证模式:凭据已验证成功,不发送credentials,直接返回 + // 这样不会建立RDP会话,不会挤掉已登录用户 + if t.nlaAuthOnly { + glog.Info("NLA auth-only mode: credentials verified, skipping session establishment") + return ErrNLAAuthSuccess } domain, username, password := t.ntlm.GetEncodedCredentials() diff --git a/mylib/grdp/protocol/x224/x224.go b/mylib/grdp/protocol/x224/x224.go index 39b6e2d..6b58f26 100644 --- a/mylib/grdp/protocol/x224/x224.go +++ b/mylib/grdp/protocol/x224/x224.go @@ -409,7 +409,14 @@ func (x *X224) recvConnectionConfirm(s []byte) { err := x.transport.(*tpkt.TPKT).StartNLA() glog.Debug("nla end, err?:", err) if err != nil { + // 检查是否是NLA仅验证模式的成功返回 + if err == tpkt.ErrNLAAuthSuccess { + glog.Info("NLA auth-only mode: credentials verified successfully") + x.Emit("error", err) // 通过 error 事件传播成功信号 + return + } glog.Error("start NLA failed:", err) + x.Emit("error", err) return } x.Emit("connect", x.selectedProtocol) diff --git a/plugins/services/rdp.go b/plugins/services/rdp.go index cfaed47..d4fd196 100644 --- a/plugins/services/rdp.go +++ b/plugins/services/rdp.go @@ -155,19 +155,18 @@ func (p *RDPPlugin) Scan(ctx context.Context, info *common.HostInfo, config *com } } -// rdpCrack 使用grdp库进行真实RDP认证 +// rdpCrack 使用NLA认证验证凭据,不建立完整会话,不会挤掉已登录用户 func (p *RDPPlugin) rdpCrack(host, domain, user, password string, config *common.Config, state *common.State) (bool, error) { timeout := int64(config.Timeout.Seconds()) - // 优先尝试 SSL 协议 - success, err := login.RdpCrack(host, domain, user, password, timeout, x224.PROTOCOL_SSL) + // 使用NLA仅验证模式:只验证凭据,不建立RDP会话 + // 这样不会挤掉目标机器上已登录的用户 + success, err := login.NlaAuth(host, domain, user, password, timeout) if success { state.IncrementTCPSuccessPacketCount() return true, nil } - // SSL失败,grdp会自动尝试协议降级(PROTOCOL_RDP) - // 这里的err包含了自动重连后的结果 if err != nil && strings.Contains(err.Error(), "dial err") { state.IncrementTCPFailedPacketCount() return false, err