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

implement GetKerberoastableUsers in ldap module #4420

Merged
merged 2 commits into from
Jan 12, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions pkg/js/libs/ldap/ldap.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,103 @@ func getBaseNamingContext(opts *ldapSessionOptions, conn *ldap.Conn) (string, er
opts.baseDN = defaultNamingContext
return opts.baseDN, 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) {
opts := &ldapSessionOptions{
domain: domain,
domainController: controller,
username: username,
password: password,
}

if !protocolstate.IsHostAllowed(domain) {
// host is not valid according to network policy
return nil, protocolstate.ErrHostDenied.Msgf(domain)
}

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(),
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 := 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
}
Loading