Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bugfix] Added APOptions []int in client.GSSAPIBindRequest(...) and client.InitSecContext(...), fixes #536 #537

Conversation

p0dalirius
Copy link
Contributor

@p0dalirius p0dalirius commented Nov 14, 2024

Fixing Kerberos authentication due to missing APOption "MutualRequired"

Overview of the fix

In client.InitSecContext(...)

I have changed the client.InitSecContext(...) prototype from:

func (client *Client) InitSecContext(target string, input []byte) ([]byte, bool, error)

to:

func (client *Client) InitSecContext(target string, input []byte, APOptions []int) ([]byte, bool, error)

to be able to pass specific flags through the APOptions []int to the call to spnego.NewKRB5TokenAPREQ(client.Client, tkt, ekey, gssapiFlags, APOptions)

In client.GSSAPIBind(...)

I have left this function as it was, so it can be used in the generic case of GSSAPI authentication without the need to pass specific AP Options flags in parameters.

In client.GSSAPIBindRequest(...)

I have changed the client.GSSAPIBindRequest(...) prototype from:

func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest) error

to

func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest, APOptions []int) error

Example of a working code for Kerberos authentication

package main

import (
	"encoding/hex"
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/go-ldap/ldap/v3"
	"github.com/jcmturner/gokrb5/v8/iana/flags"
	"github.com/go-ldap/ldap/v3/gssapi"
	"github.com/jcmturner/gokrb5/v8/client"
	"github.com/jcmturner/gokrb5/v8/config"
)

func main() {
	fqdnLDAPHost := "SRV-DC01.lab.local"
	baseDN := "DC=LAB,DC=local"

	realm := "lab.local"
	realm = strings.ToUpper(realm)
	// This is always in uppercase, if not we get the error:
	// error performing GSSAPI bind: [Root cause: KRBMessage_Handling_Error]
	// | KRBMessage_Handling_Error: AS Exchange Error: AS_REP is not valid or client password/keytab incorrect
	// |  | KRBMessage_Handling_Error: CRealm in response does not match what was requested.
	// |  |  | Requested: lab.local;
	// |  |  | Reply: lab.local
	// | 2024/10/08 15:36:16 error querying AD: LDAP Result Code 1 "Operations Error": 000004DC: LdapErr: DSID-0C090A5C,
	// | comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v4563

	username := "Administrator"
	// error performing GSSAPI bind: [Root cause: KDC_Error] KDC_Error: AS Exchange Error: kerberos error response from KDC:
	// KRB Error: (6) KDC_ERR_C_PRINCIPAL_UNKNOWN Client not found in Kerberos database
	// KDC_ERR_C_PRINCIPAL_UNKNOWN (error code 6) for these means that the domain controller to which the request
	// was made does not host the account and the client should choose a different domain controller.
	// src: https://learn.microsoft.com/en-us/troubleshoot/windows-server/certificates-and-public-key-infrastructure-pki/kdc-err-c-principal-unknown-s4u2self-request
	// ==> This means this username does not exist

	password := "Admin123!"

	servicePrincipalName := fmt.Sprintf("ldap/%s", fqdnLDAPHost)

	krb5Conf := config.New()
	// LibDefaults
	krb5Conf.LibDefaults.AllowWeakCrypto = true
	krb5Conf.LibDefaults.DefaultRealm = realm
	krb5Conf.LibDefaults.DNSLookupRealm = false
	krb5Conf.LibDefaults.DNSLookupKDC = false
	krb5Conf.LibDefaults.TicketLifetime = time.Duration(24) * time.Hour
	krb5Conf.LibDefaults.RenewLifetime = time.Duration(24*7) * time.Hour
	krb5Conf.LibDefaults.Forwardable = true
	krb5Conf.LibDefaults.Proxiable = true
	krb5Conf.LibDefaults.RDNS = false
	krb5Conf.LibDefaults.UDPPreferenceLimit = 1 // Force use of tcp
	krb5Conf.LibDefaults.DefaultTGSEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "arcfour-hmac-md5"}
	krb5Conf.LibDefaults.DefaultTktEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "arcfour-hmac-md5"}
	krb5Conf.LibDefaults.PermittedEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "arcfour-hmac-md5"}
	krb5Conf.LibDefaults.PermittedEnctypeIDs = []int32{18, 17, 23}
	krb5Conf.LibDefaults.DefaultTGSEnctypeIDs = []int32{18, 17, 23}
	krb5Conf.LibDefaults.DefaultTktEnctypeIDs = []int32{18, 17, 23}
	krb5Conf.LibDefaults.PreferredPreauthTypes = []int{18, 17, 23}

	// Realms
	krb5Conf.Realms = append(krb5Conf.Realms, config.Realm{
		Realm:         realm,
		AdminServer:   []string{fqdnLDAPHost},
		DefaultDomain: realm,
		KDC:           []string{fmt.Sprintf("%s:88", fqdnLDAPHost)},
		KPasswdServer: []string{fmt.Sprintf("%s:464", fqdnLDAPHost)},
		MasterKDC:     []string{fqdnLDAPHost},
	})

	// Domain Realm
	krb5Conf.DomainRealm[strings.ToLower(realm)] = realm
	krb5Conf.DomainRealm[fmt.Sprintf(".%s", strings.ToLower(realm))] = realm

	printKrb5Conf(krb5Conf)

	// Connect to LDAP server
	bindString := fmt.Sprintf("ldaps://%s:636", fqdnLDAPHost)
	ldapConnection, err := ldap.DialURL(
		bindString,
		ldap.DialWithTLSConfig(
			&tls.Config{
				InsecureSkipVerify: true,
			},
		),
	)
	if err != nil {
		log.Printf("[error] ldap.DialURL(\"%s\"): %s\n", bindString, err)
		return
	} else {
		log.Printf("[debug] ldap.DialURL(\"%s\"): success\n", bindString)
	}
	ldapConnection.Debug = true

	// Initialize kerberos client
	// Inspired from: https://github.com/go-ldap/ldap/blob/06d50d1ad03bcd323e48f2fe174d95ceb31b8b90/v3/gssapi/client.go#L51
	kerberosClient := gssapi.Client{
		Client: client.NewWithPassword(
			username,
			realm,
			password,
			krb5Conf,
			// Active Directory does not commonly support FAST negotiationso you will need to disable this on the client.
			// If this is the case you will see this error: KDC did not respond appropriately to FAST negotiation
			// https://github.com/jcmturner/gokrb5/blob/master/USAGE.md#active-directory-kdc-and-fast-negotiation
			client.DisablePAFXFAST(true),
		),
	}
	defer kerberosClient.Close()

	// Initiating ldap GSSAPIBind
	err = ldapConnection.GSSAPIBindRequest(
		&kerberosClient,
		&ldap.GSSAPIBindRequest{
			ServicePrincipalName: servicePrincipalName,
			AuthZID:              "",
		},
		[]int{flags.APOptionMutualRequired},
        )
	if err != nil {
		log.Printf("[error] ldapConnection.GSSAPIBind(): %s\n", err)
		return
	} else {
		log.Printf("[debug] ldapConnection.GSSAPIBind(): success\n")
	}

	// Successfully bound
	searchRequest := ldap.NewSearchRequest(
		baseDN,
		ldap.ScopeWholeSubtree,
		ldap.NeverDerefAliases,
		0,
		0,
		false,
		"(objectClass=user)",
		[]string{"distinguishedName"},
		nil,
	)
	ldapResults, err := ldapConnection.SearchWithPaging(searchRequest, 1000)
	if err != nil {
		log.Fatalf("[error] ldapConnection.Search(): %v\n", err)
		return
	} else {
		log.Printf("[debug] ldapConnection.Search(): success\n")
	}

	for _, entry := range ldapResults.Entries {
		fmt.Printf(" - %s", entry.DN)
	}

	log.Printf("[debug] All done!\n")
}
}

Summary

These APOptions []int can now be set when calling client.GSSAPIBindRequest(...), which will then pass it to the underlying client.InitSecContext(...), which will then be processed in the call to spnego.NewKRB5TokenAPREQ(client.Client, tkt, ekey, gssapiFlags, APOptions)

Best Regards,

@cpuschma
Copy link
Member

Hi @p0dalirius,

thank you very much for your PR and the work you put into debugging this issue (#536)! My strengths are not at all in Kerberos and my knowledge of the protocol is practically zero, so thank you again!

But: Modifying the signature of an existing function will definitely interfere with existing code. Even the addition of changes that are only noticed by linters has caused trouble in the past (#463).

If there is really no other way to implement Kerberos authentication, I would be willing to do so and include it in the next release, but only if no one objects. @go-ldap/committers thoughts?

@p0dalirius
Copy link
Contributor Author

p0dalirius commented Nov 18, 2024

@cpuschma, before merging, It would be nice if someone can try to connect to a UNIX ldap server with GSSAPI to confirm that it still works with this fix

I will try to setup one and test it

@stefanmcshane
Copy link
Contributor

Hi @p0dalirius,

thank you very much for your PR and the work you put into debugging this issue (#536)! My strengths are not at all in Kerberos and my knowledge of the protocol is practically zero, so thank you again!

But: Modifying the signature of an existing function will definitely interfere with existing code. Even the addition of changes that are only noticed by linters has caused trouble in the past (#463).

If there is really no other way to implement Kerberos authentication, I would be willing to do so and include it in the next release, but only if no one objects. @go-ldap/committers thoughts?

Using functional options instead of a slice of ints should not break existing implementations

@stevensbkang
Copy link

Could someone please advise if this will be merged soon? :)

Copy link
Member

@johnweldon johnweldon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the difficulties with this PR is the lack of GSSAPI knowledge by many of the maintainers - if someone with GSSAPI experience could weigh in that would be helpful.

That being said there are some things that should be done differently, and I've tried to point out some of them in this review.

I like the idea of using functional options to avoid future breaking changes, but I'm not sure there's really a need for it in this particular case.

@@ -99,7 +103,7 @@ func (client *Client) DeleteSecContext() error {
// InitSecContext initiates the establishment of a security context for
// GSS-API between the client and server.
// See RFC 4752 section 3.1.
func (client *Client) InitSecContext(target string, input []byte) ([]byte, bool, error) {
func (client *Client) InitSecContext(target string, input []byte, APOptions []int) ([]byte, bool, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not break the API.

I suggest adding a new func (*Client) InitSecContextWithOptions(string, []byte, []int) ([]byte, bool, error) and then modify the internals of the original InitSecContext to simply call the new InitSecContextWithOptions, passing in suitable defaults.

@FlipB
Copy link
Contributor

FlipB commented Dec 20, 2024

The earlier commits would have broken the client in gssapi/sspi.go.
These last changes keep the krb5 specific details in the krb5 client, so that's good.

(I think it makes sense to add asserts that both clients implement the interface to make breaking changes less likely, although you would still have to build for Windows (looks like CI only builds for linux) to be sure.)

@ivan-san
Copy link

Hi! I also have a problem to connect to LDAP with Kerberos from a linux machine. After applying the fix from this pull request, I got another error:

LDAP Result Code 49 "Invalid Credentials": 80090346: LdapErr: DSID-0C0905E6, comment: AcceptSecurityContext error, data 80090346, v3839.

As I understand it is because that go-ldap doesn't support channel binding.
Does anyone know how to fix this?

@rtpt-erikgeiser
Copy link
Contributor

@ivan-san Yes, this is likely due to missing channel bindings. Unfortunately channel bindings cannot easily be implemented in github.com/go-ldap/ldap because it needs to be supported by either the NTLM or the Kerberos library that github.com/go-ldap/ldap uses under the hood. Both of them do not support it at all. However, with a lot of hacks and workarounds I managed to get it to work, and I hope to be able to release the code soon.

However, this error is very likely not related to #537 in any way.

@ivan-san
Copy link

However, with a lot of hacks and workarounds I managed to get it to work, and I hope to be able to release the code soon.

It would be great to see it! Thanks a lot!

@rtpt-erikgeiser
Copy link
Contributor

@ivan-san It is published now: https://github.com/RedTeamPentesting/adauth

@cpuschma
Copy link
Member

I did some testing on Active Directory and OpenLDAP directory servers and couldn't find any problems. @p0dalirius, can you update your branch and take a look at John Weldons comment so we can start merging this? We got rid of the copy of the code outside the v3 directory, so those files can be safely disregarded

Thank you alot for you hard work figuring this problem out!

@p0dalirius p0dalirius force-pushed the fix-ldap-gssapi-kerberos-auth-on-windows-active-directory branch from a55dbd6 to b497d00 Compare February 21, 2025 15:18
Copy link
Contributor Author

@p0dalirius p0dalirius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added GSSAPIClient.InitSecContextWithOptions(...), fixes #536

@p0dalirius
Copy link
Contributor Author

p0dalirius commented Feb 21, 2025

Hi everyone,

I have updated my changes to resolve conflicts and follow @johnweldon's advice

@p0dalirius
Copy link
Contributor Author

p0dalirius commented Feb 21, 2025

Hi everyone,

I have updated my changes to resolve conflicts and follow @johnweldon's advice

To sum up:

GSSAPIClient

The code of the original GSSAPIClient.InitSecContext(...) function was moved to a new function:

func (client *Client) InitSecContextWithOptions(target string, input []byte, APOptions []int) ([]byte, bool, error)

And the original GSSAPIClient.InitSecContext(...) becomes:

func (client *Client) InitSecContext(target string, input []byte) ([]byte, bool, error) {
	return client.InitSecContextWithOptions(target, input, []int{})
}

Conn

The code of the original Conn.GSSAPIBindRequest(...) function was moved to a new function:

func (l *Conn) GSSAPIBindRequestWithAPOptions(client GSSAPIClient, req *GSSAPIBindRequest, APOptions []int) error

And the original Conn.GSSAPIBindRequest(...) becomes:

// GSSAPIBindRequest performs the GSSAPI SASL bind using the provided GSSAPI client.
func (l *Conn) GSSAPIBindRequest(client GSSAPIClient, req *GSSAPIBindRequest) error {
	return l.GSSAPIBindRequestWithAPOptions(client, req, []int{})
}

This prevents breaking changes in the API and introduces a way to use APOptions for Kerberos authentication

Example code with this new functions:

package main

import (
	"encoding/hex"
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/go-ldap/ldap/v3"
	"github.com/jcmturner/gokrb5/v8/iana/flags"
	"github.com/go-ldap/ldap/v3/gssapi"
	"github.com/jcmturner/gokrb5/v8/client"
	"github.com/jcmturner/gokrb5/v8/config"
)

func main() {
	fqdnLDAPHost := "SRV-DC01.lab.local"
	baseDN := "DC=LAB,DC=local"

	realm := "lab.local"
	realm = strings.ToUpper(realm)
	// This is always in uppercase, if not we get the error:
	// error performing GSSAPI bind: [Root cause: KRBMessage_Handling_Error]
	// | KRBMessage_Handling_Error: AS Exchange Error: AS_REP is not valid or client password/keytab incorrect
	// |  | KRBMessage_Handling_Error: CRealm in response does not match what was requested.
	// |  |  | Requested: lab.local;
	// |  |  | Reply: lab.local
	// | 2024/10/08 15:36:16 error querying AD: LDAP Result Code 1 "Operations Error": 000004DC: LdapErr: DSID-0C090A5C,
	// | comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v4563

	username := "Administrator"
	// error performing GSSAPI bind: [Root cause: KDC_Error] KDC_Error: AS Exchange Error: kerberos error response from KDC:
	// KRB Error: (6) KDC_ERR_C_PRINCIPAL_UNKNOWN Client not found in Kerberos database
	// KDC_ERR_C_PRINCIPAL_UNKNOWN (error code 6) for these means that the domain controller to which the request
	// was made does not host the account and the client should choose a different domain controller.
	// src: https://learn.microsoft.com/en-us/troubleshoot/windows-server/certificates-and-public-key-infrastructure-pki/kdc-err-c-principal-unknown-s4u2self-request
	// ==> This means this username does not exist

	password := "Admin123!"

	servicePrincipalName := fmt.Sprintf("ldap/%s", fqdnLDAPHost)

	krb5Conf := config.New()
	// LibDefaults
	krb5Conf.LibDefaults.AllowWeakCrypto = true
	krb5Conf.LibDefaults.DefaultRealm = realm
	krb5Conf.LibDefaults.DNSLookupRealm = false
	krb5Conf.LibDefaults.DNSLookupKDC = false
	krb5Conf.LibDefaults.TicketLifetime = time.Duration(24) * time.Hour
	krb5Conf.LibDefaults.RenewLifetime = time.Duration(24*7) * time.Hour
	krb5Conf.LibDefaults.Forwardable = true
	krb5Conf.LibDefaults.Proxiable = true
	krb5Conf.LibDefaults.RDNS = false
	krb5Conf.LibDefaults.UDPPreferenceLimit = 1 // Force use of tcp
	krb5Conf.LibDefaults.DefaultTGSEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "arcfour-hmac-md5"}
	krb5Conf.LibDefaults.DefaultTktEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "arcfour-hmac-md5"}
	krb5Conf.LibDefaults.PermittedEnctypes = []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "arcfour-hmac-md5"}
	krb5Conf.LibDefaults.PermittedEnctypeIDs = []int32{18, 17, 23}
	krb5Conf.LibDefaults.DefaultTGSEnctypeIDs = []int32{18, 17, 23}
	krb5Conf.LibDefaults.DefaultTktEnctypeIDs = []int32{18, 17, 23}
	krb5Conf.LibDefaults.PreferredPreauthTypes = []int{18, 17, 23}

	// Realms
	krb5Conf.Realms = append(krb5Conf.Realms, config.Realm{
		Realm:         realm,
		AdminServer:   []string{fqdnLDAPHost},
		DefaultDomain: realm,
		KDC:           []string{fmt.Sprintf("%s:88", fqdnLDAPHost)},
		KPasswdServer: []string{fmt.Sprintf("%s:464", fqdnLDAPHost)},
		MasterKDC:     []string{fqdnLDAPHost},
	})

	// Domain Realm
	krb5Conf.DomainRealm[strings.ToLower(realm)] = realm
	krb5Conf.DomainRealm[fmt.Sprintf(".%s", strings.ToLower(realm))] = realm

	printKrb5Conf(krb5Conf)

	// Connect to LDAP server
	bindString := fmt.Sprintf("ldaps://%s:636", fqdnLDAPHost)
	ldapConnection, err := ldap.DialURL(
		bindString,
		ldap.DialWithTLSConfig(
			&tls.Config{
				InsecureSkipVerify: true,
			},
		),
	)
	if err != nil {
		log.Printf("[error] ldap.DialURL(\"%s\"): %s\n", bindString, err)
		return
	} else {
		log.Printf("[debug] ldap.DialURL(\"%s\"): success\n", bindString)
	}
	ldapConnection.Debug = true

	// Initialize kerberos client
	// Inspired from: https://github.com/go-ldap/ldap/blob/06d50d1ad03bcd323e48f2fe174d95ceb31b8b90/v3/gssapi/client.go#L51
	kerberosClient := gssapi.Client{
		Client: client.NewWithPassword(
			username,
			realm,
			password,
			krb5Conf,
			// Active Directory does not commonly support FAST negotiationso you will need to disable this on the client.
			// If this is the case you will see this error: KDC did not respond appropriately to FAST negotiation
			// https://github.com/jcmturner/gokrb5/blob/master/USAGE.md#active-directory-kdc-and-fast-negotiation
			client.DisablePAFXFAST(true),
		),
	}
	defer kerberosClient.Close()

	// Initiating ldap GSSAPIBind
	err = ldapConnection.GSSAPIBindRequestWithAPOptions(
		&kerberosClient,
		&ldap.GSSAPIBindRequest{
			ServicePrincipalName: servicePrincipalName,
			AuthZID:              "",
		},
		[]int{flags.APOptionMutualRequired},
        )
	if err != nil {
		log.Printf("[error] ldapConnection.GSSAPIBind(): %s\n", err)
		return
	} else {
		log.Printf("[debug] ldapConnection.GSSAPIBind(): success\n")
	}

	// Successfully bound
	searchRequest := ldap.NewSearchRequest(
		baseDN,
		ldap.ScopeWholeSubtree,
		ldap.NeverDerefAliases,
		0,
		0,
		false,
		"(objectClass=user)",
		[]string{"distinguishedName"},
		nil,
	)
	ldapResults, err := ldapConnection.SearchWithPaging(searchRequest, 1000)
	if err != nil {
		log.Fatalf("[error] ldapConnection.Search(): %v\n", err)
		return
	} else {
		log.Printf("[debug] ldapConnection.Search(): success\n")
	}

	for _, entry := range ldapResults.Entries {
		fmt.Printf(" - %s", entry.DN)
	}

	log.Printf("[debug] All done!\n")
}
}

@cpuschma I think everything is now in order, do you see anything else to change?

Best regards,

@cpuschma
Copy link
Member

@p0dalirius LGTM! Thank you again for your changes!

@cpuschma cpuschma merged commit b7e97bb into go-ldap:master Feb 21, 2025
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
8 participants