Skip to content

Commit c2c0783

Browse files
authored
Adding NTLMSSP support (#265)
* added ntlm bind * adding doc comments * copy changes to older version
1 parent d49ce32 commit c2c0783

File tree

2 files changed

+262
-0
lines changed

2 files changed

+262
-0
lines changed

bind.go

+131
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"strings"
1212

1313
ber "github.com/go-asn1-ber/asn1-ber"
14+
"github.com/Azure/go-ntlmssp"
15+
1416
)
1517

1618
// SimpleBindRequest represents a username/password bind operation
@@ -387,3 +389,132 @@ func (l *Conn) ExternalBind() error {
387389

388390
return GetLDAPError(packet)
389391
}
392+
393+
// NTLMBind performs an NTLMSSP bind leveraging https://github.com/Azure/go-ntlmssp
394+
395+
// NTLMBindRequest represents an NTLMSSP bind operation
396+
type NTLMBindRequest struct {
397+
// Domain is the AD Domain to authenticate too. If not specified, it will be grabbed from the NTLMSSP Challenge
398+
Domain string
399+
// Username is the name of the Directory object that the client wishes to bind as
400+
Username string
401+
// Password is the credentials to bind with
402+
Password string
403+
// Controls are optional controls to send with the bind request
404+
Controls []Control
405+
}
406+
407+
func (req *NTLMBindRequest) appendTo(envelope *ber.Packet) error {
408+
request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request")
409+
request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version"))
410+
request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name"))
411+
412+
// generate an NTLMSSP Negotiation message for the specified domain (it can be blank)
413+
negMessage, err := ntlmssp.NewNegotiateMessage(req.Domain, "")
414+
if err != nil {
415+
return fmt.Errorf("err creating negmessage: %s", err)
416+
}
417+
418+
// append the generated NTLMSSP message as a TagEnumerated BER value
419+
auth := ber.Encode(ber.ClassContext, ber.TypePrimitive, ber.TagEnumerated, negMessage, "authentication")
420+
request.AppendChild(auth)
421+
envelope.AppendChild(request)
422+
if len(req.Controls) > 0 {
423+
envelope.AppendChild(encodeControls(req.Controls))
424+
}
425+
return nil
426+
}
427+
428+
// NTLMBindResult contains the response from the server
429+
type NTLMBindResult struct {
430+
Controls []Control
431+
}
432+
433+
// NTLMBind performs an NTLMSSP Bind with the given domain, username and password
434+
func (l *Conn) NTLMBind(domain, username, password string) error {
435+
req := &NTLMBindRequest{
436+
Domain: domain,
437+
Username: username,
438+
Password: password,
439+
}
440+
_, err := l.NTLMChallengeBind(req)
441+
return err
442+
}
443+
444+
// NTLMChallengeBind performs the NTLMSSP bind operation defined in the given request
445+
func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindResult, error) {
446+
if ntlmBindRequest.Password == "" {
447+
return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client"))
448+
}
449+
450+
msgCtx, err := l.doRequest(ntlmBindRequest)
451+
if err != nil {
452+
return nil, err
453+
}
454+
defer l.finishMessage(msgCtx)
455+
packet, err := l.readPacket(msgCtx)
456+
if err != nil {
457+
return nil, err
458+
}
459+
l.Debug.Printf("%d: got response %p", msgCtx.id, packet)
460+
if l.Debug {
461+
if err = addLDAPDescriptions(packet); err != nil {
462+
return nil, err
463+
}
464+
ber.PrintPacket(packet)
465+
}
466+
result := &NTLMBindResult{
467+
Controls: make([]Control, 0),
468+
}
469+
var ntlmsspChallenge []byte
470+
471+
// now find the NTLM Response Message
472+
if len(packet.Children) == 2 {
473+
if len(packet.Children[1].Children) == 3 {
474+
child := packet.Children[1].Children[1]
475+
ntlmsspChallenge = child.ByteValue
476+
// Check to make sure we got the right message. It will always start with NTLMSSP
477+
if !bytes.Equal(ntlmsspChallenge[:7], []byte("NTLMSSP")) {
478+
return result, GetLDAPError(packet)
479+
}
480+
l.Debug.Printf("%d: found ntlmssp challenge", msgCtx.id)
481+
}
482+
}
483+
if ntlmsspChallenge != nil {
484+
// generate a response message to the challenge with the given Username/Password
485+
responseMessage, err := ntlmssp.ProcessChallenge(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Password)
486+
if err != nil {
487+
return result, fmt.Errorf("parsing ntlm-challenge: %s", err)
488+
}
489+
packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
490+
packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID"))
491+
492+
request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request")
493+
request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version"))
494+
request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name"))
495+
496+
// append the challenge response message as a TagEmbeddedPDV BER value
497+
auth := ber.Encode(ber.ClassContext, ber.TypePrimitive, ber.TagEmbeddedPDV, responseMessage, "authentication")
498+
499+
request.AppendChild(auth)
500+
packet.AppendChild(request)
501+
msgCtx, err = l.sendMessage(packet)
502+
if err != nil {
503+
return nil, fmt.Errorf("send message: %s", err)
504+
}
505+
defer l.finishMessage(msgCtx)
506+
packetResponse, ok := <-msgCtx.responses
507+
if !ok {
508+
return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed"))
509+
}
510+
packet, err = packetResponse.ReadPacket()
511+
l.Debug.Printf("%d: got response %p", msgCtx.id, packet)
512+
if err != nil {
513+
return nil, fmt.Errorf("read packet: %s", err)
514+
}
515+
516+
}
517+
518+
err = GetLDAPError(packet)
519+
return result, err
520+
}

v3/bind.go

+131
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"strings"
1212

1313
ber "github.com/go-asn1-ber/asn1-ber"
14+
"github.com/Azure/go-ntlmssp"
15+
1416
)
1517

1618
// SimpleBindRequest represents a username/password bind operation
@@ -387,3 +389,132 @@ func (l *Conn) ExternalBind() error {
387389

388390
return GetLDAPError(packet)
389391
}
392+
393+
// NTLMBind performs an NTLMSSP bind leveraging https://github.com/Azure/go-ntlmssp
394+
395+
// NTLMBindRequest represents an NTLMSSP bind operation
396+
type NTLMBindRequest struct {
397+
// Domain is the AD Domain to authenticate too. If not specified, it will be grabbed from the NTLMSSP Challenge
398+
Domain string
399+
// Username is the name of the Directory object that the client wishes to bind as
400+
Username string
401+
// Password is the credentials to bind with
402+
Password string
403+
// Controls are optional controls to send with the bind request
404+
Controls []Control
405+
}
406+
407+
func (req *NTLMBindRequest) appendTo(envelope *ber.Packet) error {
408+
request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request")
409+
request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version"))
410+
request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name"))
411+
412+
// generate an NTLMSSP Negotiation message for the specified domain (it can be blank)
413+
negMessage, err := ntlmssp.NewNegotiateMessage(req.Domain, "")
414+
if err != nil {
415+
return fmt.Errorf("err creating negmessage: %s", err)
416+
}
417+
418+
// append the generated NTLMSSP message as a TagEnumerated BER value
419+
auth := ber.Encode(ber.ClassContext, ber.TypePrimitive, ber.TagEnumerated, negMessage, "authentication")
420+
request.AppendChild(auth)
421+
envelope.AppendChild(request)
422+
if len(req.Controls) > 0 {
423+
envelope.AppendChild(encodeControls(req.Controls))
424+
}
425+
return nil
426+
}
427+
428+
// NTLMBindResult contains the response from the server
429+
type NTLMBindResult struct {
430+
Controls []Control
431+
}
432+
433+
// NTLMBind performs an NTLMSSP Bind with the given domain, username and password
434+
func (l *Conn) NTLMBind(domain, username, password string) error {
435+
req := &NTLMBindRequest{
436+
Domain: domain,
437+
Username: username,
438+
Password: password,
439+
}
440+
_, err := l.NTLMChallengeBind(req)
441+
return err
442+
}
443+
444+
// NTLMChallengeBind performs the NTLMSSP bind operation defined in the given request
445+
func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindResult, error) {
446+
if ntlmBindRequest.Password == "" {
447+
return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client"))
448+
}
449+
450+
msgCtx, err := l.doRequest(ntlmBindRequest)
451+
if err != nil {
452+
return nil, err
453+
}
454+
defer l.finishMessage(msgCtx)
455+
packet, err := l.readPacket(msgCtx)
456+
if err != nil {
457+
return nil, err
458+
}
459+
l.Debug.Printf("%d: got response %p", msgCtx.id, packet)
460+
if l.Debug {
461+
if err = addLDAPDescriptions(packet); err != nil {
462+
return nil, err
463+
}
464+
ber.PrintPacket(packet)
465+
}
466+
result := &NTLMBindResult{
467+
Controls: make([]Control, 0),
468+
}
469+
var ntlmsspChallenge []byte
470+
471+
// now find the NTLM Response Message
472+
if len(packet.Children) == 2 {
473+
if len(packet.Children[1].Children) == 3 {
474+
child := packet.Children[1].Children[1]
475+
ntlmsspChallenge = child.ByteValue
476+
// Check to make sure we got the right message. It will always start with NTLMSSP
477+
if !bytes.Equal(ntlmsspChallenge[:7], []byte("NTLMSSP")) {
478+
return result, GetLDAPError(packet)
479+
}
480+
l.Debug.Printf("%d: found ntlmssp challenge", msgCtx.id)
481+
}
482+
}
483+
if ntlmsspChallenge != nil {
484+
// generate a response message to the challenge with the given Username/Password
485+
responseMessage, err := ntlmssp.ProcessChallenge(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Password)
486+
if err != nil {
487+
return result, fmt.Errorf("parsing ntlm-challenge: %s", err)
488+
}
489+
packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
490+
packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID"))
491+
492+
request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request")
493+
request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version"))
494+
request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name"))
495+
496+
// append the challenge response message as a TagEmbeddedPDV BER value
497+
auth := ber.Encode(ber.ClassContext, ber.TypePrimitive, ber.TagEmbeddedPDV, responseMessage, "authentication")
498+
499+
request.AppendChild(auth)
500+
packet.AppendChild(request)
501+
msgCtx, err = l.sendMessage(packet)
502+
if err != nil {
503+
return nil, fmt.Errorf("send message: %s", err)
504+
}
505+
defer l.finishMessage(msgCtx)
506+
packetResponse, ok := <-msgCtx.responses
507+
if !ok {
508+
return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed"))
509+
}
510+
packet, err = packetResponse.ReadPacket()
511+
l.Debug.Printf("%d: got response %p", msgCtx.id, packet)
512+
if err != nil {
513+
return nil, fmt.Errorf("read packet: %s", err)
514+
}
515+
516+
}
517+
518+
err = GetLDAPError(packet)
519+
return result, err
520+
}

0 commit comments

Comments
 (0)