From 3c5c054124b9bb2eee90fc67278fdff9ebf73278 Mon Sep 17 00:00:00 2001 From: Lars Karlslund Date: Wed, 14 Feb 2024 15:11:47 +0100 Subject: [PATCH] Added native Windows LDAP collection for Windows builds --- go.mod | 11 +- go.sum | 13 +- .../activedirectory/collect/cli.go | 27 +- .../activedirectory/collect/ldap_common.go | 19 +- .../collect/ldap_multiplatform.go | 24 +- .../collect/ldap_native_enums_windows.go | 160 ++++ .../activedirectory/collect/ldap_windows.go | 887 ++++++++++++++++++ readme.MD | 36 +- 8 files changed, 1130 insertions(+), 47 deletions(-) create mode 100644 modules/integrations/activedirectory/collect/ldap_native_enums_windows.go diff --git a/go.mod b/go.mod index db2d9b5..9e57c9a 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/go-ini/ini v1.67.0 github.com/gobwas/glob v0.2.3 github.com/gofrs/uuid v4.4.0+incompatible - github.com/gorilla/mux v1.8.1 github.com/gravwell/gravwell/v3 v3.8.13 github.com/icza/gox v0.0.0-20230924165045-adcb03233bb5 github.com/json-iterator/go v1.1.12 @@ -107,9 +106,12 @@ require ( github.com/elastic/go-windows v1.0.1 github.com/felixge/fgtrace v0.2.0 github.com/gammazero/deque v0.2.1 + github.com/gin-contrib/pprof v1.4.0 + github.com/gin-contrib/static v0.0.1 + github.com/golang-auth/go-channelbinding v1.0.1 + github.com/gorilla/websocket v1.5.1 github.com/jcmturner/gokrb5/v8 v8.4.4 github.com/lkarlslund/gonk v0.0.0-20231113084556-53a1781342e9 - github.com/peterrk/slices v1.0.0 www.velocidex.com/golang/go-ese v0.2.1-0.20240207005444-85d57b555f8b ) @@ -121,15 +123,13 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/pprof v1.4.0 // indirect - github.com/gin-contrib/static v0.0.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/google/gopacket v1.1.19 // indirect - github.com/gorilla/websocket v1.5.1 // indirect github.com/gravwell/buffer v0.0.0-20220728204757-23339f4bab66 // indirect github.com/gravwell/ipfix v1.4.5 // indirect github.com/h2non/filetype v1.1.3 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/jake-scott/go-gssapi v0.2.2 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.7.6 // indirect @@ -141,6 +141,7 @@ require ( github.com/open-networks/go-msgraph v0.3.4 // indirect github.com/open2b/scriggo v0.56.1 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/peterrk/slices v1.0.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/tealeg/xlsx v1.0.5 // indirect diff --git a/go.sum b/go.sum index f914b75..0ae63ad 100644 --- a/go.sum +++ b/go.sum @@ -233,6 +233,8 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-auth/go-channelbinding v1.0.1 h1:Sc60eXHEyLxKS0BWaM6FtmhVE+stTyTUxp1cLQaGgY0= +github.com/golang-auth/go-channelbinding v1.0.1/go.mod h1:tWhkagITD+NfomCcnMM/de/ddpxO5dbDTFsgGfQSHhk= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -307,8 +309,6 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE= github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= @@ -345,6 +345,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/inhies/go-bytesize v0.0.0-20201103132853-d0aed0d254f8/go.mod h1:KrtyD5PFj++GKkFS/7/RRrfnRhAMGQwy75GLCHWrCNs= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= +github.com/jake-scott/go-gssapi v0.2.2 h1:25Ri4inVUqynNf3ktySSvJIFmgdYUzBgfJ0UF4Hta+Y= +github.com/jake-scott/go-gssapi v0.2.2/go.mod h1:0jkvPgty8wGjbwQ+CznXRjhqJjBPu3zRuPNgUXfmZd4= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -388,10 +390,11 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= @@ -449,7 +452,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= @@ -517,6 +519,7 @@ github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= @@ -900,8 +903,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/modules/integrations/activedirectory/collect/cli.go b/modules/integrations/activedirectory/collect/cli.go index 16e57a8..3145f36 100644 --- a/modules/integrations/activedirectory/collect/cli.go +++ b/modules/integrations/activedirectory/collect/cli.go @@ -46,9 +46,9 @@ var ( user = Command.Flags().String("username", "", "username to connect with") pass = Command.Flags().String("password", "", "password to connect with (use ! for blank password)") - tlsmodeString = Command.Flags().String("tlsmode", "NoTLS", "Transport mode (TLS, StartTLS, NoTLS)") - - ignoreCert = Command.Flags().Bool("ignorecert", false, "Disable certificate checks") + tlsmodeString = Command.Flags().String("tlsmode", "NoTLS", "Transport mode (TLS, StartTLS, NoTLS)") + channelbinding = Command.Flags().Bool("channelbinding", true, "Enable channel binding when connecting to LDAP") + ignoreCert = Command.Flags().Bool("ignorecert", false, "Disable certificate checks") ldapdebug = Command.Flags().Bool("ldapdebug", false, "Enable LDAP debugging") @@ -64,7 +64,7 @@ var ( collectobjects = Command.Flags().String("objects", "auto", "Collect Active Directory Objects (users, groups etc)") collectgpos = Command.Flags().String("gpos", "auto", "Collect Group Policy file contents") gpopath = Command.Flags().String("gpopath", "", "Override path to GPOs, useful for non Windows OS'es with mounted drive (/mnt/policies/ or similar), but will break ACL feature") - AuthmodeString = Command.Flags().String("authmode", "ntlm", "Bind mode: unauth/anonymous, basic/simple, digest/md5, kerberoscache, ntlm, ntlmpth (password is hash), negotiate/sspi") + AuthmodeString = Command.Flags().String("authmode", "ntlm", "Bind mode: unauth/anonymous, basic/simple, digest/md5, kerberoscache, ntlm, ntlmpth (password is hash)") purgeolddata = Command.Flags().Bool("purgeolddata", false, "Purge existing data from the datapath if connection to DC is successfull") @@ -294,15 +294,16 @@ func Execute(cmd *cobra.Command, args []string) error { } else { // Active Directory dump directly from AD controller options := LDAPOptions{ - Domain: *domain, - Port: uint16(*port), - AuthMode: authmode, - User: *user, - Password: *pass, - AuthDomain: *authdomain, - TLSMode: tlsmode, - IgnoreCert: *ignoreCert, - Debug: *ldapdebug, + Domain: *domain, + Port: uint16(*port), + AuthMode: authmode, + User: *user, + Password: *pass, + AuthDomain: *authdomain, + TLSMode: tlsmode, + IgnoreCert: *ignoreCert, + Debug: *ldapdebug, + Channelbinding: *channelbinding, } var ad LDAPDumper diff --git a/modules/integrations/activedirectory/collect/ldap_common.go b/modules/integrations/activedirectory/collect/ldap_common.go index 16d1294..78b2829 100644 --- a/modules/integrations/activedirectory/collect/ldap_common.go +++ b/modules/integrations/activedirectory/collect/ldap_common.go @@ -136,15 +136,16 @@ const ( ) type LDAPOptions struct { - Domain string - Server string - Port uint16 - User string - Password string - AuthDomain string - AuthMode AuthMode - TLSMode TLSmode - SizeLimit int + Domain string + Server string + Port uint16 + User string + Password string + AuthDomain string + AuthMode AuthMode + TLSMode TLSmode + Channelbinding bool + SizeLimit int IgnoreCert bool diff --git a/modules/integrations/activedirectory/collect/ldap_multiplatform.go b/modules/integrations/activedirectory/collect/ldap_multiplatform.go index 6b1515c..defd0d0 100644 --- a/modules/integrations/activedirectory/collect/ldap_multiplatform.go +++ b/modules/integrations/activedirectory/collect/ldap_multiplatform.go @@ -11,6 +11,7 @@ import ( osuser "os/user" ber "github.com/go-asn1-ber/asn1-ber" + cb "github.com/golang-auth/go-channelbinding" "github.com/jcmturner/gokrb5/v8/client" "github.com/jcmturner/gokrb5/v8/config" "github.com/jcmturner/gokrb5/v8/credentials" @@ -29,6 +30,9 @@ type AD struct { } func (ad *AD) Connect() error { + var cbData []byte + _ = cbData // for later + if ad.AuthDomain == "" { ad.AuthDomain = ad.Domain } @@ -52,17 +56,33 @@ func (ad *AD) Connect() error { if err != nil { return err } + ad.conn = conn case TLS: config := &tls.Config{ ServerName: ad.Server, InsecureSkipVerify: ad.IgnoreCert, + MaxVersion: tls.VersionTLS12, } - conn, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ad.Server, ad.Port), config) + + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", ad.Server, ad.Port), config) if err != nil { return err } - ad.conn = conn + + if ad.Channelbinding { + tlsState := conn.ConnectionState() + if len(tlsState.PeerCertificates) == 0 { + return errors.New("no peer certificates for channel binding") + } + cbData, err = cb.MakeTLSChannelBinding(tlsState, tlsState.PeerCertificates[0], cb.TLSChannelBindingEndpoint) + if err != nil { + return err + } + } + + ad.conn = ldap.NewConn(conn, true) + ad.conn.Start() default: return errors.New("unknown transport mode") } diff --git a/modules/integrations/activedirectory/collect/ldap_native_enums_windows.go b/modules/integrations/activedirectory/collect/ldap_native_enums_windows.go new file mode 100644 index 0000000..08c76dd --- /dev/null +++ b/modules/integrations/activedirectory/collect/ldap_native_enums_windows.go @@ -0,0 +1,160 @@ +// Code generated by "enumer -type=LDAPAuth -json -output ldap_enums.go"; DO NOT EDIT. + +package collect + +import ( + "encoding/json" + "fmt" + "strings" +) + +const ( + _LDAPAuthName_0 = "LDAP_AUTH_SIMPLE" + _LDAPAuthLowerName_0 = "ldap_auth_simple" + _LDAPAuthName_1 = "LDAP_AUTH_SASL" + _LDAPAuthLowerName_1 = "ldap_auth_sasl" + _LDAPAuthName_2 = "LDAP_AUTH_OTHERKIND" + _LDAPAuthLowerName_2 = "ldap_auth_otherkind" + _LDAPAuthName_3 = "LDAP_AUTH_NEGOTIATE" + _LDAPAuthLowerName_3 = "ldap_auth_negotiate" + _LDAPAuthName_4 = "LDAP_AUTH_MSN" + _LDAPAuthLowerName_4 = "ldap_auth_msn" + _LDAPAuthName_5 = "LDAP_AUTH_NTLM" + _LDAPAuthLowerName_5 = "ldap_auth_ntlm" + _LDAPAuthName_6 = "LDAP_AUTH_DPA" + _LDAPAuthLowerName_6 = "ldap_auth_dpa" + _LDAPAuthName_7 = "LDAP_AUTH_DIGEST" + _LDAPAuthLowerName_7 = "ldap_auth_digest" +) + +var ( + _LDAPAuthIndex_0 = [...]uint8{0, 16} + _LDAPAuthIndex_1 = [...]uint8{0, 14} + _LDAPAuthIndex_2 = [...]uint8{0, 19} + _LDAPAuthIndex_3 = [...]uint8{0, 19} + _LDAPAuthIndex_4 = [...]uint8{0, 13} + _LDAPAuthIndex_5 = [...]uint8{0, 14} + _LDAPAuthIndex_6 = [...]uint8{0, 13} + _LDAPAuthIndex_7 = [...]uint8{0, 16} +) + +func (i LDAPAuth) String() string { + switch { + case i == 128: + return _LDAPAuthName_0 + case i == 131: + return _LDAPAuthName_1 + case i == 134: + return _LDAPAuthName_2 + case i == 1158: + return _LDAPAuthName_3 + case i == 2182: + return _LDAPAuthName_4 + case i == 4230: + return _LDAPAuthName_5 + case i == 8326: + return _LDAPAuthName_6 + case i == 16518: + return _LDAPAuthName_7 + default: + return fmt.Sprintf("LDAPAuth(%d)", i) + } +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _LDAPAuthNoOp() { + var x [1]struct{} + _ = x[LDAP_AUTH_SIMPLE-(128)] + _ = x[LDAP_AUTH_SASL-(131)] + _ = x[LDAP_AUTH_OTHERKIND-(134)] + _ = x[LDAP_AUTH_NEGOTIATE-(1158)] + _ = x[LDAP_AUTH_MSN-(2182)] + _ = x[LDAP_AUTH_NTLM-(4230)] + _ = x[LDAP_AUTH_DPA-(8326)] + _ = x[LDAP_AUTH_DIGEST-(16518)] +} + +var _LDAPAuthValues = []LDAPAuth{LDAP_AUTH_SIMPLE, LDAP_AUTH_SASL, LDAP_AUTH_OTHERKIND, LDAP_AUTH_NEGOTIATE, LDAP_AUTH_MSN, LDAP_AUTH_NTLM, LDAP_AUTH_DPA, LDAP_AUTH_DIGEST} + +var _LDAPAuthNameToValueMap = map[string]LDAPAuth{ + _LDAPAuthName_0[0:16]: LDAP_AUTH_SIMPLE, + _LDAPAuthLowerName_0[0:16]: LDAP_AUTH_SIMPLE, + _LDAPAuthName_1[0:14]: LDAP_AUTH_SASL, + _LDAPAuthLowerName_1[0:14]: LDAP_AUTH_SASL, + _LDAPAuthName_2[0:19]: LDAP_AUTH_OTHERKIND, + _LDAPAuthLowerName_2[0:19]: LDAP_AUTH_OTHERKIND, + _LDAPAuthName_3[0:19]: LDAP_AUTH_NEGOTIATE, + _LDAPAuthLowerName_3[0:19]: LDAP_AUTH_NEGOTIATE, + _LDAPAuthName_4[0:13]: LDAP_AUTH_MSN, + _LDAPAuthLowerName_4[0:13]: LDAP_AUTH_MSN, + _LDAPAuthName_5[0:14]: LDAP_AUTH_NTLM, + _LDAPAuthLowerName_5[0:14]: LDAP_AUTH_NTLM, + _LDAPAuthName_6[0:13]: LDAP_AUTH_DPA, + _LDAPAuthLowerName_6[0:13]: LDAP_AUTH_DPA, + _LDAPAuthName_7[0:16]: LDAP_AUTH_DIGEST, + _LDAPAuthLowerName_7[0:16]: LDAP_AUTH_DIGEST, +} + +var _LDAPAuthNames = []string{ + _LDAPAuthName_0[0:16], + _LDAPAuthName_1[0:14], + _LDAPAuthName_2[0:19], + _LDAPAuthName_3[0:19], + _LDAPAuthName_4[0:13], + _LDAPAuthName_5[0:14], + _LDAPAuthName_6[0:13], + _LDAPAuthName_7[0:16], +} + +// LDAPAuthString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func LDAPAuthString(s string) (LDAPAuth, error) { + if val, ok := _LDAPAuthNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _LDAPAuthNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to LDAPAuth values", s) +} + +// LDAPAuthValues returns all values of the enum +func LDAPAuthValues() []LDAPAuth { + return _LDAPAuthValues +} + +// LDAPAuthStrings returns a slice of all String values of the enum +func LDAPAuthStrings() []string { + strs := make([]string, len(_LDAPAuthNames)) + copy(strs, _LDAPAuthNames) + return strs +} + +// IsALDAPAuth returns "true" if the value is listed in the enum definition. "false" otherwise +func (i LDAPAuth) IsALDAPAuth() bool { + for _, v := range _LDAPAuthValues { + if i == v { + return true + } + } + return false +} + +// MarshalJSON implements the json.Marshaler interface for LDAPAuth +func (i LDAPAuth) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for LDAPAuth +func (i *LDAPAuth) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("LDAPAuth should be a string, got %s", data) + } + + var err error + *i, err = LDAPAuthString(s) + return err +} diff --git a/modules/integrations/activedirectory/collect/ldap_windows.go b/modules/integrations/activedirectory/collect/ldap_windows.go index 26f9b98..93869be 100644 --- a/modules/integrations/activedirectory/collect/ldap_windows.go +++ b/modules/integrations/activedirectory/collect/ldap_windows.go @@ -1,10 +1,897 @@ +//go:build windows +// +build windows + package collect +import "C" + import ( + "bytes" + "fmt" + "os" + "runtime" + "syscall" + "time" + "unicode/utf16" + "unsafe" + + ber "github.com/go-asn1-ber/asn1-ber" + "github.com/lkarlslund/adalanche/modules/integrations/activedirectory" + "github.com/lkarlslund/adalanche/modules/ui" ldap "github.com/lkarlslund/ldap/v3" "github.com/lkarlslund/ldap/v3/gssapi" + "github.com/pierrec/lz4/v4" + "github.com/pkg/errors" + "github.com/schollz/progressbar/v3" + "github.com/tinylib/msgp/msgp" ) func GetSSPIClient() (ldap.GSSAPIClient, error) { return gssapi.NewSSPIClient() } + +//go:generate go run github.com/dmarkham/enumer -type=LDAPAuth -json -output ldap_native_enums_windows.go + +type LDAPAuth uint + +const ( + LDAP_AUTH_SIMPLE LDAPAuth = 0x80 + LDAP_AUTH_SASL LDAPAuth = 0x83 + LDAP_AUTH_OTHERKIND LDAPAuth = 0x86 + LDAP_AUTH_MSN LDAPAuth = LDAP_AUTH_OTHERKIND | 0x0800 + LDAP_AUTH_NEGOTIATE LDAPAuth = LDAP_AUTH_OTHERKIND | 0x0400 + LDAP_AUTH_NTLM LDAPAuth = LDAP_AUTH_OTHERKIND | 0x1000 + LDAP_AUTH_DPA LDAPAuth = LDAP_AUTH_OTHERKIND | 0x2000 + LDAP_AUTH_DIGEST LDAPAuth = LDAP_AUTH_OTHERKIND | 0x4000 + LDAP_AUTH_SSPI LDAPAuth = LDAP_AUTH_NEGOTIATE +) + +var ( + nativeldap = Command.Flags().Bool("nativeldap", true, "Use native Windows LDAP library rather than multiplatform Golang LDAP library") + referrals = Command.Flags().Bool("referrals", false, "Follow referrals (native Windows LDAP only)") + timeout = Command.Flags().Duration("timeout", time.Second*30, "timeout for ldap operations (native Windows LDAP only)") + signing = Command.Flags().Bool("signing", false, "enable encryption and signing over non-TLS sessions") +) + +var ignoreCertCallback = syscall.NewCallback(func(connection uintptr, trustedcas uintptr, ppServerCert uintptr) uintptr { + return 1 +}) + +func init() { + + CreateDumper = func(opts LDAPOptions) LDAPDumper { + if *nativeldap { + return &WAD{ + LDAPOptions: opts, + } + } else { + return &AD{ + LDAPOptions: opts, + } + } + } + + authmodeflag := Command.Flag("authmode") + authmodeflag.DefValue = "negotiate" + authmodeflag.Value.Set("negotiate") + authmodeflag.Usage = "Bind mode: unauth/anonymous, basic/simple, digest/md5, ntlm, negotiate/sspi - multiplatform only: kerberoscache, ntlmpth (password is hash)" +} + +// Windows native LDAP AD dumper +type WAD struct { + LDAPOptions + + conn WLDAP +} + +func (a *WAD) Connect() error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + switch a.TLSMode { + case NoTLS, StartTLS: + ui.Info().Msgf("Setting up unencrypted LDAP session to %s:%d", a.Server, a.Port) + ldap, _, err := wldap32_ldap_init.Call(uintptr(unsafe.Pointer(MakeCString(a.Server))), uintptr(a.Port)) + if err != syscall.Errno(0) { + return err + } + + a.conn = WLDAP(ldap) + case TLS: + ui.Info().Msgf("Setting up TLS encrypted LDAP session to %s:%d", a.Server, a.Port) + ldap, _, err := wldap32_ldap_sslinit.Call(uintptr(unsafe.Pointer(MakeCString(a.Server))), uintptr(a.Port), uintptr(1)) + if err != syscall.Errno(0) { + return err + } + + a.conn = WLDAP(ldap) + default: + return errors.New("unknown transport mode") + } + + timeout_secs := int32(timeout.Seconds()) + + a.conn.set_option(LDAP_OPT_PROTOCOL_VERSION, LDAP_VERSION3) + + a.conn.set_option(LDAP_OPT_SIZELIMIT, uintptr(a.SizeLimit)) + + if *signing { + if a.TLSMode != NoTLS { + return fmt.Errorf("Can't enable signing on anything but unencrypted connections (--tlsmode NoTLS)") + } + if a.AuthMode != NTLM && a.AuthMode != Negotiate { + return fmt.Errorf("Can't enable signing on anything but NTLM or NEGOTIATE sessions (--authmode ntlm or --authmode negotiate)") + } + a.conn.set_option(LDAP_OPT_SIGN, 1) + } + + if *referrals { + a.conn.set_option(LDAP_OPT_REFERRALS, 1) + } else { + a.conn.set_option(LDAP_OPT_REFERRALS, 0) + } + + if *ignoreCert { + a.conn.set_option_direct(LDAP_OPT_SERVER_CERTIFICATE, ignoreCertCallback) + } + + ui.Info().Msgf("Connecting to %s:%d", a.Server, a.Port) + res, _, _ := wldap32_ldap_connect.Call(uintptr(a.conn), uintptr(unsafe.Pointer(&timeout_secs))) + if LDAPError(res) != LDAP_SUCCESS { + a.conn.unbind() + if LDAPError(res) == LDAP_SERVER_DOWN { + return fmt.Errorf("ldap_connect failed with %v, connection issue or invalid certificate (try --ignorecert)", LDAPError(res)) + } + return fmt.Errorf("ldap_connect failed with %v", LDAPError(res)) + } + + if a.TLSMode == StartTLS { + ui.Info().Msg("Upgrading unencrypted connection to TLS") + var errorval uint64 + res, _, err := wldap32_ldap_start_tls_s.Call( + uintptr(a.conn), + uintptr(unsafe.Pointer(&errorval)), + 0, + 0, + 0, + ) + if err != syscall.Errno(0) { + return err + } + if LDAPError(res) == LDAP_SERVER_DOWN { + return fmt.Errorf("ldap_connect failed with %v, connection issue or invalid certificate (try --ignorecert)", LDAPError(res)) + } + if LDAPError(res) != LDAP_SUCCESS { + return fmt.Errorf("ldap_start_tls_s failed with %v (code %v)", LDAPError(res), errorval) + } + } + + // https://docs.microsoft.com/en-us/windows/win32/api/winldap/nf-winldap-ldap_bind_s + var err error + switch a.AuthMode { + case Anonymous: + ui.Info().Msg("Anonymous bind") + res, _, err = wldap32_ldap_bind_s.Call( + uintptr(a.conn), + uintptr(unsafe.Pointer(MakeCString(""))), + uintptr(unsafe.Pointer(MakeCString(""))), + uintptr(LDAP_AUTH_SIMPLE), + ) + case Basic: + ui.Info().Msg("Simple bind") + res, _, err = wldap32_ldap_bind_s.Call( + uintptr(a.conn), + uintptr(unsafe.Pointer(MakeCString(a.User))), + uintptr(unsafe.Pointer(MakeCString(a.Password))), + uintptr(LDAP_AUTH_SIMPLE), + ) + case Digest, NTLM, Negotiate: + var ldapauthmode LDAPAuth + switch a.AuthMode { + case Digest: + ldapauthmode = LDAP_AUTH_DIGEST + case NTLM: + ldapauthmode = LDAP_AUTH_NTLM + case Negotiate: + ldapauthmode = LDAP_AUTH_NEGOTIATE + } + if a.User == "" { + ui.Info().Msgf("Using current user authentication mode %v", ldapauthmode) + res, _, err = wldap32_ldap_bind_s.Call( + uintptr(a.conn), + 0, + 0, + uintptr(ldapauthmode), + ) + } else { + ui.Info().Msgf("Using user %v authentication mode %v", a.User, ldapauthmode) + auth := SEC_WINNT_AUTH_IDENTITY_A{ + User: MakeWCString(a.User), + UserLength: uint32(len(a.User)), + Domain: MakeWCString(a.AuthDomain), + DomainLength: uint32(len(a.AuthDomain)), + Password: MakeWCString(a.Password), + PasswordLength: uint32(len(a.Password)), + Flags: SEC_WINNT_AUTH_IDENTITY_UNICODE, + } + res, _, err = wldap32_ldap_bind_s.Call( + uintptr(a.conn), + 0, + uintptr(unsafe.Pointer(&auth)), + uintptr(ldapauthmode), + ) + } + default: + return fmt.Errorf("Unsupported auth mode for native Windows LDAP: %v", a.AuthMode) + } + + if err != syscall.Errno(0) { + return err + } + if LDAPError(res) != LDAP_SUCCESS { + return fmt.Errorf("ldap_bind_s failed with %v", LDAPError(res)) + } + + return nil +} + +func (a *WAD) Disconnect() error { + if a.conn == 0 { + return errors.New("not connected") + } + err := a.conn.unbind_s() + a.conn = 0 + return err +} + +func (a *WAD) Dump(do DumpOptions) ([]activedirectory.RawObject, error) { + timeout_secs := int32(timeout.Seconds()) + + var e *msgp.Writer + if do.WriteToFile != "" { + outfile, err := os.Create(do.WriteToFile) + if err != nil { + return nil, fmt.Errorf("problem opening domain cache file: %v", err) + } + defer outfile.Close() + + boutfile := lz4.NewWriter(outfile) + lz4options := []lz4.Option{ + lz4.BlockChecksumOption(true), + // lz4.BlockSizeOption(lz4.BlockSize(51 * 1024)), + lz4.ChecksumOption(true), + lz4.CompressionLevelOption(lz4.Level9), + lz4.ConcurrencyOption(-1), + } + boutfile.Apply(lz4options...) + defer boutfile.Close() + e = msgp.NewWriter(boutfile) + } + + bar := progressbar.NewOptions(-1, + progressbar.OptionSetDescription("Dumping from "+do.SearchBase+" ..."), + progressbar.OptionShowCount(), + progressbar.OptionShowIts(), + progressbar.OptionSetItsString("objects"), + progressbar.OptionOnCompletion(func() { fmt.Println() }), + progressbar.OptionThrottle(time.Second*1), + ) + + var objects []activedirectory.RawObject + var err error + + var scarray []*LDAPControl // 0 = paging, 1 = NoSACL, 2 = nil + if do.ChunkSize > 0 { + paging, err := a.conn.CreatePageControl(nil, uint32(do.ChunkSize)) + if err != nil { + return nil, err + } + scarray = append(scarray, paging) + } + + if do.NoSACL { + nosaclcontrol := LDAPControl{ + oid: MakeCString("1.2.840.113556.1.4.801"), + iscritical: true, + } + + value := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control Value Sequence") + value.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 7, "Integer")) + berdata := value.Bytes() + + nosaclcontrol.ber = LDAPBerval{ + val: &berdata[0], + len: uint64(len(berdata)), + } + + scarray = append(scarray, &nosaclcontrol) + } + + if do.Query == "" { + do.Query = "(objectClass=*)" + } + + scarray = append(scarray, nil) // zero terminated array + ui.Trace().Msgf("Searching for %v at '%v'", do.Query, do.SearchBase) + + for { + search, err := a.conn.search(do.SearchBase, do.Query, do.Scope, do.Attributes, &scarray[0], int(timeout_secs), do.ChunkSize) + if err != nil { + return nil, err + } + + // Paging loop + controls, returncode, err := search.parse() + if err != nil { + return nil, err + } + if LDAPError(returncode) != LDAP_SUCCESS { + return nil, fmt.Errorf("ldap_search_ext_s returned %v", LDAPError(returncode)) + } + + var entry LDAPMessage + entry, err = search.first_entry() + for err == nil { + var item activedirectory.RawObject + item.Init() + + dn := entry.get_dn() + if dn != nil { + item.DistinguishedName = dn.String() + mem_free(uintptr(unsafe.Pointer(dn))) + } + + var ber LDAPBERElement + attr := entry.first_attribute(&ber) + + if attr == nil { + ui.Warn().Msgf("No attribute data for %v", item.DistinguishedName) + } + + for attr != nil { + attrName := attr.String() + + lvalues := entry.get_values_len(attr) + + var numvalues int + for i := 0; i < len(lvalues); i++ { + if lvalues[i] == nil { + numvalues = i + break + } + } + + if numvalues == 0 && attrName != "member" { + ui.Warn().Msgf("Object %v attribute %v has no values", item.DistinguishedName, attrName) + } + + values := make([]string, numvalues) + for i := 0; i < numvalues; i++ { + if do.ReturnObjects { + // Dedup if we're returning objects + values[i] = string(lvalues[i].Data()) + } else { + // Don't bother deduping if we're not returning objects + values[i] = string(lvalues[i].Data()) + } + } + + lvalues.free() + + mem_free(uintptr(unsafe.Pointer(attr))) + + item.Attributes[attrName] = values + + attr = entry.next_attribute(ber) + } + + if e != nil { + err = item.EncodeMsg(e) + if err != nil { + return nil, fmt.Errorf("problem encoding LDAP object %v: %v", item.DistinguishedName, err) + } + } + + if do.OnObject != nil { + do.OnObject(&item) + } + + if do.ReturnObjects { + // Grow one page at a time + if len(objects) == cap(objects) { + newobjects := make([]activedirectory.RawObject, len(objects), cap(objects)+do.ChunkSize) + copy(newobjects, objects) + objects = newobjects + } + objects = append(objects, item) + } + + bar.Add(1) + + entry = entry.next_entry() + if uintptr(entry.msg) == 0 { + break + } + } + search.free() + + if do.ChunkSize > 0 { + cookie, _, err := a.conn.ParsePageControl(controls) + if err != nil { + ui.Debug().Msgf("Error parsing page controls: %v", err) + } + + if cookie == nil || cookie.len == 0 { + ui.Trace().Msgf("No more results") + break + } + + ui.Trace().Msgf("Continuing search with cookie %0X", cookie.Data()) + + paging, err := a.conn.CreatePageControl(cookie, uint32(do.ChunkSize)) + if err != nil { + return nil, err + } + + // Free the old pagingcontrol that was created by DLL + oldcontrol := scarray[0] + scarray[0] = paging + oldcontrol.free() + } else { + // No paging requested + break + } + } + + runtime.KeepAlive(scarray) + + bar.Finish() + if e != nil { + e.Flush() + } + + return objects, err +} + +var ( + wldap32 = syscall.NewLazyDLL("Wldap32.dll") + wldap32_ldap_bind_s = wldap32.NewProc("ldap_bind_s") + wldap32_ldap_connect = wldap32.NewProc("ldap_connect") + wldap32_ldap_count_entries = wldap32.NewProc("ldap_count_entries") + wldap32_ldap_count_values = wldap32.NewProc("ldap_count_values") + wldap32_ldap_get_dn = wldap32.NewProc("ldap_get_dn") + wldap32_ldap_get_option = wldap32.NewProc("ldap_get_option") + wldap32_ldap_get_values = wldap32.NewProc("ldap_get_values") + wldap32_ldap_get_values_len = wldap32.NewProc("ldap_get_values_len") + wldap32_ldap_first_entry = wldap32.NewProc("ldap_first_entry") + wldap32_ldap_first_attribute = wldap32.NewProc("ldap_first_attribute") + wldap32_ldap_init = wldap32.NewProc("ldap_init") + wldap32_ldap_sslinit = wldap32.NewProc("ldap_sslinit") + wldap32_ldap_start_tls_s = wldap32.NewProc("ldap_start_tls_sA") + wldap32_ldap_memfree = wldap32.NewProc("ldap_memfree") + wldap32_ldap_msgfree = wldap32.NewProc("ldap_msgfree") + wldap32_ldap_next_entry = wldap32.NewProc("ldap_next_entry") + wldap32_ldap_next_attribute = wldap32.NewProc("ldap_next_attribute") + wldap32_ldap_search_ext_s = wldap32.NewProc("ldap_search_ext_s") + wldap32_ldap_set_option = wldap32.NewProc("ldap_set_option") + wldap32_ldap_unbind = wldap32.NewProc("ldap_unbind") + wldap32_ldap_unbind_s = wldap32.NewProc("ldap_unbind_s") + wldap32_ldap_value_free = wldap32.NewProc("ldap_value_free") + wldap32_ldap_value_free_len = wldap32.NewProc("ldap_value_free_len") + + wldap32_ldap_parse_result = wldap32.NewProc("ldap_parse_result") + wldap32_ldap_create_page_control = wldap32.NewProc("ldap_create_page_control") + wldap32_ldap_parse_page_control = wldap32.NewProc("ldap_parse_page_control") + wldap32_ldap_control_free = wldap32.NewProc("ldap_control_free") + + // wldap32_ber_alloc_t = wldap32.NewProc("ber_alloc_t") + // wldap32_ber_flatten = wldap32.NewProc("ber_flatten") + // wldap32_ber_bvfree = wldap32.NewProc("ber_bvfree") + wldap32_ber_free = wldap32.NewProc("ber_free") +) + +const ( + SEC_WINNT_AUTH_IDENTITY_ANSI = 0x1 + SEC_WINNT_AUTH_IDENTITY_UNICODE = 0x2 +) + +type WLDAP syscall.Handle + +type LDAPValues [16384]LDAPValue + +type LDAPValue uintptr + +type LDAPMessage struct { + msg uintptr + ldap WLDAP +} + +func (ldm LDAPMessage) get_dn() *CString { + res, _, err := wldap32_ldap_get_dn.Call(uintptr(ldm.ldap), ldm.msg) + if err != syscall.Errno(0) { + return nil + } + return (*CString)(unsafe.Pointer(res)) +} + +func (ber LDAPBERElement) free() { + wldap32_ber_free.Call(uintptr(ber), uintptr(0)) +} + +func mem_free(ptr uintptr) error { + res, _, err := wldap32_ldap_memfree.Call(ptr) + if err != syscall.Errno(0) { + return err + } + if LDAPError(res) != LDAP_SUCCESS { + return fmt.Errorf("ldap_memfree failed: %v", LDAPError(res)) + } + return nil +} + +func (value LDAPValue) free() error { + res, res2, err := wldap32_ldap_value_free.Call(uintptr(unsafe.Pointer(value))) + + if LDAPError(res) != LDAP_SUCCESS { + return fmt.Errorf("ldap_value_free failed with %s %v %v", LDAPError(res), res2, err) + } + return nil +} + +func (value *LDAPBerval) Get() []byte { + if value.val == nil { + return nil + } + return GoBytes(value.val, int(value.len)) +} + +func (value *LDAPBervalues) free() error { + res, res2, err := wldap32_ldap_value_free_len.Call(uintptr(unsafe.Pointer(value))) + if err != syscall.Errno(0) { + return err + } + if LDAPError(res) != LDAP_SUCCESS { + return fmt.Errorf("ldap_value_free_len failed with %s %v %v", LDAPError(res), res2, err) + } + return nil +} + +func (msg LDAPMessage) free() error { + res, res2, err := wldap32_ldap_msgfree.Call(uintptr(msg.msg)) + if err != syscall.Errno(0) { + return err + } + if LDAPError(res) != LDAP_SUCCESS { + return fmt.Errorf("ldap_msgfree failed with %s %v %v", LDAPError(res), res2, err) + } + return nil +} + +func (ldap WLDAP) set_option(option LDAPOption, value uintptr) error { + res, res2, err := wldap32_ldap_set_option.Call(uintptr(ldap), uintptr(option), uintptr(unsafe.Pointer(&value))) + if err != syscall.Errno(0) { + return err + } + if LDAPError(res) != LDAP_SUCCESS { + return fmt.Errorf("ldap_value_free_len failed with %s %v %v", LDAPError(res), res2, err) + } + return nil +} + +func (ldap WLDAP) set_option_direct(option LDAPOption, value uintptr) error { + res, res2, err := wldap32_ldap_set_option.Call(uintptr(ldap), uintptr(option), value) + if err != syscall.Errno(0) { + return err + } + if LDAPError(res) != LDAP_SUCCESS { + return fmt.Errorf("ldap_value_free_len failed with %s %v %v", LDAPError(res), res2, err) + } + return nil +} + +func (ldap WLDAP) get_option(option LDAPOption) (int32, error) { + var result int32 + res, res2, err := wldap32_ldap_get_option.Call(uintptr(ldap), uintptr(option), uintptr(unsafe.Pointer(&result))) + if err != syscall.Errno(0) { + return result, err + } + if LDAPError(res) != LDAP_SUCCESS { + return result, fmt.Errorf("ldap_get_option failed with %s %v %v", LDAPError(res), res2, err) + } + return result, nil +} + +func (msg LDAPMessage) count_entries() (int, error) { + res, _, err := wldap32_ldap_count_entries.Call(uintptr(msg.ldap), uintptr(unsafe.Pointer(msg.msg))) + if err != syscall.Errno(0) { + return 0, err + } + return int(res), nil +} + +func (msg LDAPMessage) first_entry() (LDAPMessage, error) { + res, _, err := wldap32_ldap_first_entry.Call(uintptr(msg.ldap), uintptr(unsafe.Pointer(msg.msg))) + if err != syscall.Errno(0) { + return LDAPMessage{}, err + } + return LDAPMessage{ + msg: res, + ldap: msg.ldap, + }, nil +} + +type LDAPBervalues [16384]*LDAPBerval + +func (msg LDAPMessage) get_values_len(key *CString) *LDAPBervalues { + res, _, _ := wldap32_ldap_get_values_len.Call(uintptr(msg.ldap), uintptr(unsafe.Pointer(msg.msg)), uintptr(unsafe.Pointer(key))) + return (*LDAPBervalues)(unsafe.Pointer(res)) +} + +type LDAPBERElement uintptr + +func (msg LDAPMessage) parse() (**LDAPControl, uint32, error) { + var servercontrols **LDAPControl + var returncode uint32 + res, _, err := wldap32_ldap_parse_result.Call( + uintptr(msg.ldap), + uintptr(unsafe.Pointer(msg.msg)), + uintptr(unsafe.Pointer(&returncode)), // ReturnCode + 0, // MatchedDNs + 0, // Errormessage + 0, // Referrals + uintptr(unsafe.Pointer(&servercontrols)), // Servercontrols + 0, // Freeit + ) + if err != syscall.Errno(0) { + return nil, 0, err + } + if LDAPError(res) != LDAP_SUCCESS { + return nil, 0, fmt.Errorf("ldap_parse_result failed with %s", LDAPError(res)) + } + return servercontrols, returncode, nil +} + +func (msg LDAPMessage) first_attribute(ber *LDAPBERElement) *CString { + ptr, _, _ := wldap32_ldap_first_attribute.Call(uintptr(msg.ldap), uintptr(unsafe.Pointer(msg.msg)), uintptr(unsafe.Pointer(ber))) + return (*CString)(unsafe.Pointer(ptr)) +} + +func (msg LDAPMessage) next_attribute(ber LDAPBERElement) *CString { + ptr, _, _ := wldap32_ldap_next_attribute.Call(uintptr(msg.ldap), uintptr(unsafe.Pointer(msg.msg)), uintptr(unsafe.Pointer(ber))) + return (*CString)(unsafe.Pointer(ptr)) +} + +func (msg LDAPMessage) next_entry() LDAPMessage { + res, _, _ := wldap32_ldap_next_entry.Call(uintptr(msg.ldap), uintptr(unsafe.Pointer(msg.msg))) + return LDAPMessage{ + msg: res, + ldap: msg.ldap, + } +} + +func (val *LDAPValues) count_values() (int, error) { + res, _, err := wldap32_ldap_count_values.Call(uintptr(unsafe.Pointer(val))) + return int(res), err +} + +func (ldap WLDAP) bind_s(username string, password string) error { + var res uintptr + var err error + + if username != "" && password != "" { + // username should be in format "name@domain" not in DN or simple user name format + res, _, err = wldap32_ldap_bind_s.Call(uintptr(ldap), uintptr(unsafe.Pointer(MakeCString(username))), uintptr(unsafe.Pointer(MakeCString(password))), uintptr(LDAP_AUTH_SIMPLE)) + } else { + res, _, err = wldap32_ldap_bind_s.Call(uintptr(ldap), uintptr(unsafe.Pointer(nil)), uintptr(unsafe.Pointer(nil)), uintptr(LDAP_AUTH_NEGOTIATE)) + } + + if err != syscall.Errno(0) { + return err + } + + if LDAPError(res) != LDAP_SUCCESS { + return fmt.Errorf("ldap_bind_s failed with %s", LDAPError(res)) + } + + return nil +} + +func (ldap WLDAP) search(base string, filter string, scope int, attributes []string, servercontrols **LDAPControl, timeout, chunksize int) (LDAPMessage, error) { + cbase := MakeCString(base) + cfilter := MakeCString(filter) + msg := LDAPMessage{ldap: ldap} + + l_timeval := struct { + tv_sec int32 + tv_usec int32 + }{ + int32(timeout), + 0, + } + + res, _, err := wldap32_ldap_search_ext_s.Call( + uintptr(ldap), + uintptr(unsafe.Pointer(cbase)), + uintptr(scope), + uintptr(unsafe.Pointer(cfilter)), + 0, // Attributes to fetch + 0, // Get both attributes and values + uintptr(unsafe.Pointer(servercontrols)), // ServerControls + 0, // ClientControls + uintptr(unsafe.Pointer(&l_timeval)), // Timeout + uintptr(chunksize), // Results per page + uintptr(unsafe.Pointer(&msg.msg)), + ) + runtime.KeepAlive(cbase) + runtime.KeepAlive(cfilter) + runtime.KeepAlive(servercontrols) + runtime.KeepAlive(l_timeval) + + if err != syscall.Errno(0) { + return msg, err + } + + if LDAPError(res) != LDAP_SUCCESS { + return msg, fmt.Errorf("ldap_search_s failed with %s", LDAPError(res)) + } + + return msg, nil +} + +type LDAPBerval struct { + len uint64 + val *uint8 +} + +func (lbv LDAPBerval) Data() []byte { + return GoBytes(lbv.val, int(lbv.len)) +} + +func (lbv LDAPBerval) String() string { + return fmt.Sprintf("%0 X", lbv.Data()) +} + +func (ldap WLDAP) unbind() error { + res, res2, err := wldap32_ldap_unbind.Call(uintptr(ldap)) + if err != syscall.Errno(0) { + return err + } + if LDAPError(res) != LDAP_SUCCESS { + return fmt.Errorf("ldap_unbind failed with %s %v %v", LDAPError(res), res2, err) + } + return nil +} + +func (ldap WLDAP) unbind_s() error { + res, res2, err := wldap32_ldap_unbind_s.Call(uintptr(ldap)) + if err != syscall.Errno(0) { + return err + } + if LDAPError(res) != LDAP_SUCCESS { + return fmt.Errorf("ldap_unbind_s failed with %s %v %v", LDAPError(res), res2, err) + } + return nil +} + +type LDAPControls [16384]*LDAPControl + +type LDAPControl struct { + oid *CString + ber LDAPBerval + iscritical bool +} + +// Only call this for controls that are returned from the DLL +func (lc *LDAPControl) free() error { + res, _, err := wldap32_ldap_control_free.Call(uintptr(unsafe.Pointer(lc))) + if err != syscall.Errno(0) { + return err + } + if LDAPError(res) != LDAP_SUCCESS { + return fmt.Errorf("ldap_control_free failed with %s", LDAPError(res)) + } + return nil +} + +func (ldap WLDAP) CreatePageControl(cookie *LDAPBerval, pagesize uint32) (*LDAPControl, error) { + var control *LDAPControl + iscritical := byte(1) + res, _, err := wldap32_ldap_create_page_control.Call( + uintptr(ldap), + uintptr(pagesize), + uintptr(unsafe.Pointer(cookie)), + uintptr(iscritical), + uintptr(unsafe.Pointer(&control)), + ) + if err != syscall.Errno(0) { + return nil, err + } + if LDAPError(res) != LDAP_SUCCESS { + return nil, fmt.Errorf("ldap_create_page_control failed with %s", LDAPError(res)) + } + return control, nil +} + +func (ldap WLDAP) ParsePageControl(lc **LDAPControl) (*LDAPBerval, uint32, error) { + var cookie *LDAPBerval + var totalcount uint32 + res, _, err := wldap32_ldap_parse_page_control.Call( + uintptr(unsafe.Pointer(ldap)), + uintptr(unsafe.Pointer(lc)), + uintptr(unsafe.Pointer(&totalcount)), + uintptr(unsafe.Pointer(&cookie)), + ) + if err != syscall.Errno(0) { + return nil, 0, err + } + if LDAPError(res) != LDAP_SUCCESS { + return nil, 0, fmt.Errorf("ldap_parse_page_control failed with %s", LDAPError(res)) + } + return cookie, totalcount, nil +} + +func GoBytes(ptr *uint8, length int) []byte { + data := (*[1000000]byte)(unsafe.Pointer(ptr)) + result := make([]byte, length) + copy(result, data[:length]) + return result +} + +func GoString(ptr *uint8) string { + if ptr == nil { + return "" + } + + res := (*[1000000]byte)(unsafe.Pointer(ptr)) + length := bytes.IndexByte(res[:], 0) + if length < 1 { + panic("zero terminated string is not zero terminated") + } + return string(res[:length]) +} + +func MakeCString(input string) *CString { + chars := append([]byte(input), 0) // null terminated + return (*CString)(&chars[0]) +} + +func MakeWCString(input string) *WCString { + output := utf16.Encode([]rune(input + "\x00")) + return (*WCString)(&output[0]) +} + +type WCString uint16 + +type CString uint8 + +func (ptr *WCString) String() string { + data := (*[16384]uint16)(unsafe.Pointer(ptr)) + length := 0 + for i := 0; i < 16384; i++ { + if data[i] == 0 { + length = i + break + } + i++ + } + return string(utf16.Decode(data[:length])) +} + +func (ptr *CString) String() string { + data := (*[16384]uint8)(unsafe.Pointer(ptr)) + length := bytes.IndexByte(data[:], 0) + if length < 1 { + return "" + } + return string(data[:length]) +} + +type SEC_WINNT_AUTH_IDENTITY_A struct { + User *WCString + UserLength uint32 + Domain *WCString + DomainLength uint32 + Password *WCString + PasswordLength uint32 + Flags uint32 +} diff --git a/readme.MD b/readme.MD index c6d942d..c82be34 100644 --- a/readme.MD +++ b/readme.MD @@ -57,6 +57,18 @@ The primary source of data is from Active Directory, and is intiated with this c adalanche collect activedirectory [--options ...] +*Windows versions of Adalanche will default to using the native Windows LDAP library to connect to Active Directory, while non Windows version will use the multiplatform LDAP library. You can force Adalanche on Windows to use the multiplatform library with the --nativeldap=false option - this allows you to use a hash as a password and also to use a kerberos cache file for authentication.* + +| Feature | Windows LDAP | Multiplatform LDAP | +| ------- | ------------ | ------------------ | +| Unauthenticated bind | Yes | Yes | +| Simple bind | Yes | Yes | +| Digest bind | Yes | Yes | +| Kerberos | Yes, via NEGOTIATE | Yes (cache file) | +| NTLM | Yes | Yes | +| NTLM (hash) | No | Yes | + + If you're on a non-domain joined Windows machine or another OS, you'll need at least the --domain parameter, as well as username and password (you'll be prompted for password if Adalanche needs it and you didn't provide it on command line - beware of SysMon or other command line logging tools that might capture your password). LDAP (unencrypted port 389) is default. You can switch to TLS (port 636) with --tlsmode tls option. @@ -75,23 +87,19 @@ From domain joined Windows machine using other credentials than logged in: There are more options available, for instance on what LDAP contexts to collect, whether to collect GPOs or not etc. Please be aware that you can collect GPOs from Linux by mounting sysvol locally and pointing Adalanche to this path for GPO collection - but you will lose ACL analysis for the individual files. -__Note: LDAP RESULT CODE 49__ - -*There is a limitation in the LDAP library that Adalanche uses, which can result in this error: "LDAP Result Code 49 "Invalid Credentials": 8009030C: LdapErr: DSID-0C0906B5, comment: AcceptSecurityContext error, data 52e, v4563".* - -This is usually "Channel Binding" or "Signing" requirements for SSL enabled connections over LDAP, as part of Microsofts hardening efforts on making LDAP more secure. +#### LDAP RESULT CODE 49 -Here are suggested alternative solutions: +- __Wrong credentials (username/password)__ +"LDAP Result Code 49 "Invalid Credentials": 8009030C: LdapErr: DSID-0C0906B5, comment: AcceptSecurityContext error, data 52e, v4563" +You've entered wrong credentials or the account is blocked. -#### Dump data over plaintext LDAP (default mode) - -Use this command: - -adalanche collect activedirectory +- __Channel binding requirements__ +"LDAP Result Code 49 "Invalid Credentials": 80090346: LdapErr: DSID-0C0906B5, comment: AcceptSecurityContext error, data 80090346, v4563" +This is a "Channel Binding" requirement for SSL enabled connections over LDAP, as part of Microsofts hardening efforts on making LDAP more secure. This is currently unsupported by Adalanche on non Windows platforms due to LDAP library limitations - try running the collection from a Windows machine. #### Dump data using SysInternals AD Explorer -The SysInternals AD Explorer (adexplorer64.exe) is an enhanced GUI application that allows you to poke around in all objects and see all attributes. It leverages the Windows LDAP library (just like Users & Computers etc.) This supports both "Channel Binding" and "Signing" for LDAP transport. It also has a handy "snapshot" feature, that allows you do dump the entire AD into a proprietary file, which Adalanche can ingest as an alternative to talking directly to LDAP. +The SysInternals AD Explorer (adexplorer64.exe) is an enhanced GUI application that allows you to poke around in all objects and see all attributes. It also leverages the Windows LDAP library (just like Users & Computers etc.), and might be an option if you're not allowed to run Adalanche directly due to security concerns. By utilizing the "snapshot" feature, it allows you do dump the entire AD into a proprietary file, which Adalanche can ingest as an alternative to talking directly to LDAP. The procedure for using AD Explorer as a data source is: @@ -101,7 +109,9 @@ The procedure for using AD Explorer as a data source is: - Run Adalanche to collect Active Directory object and GPO data: adalanche collect activedirectory --adexplorerfile=yoursavedfile.bin -You will then have compressed AD data and GPO data in your datapath like a normal collection run. You can delete the AD Explorer data file now, as this is converted into Adalanche native format. +If you can't reach GPOs from where you're importing, you can either disable GPO imports --gpos=false or copy the Group Policy folder from SYSVOL and point to that with --gpopath=your-copied-GPO-folder, but you'll lose ACL analysis for the individual GPO files. + +You will then have compressed AD data (and potentially GPO data) in your datapath like a normal collection run. You can delete the AD Explorer data file now, as this is converted into Adalanche native format, and you can now run analysis mode. ## Gathering Local Machine data (Windows)