mirror of
https://github.com/mandiant/gopacket
synced 2026-06-21 13:57:02 +00:00
smb2: support true anonymous (null session) NTLM bind
Allow NTLMInitiator with empty User/Password/Hash to establish an SMB anonymous (null) session instead of refusing at dial time. - client.go: drop the dial-time guard that rejected empty-user NTLM initiators outright. - internal/ntlm/client.go: add an anonymous AUTHENTICATE branch per [MS-NLMP] 3.1.5.1.2 -- empty NtChallengeResponse, a single 0x00 byte LmChallengeResponse, NTLMSSP_ANONYMOUS set, no MIC and no session key. DomainName/UserName/Workstation are forced empty and the signing, sealing and key-exchange flags are cleared (leaving KEY_EXCH set with a zero-length EncryptedRandomSessionKey makes strict servers such as Samba reject the bind with STATUS_INVALID_PARAMETER). - initiator.go: nil-guard Sum/SessionKey/infoMap so the keyless anonymous path cannot panic. Verified against Samba: a server permitting null sessions accepts the bind (logged as ANONYMOUS LOGON, S-1-5-7) and lists shares; a server with restrict anonymous=2 fails gracefully via os.ErrPermission; authenticated binds are unchanged.
This commit is contained in:
Vendored
-5
@@ -46,11 +46,6 @@ func (d *Dialer) DialContext(ctx context.Context, tcpConn net.Conn) (*Session, e
|
||||
if d.Initiator == nil {
|
||||
return nil, &InternalError{"Initiator is empty"}
|
||||
}
|
||||
if i, ok := d.Initiator.(*NTLMInitiator); ok {
|
||||
if i.User == "" {
|
||||
return nil, &InternalError{"Anonymous account is not supported yet. Use guest account instead"}
|
||||
}
|
||||
}
|
||||
|
||||
maxCreditBalance := d.MaxCreditBalance
|
||||
if maxCreditBalance == 0 {
|
||||
|
||||
Vendored
+9
@@ -58,15 +58,24 @@ func (i *NTLMInitiator) AcceptSecContext(sc []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (i *NTLMInitiator) Sum(bs []byte) []byte {
|
||||
if i.ntlm == nil || i.ntlm.Session() == nil {
|
||||
return nil
|
||||
}
|
||||
mic, _ := i.ntlm.Session().Sum(bs, i.seqNum)
|
||||
return mic
|
||||
}
|
||||
|
||||
func (i *NTLMInitiator) SessionKey() []byte {
|
||||
if i.ntlm == nil || i.ntlm.Session() == nil {
|
||||
return nil
|
||||
}
|
||||
return i.ntlm.Session().SessionKey()
|
||||
}
|
||||
|
||||
func (i *NTLMInitiator) infoMap() *ntlm.InfoMap {
|
||||
if i.ntlm == nil || i.ntlm.Session() == nil {
|
||||
return nil
|
||||
}
|
||||
return i.ntlm.Session().InfoMap()
|
||||
}
|
||||
|
||||
|
||||
+38
-2
@@ -134,7 +134,17 @@ func (c *Client) Authenticate(cmsg []byte) (amsg []byte, err error) {
|
||||
user := utf16le.EncodeStringToBytes(c.User)
|
||||
workstation := utf16le.EncodeStringToBytes(c.Workstation)
|
||||
|
||||
if domain == nil {
|
||||
// Anonymous (null session) bind: no username, no password, no hash.
|
||||
// Per [MS-NLMP] 3.1.5.1.2 DomainName, UserName and Workstation must all
|
||||
// be empty, so force them empty here (UserName already is) and suppress
|
||||
// the targetName fallback below regardless of any value the caller set.
|
||||
anonymous := c.User == "" && c.Password == "" && c.Hash == nil
|
||||
if anonymous {
|
||||
domain = nil
|
||||
workstation = nil
|
||||
}
|
||||
|
||||
if domain == nil && !anonymous {
|
||||
domain = targetName
|
||||
}
|
||||
|
||||
@@ -179,7 +189,7 @@ func (c *Client) Authenticate(cmsg []byte) (amsg []byte, err error) {
|
||||
off += len
|
||||
}
|
||||
|
||||
if c.User != "" || c.Password != "" || c.Hash != nil {
|
||||
if !anonymous {
|
||||
var err error
|
||||
var h hash.Hash
|
||||
|
||||
@@ -308,6 +318,32 @@ func (c *Client) Authenticate(cmsg []byte) (amsg []byte, err error) {
|
||||
}
|
||||
|
||||
c.session = session
|
||||
} else {
|
||||
// Anonymous authentication [MS-NLMP] 3.1.5.1.2:
|
||||
// NtChallengeResponse: empty
|
||||
// LmChallengeResponse: Z(1) — a single 0x00 byte
|
||||
// NTLMSSP_ANONYMOUS set; no MIC; no session key (null sessions aren't signed).
|
||||
// Clear the signing/sealing/key-exchange flags: we establish no session
|
||||
// key, so leaving KEY_EXCH set with a zero-length EncryptedRandomSessionKey
|
||||
// makes strict servers (e.g. Samba) reject the bind with INVALID_PARAMETER.
|
||||
flags |= NTLMSSP_ANONYMOUS
|
||||
flags &^= NTLMSSP_NEGOTIATE_KEY_EXCH | NTLMSSP_NEGOTIATE_SIGN | NTLMSSP_NEGOTIATE_SEAL | NTLMSSP_NEGOTIATE_ALWAYS_SIGN
|
||||
|
||||
// LmChallengeResponse = single 0x00 byte at the current payload offset.
|
||||
le.PutUint16(amsg[12:14], 1) // LmChallengeResponseLen
|
||||
le.PutUint16(amsg[14:16], 1) // LmChallengeResponseMaxLen
|
||||
le.PutUint32(amsg[16:20], uint32(off)) // LmChallengeResponseBufferOffset
|
||||
amsg[off] = 0
|
||||
off++
|
||||
// NtChallengeResponse + EncryptedRandomSessionKey fields stay zero (empty).
|
||||
|
||||
le.PutUint32(amsg[60:64], flags) // NegotiateFlags (with ANONYMOUS)
|
||||
copy(amsg[64:], version) // Version
|
||||
// No MIC: leave amsg[72:88] zero. c.session stays nil.
|
||||
|
||||
// Trim the buffer (it was sized for a full NTLMv2 response) so no
|
||||
// stray trailing zero bytes ride along in the token.
|
||||
amsg = amsg[:off]
|
||||
}
|
||||
|
||||
return amsg, nil
|
||||
|
||||
+73
@@ -211,6 +211,79 @@ func TestSeal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnonymousAuthenticate(t *testing.T) {
|
||||
// Empty User/Password/Hash => true anonymous (null session) bind.
|
||||
// Set Domain/Workstation to confirm they're stripped from the token
|
||||
// rather than leaked into the payload.
|
||||
c := &Client{
|
||||
Domain: "SHOULD_NOT_APPEAR",
|
||||
Workstation: "SHOULD_NOT_APPEAR",
|
||||
}
|
||||
|
||||
s := NewServer("server")
|
||||
|
||||
nmsg, err := c.Negotiate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmsg, err := s.Challenge(nmsg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
amsg, err := c.Authenticate(cmsg)
|
||||
if err != nil {
|
||||
t.Fatalf("anonymous Authenticate returned error: %v", err)
|
||||
}
|
||||
if amsg == nil {
|
||||
t.Fatal("anonymous Authenticate returned nil message")
|
||||
}
|
||||
|
||||
if le.Uint32(amsg[60:64])&NTLMSSP_ANONYMOUS == 0 {
|
||||
t.Errorf("NTLMSSP_ANONYMOUS flag not set in NegotiateFlags %#x", le.Uint32(amsg[60:64]))
|
||||
}
|
||||
|
||||
if got := le.Uint16(amsg[12:14]); got != 1 {
|
||||
t.Errorf("LmChallengeResponseLen = %d, want 1", got)
|
||||
}
|
||||
|
||||
if got := le.Uint16(amsg[20:22]); got != 0 {
|
||||
t.Errorf("NtChallengeResponseLen = %d, want 0", got)
|
||||
}
|
||||
|
||||
if got := le.Uint16(amsg[28:30]); got != 0 {
|
||||
t.Errorf("DomainNameLen = %d, want 0 (must not leak)", got)
|
||||
}
|
||||
|
||||
if got := le.Uint16(amsg[36:38]); got != 0 {
|
||||
t.Errorf("UserNameLen = %d, want 0", got)
|
||||
}
|
||||
|
||||
if got := le.Uint16(amsg[44:46]); got != 0 {
|
||||
t.Errorf("WorkstationLen = %d, want 0 (must not leak)", got)
|
||||
}
|
||||
|
||||
// LmChallengeResponse must be a single 0x00 byte at its declared offset.
|
||||
lmOff := le.Uint32(amsg[16:20])
|
||||
if int(lmOff) >= len(amsg) {
|
||||
t.Fatalf("LmChallengeResponseBufferOffset %d out of range (len %d)", lmOff, len(amsg))
|
||||
}
|
||||
if amsg[lmOff] != 0 {
|
||||
t.Errorf("LmChallengeResponse byte = %#x, want 0x00", amsg[lmOff])
|
||||
}
|
||||
|
||||
// No session key for a null session.
|
||||
if c.Session() != nil {
|
||||
t.Error("expected nil session for anonymous bind")
|
||||
}
|
||||
|
||||
// No MIC on the anonymous path.
|
||||
if !bytes.Equal(amsg[72:88], make([]byte, 16)) {
|
||||
t.Errorf("expected zero MIC, got %x", amsg[72:88])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientServer(t *testing.T) {
|
||||
c := &Client{
|
||||
User: "user",
|
||||
|
||||
Reference in New Issue
Block a user