shadow1ng/fscan · error

NLA auth failed: error code %d (0x%X)

Error message

NLA auth failed: error code %d (0x%X)

What it means

recvPubKeyInc inspects the server's TSRequest during NLA authentication; a nonzero ErrorCode means the server rejected the NTLM credentials (e.g. 0xC000006D STATUS_LOGON_FAILURE for a bad password). The library surfaces the raw NTSTATUS code so the caller can distinguish credential rejection from protocol failures.

Source

Thrown at libs/grdp/protocol/tpkt/tpkt.go:336

}

// 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)
		return err
	}

	// 检查服务器是否返回错误码(认证失败)
	// 常见错误码: 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 (0x%X)", tsreq.ErrorCode, uint32(tsreq.ErrorCode))
	}

	// 验证 PubKeyAuth 不为空(认证成功的标志)
	if len(tsreq.PubKeyAuth) == 0 {
		glog.Error("NLA authentication failed: empty PubKeyAuth")
		return fmt.Errorf("NLA auth failed: empty PubKeyAuth")
	}

	glog.Trace("PubKeyAuth:", tsreq.PubKeyAuth)

	// 尝试解密验证公钥,但不作为强制失败条件
	// 因为某些Windows版本的响应格式可能略有不同
	pubkey := t.ntlmSec.GssDecrypt(tsreq.PubKeyAuth)
	if pubkey == nil {
		glog.Debug("GssDecrypt returned nil, but continuing since no ErrorCode was returned")
	}

	// NLA仅验证模式:凭据已验证成功,不发送credentials,直接返回

View on GitHub (pinned to 95cc12e753)

Solutions

  1. Verify username/password/domain by logging in interactively on the target machine
  2. Use DOMAIN\user or user@domain format consistently; try '.\user' for local accounts
  3. Check the account is not locked/expired and has Remote Desktop Users membership
  4. Read the 0x%X code: 0xC000006D/0xC000006A = bad password, 0xC0000072 = disabled account, 0xC0000234 = locked out

Example fix

// before
err := client.Login("10.0.0.5", "admin", "password")
if err != nil { panic(err) }
// after
var authErr *NLAAuthError
if err := client.Login("10.0.0.5", "admin", password); err != nil {
    if errors.As(err, &authErr) && authErr.Code == 0xC000006D {
        // bad credentials: re-prompt instead of retrying
        return rePromptForCredentials()
    }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

if user == "" || pass == "" {
    return errors.New("NLA requires non-empty credentials; supply username and password before calling Login")
}
if !strings.Contains(user, "\\") && !strings.Contains(user, "@") && domain == "" {
    // ambiguous account: will likely fail with STATUS_LOGON_FAILURE
    log.Println("warning: no domain qualifier on username")
}

Type guard

func isLogonFailure(err error) bool {
    var nlaErr *NLAAuthError
    return errors.As(err, &nlaErr) && nlaErr.Code == 0xC000006D
}

Try / catch

err := client.Login(host, user, pass)
var nlaErr *NLAAuthError
if errors.As(err, &nlaErr) {
    switch nlaErr.Code {
    case 0xC000006D, 0xC000006A:
        return rePromptCredentials()
    case 0xC0000072:
        return errors.New("account disabled")
    case 0xC0000234:
        return errors.New("account locked out")
    }
    return err
}

Prevention

When it happens

Trigger: StartNLA with username/password/domain that the server rejects: wrong password, expired/locked account, user lacking RDP rights, or domain mis specification causing the NTLM exchange to fail server-side.

Common situations: Typo'd or rotated passwords; account lockout policies; connecting with a local account when a domain account is required (or vice versa); user not in 'Remote Desktop Users'; stale cached credentials in automation config.

Related errors


AI-assisted analysis of shadow1ng/fscan@95cc12e753 (2026-09-06). Data as JSON: /api/errors/072c4ba9009bd373. Report an issue: GitHub.