From a167e6c57b80f5dc7be231231b62d7c594ee2770 Mon Sep 17 00:00:00 2001 From: 5amu Date: Sat, 20 Jan 2024 00:39:29 +0100 Subject: [PATCH 01/17] make protocolstate.IsHostAllowed check the domaincontroller, not the domain --- pkg/js/libs/ldap/ldap.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go index 1060df0e27..9bcebabc2d 100644 --- a/pkg/js/libs/ldap/ldap.go +++ b/pkg/js/libs/ldap/ldap.go @@ -55,9 +55,9 @@ func (c *LdapClient) CollectLdapMetadata(domain string, controller string) (LDAP domainController: controller, } - if !protocolstate.IsHostAllowed(domain) { + if !protocolstate.IsHostAllowed(controller) { // host is not valid according to network policy - return LDAPMetadata{}, protocolstate.ErrHostDenied.Msgf(domain) + return LDAPMetadata{}, protocolstate.ErrHostDenied.Msgf(controller) } conn, err := c.newLdapSession(opts) @@ -230,9 +230,9 @@ func (c *LdapClient) GetKerberoastableUsers(domain, controller string, username, password: password, } - if !protocolstate.IsHostAllowed(domain) { + if !protocolstate.IsHostAllowed(controller) { // host is not valid according to network policy - return nil, protocolstate.ErrHostDenied.Msgf(domain) + return nil, protocolstate.ErrHostDenied.Msgf(controller) } conn, err := c.newLdapSession(opts) From 73a73eeeacd31e252ba6369a786b70b8e2bce35d Mon Sep 17 00:00:00 2001 From: 5amu Date: Sat, 20 Jan 2024 01:50:54 +0100 Subject: [PATCH 02/17] implement method to connect to and verify the ldap server (IsLdap -> Connect) --- pkg/js/libs/ldap/ldap.go | 63 +++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go index 9bcebabc2d..621d721a34 100644 --- a/pkg/js/libs/ldap/ldap.go +++ b/pkg/js/libs/ldap/ldap.go @@ -2,7 +2,9 @@ package ldap import ( "context" + "crypto/tls" "fmt" + "net" "strings" "time" @@ -16,35 +18,62 @@ import ( // Client is a client for ldap protocol in golang. // // It is a wrapper around the standard library ldap package. -type LdapClient struct{} +type LdapClient struct { + BaseDN string + Realm string + Host string + Conn *ldap.Conn + Port int + UseSSL bool + TLS bool +} -// IsLdap checks if the given host and port are running ldap server. -func (c *LdapClient) IsLdap(host string, port int) (bool, error) { +// Connect is a method for LdapClient that stores information about of the ldap +// connection, tests it and verifies that the server is a valid ldap server +// +// returns the success status +func (c *LdapClient) Connect(host string, port int, ssl, istls bool) (bool, error) { + if c.Conn != nil { + return true, nil + } if !protocolstate.IsHostAllowed(host) { // host is not valid according to network policy return false, protocolstate.ErrHostDenied.Msgf(host) } - timeout := 10 * time.Second - - conn, err := protocolstate.Dialer.Dial(context.TODO(), "tcp", fmt.Sprintf("%s:%d", host, port)) - - if err != nil { - return false, err + var err error + var con net.Conn + if ssl { + con, err = protocolstate.Dialer.DialTLS(context.TODO(), "tcp", fmt.Sprintf("%s:%d", host, port)) + } else { + con, err = protocolstate.Dialer.Dial(context.TODO(), "tcp", fmt.Sprintf("%s:%d", host, port)) } - defer conn.Close() - - _ = conn.SetDeadline(time.Now().Add(timeout)) - - plugin := &pluginldap.LDAPPlugin{} - service, err := plugin.Run(conn, timeout, plugins.Target{Host: host}) if err != nil { return false, err } - if service == nil { - return false, nil + + c.Conn = ldap.NewConn(con, ssl) + if istls && !ssl { + // Here if it is not a valid ldap server, the StartTLS will return an error, + // so, if this check succeeds, there is no need to check if the host is has an LDAP Server: + // https://github.com/go-ldap/ldap/blob/cdb0754f666833c3e287503ed52d535a41ba10f6/v3/conn.go#L334 + if err := c.Conn.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { + return false, err + } + } else { + // If the connection is unencrypted or targets LDAPS we check here if the host actually + // exposes an actual LDAP server + plugin := &pluginldap.LDAPPlugin{} + if service, err := plugin.Run(con, 10*time.Second, plugins.Target{Host: host}); err != nil || service == nil { + return false, err + } } + + c.Host = host + c.Port = port + c.TLS = istls + c.UseSSL = ssl return true, nil } From fe59057c0dee5e9a20fa01f911b717e963c54c86 Mon Sep 17 00:00:00 2001 From: 5amu Date: Sat, 20 Jan 2024 12:22:36 +0100 Subject: [PATCH 03/17] implement a generic search that returns a list of objects given a filter and desired attributes --- pkg/js/libs/ldap/ldap.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go index 621d721a34..9d22965eec 100644 --- a/pkg/js/libs/ldap/ldap.go +++ b/pkg/js/libs/ldap/ldap.go @@ -77,6 +77,35 @@ func (c *LdapClient) Connect(host string, port int, ssl, istls bool) (bool, erro return true, nil } +// Search is a method that uses the already Connect()'ed client to query the LDAP +// server, works for openldap and for Microsoft's Active Directory Ldap +// +// accepts whatever filter and returns a list of maps having provided attributes +// as keys and associated values mirroring the ones returned by ldap +func (c *LdapClient) Search(filter string, attributes ...string) ([]map[string][]string, error) { + res, err := c.Conn.Search(ldap.NewSearchRequest( + c.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, + 0, 0, false, filter, attributes, nil, + )) + if err != nil { + return nil, err + } + + if len(res.Entries) == 0 { + return nil, fmt.Errorf("no result found in search") + } + + var out []map[string][]string + for _, r := range res.Entries { + app := make(map[string][]string) + for _, a := range attributes { + app[a] = r.GetAttributeValues(a) + } + out = append(out, app) + } + return out, nil +} + // CollectLdapMetadata collects metadata from ldap server. func (c *LdapClient) CollectLdapMetadata(domain string, controller string) (LDAPMetadata, error) { opts := &ldapSessionOptions{ From bd1238d27eae52306c4fe02969d8c75ae271b1f0 Mon Sep 17 00:00:00 2001 From: 5amu Date: Sat, 20 Jan 2024 21:02:08 +0100 Subject: [PATCH 04/17] implement authentication methods and change underlying connection because of bugs --- pkg/js/libs/ldap/ldap.go | 61 +++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go index 9d22965eec..6537d26640 100644 --- a/pkg/js/libs/ldap/ldap.go +++ b/pkg/js/libs/ldap/ldap.go @@ -4,15 +4,10 @@ import ( "context" "crypto/tls" "fmt" - "net" "strings" - "time" "github.com/go-ldap/ldap/v3" - "github.com/praetorian-inc/fingerprintx/pkg/plugins" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" - - pluginldap "github.com/praetorian-inc/fingerprintx/pkg/plugins/services/ldap" ) // Client is a client for ldap protocol in golang. @@ -43,17 +38,19 @@ func (c *LdapClient) Connect(host string, port int, ssl, istls bool) (bool, erro } var err error - var con net.Conn if ssl { - con, err = protocolstate.Dialer.DialTLS(context.TODO(), "tcp", fmt.Sprintf("%s:%d", host, port)) + config := &tls.Config{ + InsecureSkipVerify: true, + ServerName: host, + } + c.Conn, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", host, port), config) } else { - con, err = protocolstate.Dialer.Dial(context.TODO(), "tcp", fmt.Sprintf("%s:%d", host, port)) + c.Conn, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port)) } if err != nil { return false, err } - c.Conn = ldap.NewConn(con, ssl) if istls && !ssl { // Here if it is not a valid ldap server, the StartTLS will return an error, // so, if this check succeeds, there is no need to check if the host is has an LDAP Server: @@ -61,13 +58,6 @@ func (c *LdapClient) Connect(host string, port int, ssl, istls bool) (bool, erro if err := c.Conn.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { return false, err } - } else { - // If the connection is unencrypted or targets LDAPS we check here if the host actually - // exposes an actual LDAP server - plugin := &pluginldap.LDAPPlugin{} - if service, err := plugin.Run(con, 10*time.Second, plugins.Target{Host: host}); err != nil || service == nil { - return false, err - } } c.Host = host @@ -77,6 +67,45 @@ func (c *LdapClient) Connect(host string, port int, ssl, istls bool) (bool, erro return true, nil } +func (c *LdapClient) Authenticate(realm string, username, password string) (bool, error) { + if c.Conn == nil { + return false, fmt.Errorf("no existing connection") + } + + c.Realm = realm + c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(realm, "."), ",dc=")) + + if err := c.Conn.NTLMBind(realm, username, password); err == nil { + // if bind with NTLMBind(), there is nothing + // else to do, you are authenticated + return true, nil + } + + switch password { + case "": + if err := c.Conn.UnauthenticatedBind(username); err != nil { + return false, err + } + default: + if err := c.Conn.Bind(username, password); err != nil { + return false, err + } + } + return true, nil +} + +func (c *LdapClient) AuthenticateWithNTLMHash(realm string, username, hash string) (bool, error) { + if c.Conn == nil { + return false, fmt.Errorf("no existing connection") + } + c.Realm = realm + c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(realm, "."), ",dc=")) + if err := c.Conn.NTLMBindWithHash(realm, username, hash); err != nil { + return false, err + } + return true, nil +} + // Search is a method that uses the already Connect()'ed client to query the LDAP // server, works for openldap and for Microsoft's Active Directory Ldap // From 9821700f8e360732e8cbb785b49521f453b84481 Mon Sep 17 00:00:00 2001 From: 5amu Date: Sat, 20 Jan 2024 23:57:57 +0100 Subject: [PATCH 05/17] simplify CollectMetadata and remove session creation at runtime --- pkg/js/libs/ldap/ldap.go | 162 ++++----------------------------------- 1 file changed, 13 insertions(+), 149 deletions(-) diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go index 6537d26640..7d3b32c3c6 100644 --- a/pkg/js/libs/ldap/ldap.go +++ b/pkg/js/libs/ldap/ldap.go @@ -1,7 +1,6 @@ package ldap import ( - "context" "crypto/tls" "fmt" "strings" @@ -135,60 +134,8 @@ func (c *LdapClient) Search(filter string, attributes ...string) ([]map[string][ return out, nil } -// CollectLdapMetadata collects metadata from ldap server. -func (c *LdapClient) CollectLdapMetadata(domain string, controller string) (LDAPMetadata, error) { - opts := &ldapSessionOptions{ - domain: domain, - domainController: controller, - } - - if !protocolstate.IsHostAllowed(controller) { - // host is not valid according to network policy - return LDAPMetadata{}, protocolstate.ErrHostDenied.Msgf(controller) - } - - conn, err := c.newLdapSession(opts) - if err != nil { - return LDAPMetadata{}, err - } - defer c.close(conn) - - return c.collectLdapMetadata(conn, opts) -} - -type ldapSessionOptions struct { - domain string - domainController string - port int - username string - password string - baseDN string -} - -func (c *LdapClient) newLdapSession(opts *ldapSessionOptions) (*ldap.Conn, error) { - port := opts.port - dc := opts.domainController - if port == 0 { - port = 389 - } - - conn, err := protocolstate.Dialer.Dial(context.TODO(), "tcp", fmt.Sprintf("%s:%d", dc, port)) - if err != nil { - return nil, err - } - - lConn := ldap.NewConn(conn, false) - lConn.Start() - - return lConn, nil -} - -func (c *LdapClient) close(conn *ldap.Conn) { - conn.Close() -} - -// LDAPMetadata is the metadata for ldap server. -type LDAPMetadata struct { +// Metadata is the metadata for ldap server. +type Metadata struct { BaseDN string Domain string DefaultNamingContext string @@ -198,23 +145,17 @@ type LDAPMetadata struct { DnsHostName string } -func (c *LdapClient) collectLdapMetadata(lConn *ldap.Conn, opts *ldapSessionOptions) (LDAPMetadata, error) { - metadata := LDAPMetadata{} - - var err error - if opts.username == "" { - err = lConn.UnauthenticatedBind("") - } else { - err = lConn.Bind(opts.username, opts.password) - } - if err != nil { - return metadata, err +// CollectLdapMetadata collects metadata from ldap server. +func (c *LdapClient) CollectMetadata(domain string, controller string) (Metadata, error) { + if c.Conn == nil { + return Metadata{}, fmt.Errorf("no existing connection") } + defer c.Conn.Close() - baseDN, _ := getBaseNamingContext(opts, lConn) + var metadata Metadata - metadata.BaseDN = baseDN - metadata.Domain = parseDC(baseDN) + metadata.BaseDN = c.BaseDN + metadata.Domain = c.Realm srMetadata := ldap.NewSearchRequest( "", @@ -230,7 +171,7 @@ func (c *LdapClient) collectLdapMetadata(lConn *ldap.Conn, opts *ldapSessionOpti "dnsHostName", }, nil) - resMetadata, err := lConn.Search(srMetadata) + resMetadata, err := c.Conn.Search(srMetadata) if err != nil { return metadata, err } @@ -254,43 +195,6 @@ func (c *LdapClient) collectLdapMetadata(lConn *ldap.Conn, opts *ldapSessionOpti return metadata, nil } -func parseDC(input string) string { - parts := strings.Split(strings.ToLower(input), ",") - - for i, part := range parts { - parts[i] = strings.TrimPrefix(part, "dc=") - } - - return strings.Join(parts, ".") -} - -func getBaseNamingContext(opts *ldapSessionOptions, conn *ldap.Conn) (string, error) { - if opts.baseDN != "" { - return opts.baseDN, nil - } - sr := ldap.NewSearchRequest( - "", - ldap.ScopeBaseObject, - ldap.NeverDerefAliases, - 0, 0, false, - "(objectClass=*)", - []string{"defaultNamingContext"}, - nil) - res, err := conn.Search(sr) - if err != nil { - return "", err - } - if len(res.Entries) == 0 { - return "", fmt.Errorf("error getting metadata: No LDAP responses from server") - } - defaultNamingContext := res.Entries[0].GetAttributeValue("defaultNamingContext") - if defaultNamingContext == "" { - return "", fmt.Errorf("error getting metadata: attribute defaultNamingContext missing") - } - opts.baseDN = defaultNamingContext - return opts.baseDN, nil -} - // KerberoastableUser contains the important fields of the Active Directory // kerberoastable user type KerberoastableUser struct { @@ -310,48 +214,8 @@ type KerberoastableUser struct { // Returns a list of KerberoastableUser, if an error occurs, returns an empty // slice and the raised error func (c *LdapClient) GetKerberoastableUsers(domain, controller string, username, password string) ([]KerberoastableUser, error) { - opts := &ldapSessionOptions{ - domain: domain, - domainController: controller, - username: username, - password: password, - } - - if !protocolstate.IsHostAllowed(controller) { - // host is not valid according to network policy - return nil, protocolstate.ErrHostDenied.Msgf(controller) - } - - conn, err := c.newLdapSession(opts) - if err != nil { - return nil, err - } - defer c.close(conn) - - domainParts := strings.Split(domain, ".") - if username == "" { - err = conn.UnauthenticatedBind("") - } else { - err = conn.Bind( - fmt.Sprintf("%v\\%v", domainParts[0], username), - password, - ) - } - if err != nil { - return nil, err - } - - var baseDN strings.Builder - for i, part := range domainParts { - baseDN.WriteString("DC=") - baseDN.WriteString(part) - if i != len(domainParts)-1 { - baseDN.WriteString(",") - } - } - sr := ldap.NewSearchRequest( - baseDN.String(), + c.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, @@ -368,7 +232,7 @@ func (c *LdapClient) GetKerberoastableUsers(domain, controller string, username, nil, ) - res, err := conn.Search(sr) + res, err := c.Conn.Search(sr) if err != nil { return nil, err } From cb0d98e4b234d2b96b82d3b3d9a59ab4d98e6342 Mon Sep 17 00:00:00 2001 From: 5amu Date: Sat, 20 Jan 2024 23:58:56 +0100 Subject: [PATCH 06/17] do not append an empty map when generating output in Search() --- pkg/js/libs/ldap/ldap.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go index 7d3b32c3c6..b9edc3b3d3 100644 --- a/pkg/js/libs/ldap/ldap.go +++ b/pkg/js/libs/ldap/ldap.go @@ -126,10 +126,17 @@ func (c *LdapClient) Search(filter string, attributes ...string) ([]map[string][ var out []map[string][]string for _, r := range res.Entries { app := make(map[string][]string) + empty := true for _, a := range attributes { - app[a] = r.GetAttributeValues(a) + v := r.GetAttributeValues(a) + if len(v) > 0 { + app[a] = v + empty = false + } + } + if !empty { + out = append(out, app) } - out = append(out, app) } return out, nil } From 6bf8f8769b848330edcc82a9deebf2be045af180 Mon Sep 17 00:00:00 2001 From: 5amu Date: Sun, 21 Jan 2024 12:53:41 +0100 Subject: [PATCH 07/17] define frequently used filters and AD UAC filters --- pkg/js/libs/ldap/ldap.go | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go index b9edc3b3d3..38b733b5e3 100644 --- a/pkg/js/libs/ldap/ldap.go +++ b/pkg/js/libs/ldap/ldap.go @@ -105,6 +105,60 @@ func (c *LdapClient) AuthenticateWithNTLMHash(realm string, username, hash strin return true, nil } +// LDAP makes you search using an OID +// http://oid-info.com/get/1.2.840.113556.1.4.803 +// +// The one for the userAccountControl in MS Active Directory is +// 1.2.840.113556.1.4.803 (LDAP_MATCHING_RULE_BIT_AND) +// +// We can look at the enabled flags using a query like (!(userAccountControl:1.2.840.113556.1.4.803:=2)) +// +// https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties +const ( + FilterIsPerson = "(objectCategory=person)" + FilterIsGroup = "(objectCategory=group)" + FilterIsComputer = "(objectCategory=computer)" + FilterIsAdmin = "(adminCount=1)" + FilterLogonScript = "(userAccountControl:1.2.840.113556.1.4.803:=1)" // The logon script will be run. + FilterAccountDisabled = "(userAccountControl:1.2.840.113556.1.4.803:=2)" // The user account is disabled. + FilterAccountEnabled = "(!(userAccountControl:1.2.840.113556.1.4.803:=2))" // The user account is enabled. + FilterHomedirRequired = "(userAccountControl:1.2.840.113556.1.4.803:=8)" // The home folder is required. + FilterLockout = "(userAccountControl:1.2.840.113556.1.4.803:=16)" // The user is locked out. + FilterPasswordNotRequired = "(userAccountControl:1.2.840.113556.1.4.803:=32)" // No password is required. + FilterPasswordCantChange = "(userAccountControl:1.2.840.113556.1.4.803:=64)" // The user can't change the password. + FilterCanSendEncryptedPassword = "(userAccountControl:1.2.840.113556.1.4.803:=128)" // The user can send an encrypted password. + FilterIsDuplicateAccount = "(userAccountControl:1.2.840.113556.1.4.803:=256)" // It's an account for users whose primary account is in another domain. + FilterIsNormalAccount = "(userAccountControl:1.2.840.113556.1.4.803:=512)" // It's a default account type that represents a typical user. + FilterInterdomainTrustAccount = "(userAccountControl:1.2.840.113556.1.4.803:=2048)" // It's a permit to trust an account for a system domain that trusts other domains. + FilterWorkstationTrustAccount = "(userAccountControl:1.2.840.113556.1.4.803:=4096)" // It's a computer account for a computer that is running old Windows builds. + FilterServerTrustAccount = "(userAccountControl:1.2.840.113556.1.4.803:=8192)" // It's a computer account for a domain controller that is a member of this domain. + FilterDontExpirePassword = "(userAccountControl:1.2.840.113556.1.4.803:=65536)" // Represents the password, which should never expire on the account. + FilterMnsLogonAccount = "(userAccountControl:1.2.840.113556.1.4.803:=131072)" // It's an MNS logon account. + FilterSmartCardRequired = "(userAccountControl:1.2.840.113556.1.4.803:=262144)" // When this flag is set, it forces the user to log on by using a smart card. + FilterTrustedForDelegation = "(userAccountControl:1.2.840.113556.1.4.803:=524288)" // When this flag is set, the service account (the user or computer account) under which a service runs is trusted for Kerberos delegation. + FilterNotDelegated = "(userAccountControl:1.2.840.113556.1.4.803:=1048576)" // When this flag is set, the security context of the user isn't delegated to a service even if the service account is set as trusted for Kerberos delegation. + FilterUseDesKeyOnly = "(userAccountControl:1.2.840.113556.1.4.803:=2097152)" // Restrict this principal to use only Data Encryption Standard (DES) encryption types for keys. + FilterDontRequirePreauth = "(userAccountControl:1.2.840.113556.1.4.803:=4194304)" // This account doesn't require Kerberos pre-authentication for logging on. + FilterPasswordExpired = "(userAccountControl:1.2.840.113556.1.4.803:=8388608)" // The user's password has expired. + FilterTrustedToAuthForDelegation = "(userAccountControl:1.2.840.113556.1.4.803:=16777216)" // The account is enabled for delegation. + FilterPartialSecretsAccount = "(userAccountControl:1.2.840.113556.1.4.803:=67108864)" // The account is a read-only domain controller (RODC). + +) + +func JoinFilters(filters ...string) string { + var builder strings.Builder + builder.WriteString("(&") + for _, s := range filters { + builder.WriteString(s) + } + builder.WriteString(")") + return builder.String() +} + +func NegativeFilter(filter string) string { + return fmt.Sprintf("(!%s)", filter) +} + // Search is a method that uses the already Connect()'ed client to query the LDAP // server, works for openldap and for Microsoft's Active Directory Ldap // From 893129eb17884b421da4e2b5df3fd61f37c33645 Mon Sep 17 00:00:00 2001 From: 5amu Date: Sun, 21 Jan 2024 12:54:49 +0100 Subject: [PATCH 08/17] reflect changes for gojs --- pkg/js/generated/go/libldap/ldap.go | 37 +++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/pkg/js/generated/go/libldap/ldap.go b/pkg/js/generated/go/libldap/ldap.go index 1b89f4be99..fc4841acc3 100644 --- a/pkg/js/generated/go/libldap/ldap.go +++ b/pkg/js/generated/go/libldap/ldap.go @@ -15,16 +15,45 @@ func init() { module.Set( gojs.Objects{ // Functions + "JoinFilters": func(f ...string) string { return lib_ldap.JoinFilters(f...) }, + "NegativeFilter": func(f string) string { return lib_ldap.NegativeFilter(f) }, // Var and consts + "FilterIsPerson": func() string { return lib_ldap.FilterIsPerson }, + "FilterIsGroup": func() string { return lib_ldap.FilterIsGroup }, + "FilterIsComputer": func() string { return lib_ldap.FilterIsComputer }, + "FilterIsAdmin": func() string { return lib_ldap.FilterIsAdmin }, + "FilterLogonScript": func() string { return lib_ldap.FilterLogonScript }, + "FilterAccountDisabled": func() string { return lib_ldap.FilterAccountDisabled }, + "FilterAccountEnabled": func() string { return lib_ldap.FilterAccountEnabled }, + "FilterHomedirRequired": func() string { return lib_ldap.FilterHomedirRequired }, + "FilterLockout": func() string { return lib_ldap.FilterLockout }, + "FilterPasswordNotRequired": func() string { return lib_ldap.FilterPasswordNotRequired }, + "FilterPasswordCantChange": func() string { return lib_ldap.FilterPasswordCantChange }, + "FilterCanSendEncryptedPassword": func() string { return lib_ldap.FilterCanSendEncryptedPassword }, + "FilterIsDuplicateAccount": func() string { return lib_ldap.FilterIsDuplicateAccount }, + "FilterIsNormalAccount": func() string { return lib_ldap.FilterIsNormalAccount }, + "FilterInterdomainTrustAccount": func() string { return lib_ldap.FilterInterdomainTrustAccount }, + "FilterWorkstationTrustAccount": func() string { return lib_ldap.FilterWorkstationTrustAccount }, + "FilterServerTrustAccount": func() string { return lib_ldap.FilterServerTrustAccount }, + "FilterDontExpirePassword": func() string { return lib_ldap.FilterDontExpirePassword }, + "FilterMnsLogonAccount": func() string { return lib_ldap.FilterMnsLogonAccount }, + "FilterSmartCardRequired": func() string { return lib_ldap.FilterSmartCardRequired }, + "FilterTrustedForDelegation": func() string { return lib_ldap.FilterTrustedForDelegation }, + "FilterNotDelegated": func() string { return lib_ldap.FilterNotDelegated }, + "FilterUseDesKeyOnly": func() string { return lib_ldap.FilterUseDesKeyOnly }, + "FilterDontRequirePreauth": func() string { return lib_ldap.FilterDontRequirePreauth }, + "FilterPasswordExpired": func() string { return lib_ldap.FilterPasswordExpired }, + "FilterTrustedToAuthForDelegation": func() string { return lib_ldap.FilterTrustedToAuthForDelegation }, + "FilterPartialSecretsAccount": func() string { return lib_ldap.FilterPartialSecretsAccount }, // Types (value type) - "LDAPMetadata": func() lib_ldap.LDAPMetadata { return lib_ldap.LDAPMetadata{} }, - "LdapClient": func() lib_ldap.LdapClient { return lib_ldap.LdapClient{} }, + "Metadata": func() lib_ldap.Metadata { return lib_ldap.Metadata{} }, + "LdapClient": func() lib_ldap.LdapClient { return lib_ldap.LdapClient{} }, // Types (pointer type) - "NewLDAPMetadata": func() *lib_ldap.LDAPMetadata { return &lib_ldap.LDAPMetadata{} }, - "NewLdapClient": func() *lib_ldap.LdapClient { return &lib_ldap.LdapClient{} }, + "NewMetadata": func() *lib_ldap.Metadata { return &lib_ldap.Metadata{} }, + "NewLdapClient": func() *lib_ldap.LdapClient { return &lib_ldap.LdapClient{} }, }, ).Register() } From 89d30d94d6c8515919dfa3a40bf07ae162991c01 Mon Sep 17 00:00:00 2001 From: 5amu Date: Sun, 21 Jan 2024 16:55:17 +0100 Subject: [PATCH 09/17] implement generic method to find AD objects --- pkg/js/libs/ldap/adenum.go | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 pkg/js/libs/ldap/adenum.go diff --git a/pkg/js/libs/ldap/adenum.go b/pkg/js/libs/ldap/adenum.go new file mode 100644 index 0000000000..db8da6e78c --- /dev/null +++ b/pkg/js/libs/ldap/adenum.go @@ -0,0 +1,55 @@ +package ldap + +import ( + "fmt" + + "github.com/go-ldap/ldap/v3" +) + +type ADObject struct { + DistinguishedName string + SAMAccountName string + PWDLastSet string + LastLogon string + MemberOf []string + ServicePrincipalName []string +} + +func (c *LdapClient) FindADObjects(filter string) ([]ADObject, error) { + sr := ldap.NewSearchRequest( + c.BaseDN, ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, 0, 0, false, + filter, + []string{ + "distinguishedName", + "sAMAccountName", + "pwdLastSet", + "lastLogon", + "memberOf", + "servicePrincipalName", + }, + nil, + ) + + res, err := c.Conn.Search(sr) + if err != nil { + return nil, err + } + + if len(res.Entries) == 0 { + return nil, fmt.Errorf("no object returned from query") + } + + var objects []ADObject + for _, obj := range res.Entries { + objects = append(objects, ADObject{ + DistinguishedName: obj.GetAttributeValue("distinguishedName"), + SAMAccountName: obj.GetAttributeValue("sAMAccountName"), + PWDLastSet: obj.GetAttributeValue("pwdLastSet"), + LastLogon: obj.GetAttributeValue("lastLogon"), + MemberOf: obj.GetAttributeValues("memberOf"), + ServicePrincipalName: obj.GetAttributeValues("servicePrincipalName"), + }) + } + return objects, nil +} From 9d23f5f88f6156e28697d80b1e1906d72ab1d2c2 Mon Sep 17 00:00:00 2001 From: 5amu Date: Sun, 21 Jan 2024 17:11:28 +0100 Subject: [PATCH 10/17] implement enumeration methods + rewrite kerberoastable --- pkg/js/libs/ldap/adenum.go | 36 ++++++++++++++++++++++ pkg/js/libs/ldap/ldap.go | 61 +------------------------------------- 2 files changed, 37 insertions(+), 60 deletions(-) diff --git a/pkg/js/libs/ldap/adenum.go b/pkg/js/libs/ldap/adenum.go index db8da6e78c..a0551b1791 100644 --- a/pkg/js/libs/ldap/adenum.go +++ b/pkg/js/libs/ldap/adenum.go @@ -53,3 +53,39 @@ func (c *LdapClient) FindADObjects(filter string) ([]ADObject, error) { } return objects, nil } + +func (c *LdapClient) GetADUsers() ([]ADObject, error) { + return c.FindADObjects(FilterIsPerson) +} + +func (c *LdapClient) GetADActiveUsers() ([]ADObject, error) { + return c.FindADObjects(JoinFilters(FilterIsPerson, FilterAccountEnabled)) +} + +func (c *LdapClient) GetADUserWithNeverExpiringPasswords() ([]ADObject, error) { + return c.FindADObjects(JoinFilters(FilterIsPerson, FilterDontExpirePassword)) +} + +func (c *LdapClient) GetADUserTrustedForDelegation() ([]ADObject, error) { + return c.FindADObjects(JoinFilters(FilterIsPerson, FilterTrustedForDelegation)) +} + +func (c *LdapClient) GetADUserWithPasswordNotRequired() ([]ADObject, error) { + return c.FindADObjects(JoinFilters(FilterIsPerson, FilterPasswordNotRequired)) +} + +func (c *LdapClient) GetADGroups() ([]ADObject, error) { + return c.FindADObjects(FilterIsGroup) +} + +func (c *LdapClient) GetADDCList() ([]ADObject, error) { + return c.FindADObjects(JoinFilters(FilterIsComputer, FilterAccountEnabled, FilterServerTrustAccount)) +} + +func (c *LdapClient) GetADAdmins() ([]ADObject, error) { + return c.FindADObjects(JoinFilters(FilterIsPerson, FilterAccountEnabled, FilterIsAdmin)) +} + +func (c *LdapClient) GetADUserKerberoastable() ([]ADObject, error) { + return c.FindADObjects(JoinFilters(FilterIsPerson, FilterAccountEnabled, FilterHasServicePrincipalName)) +} diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go index 38b733b5e3..02f5900b4f 100644 --- a/pkg/js/libs/ldap/ldap.go +++ b/pkg/js/libs/ldap/ldap.go @@ -119,6 +119,7 @@ const ( FilterIsGroup = "(objectCategory=group)" FilterIsComputer = "(objectCategory=computer)" FilterIsAdmin = "(adminCount=1)" + FilterHasServicePrincipalName = "(servicePrincipalName=*)" FilterLogonScript = "(userAccountControl:1.2.840.113556.1.4.803:=1)" // The logon script will be run. FilterAccountDisabled = "(userAccountControl:1.2.840.113556.1.4.803:=2)" // The user account is disabled. FilterAccountEnabled = "(!(userAccountControl:1.2.840.113556.1.4.803:=2))" // The user account is enabled. @@ -255,63 +256,3 @@ func (c *LdapClient) CollectMetadata(domain string, controller string) (Metadata } return metadata, nil } - -// KerberoastableUser contains the important fields of the Active Directory -// kerberoastable user -type KerberoastableUser struct { - SAMAccountName string - ServicePrincipalName string - PWDLastSet string - MemberOf string - UserAccountControl string - LastLogon string -} - -// GetKerberoastableUsers collects all "person" users that have an SPN -// associated with them. The LDAP filter is built with the same logic as -// "GetUserSPNs.py", the well-known impacket example by Forta. -// https://github.com/fortra/impacket/blob/master/examples/GetUserSPNs.py#L297 -// -// Returns a list of KerberoastableUser, if an error occurs, returns an empty -// slice and the raised error -func (c *LdapClient) GetKerberoastableUsers(domain, controller string, username, password string) ([]KerberoastableUser, error) { - sr := ldap.NewSearchRequest( - c.BaseDN, - ldap.ScopeWholeSubtree, - ldap.NeverDerefAliases, - 0, 0, false, - // (&(is_user) (!(account_is_disabled)) (has_SPN)) - "(&(objectCategory=person)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(servicePrincipalName=*))", - []string{ - "SAMAccountName", - "ServicePrincipalName", - "pwdLastSet", - "MemberOf", - "userAccountControl", - "lastLogon", - }, - nil, - ) - - res, err := c.Conn.Search(sr) - if err != nil { - return nil, err - } - - if len(res.Entries) == 0 { - return nil, fmt.Errorf("no kerberoastable user found") - } - - var ku []KerberoastableUser - for _, usr := range res.Entries { - ku = append(ku, KerberoastableUser{ - SAMAccountName: usr.GetAttributeValue("sAMAccountName"), - ServicePrincipalName: usr.GetAttributeValue("servicePrincipalName"), - PWDLastSet: usr.GetAttributeValue("pwdLastSet"), - MemberOf: usr.GetAttributeValue("MemberOf"), - UserAccountControl: usr.GetAttributeValue("userAccountControl"), - LastLogon: usr.GetAttributeValue("lastLogon"), - }) - } - return ku, nil -} From 95d028c5f9fa72f6ab8ce9f237221fef2130a303 Mon Sep 17 00:00:00 2001 From: 5amu Date: Sun, 21 Jan 2024 17:12:09 +0100 Subject: [PATCH 11/17] move ad filters to adenum.go --- pkg/js/libs/ldap/adenum.go | 56 ++++++++++++++++++++++++++++++++++++++ pkg/js/libs/ldap/ldap.go | 55 ------------------------------------- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/pkg/js/libs/ldap/adenum.go b/pkg/js/libs/ldap/adenum.go index a0551b1791..c10e3b0a06 100644 --- a/pkg/js/libs/ldap/adenum.go +++ b/pkg/js/libs/ldap/adenum.go @@ -2,10 +2,66 @@ package ldap import ( "fmt" + "strings" "github.com/go-ldap/ldap/v3" ) +// LDAP makes you search using an OID +// http://oid-info.com/get/1.2.840.113556.1.4.803 +// +// The one for the userAccountControl in MS Active Directory is +// 1.2.840.113556.1.4.803 (LDAP_MATCHING_RULE_BIT_AND) +// +// We can look at the enabled flags using a query like (!(userAccountControl:1.2.840.113556.1.4.803:=2)) +// +// https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties +const ( + FilterIsPerson = "(objectCategory=person)" + FilterIsGroup = "(objectCategory=group)" + FilterIsComputer = "(objectCategory=computer)" + FilterIsAdmin = "(adminCount=1)" + FilterHasServicePrincipalName = "(servicePrincipalName=*)" + FilterLogonScript = "(userAccountControl:1.2.840.113556.1.4.803:=1)" // The logon script will be run. + FilterAccountDisabled = "(userAccountControl:1.2.840.113556.1.4.803:=2)" // The user account is disabled. + FilterAccountEnabled = "(!(userAccountControl:1.2.840.113556.1.4.803:=2))" // The user account is enabled. + FilterHomedirRequired = "(userAccountControl:1.2.840.113556.1.4.803:=8)" // The home folder is required. + FilterLockout = "(userAccountControl:1.2.840.113556.1.4.803:=16)" // The user is locked out. + FilterPasswordNotRequired = "(userAccountControl:1.2.840.113556.1.4.803:=32)" // No password is required. + FilterPasswordCantChange = "(userAccountControl:1.2.840.113556.1.4.803:=64)" // The user can't change the password. + FilterCanSendEncryptedPassword = "(userAccountControl:1.2.840.113556.1.4.803:=128)" // The user can send an encrypted password. + FilterIsDuplicateAccount = "(userAccountControl:1.2.840.113556.1.4.803:=256)" // It's an account for users whose primary account is in another domain. + FilterIsNormalAccount = "(userAccountControl:1.2.840.113556.1.4.803:=512)" // It's a default account type that represents a typical user. + FilterInterdomainTrustAccount = "(userAccountControl:1.2.840.113556.1.4.803:=2048)" // It's a permit to trust an account for a system domain that trusts other domains. + FilterWorkstationTrustAccount = "(userAccountControl:1.2.840.113556.1.4.803:=4096)" // It's a computer account for a computer that is running old Windows builds. + FilterServerTrustAccount = "(userAccountControl:1.2.840.113556.1.4.803:=8192)" // It's a computer account for a domain controller that is a member of this domain. + FilterDontExpirePassword = "(userAccountControl:1.2.840.113556.1.4.803:=65536)" // Represents the password, which should never expire on the account. + FilterMnsLogonAccount = "(userAccountControl:1.2.840.113556.1.4.803:=131072)" // It's an MNS logon account. + FilterSmartCardRequired = "(userAccountControl:1.2.840.113556.1.4.803:=262144)" // When this flag is set, it forces the user to log on by using a smart card. + FilterTrustedForDelegation = "(userAccountControl:1.2.840.113556.1.4.803:=524288)" // When this flag is set, the service account (the user or computer account) under which a service runs is trusted for Kerberos delegation. + FilterNotDelegated = "(userAccountControl:1.2.840.113556.1.4.803:=1048576)" // When this flag is set, the security context of the user isn't delegated to a service even if the service account is set as trusted for Kerberos delegation. + FilterUseDesKeyOnly = "(userAccountControl:1.2.840.113556.1.4.803:=2097152)" // Restrict this principal to use only Data Encryption Standard (DES) encryption types for keys. + FilterDontRequirePreauth = "(userAccountControl:1.2.840.113556.1.4.803:=4194304)" // This account doesn't require Kerberos pre-authentication for logging on. + FilterPasswordExpired = "(userAccountControl:1.2.840.113556.1.4.803:=8388608)" // The user's password has expired. + FilterTrustedToAuthForDelegation = "(userAccountControl:1.2.840.113556.1.4.803:=16777216)" // The account is enabled for delegation. + FilterPartialSecretsAccount = "(userAccountControl:1.2.840.113556.1.4.803:=67108864)" // The account is a read-only domain controller (RODC). + +) + +func JoinFilters(filters ...string) string { + var builder strings.Builder + builder.WriteString("(&") + for _, s := range filters { + builder.WriteString(s) + } + builder.WriteString(")") + return builder.String() +} + +func NegativeFilter(filter string) string { + return fmt.Sprintf("(!%s)", filter) +} + type ADObject struct { DistinguishedName string SAMAccountName string diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go index 02f5900b4f..2815d10b15 100644 --- a/pkg/js/libs/ldap/ldap.go +++ b/pkg/js/libs/ldap/ldap.go @@ -105,61 +105,6 @@ func (c *LdapClient) AuthenticateWithNTLMHash(realm string, username, hash strin return true, nil } -// LDAP makes you search using an OID -// http://oid-info.com/get/1.2.840.113556.1.4.803 -// -// The one for the userAccountControl in MS Active Directory is -// 1.2.840.113556.1.4.803 (LDAP_MATCHING_RULE_BIT_AND) -// -// We can look at the enabled flags using a query like (!(userAccountControl:1.2.840.113556.1.4.803:=2)) -// -// https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties -const ( - FilterIsPerson = "(objectCategory=person)" - FilterIsGroup = "(objectCategory=group)" - FilterIsComputer = "(objectCategory=computer)" - FilterIsAdmin = "(adminCount=1)" - FilterHasServicePrincipalName = "(servicePrincipalName=*)" - FilterLogonScript = "(userAccountControl:1.2.840.113556.1.4.803:=1)" // The logon script will be run. - FilterAccountDisabled = "(userAccountControl:1.2.840.113556.1.4.803:=2)" // The user account is disabled. - FilterAccountEnabled = "(!(userAccountControl:1.2.840.113556.1.4.803:=2))" // The user account is enabled. - FilterHomedirRequired = "(userAccountControl:1.2.840.113556.1.4.803:=8)" // The home folder is required. - FilterLockout = "(userAccountControl:1.2.840.113556.1.4.803:=16)" // The user is locked out. - FilterPasswordNotRequired = "(userAccountControl:1.2.840.113556.1.4.803:=32)" // No password is required. - FilterPasswordCantChange = "(userAccountControl:1.2.840.113556.1.4.803:=64)" // The user can't change the password. - FilterCanSendEncryptedPassword = "(userAccountControl:1.2.840.113556.1.4.803:=128)" // The user can send an encrypted password. - FilterIsDuplicateAccount = "(userAccountControl:1.2.840.113556.1.4.803:=256)" // It's an account for users whose primary account is in another domain. - FilterIsNormalAccount = "(userAccountControl:1.2.840.113556.1.4.803:=512)" // It's a default account type that represents a typical user. - FilterInterdomainTrustAccount = "(userAccountControl:1.2.840.113556.1.4.803:=2048)" // It's a permit to trust an account for a system domain that trusts other domains. - FilterWorkstationTrustAccount = "(userAccountControl:1.2.840.113556.1.4.803:=4096)" // It's a computer account for a computer that is running old Windows builds. - FilterServerTrustAccount = "(userAccountControl:1.2.840.113556.1.4.803:=8192)" // It's a computer account for a domain controller that is a member of this domain. - FilterDontExpirePassword = "(userAccountControl:1.2.840.113556.1.4.803:=65536)" // Represents the password, which should never expire on the account. - FilterMnsLogonAccount = "(userAccountControl:1.2.840.113556.1.4.803:=131072)" // It's an MNS logon account. - FilterSmartCardRequired = "(userAccountControl:1.2.840.113556.1.4.803:=262144)" // When this flag is set, it forces the user to log on by using a smart card. - FilterTrustedForDelegation = "(userAccountControl:1.2.840.113556.1.4.803:=524288)" // When this flag is set, the service account (the user or computer account) under which a service runs is trusted for Kerberos delegation. - FilterNotDelegated = "(userAccountControl:1.2.840.113556.1.4.803:=1048576)" // When this flag is set, the security context of the user isn't delegated to a service even if the service account is set as trusted for Kerberos delegation. - FilterUseDesKeyOnly = "(userAccountControl:1.2.840.113556.1.4.803:=2097152)" // Restrict this principal to use only Data Encryption Standard (DES) encryption types for keys. - FilterDontRequirePreauth = "(userAccountControl:1.2.840.113556.1.4.803:=4194304)" // This account doesn't require Kerberos pre-authentication for logging on. - FilterPasswordExpired = "(userAccountControl:1.2.840.113556.1.4.803:=8388608)" // The user's password has expired. - FilterTrustedToAuthForDelegation = "(userAccountControl:1.2.840.113556.1.4.803:=16777216)" // The account is enabled for delegation. - FilterPartialSecretsAccount = "(userAccountControl:1.2.840.113556.1.4.803:=67108864)" // The account is a read-only domain controller (RODC). - -) - -func JoinFilters(filters ...string) string { - var builder strings.Builder - builder.WriteString("(&") - for _, s := range filters { - builder.WriteString(s) - } - builder.WriteString(")") - return builder.String() -} - -func NegativeFilter(filter string) string { - return fmt.Sprintf("(!%s)", filter) -} - // Search is a method that uses the already Connect()'ed client to query the LDAP // server, works for openldap and for Microsoft's Active Directory Ldap // From c703fffe80a355abecc8951ea6d60eb4ced58ca9 Mon Sep 17 00:00:00 2001 From: 5amu Date: Sun, 21 Jan 2024 17:31:08 +0100 Subject: [PATCH 12/17] implement method to grab domain SID --- pkg/js/libs/ldap/adenum.go | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pkg/js/libs/ldap/adenum.go b/pkg/js/libs/ldap/adenum.go index c10e3b0a06..3c1363603a 100644 --- a/pkg/js/libs/ldap/adenum.go +++ b/pkg/js/libs/ldap/adenum.go @@ -145,3 +145,50 @@ func (c *LdapClient) GetADAdmins() ([]ADObject, error) { func (c *LdapClient) GetADUserKerberoastable() ([]ADObject, error) { return c.FindADObjects(JoinFilters(FilterIsPerson, FilterAccountEnabled, FilterHasServicePrincipalName)) } + +func decodeSID(b []byte) string { + revisionLvl := int(b[0]) + subAuthorityCount := int(b[1]) & 0xFF + + var authority int + for i := 2; i <= 7; i++ { + authority = authority | int(b[i])<<(8*(5-(i-2))) + } + + var size = 4 + var offset = 8 + var subAuthorities []int + for i := 0; i < subAuthorityCount; i++ { + var subAuthority int + for k := 0; k < size; k++ { + subAuthority = subAuthority | (int(b[offset+k])&0xFF)<<(8*k) + } + subAuthorities = append(subAuthorities, subAuthority) + offset += size + } + + var builder strings.Builder + builder.WriteString("S-") + builder.WriteString(fmt.Sprintf("%d-", revisionLvl)) + builder.WriteString(fmt.Sprintf("%d", authority)) + for _, v := range subAuthorities { + builder.WriteString(fmt.Sprintf("-%d", v)) + } + return builder.String() +} + +func (c *LdapClient) GetADDomainSID() (string, error) { + r, err := c.Search(FilterServerTrustAccount, "objectSid") + if err != nil { + return "", err + } + + if len(r) < 1 { + return "", fmt.Errorf("no result from GetADDomainSID query") + } + + if len(r[0]["objectSid"]) < 1 { + return "", fmt.Errorf("could not grab DomainSID") + } + return decodeSID([]byte(r[0]["objectSid"][0])), nil +} From 642c99bcff935b20a13eae7d49d9f4245f613ccf Mon Sep 17 00:00:00 2001 From: 5amu Date: Sun, 21 Jan 2024 17:57:23 +0100 Subject: [PATCH 13/17] move DecodeSID to utils.go making it a generic function exposed by the module --- pkg/js/generated/go/libldap/ldap.go | 1 + pkg/js/libs/ldap/adenum.go | 33 +------------------------ pkg/js/libs/ldap/utils.go | 38 +++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 32 deletions(-) create mode 100644 pkg/js/libs/ldap/utils.go diff --git a/pkg/js/generated/go/libldap/ldap.go b/pkg/js/generated/go/libldap/ldap.go index fc4841acc3..c45d237a92 100644 --- a/pkg/js/generated/go/libldap/ldap.go +++ b/pkg/js/generated/go/libldap/ldap.go @@ -17,6 +17,7 @@ func init() { // Functions "JoinFilters": func(f ...string) string { return lib_ldap.JoinFilters(f...) }, "NegativeFilter": func(f string) string { return lib_ldap.NegativeFilter(f) }, + "DecodeSID": func(s string) string { return lib_ldap.DecodeSID(s) }, // Var and consts "FilterIsPerson": func() string { return lib_ldap.FilterIsPerson }, diff --git a/pkg/js/libs/ldap/adenum.go b/pkg/js/libs/ldap/adenum.go index 3c1363603a..074a158283 100644 --- a/pkg/js/libs/ldap/adenum.go +++ b/pkg/js/libs/ldap/adenum.go @@ -146,37 +146,6 @@ func (c *LdapClient) GetADUserKerberoastable() ([]ADObject, error) { return c.FindADObjects(JoinFilters(FilterIsPerson, FilterAccountEnabled, FilterHasServicePrincipalName)) } -func decodeSID(b []byte) string { - revisionLvl := int(b[0]) - subAuthorityCount := int(b[1]) & 0xFF - - var authority int - for i := 2; i <= 7; i++ { - authority = authority | int(b[i])<<(8*(5-(i-2))) - } - - var size = 4 - var offset = 8 - var subAuthorities []int - for i := 0; i < subAuthorityCount; i++ { - var subAuthority int - for k := 0; k < size; k++ { - subAuthority = subAuthority | (int(b[offset+k])&0xFF)<<(8*k) - } - subAuthorities = append(subAuthorities, subAuthority) - offset += size - } - - var builder strings.Builder - builder.WriteString("S-") - builder.WriteString(fmt.Sprintf("%d-", revisionLvl)) - builder.WriteString(fmt.Sprintf("%d", authority)) - for _, v := range subAuthorities { - builder.WriteString(fmt.Sprintf("-%d", v)) - } - return builder.String() -} - func (c *LdapClient) GetADDomainSID() (string, error) { r, err := c.Search(FilterServerTrustAccount, "objectSid") if err != nil { @@ -190,5 +159,5 @@ func (c *LdapClient) GetADDomainSID() (string, error) { if len(r[0]["objectSid"]) < 1 { return "", fmt.Errorf("could not grab DomainSID") } - return decodeSID([]byte(r[0]["objectSid"][0])), nil + return DecodeSID(r[0]["objectSid"][0]), nil } diff --git a/pkg/js/libs/ldap/utils.go b/pkg/js/libs/ldap/utils.go new file mode 100644 index 0000000000..0d95de50be --- /dev/null +++ b/pkg/js/libs/ldap/utils.go @@ -0,0 +1,38 @@ +package ldap + +import ( + "fmt" + "strings" +) + +func DecodeSID(s string) string { + b := []byte(s) + revisionLvl := int(b[0]) + subAuthorityCount := int(b[1]) & 0xFF + + var authority int + for i := 2; i <= 7; i++ { + authority = authority | int(b[i])<<(8*(5-(i-2))) + } + + var size = 4 + var offset = 8 + var subAuthorities []int + for i := 0; i < subAuthorityCount; i++ { + var subAuthority int + for k := 0; k < size; k++ { + subAuthority = subAuthority | (int(b[offset+k])&0xFF)<<(8*k) + } + subAuthorities = append(subAuthorities, subAuthority) + offset += size + } + + var builder strings.Builder + builder.WriteString("S-") + builder.WriteString(fmt.Sprintf("%d-", revisionLvl)) + builder.WriteString(fmt.Sprintf("%d", authority)) + for _, v := range subAuthorities { + builder.WriteString(fmt.Sprintf("-%d", v)) + } + return builder.String() +} From 2019dab187449e77160866760d69ebed50e331b2 Mon Sep 17 00:00:00 2001 From: 5amu Date: Sun, 21 Jan 2024 18:14:20 +0100 Subject: [PATCH 14/17] implement utilities for timestamps --- pkg/js/generated/go/libldap/ldap.go | 8 +++++--- pkg/js/libs/ldap/adenum.go | 4 ++-- pkg/js/libs/ldap/utils.go | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/pkg/js/generated/go/libldap/ldap.go b/pkg/js/generated/go/libldap/ldap.go index c45d237a92..cac9b83b97 100644 --- a/pkg/js/generated/go/libldap/ldap.go +++ b/pkg/js/generated/go/libldap/ldap.go @@ -15,9 +15,11 @@ func init() { module.Set( gojs.Objects{ // Functions - "JoinFilters": func(f ...string) string { return lib_ldap.JoinFilters(f...) }, - "NegativeFilter": func(f string) string { return lib_ldap.NegativeFilter(f) }, - "DecodeSID": func(s string) string { return lib_ldap.DecodeSID(s) }, + "JoinFilters": func(f ...string) string { return lib_ldap.JoinFilters(f...) }, + "NegativeFilter": func(f string) string { return lib_ldap.NegativeFilter(f) }, + "DecodeSID": func(s string) string { return lib_ldap.DecodeSID(s) }, + "DecodeADTimestamp": func(s string) string { return lib_ldap.DecodeADTimestamp(s) }, + "DecodeZuluTimestamp": func(s string) string { return lib_ldap.DecodeZuluTimestamp(s) }, // Var and consts "FilterIsPerson": func() string { return lib_ldap.FilterIsPerson }, diff --git a/pkg/js/libs/ldap/adenum.go b/pkg/js/libs/ldap/adenum.go index 074a158283..a5b0c09bb2 100644 --- a/pkg/js/libs/ldap/adenum.go +++ b/pkg/js/libs/ldap/adenum.go @@ -101,8 +101,8 @@ func (c *LdapClient) FindADObjects(filter string) ([]ADObject, error) { objects = append(objects, ADObject{ DistinguishedName: obj.GetAttributeValue("distinguishedName"), SAMAccountName: obj.GetAttributeValue("sAMAccountName"), - PWDLastSet: obj.GetAttributeValue("pwdLastSet"), - LastLogon: obj.GetAttributeValue("lastLogon"), + PWDLastSet: DecodeADTimestamp(obj.GetAttributeValue("pwdLastSet")), + LastLogon: DecodeADTimestamp(obj.GetAttributeValue("lastLogon")), MemberOf: obj.GetAttributeValues("memberOf"), ServicePrincipalName: obj.GetAttributeValues("servicePrincipalName"), }) diff --git a/pkg/js/libs/ldap/utils.go b/pkg/js/libs/ldap/utils.go index 0d95de50be..9bd0f01091 100644 --- a/pkg/js/libs/ldap/utils.go +++ b/pkg/js/libs/ldap/utils.go @@ -2,7 +2,9 @@ package ldap import ( "fmt" + "strconv" "strings" + "time" ) func DecodeSID(s string) string { @@ -36,3 +38,21 @@ func DecodeSID(s string) string { } return builder.String() } + +func DecodeADTimestamp(timestamp string) string { + adtime, _ := strconv.ParseInt(timestamp, 10, 64) + if (adtime == 9223372036854775807) || (adtime == 0) { + return "Not Set" + } + unixtime_int64 := adtime/(10*1000*1000) - 11644473600 + unixtime := time.Unix(unixtime_int64, 0) + return unixtime.Format("2006-01-02 3:4:5 pm") +} + +func DecodeZuluTimestamp(timestamp string) string { + zulu, err := time.Parse(time.RFC3339, timestamp) + if err != nil { + return "" + } + return zulu.Format("2006-01-02 3:4:5 pm") +} From 2f926c4f7236912ecd86e899ebe16a7d2d20f82f Mon Sep 17 00:00:00 2001 From: 5amu Date: Sun, 21 Jan 2024 19:50:33 +0100 Subject: [PATCH 15/17] implement method to close the ldap connection --- pkg/js/libs/ldap/ldap.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go index 2815d10b15..a61eabd261 100644 --- a/pkg/js/libs/ldap/ldap.go +++ b/pkg/js/libs/ldap/ldap.go @@ -201,3 +201,7 @@ func (c *LdapClient) CollectMetadata(domain string, controller string) (Metadata } return metadata, nil } + +func (c *LdapClient) Close() { + c.Conn.Close() +} From fd2ab3ce8a0e98e61391e727dd7588021b4efa8f Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Tue, 6 Feb 2024 03:55:57 +0530 Subject: [PATCH 16/17] refactor to use nucleijs utils --- pkg/js/generated/go/libldap/ldap.go | 6 +- pkg/js/libs/kerberos/kerberosx.go | 4 +- pkg/js/libs/ldap/adenum.go | 88 +++++---- pkg/js/libs/ldap/ldap.go | 287 ++++++++++++++++++---------- 4 files changed, 251 insertions(+), 134 deletions(-) diff --git a/pkg/js/generated/go/libldap/ldap.go b/pkg/js/generated/go/libldap/ldap.go index cac9b83b97..ffb2aeaa8b 100644 --- a/pkg/js/generated/go/libldap/ldap.go +++ b/pkg/js/generated/go/libldap/ldap.go @@ -52,11 +52,11 @@ func init() { // Types (value type) "Metadata": func() lib_ldap.Metadata { return lib_ldap.Metadata{} }, - "LdapClient": func() lib_ldap.LdapClient { return lib_ldap.LdapClient{} }, + "LdapClient": lib_ldap.NewClient, // Types (pointer type) - "NewMetadata": func() *lib_ldap.Metadata { return &lib_ldap.Metadata{} }, - "NewLdapClient": func() *lib_ldap.LdapClient { return &lib_ldap.LdapClient{} }, + // "NewMetadata": func() *lib_ldap.Metadata { return &lib_ldap.Metadata{} }, + // "NewLdapClient": func() *lib_ldap.LdapClient { return &lib_ldap.LdapClient{} }, }, ).Register() } diff --git a/pkg/js/libs/kerberos/kerberosx.go b/pkg/js/libs/kerberos/kerberosx.go index b9c95e06f1..7d2be507f2 100644 --- a/pkg/js/libs/kerberos/kerberosx.go +++ b/pkg/js/libs/kerberos/kerberosx.go @@ -53,7 +53,7 @@ func (c *Config) SetTimeout(timeout int) *Config { // DomainController: dc.acme.com (Domain Controller / Active Directory Server) // KDC: kdc.acme.com (Key Distribution Center / Authentication Server) -// Updated Package definations and structure +// Client is kerberos client type Client struct { nj *utils.NucleiJS // helper functions/bindings Krb5Config *kconfig.Config @@ -71,7 +71,7 @@ type Client struct { func NewKerberosClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object { // setup nucleijs utils c := &Client{nj: utils.NewNucleiJS(runtime)} - c.nj.ObjectSig = "Client(domain, controller)" // will be included in error messages + c.nj.ObjectSig = "Client(domain, {controller})" // will be included in error messages // get arguments (type assertion is efficient than reflection) // when accepting type as input like net.Conn we can use utils.GetArg diff --git a/pkg/js/libs/ldap/adenum.go b/pkg/js/libs/ldap/adenum.go index a5b0c09bb2..9222a132d5 100644 --- a/pkg/js/libs/ldap/adenum.go +++ b/pkg/js/libs/ldap/adenum.go @@ -48,6 +48,7 @@ const ( ) +// JoinFilters joins multiple filters into a single filter func JoinFilters(filters ...string) string { var builder strings.Builder builder.WriteString("(&") @@ -58,10 +59,12 @@ func JoinFilters(filters ...string) string { return builder.String() } +// NegativeFilter returns a negative filter for a given filter func NegativeFilter(filter string) string { return fmt.Sprintf("(!%s)", filter) } +// ADObject represents an Active Directory object type ADObject struct { DistinguishedName string SAMAccountName string @@ -71,7 +74,12 @@ type ADObject struct { ServicePrincipalName []string } -func (c *LdapClient) FindADObjects(filter string) ([]ADObject, error) { +// FindADObjects finds AD objects based on a filter +// and returns them as a list of ADObject +// @param filter: string +// @return []ADObject +func (c *Client) FindADObjects(filter string) []ADObject { + c.nj.Require(c.conn != nil, "no existing connection") sr := ldap.NewSearchRequest( c.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, @@ -87,14 +95,8 @@ func (c *LdapClient) FindADObjects(filter string) ([]ADObject, error) { nil, ) - res, err := c.Conn.Search(sr) - if err != nil { - return nil, err - } - - if len(res.Entries) == 0 { - return nil, fmt.Errorf("no object returned from query") - } + res, err := c.conn.Search(sr) + c.nj.HandleError(err, "ldap search request failed") var objects []ADObject for _, obj := range res.Entries { @@ -107,57 +109,77 @@ func (c *LdapClient) FindADObjects(filter string) ([]ADObject, error) { ServicePrincipalName: obj.GetAttributeValues("servicePrincipalName"), }) } - return objects, nil + return objects } -func (c *LdapClient) GetADUsers() ([]ADObject, error) { +// GetADUsers returns all AD users +// using FilterIsPerson filter query +// @return []ADObject +func (c *Client) GetADUsers() []ADObject { return c.FindADObjects(FilterIsPerson) } -func (c *LdapClient) GetADActiveUsers() ([]ADObject, error) { +// GetADActiveUsers returns all AD users +// using FilterIsPerson and FilterAccountEnabled filter query +// @return []ADObject +func (c *Client) GetADActiveUsers() []ADObject { return c.FindADObjects(JoinFilters(FilterIsPerson, FilterAccountEnabled)) } -func (c *LdapClient) GetADUserWithNeverExpiringPasswords() ([]ADObject, error) { +// GetAdUserWithNeverExpiringPasswords returns all AD users +// using FilterIsPerson and FilterDontExpirePassword filter query +// @return []ADObject +func (c *Client) GetADUserWithNeverExpiringPasswords() []ADObject { return c.FindADObjects(JoinFilters(FilterIsPerson, FilterDontExpirePassword)) } -func (c *LdapClient) GetADUserTrustedForDelegation() ([]ADObject, error) { +// GetADUserTrustedForDelegation returns all AD users that are trusted for delegation +// using FilterIsPerson and FilterTrustedForDelegation filter query +// @return []ADObject +func (c *Client) GetADUserTrustedForDelegation() []ADObject { return c.FindADObjects(JoinFilters(FilterIsPerson, FilterTrustedForDelegation)) } -func (c *LdapClient) GetADUserWithPasswordNotRequired() ([]ADObject, error) { +// GetADUserWithPasswordNotRequired returns all AD users that do not require a password +// using FilterIsPerson and FilterPasswordNotRequired filter query +// @return []ADObject +func (c *Client) GetADUserWithPasswordNotRequired() []ADObject { return c.FindADObjects(JoinFilters(FilterIsPerson, FilterPasswordNotRequired)) } -func (c *LdapClient) GetADGroups() ([]ADObject, error) { +// GetADGroups returns all AD groups +// using FilterIsGroup filter query +// @return []ADObject +func (c *Client) GetADGroups() []ADObject { return c.FindADObjects(FilterIsGroup) } -func (c *LdapClient) GetADDCList() ([]ADObject, error) { +// GetADDCList returns all AD domain controllers +// using FilterIsComputer, FilterAccountEnabled and FilterServerTrustAccount filter query +// @return []ADObject +func (c *Client) GetADDCList() []ADObject { return c.FindADObjects(JoinFilters(FilterIsComputer, FilterAccountEnabled, FilterServerTrustAccount)) } -func (c *LdapClient) GetADAdmins() ([]ADObject, error) { +// GetADAdmins returns all AD admins +// using FilterIsPerson, FilterAccountEnabled and FilterIsAdmin filter query +// @return []ADObject +func (c *Client) GetADAdmins() []ADObject { return c.FindADObjects(JoinFilters(FilterIsPerson, FilterAccountEnabled, FilterIsAdmin)) } -func (c *LdapClient) GetADUserKerberoastable() ([]ADObject, error) { +// GetADUserKerberoastable returns all AD users that are kerberoastable +// using FilterIsPerson, FilterAccountEnabled and FilterHasServicePrincipalName filter query +// @return []ADObject +func (c *Client) GetADUserKerberoastable() []ADObject { return c.FindADObjects(JoinFilters(FilterIsPerson, FilterAccountEnabled, FilterHasServicePrincipalName)) } -func (c *LdapClient) GetADDomainSID() (string, error) { - r, err := c.Search(FilterServerTrustAccount, "objectSid") - if err != nil { - return "", err - } - - if len(r) < 1 { - return "", fmt.Errorf("no result from GetADDomainSID query") - } - - if len(r[0]["objectSid"]) < 1 { - return "", fmt.Errorf("could not grab DomainSID") - } - return DecodeSID(r[0]["objectSid"][0]), nil +// GetADDomainSID returns the SID of the AD domain +// @return string +func (c *Client) GetADDomainSID() string { + r := c.Search(FilterServerTrustAccount, "objectSid") + c.nj.Require(len(r) > 0, "no result from GetADDomainSID query") + c.nj.Require(len(r[0]["objectSid"]) > 0, "could not grab DomainSID") + return DecodeSID(r[0]["objectSid"][0]) } diff --git a/pkg/js/libs/ldap/ldap.go b/pkg/js/libs/ldap/ldap.go index a61eabd261..3381859009 100644 --- a/pkg/js/libs/ldap/ldap.go +++ b/pkg/js/libs/ldap/ldap.go @@ -1,128 +1,194 @@ package ldap import ( + "context" "crypto/tls" "fmt" + "net" + "net/url" "strings" + "github.com/dop251/goja" "github.com/go-ldap/ldap/v3" + "github.com/projectdiscovery/nuclei/v3/pkg/js/utils" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" ) -// Client is a client for ldap protocol in golang. -// -// It is a wrapper around the standard library ldap package. -type LdapClient struct { - BaseDN string - Realm string - Host string - Conn *ldap.Conn - Port int - UseSSL bool - TLS bool +// Client is a client for ldap protocol in nuclei +type Client struct { + Host string // Hostname + Port int // Port + Realm string // Realm + BaseDN string // BaseDN (generated from Realm) + + // unexported + nj *utils.NucleiJS // nuclei js utils + conn *ldap.Conn + cfg Config } -// Connect is a method for LdapClient that stores information about of the ldap -// connection, tests it and verifies that the server is a valid ldap server -// -// returns the success status -func (c *LdapClient) Connect(host string, port int, ssl, istls bool) (bool, error) { - if c.Conn != nil { - return true, nil - } +// Config is extra configuration for the ldap client +type Config struct { + // Timeout is the timeout for the ldap client in seconds + Timeout int + ServerName string // default to host (when using tls) + Upgrade bool // when true first connects to non-tls and then upgrades to tls +} - if !protocolstate.IsHostAllowed(host) { - // host is not valid according to network policy - return false, protocolstate.ErrHostDenied.Msgf(host) - } +// Constructor for creating a new ldap client +// The following schemas are supported for url: ldap://, ldaps://, ldapi://, +// and cldap:// (RFC1798, deprecated but used by Active Directory). +// ldaps uses TLS/SSL, ldapi uses a Unix domain socket, and cldap uses connectionless LDAP. +// Signature: Client(ldapUrl,Realm) +// @param ldapUrl: string +// @param Realm: string +// @param Config: Config +// @return Client +// @throws error when the ldap url is invalid or connection fails +func NewClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object { + // setup nucleijs utils + c := &Client{nj: utils.NewNucleiJS(runtime)} + c.nj.ObjectSig = "Client(ldapUrl,Realm,{Config})" // will be included in error messages + + // get arguments (type assertion is efficient than reflection) + ldapUrl, _ := c.nj.GetArg(call.Arguments, 0).(string) + realm, _ := c.nj.GetArg(call.Arguments, 1).(string) + c.cfg = utils.GetStructTypeSafe[Config](c.nj, call.Arguments, 2, Config{}) - var err error - if ssl { - config := &tls.Config{ - InsecureSkipVerify: true, - ServerName: host, + // validate arguments + c.nj.Require(ldapUrl != "", "ldap url cannot be empty") + c.nj.Require(realm != "", "realm cannot be empty") + + u, err := url.Parse(ldapUrl) + c.nj.HandleError(err, "invalid ldap url supported schemas are ldap://, ldaps://, ldapi://, and cldap://") + + var conn net.Conn + if u.Scheme == "ldapi" { + if u.Path == "" || u.Path == "/" { + u.Path = "/var/run/slapd/ldapi" } - c.Conn, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", host, port), config) + conn, err = protocolstate.Dialer.Dial(context.TODO(), "unix", u.Path) + c.nj.HandleError(err, "failed to connect to ldap server") } else { - c.Conn, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port)) - } - if err != nil { - return false, err - } + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + // we assume that error is due to missing port + host = u.Host + port = "" + } + if u.Scheme == "" { + // default to ldap + u.Scheme = "ldap" + } - if istls && !ssl { - // Here if it is not a valid ldap server, the StartTLS will return an error, - // so, if this check succeeds, there is no need to check if the host is has an LDAP Server: - // https://github.com/go-ldap/ldap/blob/cdb0754f666833c3e287503ed52d535a41ba10f6/v3/conn.go#L334 - if err := c.Conn.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { - return false, err + switch u.Scheme { + case "cldap": + if port == "" { + port = ldap.DefaultLdapPort + } + conn, err = protocolstate.Dialer.Dial(context.TODO(), "udp", net.JoinHostPort(host, port)) + case "ldap": + if port == "" { + port = ldap.DefaultLdapPort + } + conn, err = protocolstate.Dialer.Dial(context.TODO(), "tcp", net.JoinHostPort(host, port)) + case "ldaps": + if port == "" { + port = ldap.DefaultLdapsPort + } + serverName := host + if c.cfg.ServerName != "" { + serverName = c.cfg.ServerName + } + conn, err = protocolstate.Dialer.DialTLSWithConfig(context.TODO(), "tcp", net.JoinHostPort(host, port), + &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS10, ServerName: serverName}) + default: + err = fmt.Errorf("unsupported ldap url schema %v", u.Scheme) } + c.nj.HandleError(err, "failed to connect to ldap server") + } + c.conn = ldap.NewConn(conn, u.Scheme == "ldaps") + if u.Scheme != "ldaps" && c.cfg.Upgrade { + serverName := u.Hostname() + if c.cfg.ServerName != "" { + serverName = c.cfg.ServerName + } + if err := c.conn.StartTLS(&tls.Config{InsecureSkipVerify: true, ServerName: serverName}); err != nil { + c.nj.HandleError(err, "failed to upgrade to tls") + } + } else { + c.conn.Start() } - c.Host = host - c.Port = port - c.TLS = istls - c.UseSSL = ssl - return true, nil + return utils.LinkConstructor(call, runtime, c) } -func (c *LdapClient) Authenticate(realm string, username, password string) (bool, error) { - if c.Conn == nil { - return false, fmt.Errorf("no existing connection") +// Authenticate authenticates with the ldap server using the given username and password +// performs NTLMBind first and then Bind/UnauthenticatedBind if NTLMBind fails +// Signature: Authenticate(username, password) +// @param username: string +// @param password: string (can be empty for unauthenticated bind) +// @throws error if authentication fails +func (c *Client) Authenticate(username, password string) { + c.nj.Require(c.conn != nil, "no existing connection") + if c.BaseDN == "" { + c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc=")) } - - c.Realm = realm - c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(realm, "."), ",dc=")) - - if err := c.Conn.NTLMBind(realm, username, password); err == nil { + if err := c.conn.NTLMBind(c.Realm, username, password); err == nil { // if bind with NTLMBind(), there is nothing // else to do, you are authenticated - return true, nil + return } switch password { case "": - if err := c.Conn.UnauthenticatedBind(username); err != nil { - return false, err + if err := c.conn.UnauthenticatedBind(username); err != nil { + c.nj.ThrowError(err) } default: - if err := c.Conn.Bind(username, password); err != nil { - return false, err + if err := c.conn.Bind(username, password); err != nil { + c.nj.ThrowError(err) } } - return true, nil } -func (c *LdapClient) AuthenticateWithNTLMHash(realm string, username, hash string) (bool, error) { - if c.Conn == nil { - return false, fmt.Errorf("no existing connection") +// AuthenticateWithNTLMHash authenticates with the ldap server using the given username and NTLM hash +// Signature: AuthenticateWithNTLMHash(username, hash) +// @param username: string +// @param hash: string +// @throws error if authentication fails +func (c *Client) AuthenticateWithNTLMHash(username, hash string) { + c.nj.Require(c.conn != nil, "no existing connection") + if c.BaseDN == "" { + c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc=")) } - c.Realm = realm - c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(realm, "."), ",dc=")) - if err := c.Conn.NTLMBindWithHash(realm, username, hash); err != nil { - return false, err + if err := c.conn.NTLMBindWithHash(c.Realm, username, hash); err != nil { + c.nj.ThrowError(err) } - return true, nil } -// Search is a method that uses the already Connect()'ed client to query the LDAP -// server, works for openldap and for Microsoft's Active Directory Ldap -// -// accepts whatever filter and returns a list of maps having provided attributes +// Search accepts whatever filter and returns a list of maps having provided attributes // as keys and associated values mirroring the ones returned by ldap -func (c *LdapClient) Search(filter string, attributes ...string) ([]map[string][]string, error) { - res, err := c.Conn.Search(ldap.NewSearchRequest( - c.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, - 0, 0, false, filter, attributes, nil, - )) - if err != nil { - return nil, err - } +// Signature: Search(filter, attributes...) +// @param filter: string +// @param attributes: ...string +// @return []map[string][]string +func (c *Client) Search(filter string, attributes ...string) []map[string][]string { + c.nj.Require(c.conn != nil, "no existing connection") + res, err := c.conn.Search( + ldap.NewSearchRequest( + c.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, + 0, 0, false, filter, attributes, nil, + ), + ) + c.nj.HandleError(err, "ldap search request failed") if len(res.Entries) == 0 { - return nil, fmt.Errorf("no result found in search") + // return empty list + return nil } + // convert ldap.Entry to []map[string][]string var out []map[string][]string for _, r := range res.Entries { app := make(map[string][]string) @@ -138,7 +204,36 @@ func (c *LdapClient) Search(filter string, attributes ...string) ([]map[string][ out = append(out, app) } } - return out, nil + return out +} + +// AdvancedSearch accepts all values of search request type and return Ldap Entry +// its up to user to handle the response +// Signature: AdvancedSearch(Scope, DerefAliases, SizeLimit, TimeLimit, TypesOnly, Filter, Attributes, Controls) +// @param Scope: int +// @param DerefAliases: int +// @param SizeLimit: int +// @param TimeLimit: int +// @param TypesOnly: bool +// @param Filter: string +// @param Attributes: []string +// @param Controls: []ldap.Control +// @return ldap.SearchResult +func (c *Client) AdvancedSearch( + Scope, DerefAliases, SizeLimit, TimeLimit int, + TypesOnly bool, + Filter string, + Attributes []string, + Controls []ldap.Control) ldap.SearchResult { + c.nj.Require(c.conn != nil, "no existing connection") + if c.BaseDN == "" { + c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc=")) + } + req := ldap.NewSearchRequest(c.BaseDN, Scope, DerefAliases, SizeLimit, TimeLimit, TypesOnly, Filter, Attributes, Controls) + res, err := c.conn.Search(req) + c.nj.HandleError(err, "ldap search request failed") + c.nj.Require(res != nil, "ldap search request failed got nil response") + return *res } // Metadata is the metadata for ldap server. @@ -153,16 +248,16 @@ type Metadata struct { } // CollectLdapMetadata collects metadata from ldap server. -func (c *LdapClient) CollectMetadata(domain string, controller string) (Metadata, error) { - if c.Conn == nil { - return Metadata{}, fmt.Errorf("no existing connection") - } - defer c.Conn.Close() - +// Signature: CollectMetadata(domain, controller) +// @return Metadata +func (c *Client) CollectMetadata() Metadata { + c.nj.Require(c.conn != nil, "no existing connection") var metadata Metadata - - metadata.BaseDN = c.BaseDN metadata.Domain = c.Realm + if c.BaseDN == "" { + c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc=")) + } + metadata.BaseDN = c.BaseDN srMetadata := ldap.NewSearchRequest( "", @@ -178,10 +273,9 @@ func (c *LdapClient) CollectMetadata(domain string, controller string) (Metadata "dnsHostName", }, nil) - resMetadata, err := c.Conn.Search(srMetadata) - if err != nil { - return metadata, err - } + resMetadata, err := c.conn.Search(srMetadata) + c.nj.HandleError(err, "ldap search request failed") + for _, entry := range resMetadata.Entries { for _, attr := range entry.Attributes { value := entry.GetAttributeValue(attr.Name) @@ -199,9 +293,10 @@ func (c *LdapClient) CollectMetadata(domain string, controller string) (Metadata } } } - return metadata, nil + return metadata } -func (c *LdapClient) Close() { - c.Conn.Close() +// close the ldap connection +func (c *Client) Close() { + c.conn.Close() } From 66bc616fd2e29c22f75b516b14a07d7c8f179707 Mon Sep 17 00:00:00 2001 From: Tarun Koyalwar Date: Tue, 6 Feb 2024 04:02:53 +0530 Subject: [PATCH 17/17] update js proto bindings --- pkg/js/generated/go/libldap/ldap.go | 76 +++++++++++++++-------------- pkg/js/libs/ldap/utils.go | 4 ++ 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/pkg/js/generated/go/libldap/ldap.go b/pkg/js/generated/go/libldap/ldap.go index ffb2aeaa8b..32ac0d4f5b 100644 --- a/pkg/js/generated/go/libldap/ldap.go +++ b/pkg/js/generated/go/libldap/ldap.go @@ -15,48 +15,52 @@ func init() { module.Set( gojs.Objects{ // Functions - "JoinFilters": func(f ...string) string { return lib_ldap.JoinFilters(f...) }, - "NegativeFilter": func(f string) string { return lib_ldap.NegativeFilter(f) }, - "DecodeSID": func(s string) string { return lib_ldap.DecodeSID(s) }, - "DecodeADTimestamp": func(s string) string { return lib_ldap.DecodeADTimestamp(s) }, - "DecodeZuluTimestamp": func(s string) string { return lib_ldap.DecodeZuluTimestamp(s) }, + "DecodeADTimestamp": lib_ldap.DecodeADTimestamp, + "DecodeSID": lib_ldap.DecodeSID, + "DecodeZuluTimestamp": lib_ldap.DecodeZuluTimestamp, + "JoinFilters": lib_ldap.JoinFilters, + "NegativeFilter": lib_ldap.NegativeFilter, // Var and consts - "FilterIsPerson": func() string { return lib_ldap.FilterIsPerson }, - "FilterIsGroup": func() string { return lib_ldap.FilterIsGroup }, - "FilterIsComputer": func() string { return lib_ldap.FilterIsComputer }, - "FilterIsAdmin": func() string { return lib_ldap.FilterIsAdmin }, - "FilterLogonScript": func() string { return lib_ldap.FilterLogonScript }, - "FilterAccountDisabled": func() string { return lib_ldap.FilterAccountDisabled }, - "FilterAccountEnabled": func() string { return lib_ldap.FilterAccountEnabled }, - "FilterHomedirRequired": func() string { return lib_ldap.FilterHomedirRequired }, - "FilterLockout": func() string { return lib_ldap.FilterLockout }, - "FilterPasswordNotRequired": func() string { return lib_ldap.FilterPasswordNotRequired }, - "FilterPasswordCantChange": func() string { return lib_ldap.FilterPasswordCantChange }, - "FilterCanSendEncryptedPassword": func() string { return lib_ldap.FilterCanSendEncryptedPassword }, - "FilterIsDuplicateAccount": func() string { return lib_ldap.FilterIsDuplicateAccount }, - "FilterIsNormalAccount": func() string { return lib_ldap.FilterIsNormalAccount }, - "FilterInterdomainTrustAccount": func() string { return lib_ldap.FilterInterdomainTrustAccount }, - "FilterWorkstationTrustAccount": func() string { return lib_ldap.FilterWorkstationTrustAccount }, - "FilterServerTrustAccount": func() string { return lib_ldap.FilterServerTrustAccount }, - "FilterDontExpirePassword": func() string { return lib_ldap.FilterDontExpirePassword }, - "FilterMnsLogonAccount": func() string { return lib_ldap.FilterMnsLogonAccount }, - "FilterSmartCardRequired": func() string { return lib_ldap.FilterSmartCardRequired }, - "FilterTrustedForDelegation": func() string { return lib_ldap.FilterTrustedForDelegation }, - "FilterNotDelegated": func() string { return lib_ldap.FilterNotDelegated }, - "FilterUseDesKeyOnly": func() string { return lib_ldap.FilterUseDesKeyOnly }, - "FilterDontRequirePreauth": func() string { return lib_ldap.FilterDontRequirePreauth }, - "FilterPasswordExpired": func() string { return lib_ldap.FilterPasswordExpired }, - "FilterTrustedToAuthForDelegation": func() string { return lib_ldap.FilterTrustedToAuthForDelegation }, - "FilterPartialSecretsAccount": func() string { return lib_ldap.FilterPartialSecretsAccount }, + "FilterAccountDisabled": lib_ldap.FilterAccountDisabled, + "FilterAccountEnabled": lib_ldap.FilterAccountEnabled, + "FilterCanSendEncryptedPassword": lib_ldap.FilterCanSendEncryptedPassword, + "FilterDontExpirePassword": lib_ldap.FilterDontExpirePassword, + "FilterDontRequirePreauth": lib_ldap.FilterDontRequirePreauth, + "FilterHasServicePrincipalName": lib_ldap.FilterHasServicePrincipalName, + "FilterHomedirRequired": lib_ldap.FilterHomedirRequired, + "FilterInterdomainTrustAccount": lib_ldap.FilterInterdomainTrustAccount, + "FilterIsAdmin": lib_ldap.FilterIsAdmin, + "FilterIsComputer": lib_ldap.FilterIsComputer, + "FilterIsDuplicateAccount": lib_ldap.FilterIsDuplicateAccount, + "FilterIsGroup": lib_ldap.FilterIsGroup, + "FilterIsNormalAccount": lib_ldap.FilterIsNormalAccount, + "FilterIsPerson": lib_ldap.FilterIsPerson, + "FilterLockout": lib_ldap.FilterLockout, + "FilterLogonScript": lib_ldap.FilterLogonScript, + "FilterMnsLogonAccount": lib_ldap.FilterMnsLogonAccount, + "FilterNotDelegated": lib_ldap.FilterNotDelegated, + "FilterPartialSecretsAccount": lib_ldap.FilterPartialSecretsAccount, + "FilterPasswordCantChange": lib_ldap.FilterPasswordCantChange, + "FilterPasswordExpired": lib_ldap.FilterPasswordExpired, + "FilterPasswordNotRequired": lib_ldap.FilterPasswordNotRequired, + "FilterServerTrustAccount": lib_ldap.FilterServerTrustAccount, + "FilterSmartCardRequired": lib_ldap.FilterSmartCardRequired, + "FilterTrustedForDelegation": lib_ldap.FilterTrustedForDelegation, + "FilterTrustedToAuthForDelegation": lib_ldap.FilterTrustedToAuthForDelegation, + "FilterUseDesKeyOnly": lib_ldap.FilterUseDesKeyOnly, + "FilterWorkstationTrustAccount": lib_ldap.FilterWorkstationTrustAccount, // Types (value type) - "Metadata": func() lib_ldap.Metadata { return lib_ldap.Metadata{} }, - "LdapClient": lib_ldap.NewClient, + "ADObject": func() lib_ldap.ADObject { return lib_ldap.ADObject{} }, + "Client": lib_ldap.NewClient, + "Config": func() lib_ldap.Config { return lib_ldap.Config{} }, + "Metadata": func() lib_ldap.Metadata { return lib_ldap.Metadata{} }, // Types (pointer type) - // "NewMetadata": func() *lib_ldap.Metadata { return &lib_ldap.Metadata{} }, - // "NewLdapClient": func() *lib_ldap.LdapClient { return &lib_ldap.LdapClient{} }, + "NewADObject": func() *lib_ldap.ADObject { return &lib_ldap.ADObject{} }, + "NewConfig": func() *lib_ldap.Config { return &lib_ldap.Config{} }, + "NewMetadata": func() *lib_ldap.Metadata { return &lib_ldap.Metadata{} }, }, ).Register() } diff --git a/pkg/js/libs/ldap/utils.go b/pkg/js/libs/ldap/utils.go index 9bd0f01091..3c20a411c0 100644 --- a/pkg/js/libs/ldap/utils.go +++ b/pkg/js/libs/ldap/utils.go @@ -7,6 +7,7 @@ import ( "time" ) +// DecodeSID decodes a SID string func DecodeSID(s string) string { b := []byte(s) revisionLvl := int(b[0]) @@ -39,6 +40,7 @@ func DecodeSID(s string) string { return builder.String() } +// DecodeADTimestamp decodes an Active Directory timestamp func DecodeADTimestamp(timestamp string) string { adtime, _ := strconv.ParseInt(timestamp, 10, 64) if (adtime == 9223372036854775807) || (adtime == 0) { @@ -49,6 +51,8 @@ func DecodeADTimestamp(timestamp string) string { return unixtime.Format("2006-01-02 3:4:5 pm") } +// DecodeZuluTimestamp decodes a Zulu timestamp +// example: 2021-08-25T14:00:00Z func DecodeZuluTimestamp(timestamp string) string { zulu, err := time.Parse(time.RFC3339, timestamp) if err != nil {