Skip to content

Commit

Permalink
Merge pull request #3 from go-ldap/master
Browse files Browse the repository at this point in the history
upgrade code
  • Loading branch information
linanh authored Jan 22, 2021
2 parents 3d5cedb + c9551dc commit 82be508
Show file tree
Hide file tree
Showing 26 changed files with 772 additions and 113 deletions.
7 changes: 3 additions & 4 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jobs:
strategy:
matrix:
go: [
'1.15',
'1.14',
'1.13',
]
Expand Down Expand Up @@ -42,8 +43,6 @@ jobs:
'1.9',
'1.8',
'1.7',
'1.6',
'1.5',
]
name: Go ${{ matrix.go }}.x PR Validate (Legacy)
env:
Expand Down Expand Up @@ -75,8 +74,8 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v1
uses: golangci/golangci-lint-action@v2
with:
version: v1.26
version: v1.33
only-new-issues: true

151 changes: 151 additions & 0 deletions bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"math/rand"
"strings"

"github.com/Azure/go-ntlmssp"
ber "github.com/go-asn1-ber/asn1-ber"
)

Expand Down Expand Up @@ -387,3 +388,153 @@ func (l *Conn) ExternalBind() error {

return GetLDAPError(packet)
}

// NTLMBind performs an NTLMSSP bind leveraging https://github.com/Azure/go-ntlmssp

// NTLMBindRequest represents an NTLMSSP bind operation
type NTLMBindRequest struct {
// Domain is the AD Domain to authenticate too. If not specified, it will be grabbed from the NTLMSSP Challenge
Domain string
// Username is the name of the Directory object that the client wishes to bind as
Username string
// Password is the credentials to bind with
Password string
// Hash is the hex NTLM hash to bind with. Password or hash must be provided
Hash string
// Controls are optional controls to send with the bind request
Controls []Control
}

func (req *NTLMBindRequest) appendTo(envelope *ber.Packet) error {
request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request")
request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version"))
request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name"))

// generate an NTLMSSP Negotiation message for the specified domain (it can be blank)
negMessage, err := ntlmssp.NewNegotiateMessage(req.Domain, "")
if err != nil {
return fmt.Errorf("err creating negmessage: %s", err)
}

// append the generated NTLMSSP message as a TagEnumerated BER value
auth := ber.Encode(ber.ClassContext, ber.TypePrimitive, ber.TagEnumerated, negMessage, "authentication")
request.AppendChild(auth)
envelope.AppendChild(request)
if len(req.Controls) > 0 {
envelope.AppendChild(encodeControls(req.Controls))
}
return nil
}

// NTLMBindResult contains the response from the server
type NTLMBindResult struct {
Controls []Control
}

// NTLMBind performs an NTLMSSP Bind with the given domain, username and password
func (l *Conn) NTLMBind(domain, username, password string) error {
req := &NTLMBindRequest{
Domain: domain,
Username: username,
Password: password,
}
_, err := l.NTLMChallengeBind(req)
return err
}

// NTLMBindWithHash performs an NTLM Bind with an NTLM hash instead of plaintext password (pass-the-hash)
func (l *Conn) NTLMBindWithHash(domain, username, hash string) error {
req := &NTLMBindRequest{
Domain: domain,
Username: username,
Hash: hash,
}
_, err := l.NTLMChallengeBind(req)
return err
}

// NTLMChallengeBind performs the NTLMSSP bind operation defined in the given request
func (l *Conn) NTLMChallengeBind(ntlmBindRequest *NTLMBindRequest) (*NTLMBindResult, error) {
if ntlmBindRequest.Password == "" && ntlmBindRequest.Hash == "" {
return nil, NewError(ErrorEmptyPassword, errors.New("ldap: empty password not allowed by the client"))
}

msgCtx, err := l.doRequest(ntlmBindRequest)
if err != nil {
return nil, err
}
defer l.finishMessage(msgCtx)
packet, err := l.readPacket(msgCtx)
if err != nil {
return nil, err
}
l.Debug.Printf("%d: got response %p", msgCtx.id, packet)
if l.Debug {
if err = addLDAPDescriptions(packet); err != nil {
return nil, err
}
ber.PrintPacket(packet)
}
result := &NTLMBindResult{
Controls: make([]Control, 0),
}
var ntlmsspChallenge []byte

// now find the NTLM Response Message
if len(packet.Children) == 2 {
if len(packet.Children[1].Children) == 3 {
child := packet.Children[1].Children[1]
ntlmsspChallenge = child.ByteValue
// Check to make sure we got the right message. It will always start with NTLMSSP
if !bytes.Equal(ntlmsspChallenge[:7], []byte("NTLMSSP")) {
return result, GetLDAPError(packet)
}
l.Debug.Printf("%d: found ntlmssp challenge", msgCtx.id)
}
}
if ntlmsspChallenge != nil {
var err error
var responseMessage []byte
// generate a response message to the challenge with the given Username/Password if password is provided
if ntlmBindRequest.Password != "" {
responseMessage, err = ntlmssp.ProcessChallenge(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Password)
} else if ntlmBindRequest.Hash != "" {
responseMessage, err = ntlmssp.ProcessChallengeWithHash(ntlmsspChallenge, ntlmBindRequest.Username, ntlmBindRequest.Hash)
} else {
err = fmt.Errorf("need a password or hash to generate reply")
}
if err != nil {
return result, fmt.Errorf("parsing ntlm-challenge: %s", err)
}
packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID"))

request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request")
request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version"))
request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "User Name"))

// append the challenge response message as a TagEmbeddedPDV BER value
auth := ber.Encode(ber.ClassContext, ber.TypePrimitive, ber.TagEmbeddedPDV, responseMessage, "authentication")

request.AppendChild(auth)
packet.AppendChild(request)
msgCtx, err = l.sendMessage(packet)
if err != nil {
return nil, fmt.Errorf("send message: %s", err)
}
defer l.finishMessage(msgCtx)
packetResponse, ok := <-msgCtx.responses
if !ok {
return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed"))
}
packet, err = packetResponse.ReadPacket()
l.Debug.Printf("%d: got response %p", msgCtx.id, packet)
if err != nil {
return nil, fmt.Errorf("read packet: %s", err)
}

}

err = GetLDAPError(packet)
return result, err
}
4 changes: 3 additions & 1 deletion conn.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ldap

import (
"bufio"
"crypto/tls"
"errors"
"fmt"
Expand Down Expand Up @@ -532,12 +533,13 @@ func (l *Conn) reader() {
}
}()

bufConn := bufio.NewReader(l.conn)
for {
if cleanstop {
l.Debug.Printf("reader clean stopping (without closing the connection)")
return
}
packet, err := ber.ReadPacket(l.conn)
packet, err := ber.ReadPacket(bufConn)
if err != nil {
// A read error is expected here if we are closing the connection...
if !l.IsClosing() {
Expand Down
33 changes: 13 additions & 20 deletions control.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,33 +404,26 @@ func DecodeControl(packet *ber.Packet) (Control, error) {
if child.Tag == 0 {
//Warning
warningPacket := child.Children[0]
packet, err := ber.DecodePacketErr(warningPacket.Data.Bytes())
val, err := ber.ParseInt64(warningPacket.Data.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to decode data bytes: %s", err)
}
val, ok := packet.Value.(int64)
if ok {
if warningPacket.Tag == 0 {
//timeBeforeExpiration
c.Expire = val
warningPacket.Value = c.Expire
} else if warningPacket.Tag == 1 {
//graceAuthNsRemaining
c.Grace = val
warningPacket.Value = c.Grace
}
if warningPacket.Tag == 0 {
//timeBeforeExpiration
c.Expire = val
warningPacket.Value = c.Expire
} else if warningPacket.Tag == 1 {
//graceAuthNsRemaining
c.Grace = val
warningPacket.Value = c.Grace
}
} else if child.Tag == 1 {
// Error
packet, err := ber.DecodePacketErr(child.Data.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to decode data bytes: %s", err)
}
val, ok := packet.Value.(int8)
if !ok {
// what to do?
val = -1
bs := child.Data.Bytes()
if len(bs) != 1 || bs[0] > 8 {
return nil, fmt.Errorf("failed to decode data bytes: %s", "invalid PasswordPolicyResponse enum value")
}
val := int8(bs[0])
c.Error = val
child.Value = c.Error
c.ErrorString = BeheraPasswordPolicyErrorMap[c.Error]
Expand Down
53 changes: 53 additions & 0 deletions control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,58 @@ func runAddControlDescriptions(t *testing.T, originalControl Control, childDescr
t.Errorf("%sdescription not as expected: %s != %s", header, encodedPacket.Children[i].Description, desc)
}
}
}

func TestDecodeControl(t *testing.T) {
type args struct {
packet *ber.Packet
}

tests := []struct {
name string
args args
want Control
wantErr bool
}{
{name: "timeBeforeExpiration", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x29, 0x30, 0x27, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0xa, 0x30, 0x8, 0xa0, 0x6, 0x80, 0x4, 0x7f, 0xff, 0xf6, 0x5c})},
want: &ControlBeheraPasswordPolicy{Expire: 2147481180, Grace: -1, Error: -1, ErrorString: ""}, wantErr: false},
{name: "graceAuthNsRemaining", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x26, 0x30, 0x24, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x7, 0x30, 0x5, 0xa0, 0x3, 0x81, 0x1, 0x11})},
want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: 17, Error: -1, ErrorString: ""}, wantErr: false},
{name: "passwordExpired", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x0})},
want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 0, ErrorString: "Password expired"}, wantErr: false},
{name: "accountLocked", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x1})},
want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 1, ErrorString: "Account locked"}, wantErr: false},
{name: "passwordModNotAllowed", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x3})},
want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 3, ErrorString: "Policy prevents password modification"}, wantErr: false},
{name: "mustSupplyOldPassword", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x4})},
want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 4, ErrorString: "Policy requires old password in order to change password"}, wantErr: false},
{name: "insufficientPasswordQuality", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x5})},
want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 5, ErrorString: "Password fails quality checks"}, wantErr: false},
{name: "passwordTooShort", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x6})},
want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 6, ErrorString: "Password is too short for policy"}, wantErr: false},
{name: "passwordTooYoung", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x7})},
want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 7, ErrorString: "Password has been changed too recently"}, wantErr: false},
{name: "passwordInHistory", args: args{packet: ber.DecodePacket([]byte{0xa0, 0x24, 0x30, 0x22, 0x4, 0x19, 0x31, 0x2e, 0x33, 0x2e, 0x36, 0x2e, 0x31, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x34, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x37, 0x2e, 0x38, 0x2e, 0x35, 0x2e, 0x31, 0x4, 0x5, 0x30, 0x3, 0x81, 0x1, 0x8})},
want: &ControlBeheraPasswordPolicy{Expire: -1, Grace: -1, Error: 8, ErrorString: "New password is in list of old passwords"}, wantErr: false},
}
for i := range tests {
err := addControlDescriptions(tests[i].args.packet)
if err != nil {
t.Fatal(err)
}
tests[i].args.packet = tests[i].args.packet.Children[0]
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := DecodeControl(tt.args.packet)
if (err != nil) != tt.wantErr {
t.Errorf("DecodeControl() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("DecodeControl() got = %v, want %v", got, tt.want)
}
})
}
}
2 changes: 1 addition & 1 deletion dn.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func ParseDN(str string) (*DN, error) {
if len(str) > i+1 && str[i+1] == '#' {
i += 2
index := strings.IndexAny(str[i:], ",+")
data := str
var data string
if index > 0 {
data = str[i : i+index]
} else {
Expand Down
31 changes: 24 additions & 7 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ type Error struct {
ResultCode uint16
// MatchedDN is the matchedDN returned if any
MatchedDN string
// Packet is the returned packet if any
Packet *ber.Packet
}

func (e *Error) Error() string {
Expand All @@ -201,28 +203,32 @@ func GetLDAPError(packet *ber.Packet) error {
if len(packet.Children) >= 2 {
response := packet.Children[1]
if response == nil {
return &Error{ResultCode: ErrorUnexpectedResponse, Err: fmt.Errorf("Empty response in packet")}
return &Error{ResultCode: ErrorUnexpectedResponse, Err: fmt.Errorf("Empty response in packet"), Packet: packet}
}
if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 {
resultCode := uint16(response.Children[0].Value.(int64))
if resultCode == 0 { // No error
return nil
}
return &Error{ResultCode: resultCode, MatchedDN: response.Children[1].Value.(string),
Err: fmt.Errorf("%s", response.Children[2].Value.(string))}
return &Error{
ResultCode: resultCode,
MatchedDN: response.Children[1].Value.(string),
Err: fmt.Errorf("%s", response.Children[2].Value.(string)),
Packet: packet,
}
}
}

return &Error{ResultCode: ErrorNetwork, Err: fmt.Errorf("Invalid packet format")}
return &Error{ResultCode: ErrorNetwork, Err: fmt.Errorf("Invalid packet format"), Packet: packet}
}

// NewError creates an LDAP error with the given code and underlying error
func NewError(resultCode uint16, err error) error {
return &Error{ResultCode: resultCode, Err: err}
}

// IsErrorWithCode returns true if the given error is an LDAP error with the given result code
func IsErrorWithCode(err error, desiredResultCode uint16) bool {
// IsErrorAnyOf returns true if the given error is an LDAP error with any one of the given result codes
func IsErrorAnyOf(err error, codes ...uint16) bool {
if err == nil {
return false
}
Expand All @@ -232,5 +238,16 @@ func IsErrorWithCode(err error, desiredResultCode uint16) bool {
return false
}

return serverError.ResultCode == desiredResultCode
for _, code := range codes {
if serverError.ResultCode == code {
return true
}
}

return false
}

// IsErrorWithCode returns true if the given error is an LDAP error with the given result code
func IsErrorWithCode(err error, desiredResultCode uint16) bool {
return IsErrorAnyOf(err, desiredResultCode)
}
Loading

0 comments on commit 82be508

Please sign in to comment.