mirror of https://github.com/daffainfo/nuclei.git
commit
7647af1722
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
Loading…
Reference in New Issue