Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions pkg/third_party/smb2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions pkg/third_party/smb2/initiator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
40 changes: 38 additions & 2 deletions pkg/third_party/smb2/internal/ntlm/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions pkg/third_party/smb2/internal/ntlm/ntlm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down