From 128f39c627dc5991d471dc1db5544c31c0a964e0 Mon Sep 17 00:00:00 2001 From: puzzlepeaches Date: Tue, 18 Mar 2025 08:13:01 -0500 Subject: [PATCH 1/5] added flags and readme --- README.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 32 ++++++++++++------- 2 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe5908a --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# ldapcheck + +A lightweight LDAP signing and channel binding enumeration and testing tool designed for Active Directory environments. + +## Features + +- Domain Controller discovery via DNS +- NTLM authentication support +- Support for both password and hash-based authentication (Pass-the-Hash) +- Configurable connection timeouts +- Multiple output formats for discovered DCs and relay targets +- File-based target input support + +## Installation + +```bash +go install github.com/DriftSec/ldapcheck@latest +``` + +Or build from source: + +```bash +git clone https://github.com/DriftSec/ldapcheck.git +cd ldapcheck +go build +``` + +## Usage + +Basic usage examples: + +```bash +# Test single target with username/password +ldapcheck -t dc.domain.com -u user@domain.com -p password + +# Test single target with NTLM hash +ldapcheck -t dc.domain.com -u user@domain.com -H + +# Discover DCs for a domain +ldapcheck -T domain.com + +# Test multiple targets from a file +ldapcheck -t targets.txt -u user@domain.com -p password +``` + +### Command Line Options + +``` +ldapcheck -h + + -H string + user NTLM hash + -T string + Query this domain for LDAP targets + -dc-out string + output file for discovered DCs (one per line) + -p string + user password + -relay-out string + output file for relay targets (format: ldap[s]://host) + -t string + target address or file containing targets + -timeout duration + timeout for LDAP connections (default 5s) + -u string + username, formats: user@domain or domain\user +``` + +## Output Formats + +### DC List Output (-dc-out) + +The `-dc-out` option generates a list of discovered Domain Controllers. Each line in the output file represents a single DC. + +Example output: + +``` +dc1.domain.com +dc1.domain.com +dc2.domain.com +dc2.domain.com +``` + +### Relay List Output (-relay-out) + +The `-relay-out` option generates a list of relay targets. Each line in the output file represents a single relay target for use with ntlmrelayx.py + +Example output: + +``` +ldap://dc1.domain.com +ldaps://dc1.domain.com +ldap://dc2.domain.com +``` \ No newline at end of file diff --git a/main.go b/main.go index eeaeb68..909bd93 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "bufio" - "crypto/tls" "errors" "flag" "fmt" @@ -10,6 +9,7 @@ import ( "net" "os" "strings" + "time" // use our own go-ldap so we can ensure we dont include CBT "github.com/DriftSec/ldapcheck/ldap" @@ -31,7 +31,9 @@ var ( domain string domQuery string relayFile string + dcFile string relayLst []string + timeout time.Duration ) func main() { @@ -40,7 +42,9 @@ func main() { flag.StringVar(&dom_user, "u", "", "username, formats: user@domain or domain\\user") flag.StringVar(&pass, "p", "", "user password") flag.StringVar(&hash, "H", "", "user NTLM hash") - flag.StringVar(&relayFile, "o", "", "generate a relay list for use with ntlmrelayx.py") + flag.StringVar(&relayFile, "relay-out", "", "output file for relay targets (format: ldap[s]://host)") + flag.StringVar(&dcFile, "dc-out", "", "output file for discovered DCs (one per line)") + flag.DurationVar(&timeout, "timeout", 5*time.Second, "timeout for LDAP connections") flag.Parse() if targetArg == "" && domQuery == "" { @@ -99,16 +103,21 @@ func main() { if len(targets) < 1 { log.Fatal("[ERROR] No targets!") } - for _, target := range targets { + // For LDAP connections + dialOpts := []ldap.DialOpt{ + ldap.DialWithDialer(&net.Dialer{Timeout: timeout}), + } + + for _, target := range targets { fmt.Println("[!] Checking " + target) // Check LDAP for signing, if we have creds if dom_user != "" { - ldapURL := "ldap://" + target + ":389" - - l, err := ldap.DialURL(ldapURL) + ldapURL := fmt.Sprintf("ldap://%s:389", target) + l, err := ldap.DialURL(ldapURL, dialOpts...) if err != nil { - log.Fatal(err) + log.Printf("[ERROR] Failed to connect to %s: %v", target, err) + continue } defer l.Close() @@ -128,16 +137,15 @@ func main() { } else { fmt.Println(colorGreen + " signing is NOT enforced, we can relay to ldap://" + target + colorReset) relayLst = append(relayLst, "ldap://"+target) - } } // Check LDAPS for channel binding - ldapsURL := "ldaps://" + target + ":636" - - ls, err := ldap.DialURL(ldapsURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true})) + ldapsURL := fmt.Sprintf("ldaps://%s:636", target) + ls, err := ldap.DialURL(ldapsURL, dialOpts...) if err != nil { - log.Fatal(err) + log.Printf("[ERROR] Failed to connect to %s: %v", target, err) + continue } defer ls.Close() err = ls.NTLMBind("blah", "blah", "blah") From dbcc383779e6adf6c2d224eb70a65f6355645364 Mon Sep 17 00:00:00 2001 From: puzzlepeaches Date: Tue, 18 Mar 2025 08:15:19 -0500 Subject: [PATCH 2/5] added flags and readme --- main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.go b/main.go index 909bd93..0fc343e 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "crypto/tls" "errors" "flag" "fmt" @@ -107,6 +108,7 @@ func main() { // For LDAP connections dialOpts := []ldap.DialOpt{ ldap.DialWithDialer(&net.Dialer{Timeout: timeout}), + ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}), } for _, target := range targets { From 5453dd767fc88e2f3d43d3e8d9b432e74d17f8ac Mon Sep 17 00:00:00 2001 From: puzzlepeaches Date: Tue, 18 Mar 2025 08:16:34 -0500 Subject: [PATCH 3/5] added flags and readme --- main.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 0fc343e..a4b4c69 100644 --- a/main.go +++ b/main.go @@ -118,7 +118,11 @@ func main() { ldapURL := fmt.Sprintf("ldap://%s:389", target) l, err := ldap.DialURL(ldapURL, dialOpts...) if err != nil { - log.Printf("[ERROR] Failed to connect to %s: %v", target, err) + if strings.Contains(err.Error(), "i/o timeout") { + fmt.Printf("\tUnable to connect to LDAP (389): Connection timed out\n") + } else { + log.Printf("[ERROR] Failed to connect to %s: %v", target, err) + } continue } defer l.Close() @@ -146,7 +150,11 @@ func main() { ldapsURL := fmt.Sprintf("ldaps://%s:636", target) ls, err := ldap.DialURL(ldapsURL, dialOpts...) if err != nil { - log.Printf("[ERROR] Failed to connect to %s: %v", target, err) + if strings.Contains(err.Error(), "i/o timeout") { + fmt.Printf("\tUnable to connect to LDAPS (636): Connection timed out\n") + } else { + log.Printf("[ERROR] Failed to connect to %s: %v", target, err) + } continue } defer ls.Close() From ddbd1e9e574397f2db54725fc9b51d68209f547d Mon Sep 17 00:00:00 2001 From: puzzlepeaches Date: Tue, 18 Mar 2025 08:18:54 -0500 Subject: [PATCH 4/5] added flags and readme --- main.go | 69 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/main.go b/main.go index a4b4c69..6964bea 100644 --- a/main.go +++ b/main.go @@ -113,6 +113,8 @@ func main() { for _, target := range targets { fmt.Println("[!] Checking " + target) + successfulConnection := false + // Check LDAP for signing, if we have creds if dom_user != "" { ldapURL := fmt.Sprintf("ldap://%s:389", target) @@ -123,26 +125,26 @@ func main() { } else { log.Printf("[ERROR] Failed to connect to %s: %v", target, err) } - continue - } - defer l.Close() - - // err = l.Bind(user+"@"+domain, pass) - if pass != "" { - err = l.NTLMBind(domain, user, pass) - } else if hash != "" { - err = l.NTLMBindWithHash(domain, user, hash) } else { - log.Fatal("[ERROR] Must specify -p or -H to authenticate") - } + defer l.Close() + successfulConnection = true + // err = l.Bind(user+"@"+domain, pass) + if pass != "" { + err = l.NTLMBind(domain, user, pass) + } else if hash != "" { + err = l.NTLMBindWithHash(domain, user, hash) + } else { + log.Fatal("[ERROR] Must specify -p or -H to authenticate") + } - if err != nil && strings.Contains(err.Error(), "Strong Auth Required") { - fmt.Println(colorRed + " signing is enforced on ldap://" + target + colorReset) - } else if err != nil && strings.Contains(err.Error(), "Invalid Credentials") { - fmt.Println("LDAP: Auth Failed, valid creds are required to check signing!") - } else { - fmt.Println(colorGreen + " signing is NOT enforced, we can relay to ldap://" + target + colorReset) - relayLst = append(relayLst, "ldap://"+target) + if err != nil && strings.Contains(err.Error(), "Strong Auth Required") { + fmt.Println(colorRed + " signing is enforced on ldap://" + target + colorReset) + } else if err != nil && strings.Contains(err.Error(), "Invalid Credentials") { + fmt.Println("LDAP: Auth Failed, valid creds are required to check signing!") + } else { + fmt.Println(colorGreen + " signing is NOT enforced, we can relay to ldap://" + target + colorReset) + relayLst = append(relayLst, "ldap://"+target) + } } } @@ -155,17 +157,32 @@ func main() { } else { log.Printf("[ERROR] Failed to connect to %s: %v", target, err) } - continue - } - defer ls.Close() - err = ls.NTLMBind("blah", "blah", "blah") - if err != nil && strings.Contains(err.Error(), "data 80090346") { - fmt.Println(colorRed + " channel binding is enforced on ldaps://" + target + colorReset) } else { - fmt.Println(colorGreen + " channel binding is NOT enforced, we can relay to ldaps://" + target + colorReset) - relayLst = append(relayLst, "ldaps://"+target) + defer ls.Close() + successfulConnection = true + err = ls.NTLMBind("blah", "blah", "blah") + if err != nil && strings.Contains(err.Error(), "data 80090346") { + fmt.Println(colorRed + " channel binding is enforced on ldaps://" + target + colorReset) + } else { + fmt.Println(colorGreen + " channel binding is NOT enforced, we can relay to ldaps://" + target + colorReset) + relayLst = append(relayLst, "ldaps://"+target) + } + } + + // Add to DC list only if we had at least one successful connection + if successfulConnection && dcFile != "" { + dcLst = append(dcLst, target) + } + } + + // Write files at the end + if len(dcLst) > 0 && dcFile != "" { + err := writeLines(dcLst, dcFile) + if err != nil { + log.Fatal(err) } } + if len(relayLst) > 0 && relayFile != "" { err := writeLines(relayLst, relayFile) if err != nil { From 41a486971335ead94811764d7f4bcaa33c6f53fc Mon Sep 17 00:00:00 2001 From: puzzlepeaches Date: Tue, 18 Mar 2025 08:19:35 -0500 Subject: [PATCH 5/5] added flags and readme --- main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/main.go b/main.go index 6964bea..b9451e7 100644 --- a/main.go +++ b/main.go @@ -34,6 +34,7 @@ var ( relayFile string dcFile string relayLst []string + dcLst []string timeout time.Duration )