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..b9451e7 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,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 +32,10 @@ var ( domain string domQuery string relayFile string + dcFile string relayLst []string + dcLst []string + timeout time.Duration ) func main() { @@ -40,7 +44,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,55 +105,85 @@ 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}), + ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}), + } + + for _, target := range targets { fmt.Println("[!] Checking " + target) + successfulConnection := false + // 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) + 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) + } + } else { + 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) + } } - 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) + // Check LDAPS for channel binding + ldapsURL := fmt.Sprintf("ldaps://%s:636", target) + ls, err := ldap.DialURL(ldapsURL, dialOpts...) + if err != nil { + if strings.Contains(err.Error(), "i/o timeout") { + fmt.Printf("\tUnable to connect to LDAPS (636): Connection timed out\n") } else { - log.Fatal("[ERROR] Must specify -p or -H to authenticate") + log.Printf("[ERROR] Failed to connect to %s: %v", target, err) } - - 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 { + 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 + " signing is NOT enforced, we can relay to ldap://" + target + colorReset) - relayLst = append(relayLst, "ldap://"+target) - + fmt.Println(colorGreen + " channel binding is NOT enforced, we can relay to ldaps://" + target + colorReset) + relayLst = append(relayLst, "ldaps://"+target) } } - // Check LDAPS for channel binding - ldapsURL := "ldaps://" + target + ":636" + // Add to DC list only if we had at least one successful connection + if successfulConnection && dcFile != "" { + dcLst = append(dcLst, target) + } + } - ls, err := ldap.DialURL(ldapsURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true})) + // Write files at the end + if len(dcLst) > 0 && dcFile != "" { + err := writeLines(dcLst, dcFile) if err != nil { log.Fatal(err) } - 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) - } } + if len(relayLst) > 0 && relayFile != "" { err := writeLines(relayLst, relayFile) if err != nil {