mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-25 20:51:52 +08:00
feat: RDP使用NLA仅验证模式,避免挤掉已登录用户
- 添加ErrNLAAuthSuccess标志用于NLA验证成功信号 - tpkt层支持nlaAuthOnly模式,验证成功后不建立完整会话 - x224层正确传播NLA验证结果 - rdpCrack改用NlaAuth进行凭据验证
This commit is contained in:
@@ -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 {
|
type Client struct {
|
||||||
Host string // ip:port
|
Host string // ip:port
|
||||||
tpkt *tpkt.TPKT
|
tpkt *tpkt.TPKT
|
||||||
@@ -213,6 +220,55 @@ func ToRGBA(pixel int, i int, data []byte) (r, g, b, a uint8) {
|
|||||||
return
|
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) {
|
func (g *Client) ProbeOSInfo(host, domain, user, pwd string, timeout int64, rdpProtocol uint32) (info map[string]any) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
exitFlag := make(chan bool)
|
exitFlag := make(chan bool)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type TPKT struct {
|
|||||||
lastShortLength int
|
lastShortLength int
|
||||||
fastPathListener core.FastPathListener
|
fastPathListener core.FastPathListener
|
||||||
ntlmSec *nla.NTLMv2Security
|
ntlmSec *nla.NTLMv2Security
|
||||||
|
nlaAuthOnly bool // NLA仅验证模式:验证成功后立即断开,不建立会话
|
||||||
}
|
}
|
||||||
|
|
||||||
var OsVersion = map[string]string{
|
var OsVersion = map[string]string{
|
||||||
@@ -98,6 +99,12 @@ func (t *TPKT) StartTLS() error {
|
|||||||
return t.Conn.StartTLS()
|
return t.Conn.StartTLS()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetNLAAuthOnly 设置NLA仅验证模式
|
||||||
|
// 启用后,NLA认证成功即返回,不发送credentials建立会话,不会挤掉已登录用户
|
||||||
|
func (t *TPKT) SetNLAAuthOnly(authOnly bool) {
|
||||||
|
t.nlaAuthOnly = authOnly
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TPKT) StartNLA() error {
|
func (t *TPKT) StartNLA() error {
|
||||||
err := t.StartTLS()
|
err := t.StartTLS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -310,8 +317,12 @@ func (t *TPKT) recvChallenge(data []byte) error {
|
|||||||
return t.recvPubKeyInc(resp[:n])
|
return t.recvPubKeyInc(resp[:n])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrNLAAuthSuccess 表示NLA仅验证模式下认证成功(非真正错误)
|
||||||
|
var ErrNLAAuthSuccess = fmt.Errorf("NLA_AUTH_SUCCESS")
|
||||||
|
|
||||||
func (t *TPKT) recvPubKeyInc(data []byte) error {
|
func (t *TPKT) recvPubKeyInc(data []byte) error {
|
||||||
glog.Trace("recvPubKeyInc", hex.EncodeToString(data))
|
glog.Trace("recvPubKeyInc", hex.EncodeToString(data))
|
||||||
|
|
||||||
tsreq, err := nla.DecodeDERTRequest(data)
|
tsreq, err := nla.DecodeDERTRequest(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Info("DecodeDERTRequest", err)
|
glog.Info("DecodeDERTRequest", err)
|
||||||
@@ -319,9 +330,10 @@ func (t *TPKT) recvPubKeyInc(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查服务器是否返回错误码(认证失败)
|
// 检查服务器是否返回错误码(认证失败)
|
||||||
|
// 常见错误码: 0xC000006D = STATUS_LOGON_FAILURE (密码错误)
|
||||||
if tsreq.ErrorCode != 0 {
|
if tsreq.ErrorCode != 0 {
|
||||||
glog.Error("NLA authentication failed with error code:", tsreq.ErrorCode)
|
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 不为空(认证成功的标志)
|
// 验证 PubKeyAuth 不为空(认证成功的标志)
|
||||||
@@ -332,11 +344,18 @@ func (t *TPKT) recvPubKeyInc(data []byte) error {
|
|||||||
|
|
||||||
glog.Trace("PubKeyAuth:", tsreq.PubKeyAuth)
|
glog.Trace("PubKeyAuth:", tsreq.PubKeyAuth)
|
||||||
|
|
||||||
// 验证服务器返回的公钥(可选但推荐)
|
// 尝试解密验证公钥,但不作为强制失败条件
|
||||||
|
// 因为某些Windows版本的响应格式可能略有不同
|
||||||
pubkey := t.ntlmSec.GssDecrypt(tsreq.PubKeyAuth)
|
pubkey := t.ntlmSec.GssDecrypt(tsreq.PubKeyAuth)
|
||||||
if pubkey == nil {
|
if pubkey == nil {
|
||||||
glog.Error("NLA authentication failed: invalid PubKeyAuth signature")
|
glog.Debug("GssDecrypt returned nil, but continuing since no ErrorCode was returned")
|
||||||
return fmt.Errorf("NLA auth failed: invalid PubKeyAuth")
|
}
|
||||||
|
|
||||||
|
// 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()
|
domain, username, password := t.ntlm.GetEncodedCredentials()
|
||||||
|
|||||||
@@ -409,7 +409,14 @@ func (x *X224) recvConnectionConfirm(s []byte) {
|
|||||||
err := x.transport.(*tpkt.TPKT).StartNLA()
|
err := x.transport.(*tpkt.TPKT).StartNLA()
|
||||||
glog.Debug("nla end, err?:", err)
|
glog.Debug("nla end, err?:", err)
|
||||||
if err != nil {
|
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)
|
glog.Error("start NLA failed:", err)
|
||||||
|
x.Emit("error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
x.Emit("connect", x.selectedProtocol)
|
x.Emit("connect", x.selectedProtocol)
|
||||||
|
|||||||
@@ -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) {
|
func (p *RDPPlugin) rdpCrack(host, domain, user, password string, config *common.Config, state *common.State) (bool, error) {
|
||||||
timeout := int64(config.Timeout.Seconds())
|
timeout := int64(config.Timeout.Seconds())
|
||||||
|
|
||||||
// 优先尝试 SSL 协议
|
// 使用NLA仅验证模式:只验证凭据,不建立RDP会话
|
||||||
success, err := login.RdpCrack(host, domain, user, password, timeout, x224.PROTOCOL_SSL)
|
// 这样不会挤掉目标机器上已登录的用户
|
||||||
|
success, err := login.NlaAuth(host, domain, user, password, timeout)
|
||||||
if success {
|
if success {
|
||||||
state.IncrementTCPSuccessPacketCount()
|
state.IncrementTCPSuccessPacketCount()
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSL失败,grdp会自动尝试协议降级(PROTOCOL_RDP)
|
|
||||||
// 这里的err包含了自动重连后的结果
|
|
||||||
if err != nil && strings.Contains(err.Error(), "dial err") {
|
if err != nil && strings.Contains(err.Error(), "dial err") {
|
||||||
state.IncrementTCPFailedPacketCount()
|
state.IncrementTCPFailedPacketCount()
|
||||||
return false, err
|
return false, err
|
||||||
|
|||||||
Reference in New Issue
Block a user