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

[bug] Could not make a working code example on connecting to LDAP with Kerberos from a linux machine to a Windows Server #536

Open
p0dalirius opened this issue Nov 13, 2024 Discussed in #533 · 13 comments · May be fixed by #537

Comments

@p0dalirius
Copy link

Discussed in https://github.com/orgs/go-ldap/discussions/533

Originally posted by p0dalirius October 11, 2024
Hi,

Has anyone been able to connect to a remote Windows Server LDAP service using Kerberos from a linux machine using this library? From what I understand this should be feasible, but I can't find a working example. I am trying to connect to the domain controller SRV-DC01 of my domain LAB.local running on Windows Server 2019, this is a default fresh installation.

Initially I had a KDC did not respond appropriately to FAST negotiation because I did not use the client.DisablePAFXFAST(true) option in client.NewWithPassword(). Now I pass all the authentications steps up to the SASL bind on LDAP, and I get a LDAP Result Code 49 "Invalid Credentials": 80090308: LdapErr: DSID-0C09058A, comment: AcceptSecurityContext error, data 57, v4563 eventhough my credentials are valid.

This is the example program to connect to LDAP using Kerberos:

package main

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

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

func printKrb5Conf(krb5Conf *config.Config) {
	log.Printf("[debug] kerberos config:\n")
	fmt.Printf("  ├─LibDefaults:\n")
	fmt.Printf("  │  ├─ \x1b[94mAllowWeakCrypto\x1b[0m         : \x1b[93m%t\x1b[0m\n", krb5Conf.LibDefaults.AllowWeakCrypto)
	fmt.Printf("  │  ├─ \x1b[94mCanonicalize\x1b[0m            : \x1b[93m%t\x1b[0m\n", krb5Conf.LibDefaults.Canonicalize)
	fmt.Printf("  │  ├─ \x1b[94mCCacheType\x1b[0m              : \x1b[93m%d\x1b[0m\n", krb5Conf.LibDefaults.CCacheType)
	fmt.Printf("  │  ├─ \x1b[94mClockskew\x1b[0m               : \x1b[93m%s\x1b[0m\n", krb5Conf.LibDefaults.Clockskew)
	fmt.Printf("  │  ├─ \x1b[94mDefaultClientKeytabName\x1b[0m : \x1b[93m%s\x1b[0m\n", krb5Conf.LibDefaults.DefaultClientKeytabName)
	fmt.Printf("  │  ├─ \x1b[94mDefaultKeytabName\x1b[0m       : \x1b[93m%s\x1b[0m\n", krb5Conf.LibDefaults.DefaultKeytabName)
	fmt.Printf("  │  ├─ \x1b[94mDefaultRealm\x1b[0m            : \x1b[93m%s\x1b[0m\n", krb5Conf.LibDefaults.DefaultRealm)
	fmt.Printf("  │  ├─ \x1b[94mDefaultTGSEnctypes\x1b[0m      : \x1b[93m%v\x1b[0m\n", krb5Conf.LibDefaults.DefaultTGSEnctypes)
	fmt.Printf("  │  ├─ \x1b[94mDefaultTktEnctypes\x1b[0m      : \x1b[93m%v\x1b[0m\n", krb5Conf.LibDefaults.DefaultTktEnctypes)
	fmt.Printf("  │  ├─ \x1b[94mDefaultTGSEnctypeIDs\x1b[0m    : \x1b[93m%v\x1b[0m\n", krb5Conf.LibDefaults.DefaultTGSEnctypeIDs)
	fmt.Printf("  │  ├─ \x1b[94mDefaultTktEnctypeIDs\x1b[0m    : \x1b[93m%v\x1b[0m\n", krb5Conf.LibDefaults.DefaultTktEnctypeIDs)
	fmt.Printf("  │  ├─ \x1b[94mDNSCanonicalizeHostname\x1b[0m : \x1b[93m%t\x1b[0m\n", krb5Conf.LibDefaults.DNSCanonicalizeHostname)
	fmt.Printf("  │  ├─ \x1b[94mDNSLookupKDC\x1b[0m            : \x1b[93m%t\x1b[0m\n", krb5Conf.LibDefaults.DNSLookupKDC)
	fmt.Printf("  │  ├─ \x1b[94mDNSLookupRealm\x1b[0m          : \x1b[93m%t\x1b[0m\n", krb5Conf.LibDefaults.DNSLookupRealm)
	fmt.Printf("  │  ├─ \x1b[94mForwardable\x1b[0m             : \x1b[93m%t\x1b[0m\n", krb5Conf.LibDefaults.Forwardable)
	fmt.Printf("  │  ├─ \x1b[94mK5LoginDirectory\x1b[0m        : \x1b[93m%s\x1b[0m\n", krb5Conf.LibDefaults.K5LoginDirectory)
	fmt.Printf("  │  ├─ \x1b[94mKDCDefaultOptions\x1b[0m       : \x1b[93m0x%08x\x1b[0m\n", krb5Conf.LibDefaults.KDCDefaultOptions.Bytes)
	fmt.Printf("  │  ├─ \x1b[94mKDCTimeSync\x1b[0m             : \x1b[93m%d\x1b[0m\n", krb5Conf.LibDefaults.KDCTimeSync)
	fmt.Printf("  │  ├─ \x1b[94mNoAddresses\x1b[0m             : \x1b[93m%t\x1b[0m\n", krb5Conf.LibDefaults.NoAddresses)
	fmt.Printf("  │  ├─ \x1b[94mPermittedEnctypes\x1b[0m       : \x1b[93m%v\x1b[0m\n", krb5Conf.LibDefaults.PermittedEnctypes)
	fmt.Printf("  │  ├─ \x1b[94mProxiable\x1b[0m               : \x1b[93m%t\x1b[0m\n", krb5Conf.LibDefaults.Proxiable)
	fmt.Printf("  │  ├─ \x1b[94mTicketLifetime\x1b[0m          : \x1b[93m%s\x1b[0m\n", krb5Conf.LibDefaults.TicketLifetime)
	fmt.Printf("  │  ├─ \x1b[94mUDPPreferenceLimit\x1b[0m      : \x1b[93m%d\x1b[0m\n", krb5Conf.LibDefaults.UDPPreferenceLimit)
	fmt.Printf("  │  ├─ \x1b[94mVerifyAPReqNofail\x1b[0m       : \x1b[93m%t\x1b[0m\n", krb5Conf.LibDefaults.VerifyAPReqNofail)
	fmt.Printf("  │  └────\n")

	fmt.Printf("  ├─Realms:\n")
	for _, realm := range krb5Conf.Realms {
		fmt.Printf("  │  │  <Realm '%s'>\n", realm.Realm)
		fmt.Printf("  │  │  ├─ \x1b[94mRealm\x1b[0m         : \x1b[93m%s\x1b[0m\n", realm.Realm)
		fmt.Printf("  │  │  ├─ \x1b[94mAdminServer\x1b[0m   : \x1b[93m%v\x1b[0m\n", realm.AdminServer)
		fmt.Printf("  │  │  ├─ \x1b[94mDefaultDomain\x1b[0m : \x1b[93m%s\x1b[0m\n", realm.DefaultDomain)
		fmt.Printf("  │  │  ├─ \x1b[94mKDC\x1b[0m           : \x1b[93m%v\x1b[0m\n", realm.KDC)
		fmt.Printf("  │  │  ├─ \x1b[94mKPasswdServer\x1b[0m : \x1b[93m%v\x1b[0m\n", realm.KPasswdServer)
		fmt.Printf("  │  │  ├─ \x1b[94mMasterKDC\x1b[0m     : \x1b[93m%v\x1b[0m\n", realm.MasterKDC)
		fmt.Printf("  │  │  └────\n")
	}
	fmt.Printf("  │  └────\n")

	fmt.Printf("  ├─DomainRealm:\n")
	for key, value := range krb5Conf.DomainRealm {
		fmt.Printf("  │  ├─ \x1b[94m%s\x1b[0m : \x1b[93m%s\x1b[0m\n", key, value)
	}
	fmt.Printf("  │  └────\n")

	fmt.Printf("  └────\n")
}

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("ldap://%s:389", 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()

	// Retrieving serviceTicket, encryptionKey to print them
	// serviceTicket, encryptionKey, err := kerberosClient.GetServiceTicket(servicePrincipalName)
	// if err != nil {
	// 	log.Printf("[error] kerberosClient.GetServiceTicket(): %s\n", err)
	// 	return
	// } else {
	// 	log.Printf("[debug] kerberosClient.GetServiceTicket(): success\n")
	// }
	// log.Printf("[debug] encryptionKey: %s\n", hex.EncodeToString(encryptionKey.KeyValue))
	// marshalledServiceTicket, err := serviceTicket.Marshal()
	// if err != nil {
	// 	log.Printf("[error] serviceTicket.Marshal(): %s\n", err)
	// 	return
	// }
	// log.Printf("[debug] serviceTicket: %s\n", hex.EncodeToString(marshalledServiceTicket))

	// Initiating ldap GSSAPIBind
	err = ldapConnection.GSSAPIBind(&kerberosClient, servicePrincipalName, "")
	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")
}
}

At this point I am stuck with a LDAP Result Code 49 "Invalid Credentials": 80090308: LdapErr: DSID-0C09058A, comment: AcceptSecurityContext error, data 57, v4563 error, eventhough my credentials are valid:

2024/10/13 12:40:11 [debug] kerberos config:
  ├─LibDefaults:
  │  ├─ AllowWeakCrypto         : true
  │  ├─ Canonicalize            : false
  │  ├─ CCacheType              : 4
  │  ├─ Clockskew               : 5m0s
  │  ├─ DefaultClientKeytabName : /usr/local/var/krb5/user/1000/client.keytab
  │  ├─ DefaultKeytabName       : /etc/krb5.keytab
  │  ├─ DefaultRealm            : LAB.LOCAL
  │  ├─ DefaultTGSEnctypes      : [aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 arcfour-hmac-md5]
  │  ├─ DefaultTktEnctypes      : [aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 arcfour-hmac-md5]
  │  ├─ DefaultTGSEnctypeIDs    : [18 17 23]
  │  ├─ DefaultTktEnctypeIDs    : [18 17 23]
  │  ├─ DNSCanonicalizeHostname : true
  │  ├─ DNSLookupKDC            : false
  │  ├─ DNSLookupRealm          : false
  │  ├─ Forwardable             : true
  │  ├─ K5LoginDirectory        : /home/podalirius
  │  ├─ KDCDefaultOptions       : 0x00000010
  │  ├─ KDCTimeSync             : 1
  │  ├─ NoAddresses             : true
  │  ├─ PermittedEnctypes       : [aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 arcfour-hmac-md5]
  │  ├─ Proxiable               : true
  │  ├─ TicketLifetime          : 24h0m0s
  │  ├─ UDPPreferenceLimit      : 1
  │  ├─ VerifyAPReqNofail       : false
  │  └────
  ├─Realms:
  │  │  <Realm 'LAB.LOCAL'>
  │  │  ├─ Realm         : LAB.LOCAL
  │  │  ├─ AdminServer   : [SRV-DC01.lab.local]
  │  │  ├─ DefaultDomain : LAB.LOCAL
  │  │  ├─ KDC           : [SRV-DC01.lab.local:88]
  │  │  ├─ KPasswdServer : [SRV-DC01.lab.local:464]
  │  │  ├─ MasterKDC     : [SRV-DC01.lab.local]
  │  │  └────
  │  └────
  ├─DomainRealm:
  │  ├─ .lab.local : LAB.LOCAL
  │  ├─ lab.local : LAB.LOCAL
  │  └────
  └────
2024/10/13 12:40:11 [debug] ldap.DialURL("ldap://SRV-DC01.lab.local:389"): success
2024/10/13 12:40:11 [debug] kerberosClient.GetServiceTicket(): success
2024/10/13 12:40:11 [debug] encryptionKey: 45a2ee28527458de223412309a030d75bf9e7031a43e80c27b687bbb5dd7ff45
2024/10/13 12:40:11 [debug] serviceTicket: 618204f2308204eea003020105a10b1b094c41422e4c4f43414ca2253023a003020101a11c301a1b046c6461701b125352562d444330312e6c61622e6c6f63616ca38204b1308204ada003020112a103020108a282049f0482049b05292391aad5c6980738ad2a1f4f0faa130d8f1147620c14c443c5be0a5fea0d81bab9efb974a475e049e66932e8c414c64012a0f30aeceb9c5366b885ef9d75a5cb3e3bffa5220f10902592533a110625cf2fe300e6e767528a743c10df2300837d3122e45b16f9a2fc62906222ef0a355d169aabb8099abd53f84298456893578513dd216c2bed2aeee1d4845c76d877308b1b50ee45edbf8cd404e8180430812f146eca2e19c20ba4f2ec26b5c53ccd2ec516f994eea55355fb8c4c6b1279ecafa2ebf7f46f023776a2cd2fea5d8fa73ee7dcbede0764e5ab498584a9ef1f1163f998b9d442c55c321dc6143de1c921cb1b5cd407ff4160fe64a41932434a57b14aac6c621e0345a7c3d2f2a1195f17176191575a1cb66a122bc1a75fa9ac70c32bb55d06447a56d6924fef1145fcce89dc5e4c4ce8530aff17c12d233ca9386bbe298f3333ab12c9461b1de3f2032864912094d563fdac47a83b1006c048eba63b696636c7b181ca0174da73ffc9d69edf026151792a2262be4af502407f55a21fb0fd033602404ce3547e9032de57a18e257ce89518e727ecb9bf2739d0c2f8cab0799041504139d77975726b9d2f18a2065b0f93a5ebfb66744eff81561470856f18208aab1c9385924047f1de28fb1bbc7d4b9ed03638d2c5c75539f7ca8b0d66128d3fecadd578fa568d68a7afed39e2a6996967f928cae7d0040ba5718f21e0d6c3f449082f0f1c5f076ca148e1ed8aa32556c58718ded6b95ce97916fb2071e125b8fb06bfc6222156bd6a8be8f861f2c482701285ccb916ba644b8d8e5957d070fe2a7cd5709073e2131ba7cf8412621d05936dc751f700c1d5646389f81f3391e2677447e233528f6f9ba27b8c36309535c8287b0b70d014e49afb5d374c7f9576db40933113cf31f7d946e15f5457e4eeecb60babcdae6705ff9ec9d555cfc770f400da30a25342b87a752f859cbf0e0d73ed1c68fe0507c63913cbf5c9ff58d86c62bec590f281c85d1cbd6d72a75c8b582ac28c5c6169d1b931f4ab361d6314ac4eb85cbf7beba49e9601f639315f60afc7fa593fefb0044ad533553390135637bd465cbf63e438ad0508debd7fc1eb9c9093c1b8de2653568eae5bc909e8905c57fc3d5a5956b612d318ad28ff128529186868e526c8852fc46ea24979d6b1b589757ac22c343b8b782450ed937312a68dbad8ee64b459f54162865f455dc1b28a5f468a1e1adf22f5e6b57429f69ebac7210823df2ddb951aecead64cedb7e47b15a4d09620eda5a51dd73dc34ab99154d3449bef40bf6a2873f87c4c8307301553127e2a7564fb3361ecb4ad4907c45ddd4ed0e459fbdc1874bb8705929924df7346c82abc9df3758f02e21e98df8f8dc336cf5591351be0fec7e1667646ee22846bd126c04291be113a57758a1e7c00b7abf702dab81d9a901a1992a081bdda0bef8c91ddd7dd81ab951a2bd1427801e6400aba5144311c3853f1ba8168a18c855221a6e3898ab54e6ab2a1fbea9f2d8083e65a4231bc7527c0eaf342d57fef183d9ca7189883afaf3d57e09324fde1c1dcea5f4106c6f2ffaf0a23191e8889316a91fafd225b43c96473e2dae5263c6b61e42b64b6abbde0258d0870a32dfe50b6b3ceb027ce4386c2c4520bfafab4c1aa
2024/10/13 12:40:11 flags&startTLS = 0
2024/10/13 12:40:11 1: waiting for response
2024/10/13 12:40:11 Sending message 1
2024/10/13 12:40:11 Receiving message 1
2024/10/13 12:40:11 1: got response 0xc0001622a0
LDAP Response: (Universal, Constructed, Sequence and Sequence of) Len=99 "<nil>"
 Message ID: (Universal, Primitive, Integer) Len=1 "1"
 Bind Response: (Application, Constructed, 0x01) Len=94 "<nil>"
  Result Code (Invalid Credentials): (Universal, Primitive, Enumerated) Len=1 "49"
  Matched DN (): (Universal, Primitive, Octet String) Len=0 ""
  Error Message: (Universal, Primitive, Octet String) Len=87 "80090308: LdapErr: DSID-0C09058A, comment: AcceptSecurityContext error, data 57, v4563\x00"
2024/10/13 12:40:11 1: got response 0xc0001622a0
LDAP Response: (Universal, Constructed, Sequence and Sequence of) Len=99 "<nil>"
 Message ID: (Universal, Primitive, Integer) Len=1 "1"
 Bind Response: (Application, Constructed, 0x01) Len=94 "<nil>"
  Result Code (Invalid Credentials): (Universal, Primitive, Enumerated) Len=1 "49"
  Matched DN (): (Universal, Primitive, Octet String) Len=0 ""
  Error Message: (Universal, Primitive, Octet String) Len=87 "80090308: LdapErr: DSID-0C09058A, comment: AcceptSecurityContext error, data 57, v4563\x00"
2024/10/13 12:40:11 [error] ldapConnection.GSSAPIBind(): LDAP Result Code 49 "Invalid Credentials": 80090308: LdapErr: DSID-0C09058A, comment: AcceptSecurityContext error, data 57, v4563

I have a running Wireshark and I got the following packets:

image

My latest TGS-REP packet (number 27) is the following:

Kerberos
    Record Mark: 1607 bytes
        0... .... .... .... .... .... .... .... = Reserved: Not set
        .000 0000 0000 0000 0000 0110 0100 0111 = Record Length: 1607
    tgs-rep
        pvno: 5
        msg-type: krb-tgs-rep (13)
        crealm: LAB.LOCAL
        cname
            name-type: kRB5-NT-PRINCIPAL (1)
            cname-string: 1 item
                CNameString: Administrator
        ticket
            tkt-vno: 5
            realm: LAB.LOCAL
            sname
                name-type: kRB5-NT-PRINCIPAL (1)
                sname-string: 2 items
                    SNameString: ldap
                    SNameString: SRV-DC01.lab.local
            enc-part
                etype: eTYPE-AES256-CTS-HMAC-SHA1-96 (18)
                kvno: 8
                cipher [truncated]: da6a036441feb57af40b98f06d051d7b903167536f6d358da9a7f437f185fd9abc8db12aa82c29101411fc1c35e132a2ef8ff125f25e7fb3aef307d20ab5a1ff83285e783d33d30a9d7287c89903c2015748f1ae5f57b3550b171e5607e5571ffaa3811676c0164b0929b0c1378
        enc-part
            etype: eTYPE-AES256-CTS-HMAC-SHA1-96 (18)
            cipher [truncated]: d9cca9bdd213e0f6ef8cf90f436ee435d98d08b1ad36c4a80fcc33603073411607041cc4d88a30b938ec508c961e6ec346771bc3a13f0a9d77f2bfcf562b545eef12d14e697369a8c7208f16c25b0980146d51cc448cfd6eb35e597f6dc7460cd53f6efc04ac68a6f77af883811

And when binding using SASL bindRequest(1) "<ROOT>" sasl (pkt number 32) I have:

Lightweight Directory Access Protocol
    LDAPMessage bindRequest(1) "<ROOT>" sasl
        messageID: 1
        protocolOp: bindRequest (0)
            bindRequest
                version: 3
                name: 
                authentication: sasl (3)
                    sasl
                        mechanism: GSSAPI
                        credentials [truncated]: 608205cf06092a864886f71201020201006e8205be308205baa003020105a10302010ea20703050000000000a38204f6618204f2308204eea003020105a10b1b094c41422e4c4f43414ca2253023a003020101a11c301a1b046c6461701b125352562d444330312e6c6162
                        GSS-API Generic Security Service Application Program Interface
                            OID: 1.2.840.113554.1.2.2 (KRB5 - Kerberos 5)
                            krb5_blob [truncated]: 01006e8205be308205baa003020105a10302010ea20703050000000000a38204f6618204f2308204eea003020105a10b1b094c41422e4c4f43414ca2253023a003020101a11c301a1b046c6461701b125352562d444330312e6c61622e6c6f63616ca38204b1308204ada003
                                krb5_tok_id: KRB5_AP_REQ (0x0001)
                                Kerberos
                                    ap-req
                                        pvno: 5
                                        msg-type: krb-ap-req (14)
                                        Padding: 0
                                        ap-options: 00000000
                                            0... .... = reserved: False
                                            .0.. .... = use-session-key: False
                                            ..0. .... = mutual-required: False
                                        ticket
                                            tkt-vno: 5
                                            realm: LAB.LOCAL
                                            sname
                                                name-type: kRB5-NT-PRINCIPAL (1)
                                                sname-string: 2 items
                                                    SNameString: ldap
                                                    SNameString: SRV-DC01.lab.local
                                            enc-part
                                                etype: eTYPE-AES256-CTS-HMAC-SHA1-96 (18)
                                                kvno: 8
                                                cipher [truncated]: da6a036441feb57af40b98f06d051d7b903167536f6d358da9a7f437f185fd9abc8db12aa82c29101411fc1c35e132a2ef8ff125f25e7fb3aef307d20ab5a1ff83285e783d33d30a9d7287c89903c2015748f1ae5f57b3550b171e5607e5571ffaa3811676c0164b0929b0c1378
                                        authenticator
                                            etype: eTYPE-AES256-CTS-HMAC-SHA1-96 (18)
                                            kvno: 8
                                            cipher [truncated]: e90bd108abe5d7545d134a0390a1376253c87398750ef295ed1064a9e87ed571201c77c90c3672b0a19d73da62149c98ca764a7cf2d2afedab37dfe0e39ca706192d1578a0b59d517fe22e4c31ec094af484cb7af54308a13267aefbde7be441dd8dfd19b4e57a3ead015d8c90e
        [Response In: 34]

And I get a bindResponse invalidCredentials (pkt number 34):

Lightweight Directory Access Protocol
    LDAPMessage bindResponse(1) invalidCredentials (80090308: LdapErr: DSID-0C09058A, comment: AcceptSecurityContext error, data 57, v4563)
        messageID: 1
        protocolOp: bindResponse (1)
            bindResponse
                resultCode: invalidCredentials (49)
                matchedDN: 
                errorMessage: 80090308: LdapErr: DSID-0C09058A, comment: AcceptSecurityContext error, data 57, v4563
        [Response To: 32]
        [Time: 0.000343459 seconds]

The credentials used are valid on the domain (I can login, and furthermore the initial Kerberos authentication do work until the ldapConnection.GSSAPIBind() call)

If anyone have a working example or can tell me what goes wrong here I'd love that!

Thank you in advance for your help!

Best regards,

@p0dalirius
Copy link
Author

p0dalirius commented Nov 13, 2024

TLDR; On a freshly installed Windows Server 2019, I cannot connect using Kerberos with go-ldap, always get Invalid Credentials (49) eventhough my credentials are valid on RDP or on LDAP with NTLM, and Kerberos works with other tools.

@VITEK-THE-BEST
Copy link

I am experiencing the same issue. The following code works fine:

conn, _ = ldap.DialURL("ldap://ldap.example.com")

if err := conn.Bind(cfg.User+"@"+cfg.Domain.Name, cfg.Password); err != nil {
    return err
}

However, when I try to use GSSAPI, I get an error:

client, _ := gssapi.NewClientFromCCache(
	"/tmp/krb5cc_0",
	"/etc/krb5.conf",
	client.DisablePAFXFAST(true),
)
// OR 
client, _ := gssapi.NewClientWithPassword(
	"admin",
	"MY.REALM",
	"PASSWORD",
	"/etc/krb5.conf",
	client.DisablePAFXFAST(true),
	client.AssumePreAuthentication(false),
)

if err := client.Login(); err != nil {
	return err
}
conn, _ = ldap.DialURL("ldap://ldap.example.com")

err = conn.GSSAPIBind(client, "ldap/"+cfg.Service, "")
if err != nil {
	return err
}

This results in the following error:

LDAP Result Code 49 "Invalid Credentials": 80090308: LdapErr: DSID-0C090516, comment: AcceptSecurityContext error, data 57, v3839

I am running the code inside a container. In the container, I install this:

RUN apt-get update && apt-get install -y \
    bash \
    dnsutils \
    samba \
    ldap-utils \
    iputils-ping \
    krb5-user \
    ntp \
    libsasl2-modules-gssapi-mit

Additionally, I tested the connection using the following command and it worked as expected:

ldapwhoami -Y GSSAPI -H ldap://ldap.example.com

SASL/GSSAPI authentication started
SASL username: [email protected]
SASL SSF: 64
SASL data security layer installed.
u:MY\admin

@rewindrepeat
Copy link

I had the exact same error as @p0dalirius .

I managed to get it working by following @jdef's advice in #340 , namely adding []int{flags.APOptionMutualRequired} as last parameter to spnego.NewKRB5TokenAPREQ and switching the order of checksum and payload when unmarshaling the wraptoken.

You can find a working example in this gist

@p0dalirius
Copy link
Author

p0dalirius commented Nov 13, 2024

Hey @rewindrepeat,

Thank you for your advice, I used your patched gssapi/client.go file into my proof of concept project and it worked perfectly THANK YOU 🙏 .

image

For anyone interested I provide the full proof of concept code of this issue here: poc_ldap_gssapi_patch.tar.gz

Root Cause

As stated by @jdef and @rewindrepeat, the problem can be fixed by adding the APOptionMutualRequired flag in the call to spnego.NewKRB5TokenAPREQ() in v3/gssapi/client.go#L113:

token, err := spnego.NewKRB5TokenAPREQ(client.Client, tkt, ekey, gssapiFlags, []int{})

The fix should be:

         token, err := spnego.NewKRB5TokenAPREQ(client.Client, tkt, ekey, gssapiFlags, []int{flags.APOptionMutualRequired})

More details on this in the section-5.5.1 of RFC 4120 (https://datatracker.ietf.org/doc/html/rfc4120#section-5.5.1) ):

mutual-required: The MUTUAL-REQUIRED option tells the server that the client requires mutual authentication, and that it must respond with a KRB_AP_REP message.

==> Which is exactly what we want here, for a Kerberos authentication

Potential Fix

  • Would it make side effects on other features if we added flags.APOptionMutualRequired to the call to spnego.NewKRB5TokenAPREQ() in v3/gssapi/client.go#L113?

  • If so, it would be useful to be able to set these AP Options through other functions or in parameters of client.InitSecContext(), because in some cases we could need to add other options from the RFC as well.

Best regards,

@rewindrepeat
Copy link

Glad the patch is working for you @p0dalirius

A potential fix should also include a modified version of gokrb5's Unsmarshal function for WrapToken, included in the patched example client as func UnmarshalWrapToken (accompanied by required func getGssWrapTokenId)

Without the modified unmarshal token.Verify() results in a checksum mismatch error, as mentioned here

@VITEK-THE-BEST
Copy link

Hi @rewindrepeat thank you very much! Everything works for me too!

p0dalirius added a commit to p0dalirius/ldap that referenced this issue Nov 14, 2024
p0dalirius added a commit to p0dalirius/ldap that referenced this issue Nov 14, 2024
p0dalirius pushed a commit to p0dalirius/ldap that referenced this issue Nov 14, 2024
p0dalirius added a commit to p0dalirius/ldap that referenced this issue Nov 14, 2024
@p0dalirius
Copy link
Author

p0dalirius commented Nov 14, 2024

Hey @rewindrepeat, @VITEK-THE-BEST,

I have proposed a fix for the Kerberos authentication problem by adding an option to pass the APOptions list of flags when creating a client.GSSAPIBindRequest(...)

What do you think about this?

Best regards,

@rewindrepeat
Copy link

Hey @p0dalirius,

I think passing the APOptions flags as a parameter is the way to go, this looks good to me.

But hard wiring the UnmarshalWrapToken func will most likely break existing code. According to RFC 4121, the implementation in gokrb5 should be correct and it seems like it's only ActiveDirectory that's implementing the wrap token differently? (I assume the current implementation of GSSAPI/SASL in go-ldap has been working with other LDAP servers and the problem only occurs with ActiveDirectory.)

Maybe one could pass another parameter to bind.GSSAPIBindRequest, which can then be passed to client.NegotiateSaslAuth to determine if the standard unmarshal function from gokrb5 should be used or the modfied version (which could be included in the client code, as in your PR).

I don't know, maybe the maintainers could chip in with how they would like to have this implemented?

@olde-ducke
Copy link

I may be reading it wrong, but RFC 4121 also mentions that Wrap token (excluding the header) may be rotated to the right by RRC octets.

A few months ago, when dealing with the same problem, I followed @jdef's advice from #340 and manually swapped checksum and payload, which allowed me to successfully connect to Active Directory, but broke FreeIPA token verification instead. Then I noticed that Active Directory responds with RRC set to 12, unlike FreeIPA (which responds with 0), I checked the RFC 4121 and gokrb5 sources and did not find anything described in section 4.2.5 in the unmarshal method.

I ended up replacing unmarshal method with a function that rotates token back RRC octets (12 actually ended up being exactly the number of rotations needed to swap payload and checksum places) before writing checksum and payload to the struct, which worked for both Active Directory and FreeIPA, at least in my case.

@jdef
Copy link

jdef commented Nov 21, 2024 via email

@rewindrepeat
Copy link

rewindrepeat commented Nov 21, 2024

Thanks @olde-ducke , that's the missing puzzle piece 😃

Do you still have the code of your unmarshal method and could share it?

Adapting this answer from SO I ended up with this rotateLeft function:

func rotateLeft(data []byte, k int) []byte {
	if k < 0 || len(data) == 0 {
		return data
	}

	r := k % len(data)

	data = append(data[r:], data[:r]...)

	return data
}

and this UnmarshalWrapToken func :

func UnmarshalWrapToken(wt *gssapi.WrapToken, b []byte, expectFromAcceptor bool) error {
	// Check if we can read a whole header
	if len(b) < 16 {
		return errors.New("bytes shorter than header length")
	}
	// Is the Token ID correct?
	if !bytes.Equal(getGssWrapTokenId()[:], b[0:2]) {
		return fmt.Errorf("wrong Token ID. Expected %s, was %s",
			hex.EncodeToString(getGssWrapTokenId()[:]),
			hex.EncodeToString(b[0:2]))
	}
	// Check the acceptor flag
	flags := b[2]
	isFromAcceptor := flags&0x01 == 1
	if isFromAcceptor && !expectFromAcceptor {
		return errors.New("unexpected acceptor flag is set: not expecting a token from the acceptor")
	}
	if !isFromAcceptor && expectFromAcceptor {
		return errors.New("expected acceptor flag is not set: expecting a token from the acceptor, not the initiator")
	}
	// Check the filler byte
	if b[3] != gssapi.FillerByte {
		return fmt.Errorf("unexpected filler byte: expecting 0xFF, was %s ", hex.EncodeToString(b[3:4]))
	}
	checksumL := binary.BigEndian.Uint16(b[4:6])
	// Sanity check on the checksum length
	if int(checksumL) > len(b)-gssapi.HdrLen {
		return fmt.Errorf("inconsistent checksum length: %d bytes to parse, checksum length is %d", len(b), checksumL)
	}

	rrc := binary.BigEndian.Uint16(b[6:8])
	data := b[16:]

	if rrc > 0 {
		// data was rotated to the right during wrap, now rotate left during unwrap
		data = rotateLeft(data, int(rrc))
	}

	payloadL := len(data) - int(checksumL)

	wt.Flags = flags
	wt.EC = checksumL
	wt.RRC = rrc
	wt.SndSeqNum = binary.BigEndian.Uint64(b[8:16])
	wt.Payload = data[:payloadL]
	wt.CheckSum = data[payloadL:]

	return nil
}

It "works", but I'm not sure if my implementation is correct or if I'm doing something stupid.

Also, looking at the MIT krb5 code, it seems like they're doing some additional checks and stuff based on other values, but I fail to understand it all.

But maybe simply taking the value in the RRC field and rotating by it will work in most cases.

@olde-ducke
Copy link

olde-ducke commented Nov 23, 2024

Hey @rewindrepeat, sorry for the late reply.

My code is pretty much the same as yours, although I managed to do the rotation in-place, but now that I think about it, it is probably not a good idea to modify the original raw response.

Looking at your code and MIT krb5 sources, I would probably do something like this now:

func rotateLeft(data []byte, rc int) []byte {
	if len(data) == 0 || rc%len(data) <= 0 {
		return data
	}

	rc %= len(data)

	return append(data[rc:], data[:rc]...)
}

func UnmarshalWrapToken(wt *gssapi.WrapToken, b []byte, expectFromAcceptor bool) error {
	// original Unwrap code

	rrc := binary.BigEndian.Uint16(b[6:8])

	data := b[16:]
	data = rotateLeft(data, int(rrc))

	payloadL := len(data) - int(checksumL)

	wt.Flags = flags
	wt.EC = checksumL
	wt.RRC = rrc
	wt.SndSeqNum = binary.BigEndian.Uint64(b[8:16])
	wt.Payload = data[:payloadL]
	wt.CheckSum = data[payloadL:]

	return nil
}

But your code works just fine on my end.

And yes, there's are a lot more things going on inside the MIT krb5 unwrap_v3 function (if I'm even looking in the right place). There can be three flags set in response, but only the first one (SentByAcceptor) is ever checked by gokrb5, the third one (AcceptorSubkey) is checked in the go-ldap client, but the second one (Sealed), which seems to require an extra decryption step for possibly rotated payload, is completely missing. If the second flag is set, the EC field is also seems to be wrong. The decryption step also requires a client key for the payload to be decrypted, which is not currently present in this context.

Unfortunately, I have no idea how to trigger this flag to be set, or who even initiates Wrap tokens with a confidentiality mechanism in the first place.

There is a flag gssapi.ContextFlagConf and according to RFC 2743 1.2.1.2 it seems that the client initiates this mechanism, but setting it did not change anything for me, the sealed flag is still set to 0 for both FreeIPA and Active Directory.

It might be a good idea to throw a loud NOT IMPLEMENTED error as soon as this flag is seen, so that it is easier to understand what exactly caused an error, rather than wondering why verification or authentication suddenly fails for no clear reason.

Setting either of these two flags will result in LDAP error 49: Invalid Credentials": 80090308: LdapErr: DSID-0C09058A, comment: AcceptSecurityContext error, data 5, v4563 from Active Directory (FreeIPA does not care), which I think means that Sequence Number should be handled properly for it to work.

All of the above (except the sequence number, which I think should be handled by the GSS-API client) should probably be implemented or fixed in gokrb5 itself, but it is currently inactive and the maintainer has not been around for over a year at this point.
I have found some other non-GSS-API related problems in gokrb5 that I would like to be fixed, for example the following krb5.conf causes the configuration parser to panic:

[realms]
  EXAMPLE.ORG = {}

with panic: runtime error: slice bounds out of range [1:0]. But it is what it is.

@rewindrepeat
Copy link

Thanks for the code snippet, @olde-ducke!

Unfortunately, I have no idea how to trigger this flag to be set, or who even initiates Wrap tokens with a confidentiality mechanism in the first place.

In the ggs-sample from the krb5 sources, the client is initiating the encryption of the wrap token data, although I don't see them setting or checking the GSS_C_CONF_FLAG and just encrypting the data anyway... Maybe that's okay because kerberos supports it and the sample server doesn't care if it was negotiated.

There is a flag gssapi.ContextFlagConf and according to RFC 2743 1.2.1.2 it seems that the client initiates this mechanism, but setting it did not change anything for me, the sealed flag is still set to 0 for both FreeIPA and Active Directory.

The Sealed flag in the wrap token from the server might not be set because of SASL. In section 3.2. of RFC 4752 it is said in the second to last paragraph "The server must then pass the plaintext to GSS_Wrap with conf_flag set to FALSE and issue the generated output_message to the client in a challenge." In the krb5 sources and the C bindings RFC, the conf_req_flag parameter to gss_wrap determines if encryption is used.

In section 3.1, last paragraph, the client also uses gss_wrap with conf_flag set to false.

So I guess even though gokrb5 cannot handle encryption for wrap tokens, it can still be used for SASL auth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants