Merge pull request #4667 from 5amu/ldap-protocol-enhancements

ldap protocol enhancements
dev
Tarun Koyalwar 2024-02-06 04:28:50 +05:30 committed by GitHub
commit 7647af1722
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 521 additions and 241 deletions

View File

@ -15,16 +15,52 @@ func init() {
module.Set(
gojs.Objects{
// Functions
"DecodeADTimestamp": lib_ldap.DecodeADTimestamp,
"DecodeSID": lib_ldap.DecodeSID,
"DecodeZuluTimestamp": lib_ldap.DecodeZuluTimestamp,
"JoinFilters": lib_ldap.JoinFilters,
"NegativeFilter": lib_ldap.NegativeFilter,
// Var and consts
"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)
"LDAPMetadata": func() lib_ldap.LDAPMetadata { return lib_ldap.LDAPMetadata{} },
"LdapClient": func() lib_ldap.LdapClient { return lib_ldap.LdapClient{} },
"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)
"NewLDAPMetadata": func() *lib_ldap.LDAPMetadata { return &lib_ldap.LDAPMetadata{} },
"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()
}

View File

@ -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

185
pkg/js/libs/ldap/adenum.go Normal file
View File

@ -0,0 +1,185 @@
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).
)
// JoinFilters joins multiple filters into a single filter
func JoinFilters(filters ...string) string {
var builder strings.Builder
builder.WriteString("(&")
for _, s := range filters {
builder.WriteString(s)
}
builder.WriteString(")")
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
PWDLastSet string
LastLogon string
MemberOf []string
ServicePrincipalName []string
}
// 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,
filter,
[]string{
"distinguishedName",
"sAMAccountName",
"pwdLastSet",
"lastLogon",
"memberOf",
"servicePrincipalName",
},
nil,
)
res, err := c.conn.Search(sr)
c.nj.HandleError(err, "ldap search request failed")
var objects []ADObject
for _, obj := range res.Entries {
objects = append(objects, ADObject{
DistinguishedName: obj.GetAttributeValue("distinguishedName"),
SAMAccountName: obj.GetAttributeValue("sAMAccountName"),
PWDLastSet: DecodeADTimestamp(obj.GetAttributeValue("pwdLastSet")),
LastLogon: DecodeADTimestamp(obj.GetAttributeValue("lastLogon")),
MemberOf: obj.GetAttributeValues("memberOf"),
ServicePrincipalName: obj.GetAttributeValues("servicePrincipalName"),
})
}
return objects
}
// GetADUsers returns all AD users
// using FilterIsPerson filter query
// @return []ADObject
func (c *Client) GetADUsers() []ADObject {
return c.FindADObjects(FilterIsPerson)
}
// GetADActiveUsers returns all AD users
// using FilterIsPerson and FilterAccountEnabled filter query
// @return []ADObject
func (c *Client) GetADActiveUsers() []ADObject {
return c.FindADObjects(JoinFilters(FilterIsPerson, FilterAccountEnabled))
}
// GetAdUserWithNeverExpiringPasswords returns all AD users
// using FilterIsPerson and FilterDontExpirePassword filter query
// @return []ADObject
func (c *Client) GetADUserWithNeverExpiringPasswords() []ADObject {
return c.FindADObjects(JoinFilters(FilterIsPerson, FilterDontExpirePassword))
}
// 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))
}
// 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))
}
// GetADGroups returns all AD groups
// using FilterIsGroup filter query
// @return []ADObject
func (c *Client) GetADGroups() []ADObject {
return c.FindADObjects(FilterIsGroup)
}
// 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))
}
// 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))
}
// 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))
}
// 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])
}

View File

@ -2,106 +2,242 @@ package ldap
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/url"
"strings"
"time"
"github.com/dop251/goja"
"github.com/go-ldap/ldap/v3"
"github.com/praetorian-inc/fingerprintx/pkg/plugins"
"github.com/projectdiscovery/nuclei/v3/pkg/js/utils"
"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.
//
// It is a wrapper around the standard library ldap package.
type LdapClient struct{}
// 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)
// IsLdap checks if the given host and port are running ldap server.
func (c *LdapClient) IsLdap(host string, port int) (bool, error) {
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
}
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
}
return true, nil
// unexported
nj *utils.NucleiJS // nuclei js utils
conn *ldap.Conn
cfg Config
}
// 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(domain) {
// host is not valid according to network policy
return LDAPMetadata{}, protocolstate.ErrHostDenied.Msgf(domain)
}
conn, err := c.newLdapSession(opts)
if err != nil {
return LDAPMetadata{}, err
}
defer c.close(conn)
return c.collectLdapMetadata(conn, opts)
// 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
}
type ldapSessionOptions struct {
domain string
domainController string
port int
username string
password string
baseDN string
}
// 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
func (c *LdapClient) newLdapSession(opts *ldapSessionOptions) (*ldap.Conn, error) {
port := opts.port
dc := opts.domainController
if port == 0 {
port = 389
// 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{})
// 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"
}
conn, err = protocolstate.Dialer.Dial(context.TODO(), "unix", u.Path)
c.nj.HandleError(err, "failed to connect to ldap server")
} else {
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"
}
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()
}
conn, err := protocolstate.Dialer.Dial(context.TODO(), "tcp", fmt.Sprintf("%s:%d", dc, port))
if err != nil {
return nil, err
return utils.LinkConstructor(call, runtime, c)
}
// 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="))
}
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
}
lConn := ldap.NewConn(conn, false)
lConn.Start()
return lConn, nil
switch password {
case "":
if err := c.conn.UnauthenticatedBind(username); err != nil {
c.nj.ThrowError(err)
}
default:
if err := c.conn.Bind(username, password); err != nil {
c.nj.ThrowError(err)
}
}
}
func (c *LdapClient) close(conn *ldap.Conn) {
conn.Close()
// 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="))
}
if err := c.conn.NTLMBindWithHash(c.Realm, username, hash); err != nil {
c.nj.ThrowError(err)
}
}
// LDAPMetadata is the metadata for ldap server.
type LDAPMetadata struct {
// Search accepts whatever filter and returns a list of maps having provided attributes
// as keys and associated values mirroring the ones returned by ldap
// 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 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)
empty := true
for _, a := range attributes {
v := r.GetAttributeValues(a)
if len(v) > 0 {
app[a] = v
empty = false
}
}
if !empty {
out = append(out, app)
}
}
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.
type Metadata struct {
BaseDN string
Domain string
DefaultNamingContext string
@ -111,23 +247,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)
// CollectLdapMetadata collects metadata from ldap server.
// Signature: CollectMetadata(domain, controller)
// @return Metadata
func (c *Client) CollectMetadata() Metadata {
c.nj.Require(c.conn != nil, "no existing connection")
var metadata Metadata
metadata.Domain = c.Realm
if c.BaseDN == "" {
c.BaseDN = fmt.Sprintf("dc=%s", strings.Join(strings.Split(c.Realm, "."), ",dc="))
}
if err != nil {
return metadata, err
}
baseDN, _ := getBaseNamingContext(opts, lConn)
metadata.BaseDN = baseDN
metadata.Domain = parseDC(baseDN)
metadata.BaseDN = c.BaseDN
srMetadata := ldap.NewSearchRequest(
"",
@ -143,10 +273,9 @@ func (c *LdapClient) collectLdapMetadata(lConn *ldap.Conn, opts *ldapSessionOpti
"dnsHostName",
},
nil)
resMetadata, err := lConn.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)
@ -164,142 +293,10 @@ func (c *LdapClient) collectLdapMetadata(lConn *ldap.Conn, opts *ldapSessionOpti
}
}
}
return metadata, nil
return metadata
}
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 {
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
// close the ldap connection
func (c *Client) Close() {
c.conn.Close()
}

62
pkg/js/libs/ldap/utils.go Normal file
View File

@ -0,0 +1,62 @@
package ldap
import (
"fmt"
"strconv"
"strings"
"time"
)
// DecodeSID decodes a SID string
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()
}
// DecodeADTimestamp decodes an Active Directory timestamp
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")
}
// 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 {
return ""
}
return zulu.Format("2006-01-02 3:4:5 pm")
}