Files
psycep 9e78779102 version: centralize banner through flags.Banner()
Before this, 26 tools hardcoded the literal "gopacket vX.Y.Z-beta -
Copyright 2026 Google LLC" banner in print statements, totaling 34
occurrences across the tree. Every version bump meant updating the
same string 34 times.

Switches every tool to call flags.Banner(), which builds the banner
from flags.Version. The Version const in pkg/flags/flags.go is now
the single source of truth; future version bumps are a one-line
change.

Mechanical split:
- 3 common print patterns (fmt.Println, fmt.Fprintln os.Stderr,
  fmt.Fprintf with trailing \n\n) swept with sed.
- 6 heredoc-style flag.Usage functions had the banner line on a
  backtick-quoted format string. Each was split into a separate
  fmt.Fprintln(os.Stderr, flags.Banner()) followed by the existing
  Fprintf with the banner line trimmed off the format string.
- 9 tools needed a new pkg/flags import added.
- tools/describeTicket already imports github.com/jcmturner/gokrb5
  /v8/iana/flags for Kerberos flag constants, so pkg/flags is aliased
  as gopflags there to avoid the name collision.

Verified: go build, go vet, and go test ./pkg/transport/ all clean
under default, CGO_ENABLED=0, and GOOS=windows CGO_ENABLED=0. Spot-
checked -h output on samrdump, rpcmap, mssqlinstance, ping, and
describeTicket; each renders the banner identically to before.
2026-04-22 13:25:06 -05:00

277 lines
7.6 KiB
Go

// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"crypto/tls"
"flag"
"fmt"
"net"
"os"
"strings"
"time"
goldap "github.com/go-ldap/ldap/v3"
"github.com/mandiant/gopacket/internal/build"
"github.com/mandiant/gopacket/pkg/flags"
"github.com/mandiant/gopacket/pkg/transport"
)
var (
dcIP = flag.String("dc-ip", "", "IP Address of a domain controller or DNS resolver for the domain")
dcHost = flag.String("dc-host", "", "Hostname of a specific DC to check (skips DNS discovery)")
domain = flag.String("domain", "", "Domain name")
timeout = flag.Int("timeout", 15, "DNS timeout in seconds")
)
func main() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, flags.Banner())
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "LDAP signing and channel binding enumeration utility.")
fmt.Fprintln(os.Stderr)
fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0])
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Options:")
flag.PrintDefaults()
}
// Standard flags
debug := flag.Bool("debug", false, "Turn DEBUG output ON")
ts := flag.Bool("ts", false, "Adds timestamp to every logging output")
configureProxy := flags.RegisterProxyFlag()
flags.CheckHelp()
flag.Parse()
configureProxy()
// Validate required flags
if *dcHost == "" && (*dcIP == "" || *domain == "") {
fmt.Fprintln(os.Stderr, "Error: Either -dc-host or both -dc-ip and -domain are required")
flag.Usage()
os.Exit(1)
}
build.Debug = *debug
build.Timestamp = *ts
checker := &LDAPChecker{
domain: *domain,
dcIP: *dcIP,
dcHost: *dcHost,
timeout: time.Duration(*timeout) * time.Second,
}
if *domain != "" {
logInfo("Targeted domain: %s", *domain)
}
if err := checker.Run(); err != nil {
logError("%v", err)
os.Exit(1)
}
}
type LDAPChecker struct {
domain string
dcIP string
dcHost string
timeout time.Duration
}
func (c *LDAPChecker) Run() error {
var dcList []string
if c.dcHost != "" {
// Direct DC specified, skip DNS discovery
dcList = []string{c.dcHost}
logInfo("Checking specified domain controller: %s", c.dcHost)
} else {
// Discover DCs via DNS
var err error
dcList, err = c.listDCs()
if err != nil {
return fmt.Errorf("failed to enumerate domain controllers: %v", err)
}
logInfo("Found %d domain controller(s) in %s", len(dcList), c.domain)
}
for _, dc := range dcList {
signingRequired := c.checkLDAPSigning(dc)
channelBindingStatus := c.checkLDAPSChannelBinding(dc)
fmt.Printf("Hostname: %s\n", dc)
fmt.Printf("\t> LDAP Signing Required: %v\n", signingRequired)
fmt.Printf("\t> LDAPS Channel Binding Status: %s\n", channelBindingStatus)
}
return nil
}
func (c *LDAPChecker) listDCs() ([]string, error) {
// Custom resolver using the DC as DNS server. Under -proxy the resolver's
// UDP attempt will fail at the SOCKS5 layer; supply -dc-host directly to
// skip this lookup when proxied.
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
return transport.DialContext(ctx, network, net.JoinHostPort(c.dcIP, "53"))
},
}
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
defer cancel()
// Query SRV records for domain controllers
srvQuery := fmt.Sprintf("_ldap._tcp.dc._msdcs.%s", c.domain)
_, srvs, err := resolver.LookupSRV(ctx, "", "", srvQuery)
if err != nil {
return nil, fmt.Errorf("DNS SRV lookup failed: %v", err)
}
var dcList []string
for _, srv := range srvs {
hostname := strings.TrimSuffix(srv.Target, ".")
dcList = append(dcList, hostname)
}
return dcList, nil
}
func (c *LDAPChecker) checkLDAPSigning(hostname string) bool {
// Try to connect to LDAP without signing
address := net.JoinHostPort(hostname, "389")
conn, err := transport.DialTimeout("tcp", address, int(c.timeout.Seconds()))
if err != nil {
logDebug("Failed to connect to %s: %v", address, err)
return false
}
ldapConn := goldap.NewConn(conn, false)
ldapConn.Start()
defer ldapConn.Close()
// Try anonymous bind (or bind with empty credentials)
// If signing is required, we'll get "strongerAuthRequired" error
err = ldapConn.UnauthenticatedBind("")
if err != nil {
errStr := err.Error()
if strings.Contains(errStr, "strongerAuthRequired") ||
strings.Contains(errStr, "Strong Auth Required") ||
strings.Contains(strings.ToLower(errStr), "stronger") {
logDebug("LDAP signing is enforced on %s", hostname)
return true
}
logDebug("LDAP bind error on %s: %v", hostname, err)
} else {
logDebug("LDAP signing is not enforced on %s", hostname)
}
return false
}
func (c *LDAPChecker) checkLDAPSChannelBinding(hostname string) string {
address := net.JoinHostPort(hostname, "636")
// Connect with TLS
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: hostname,
}
conn, err := transport.DialTLS("tcp", address, tlsConfig)
if err != nil {
logDebug("Failed to connect to LDAPS on %s: %v", hostname, err)
if strings.Contains(err.Error(), "connection reset") ||
strings.Contains(err.Error(), "EOF") {
return "No TLS cert"
}
return "Connection Failed"
}
ldapConn := goldap.NewConn(conn, true)
ldapConn.Start()
defer ldapConn.Close()
// Try NTLM bind with invalid credentials
// The error response tells us about channel binding requirements
//
// Error codes:
// - data 80090346: SEC_E_BAD_BINDINGS - Channel binding required
// - data 52e: Invalid credentials (normal, means CBT not strictly required)
domain := c.domain
if domain == "" {
domain = "WORKGROUP"
}
// Use NTLMBindWithHash with an invalid hash to trigger NTLM auth
err = ldapConn.NTLMBindWithHash(domain, "invaliduser", "00000000000000000000000000000000")
if err != nil {
errStr := err.Error()
// Check for channel binding required error
if strings.Contains(errStr, "80090346") {
logDebug("LDAPS channel binding is set to 'Always' on %s", hostname)
return "Always"
}
// Invalid credentials - CBT not strictly required
if strings.Contains(errStr, "52e") || strings.Contains(errStr, "Invalid Credentials") {
logDebug("LDAPS channel binding is 'Never' or 'When Supported' on %s", hostname)
return "Never"
}
// Check for NTLM disabled
if strings.Contains(errStr, "Strong Auth Required") || strings.Contains(errStr, "strongerAuthRequired") {
logDebug("NTLM may be disabled on %s: %v", hostname, err)
return "Unknown (NTLM disabled?)"
}
logDebug("LDAPS bind error on %s: %v", hostname, err)
}
return "Never"
}
func logInfo(format string, args ...interface{}) {
prefix := "[*] "
if build.Timestamp {
prefix = time.Now().Format("2006-01-02 15:04:05 ") + prefix
}
fmt.Printf(prefix+format+"\n", args...)
}
func logError(format string, args ...interface{}) {
prefix := "[-] "
if build.Timestamp {
prefix = time.Now().Format("2006-01-02 15:04:05 ") + prefix
}
fmt.Fprintf(os.Stderr, prefix+format+"\n", args...)
}
func logDebug(format string, args ...interface{}) {
if !build.Debug {
return
}
prefix := "[DEBUG] "
if build.Timestamp {
prefix = time.Now().Format("2006-01-02 15:04:05 ") + prefix
}
fmt.Printf(prefix+format+"\n", args...)
}