Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <NTLM_HASH>

# 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
```
100 changes: 68 additions & 32 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -31,7 +32,10 @@ var (
domain string
domQuery string
relayFile string
dcFile string
relayLst []string
dcLst []string
timeout time.Duration
)

func main() {
Expand All @@ -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 == "" {
Expand Down Expand Up @@ -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 {
Expand Down